From 20156bee2b1b56d959aa6990820a13e7c4d45968 Mon Sep 17 00:00:00 2001 From: Padraig Gleeson Date: Mon, 5 Oct 2026 13:36:11 +0100 Subject: [PATCH 1/2] Remove -f requirement for wcon file argument; to v0.2.0 --- README.md | 2 +- pyproject.toml | 2 +- test_all.sh | 8 +++--- wconviewer/WormView.py | 62 +++++++++++++++++++++++++++++++----------- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0791e53..45b19f2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Saving animations with `-nogui` requires the `ffmpeg` binary to be installed on ## Usage ``` -wconviewer -f examples/simdata.wcon +wconviewer examples/simdata.wcon ``` Run `wconviewer -h` for all options (e.g. `-nogui` to save an `.mp4` instead of showing a window). diff --git a/pyproject.toml b/pyproject.toml index b36d89a..f56ee78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "WCONViewer" -version = "0.1.0" +version = "0.2.0" description = "Viewer for WCON (Worm tracker Commons Object Notation) files in 2D" readme = "README.md" license = { file = "LICENSE" } diff --git a/test_all.sh b/test_all.sh index 751bef4..dc031e4 100755 --- a/test_all.sh +++ b/test_all.sh @@ -6,8 +6,8 @@ ruff check wconviewer app.py pip install . -wconviewer -f examples/simdata.wcon -nogui -z # Test reloading WCON in Player -wconviewer -f examples/worm_motion_log.wcon -nogui -g -wconviewer -f examples/output_W2D.wcon -nogui -g -z +wconviewer examples/simdata.wcon -nogui -z # Test reloading WCON in Player +wconviewer examples/worm_motion_log.wcon -nogui -g +wconviewer examples/output_W2D.wcon -nogui -g -z -#wconviewer -f examples/N2\ on\ food\ L_2009_09_15__11_01_01___8___1.wcon.zip -nogui +#wconviewer examples/N2\ on\ food\ L_2009_09_15__11_01_01___8___1.wcon.zip -nogui diff --git a/wconviewer/WormView.py b/wconviewer/WormView.py index 641a0f5..7f9a063 100644 --- a/wconviewer/WormView.py +++ b/wconviewer/WormView.py @@ -143,7 +143,13 @@ def set_show_grid(self, value): self.ax.grid(False, which="major") self.ax.grid(False, which="minor") - def get_plot(self, args): + def get_plot( + self, + wcon_file, + ignore_wcon_perimeter=False, + suppress_automatic_generation=False, + minor_radius=40e-3, + ): # global times, t_units, x, y, px, py, ax self.fig, self.ax = plt.subplots() @@ -151,7 +157,7 @@ def get_plot(self, args): plt.get_current_fig_manager().set_window_title("WCON replay") self.ax.set_aspect("equal") - self.wcon = SimpleWCON(args.wcon_file) + self.wcon = SimpleWCON(wcon_file) self.ax.set_xlabel("x (%s)" % self.wcon.x_units_used) self.ax.set_ylabel("y (%s)" % self.wcon.y_units_used) @@ -181,20 +187,20 @@ def get_plot(self, args): print("No objects found") if self.wcon.px is not None and self.wcon.py is not None: - if args.ignore_wcon_perimeter: + if ignore_wcon_perimeter: print( "Ignoring (px, py) values in WCON file and computing perimeter from midline." ) - self.px, self.py = self.get_perimeter(self.x, self.y, args.minor_radius) + self.px, self.py = self.get_perimeter(self.x, self.y, minor_radius) else: print("Using (px, py) from WCON file") self.px = np.array(self.wcon.px).T self.py = np.array(self.wcon.py).T else: - if not args.suppress_automatic_generation and self.wcon.x.shape[0] >= 2: + if not suppress_automatic_generation and self.wcon.x.shape[0] >= 2: print("Computing perimeter from midline") self.px, self.py = self.get_perimeter( - self.wcon.x, self.wcon.y, args.minor_radius + self.wcon.x, self.wcon.y, minor_radius ) else: print("Not computing perimeter from midline") @@ -273,11 +279,10 @@ def parse_args(): description="Open a player for the worm behaviour." ) parser.add_argument( - "-f", - "--wcon_file", + "wcon_file", type=validate_file, + metavar="WCON_FILE", help="WCON file path", - required=True, ) parser.add_argument( "-nogui", action="store_true", help="Just load file, don't show GUI" @@ -333,13 +338,38 @@ def main(): print(" - Arguments parsed: %s" % args) + show_worm_view( + args.wcon_file, + args.show_head, + args.zoom_to_worm, + args.show_grid, + args.nogui, + args.ignore_wcon_perimeter, + args.suppress_automatic_generation, + args.minor_radius, + ) + + +def show_worm_view( + wcon_file: str, + show_head: bool = False, + zoom_to_worm: bool = False, + show_grid: bool = False, + nogui: bool = False, + ignore_wcon_perimeter: bool = False, + suppress_automatic_generation: bool = False, + minor_radius: float = 40e-3, +): + wv = WormView( - show_head=args.show_head, - zoom_to_worm=args.zoom_to_worm, - show_grid=args.show_grid, + show_head=show_head, + zoom_to_worm=zoom_to_worm, + show_grid=show_grid, ) - fig, ax = wv.get_plot(args) + fig, ax = wv.get_plot( + wcon_file, ignore_wcon_perimeter, suppress_automatic_generation, minor_radius + ) def update(ti): print(" ------ Animating the plot for time index: %d" % ti) @@ -357,12 +387,12 @@ def update(ti): on_toggle_zoom=wv.set_zoom_to_worm, ) - if args.nogui: + if nogui: # Checkboxes are only useful for interactive control; don't bake them # into the frames used for the saved movie. anim.check_buttons.ax.set_visible(False) - if not args.nogui: + if not nogui: plt.show() else: print("GUI suppressed, exiting without showing %s." % anim) @@ -370,7 +400,7 @@ def update(ti): from matplotlib.animation import FFMpegWriter FFwriter = FFMpegWriter(fps=10) - mp4_file = args.wcon_file.replace(".wcon.zip", ".wcon").replace(".wcon", ".mp4") + mp4_file = wcon_file.replace(".wcon.zip", ".wcon").replace(".wcon", ".mp4") print(f"Saving animation to: {mp4_file}") anim.save(mp4_file, writer=FFwriter) From c28f4e95a4ac012e19f8a86ffb1362697e023fc3 Mon Sep 17 00:00:00 2001 From: Padraig Gleeson Date: Mon, 5 Oct 2026 14:04:46 +0100 Subject: [PATCH 2/2] Add start play option --- .gitignore | 1 + wconviewer/Player.py | 10 ++++++++++ wconviewer/WormView.py | 3 +++ 3 files changed, 14 insertions(+) diff --git a/.gitignore b/.gitignore index 2d24004..7f22cdf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /old /build /*.egg-info +/wconviewer/__pycache__ diff --git a/wconviewer/Player.py b/wconviewer/Player.py index b42e928..5ff804e 100644 --- a/wconviewer/Player.py +++ b/wconviewer/Player.py @@ -25,6 +25,7 @@ def __init__( zoom_state=False, on_toggle_grid=None, on_toggle_zoom=None, + start_play=True, **kwargs, ): self.i = 0 @@ -53,6 +54,15 @@ def __init__( self.times = times self.t_units = t_units + if not start_play: + # The animation timer is started by matplotlib on the first draw, so + # pause it from a draw callback registered after matplotlib's own. + def pause_after_first_draw(event): + self.fig.canvas.mpl_disconnect(cid) + self.stop() + + cid = self.fig.canvas.mpl_connect("draw_event", pause_after_first_draw) + def play(self): while self.runs: self.i = self.i + self.step * (1 if self.forwards else -1) diff --git a/wconviewer/WormView.py b/wconviewer/WormView.py index 7f9a063..77c34f5 100644 --- a/wconviewer/WormView.py +++ b/wconviewer/WormView.py @@ -359,6 +359,7 @@ def show_worm_view( ignore_wcon_perimeter: bool = False, suppress_automatic_generation: bool = False, minor_radius: float = 40e-3, + start_play: bool = True, ): wv = WormView( @@ -385,6 +386,8 @@ def update(ti): zoom_state=wv.zoom_to_worm, on_toggle_grid=wv.set_show_grid, on_toggle_zoom=wv.set_zoom_to_worm, + # When saving a movie (nogui) the animation always runs through + start_play=start_play or nogui, ) if nogui: