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

Allow rounding histogram bin edges and reducing bin counts #1201

Merged
merged 1 commit into from
Jun 9, 2023
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Better DICOM multi-level detection ([#1196](../../pull/1196))
- Added an internal field to report populated tile levels in some sources ([#1197](../../pull/1197), [#1199](../../pull/1199))
- Allow specifying an empty style dict ([#1200](../../pull/1200))
- Allow rounding histogram bin edges and reducing bin counts ([#1201](../../pull/1201))

### Changes
- Change how extensions and fallback priorities interact ([#1192](../../pull/1192))
Expand Down
8 changes: 8 additions & 0 deletions girder/girder_large_image/rest/tiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,10 @@ def getTilesPixel(self, item, params):
.param('rangeMax', 'The maximum value in the histogram. Defaults to '
'the maximum value in the image.',
required=False, dataType='float')
.param('roundRange', 'If true and neither a minimum or maximum is '
'specified for the range, round the bin edges and adjust the '
'number of bins for integer data with smaller ranges.',
required=False, dataType='boolean', default=False)
.param('density', 'If true, scale the results by the number of '
'samples.', required=False, dataType='boolean', default=False)
.errorResponse('ID was invalid.')
Expand Down Expand Up @@ -1020,12 +1024,16 @@ def getHistogram(self, item, params):
('bins', int),
('rangeMin', int),
('rangeMax', int),
('roundRange', bool),
('density', bool),
])
_handleETag('getHistogram', item, params)
histRange = None
if 'rangeMin' in params or 'rangeMax' in params:
histRange = [params.pop('rangeMin', 0), params.pop('rangeMax', 256)]
if params.get('roundRange'):
if params.pop('roundRange', False) and histRange is None:
histRange = 'round'
result = self.imageItemModel.histogram(item, range=histRange, **params)
result = result['histogram']
# Cast everything to lists and floats so json with encode properly
Expand Down
13 changes: 13 additions & 0 deletions girder/test_girder/test_tiles_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,19 @@ def testTilesHistogram(server, admin, fsAssetstore):
assert len(resp.json) == 3
assert len(resp.json[0]['hist']) == 256

resp = server.request(
path='/item/%s/tiles/histogram' % itemId,
params={'width': 2048, 'height': 2048, 'resample': False,
'roundRange': False, 'bins': 512})
assert len(resp.json) == 3
assert len(resp.json[0]['hist']) == 512
resp = server.request(
path='/item/%s/tiles/histogram' % itemId,
params={'width': 2048, 'height': 2048, 'resample': False,
'roundRange': True, 'bins': 512})
assert len(resp.json) == 3
assert len(resp.json[0]['hist']) == 256


@pytest.mark.usefixtures('unbindLargeImage')
@pytest.mark.plugin('large_image')
Expand Down
23 changes: 17 additions & 6 deletions large_image/tilesource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,17 +957,20 @@ def histogram(self, dtype=None, onlyMinMax=False, bins=256, # noqa
:param range: if None, use the computed min and (max + 1). Otherwise,
this is the range passed to numpy.histogram. Note this is only
accessible via kwargs as it otherwise overloads the range function.
If 'round', use the computed values, but the number of bins may be
reduced or the bin_edges rounded to integer values for
integer-based source data.
:param args: parameters to pass to the tileIterator.
:param kwargs: parameters to pass to the tileIterator.
:returns: if onlyMinMax is true, this is a dictionary with keys min and
max, each of which is a numpy array with the minimum and maximum of
all of the bands. If onlyMinMax is False, this is a dictionary
with a single key 'histogram' that contains a list of histograms
per band. Each entry is a dictionary with min, max, range, hist,
and bin_edges. range is [min, (max + 1)]. hist is the counts
(normalized if density is True) for each bin. bin_edges is an
array one longer than the hist array that contains the boundaries
between bins.
bins, and bin_edges. range is [min, (max + 1)]. hist is the
counts (normalized if density is True) for each bin. bins is the
number of bins used. bin_edges is an array one longer than the
hist array that contains the boundaries between bins.
"""
lastlog = time.time()
kwargs = kwargs.copy()
Expand Down Expand Up @@ -1024,11 +1027,19 @@ def histogram(self, dtype=None, onlyMinMax=False, bins=256, # noqa
'mean': results['mean'][idx],
'stdev': results['stdev'][idx],
'range': ((results['min'][idx], results['max'][idx] + 1)
if histRange is None else histRange),
if histRange is None or histRange == 'round' else histRange),
'hist': None,
'bin_edges': None,
'bins': bins,
'density': bool(density),
} for idx in range(len(results['min']))]
if histRange == 'round' and numpy.issubdtype(dtype or self.dtype, numpy.integer):
for record in results['histogram']:
if (record['range'][1] - record['range'][0]) < bins * 10:
step = int(math.ceil((record['range'][1] - record['range'][0]) / bins))
rbins = int(math.ceil((record['range'][1] - record['range'][0]) / step))
record['range'] = (record['range'][0], record['range'][0] + step * rbins)
record['bins'] = rbins
for tile in self.tileIterator(format=TILE_FORMAT_NUMPY, *args, **kwargs):
if time.time() - lastlog > 10:
self.logger.info(
Expand All @@ -1044,7 +1055,7 @@ def histogram(self, dtype=None, onlyMinMax=False, bins=256, # noqa
for idx in range(len(results['min'])):
entry = results['histogram'][idx]
hist, bin_edges = numpy.histogram(
tile[:, :, idx], bins, entry['range'], density=False)
tile[:, :, idx], entry['bins'], entry['range'], density=False)
if entry['hist'] is None:
entry['hist'] = hist
entry['bin_edges'] = bin_edges
Expand Down
10 changes: 10 additions & 0 deletions test/test_source_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,7 @@ def testHistogram():
hist = source.histogram(bins=8, output={'maxWidth': 1024}, resample=False)
assert len(hist['histogram']) == 3
assert hist['histogram'][0]['range'] == (0, 256)
assert len(hist['histogram'][0]['bin_edges']) == 9
assert len(list(hist['histogram'][0]['hist'])) == 8
assert list(hist['histogram'][0]['bin_edges']) == [0, 32, 64, 96, 128, 160, 192, 224, 256]
assert hist['histogram'][0]['samples'] == 700416
Expand All @@ -730,6 +731,15 @@ def testHistogram():
assert hist['histogram'][0]['samples'] == 2801664
assert 6e-5 < hist['histogram'][0]['hist'][128] < 8e-5

hist = source.histogram(bins=512, output={'maxWidth': 2048}, resample=False)
assert hist['histogram'][0]['range'] == (0, 256)
assert len(hist['histogram'][0]['bin_edges']) == 513

hist = source.histogram(bins=512, output={'maxWidth': 2048}, resample=False,
range='round')
assert hist['histogram'][0]['range'] == (0, 256)
assert len(hist['histogram'][0]['bin_edges']) == 257


def testSingleTileIteratorResample():
imagePath = datastore.fetch('sample_image.ptif')
Expand Down