diff --git a/src/microglia_analyzer/__init__.py b/src/microglia_analyzer/__init__.py index a26cc84..d4d0d6a 100644 --- a/src/microglia_analyzer/__init__.py +++ b/src/microglia_analyzer/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.1.0" +__version__ = "0.2.0" import re @@ -10,4 +10,4 @@ The networks used in this package (A 2D UNet and a YOLOv5) rely on images that have a pixel size of 0.325 µm. Images with a different pixel size will be resized to artificially have a pixel size of 0.325 µm. It is from these resized images that we will extract the patches. -""" \ No newline at end of file +""" diff --git a/src/microglia_analyzer/_widget.py b/src/microglia_analyzer/_widget.py index 3d6d467..5b47440 100644 --- a/src/microglia_analyzer/_widget.py +++ b/src/microglia_analyzer/_widget.py @@ -6,7 +6,7 @@ from qtpy.QtCore import QThread, Qt from PyQt5.QtGui import QFont, QDoubleValidator, QColor -from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtCore import pyqtSignal, Qt, QLocale import napari from napari.utils.notifications import show_info @@ -91,6 +91,7 @@ def calibration_panel(self): # Create QLineEdit for float input self.calibration_input = QLineEdit() float_validator = QDoubleValidator() + float_validator.setLocale(QLocale(QLocale.English)) float_validator.setNotation(QDoubleValidator.StandardNotation) self.calibration_input.setValidator(float_validator) nav_layout.addWidget(self.calibration_input) @@ -146,12 +147,12 @@ def segment_microglia_panel(self): h_layout.addWidget(self.probability_threshold_label) self.probability_threshold_slider = QSlider(Qt.Horizontal) self.probability_threshold_slider.setRange(0, 100) - self.probability_threshold_slider.setValue(5) + self.probability_threshold_slider.setValue(40) self.probability_threshold_slider.setTickInterval(1) self.probability_threshold_slider.setTickPosition(QSlider.TicksBelow) self.probability_threshold_slider.valueChanged.connect(self.proba_threshold_update) h_layout.addWidget(self.probability_threshold_slider) - self.proba_value_label = QLabel("5%") + self.proba_value_label = QLabel("40%") h_layout.addWidget(self.proba_value_label) layout.addLayout(h_layout) @@ -400,7 +401,7 @@ def show_classification(self): tps = [(c, None, b) for c, b in zip(classification['classes'], classification['boxes'])] boxes, colors = boxes_as_napari_shapes(tps, True) if _YOLO_LAYER_NAME not in self.viewer.layers: - layer = self.viewer.add_shapes(boxes, name=_YOLO_LAYER_NAME, edge_color=colors, face_color='#00000000', edge_width=4) + layer = self.viewer.add_shapes(boxes, name=_YOLO_LAYER_NAME, edge_color=colors, face_color='#00000000', edge_width=4, visible=False) else: layer = self.viewer.layers[_YOLO_LAYER_NAME] layer.data = boxes @@ -493,6 +494,16 @@ def set_sources_folder(self, folder_path): self.images_combo.clear() self.images_combo.addItems(self.get_all_tiff_files(folder_path)) + def reset_layers(self): + if _SEGMENTATION_LAYER_NAME in self.viewer.layers: + self.viewer.layers[_SEGMENTATION_LAYER_NAME].data = np.zeros_like(self.viewer.layers[_SEGMENTATION_LAYER_NAME].data) + if _CLASSIFICATION_LAYER_NAME in self.viewer.layers: + self.viewer.layers[_CLASSIFICATION_LAYER_NAME].data = [] + if _YOLO_LAYER_NAME in self.viewer.layers: + self.viewer.layers[_YOLO_LAYER_NAME].data = [] + if _SKELETON_LAYER_NAME in self.viewer.layers: + self.viewer.layers[_SKELETON_LAYER_NAME].data = np.zeros_like(self.viewer.layers[_SKELETON_LAYER_NAME].data) + def open_image(self, image_path): data = tifffile.imread(image_path) layer = None @@ -501,6 +512,7 @@ def open_image(self, image_path): layer.data = data else: layer = self.viewer.add_image(data, name=_IMAGE_LAYER_NAME, colormap='gray') + self.reset_layers() self.mam.set_input_image(data.copy()) if self.mam.calibration is not None: self.set_calibration(*self.mam.calibration) diff --git a/src/microglia_analyzer/dl/unet2d_training.py b/src/microglia_analyzer/dl/unet2d_training.py index d6b715d..6e2bc02 100644 --- a/src/microglia_analyzer/dl/unet2d_training.py +++ b/src/microglia_analyzer/dl/unet2d_training.py @@ -96,12 +96,12 @@ batch_size = 8 epochs = 500 unet_depth = 2 -num_filters_start = 24 -dropout_rate = 0.2 +num_filters_start = 32 +dropout_rate = 0.25 optimizer = 'Adam' learning_rate = 0.001 skeleton_coef = 0.2 -bce_coef = 0.25 +bce_coef = 0.5 early_stop_patience = 50 dilation_kernel = diamond(1) loss = dice_skeleton_loss(skeleton_coef, bce_coef) @@ -658,7 +658,8 @@ def open_pair(input_path, mask_path, training, img_only): def pairs_generator(src, training, img_only): source = src.decode('utf-8') _, l_files = get_data_pools(os.path.join(working_directory, source), [inputs_name], True) - l_files = sorted(list(l_files)) + l_files = list(l_files) + random.shuffle(l_files) i = 0 while i < len(l_files): input_path = os.path.join(working_directory, source, inputs_name, l_files[i]) @@ -825,6 +826,8 @@ def create_unet2d_model(input_shape): x = Conv2D(num_filters, 3, activation='relu', padding='same', kernel_initializer='he_normal')(x) # x = BatchNormalization()(x) x = Conv2D(num_filters, 3, activation='relu', padding='same', kernel_initializer='he_normal')(x) + if i > 0: + x = BatchNormalization()(x) # x = BatchNormalization()(x) outputs = Conv2D(1, 1, activation='sigmoid')(x) @@ -923,7 +926,7 @@ def cosine_annealing(epoch, _): return float(learning_rate * decayed) def train_model(model, train_dataset, val_dataset, output_path): - plot_model(model, to_file=os.path.join(output_path, 'architecture.png'), show_shapes=True) + #plot_model(model, to_file=os.path.join(output_path, 'architecture.png'), show_shapes=True) print(f"💾 Exporting model to: {output_path}") checkpoint = ModelCheckpoint(os.path.join(output_path, 'best.keras'), save_best_only=True, monitor='val_loss', mode='min') diff --git a/src/microglia_analyzer/experimental/segment_microglia.py b/src/microglia_analyzer/experimental/segment_microglia.py index 6e43097..83fbce3 100644 --- a/src/microglia_analyzer/experimental/segment_microglia.py +++ b/src/microglia_analyzer/experimental/segment_microglia.py @@ -18,7 +18,7 @@ def normalize_batch(batch): bce_coef = 0.7 class MicrogliaSegmenter(object): - def __init__(self, model_path, image_path, tile_size=512, overlap=128): + def __init__(self, model_path, image_path, tile_size=512, overlap=256): if not os.path.isfile(model_path): raise FileNotFoundError(f"Model file {model_path} not found") if not model_path.endswith(".keras"): @@ -27,7 +27,7 @@ def __init__(self, model_path, image_path, tile_size=512, overlap=128): model_path, custom_objects={ "bcl": bce_dice_loss(bce_coef), - "dsl": dice_skeleton_loss(skeleton_coef, bce_coef) + "_dice_skeleton_loss": dice_skeleton_loss(skeleton_coef, bce_coef) } ) if not os.path.isfile(image_path): @@ -48,14 +48,16 @@ def inference(self): tiles = np.array(tiles_manager.image_to_tiles(self.image)) predictions = np.squeeze(self.model.predict(tiles, batch_size=8)) tifffile.imwrite("/tmp/tiles.tif", predictions) - normalize_batch(predictions) + tifffile.imwrite("/tmp/coefs.tif", tiles_manager.blending_coefs) + # normalize_batch(predictions) probabilities = tiles_manager.tiles_to_image(predictions) return probabilities + if __name__ == "__main__": output_path = "/home/benedetti/Downloads/training-audrey/output/" model_path = "/home/benedetti/Downloads/training-audrey/models/unet-V007/best.keras" - folder_path = "/home/benedetti/Documents/projects/2060-microglia/data/raw-data/tiff-data" + folder_path = "/home/benedetti/Downloads/training-audrey/raw/" content = [f for f in os.listdir(folder_path) if f.endswith(".tif")] for i, image_name in enumerate(content): print(f"{i+1}/{len(content)}: {image_name}") diff --git a/src/microglia_analyzer/experimental/tiles.py b/src/microglia_analyzer/experimental/tiles.py index 574231b..1a4781d 100644 --- a/src/microglia_analyzer/experimental/tiles.py +++ b/src/microglia_analyzer/experimental/tiles.py @@ -53,14 +53,37 @@ def generate_checkerboard(width, height, num_squares_x, num_squares_y): img = Image.fromarray(checkerboard) return img -# Générer une image de 2048x2048 avec des cases de 128x128 -checkerboard_img = np.squeeze(np.array(generate_checkerboard(2048, 2048, 16, 16))) -tifffile.imwrite("/tmp/original.tif", checkerboard_img) -tiles_manager = ImageTiler2D(512, 128, checkerboard_img.shape) -tiles = tiles_manager.image_to_tiles(checkerboard_img) -tifffile.imwrite("/tmp/checkerboard.tif", tiles) -merged = tiles_manager.tiles_to_image(tiles) -tifffile.imwrite("/tmp/merged.tif", merged) +if __name__ == "__main__": + import os + import tifffile + import numpy as np + output_path = "/tmp/dump/" + os.makedirs(output_path, exist_ok=True) -tifffile.imwrite("/tmp/coefs.tif", tiles_manager.blending_coefs) -tifffile.imwrite("/tmp/gradient.tif", tiles_manager.tiles_to_image(tiles_manager.blending_coefs)) \ No newline at end of file + shapes = [ + (2048, 2048), + (1024, 1024) + ] + for shape in shapes: + print("-----------") + image = np.ones(shape, dtype=np.float32) + tiles_manager = ImageTiler2D(512, 128, shape) + for t in tiles_manager.layout: + print(t) + tiles = tiles_manager.image_to_tiles(image) + merged = tiles_manager.tiles_to_image(tiles) + for i in range(len(tiles)): + tifffile.imwrite(os.path.join(output_path, f"{shape[0]}_{str(i).zfill(2)}.tif"), tiles_manager.blending_coefs[i]) + tifffile.imwrite(os.path.join(output_path, f"{shape[0]}_merged.tif"), merged) + +if __name__ == "": + # Générer une image de 2048x2048 avec des cases de 128x128 + checkerboard_img = np.squeeze(np.array(generate_checkerboard(2048, 2048, 16, 16))) + tifffile.imwrite("/tmp/original.tif", checkerboard_img) + tiles_manager = ImageTiler2D(512, 128, checkerboard_img.shape) + tiles = tiles_manager.image_to_tiles(checkerboard_img) + tifffile.imwrite("/tmp/checkerboard.tif", tiles) + merged = tiles_manager.tiles_to_image(tiles) + tifffile.imwrite("/tmp/merged.tif", merged) + tifffile.imwrite("/tmp/coefs.tif", tiles_manager.blending_coefs) + tifffile.imwrite("/tmp/gradient.tif", tiles_manager.tiles_to_image(tiles_manager.blending_coefs)) \ No newline at end of file diff --git a/src/microglia_analyzer/ma_worker.py b/src/microglia_analyzer/ma_worker.py index 80caa20..697d497 100644 --- a/src/microglia_analyzer/ma_worker.py +++ b/src/microglia_analyzer/ma_worker.py @@ -2,6 +2,8 @@ import shutil import pint import json +import pathlib +import platform import tifffile import numpy as np @@ -165,6 +167,9 @@ def set_classification_model(self, path, use="best", reload=False): if not os.path.isfile(confusion_matrix_path): raise ValueError("The training of this model is not complete.") self.classification_model_path = weights_path + plt = platform.system() + if plt == "Windows": + pathlib.PosixPath = pathlib.WindowsPath self.classification_model = torch.hub.load( 'ultralytics/yolov5', 'custom', @@ -287,7 +292,7 @@ def classification_postprocessing(self): used.add(i) self.classifications = clean_boxes - def bind_classifications(self): + def _bind_classifications(self): labeled = self.mask regions = regionprops(labeled) bindings = {int(l): (None, 0.0, None) for l in np.unique(labeled) if l != 0} # label: (class, IoU) @@ -298,10 +303,35 @@ def bind_classifications(self): x1, y1, x2, y2 = list(map(int, box)) detect_bbox = [y1, x1, y2, x2] iou = calculate_iou(seg_bbox, detect_bbox) - if iou > 0.8 and iou > bindings[region.label][1]: + if iou > bindings[region.label][1]: # iou > 0.2 and bindings[region.label] = (cls, iou, seg_bbox) self.bindings = bindings + def bind_classifications(self): + labeled = self.mask + regions = regionprops(labeled) + # box_index: (class, IoU, label, seg_bbox) + bindings = {b: (None, 0.0, None, None) for b in range(len(self.classifications['boxes']))} + + for b_index, (box, cls) in enumerate(zip(self.classifications['boxes'], self.classifications['classes'])): + x1, y1, x2, y2 = list(map(int, box)) + detect_bbox = [y1, x1, y2, x2] + for region in regions: + seg_bbox = list(map(int, region.bbox)) + iou = calculate_iou(seg_bbox, detect_bbox) + if iou > bindings[b_index][1]: + bindings[b_index] = (cls, iou, region.label, seg_bbox) + + self.bindings = self.flip_dict(bindings) + + def flip_dict(self, d): + flipped = {} + for _, (cls, iou, label, seg_bbox) in d.items(): + if seg_bbox is None: + continue + flipped[label] = (cls, iou, seg_bbox) + return flipped + def analyze_skeleton(self, mask): skeleton = skeletonize(mask) skel = Skeleton(skeleton) @@ -360,7 +390,7 @@ def as_csv(self, identifier): if i == 0: values[0] = identifier graph_measures = self.graph_metrics[label] - iou, class_value = self.bindings[label][:2] + class_value, iou = self.bindings[label][:2] class_value = self.classes[int(class_value)] if class_value is not None else "" values += [graph_measures[key] for key in graph_measure_keys] + [iou, class_value] line = ", ".join([str(v) for v in values])