pyomeca / ezc3d

Easy to use C3D reader/writer for C++, Python and Matlab
https://pyomeca.github.io/Documentation/ezc3d/index.html
MIT License
142 stars 44 forks source link

crop c3d #321

Closed mickaelbegon closed 3 months ago

mickaelbegon commented 3 months ago

Hi @pariterre,

Could you please provide a Python example for cropping a c3d and save it in another file?

Mickael

mickaelbegon commented 3 months ago

I started writing a Python code but at least one dimension is not well informed. I will send you the c3d file on Element.

Traceback (most recent call last): File "", line 1, in File "/Users/mickaelbegon/miniconda3/envs/bioptim_04_24/lib/python3.11/site-packages/ezc3d/init.py", line 573, in write dim = [len(old_param["value"])] ^^^^^^^^^^^^^^^^^^^^^^^ TypeError: len() of unsized object

import ezc3d
import numpy as np

## Example to remove unlabelled markers in Nexus/Vicon (identified by "*") and crop it
directory = "/Users/mickaelbegon/Downloads/CaMa"
file_name = "L_Squat.c3d"
start_time = 2.955
end_time = 4.977
test = True

# Construct the full file path
file_path = os.path.join(directory, file_name)

# Read the c3d file
c3d = ezc3d.c3d(file_path)
print(f"Read file: {file_name}")

print(f"Remove unlabelled markers")
labels = c3d["parameters"]["POINT"]["LABELS"]["value"]
descriptions = c3d['parameters']['POINT']['DESCRIPTIONS']['value']
points_data = c3d["data"]["points"]

# Find labelled markers (those that do not start with '*')
labelled_markers = [lab for lab in labels if not lab.startswith('*')]

# Find indices of labelled markers
labelled_indices = [labels.index(marker) for marker in labelled_markers]

# Crop descriptions and points's data to only keep those corresponding to labelled markers
cropped_labels = [labels[i] for i in labelled_indices]
cropped_descriptions = [descriptions[i] for i in labelled_indices]
cropped_points_data = points_data[:, labelled_indices, :]

# Update the c3d file's descriptions with the cropped list
c3d['parameters']['POINT']['USED']['value'] = np.array(len(cropped_labels))
c3d['header']['points']['size'] = len(cropped_labels)
c3d['parameters']['POINT']['LABELS']['value'] = cropped_labels
c3d['parameters']['POINT']['DESCRIPTIONS']['value'] = cropped_descriptions
c3d["data"]["points"] = cropped_points_data

# Remove the meta_points if it exists
if 'meta_points' in c3d['data']:
    del c3d['data']['meta_points']

print(f"Crop markers to selected period")
start_point_idx = int(np.floor(start_time * c3d['parameters']['POINT']['RATE']['value']))-1
end_point_idx =int(np.ceil(end_time * c3d['parameters']['POINT']['RATE']['value']))
cropped_points_data = cropped_points_data[:, :, start_point_idx:end_point_idx]

c3d['parameters']['POINT']['FRAMES']['value'] = cropped_points_data.shape[2]
c3d['parameters']['POINT']['DATA_START']['value'] += start_point_idx - 1
c3d['header']['points']['first_frame'] += start_point_idx - 1
c3d['header']['points']['last_frame'] = c3d['header']['points']['first_frame'] + cropped_points_data.shape[2] - 1
c3d["data"]["points"] = cropped_points_data

print(f"Crop analogs to selected period")
ratio = int(c3d['parameters']['ANALOG']['RATE']['value'] / c3d['parameters']['POINT']['RATE']['value'])
start_analog_idx = (start_point_idx) * ratio
end_analog_idx = (end_point_idx) * ratio
analog_data = c3d["data"]["analogs"]
cropped_analog_data = analog_data[:, :, start_analog_idx:end_analog_idx]

c3d['header']['analogs']['first_frame'] = c3d['header']['points']['first_frame'] * ratio
c3d['header']['analogs']['last_frame'] = c3d['header']['points']['last_frame'] * ratio + (ratio - 1)

if test is True:
    test_analog_data = c3d["data"]["analogs"][:, :, ::20]
    test_analog_data_cropped = test_analog_data[:, :, start_point_idx:end_point_idx]
    test_analog_data_cropped[:, 1, 0] == cropped_analog_data[:, 1, 0]
    test_analog_data_cropped[:, 1, -1] == cropped_analog_data[:, 1, -ratio]

c3d["data"]["analogs"] = cropped_analog_data

# Save the modified file with a new name
new_file_name = f"{os.path.splitext(file_name)[0]}_modified.c3d"
new_file_path = os.path.join(directory, new_file_name)
print(f"Write modified file: {new_file_name}")
c3d.write(new_file_path)
mickaelbegon commented 3 months ago

are these two variables supposed to be the same?

c3d['parameters']['POINT']['DATA_START']['value'] 
c3d['header']['points']['first_frame']
pariterre commented 3 months ago

Hello there!

The script you provided seems slightly complicated... so I feel I may have oversimplified what you are trying to achieve. As far as I am concerned the following snippet does what you need. Can you confirm that I am not oversimplifying?

import ezc3d
import numpy as np

file_path = "PATH_TO_C3D.c3d"
start_time = 2.955
end_time = 4.977

# Read the c3d file
c3d = ezc3d.c3d(file_path)

# Rewrite important parameters and the data only keeping the labelled markers that don't start with "*"
labels = c3d["parameters"]["POINT"]["LABELS"]["value"]
descriptions = c3d["parameters"]["POINT"]["DESCRIPTIONS"]["value"]
indices_to_keep = [i for i, lab in enumerate(labels) if not lab.startswith("*")]
c3d["parameters"]["POINT"]["LABELS"]["value"] = [labels[i] for i in indices_to_keep]
c3d["parameters"]["POINT"]["DESCRIPTIONS"]["value"] = [descriptions[i] for i in indices_to_keep]

# Change the data accondingly
c3d["data"]["points"] = c3d["data"]["points"][:, indices_to_keep, :]
del c3d["data"]["meta_points"]  # Let ezc3d do the job for the meta_points

# Change the time parameters
start_point_idx = int(np.floor(start_time * c3d["parameters"]["POINT"]["RATE"]["value"]))
end_point_idx = int(np.ceil(end_time * c3d["parameters"]["POINT"]["RATE"]["value"]))
c3d["header"]["points"]["first_frame"] = start_point_idx
c3d["header"]["points"]["last_frame"] = end_point_idx

# Save the modified file with a new name
c3d["parameters"]["ANALOG"]["UNITS"]["value"] = []  # This seems like a bug in ezc3d, as it should not be a problem
c3d.write("new_modified.c3d")

# Read the new file
c3d_copied = ezc3d.c3d("new_modified.c3d")
mickaelbegon commented 3 months ago

Than you. If I understand well ['parameters']['POINT']['USED']['value']; ['header']['points']['size']; and probably more are all updated when writing the c3d file ?

Your approach will not reduce much the size of the c3d (all data are still inside). I found my bug: c3d['parameters']['POINT']['FRAMES']['value'] needed to be a list of array.

# Read the c3d file
c3d = ezc3d.c3d(file_path)
print(f"Read file: {file_name}")

print(f"Remove unlabelled markers")
labels = c3d["parameters"]["POINT"]["LABELS"]["value"]
descriptions = c3d['parameters']['POINT']['DESCRIPTIONS']['value']
indices_to_keep = [i for i, lab in enumerate(labels) if not lab.startswith("*")]
c3d["parameters"]["POINT"]["LABELS"]["value"] = [labels[i] for i in indices_to_keep]
c3d["parameters"]["POINT"]["DESCRIPTIONS"]["value"] = [descriptions[i] for i in indices_to_keep]

# Change the data accordingly
c3d["data"]["points"] = c3d["data"]["points"][:, indices_to_keep, :]
del c3d["data"]["meta_points"]  # Let ezc3d do the job for the meta_points

print(f"Crop markers to selected period")
start_point_idx = int(np.floor(start_time * c3d['parameters']['POINT']['RATE']['value']))-1
end_point_idx =int(np.ceil(end_time * c3d['parameters']['POINT']['RATE']['value']))
cropped_points_data = c3d["data"]["points"][:, :, start_point_idx:end_point_idx]
c3d['parameters']['POINT']['FRAMES']['value'] = np.array([cropped_points_data.shape[2]])
c3d["header"]["points"]["first_frame"] = 1
c3d["header"]["points"]["last_frame"] = cropped_points_data.shape[2]
c3d["data"]["points"] = cropped_points_data

print(f"Crop analogs to selected period")
ratio = int(c3d['parameters']['ANALOG']['RATE']['value'] / c3d['parameters']['POINT']['RATE']['value'])
start_analog_idx = (start_point_idx) * ratio
end_analog_idx = (end_point_idx) * ratio
analog_data = c3d["data"]["analogs"]
cropped_analog_data = analog_data[:, :, start_analog_idx:end_analog_idx]

c3d['header']['analogs']['first_frame'] = 0
c3d['header']['analogs']['last_frame'] = cropped_analog_data.shape[2]
c3d["data"]["analogs"] = cropped_analog_data
c3d["parameters"]["ANALOG"]["UNITS"]["value"] = []  # This seems like a bug in ezc3d, as it should not be a problem

# Save the modified file with a new name
new_file_name = f"{os.path.splitext(file_name)[0]}_cropped.c3d"
new_file_path = os.path.join(directory, new_file_name)
print(f"Write modified file: {new_file_name}")
c3d.write(new_file_path)
pariterre commented 3 months ago

['parameters']['POINT']['USED']['value']; ['header']['points']['size']; and probably more are all updated when writing the c3d file ?

That is correct, most of these NEED to be very precise, so ezc3d won't let the user set them as they please, it will be updated to ensure internal consistency.

The "starting" flag won't reduce the size of the data as 0.00 will be registered, for the non values. If you want to reduce the data, you must also remove the data points before the starting index. If you do so, you must reset the first_frame to 0 though

mickaelbegon commented 3 months ago

@pariterre, I let you decide if you want to put an example to crop (and not change the start - end) a c3d file in the python example and you may close this issue. I don't know why c3d['parameters']['POINT']['FRAMES']['value'] needs to be a list of arrays... and not an integer. Perhaps it may be good to have a specific error.

mickaelbegon commented 3 months ago

by the way, thank you for the help.

pariterre commented 3 months ago

Done in #324

pariterre commented 3 months ago

For posterity:

c3d["parameters"]["ANALOG"]["UNITS"]["value"] = [] # This seems like a bug in ezc3d, as it should not be a problem

This was not a bug, it was because the underlying c3d had unsupported value formatting from the 35th element. This is why we had to remove the values.