Ai Segmentation Workflow
Advanced AI segmentation workflow.
This tutorial script demonstrates a complete post-segmentation pipeline that:
- runs TotalSegmentator or reuses existing masks,
- converts masks into printable surface meshes,
- cleans the meshes for downstream manufacturing workflows,
- computes wall thickness on each mesh,
- optionally performs gray value analysis against the source CT,
- exports STL meshes, color-mapped PLY meshes, screenshots, and reports.
Prerequisites
- The Volvicon application must be running
- At least one multilabel segmentation mask must be available in the project, or TotalSegmentator must be installed and configured in the AI Segmentation module.
Scripting Api Initialization​
import ScriptingApi as api
import os
from datetime import datetime
# Create Application instance
app = api.Application()
ai_segmentation = app.get_ai_segmentation()
volume_operations = app.get_volume_operations()
mask_operations = app.get_mask_operations()
surface_operations = app.get_surface_operations()
analysis_operations = app.get_analysis_operations()
surface_render_properties_operations = surface_operations.get_render_properties_operations()
Workflow Configuration​
# Set REUSE_EXISTING_MASKS to True if you want to skip AI segmentation and run
# the downstream mesh workflow on masks that already exist in the project.
REUSE_EXISTING_MASKS = True
# TotalSegmentator configuration.
TOTAL_SEGMENTATOR_TASK = "total"
TOTAL_SEGMENTATOR_DEVICE = "gpu"
TOTAL_SEGMENTATOR_FASTEST = True
MAKE_IMAGE_ORIENTATION_COMPATIBLE = True
# When the workflow receives a single multilabel mask, it can split only the
# largest structures to keep the tutorial focused on printable anatomy.
SEPARATE_ONLY_LARGEST_LABELS = True
NUM_LARGEST_LABELS = 1
# Optional advanced step for reporting volume intensities on each surface.
ENABLE_GRAY_VALUE_ANALYSIS = True
# Surface preparation and wall-thickness display settings.
SMOOTH_ITERATIONS = 40
SMOOTH_FACTOR = 0.02
REDUCE_PERCENTAGE = 90.0
WALL_THICKNESS_SEARCH_ANGLE_DEG = 20.0
WALL_THICKNESS_DISPLAY_RANGE_MM = [2.0, 20.0]
OUTPUT_PREFIX = "Volvicon_AI_Segmentation_Workflow"
def print_step(step_number, title):
print("\n" + "=" * 80)
print(f"STEP {step_number}: {title}")
print("=" * 80)
def print_info(message):
print(f"[INFO] {message}")
def print_warning(message):
print(f"[WARN] {message}")
def print_error(message):
print(f"[ERROR] {message}")
def safe_name(name):
invalid_chars = '<>:"/\\|?*'
sanitized = "".join("_" if character in invalid_chars else character for character in name)
sanitized = sanitized.strip().replace(" ", "_")
return sanitized or "object"
def format_value(value, decimals=3, suffix=""):
if value is None:
return "n/a"
if isinstance(value, str):
return value
return f"{float(value):.{decimals}f}{suffix}"
def ensure_directory(*parts):
directory = os.path.join(*parts)
os.makedirs(directory, exist_ok=True)
return directory
def build_output_layout(output_root):
analysis_root = ensure_directory(output_root, "analysis")
mesh_root = ensure_directory(output_root, "meshes")
return {
"root": output_root,
"reports": ensure_directory(output_root, "reports"),
"screenshots": ensure_directory(output_root, "screenshots"),
"wall_thickness": ensure_directory(analysis_root, "wall_thickness"),
"gray_value": ensure_directory(analysis_root, "gray_value"),
"stl_meshes": ensure_directory(mesh_root, "stl"),
"colored_meshes": ensure_directory(mesh_root, "analysis_color_ply"),
}
def to_rgb8(color_components):
rgb = [0.0, 0.0, 0.0]
for index in range(min(3, len(color_components))):
rgb[index] = float(color_components[index])
return [max(0, min(255, int(round(component * 255.0)))) for component in rgb]
def create_or_reuse_masks(volume_name):
existing_masks = app.get_all_mask_names()
if REUSE_EXISTING_MASKS and existing_masks:
print_info(f"Reusing {len(existing_masks)} existing mask(s): {existing_masks}")
return existing_masks, "existing"
ai_segmentation.set_model_type(api.AiSegmentationModelType.TotalSegmentator)
if not ai_segmentation.get_installation_status():
print_info("Installing the active TotalSegmentator environment. This may take several minutes.")
if not ai_segmentation.install_model(True):
raise RuntimeError("TotalSegmentator installation failed.")
total_segmentator_params = ai_segmentation.get_default_total_segmentator_params()
total_segmentator_params.task = TOTAL_SEGMENTATOR_TASK
total_segmentator_params.fastest = TOTAL_SEGMENTATOR_FASTEST
total_segmentator_params.device = TOTAL_SEGMENTATOR_DEVICE
mask_names = ai_segmentation.run_total_segmentator(
[volume_name],
total_segmentator_params,
True,
True,
[],
MAKE_IMAGE_ORIENTATION_COMPATIBLE,
)
if not mask_names:
raise RuntimeError("Segmentation finished without creating any masks.")
print_info(f"Generated {len(mask_names)} segmentation mask(s): {mask_names}")
return mask_names, "totalsegmentator"
def prepare_structure_masks(volume_name, initial_masks):
if len(initial_masks) > 1:
print_info("Multiple masks are already available. The workflow will process them directly.")
return initial_masks, None
candidate_mask = initial_masks[0]
try:
split_masks = mask_operations.split_multi_label_mask(
volume_name,
candidate_mask,
SEPARATE_ONLY_LARGEST_LABELS,
max(1, NUM_LARGEST_LABELS),
)
except Exception as exception:
print_warning(f"Mask splitting failed for '{candidate_mask}'. Continuing with the original mask. Details: {exception}")
return [candidate_mask], candidate_mask
if not split_masks:
print_warning(f"Mask splitting produced no child masks for '{candidate_mask}'. Continuing with the original mask.")
return [candidate_mask], candidate_mask
print_info(f"Prepared {len(split_masks)} structure mask(s) from '{candidate_mask}'.")
return split_masks, candidate_mask
def convert_masks_to_surfaces(mask_names):
mask_to_surface_params = api.MaskToSurfaceParams()
surface_records = []
for index, mask_name in enumerate(mask_names, start=1):
try:
created_surfaces = mask_operations.convert_to_surface_objects([mask_name], mask_to_surface_params)
if not created_surfaces:
print_warning(f"No surface was created for mask '{mask_name}'.")
continue
surface_name = created_surfaces[0]
surface_records.append(
{
"index": index,
"mask_name": mask_name,
"surface_name": surface_name,
}
)
print_info(f"Converted mask '{mask_name}' to surface '{surface_name}'.")
except Exception as exception:
print_warning(f"Failed to convert mask '{mask_name}' to a surface. Details: {exception}")
return surface_records
def optimize_surface_for_printing(surface_name):
# Remove disconnected surfaces and retain only the largest one
shell_filter_params = api.SurfaceFilterShellsParams()
shell_filter_params.largest_shells = 1
surface_operations.filter_shells(surface_name, api.SurfaceFilterShellsMethod.LargestShells, shell_filter_params)
# Apply smoothing to improve surface quality for 3D printing.
surface_operations.smooth_smart([surface_name], SMOOTH_ITERATIONS, SMOOTH_FACTOR)
# Reduce or decimate the surface to optimize for 3D printing.
surface_operations.reduce([surface_name], REDUCE_PERCENTAGE)
# Remesh the surface to create a more uniform triangle distribution.
surface_operations.remesh([surface_name], api.SurfaceRemeshMethod.Regular)
# Run diagnostics_checks and fix any issues that could impact 3D printability, such as non-manifold edges or holes.
diagnostics_checks = api.SurfaceDiagnosticsChecks()
surface_operations.fix_surface(surface_name, diagnostics_checks)
def run_wall_thickness_analysis(surface_record, output_layout):
surface_name = surface_record["surface_name"]
file_stem = f"{surface_record['index']:02d}_{safe_name(surface_name)}"
# Restore the default visualization settings for all analyses on the surface to ensure a
# clean starting point for the wall-thickness analysis display.
for analysis in app.get_all_analysis_names():
analysis_operations.restore_visualizations(analysis)
# Configure the wall-thickness analysis parameters.
wall_thickness_params = api.WallThicknessParams()
wall_thickness_params.method = api.WallThicknessMethod.RayCasting
wall_thickness_params.max_wall_thickness = WALL_THICKNESS_DISPLAY_RANGE_MM[1]
wall_thickness_params.search_angle = WALL_THICKNESS_SEARCH_ANGLE_DEG
# Create the wall-thickness analysis object for the surface.
analysis_name = analysis_operations.create_wall_thickness_analysis_surface("", surface_name, wall_thickness_params)
if not analysis_name:
raise RuntimeError("Failed to create the wall-thickness analysis object.")
# Run the analysis and wait for it to complete. The results will be stored in the analysis object and can be retrieved after completion.
if not analysis_operations.run_analysis(analysis_name):
raise RuntimeError(f"run_analysis returned False for '{analysis_name}'.")
if not analysis_operations.has_analysis_results(analysis_name):
raise RuntimeError(f"No wall-thickness results are available for '{analysis_name}'.")
# Retrieve the results and statistics objects from the analysis.
results : api.WallThicknessAnalysisResults = analysis_operations.get_wall_thickness_analysis_results(analysis_name)
stats : api.WallThicknessAnalysisStatistics = results.statistics
# Write the wall-thickness results to a text file for reporting and record-keeping.
output_path = os.path.join(output_layout["wall_thickness"], f"{file_stem}_wall_thickness.txt")
analysis_operations.write_wall_thickness_results_to_disk(results, output_path)
# Configure the display settings for the wall-thickness analysis to use a color map that highlights thin and thick areas,
# and set the display range to focus on the most relevant thickness values for 3D printing.
display_settings = analysis_operations.get_display_settings(analysis_name)
display_settings.lookup_table_type = api.LookupTableType.ReverseRainbow
display_settings.range = list(WALL_THICKNESS_DISPLAY_RANGE_MM)
analysis_operations.set_display_settings(analysis_name, display_settings)
analysis_operations.update_analysis(analysis_name)
print_info(
" | ".join(
[
f"Wall thickness for '{surface_name}'",
f"min={format_value(stats.min_value, suffix=' mm')}",
f"mean={format_value(stats.mean_value, suffix=' mm')}",
f"max={format_value(stats.max_value, suffix=' mm')}",
f"within range={format_value(stats.percentage_within_range, suffix='%')}",
]
)
)
return {
"analysis_name": analysis_name,
"report_path": output_path,
"statistics": {
"min_value": stats.min_value,
"max_value": stats.max_value,
"mean_value": stats.mean_value,
"std_deviation": stats.std_deviation,
"percentage_within_range": stats.percentage_within_range,
"percentage_below_range": stats.percentage_below_range,
"percentage_above_range": stats.percentage_above_range,
"total_area": stats.total_area,
},
}
def export_surface_meshes(surface_record, output_layout):
surface_name = surface_record["surface_name"]
wall_thickness_name = surface_record["wall_thickness"]["analysis_name"]
file_stem = f"{surface_record['index']:02d}_{safe_name(surface_name)}"
stl_path = os.path.join(output_layout["stl_meshes"], f"{file_stem}.stl")
if not surface_operations.export_surface_to_disk(surface_name, stl_path, False):
raise RuntimeError(f"Failed to export STL for '{surface_name}'.")
analysis_operations.update_analysis(wall_thickness_name)
# Ensure that the surface per-vertex colors are visible in the render view before exporting the color-mapped PLY,
# otherwise the exported geometry data may not contain the color arrays.
surface_render_properties_operations.set_scalar_visibility([surface_name], True)
# Export a color-mapped PLY mesh that encodes the wall-thickness values as vertex colors.
colored_mesh_path = os.path.join(output_layout["colored_meshes"], f"{file_stem}_wall_thickness.ply")
ok = surface_operations.export_surface_to_disk(surface_name, colored_mesh_path)
if not ok:
print_warning(f"'{surface_name}' failed to export.")
else:
print_info(f"Exported color-mapped PLY mesh to '{colored_mesh_path}'.")
return {
"stl_path": stl_path,
"colored_mesh_path": colored_mesh_path,
}
def capture_surface_screenshot(surface_record, output_layout):
surface_name = surface_record["surface_name"]
wall_thickness_name = surface_record["wall_thickness"]["analysis_name"]
file_stem = f"{surface_record['index']:02d}_{safe_name(surface_name)}"
app.set_volumes_visible(app.get_all_volume_names(), False)
app.set_masks_visible(app.get_all_mask_names(), False)
app.isolate_surfaces([surface_name])
surface_render_properties_operations.set_opacity([surface_name], 1.0)
surface_render_properties_operations.set_scalar_visibility([surface_name], True)
analysis_operations.update_analysis(wall_thickness_name)
screenshot_path = os.path.join(output_layout["screenshots"], f"{file_stem}_wall_thickness.png")
if not app.save_snapshot_to_disk(api.SnapshotType.Scene, screenshot_path, "PNG"):
raise RuntimeError(f"Failed to save the screenshot for '{surface_name}'.")
print_info(f"Saved wall-thickness screenshot to '{screenshot_path}'.")
return screenshot_path
def run_gray_value_analysis(surface_record, volume_name, output_layout):
surface_name = surface_record["surface_name"]
file_stem = f"{surface_record['index']:02d}_{safe_name(surface_name)}"
# If the workflow has already run a wall-thickness analysis, there may be existing visualization
# settings that could interfere with the gray-value analysis display. To ensure a clean starting
# point, we can restore the default visualization settings for all analyses on the surface before
# creating the new gray-value analysis.
for analysis in app.get_all_analysis_names():
analysis_operations.restore_visualizations(analysis)
# Configure the gray-value analysis parameters.
gray_value_params = api.GrayValueParams()
analysis_name = analysis_operations.create_gray_value_analysis_surface("", surface_name, volume_name, gray_value_params)
if not analysis_name:
raise RuntimeError("Failed to create the gray-value analysis object.")
# Run the analysis and wait for it to complete. The results will be stored in the analysis object and can be retrieved after completion.
if not analysis_operations.run_analysis(analysis_name):
raise RuntimeError(f"run_analysis returned False for '{analysis_name}'.")
if not analysis_operations.has_analysis_results(analysis_name):
raise RuntimeError(f"No gray-value results are available for '{analysis_name}'.")
# Retrieve the results and statistics objects from the analysis.
results : api.GrayValueAnalysisResults = analysis_operations.get_gray_value_analysis_results(analysis_name)
stats : api.GrayValueAnalysisStatistics = results.statistics
# Write the gray-value results to a text file for reporting and record-keeping.
output_path = os.path.join(output_layout["gray_value"], f"{file_stem}_gray_value.txt")
analysis_operations.write_gray_value_results_to_disk(results, output_path)
print_info(
" | ".join(
[
f"Gray values for '{surface_name}'",
f"min={format_value(stats.min_value)}",
f"mean={format_value(stats.mean_value)}",
f"max={format_value(stats.max_value)}",
f"std={format_value(stats.std_deviation)}",
]
)
)
# Configure the display settings for the gray-value analysis to use a color map that
# highlights different intensity ranges.
display_settings = analysis_operations.get_display_settings(analysis_name)
display_settings.lookup_table_type = api.LookupTableType.ReverseRainbow
display_settings.range = volume_operations.get_scalar_range(app.get_active_volume_name())
analysis_operations.set_display_settings(analysis_name, display_settings)
analysis_operations.update_analysis(analysis_name)
# Export a color-mapped PLY mesh that encodes the gray-value analysis results as vertex/face colors.
colored_mesh_path = os.path.join(output_layout["colored_meshes"], f"{file_stem}_gray_value.ply")
ok = surface_operations.export_surface_to_disk(surface_name, colored_mesh_path)
if not ok:
print_warning(f"'{surface_name}' failed to export.")
else:
print_info(f"Exported color-mapped PLY mesh to '{colored_mesh_path}'.")
return {
"analysis_name": analysis_name,
"report_path": output_path,
"statistics": {
"min_value": stats.min_value,
"max_value": stats.max_value,
"mean_value": stats.mean_value,
"std_deviation": stats.std_deviation,
},
}
def write_summary_report(report_path, timestamp, volume_name, output_layout, segmentation_source, multilabel_mask_name, surface_records):
with open(report_path, "w", encoding="utf-8", newline="\n") as stream:
stream.write("=" * 80 + "\n")
stream.write("VOLVICON AI SEGMENTATION AND MESH PREPARATION WORKFLOW REPORT\n")
stream.write("=" * 80 + "\n\n")
stream.write(f"Timestamp: {timestamp}\n")
stream.write(f"Source volume: {volume_name}\n")
stream.write(f"Segmentation source: {segmentation_source}\n")
stream.write(f"Primary multilabel mask: {multilabel_mask_name or 'n/a'}\n")
stream.write(f"Output root: {output_layout['root']}\n\n")
stream.write("CONFIGURATION\n")
stream.write("-" * 80 + "\n")
stream.write(f"TotalSegmentator task: {TOTAL_SEGMENTATOR_TASK}\n")
stream.write(f"TotalSegmentator device: {TOTAL_SEGMENTATOR_DEVICE}\n")
stream.write(f"Reuse existing masks: {REUSE_EXISTING_MASKS}\n")
stream.write(f"Largest-label splitting: {SEPARATE_ONLY_LARGEST_LABELS}\n")
stream.write(f"Requested largest labels: {NUM_LARGEST_LABELS}\n")
stream.write(f"Gray-value analysis enabled: {ENABLE_GRAY_VALUE_ANALYSIS}\n")
stream.write(
f"Wall-thickness display range (mm): {WALL_THICKNESS_DISPLAY_RANGE_MM[0]} to {WALL_THICKNESS_DISPLAY_RANGE_MM[1]}\n\n"
)
stream.write("STRUCTURE RESULTS\n")
stream.write("-" * 80 + "\n")
for surface_record in surface_records:
stream.write(
f"\n[{surface_record['index']:02d}] Mask '{surface_record['mask_name']}' -> Surface '{surface_record['surface_name']}'\n"
)
if surface_record.get("wall_thickness"):
wall_thickness = surface_record["wall_thickness"]
stream.write(" Wall thickness:\n")
stream.write(f" Analysis: {wall_thickness['analysis_name']}\n")
stream.write(
f" Min / Mean / Max (mm): {format_value(wall_thickness['statistics']['min_value'])} / "
f"{format_value(wall_thickness['statistics']['mean_value'])} / "
f"{format_value(wall_thickness['statistics']['max_value'])}\n"
)
stream.write(f" Std. deviation (mm): {format_value(wall_thickness['statistics']['std_deviation'])}\n")
stream.write(
f" Area within range (%): {format_value(wall_thickness['statistics']['percentage_within_range'])}\n"
)
stream.write(f" Text report: {wall_thickness['report_path']}\n")
else:
stream.write(" Wall thickness: not available\n")
if surface_record.get("exports"):
exports = surface_record["exports"]
stream.write(" Mesh exports:\n")
stream.write(f" STL: {exports['stl_path']}\n")
stream.write(f" Wall-thickness PLY: {exports['colored_mesh_path']}\n")
if surface_record.get("screenshot_path"):
stream.write(f" Screenshot: {surface_record['screenshot_path']}\n")
if surface_record.get("gray_value"):
gray_value = surface_record["gray_value"]
stream.write(" Gray-value analysis:\n")
stream.write(f" Analysis: {gray_value['analysis_name']}\n")
stream.write(
f" Min / Mean / Max: {format_value(gray_value['statistics']['min_value'])} / "
f"{format_value(gray_value['statistics']['mean_value'])} / "
f"{format_value(gray_value['statistics']['max_value'])}\n"
)
stream.write(f" Std. deviation: {format_value(gray_value['statistics']['std_deviation'])}\n")
stream.write(f" Text report: {gray_value['report_path']}\n")
elif ENABLE_GRAY_VALUE_ANALYSIS:
stream.write(" Gray-value analysis: not available\n")
else:
stream.write(" Gray-value analysis: disabled by configuration\n")
stream.write("\nEXPORT DIRECTORIES\n")
stream.write("-" * 80 + "\n")
stream.write(f"STL meshes: {output_layout['stl_meshes']}\n")
stream.write(f"Colored PLY meshes: {output_layout['colored_meshes']}\n")
stream.write(f"Wall-thickness reports: {output_layout['wall_thickness']}\n")
stream.write(f"Gray-value reports: {output_layout['gray_value']}\n")
stream.write(f"Screenshots: {output_layout['screenshots']}\n")
def main():
volume_name = app.get_active_volume_name()
if not volume_name:
print_error("No active volume is available. Open a CT scan before running this workflow.")
raise SystemExit(1)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_root = os.path.abspath(os.path.expanduser(f"~/{OUTPUT_PREFIX}_{timestamp}"))
output_layout = build_output_layout(output_root)
print_info(f"Active volume: {volume_name}")
print_info(f"Output root: {output_root}")
print_step(1, "Create or reuse segmentation masks")
try:
initial_masks, segmentation_source = create_or_reuse_masks(volume_name)
except Exception as exception:
print_error(f"Segmentation step failed. Details: {exception}")
raise SystemExit(1)
print_step(2, "Prepare structure masks for downstream mesh generation")
structure_masks, multilabel_mask_name = prepare_structure_masks(volume_name, initial_masks)
if not structure_masks:
print_error("No masks are available for surface generation.")
raise SystemExit(1)
print_step(3, "Convert masks to surfaces")
surface_records = convert_masks_to_surfaces(structure_masks)
if not surface_records:
print_error("The workflow could not create any surfaces from the prepared masks.")
raise SystemExit(1)
print_step(4, "Optimize the surfaces for 3D-printing workflows")
for surface_record in surface_records:
surface_name = surface_record["surface_name"]
try:
optimize_surface_for_printing(surface_name)
print_info(f"Optimized surface '{surface_name}'.")
except Exception as exception:
print_warning(f"Surface optimization failed for '{surface_name}'. Continuing with the current mesh. Details: {exception}")
print_step(5, "Run wall-thickness analysis and save per-surface reports")
wall_thickness_successes = 0
for surface_record in surface_records:
surface_name = surface_record["surface_name"]
try:
surface_record["wall_thickness"] = run_wall_thickness_analysis(surface_record, output_layout)
wall_thickness_successes += 1
except Exception as exception:
surface_record["wall_thickness"] = None
print_warning(f"Wall-thickness analysis failed for '{surface_name}'. Details: {exception}")
print_step(6, "Export printable STL meshes and wall-thickness color-mapped PLY meshes")
for surface_record in surface_records:
surface_name = surface_record["surface_name"]
if not surface_record.get("wall_thickness"):
print_warning(f"Skipping color-mapped mesh export for '{surface_name}' because no wall-thickness analysis is available.")
continue
try:
surface_record["exports"] = export_surface_meshes(surface_record, output_layout)
except Exception as exception:
print_warning(f"Mesh export failed for '{surface_name}'. Details: {exception}")
print_step(7, "Capture wall-thickness screenshots")
for surface_record in surface_records:
surface_name = surface_record["surface_name"]
if not surface_record.get("wall_thickness"):
print_warning(f"Skipping screenshot for '{surface_name}' because no wall-thickness analysis is available.")
continue
try:
surface_record["screenshot_path"] = capture_surface_screenshot(surface_record, output_layout)
except Exception as exception:
print_warning(f"Screenshot capture failed for '{surface_name}'. Details: {exception}")
if ENABLE_GRAY_VALUE_ANALYSIS:
print_step(8, "Optional gray-value analysis on the generated surfaces")
for surface_record in surface_records:
surface_name = surface_record["surface_name"]
try:
surface_record["gray_value"] = run_gray_value_analysis(surface_record, volume_name, output_layout)
except Exception as exception:
surface_record["gray_value"] = None
print_warning(f"Gray-value analysis failed for '{surface_name}'. Details: {exception}")
else:
print_step(8, "Optional gray-value analysis on the generated surfaces")
print_info("Gray-value analysis is disabled by configuration.")
print_step(9, "Write the workflow summary report")
report_path = os.path.join(output_layout["reports"], "workflow_summary.txt")
try:
write_summary_report(
report_path,
timestamp,
volume_name,
output_layout,
segmentation_source,
multilabel_mask_name,
surface_records,
)
print_info(f"Summary report saved to '{report_path}'.")
except Exception as exception:
print_warning(f"Failed to write the summary report. Details: {exception}")
print("\n" + "=" * 80)
print("WORKFLOW COMPLETED")
print("=" * 80)
print(f"Processed surfaces: {len(surface_records)}")
print(f"Wall-thickness analyses completed: {wall_thickness_successes}")
print(f"Output root: {output_root}")
print(f"STL meshes: {output_layout['stl_meshes']}")
print(f"Color-mapped PLY meshes: {output_layout['colored_meshes']}")
print(f"Screenshots: {output_layout['screenshots']}")
print(f"Summary report: {report_path}")
print("=" * 80)
if __name__ == "__main__":
main()
Related Resources​
- API Reference - API documentation
- Quick Reference - Common methods at a glance