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?

I’ve implemented material parameter adjustment on export because without that the meshes looked very bright, washed-out in Blender. The main problem is that Blender - and probably most other modern rendering engines - use physically based rendering (PBR), which works very differently than OBJ’s simple Phong-style model. It may not be possible to convert materials between these models in a way that preserves appearance across any lighting conditions.

@JohnWick and @JASON what software and renderers do you use? What material properties do you set in the display node (Models module / 3D Display / Advanced → Ambient, Diffuse, Specular, Power)? What lights do you set up in the other software? Do you set custom lights in Slicer (in Sandbox extension’s Lights module)? Can you propose an algorithm that combines the model color and material properties into .mtl file content in a way that you get consistent appearance in Slicer and the other software?

@lassoan Thanks for the reply. In my experience, if the OBJ export writes the mtl with diffuse color the same as segment’s color (as the Open Anatomy module does), this allows the same color to be imported into the external 3D software correctly.

One observation that is likely related to reports of colors looking ‘washed-out’ in Blender is that the color Slicer uses from the color-picker is in sRGB color space, but Blender interprets the color as being linear.

OBJ is an older format and predates the use of linear color space rendering, so sRGB colors in mtl is the norm. Autodesk 3D softwares allows for color management that can automatically map colors & textures from sRGB to linear.

Blender will map sRGB textures to linear, but doesn’t do the same for colors in mtl files during OBJ import. A user can get around this by approximating sRGB to linear conversion by applying a gamma value of 2.2.

import bpy
for mat in bpy.data.materials:
    if not mat.use_nodes: continue
    for node in mat.node_tree.nodes:
        if node.type == 'BSDF_PRINCIPLED':
            color_input = node.inputs.get("Base Color")
            if color_input and not color_input.is_linked:
                c = color_input.default_value
                color_input.default_value = (pow(c[0], 2.2), pow(c[1], 2.2), pow(c[2], 2.2), c[3])
                mat.update_tag()
            break

A robust OBJ export solution might include options for linear or sRGB color output.

Here is a segmentation in Slicer (top), and initial import in Blender (left) where the colors are incorrectly interpreted as linear. On the bottom right is Blender after running the above script with the gamma shift.

Thanks @JASON that’s really helpful :+1:

I agree that obj is probably just too simplistic to satisfy modern use cases, but the issue of default lighting in the different places you want to import the data makes it hard (or impossible really) for Slicer to export something that will look the same everywhere.

But I guess we could provide some more export options that could help and maybe gltf or USD would be more expressive formats. Did anything ever happen with this thread?