Python Tutorials
Discover how to create Python extensions for CamiTK using popular libraries like NumPy, SciPy, ITK, VTK, or OpenCV. Each tutorial includes source code.
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.
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
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.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.# 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()
# 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.# 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.
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.# 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()
# 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.
# 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()
# 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()
# 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()
# 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()