Python Tutorials

Explore Python tutorials for CamiTK. Learn how to create extensions using ITK, VTK, OpenCV, and more.

Explore Python Tutorials

Computer Vision

This extension contains five actions that demonstrate video acquisition using OpenCV.

This tutorial requires opencv-python for the video/image acquisition as well as PySide to manage the acquisition timers.

Capture Webcam Image Acquisition OpenCV Python Computer Vision Video
opencv-python Qt

Acquire an image using a video camera.

During startup, this action checks the cameras available to the computer and prepares a list with their index.

Choose the camera id in the list, and click "Apply()" to acquire an image.

After the first acquisition, you can select the newly created image component to start video acquisition using the 'Video Capture' action also provided by this extension.

# This is a CamiTK python action
#
# Acquire an image using a camera.
# During startup, this action checks the cameras available
# to the computer and prepares a list with their index. 
# The user chooses the camera id in the list, and clicks "Apply()"
# to acquire an image.
# After an acquisition, the user can select the newly created image
# component to start video acquisition using the 'Video Capture' action.
import camitk
import cv2
import numpy as np

def list_available_cameras(max_cameras=10):
    available_cameras = []

    for index in range(max_cameras):
        cap = cv2.VideoCapture(index)
        if cap.isOpened():
            available_cameras.append(index)
            cap.release()

    return available_cameras
        
def capture_image_from_camera(camera_index = 0):
    # Initialize the camera using the given video device
    cap = cv2.VideoCapture(camera_index)

    if not cap.isOpened():
        camitk.warning(f"Error: Could not open capturing device #{camera_index}.")
        return None

    # Capture frame-by-frame
    ret, frame = cap.read()

    if not ret:
        camitk.warning("Error: Could not read frame.")
        return None

    # Convert the image from BGR to RGB
    rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    
    # Release the camera
    cap.release()

    return rgb_frame

def init(self:camitk.Action):
    available_cameras = list_available_cameras()
    
    if available_cameras:
        prop = camitk.Property("Video Capture Device", available_cameras[0], "Camera to use for image acquisition (0 is usually the PC webcam)", "")
        prop.setAttribute("enumNames", available_cameras);
        prop.setEnumTypeName(r'VideoCaptureDeviceId');
        self.addParameter(prop)
        self.setApplyButtonText("Take Snapshot")
    else:
        print("No cameras found.")
        return
    
    selected_camera = self.getParameterValue("Video Capture Device")
    enumValues = self.getProperty("Video Capture Device").getAttribute("enumNames")   
    camitk.info(f"init() selected camera: {selected_camera} → {enumValues[selected_camera]}")

def process(self:camitk.Action):
    camitk.info(f"process()")
    selected_camera = self.getParameterValue("Video Capture Device")
    enumValues = self.getProperty("Video Capture Device").getAttribute("enumNames")   
    camitk.info(f"process() selected camera: {selected_camera} → {enumValues[selected_camera]}")
    image = capture_image_from_camera(int(enumValues[selected_camera]))

    if image is not None:
        new_image_component = camitk.newImageComponentFromNumpy(image, "Webcam Image")
    
    self.refreshApplication() # similar to what would be done in C++
    # or camitk.refresh()

Video acquisition using OpenCV. It continuously replaces the image data of the selected component using the video feed.

This action uses a timer to refresh the image.

Note that you need to run the "Acquire Image" first in order to initialize the image with the camera image dimensions.

# This is a CamiTK python action
#
# Video acquisition using OpenCV. It continuously replaces the image data of the selected
# component using the video feed.
# This action uses a timer to refresh the image.
# Note that you need to run the "Acquire Image" action first in order to get an image
# component with the proper dimensions.

import camitk
import cv2
# Ensuring Qt5 and Qt6 compatibility of this tutorial
# (will be removed in the next version)
try:
    from PySide6.QtCore import QTimer
except ImportError:
    try:
        from PySide2.QtCore import QTimer
    except ImportError:
        raise ImportError("Neither PySide6 nor PySide2 is installed. Check the requirements field of your extension .camitk file.")

def capture(self):
    ret, frame = self.capture_device.read()

    if not ret:
        camitk.warning("Error: Could not read frame.")
        return None
    else:
        # Convert the image from BGR to RGB
        rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        return rgb_frame

def capture_loop(self):
    rgb_frame = capture(self)
    if rgb_frame is not None and self.image_component:
        self.image_component.replaceImageData(rgb_frame)

    self.setParameterValue("Video Frame", self.getParameterValue("Video Frame")+1)
    
    # uncomment the next line to update the GUI
    self.saveState()
    self.updateWidget()

def init(self:camitk.Action):
    self.timer = None
    self.camera_index = 0
    self.setApplyButtonText("Start")

def process(self:camitk.Action):  
    # create the timer
    if not self.timer:
        self.timer = QTimer()
        connected = self.timer.timeout.connect(lambda: capture_loop(self))
        self.image_component = self.getTargets()[-1]
    
    # if the acquisition is paused
    if not self.timer.isActive():
        self.setParameterValue("Video Frame", 0)
        
        # open the camera
        self.capture_device = cv2.VideoCapture(self.camera_index)
        if not self.capture_device.isOpened():
            camitk.warning(f"error: Could not open capturing device #{self.camera_index}.")
            return
        
        camitk.info("Starting Image Acquisition")
        self.setApplyButtonText("Stop")
        self.timer.start(10)
    else:
        camitk.info("Stopping Image acquisition")
        self.setApplyButtonText("Start")
        self.timer.stop()
        # Release the camera
        self.capture_device.release()

Video acquisition using OpenCV.

Creates a new RGB image component and continuously replaces its image data using the video feed.

This action uses a timer to refresh the acquisition.

# This is a CamiTK python action
#
# Video acquisition using OpenCV.
# 
# Creates a new RGB image component and continuously replaces its image data using the video feed.
# This action uses a timer to refresh the image.

import camitk
import cv2
# Ensuring Qt5 and Qt6 compatibility of this tutorial
# (will be removed in the next version)
try:
    from PySide6.QtCore import QTimer
except ImportError:
    try:
        from PySide2.QtCore import QTimer
    except ImportError:
        raise ImportError("Neither PySide6 nor PySide2 is installed. Check the requirements field of your extension .camitk file.")

def capture(self):
    ret, frame = self.capture_device.read()

    if not ret:
        camitk.warning("Error: Could not read frame.")
        return None
    else:
        # Convert the image from BGR to RGB
        rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        return rgb_frame

def capture_loop(self):
    rgb_frame = capture(self)
    if rgb_frame is not None and self.image_component:
        self.image_component.replaceImageData(rgb_frame)

def init(self:camitk.Action):
    self.timer:QTimer = None
    self.camera_index:int = 0
    self.image_component:camitk.ImageComponent = None
    self.setApplyButtonText("Start")

def process(self:camitk.Action):  
    if not self.timer:
        self.timer = QTimer()
        connected = self.timer.timeout.connect(lambda: capture_loop(self))
        
    if not self.timer.isActive():
        self.capture_device = cv2.VideoCapture(self.camera_index)
        if not self.capture_device.isOpened():
            camitk.warning(f"Error: Could not open capturing device #{self.camera_index}.")
            return

        rgb_frame = capture(self)
        if rgb_frame is not None and not self.image_component:
            self.image_component = camitk.newImageComponentFromNumpy(rgb_frame, "Webcam Video")
            self.refreshApplication()

        self.setApplyButtonText("Stop")    
        self.timer.start(10)
    else:      
        self.setApplyButtonText("Restart")    
        self.timer.stop()
        # Release the camera
        self.capture_device.release()

Same as 'Acquire Video' action, but using a external python script to detect available cameras (see 'Acquire Image') and grab video image using OpenCV.

This demonstrates how you can use a clean design in your action by using an architecture where independent code is glued to CamiTK to offer UI and interoperability.

# This is a CamiTK python action
#
# Video acquisition using an external python script video_capture_device to detect 
# available cameras (see 'Acquire Image') and grab video image using OpenCV
# 
# This demonstrates how you can use a clean design in your action 
# by using an architecture where your independent code stays in your external script
# and is glued into CamiTK using a Python Extension to offer UI and interoperability.

import camitk
import cv2
from video_capture_device import VideoCaptureDevice
# Ensuring Qt5 and Qt6 compatibility of this tutorial
# (will be removed in the next version)
try:
    from PySide6.QtCore import QTimer
except ImportError:
    try:
        from PySide2.QtCore import QTimer
    except ImportError:
        raise ImportError("Neither PySide6 nor PySide2 is installed. Check the requirements field of your extension .camitk file.")

def capture_loop(self):
    rgb_frame = self.capture_device.capture()
    if rgb_frame is not None and self.image_component:
        self.image_component.replaceImageData(rgb_frame)

def init(self:camitk.Action):
    self.timer:QTimer = None
    self.image_component:camitk.ImageComponent = None
    self.capture_device:VideoCaptureDevice = None

    available_cameras = VideoCaptureDevice.list_available_cameras()
    
    if available_cameras:
        prop = camitk.Property("Video Capture Device", available_cameras[0], "Camera to use for image acquisition (0 is usually the PC webcam)", "")
        prop.setAttribute("enumNames", available_cameras);
        prop.setEnumTypeName(r'VideoCaptureDeviceId');
        self.addParameter(prop)
        self.setApplyButtonText("Start")
    else:
        print("No cameras found.")

def process(self:camitk.Action):  
    if not self.timer:
        self.timer = QTimer()
        connected = self.timer.timeout.connect(lambda: capture_loop(self))
        
    if not self.timer.isActive():
        # get the user choice
        selected_camera = self.getParameterValue("Video Capture Device")
        enumValues = self.getProperty("Video Capture Device").getAttribute("enumNames")   

        # instantiate the class defined in the external script
        self.capture_device = VideoCaptureDevice(int(enumValues[selected_camera]))
        self.capture_device.start()

        # first capture: create a new component
        rgb_frame = self.capture_device.capture()
        if rgb_frame is not None and not self.image_component:
            self.image_component = camitk.newImageComponentFromNumpy(rgb_frame, "Webcam Video")
            self.refreshApplication()
        
        # start acquisition timer
        self.setApplyButtonText("Stop")
        self.timer.start(10)
    else:      
        self.setApplyButtonText("Restart")
        self.timer.stop()
        # Release the camera
        self.capture_device.stop()

Video acquisition using OpenCV.

Creates a new image component and continuously replaces its image data using the video feed.

Three types of videos can be acquired:

  • grayscale video
  • RGB video
  • HSV mode video

You can pause/restart the video acquisition at any time. This action uses a timer to refresh the image.

# This is a CamiTK python action
#
# Video acquisition using OpenCV.
#
# This action creates a new  image component and continuously replaces
# its image data using the video feed. 
#
# Three types of video can be acquired:
# - grayscale video
# - RGB video
# - HSV mode video
# 
# You can pause/restart the video acquisition at any time.
# 
# This action uses a timer to refresh the image.

import camitk
import cv2
# Ensuring Qt5 and Qt6 compatibility of this tutorial
# (will be removed in the next version)
try:
    from PySide6.QtCore import QTimer
except ImportError:
    try:
        from PySide2.QtCore import QTimer
    except ImportError:
        raise ImportError("Neither PySide6 nor PySide2 is installed. Check the requirements field of your extension .camitk file.")

def capture(self):
    ret, frame = self.capture_device.read()

    if not ret:
        camitk.warning("Error: Could not read frame.")
        return None
    else:
        # Convert the image from BGR to RGB
        rgb_frame = cv2.cvtColor(frame, self.mode)
        return rgb_frame

def capture_loop(self):
    rgb_frame = capture(self)
    if rgb_frame is not None and camitk.Application.isAlive(self.image_component):
        self.image_component.replaceImageData(rgb_frame)
    else:
        self.timer.stop()
        # Release the camera
        self.capture_device.release()

def init(self:camitk.Action):
    self.timer:QTimer = None
    self.image_component_color:camitk.ImageComponent = None
    self.image_component_grayscale:camitk.ImageComponent = None
    self.image_component:camitk.ImageComponent = None
    self.camera_index = 0
    self.setApplyButtonText("Start")

def process(self:camitk.Action):
    # get the type of image to acquire
    enumValues = self.getProperty("Mode").getAttribute("enumNames")
    mode = enumValues[self.getParameterValue("Mode")]
    if mode == "Color":
        self.mode = cv2.COLOR_BGR2RGB
    elif mode == "Grayscale":
        self.mode = cv2.COLOR_BGR2GRAY
    else:
        self.mode = cv2.COLOR_BGR2HSV

    # create timer if needed
    if not self.timer:
        self.timer = QTimer()
        connected = self.timer.timeout.connect(lambda: capture_loop(self))
        
    if not self.timer.isActive():
        self.capture_device = cv2.VideoCapture(self.camera_index)
        if not self.capture_device.isOpened():
            camitk.warning(f"Error: Could not open capturing device #{self.camera_index}.")
            return

        rgb_frame = capture(self)
        if rgb_frame is not None:            
            if not camitk.Application.isAlive(self.image_component_grayscale):
                self.image_component_grayscale = None
            if not camitk.Application.isAlive(self.image_component_color):
                self.image_component_color = None
            
            if not self.image_component or not camitk.Application.isAlive(self.image_component):
                if mode == "Grayscale":
                    self.image_component_grayscale = camitk.newImageComponentFromNumpy(rgb_frame, "Webcam Video")
                    self.image_component = self.image_component_grayscale
                else:
                    self.image_component_color = camitk.newImageComponentFromNumpy(rgb_frame, "Webcam Video")
                    self.image_component = self.image_component_color   
            else:
                if mode == "Grayscale" and self.image_component != self.image_component_grayscale:
                    if self.image_component_grayscale is not None:
                        self.image_component = self.image_component_grayscale
                    else:
                        self.image_component_grayscale = camitk.newImageComponentFromNumpy(rgb_frame, "Webcam Video")
                        self.image_component = self.image_component_grayscale
                elif mode != "Grayscale" and self.image_component != self.image_component_color:
                    if self.image_component_color is not None:
                        self.image_component = self.image_component_color
                    else:
                        self.image_component_color = camitk.newImageComponentFromNumpy(rgb_frame, "Webcam Video")
                        self.image_component = self.image_component_color
                        
            self.image_component.setName(f"Webcam Video {mode}")

            # refresh application to show the newly created image component        
            self.refreshApplication()
        
        self.setApplyButtonText("Stop")
        self.timer.start(10)
    else:
        self.setApplyButtonText("Restart")
        self.timer.stop()
        # Release the camera
        self.capture_device.release()
Action Pipeline

This extension contains an action that demonstrates how to combine several actions (provided in the default CamiTK extensions) into a single action:

  • Image binarization
  • Reconstruction using the marching cube method
  • Visualization of mesh contours in the image viewers
Segmentation Action Pipeline Mesh Reconstruction Threshold Python

Build a CamiTK action pipeline that:

  • Binarize the input image using a threshold
  • Reconstruct a mesh from the binary image
  • Display the reconstructed mesh projection in the image viewers
  • These actions are applied automatically in this order using existing actions. The pipeline input/output plumbing is configured programmatically.

# This is a CamiTK python action
#
# Use registered (existing) SDK actions in a pipeline to
# - binarize the image using a threshold
# - reconstruct a mesh from the binary image
# - show the mesh projection in the image
# all in one go

import camitk

def init(self:camitk.Action):
    self.mesh = None
    return

def process(self:camitk.Action):
    # get the current target
    image = self.getTargets()[-1]
    
    # get current threshold from the GUI and set the threshold property of the threshold action
    currentThreshold = self.getParameterValue("Threshold")
    self.thresholdAction.setParameterValue("Low Threshold", currentThreshold)
    
    #-- 1. compute the threshold image (binary image)
    self.thresholdImage = camitk.Application.applyAction("Threshold (VTK)", image)
    
    #-- 2. chain this into a pipeline to obtain the reconstruction
    # setup reconstruction action parameter (can also)    
    reconstructionAction = camitk.Application.getAction("Reconstruction")
    reconstructionAction.setParameterValue("Subsample original image?", True)
    self.mesh = camitk.Application.applyAction("Reconstruction", self.thresholdImage)
    # Rename the output and change 
    self.mesh.setName(image.getName() + " mesh")
    self.mesh.setColor([0.66, 0.33, 1.0, 0.3]) # you don't have to add an alpha

    #-- 3. some cleanup: close the intermediary component (force closing = True, 
    # which closes the component even if it is set as modified)
    if self.getParameterValue("Close Intermediate Images"):
        camitk.Application.close(self.thresholdImage, True)
    
    #-- 4. show the mesh projection
    meshProjectionAction = camitk.Application.getAction("Mesh Projection")
    # setup default mesh projection parameters
    meshProjectionAction.setParameterValue("ImageComponent List", 0) # assume there the first image component is the correct one
    meshProjectionAction.setParameterValue("Contour Line Width", 3)
    camitk.Application.applyAction("Mesh Projection", self.mesh)
    # Set the visibility boolean once everything is setup 
    # (this is specific to the "Mesh Projection" action logic, which requires this parameter to be set 
    # only after the action is applied)
    meshProjectionAction.setParameterValue("Show Mesh Projection", True)

    self.refreshApplication()
    return True

def targetDefined(self:camitk.Action):
    image = self.getTargets()[-1]
    
    self.thresholdAction = camitk.Application.getAction("Threshold (VTK)")
    # uncomment the next line to assert that the threshold action is loaded and available
    # assert self.thresholdAction is not None, f"Cannot find the 'Threshold (VTK)' action. Please check your configuration"
    
    # let the threshold action compute the low threshold value (the action computes (max-min)/2)
    # from the voxels values and use it to update the GUI
    self.thresholdAction.setInputComponent(image)
    adjustedThreshold = self.thresholdAction.getParameterValue("Low Threshold")
    self.setParameterValue("Threshold", adjustedThreshold)
Frames and Transformations
This extension contains two actions that demonstrate how to manage frames of reference and geometrical transformations of any CamiTK component, and how to update the transform between two frames of reference using a random homogeneous transforms generated by SciPy.
Frame Python Transformation
numpy scipy

This action adds, updates and removes a transformation between a mesh and an image component. This action demonstrates the Transformation Manager API and does not alter the current state of frames or transformations..

The final result should not be visible, as all operations performed in the process method are reset. The state of the frames of reference and transformations is restored to what it was the action was applied.

Note:As the action configure gui to No GUI, triggering the action automatically executes the process method (the user does not need to click on the Apply button).

# This is a CamiTK python action
#
# Test frames and transformations
# This action will add, update and remove a transformation between two components.
# For image component you can choose to use the data or main frame.

import camitk
import numpy as np

from generate_random_transform import random_homogeneous_transform

def process(self:camitk.Action):
    # get the first mesh and first image components if any opened
    mesh, image = getComponents()
    
    if mesh is not None and image is not None:
        print(f"found {mesh.getName()} and {image.getName()}")
                
        # 1. move the mesh to the image data frame
        original_mesh_frame = mesh.getFrame()
        mesh.setFrame(image.getDataFrame())
        assert mesh.getFrame() == image.getDataFrame(), f"mesh frame should be equals to image data frame {image.getDataFrame().getName()} not {mesh.getFrame().getName()}"

        self.refreshApplication()
        
        # 2. create a new independent frame
        new_frame = camitk.TransformationManager.addFrameOfReference("new mesh frame")
        mesh.setFrame(new_frame)
        
        self.refreshApplication()
        
        # 3. add identity transformation from mesh frame to the image data frame
        tr = camitk.TransformationManager.addTransformation(mesh.getFrame(), image.getDataFrame())
        
        # update transformation using from/to using randomly generated transform
        camitk.TransformationManager.updateTransformation(mesh.getFrame(), image.getDataFrame(), random_homogeneous_transform())

        # update transformation using the tr object itself
        camitk.TransformationManager.updateTransformation(tr, random_homogeneous_transform())
        
        # test getting world frame
        world = camitk.TransformationManager.getWorldFrame()
        print(f"world frame is called {world.getName()}")
        
        # update the transformation, the mesh should be displayed as if it was in the image data frame
        camitk.TransformationManager.updateTransformation(tr, np.eye(4))
    
        # remove transformation
        camitk.TransformationManager.removeTransformation(mesh.getFrame(), image.getDataFrame())        
        # or
        # camitk.TransformationManager.removeTransformation(tr)

        # back to the start
        mesh.setFrame(original_mesh_frame)
        
        self.refreshApplication()
        return True
    else:
        camitk.warning("Nothing to do! This actions needs at least one opened mesh and one opened image.")
        return False

def getComponents():
    components = camitk.Application.getTopLevelComponents()
    # automatically select the first available mesh and first available image component
    mesh = next((c for c in components if isinstance(c, camitk.MeshComponent)), None)
    image = next((c for c in components if isinstance(c, camitk.ImageComponent)), None)
    return mesh, image

This action shows how to:

  • Dynamically update an enum property drop-down values.
  • Update the transform between two components (or to be exact between their frames of references)
  • Call a python function defined in a separate python script (that generates a random transform using SciPy)
# This is a CamiTK python action
#
# Set random transformation between two frames
import camitk

from generate_random_transform import random_homogeneous_transform

def process(self:camitk.Action):
    frames = camitk.TransformationManager.getFramesOfReference()
    
    selected_from_index = self.getParameterValue("From") # the index in the enum
    selected_from_value = self.getProperty("From").getAttribute("enumNames")[selected_from_index]
    print(f"{[f.getName() for f in frames]} vs { selected_from_value }")
    from_frame = next((f for f in frames if f.getName() == selected_from_value), None)
    assert from_frame is not None, f"Source frame does not exist anymore"

    # more direct if we assume that frame did not change since the action was triggered
    to_frame = frames[self.getParameterValue("To")]
    camitk.TransformationManager.updateTransformation(from_frame, to_frame, random_homogeneous_transform())

    self.refreshApplication()

def targetDefined(self:camitk.Action):
    # get all frames
    frames = camitk.TransformationManager.getFramesOfReference()
    frame_names = [ f.getName() for f in frames ]
    # set the dropdown From and To parameters
    self.getProperty("From").setAttribute("enumNames", frame_names)
    self.getProperty("To").setAttribute("enumNames", frame_names)

Mesh Processing
This extension contains two actions illustrating how to use VTK, NumPy and Matplotlib to visualize mesh barycenter and create a spherical mesh.
Mesh Processing Barycenter NumPy Sphere VTK Python Matplotlib
numpy vtk matplotlib

Computes the mesh barycenter and uses Matplotlib to show a 3D interactive plots of a sample of the points and the mesh barycenter.
# This is a CamiTK python action
#
# Testing MeshComponent in python: compute the mesh barycenter and use matplotlib
# to show a 3D plots of a sample of the points and the barycenter.

import numpy as np
import camitk
import matplotlib
matplotlib.use('QtAgg') # either Qt5 or Qt6
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D  # Needed to activate 3D projection

def init(self:camitk.Action):
    camitk.info("CamiTK version: " + camitk.__version__)
    self.setApplyButtonText("Show Interactive Plot")

def compute_barycenter(self:camitk.Action):
    points = self.getTargets()[-1].getPointSetAsNumpy()
    barycenter = np.mean(points, axis=0)
    # update action widget
    self.setParameterValue("Barycenter", "QVector3D(" + str(barycenter[0]) + "," + str(barycenter[1]) + "," + str(barycenter[2]) +")")
    return points, barycenter

def targetDefined(self:camitk.Action):
    compute_barycenter(self)

def subsample_points(points, max_points):
    n = points.shape[0]
    if n <= max_points:
        return points  # nothing to do
    indices = np.random.choice(n, size=max_points, replace=False)
    return points[indices]

def process(self:camitk.Action):
    points, barycenter = compute_barycenter(self)

    # 3D matplot is too slow for big mesh, so only samples the points up to 1000 points
    subsampled = subsample_points(points=points, max_points=1000)

    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    plt.get_current_fig_manager().set_window_title(f"Barycenter of {self.getTargets()[-1].getName()}")

    # Plot all points
    ax.scatter(subsampled[:,0], subsampled[:,1], subsampled[:,2], c='blue', marker='o', label='Points')

    # Plot barycenter
    ax.scatter(barycenter[0], barycenter[1], barycenter[2],
            c='red', marker='^', s=100, label='Barycenter')

    # Labels
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')

    # Legend
    ax.legend()

    # Equal aspect ratio (optional but looks better)
    ax.set_box_aspect([np.ptp(subsampled[:,0]), np.ptp(subsampled[:,1]), np.ptp(subsampled[:,2])])

    plt.show()

Create a sphere using VTK Python module and import it to CamiTK as a MeshComponent
# This is a CamiTK python action
#
# Create a sphere in python VTK and transform it to a MeshComponent in CamiTK

import camitk
# import vtk
import vtkmodules.all as vtk
from vtk.util import numpy_support

def init(self:camitk.Action):
    camitk.info("CamiTK version: " + camitk.__version__)

def process(self:camitk.Action):
    sphere = vtk.vtkSphereSource()
    sphere.SetRadius(self.getParameterValue("Radius"))
    sphere.SetThetaResolution(self.getParameterValue("Theta Resolution"))
    sphere.SetPhiResolution(self.getParameterValue("Phi Resolution"))
    sphere.Update()

    polydata = sphere.GetOutput()

    points = numpy_support.vtk_to_numpy(polydata.GetPoints().GetData())
    polys = numpy_support.vtk_to_numpy(polydata.GetPolys().GetData()).reshape(-1, 4) # (n_triangles, 4)
    print("points",points)
    print("polys", polys)
    # Each row = [3, p0, p1, p2] -> meaning "triangle with 3 points"
    # As the cell type is not specified, the type of cell is deduced from the number of vertex per polygon (3 → triangles)

    msh = camitk.newMeshComponentFromNumpy("Sphere " + str(self.getParameterValue("Radius")), points, polys)
    camitk.refresh() # similar to C++ self.refreshApplication() 
    return
Mesh Animation
This extension contains one action that demonstrates how to animate a mesh by randomly displacing its vertices over time via a timer-based animation, and which visualizes these dynamic changes using a displacement color scale.
Shaker Mesh Animation Mesh Deformation Python
numpy Qt

This action randomly displaces the vertices of a MeshComponent (as shown in the C++ Shaker tutorial). Click on ‘Show Displacement’ to display the norm of the displacement as a color gradient on the mesh. It uses a Qt timer to create the animation.
# This is a CamiTK python action
#
# This action displaces the points of a MeshComponent randomly 
# (this is the equivalent of the Shaker tutorial)

import camitk
import numpy as np
# Ensuring Qt5 and Qt6 compatibility of this tutorial
# (will be removed in the next version)
try:
    from PySide6.QtCore import QTimer
except ImportError:
    try:
        from PySide2.QtCore import QTimer
    except ImportError:
        raise ImportError("Neither PySide6 nor PySide2 is installed. Check the requirements field of your extension .camitk file.")

def tick(self:camitk.Action):
    # get the point position as a numpy array
    points = self.mesh.getPointSetAsNumpy();
    percent = self.getParameterValue("Intensity")
    
    # check dimensions
    assert points.ndim == 2 and points.shape[1] == 3, "Input must be an (N, 3) array"
    assert 0 <= percent <= 100, "Percent must be between 0 and 100"

    # Compute bounding box
    min_pt = points.min(axis=0)
    max_pt = points.max(axis=0)
    diag = np.linalg.norm(max_pt - min_pt)
    move_distance = (percent / 100.0) * diag
    
    # Generate random directions per point
    random_vectors = np.random.randn(*points.shape)
    norms = np.linalg.norm(random_vectors, axis=1, keepdims=True)
    directions = random_vectors / norms  # Normalize each row

    # Scale and move
    step_displacements = directions * move_distance
    moved_points = points + step_displacements
        
    # update mesh
    self.mesh.replacePointSet(moved_points)
    
    # compute displacements
    if self.getParameterValue("Show Displacement"):
        total_displacement = np.linalg.norm(moved_points - self.initial_position[self.mesh.getName()], axis=1)
        self.mesh.addPointData("Displacement", total_displacement)
    else:
        self.mesh.setDataRepresentationOff()
    
    # update interval
    interval = self.getParameterValue("Update Intervals")
    self.timer.setInterval(interval)
    
def init(self:camitk.Action):
    self.timer = None
    self.setApplyButtonText("Start Shaking")
    self.initial_position = {}    

def process(self:camitk.Action):
    self.mesh = self.getTargets()[-1]
    
    # store initial position
    if self.mesh.getName() not in self.initial_position:
        self.initial_position[self.mesh.getName()] = self.mesh.getPointSetAsNumpy();
    
    if not self.timer:
        self.timer = QTimer()
        connected = self.timer.timeout.connect(lambda: tick(self))     

    if not self.timer.isActive():
        self.setApplyButtonText("Stop Shaking")
        interval = self.getParameterValue("Update Intervals")
        self.timer.start(interval)
    else:
        self.mesh.replacePointSet(self.initial_position[self.mesh.getName()])
        self.setApplyButtonText("Restart Shaking")
        self.timer.stop()
    
def parameterChanged(self:camitk.Action, name:str):
    if not self.getParameterValue("Show Displacement"):
        self.mesh.setDataRepresentationOff()
Component Extension

This extension contains an action that demonstrates how to create and register a new component extension.

The new Component extension handles "pymixed" files. .pymixed files are JSON files that containing a dictionary with the path to an image and a mesh (this is the equivalent of the "mixed" component in the C++ tutorials). .pymixed are automatically registered in CamiTK and opening a .pymixed file will open the image and the mesh. An example of "pymixed" file is given in the testdata subdirectory, the mixedcomponentextension subdirectory contains the component extension source code.

Component Extension Mixed Files Register Extension Python

This action registers the new Python component extension for managing .pymixed files.

The registration is automatically called during initialization of the action.

The registration status is displayed in the action's GUI, clicking Apply simply re-check the status.

import camitk
# This line will run the mixedcomponentextension/__init__.py 
# which register the component extension to the CamiTK application
from mixedcomponentextension import MixedComponentExtension

def check_registration():
    return "pymixed" in camitk.ExtensionManager.getFileExtensions()

def init(self:camitk.Action):
    isRegistered = check_registration()
    self.setParameterValue("Python Mixed Extension Registered", isRegistered)
    self.setApplyButtonText("Check Registration")
    if isRegistered:
        camitk.trace("MixedComponentExtension registered.")
    else:
        camitk.warning("MixedComponentExtension was not registered")
    return isRegistered

def process(self:camitk.Action):
    self.setParameterValue("Python Mixed Extension Registered", check_registration())
    return True # nothing to do as the registration was already done

Image Processing
This extension contains three actions illustrating how to use ITK, NumPy, SciPy and Qt to visualize histograms of medical images, apply a threshold or apply a Gaussian filter.
Gaussian ITK Histogram SciPy Image Processing Threshold NumPy Filter Python Qt Matplotlib
itk scipy numpy matplotlib Qt

Use NumPy to threshold an image. Mininum, maximum, step and initial values are automatically updated for the selected image.
# This is a CamiTK python action
#
# Using numpy to threshold an image

import numpy as np
import camitk

def print_numpy_array_info(msg, image_data, threshold):
    # number of voxels below threshold but over 0
    count = np.sum((image_data < threshold) & (image_data > 0))
    # Mask values in range [0, threshold)
    masked = image_data[(image_data > 0) & (image_data < threshold)]
    # Get the number of unique values between 0 and threshold
    num_unique = np.unique(masked).size
    # full info
    camitk.info(msg + ":\n- size: " +str(image_data.shape) 
        + "\n- dim:" + str(image_data.ndim) + " of type " + str(type(image_data.ndim)) 
        + "\n- values: [" + str(image_data.min()) + ".." + str(image_data.max()) 
        + "]\n- nb of voxels < " + str(threshold) + ": " + str(count) + " voxels with " + str(num_unique) + " different values")

def process(self:camitk.Action):
    image_component = self.getTargets()[-1] # last target (should be the same as [0])
    image_data = image_component.getImageDataAsNumpy()
    
    # use numpy to threshold (set values that are below the threshold to 0)
    threshold = self.getParameterValue("Threshold")
    image_data[image_data<threshold] = image_data.min()

    # Test replace_image_data
    # image_component.replace_image_data(image_data)

    # Create image from data
    new_image_component = camitk.newImageComponentFromNumpy(image_data, "Image From Python", image_component.getSpacing())
    new_image_component.setFrameFrom(image_component)

    # other ways to call
    #camitk.newImageComponentFromNumpy(image_data) # default name="image", spacing=(1,1,1)
    #camitk.newImageComponentFromNumpy(image_data, spacing=image_data_info["spacing"]) # default name="image", spacing=(1,1,1)
    #camitk.newImageComponentFromNumpy(image_data, "label_map", spacing=(0.5, 0.5, 1.0))
    #camitk.newImageComponentFromNumpy(image_data, spacing=(0.5, 0.5, 1.0), name="threshold image")
    
    self.refreshApplication() # similar to what would be done in C++
    # or camitk.refresh()

    return True

def targetDefined(self:camitk.Action):
    targets = self.getTargets()
    image_component = self.getTargets()[-1] # last target (should be the same as [0])

    image_data = image_component.getImageDataAsNumpy()
    minValue = image_data.min()
    maxValue = int(image_data.max())
    step = (maxValue - minValue) / 255.0;
    
    threshold = self.getProperty("Threshold")    
    threshold.setAttribute("minimum", int(minValue)) # int(minValue) otherwise pybind11 converts 'numpy.int16' to QMap<QString, QVariant>'
    threshold.setAttribute("maximum", maxValue)
    threshold.setAttribute("singleStep", int(step))
    self.setParameterValue("Threshold", int(minValue + (maxValue - minValue)/2))

def parameterChanged(self:camitk.Action, name:str):
    camitk.info("Parameter " + name + " changed, new value: " + str(self.getParameterValue(name)))

Using NumPy to compute an image histogram and show the histogram in a separate interactive dialog using Matplotlib.

Note:As the action configure gui to No GUI, triggering the action automatically executes the process method (the user does not need to click on the Apply button).

# This is a CamiTK python action
#
# Compute and plot image histogram using numpy and matplotlib
import camitk
import numpy as np
import matplotlib
matplotlib.use('QtAgg')  # either Qt5 or Qt6
import matplotlib.pyplot as plt

def process(self:camitk.Action):
    image_component = self.getTargets()[0]
    image_data = image_component.getImageDataAsNumpy()

    flattened = image_data.ravel()  # or array3d.flatten()

    fig = plt.figure()
    fig.canvas.manager.set_window_title(f"Histogram of {image_component.getName()}")

    # Plot histogram
    plt.hist(flattened, bins=50, color='blue', alpha=0.7)
    plt.title("Histogram '" + image_component.getName() + "' voxel values")
    plt.xlabel("Value")
    plt.ylabel("Frequency")
    plt.grid(True)
    plt.show()

Apply a Gaussian filter using both ITK and SciPy. This action creates two images from the selected input image: one is filtered by the ITK Gaussian filter and the other by SciPy Gaussian filter. Mean, variance and image correlation are then calculated using NumPy and display in a Qt message box.
# This is a CamiTK python action
#
# Apply a Gaussian filter to an ImageComponent using ITK and SciPy

import camitk

import itk
import numpy as np
from scipy import ndimage
# Ensuring Qt5 and Qt6 compatibility of this tutorial
# (will be removed in the next version)
try:
    from PySide6.QtWidgets import *
except ImportError:
    try:
        from PySide2.QtWidgets import *
    except ImportError:
        raise ImportError("Neither PySide6 nor PySide2 is installed. Check the requirements field of your extension .camitk file.")

def init(self:camitk.Action):
    import scipy # only needed for printing SciPy version
    self.setParameterValue("Versions", f"<ul><li>ITK {itk.__version__}</li><li>SciPy {scipy.__version__}</li><li>NumPy {np.__version__}</li></ul>")

def gaussian_filter_scipy(input_array: np.ndarray, sigma: float = 1.0):
    scypi_smoothed = ndimage.gaussian_filter(input_array, sigma=sigma)
    return scypi_smoothed

def gaussian_filter_itk(input_array: np.ndarray, sigma: float = 1.0):
    #-- 1. Convert NumPy array to ITK image
    image_type = itk.Image[itk.F, 3]  # Float image, 3D
    itk_image = itk.image_from_array(input_array.astype(np.float32))

    #-- 2. Apply Gaussian filter
    gaussian_filter = itk.SmoothingRecursiveGaussianImageFilter[image_type, image_type].New()
    gaussian_filter.SetInput(itk_image)
    camitk.info("Sigma: " + str(sigma))
    gaussian_filter.SetSigma(sigma)
    gaussian_filter.Update()

    #-- 3. Convert back to NumPy
    smoothed_array = itk.array_from_image(gaussian_filter.GetOutput())
    return smoothed_array

def process(self:camitk.Action):
    #-- 1. get the target image data as numpy array
    image_component = self.getTargets()[-1] # last target (should be the same as [0])
    image_data = image_component.getImageDataAsNumpy()
    
    #-- 2. get the sigma value from the UI
    sigma = self.getParameterValue("Sigma")
    camitk.info("Sigma: " + str(sigma))

    #-- 3. use itk filter
    smoothed_itk = gaussian_filter_itk(input_array=image_data, sigma=sigma)

    #-- 4. create image from data (do not forget to use the original image spacing)
    new_image_component_itk = camitk.newImageComponentFromNumpy(smoothed_itk, image_component.getName() + " (Gaussian itk)", image_component.getSpacing())
    new_image_component_itk.setFrameFrom(image_component)

    #-- 5. use scipy
    smoothed_scipy = gaussian_filter_scipy(input_array=image_data, sigma=sigma)

    #-- 6. create image from data (do not forget to use the original image spacing)
    new_image_component_scipy = camitk.newImageComponentFromNumpy(smoothed_scipy, image_component.getName() + " (Gaussian scipy)", image_component.getSpacing())
    new_image_component_scipy.setFrameFrom(image_component)

    #-- 7. show the images
    self.refreshApplication() # similar to what would be done in C++
    # or camitk.refresh()

    #-- show comparison dialog
    message = []
    message.append("<h3>Image Statistics</h3>")
    message.append("<ul>")

    image_itk = new_image_component_itk.getImageDataAsNumpy()
    mean_itk, var_itk = np.mean(image_itk), np.var(image_itk)    
    message.append(f"<li><b>ITK</b> mean: {mean_itk:.3f}, variance: {var_itk:.3f}</li>")

    image_scipy = new_image_component_scipy.getImageDataAsNumpy()
    mean_scipy, var_scipy = np.mean(image_scipy), np.var(image_scipy)
    message.append(f"<li><b>SciPy</b> mean: {mean_scipy:.3f} variance: {var_scipy:.3f}</li>")

    message.append("</ul>")
    message.append("<h3>Image Correlation</h3>")
    message.append("<ul>")

    correlation = np.corrcoef(image_data.flatten(), image_itk.flatten())[0, 1]
    message.append(f"<li>Between original and ITK: {correlation:.3f}</li>")
    correlation = np.corrcoef(image_data.flatten(), image_scipy.flatten())[0, 1]
    message.append(f"<li>Between original and SciPy: {correlation:.3f}</li>")

    correlation = np.corrcoef(image_itk.flatten(), image_scipy.flatten())[0, 1]
    message.append(f"<li>Between ITK and Scipy: {correlation:.3f}</li>")
    message.append("</ul>")

    if correlation < 0.999:
        camitk.warning("Gaussian images are different.")

    messageDialog = QMessageBox()
    messageDialog.setIcon(QMessageBox.Information)

    # setting message for Message Box
    messageDialog.setText(''.join(message))
    
    # setting Message box window title
    messageDialog.setWindowTitle("Gaussian Filter Comparison")
    
    # declaring buttons on Message Box
    messageDialog.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
    messageDialog.exec_()
State and Timer

This extension contains four actions that demonstrate how to save/restore the action state from Python using either an action parameter or the "self" python dict as well as how to use timer to call a background process at a given interval.

The interaction with the CamiTK application is visualized using the (automatically created) action GUI.

This extension requires PySide to manage the timers.

Timer Self Action __dict__ Python Action Parameter
Qt

Simple test of the interaction between python and the CamiTK application using action parameters. This is a simple test where a counter is incremented in the process() method. It demonstrates how a value computed in python can be saved in the action parameters between two calls to 'apply()' using an action parameter. The process() method increments the value of the parameter (the user can also modify the value in the CamiTK interface before clicking on 'Apply')
# This is a CamiTK python action
#
# Simple test of the interaction between python and the CamiTK application
# using action parameters. 
# 
# This is a simple test where a counter is incremented in the process() method.
# It demonstrates how a value computed in python can be saved in the action 
# parameters between two calls to 'apply()' using an action parameter. 
# The process() method increments the value of the parameter 
# The user can also modify the value in the CamiTK GUI before clicking on 'Apply'
import camitk

def process(self:camitk.Action):
    counter = self.getParameterValue("Counter")
    counter += 1
    self.setParameterValue("Counter", counter)
    self.updateWidget()

   

Simple test of the interaction between python and the CamiTK application using 'self'. This is a simple test where a counter is incremented in the process() method using an attributes saved in the action's self dict. It demonstrates how state can be preserved between action 'apply' calls using internal variables. Note that the parameter is set to read only for this action as a way to prove the internal self.counter increments properly.
# This is a CamiTK python action
#
# Simple test of the interaction between python and the CamiTK application
# using 'self'. 
# This is a simple test where a counter is incremented in the process() method
# using an attribute saved in the action's self dict.
# 
# It demonstrates how state can be preserved between action 'apply' calls using
# internal variables. Note that the parameter is set to 'read only' for this 
# action as a way to prove the internal self.counter increments properly.

import camitk

def init(self:camitk.Action):
    self.counter = 42

def process(self:camitk.Action):    
    self.counter += 1
    self.setParameterValue("Counter", self.counter)
    # to update the GUI
    self.updateWidget()

Test the interaction between python and the CamiTK application using a parameter and a timer. This is a simple test where a counter is incremented in the tick() method called by a timer() that lives in Python. It demonstrates how state can be preserved between two timer ticks calls using the action parameters. Click 'Start'/'Stop' to start/stop the timer. Note that the timer interval is slowed down/accelerated automatically within the tick() method.
# This is a CamiTK python action
#
# Test the interaction between python and the CamiTK application using a parameter and a timer.
# This is a simple test where a counter is incremented in the tick() method called by a timer()
# that lives in Python.
# 
# It demonstrates how state can be preserved between two timer ticks calls using the action
# parameters. 
# Click 'Start'/'Stop' to start/stop the timer. 
# Note that the timer interval is slowed down/accelerated automatically within the tick() method.

import camitk
import sys
# Ensuring Qt5 and Qt6 compatibility of this tutorial
# (will be removed in the next version)
try:
    from PySide6.QtCore import QTimer
except ImportError:
    try:
        from PySide2.QtCore import QTimer
    except ImportError:
        raise ImportError("Neither PySide6 nor PySide2 is installed. Check the requirements field of your extension .camitk file.")

def tick(self:camitk.Action):
    counter = self.getParameterValue("Counter")
    print(f"test_parameter_interaction_timer tick {counter} (this is stderr)", file=sys.stderr) # first print is on stderr

    counter += 1
    self.setParameterValue("Counter", counter)

    self.timer.setInterval(int(1000.0 / float(counter%10 + 1)))
    self.updateWidget()
    print(f"test_parameter_interaction_timer tick {counter} (this is stdout)") # second print on stdout
    
def init(self:camitk.Action):
    print("test_parameter_interaction_timer init", file=sys.stderr)
    self.timer = None
    self.setApplyButtonText("Start")

def process(self:camitk.Action):
    self.counter = self.getParameterValue("Counter")
    print(f"test_parameter_interaction_timer process {self.counter}", file=sys.stderr)

    if not self.timer:
        self.timer = QTimer()
        connected = self.timer.timeout.connect(lambda: tick(self))     

    if not self.timer.isActive():
        self.setApplyButtonText("Stop")
        self.timer.start(1000)
    else:
        self.setApplyButtonText("Restart")
        self.timer.stop()

Test the interaction between python and the CamiTK application using 'self' and a timer. This is a simple test where a counter is incremented in the tick() method called by a timer() that lives in Python. It demonstrates how state can be preserved between two timer ticks calls using internal variables. Click 'Start'/'Stop' to start/stop the timer. Note that the timer interval is slowed down/accelerated automatically within the tick() method. Note also that the parameter is set to read only as a way to prove the internal self.counter increments properly.
# This is a CamiTK python action
#
# Test the interaction between python and the CamiTK application using 'self' and a timer.
# This is a simple test where a counter is incremented in the tick() method called by a
# timer() that lives in Python.
# It demonstrates how state can be preserved between two timer ticks calls using internal
# variables. 
# 
# Click 'Start'/'Stop' to start/stop the timer. Note that the timer interval is slowed
# down/accelerated automatically within the tick() method.
# Note also that the parameter is set to 'read only' as a way to prove the internal
# self.counter increments properly.

import camitk
# Ensuring Qt5 and Qt6 compatibility of this tutorial
# (will be removed in the next version)
try:
    from PySide6.QtCore import QTimer
except ImportError:
    try:
        from PySide2.QtCore import QTimer
    except ImportError:
        raise ImportError("Neither PySide6 nor PySide2 is installed. Check the requirements field of your extension .camitk file.")

def tick(self:camitk.Action):
    self.counter += 1
    camitk.info(f"counter: {self.counter}")

    # Backup state C++ memory is required as self.counter has changed in a 
    # method that is not called directly from C++ (init, process, targetDefined, parameterChanged),
    # i.e., a function called from python by python.
    # This is required to ensure the C++/python values are properly synchronized
    self.saveState()
    
    # call to set ParameterValue must be done _after_ saveState()
    self.setParameterValue("Counter", self.counter)
    self.timer.setInterval(int(1000.0 / float(self.counter%10 + 1)))

    # update action (default) widget
    self.updateWidget()

def init(self:camitk.Action):
    self.timer = None
    self.setApplyButtonText("Start")

def process(self:camitk.Action):
    self.counter = self.getParameterValue("Counter")

    if not self.timer:
        self.timer = QTimer()
        connected = self.timer.timeout.connect(lambda: tick(self))     

    if not self.timer.isActive():
        self.setApplyButtonText("Stop")
        self.timer.start(1000)
    else:
        self.setApplyButtonText("Restart")
        self.timer.stop()

Download All Tutorials

Download All Python Tutorials

Get all tutorials in a single archive.