From bcd893ddbf57c8ed683e31de2e6ceeec97dff64f Mon Sep 17 00:00:00 2001 From: Padraig Gleeson Date: Fri, 13 Nov 2026 10:33:58 +0000 Subject: [PATCH 1/2] correctly compute body curv --- pyproject.toml | 2 +- wconviewer/Utils.py | 72 +++++++++++++++++++++++------------------- wconviewer/WormView.py | 15 +++++---- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c7ccd7f..b024228 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "WCONViewer" -version = "0.3.0" +version = "0.3.1" description = "Viewer for WCON (Worm tracker Commons Object Notation) files in 2D" readme = "README.md" license = { file = "LICENSE" } diff --git a/wconviewer/Utils.py b/wconviewer/Utils.py index b5328bc..48d3d8f 100644 --- a/wconviewer/Utils.py +++ b/wconviewer/Utils.py @@ -135,40 +135,42 @@ def signed(vx, vy): } -def compute_body_curvature(ts, x, y): +def compute_body_curvature(ts, x, y, head="L"): """ Angle (degrees, 180 = straight) between the points 2 before and 2 after each body point, for all body points except the 2 at each end. - Returns an array of shape (n_frames, n_body_points - 4). - """ - body_curv_data = np.zeros((len(ts), x.shape[0] - 4)) - - for ti in range(len(ts)): - for i in range(x.shape[0])[2:-2]: - x1 = x[i - 2, ti] - xc = x[i, ti] - x2 = x[i + 2, ti] - - y1 = y[i - 2, ti] - yc = y[i, ti] - y2 = y[i + 2, ti] - - a1 = math.atan2(y1 - yc, x1 - xc) - a2 = math.atan2(y2 - yc, x2 - xc) - angle = a2 - a1 - if angle < 0: - angle += 2 * math.pi - if angle > 2 * math.pi: - angle = 2 * math.pi - angle + The points are taken in order from the head to the tail, using the WCON head + value ("L", "R" or "?", single or per frame), so the first column is nearest + the head. If the head is "R" the order of the points is reversed for that + frame, and where the head is unknown ("?" or null), point 0 is assumed to be + the head. - deg = 360 * (angle / (2 * math.pi)) + Returns an array of shape (n_frames, n_body_points - 4), or None if there + are too few body points (e.g. single point trackers) to calculate it. + """ + n_points = x.shape[0] + if n_points < 5: + print_( + "Cannot calculate body curvature with only %i body points (need 5+)" + % n_points + ) + return None - body_curv_data[ti][i - 2] = deg + # Order the points from head to tail in each frame + reverse = np.array([h == "R" for h in np.broadcast_to(head, len(ts))]) + order = np.arange(n_points)[:, None] + order = np.where(reverse[None, :], n_points - 1 - order, order) + frames = np.arange(len(ts))[None, :] + x = x[order, frames] + y = y[order, frames] - # print("At t=%s, i=%s: angle from between (%s,%s) - (%s,%s) - (%s,%s) = %s, %sdeg"%(ts[ti], i, x1,y1,xc,yc,x2,y2,angle,deg)) + # Angles of the lines from each body point to the points 2 before and 2 after + a1 = np.arctan2(y[:-4] - y[2:-2], x[:-4] - x[2:-2]) + a2 = np.arctan2(y[4:] - y[2:-2], x[4:] - x[2:-2]) + angle = np.mod(a2 - a1, 2 * np.pi) - return body_curv_data + return np.degrees(angle).T def generate_midlines_plot(ts, x, y, spacing=0.05, save_to=None, t_units="s"): @@ -217,7 +219,7 @@ def generate_speeds_plot( Plot the output of compute_speeds, with the same x/y swap as plot_midlines. """ fig = plt.figure() - info = "Speed of worm, signed by direction, and in x & y (along body) directions" + info = "Speed of worm" window = speeds.get("window", 1) if window > 1: info += ", over %i time steps" % window @@ -300,24 +302,30 @@ def generate_body_curvature_plot( t_units="s", ): """ - Plot the output of compute_body_curvature as a heat map of curvature against - time and position along the body. + Plot the output of compute_body_curvature as a heat map (a kymograph) of + curvature against time and position along the body, with the head at the + bottom and the tail at the top. Returns None if body_curv_data is None. """ + if body_curv_data is None: + return None + fig = plt.figure() plt.xlabel("Time (%s)" % t_units) - plt.ylabel("Percentage along worm") plot0 = plt.imshow( body_curv_data.transpose(), interpolation="nearest", aspect="auto", + origin="lower", cmap=cmap, vmin=min_curvature, vmax=max_curvature, ) ax = plt.gca() - info = "Propagation of curvature along body of worm (180=straight)" + # Just label the first (head) and last (tail) body points + ax.set_yticks([0, body_curv_data.shape[1] - 1], ["Head", "Tail"]) + info = "Worm body curvature" fig.canvas.manager.set_window_title(info) plt.title(info) @@ -384,7 +392,7 @@ def analyse_wcon_file( if plot_body_curvature: generate_body_curvature_plot( ts, - compute_body_curvature(ts, x, y), + compute_body_curvature(ts, x, y, head), save_to=save_figure_curvature_to, t_units=t_units, ) diff --git a/wconviewer/WormView.py b/wconviewer/WormView.py index 1039057..58f8c0a 100644 --- a/wconviewer/WormView.py +++ b/wconviewer/WormView.py @@ -296,7 +296,7 @@ def update(self, ti): (self.head_trace_plot,) = self.ax.plot( [], [], - color="grey", + color="blue", marker=".", markersize=0.5, linewidth=0, @@ -532,9 +532,9 @@ def show_worm_view( # with a function giving the x value of the line for a time index time_lines = [] - def add_time_line(fig_other, x_for_index, window_x): + def add_time_line(fig_other, x_for_index, window_x, color="red"): time_lines.append( - (fig_other.axes[0].axvline(0, color="red", linewidth=1), x_for_index) + (fig_other.axes[0].axvline(0, color=color, linewidth=1), x_for_index) ) # Only the Qt backends can position the window (not e.g. Agg) window = getattr(fig_other.canvas.manager, "window", None) @@ -547,12 +547,15 @@ def add_time_line(fig_other, x_for_index, window_x): generate_body_curvature_plot, ) - body_curv_data = compute_body_curvature(wv.wcon.times, wv.wcon.x, wv.wcon.y) + body_curv_data = compute_body_curvature( + wv.wcon.times, wv.wcon.x, wv.wcon.y, wv.wcon.head + ) fig_curv = generate_body_curvature_plot( wv.wcon.times, body_curv_data, t_units=wv.wcon.t_units_used ) - # x axis is the time index - add_time_line(fig_curv, lambda ti: ti, 10) + if fig_curv is not None: + # x axis is the time index + add_time_line(fig_curv, lambda ti: ti, 10, color="black") else: print(" - Not showing curvature plot") From 5e8c5beb203ea7e16412708dcc165aa9a190afc9 Mon Sep 17 00:00:00 2001 From: Padraig Gleeson Date: Fri, 13 Nov 2026 10:38:12 +0000 Subject: [PATCH 2/2] Update GHA versions --- .github/dependabot.yml | 14 ++++++++++++++ .github/workflows/build.yml | 6 +++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..28cb43e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# Source - https://stackoverflow.com/a/78380165 +# Posted by Stefan, modified by community. See post 'Timeline' for change history +# Retrieved 2026-04-14, License - CC BY-SA 4.0 + +# Set update schedule for GitHub Actions + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 393039f..0da69c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,13 +14,13 @@ jobs: fail-fast: false matrix: runs-on: [ubuntu-latest] - python-version: [ "3.10", "3.12" ] + python-version: [ "3.10", "3.12", "3.14" ] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }}