Skip to content

Commit b9f8ac4

Browse files
committed
Add movie saving option
1 parent 6b5405c commit b9f8ac4

File tree

5 files changed

+38026
-56
lines changed

5 files changed

+38026
-56
lines changed

Player.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def __init__(
1919
mini=0,
2020
maxi=100,
2121
pos=(0.125, 0.92),
22-
**kwargs
22+
**kwargs,
2323
):
2424
self.i = 0
2525
self.min = mini
@@ -37,7 +37,7 @@ def __init__(
3737
init_func=init_func,
3838
fargs=fargs,
3939
save_count=save_count,
40-
**kwargs
40+
**kwargs,
4141
)
4242

4343
def play(self):
@@ -92,11 +92,11 @@ def setup(self, pos):
9292
fax = divider.append_axes("right", size="80%", pad=0.05)
9393
ofax = divider.append_axes("right", size="100%", pad=0.05)
9494
sliderax = divider.append_axes("right", size="500%", pad=0.07)
95-
self.button_oneback = matplotlib.widgets.Button(playerax, label="$\u29CF$")
96-
self.button_back = matplotlib.widgets.Button(bax, label="$\u25C0$")
97-
self.button_stop = matplotlib.widgets.Button(sax, label="$\u25A0$")
98-
self.button_forward = matplotlib.widgets.Button(fax, label="$\u25B6$")
99-
self.button_oneforward = matplotlib.widgets.Button(ofax, label="$\u29D0$")
95+
self.button_oneback = matplotlib.widgets.Button(playerax, label="$\u29cf$")
96+
self.button_back = matplotlib.widgets.Button(bax, label="$\u25c0$")
97+
self.button_stop = matplotlib.widgets.Button(sax, label="$\u25a0$")
98+
self.button_forward = matplotlib.widgets.Button(fax, label="$\u25b6$")
99+
self.button_oneforward = matplotlib.widgets.Button(ofax, label="$\u29d0$")
100100
self.button_oneback.on_clicked(self.onebackward)
101101
self.button_back.on_clicked(self.backward)
102102
self.button_stop.on_clicked(self.stop)

WormView.py

Lines changed: 96 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,81 +11,111 @@
1111
midline_plot = None
1212
perimeter_plot = None
1313

14+
1415
def validate_file(file_path):
1516
if not os.path.exists(file_path):
1617
raise argparse.ArgumentTypeError(f"The file {file_path} does not exist.")
1718
if not os.path.isfile(file_path):
1819
raise argparse.ArgumentTypeError(f"{file_path} is not a valid file.")
1920
return file_path
2021

22+
2123
def get_perimeter(x, y, r):
2224
n_bar = x.shape[0]
2325
num_steps = x.shape[1]
2426

2527
n_seg = int(n_bar - 1)
2628

2729
# radii along the body of the worm
28-
r_i = np.array([
29-
r * abs(math.sin(math.acos(((i) - n_seg / 2.0) / (n_seg / 2.0 + 0.2))))
30-
for i in range(n_bar)
31-
]).reshape(-1, 1)
30+
r_i = np.array(
31+
[
32+
r * abs(math.sin(math.acos(((i) - n_seg / 2.0) / (n_seg / 2.0 + 0.2))))
33+
for i in range(n_bar)
34+
]
35+
).reshape(-1, 1)
3236

3337
diff_x = np.diff(x, axis=0)
3438
diff_y = np.diff(y, axis=0)
3539

3640
arctan = np.arctan2(diff_x, -diff_y)
3741
d_arr = np.zeros((n_bar, num_steps))
3842

39-
d_mask = np.full((n_bar,num_steps), False)
40-
arctan_diff = np.abs(np.diff(arctan, axis = 0)) > np.pi
41-
d_mask[1:-1,:] = arctan_diff
42-
43+
d_mask = np.full((n_bar, num_steps), False)
44+
arctan_diff = np.abs(np.diff(arctan, axis=0)) > np.pi
45+
d_mask[1:-1, :] = arctan_diff
4346

4447
# d of worm endpoints is based off of two points, whereas d of non-endpoints is based off of 3 (x, y) points
45-
48+
4649
d_arr[:-1, :] = arctan
4750
d_arr[1:, :] = d_arr[1:, :] + arctan
4851
d_arr[1:-1, :] = d_arr[1:-1, :] / 2
49-
d_arr = d_arr - np.pi*d_mask
50-
dx = np.cos(d_arr)*r_i
51-
dy = np.sin(d_arr)*r_i
52-
52+
d_arr = d_arr - np.pi * d_mask
53+
dx = np.cos(d_arr) * r_i
54+
dy = np.sin(d_arr) * r_i
5355

54-
px = np.zeros((2*n_bar, x.shape[1]))
55-
py = np.zeros((2*n_bar, x.shape[1]))
56+
px = np.zeros((2 * n_bar, x.shape[1]))
57+
py = np.zeros((2 * n_bar, x.shape[1]))
5658

5759
px[:n_bar, :] = x - dx
58-
px[n_bar:, :] = np.flipud(x + dx) # Make perimeter counter-clockwise
60+
px[n_bar:, :] = np.flipud(x + dx) # Make perimeter counter-clockwise
5961

6062
py[:n_bar, :] = y - dy
61-
py[n_bar:, :] = np.flipud(y + dy) # Make perimeter counter-clockwise
63+
py[n_bar:, :] = np.flipud(y + dy) # Make perimeter counter-clockwise
6264

6365
return px, py
6466

6567

6668
def main():
67-
68-
# Default behavior is to use (px, py) if it exists, and if it doesn’t then automatically generate the perimeter from the midline.
69-
parser = argparse.ArgumentParser(description="Open a player for the worm behaviour.")
70-
parser.add_argument('-f', '--wcon_file', type=validate_file, help='WCON file path', required=True )
71-
parser.add_argument('-nogui', action='store_true', help="Just load file, don't show GUI")
72-
parser.add_argument('-s', '--suppress_automatic_generation', action='store_true', help='Suppress the automatic generation of a perimeter which would be computed from the midline of the worm. If (px, py) is not specified in the WCON, a perimeter will not be shown.')
73-
parser.add_argument('-i', '--ignore_wcon_perimeter', action='store_true', help='Ignore (px, py) values in the WCON. Instead, a perimeter is automatically generated based on the midline of the worm.')
74-
parser.add_argument('-r', '--minor_radius', type=float, default=40e-3, help='Minor radius of the worm in millimeters (default: 40e-3)', required=False)
69+
# Default behavior is to use (px, py) if it exists, and if it doesn’t then automatically generate the perimeter from the midline.
70+
parser = argparse.ArgumentParser(
71+
description="Open a player for the worm behaviour."
72+
)
73+
parser.add_argument(
74+
"-f", "--wcon_file", type=validate_file, help="WCON file path", required=True
75+
)
76+
parser.add_argument(
77+
"-nogui", action="store_true", help="Just load file, don't show GUI"
78+
)
79+
parser.add_argument(
80+
"-s",
81+
"--suppress_automatic_generation",
82+
action="store_true",
83+
help="Suppress the automatic generation of a perimeter which would be computed from the midline of the worm. If (px, py) is not specified in the WCON, a perimeter will not be shown.",
84+
)
85+
parser.add_argument(
86+
"-i",
87+
"--ignore_wcon_perimeter",
88+
action="store_true",
89+
help="Ignore (px, py) values in the WCON. Instead, a perimeter is automatically generated based on the midline of the worm.",
90+
)
91+
parser.add_argument(
92+
"-r",
93+
"--minor_radius",
94+
type=float,
95+
default=40e-3,
96+
help="Minor radius of the worm in millimeters (default: 40e-3)",
97+
required=False,
98+
)
7599

76100
args = parser.parse_args()
77101

78102
fig, ax = plt.subplots()
79103
plt.get_current_fig_manager().set_window_title("WCON replay")
80104
ax.set_aspect("equal")
81105

82-
with open(args.wcon_file, 'r') as f:
106+
with open(args.wcon_file, "r") as f:
83107
wcon = json.load(f)
84-
108+
85109
if "@CelegansNeuromechanicalGaitModulation" in wcon:
86-
center_x_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"]["circles"]["x"]
87-
center_y_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"]["circles"]["y"]
88-
radius_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"]["circles"]["r"]
110+
center_x_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"][
111+
"circles"
112+
]["x"]
113+
center_y_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"][
114+
"circles"
115+
]["y"]
116+
radius_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"][
117+
"circles"
118+
]["r"]
89119

90120
for center_x, center_y, radius in zip(center_x_arr, center_y_arr, radius_arr):
91121
circle = plt.Circle((center_x, center_y), radius, color="b")
@@ -94,32 +124,35 @@ def main():
94124
print("No objects found")
95125

96126
# Set the limits of the plot since we don't have any objects to help with autoscaling
97-
127+
98128
ax.set_ylim([-1.5, 1.5])
99129

100130
t = np.array(wcon["data"][0]["t"])
101131
x = np.array(wcon["data"][0]["x"]).T
102132
y = np.array(wcon["data"][0]["y"]).T
103133

104-
print(f"Range of time: {t[0]}->{t[0]}; x range: {x.max()}->{x.min()}; y range: {y.max()}->{y.min()}")
134+
print(
135+
f"Range of time: {t[0]}->{t[0]}; x range: {x.max()}->{x.min()}; y range: {y.max()}->{y.min()}"
136+
)
105137
factor = 0.05
106-
if abs(x.max()-x.min())>abs(y.max()-y.min()):
107-
side = abs(x.max()-x.min())
108-
ax.set_xlim([x.min()-side*factor, x.max()+side*factor])
109-
mid = (y.max()+y.min())/2
110-
ax.set_ylim([mid-side*(.5+factor), mid+side*(.5+factor)])
138+
if abs(x.max() - x.min()) > abs(y.max() - y.min()):
139+
side = abs(x.max() - x.min())
140+
ax.set_xlim([x.min() - side * factor, x.max() + side * factor])
141+
mid = (y.max() + y.min()) / 2
142+
ax.set_ylim([mid - side * (0.5 + factor), mid + side * (0.5 + factor)])
111143
else:
112-
side = abs(y.max()-y.min())
113-
ax.set_ylim([y.min()-side*factor, y.max()+side*factor])
114-
mid = (x.max()+x.min())/2
115-
ax.set_xlim([mid-side*(.5+factor), mid+side*(.5+factor)])
116-
144+
side = abs(y.max() - y.min())
145+
ax.set_ylim([y.min() - side * factor, y.max() + side * factor])
146+
mid = (x.max() + x.min()) / 2
147+
ax.set_xlim([mid - side * (0.5 + factor), mid + side * (0.5 + factor)])
117148

118149
num_steps = t.size
119150

120151
if "px" in wcon["data"][0] and "py" in wcon["data"][0]:
121152
if args.ignore_wcon_perimeter:
122-
print("Ignoring (px, py) values in WCON file and computing perimeter from midline.")
153+
print(
154+
"Ignoring (px, py) values in WCON file and computing perimeter from midline."
155+
)
123156
px, py = get_perimeter(x, y, args.minor_radius)
124157
else:
125158
print("Using (px, py) from WCON file")
@@ -142,23 +175,37 @@ def update(ti):
142175
print("Time step: %s, fract: %f, color: %s" % (ti, f, color))
143176

144177
if midline_plot is None:
145-
(midline_plot,) = ax.plot(x[:, ti], y[:, ti], color="g", label="t=%sms" % t[ti], linewidth=0.5)
178+
(midline_plot,) = ax.plot(
179+
x[:, ti], y[:, ti], color="g", label="t=%sms" % t[ti], linewidth=0.5
180+
)
146181
else:
147182
midline_plot.set_data(x[:, ti], y[:, ti])
148-
183+
149184
if px is not None and py is not None:
150185
if perimeter_plot is None:
151-
(perimeter_plot,) = ax.plot(px[:, ti], py[:, ti], color="grey", linewidth=1)
186+
(perimeter_plot,) = ax.plot(
187+
px[:, ti], py[:, ti], color="grey", linewidth=1
188+
)
152189
else:
153190
perimeter_plot.set_data(px[:, ti], py[:, ti])
154191

155-
ani = Player(fig, update, maxi=num_steps - 1)
192+
anim = Player(fig, update, maxi=num_steps - 1)
156193

157-
# TODO WormViewCSV and WormViewWCON - should WormViewCSV just be the original WormView? That's what it initially did.
158-
# TODO Could take out Player and WormViewWCON into separate repo - Taking out Player could be ugly. It is quite coupled with WormView due to the update function.
194+
# TODO WormViewCSV and WormViewWCON - should WormViewCSV just be the original WormView? That's what it initially did.
195+
# TODO Could take out Player and WormViewWCON into separate repo - Taking out Player could be ugly. It is quite coupled with WormView due to the update function.
159196

160197
if not args.nogui:
161198
plt.show()
199+
else:
200+
print("GUI suppressed, exiting without showing %s." % anim)
201+
202+
from matplotlib.animation import FFMpegWriter
203+
204+
FFwriter = FFMpegWriter(fps=10)
205+
mp4_file = args.wcon_file.replace(".wcon", ".mp4")
206+
print(f"Saving animation to: {mp4_file}")
207+
anim.save(mp4_file, writer=FFwriter)
208+
162209

163210
if __name__ == "__main__":
164211
sys.exit(main())

examples/simdata.mp4

99 KB
Binary file not shown.

0 commit comments

Comments
 (0)