Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handle images without enough tile layers #1648

Merged
merged 1 commit into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## 1.29.12

### Improvements

- Better handle images without enough tile layers ([#1648](../../pull/1648))

## 1.29.11

### Changes
Expand Down
42 changes: 39 additions & 3 deletions large_image/tilesource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1491,7 +1491,8 @@ def _nonemptyLevelsList(self, frame: Optional[int] = 0) -> List[bool]:
"""
return [True] * self.levels

def _getTileFromEmptyLevel(self, x: int, y: int, z: int, **kwargs) -> PIL.Image.Image:
def _getTileFromEmptyLevel(self, x: int, y: int, z: int, **kwargs) -> Tuple[
Union[PIL.Image.Image, np.ndarray], str]:
"""
Given the x, y, z tile location in an unpopulated level, get tiles from
higher resolution levels to make the lower-res tile.
Expand All @@ -1508,6 +1509,39 @@ def _getTileFromEmptyLevel(self, x: int, y: int, z: int, **kwargs) -> PIL.Image.
while dirlist[z] is None:
scale *= 2
z += 1
# if scale >= max(tileWidth, tileHeight), we can just get one tile per
# pixel at this point. If dtype is not uint8 or the number of bands is
# greater than 4, also just use nearest neighbor.
if (scale >= max(self.tileWidth, self.tileHeight) or
(self.dtype and self.dtype != np.uint8) or
(self.bandCount and self.bandCount > 4)):
nptile = np.zeros((self.tileHeight, self.tileWidth, cast(int, self.bandCount)))
maxX = 2.0 ** (z + 1 - self.levels) * self.sizeX / self.tileWidth
maxY = 2.0 ** (z + 1 - self.levels) * self.sizeY / self.tileHeight
for newY in range(scale):
sty = (y * scale + newY) * self.tileHeight
dy = sty % scale
ty = (newY * self.tileHeight) // scale
if (newY and y * scale + newY >= maxY) or dy >= self.tileHeight:
continue
for newX in range(scale):
stx = (x * scale + newX) * self.tileWidth
dx = stx % scale
if (newX and x * scale + newX >= maxX) or dx >= self.tileWidth:
continue
tx = (newX * self.tileWidth) // scale
if time.time() - lastlog > 10:
self.logger.info(
'Compositing tile from higher resolution tiles x=%d y=%d z=%d',
x * scale + newX, y * scale + newY, z)
lastlog = time.time()
subtile = self.getTile(
x * scale + newX, y * scale + newY, z,
pilImageAllowed=False, numpyAllowed='always',
sparseFallback=True, edge=False, frame=kwargs.get('frame'))
subtile = subtile[dx::scale, dy::scale]
nptile[ty:ty + subtile.shape[0], tx:tx + subtile.shape[1]] = subtile
return nptile, TILE_FORMAT_NUMPY
while z - basez > self._maxSkippedLevels:
z -= self._maxSkippedLevels
scale = int(scale / 2 ** self._maxSkippedLevels)
Expand All @@ -1530,10 +1564,12 @@ def _getTileFromEmptyLevel(self, x: int, y: int, z: int, **kwargs) -> PIL.Image.
pilImageAllowed=True, numpyAllowed=False,
sparseFallback=True, edge=False, frame=kwargs.get('frame'))
subtile = _imageToPIL(subtile)
mode = subtile.mode
tile.paste(subtile, (newX * self.tileWidth,
newY * self.tileHeight))
return tile.resize((self.tileWidth, self.tileHeight),
getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS)
return tile.resize(
(self.tileWidth, self.tileHeight),
getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS).convert(mode), TILE_FORMAT_PIL

@methodcache()
def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False,
Expand Down
10 changes: 5 additions & 5 deletions large_image/tilesource/tileiterator.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,18 +234,18 @@ def _tileIteratorInfo(self, **kwargs) -> Optional[Dict[str, Any]]: # noqa
# If we are scaling the result, pick the tile level that is at least
# the resolution we need and is preferred by the tile source.
if outWidth != regionWidth or outHeight != regionHeight:
newLevel = source.getPreferredLevel(preferredLevel + int(
math.ceil(round(math.log(max(float(outWidth) / regionWidth,
newLevel = source.getPreferredLevel(max(0, preferredLevel + int(
math.ceil(round(math.log(min(float(outWidth) / regionWidth,
float(outHeight) / regionHeight)) /
math.log(2), 4))))
math.log(2), 4)))))
if newLevel < preferredLevel:
# scale the bounds to the level we will use
factor = 2 ** (preferredLevel - newLevel)
left = int(left / factor)
right = int(right / factor)
right = max(int(right / factor), left + 1)
regionWidth = right - left
top = int(top / factor)
bottom = int(bottom / factor)
bottom = max(int(bottom / factor), top + 1)
regionHeight = bottom - top
preferredLevel = newLevel
requestedScale /= factor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs):
height = min(height, sizeYAtScale - offsety)

if scale >= 2 ** self._maxSkippedLevels:
tile = self._getTileFromEmptyLevel(x, y, z, **kwargs)
tile, _format = self._getTileFromEmptyLevel(x, y, z, **kwargs)
tile = large_image.tilesource.base._imageToNumpy(tile)[0]
format = TILE_FORMAT_NUMPY
else:
Expand Down
2 changes: 1 addition & 1 deletion sources/openjpeg/large_image_source_openjpeg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs):
x0, y0, x1, y1, step = self._xyzToCorners(x, y, z)
scale = None
if self._minlevel - z > self._maxSkippedLevels:
tile = self._getTileFromEmptyLevel(x, y, z, **kwargs)
tile, _format = self._getTileFromEmptyLevel(x, y, z, **kwargs)
tile = _imageToNumpy(tile)[0]
else:
if z < self._minlevel:
Expand Down
5 changes: 3 additions & 2 deletions sources/openslide/large_image_source_openslide/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs):
# scale we computed in the __init__ process for this svs level tells
# how much larger a region we need to read.
if svslevel['scale'] > 2 ** self._maxSkippedLevels:
tile = self._getTileFromEmptyLevel(x, y, z, **kwargs)
tile, format = self._getTileFromEmptyLevel(x, y, z, **kwargs)
else:
retries = 3
while retries > 0:
Expand All @@ -333,6 +333,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs):
(offsetx, offsety), svslevel['svslevel'],
(self.tileWidth * svslevel['scale'],
self.tileHeight * svslevel['scale']))
format = TILE_FORMAT_PIL
break
except openslide.lowlevel.OpenSlideError as exc:
self._largeImagePath = str(self._getLargeImagePath())
Expand All @@ -352,7 +353,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs):
if svslevel['scale'] != 1:
tile = tile.resize((self.tileWidth, self.tileHeight),
getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS)
return self._outputTile(tile, TILE_FORMAT_PIL, x, y, z, pilImageAllowed,
return self._outputTile(tile, format, x, y, z, pilImageAllowed,
numpyAllowed, **kwargs)

def getPreferredLevel(self, level):
Expand Down
3 changes: 1 addition & 2 deletions sources/tiff/large_image_source_tiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False,
if dir is None:
try:
if not kwargs.get('inSparseFallback'):
tile = self._getTileFromEmptyLevel(x, y, z, **kwargs)
tile, format = self._getTileFromEmptyLevel(x, y, z, **kwargs)
else:
raise IOTiffError('Missing z level %d' % z)
except Exception:
Expand All @@ -677,7 +677,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False,
else:
raise
allowStyle = False
format = TILE_FORMAT_PIL
else:
tile = dir.getTile(x, y, asarray=numpyAllowed == 'always')
format = 'JPEG'
Expand Down
13 changes: 12 additions & 1 deletion sources/tifffile/large_image_source_tifffile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,17 @@ def _nonemptyLevelsList(self, frame=0):
self._nonempty_levels_list[frame] = nonempty
return nonempty

def getPreferredLevel(self, level):
"""
Given a desired level (0 is minimum resolution, self.levels - 1 is max
resolution), return the level that contains actual data that is no
lower resolution.

:param level: desired level
:returns level: a level with actual data that is no lower resolution.
"""
return max(0, min(level, self.levels - 1))

def _getZarrArray(self, series, sidx):
with self._zarrlock:
if sidx not in self._zarrcache:
Expand Down Expand Up @@ -596,7 +607,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs):
else:
bza = za
if step > 2 ** self._maxSkippedLevels:
tile = self._getTileFromEmptyLevel(x, y, z, **kwargs)
tile, _format = self._getTileFromEmptyLevel(x, y, z, **kwargs)
tile = large_image.tilesource.base._imageToNumpy(tile)[0]
else:
sel = []
Expand Down
2 changes: 1 addition & 1 deletion sources/zarr/large_image_source_zarr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs):
y1 //= scale
step //= scale
if step > 2 ** self._maxSkippedLevels:
tile = self._getTileFromEmptyLevel(x, y, z, **kwargs)
tile, _format = self._getTileFromEmptyLevel(x, y, z, **kwargs)
tile = large_image.tilesource.base._imageToNumpy(tile)[0]
else:
idx = [slice(None) for _ in arr.shape]
Expand Down
2 changes: 1 addition & 1 deletion test/test_source_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ def testStyleFunctions():
region2, _ = sourceFunc2.getRegion(
output=dict(maxWidth=50),
format=large_image.constants.TILE_FORMAT_NUMPY)
assert np.any(region2 != region1)
assert np.any(region2[:, :, :3] != region1)
sourceFunc3 = large_image.open(imagePath, style={
'function': {
'name': 'large_image.tilesource.stylefuncs.maskPixelValues',
Expand Down