diff --git a/.ruff.toml b/.ruff.toml index 661f7ef..4c2313e 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -3,7 +3,7 @@ line-length = 88 target-version = "py311" extend-exclude = ["rationai/qc/blur/piqe.py"] - +force-exclude = true [format] # Enable reformatting of code snippets in docstrings. docstring-code-format = true diff --git a/docs/getting-started/quality-control.md b/docs/getting-started/quality-control.md index d7a11c5..9bb5618 100644 --- a/docs/getting-started/quality-control.md +++ b/docs/getting-started/quality-control.md @@ -38,7 +38,6 @@ Next, we will need to prepare the arguments for the [residual_artifacts_and_cove function and call it on the input image: ```python linenums="9" - conversion = StandardConversions.RGB2HER nucleus_area = 150 res_index = 2 @@ -89,25 +88,25 @@ from rationai.masks import tissue_mask from rationai.qc import blur_score_piqe -img_a = np.asarray(Image.open("blur_A.png").convert("RGB")) # Focused image -img_b = np.asarray(Image.open("blur_B.png").convert("RGB")) # Partially blurred image -img_c = np.asarray(Image.open("blur_C.png").convert("RGB")) # Blurred image +img_a = np.asarray(Image.open("blur_A.png").convert("RGB")) # Focused image +img_b = np.asarray(Image.open("blur_B.png").convert("RGB")) # Partially blurred image +img_c = np.asarray(Image.open("blur_C.png").convert("RGB")) # Blurred image ``` Next, we will need to prepare the arguments for the [blur_score_piqe](../api/blur/blur-score-piqe.md) function and call it on the input images: ```python linenums="12" -pixel_size = 0.44 # pixel size of input images in micrometers +pixel_size = 0.44 # pixel size of input images in micrometers # blur score without tissue mask blur_score_a = blur_score_piqe(img_a, pixel_size) blur_score_b = blur_score_piqe(img_b, pixel_size) tissue_mask_img = tissue_mask( - pyvips.Image.new_from_array(img_c), mpp=pixel_size + pyvips.Image.new_from_array(img_c), mpp=pixel_size ).numpy() -tissue_mask_img = (tissue_mask_img > 0).astype(int) # binarize mask +tissue_mask_img = (tissue_mask_img > 0).astype(int) # binarize mask # blur score with tissue mask blur_score_c = blur_score_piqe(img_c, pixel_size, tissue_mask_img) @@ -124,7 +123,6 @@ After obtaining the results, we can save the computed blur score masks. Since `blur_score_coverage` is homogeneous across the image, we can just print out any value from the score mask for quick inspection: ```python linenums="29" - print(blur_score_a["blur_score_coverage"][0][0]) # save the coverage masks @@ -205,30 +203,32 @@ from rationai.qc import folding mpp = 1.76 img = np.asarray(Image.open("fold.png").convert("RGB")) # Investigated image -local_area_image = np.asarray(Image.open("fold_area.png").convert("RGB")) # Local area of investigated image +neigborhood_image = np.asarray( + Image.open("fold_area.png").convert("RGB") +) # Neighborhood area of investigated image ``` Now we can calculate the tissue masks: ```python linenums="12" -img_mask = tissue_mask( - pyvips.Image.new_from_array(img), mpp=mpp -).numpy() > 0 +img_mask = tissue_mask(pyvips.Image.new_from_array(img), mpp=mpp).numpy() > 0 -img_area_mask = tissue_mask( - pyvips.Image.new_from_array(local_area_image), mpp=mpp -).numpy() > 0 +neighborhood_mask = ( + tissue_mask(pyvips.Image.new_from_array(neigborhood_image), mpp=mpp).numpy() > 0 +) ``` Now we have all the arguments prepared. The folding function can be called: ```python linenums="20" -artifacts = folding(img=img, - mpp=mpp, - hematoxylin_eosin_stained=True, - tissue_mask=img_mask, - local_tiles=local_area_image, - local_mask=img_area_mask) +artifacts = folding( + img=img, + mpp=mpp, + hematoxylin_eosin_stained=True, + tissue_mask=img_mask, + neighborhood_tiles=neigborhood_image, + neighborhood_mask=neighborhood_mask, +) ``` #### Results diff --git a/rationai/qc/blur/blur_score_laplacian.py b/rationai/qc/blur/blur_score_laplacian.py index 7e48cad..00a3fc1 100644 --- a/rationai/qc/blur/blur_score_laplacian.py +++ b/rationai/qc/blur/blur_score_laplacian.py @@ -1,8 +1,8 @@ import numpy as np -from rationai.staining import ColorConversion, convert_color +from rationai.staining import StandardConversions, convert_color from skimage.color import rgb2gray from skimage.filters import laplace, threshold_otsu -from skimage.morphology import binary_dilation, binary_erosion +from skimage.morphology import dilation, erosion from rationai.qc.blur.utils import ( get_coverage_mask, @@ -45,10 +45,12 @@ def blur_score_laplacian( Note: The returned dictionary contains the following values: - | Key | Description | - |-----------------------|---------------------------------| - | `blur_score_per_pixel`| Binary mask of the blur score. | - | `blur_score_coverage` | Coverage mask of the blur score.| + | Key | Description | + |-----------------------------|-------------------------------------------------------| + | `blur_score_per_pixel` | Binary mask of the blur score. | + | `blur_score_coverage` | Coverage mask of the blur score. | + | `number_of_examined_pixels` | Number of pixels that were evaluated by the function. | + | `number_of_flagged_pixels` | Number of pixels labeled as artifacts. | Examples: ```python @@ -73,7 +75,7 @@ def blur_score_laplacian( if foreground_mask is None: foreground_mask = simple_foreground_mask(grayscale_img) - hematoxylin, _, _ = convert_color(img, ColorConversion.RGB2HER) + hematoxylin, _, _ = convert_color(img, StandardConversions.RGB2HER) hematoxylin_threshold = threshold_otsu(hematoxylin) hematoxylin_mask = hematoxylin > hematoxylin_threshold @@ -84,20 +86,20 @@ def blur_score_laplacian( # 2 pixels wide border is removed from the pooling mask for each hematoxylin nucleus # to ignore gradient values on the edges of the nuclei pooling_mask = foreground_mask * ~( - binary_dilation(hematoxylin_mask, footprint=footprint) - ^ binary_erosion(hematoxylin_mask, footprint=footprint) + dilation(hematoxylin_mask, footprint=footprint) + ^ erosion(hematoxylin_mask, footprint=footprint) ) blur_score = np.abs(gradient) - blur_score_per_pixel = masked_average_pooling(blur_score, pooling_mask) + blur_score_pooled = masked_average_pooling(blur_score, pooling_mask) # Threshold was set to 5 based on empirical testing # Can be adjusted based on the desired sensitivity # Higher threshold means more pixels are considered blurred - blur_score_per_pixel = blur_score_per_pixel < threshold + blur_score_per_pixel = blur_score_pooled < threshold # activity_mask is multiplied by the foreground mask to nullify background pixels - blur_score_per_pixel = blur_score_per_pixel * foreground_mask + blur_score_per_pixel *= foreground_mask return { "blur_score_per_pixel": blur_score_per_pixel, @@ -106,4 +108,6 @@ def blur_score_laplacian( detection_mask=blur_score_per_pixel, foreground_mask=foreground_mask, ), + "number_of_examined_pixels": int(np.count_nonzero(foreground_mask)), + "number_of_flagged_pixels": int(np.count_nonzero(blur_score_per_pixel)), } diff --git a/rationai/qc/blur/blur_score_piqe.py b/rationai/qc/blur/blur_score_piqe.py index c9f4958..64f4810 100644 --- a/rationai/qc/blur/blur_score_piqe.py +++ b/rationai/qc/blur/blur_score_piqe.py @@ -33,10 +33,12 @@ def blur_score_piqe( Note: The returned dictionary contains the following values: - | Key | Description | - |-----------------------|---------------------------------| - | `blur_score_per_pixel`| Binary mask of the blur score. | - | `blur_score_coverage` | Coverage mask of the blur score.| + | Key | Description | + |-----------------------------|-------------------------------------------------------| + | `blur_score_per_pixel` | Binary mask of the blur score. | + | `blur_score_coverage` | Coverage mask of the blur score. | + | `number_of_examined_pixels` | Number of pixels that were evaluated by the function. | + | `number_of_flagged_pixels` | Number of pixels labeled as artifacts. | Examples: ```python @@ -77,19 +79,22 @@ def blur_score_piqe( # Activity mask is one column shorter for images # with dimensions that are not divisible by 16 if grayscale_img.shape != activity_mask.shape: - activity_mask = np.pad(activity_mask, ((0, 0), (0, 1)), mode="edge") + pad_h = grayscale_img.shape[0] - activity_mask.shape[0] + pad_w = grayscale_img.shape[1] - activity_mask.shape[1] + + activity_mask = np.pad(activity_mask, ((0, pad_h), (0, pad_w)), mode="edge") if foreground_mask is None: foreground_mask = simple_foreground_mask(grayscale_img) - # activity_mask is inverted, blurred areas and empty areas (background) are set to 1 - activity_mask = ~(activity_mask > 0) - # activity_mask is multiplied by the foreground mask to nullify background pixels - activity_mask = activity_mask * foreground_mask + # Invert and restrict the mask to foreground + activity_mask = foreground_mask * ~(activity_mask > 0) return { - "blur_score_per_pixel": activity_mask > 0, + "blur_score_per_pixel": activity_mask, "blur_score_coverage": get_coverage_mask( grayscale_img, activity_mask, foreground_mask ), + "number_of_examined_pixels": int(np.count_nonzero(foreground_mask)), + "number_of_flagged_pixels": int(np.count_nonzero(activity_mask)), } diff --git a/rationai/qc/blur/blur_score_roberts.py b/rationai/qc/blur/blur_score_roberts.py index 8cfd728..781edc8 100644 --- a/rationai/qc/blur/blur_score_roberts.py +++ b/rationai/qc/blur/blur_score_roberts.py @@ -40,10 +40,12 @@ def blur_score_roberts( Note: The returned dictionary contains the following values: - | Key | Description | - |-----------------------|---------------------------------| - | `blur_score_per_pixel`| Binary mask of the blur score. | - | `blur_score_coverage` | Coverage mask of the blur score.| + | Key | Description | + |-----------------------------|-------------------------------------------------------| + | `blur_score_per_pixel` | Binary mask of the blur score. | + | `blur_score_coverage` | Coverage mask of the blur score. | + | `number_of_examined_pixels` | Number of pixels that were evaluated by the function. | + | `number_of_flagged_pixels` | Number of pixels labeled as artifacts. | Examples: ```python @@ -72,17 +74,17 @@ def blur_score_roberts( gaussian_gradient = roberts(gaussian(grayscale_img, sigma=1)) blur_score = np.abs(gradient - gaussian_gradient) - blur_score_per_pixel = masked_average_pooling( + blur_score_pooled = masked_average_pooling( arr=blur_score, foreground_mask=foreground_mask ) # Threshold was set to 2 based on empirical testing # Can be adjusted based on the desired sensitivity # Higher threshold means more pixels are considered blurred - blur_score_per_pixel = blur_score_per_pixel < threshold + blur_score_per_pixel = blur_score_pooled < threshold # activity_mask is multiplied by the foreground mask to nullify background pixels - blur_score_per_pixel = blur_score_per_pixel * foreground_mask + blur_score_per_pixel *= foreground_mask return { "blur_score_per_pixel": blur_score_per_pixel, @@ -91,4 +93,6 @@ def blur_score_roberts( detection_mask=blur_score_per_pixel, foreground_mask=foreground_mask, ), + "number_of_examined_pixels": int(np.count_nonzero(foreground_mask)), + "number_of_flagged_pixels": int(np.count_nonzero(blur_score_per_pixel)), } diff --git a/rationai/qc/blur/utils.py b/rationai/qc/blur/utils.py index 3232ab4..d4a2950 100644 --- a/rationai/qc/blur/utils.py +++ b/rationai/qc/blur/utils.py @@ -1,10 +1,15 @@ +from typing import Any + import numpy as np -from skimage.morphology import area_opening, binary_erosion +from numpy.typing import NDArray +from skimage.morphology import area_opening, erosion -from rationai.qc.typing import BinaryMask, GrayScaleImage, NDArray +from rationai.qc.typing import BinaryMask, FloatingPointImage -def masked_average_pooling(arr, foreground_mask): +def masked_average_pooling( + arr: FloatingPointImage, foreground_mask: BinaryMask +) -> FloatingPointImage: """Perform masked average pooling, creating mask of 16x16 blocks. For images with dimensions not divisible by 16, the function pads the image @@ -55,14 +60,14 @@ def masked_average_pooling(arr, foreground_mask): def get_coverage_mask( - grayscale_img: GrayScaleImage, + tissue_img: NDArray[Any], detection_mask: BinaryMask, foreground_mask: BinaryMask, -) -> NDArray[np.float64]: - """Creates a blur coverage mask based on the grayscale image and binary mask of blur detections. +) -> FloatingPointImage: + """Creates a blur coverage mask based on the tissue image and binary mask of blur detections. Args: - grayscale_img: Grayscale image of the tissue. + tissue_img: Image of the tissue. detection_mask: Binary image of the blur detection. foreground_mask: Binary mask of the tissue, where 1 represents tissue and 0 represents background. @@ -72,7 +77,7 @@ def get_coverage_mask( """ foreground_area = np.count_nonzero(foreground_mask) - coverage_mask = np.ones_like(grayscale_img).astype(float) + coverage_mask = np.ones_like(tissue_img).astype(np.float64) coverage = ( (np.sum(detection_mask * foreground_mask) / foreground_area) if foreground_area > 0 @@ -84,7 +89,7 @@ def get_coverage_mask( def simple_foreground_mask( - grayscale_img: NDArray[np.float64], + grayscale_img: FloatingPointImage, threshold: float = 0.92, # ~235/255 ) -> BinaryMask: """Creates a simple foreground mask using thresholding and morphological operations. @@ -101,6 +106,6 @@ def simple_foreground_mask( foreground_mask = grayscale_img < threshold foreground_mask = area_opening(foreground_mask) - foreground_mask = binary_erosion(foreground_mask, footprint=np.ones((5, 5))) + foreground_mask = erosion(foreground_mask, footprint=np.ones((5, 5))) return foreground_mask diff --git a/rationai/qc/folding/folding.py b/rationai/qc/folding/folding.py index 1225a35..aa57664 100644 --- a/rationai/qc/folding/folding.py +++ b/rationai/qc/folding/folding.py @@ -1,38 +1,40 @@ import numpy as np from numpy.ma import MaskedArray -from numpy.typing import NDArray -from rationai.staining import ColorConversion, convert_color +from rationai.staining import StandardConversions, convert_color from skimage.color import rgb2hsv from skimage.filters import threshold_yen -from skimage.morphology import binary_opening, disk, reconstruction +from skimage.morphology import disk, opening, reconstruction -from rationai.qc.typing import BinaryMask, FoldArtifacts, RGBImage +from rationai.qc.typing import BinaryMask, FloatingPointImage, FoldArtifacts, RGBImage def _get_threshold( - img: NDArray[np.float64], + img: FloatingPointImage, mask: BinaryMask, - local_tiles: NDArray[np.float64] | None = None, - local_mask: BinaryMask | None = None, + neighborhood_tiles: FloatingPointImage | None = None, + neighborhood_mask: BinaryMask | None = None, ) -> float: """Calculates adequate threshold from given images. Args: img: A given channel of an image. mask: Background mask of an image. - local_tiles: Optional n*n tiles in local neighbourhood of tile. Defaults to None. - local_mask: Optional n*n background mask of local_tiles. Defaults to None. + neighborhood_tiles: Optional n*n tiles in local neighbourhood of tile. Defaults to None. + neighborhood_mask: Optional n*n background mask of neighborhood_tiles. Defaults to None. Returns: Value which can be used to threshold the image. """ - if local_tiles is not None and local_mask is not None: - local_values = MaskedArray(local_tiles, ~local_mask).compressed() + if (neighborhood_tiles is None) != (neighborhood_mask is None): + raise ValueError( + "Both neighborhood_tiles and neighborhood_mask must be provided together." + ) + + if neighborhood_tiles is not None and neighborhood_mask is not None: + local_values = MaskedArray(neighborhood_tiles, ~neighborhood_mask).compressed() if local_values.size > 0: return threshold_yen(local_values) - else: - return 1.0 values = MaskedArray(img, ~mask).compressed() @@ -47,8 +49,8 @@ def folding( mpp: float, hematoxylin_eosin_stained: bool, tissue_mask: BinaryMask, - local_tiles: RGBImage | None = None, - local_mask: BinaryMask | None = None, + neighborhood_tiles: RGBImage | None = None, + neighborhood_mask: BinaryMask | None = None, cell_nucleus_size: float = 7, ) -> FoldArtifacts: """Creates a binary mask of folding artifacts. @@ -59,8 +61,8 @@ def folding( hematoxylin_eosin_stained: True if image is stained using Hematoxylin and Eosin. tissue_mask: A mask, where the tissue is labeled 1 and the background 0, should be as pixel-precise as possible. - local_tiles: A local area surrounding the given tile. - local_mask: Tissue mask of local_tiles. + neighborhood_tiles: An image of neighborhood surrounding the given tile. + neighborhood_mask: Tissue mask of neighborhood_tiles. cell_nucleus_size: Cell nucleus size in microns. This value is used for morphological operations. If estimating the value, it is better to overestimate the value. The default value is 7 based on empirical observations. @@ -71,12 +73,14 @@ def folding( Note: The returned dictionary contains the following values: - | Key | Description | - |---------------------------|---------------------------------------| - | `folding` | Binary mask of the detected folds. | - | `thresholded_saturation` | | - | `thresholded_value` | | - | `thresholded_eosin` | | + | Key | Description | + |-----------------------------|-------------------------------------------------------| + | `folding_per_pixel` | Binary mask of the detected folds. | + | `thresholded_saturation` | | + | `thresholded_value` | | + | `thresholded_eosin` | | + | `number_of_examined_pixels` | Number of pixels that were evaluated by the function. | + | `number_of_flagged_pixels` | Number of pixels labeled as artifacts. | Examples: ```python @@ -90,7 +94,7 @@ def folding( result = folding(img, 8, False, tissue_mask) - mask = result["folding"] # Contains values 0 and 1 + mask = result["folding_per_pixel"] # Contains values 0 and 1 ``` """ @@ -102,35 +106,37 @@ def folding( None, None, ) - if local_tiles is not None: - hsv_local = rgb2hsv(local_tiles) + if neighborhood_tiles is not None: + hsv_local = rgb2hsv(neighborhood_tiles) local_saturation_channel, local_value_channel = ( hsv_local[:, :, 1], hsv_local[:, :, 2], ) local_value_channel = 1 - local_value_channel if hematoxylin_eosin_stained: - _, eosin_channel, _ = convert_color(tile, ColorConversion.RGB2HER) - if local_tiles is not None: + _, eosin_channel, _ = convert_color(tile, StandardConversions.RGB2HER) + if neighborhood_tiles is not None: _, local_eosin_channel, _ = convert_color( - local_tiles, ColorConversion.RGB2HER + neighborhood_tiles, StandardConversions.RGB2HER ) else: eosin_channel = np.ones_like(tissue_mask) - if local_tiles is not None: - local_eosin_channel = np.ones_like(local_tiles) + if neighborhood_tiles is not None: + local_eosin_channel = np.ones( + neighborhood_tiles.shape[:2], dtype=np.float64 + ) inverted_value_channel = 1 - value_channel value_threshold = _get_threshold( - inverted_value_channel, tissue_mask, local_value_channel, local_mask + inverted_value_channel, tissue_mask, local_value_channel, neighborhood_mask ) saturation_threshold = _get_threshold( - saturation_channel, tissue_mask, local_saturation_channel, local_mask + saturation_channel, tissue_mask, local_saturation_channel, neighborhood_mask ) - if hematoxylin_eosin_stained: + if hematoxylin_eosin_stained and eosin_channel is not None: eosin_threshold = _get_threshold( - eosin_channel, tissue_mask, local_eosin_channel, local_mask + eosin_channel, tissue_mask, local_eosin_channel, neighborhood_mask ) else: eosin_threshold = 0 @@ -140,30 +146,30 @@ def folding( thresholded_eosin = eosin_channel > eosin_threshold if ( - np.sum(thresholded_value) * 2 > tile.size - or np.sum(thresholded_saturation) * 2 > tile.size - or (hematoxylin_eosin_stained and np.sum(thresholded_eosin) * 2 > tile.size) + np.sum(thresholded_value) * 2 > np.prod(tile.shape[:2]) + or np.sum(thresholded_saturation) * 2 > np.prod(tile.shape[:2]) + or ( + hematoxylin_eosin_stained + and np.sum(thresholded_eosin) * 2 > np.prod(tile.shape[:2]) + ) ): thresholded_value = np.zeros(tile.shape) - folding_test_markers = binary_opening( + folding_test_markers = opening( thresholded_eosin & thresholded_saturation & thresholded_value, disk(cell_nucleus_size // (mpp)), ) if hematoxylin_eosin_stained: folding_test = reconstruction(folding_test_markers, thresholded_eosin) - result: FoldArtifacts = { - "folding": folding_test, - "thresholded_saturation": thresholded_saturation, - "thresholded_eosin": thresholded_eosin, - "thresholded_value": thresholded_value, - } - return result - result: FoldArtifacts = { - "folding": folding_test, + else: + folding_test = folding_test_markers + + return { + "folding_per_pixel": folding_test, "thresholded_saturation": thresholded_saturation, "thresholded_eosin": thresholded_eosin, "thresholded_value": thresholded_value, + "number_of_examined_pixels": int(np.count_nonzero(tissue_mask)), + "number_of_flagged_pixels": int(np.count_nonzero(folding_test)), } - return result diff --git a/rationai/qc/residual_artifacts/residual_artifacts_and_coverage.py b/rationai/qc/residual_artifacts/residual_artifacts_and_coverage.py index b9cee91..a7e431c 100644 --- a/rationai/qc/residual_artifacts/residual_artifacts_and_coverage.py +++ b/rationai/qc/residual_artifacts/residual_artifacts_and_coverage.py @@ -39,7 +39,7 @@ def _get_debris_coverage( nucleus_area: float, res_index: int, threshold: float, -) -> tuple[float, BinaryMask]: +) -> tuple[int, int, BinaryMask]: """Calculates the percentage of the foreground debris coverage. Args: @@ -55,8 +55,8 @@ def _get_debris_coverage( slightly higher values could also provide reasonable results. Returns: - Number of pixels marked as artifact compared to number - of foreground pixels and thresholded residual channel. + Number of foreground pixels examined, number of foreground pixels + marked as artifact, and the thresholded residual channel. """ residual = np.asarray( convert_color(tile=img, conversion=conv)[res_index], dtype=np.float64 @@ -73,11 +73,9 @@ def _get_debris_coverage( residual_mask *= foreground_mask if foreground_area <= 0: - return 0.0, residual_mask + return 0, 0, residual_mask - return float( - round(np.count_nonzero(residual_mask) / foreground_area, 4) - ), residual_mask + return int(foreground_area), int(np.count_nonzero(residual_mask)), residual_mask def residual_artifacts_and_coverage( @@ -102,35 +100,37 @@ def residual_artifacts_and_coverage( slightly higher values could also provide reasonable results. Returns: - Dictionary with a binary coverage mask and a coverage number. + Dictionary with a number of examined pixels, number of flagged pixels, + and a binary mask of artifacts. Note: The returned dictionary contains the following values: - | Key | Description | - |-------------------|-----------------------------------------------------------------------------------------------| - | `coverage_mask` | Binary mask of the detected residual artifacts. | - | `coverage` | A number that states what portion of the image's foreground area is covered by the artifacts. | + | Key | Description | + |-----------------------------|-------------------------------------------------------| + | `artifacts_per_pixel` | Binary mask of the detected residual artifacts. | + | `number_of_examined_pixels` | Number of pixels that were evaluated by the function. | + | `number_of_flagged_pixels` | Number of pixels labeled as artifacts. | Examples: ```python from skimage.data import immunohistochemistry from rationai.qc import residual_artifacts_and_coverage - from rationai.staining import ColorConversion + from rationai.staining import StandardConversions img = immunohistochemistry() result = residual_artifacts_and_coverage( - img, ColorConversion.RGB2HDR, nucleus_area=150, res_index=2, threshold=0.013 + img, StandardConversions.RGB2HDR, nucleus_area=150, res_index=2, threshold=0.013 ) - mask = result["coverage_mask"] # Contains values 0 and 1 - print(result["coverage"]) + mask = result["artifacts_per_pixel"] # Contains values 0 and 1 + print(result["number_of_flagged_pixels"], result["number_of_examined_pixels"]) ``` """ - coverage, cov_heatmap = _get_debris_coverage( + num_examined, num_flagged, cov_heatmap = _get_debris_coverage( img=img, conv=conversion, nucleus_area=nucleus_area, @@ -139,8 +139,9 @@ def residual_artifacts_and_coverage( ) result: ResidualArtifacts = { - "coverage_mask": cov_heatmap, - "coverage": coverage, + "artifacts_per_pixel": cov_heatmap, + "number_of_examined_pixels": num_examined, + "number_of_flagged_pixels": num_flagged, } return result diff --git a/rationai/qc/staining/color_difference.py b/rationai/qc/staining/color_difference.py index d59b426..14acfff 100644 --- a/rationai/qc/staining/color_difference.py +++ b/rationai/qc/staining/color_difference.py @@ -1,6 +1,6 @@ import numpy as np from numpy.typing import NDArray -from rationai.staining import ColorConversion, ConversionType +from rationai.staining import ColorConversion, ConversionDirection from skimage.color import deltaE_cie76, deltaE_ciede94, deltaE_ciede2000, rgb2lab from rationai.qc.typing import Stain @@ -13,10 +13,10 @@ def _stain2rgb(stain: Stain) -> NDArray[np.float64]: def reference_stain(conversion: ColorConversion, index: int) -> Stain: """Return specified stain vector for a given conversion.""" - if conversion.conv_type == ConversionType.RGB2STAIN: + if conversion.direction == ConversionDirection.RGB2STAIN: conversion = conversion.inverse - return np.array(conversion.value[0][index], dtype=np.float64) + return conversion.matrix[index] def color_difference( diff --git a/rationai/qc/staining/staining_difference.py b/rationai/qc/staining/staining_difference.py index 7228350..45c3574 100644 --- a/rationai/qc/staining/staining_difference.py +++ b/rationai/qc/staining/staining_difference.py @@ -82,7 +82,7 @@ def staining_difference( from skimage.data import immunohistochemistry from rationai.qc.staining import dominant_stains, staining_difference - from rationai.staining import ColorConversion + from rationai.staining import StandardConversions img = immunohistochemistry() @@ -91,7 +91,7 @@ def staining_difference( stain1, stain2 = stains["stain1"], stains["stain2"] result = staining_difference( - ColorConversion.RGB2HDR, stain1, stain2, 33, 25, "ciede_2000", "cie_76" + StandardConversions.RGB2HDR, stain1, stain2, 33, 25, "ciede_2000", "cie_76" ) print(result["correct_staining"]) print(result["stain_diff1"], result["stain_diff2"]) diff --git a/rationai/qc/typing.py b/rationai/qc/typing.py index 42cc42f..c1c8fdb 100644 --- a/rationai/qc/typing.py +++ b/rationai/qc/typing.py @@ -19,6 +19,11 @@ Single channel grayscale image represented by a numpy array. """ +FloatingPointImage: TypeAlias = NDArray[np.float64] +""" +Floating point image represented by a numpy array. +""" + Stain: TypeAlias = NDArray[np.float64] """ Single stain vector (of 3 values) represented by a numpy array. @@ -51,16 +56,19 @@ class DominantStains(TypedDict): class ResidualArtifacts(TypedDict): - """Dictionary containing a coverage mask and a coverage number.""" + """Dictionary containing an artifact mask and a number of examined and flagged pixels.""" - coverage_mask: BinaryMask + artifacts_per_pixel: BinaryMask """Binary mask of the detected residual artifacts.""" - coverage: float - """A number that states what portion of the image's foreground area - is covered by the artifacts. + number_of_examined_pixels: int + """Number of pixels that were examined by the function and could + be theoretically marked as artifacts. """ + number_of_flagged_pixels: int + """Number of pixels that were labeled as artifacts by the function.""" + class StainingDifference(TypedDict): """Dictionary with color differences and a correct staining verdict.""" @@ -83,23 +91,45 @@ class BlurScore(TypedDict): blur_score_per_pixel: BinaryMask """Binary mask composed of 16x16 px blocks marking blurred area""" - blur_score_coverage: NDArray[np.float64] + blur_score_coverage: FloatingPointImage """Coverage mask of the blur score. Coverage ranges from 0.0 to 1.0. 0.0 - no blur, 1.0 - full blur """ + number_of_examined_pixels: int + """Number of pixels that were examined by the function and could + be theoretically marked as artifacts. + """ + + number_of_flagged_pixels: int + """Number of pixels that were labeled as artifacts by the function.""" + class FoldArtifacts(TypedDict): """Dictionary containing the fold detection mask and intermediate results.""" - folding: BinaryMask + folding_per_pixel: BinaryMask """Mask of the fold detection""" thresholded_saturation: BinaryMask - """Thresholded saturation channel. Intermediate result of folding detection. Can be used for debugging.""" + """Thresholded saturation channel. Intermediate result of folding detection. + Can be used for debugging. + """ thresholded_value: BinaryMask - """Thresholded value channel.Intermediate result of folding detection. Can be used for debugging.""" + """Thresholded value channel. Intermediate result of folding detection. + Can be used for debugging. + """ thresholded_eosin: BinaryMask - """Thresholded eosin channel.Intermediate result of folding detection. Can be used for debugging.""" + """Thresholded eosin channel. Intermediate result of folding detection. + Can be used for debugging. + """ + + number_of_examined_pixels: int + """Number of pixels that were examined by the function and could + be theoretically marked as artifacts. + """ + + number_of_flagged_pixels: int + """Number of pixels that were labeled as artifacts by the function.""" diff --git a/uv.lock b/uv.lock index 445d213..6979abf 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -1930,28 +1930,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, - { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, - { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, - { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, - { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, + { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, + { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, + { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, + { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, ] [[package]] @@ -2369,6 +2368,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, { url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" },