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 }}