a b/docs/statistics/main.html
1
<!doctype html>
2
<html lang="en">
3
<head>
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
6
<meta name="generator" content="pdoc 0.10.0" />
7
<title>pymskt.statistics.main API documentation</title>
8
<meta name="description" content="" />
9
<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
10
<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
11
<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
12
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
13
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
14
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
15
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
16
<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
17
</head>
18
<body>
19
<main>
20
<article id="content">
21
<header>
22
<h1 class="title">Module <code>pymskt.statistics.main</code></h1>
23
</header>
24
<section id="section-intro">
25
<details class="source">
26
<summary>
27
<span>Expand source code</span>
28
</summary>
29
<pre><code class="python">from re import sub
30
import numpy as np
31
from datetime import date
32
import os
33
34
import pyvista as pv
35
import pyacvd
36
37
38
from pymskt.mesh.meshRegistration import get_icp_transform, non_rigidly_register
39
from pymskt.mesh.meshTools import get_mesh_physical_point_coords, set_mesh_physical_point_coords, resample_surface
40
from pymskt.mesh.meshTransform import apply_transform
41
from pymskt.mesh.utils import get_symmetric_surface_distance, vtk_deep_copy
42
from pymskt.mesh import io 
43
44
today = date.today()
45
46
class FindReferenceMeshICP:
47
    &#34;&#34;&#34;
48
    For list of meshes perform all possible ICP registrations to identify mesh with smallest
49
    surface error to all other meshes. 
50
51
    Parameters
52
    ----------
53
    list_meshes : _type_
54
        _description_
55
    &#34;&#34;&#34;
56
57
    def __init__(
58
        self,
59
        list_mesh_paths,
60
        max_n_iter=1000,
61
        n_landmarks=1000,
62
        reg_mode=&#39;similarity&#39;,
63
        verbose=True
64
    ):
65
        &#34;&#34;&#34;
66
        Perform ICP registration between all pairs of meshes. Calculate
67
        symmetric surface distance for all registered meshes. Find target
68
        mesh with smallest mean surface error to all other meshes. 
69
70
        This smallest error mesh is the refrence mesh for the next step of
71
        SSM pipelines (procrustes using non-rigid registration)
72
73
        Parameters
74
        ----------
75
        list_mesh_paths : _type_
76
            _description_
77
        max_n_iter : int, optional
78
            _description_, by default 1000
79
        n_landmarks : int, optional
80
            _description_, by default 1000
81
        reg_mode : str, optional
82
            _description_, by default &#39;similarity&#39;
83
        verbose : bool, optional
84
            _description_, by default True
85
        &#34;&#34;&#34;
86
        self.list_mesh_paths = list_mesh_paths
87
        self.n_meshes = len(list_mesh_paths)
88
        self._symm_surface_distances = np.zeros((self.n_meshes, self.n_meshes), dtype=float)
89
        self._mean_errors = None
90
91
        self.max_n_iter = max_n_iter
92
        self.n_landmarks = n_landmarks
93
        self.reg_mode = reg_mode
94
95
        self.verbose=verbose
96
97
        self._ref_idx = None
98
        self._ref_path = None
99
100
101
    def register_meshes(self, idx1_target, idx2_source):
102
        target = io.read_vtk(self.list_mesh_paths[idx1_target])
103
        source = io.read_vtk(self.list_mesh_paths[idx2_source])
104
105
        icp_transform = get_icp_transform(
106
            source, 
107
            target, 
108
            max_n_iter=self.max_n_iter, 
109
            n_landmarks=self.n_landmarks, 
110
            reg_mode=self.reg_mode
111
        )
112
113
        transformed_source = apply_transform(source, icp_transform)
114
115
        symmetric_surf_distance = get_symmetric_surface_distance(target, transformed_source)
116
117
        self._symm_surface_distances[idx1_target, idx2_source] = symmetric_surf_distance
118
    
119
    def get_template_idx(self):
120
        self._mean_errors = np.mean(self._symm_surface_distances, axis=1)
121
        self._ref_idx = np.argmin(self._mean_errors)
122
        self._ref_path = self.list_mesh_paths[self._ref_idx]
123
124
125
    def execute(self):
126
        if self.verbose is True:
127
            print(f&#39;Starting registrations, there are {len(self.list_mesh_paths)} meshes&#39;)
128
        for idx1_target, target_path in enumerate(self.list_mesh_paths):
129
            if self.verbose is True:
130
                print(f&#39;\tStarting target mesh {idx1_target}&#39;)
131
            for idx2_source, source_path in enumerate(self.list_mesh_paths):
132
                if self.verbose is True:
133
                    print(f&#39;\t\tStarting source mesh {idx2_source}&#39;)
134
                # If the target &amp; mesh are same skip, errors = 0
135
                if idx1_target == idx2_source:
136
                    continue
137
                else:
138
                    self.register_meshes(idx1_target, idx2_source)
139
        if self.verbose is True:
140
            print(&#39;Finished all registrations!&#39;)
141
        
142
        self.get_template_idx()
143
    
144
    @property
145
    def ref_idx(self):
146
        return self._ref_idx
147
    
148
    @property
149
    def ref_path(self):
150
        return self._ref_path
151
    
152
    @property
153
    def symm_surface_distances(self):
154
        return self._symm_surface_distances
155
    
156
    @property
157
    def mean_errors(self):
158
        return self._mean_errors
159
    
160
161
class ProcrustesRegistration:
162
    # https://en.wikipedia.org/wiki/Generalized_Procrustes_analysis
163
    def __init__(
164
        self,
165
        path_ref_mesh,
166
        list_mesh_paths,
167
        tolerance1=2e-1,
168
        tolerance2=1e-2,
169
        max_n_registration_steps=10,
170
        verbose=True,
171
        remesh_each_step=False,
172
        patience=2,
173
        ref_mesh_eigenmap_as_reference=True,
174
        registering_secondary_bone=False,    # True if registering secondary bone of joint, after
175
                                             # primary already used for initial registration. E.g., 
176
                                             # already did femur for knee, now applying to tibia/patella
177
        **kwargs
178
    ):
179
        self.path_ref_mesh = path_ref_mesh
180
        self.list_mesh_paths = list_mesh_paths
181
        # Ensure that path_ref_mesh is in list &amp; at index 0
182
        if self.path_ref_mesh in self.list_mesh_paths:
183
            path_ref_idx = self.list_mesh_paths.index(self.path_ref_mesh)
184
            self.list_mesh_paths.pop(path_ref_idx)
185
        self.list_mesh_paths.insert(0, self.path_ref_mesh)
186
187
188
        self._ref_mesh = io.read_vtk(self.path_ref_mesh)
189
        self.n_points = self._ref_mesh.GetNumberOfPoints()
190
        self.ref_mesh_eigenmap_as_reference = ref_mesh_eigenmap_as_reference
191
192
        self.mean_mesh = None
193
194
        self.tolerance1 = tolerance1
195
        self.tolerance2 = tolerance2
196
        self.max_n_registration_steps = max_n_registration_steps
197
198
        self.kwargs = kwargs
199
        # ORIGINALLY THIS WAS THE LOGIC:
200
        # Ensure that the source mesh (mean, or reference) is the base mesh
201
        # We want all meshes aligned with this reference. Then we want
202
        # to apply a &#34;warp&#34; of the ref/mean mesh to make it
203
        # EXCETION - if we are registering a secondary bone in a joint model
204
        # E.g., for registering tibia/patella in knee model. 
205
        self.kwargs[&#39;icp_register_first&#39;] = True
206
        if registering_secondary_bone is False:
207
            self.kwargs[&#39;icp_reg_target_to_source&#39;] = True
208
        elif registering_secondary_bone is True:
209
            self.kwargs[&#39;icp_reg_target_to_source&#39;] = False
210
211
        self._registered_pt_coords = np.zeros((len(list_mesh_paths), self.n_points, 3), dtype=float)
212
        self._registered_pt_coords[0, :, :] = get_mesh_physical_point_coords(self._ref_mesh)
213
214
        self.sym_error = 100
215
        self.list_errors = []
216
        self.list_ref_meshes = []
217
        self.reg_idx = 0
218
219
        self.patience = patience
220
        self.patience_idx = 0
221
        self._best_score = 100
222
223
        self.verbose = verbose
224
225
        self.remesh_each_step = remesh_each_step
226
227
        self.error_2_error_change = 100
228
229
    def register(self, ref_mesh_source, other_mesh_idx):
230
        target_mesh = io.read_vtk(self.list_mesh_paths[other_mesh_idx])
231
232
        registered_mesh = non_rigidly_register(
233
            target_mesh=target_mesh,
234
            source_mesh=ref_mesh_source,
235
            target_eigenmap_as_reference=not self.ref_mesh_eigenmap_as_reference,
236
            **self.kwargs
237
        )
238
239
        return get_mesh_physical_point_coords(registered_mesh)
240
    
241
    def execute(self):
242
        # create placeholder to store registered point clouds &amp; update inherited one only if also storing 
243
        registered_pt_coords = np.zeros_like(self._registered_pt_coords)
244
245
        # keep doing registrations until max# is hit, or the minimum error between registrations is hit. 
246
        while (
247
            (self.reg_idx &lt; self.max_n_registration_steps) &amp; 
248
            (self.sym_error &gt; self.tolerance1) &amp; 
249
            (self.error_2_error_change &gt; self.tolerance2)
250
        ):
251
            if self.verbose is True:
252
                print(f&#39;Starting registration round {self.reg_idx}&#39;)
253
            
254
            # If its not the very first iteration - check whether or not we want to re-mesh after every iteration. 
255
            if (self.reg_idx != 0) &amp; (self.remesh_each_step is True):
256
                n_points = self._ref_mesh.GetNumberOfPoints()
257
                self._ref_mesh = resample_surface(self._ref_mesh, subdivisions=2, clusters=n_points)
258
                if n_points != self.n_points:
259
                    print(f&#39;Updating n_points for mesh from {self.n_points} to {self._ref_mesh.GetNumberOfPoints()}&#39;)
260
                    # re-create the array to store registered points as the # vertices might change after re-meshing. 
261
                    # also update n_points. 
262
                    self.n_points = n_points
263
                    registered_pt_coords = np.zeros((len(self.list_mesh_paths), self.n_points, 3), dtype=float)
264
265
            # register the reference mesh to all other meshes
266
            for idx, path in enumerate(self.list_mesh_paths):
267
                if self.verbose is True:
268
                    print(f&#39;\tRegistering to mesh # {idx}&#39;)
269
                # skip the first mesh in the list if its the first round (its the reference)
270
                if (self.reg_idx == 0) &amp; (idx == 0):
271
                    # first iteration &amp; ref mesh, just use points as they are. 
272
                    registered_pt_coords[idx, :, :] = get_mesh_physical_point_coords(self._ref_mesh)
273
                    continue
274
                # register &amp; save registered coordinates in the pre-allocated array
275
                registered_pt_coords[idx, :, :] = self.register(self._ref_mesh, idx)
276
            
277
            # Calculate the mean bone shape &amp; create new mean bone shape mesh
278
            mean_shape = np.mean(registered_pt_coords, axis=0)
279
            mean_mesh = vtk_deep_copy(self._ref_mesh)
280
            set_mesh_physical_point_coords(mean_mesh, mean_shape)
281
            # store in list of reference meshes
282
            self.list_ref_meshes.append(mean_mesh)
283
284
            # Get surface distance between previous reference mesh and the new mean
285
            sym_error = get_symmetric_surface_distance(self._ref_mesh, mean_mesh)
286
            self.error_2_error_change = np.abs(sym_error - self.sym_error)
287
            self.sym_error = sym_error
288
            if self.sym_error &gt;= self._best_score:
289
                # if the error isnt going down, then keep track of that and done save the
290
                # new reference mesh. 
291
                self.patience_idx += 1
292
            else: 
293
                self.patience_idx = 0
294
                # ONLY UPDATE THE REF_MESH or the REGISTERED_PTS WHEN THE INTER-MESH (REF) ERROR GETS BETTER
295
                # NOT SURE IF THIS IS A GOOD IDEA - MIGHT WANT A BETTER CRITERIA? 
296
                self._ref_mesh = mean_mesh
297
                self._registered_pt_coords = registered_pt_coords
298
            # Store the symmetric error values so they can be plotted later
299
            self.list_errors.append(self.sym_error)
300
            if self.verbose is True:
301
                print(f&#39;\t\tSymmetric surface error: {self.sym_error}&#39;)
302
            
303
            if self.patience_idx &gt;= self.patience:
304
                print(f&#39;Early stopping initiated - no improvment for {self.patience_idx} iterations, patience is: {self.patience}&#39;)
305
                break           
306
                
307
            self.reg_idx += 1
308
309
    def save_meshes(
310
        self, 
311
        mesh_suffix=f&#39;procrustes_registered_{today.strftime(&#34;%b&#34;)}_{today.day}_{today.year}&#39;,
312
        folder=None
313
    ):  
314
        if folder is not None:
315
            os.makedirs(folder, exist_ok=True)
316
        mesh = vtk_deep_copy(self._ref_mesh)
317
        for idx, path in enumerate(self.list_mesh_paths):
318
            # parse folder / filename for saving
319
            orig_folder = os.path.dirname(path)
320
            orig_filename = os.path.basename(path)
321
            base_filename = orig_filename[: orig_filename.rfind(&#34;.&#34;)]
322
            filename = f&#39;{base_filename}_{mesh_suffix}_{idx}.vtk&#39;
323
            if folder is None:
324
                path_to_save = os.path.join(orig_folder, filename)
325
            else:
326
                path_to_save = os.path.join(os.path.abspath(folder), filename)        
327
            
328
            # Keep recycling the same base mesh, just move the x/y/z point coords around. 
329
            set_mesh_physical_point_coords(mesh, self._registered_pt_coords[idx, :, :])
330
            # save mesh to disk
331
            io.write_vtk(mesh, path_to_save)
332
    
333
    def save_ref_mesh(self, path):
334
        io.write_vtk(self._ref_mesh, path)
335
336
    @property
337
    def ref_mesh(self):
338
        return self._ref_mesh
339
    
340
    @property
341
    def registered_pt_coords(self):
342
        return self._registered_pt_coords
343
    </code></pre>
344
</details>
345
</section>
346
<section>
347
</section>
348
<section>
349
</section>
350
<section>
351
</section>
352
<section>
353
<h2 class="section-title" id="header-classes">Classes</h2>
354
<dl>
355
<dt id="pymskt.statistics.main.FindReferenceMeshICP"><code class="flex name class">
356
<span>class <span class="ident">FindReferenceMeshICP</span></span>
357
<span>(</span><span>list_mesh_paths, max_n_iter=1000, n_landmarks=1000, reg_mode='similarity', verbose=True)</span>
358
</code></dt>
359
<dd>
360
<div class="desc"><p>For list of meshes perform all possible ICP registrations to identify mesh with smallest
361
surface error to all other meshes. </p>
362
<h2 id="parameters">Parameters</h2>
363
<dl>
364
<dt><strong><code>list_meshes</code></strong> :&ensp;<code>_type_</code></dt>
365
<dd><em>description</em></dd>
366
</dl>
367
<p>Perform ICP registration between all pairs of meshes. Calculate
368
symmetric surface distance for all registered meshes. Find target
369
mesh with smallest mean surface error to all other meshes. </p>
370
<p>This smallest error mesh is the refrence mesh for the next step of
371
SSM pipelines (procrustes using non-rigid registration)</p>
372
<h2 id="parameters_1">Parameters</h2>
373
<dl>
374
<dt><strong><code>list_mesh_paths</code></strong> :&ensp;<code>_type_</code></dt>
375
<dd><em>description</em></dd>
376
<dt><strong><code>max_n_iter</code></strong> :&ensp;<code>int</code>, optional</dt>
377
<dd><em>description</em>, by default 1000</dd>
378
<dt><strong><code>n_landmarks</code></strong> :&ensp;<code>int</code>, optional</dt>
379
<dd><em>description</em>, by default 1000</dd>
380
<dt><strong><code>reg_mode</code></strong> :&ensp;<code>str</code>, optional</dt>
381
<dd><em>description</em>, by default 'similarity'</dd>
382
<dt><strong><code>verbose</code></strong> :&ensp;<code>bool</code>, optional</dt>
383
<dd><em>description</em>, by default True</dd>
384
</dl></div>
385
<details class="source">
386
<summary>
387
<span>Expand source code</span>
388
</summary>
389
<pre><code class="python">class FindReferenceMeshICP:
390
    &#34;&#34;&#34;
391
    For list of meshes perform all possible ICP registrations to identify mesh with smallest
392
    surface error to all other meshes. 
393
394
    Parameters
395
    ----------
396
    list_meshes : _type_
397
        _description_
398
    &#34;&#34;&#34;
399
400
    def __init__(
401
        self,
402
        list_mesh_paths,
403
        max_n_iter=1000,
404
        n_landmarks=1000,
405
        reg_mode=&#39;similarity&#39;,
406
        verbose=True
407
    ):
408
        &#34;&#34;&#34;
409
        Perform ICP registration between all pairs of meshes. Calculate
410
        symmetric surface distance for all registered meshes. Find target
411
        mesh with smallest mean surface error to all other meshes. 
412
413
        This smallest error mesh is the refrence mesh for the next step of
414
        SSM pipelines (procrustes using non-rigid registration)
415
416
        Parameters
417
        ----------
418
        list_mesh_paths : _type_
419
            _description_
420
        max_n_iter : int, optional
421
            _description_, by default 1000
422
        n_landmarks : int, optional
423
            _description_, by default 1000
424
        reg_mode : str, optional
425
            _description_, by default &#39;similarity&#39;
426
        verbose : bool, optional
427
            _description_, by default True
428
        &#34;&#34;&#34;
429
        self.list_mesh_paths = list_mesh_paths
430
        self.n_meshes = len(list_mesh_paths)
431
        self._symm_surface_distances = np.zeros((self.n_meshes, self.n_meshes), dtype=float)
432
        self._mean_errors = None
433
434
        self.max_n_iter = max_n_iter
435
        self.n_landmarks = n_landmarks
436
        self.reg_mode = reg_mode
437
438
        self.verbose=verbose
439
440
        self._ref_idx = None
441
        self._ref_path = None
442
443
444
    def register_meshes(self, idx1_target, idx2_source):
445
        target = io.read_vtk(self.list_mesh_paths[idx1_target])
446
        source = io.read_vtk(self.list_mesh_paths[idx2_source])
447
448
        icp_transform = get_icp_transform(
449
            source, 
450
            target, 
451
            max_n_iter=self.max_n_iter, 
452
            n_landmarks=self.n_landmarks, 
453
            reg_mode=self.reg_mode
454
        )
455
456
        transformed_source = apply_transform(source, icp_transform)
457
458
        symmetric_surf_distance = get_symmetric_surface_distance(target, transformed_source)
459
460
        self._symm_surface_distances[idx1_target, idx2_source] = symmetric_surf_distance
461
    
462
    def get_template_idx(self):
463
        self._mean_errors = np.mean(self._symm_surface_distances, axis=1)
464
        self._ref_idx = np.argmin(self._mean_errors)
465
        self._ref_path = self.list_mesh_paths[self._ref_idx]
466
467
468
    def execute(self):
469
        if self.verbose is True:
470
            print(f&#39;Starting registrations, there are {len(self.list_mesh_paths)} meshes&#39;)
471
        for idx1_target, target_path in enumerate(self.list_mesh_paths):
472
            if self.verbose is True:
473
                print(f&#39;\tStarting target mesh {idx1_target}&#39;)
474
            for idx2_source, source_path in enumerate(self.list_mesh_paths):
475
                if self.verbose is True:
476
                    print(f&#39;\t\tStarting source mesh {idx2_source}&#39;)
477
                # If the target &amp; mesh are same skip, errors = 0
478
                if idx1_target == idx2_source:
479
                    continue
480
                else:
481
                    self.register_meshes(idx1_target, idx2_source)
482
        if self.verbose is True:
483
            print(&#39;Finished all registrations!&#39;)
484
        
485
        self.get_template_idx()
486
    
487
    @property
488
    def ref_idx(self):
489
        return self._ref_idx
490
    
491
    @property
492
    def ref_path(self):
493
        return self._ref_path
494
    
495
    @property
496
    def symm_surface_distances(self):
497
        return self._symm_surface_distances
498
    
499
    @property
500
    def mean_errors(self):
501
        return self._mean_errors</code></pre>
502
</details>
503
<h3>Instance variables</h3>
504
<dl>
505
<dt id="pymskt.statistics.main.FindReferenceMeshICP.mean_errors"><code class="name">var <span class="ident">mean_errors</span></code></dt>
506
<dd>
507
<div class="desc"></div>
508
<details class="source">
509
<summary>
510
<span>Expand source code</span>
511
</summary>
512
<pre><code class="python">@property
513
def mean_errors(self):
514
    return self._mean_errors</code></pre>
515
</details>
516
</dd>
517
<dt id="pymskt.statistics.main.FindReferenceMeshICP.ref_idx"><code class="name">var <span class="ident">ref_idx</span></code></dt>
518
<dd>
519
<div class="desc"></div>
520
<details class="source">
521
<summary>
522
<span>Expand source code</span>
523
</summary>
524
<pre><code class="python">@property
525
def ref_idx(self):
526
    return self._ref_idx</code></pre>
527
</details>
528
</dd>
529
<dt id="pymskt.statistics.main.FindReferenceMeshICP.ref_path"><code class="name">var <span class="ident">ref_path</span></code></dt>
530
<dd>
531
<div class="desc"></div>
532
<details class="source">
533
<summary>
534
<span>Expand source code</span>
535
</summary>
536
<pre><code class="python">@property
537
def ref_path(self):
538
    return self._ref_path</code></pre>
539
</details>
540
</dd>
541
<dt id="pymskt.statistics.main.FindReferenceMeshICP.symm_surface_distances"><code class="name">var <span class="ident">symm_surface_distances</span></code></dt>
542
<dd>
543
<div class="desc"></div>
544
<details class="source">
545
<summary>
546
<span>Expand source code</span>
547
</summary>
548
<pre><code class="python">@property
549
def symm_surface_distances(self):
550
    return self._symm_surface_distances</code></pre>
551
</details>
552
</dd>
553
</dl>
554
<h3>Methods</h3>
555
<dl>
556
<dt id="pymskt.statistics.main.FindReferenceMeshICP.execute"><code class="name flex">
557
<span>def <span class="ident">execute</span></span>(<span>self)</span>
558
</code></dt>
559
<dd>
560
<div class="desc"></div>
561
<details class="source">
562
<summary>
563
<span>Expand source code</span>
564
</summary>
565
<pre><code class="python">def execute(self):
566
    if self.verbose is True:
567
        print(f&#39;Starting registrations, there are {len(self.list_mesh_paths)} meshes&#39;)
568
    for idx1_target, target_path in enumerate(self.list_mesh_paths):
569
        if self.verbose is True:
570
            print(f&#39;\tStarting target mesh {idx1_target}&#39;)
571
        for idx2_source, source_path in enumerate(self.list_mesh_paths):
572
            if self.verbose is True:
573
                print(f&#39;\t\tStarting source mesh {idx2_source}&#39;)
574
            # If the target &amp; mesh are same skip, errors = 0
575
            if idx1_target == idx2_source:
576
                continue
577
            else:
578
                self.register_meshes(idx1_target, idx2_source)
579
    if self.verbose is True:
580
        print(&#39;Finished all registrations!&#39;)
581
    
582
    self.get_template_idx()</code></pre>
583
</details>
584
</dd>
585
<dt id="pymskt.statistics.main.FindReferenceMeshICP.get_template_idx"><code class="name flex">
586
<span>def <span class="ident">get_template_idx</span></span>(<span>self)</span>
587
</code></dt>
588
<dd>
589
<div class="desc"></div>
590
<details class="source">
591
<summary>
592
<span>Expand source code</span>
593
</summary>
594
<pre><code class="python">def get_template_idx(self):
595
    self._mean_errors = np.mean(self._symm_surface_distances, axis=1)
596
    self._ref_idx = np.argmin(self._mean_errors)
597
    self._ref_path = self.list_mesh_paths[self._ref_idx]</code></pre>
598
</details>
599
</dd>
600
<dt id="pymskt.statistics.main.FindReferenceMeshICP.register_meshes"><code class="name flex">
601
<span>def <span class="ident">register_meshes</span></span>(<span>self, idx1_target, idx2_source)</span>
602
</code></dt>
603
<dd>
604
<div class="desc"></div>
605
<details class="source">
606
<summary>
607
<span>Expand source code</span>
608
</summary>
609
<pre><code class="python">def register_meshes(self, idx1_target, idx2_source):
610
    target = io.read_vtk(self.list_mesh_paths[idx1_target])
611
    source = io.read_vtk(self.list_mesh_paths[idx2_source])
612
613
    icp_transform = get_icp_transform(
614
        source, 
615
        target, 
616
        max_n_iter=self.max_n_iter, 
617
        n_landmarks=self.n_landmarks, 
618
        reg_mode=self.reg_mode
619
    )
620
621
    transformed_source = apply_transform(source, icp_transform)
622
623
    symmetric_surf_distance = get_symmetric_surface_distance(target, transformed_source)
624
625
    self._symm_surface_distances[idx1_target, idx2_source] = symmetric_surf_distance</code></pre>
626
</details>
627
</dd>
628
</dl>
629
</dd>
630
<dt id="pymskt.statistics.main.ProcrustesRegistration"><code class="flex name class">
631
<span>class <span class="ident">ProcrustesRegistration</span></span>
632
<span>(</span><span>path_ref_mesh, list_mesh_paths, tolerance1=0.2, tolerance2=0.01, max_n_registration_steps=10, verbose=True, remesh_each_step=False, patience=2, ref_mesh_eigenmap_as_reference=True, registering_secondary_bone=False, **kwargs)</span>
633
</code></dt>
634
<dd>
635
<div class="desc"></div>
636
<details class="source">
637
<summary>
638
<span>Expand source code</span>
639
</summary>
640
<pre><code class="python">class ProcrustesRegistration:
641
    # https://en.wikipedia.org/wiki/Generalized_Procrustes_analysis
642
    def __init__(
643
        self,
644
        path_ref_mesh,
645
        list_mesh_paths,
646
        tolerance1=2e-1,
647
        tolerance2=1e-2,
648
        max_n_registration_steps=10,
649
        verbose=True,
650
        remesh_each_step=False,
651
        patience=2,
652
        ref_mesh_eigenmap_as_reference=True,
653
        registering_secondary_bone=False,    # True if registering secondary bone of joint, after
654
                                             # primary already used for initial registration. E.g., 
655
                                             # already did femur for knee, now applying to tibia/patella
656
        **kwargs
657
    ):
658
        self.path_ref_mesh = path_ref_mesh
659
        self.list_mesh_paths = list_mesh_paths
660
        # Ensure that path_ref_mesh is in list &amp; at index 0
661
        if self.path_ref_mesh in self.list_mesh_paths:
662
            path_ref_idx = self.list_mesh_paths.index(self.path_ref_mesh)
663
            self.list_mesh_paths.pop(path_ref_idx)
664
        self.list_mesh_paths.insert(0, self.path_ref_mesh)
665
666
667
        self._ref_mesh = io.read_vtk(self.path_ref_mesh)
668
        self.n_points = self._ref_mesh.GetNumberOfPoints()
669
        self.ref_mesh_eigenmap_as_reference = ref_mesh_eigenmap_as_reference
670
671
        self.mean_mesh = None
672
673
        self.tolerance1 = tolerance1
674
        self.tolerance2 = tolerance2
675
        self.max_n_registration_steps = max_n_registration_steps
676
677
        self.kwargs = kwargs
678
        # ORIGINALLY THIS WAS THE LOGIC:
679
        # Ensure that the source mesh (mean, or reference) is the base mesh
680
        # We want all meshes aligned with this reference. Then we want
681
        # to apply a &#34;warp&#34; of the ref/mean mesh to make it
682
        # EXCETION - if we are registering a secondary bone in a joint model
683
        # E.g., for registering tibia/patella in knee model. 
684
        self.kwargs[&#39;icp_register_first&#39;] = True
685
        if registering_secondary_bone is False:
686
            self.kwargs[&#39;icp_reg_target_to_source&#39;] = True
687
        elif registering_secondary_bone is True:
688
            self.kwargs[&#39;icp_reg_target_to_source&#39;] = False
689
690
        self._registered_pt_coords = np.zeros((len(list_mesh_paths), self.n_points, 3), dtype=float)
691
        self._registered_pt_coords[0, :, :] = get_mesh_physical_point_coords(self._ref_mesh)
692
693
        self.sym_error = 100
694
        self.list_errors = []
695
        self.list_ref_meshes = []
696
        self.reg_idx = 0
697
698
        self.patience = patience
699
        self.patience_idx = 0
700
        self._best_score = 100
701
702
        self.verbose = verbose
703
704
        self.remesh_each_step = remesh_each_step
705
706
        self.error_2_error_change = 100
707
708
    def register(self, ref_mesh_source, other_mesh_idx):
709
        target_mesh = io.read_vtk(self.list_mesh_paths[other_mesh_idx])
710
711
        registered_mesh = non_rigidly_register(
712
            target_mesh=target_mesh,
713
            source_mesh=ref_mesh_source,
714
            target_eigenmap_as_reference=not self.ref_mesh_eigenmap_as_reference,
715
            **self.kwargs
716
        )
717
718
        return get_mesh_physical_point_coords(registered_mesh)
719
    
720
    def execute(self):
721
        # create placeholder to store registered point clouds &amp; update inherited one only if also storing 
722
        registered_pt_coords = np.zeros_like(self._registered_pt_coords)
723
724
        # keep doing registrations until max# is hit, or the minimum error between registrations is hit. 
725
        while (
726
            (self.reg_idx &lt; self.max_n_registration_steps) &amp; 
727
            (self.sym_error &gt; self.tolerance1) &amp; 
728
            (self.error_2_error_change &gt; self.tolerance2)
729
        ):
730
            if self.verbose is True:
731
                print(f&#39;Starting registration round {self.reg_idx}&#39;)
732
            
733
            # If its not the very first iteration - check whether or not we want to re-mesh after every iteration. 
734
            if (self.reg_idx != 0) &amp; (self.remesh_each_step is True):
735
                n_points = self._ref_mesh.GetNumberOfPoints()
736
                self._ref_mesh = resample_surface(self._ref_mesh, subdivisions=2, clusters=n_points)
737
                if n_points != self.n_points:
738
                    print(f&#39;Updating n_points for mesh from {self.n_points} to {self._ref_mesh.GetNumberOfPoints()}&#39;)
739
                    # re-create the array to store registered points as the # vertices might change after re-meshing. 
740
                    # also update n_points. 
741
                    self.n_points = n_points
742
                    registered_pt_coords = np.zeros((len(self.list_mesh_paths), self.n_points, 3), dtype=float)
743
744
            # register the reference mesh to all other meshes
745
            for idx, path in enumerate(self.list_mesh_paths):
746
                if self.verbose is True:
747
                    print(f&#39;\tRegistering to mesh # {idx}&#39;)
748
                # skip the first mesh in the list if its the first round (its the reference)
749
                if (self.reg_idx == 0) &amp; (idx == 0):
750
                    # first iteration &amp; ref mesh, just use points as they are. 
751
                    registered_pt_coords[idx, :, :] = get_mesh_physical_point_coords(self._ref_mesh)
752
                    continue
753
                # register &amp; save registered coordinates in the pre-allocated array
754
                registered_pt_coords[idx, :, :] = self.register(self._ref_mesh, idx)
755
            
756
            # Calculate the mean bone shape &amp; create new mean bone shape mesh
757
            mean_shape = np.mean(registered_pt_coords, axis=0)
758
            mean_mesh = vtk_deep_copy(self._ref_mesh)
759
            set_mesh_physical_point_coords(mean_mesh, mean_shape)
760
            # store in list of reference meshes
761
            self.list_ref_meshes.append(mean_mesh)
762
763
            # Get surface distance between previous reference mesh and the new mean
764
            sym_error = get_symmetric_surface_distance(self._ref_mesh, mean_mesh)
765
            self.error_2_error_change = np.abs(sym_error - self.sym_error)
766
            self.sym_error = sym_error
767
            if self.sym_error &gt;= self._best_score:
768
                # if the error isnt going down, then keep track of that and done save the
769
                # new reference mesh. 
770
                self.patience_idx += 1
771
            else: 
772
                self.patience_idx = 0
773
                # ONLY UPDATE THE REF_MESH or the REGISTERED_PTS WHEN THE INTER-MESH (REF) ERROR GETS BETTER
774
                # NOT SURE IF THIS IS A GOOD IDEA - MIGHT WANT A BETTER CRITERIA? 
775
                self._ref_mesh = mean_mesh
776
                self._registered_pt_coords = registered_pt_coords
777
            # Store the symmetric error values so they can be plotted later
778
            self.list_errors.append(self.sym_error)
779
            if self.verbose is True:
780
                print(f&#39;\t\tSymmetric surface error: {self.sym_error}&#39;)
781
            
782
            if self.patience_idx &gt;= self.patience:
783
                print(f&#39;Early stopping initiated - no improvment for {self.patience_idx} iterations, patience is: {self.patience}&#39;)
784
                break           
785
                
786
            self.reg_idx += 1
787
788
    def save_meshes(
789
        self, 
790
        mesh_suffix=f&#39;procrustes_registered_{today.strftime(&#34;%b&#34;)}_{today.day}_{today.year}&#39;,
791
        folder=None
792
    ):  
793
        if folder is not None:
794
            os.makedirs(folder, exist_ok=True)
795
        mesh = vtk_deep_copy(self._ref_mesh)
796
        for idx, path in enumerate(self.list_mesh_paths):
797
            # parse folder / filename for saving
798
            orig_folder = os.path.dirname(path)
799
            orig_filename = os.path.basename(path)
800
            base_filename = orig_filename[: orig_filename.rfind(&#34;.&#34;)]
801
            filename = f&#39;{base_filename}_{mesh_suffix}_{idx}.vtk&#39;
802
            if folder is None:
803
                path_to_save = os.path.join(orig_folder, filename)
804
            else:
805
                path_to_save = os.path.join(os.path.abspath(folder), filename)        
806
            
807
            # Keep recycling the same base mesh, just move the x/y/z point coords around. 
808
            set_mesh_physical_point_coords(mesh, self._registered_pt_coords[idx, :, :])
809
            # save mesh to disk
810
            io.write_vtk(mesh, path_to_save)
811
    
812
    def save_ref_mesh(self, path):
813
        io.write_vtk(self._ref_mesh, path)
814
815
    @property
816
    def ref_mesh(self):
817
        return self._ref_mesh
818
    
819
    @property
820
    def registered_pt_coords(self):
821
        return self._registered_pt_coords</code></pre>
822
</details>
823
<h3>Instance variables</h3>
824
<dl>
825
<dt id="pymskt.statistics.main.ProcrustesRegistration.ref_mesh"><code class="name">var <span class="ident">ref_mesh</span></code></dt>
826
<dd>
827
<div class="desc"></div>
828
<details class="source">
829
<summary>
830
<span>Expand source code</span>
831
</summary>
832
<pre><code class="python">@property
833
def ref_mesh(self):
834
    return self._ref_mesh</code></pre>
835
</details>
836
</dd>
837
<dt id="pymskt.statistics.main.ProcrustesRegistration.registered_pt_coords"><code class="name">var <span class="ident">registered_pt_coords</span></code></dt>
838
<dd>
839
<div class="desc"></div>
840
<details class="source">
841
<summary>
842
<span>Expand source code</span>
843
</summary>
844
<pre><code class="python">@property
845
def registered_pt_coords(self):
846
    return self._registered_pt_coords</code></pre>
847
</details>
848
</dd>
849
</dl>
850
<h3>Methods</h3>
851
<dl>
852
<dt id="pymskt.statistics.main.ProcrustesRegistration.execute"><code class="name flex">
853
<span>def <span class="ident">execute</span></span>(<span>self)</span>
854
</code></dt>
855
<dd>
856
<div class="desc"></div>
857
<details class="source">
858
<summary>
859
<span>Expand source code</span>
860
</summary>
861
<pre><code class="python">def execute(self):
862
    # create placeholder to store registered point clouds &amp; update inherited one only if also storing 
863
    registered_pt_coords = np.zeros_like(self._registered_pt_coords)
864
865
    # keep doing registrations until max# is hit, or the minimum error between registrations is hit. 
866
    while (
867
        (self.reg_idx &lt; self.max_n_registration_steps) &amp; 
868
        (self.sym_error &gt; self.tolerance1) &amp; 
869
        (self.error_2_error_change &gt; self.tolerance2)
870
    ):
871
        if self.verbose is True:
872
            print(f&#39;Starting registration round {self.reg_idx}&#39;)
873
        
874
        # If its not the very first iteration - check whether or not we want to re-mesh after every iteration. 
875
        if (self.reg_idx != 0) &amp; (self.remesh_each_step is True):
876
            n_points = self._ref_mesh.GetNumberOfPoints()
877
            self._ref_mesh = resample_surface(self._ref_mesh, subdivisions=2, clusters=n_points)
878
            if n_points != self.n_points:
879
                print(f&#39;Updating n_points for mesh from {self.n_points} to {self._ref_mesh.GetNumberOfPoints()}&#39;)
880
                # re-create the array to store registered points as the # vertices might change after re-meshing. 
881
                # also update n_points. 
882
                self.n_points = n_points
883
                registered_pt_coords = np.zeros((len(self.list_mesh_paths), self.n_points, 3), dtype=float)
884
885
        # register the reference mesh to all other meshes
886
        for idx, path in enumerate(self.list_mesh_paths):
887
            if self.verbose is True:
888
                print(f&#39;\tRegistering to mesh # {idx}&#39;)
889
            # skip the first mesh in the list if its the first round (its the reference)
890
            if (self.reg_idx == 0) &amp; (idx == 0):
891
                # first iteration &amp; ref mesh, just use points as they are. 
892
                registered_pt_coords[idx, :, :] = get_mesh_physical_point_coords(self._ref_mesh)
893
                continue
894
            # register &amp; save registered coordinates in the pre-allocated array
895
            registered_pt_coords[idx, :, :] = self.register(self._ref_mesh, idx)
896
        
897
        # Calculate the mean bone shape &amp; create new mean bone shape mesh
898
        mean_shape = np.mean(registered_pt_coords, axis=0)
899
        mean_mesh = vtk_deep_copy(self._ref_mesh)
900
        set_mesh_physical_point_coords(mean_mesh, mean_shape)
901
        # store in list of reference meshes
902
        self.list_ref_meshes.append(mean_mesh)
903
904
        # Get surface distance between previous reference mesh and the new mean
905
        sym_error = get_symmetric_surface_distance(self._ref_mesh, mean_mesh)
906
        self.error_2_error_change = np.abs(sym_error - self.sym_error)
907
        self.sym_error = sym_error
908
        if self.sym_error &gt;= self._best_score:
909
            # if the error isnt going down, then keep track of that and done save the
910
            # new reference mesh. 
911
            self.patience_idx += 1
912
        else: 
913
            self.patience_idx = 0
914
            # ONLY UPDATE THE REF_MESH or the REGISTERED_PTS WHEN THE INTER-MESH (REF) ERROR GETS BETTER
915
            # NOT SURE IF THIS IS A GOOD IDEA - MIGHT WANT A BETTER CRITERIA? 
916
            self._ref_mesh = mean_mesh
917
            self._registered_pt_coords = registered_pt_coords
918
        # Store the symmetric error values so they can be plotted later
919
        self.list_errors.append(self.sym_error)
920
        if self.verbose is True:
921
            print(f&#39;\t\tSymmetric surface error: {self.sym_error}&#39;)
922
        
923
        if self.patience_idx &gt;= self.patience:
924
            print(f&#39;Early stopping initiated - no improvment for {self.patience_idx} iterations, patience is: {self.patience}&#39;)
925
            break           
926
            
927
        self.reg_idx += 1</code></pre>
928
</details>
929
</dd>
930
<dt id="pymskt.statistics.main.ProcrustesRegistration.register"><code class="name flex">
931
<span>def <span class="ident">register</span></span>(<span>self, ref_mesh_source, other_mesh_idx)</span>
932
</code></dt>
933
<dd>
934
<div class="desc"></div>
935
<details class="source">
936
<summary>
937
<span>Expand source code</span>
938
</summary>
939
<pre><code class="python">def register(self, ref_mesh_source, other_mesh_idx):
940
    target_mesh = io.read_vtk(self.list_mesh_paths[other_mesh_idx])
941
942
    registered_mesh = non_rigidly_register(
943
        target_mesh=target_mesh,
944
        source_mesh=ref_mesh_source,
945
        target_eigenmap_as_reference=not self.ref_mesh_eigenmap_as_reference,
946
        **self.kwargs
947
    )
948
949
    return get_mesh_physical_point_coords(registered_mesh)</code></pre>
950
</details>
951
</dd>
952
<dt id="pymskt.statistics.main.ProcrustesRegistration.save_meshes"><code class="name flex">
953
<span>def <span class="ident">save_meshes</span></span>(<span>self, mesh_suffix='procrustes_registered_Jul_31_2022', folder=None)</span>
954
</code></dt>
955
<dd>
956
<div class="desc"></div>
957
<details class="source">
958
<summary>
959
<span>Expand source code</span>
960
</summary>
961
<pre><code class="python">def save_meshes(
962
    self, 
963
    mesh_suffix=f&#39;procrustes_registered_{today.strftime(&#34;%b&#34;)}_{today.day}_{today.year}&#39;,
964
    folder=None
965
):  
966
    if folder is not None:
967
        os.makedirs(folder, exist_ok=True)
968
    mesh = vtk_deep_copy(self._ref_mesh)
969
    for idx, path in enumerate(self.list_mesh_paths):
970
        # parse folder / filename for saving
971
        orig_folder = os.path.dirname(path)
972
        orig_filename = os.path.basename(path)
973
        base_filename = orig_filename[: orig_filename.rfind(&#34;.&#34;)]
974
        filename = f&#39;{base_filename}_{mesh_suffix}_{idx}.vtk&#39;
975
        if folder is None:
976
            path_to_save = os.path.join(orig_folder, filename)
977
        else:
978
            path_to_save = os.path.join(os.path.abspath(folder), filename)        
979
        
980
        # Keep recycling the same base mesh, just move the x/y/z point coords around. 
981
        set_mesh_physical_point_coords(mesh, self._registered_pt_coords[idx, :, :])
982
        # save mesh to disk
983
        io.write_vtk(mesh, path_to_save)</code></pre>
984
</details>
985
</dd>
986
<dt id="pymskt.statistics.main.ProcrustesRegistration.save_ref_mesh"><code class="name flex">
987
<span>def <span class="ident">save_ref_mesh</span></span>(<span>self, path)</span>
988
</code></dt>
989
<dd>
990
<div class="desc"></div>
991
<details class="source">
992
<summary>
993
<span>Expand source code</span>
994
</summary>
995
<pre><code class="python">def save_ref_mesh(self, path):
996
    io.write_vtk(self._ref_mesh, path)</code></pre>
997
</details>
998
</dd>
999
</dl>
1000
</dd>
1001
</dl>
1002
</section>
1003
</article>
1004
<nav id="sidebar">
1005
<h1>Index</h1>
1006
<div class="toc">
1007
<ul></ul>
1008
</div>
1009
<ul id="index">
1010
<li><h3>Super-module</h3>
1011
<ul>
1012
<li><code><a title="pymskt.statistics" href="index.html">pymskt.statistics</a></code></li>
1013
</ul>
1014
</li>
1015
<li><h3><a href="#header-classes">Classes</a></h3>
1016
<ul>
1017
<li>
1018
<h4><code><a title="pymskt.statistics.main.FindReferenceMeshICP" href="#pymskt.statistics.main.FindReferenceMeshICP">FindReferenceMeshICP</a></code></h4>
1019
<ul class="">
1020
<li><code><a title="pymskt.statistics.main.FindReferenceMeshICP.execute" href="#pymskt.statistics.main.FindReferenceMeshICP.execute">execute</a></code></li>
1021
<li><code><a title="pymskt.statistics.main.FindReferenceMeshICP.get_template_idx" href="#pymskt.statistics.main.FindReferenceMeshICP.get_template_idx">get_template_idx</a></code></li>
1022
<li><code><a title="pymskt.statistics.main.FindReferenceMeshICP.mean_errors" href="#pymskt.statistics.main.FindReferenceMeshICP.mean_errors">mean_errors</a></code></li>
1023
<li><code><a title="pymskt.statistics.main.FindReferenceMeshICP.ref_idx" href="#pymskt.statistics.main.FindReferenceMeshICP.ref_idx">ref_idx</a></code></li>
1024
<li><code><a title="pymskt.statistics.main.FindReferenceMeshICP.ref_path" href="#pymskt.statistics.main.FindReferenceMeshICP.ref_path">ref_path</a></code></li>
1025
<li><code><a title="pymskt.statistics.main.FindReferenceMeshICP.register_meshes" href="#pymskt.statistics.main.FindReferenceMeshICP.register_meshes">register_meshes</a></code></li>
1026
<li><code><a title="pymskt.statistics.main.FindReferenceMeshICP.symm_surface_distances" href="#pymskt.statistics.main.FindReferenceMeshICP.symm_surface_distances">symm_surface_distances</a></code></li>
1027
</ul>
1028
</li>
1029
<li>
1030
<h4><code><a title="pymskt.statistics.main.ProcrustesRegistration" href="#pymskt.statistics.main.ProcrustesRegistration">ProcrustesRegistration</a></code></h4>
1031
<ul class="">
1032
<li><code><a title="pymskt.statistics.main.ProcrustesRegistration.execute" href="#pymskt.statistics.main.ProcrustesRegistration.execute">execute</a></code></li>
1033
<li><code><a title="pymskt.statistics.main.ProcrustesRegistration.ref_mesh" href="#pymskt.statistics.main.ProcrustesRegistration.ref_mesh">ref_mesh</a></code></li>
1034
<li><code><a title="pymskt.statistics.main.ProcrustesRegistration.register" href="#pymskt.statistics.main.ProcrustesRegistration.register">register</a></code></li>
1035
<li><code><a title="pymskt.statistics.main.ProcrustesRegistration.registered_pt_coords" href="#pymskt.statistics.main.ProcrustesRegistration.registered_pt_coords">registered_pt_coords</a></code></li>
1036
<li><code><a title="pymskt.statistics.main.ProcrustesRegistration.save_meshes" href="#pymskt.statistics.main.ProcrustesRegistration.save_meshes">save_meshes</a></code></li>
1037
<li><code><a title="pymskt.statistics.main.ProcrustesRegistration.save_ref_mesh" href="#pymskt.statistics.main.ProcrustesRegistration.save_ref_mesh">save_ref_mesh</a></code></li>
1038
</ul>
1039
</li>
1040
</ul>
1041
</li>
1042
</ul>
1043
</nav>
1044
</main>
1045
<footer id="footer">
1046
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.10.0</a>.</p>
1047
</footer>
1048
</body>
1049
</html>