c3d-parser 0.3.4__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {c3d_parser-0.3.4/src/c3d_parser.egg-info → c3d_parser-0.5.0}/PKG-INFO +4 -2
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/requirements.txt +3 -1
- c3d_parser-0.5.0/src/c3d_parser/__init__.py +2 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/c3d_parser.py +89 -25
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/osim.py +15 -1
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/osim_resources/external_loads_template.xml +2 -2
- c3d_parser-0.5.0/src/c3d_parser/view/dialogs/delete_marker_set_dialog.py +52 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/dialogs/marker_set_dialog.py +12 -0
- c3d_parser-0.5.0/src/c3d_parser/view/dialogs/options_dialog.py +105 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/main_window.py +176 -34
- c3d_parser-0.5.0/src/c3d_parser/view/ui/ui_delete_marker_set_dialog.py +87 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/ui/ui_main_window.py +11 -0
- c3d_parser-0.5.0/src/c3d_parser/view/ui/ui_options_dialog.py +248 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/widgets.py +4 -1
- {c3d_parser-0.3.4 → c3d_parser-0.5.0/src/c3d_parser.egg-info}/PKG-INFO +4 -2
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser.egg-info/SOURCES.txt +2 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser.egg-info/requires.txt +3 -1
- c3d_parser-0.3.4/src/c3d_parser/__init__.py +0 -2
- c3d_parser-0.3.4/src/c3d_parser/view/dialogs/options_dialog.py +0 -62
- c3d_parser-0.3.4/src/c3d_parser/view/ui/ui_options_dialog.py +0 -136
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/LICENSE +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/README.md +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/pyproject.toml +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/setup.cfg +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/application.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/c3d_patch.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/osim_resources/ik_task_set.xml +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/utils.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/general.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/logging.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/AUC.json +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/FMC.json +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/MH.json +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/QCMAS.json +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/RBWH.json +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/RCH.json +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/Sydney.json +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/splash_rc.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/splashscreen.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/dialogs/about_dialog.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/dialogs/marker_set_import_dialog.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/ui/resources_rc.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/ui/ui_marker_set_dialog.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/ui/ui_marker_set_import_dialog.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/utils.py +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser.egg-info/dependency_links.txt +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser.egg-info/entry_points.txt +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser.egg-info/top_level.txt +0 -0
- {c3d_parser-0.3.4 → c3d_parser-0.5.0}/tests/test_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c3d-parser
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: C3D parser for gait data harmonisation.
|
|
5
5
|
Author-email: Timothy Salemink <tim.nicolas@outlook.com>, Sally Jack <sallyjaack@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -12,10 +12,12 @@ Requires-Dist: numpy==1.26.4
|
|
|
12
12
|
Requires-Dist: pandas
|
|
13
13
|
Requires-Dist: c3d==0.5.2
|
|
14
14
|
Requires-Dist: trc-data-reader
|
|
15
|
-
Requires-Dist: opensim-model-creator>=0.
|
|
15
|
+
Requires-Dist: opensim-model-creator>=0.3.0
|
|
16
16
|
Requires-Dist: PySide6
|
|
17
17
|
Requires-Dist: matplotlib
|
|
18
18
|
Requires-Dist: mplcursors
|
|
19
|
+
Requires-Dist: pyvistaqt
|
|
20
|
+
Requires-Dist: ll-visualiser>=0.3.0
|
|
19
21
|
Dynamic: license-file
|
|
20
22
|
|
|
21
23
|
# C3D-parser
|
|
@@ -7,6 +7,7 @@ import logging
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
10
|
+
from datetime import datetime
|
|
10
11
|
from collections import defaultdict
|
|
11
12
|
from scipy import signal, interpolate
|
|
12
13
|
from scipy.spatial.transform import Rotation
|
|
@@ -30,6 +31,7 @@ class CancelException(Exception):
|
|
|
30
31
|
pass
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
torso_markers = ["C7", "T2", "T10", "MAN"]
|
|
33
35
|
required_markers = [{"LASI", "RASI"}, {"LKNE", "RKNE"}, {"LANK", "RANK"}, {"LMED", "RMED"}, {"LHEE", "RHEE"},
|
|
34
36
|
({"LPSI", "RPSI"}, {"SACR"}), ({"LKNEM", "RKNEM"}, {"LKAX", "RKAX"})]
|
|
35
37
|
|
|
@@ -76,6 +78,8 @@ def parse_session(static_trial, dynamic_trials, input_directory, output_director
|
|
|
76
78
|
grf_file_paths[trial] = grf_file_path
|
|
77
79
|
deidentified_file_names[trial] = os.path.basename(trc_file_path).rsplit(".", 1)[0]
|
|
78
80
|
|
|
81
|
+
write_c3d_parser_history(input_directory, static_trial, deidentified_file_names)
|
|
82
|
+
|
|
79
83
|
dynamic_trc_path = list(trc_file_paths.values())[0] if trc_file_paths else ""
|
|
80
84
|
osim_model = create_osim_model(static_trc_path, dynamic_trc_path, frame, marker_diameter, static_data,
|
|
81
85
|
output_directory, optimise_knee_axis, progress_tracker)
|
|
@@ -85,7 +89,7 @@ def parse_session(static_trial, dynamic_trials, input_directory, output_director
|
|
|
85
89
|
|
|
86
90
|
progress_tracker.progress.emit("Running IK and ID", "black")
|
|
87
91
|
|
|
88
|
-
for trial in
|
|
92
|
+
for trial in trc_file_paths.keys():
|
|
89
93
|
ik_data, ik_output = run_ik(osim_model, trc_file_paths[trial], output_directory, marker_data_rate)
|
|
90
94
|
ik_data = pd.concat([ik_data, foot_progression_data[trial]], axis=1)
|
|
91
95
|
id_data = run_id(osim_model, ik_data, ik_output, grf_file_paths[trial], output_directory, marker_data_rate, event_data[trial], weight)
|
|
@@ -157,10 +161,12 @@ def parse_dynamic_trial(c3d_file, lab, output_directory, trial_index, marker_dat
|
|
|
157
161
|
analog_data = concatenate_grf_data(analog_data, events, mean_centre)
|
|
158
162
|
scale_grf_data(analog_data)
|
|
159
163
|
|
|
160
|
-
# Rotate trials for +X walking direction.
|
|
164
|
+
# Rotate trials for +X walking direction and +Y vertical.
|
|
161
165
|
rotation_matrix = get_global_rotation(frame_data)
|
|
162
166
|
rotate_trc_data(frame_data, rotation_matrix)
|
|
163
167
|
rotate_grf_data(analog_data, rotation_matrix)
|
|
168
|
+
rotate_trc_y_vertical(frame_data)
|
|
169
|
+
rotate_grf_y_vertical(analog_data)
|
|
164
170
|
|
|
165
171
|
# Write GRF data.
|
|
166
172
|
grf_directory = os.path.join(output_directory, 'grf')
|
|
@@ -182,6 +188,26 @@ def parse_dynamic_trial(c3d_file, lab, output_directory, trial_index, marker_dat
|
|
|
182
188
|
return analog_data, events, foot_progression, s_t_data, trc_file_path, grf_file_path
|
|
183
189
|
|
|
184
190
|
|
|
191
|
+
def write_c3d_parser_history(input_directory, static_trial, deidentified_file_names):
|
|
192
|
+
log_path = os.path.join(input_directory, "c3d_parser_history.log")
|
|
193
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
with open(log_path, "w", encoding="utf-8") as f:
|
|
197
|
+
f.write("C3D-Parser History\n")
|
|
198
|
+
f.write(f"Last Run: {timestamp}\n\n")
|
|
199
|
+
|
|
200
|
+
f.write(f"Static trial: {static_trial}\n\n")
|
|
201
|
+
|
|
202
|
+
if deidentified_file_names:
|
|
203
|
+
f.write("Deidentified file names:\n")
|
|
204
|
+
for original_name, updated_name in deidentified_file_names.items():
|
|
205
|
+
f.write(f"{original_name}: {updated_name}\n")
|
|
206
|
+
|
|
207
|
+
except (OSError, IOError):
|
|
208
|
+
raise ParserError("Could not write c3d_parser_history.log")
|
|
209
|
+
|
|
210
|
+
|
|
185
211
|
def run_ik(osim_model, trc_file_path, output_directory, marker_data_rate):
|
|
186
212
|
# Perform inverse kinematics.
|
|
187
213
|
file_name = os.path.splitext(os.path.basename(trc_file_path))[0]
|
|
@@ -276,6 +302,8 @@ def set_marker_data(trc_data, frame_data, rate=100):
|
|
|
276
302
|
trc_data['DataRate'] = rate
|
|
277
303
|
trc_data['CameraRate'] = rate
|
|
278
304
|
trc_data['NumFrames'] = frame_data.shape[0]
|
|
305
|
+
trc_data['OrigDataStartFrame'] = trc_data['Frame#'][0]
|
|
306
|
+
trc_data['OrigNumFrames'] = frame_data.shape[0]
|
|
279
307
|
|
|
280
308
|
|
|
281
309
|
def write_trc_data(trc_data, file_name, output_directory):
|
|
@@ -329,6 +357,8 @@ def harmonise_markers(frame_data, lab, required_markers):
|
|
|
329
357
|
if None in frame_data.columns:
|
|
330
358
|
frame_data.drop(columns=frame_data.columns[frame_data.columns.isna()], inplace=True)
|
|
331
359
|
|
|
360
|
+
# TODO: This error window does not indicate which trial has the issue.
|
|
361
|
+
# Update this.
|
|
332
362
|
# Ensure required markers are present.
|
|
333
363
|
available = set(frame_data.columns)
|
|
334
364
|
for item in required_markers:
|
|
@@ -353,7 +383,9 @@ def trim_frames(frame_data):
|
|
|
353
383
|
for marker_index in range(1, len(frame)):
|
|
354
384
|
coordinates = frame.iloc[marker_index]
|
|
355
385
|
if math.isnan(coordinates[0]):
|
|
356
|
-
|
|
386
|
+
marker_name = frame_data.columns[marker_index]
|
|
387
|
+
if marker_name not in torso_markers:
|
|
388
|
+
missing_markers.append(marker_name)
|
|
357
389
|
if missing_markers:
|
|
358
390
|
incomplete_frames[frame_number] = missing_markers
|
|
359
391
|
|
|
@@ -382,8 +414,10 @@ def trim_frames(frame_data):
|
|
|
382
414
|
if not drop_frames.empty:
|
|
383
415
|
frame_data.drop(drop_frames, inplace=True)
|
|
384
416
|
|
|
417
|
+
# TODO: If we convert this to a dictionary we can keep the markers that are missing and check if they are required.
|
|
385
418
|
remaining_frames = [frame for frame in incomplete_frames.keys() if trim_start <= frame <= trim_end]
|
|
386
419
|
if remaining_frames:
|
|
420
|
+
# TODO: We should raise a ParserError (skip this trial) if the missing markers are required...?
|
|
387
421
|
logger.warn(f"Frames {remaining_frames} are incomplete.")
|
|
388
422
|
|
|
389
423
|
return trim_start, trim_end
|
|
@@ -466,6 +500,11 @@ def get_static_rotation(frame_data):
|
|
|
466
500
|
raise ParserError("ASIS markers not found. Cannot determine static trial rotation.")
|
|
467
501
|
|
|
468
502
|
|
|
503
|
+
def rotate_trc_y_vertical(frame_data):
|
|
504
|
+
rotation_matrix = np.array([[1, 0, 0], [0, 0, 1], [0, -1, 0]])
|
|
505
|
+
rotate_trc_data(frame_data, rotation_matrix)
|
|
506
|
+
|
|
507
|
+
|
|
469
508
|
def rotate_trc_data(frame_data, rotation_matrix):
|
|
470
509
|
identity_matrix = np.eye(3)
|
|
471
510
|
if np.array_equal(rotation_matrix, identity_matrix):
|
|
@@ -477,6 +516,11 @@ def rotate_trc_data(frame_data, rotation_matrix):
|
|
|
477
516
|
)
|
|
478
517
|
|
|
479
518
|
|
|
519
|
+
def rotate_grf_y_vertical(analog_data):
|
|
520
|
+
rotation_matrix = np.array([[1, 0, 0], [0, 0, 1], [0, -1, 0]])
|
|
521
|
+
rotate_grf_data(analog_data, rotation_matrix)
|
|
522
|
+
|
|
523
|
+
|
|
480
524
|
def rotate_grf_data(analog_data, rotation_matrix):
|
|
481
525
|
identity_matrix = np.eye(3)
|
|
482
526
|
if np.array_equal(rotation_matrix, identity_matrix):
|
|
@@ -523,14 +567,21 @@ def extract_data(file_path, start_frame, end_frame):
|
|
|
523
567
|
if 'EVENT' not in reader:
|
|
524
568
|
raise ParserError("No events found in dynamic trial.")
|
|
525
569
|
|
|
570
|
+
def get_metadata(object, key):
|
|
571
|
+
value = object.get(key)
|
|
572
|
+
if value is None:
|
|
573
|
+
raise ParserError(f"Missing required metadata: {key}. Skipping trial.")
|
|
574
|
+
return value
|
|
575
|
+
|
|
526
576
|
# Extract analog data
|
|
527
577
|
time_increment = 1 / reader.analog_rate
|
|
528
578
|
start = (start_frame - 1) / reader.point_rate
|
|
529
|
-
|
|
530
|
-
|
|
579
|
+
stop_analog = end_frame / reader.point_rate - (time_increment / 2)
|
|
580
|
+
stop_marker = (end_frame - 1) / reader.point_rate
|
|
581
|
+
times = np.arange(start, stop_analog, time_increment).tolist()
|
|
531
582
|
analog_data = {'time': times}
|
|
532
583
|
|
|
533
|
-
labels = reader
|
|
584
|
+
labels = get_metadata(reader, 'ANALOG:LABELS').string_array
|
|
534
585
|
analog_data.update({label: [] for label in labels})
|
|
535
586
|
for i, points, analog in reader.read_frames():
|
|
536
587
|
if i < start_frame or end_frame < i:
|
|
@@ -543,11 +594,11 @@ def extract_data(file_path, start_frame, end_frame):
|
|
|
543
594
|
analog_data = pd.DataFrame(analog_data)
|
|
544
595
|
|
|
545
596
|
# Extract event information.
|
|
546
|
-
event_group = reader
|
|
547
|
-
event_count = event_group
|
|
548
|
-
contexts = event_group
|
|
549
|
-
labels = event_group
|
|
550
|
-
times = event_group
|
|
597
|
+
event_group = get_metadata(reader, 'EVENT')
|
|
598
|
+
event_count = get_metadata(event_group, 'USED').int8_value
|
|
599
|
+
contexts = get_metadata(event_group, 'CONTEXTS').string_array
|
|
600
|
+
labels = get_metadata(event_group, 'LABELS').string_array
|
|
601
|
+
times = get_metadata(event_group, 'TIMES').float_array
|
|
551
602
|
events = {'Left': {}, 'Right': {}}
|
|
552
603
|
|
|
553
604
|
for i in range(event_count):
|
|
@@ -555,6 +606,7 @@ def extract_data(file_path, start_frame, end_frame):
|
|
|
555
606
|
if foot:
|
|
556
607
|
label = labels[i].strip()
|
|
557
608
|
event_time = times[i][1]
|
|
609
|
+
event_time = round(float(event_time), 4)
|
|
558
610
|
events[foot][event_time] = label
|
|
559
611
|
if not any(events.values()):
|
|
560
612
|
raise ParserError("Event context (side) missing.")
|
|
@@ -580,23 +632,31 @@ def extract_data(file_path, start_frame, end_frame):
|
|
|
580
632
|
for foot, events in annotated_events.items():
|
|
581
633
|
for stride_number, stride_events in events.items():
|
|
582
634
|
for event_time, event_type in stride_events.items():
|
|
583
|
-
if start
|
|
635
|
+
if start <= event_time <= stop_marker:
|
|
584
636
|
if stride_number not in trimmed_events[foot]:
|
|
585
637
|
trimmed_events[foot][stride_number] = {}
|
|
586
638
|
trimmed_events[foot][stride_number][event_time] = event_type
|
|
587
639
|
else:
|
|
588
640
|
logger.warn(f"Event at {event_time}s is outside the trial's valid range "
|
|
589
|
-
f"of time stamps ({start}s - {
|
|
641
|
+
f"of time stamps ({start}s - {stop_marker}s).")
|
|
590
642
|
|
|
591
643
|
# Get number of force plates.
|
|
592
|
-
plate_count = reader
|
|
644
|
+
plate_count = get_metadata(reader, 'FORCE_PLATFORM:USED').int8_value
|
|
593
645
|
|
|
594
646
|
# Rotate GRF data to align with global CS.
|
|
595
|
-
corners = reader
|
|
647
|
+
corners = get_metadata(reader, 'FORCE_PLATFORM:CORNERS').float_array
|
|
596
648
|
|
|
597
649
|
return analog_data, reader.analog_rate, trimmed_events, plate_count, corners
|
|
598
650
|
|
|
599
651
|
|
|
652
|
+
# TODO: Georgio asked if we could maybe provide a way to approximate some of these values (from markers),
|
|
653
|
+
# if they haven't measured them.
|
|
654
|
+
# ...
|
|
655
|
+
# Thor suggests taking this value from the model.
|
|
656
|
+
# ...
|
|
657
|
+
# Discuss this with Elyse to see if she's Ok with this/
|
|
658
|
+
# If she can get the other labs to agree/
|
|
659
|
+
# See how each lab measures this.
|
|
600
660
|
def extract_static_data(file_path):
|
|
601
661
|
with open(file_path, 'rb') as handle:
|
|
602
662
|
reader = c3d.Reader(handle)
|
|
@@ -897,6 +957,8 @@ def scale_grf_data(analog_data):
|
|
|
897
957
|
analog_data[columns] = analog_data[columns] / 1000
|
|
898
958
|
|
|
899
959
|
|
|
960
|
+
# TODO: This incorrectly classifies Adelaide's static trials as dynamic.
|
|
961
|
+
# Use `calculate_distance_covered`...?
|
|
900
962
|
def is_dynamic(file_path):
|
|
901
963
|
with open(file_path, 'rb') as handle:
|
|
902
964
|
try:
|
|
@@ -968,6 +1030,9 @@ def normalise_grf_data(data, events):
|
|
|
968
1030
|
data_segment = force_data.iloc[start:frame, 1:]
|
|
969
1031
|
start = None if event_plate is None else frame
|
|
970
1032
|
|
|
1033
|
+
# TODO: These need fixed!!!
|
|
1034
|
+
# To align with new orientation.
|
|
1035
|
+
# NOW!
|
|
971
1036
|
# Perform side-specific transformations.
|
|
972
1037
|
if "ground_force_vy" in data_segment.columns:
|
|
973
1038
|
data_segment["ground_force_vy"] = -data_segment["ground_force_vy"]
|
|
@@ -1018,9 +1083,8 @@ def normalise_kinematics(kinematic_data, events):
|
|
|
1018
1083
|
if foot == "Left":
|
|
1019
1084
|
data_segment["pelvis_rotation"] = -data_segment["pelvis_rotation"]
|
|
1020
1085
|
if foot == "Right":
|
|
1021
|
-
data_segment["pelvis_list"] = -
|
|
1086
|
+
data_segment["pelvis_list"] = -data_segment["pelvis_list"]
|
|
1022
1087
|
data_segment["pelvis_tilt"] = -data_segment["pelvis_tilt"]
|
|
1023
|
-
data_segment["pelvis_list"] -= 90
|
|
1024
1088
|
|
|
1025
1089
|
normalised_data[foot][file_name][stride_number - 1] = data_segment.values.T
|
|
1026
1090
|
start = data[data['time'] <= event_time].index[-1]
|
|
@@ -1189,7 +1253,7 @@ def calculate_spatiotemporal_data(frame_data, events, static_data):
|
|
|
1189
1253
|
step_length = heel_coordinates[0] - previous_coordinates[0] # type: ignore
|
|
1190
1254
|
step_lengths[opposite_foot][stride_numbers[opposite_foot]] = step_length / 1000
|
|
1191
1255
|
|
|
1192
|
-
step_width = abs(heel_coordinates[
|
|
1256
|
+
step_width = abs(heel_coordinates[2] - previous_coordinates[2]) # type: ignore
|
|
1193
1257
|
step_widths[opposite_foot][stride_numbers[opposite_foot]] = step_width / 1000
|
|
1194
1258
|
|
|
1195
1259
|
# Calculate stance and swing phases.
|
|
@@ -1280,11 +1344,11 @@ def calculate_distance_covered(frame_data, start_time=None, end_time=None):
|
|
|
1280
1344
|
|
|
1281
1345
|
start_pos = frame_data.loc[start_frame, ['LASI', 'RASI']].mean()
|
|
1282
1346
|
end_pos = frame_data.loc[end_frame, ['LASI', 'RASI']].mean()
|
|
1283
|
-
walking_direction = end_pos[
|
|
1347
|
+
walking_direction = end_pos[[0, 2]] - start_pos[[0, 2]]
|
|
1284
1348
|
walking_direction /= np.linalg.norm(walking_direction)
|
|
1285
1349
|
|
|
1286
1350
|
distance_vector = end_pos - start_pos
|
|
1287
|
-
distance_travelled = np.dot(distance_vector[
|
|
1351
|
+
distance_travelled = np.dot(distance_vector[[0, 2]], walking_direction)
|
|
1288
1352
|
|
|
1289
1353
|
return distance_travelled
|
|
1290
1354
|
|
|
@@ -1292,7 +1356,7 @@ def calculate_distance_covered(frame_data, start_time=None, end_time=None):
|
|
|
1292
1356
|
def calculate_walking_direction(frame_data):
|
|
1293
1357
|
start_pos = frame_data[['LASI', 'RASI']].iloc[0].mean(axis=0)
|
|
1294
1358
|
end_pos = frame_data[['LASI', 'RASI']].iloc[-1].mean(axis=0)
|
|
1295
|
-
walking_direction = end_pos[
|
|
1359
|
+
walking_direction = end_pos[[0, 2]] - start_pos[[0, 2]]
|
|
1296
1360
|
walking_direction /= np.linalg.norm(walking_direction)
|
|
1297
1361
|
|
|
1298
1362
|
return walking_direction
|
|
@@ -1305,15 +1369,15 @@ def calculate_foot_progression_angles(frame_data):
|
|
|
1305
1369
|
foot_progression = pd.DataFrame()
|
|
1306
1370
|
for foot in ['Left', 'Right']:
|
|
1307
1371
|
side = foot[0]
|
|
1308
|
-
|
|
1309
|
-
|
|
1372
|
+
heel_xz = np.stack(frame_data[f"{side}HEE"].values)[:, [0, 2]]
|
|
1373
|
+
toe_xz = np.stack(frame_data[f"{side}TOE"].values)[:, [0, 2]]
|
|
1310
1374
|
|
|
1311
|
-
foot_vectors =
|
|
1375
|
+
foot_vectors = toe_xz - heel_xz
|
|
1312
1376
|
foot_unit_vectors = foot_vectors / np.linalg.norm(foot_vectors, axis=1, keepdims=True)
|
|
1313
1377
|
foot_angles = np.arctan2(foot_unit_vectors[:, 1], foot_unit_vectors[:, 0])
|
|
1314
1378
|
angles = np.degrees(foot_angles - walking_angle)
|
|
1315
1379
|
|
|
1316
|
-
if foot == '
|
|
1380
|
+
if foot == 'Right':
|
|
1317
1381
|
angles = -angles
|
|
1318
1382
|
|
|
1319
1383
|
foot_progression[f'foot_progression_{side.lower()}'] = angles
|
|
@@ -18,6 +18,11 @@ def perform_ik(osim_file, trc_file, output_file):
|
|
|
18
18
|
ik_tool.setModel(model)
|
|
19
19
|
ik_tool.setMarkerDataFileName(trc_file)
|
|
20
20
|
ik_tool.set_IKTaskSet(osim.IKTaskSet(IK_TASK_SET))
|
|
21
|
+
|
|
22
|
+
# TODO: Fix Teresa's OpenSim issue.
|
|
23
|
+
# ik_tool.setResultsDir(r"C:\Users\MyUser\AppData\Local\MyApp\results")
|
|
24
|
+
# ik_tool.setResultsDir(output_directory)
|
|
25
|
+
|
|
21
26
|
ik_tool.setOutputMotionFileName(output_file)
|
|
22
27
|
ik_tool.set_report_errors(False)
|
|
23
28
|
ik_tool.run()
|
|
@@ -34,10 +39,19 @@ def perform_id(osim_file, ik_file, grf_file, output_file):
|
|
|
34
39
|
id_tool = osim.InverseDynamicsTool()
|
|
35
40
|
id_tool.setModel(model)
|
|
36
41
|
id_tool.setCoordinatesFileName(ik_file)
|
|
37
|
-
|
|
42
|
+
|
|
43
|
+
# TODO: Temporarily disable.
|
|
44
|
+
# Now when I add this back in the ID_Tool crashes at run...
|
|
45
|
+
# id_tool.setExternalLoadsFileName(external_loads_file)
|
|
46
|
+
|
|
38
47
|
id_tool.setResultsDir(output_directory)
|
|
39
48
|
id_tool.setOutputGenForceFileName(output_file_name)
|
|
49
|
+
|
|
50
|
+
# TODO: We shouldn't need this since the data (IK results) are already filtered.
|
|
51
|
+
# TODO: We may be filtering multiple times and this may be related to the kinetic issues...?
|
|
40
52
|
id_tool.setLowpassCutoffFrequency(6)
|
|
53
|
+
# id_tool.setLowpassCutoffFrequency(4)
|
|
54
|
+
|
|
41
55
|
id_tool.setStartTime(time_values[0])
|
|
42
56
|
id_tool.setEndTime(time_values[-1])
|
|
43
57
|
id_tool.run()
|
{c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/osim_resources/external_loads_template.xml
RENAMED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<objects>
|
|
5
5
|
<ExternalForce name="externalforce_l">
|
|
6
6
|
<!--Name of the body the force is applied to.-->
|
|
7
|
-
<applied_to_body>
|
|
7
|
+
<applied_to_body>calcn_l</applied_to_body>
|
|
8
8
|
<!--Name of the body the force is expressed in (default is ground).-->
|
|
9
9
|
<force_expressed_in_body>ground</force_expressed_in_body>
|
|
10
10
|
<!--Name of the body the point is expressed in (default is ground).-->
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</ExternalForce>
|
|
21
21
|
<ExternalForce name="externalforce_r">
|
|
22
22
|
<!--Name of the body the force is applied to.-->
|
|
23
|
-
<applied_to_body>
|
|
23
|
+
<applied_to_body>calcn_r</applied_to_body>
|
|
24
24
|
<!--Name of the body the force is expressed in (default is ground).-->
|
|
25
25
|
<force_expressed_in_body>ground</force_expressed_in_body>
|
|
26
26
|
<!--Name of the body the point is expressed in (default is ground).-->
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
from PySide6.QtCore import Signal
|
|
4
|
+
from PySide6.QtWidgets import QDialog, QMessageBox
|
|
5
|
+
|
|
6
|
+
from c3d_parser.core.c3d_parser import ParserError
|
|
7
|
+
from c3d_parser.settings.general import get_marker_maps_dir
|
|
8
|
+
from c3d_parser.view.ui.ui_delete_marker_set_dialog import Ui_DeleteMarkerSetDialog
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DeleteMarkerSetDialog(QDialog):
|
|
12
|
+
error_occurred = Signal(Exception)
|
|
13
|
+
|
|
14
|
+
def __init__(self, parent=None):
|
|
15
|
+
super(DeleteMarkerSetDialog, self).__init__(parent)
|
|
16
|
+
self._ui = Ui_DeleteMarkerSetDialog()
|
|
17
|
+
self._ui.setupUi(self)
|
|
18
|
+
|
|
19
|
+
self._setup_combo_box()
|
|
20
|
+
|
|
21
|
+
self._make_connections()
|
|
22
|
+
|
|
23
|
+
def _make_connections(self):
|
|
24
|
+
self._ui.pushButtonDelete.clicked.connect(self._delete_marker_set)
|
|
25
|
+
|
|
26
|
+
def _setup_combo_box(self):
|
|
27
|
+
marker_maps_dir = get_marker_maps_dir()
|
|
28
|
+
labs = [os.path.splitext(lab)[0] for lab in os.listdir(marker_maps_dir)]
|
|
29
|
+
self._ui.comboBoxMarkerSet.clear()
|
|
30
|
+
self._ui.comboBoxMarkerSet.addItems(labs)
|
|
31
|
+
|
|
32
|
+
def _delete_marker_set(self):
|
|
33
|
+
marker_maps_dir = get_marker_maps_dir()
|
|
34
|
+
selected = self._ui.comboBoxMarkerSet.currentText()
|
|
35
|
+
|
|
36
|
+
reply = QMessageBox.question( self, "Confirm Deletion",
|
|
37
|
+
"Are you sure you want to permanently delete this marker set?",
|
|
38
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
39
|
+
QMessageBox.StandardButton.No)
|
|
40
|
+
if reply == QMessageBox.StandardButton.No:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
match = next((f for f in os.listdir(marker_maps_dir) if os.path.splitext(f)[0] == selected), None)
|
|
44
|
+
if match:
|
|
45
|
+
file_path = os.path.join(marker_maps_dir, match)
|
|
46
|
+
if os.path.isfile(file_path):
|
|
47
|
+
try:
|
|
48
|
+
os.remove(file_path)
|
|
49
|
+
self._ui.comboBoxMarkerSet.removeItem(self._ui.comboBoxMarkerSet.currentIndex())
|
|
50
|
+
except PermissionError:
|
|
51
|
+
e = ParserError(f"PermissionError: Could not delete marker set: {selected}")
|
|
52
|
+
self.error_occurred.emit(e)
|
|
@@ -33,6 +33,10 @@ class MarkerSetDialog(QtWidgets.QDialog):
|
|
|
33
33
|
self._ui.pushButtonImport.clicked.connect(self._import_marker_map)
|
|
34
34
|
self._ui.pushButtonSave.clicked.connect(self._validate_marker_set)
|
|
35
35
|
|
|
36
|
+
for marker in markers:
|
|
37
|
+
combo_box = getattr(self._ui, f"comboBox{marker}")
|
|
38
|
+
combo_box.currentTextChanged.connect(self.update_combo_box_style)
|
|
39
|
+
|
|
36
40
|
def set_marker_names(self, marker_names):
|
|
37
41
|
for marker in markers:
|
|
38
42
|
combo_box = getattr(self._ui, f"comboBox{marker}")
|
|
@@ -58,6 +62,14 @@ class MarkerSetDialog(QtWidgets.QDialog):
|
|
|
58
62
|
combo_box = getattr(self._ui, f"comboBox{key}")
|
|
59
63
|
combo_box.setCurrentText(value)
|
|
60
64
|
|
|
65
|
+
def update_combo_box_style(self, text):
|
|
66
|
+
combo_box = self.sender()
|
|
67
|
+
items = [combo_box.itemText(i) for i in range(combo_box.count())]
|
|
68
|
+
if text and text not in items:
|
|
69
|
+
combo_box.setStyleSheet(INVALID_STYLE_SHEET)
|
|
70
|
+
else:
|
|
71
|
+
combo_box.setStyleSheet(DEFAULT_STYLE_SHEET)
|
|
72
|
+
|
|
61
73
|
def _import_marker_map(self):
|
|
62
74
|
dlg = MarkerSetImportDialog(self)
|
|
63
75
|
if dlg.exec():
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from PySide6.QtWidgets import QDialog, QFileDialog
|
|
5
|
+
from PySide6.QtGui import QRegularExpressionValidator
|
|
6
|
+
from PySide6.QtCore import QRegularExpression
|
|
7
|
+
|
|
8
|
+
from c3d_parser.view.ui.ui_options_dialog import Ui_OptionsDialog
|
|
9
|
+
from c3d_parser.settings.general import DEFAULT_STYLE_SHEET, INVALID_STYLE_SHEET
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OptionsDialog(QDialog):
|
|
13
|
+
|
|
14
|
+
def __init__(self, parent=None):
|
|
15
|
+
super(OptionsDialog, self).__init__(parent)
|
|
16
|
+
self._ui = Ui_OptionsDialog()
|
|
17
|
+
self._ui.setupUi(self)
|
|
18
|
+
|
|
19
|
+
self._colour_boxes = {
|
|
20
|
+
self._ui.lineEditLeftColour: self._ui.labelLeftColour,
|
|
21
|
+
self._ui.lineEditRightColour: self._ui.labelRightColour,
|
|
22
|
+
self._ui.lineEditSelectionColour: self._ui.labelSelectionColour
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
self._setup_colour_settings()
|
|
26
|
+
self._disable_default_button_selection()
|
|
27
|
+
|
|
28
|
+
self._make_connections()
|
|
29
|
+
|
|
30
|
+
def _make_connections(self):
|
|
31
|
+
self._ui.lineEditRootInput.textChanged.connect(self._validate_data_directory)
|
|
32
|
+
self._ui.lineEditRootOutput.textChanged.connect(self._validate_data_directory)
|
|
33
|
+
self._ui.pushButtonRootInputChooser.clicked.connect(self._open_input_directory_chooser)
|
|
34
|
+
self._ui.pushButtonRootOutputChooser.clicked.connect(self._open_output_directory_chooser)
|
|
35
|
+
|
|
36
|
+
def _setup_colour_settings(self):
|
|
37
|
+
self._validator = QRegularExpressionValidator(QRegularExpression("^#[0-9A-Fa-f]{6}$"))
|
|
38
|
+
self._ui.lineEditLeftColour.setValidator(self._validator)
|
|
39
|
+
self._ui.lineEditLeftColour.textChanged.connect(self._colour_code_changed)
|
|
40
|
+
self._ui.lineEditRightColour.setValidator(self._validator)
|
|
41
|
+
self._ui.lineEditRightColour.textChanged.connect(self._colour_code_changed)
|
|
42
|
+
self._ui.lineEditSelectionColour.setValidator(self._validator)
|
|
43
|
+
self._ui.lineEditSelectionColour.textChanged.connect(self._colour_code_changed)
|
|
44
|
+
|
|
45
|
+
def _colour_code_changed(self, text):
|
|
46
|
+
line_edit = self.sender()
|
|
47
|
+
state, _, _ = self._validator.validate(text, 0)
|
|
48
|
+
if state == QRegularExpressionValidator.State.Acceptable:
|
|
49
|
+
line_edit.setStyleSheet(DEFAULT_STYLE_SHEET)
|
|
50
|
+
colour_box = self._colour_boxes[line_edit]
|
|
51
|
+
colour_box.setStyleSheet(f"border: 1px solid black;"
|
|
52
|
+
f"border-radius: 2px; background-color: {text};")
|
|
53
|
+
else:
|
|
54
|
+
line_edit.setStyleSheet(INVALID_STYLE_SHEET)
|
|
55
|
+
|
|
56
|
+
def _disable_default_button_selection(self):
|
|
57
|
+
self._ui.pushButtonOK.setAutoDefault(False)
|
|
58
|
+
self._ui.pushButtonCancel.setAutoDefault(False)
|
|
59
|
+
self._ui.pushButtonRootInputChooser.setAutoDefault(False)
|
|
60
|
+
self._ui.pushButtonRootOutputChooser.setAutoDefault(False)
|
|
61
|
+
|
|
62
|
+
def load(self, options):
|
|
63
|
+
self._ui.doubleSpinBoxLineWidth.setValue(options['line_width'])
|
|
64
|
+
self._ui.lineEditRootInput.setText(options['input_data_directory'])
|
|
65
|
+
self._ui.lineEditRootOutput.setText(options['output_data_directory'])
|
|
66
|
+
self._ui.checkBoxOptimiseKneeAxis.setChecked(options['optimise_knee_axis'])
|
|
67
|
+
self._ui.lineEditLeftColour.setText(options['colour_left'])
|
|
68
|
+
self._ui.lineEditRightColour.setText(options['colour_right'])
|
|
69
|
+
self._ui.lineEditSelectionColour.setText(options['colour_selection'])
|
|
70
|
+
|
|
71
|
+
def save(self):
|
|
72
|
+
options = {
|
|
73
|
+
'line_width': self._ui.doubleSpinBoxLineWidth.value(),
|
|
74
|
+
'input_data_directory': self._ui.lineEditRootInput.text(),
|
|
75
|
+
'output_data_directory': self._ui.lineEditRootOutput.text(),
|
|
76
|
+
'optimise_knee_axis': self._ui.checkBoxOptimiseKneeAxis.isChecked(),
|
|
77
|
+
'colour_left': self._ui.lineEditLeftColour.text(),
|
|
78
|
+
'colour_right': self._ui.lineEditRightColour.text(),
|
|
79
|
+
'colour_selection': self._ui.lineEditSelectionColour.text()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return options
|
|
83
|
+
|
|
84
|
+
def _validate_data_directory(self):
|
|
85
|
+
line_edit = self.sender()
|
|
86
|
+
directory = line_edit.text()
|
|
87
|
+
directory_valid = len(directory) and os.path.isdir(directory)
|
|
88
|
+
line_edit.setStyleSheet(DEFAULT_STYLE_SHEET if directory_valid else INVALID_STYLE_SHEET)
|
|
89
|
+
|
|
90
|
+
def _open_input_directory_chooser(self):
|
|
91
|
+
self._open_directory_chooser(self._ui.lineEditRootInput)
|
|
92
|
+
|
|
93
|
+
def _open_output_directory_chooser(self):
|
|
94
|
+
self._open_directory_chooser(self._ui.lineEditRootOutput)
|
|
95
|
+
|
|
96
|
+
def _open_directory_chooser(self, line_edit):
|
|
97
|
+
current_directory = line_edit.text()
|
|
98
|
+
if not os.path.isdir(current_directory):
|
|
99
|
+
current_directory = ''
|
|
100
|
+
|
|
101
|
+
directory = QFileDialog.getExistingDirectory(
|
|
102
|
+
self, 'Select Directory', current_directory)
|
|
103
|
+
|
|
104
|
+
if directory:
|
|
105
|
+
line_edit.setText(directory)
|