From fea218f061b461b0509a2a119cc53a19529cf80f Mon Sep 17 00:00:00 2001 From: MikeLippincott <1michaell2017@gmail.com> Date: Tue, 21 Apr 2026 08:31:12 -0600 Subject: [PATCH] add colocalization module --- README.md | 2 +- pyproject.toml | 3 + .../featurization/colocalization.py | 503 +++++++++++++++++- src/zedprofiler/image_utils/image_utils.py | 327 ++++++++++++ tests/featurization/test_colocalization.py | 128 +++++ tests/test_image_utils.py | 102 ++++ uv.lock | 224 +++++++- 7 files changed, 1279 insertions(+), 10 deletions(-) create mode 100644 src/zedprofiler/image_utils/image_utils.py create mode 100644 tests/featurization/test_colocalization.py create mode 100644 tests/test_image_utils.py diff --git a/README.md b/README.md index 2eb2537..4a1b214 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ZedProfiler -[![Coverage](https://img.shields.io/badge/coverage-87%25-green)](#quality-gates) +[![Coverage](https://img.shields.io/badge/coverage-96%25-brightgreen)](#quality-gates) CPU-first 3D image feature extraction toolkit for high-content and high-throughput image-based profiling. diff --git a/pyproject.toml b/pyproject.toml index 811b86e..6b478f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,10 @@ classifiers = [ dependencies = [ "fire>=0.7.1", "jinja2>=3.1.6", + "numpy>=2.2", "pandas>=3.0.2", + "scikit-image>=0.25", + "scipy>=1.15", ] scripts.ZedProfiler = "ZedProfiler.cli:trigger" diff --git a/src/zedprofiler/featurization/colocalization.py b/src/zedprofiler/featurization/colocalization.py index 3f798c8..dd8ac49 100644 --- a/src/zedprofiler/featurization/colocalization.py +++ b/src/zedprofiler/featurization/colocalization.py @@ -1,10 +1,503 @@ -"""Colocalization featurization module scaffold.""" +"""Colocalization feature extraction utilities for 3D image objects. + +Computes per-object colocalization metrics (Pearson correlation, Manders +coefficients, overlap coefficient, K1/K2 coefficients) between pairs of +fluorescence channels using the Costes automatic thresholding method. +""" from __future__ import annotations -from zedprofiler.exceptions import ZedProfilerError +from typing import Dict, Tuple + +import numpy +import scipy.ndimage +import skimage + +from zedprofiler.image_utils.image_utils import ( + crop_3D_image, + new_crop_border, + select_objects_from_label, +) + +COSTES_R_FAR_THRESHOLD = 0.45 +COSTES_R_MID_THRESHOLD = 0.35 +COSTES_R_NEAR_THRESHOLD = 0.25 +MIN_PEARSON_POINTS = 2 +WIDE_BISECTION_WINDOW = 6 +UINT8_MAX = 255 +UINT16_MAX = 65535 + + +def _require_scipy() -> None: + if scipy is None: + raise ModuleNotFoundError( + "scipy is required for colocalization features. " + "Install zedprofiler with scipy." + ) + + +def _require_skimage() -> None: + if skimage is None: + raise ModuleNotFoundError( + "scikit-image is required for colocalization features. " + "Install zedprofiler with scikit-image." + ) + + +def linear_costes_threshold_calculation( + first_image: numpy.ndarray, + second_image: numpy.ndarray, + scale_max: int = 255, + fast_costes: str = "Accurate", +) -> Tuple[float, float]: + """ + Finds the Costes Automatic Threshold for colocalization using a linear algorithm. + Candidate thresholds are gradually decreased until Pearson R falls below 0. + If "Fast" mode is enabled the "steps" between tested thresholds will be increased + when Pearson R is much greater than 0. The other mode is "Accurate" which + will always step down by the same amount. + + Parameters + ---------- + first_image : numpy.ndarray + The first fluorescence image. + second_image : numpy.ndarray + The second fluorescence image. + scale_max : int, optional + The maximum value for the image scale, by default 255. + fast_costes : str, optional + The mode for the Costes threshold calculation, by default "Accurate". + + Returns + ------- + Tuple[float, float] + The calculated thresholds for the first and second images. + """ + _require_scipy() + i_step = 1 / scale_max # Step size for the threshold as a float + non_zero = (first_image > 0) | (second_image > 0) + xvar = numpy.var(first_image[non_zero], axis=0, ddof=1) + yvar = numpy.var(second_image[non_zero], axis=0, ddof=1) + + xmean = numpy.mean(first_image[non_zero], axis=0) + ymean = numpy.mean(second_image[non_zero], axis=0) + + z = first_image[non_zero] + second_image[non_zero] + zvar = numpy.var(z, axis=0, ddof=1) + + covar = 0.5 * (zvar - (xvar + yvar)) + + denom = 2 * covar + num = (yvar - xvar) + numpy.sqrt( + (yvar - xvar) * (yvar - xvar) + 4 * (covar * covar) + ) + a = num / denom + b = ymean - a * xmean + + # Start at 1 step above the maximum value + img_max = max(first_image.max(), second_image.max()) + i = i_step * ((img_max // i_step) + 1) + + num_true = None + first_image_max = first_image.max() + second_image_max = second_image.max() + + # Initialise without a threshold + costReg, _ = scipy.stats.pearsonr(first_image, second_image) + thr_first_image_c = i + thr_second_image_c = (a * i) + b + while i > first_image_max and (a * i) + b > second_image_max: + i -= i_step + while i > i_step: + thr_first_image_c = i + thr_second_image_c = (a * i) + b + combt = (first_image < thr_first_image_c) | (second_image < thr_second_image_c) + try: + # Only run pearsonr if the input has changed. + if (positives := numpy.count_nonzero(combt)) != num_true: + costReg, _ = scipy.stats.pearsonr( + first_image[combt], second_image[combt] + ) + num_true = positives + + if costReg <= 0: + break + elif fast_costes == "Accurate" or i < i_step * 10: + i -= i_step + elif costReg > COSTES_R_FAR_THRESHOLD: + # We're way off, step down 10x + i -= i_step * 10 + elif costReg > COSTES_R_MID_THRESHOLD: + # Still far from 0, step 5x + i -= i_step * 5 + elif costReg > COSTES_R_NEAR_THRESHOLD: + # Step 2x + i -= i_step * 2 + else: + i -= i_step + except ValueError: + break + return thr_first_image_c, thr_second_image_c + + +def bisection_costes_threshold_calculation( + first_image: numpy.ndarray, second_image: numpy.ndarray, scale_max: int = 255 +) -> tuple[float, float]: + """ + Finds the Costes Automatic Threshold for colocalization using a bisection algorithm. + Candidate thresholds are selected from within a window of possible intensities, + this window is narrowed based on the R value of each tested candidate. + We're looking for the first point at 0, and R value can become highly variable + at lower thresholds in some samples. Therefore the candidate tested in each + loop is 1/6th of the window size below the maximum value + (as opposed to the midpoint). + + Parameters + ---------- + first_image : numpy.ndarray + The first fluorescence image. + second_image : numpy.ndarray + The second fluorescence image. + scale_max : int, optional + The maximum value for the image scale, by default 255. + + Returns + ------- + Tuple[float, float] + The calculated thresholds for the first and second images. + """ + _require_scipy() + + non_zero = (first_image > 0) | (second_image > 0) + xvar = numpy.var(first_image[non_zero], axis=0, ddof=1) + yvar = numpy.var(second_image[non_zero], axis=0, ddof=1) + + xmean = numpy.mean(first_image[non_zero], axis=0) + ymean = numpy.mean(second_image[non_zero], axis=0) + + z = first_image[non_zero] + second_image[non_zero] + zvar = numpy.var(z, axis=0, ddof=1) + + covar = 0.5 * (zvar - (xvar + yvar)) + + denom = 2 * covar + num = (yvar - xvar) + numpy.sqrt((yvar - xvar) * (yvar - xvar) + 4 * (covar**2)) + a = num / denom + b = ymean - a * xmean + + # Initialise variables + left = 1 + right = scale_max + mid = ((right - left) // (6 / 5)) + left + lastmid = 0 + # Marks the value with the last positive R value. + valid = 1 + + while lastmid != mid: + thr_first_image_c = mid / scale_max + thr_second_image_c = (a * thr_first_image_c) + b + combt = (first_image < thr_first_image_c) | (second_image < thr_second_image_c) + if numpy.count_nonzero(combt) <= MIN_PEARSON_POINTS: + # Can't run meaningful Pearson with only a few values. + left = mid - 1 + else: + try: + costReg, _ = scipy.stats.pearsonr( + first_image[combt], second_image[combt] + ) + if costReg < 0: + left = mid - 1 + elif costReg >= 0: + right = mid + 1 + valid = mid + except ValueError: + # Catch misc Pearson errors with low sample numbers + left = mid - 1 + lastmid = mid + if right - left > WIDE_BISECTION_WINDOW: + mid = ((right - left) // (6 / 5)) + left + else: + mid = ((right - left) // 2) + left + + thr_first_image_c = (valid - 1) / scale_max + thr_second_image_c = (a * thr_first_image_c) + b + + return thr_first_image_c, thr_second_image_c + + +def prepare_two_images_for_colocalization( # noqa: PLR0913 + label_object1: numpy.ndarray, + label_object2: numpy.ndarray, + image_object1: numpy.ndarray, + image_object2: numpy.ndarray, + object_id1: int, + object_id2: int, +) -> Tuple[numpy.ndarray, numpy.ndarray]: + """ + Prepare two images for colocalization analysis by cropping to object bbox. + It selects objects from label images, calculates their bounding boxes, + and crops both images accordingly. + + Parameters + ---------- + label_object1 : numpy.ndarray + The segmented label image for the first object. + label_object2 : numpy.ndarray + The segmented label image for the second object. + image_object1 : numpy.ndarray + The spectral image to crop for the first object. + image_object2 : numpy.ndarray + The spectral image to crop for the second object. + object_id1 : int + The object index to select from the label image for the first object. + object_id2 : int + The object index to select from the label image for the second object. + + Returns + ------- + Tuple[numpy.ndarray, numpy.ndarray] + The two cropped images for colocalization analysis. + """ + _require_skimage() + label_object1 = select_objects_from_label(label_object1, object_id1) + label_object2 = select_objects_from_label(label_object2, object_id2) + # get the image bbox + props_image1 = skimage.measure.regionprops_table(label_object1, properties=["bbox"]) + bbox_image1 = ( + props_image1["bbox-0"][0], # z min + props_image1["bbox-1"][0], # y min + props_image1["bbox-2"][0], # x min + props_image1["bbox-3"][0], # z max + props_image1["bbox-4"][0], # y max + props_image1["bbox-5"][0], # x max + ) + + props_image2 = skimage.measure.regionprops_table(label_object2, properties=["bbox"]) + bbox_image2 = ( + props_image2["bbox-0"][0], # z min + props_image2["bbox-1"][0], # y min + props_image2["bbox-2"][0], # x min + props_image2["bbox-3"][0], # z max + props_image2["bbox-4"][0], # y max + props_image2["bbox-5"][0], # x max + ) + + new_bbox1, new_bbox2 = new_crop_border(bbox_image1, bbox_image2, image_object1) + + cropped_image_1 = crop_3D_image(image_object1, new_bbox1) + cropped_image_2 = crop_3D_image(image_object2, new_bbox2) + return cropped_image_1, cropped_image_2 + + +def compute_colocalization( # noqa: PLR0912, PLR0915 + cropped_image_1: numpy.ndarray, + cropped_image_2: numpy.ndarray, + thr: int = 15, + fast_costes: str = "Accurate", +) -> Dict[str, float]: + """ + This function calculates the colocalization coefficients between two images. + It computes the correlation coefficient, Manders' coefficients, overlap coefficient, + and Costes' coefficients. The results are returned as a dictionary. + + Parameters + ---------- + cropped_image_1 : numpy.ndarray + The first cropped image. + cropped_image_2 : numpy.ndarray + The second cropped image. + thr : int, optional + The threshold for the Manders' coefficients, by default 15 + fast_costes : str, optional + The mode for Costes' threshold calculation, by default "Accurate". + Options are "Accurate" or "Fast". + "Accurate" uses a linear algorithm, while "Fast" uses a bisection algorithm. + The "Fast" mode is faster but less accurate. + + Returns + ------- + Dict[str, float] + The output features for colocalization analysis. + """ + _require_scipy() + results = {} + ################################################################################################ + # Calculate the correlation coefficient between the two images + # This is the Pearson correlation coefficient + # Pearson correlation coeffecient = cov(X, Y) / (std(X) * std(Y)) + # where cov(X, Y) is the covariance of X and Y + # where X and Y are the two images + # std(X) is the standard deviation of X + # std(Y) is the standard deviation of Y + # cov(X, Y) = sum((X - mean(X)) * (Y - mean(Y))) / (N - 1) + # std(X) = sqrt(sum((X - mean(X)) ** 2) / (N - 1)) + # thus N -1 cancels out in the calculation below + ################################################################################################ + mean1 = numpy.mean(cropped_image_1) + mean2 = numpy.mean(cropped_image_2) + std1 = numpy.sqrt(numpy.sum((cropped_image_1 - mean1) ** 2)) + std2 = numpy.sqrt(numpy.sum((cropped_image_2 - mean2) ** 2)) + x = cropped_image_1 - mean1 # x is not the same as the x dimension here + y = cropped_image_2 - mean2 # y is not the same as the y dimension here + corr = numpy.sum(x * y) / (std1 * std2) + + ################################################################################################ + # Calculate the Manders' coefficients + ################################################################################################ + + # Threshold as percentage of maximum intensity of objects in each channel + try: + tff = (thr / 100) * numpy.max(cropped_image_1) + tss = (thr / 100) * numpy.max(cropped_image_2) + # Ensure thresholds are at least 1 to avoid zero thresholding + # if an errors occurs this is probably due to empty images + # or images where the bbox is incredibly small and inconsistent + # or the bbox is on the border of the image + # in which case we want to remove anyway + except ValueError: + M1, M2 = 0.0, 0.0 + else: + # get the thresholds + combined_thresh = (cropped_image_1 >= tff) & (cropped_image_2 >= tss) + + first_image_thresh = cropped_image_1[combined_thresh] + second_image_thresh = cropped_image_2[combined_thresh] + + tot_first_image_thr = scipy.ndimage.sum( + cropped_image_1[cropped_image_1 >= tff], + ) + tot_second_image_thr = scipy.ndimage.sum( + cropped_image_2[cropped_image_2 >= tss] + ) + + if tot_first_image_thr > 0 and tot_second_image_thr > 0: + M1 = scipy.ndimage.sum(first_image_thresh) / tot_first_image_thr + M2 = scipy.ndimage.sum(second_image_thresh) / tot_second_image_thr + else: + M1, M2 = 0.0, 0.0 + ################################################################################################ + # Calculate the overlap coefficient + ################################################################################################ + + fpsq = scipy.ndimage.sum( + cropped_image_1[combined_thresh] ** 2, + ) + spsq = scipy.ndimage.sum( + cropped_image_2[combined_thresh] ** 2, + ) + pdt = numpy.sqrt(numpy.array(fpsq) * numpy.array(spsq)) + overlap = ( + scipy.ndimage.sum( + cropped_image_1[combined_thresh] * cropped_image_2[combined_thresh], + ) + / pdt + ) + # leave in for now + K1 = scipy.ndimage.sum( + cropped_image_1[combined_thresh] * cropped_image_2[combined_thresh], + ) / (numpy.array(fpsq)) + K2 = scipy.ndimage.sum( + cropped_image_1[combined_thresh] * cropped_image_2[combined_thresh], + ) / (numpy.array(spsq)) + if K1 == K2: + pass + + # first_pixels, second_pixels = flattened image arrays + # combined_thresh = boolean mask of pixels above threshold in both channels + # fi_thresh, si_thresh = thresholded intensities (same shape as pixels) + + # --- Rank computation --- + # Flatten images for ranking + img1_flat = cropped_image_1.flatten() + img2_flat = cropped_image_2.flatten() + + # --- Rank computation --- + sorted_idx_1 = numpy.argsort(img1_flat) + sorted_idx_2 = numpy.argsort(img2_flat) + + # Create rank arrays + rank_1_flat = numpy.empty_like(sorted_idx_1, dtype=float) + rank_2_flat = numpy.empty_like(sorted_idx_2, dtype=float) + rank_1_flat[sorted_idx_1] = numpy.arange(len(sorted_idx_1)) + rank_2_flat[sorted_idx_2] = numpy.arange(len(sorted_idx_2)) + + # Reshape back to original shape + rank_im1 = rank_1_flat.reshape(cropped_image_1.shape) + rank_im2 = rank_2_flat.reshape(cropped_image_2.shape) + + # --- Rank difference weight --- + R = max(rank_im1.max(), rank_im2.max()) + 1 + Di = numpy.abs(rank_im1 - rank_im2) + weight = (R - Di) / R + + # Get weights for thresholded pixels + weight_thresh = weight[combined_thresh] + + # Get thresholded values (no double-thresholding!) + first_image_thresh_final = first_image_thresh + second_image_thresh_final = second_image_thresh + + # --- Calculate weighted colocalization --- + if numpy.any(combined_thresh) and len(first_image_thresh_final) > 0: + weighted_sum_1 = numpy.sum(first_image_thresh_final * weight_thresh) + weighted_sum_2 = numpy.sum(second_image_thresh_final * weight_thresh) + + total_1 = numpy.sum(first_image_thresh_final) + total_2 = numpy.sum(second_image_thresh_final) + + RWC1 = weighted_sum_1 / total_1 if total_1 > 0 else 0.0 + RWC2 = weighted_sum_2 / total_2 if total_2 > 0 else 0.0 + else: + RWC1, RWC2 = 0.0, 0.0 + ################################################################################################ + # Calculate the Costes' coefficient + ################################################################################################ + + # Orthogonal Regression for Costes' automated threshold + if numpy.max(cropped_image_1) > UINT8_MAX or numpy.max(cropped_image_2) > UINT8_MAX: + scale = UINT16_MAX + else: + scale = UINT8_MAX + + if fast_costes == "Accurate": + thr_first_image_c, thr_second_image_c = bisection_costes_threshold_calculation( + cropped_image_1, cropped_image_2, scale + ) + else: + thr_first_image_c, thr_second_image_c = linear_costes_threshold_calculation( + cropped_image_1, cropped_image_2, scale, fast_costes + ) + + # Costes' thershold for entire image is applied to each object + first_image_above_thr = cropped_image_1 > thr_first_image_c + second_image_above_thr = cropped_image_2 > thr_second_image_c + combined_thresh_c = first_image_above_thr & second_image_above_thr + first_image_thresh_c = cropped_image_1[combined_thresh_c] + second_image_thresh_c = cropped_image_2[combined_thresh_c] + + tot_first_image_thr_c = scipy.ndimage.sum( + cropped_image_1[cropped_image_1 >= thr_first_image_c], + ) + + tot_second_image_thr_c = scipy.ndimage.sum( + cropped_image_2[cropped_image_2 >= thr_second_image_c], + ) + if tot_first_image_thr_c > 0 and tot_second_image_thr_c > 0: + C1 = scipy.ndimage.sum(first_image_thresh_c) / tot_first_image_thr_c + C2 = scipy.ndimage.sum(second_image_thresh_c) / tot_second_image_thr_c + else: + C1, C2 = 0.0, 0.0 + ################################################################################################ + # write the results to the output dictionary + ################################################################################################ + results["Correlation"] = corr + results["MandersCoeffM1"] = M1 + results["MandersCoeffM2"] = M2 + results["OverlapCoeff"] = overlap + results["MandersCoeffCostesM1"] = C1 + results["MandersCoeffCostesM2"] = C2 + results["RankWeightedColocalizationCoeff1"] = RWC1 + results["RankWeightedColocalizationCoeff2"] = RWC2 -def compute() -> dict[str, list[float]]: - """Placeholder for colocalization computation implementation.""" - raise ZedProfilerError("colocalization.compute is not implemented yet") + return results diff --git a/src/zedprofiler/image_utils/image_utils.py b/src/zedprofiler/image_utils/image_utils.py new file mode 100644 index 0000000..f3dc58c --- /dev/null +++ b/src/zedprofiler/image_utils/image_utils.py @@ -0,0 +1,327 @@ +from typing import Tuple, Union + +import numpy + +BBoxCoord = Union[int, float] +BBox3D = tuple[BBoxCoord, BBoxCoord, BBoxCoord, BBoxCoord, BBoxCoord, BBoxCoord] + + +def select_objects_from_label( + label_image: numpy.ndarray, object_ids: list +) -> numpy.ndarray: + """ + Selects objects from a label image based on the provided object IDs. + + Parameters + ---------- + label_image : numpy.ndarray + The segmented label image. + object_ids : list + The object IDs to select. + + Returns + ------- + numpy.ndarray + The label image with only the selected objects. + """ + label_image = label_image.copy() + label_image[label_image != object_ids] = 0 + return label_image + + +def expand_box( + min_coor: int, max_coord: int, current_min: int, current_max: int, expand_by: int +) -> Union[Tuple[int, int], ValueError]: + """ + Expand the bounding box of an object in a 3D image. + + Parameters + ---------- + min_coor : int + The minimum coordinate of the image for any dimension. + max_coord : int + The maximum coordinate of the image for any dimension. + current_min : int + The current minimum coordinate of an object's bounding box + for any dimension. + current_max : int + The current maximum coordinate of an object's bounding box + for any dimension. + expand_by : int + The amount to expand the bounding box by. + + Returns + ------- + Union[Tuple[int, int], ValueError] + The new minimum and maximum coordinates of the bounding box. + Raises ValueError if the expansion is not possible. + """ + + if max_coord - min_coor - (current_max - current_min) < expand_by: + return ValueError("Cannot expand box by the requested amount") + while expand_by > 0: + if current_min > min_coor: + current_min -= 1 + expand_by -= 1 + elif current_max < max_coord: + current_max += 1 + expand_by -= 1 + + return current_min, current_max + + +def new_crop_border( + bbox1: BBox3D, + bbox2: BBox3D, + image: numpy.ndarray, +) -> tuple[BBox3D, BBox3D]: + """ + Expand the bounding boxes of two objects in a 3D image to match their sizes. + + Parameters + ---------- + bbox1 : BBox3D + The bounding box of the first object. + bbox2 : BBox3D + The bounding box of the second object. + image : numpy.ndarray + The image to crop for each of the bounding boxes. + + Returns + ------- + tuple[BBox3D, BBox3D] + The new bounding boxes of the two objects. + Raises + ValueError + If the expansion is not possible. + """ + i1z1, i1y1, i1x1, i1z2, i1y2, i1x2 = bbox1 + i2z1, i2y1, i2x1, i2z2, i2y2, i2x2 = bbox2 + z_range1 = i1z2 - i1z1 + y_range1 = i1y2 - i1y1 + x_range1 = i1x2 - i1x1 + z_range2 = i2z2 - i2z1 + y_range2 = i2y2 - i2y1 + x_range2 = i2x2 - i2x1 + z_diff = numpy.abs(z_range1 - z_range2) + y_diff = numpy.abs(y_range1 - y_range2) + x_diff = numpy.abs(x_range1 - x_range2) + min_z_coord = 0 + max_z_coord = image.shape[0] + min_y_coord = 0 + max_y_coord = image.shape[1] + min_x_coord = 0 + max_x_coord = image.shape[2] + if z_range1 < z_range2: + i1z1, i1z2 = expand_box( + min_coor=min_z_coord, + max_coord=max_z_coord, + current_min=i1z1, + current_max=i1z2, + expand_by=z_diff, + ) + elif z_range1 > z_range2: + i2z1, i2z2 = expand_box( + min_coor=min_z_coord, + max_coord=max_z_coord, + current_min=i2z1, + current_max=i2z2, + expand_by=z_diff, + ) + if y_range1 < y_range2: + i1y1, i1y2 = expand_box( + min_coor=min_y_coord, + max_coord=max_y_coord, + current_min=i1y1, + current_max=i1y2, + expand_by=y_diff, + ) + elif y_range1 > y_range2: + i2y1, i2y2 = expand_box( + min_coor=min_y_coord, + max_coord=max_y_coord, + current_min=i2y1, + current_max=i2y2, + expand_by=y_diff, + ) + if x_range1 < x_range2: + i1x1, i1x2 = expand_box( + min_coor=min_x_coord, + max_coord=max_x_coord, + current_min=i1x1, + current_max=i1x2, + expand_by=x_diff, + ) + elif x_range1 > x_range2: + i2x1, i2x2 = expand_box( + min_coor=min_x_coord, + max_coord=max_x_coord, + current_min=i2x1, + current_max=i2x2, + expand_by=x_diff, + ) + return (i1z1, i1y1, i1x1, i1z2, i1y2, i1x2), (i2z1, i2y1, i2x1, i2z2, i2y2, i2x2) + + +# crop the image to the bbox of the mask +def crop_3D_image( + image: numpy.ndarray, + bbox: BBox3D, +) -> numpy.ndarray: + """ + Crop a 3D image to the bounding box of a mask. + + Parameters + ---------- + image : numpy.ndarray + The image to crop. + bbox : BBox3D + The bounding box of the mask. + + Returns + ------- + numpy.ndarray + The cropped image. + """ + z1, y1, x1, z2, y2, x2 = bbox + return image[z1:z2, y1:y2, x1:x2] + + +def single_3D_image_expand_bbox( + image: numpy.ndarray, + bbox: tuple[int, int, int, int, int, int], + expand_pixels: int, + anisotropy_factor: int, +) -> tuple[int, int, int, int, int, int]: + """ + Expand the bbox in a way that keeps the crop within the + confines of the image volume + + Parameters + ---------- + image : numpy.ndarray + 3D image array from which the bbox was derived + bbox : tuple[int, int, int, int, int, int] + 3D bbox in the format (zmin, ymin, xmin, zmax, ymax, xmax) + expand_pixels : int + number of pixels to expand the bbox in each direction (z, y, x) + the coordinates become isotropic here so the expansion is + the same across dimensions, + but the anisotropy factor is used to adjust for the z dimension + anisotropy_factor : int + The ratio of "pixel" size in um between the z dimension and the x/y dimensions. + This is used to adjust the expansion of the bbox in the z dimension to account + for anisotropy in the image volume. + For example, if the z spacing is 5um and the x/y spacing is 1um, + then the anisotropy factor would be 5. + + Returns + ------- + tuple[int, int, int, int, int, int] + Updated bbox in the format (zmin, ymin, xmin, zmax, ymax, xmax) + after expansion and adjustment for anisotropy + """ + z1, y1, x1, z2, y2, x2 = bbox + zmin, ymin, xmin = 0, 0, 0 + zmax, ymax, xmax = image.shape + # adjust the anisotropy factor for the z dimension + z1, z2 = z1 * anisotropy_factor, z2 * anisotropy_factor + zmax = zmax * anisotropy_factor + # expand the bbox by the specified number of pixels in each direction + z1_expanded = z1 - expand_pixels + y1_expanded = y1 - expand_pixels + x1_expanded = x1 - expand_pixels + z2_expanded = z2 + expand_pixels + y2_expanded = y2 + expand_pixels + x2_expanded = x2 + expand_pixels + # convert the expanded bbox back to the original z dimension scale + z1_expanded = numpy.floor(z1_expanded / anisotropy_factor) + z2_expanded = numpy.ceil(z2_expanded / anisotropy_factor) + # ensure the expanded bbox does not go outside the image boundaries + z1_expanded, z2_expanded = ( + max(z1_expanded, numpy.floor(zmin / anisotropy_factor)).astype(int), + min(z2_expanded, numpy.ceil(zmax / anisotropy_factor)).astype(int), + ) + y1_expanded, y2_expanded = max(y1_expanded, ymin), min(y2_expanded, ymax) + x1_expanded, x2_expanded = max(x1_expanded, xmin), min(x2_expanded, xmax) + + return ( + z1_expanded, + y1_expanded, + x1_expanded, + z2_expanded, + y2_expanded, + x2_expanded, + ) + + +def check_for_xy_squareness(bbox: tuple[int, int, int, int, int, int]) -> float: + """ + This function returns the ratio of the x length to the y length + A value of 1 indicates a square bbox is present + + Parameters + ---------- + bbox : The bbox to check + (z_min, y_min, x_min, z_max, y_max, x_max) + Where each value is an int representing the pixel coordinate + of the bbox in that dimension + + Returns + ------- + float + The ratio of the y length to the x length of the bbox. + A value of 1 indicates a square bbox. + """ + _z_min, y_min, x_min, _z_max, y_max, x_max = bbox + x_length = x_max - x_min + if x_length == 0: + raise ValueError( + "Cannot compute xy squareness for bbox with zero width in x dimension " + f"(bbox={bbox})." + ) + xy_squareness = (y_max - y_min) / x_length + return xy_squareness + + +def square_off_xy_crop_bbox( + bbox: tuple[int, int, int, int, int, int], +) -> tuple[int, int, int, int, int, int]: + """ + Adjust the bbox to be square in the XY plane. + + The function computes the new bbox from the current X/Y dimensions. + + Parameters + ---------- + bbox : tuple[int, int, int, int, int, int] + The bbox to adjust: + (z_min, y_min, x_min, z_max, y_max, x_max) + + Each value is an integer pixel coordinate in that dimension. + + Returns + ------- + tuple[int, int, int, int, int, int] + The adjusted bbox that is square in the XY plane: + (z_min, new_y_min, new_x_min, z_max, new_y_max, new_x_max) + + Each value is an integer pixel coordinate in that dimension. + """ + zmin, ymin, xmin, zmax, ymax, xmax = bbox + # first find the larger dimension between x and y + x_size = xmax - xmin + y_size = ymax - ymin + if x_size > y_size: + # need to expand y dimension + new_ymin = int(ymin - (x_size - y_size) / 2) + new_ymax = int(ymax + (x_size - y_size) / 2) + return (zmin, new_ymin, xmin, zmax, new_ymax, xmax) + elif y_size > x_size: + # need to expand x dimension + new_xmin = int(xmin - (y_size - x_size) / 2) + new_xmax = int(xmax + (y_size - x_size) / 2) + return (zmin, ymin, new_xmin, zmax, ymax, new_xmax) + else: + # already square + return bbox diff --git a/tests/featurization/test_colocalization.py b/tests/featurization/test_colocalization.py new file mode 100644 index 0000000..d18cfb1 --- /dev/null +++ b/tests/featurization/test_colocalization.py @@ -0,0 +1,128 @@ +import numpy as np +import pytest +from _pytest.monkeypatch import MonkeyPatch + +from zedprofiler.featurization import colocalization as coloc + + +def test_linear_costes_threshold_calculation_returns_finite_thresholds() -> None: + x = np.linspace(0.01, 1.0, 200) + y = 0.85 * x + 0.05 * np.sin(np.arange(x.size)) + t1, t2 = coloc.linear_costes_threshold_calculation( + x, y, scale_max=255, fast_costes="Fast" + ) + assert np.isfinite(t1) + assert np.isfinite(t2) + + +def test_bisection_costes_threshold_calculation_returns_finite_thresholds() -> None: + x = np.linspace(0.01, 1.0, 200) + y = 0.9 * x + 0.03 * np.cos(np.arange(x.size)) + t1, t2 = coloc.bisection_costes_threshold_calculation(x, y, scale_max=255) + assert np.isfinite(t1) + assert np.isfinite(t2) + + +def test_prepare_two_images_for_colocalization_crops_expected_regions( + monkeypatch: MonkeyPatch, +) -> None: + label1 = np.zeros((4, 4, 4), dtype=int) + label2 = np.zeros((4, 4, 4), dtype=int) + label1[1:3, 1:3, 1:3] = 1 + label2[0:2, 0:2, 0:2] = 2 + + img1 = np.arange(64).reshape(4, 4, 4) + img2 = np.arange(100, 164).reshape(4, 4, 4) + + monkeypatch.setattr(coloc, "select_objects_from_label", lambda arr, _: arr) + monkeypatch.setattr(coloc, "new_crop_border", lambda b1, b2, _img: (b1, b2)) + monkeypatch.setattr( + coloc, + "crop_3D_image", + lambda img, bbox: img[bbox[0] : bbox[3], bbox[1] : bbox[4], bbox[2] : bbox[5]], + ) + + out1, out2 = coloc.prepare_two_images_for_colocalization( + label1, label2, img1, img2, object_id1=1, object_id2=2 + ) + + assert out1.shape == (2, 2, 2) + assert out2.shape == (2, 2, 2) + np.testing.assert_array_equal(out1, img1[1:3, 1:3, 1:3]) + np.testing.assert_array_equal(out2, img2[0:2, 0:2, 0:2]) + + +def test_compute_colocalization_identical_images_are_highly_colocalized() -> None: + arr = np.array( + [ + [[1, 2], [3, 4]], + [[5, 6], [7, 8]], + ], + dtype=float, + ) + res = coloc.compute_colocalization(arr, arr, thr=0, fast_costes="Fast") + + expected_keys = { + "Correlation", + "MandersCoeffM1", + "MandersCoeffM2", + "OverlapCoeff", + "MandersCoeffCostesM1", + "MandersCoeffCostesM2", + "RankWeightedColocalizationCoeff1", + "RankWeightedColocalizationCoeff2", + } + assert expected_keys.issubset(res.keys()) + assert res["Correlation"] == pytest.approx(1.0, rel=1e-6) + assert res["MandersCoeffM1"] == pytest.approx(1.0, rel=1e-6) + assert res["MandersCoeffM2"] == pytest.approx(1.0, rel=1e-6) + assert res["OverlapCoeff"] == pytest.approx(1.0, rel=1e-6) + + +def test_compute_colocalization_empty_combined_threshold_path() -> None: + a = np.ones((2, 2, 2), dtype=float) + b = np.ones((2, 2, 2), dtype=float) * 2.0 + + res = coloc.compute_colocalization(a, b, thr=200, fast_costes="Fast") + + assert res["MandersCoeffM1"] == 0.0 + assert res["MandersCoeffM2"] == 0.0 + assert res["RankWeightedColocalizationCoeff1"] == 0.0 + assert res["RankWeightedColocalizationCoeff2"] == 0.0 + assert np.isnan(res["OverlapCoeff"]) + + +def test_compute_colocalization_costes_dispatch(monkeypatch: MonkeyPatch) -> None: + calls = {"bisection": 0, "linear": 0} + + def fake_bisection( + _i1: np.ndarray, _i2: np.ndarray, _scale: int + ) -> tuple[float, float]: + calls["bisection"] += 1 + return 0.1, 0.1 + + def fake_linear( + _i1: np.ndarray, + _i2: np.ndarray, + _scale: int, + _mode: str, + ) -> tuple[float, float]: + calls["linear"] += 1 + return 0.1, 0.1 + + monkeypatch.setattr(coloc, "bisection_costes_threshold_calculation", fake_bisection) + monkeypatch.setattr(coloc, "linear_costes_threshold_calculation", fake_linear) + + img1 = np.arange(1, 9, dtype=float).reshape(2, 2, 2) + img2 = img1 + 1.0 + + coloc.compute_colocalization(img1, img2, fast_costes="Accurate") + coloc.compute_colocalization(img1, img2, fast_costes="Fast") + + assert calls["bisection"] == 1 + assert calls["linear"] == 1 + + +def test_compute_colocalization_empty_input_raises() -> None: + with pytest.raises(UnboundLocalError): + coloc.compute_colocalization(np.array([]), np.array([])) diff --git a/tests/test_image_utils.py b/tests/test_image_utils.py new file mode 100644 index 0000000..311c470 --- /dev/null +++ b/tests/test_image_utils.py @@ -0,0 +1,102 @@ +import numpy as np +import pytest + +from zedprofiler.image_utils.image_utils import ( + check_for_xy_squareness, + crop_3D_image, + expand_box, + new_crop_border, + select_objects_from_label, + single_3D_image_expand_bbox, + square_off_xy_crop_bbox, +) + + +def test_select_objects_from_label_filters_values() -> None: + label_image = np.array([[0, 1, 2], [2, 1, 3]]) + selected = select_objects_from_label(label_image, 2) + + np.testing.assert_array_equal(selected, np.array([[0, 0, 2], [2, 0, 0]])) + # Ensure the original array is unchanged. + np.testing.assert_array_equal(label_image, np.array([[0, 1, 2], [2, 1, 3]])) + + +def test_expand_box_expands_from_min_edge_first() -> None: + new_min, new_max = expand_box(0, 10, 3, 6, 2) + assert (new_min, new_max) == (1, 6) + + +def test_expand_box_returns_value_error_when_impossible() -> None: + result = expand_box(0, 5, 1, 4, 3) + assert isinstance(result, ValueError) + + +def test_new_crop_border_expands_first_bbox_when_second_is_larger() -> None: + bbox1 = (2, 2, 2, 4, 4, 4) + bbox2 = (1, 1, 1, 6, 6, 6) + image = np.zeros((10, 10, 10)) + + new_bbox1, new_bbox2 = new_crop_border(bbox1, bbox2, image) + + assert new_bbox2 == bbox2 + assert new_bbox1 == (0, 0, 0, 5, 5, 5) + + +def test_new_crop_border_expands_second_bbox_when_first_is_larger() -> None: + bbox1 = (1, 1, 1, 7, 7, 7) + bbox2 = (2, 2, 2, 4, 4, 4) + image = np.zeros((10, 10, 10)) + + new_bbox1, new_bbox2 = new_crop_border(bbox1, bbox2, image) + + assert new_bbox1 == bbox1 + assert new_bbox2 == (0, 0, 0, 6, 6, 6) + + +def test_crop_3d_image_returns_expected_subvolume() -> None: + image = np.arange(4 * 5 * 6).reshape(4, 5, 6) + cropped = crop_3D_image(image, (1, 2, 1, 3, 5, 5)) + + np.testing.assert_array_equal(cropped, image[1:3, 2:5, 1:5]) + + +def test_single_3d_image_expand_bbox_adjusts_for_anisotropy_and_bounds() -> None: + image = np.zeros((5, 10, 10)) + bbox = (1, 3, 3, 2, 5, 5) + + expanded = single_3D_image_expand_bbox( + image=image, + bbox=bbox, + expand_pixels=4, + anisotropy_factor=2, + ) + + assert expanded == (0, 0, 0, 4, 9, 9) + + +def test_check_for_xy_squareness_returns_ratio() -> None: + ratio = check_for_xy_squareness((0, 10, 20, 5, 30, 30)) + assert ratio == pytest.approx(2.0) + + +def test_check_for_xy_squareness_raises_for_zero_x_width() -> None: + with pytest.raises(ValueError, match="zero width"): + check_for_xy_squareness((0, 1, 5, 2, 6, 5)) + + +def test_square_off_xy_crop_bbox_expands_y_dimension() -> None: + bbox = (0, 10, 20, 4, 14, 30) + adjusted = square_off_xy_crop_bbox(bbox) + assert adjusted == (0, 7, 20, 4, 17, 30) + + +def test_square_off_xy_crop_bbox_expands_x_dimension() -> None: + bbox = (0, 10, 20, 4, 20, 24) + adjusted = square_off_xy_crop_bbox(bbox) + assert adjusted == (0, 10, 17, 4, 20, 27) + + +def test_square_off_xy_crop_bbox_keeps_square_bbox_unchanged() -> None: + bbox = (0, 1, 2, 5, 9, 10) + adjusted = square_off_xy_crop_bbox(bbox) + assert adjusted == bbox diff --git a/uv.lock b/uv.lock index e5ad51b..189e2fc 100644 --- a/uv.lock +++ b/uv.lock @@ -496,6 +496,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "imageio" +version = "2.37.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646 }, +] + [[package]] name = "imagesize" version = "1.4.1" @@ -888,6 +902,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl", hash = "sha256:d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9", size = 170478 }, ] +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044 }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1081,6 +1107,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, ] +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504 }, +] + [[package]] name = "notebook-shim" version = "0.2.4" @@ -1292,6 +1327,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837 }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528 }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401 }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094 }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402 }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005 }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669 }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194 }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423 }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667 }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580 }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896 }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266 }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508 }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927 }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624 }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252 }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550 }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114 }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667 }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966 }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241 }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592 }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542 }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765 }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848 }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515 }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159 }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185 }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386 }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384 }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599 }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021 }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360 }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628 }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321 }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723 }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400 }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835 }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225 }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541 }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251 }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807 }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935 }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720 }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498 }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413 }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084 }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579 }, +] + [[package]] name = "platformdirs" version = "4.3.8" @@ -1692,6 +1785,109 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614 }, ] +[[package]] +name = "scikit-image" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "lazy-loader" }, + { name = "networkx" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "scipy" }, + { name = "tifffile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329 }, + { url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726 }, + { url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910 }, + { url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939 }, + { url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938 }, + { url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243 }, + { url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770 }, + { url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506 }, + { url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278 }, + { url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142 }, + { url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086 }, + { url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667 }, + { url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966 }, + { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526 }, + { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629 }, + { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755 }, + { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810 }, + { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717 }, + { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520 }, + { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340 }, + { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839 }, + { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021 }, + { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490 }, + { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782 }, + { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060 }, + { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628 }, + { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369 }, + { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431 }, + { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362 }, + { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151 }, + { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484 }, + { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501 }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199 }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001 }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719 }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595 }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429 }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952 }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063 }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449 }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943 }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621 }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708 }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135 }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977 }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601 }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667 }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159 }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771 }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910 }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980 }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543 }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510 }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131 }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032 }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766 }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007 }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333 }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066 }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763 }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984 }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877 }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750 }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858 }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723 }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098 }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397 }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163 }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291 }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317 }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327 }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165 }, +] + [[package]] name = "send2trash" version = "1.8.3" @@ -1895,6 +2091,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154 }, ] +[[package]] +name = "tifffile" +version = "2026.4.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/4a/e687f5957fead200faad58dbf9c9431a2bbb118040e96f5fb8a55f7ebc50/tifffile-2026.4.11.tar.gz", hash = "sha256:17758ff0c0d4db385792a083ad3ca51fcb0f4d942642f4d8f8bc1287fdcf17bc", size = 394956 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/9f/74f110b4271ded519c7add4341cbabc824de26817ff1c345b3109df9e99c/tifffile-2026.4.11-py3-none-any.whl", hash = "sha256:9b94ffeddb39e97601af646345e8808f885773de01b299e480ed6d3a41509ec9", size = 248227 }, +] + [[package]] name = "tinycss2" version = "1.4.0" @@ -2023,7 +2232,11 @@ source = { editable = "." } dependencies = [ { name = "fire" }, { name = "jinja2" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "pandas" }, + { name = "scikit-image" }, + { name = "scipy" }, ] [package.dev-dependencies] @@ -2050,20 +2263,23 @@ notebooks = [ requires-dist = [ { name = "fire", specifier = ">=0.7.1" }, { name = "jinja2", specifier = ">=3.1.6" }, + { name = "numpy", specifier = ">=2.2" }, { name = "pandas", specifier = ">=3.0.2" }, + { name = "scikit-image", specifier = ">=0.25" }, + { name = "scipy", specifier = ">=1.15" }, ] [package.metadata.requires-dev] dev = [ - { name = "poethepoet", specifier = ">=0.44.0" }, + { name = "poethepoet", specifier = ">=0.44" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-cov", specifier = ">=5" }, { name = "ruff", specifier = ">=0.15.10" }, ] docs = [ - { name = "myst-nb", specifier = ">=1.4.0" }, - { name = "pydata-sphinx-theme", specifier = ">=0.17.0" }, - { name = "sphinx", specifier = ">=9.1.0" }, + { name = "myst-nb", specifier = ">=1.4" }, + { name = "pydata-sphinx-theme", specifier = ">=0.17" }, + { name = "sphinx", specifier = ">=9.1" }, ] notebooks = [ { name = "black", specifier = ">=26.3.1" },