|
a |
|
b/registration/spine model/registration.py |
|
|
1 |
""" |
|
|
2 |
Created by: Rishav Raj and Qie Shang Pua, University of New South Wales |
|
|
3 |
This program registers a segmented spine model from an ultrasound scan to a ground truth spine model. |
|
|
4 |
Input: 1. The spine model in STL format |
|
|
5 |
2. The csv file for segmented points. |
|
|
6 |
Output: Two Figures - Registration of STL Model (Ground Truth) with the Segmented Spine Model |
|
|
7 |
- Error Distribution |
|
|
8 |
|
|
|
9 |
Note: This program is only valid for phantom 3 scan 2 |
|
|
10 |
""" |
|
|
11 |
|
|
|
12 |
# Modules to import |
|
|
13 |
import numpy as np |
|
|
14 |
import math |
|
|
15 |
import pandas as pd |
|
|
16 |
from stl.mesh import Mesh |
|
|
17 |
import open3d |
|
|
18 |
import scipy.spatial |
|
|
19 |
import scipy.io |
|
|
20 |
import copy |
|
|
21 |
import trimesh |
|
|
22 |
import matplotlib.pyplot as plt |
|
|
23 |
|
|
|
24 |
|
|
|
25 |
# ICP registration |
|
|
26 |
def draw_registration_result_original_color(source, target, transformation): |
|
|
27 |
source_temp = copy.deepcopy(source) |
|
|
28 |
source_temp.transform(transformation) |
|
|
29 |
source_temp.paint_uniform_color([0.5, 0.5, 0.5]) |
|
|
30 |
open3d.visualization.draw_geometries([source_temp, target]) |
|
|
31 |
|
|
|
32 |
def registration(raw): |
|
|
33 |
|
|
|
34 |
correct = np.array(raw) |
|
|
35 |
|
|
|
36 |
# To shift up the points, 11 is a threshold to determine whether to shift up or not |
|
|
37 |
numCols = len(raw[2]) |
|
|
38 |
to_shift_up = [0] * numCols |
|
|
39 |
for col in range(numCols): |
|
|
40 |
if raw[2][col] < 11: |
|
|
41 |
to_shift_up[col] = 30 |
|
|
42 |
else: |
|
|
43 |
to_shift_up[col] = 0 |
|
|
44 |
|
|
|
45 |
raw[0] = raw[0] + to_shift_up |
|
|
46 |
|
|
|
47 |
# Axes Transformation, values are selected through trial and error as per MATLAB code |
|
|
48 |
x_shift = -43.5; |
|
|
49 |
y_shift = 95; |
|
|
50 |
z_shift = 148; |
|
|
51 |
|
|
|
52 |
correct[0] = raw[1] * 90.0 / 569.0 + x_shift |
|
|
53 |
correct[1] = (raw[0] * (-88) / 569) + y_shift |
|
|
54 |
correct[2] = raw[2] * (-5.01) + z_shift |
|
|
55 |
|
|
|
56 |
np.set_printoptions(precision=3) # Prints array in 3 decimal places |
|
|
57 |
|
|
|
58 |
# Rotation |
|
|
59 |
skew_value = 0.08 |
|
|
60 |
skew_y = np.array([[1, 0, skew_value], [0, 1, 0], [0, 0, 1]]) |
|
|
61 |
|
|
|
62 |
correct = np.dot(skew_y, correct) |
|
|
63 |
|
|
|
64 |
tz = np.deg2rad(1) |
|
|
65 |
ty = np.deg2rad(-0.5) |
|
|
66 |
|
|
|
67 |
Rz = [[math.cos(tz), -1 * math.sin(tz), 0], [math.sin(tz), math.cos(tz), 0], [0, 0, 1]] |
|
|
68 |
Ry = [[math.cos(ty), 0, math.sin(ty)], [0, 1, 0], [-1 * math.sin(ty), 0, math.cos(ty)]] |
|
|
69 |
|
|
|
70 |
correct = np.dot(Rz, correct) |
|
|
71 |
correct = np.dot(Ry, correct) |
|
|
72 |
|
|
|
73 |
# Get STL model |
|
|
74 |
path = "/Users/puaqieshang/Desktop/Taste of Research/everything/models/Segmentation_bone.stl" |
|
|
75 |
stl_mesh = Mesh.from_file(path) |
|
|
76 |
points = np.around(np.unique(stl_mesh.vectors.reshape([stl_mesh.vectors.size // 3, 3]), axis=0), 2) |
|
|
77 |
|
|
|
78 |
# Displays the model, eliminate outlier points that are too far |
|
|
79 |
# Range values for each axis as selected per MATLAB code |
|
|
80 |
minz = -200 |
|
|
81 |
maxz = 400 |
|
|
82 |
|
|
|
83 |
miny = -100 |
|
|
84 |
maxy = 400 |
|
|
85 |
|
|
|
86 |
minx = -100 |
|
|
87 |
maxx = 500 |
|
|
88 |
|
|
|
89 |
# p represents point cloud |
|
|
90 |
px = np.bitwise_and(points[0, :] > minx, points[0, :] < maxx) |
|
|
91 |
py = np.bitwise_and(points[1, :] > miny, points[1, :] < maxy) |
|
|
92 |
pz = np.bitwise_and(points[2, :] > minz, points[2, :] < maxz) |
|
|
93 |
p = np.bitwise_and(px, py, pz) |
|
|
94 |
|
|
|
95 |
# Transpose segmented points to be in dimensions (number of rows, 3) |
|
|
96 |
segmented_points = np.transpose(correct) |
|
|
97 |
|
|
|
98 |
num_rows = np.ma.size(segmented_points, 0) |
|
|
99 |
points = np.transpose(points) |
|
|
100 |
|
|
|
101 |
# Array to store the error for colour map |
|
|
102 |
error = np.ones((num_rows, 1)) |
|
|
103 |
|
|
|
104 |
# For loop determines the colour map or the error when mapping segmented points to stl |
|
|
105 |
for i in range(num_rows): # 39372 |
|
|
106 |
point = segmented_points[i, :] # extracting each row from segmented points |
|
|
107 |
point_p2 = points[:, i] |
|
|
108 |
|
|
|
109 |
# euclidean distance between the segmented points and the stl model |
|
|
110 |
dist = scipy.spatial.distance.euclidean(point_p2, point) |
|
|
111 |
dist = dist / 100 |
|
|
112 |
|
|
|
113 |
if (dist > 10): |
|
|
114 |
error[i] = 0; # if the error is too big then it won't display on the colour map because |
|
|
115 |
# it is a point of less interest |
|
|
116 |
elif np.mean(points[p, 1]) < point[1]: |
|
|
117 |
error[i] = np.mean(dist) |
|
|
118 |
else: |
|
|
119 |
error[i] = np.mean(dist) |
|
|
120 |
|
|
|
121 |
# ignore negative errors |
|
|
122 |
for i in range(num_rows): |
|
|
123 |
if (error[i] < 0): |
|
|
124 |
error[i] = 0 |
|
|
125 |
|
|
|
126 |
# Outputs maximum and minimum error |
|
|
127 |
highest = max(error) |
|
|
128 |
lowest = min(error) |
|
|
129 |
print(f"The maximum error is {highest}mm") |
|
|
130 |
print(f"The minimum error is {lowest}mm") |
|
|
131 |
|
|
|
132 |
# Use a temp variable to store the error array of each point |
|
|
133 |
temp = error; |
|
|
134 |
|
|
|
135 |
# Convert error array into rgb and store in colours array |
|
|
136 |
error = error * 255 / highest |
|
|
137 |
zeroes = np.zeros((39372, 2)) |
|
|
138 |
colours = np.append(zeroes, error, axis=1) |
|
|
139 |
|
|
|
140 |
# Displays colour map depending on error, the threshold are selected using trial and error |
|
|
141 |
for i in range(len(temp)): |
|
|
142 |
if temp[i] > 1.7: |
|
|
143 |
colours[i] = [0, 255, 0] # green colour means more error |
|
|
144 |
continue |
|
|
145 |
|
|
|
146 |
if temp[i] < 0.5: |
|
|
147 |
colours[i] = [0, 0, 255] # blue colour means less error |
|
|
148 |
continue |
|
|
149 |
|
|
|
150 |
|
|
|
151 |
# Display the STL Point Cloud and Segmented Points |
|
|
152 |
pcd_image = open3d.geometry.PointCloud() |
|
|
153 |
pcd_image.points = open3d.utility.Vector3dVector(segmented_points) |
|
|
154 |
pcd_image.colors = open3d.utility.Vector3dVector(colours) |
|
|
155 |
|
|
|
156 |
# Writing segmented points cloud to an open3d image |
|
|
157 |
open3d.io.write_point_cloud("point_cloud_segmented.ply", pcd_image) |
|
|
158 |
# Read the point cloud |
|
|
159 |
pcd_image = open3d.io.read_point_cloud("point_cloud_segmented.ply") |
|
|
160 |
|
|
|
161 |
# Load original STL file |
|
|
162 |
mesh1 = trimesh.load('/Users/puaqieshang/Desktop/Taste of Research/everything/models/Segmentation_bone.stl') |
|
|
163 |
# Convert stl file to ply. The STl file is the ground truth model |
|
|
164 |
mesh2 = mesh1.copy() |
|
|
165 |
mesh2.export('ground_truth.ply') |
|
|
166 |
|
|
|
167 |
# Set parameters for icp registration |
|
|
168 |
source = open3d.io.read_point_cloud("ground_truth.ply") |
|
|
169 |
target = open3d.io.read_point_cloud("point_cloud_segmented.ply") |
|
|
170 |
threshold = 0.005 |
|
|
171 |
|
|
|
172 |
# Transformation matrix - Identity Matrix |
|
|
173 |
trans_init = np.identity(4) |
|
|
174 |
|
|
|
175 |
# Evaluate registration performance |
|
|
176 |
evaluation = open3d.registration.evaluate_registration(source, target,threshold, trans_init) |
|
|
177 |
|
|
|
178 |
# Obtain a registered model |
|
|
179 |
reg_p2p = open3d.registration.registration_icp(source, target, threshold, |
|
|
180 |
trans_init,open3d.registration.TransformationEstimationPointToPoint()) |
|
|
181 |
|
|
|
182 |
# Plot registered model |
|
|
183 |
draw_registration_result_original_color(source, target, reg_p2p.transformation) |
|
|
184 |
|
|
|
185 |
# Plot the error distribution |
|
|
186 |
plt.hist(temp, bins=50) |
|
|
187 |
plt.gca().set(title='Error/Distance', ylabel='Frequency'); |
|
|
188 |
plt.show() |
|
|
189 |
|
|
|
190 |
# Main file |
|
|
191 |
def main(): |
|
|
192 |
|
|
|
193 |
# Retrieve segmented_points data by running registration.m on MATLAB |
|
|
194 |
# change path if located at another file |
|
|
195 |
segmented_points_path = "/Users/puaqieshang/Desktop/raw_segmented_points.csv" |
|
|
196 |
df = pd.read_csv(segmented_points_path, header=None) |
|
|
197 |
registration(np.array(df)) |
|
|
198 |
|
|
|
199 |
main() |