@@ -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
490724Representation 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+
609858Geometry 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