Skip to content

Commit b26763a

Browse files
authored
Merge pull request #64 from Leengit/debug_mask
BUG: Use magnification rather than level for large_image read
2 parents 6e99369 + e04cbe5 commit b26763a

File tree

3 files changed

+153
-102
lines changed

3 files changed

+153
-102
lines changed

histomics_stream/configure.py

Lines changed: 97 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88

99
class FindResolutionForSlide:
10-
"""A class that computes read parameters for slides.
10+
"""
11+
A class that computes read parameters for slides.
1112
1213
An instance of class FindResolutionForSlide is a callable that
1314
will add level, target_magnification, scan_magnification,
@@ -22,8 +23,8 @@ class FindResolutionForSlide:
2223
The path of the image file to be read.
2324
2425
target_magnification : float
25-
The desired objective magnification for generated tiles. For
26-
example, a value of 10 corresponds to about 1 micron per pixel
26+
The desired objective magnification for generated tiles. For
27+
example, a value of 10 corresponds to about 1 micron per pixel
2728
and a value of 20 corresponds to about 0.5 microns per pixel.
2829
2930
magnification_source : str in ["scan", "native", "exact"]
@@ -32,15 +33,15 @@ class FindResolutionForSlide:
3233
magnification.
3334
3435
"native" will produce tiles from the nearest available
35-
magnification equal to or greater than target_magnification
36-
(within a 2% tolerance). The "native" option is useful when
37-
you want to handle resizing of tiles to target_magnification
36+
magnification equal to or greater than target_magnification
37+
(within a 2% tolerance). The "native" option is useful when
38+
you want to handle resizing of tiles to target_magnification
3839
on your own.
3940
4041
"exact" will produce tiles using "native" option and then
41-
resize these tiles to match target_magnification. Resizing
42-
is handled by PIL using the Lanczos antialiasing filter
43-
since the resizing shrinks the tile by definition.
42+
resize these tiles to match target_magnification. Resizing is
43+
handled by PIL using the Lanczos antialiasing filter since the
44+
resizing shrinks the tile by definition.
4445
4546
For either "scan" or "native", the size of the read and
4647
returned tiles will be (tile_height * returned_magnification /
@@ -49,19 +50,24 @@ class FindResolutionForSlide:
4950
tiles will be (tile_height, tile_width).
5051
5152
This procedure sets values in the slide dictionary to capture
52-
the scan, read, and returned magnification of the tiles. This is
53-
helpful for example to resize results to the scan magnification
54-
for visualization in HistomicsUI, or to resize between native
55-
and target magnification when using "native". "scan_magnification"
56-
is the highest magnification from the source file; "read_magnification"
57-
is the magnification read from the source file; "returned_magnification"
58-
is the magnification of the returned tiles which is same as
59-
"read_magnification" in the case of "scan" or "native" or
60-
"target_magnification" in the case of "exact".
53+
the scan, read, and returned magnification of the tiles. This
54+
is helpful for example to resize results to the scan
55+
magnification for visualization in HistomicsUI, or to resize
56+
between native and target magnification when using
57+
"native". "scan_magnification" is the highest magnification
58+
from the source file; "read_magnification" is the
59+
magnification read from the source file;
60+
"returned_magnification" is the magnification of the returned
61+
tiles which is same as "read_magnification" in the case of
62+
"scan" or "native" or "target_magnification" in the case of
63+
"exact".
6164
"""
6265

6366
def __init__(self, study, target_magnification, magnification_source):
64-
"""Sanity check the supplied parameters and store them for later use."""
67+
"""
68+
Sanity check the supplied parameters and store them for later
69+
use.
70+
"""
6571
# Check values.
6672
if not ("version" in study and study["version"] == "version-1"):
6773
raise ValueError('study["version"] must exist and be equal to "version-1".')
@@ -82,7 +88,7 @@ def __init__(self, study, target_magnification, magnification_source):
8288
)
8389

8490
# Save values.
85-
self.target_magnification = target_magnification
91+
self.target_magnification = float(target_magnification)
8692
self.magnification_source = magnification_source
8793

8894
def __call__(self, slide):
@@ -106,13 +112,11 @@ def __call__(self, slide):
106112
ts = large_image.open(filename)
107113

108114
# scan_magnification = highest available magnification from source
109-
scan_magnification = np.float32(
110-
ts.getNativeMagnification()["magnification"]
111-
)
115+
scan_magnification = float(ts.getNativeMagnification()["magnification"])
112116

113117
if self.magnification_source == "exact":
114-
# Use the tile-source level that large_image is
115-
# willing to interpolate for us.
118+
# Use the tile-source level that large_image is willing to interpolate
119+
# for us.
116120
preferred_levels = [
117121
ts.getLevelForMagnification(
118122
self.target_magnification, rounding=False
@@ -130,7 +134,7 @@ def __call__(self, slide):
130134

131135
estimated_magnifications = np.array(
132136
[
133-
ts.getMagnificationForLevel(level)["magnification"]
137+
float(ts.getMagnificationForLevel(level)["magnification"])
134138
for level in preferred_levels
135139
]
136140
)
@@ -139,16 +143,24 @@ def __call__(self, slide):
139143
(level, returned_magnification) = self._get_level_and_magnifications(
140144
self.target_magnification, estimated_magnifications
141145
)
142-
# Rather than as the index into preferred_levels, change
143-
# level to be the value that large_image uses
146+
# Rather than as the index into preferred_levels, change level to be the
147+
# value that large_image uses
144148
level = preferred_levels[level]
145149

146-
# If large_image is resampling a native level for us, it
147-
# is starting with the preferred level that is the least
148-
# one that is not smaller than the resampled level.
149-
read_magnification = ts.getMagnificationForLevel(
150-
min([ts.getPreferredLevel(i) for i in range(ts.levels) if i >= level])
151-
)["magnification"]
150+
# If large_image is resampling a native level for us, it is starting with
151+
# the preferred level that is the least one that is not smaller than the
152+
# resampled level.
153+
read_magnification = float(
154+
ts.getMagnificationForLevel(
155+
min(
156+
[
157+
ts.getPreferredLevel(i)
158+
for i in range(ts.levels)
159+
if i >= level
160+
]
161+
)
162+
)["magnification"]
163+
)
152164

153165
slide["target_magnification"] = self.target_magnification
154166
slide["scan_magnification"] = scan_magnification
@@ -157,12 +169,12 @@ def __call__(self, slide):
157169

158170
# We don't want to walk off the right or bottom of the slide so we are
159171
# conservative as to how many pixels large_image will return for us.
160-
# 1) large_image starts with an image that is of read_mangification; we
161-
# compute the dimensions for read_magnification with math.floor from the
162-
# dimensions of scan_magnification (i.e., ts.sizeX and ts.sizeY) to be
163-
# conservative.
172+
# 1) large_image starts with an image that is of
173+
# read_magnification; we compute the dimensions for read_magnification
174+
# with math.floor from the dimensions of scan_magnification (i.e.,
175+
# ts.sizeX and ts.sizeY) to be conservative.
164176
# 2) large_image or external software may resampled from the
165-
# read_mangification to the target_magnification; we compute dimensions
177+
# read_magnification to the target_magnification; we compute dimensions
166178
# for the target_magnification with math.floor from the
167179
# read_magnification to be conservative.
168180
number_pixel_rows_for_slide = ts.sizeY
@@ -199,7 +211,7 @@ def __call__(self, slide):
199211
source_group = zarr.open(store, mode="r")
200212

201213
# scan_magnification = highest available magnification from source
202-
scan_magnification = np.float32(
214+
scan_magnification = float(
203215
source_group.attrs[os.PROPERTY_NAME_OBJECTIVE_POWER]
204216
)
205217

@@ -217,8 +229,8 @@ def __call__(self, slide):
217229
(level, returned_magnification) = self._get_level_and_magnifications(
218230
self.target_magnification, estimated_magnifications
219231
)
220-
# Rather than as the index into preferred_levels, change
221-
# level to be the value that zarr uses
232+
# Rather than as the index into preferred_levels, change level to be the
233+
# value that zarr uses
222234
level = preferred_levels[level]
223235

224236
slide["target_magnification"] = self.target_magnification
@@ -267,7 +279,9 @@ def __call__(self, slide):
267279
def _get_level_and_magnifications(
268280
self, target_magnification, estimated_magnifications
269281
):
270-
"""A private subroutine that computes level and magnifications."""
282+
"""
283+
A private subroutine that computes level and magnifications.
284+
"""
271285
# calculate difference with magnification levels
272286

273287
magnification_tolerance = 0.02
@@ -291,7 +305,8 @@ def _get_level_and_magnifications(
291305

292306

293307
class TilesByGridAndMask:
294-
"""Select tiles according to a regular grid. Optionally, restrict
308+
"""
309+
Select tiles according to a regular grid. Optionally, restrict
295310
the list by a mask that is read from a file. Optionally, further
296311
select a random subset of them.
297312
@@ -302,7 +317,8 @@ class TilesByGridAndMask:
302317
Parameters for the constructor
303318
------------------------------
304319
study : dictionary
305-
The study dictionary from which to read parameters about the study.
320+
The study dictionary from which to read parameters about the
321+
study.
306322
randomly_select: int
307323
The number of tiles to be randomly selected from the list that
308324
would otherwise be written to the slide dictionary. A value
@@ -336,12 +352,15 @@ def __init__(
336352
self,
337353
study,
338354
randomly_select=-1, # Defaults to select all
339-
number_pixel_overlap_rows_for_tile=0, # Defaults to no overlap between adjacent tiles
355+
number_pixel_overlap_rows_for_tile=0, # Defaults to no overlap
340356
number_pixel_overlap_columns_for_tile=0,
341357
mask_filename="", # Defaults to no masking
342358
mask_threshold=0.0, # Defaults to any overlap with the mask
343359
):
344-
"""Sanity check the supplied parameters and store them for later use."""
360+
"""
361+
Sanity check the supplied parameters and store them for later
362+
use.
363+
"""
345364
# Check values.
346365
if not ("version" in study and study["version"] == "version-1"):
347366
raise ValueError('study["version"] must exist and be equal to "version-1".')
@@ -416,8 +435,10 @@ def __init__(
416435
self.mask_threshold = mask_threshold
417436

418437
def __call__(self, slide):
419-
"""Select tiles according to a regular grid. Optionally, restrict the list by a mask.
420-
Optionally, select a random subset of them.
438+
"""
439+
Select tiles according to a regular grid. Optionally,
440+
restrict the list by a mask. Optionally, select a random
441+
subset of them.
421442
"""
422443
# Check values.
423444
if "number_pixel_rows_for_slide" not in slide:
@@ -561,20 +582,20 @@ def mask_rejects(self, top, left):
561582
bottom = top + self.number_pixel_rows_for_tile
562583
right = left + self.number_pixel_columns_for_tile
563584
mask_top = (
564-
top / self.number_pixel_rows_for_slide * self.number_pixel_rows_for_mask
585+
top * self.number_pixel_rows_for_mask / self.number_pixel_rows_for_slide
565586
)
566587
mask_bottom = (
567-
bottom / self.number_pixel_rows_for_slide * self.number_pixel_rows_for_mask
588+
bottom * self.number_pixel_rows_for_mask / self.number_pixel_rows_for_slide
568589
)
569590
mask_left = (
570591
left
571-
/ self.number_pixel_columns_for_slide
572592
* self.number_pixel_columns_for_mask
593+
/ self.number_pixel_columns_for_slide
573594
)
574595
mask_right = (
575596
right
576-
/ self.number_pixel_columns_for_slide
577597
* self.number_pixel_columns_for_mask
598+
/ self.number_pixel_columns_for_slide
578599
)
579600
cumulative_top_left = self.interpolate_cumulative(mask_top, mask_left)
580601
cumulative_top_right = self.interpolate_cumulative(mask_top, mask_right)
@@ -598,7 +619,8 @@ def mask_rejects(self, top, left):
598619

599620

600621
class TilesByList:
601-
"""Select the tiles supplied by the user. Optionally, select a
622+
"""
623+
Select the tiles supplied by the user. Optionally, select a
602624
random subset of them.
603625
604626
An instance of class TilesByList is a callable that will select
@@ -608,7 +630,8 @@ class TilesByList:
608630
Parameters for the constructor
609631
------------------------------
610632
study : dictionary
611-
The study dictionary from which to read parameters about the study.
633+
The study dictionary from which to read parameters about the
634+
study.
612635
randomly_select: int
613636
The number of tiles to be randomly selected from the list that
614637
would otherwise be written to the slide dictionary. A value
@@ -628,7 +651,10 @@ def __init__(
628651
randomly_select=-1, # Defaults to select all
629652
tiles_dictionary={}, # {'AB234': {'tile_top': top0, 'tile_left': left0}, 'CD43': {'tile_top': top1, 'tile_left': left1}, ...}
630653
):
631-
"""Sanity check the supplied parameters and store them for later use."""
654+
"""
655+
Sanity check the supplied parameters and store them for later
656+
use.
657+
"""
632658
# Check values
633659
if not ("version" in study and study["version"] == "version-1"):
634660
raise ValueError('study["version"] must exist and be equal to "version-1".')
@@ -701,7 +727,10 @@ def __init__(
701727
) # in case user changes it later
702728

703729
def __call__(self, slide):
704-
"""Select the tiles supplied by the user. Optionally, select a random subset of them."""
730+
"""
731+
Select the tiles supplied by the user. Optionally, select a
732+
random subset of them.
733+
"""
705734
tiles = slide["tiles"] = copy.deepcopy(
706735
self.tiles_dictionary
707736
) # in case __call__ is called again.
@@ -715,7 +744,8 @@ def __call__(self, slide):
715744

716745

717746
class TilesRandomly:
718-
"""Select a random subset of all possible tiles.
747+
"""
748+
Select a random subset of all possible tiles.
719749
720750
An instance of class TilesRandomly is a callable that will select
721751
the coordinates of tiles to be taken from a slide. The selected
@@ -724,15 +754,19 @@ class TilesRandomly:
724754
Parameters for the constructor
725755
------------------------------
726756
study : dictionary
727-
The study dictionary from which to read parameters about the study.
757+
The study dictionary from which to read parameters about the
758+
study.
728759
randomly_select: int
729760
The number of tiles to be randomly selected from the slide.
730761
The value must be positive. A value of 1 is the default.
731762
732763
"""
733764

734765
def __init__(self, study, randomly_select=1): # Defaults to select one
735-
"""Sanity check the supplied parameters and store them for later use."""
766+
"""
767+
Sanity check the supplied parameters and store them for later
768+
use.
769+
"""
736770
# Check values.
737771
if not ("version" in study and study["version"] == "version-1"):
738772
raise ValueError('study["version"] must exist and be equal to "version-1".')
@@ -766,7 +800,9 @@ def __init__(self, study, randomly_select=1): # Defaults to select one
766800
self.randomly_select = randomly_select
767801

768802
def __call__(self, slide):
769-
"""Select a random subset of all possible tiles."""
803+
"""
804+
Select a random subset of all possible tiles.
805+
"""
770806
if "number_pixel_rows_for_slide" not in slide:
771807
raise ValueError(
772808
'slide["number_pixel_rows_for_slide"] must be already set.'

0 commit comments

Comments
 (0)