c3d-parser 1.3.0__tar.gz → 1.4.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 (47) hide show
  1. {c3d_parser-1.3.0/src/c3d_parser.egg-info → c3d_parser-1.4.0}/PKG-INFO +1 -1
  2. c3d_parser-1.4.0/src/c3d_parser/__init__.py +2 -0
  3. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/core/c3d_parser.py +50 -21
  4. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/main_window.py +33 -29
  5. {c3d_parser-1.3.0 → c3d_parser-1.4.0/src/c3d_parser.egg-info}/PKG-INFO +1 -1
  6. c3d_parser-1.3.0/src/c3d_parser/__init__.py +0 -2
  7. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/LICENSE +0 -0
  8. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/README.md +0 -0
  9. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/pyproject.toml +0 -0
  10. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/requirements.txt +0 -0
  11. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/setup.cfg +0 -0
  12. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/application.py +0 -0
  13. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/core/c3d_patch.py +0 -0
  14. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/core/osim.py +0 -0
  15. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/core/osim_resources/external_loads_template.xml +0 -0
  16. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/core/osim_resources/ik_task_set.xml +0 -0
  17. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/core/utils.py +0 -0
  18. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/settings/general.py +0 -0
  19. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/settings/logging.py +0 -0
  20. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/settings/marker_maps/AUC.json +0 -0
  21. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/settings/marker_maps/FMC.json +0 -0
  22. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/settings/marker_maps/MH.json +0 -0
  23. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/settings/marker_maps/QCMAS.json +0 -0
  24. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/settings/marker_maps/RBWH.json +0 -0
  25. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/settings/marker_maps/RCH.json +0 -0
  26. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/settings/marker_maps/Sydney.json +0 -0
  27. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/splash_rc.py +0 -0
  28. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/splashscreen.py +0 -0
  29. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/dialogs/about_dialog.py +0 -0
  30. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/dialogs/delete_marker_set_dialog.py +0 -0
  31. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/dialogs/marker_set_dialog.py +0 -0
  32. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/dialogs/marker_set_import_dialog.py +0 -0
  33. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/dialogs/options_dialog.py +0 -0
  34. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/ui/resources_rc.py +0 -0
  35. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/ui/ui_delete_marker_set_dialog.py +0 -0
  36. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/ui/ui_main_window.py +0 -0
  37. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/ui/ui_marker_set_dialog.py +0 -0
  38. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/ui/ui_marker_set_import_dialog.py +0 -0
  39. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/ui/ui_options_dialog.py +0 -0
  40. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/utils.py +0 -0
  41. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser/view/widgets.py +0 -0
  42. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser.egg-info/SOURCES.txt +0 -0
  43. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser.egg-info/dependency_links.txt +0 -0
  44. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser.egg-info/entry_points.txt +0 -0
  45. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser.egg-info/requires.txt +0 -0
  46. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/src/c3d_parser.egg-info/top_level.txt +0 -0
  47. {c3d_parser-1.3.0 → c3d_parser-1.4.0}/tests/test_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c3d-parser
3
- Version: 1.3.0
3
+ Version: 1.4.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
@@ -0,0 +1,2 @@
1
+
2
+ __version__ = "1.4.0"
@@ -16,6 +16,7 @@ from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR
16
16
 
17
17
  from trc import TRCData
18
18
  from opensim_model_creator.Create_Model import create_model
19
+ from ll_visualiser.utils import get_fit_metrics, load_landmarks, define_measurements
19
20
 
20
21
  from c3d_parser.core.c3d_patch import c3d
21
22
  from c3d_parser.core.utils import clear_directory
@@ -82,13 +83,13 @@ def parse_session(static_trial, dynamic_trials, input_directory, output_director
82
83
  grf_file_paths[trial] = grf_file_path
83
84
  deidentified_file_names[trial] = os.path.basename(trc_file_path).rsplit(".", 1)[0]
84
85
 
85
- write_c3d_parser_history(input_directory, static_trial, deidentified_file_names)
86
-
87
86
  logger.info("Fitting shape model.")
88
87
  dynamic_trc_path = list(trc_file_paths.values())[0] if trc_file_paths else ""
89
88
  osim_model = create_osim_model(static_trc_path, dynamic_trc_path, frame, marker_diameter, static_data,
90
89
  output_directory, left_foot_flat, right_foot_flat, optimise_knee_axis, progress_tracker)
91
90
 
91
+ write_c3d_parser_history(input_directory, output_directory, static_trial, deidentified_file_names, static_data)
92
+
92
93
  if not trc_file_paths:
93
94
  raise CancelException("No dynamic trials found.")
94
95
 
@@ -236,7 +237,7 @@ def parse_dynamic_trial(c3d_file, lab, output_directory, trial_index, marker_dat
236
237
  return analog_data, events, s_t_data, trc_file_path, grf_file_path
237
238
 
238
239
 
239
- def write_c3d_parser_history(input_directory, static_trial, deidentified_file_names):
240
+ def write_c3d_parser_history(input_directory, output_directory, static_trial, deidentified_file_names, static_data):
240
241
  log_path = os.path.join(input_directory, "c3d_parser_history.log")
241
242
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
242
243
 
@@ -251,11 +252,56 @@ def write_c3d_parser_history(input_directory, static_trial, deidentified_file_na
251
252
  f.write("Deidentified file names:\n")
252
253
  for original_name, updated_name in deidentified_file_names.items():
253
254
  f.write(f"{original_name}: {updated_name}\n")
255
+ f.write("\n")
256
+
257
+ if static_data:
258
+ units = {
259
+ "Height": "mm",
260
+ "Weight": "kg",
261
+ "ASIS Width": "mm",
262
+ "Left Knee Width": "mm",
263
+ "Right Knee Width": "mm",
264
+ "Left Ankle Width": "mm",
265
+ "Right Ankle Width": "mm",
266
+ "Left Leg Length": "mm",
267
+ "Right Leg Length": "mm",
268
+ }
269
+ f.write("Subject Information:\n")
270
+ for key, value in static_data.items():
271
+ label = "Body Mass" if key == "Weight" else key
272
+ unit = f" {units[key]}" if key in units else ""
273
+ f.write(f"{label}: {value}{unit}\n")
274
+
275
+ metrics, measurements = define_metrics_and_measurements(output_directory)
276
+ if metrics:
277
+ f.write("\n")
278
+ f.write("ASM Fit Metrics:\n")
279
+ for key, value in metrics.items():
280
+ f.write(f"{key}: {value} mm\n")
281
+ if measurements:
282
+ f.write("\n")
283
+ f.write("Bone Surface Measurements:\n")
284
+ for key, value in measurements.items():
285
+ f.write(f"{key}: {value} mm\n")
254
286
 
255
287
  except (OSError, IOError):
256
288
  raise ParserError("Could not write c3d_parser_history.log")
257
289
 
258
290
 
291
+ def define_metrics_and_measurements(output_directory):
292
+ model_directory = os.path.join(output_directory, "Models", "Meshes")
293
+ left_predicted_landmark_file = os.path.join(model_directory, 'predicted_lms_left.txt')
294
+ right_predicted_landmark_file = os.path.join(model_directory, 'predicted_lms_right.txt')
295
+
296
+ left_predicted_landmarks = load_landmarks(left_predicted_landmark_file)
297
+ right_predicted_landmarks = load_landmarks(right_predicted_landmark_file)
298
+
299
+ metrics = get_fit_metrics(model_directory)
300
+ measurements = define_measurements(left_predicted_landmarks, right_predicted_landmarks)
301
+
302
+ return metrics, measurements
303
+
304
+
259
305
  def write_event_data(events, file_name, output_directory):
260
306
  event_directory = os.path.join(output_directory, "events")
261
307
  if not os.path.exists(event_directory):
@@ -421,8 +467,6 @@ def harmonise_markers(frame_data, lab, required_markers):
421
467
  if None in frame_data.columns:
422
468
  frame_data.drop(columns=frame_data.columns[frame_data.columns.isna()], inplace=True)
423
469
 
424
- # TODO: This error window does not indicate which trial has the issue.
425
- # Update this.
426
470
  # Ensure required markers are present.
427
471
  available = set(frame_data.columns)
428
472
  for item in required_markers:
@@ -478,10 +522,8 @@ def trim_frames(frame_data):
478
522
  if not drop_frames.empty:
479
523
  frame_data.drop(drop_frames, inplace=True)
480
524
 
481
- # TODO: If we convert this to a dictionary we can keep the markers that are missing and check if they are required.
482
525
  remaining_frames = [frame for frame in incomplete_frames.keys() if trim_start <= frame <= trim_end]
483
526
  if remaining_frames:
484
- # TODO: We should raise a ParserError (skip this trial) if the missing markers are required...?
485
527
  logger.warn(f"Frames {remaining_frames} are incomplete.")
486
528
 
487
529
  return trim_start, trim_end
@@ -1045,8 +1087,6 @@ def scale_grf_data(analog_data):
1045
1087
  analog_data[columns] = analog_data[columns] / 1000
1046
1088
 
1047
1089
 
1048
- # TODO: This incorrectly classifies Adelaide's static trials as dynamic.
1049
- # Use `calculate_distance_covered`...?
1050
1090
  def is_dynamic(file_path):
1051
1091
  with open(file_path, 'rb') as handle:
1052
1092
  try:
@@ -1222,11 +1262,6 @@ def normalise_kinetics(kinetic_data, events):
1222
1262
  normalised_data[foot][file_name] = {}
1223
1263
  data_segment = data.iloc[start:frame, 1:]
1224
1264
 
1225
- # TODO: If the model +tve and -tve are wrong, then we can't just flip these.
1226
- # Because the non-GRF parts of the moments would also be flipped.
1227
- # Just visually check the results against some clinical kinetics.
1228
- # ...
1229
- # Looks pretty good so far.
1230
1265
  # Perform sign transformations.
1231
1266
  data_segment[f"hip_flexion_{side}_moment"] = -data_segment[f"hip_flexion_{side}_moment"]
1232
1267
  data_segment[f"hip_adduction_{side}_moment"] = -data_segment[f"hip_adduction_{side}_moment"]
@@ -1250,13 +1285,12 @@ def normalise_kinetics(kinetic_data, events):
1250
1285
  return normalised_data
1251
1286
 
1252
1287
 
1253
- def write_normalised_grfs(grf_data, selected_trials, output_directory):
1288
+ def write_normalised_grfs(grf_data, selected_trials, excluded_cycles, output_directory):
1254
1289
  normalised_directory = os.path.join(output_directory, 'normalised')
1255
1290
  if not os.path.exists(normalised_directory):
1256
1291
  os.makedirs(normalised_directory)
1257
1292
  output_file = os.path.join(normalised_directory, f"combined_grf.csv")
1258
1293
  columns = ['anterior_posterior', 'medial_lateral', 'vertical']
1259
- excluded_cycles = set()
1260
1294
  write_normalised_data(grf_data, columns, selected_trials, excluded_cycles, output_file)
1261
1295
 
1262
1296
 
@@ -1314,9 +1348,6 @@ def write_normalised_data(data, column_names, selected_trials, excluded_cycles,
1314
1348
  file.write('\n')
1315
1349
 
1316
1350
 
1317
- # TODO: Rework this function to be less sequential.
1318
- # It should be able to calculate metrics for walking and running gait.
1319
- # Put this at the start of the sequential bit.
1320
1351
  def calculate_spatiotemporal_data(frame_data, events, static_data):
1321
1352
  stride_lengths = {"Left": {}, "Right": {}}
1322
1353
  step_lengths = {"Left": {}, "Right": {}}
@@ -1339,8 +1370,6 @@ def calculate_spatiotemporal_data(frame_data, events, static_data):
1339
1370
  for event_time, (event_type, event_plate) in stride_events.items():
1340
1371
  time_ordered_events[event_time][foot] = [event_type, stride_number]
1341
1372
 
1342
- # TODO: We should be able to use this to calculate ALL measurements non-sequentially.
1343
- # TODO: Double check that we don't have situations where the events are not ordered in increasing stride number.
1344
1373
  # Add terminal foot-strike to events for each cycle.
1345
1374
  full_cycle_events = copy.deepcopy(events)
1346
1375
  for foot, foot_events in full_cycle_events.items():
@@ -57,7 +57,6 @@ class ProgressTracker(QObject):
57
57
  progress = Signal(str, str)
58
58
 
59
59
 
60
- # TODO: CGM2 event detector on GitHub...?
61
60
  class MainWindow(QMainWindow):
62
61
 
63
62
  def __init__(self):
@@ -72,7 +71,7 @@ class MainWindow(QMainWindow):
72
71
  self._line_width = 1.0
73
72
  self._input_data_directory = ''
74
73
  self._output_data_directory = ''
75
- self._optimise_knee_axis = True
74
+ self._optimise_knee_axis = False
76
75
  self._filter_trc = True
77
76
  self._filter_grf = True
78
77
  self._use_custom_ik_task_set = False
@@ -112,6 +111,13 @@ class MainWindow(QMainWindow):
112
111
  self._kinetic_curves = GaitCurves(self._kinetic_canvas, colours)
113
112
  self._boxes = None
114
113
 
114
+ self._kinematic_curves.signals.cycles_excluded.connect(self._kinetic_curves.exclude_cycles)
115
+ self._kinematic_curves.signals.cycles_excluded.connect(self._grf_curves.exclude_cycles)
116
+ self._kinematic_curves.signals.cycles_included.connect(self._kinetic_curves.include_cycles)
117
+ self._kinematic_curves.signals.cycles_included.connect(self._grf_curves.include_cycles)
118
+ self._kinetic_curves.signals.cycles_excluded.connect(self._grf_curves.exclude_cycles)
119
+ self._kinetic_curves.signals.cycles_included.connect(self._grf_curves.include_cycles)
120
+
115
121
  def _setup_progress_bar(self):
116
122
  self._progress_text = ""
117
123
  self._progress_value = 0
@@ -248,12 +254,6 @@ class MainWindow(QMainWindow):
248
254
  table.deleteLater()
249
255
  self._spatiotemporal_tables = []
250
256
 
251
- # TODO: The legend is not correctly displayed if the ST tab is open while processing...
252
- # Repositioning and updating the legend do nothing.
253
-
254
- # TODO: The border is still having issues.
255
- # Seems to be related to the area being scrollable.
256
- # Only happens on some PCs/monitors...
257
257
  def _visualise_spatiotemporal_data(self):
258
258
 
259
259
  # Temporarily disable spatio-temporal visualisation for running gait.
@@ -684,8 +684,9 @@ class MainWindow(QMainWindow):
684
684
  selected_trials = {key: value for key, value in self._deidentified_file_names.items() if key in selected}
685
685
  kinematic_exclusions = self._kinematic_curves.get_excluded_cycles()
686
686
  kinetic_exclusions = self._kinetic_curves.get_excluded_cycles()
687
+ grf_exclusions = self._grf_curves.get_excluded_cycles()
687
688
  self._update_s_t_data_from_tables()
688
- write_normalised_grfs(self._grf_data, selected_trials, self._output_directory)
689
+ write_normalised_grfs(self._grf_data, selected_trials, grf_exclusions, self._output_directory)
689
690
  write_normalised_kinematics(self._kinematic_data, selected_trials, kinematic_exclusions, self._output_directory)
690
691
  write_normalised_kinetics(self._kinetic_data, selected_trials, kinetic_exclusions, self._output_directory)
691
692
  write_spatiotemporal_data(self._s_t_data, selected_trials, self._output_directory)
@@ -887,7 +888,6 @@ class MainWindow(QMainWindow):
887
888
  self._update_s_t_colours()
888
889
  self._update_legend()
889
890
 
890
- # TODO: For some reason the legend doesn't get created at all if we are visualising the ST tab while processing...
891
891
  def _update_legend(self):
892
892
  if self._boxes:
893
893
  for side, boxes in self._boxes.items():
@@ -979,8 +979,6 @@ class MainWindow(QMainWindow):
979
979
  settings.beginGroup('MainWindow')
980
980
  settings.setValue('size', self.size())
981
981
  settings.setValue('pos', self.pos())
982
- # TODO: The taskbar icon fails if the application opens in full-screen...
983
- # DON'T open in full screen...?
984
982
  settings.setValue('is_maximized', self.isMaximized())
985
983
  settings.setValue('lab', self._ui.comboBoxLab.currentText())
986
984
  settings.setValue('marker_diameter', self._ui.doubleSpinBoxMarkerDiameter.value())
@@ -1098,6 +1096,11 @@ class MainWindow(QMainWindow):
1098
1096
  curves.exclude_side(file_name, "Right")
1099
1097
 
1100
1098
 
1099
+ class CurveSignals(QObject):
1100
+ cycles_included = Signal(list)
1101
+ cycles_excluded = Signal(list)
1102
+
1103
+
1101
1104
  class GaitCurves(defaultdict):
1102
1105
 
1103
1106
  def __init__(self, canvas, colours):
@@ -1107,6 +1110,8 @@ class GaitCurves(defaultdict):
1107
1110
  self._canvas.mpl_connect('pick_event', lambda event: self.toggle_cycle(event))
1108
1111
  self._canvas.mpl_connect('button_press_event', lambda event: self.show_curve_menu(event))
1109
1112
 
1113
+ self.signals = CurveSignals()
1114
+
1110
1115
  self._selected_curves = []
1111
1116
  self._excluded_cycles = set()
1112
1117
 
@@ -1173,9 +1178,6 @@ class GaitCurves(defaultdict):
1173
1178
  line.set_color(colour)
1174
1179
  line.set_zorder(z_order)
1175
1180
 
1176
- # TODO: Replace all these with `draw_idle`?
1177
- # Just the ones that handle many curves...?
1178
- # Probably all...?
1179
1181
  self._canvas.draw()
1180
1182
 
1181
1183
  def show_curve_menu(self, event):
@@ -1201,15 +1203,16 @@ class GaitCurves(defaultdict):
1201
1203
  context_menu.exec_(self._canvas.mapToGlobal(point))
1202
1204
 
1203
1205
  def include_curves(self):
1204
- self._excluded_cycles.difference_update(self._selected_curves)
1205
- for file_name, cycle in self._selected_curves:
1206
- colour = self._colour_left if "Left" in cycle else self._colour_right
1207
- for line in self[file_name][cycle]:
1208
- line.set_linestyle('solid')
1209
- line.set_color(colour)
1210
- line.set_zorder(2)
1206
+ identifiers = list(self._selected_curves)
1207
+ self.include_cycles(identifiers)
1211
1208
  self._selected_curves = []
1212
1209
 
1210
+ self.signals.cycles_included.emit(identifiers)
1211
+
1212
+ def include_cycles(self, identifiers):
1213
+ for file_name, cycle in identifiers:
1214
+ colour = self._colour_left if "Left" in cycle else self._colour_right
1215
+ self._include_cycle(file_name, cycle, colour)
1213
1216
  self._canvas.draw()
1214
1217
 
1215
1218
  def include_all(self):
@@ -1261,15 +1264,16 @@ class GaitCurves(defaultdict):
1261
1264
  self._canvas.draw_idle()
1262
1265
 
1263
1266
  def exclude_curves(self):
1264
- self._excluded_cycles.update(self._selected_curves)
1265
- for file_name, cycle in self._selected_curves:
1266
- colour = self._colour_left if "Left" in cycle else self._colour_right
1267
- for line in self[file_name][cycle]:
1268
- line.set_linestyle('dotted')
1269
- line.set_color(colour)
1270
- line.set_zorder(1)
1267
+ identifiers = list(self._selected_curves)
1268
+ self.exclude_cycles(identifiers)
1271
1269
  self._selected_curves = []
1272
1270
 
1271
+ self.signals.cycles_excluded.emit(identifiers)
1272
+
1273
+ def exclude_cycles(self, identifiers):
1274
+ for file_name, cycle in identifiers:
1275
+ colour = self._colour_left if "Left" in cycle else self._colour_right
1276
+ self._exclude_cycle(file_name, cycle, colour)
1273
1277
  self._canvas.draw()
1274
1278
 
1275
1279
  def exclude_all(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c3d-parser
3
- Version: 1.3.0
3
+ Version: 1.4.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
@@ -1,2 +0,0 @@
1
-
2
- __version__ = "1.3.0"
File without changes
File without changes
File without changes
File without changes
File without changes