Color tones change after OBJ export – how to preserve original shading?

Dear 3D Slicer Developers and Community,

I am experiencing an issue when exporting one or more 3D models from 3D Slicer in OBJ format. While the original color itself (i.e., the RGB values) is preserved in the exported .mtl file, the rendered models appear significantly darker or have noticeably altered tones in external applications.

To illustrate the problem, I have attached a screenshot showing a side-by-side comparison of the same model in 3D Slicer and in an external viewer, along with the corresponding normalized RGB values from the .mtl file. It seems that the tone or brightness of the colors is not accurately preserved during export.

Has anyone encountered this before? Is there a known way to ensure that the visual appearance (including brightness and tone) of the colors set in Slicer are exported faithfully into the .obj + .mtl pair?

Any insights or suggestions on how to fix or work around this would be greatly appreciated.

Thank you in advance!

Best regards,

What external application are you using to look at these model? The difference in rendering algorithms and shading might be the main reason you are not seeing them the same way.

Dear muratmaga,

Thank you for your response and for pointing out the potential role of rendering differences.

To clarify: we are using our own custom web-based OpenGL renderer to visualize the exported models. This renderer reads the .obj file along with its accompanying .mtl file, exactly as exported from 3D Slicer.

Upon inspecting the .mtl file, we observed that the normalized RGB values seem to be approximately one-third of the actual color intensity shown in 3D Slicer. This suggests that the brightness or tone is already altered in the exported .mtl file itself, even before rendering.

We would appreciate any insight into how 3D Slicer maps its internal color settings to the .mtl format upon export, and whether there is a recommended way to preserve the original appearance more accurately.

Best regards,

1 Like

@JohnWick I test and get the same result. Export to OBJ from Segmentations module, or by converting the segmentation to model and export as OBJ will both result in the mtl file with RGB values that are exactly 1/3 the RGB values in Slicer.

If you install and export to OBJ using the OpenAnatomy Export module, the correct RGB values are used.

If you export to OBJ using python method storageNode.WriteData(), you get the same 1/3 RGB values:

outpath = r'X:\test1.obj'
modelNode = slicer.util.getNode('Segment_1')
storageNode = modelNode.GetStorageNode()
if not storageNode:
    storageNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelStorageNode")
    modelNode.SetAndObserveStorageNodeID(storageNode.GetID())
storageNode.SetFileName(outpath)
storageNode.SetWriteFileFormat('obj')
storageNode.WriteData(modelNode)

Export using VTK method vtk.vtkOBJExporter() will result in the correct RGB values being used :

outpath = r'X:\test2'

modelNode = slicer.util.getNode('Segment_1')
polyData = modelNode.GetPolyData()

displayNode = modelNode.GetDisplayNode()
color = displayNode.GetColor()
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(polyData)
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetColor(color)
renderer = vtk.vtkRenderer()
renderer.AddActor(actor)
renderWindow = vtk.vtkRenderWindow()
renderWindow.AddRenderer(renderer)
renderWindowInteractor = vtk.vtkRenderWindowInteractor()
renderWindowInteractor.SetRenderWindow(renderWindow)
exporter = vtk.vtkOBJExporter()
exporter.SetRenderWindow(renderWindow)
exporter.SetFilePrefix(outpath)
exporter.Write()

The reason for that is in this block of code:

I imagine different renderers make different use of these parameters, but if anyone knows of a definitive standard for OBJ/MTL or a test suite that defines how interchange should be handled then I think we should change Slicer’s behavior to match those standards.

3 Likes

@JASON and @pieper Thank you very much for your clear explanation β€” I really appreciate the detailed guidance!
It seems to be a general issue; for that reason, I export the models in OBJ format and then multiply the Kd values in each .mtl file using the following code:

import os

# Folder containing the .mtl files
folder = r"path"

# Loop over all .mtl files
for filename in os.listdir(folder):
  if filename.endswith(".mtl"):
      filepath = os.path.join(folder, filename)

      # Read the file lines
      with open(filepath, "r") as file:
          lines = file.readlines()

      # Modify Kd line
      new_lines = []
      for line in lines:
          if line.startswith("Kd "):
              parts = line.strip().split()
              # Convert to float and multiply by 3, with max value capped at 1.0
              kd_values = [min(1.0, float(x) * 3) for x in parts[1:]]
              new_line = "Kd " + " ".join(f"{v:.3f}" for v in kd_values) + "\n"
              new_lines.append(new_line)
          else:
              new_lines.append(line)

      # Write the modified lines back to the file
      with open(filepath, "w") as file:
          file.writelines(new_lines)
1 Like

@pieper The comment warns :

722. // OBJ exporter sets the same color for ambient, diffuse, specular
723. // so we scale it by 1/3 to avoid having too bright material.

but the actual behavior is the output .mtl only writes the 1/3 RGB value to the diffuse property and has 0 0 0 for ambient and specular.

# wavefront mtl file written by the visualization toolkit

newmtl mtl1
Ka 0 0 0
Kd 0.3333333333333333 0.3333333333333333 0.3333333333333333
Ks 0 0 0
Ns 3
Tr 1
illum 3

I think the most complete solution would be to set the specular, SpecularPower, ambient from the vtkProperty of the Slicer material. These are editable from UI in the models module.

But the most straight-forward interpretation should be to map the label / model β€˜color’ property to the mtl diffuse property at a 1:1 ratio. I think it is reasonable to leave specular & ambient values to 0 0 0, as the exporter does now.

If you are making a change to the OBJ exporter, I would also consider altering the material name from the default value β€˜mtl1’ to a unique name that matches the file output.

spleen.obj > spleen.mtl contains:
newmtl spleen

This way, the material names are unique and identifiable to the to object it is assigned. It’s difficult to manage many materials with the same name β€˜mtl1’ in the external DCC application. I’ve made scripts to reassign material names in the DCC to match objects, but this would be a convenient default behavior.

1 Like

Excellent suggestions @JASON . @lassoan, git indicates this comment came from you; do you have thoughts on the topic?