diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a7873278..0c72f6605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Add utility functions for converting between frame and axes ([#1777](../../pull/1777)) +- Add utility functions for converting between frame and axes ([#1778](../../pull/1778)) ### Improvements @@ -14,6 +14,10 @@ - Harden sources based on more fuzz testing ([#1774](../../pull/1774)) - Default to not caching source in notebooks ([#1776](../../pull/1776)) +### Changes + +- Allow umap-learn for graphing in girder annotations in python 3.13 ([#1780](../../pull/1780)) + ### Bug Fixes - Fix scaling tiles from stripped tiffs in some instances ([#1773](../../pull/1773)) diff --git a/girder_annotation/setup.py b/girder_annotation/setup.py index 452b43b28..c0d28a1f2 100644 --- a/girder_annotation/setup.py +++ b/girder_annotation/setup.py @@ -62,7 +62,7 @@ def prerelease_local_scheme(version): 'pandas ; python_version < "3.9"', 'pandas>=2.2 ; python_version >= "3.9"', 'python-calamine ; python_version >= "3.9"', - 'umap-learn ; python_version < "3.13"', + 'umap-learn', ], 'tasks': [ f'girder-large-image[tasks]{limit_version}', diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index 96c6d9b3f..10a76cd84 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -1006,7 +1006,7 @@ def _applyStyle( # noqa elif sc.mainImage.dtype.kind == 'f': sc.dtype = 'float' sc.axis = sc.axis if sc.axis is not None else entry.get('axis') - sc.bandidx = 0 if image.shape[2] <= 2 else 1 # type: ignore[misc] + sc.bandidx = 0 if image.shape[2] <= 2 else 1 sc.band = None if ((entry.get('frame') is None and not entry.get('framedelta')) or entry.get('frame') == sc.mainFrame): @@ -1021,29 +1021,29 @@ def _applyStyle( # noqa :sc.mainImage.shape[1], :sc.mainImage.shape[2]] if (isinstance(entry.get('band'), int) and - entry['band'] >= 1 and entry['band'] <= image.shape[2]): # type: ignore[misc] + entry['band'] >= 1 and entry['band'] <= image.shape[2]): sc.bandidx = entry['band'] - 1 sc.composite = entry.get('composite', 'lighten') if (hasattr(self, '_bandnames') and entry.get('band') and str(entry['band']).lower() in self._bandnames and - image.shape[2] > self._bandnames[ # type: ignore[misc] + image.shape[2] > self._bandnames[ str(entry['band']).lower()]): sc.bandidx = self._bandnames[str(entry['band']).lower()] - if entry.get('band') == 'red' and image.shape[2] > 2: # type: ignore[misc] + if entry.get('band') == 'red' and image.shape[2] > 2: sc.bandidx = 0 - elif entry.get('band') == 'blue' and image.shape[2] > 2: # type: ignore[misc] + elif entry.get('band') == 'blue' and image.shape[2] > 2: sc.bandidx = 2 sc.band = image[:, :, 2] elif entry.get('band') == 'alpha': - sc.bandidx = (image.shape[2] - 1 if image.shape[2] in (2, 4) # type: ignore[misc] + sc.bandidx = (image.shape[2] - 1 if image.shape[2] in (2, 4) else None) - sc.band = (image[:, :, -1] if image.shape[2] in (2, 4) else # type: ignore[misc] + sc.band = (image[:, :, -1] if image.shape[2] in (2, 4) else np.full(image.shape[:2], 255, np.uint8)) sc.composite = entry.get('composite', 'multiply') if sc.band is None: sc.band = image[ - :, :, sc.bandidx # type: ignore[index] - if sc.bandidx is not None and sc.bandidx < image.shape[2] # type: ignore[misc] + :, :, sc.bandidx + if sc.bandidx is not None and sc.bandidx < image.shape[2] else 0] sc.band = self._applyStyleFunction(sc.band, sc, 'preband') sc.palette = getPaletteColors(entry.get( @@ -1076,7 +1076,7 @@ def _applyStyle( # noqa # divide. # See https://docs.gimp.org/en/gimp-concepts-layer-modes.html for # some details. - for channel in range(sc.output.shape[2]): # type: ignore[misc] + for channel in range(sc.output.shape[2]): if np.all(sc.palette[:, channel] == sc.palette[0, channel]): if ((sc.palette[0, channel] == 0 and sc.composite != 'multiply') or (sc.palette[0, channel] == 255 and sc.composite == 'multiply')): @@ -1117,7 +1117,7 @@ def _applyStyle( # noqa sc.output = (sc.output * 65535 / 255).astype(np.uint16) elif sc.dtype == 'float': sc.output /= 255 - if sc.axis is not None and 0 <= int(sc.axis) < sc.output.shape[2]: # type: ignore[misc] + if sc.axis is not None and 0 <= int(sc.axis) < sc.output.shape[2]: sc.output = sc.output[:, :, sc.axis:sc.axis + 1] sc.output = self._applyStyleFunction(sc.output, sc, 'post') return sc.output @@ -1145,7 +1145,7 @@ def _outputTileNumpyStyle( tile = self._applyStyle(tile, getattr(self, 'style', None), x, y, z, frame) if tile.shape[0] != self.tileHeight or tile.shape[1] != self.tileWidth: extend = np.zeros( - (self.tileHeight, self.tileWidth, tile.shape[2]), # type: ignore[misc] + (self.tileHeight, self.tileWidth, tile.shape[2]), dtype=tile.dtype) extend[:min(self.tileHeight, tile.shape[0]), :min(self.tileWidth, tile.shape[1])] = tile diff --git a/large_image/tilesource/stylefuncs.py b/large_image/tilesource/stylefuncs.py index eec9daee5..518a040d6 100644 --- a/large_image/tilesource/stylefuncs.py +++ b/large_image/tilesource/stylefuncs.py @@ -93,9 +93,9 @@ def medianFilter( pimg: np.ndarray = image.astype(float) if len(pimg.shape) == 2: pimg = np.resize(pimg, (pimg.shape[0], pimg.shape[1], 1)) - pimg = pimg[:, :, :fimg.shape[2]] # type: ignore[index,misc] + pimg = pimg[:, :, :fimg.shape[2]] dimg = (pimg - fimg.astype(float) * mul) * weight - pimg = pimg[:, :, :fimg.shape[2]] + dimg # type: ignore[index,misc] + pimg = pimg[:, :, :fimg.shape[2]] + dimg if clip: pimg = pimg.clip(0, clip) if len(image.shape) != 3: diff --git a/large_image/tilesource/tiledict.py b/large_image/tilesource/tiledict.py index 971cbf989..4c0826b0b 100644 --- a/large_image/tilesource/tiledict.py +++ b/large_image/tilesource/tiledict.py @@ -156,7 +156,7 @@ def _retileTile(self) -> np.ndarray: elif tileData.shape[2] < retile.shape[2]: retile = retile[:, :, :tileData.shape[2]] retile[y0:y0 + th, x0:x0 + tw] = tileData[ - :th, :tw, :retile.shape[2]] # type: ignore[misc] + :th, :tw, :retile.shape[2]] return cast(np.ndarray, retile) def _resample(self, tileData: Union[ImageBytes, PIL.Image.Image, bytes, np.ndarray]) -> Tuple[ @@ -197,7 +197,7 @@ def _resample(self, tileData: Union[ImageBytes, PIL.Image.Image, bytes, np.ndarr tileData = skimage.transform.resize( cast(np.ndarray, tileData), (self['width'], self['height'], - cast(np.ndarray, tileData).shape[2]), # type: ignore[misc] + cast(np.ndarray, tileData).shape[2]), order=3 if self.resample is True else self.resample) return tileData, pilData diff --git a/large_image/tilesource/utilities.py b/large_image/tilesource/utilities.py index 698e952cf..3912ab4cd 100644 --- a/large_image/tilesource/utilities.py +++ b/large_image/tilesource/utilities.py @@ -213,28 +213,30 @@ def _imageToPIL( mode = modesBySize[image.shape[2] - 1] if len(image.shape) == 3 and image.shape[2] == 1: image = np.resize(image, image.shape[:2]) - if image.dtype == np.uint32: + if cast(np.ndarray, image).dtype == np.uint32: image = np.floor_divide(image, 2 ** 24).astype(np.uint8) - elif image.dtype == np.uint16: + elif cast(np.ndarray, image).dtype == np.uint16: image = np.floor_divide(image, 256).astype(np.uint8) - elif image.dtype == np.int8: - image = (image.astype(float) + 128).astype(np.uint8) - elif image.dtype == np.int16: - image = np.floor_divide(image.astype(float) + 2 ** 15, 256).astype(np.uint8) - elif image.dtype == np.int32: - image = np.floor_divide(image.astype(float) + 2 ** 31, 2 ** 24).astype(np.uint8) + elif cast(np.ndarray, image).dtype == np.int8: + image = (cast(np.ndarray, image).astype(float) + 128).astype(np.uint8) + elif cast(np.ndarray, image).dtype == np.int16: + image = np.floor_divide( + cast(np.ndarray, image).astype(float) + 2 ** 15, 256).astype(np.uint8) + elif cast(np.ndarray, image).dtype == np.int32: + image = np.floor_divide( + cast(np.ndarray, image).astype(float) + 2 ** 31, 2 ** 24).astype(np.uint8) # TODO: The scaling of float data needs to be identical across all # tiles of an image. This means that we need a reference to the parent # tile source or some other way of regulating it. - # elif image.dtype.kind == 'f': + # elif cast(np.ndarray, image).dtype.kind == 'f': # if numpy.max(image) > 1: # maxl2 = math.ceil(math.log(numpy.max(image) + 1) / math.log(2)) # image = image / ((2 ** maxl2) - 1) # image = (image * 255).astype(numpy.uint8) - elif image.dtype != np.uint8: + elif cast(np.ndarray, image).dtype != np.uint8: image = np.clip(np.nan_to_num(np.where( image is None, np.nan, image), nan=0), 0, 255).astype(np.uint8) - image = PIL.Image.fromarray(image, mode) + image = PIL.Image.fromarray(cast(np.ndarray, image), mode) elif not isinstance(image, PIL.Image.Image): image = PIL.Image.open(io.BytesIO(image)) if setMode is not None and image.mode != setMode: @@ -277,9 +279,10 @@ def _imageToNumpy( mode = modesBySize[(image.shape[2] - 1) if image.shape[2] <= 4 else 3] return image, mode mode = 'L' - if len(image.shape) == 2: - image = np.resize(image, (image.shape[0], image.shape[1], 1)) - return image, mode + if len(cast(np.ndarray, image).shape) == 2: + image = np.resize(cast(np.ndarray, image), + (cast(np.ndarray, image).shape[0], cast(np.ndarray, image).shape[1], 1)) + return cast(np.ndarray, image), mode def _letterboxImage(image: PIL.Image.Image, width: int, height: int, fill: str) -> PIL.Image.Image: @@ -811,7 +814,7 @@ def fullAlphaValue(arr: Union[np.ndarray, npt.DTypeLike]) -> int: if cast(np.dtype, dtype).kind == 'u': return np.iinfo(dtype).max if isinstance(arr, np.ndarray) and cast(np.dtype, dtype).kind == 'f': - amax = np.amax(arr) + amax = cast(float, np.amax(arr)) if amax > 1 and amax < 256: return 255 if amax > 1 and amax < 65536: @@ -844,24 +847,24 @@ def _makeSameChannelDepth(arr1: np.ndarray, arr2: np.ndarray) -> Tuple[np.ndarra # If any array is RGB, make sure all arrays are RGB. for key, arr in arrays.items(): other = arrays['arr1' if key == 'arr2' else 'arr2'] - if arr.shape[2] < 3 and other.shape[2] >= 3: # type: ignore[misc] + if arr.shape[2] < 3 and other.shape[2] >= 3: newarr = np.ones( - (arr.shape[0], arr.shape[1], arr.shape[2] + 2), # type: ignore[misc] + (arr.shape[0], arr.shape[1], arr.shape[2] + 2), dtype=arr.dtype) newarr[:, :, 0:1] = arr[:, :, 0:1] newarr[:, :, 1:2] = arr[:, :, 0:1] newarr[:, :, 2:3] = arr[:, :, 0:1] - if arr.shape[2] == 2: # type: ignore[misc] + if arr.shape[2] == 2: newarr[:, :, 3:4] = arr[:, :, 1:2] arrays[key] = newarr # If only one array has an A channel, make sure all arrays have an A # channel for key, arr in arrays.items(): other = arrays['arr1' if key == 'arr2' else 'arr2'] - if arr.shape[2] < other.shape[2]: # type: ignore[misc] + if arr.shape[2] < other.shape[2]: arrays[key] = np.pad( arr, - ((0, 0), (0, 0), (0, other.shape[2] - arr.shape[2])), # type: ignore[misc] + ((0, 0), (0, 0), (0, other.shape[2] - arr.shape[2])), constant_values=fullAlphaValue(arr)) return arrays['arr1'], arrays['arr2'] @@ -886,7 +889,7 @@ def _addSubimageToImage( if (x, y, width, height) == (0, 0, subimage.shape[1], subimage.shape[0]): return subimage image = np.zeros( - (height, width, subimage.shape[2]), # type: ignore[misc] + (height, width, subimage.shape[2]), dtype=subimage.dtype) elif len(image.shape) != len(subimage.shape) or image.shape[-1] != subimage.shape[-1]: image, subimage = _makeSameChannelDepth(image, subimage) @@ -944,7 +947,7 @@ def _addRegionTileToTiled( subimage = subimage.astype('d') vimgMem = pyvips.Image.new_from_memory( np.ascontiguousarray(subimage).data, - subimage.shape[1], subimage.shape[0], subimage.shape[2], # type: ignore[misc] + subimage.shape[1], subimage.shape[0], subimage.shape[2], dtypeToGValue[subimage.dtype.char]) vimg = pyvips.Image.new_temp_file('%s.v') vimgMem.write(vimg) @@ -955,7 +958,7 @@ def _addRegionTileToTiled( 'mm_x': tile.get('mm_x') if tile else None, 'mm_y': tile.get('mm_y') if tile else None, 'magnification': tile.get('magnification') if tile else None, - 'channels': subimage.shape[2], # type: ignore[misc] + 'channels': subimage.shape[2], 'strips': {}, } if y not in image['strips']: