From 367f9c0e21e60e8dea0e4d218d77bed0cf0c293c Mon Sep 17 00:00:00 2001 From: KarenMkrtchyan <70455623+KarenMkrtchyan@users.noreply.github.com> Date: Fri, 8 Aug 2025 10:54:46 -0700 Subject: [PATCH 1/4] merging my code into this repo --- requirements.txt | 4 +- scripts/do_segmentation.py | 7 +- src/deep_learning/cellpose.py | 155 +++++++++++++++++++++++++++++++--- src/utils/config.py | 12 +++ src/utils/crop.py | 91 ++++++++++++++++++++ src/utils/data_loader.py | 33 +++++--- src/utils/image.py | 22 +++++ src/utils/mask.py | 15 ++++ 8 files changed, 312 insertions(+), 27 deletions(-) create mode 100644 src/utils/config.py create mode 100644 src/utils/crop.py create mode 100644 src/utils/image.py create mode 100644 src/utils/mask.py diff --git a/requirements.txt b/requirements.txt index c392a4f..7d74a5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ scikit-learn>=0.24.0 pandas>=1.3.0 opencv-python>=4.5.0 Pillow>=8.3.0 +numba==0.61.0 # Deep learning torch>=1.9.0 @@ -18,6 +19,7 @@ albumentations>=1.0.0 scikit-learn-extra>=0.2.0 connected-components-3d>=3.0.0 SimpleITK>=2.1.0 +cellpose ==3.1.1.1 # Development and utilities jupyterlab>=3.0.0 @@ -26,9 +28,9 @@ black>=21.6b0 isort>=5.9.0 flake8>=3.9.0 tqdm>=4.64.0 +pyyaml==6.0.2 # Logging and monitoring loguru>=0.7.0 # Optional GPU acceleration (install with: pip install cupy-cuda12x) # cupy-cuda12x>=13.0.0 # Uncomment if you have CUDA 12.x GPU support -cellpose \ No newline at end of file diff --git a/scripts/do_segmentation.py b/scripts/do_segmentation.py index ecf149b..f50b146 100644 --- a/scripts/do_segmentation.py +++ b/scripts/do_segmentation.py @@ -94,18 +94,17 @@ def main(): else: pool = multiprocessing.Pool(processes=args.workers) """ - # Completed: IF slide_id is provided, use the slide_id and data_dir to load the slides - # TODO: Multiprocessing data loader log.logger.debug("Loading slides...") slides = data_loader.load_slides(args.data_dir) - # TODO: A non offset based composite creation should be implemented in the data loader log.logger.debug("Creating composites...") - composite_images = data_loader.get_composites(slides, config.SLIDE_INDEX_OFFSET) # creaing composites should be preprocessing which is in segmentor + composite_images = cellposeSegmentor.preprocess(slides) log.logger.debug("Running Segmentation...") binary_masks = cellposeSegmentor.segment(composite_images) + image_crops, mask_crops, centers = cellposeSegmentor.postprocess() + log.logger.debug("Saving masks...") cellposeSegmentor.save_masks(binary_masks) diff --git a/src/deep_learning/cellpose.py b/src/deep_learning/cellpose.py index a8a50fe..d19a1dd 100644 --- a/src/deep_learning/cellpose.py +++ b/src/deep_learning/cellpose.py @@ -3,8 +3,17 @@ from pathlib import Path import numpy as np import cv2 -import matplotlib.pyplot as plt # use in debug console from src.deep_learning.base import BaseSegmenter +import os +import numpy as np +import cv2 +import multiprocessing +from .base import BaseSegmenter +from .utils.config import Config +from .utils.loader import load_img +from .utils.image import compute_composite +from .utils.crop import crop_single_image +from .utils.mask import binary_masks import loguru as log class CellposeSegmentor(BaseSegmenter): @@ -14,6 +23,25 @@ def __init__(self, config): This class is a wrapper around the Cellpose deep learning model for image segmentation. It inherits from BaseSegmenter and implements the segment method. + + This class provides functionality for: + - Loading grayscale microscopy images from a directory + - Combining multi-channel scans into composite images + - Running segmentation using Cellpose + - Saving mask outputs + - Extracting cropped cell images from masks + + Attributes: + model (cellpose.models.CellposeModel): The loaded Cellpose model. + config (Config): Configuration object containing paths and settings. + + Methods: + load_images(image_dir): Loads images from a directory using multiprocessing. + combine_images(images): Combines 4-channel scans into RGB composites. + segment_frames(frames): Runs Cellpose segmentation on image frames. + save_masks(masks): Saves the predicted masks to disk. + get_cell_crops(masks, images): Extracts cropped cell images and their masks. + run(image_dir): Main workflow to segment images from a directory. """ self.config = config @@ -32,19 +60,124 @@ def __init__(self, config): else: pass # For future addition of models + self.image_data = np.empty(1) + self.composite_data = np.empty(1) + self.masks = np.empty(1) + self.stacked_scans_data = [] + log.logger.debug("Cellpose Segmentor initialized.") - def save_masks(self, masks): - if not Path(self.config.PROCESSED_DATA_DIR).exists(): - self.config.PROCESSED_DATA_DIR.mkdir(parents=True, exist_ok=True) + def segment(self, images=None): + """ + Segment the input images. + + Args: + List of images (numpy.ndarray with shape NUM IMAGES * HEIGHT * WIDTH * 3): Input images to segment. + + Returns: + numpy.ndarray: Insance mask where each cell gets its own ID + """ + if not images: + images = self.composite_data - for i, mask in enumerate(masks): - mask_path = Path(self.config.PROCESSED_DATA_DIR, f"mask_{i}.png") - cv2.imwrite(mask_path, mask) + self.masks, _, _ = self.model.eval(self.composite_data, diameter=15, channels=[0, 0]) + return self.masks + + def preprocess(self, images=None): + """ + Preprocess the loaded input images before segmentation by combining different scan types into a BRG image understood by the segmentation module. + + Args: + image (numpy.ndarray): Input image to preprocess. + + Returns: + numpy.ndarray: Preprocessed image. + """ + if not images: + images = self.image_data + + frames=[] + offset = int(len(images)/4) + for i in range(offset): + image0 = images[i] + image1 = images[i+offset] + image2 = images[i+2*offset] + # skip Bright Field scan + image3 = images[i+3*offset] + stacked = np.stack([image0, image1, image2, image3], axis=-1) + self.stacked_scans_data.append(stacked) + frames.append(compute_composite(image0, image1, image2, image3)) + + self.stacked_scans_data = np.stack(np.delete(self.stacked_scans_data, 0, axis=0), axis =0) # remove the first empty array + self.composite_data = frames + return frames - def segment(self, images): - masks, _, _ = self.model.eval(images,diameter=15,channels=[0, 0]) # test if pasing all the frames at once or one at a time is faster + def postprocess(self, masks=None, images=None): + """ + Postprocess the segmentation mask. Extracts cropped cell images using the segmented masks. + + Arguments: + masks (np.ndarray): Array of segmented masks with shape (N, C, H, W). + images (np.ndarray): Array of original images with shape (N, C, H, W). + Returns: + List[np.ndarray]: List of cropped cell images. + """ + if not masks: + masks = self.masks + if not images: + images = self.stacked_scans_data + + args = [ + ( + masks[j], images[j], + ) + for j in range(len(images)) + ] + + with multiprocessing.Pool(processes=max(1, multiprocessing.cpu_count() - 2)) as pool: + results = pool.map(crop_single_image, args) + + # Flatten results + image_crops, mask_crops, centers = [], [], [] + for img_crops, msk_crops, ctrs in results: + image_crops.extend(img_crops) + mask_crops.extend(msk_crops) + centers.extend(ctrs) + + del self.image_data + del self.composite_data + del self.masks + del self.stacked_scans_data + + return ( + np.transpose(np.stack(image_crops, axis = 0), (0,3,1,2)), # Convert to (N, C, H, W,) because thats what the current extration model expects, + # it is probaly worth a look at why that choice was made and if it can be undone + binary_masks(np.stack((mask_crops), axis=0)), + np.stack(centers, axis=0) + ) + + def load_data(self, image_dir): + """ + Load images from the specified directory, and return a list of images as numpy arrays. + The returned value is optional to use and self.image_data is what the segment wants to use unless overwritten - # return np.array(masks).astype(bool).astype(np.uint8)*255 # binarize the masks for visual check + Args: + image_dir(Path): os-valid path (Use pathlib.Path) for the folder with slide data - return masks \ No newline at end of file + """ + image_files = sorted(os.listdir(image_dir)) # list index must match the order of scans + + with multiprocessing.Pool(multiprocessing.cpu_count() - 2) as p: # save one core for the system and one more for good luck + args = [(image_dir, f) for f in image_files] + frames = p.map(load_img, args) + + self.image_data = np.array(frames, dtype=np.uint16) + return self.image_data + + def save_masks(self, masks): + if not self.config.mask_output_dir.exists(): + self.config.mask_output_dir.mkdir(parents=True, exist_ok=True) + + for i, mask in enumerate(masks): + mask_path = Path(self.config.mask_output_dir, f"mask_{i}.png") + cv2.imwrite(mask_path, mask.astype(np.uint16)) \ No newline at end of file diff --git a/src/utils/config.py b/src/utils/config.py new file mode 100644 index 0000000..7591977 --- /dev/null +++ b/src/utils/config.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from pathlib import Path + +@dataclass +class Config: + pretrained_model: Path + device: str + data_dir: Path + image_extension: str + mask_output_dir: Path + offset: int = 10 + diff --git a/src/utils/crop.py b/src/utils/crop.py new file mode 100644 index 0000000..3727ab7 --- /dev/null +++ b/src/utils/crop.py @@ -0,0 +1,91 @@ +import numpy as np + +def crop_single_image(args): + """ + Util function for each worker in a multiprocessing tool. Takes in an image and returns an cropped cells, cropped masks, and (y, x) center of + each cell + """ + mask, image = args + image_crops = [] + mask_crops = [] + centers = [] + for i in range(1, np.max(mask)): + center = find_center(mask, i) + if (center[0] < 38 or center[1] < 38 or + center[0] > image.shape[0] - 38 or + center[1] > image.shape[1] - 38): + continue + centers.append(center) + crop = crop_img_from_center(center, image) + crop = multiplex_mask_on_crop(crop, mask, i, center) # 75 * 75 * 4 + image_crops.append(crop) + mask_crops.append(crop_mask_from_center(center, mask)) + return image_crops, mask_crops, centers + +def crop_img_from_center(center, image): + left = 0 # slighly assymetric, the left gets 38 pixels while the right gets 37 pixels + right = 75 + bottom = 75 + top = 0 + if(center[0]>38): # Make sure h is not out of range + if(center[0]38): # Make sure w is not out of range + if(center[1]38): # Make sure h is not out of range + if(center[0]38): # Make sure w is not out of range + if(center[1] Dict[str, str]: Returns: Dictionary mapping image paths to mask paths """ + if self.mask_dir == None: + return {} # never going to hit this line but removes errors below + pairs = {} mask_files = self._find_files(self.mask_dir, self.mask_ext) @@ -200,21 +212,18 @@ def load_slides(self, data_dir, slide_id=None): slide_path = Path(data_dir, slide_id) if not slide_path.exists(): raise ValueError(f"Slide path {slide_path} does not exist") - - + image_files = sorted(os.listdir(slide_path)) # list index must match the order of scans - frames = [] - for image_file in image_files: - image = cv2.imread(Path(slide_path, image_file), cv2.IMREAD_GRAYSCALE) - frames.append(image) - - return np.array(frames, dtype=np.uint16) + with multiprocessing.Pool(multiprocessing.cpu_count() - 2) as p: # save one core for the system and one more for good luck + args = [(slide_path, f) for f in image_files] + frames = p.map(load_img, args) + return np.array(frames, dtype=np.uint16) def compute_composite(self, dapi, ck, cd45, fitc): """ - COmbine DAPI, CK, CD45, and FITC channels into a single RGB composite image. Used by CellposeSegmentor. + Combine DAPI, CK, CD45, and FITC channels into a single RGB composite image. Used by CellposeSegmentor. Args: dapi (np.ndarray): DAPI channel image. @@ -253,6 +262,8 @@ def get_composites(self, slides, offset, save_composites=False): # skip Bright Field scan image3 = slides[i+3*offset] frames.append(self.compute_composite(image0, image1, image2, image3)) + if self.mask_dir == None: + return {} # never going to hit this line but removes errors below if save_composites: composite_path = Path(self.mask_dir, f"composite_{i}.png") diff --git a/src/utils/image.py b/src/utils/image.py new file mode 100644 index 0000000..b27961c --- /dev/null +++ b/src/utils/image.py @@ -0,0 +1,22 @@ +import numpy as np + + +def compute_composite(dapi, ck, cd45, fitc): + + dtype = dapi.dtype + max_val = np.iinfo(dapi.dtype).max + + dapi = dapi.astype(np.float32) + ck = ck.astype(np.float32) + cd45 = cd45.astype(np.float32) + fitc = fitc.astype(np.float32) + + rgb = np.zeros((dapi.shape[0], dapi.shape[1], 3), dtype='float') + + rgb[...,0] = ck+fitc + rgb[...,1] = cd45+fitc + rgb[...,2] = dapi.astype(np.float32)+fitc # why is there a random np.float32 here? + rgb[rgb > max_val] = max_val # Clips overflow + + rgb = rgb.astype(dtype) + return rgb diff --git a/src/utils/mask.py b/src/utils/mask.py new file mode 100644 index 0000000..0979cae --- /dev/null +++ b/src/utils/mask.py @@ -0,0 +1,15 @@ +import numpy as np + +def binary_masks(masks): + """ + Takes an np.array (shape N, 75, 75) with instance values and converts it to a np.array (shape N, 1, 75, 75) + with 1 or 0 in the second dimension to indicate mask or no mask. + Any nonzero value in the original mask is set to 1. + """ + # Ensure input is a numpy array + masks = np.asarray(masks) + # Create binary masks: 1 where mask > 0, else 0 + binary = (masks > 0).astype(np.uint8) + # Add a channel dimension (axis=1) + binary = binary[:, np.newaxis, :, :] + return binary From 0561f2cf6c8960cf5c103b899d61917f172d32a6 Mon Sep 17 00:00:00 2001 From: KarenMkrtchyan <70455623+KarenMkrtchyan@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:57:33 -0700 Subject: [PATCH 2/4] merged code, needs to be tested --- src/deep_learning/base.py | 22 +++++----- src/deep_learning/cellpose.py | 56 ++++++++++++++----------- src/{ => deep_learning}/utils/config.py | 0 src/{ => deep_learning}/utils/crop.py | 0 src/{ => deep_learning}/utils/image.py | 0 src/deep_learning/utils/loader.py | 10 +++++ src/{ => deep_learning}/utils/mask.py | 0 src/utils/data_loader.py | 51 +++++++++++----------- 8 files changed, 79 insertions(+), 60 deletions(-) rename src/{ => deep_learning}/utils/config.py (100%) rename src/{ => deep_learning}/utils/crop.py (100%) rename src/{ => deep_learning}/utils/image.py (100%) create mode 100644 src/deep_learning/utils/loader.py rename src/{ => deep_learning}/utils/mask.py (100%) diff --git a/src/deep_learning/base.py b/src/deep_learning/base.py index 237b8d8..35555fd 100644 --- a/src/deep_learning/base.py +++ b/src/deep_learning/base.py @@ -2,7 +2,7 @@ Base class for all traditional segmentation algorithms. """ from abc import ABC, abstractmethod -from scipy import ndimage as ndi +import numpy as np class BaseSegmenter(ABC): """Base class that all traditional segmentation algorithms should inherit from.""" @@ -17,7 +17,7 @@ def __init__(self, config=None): self.config = config or {} @abstractmethod - def segment(self, images): + def segment(self, images) -> np.ndarray: """ Segment the input images. @@ -29,7 +29,8 @@ def segment(self, images): """ pass - def preprocess(self, images_dir): # get_composites shouuld be here + @abstractmethod + def preprocess(self, images) -> np.ndarray: # get_composites shouuld be here """ Preprocess the input image before segmentation. @@ -41,15 +42,16 @@ def preprocess(self, images_dir): # get_composites shouuld be here """ pass - def postprocess(self, mask): + @abstractmethod + def postprocess(self, masks=None, images=None) -> list[np.ndarray]: """ - Postprocess the segmentation mask. - - Args: - mask (numpy.ndarray): Segmentation mask to postprocess. - + Postprocess the segmentation mask. Extracts cropped cell images using the segmented masks. + + Arguments: + masks (np.ndarray): Array of segmented masks with shape (N, C, H, W). + images (np.ndarray): Array of original images with shape (N, C, H, W). Returns: - numpy.ndarray: Postprocessed mask. + List[np.ndarray]: List of cropped cell images. """ pass \ No newline at end of file diff --git a/src/deep_learning/cellpose.py b/src/deep_learning/cellpose.py index d19a1dd..66ef54f 100644 --- a/src/deep_learning/cellpose.py +++ b/src/deep_learning/cellpose.py @@ -17,7 +17,7 @@ import loguru as log class CellposeSegmentor(BaseSegmenter): - def __init__(self, config): + def __init__(self, config: Config): """ Initialize the Cellpose segmentor. @@ -48,18 +48,15 @@ def __init__(self, config): if core.use_gpu() == False: raise ImportError("No GPU access") - if not Path(self.config.DEEP_LEARNING_MODELS_DIR).exists(): - log.logger.warning("Pretrained model path does not exist, using default model.") - self.config.DEEP_LEARNING_CONFIG["model"]["name"] = "cpsam" # Default model if not specified # not sure why syntax is so cursed - - if self.config.MODEL == 'cellpose': - self.model = models.CellposeModel(gpu = True, - pretrained_model=str(Path(self.config.DEEP_LEARNING_MODELS_DIR, self.config.DEEP_LEARNING_CONFIG["model"]["name"])), - device=torch.device(self.config.DEEP_LEARNING_CONFIG["device"])) - - else: - pass # For future addition of models - + if not self.config.data_dir.exists(): + raise FileNotFoundError(f"Data directory {self.config.data_dir} does not exist") + + if self.config.pretrained_model is None: + raise ValueError("Pretrained model must be specified") + + self.model = models.CellposeModel(gpu = True, + pretrained_model=str(self.config.pretrained_model), # ignore Pylance error, this code is correct + device=torch.device(self.config.device)) self.image_data = np.empty(1) self.composite_data = np.empty(1) self.masks = np.empty(1) @@ -67,7 +64,7 @@ def __init__(self, config): log.logger.debug("Cellpose Segmentor initialized.") - def segment(self, images=None): + def segment(self, images=None) -> np.ndarray: """ Segment the input images. @@ -83,7 +80,7 @@ def segment(self, images=None): self.masks, _, _ = self.model.eval(self.composite_data, diameter=15, channels=[0, 0]) return self.masks - def preprocess(self, images=None): + def preprocess(self, images=None) -> np.ndarray: """ Preprocess the loaded input images before segmentation by combining different scan types into a BRG image understood by the segmentation module. @@ -108,11 +105,11 @@ def preprocess(self, images=None): self.stacked_scans_data.append(stacked) frames.append(compute_composite(image0, image1, image2, image3)) - self.stacked_scans_data = np.stack(np.delete(self.stacked_scans_data, 0, axis=0), axis =0) # remove the first empty array - self.composite_data = frames - return frames + self.stacked_scans_data = np.stack(self.stacked_scans_data[1:], axis=0) # remove the first empty array + self.composite_data = np.ndarray(frames) + return np.ndarray(frames) - def postprocess(self, masks=None, images=None): + def postprocess(self, masks=None, images=None) -> list[np.ndarray]: """ Postprocess the segmentation mask. Extracts cropped cell images using the segmented masks. @@ -150,13 +147,13 @@ def postprocess(self, masks=None, images=None): del self.stacked_scans_data return ( - np.transpose(np.stack(image_crops, axis = 0), (0,3,1,2)), # Convert to (N, C, H, W,) because thats what the current extration model expects, + [ np.transpose(np.stack(image_crops, axis = 0), (0,3,1,2)), # Convert to (N, C, H, W,) because thats what the current extration model expects, # it is probaly worth a look at why that choice was made and if it can be undone binary_masks(np.stack((mask_crops), axis=0)), - np.stack(centers, axis=0) + np.stack(centers, axis=0)] ) - def load_data(self, image_dir): + def load_data(self, image_dir) -> np.ndarray: """ Load images from the specified directory, and return a list of images as numpy arrays. The returned value is optional to use and self.image_data is what the segment wants to use unless overwritten @@ -174,10 +171,19 @@ def load_data(self, image_dir): self.image_data = np.array(frames, dtype=np.uint16) return self.image_data - def save_masks(self, masks): + def save_masks(self, masks) -> None: if not self.config.mask_output_dir.exists(): self.config.mask_output_dir.mkdir(parents=True, exist_ok=True) for i, mask in enumerate(masks): - mask_path = Path(self.config.mask_output_dir, f"mask_{i}.png") - cv2.imwrite(mask_path, mask.astype(np.uint16)) \ No newline at end of file + mask_path = self.config.mask_output_dir / f"mask_{i}.png" + + # Handle mask format + if mask.dtype == bool or mask.max() <= 1: + mask_to_save = (mask * 255).astype(np.uint8) + else: + mask_to_save = mask.astype(np.uint8) + + success = cv2.imwrite(str(mask_path), mask_to_save) + if not success: + print(f"Warning: Failed to save mask {i} to {mask_path}") \ No newline at end of file diff --git a/src/utils/config.py b/src/deep_learning/utils/config.py similarity index 100% rename from src/utils/config.py rename to src/deep_learning/utils/config.py diff --git a/src/utils/crop.py b/src/deep_learning/utils/crop.py similarity index 100% rename from src/utils/crop.py rename to src/deep_learning/utils/crop.py diff --git a/src/utils/image.py b/src/deep_learning/utils/image.py similarity index 100% rename from src/utils/image.py rename to src/deep_learning/utils/image.py diff --git a/src/deep_learning/utils/loader.py b/src/deep_learning/utils/loader.py new file mode 100644 index 0000000..5ab4965 --- /dev/null +++ b/src/deep_learning/utils/loader.py @@ -0,0 +1,10 @@ +import cv2 +import os + +def load_img(args): + """ + Util function for each worker in a pool for loading in raw data images + """ + folder, filename = args + full_path = os.path.join(folder,filename) + return cv2.imread(full_path, cv2.IMREAD_GRAYSCALE) \ No newline at end of file diff --git a/src/utils/mask.py b/src/deep_learning/utils/mask.py similarity index 100% rename from src/utils/mask.py rename to src/deep_learning/utils/mask.py diff --git a/src/utils/data_loader.py b/src/utils/data_loader.py index fee8a42..26678c9 100644 --- a/src/utils/data_loader.py +++ b/src/utils/data_loader.py @@ -196,30 +196,6 @@ def get_sample_with_mask(self, idx: int) -> Tuple[np.ndarray, np.ndarray]: raise ValueError(f"No mask available for image at index {idx}") return sample.image, sample.mask - def load_slides(self, data_dir, slide_id=None): - """ - Load images from the specified directory, and return a list of images as numpy arrays. - - Args: - slides_path (str): Path to the directory containing the slide images. - - Returns: - np.ndarray (16 bit): Array of images loaded from the directory, each image is a numpy array. - """ - if slide_id is None: - slide_path = Path(data_dir) - else: - slide_path = Path(data_dir, slide_id) - if not slide_path.exists(): - raise ValueError(f"Slide path {slide_path} does not exist") - - image_files = sorted(os.listdir(slide_path)) # list index must match the order of scans - - with multiprocessing.Pool(multiprocessing.cpu_count() - 2) as p: # save one core for the system and one more for good luck - args = [(slide_path, f) for f in image_files] - frames = p.map(load_img, args) - - return np.array(frames, dtype=np.uint16) def compute_composite(self, dapi, ck, cd45, fitc): """ @@ -270,4 +246,29 @@ def get_composites(self, slides, offset, save_composites=False): cv2.imwrite(str(composite_path), frames[-1]) return frames - \ No newline at end of file + + # km + def load_slides(self, data_dir, slide_id=None): + """ + Load images from the specified directory, and return a list of images as numpy arrays. + + Args: + slides_path (str): Path to the directory containing the slide images. + + Returns: + np.ndarray (16 bit): Array of images loaded from the directory, each image is a numpy array. + """ + if slide_id is None: + slide_path = Path(data_dir) + else: + slide_path = Path(data_dir, slide_id) + if not slide_path.exists(): + raise ValueError(f"Slide path {slide_path} does not exist") + + image_files = sorted(os.listdir(slide_path)) # list index must match the order of scans + + with multiprocessing.Pool(multiprocessing.cpu_count() - 2) as p: # save one core for the system and one more for good luck + args = [(slide_path, f) for f in image_files] + frames = p.map(load_img, args) + + return np.array(frames, dtype=np.uint16) \ No newline at end of file From 8d767d1d5dcfa78c2f732e9262657170b44e79c0 Mon Sep 17 00:00:00 2001 From: Karen Mkrtchyan <70455623+KarenMkrtchyan@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:45:42 +0000 Subject: [PATCH 3/4] Add files via upload --- high_level.JPG | Bin 0 -> 33675 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 high_level.JPG diff --git a/high_level.JPG b/high_level.JPG new file mode 100644 index 0000000000000000000000000000000000000000..fc50346cbec16aa965a82e5db87b5436719fc8f3 GIT binary patch literal 33675 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
qF*T&lR>30y1LME3B9WJ2?X|tMobJH`0 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 E15?lAS^xk5 literal 0 HcmV?d00001 From af22263fa5730c4a8bd74357f1d0f995fe477041 Mon Sep 17 00:00:00 2001 From: Karen Mkrtchyan <70455623+KarenMkrtchyan@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:47:00 -0800 Subject: [PATCH 4/4] Add image to README for module overview --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a0bcd05..1c902b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Liquid Biopsy Image Segmentation This repository contains tools and models for semantic and instance segmentation of liquid biopsy microscope images. +![Parts of the modules image](./high_level.JPG) ## Features @@ -43,4 +44,4 @@ This repository contains tools and models for semantic and instance segmentation ## License -[Your license information here] \ No newline at end of file +[Your license information here]