Incorrect values when converting segmentation to binary labelmap

I have noticed that converting a segmentation to binary labelmap causes all label values to be remapped from their current value to the segment’s index (1 … n+1).

This is true for UI option “Export visible segments to binary labelmap”, as well as python method slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode.

Is there a way to create a labelmap from segmentation with the correct values?

That’s because in labelmaps 0 is reserved for background. So indexing starts at 1.

@muratmaga background label value is 0 in both the source segmentation and in the output labelmap, but the issues I’m facing is the non-zero label values are remapped to segment index.

In my case I’m using the segmentation in an AI workflow where labelmap values have semantic meaning. Opening, modifying, and saving the seg.nrrd will maintain the same segment values, but attempts to operate on the labelmap will alter the segment values.

If you want your segment names to map consistently to same values as labelmaps, you should create a color table, and specify that during the segment->labelmap conversion (use segmentations module, not the right-click one).

Here is an older example. SlicerMEMOS/MEMOS/Resources/Support/KOMP2.ctbl at main · SlicerMorph/SlicerMEMOS · GitHub

(you should really use the new csv based color table, which doesn’t have the issues associated with this older format).

OK, I can create and apply a color table from the existing values, and then get the correct values from segNode.GenerateMergedLabelmapForAllSegments().

— Create Color table

def ColorTableFromSegmentation(segNode, tableName="SegmentationColorTable"):
    segmentation = segNode.GetSegmentation()

    colorNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLColorTableNode", tableName)
    colorNode.SetTypeToUser()
    colorNode.HideFromEditorsOff()

    maxLabel = 0
    for segId in segmentation.GetSegmentIDs():
        seg = segmentation.GetSegment(segId)
        lv = int(seg.GetLabelValue())
        if lv > maxLabel:
            maxLabel = lv
    colorNode.SetNumberOfColors(maxLabel + 1)

    for segId in segmentation.GetSegmentIDs():
        seg = segmentation.GetSegment(segId)
        name       = seg.GetName()
        labelValue = int(seg.GetLabelValue())
        (r, g, b)  = seg.GetColor()

        Ri = int(round(r * 255))
        Gi = int(round(g * 255))
        Bi = int(round(b * 255))
        Ai = 255

        colorNode.SetColor(labelValue, Ri, Gi, Bi, Ai)
        colorNode.SetColorName(labelValue, name)

    return colorNode

— Apply Color table

ct = ColorTableFromSegmentation(segNode)
segNode.SetLabelmapConversionColorTableNodeID( ct.GetID() )

— Create labelmap with ConversionColorTable applied

import slicer
import vtk
import os
import tempfile
import numpy as np
from vtk.util import numpy_support

def SegNode_to_Labelmap(segNode, volNode):
    segmentation = segNode.GetSegmentation()
    mergedImageData = slicer.vtkOrientedImageData()
    extent = slicer.vtkSegmentation.EXTENT_REFERENCE_GEOMETRY
    segIDs = vtk.vtkStringArray()
    segVals = vtk.vtkIntArray()
    for segTemp in AtlasTemplate.brainTemplate_99:
        segIDs.InsertNextValue(segTemp['name'])
        segVals.InsertNextValue(int(segTemp['value']))

    success = segNode.GenerateMergedLabelmapForAllSegments(mergedImageData, extent, None, segIDs, segVals)

    print("GenerateMergedLabelmap success:", success)

    if success:
        labelmapNode = slicer.mrmlScene.AddNewNodeByClass( "vtkMRMLLabelMapVolumeNode", "Merged_Labelmap" )
        labelmapNode.SetAndObserveImageData(mergedImageData)

        #print unique values in mergedImageData
        scalars = mergedImageData.GetPointData().GetScalars()
        npArray = numpy_support.vtk_to_numpy(scalars)
        uniqueValues = np.unique(npArray)
        print("values:", uniqueValues)
        return labelmapNode
     else:
         return None

This prints a list of the correct values, however when I hover over the labelmap, I stream of errors:
[VTK] GetScalarIndex: Pixel (26, 0, 22) not in memory. [VTK] Current extent= (12, 190, 15, 223, 17, 187)

Also, the labelmap doesn’t use the colors from the color table, yet the correct colors are applied if I do NOT apply the color table.

In my opinion, maintaining the correct label values should be the default behavior when converting the labelmap.

I do find that simply importing the seg.nrrd files as volume (instead of segmentation) will result in the correct label values being respected.

This feels very hacky, but this is the best solution I have so far…

def SegNode_to_LabelmapArray(segNode):
    fd, tempPath = tempfile.mkstemp(suffix=".seg.nrrd")
    os.close(fd)

    try:
        slicer.util.saveNode(segNode, tempPath)
        volumeNode = slicer.util.loadVolume(tempPath)
        os.remove(tempPath)
        labelArray = slicer.util.arrayFromVolume(volumeNode)

        uniqueLabels = np.unique(labelArray)
        print("Unique label values in merged labelmap:", uniqueLabels)

        #remove volumeNode from scene
        slicer.mrmlScene.RemoveNode(volumeNode)

        return labelArray

    except Exception as e:
        if os.path.exists(tempPath):
            os.remove(tempPath)
        raise

Try this:

  1. Create a segmentation, with three segments, Segment_1, Segment_2, Segment_3 save it as a segmentation
  2. Create a new segmentation from scratch with two segments: Segment_1, Segment_3

Read the values in seg.nrrd with your script. What is the ordinal you get for Segment_3 in both cases? Which one is correct?

@muratmaga I set up a test that illustrates the issue and found an alternate method that maintains the correct values. Feel free to try this code to replicate results:


import slicer
import numpy as np
import vtk
from vtk.util import numpy_support


#--- Create a random labelmap with specified values
sourceValues = [2, 4, 8, 16]
shape = (64, 64, 64)
np_arr = np.random.choice(sourceValues, size=shape).astype(np.int16)
unique_vals = np.unique(np_arr)
print("1.] Source values:", unique_vals)


#--- Convert the numpy array to volume labelmap
flat_arr = np_arr.flatten(order='F')
vtk_arr = numpy_support.numpy_to_vtk(
    num_array=flat_arr,
    deep=True,
    array_type=vtk.VTK_SHORT
)
imageData = vtk.vtkImageData()
imageData.SetDimensions(shape[0], shape[1], shape[2])
imageData.SetSpacing(1.0, 1.0, 1.0)
imageData.GetPointData().SetScalars(vtk_arr)
labelmapNode = slicer.mrmlScene.AddNewNodeByClass( "vtkMRMLLabelMapVolumeNode", "RandomLabelmap")
labelmapNode.SetAndObserveImageData(imageData)
labelmapNode.CreateDefaultDisplayNodes()

#--- Check the values in the labelmap node
volImageData = labelmapNode.GetImageData()
volScalars = volImageData.GetPointData().GetScalars()
vol_np = numpy_support.vtk_to_numpy(volScalars)
unique_vals_vol = np.unique(vol_np)
print("2.] labelmap values:", unique_vals_vol)

#--- Create a segmentation node from the labelmap
segmentationNode = slicer.mrmlScene.AddNewNodeByClass( "vtkMRMLSegmentationNode", "RandomSegmentation" )
segmentationNode.CreateDefaultDisplayNodes()
segmentationLogic = slicer.vtkSlicerSegmentationsModuleLogic()
segmentationLogic.ImportLabelmapToSegmentationNode(labelmapNode, segmentationNode)
segmentation = segmentationNode.GetSegmentation()
print("--- Segmentation Node:")
for segID in segmentation.GetSegmentIDs():
    segment = segmentation.GetSegment(segID)
    labelValue = segment.GetLabelValue()
    segment.SetName(f"value_{labelValue}")
    print(f"seg: {segment.GetName()}  |  value: {labelValue}")


#--- Create a labelmap from the segmentation node : slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode
labelmapVolumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode", "labelMap_ExportAllSegments")
slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode(segmentationNode, labelmapVolumeNode, slicer.vtkSegmentation.EXTENT_REFERENCE_GEOMETRY)
#--- extract array from labelmapVolumeNode, print unique values
vol2ImageData = labelmapVolumeNode.GetImageData()
vol2Scalars = vol2ImageData.GetPointData().GetScalars()
vol2_np = numpy_support.vtk_to_numpy(vol2Scalars)
unique_vals_export = np.unique(vol2_np)
print("3.] ExportAllSegmentsToLabelmapNode values:", unique_vals_export)



#--- Create a labelmap from the segmentation node : GenerateMergedLabelmapForAllSegments
mergedImageData = slicer.vtkOrientedImageData()
extent = slicer.vtkSegmentation.EXTENT_REFERENCE_GEOMETRY
segIDsArray = vtk.vtkStringArray()
segValsArray = vtk.vtkIntArray()
for segID in segmentation.GetSegmentIDs():
    segIDsArray.InsertNextValue(segID)
    segValsArray.InsertNextValue(segmentation.GetSegment(segID).GetLabelValue())
success = segmentationNode.GenerateMergedLabelmapForAllSegments(
    mergedImageData,
    extent,
    None,
    segIDsArray,
    segValsArray
)
if success:
    vtk_arr2 = mergedImageData.GetPointData().GetScalars()
    np_arr2 = numpy_support.vtk_to_numpy(vtk_arr2)
    unique_vals2 = np.unique(np_arr2)
    print("4.] GenerateMergedLabelmapForAllSegments values:", unique_vals2)
else:
    print("GenerateMergedLabelmapForAllSegments failed")

This generates a labelmap from specified non-consecutive values that do not match index. Then we create a segmentation from the labelmap. After this I convert the segNode to labelmap using ExportAllSegmentsToLabelmapNode and demonstrate the values are altered.
I also tried the method segmentationNode.GenerateMergedLabelmapForAllSegments, which includes a parameter for the label values. Using this method, the correct values are preserved.

Output :

1.] Source values: [ 2  4  8 16]
2.] labelmap values: [ 2  4  8 16]
--- Segmentation Node:
seg: value_2  |  value: 2
seg: value_4  |  value: 4
seg: value_8  |  value: 8
seg: value_16  |  value: 16
3.] ExportAllSegmentsToLabelmapNode values: [1 2 3 4]
4.] GenerateMergedLabelmapForAllSegments values: [ 2  4  8 16]

You can of course do what you want to do, but Slicer does manage correct and consistent label indices with the use of color tables. That’s the suggested method. So it is your call. You can maintain this code, or debug while your other coded didn’t get the labels correctly from the color table.

I have confirmed that creating & applying color table using segNode.SetLabelmapConversionColorTableNodeID does NOT result in the correct label values being maintained when using UI option Export visible segments to binary labelmap or API method slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode. In both of these cases, setting the color table has no affect on the output labelmap and the values are remapped to index, just as they are when no color table is applied.

In my testing, I found only 2 ways to maintain the correct values :
1.] Save & Load the seg.nrrd as a volume
2.] Use segmentationNode.GenerateMergedLabelmapForAllSegments with the values array supplied.

@lassoan can you comment on this? It’s clearly very important for general interfacing purposes to have everything working cleanly and meeting expectations.

I cannot command for programmatic conversion, but this is what I do through the Segmentations module and works all the time.

@JASON We designed the color table based terminology selection in the Segment Editor exactly for the use case you describe - to be able to explicitly assign a label value (and label name, color, and terminology) for segmentation stored in a simple labelmap volume. Please test with the GUI first, it should all work well. Once you can complete your full workflow using the GUI, we can help you automate it by Python scripting.

Thanks, @muratmaga and @lassoan I appreciate your feedback. I can successfully get correct values using this method by creating the color table using the code above and manually exporting from Segmentations module as you suggest.

I am looking for a scripted solution here and color table has some drawbacks :

  • my max labelmap value is 10,000, the color table ends up including every possible integer between 0 and 10,000, so the *.ctbl file ends up being larger file size than the segmentation seg.nrdd
  • manual export creates a *.nii file than needs to be re-imported as volume. I could more easily just import the source *.seg.nrrd as volume and get the same correct labelmap values without the color table.

Working through this, I have found an acceptable API solution, but my humble recommendation to @lassoan is consider maintaining label values as the default behavior of Export visible segments to binary labelmap and slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode.

If you don’t need 10,000 distinct values your color table does not have to contain 10000 values. I can be just 1, 5, 10000 if you want it to be.

However, most deep-learning frameworks I used (e.g., MONAI) is not happy about non-continuous discrete values in the labelmap. We once had a segmentation with gaps in labelmaps and had to reorder the label indices to make them contiguous starting from 0.

The first value in the color table is the index, not the value, so the table has to be a length of maxValue +1. Try this :

colorNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLColorTableNode")
colorNode.SetTypeToUser()
colorNode.HideFromEditorsOff()

colorNode.SetNumberOfColors(2)
colorNode.SetColor(1, 1, 0, 0, 1)
colorNode.SetColor(10000, 1, 0, 0, 1)

ERROR:
[VTK] vtkMRMLColorTableNode::SetColor: requested entry 10000 is out of table range: 0 - 2, call SetNumberOfColors

Use the latest Slicer Preview Release and the new .csv format. It will not save the undefined colors. That said, it is good to keep the label value range small, as a large color table with lots of gaps may lead to inefficiencies (not just in Slicer but in general).

NIFTI and raw NRRD export feature is only intended for exporting. That is, when you need to translate your general-purpose segmentation (that identifies segments by standard terminology, segment name, or segment UID) to a project-specific label volume (that identifies segments by label value).

This is already implemented: when you import a segment from labelmap with a color table mapping then by default the same color table is used when you export. If you don’t want to create an existing color table then a new one is generated during export.

Number of colors is 10001. SetColor will define labels 1 and 10000, while all the other table entries will be left undefined. Undefined colors are not written to file and they are only shown in the GUI if Hide empty colors option is unchecked.


Everything should work as the user expects and it is clearly not the case now. I’m just wondering how we can change the software (GUI, maybe some logic) or user expectations (by training, documentation) to make the experience better.

I can confirm that with this csv color table, things are working as expected for labelmap export (i.e., I get non-continuous labelmap values of 0, 1, 4, 6, 8)

Thanks, I’ll check out color tables in the latest Preview Release.

If I had a time machine, I’d go back and use values 1…n, but I’m now maintaining a database of 57,000 segmentations that use the FreeSurfer convention.

ExportAllSegmentsToLabelmapNode(segmentationNode, labelmapNode, extentComputationMode) is a convenience function, which only accepts a limited set of options. It just calls slicer.modules.segmentations.logic().ExportSegmentsToLabelmapNode(segmentationNode, segmentIDs, labelmapNode, referenceVolumeNode, extentComputationMode, exportColorTable) without specifying a color mapping (exportColorTable).

If we add exportColorTable parameter then we may just as well add the referenceVolumeNode as well and then the ExportAllSegmentsToLabelmapNode becomes about the same as ExportSegmentsToLabelmapNode. Maybe we should deprecate ExportAllSegmentsToLabelmapNode and remove it from the script repository examples?

We have full examples of converting between segmentations to labelmap with custom color mapping, but maybe it is hard to find it among all the other examples or AI chatbots suggest the oversimplified convenience functions.

@JASON How did you find the methods that you can use? Reading examples in the script repository, searching the forum, reading the API documentation, asking AI chatbots, …?