Skip to content

Commit

Permalink
Allow umap-learn for graphing in girder annotations in python 3.13
Browse files Browse the repository at this point in the history
numba now supports python 3.13, which means all optional dependencies
can now be installed for Python 3.13.

With this change, CI is running with numpy 2.1.x rather than numpy
2.0.x, which changes what mypy detects as type issues.  Some types are
more properly detected, while others are missed, so there are a variety
of changes to make mypy happy.
  • Loading branch information
manthey committed Jan 21, 2025
1 parent 5e77521 commit c64647d
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 41 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion girder_annotation/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}',
Expand Down
24 changes: 12 additions & 12 deletions large_image/tilesource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)

Check warning on line 1038 in large_image/tilesource/base.py

View check run for this annotation

Codecov / codecov/patch

large_image/tilesource/base.py#L1038

Added line #L1038 was not covered by tests
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

Check warning on line 1040 in large_image/tilesource/base.py

View check run for this annotation

Codecov / codecov/patch

large_image/tilesource/base.py#L1040

Added line #L1040 was not covered by tests
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(
Expand Down Expand Up @@ -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')):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions large_image/tilesource/stylefuncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions large_image/tilesource/tiledict.py
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand Down Expand Up @@ -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

Expand Down
49 changes: 26 additions & 23 deletions large_image/tilesource/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Check warning on line 221 in large_image/tilesource/utilities.py

View check run for this annotation

Codecov / codecov/patch

large_image/tilesource/utilities.py#L221

Added line #L221 was not covered by tests
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(

Check warning on line 226 in large_image/tilesource/utilities.py

View check run for this annotation

Codecov / codecov/patch

large_image/tilesource/utilities.py#L226

Added line #L226 was not covered by tests
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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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']

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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']:
Expand Down

0 comments on commit c64647d

Please sign in to comment.