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

Zarr Sink Frame Values #1625

Merged
merged 31 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4b1dd13
Add basic APIs for specifying frame values, add test
annehaley Sep 4, 2024
68666f8
write frame values to internal metadata and reconstruct on read
annehaley Sep 4, 2024
d02dd55
refactor test and add small test
annehaley Sep 4, 2024
0aa76b4
Reformat, break up complex functions
annehaley Sep 4, 2024
6c00eed
Fix styling
annehaley Sep 4, 2024
e557c6a
Merge branch 'master' into zarr-sink-axis-values
annehaley Oct 3, 2024
ddbdad3
refactor: change storage structure of non-uniform values
annehaley Oct 7, 2024
d9d5b47
Protect against KeyErrors on single frame images
annehaley Oct 7, 2024
16d756e
Merge branch 'master' into zarr-sink-axis-values
annehaley Oct 7, 2024
e71b714
Add axis values to slider labels
annehaley Oct 7, 2024
493d709
Reformat client
annehaley Oct 7, 2024
8af9911
Output axis values as part of the algorithm progression example
manthey Oct 8, 2024
c85ebf0
Add test for edge cases and fix failures*
annehaley Oct 8, 2024
1009eb1
Add edge case to test and fix ValueErrors for mixed type comparisons
annehaley Oct 10, 2024
a3e0da1
Move logic for "Value[X]" metadata entries to base class
annehaley Oct 10, 2024
4e1ed1a
Isolate changes to base class in a new function (avoid function compl…
annehaley Oct 10, 2024
db43213
Fix type errors
annehaley Oct 10, 2024
bafd7cf
Remove extra whitespace
annehaley Oct 10, 2024
6bdb6fe
Merge branch 'master' into zarr-sink-axis-values
annehaley Oct 10, 2024
fc01d6c
Ensure frame values are cast to native python types in `getMetadata`
annehaley Oct 11, 2024
e746aad
Merge branch 'master' into zarr-sink-axis-values
annehaley Oct 11, 2024
537ade5
Add comment and fix linting
annehaley Oct 11, 2024
fbc3f93
Add example usage to documentation
annehaley Oct 16, 2024
b3fde84
Update metadata structure for non-uniform values, update frame select…
annehaley Oct 16, 2024
7ab79a4
Merge branch 'master' into zarr-sink-axis-values
annehaley Oct 16, 2024
676c6c2
Add comments and function docstrings
annehaley Oct 16, 2024
8557dff
Fix styling in FrameSelector component
annehaley Oct 16, 2024
51cc38f
Merge remote-tracking branch 'origin/master' into zarr-sink-axis-values
annehaley Oct 16, 2024
da0f8e1
Update expected values in tests
annehaley Oct 16, 2024
e41e125
Limit frame value labels to 5 significant digits
annehaley Oct 22, 2024
3876620
Merge branch 'master' into zarr-sink-axis-values
annehaley Oct 22, 2024
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
59 changes: 53 additions & 6 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -334,25 +334,60 @@ You can also composite a multi-frame image into a false-color output:
Writing an Image
----------------

If you wish to visualize numpy data, large_image can write a tiled tiff. This requires a tile source that supports writing to be installed. As of this writing, the ``large-image-source-zarr`` and ``large-image-source-vips`` sources supports this. If both are installed, the ``large-image-source-zarr`` is the default.
If you wish to visualize numpy data, ``large_image`` can write a tiled image.
This requires a tile source that supports writing to be installed.
As of this writing, the ``large-image-source-zarr`` and ``large-image-source-vips`` sources both support this.
If both are installed, the ``large-image-source-zarr`` is the default.
Some of the API options available for ``large-image-source-zarr`` are not available for ``large-image-source-vips``.

.. code-block:: python

import large_image

source = large_image.new()
for nparray, x, y in fancy_algorithm():
# We could optionally add a mask to limit the output
source.addTile(nparray, x, y)
source.write('/tmp/sample.tiff', lossy=False)

The ``large-image-source-zarr`` can be used to store multiple frame data with arbitrary axes.
Multiple Frames
~~~~~~~~~~~~~~~

``large-image-source-zarr`` can be used to store multiframe data with arbitrary axes.
The example below demonstrates the creation of an image with five axes: T, Z, Y, X, S.

.. code-block:: python

import large_image

time_values = [0.5, 1.5, 2.5, 3.5]
z_values = [3, 6, 9]
tile_pos_values = [0, 1024, 2048, 3072, 4096]

source = large_image.new()
for nparray, x, y, time, param1 in fancy_algorithm():
source.addTile(nparray, x, y, time=time, p1=param1)
for t_index, t_value in enumerate(time_values):
for z_index, z_value in enumerate(z_values):
for y_value in tile_pos_values:
for x_value in tile_pos_values:

# tile is a numpy array with shape (1024, 1024, 3)
# this shape corresponds to the following axes, respectively: (Y, X, S)
tile = get_my_data_tile(x_value, y_value, z_value, t_value)

source.addTile(
tile,
x_value,
y_value,
z=z_index,
time=t_index,

# z_value and t_value are optional parameters to store the
# true values at the provided z index and t index
z_value=z_value,
time_value=t_value,
)
source.frameUnits = dict(t='ms', z='cm')

# The writer supports a variety of formats
source.write('/tmp/sample.zarr.zip', lossy=False)

Expand All @@ -361,31 +396,37 @@ You may also choose to read tiles from one source and write modified tiles to a
.. code-block:: python

import large_image

original_source = large_image.open('path/to/original/image.tiff')
new_source = large_image.new()
for frame in original_source.getMetadata().get('frames', []):
for tile in original_source.tileIterator(frame=frame['Frame'], format='numpy'):
t, x, y = tile['tile'], tile['x'], tile['y']
tile_data, x, y = tile['tile'], tile['x'], tile['y']
kwargs = {
'z': frame['IndexZ'],
'c': frame['IndexC'],
}
modified_tile = modify_tile(t)
modified_tile = modify_tile(tile_data)
new_source.addTile(modified_tile, x=x, y=y, **kwargs)
new_source.write('path/to/new/image.tiff', lossy=False)

Multiple processes
~~~~~~~~~~~~~~~~~~

In some cases, it may be beneficial to write to a single image from multiple processes or threads:

.. code-block:: python

import large_image
import multiprocessing

# Important: Must be a pickleable function
def add_tile_to_source(tilesource, nparray, position):
tilesource.addTile(
nparray,
**position
)

source = large_image.new()
# Important: Maximum size must be allocated before any multiprocess concurrency
add_tile_to_source(source, np.zeros(1, 1, 3), dict(x=max_x, y=max_y, z=max_z))
Expand All @@ -397,4 +438,10 @@ In some cases, it may be beneficial to write to a single image from multiple pro
)
source.write('/tmp/sample.zarr.zip', lossy=False)


More examples
~~~~~~~~~~~~~

To see more examples of using ``large-image-source-zarr`` to write images, see :doc:`notebooks` and the `Zarr Sink Tests <https://github.com/girder/large_image/blob/master/test/test_sink.py>`_.

.. _Girder: https://girder.readthedocs.io/en/latest/
91 changes: 62 additions & 29 deletions docs/notebooks/zarr_sink_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 1,
"id": "4ba28d02",
"metadata": {},
"outputs": [],
Expand All @@ -37,7 +37,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 2,
"id": "63c0c38f",
"metadata": {},
"outputs": [],
Expand Down Expand Up @@ -164,7 +164,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "bf6d0e9480cb43ef9851d7bd3ca7e356",
"model_id": "97c6787b148c4b4f9639d623f32f8a3a",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -191,7 +191,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 7,
"id": "0e75e6de",
"metadata": {},
"outputs": [],
Expand Down Expand Up @@ -228,10 +228,22 @@
"data": {
"application/json": {
"IndexRange": {
"IndexI": 3
"IndexFOOTPRINT": 3
},
"IndexStride": {
"IndexI": 1
"IndexFOOTPRINT": 1
},
"ValueFOOTPRINT": {
"datatype": "int64",
"max": 50,
"min": 1,
"uniform": true,
"units": null,
"values": [
1,
10,
50
]
},
"bandCount": 3,
"channelmap": {
Expand All @@ -246,19 +258,22 @@
"Channel": "Band 1",
"Frame": 0,
"Index": 0,
"IndexI": 0
"IndexFOOTPRINT": 0,
"ValueFOOTPRINT": 1
},
{
"Channel": "Band 1",
"Frame": 1,
"Index": 1,
"IndexI": 1
"IndexFOOTPRINT": 1,
"ValueFOOTPRINT": 10
},
{
"Channel": "Band 1",
"Frame": 2,
"Index": 2,
"IndexI": 2
"IndexFOOTPRINT": 2,
"ValueFOOTPRINT": 50
}
],
"levels": 6,
Expand All @@ -281,16 +296,34 @@
" 'mm_y': 0,\n",
" 'dtype': 'float64',\n",
" 'bandCount': 3,\n",
" 'frames': [{'Frame': 0, 'IndexI': 0, 'Index': 0, 'Channel': 'Band 1'},\n",
" {'Frame': 1, 'IndexI': 1, 'Index': 1, 'Channel': 'Band 1'},\n",
" {'Frame': 2, 'IndexI': 2, 'Index': 2, 'Channel': 'Band 1'}],\n",
" 'IndexRange': {'IndexI': 3},\n",
" 'IndexStride': {'IndexI': 1},\n",
" 'frames': [{'Frame': 0,\n",
" 'IndexFOOTPRINT': 0,\n",
" 'ValueFOOTPRINT': 1,\n",
" 'Index': 0,\n",
" 'Channel': 'Band 1'},\n",
" {'Frame': 1,\n",
" 'IndexFOOTPRINT': 1,\n",
" 'ValueFOOTPRINT': 10,\n",
" 'Index': 1,\n",
" 'Channel': 'Band 1'},\n",
" {'Frame': 2,\n",
" 'IndexFOOTPRINT': 2,\n",
" 'ValueFOOTPRINT': 50,\n",
" 'Index': 2,\n",
" 'Channel': 'Band 1'}],\n",
" 'ValueFOOTPRINT': {'values': [1, 10, 50],\n",
" 'uniform': True,\n",
" 'units': None,\n",
" 'min': 1,\n",
" 'max': 50,\n",
" 'datatype': 'int64'},\n",
" 'IndexRange': {'IndexFOOTPRINT': 3},\n",
" 'IndexStride': {'IndexFOOTPRINT': 1},\n",
" 'channels': ['Band 1'],\n",
" 'channelmap': {'Band 1': 0}}"
]
},
"execution_count": 7,
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
Expand All @@ -315,7 +348,7 @@
"\n",
" # add modified tile to sink\n",
" # specify tile x, tile y, and any arbitrary frame parameters\n",
" sink.addTile(processed_tile, x=tile['x'], y=tile['y'], i=i)\n",
" sink.addTile(processed_tile, x=tile['x'], y=tile['y'], footprint=i, footprint_value=footprint_size)\n",
"# view metadata\n",
"sink.getMetadata()"
]
Expand All @@ -329,7 +362,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "9b8e6175005d4af89cb4cfada7b72983",
"model_id": "fb60c2b967274a99a632f32f99389705",
"version_major": 2,
"version_minor": 0
},
Expand Down Expand Up @@ -398,10 +431,10 @@
"data": {
"application/json": {
"IndexRange": {
"IndexI": 3
"IndexFOOTPRINT": 3
},
"IndexStride": {
"IndexI": 1
"IndexFOOTPRINT": 1
},
"bandCount": 3,
"channelmap": {
Expand All @@ -416,19 +449,19 @@
"Channel": "Band 1",
"Frame": 0,
"Index": 0,
"IndexI": 0
"IndexFOOTPRINT": 0
},
{
"Channel": "Band 1",
"Frame": 1,
"Index": 1,
"IndexI": 1
"IndexFOOTPRINT": 1
},
{
"Channel": "Band 1",
"Frame": 2,
"Index": 2,
"IndexI": 2
"IndexFOOTPRINT": 2
}
],
"levels": 4,
Expand All @@ -451,16 +484,16 @@
" 'mm_y': None,\n",
" 'dtype': 'uint16',\n",
" 'bandCount': 3,\n",
" 'frames': [{'Channel': 'Band 1', 'Frame': 0, 'Index': 0, 'IndexI': 0},\n",
" {'Channel': 'Band 1', 'Frame': 1, 'Index': 1, 'IndexI': 1},\n",
" {'Channel': 'Band 1', 'Frame': 2, 'Index': 2, 'IndexI': 2}],\n",
" 'IndexRange': {'IndexI': 3},\n",
" 'IndexStride': {'IndexI': 1},\n",
" 'frames': [{'Channel': 'Band 1', 'Frame': 0, 'Index': 0, 'IndexFOOTPRINT': 0},\n",
" {'Channel': 'Band 1', 'Frame': 1, 'Index': 1, 'IndexFOOTPRINT': 1},\n",
" {'Channel': 'Band 1', 'Frame': 2, 'Index': 2, 'IndexFOOTPRINT': 2}],\n",
" 'IndexRange': {'IndexFOOTPRINT': 3},\n",
" 'IndexStride': {'IndexFOOTPRINT': 1},\n",
" 'channels': ['Band 1'],\n",
" 'channelmap': {'Band 1': 0}}"
]
},
"execution_count": 10,
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
Expand Down Expand Up @@ -506,7 +539,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "30812bb388a0426da9806e62bf5e8711",
"model_id": "a2d3df88fbd44079877b8f314ed97e6f",
"version_major": 2,
"version_minor": 0
},
Expand Down
9 changes: 6 additions & 3 deletions examples/algorithm_progression.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,15 @@ def applyAlgorithm(self, sink, params):
**tiparams,
):
scaled = tile.get('scaled', 1)
axisparams = {
p['axis']: iteration_id[i] for i, p in enumerate(self.param_order.values())}
axisparams.update({
p['axis'] + '_value': params[i] for i, p in enumerate(self.param_order.values())})
if self.overlay:
self.addTile(
tilesink,
tile['tile'], int(tile['x'] * scaled), int(tile['y'] * scaled),
**{p['axis']: iteration_id[i] for i, p in enumerate(
self.param_order.values())})
**axisparams)
altered_data = self.algorithm(tile['tile'], *params)
mask = None
if self.overlay:
Expand All @@ -104,7 +107,7 @@ def applyAlgorithm(self, sink, params):
self.addTile(
tilesink,
altered_data, int(tile['x'] * scaled), int(tile['y'] * scaled), mask=mask,
**{p['axis']: iteration_id[i] for i, p in enumerate(self.param_order.values())})
**axisparams)
if time.time() - lastlogtime > 10:
sys.stdout.write(
f'Processed {tile["tile_position"]["position"] + 1} of '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ export default Vue.extend({
currentStyle() {
const curStyle = this.style[this.currentModeId];
return curStyle ? JSON.stringify(curStyle, null, null) : '';
},
sliderLabels() {
const labels = {};
labels.IndexC = this.imageMetadata.channels;
Object.entries(this.metadata).forEach(([key, info]) => {
if (key.includes('Value')) {
const labelKey = key.replace('Value', 'Index');
if (info.values) {
if (info.uniform) {
labels[labelKey] = info.values;
} else {
// non-uniform values have a value for every frame
// labels will change with currentFrame, so only populate current label
labels[labelKey] = new Array(this.indexInfo[labelKey].range + 1).fill('');
labels[labelKey][this.indexInfo[labelKey].current] = info.values[this.currentFrame];
}
}
}
});
return labels;
}
},
watch: {
Expand Down Expand Up @@ -328,7 +348,7 @@ export default Vue.extend({
:current-value="indexInfo[index].current"
:value-max="indexInfo[index].range"
:label="index.replace('Index', '')"
:slider-labels="index === 'IndexC' ? imageMetadata.channels : []"
:slider-labels="sliderLabels[index]"
:max-merge="indexInfo[index].maxMerge || false"
@updateMaxMerge="(v) => updateMaxMergeAxis({index, maxMerge: v})"
@updateValue="(v) => updateAxisSlider({index, frame: v})"
Expand Down
Loading