Skip to content

Commit 9a11fba

Browse files
authored
Merge pull request #67 from EveCharbie/dev
Fix: mesh file scaling
2 parents 57af8ca + 0cd7a42 commit 9a11fba

File tree

5 files changed

+149
-29
lines changed

5 files changed

+149
-29
lines changed

biobuddy/components/model_utils.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ def children_segment_names(self, parent_name: str):
9090

9191
def get_chain_between_segments(self, first_segment_name: str, last_segment_name: str) -> list[str]:
9292
"""
93-
Get the name of the segments in the kinematic chain between first_segment_name and last_segment_name
93+
Get the name of the segments in the kinematic chain between first_segment_name and last_segment_name.
94+
WARNING: This list does not include brother/sister segments.
95+
So for example, if s_geom_1 and s_geom_2 both have s_offset_parent as parent, and only s_geom_1 has a child,
96+
only s_geom_1 will be included in the chain.
9497
"""
9598
chain = []
9699
this_segment = last_segment_name
@@ -101,6 +104,30 @@ def get_chain_between_segments(self, first_segment_name: str, last_segment_name:
101104
chain.reverse()
102105
return chain
103106

107+
def get_full_segment_chain(self, segment_name: str) -> list[str]:
108+
"""
109+
Get the name of the segments in the complete fake kinematic chain derived from this segment.
110+
This includes all ghost segments if they exist, including brother/sister segments.
111+
"""
112+
if segment_name + "_parent_offset" in self.segment_names:
113+
first_segment_name = segment_name + "_parent_offset"
114+
else:
115+
first_segment_name = segment_name
116+
last_segment_name = segment_name
117+
first_segment_index = self.segment_index(first_segment_name)
118+
last_segment_index = self.segment_index(last_segment_name)
119+
segment_list = self.segment_names[first_segment_index : last_segment_index + 1]
120+
121+
# Check that the segments were in "order" (meaning that only parent-child relationships were used)
122+
for this_segment_name in segment_list.copy()[::-1]:
123+
if this_segment_name == first_segment_name:
124+
break
125+
if self.segments[this_segment_name].parent_name not in segment_list:
126+
raise NotImplementedError(
127+
f"The segments in the model are not in the correct order to get the full segment chain for {segment_name}."
128+
)
129+
return segment_list
130+
104131
@property
105132
def nb_segments(self) -> int:
106133
return len(self.segments)

biobuddy/components/real/rigidbody/segment_scaling.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,7 @@ def compute_scale_factors(
143143
) -> ScaleFactor:
144144

145145
original_marker_names = original_model.marker_names
146-
q_zeros = np.zeros((original_model.nb_q, 1))
147-
markers = original_model.markers_in_global(q_zeros)
146+
markers = original_model.markers_in_global()
148147

149148
scale_factor = []
150149
for marker_pair in self.marker_pairs:
@@ -153,7 +152,7 @@ def compute_scale_factors(
153152
marker1_position_subject = marker_positions[:, marker_names.index(marker_pair[0]), :]
154153
marker2_position_subject = marker_positions[:, marker_names.index(marker_pair[1]), :]
155154
mean_distance_subject = np.nanmean(
156-
np.linalg.norm(marker2_position_subject - marker1_position_subject, axis=0)
155+
np.linalg.norm(marker2_position_subject[:3, :] - marker1_position_subject[:3, :], axis=0)
157156
)
158157

159158
# Distance between the marker pairs in the original model

biobuddy/model_modifiers/scale_tool.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -345,21 +345,6 @@ def scale_model_geometrically(self, marker_positions: np.ndarray, marker_names:
345345
)
346346
self.scaled_model.segments[segment_name + ghost_key].segment_coordinate_system = scs_scaled
347347

348-
# Scale the meshes of the intermediary ghost segments
349-
looping_parent_name = self.original_model.segments[
350-
segment_name
351-
].parent_name # The current segment's mesh will be scaled later
352-
scale_factor = scaling_factors[segment_name].to_vector()
353-
while ghost_key not in looping_parent_name:
354-
mesh_file = deepcopy(self.original_model.segments[looping_parent_name].mesh_file)
355-
if mesh_file is not None:
356-
mesh_file.mesh_scale *= scale_factor
357-
mesh_file.mesh_translation *= scale_factor
358-
self.scaled_model.segments[looping_parent_name].mesh_file = mesh_file
359-
looping_parent_name = self.original_model.segments[looping_parent_name].parent_name
360-
# Apply it to only one of the ghost segments recognized
361-
break
362-
363348
# Apply scaling to the current segment
364349
if self.original_model.segments[segment_name].parent_name in self.scaling_segments.keys():
365350
parent_scale_factor = scaling_factors[
@@ -396,6 +381,17 @@ def scale_model_geometrically(self, marker_positions: np.ndarray, marker_names:
396381
else:
397382
self.scaled_model.segments[segment_name] = deepcopy(self.original_model.segments[segment_name])
398383

384+
# Scale the meshes from all intermediary ghost segments
385+
if segment_name in scaling_factors.keys():
386+
segment_name_list = self.original_model.get_full_segment_chain(segment_name)
387+
scale_factor = scaling_factors[segment_name].to_vector()
388+
for this_segment_name in segment_name_list:
389+
mesh_file = deepcopy(self.original_model.segments[this_segment_name].mesh_file)
390+
if mesh_file is not None:
391+
mesh_file.mesh_scale *= scale_factor
392+
mesh_file.mesh_translation *= scale_factor
393+
self.scaled_model.segments[this_segment_name].mesh_file = mesh_file
394+
399395
# Scale muscles
400396
for muscle_group in self.original_model.muscle_groups:
401397

@@ -500,11 +496,6 @@ def scale_segment(
500496
if mesh_scaled is not None:
501497
mesh_scaled.positions *= scale_factor
502498

503-
mesh_file_scaled = deepcopy(original_segment.mesh_file)
504-
if mesh_file_scaled is not None:
505-
mesh_file_scaled.mesh_scale *= scale_factor
506-
mesh_file_scaled.mesh_translation *= scale_factor
507-
508499
return SegmentReal(
509500
name=deepcopy(original_segment.name),
510501
parent_name=deepcopy(original_segment.parent_name),
@@ -515,7 +506,9 @@ def scale_segment(
515506
qdot_ranges=deepcopy(original_segment.qdot_ranges),
516507
inertia_parameters=inertia_parameters_scaled,
517508
mesh=mesh_scaled,
518-
mesh_file=mesh_file_scaled,
509+
mesh_file=deepcopy(
510+
original_segment.mesh_file
511+
), # Mesh file scaling is handled later to scale all meshes from ghost segments
519512
)
520513

521514
def scale_marker(self, original_marker: MarkerReal, scale_factor: Point) -> MarkerReal:

examples/scaling_model.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77
import numpy as np
88
import biorbd
99

10-
from biobuddy import BiomechanicalModelReal, MuscleType, MuscleStateType, MeshParser, MeshFormat, ScaleTool
10+
from biobuddy import (
11+
BiomechanicalModelReal,
12+
MuscleType,
13+
MuscleStateType,
14+
MeshParser,
15+
MeshFormat,
16+
ScaleTool,
17+
C3dData,
18+
)
1119

1220

1321
def main(visualization):
@@ -94,10 +102,9 @@ def main(visualization):
94102
# )
95103

96104
# Scale the model
105+
static_c3d = C3dData(static_filepath, first_frame=100, last_frame=200)
97106
scaled_model = scale_tool.scale(
98-
filepath=static_filepath,
99-
first_frame=100,
100-
last_frame=200,
107+
static_c3d=static_c3d,
101108
mass=80,
102109
q_regularization_weight=0.01,
103110
make_static_pose_the_models_zero=True,

tests/test_biomechanical_model_real.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,97 @@ def test_change_mesh_directories():
339339
# Check that the mesh directories have been changed
340340
assert model.segments["parent"].mesh_file.mesh_file_directory == "new_geometry"
341341
assert model.segments["child"].mesh_file.mesh_file_directory == "new_geometry"
342+
343+
344+
def test_get_full_segment_chain():
345+
# Make sure one segment works
346+
model = create_simple_model()
347+
segment_chain = model.get_full_segment_chain(segment_name="child")
348+
assert segment_chain == ["child"]
349+
350+
# Test for a logical chain of segments
351+
model = create_simple_model()
352+
model.add_segment(
353+
SegmentReal(
354+
name="new_parent_offset",
355+
parent_name="child",
356+
)
357+
)
358+
model.add_segment(
359+
SegmentReal(
360+
name="new_translation",
361+
parent_name="new_parent_offset",
362+
)
363+
)
364+
model.add_segment(
365+
SegmentReal(
366+
name="new_rotation",
367+
parent_name="new_translation",
368+
)
369+
)
370+
model.add_segment(
371+
SegmentReal(
372+
name="new_geom1",
373+
parent_name="new_rotation",
374+
)
375+
)
376+
model.add_segment(
377+
SegmentReal(
378+
name="new_geom2",
379+
parent_name="new_rotation",
380+
)
381+
)
382+
model.add_segment(
383+
SegmentReal(
384+
name="new_reset_axis",
385+
parent_name="new_geom1",
386+
)
387+
)
388+
model.add_segment(
389+
SegmentReal(
390+
name="new",
391+
parent_name="new_reset_axis",
392+
)
393+
)
394+
segment_chain = model.get_full_segment_chain(segment_name="new")
395+
assert segment_chain == [
396+
"new_parent_offset",
397+
"new_translation",
398+
"new_rotation",
399+
"new_geom1",
400+
"new_geom2",
401+
"new_reset_axis",
402+
"new",
403+
]
404+
405+
# Test for a not supported chain of segments
406+
model = create_simple_model()
407+
model.add_segment(
408+
SegmentReal(
409+
name="new_parent_offset",
410+
parent_name="child",
411+
)
412+
)
413+
model.add_segment(
414+
SegmentReal(
415+
name="bad_segment",
416+
parent_name="parent",
417+
)
418+
)
419+
model.add_segment(
420+
SegmentReal(
421+
name="new_rotation",
422+
parent_name="new_parent_offset",
423+
)
424+
)
425+
model.add_segment(
426+
SegmentReal(
427+
name="new",
428+
parent_name="new_rotation",
429+
)
430+
)
431+
with pytest.raises(
432+
NotImplementedError,
433+
match="The segments in the model are not in the correct order to get the full segment chain for new.",
434+
):
435+
model.get_full_segment_chain(segment_name="new")

0 commit comments

Comments
 (0)