Skip to content

Commit aad155c

Browse files
authored
Merge pull request #269 from ImagingDataCommons/v0.22.0dev
Version 0.22.0
2 parents 3bf759d + edf6198 commit aad155c

21 files changed

Lines changed: 4647 additions & 927 deletions
6 Bytes
Binary file not shown.
42.9 KB
Binary file not shown.

docs/ann.rst

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,20 @@ transmitted over network, etc.
184184
Reading Existing Bulk Annotation Objects
185185
----------------------------------------
186186

187-
You can read an existing bulk annotation object using `pydicom` and then convert
188-
to the `highdicom` object like this:
187+
You can read an existing bulk annotation object from file using the
188+
:func:`highdicom.ann.annread()` function:
189+
190+
.. code-block:: python
191+
192+
from pydicom import dcmread
193+
import highdicom as hd
194+
195+
ann = hd.ann.annread('data/test_files/sm_annotations.dcm')
196+
197+
assert isinstance(ann, hd.ann.MicroscopyBulkSimpleAnnotations)
198+
199+
Alternatively you can converting an existing ``pydicom.Dataset`` representing a
200+
bulk annotation object to the `highdicom` object like this:
189201

190202
.. code-block:: python
191203
@@ -198,7 +210,7 @@ to the `highdicom` object like this:
198210
199211
assert isinstance(ann, hd.ann.MicroscopyBulkSimpleAnnotations)
200212
201-
Note that this example (and the following examples) uses an example file that
213+
Note that these examples (and the following examples) uses an example file that
202214
you can access from the test data in the `highdicom` repository. It was created
203215
using exactly the code in the construction example above.
204216

@@ -298,7 +310,7 @@ passed the data in to create the annotation with `highdicom`.
298310
import numpy as np
299311
300312
graphic_data = group.get_graphic_data(
301-
coordinate_type=ann.AnnotationCoordinateType,
313+
coordinate_type=ann.annotation_coordinate_type,
302314
)
303315
assert len(graphic_data) == 2 and isinstance(graphic_data[0], np.ndarray)
304316

docs/seg.rst

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,240 @@ segments.
486486
device_serial_number='1234567890',
487487
)
488488
489+
Constructing SEG Images from a Total Pixel Matrix
490+
-------------------------------------------------
491+
492+
Some digital pathology images are represented as "tiled" images,
493+
in which the full image (known as the "total pixel matrix") is divided up
494+
into smaller rectangular regions in the row and column dimensions and each
495+
region ("tile") is stored as a frame in a multiframe DICOM image.
496+
497+
Segmentations of such images are stored as a tiled image in the same manner.
498+
There are a two options in `highdicom` for doing this. You can either pass each
499+
tile/frame individually stacked as a 1D list down the first dimension of the
500+
``pixel_array`` as we have already seen (with the location of each frame either
501+
matching that of the corresponding frame in the source image or explicitly
502+
specified in the ``plane_positions`` argument), or you can pass the 2D total
503+
pixel matrix of the segmentation and have `highdicom` automatically create the
504+
tiles for you.
505+
506+
To enable this latter option, pass the ``pixel_array`` as a single frame (i.e.
507+
a 2D labelmap array, a 3D labelmap array with a single frame stacked down the
508+
first axis, or a 4D array with a single frame stacked down the first dimension
509+
and any number of segments stacked down the last dimension) and set the
510+
``tile_pixel_array`` argument to ``True``. You can optionally choose the size
511+
(in pixels) of each tile using the ``tile_size`` argument, or, by default, the
512+
tile size of the source image will be used (regardless of whether the
513+
segmentation is represented at the same resolution as the source image).
514+
515+
If you need to specify the plane positions of the image explicitly, you should
516+
pass a single item to the ``plane_positions`` argument giving the location of
517+
the top left corner of the full total pixel matrix. Otherwise, all the usual
518+
options are available to you.
519+
520+
.. code-block:: python
521+
522+
# Use an example slide microscopy image from the highdicom test data
523+
# directory
524+
sm_image = dcmread('data/test_files/sm_image.dcm')
525+
526+
# The source image has multiple frames/tiles, but here we create a mask
527+
# corresponding to the entire total pixel matrix
528+
mask = np.zeros(
529+
(
530+
sm_image.TotalPixelMatrixRows,
531+
sm_image.TotalPixelMatrixColumns
532+
),
533+
dtype=np.uint8,
534+
)
535+
mask[38:43, 5:41] = 1
536+
537+
property_category = hd.sr.CodedConcept("91723000", "SCT", "Anatomical Structure")
538+
property_type = hd.sr.CodedConcept("84640000", "SCT", "Nucleus")
539+
segment_descriptions = [
540+
hd.seg.SegmentDescription(
541+
segment_number=1,
542+
segment_label='Segment #1',
543+
segmented_property_category=property_category,
544+
segmented_property_type=property_type,
545+
algorithm_type=hd.seg.SegmentAlgorithmTypeValues.MANUAL,
546+
),
547+
]
548+
549+
seg = hd.seg.Segmentation(
550+
source_images=[sm_image],
551+
pixel_array=mask,
552+
segmentation_type=hd.seg.SegmentationTypeValues.BINARY,
553+
segment_descriptions=segment_descriptions,
554+
series_instance_uid=hd.UID(),
555+
series_number=1,
556+
sop_instance_uid=hd.UID(),
557+
instance_number=1,
558+
manufacturer='Foo Corp.',
559+
manufacturer_model_name='Slide Segmentation Algorithm',
560+
software_versions='0.0.1',
561+
device_serial_number='1234567890',
562+
tile_pixel_array=True,
563+
)
564+
565+
# The result stores the mask as a set of 10 tiles of the non-empty region of
566+
# the total pixel matrix, each of size (10, 10), matching # the tile size of
567+
# the source image
568+
assert seg.NumberOfFrames == 10
569+
assert seg.pixel_array.shape == (10, 10, 10)
570+
571+
``"TILED_FULL"`` and ``"TILED_SPARSE"``
572+
---------------------------------------
573+
574+
When the segmentation is stored as a tiled image, there are two ways in which
575+
the locations of each frame/tile may be specified in the resulting object.
576+
These are defined by the value of the
577+
`"DimensionOrganizationType"
578+
<https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.17.html#table_C.7.6.17-1>`_
579+
attribute:
580+
581+
- ``"TILED_SPARSE"``: The position of each tile is explicitly defined in the
582+
`"PerFrameFunctionalGroupsSequence"
583+
<https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.16.html#table_C.7.6.16-1>`_
584+
of the object. This requires a potentially very long sequence to store all
585+
the per-frame metadata, but does allow for the omission of empty frames from
586+
the segmentation and other irregular tiling strategies.
587+
- ``"TILED_FULL"``: The position of each tile is implicitly defined using a
588+
predetermined order of the frames. This saves the need to store the pre-frame
589+
metadata but does not allow for the omission of empty frames of the
590+
segmentation and is generally less flexible. It may also be simpler for a
591+
receiving application to process, since the tiles are guaranteed to be
592+
regularly and consistently ordered.
593+
594+
You can control this behavior by specifying the
595+
``dimension_organization_type`` parameter and passing a value of the
596+
:class:`highdicom.DimensionOrganizationTypeValues` enum. The default value is
597+
``"TILED_SPARSE"``. Generally, the ``"TILED_FULL"`` option will be used in
598+
combination with ``tile_pixel_array`` argument.
599+
600+
601+
.. code-block:: python
602+
603+
# Using the same example as above, this time as TILED_FULL
604+
seg = hd.seg.Segmentation(
605+
source_images=[sm_image],
606+
pixel_array=mask,
607+
segmentation_type=hd.seg.SegmentationTypeValues.BINARY,
608+
segment_descriptions=segment_descriptions,
609+
series_instance_uid=hd.UID(),
610+
series_number=1,
611+
sop_instance_uid=hd.UID(),
612+
instance_number=1,
613+
manufacturer='Foo Corp.',
614+
manufacturer_model_name='Slide Segmentation Algorithm',
615+
software_versions='0.0.1',
616+
device_serial_number='1234567890',
617+
tile_pixel_array=True,
618+
omit_empty_frames=False,
619+
dimeension_organization_type=hd.DimensionOrganizationTypeValues.TILED_FULL,
620+
)
621+
622+
# The result stores the mask as a set of 25 tiles of the entire region of
623+
# the total pixel matrix, each of size (10, 10), matching the tile size of
624+
# the source image
625+
assert seg.NumberOfFrames == 25
626+
assert seg.pixel_array.shape == (25, 10, 10)
627+
628+
Multi-resolution Pyramids
629+
-------------------------
630+
631+
Whole slide digital pathology images can often be very large and as such it
632+
is common to represent them as *multi-resolution pyramids* of images, i.e.
633+
to store multiple versions of the same image at different resolutions. This
634+
helps viewers render the image at different zoom levels.
635+
636+
Within DICOM, this can also extend to segmentations derived from whole slide
637+
images. Multiple different SEG images may be stored, each representing the
638+
same segmentation at a different resolution, as different instances within a
639+
DICOM series.
640+
641+
*highdicom* provides the :func:`highdicom.seg.create_segmentation_pyramid`
642+
function to assist with this process. This function handles multiple related
643+
scenarios:
644+
645+
* Constructing a segmentation of a source image pyramid given a
646+
segmentation pixel array of the highest resolution source image.
647+
Highdicom performs the downsampling automatically to match the
648+
resolution of the other source images. For this case, pass multiple
649+
``source_images`` and a single item in ``pixel_arrays``.
650+
* Constructing a segmentation of a source image pyramid given user-provided
651+
segmentation pixel arrays for each level in the source pyramid. For this
652+
case, pass multiple ``source_images`` and a matching number of
653+
``pixel_arrays``.
654+
* Constructing a segmentation of a single source image given multiple
655+
user-provided downsampled segmentation pixel arrays. For this case, pass
656+
a single item in ``source_images``, and multiple items in
657+
``pixel_arrays``).
658+
* Constructing a segmentation of a single source image and a single
659+
segmentation pixel array by downsampling by a given list of
660+
``downsample_factors``. For this case, pass a single item in
661+
``source_images``, a single item in ``pixel_arrays``, and a list of one
662+
or more desired ``downsample_factors``.
663+
664+
Here is a simple of example of specifying a single source image and segmentation
665+
array, and having *highdicom* create a multi-resolution pyramid segmentation
666+
series at user-specified downsample factors.
667+
668+
.. code-block:: python
669+
670+
import highdicom as hd
671+
from pydicom import dcmread
672+
import numpy as np
673+
674+
675+
# Use an example slide microscopy image from the highdicom test data
676+
# directory
677+
sm_image = dcmread('data/test_files/sm_image.dcm')
678+
679+
# The source image has multiple frames/tiles, but here we create a mask
680+
# corresponding to the entire total pixel matrix
681+
mask = np.zeros(
682+
(
683+
sm_image.TotalPixelMatrixRows,
684+
sm_image.TotalPixelMatrixColumns
685+
),
686+
dtype=np.uint8,
687+
)
688+
mask[38:43, 5:41] = 1
689+
690+
property_category = hd.sr.CodedConcept("91723000", "SCT", "Anatomical Structure")
691+
property_type = hd.sr.CodedConcept("84640000", "SCT", "Nucleus")
692+
segment_descriptions = [
693+
hd.seg.SegmentDescription(
694+
segment_number=1,
695+
segment_label='Segment #1',
696+
segmented_property_category=property_category,
697+
segmented_property_type=property_type,
698+
algorithm_type=hd.seg.SegmentAlgorithmTypeValues.MANUAL,
699+
),
700+
]
701+
702+
# This will create a segmentation series of three images: one at the
703+
# original source image resolution (implicit), one at half the size, and
704+
# another at a quarter of the original size.
705+
seg_pyramid = hd.seg.create_segmentation_pyramid(
706+
source_images=[sm_image],
707+
pixel_arrays=[mask],
708+
segmentation_type=hd.seg.SegmentationTypeValues.BINARY,
709+
segment_descriptions=segment_descriptions,
710+
series_instance_uid=hd.UID(),
711+
series_number=1,
712+
manufacturer='Foo Corp.',
713+
manufacturer_model_name='Slide Segmentation Algorithm',
714+
software_versions='0.0.1',
715+
device_serial_number='1234567890',
716+
downsample_factors=[2.0, 4.0]
717+
)
718+
719+
Note that the :func:`highdicom.seg.create_segmentation_pyramid` function always
720+
behaves as if the ``tile_pixel_array`` input is ``True`` within the segmentation
721+
constructor, i.e. it assumes that the input segmentation masks represent total
722+
pixel matrices.
489723

490724
Representation of Fractional SEGs
491725
---------------------------------
@@ -606,6 +840,21 @@ and 1):
606840
- The clear frame boundaries make retrieving individual frames from
607841
``"FRACTIONAL"`` image files possible.
608842

843+
Multiprocessing
844+
---------------
845+
846+
When creating large, multiframe ``"FRACTIONAL"`` segmentations using a
847+
compressed transfer syntax, the time taken to compress the frames can become
848+
large and dominate the time taken to create the segmentation. By default,
849+
frames are compressed in series using the main process, however the ``workers``
850+
parameter allows you to specify a number of additional worker processes that
851+
will be used to compress frames in parallel. Setting ``workers`` to a negative
852+
number uses all available processes on your machine. Note that while this is
853+
likely to result in significantly lower creations times for segmentations with
854+
a very large number of frames, for segmentations with only a few frames the
855+
additional overhead of spawning processes may in fact slow the entire
856+
segmentation creation process down.
857+
609858
Geometry of SEG Images
610859
----------------------
611860

@@ -1101,6 +1350,88 @@ as stored in the SEG will be returned.
11011350
# [0. 0.2509804 0.5019608]
11021351
11031352
1353+
Reconstructing Total Pixel Matrices from Tiled Segmentations
1354+
------------------------------------------------------------
1355+
1356+
For segmentations of digital pathology images that are stored as tiled images,
1357+
the :meth:`highdicom.seg.Segmentation.get_pixels_by_source_frame()` method will
1358+
return the segmentation mask as a set of frames stacked down the first
1359+
dimension of the array. However, for such images, you typically want to work
1360+
with the large 2D total pixel matrix that is formed by correctly arranging the
1361+
tiles into a 2D array. `highdicom` provides the
1362+
:meth:`highdicom.seg.Segmentation.get_total_pixel_matrix()` method for this
1363+
purpose.
1364+
1365+
Called without any parameters, it returns a 3D array containing the full total
1366+
pixel matrix. The first two dimensions are the spatial dimensions, and the
1367+
third is the segments dimension. Behind the scenes highdicom has stitched
1368+
together the required frames stored in the original file for you. Like with the
1369+
other methods described above, setting ``combine_segments`` to ``True``
1370+
combines all the segments into, in this case, a 2D array.
1371+
1372+
.. code-block:: python
1373+
1374+
import highdicom as hd
1375+
1376+
# Read in the segmentation using highdicom
1377+
seg = hd.seg.segread('data/test_files/seg_image_sm_control.dcm')
1378+
1379+
# Get the full total pixel matrix
1380+
mask = seg.get_total_pixel_matrix()
1381+
1382+
expected_shape = (
1383+
seg.TotalPixelMatrixRows,
1384+
seg.TotalPixelMatrixColumns,
1385+
seg.number_of_segments,
1386+
)
1387+
assert mask.shape == expected_shape
1388+
1389+
# Combine the segments into a single array
1390+
mask = seg.get_total_pixel_matrix(combine_segments=True)
1391+
1392+
assert mask.shape == (seg.TotalPixelMatrixRows, seg.TotalPixelMatrixColumns)
1393+
1394+
Furthermore, you can request a sub-region of the full total pixel matrix by
1395+
specifying the start and/or stop indices for the rows and/or columns within the
1396+
total pixel matrix. Note that this method follows DICOM 1-based convention for
1397+
indexing rows and columns, i.e. the first row and column of the total pixel
1398+
matrix are indexed by the number 1 (not 0 as is common within Python). Negative
1399+
indices are also supported to index relative to the last row or column, with -1
1400+
being the index of the last row or column. Like for standard Python indexing,
1401+
the stop indices are specified as one beyond the final row/column in the
1402+
returned array. Note that the requested region does not have to start or stop
1403+
at the edges of the underlying frames: `highdicom` stitches together only the
1404+
relevant parts of the frames to create the requested image for you.
1405+
1406+
.. code-block:: python
1407+
1408+
import highdicom as hd
1409+
1410+
# Read in the segmentation using highdicom
1411+
seg = hd.seg.segread('data/test_files/seg_image_sm_control.dcm')
1412+
1413+
# Get a region of the total pixel matrix
1414+
mask = seg.get_total_pixel_matrix(
1415+
combine_segments=True,
1416+
row_start=20,
1417+
row_end=40,
1418+
column_start=10,
1419+
column_end=20,
1420+
)
1421+
1422+
assert mask.shape == (20, 10)
1423+
1424+
# A further example using negative indices. Since row_end is not provided,
1425+
# the default behavior is to include the last row in the total pixel matrix.
1426+
mask = seg.get_total_pixel_matrix(
1427+
combine_segments=True,
1428+
row_start=21,
1429+
column_start=-30,
1430+
column_end=-25,
1431+
)
1432+
1433+
assert mask.shape == (30, 5)
1434+
11041435
Viewing DICOM SEG Images
11051436
------------------------
11061437

0 commit comments

Comments
 (0)