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.
Files changed (49) hide show
  1. {c3d_parser-0.3.4/src/c3d_parser.egg-info → c3d_parser-0.5.0}/PKG-INFO +4 -2
  2. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/requirements.txt +3 -1
  3. c3d_parser-0.5.0/src/c3d_parser/__init__.py +2 -0
  4. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/c3d_parser.py +89 -25
  5. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/osim.py +15 -1
  6. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/osim_resources/external_loads_template.xml +2 -2
  7. c3d_parser-0.5.0/src/c3d_parser/view/dialogs/delete_marker_set_dialog.py +52 -0
  8. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/dialogs/marker_set_dialog.py +12 -0
  9. c3d_parser-0.5.0/src/c3d_parser/view/dialogs/options_dialog.py +105 -0
  10. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/main_window.py +176 -34
  11. c3d_parser-0.5.0/src/c3d_parser/view/ui/ui_delete_marker_set_dialog.py +87 -0
  12. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/ui/ui_main_window.py +11 -0
  13. c3d_parser-0.5.0/src/c3d_parser/view/ui/ui_options_dialog.py +248 -0
  14. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/widgets.py +4 -1
  15. {c3d_parser-0.3.4 → c3d_parser-0.5.0/src/c3d_parser.egg-info}/PKG-INFO +4 -2
  16. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser.egg-info/SOURCES.txt +2 -0
  17. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser.egg-info/requires.txt +3 -1
  18. c3d_parser-0.3.4/src/c3d_parser/__init__.py +0 -2
  19. c3d_parser-0.3.4/src/c3d_parser/view/dialogs/options_dialog.py +0 -62
  20. c3d_parser-0.3.4/src/c3d_parser/view/ui/ui_options_dialog.py +0 -136
  21. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/LICENSE +0 -0
  22. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/README.md +0 -0
  23. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/pyproject.toml +0 -0
  24. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/setup.cfg +0 -0
  25. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/application.py +0 -0
  26. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/c3d_patch.py +0 -0
  27. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/osim_resources/ik_task_set.xml +0 -0
  28. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/core/utils.py +0 -0
  29. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/general.py +0 -0
  30. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/logging.py +0 -0
  31. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/AUC.json +0 -0
  32. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/FMC.json +0 -0
  33. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/MH.json +0 -0
  34. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/QCMAS.json +0 -0
  35. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/RBWH.json +0 -0
  36. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/RCH.json +0 -0
  37. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/settings/marker_maps/Sydney.json +0 -0
  38. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/splash_rc.py +0 -0
  39. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/splashscreen.py +0 -0
  40. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/dialogs/about_dialog.py +0 -0
  41. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/dialogs/marker_set_import_dialog.py +0 -0
  42. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/ui/resources_rc.py +0 -0
  43. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/ui/ui_marker_set_dialog.py +0 -0
  44. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/ui/ui_marker_set_import_dialog.py +0 -0
  45. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser/view/utils.py +0 -0
  46. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser.egg-info/dependency_links.txt +0 -0
  47. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser.egg-info/entry_points.txt +0 -0
  48. {c3d_parser-0.3.4 → c3d_parser-0.5.0}/src/c3d_parser.egg-info/top_level.txt +0 -0
  49. {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.4
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.1.19
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
@@ -3,7 +3,9 @@ numpy==1.26.4
3
3
  pandas
4
4
  c3d==0.5.2
5
5
  trc-data-reader
6
- opensim-model-creator>=0.1.19
6
+ opensim-model-creator>=0.3.0
7
7
  PySide6
8
8
  matplotlib
9
9
  mplcursors
10
+ pyvistaqt
11
+ ll-visualiser>=0.3.0
@@ -0,0 +1,2 @@
1
+
2
+ __version__ = "0.5.0"
@@ -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 dynamic_trials:
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
- missing_markers.append(frame_data.columns[marker_index])
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
- stop = end_frame / reader.point_rate - (time_increment / 2)
530
- times = np.arange(start, stop, time_increment).tolist()
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.get('ANALOG:LABELS').string_array
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.get('EVENT')
547
- event_count = event_group.get('USED').int8_value
548
- contexts = event_group.get('CONTEXTS').string_array
549
- labels = event_group.get('LABELS').string_array
550
- times = event_group.get('TIMES').float_array
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 < event_time < stop:
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 - {stop}s).")
641
+ f"of time stamps ({start}s - {stop_marker}s).")
590
642
 
591
643
  # Get number of force plates.
592
- plate_count = reader.get('FORCE_PLATFORM:USED').int8_value
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.get('FORCE_PLATFORM:CORNERS').float_array
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"] = -(data_segment["pelvis_list"] - 180)
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[1] - previous_coordinates[1]) # type: ignore
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[:2] - start_pos[:2]
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[:2], walking_direction)
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[:2] - start_pos[:2]
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
- heel_xy = np.stack(frame_data[f"{side}HEE"].values)[:, :2]
1309
- toe_xy = np.stack(frame_data[f"{side}TOE"].values)[:, :2]
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 = toe_xy - heel_xy
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 == 'Left':
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
- id_tool.setExternalLoadsFileName(external_loads_file)
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()
@@ -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>calcn_l_b</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>calcn_r_b</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)