OSCR

Disentangling cephalopod chromatophores motor units with computer vision.

Code ↔ Paper

21 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 21 matches
  1. [1] § Materials and methods › Chromatophore video acquisition and analysis › Computational analyses ↔ chromas/source/training/neural_net_training.py, lines 1–64 · score 0.89 · cross entropy loss, neural network, Dice loss, loss function, scheduler, FCN
  2. [2] § Materials and methods › Chromatophore video acquisition and analysis › Motor unit shape and structure ↔ motor_units/mu_structure.py, lines 181–313 · score 0.89 · minimum spanning tree, epicenter_x, epicenter_y, nearest neighbor, chromatophore epicenters, FND
  3. [3] § Materials and methods › Chromatophore video acquisition and analysis › Motor unit shape and structure ↔ motor_units/distances.py, lines 1–23 · score 0.87 · furthest neighbor distances, nearest neighbor distances, epicenter_x, epicenter_y, Epicenter coordinates, NND
  4. [4] § Materials and methods › Chromatophore video acquisition and analysis › Number and influence of motor neuron per chromatophore ↔ motor_units/anisotropy.py, lines 1–63 · score 0.79 · percentage influence profile, mixing matrix, motor neuron, radial slice, ICA, component
  5. [5] § Materials and methods › Chromatophore video acquisition and analysis › Number and influence of motor neuron per chromatophore ↔ motor_units/anisotropy.py, lines 1–63 · score 0.78 · meaningful components, motor neuron, radial slices, detrended, signal, PCA
  6. [6] § Materials and methods › Chromatophore video acquisition and analysis › Expansion and contraction speeds ↔ motor_units/exp_vs_contr.py, lines 6–29 · score 0.77 · contraction dynamics, detected peak, Peak centered, radii, smoothing, traces
  7. [7] § Materials and methods › Chromatophore video acquisition and analysis › Expansion and contraction speeds ↔ motor_units/exp_vs_contr.py, lines 747–887 · score 0.73 · percentile tails, remove outliers, suppress, IQR, noise, amplitude
  8. [8] § Materials and methods › Chromatophore video acquisition and analysis › Number and influence of motor neuron per chromatophore ↔ chromas/source/ica/ica_utils.py, lines 10–56 · score 0.70 · n_components, elbow point, kneed, detrended, signal, ICA
  9. [9] § Materials and methods › Chromatophore video acquisition and analysis › Number of chromatophores per motor unit ↔ chromas/source/analysis/clustering_utils.py, lines 7–21 · score 0.65 · sklearn.cluster.AffinityPropagation, scikit-learn, metric, correlation
  10. [10] § Materials and methods › Chromatophore video acquisition and analysis › Second-order innervation ↔ motor_units/mu_cooccurence.py, lines 1–45 · score 0.65 · cluster memberships, co occurrence, simulations, chromatophore
  11. [11] § Results › Structure and size of chromatophore motor units ↔ motor_units/mu_cooccurence.py, lines 1–45 · score 0.63 · co occurrence, co occurring, chromatophore pairs, randomized, memberships, motor unit
  12. [12] § Results › Structure and size of chromatophore motor units ↔ motor_units/mu_structure.py, lines 315–347 · score 0.60 · convex hull area, cluster area, encloses, motor units, median, smaller
  13. [13] § Results › Structure and size of chromatophore motor units ↔ motor_units/mu_structure.py, lines 181–313 · score 0.59 · global nearest neighbor, furthest distance, NND, MU, motor unit, clusters
  14. [14] § Materials and methods › Chromatophore video acquisition and analysis › Computational analyses ↔ chromas/source/training/neural_net_training.py, lines 526–545 · score 0.59 · 0.05–0.5, brightness, flipping, vertical, shifts, training
  15. [15] § Materials and methods › Chromatophore video acquisition and analysis › Computational analyses ↔ chromas/source/training/unet_training.py, lines 258–277 · score 0.59 · 0.05–0.5, brightness, flipping, vertical, shifts, training
  16. [16] § Results › Interpretation of fine single-chromatophore motion ↔ motor_units/anisotropy.py, lines 136–180 · score 0.58 · cumulative explained variance, principal components, curve, PCA
  17. [17] § Results › Methodological development with Euprymna berryi ↔ chromas/source/segmentation/segmentation_neuralnet.py, lines 1–37 · score 0.58 · Sepia officinalis, individual chromatophores, species, berryi, Euprymna, segmented
  18. [18] § Results › Dynamics of chromatophore expansion and contraction ↔ motor_units/exp_vs_contr.py, lines 747–887 · score 0.57 · aligned event, band, split, IQR, outliers, tail
  19. [19] § Results › Structure and size of chromatophore motor units ↔ motor_units/mu_structure.py, lines 349–396 · score 0.54 · convex hull area, log scale, px2, motor unit, density, chromatophore
  20. [20] § Results › Structure and size of chromatophore motor units ↔ motor_units/distances.py, lines 1–23 · score 0.53 · nearest neighbor distance, closest, furthest, NND, motor unit, clusters
  21. [21] § Results › Analysis of chromatophore motion in S. officinalis ↔ motor_units/anisotropy.py, lines 136–180 · score 0.50 · Cumulative explained variance, principal component, PCA, motor

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 408 lines · 16 KB · BSD-3-Clause · 4 matches

  1. import pandas as pd
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. from scipy.spatial import distance_matrix
  5. from scipy.sparse.csgraph import minimum_spanning_tree
  6. from matplotlib.patches import Wedge
  7. from collections import defaultdict
  8. from matplotlib.image import imread
  9. from scipy.spatial import ConvexHull
  10. from scipy.stats import pearsonr
  11. """
  12. This script loads chromatophore cluster assignments and spatial coordinates,
  13. builds a set of lookup tables that map clusters to chromatophores and vice
  14. versa, and provides tools to explore the structure of those clusters. It prints
  15. cluster groups sorted by size and offers plotting utilities to visualize spatial
  16. organization, distances, and geometric structure such as convex hulls,
  17. projections, and pairwise relationships.
  18. Usage:
  19. Call `load_data(csv_path)` with a CSV file containing `chrom_id`, `label`,
  20. `epicenter_x`, and `epicenter_y`. After the data are loaded, run
  21. `list_clusters()` to inspect cluster sizes, or use the plotting functions to
  22. visualize spatial layouts and relationships. All analysis functions assume that
  23. `load_data` has been executed first.
  24. Author:
  25. Mathieu D. M. Renard (Laurent Lab, 2025)
  26. """
  27. plt.rcParams.update({
  28. 'font.size': 20, # smaller font
  29. 'axes.titlesize': 18, # smaller title
  30. 'axes.labelsize': 18, # smaller axis labels
  31. 'xtick.labelsize': 18,
  32. 'ytick.labelsize': 18,
  33. 'legend.fontsize': 14,
  34. 'lines.linewidth': 2, # thinner lines
  35. 'lines.markersize': 5, # smaller markers
  36. 'figure.dpi': 150, # control overall figure size/dpi
  37. 'figure.figsize': (6, 8), # smaller figure dimensions
  38. 'xtick.major.size': 5,
  39. 'ytick.major.size': 5,
  40. 'xtick.major.width': 1.5,
  41. 'ytick.major.width': 1.5,
  42. })
  43. # === Global state ===
  44. df_global = None
  45. label_to_chroms = None
  46. chrom_to_labels = None
  47. coords_lookup = None
  48. # === Load and prepare data ===
  49. def load_data(csv_path):
  50. global df_global, label_to_chroms, chrom_to_labels, coords_lookup
  51. df = pd.read_csv(csv_path)
  52. df = df[['chrom_id', 'label', 'epicenter_x', 'epicenter_y']].dropna().drop_duplicates()
  53. df_global = df
  54. label_to_chroms = df.groupby('label')['chrom_id'].apply(lambda x: list(set(x)))
  55. chrom_to_labels = defaultdict(set)
  56. for _, row in df.iterrows():
  57. chrom_to_labels[row['chrom_id']].add(row['label'])
  58. unique_chroms = df[['chrom_id', 'epicenter_x', 'epicenter_y']].drop_duplicates()
  59. coords_lookup = dict(zip(unique_chroms['chrom_id'], zip(unique_chroms['epicenter_x'], unique_chroms['epicenter_y'])))
  60. # === Cluster listing ===
  61. def list_clusters():
  62. print("\n📊 Cluster groups by size:\n")
  63. size_to_labels = defaultdict(list)
  64. for label, chrom_list in label_to_chroms.items():
  65. size_to_labels[len(chrom_list)].append(label)
  66. for size in sorted(size_to_labels.keys(), reverse=True):
  67. labels = size_to_labels[size]
  68. print(f"{size}: {', '.join(map(str, labels))}")
  69. # === Plotting ===
  70. def get_color_map(n):
  71. return plt.cm.get_cmap('tab10' if n <= 10 else 'hsv', n)
  72. def plot_clusters(selected_labels, title_suffix="", wedge_scale=0.003, dot_size=350, background_img_path=None):
  73. cmap = get_color_map(len(selected_labels))
  74. label_to_color = {label: cmap(i) for i, label in enumerate(selected_labels)}
  75. clustered = df_global[df_global['label'].isin(selected_labels)].copy()
  76. x_vals = [x for (x, _) in coords_lookup.values()]
  77. y_vals = [y for (_, y) in coords_lookup.values()]
  78. x_range = max(x_vals) - min(x_vals)
  79. y_range = max(y_vals) - min(y_vals)
  80. wedge_r = wedge_scale * max(x_range, y_range)
  81. plt.figure(figsize=(8, 8))
  82. if background_img_path:
  83. img = imread(background_img_path)
  84. img_height, img_width = img.shape[:2]
  85. plt.imshow(np.swapaxes(img, 0, 1), extent=[0, img_width, img_height, 0], alpha=0.5)
  86. # Compute extent from chromatophore coordinates
  87. all_coords = []
  88. for label in selected_labels:
  89. chrom_ids = label_to_chroms.get(label, [])
  90. all_coords.extend([coords_lookup[cid] for cid in chrom_ids if cid in coords_lookup])
  91. if all_coords:
  92. all_coords = np.array(all_coords)
  93. x_min, x_max = np.min(all_coords[:, 0]), np.max(all_coords[:, 0])
  94. y_min, y_max = np.min(all_coords[:, 1]), np.max(all_coords[:, 1])
  95. # Add margin
  96. margin_x = 0.5 * (x_max - x_min)
  97. margin_y = 0.5 * (y_max - y_min)
  98. # Set axis limits (note y axis is top-down to match image)
  99. plt.xlim(x_min - margin_x, x_max + margin_x)
  100. plt.ylim(y_max + margin_y, y_min - margin_y)
  101. for label in selected_labels:
  102. chrom_ids = label_to_chroms.get(label, [])
  103. coords = np.array([coords_lookup[cid] for cid in chrom_ids])
  104. if len(coords) < 2:
  105. continue
  106. # Add label next to the cluster (at centroid)
  107. # centroid_x = np.mean(coords[:, 0])
  108. # centroid_y = np.mean(coords[:, 1])
  109. # plt.text(centroid_x, centroid_y, str(label), fontsize=8, ha='center', va='center', color='black', zorder=4)
  110. dist = distance_matrix(coords, coords)
  111. mst = minimum_spanning_tree(dist).toarray()
  112. for i in range(len(coords)):
  113. for j in range(len(coords)):
  114. if mst[i, j] > 0:
  115. plt.plot([coords[i, 0], coords[j, 0]], [coords[i, 1], coords[j, 1]],
  116. color=label_to_color[label], linewidth=1, alpha=0.6)
  117. for chrom_id, all_labels in chrom_to_labels.items():
  118. active_labels = list(all_labels & set(selected_labels))
  119. if not active_labels:
  120. continue
  121. x, y = coords_lookup[chrom_id]
  122. if len(active_labels) == 1:
  123. plt.scatter(x, y, color=label_to_color[active_labels[0]], s=dot_size, zorder=3)
  124. else:
  125. angle_step = 360 / len(active_labels)
  126. for i, lbl in enumerate(active_labels):
  127. wedge = Wedge(center=(x, y), r=wedge_r,
  128. theta1=i*angle_step, theta2=(i+1)*angle_step,
  129. facecolor=label_to_color[lbl], edgecolor='black', linewidth=0.1)
  130. plt.gca().add_patch(wedge)
  131. #plt.title(f"Chromatophore Clusters {title_suffix}")
  132. plt.xlabel("epicenter_x")
  133. plt.ylabel("epicenter_y")
  134. plt.gca().set_aspect('equal')
  135. # plt.grid(True, linestyle='--', alpha=0.3)
  136. plt.tight_layout()
  137. plt.show()
  138. def plot_clusters_by_labels(labels, wedge_scale=0.002, background_img_path=None):
  139. plot_clusters(labels, title_suffix=f"(labels: {', '.join(map(str, labels))})", wedge_scale=wedge_scale, background_img_path=background_img_path)
  140. def plot_clusters_by_size(size, wedge_scale=0.003, background_img_path=None):
  141. labels = [label for label, chroms in label_to_chroms.items() if len(chroms) == size]
  142. if not labels:
  143. print(f"❌ No clusters found with size {size}")
  144. else:
  145. print(f"🔍 Plotting all clusters with size {size}: {labels}")
  146. plot_clusters(labels, title_suffix=f"(size = {size})", wedge_scale=wedge_scale, background_img_path=background_img_path)
  147. # === Main NND + FND function with plotting and area filtering ===
  148. def compute_and_plot_global_nnd(csv_path, within_cluster_only=False, area_threshold_min=None, area_threshold_max=None, background_img_path=None):
  149. df = pd.read_csv(csv_path)
  150. df = df[['chrom_id', 'label', 'epicenter_y', 'epicenter_x']].dropna().drop_duplicates()
  151. if within_cluster_only:
  152. coords = df.groupby('chrom_id')[['epicenter_y', 'epicenter_x']].mean().reset_index()
  153. df = df[['chrom_id', 'label']].drop_duplicates().merge(coords, on='chrom_id')
  154. else:
  155. df = df.groupby('chrom_id')[['epicenter_y', 'epicenter_x']].mean().reset_index()
  156. if len(df) < 2:
  157. print("❌ Not enough valid chromatophores to compute NND.")
  158. return np.nan, [], []
  159. all_nnd = []
  160. all_furthest = []
  161. if within_cluster_only:
  162. print("🔎 Computing Nearest-Neighbor, Furthest Distances, and Areas within each cluster...")
  163. grouped = df.groupby('label')
  164. plt.figure(figsize=(8, 8))
  165. cmap = plt.cm.get_cmap('tab10', len(grouped))
  166. for label, group in grouped:
  167. coords = group[['epicenter_y', 'epicenter_x']].to_numpy()
  168. if len(coords) < 2:
  169. continue
  170. # your plotting code for links
  171. # Place label once per cluster
  172. centroid_x = np.mean(coords[:, 0])
  173. centroid_y = np.mean(coords[:, 1])
  174. plt.text(centroid_x, centroid_y, str(label), fontsize=8, ha='center', va='center', color='black', zorder=4)
  175. if background_img_path:
  176. img = imread(background_img_path)
  177. img_height, img_width = img.shape[0], img.shape[1]
  178. plt.imshow(img, extent=[0, img_width, img_height, 0], alpha=0.5)
  179. for i, (label, group) in enumerate(grouped):
  180. coords = group[['epicenter_y', 'epicenter_x']].to_numpy()
  181. if len(coords) < 2:
  182. continue
  183. dist_matrix = distance_matrix(coords, coords)
  184. dist_matrix_nnd = dist_matrix.copy()
  185. np.fill_diagonal(dist_matrix_nnd, np.inf)
  186. nnd = np.min(dist_matrix_nnd, axis=1)
  187. dist_matrix_furthest = dist_matrix.copy()
  188. np.fill_diagonal(dist_matrix_furthest, -np.inf)
  189. furthest = np.max(dist_matrix_furthest, axis=1)
  190. all_nnd.extend(nnd)
  191. all_furthest.extend(furthest)
  192. area = (np.max(coords[:, 0]) - np.min(coords[:, 0])) * (np.max(coords[:, 1]) - np.min(coords[:, 1]))
  193. if (area_threshold_min is None or area >= area_threshold_min) and (area_threshold_max is None or area <= area_threshold_max):
  194. mst = minimum_spanning_tree(dist_matrix).toarray()
  195. for a in range(len(coords)):
  196. for b in range(len(coords)):
  197. if mst[a, b] > 0:
  198. plt.plot([coords[a, 0], coords[b, 0]], [coords[a, 1], coords[b, 1]],
  199. color=cmap(i), linewidth=0.6, alpha=0.5)
  200. plt.scatter(coords[:, 0], coords[:, 1], s=15, label=f"Cluster {label} (area={area:.1f})",
  201. color=cmap(i), alpha=0.8)
  202. # print(f"Cluster {label} → Area: {area:.2f}, Mean NND: {np.mean(nnd):.2f}, Mean Furthest: {np.mean(furthest):.2f}")
  203. # plt.title("Filtered Clusters by Area Range")
  204. plt.xlabel("epicenter_x")
  205. plt.ylabel("epicenter_y")
  206. plt.gca().set_aspect('equal')
  207. # plt.grid(True, linestyle='--', alpha=0.3)
  208. # plt.legend()
  209. plt.tight_layout()
  210. plt.show()
  211. else:
  212. print("🌐 Computing Global Nearest-Neighbor and Furthest Distances...")
  213. coords = df[['epicenter_y', 'epicenter_x']].to_numpy()
  214. dist_matrix = distance_matrix(coords, coords)
  215. dist_matrix_nnd = dist_matrix.copy()
  216. np.fill_diagonal(dist_matrix_nnd, np.inf)
  217. nnd = np.min(dist_matrix_nnd, axis=1)
  218. dist_matrix_furthest = dist_matrix.copy()
  219. np.fill_diagonal(dist_matrix_furthest, -np.inf)
  220. furthest = np.max(dist_matrix_furthest, axis=1)
  221. nearest_indices = np.argmin(dist_matrix_nnd, axis=1)
  222. nearest_coords = coords[nearest_indices]
  223. all_nnd = nnd
  224. all_furthest = furthest
  225. plt.figure(figsize=(8, 8))
  226. if background_img_path:
  227. img = imread(background_img_path)
  228. img_height, img_width = img.shape[0], img.shape[1]
  229. plt.imshow(img, extent=[0, img_width, img_height, 0], alpha=0.5)
  230. plt.scatter(coords[:, 0], coords[:, 1], s=10, color='black', label='Chromatophores')
  231. for i in range(len(coords)):
  232. plt.plot([coords[i, 0], nearest_coords[i, 0]], [coords[i, 1], nearest_coords[i, 1]],
  233. color='lightgray', linewidth=0.5)
  234. # plt.title("Chromatophore Epicenters and Nearest Neighbors (Global)")
  235. plt.xlabel("epicenter_x")
  236. plt.ylabel("epicenter_y")
  237. plt.gca().set_aspect('equal')
  238. # plt.grid(True, linestyle='--', alpha=0.3)
  239. # plt.legend()
  240. plt.tight_layout()
  241. plt.show()
  242. all_nnd = np.array(all_nnd)
  243. global_nnd = np.mean(all_nnd)
  244. std_nnd = np.std(all_nnd)
  245. global_furthest = np.mean(all_furthest)
  246. std_furthest = np.std(all_furthest)
  247. print(f"✅ Mean NND ± SD: {global_nnd:.3f} ± {std_nnd:.3f} pixels")
  248. print(f"📏 Mean Furthest Distance ± SD: {global_furthest:.3f} ± {std_furthest:.3f} pixels")
  249. return global_nnd, all_nnd, all_furthest
  250. # === Area distribution using convex hull ===
  251. def plot_cluster_area_distribution():
  252. areas = []
  253. for label, chrom_ids in label_to_chroms.items():
  254. coords = np.array([coords_lookup[cid] for cid in chrom_ids if cid in coords_lookup])
  255. if len(coords) < 3:
  256. continue
  257. try:
  258. hull = ConvexHull(coords)
  259. area = hull.volume # for 2D, volume is the area enclosed
  260. areas.append(area)
  261. except:
  262. continue
  263. areas = np.array(areas)
  264. plt.figure(figsize=(8, 5))
  265. plt.hist(areas, bins=30, color='skyblue', edgecolor='black')
  266. # plt.title("Distribution of Cluster Convex Hull Areas (pixels²)")
  267. plt.xlabel("area [pixels²]")
  268. plt.ylabel("number of clusters")
  269. # plt.grid(True, linestyle='--', alpha=0.5)
  270. plt.tight_layout()
  271. plt.show()
  272. threshold_area = np.percentile(areas, 90)
  273. print(f"📐 90% of clusters have a convex hull area smaller than {threshold_area:.2f} px²")
  274. print(f"📈 Total clusters: {len(areas)}")
  275. print(f"📏 Mean area: {np.mean(areas):.2f} px²")
  276. print(f"🔺 Median area: {np.median(areas):.2f} px²")
  277. print(f"🔽 Min area: {np.min(areas):.2f} px²")
  278. print(f"🔼 Max area: {np.max(areas):.2f} px²")
  279. # === Density and Correlation Plots ===
  280. def plot_density_relationships():
  281. areas, chrom_counts, densities = [], [], []
  282. for label, chrom_ids in label_to_chroms.items():
  283. coords = np.array([coords_lookup[cid] for cid in chrom_ids if cid in coords_lookup])
  284. if len(coords) < 3:
  285. continue
  286. try:
  287. hull = ConvexHull(coords)
  288. area = hull.volume
  289. except:
  290. continue
  291. n_chroms = len(coords)
  292. density = n_chroms / area if area > 0 else 0
  293. areas.append(area)
  294. chrom_counts.append(n_chroms)
  295. densities.append(density)
  296. areas = np.array(areas)
  297. chrom_counts = np.array(chrom_counts)
  298. densities = np.array(densities)
  299. fig, axs = plt.subplots(1, 2, figsize=(12, 5))
  300. axs[0].scatter(areas, chrom_counts, c=densities, cmap='viridis', s=10)
  301. axs[0].set_xlabel("convex hull area [px²]")
  302. axs[0].set_ylabel("# chromatophores")
  303. axs[0].set_title("Chrom Count vs Area")
  304. axs[0].grid(True)
  305. axs[1].scatter(areas, densities, color='steelblue', s=10)
  306. axs[1].set_xscale('log')
  307. axs[1].set_yscale('log')
  308. axs[1].set_xlabel("convex hull area [px², log scale]")
  309. axs[1].set_ylabel("density [chroms/px², log scale]")
  310. axs[1].set_title("Density vs Area (Log-Log)")
  311. axs[1].grid(True, which='both', linestyle='--', alpha=0.5)
  312. plt.tight_layout()
  313. plt.show()
  314. print("📊 Correlation coefficients:")
  315. print(f"Chrom count ↔ Area: r = {pearsonr(chrom_counts, areas)[0]:.2f}")
  316. print(f"Area ↔ Density: r = {pearsonr(areas, densities)[0]:.2f}")
  317. print(f"Chrom count ↔ Density: r = {pearsonr(chrom_counts, densities)[0]:.2f}")
  318. # csv_path = '/gpfs/laur/data/renardm/output_dir/holiday_runs/240823/GLC-06560-headfix/240823-008/ics_chunk_0_chrom_all.csv'
  319. csv_path = '/your/path/ics_chunk_1_chrom_all.csv'
  320. load_data(csv_path)
  321. # list_clusters()
  322. plot_clusters_by_labels([42, 400, 246, 150, 650, 660, 227, 228, 211, 304 ], background_img_path='/your/path/240823-024_chunk_1_frame_1.png')
  323. plot_clusters_by_size(9, background_img_path='/your/path/240823-024_chunk_1_frame_1.png')
  324. #compute_and_plot_global_nnd(csv_path)
  325. plot_cluster_area_distribution()
  326. plot_density_relationships()

mu_structure.py at commit a1fe9de, under BSD-3-Clause · at the source

Overview

Authors: Mathieu DM Renard1, Johann Ukrow1, Margot Elmaleh1, Dominic A Evans1, Yifan Wu2, Xitong Liang2,3,4,5, Gilles Laurent1
  1. Max Planck Institute for Brain Research, Frankfurt, Germany
  2. IDG/McGovern Institute for Brain Research, Peking University, Beijing, China
  3. Peking-Tsinghua Center for Life Sciences, Academy for Advanced Interdisciplinary Studies, Peking University, Beijing, China
  4. State Key Laboratory of Membrane Biology, School of Life Sciences, Peking University, Beijing, China
  5. Center for Quantitative Biology, Academy for Advanced Interdisciplinary Studies, Peking University, Beijing, China
Journal: eLife, volume 15, article RP110074
Dates: published online 8 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.7554/elife.110074 · PMID 42418335 · PMCID PMC13345625 · OpenAlex W7127351597
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: other (organism)
Methods: Statistics, Smoothing, state filtering, decompositions, Preprocessing, Connectivity, Machine learning, fMRI & imaging, Physiology & signal measures
Keywords: Other
MeSH: Cephalopoda*, Chromatophores*, Motor Neurons*, Animals (* major topic)
Topic: Cephalopods and Marine Biology (Ecology, Evolution, Behavior and Systematics, Agricultural and Biological Sciences), according to OpenAlex
Funding: National Natural Science Foundation of China (32371215); European Research Council (10114150)
Citations: cited by 1 paper (Europe PMC); 42 references in the paper
Research resources: scikit-learn (1.6.1) RRID:SCR_002577, Python (3.9+) RRID:SCR_008394, Matplotlib (3.10.0) RRID:SCR_008624, Numpy RRID:SCR_008633, Pandas RRID:SCR_018214, P-1000, Sutter Instrument RRID:SCR_021042

Abstract

Cephalopod chromatophores are skin pigment organs enabling rapid, neurally controlled camouflage, yet the organization of their motor control remains poorly understood. Previously, we developed CHROMAS, a computer-vision pipeline for high-resolution analysis of chromatophore dynamics (Ukrow et al., 2025). Here, we apply it to investigate motor control and innervation in Euprymna berryi and Sepia officinalis. By segmenting chromatophores into radial slices and analyzing anisotropic deformations, we used dimensionality reduction and source separation to estimate the number and spatial influence of motor neurons controlling individual chromatophores and groups thereof. On average, four independent components were detected per chromatophore, each forming contiguous petal-shaped domains. Clustering thousands of components revealed motor units spanning multiple chromatophores, most involving fewer than 14, with diverse geometries ranging from compact local groups to elongated or fragmented structures; chromatophore pairs were co-innervated more often than expected by chance. Expansion was consistently faster and more stereotyped than relaxation, consistent with active contraction and passive recoil. These results show that chromatophores are not uniform pixels but contrast elements fractionable into sub-territories coordinated across neighbors. This geometry of neural control enables the generation of ‘virtual chromatophores’, that is, functional groupings of adjacent chromatophore territories that act as single units, as well as that of noise in the distribution of pixel shapes.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 21 matches between paragraphs and lines of code.

gitlab.mpcdf.mpg.de/mpibr/laur/cuttlefish/chromas

License: BSD-3-Clause
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: a1fe9de3c592f020697ba60ce328671cf1c1cd8d, 24 November 2025
Languages: Python (63), JavaScript (6), C (1)
Size: 152 files, 70 scripts
Software Heritage: not archived
Found in: the references
Holds: README, license file, environment (poetry.lock, pyproject.toml, motor_units/requirements.txt, chromas/source/moving_least_squares/pyproject.toml, chromas/source/moving_least_squares/setup.py), tests, continuous integration, documentation
Not found: CITATION.cff
Tools: NumPy (43 files), Matplotlib (32 files), OpenCV (20 files), scikit-image (19 files), xarray (19 files), SciPy (15 files), pandas (8 files), scikit-learn (8 files), PyTorch (7 files), Pillow (6 files), NetworkX (1 file), Plotly (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
72 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 70 scripts, each with its path and the digest of its content;
  • 21 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data availability

The image analysis software CHROMAS is distributed via the pypi package index (https://pypi.org/project/chromas/) and is publicly released on GitLab (https://doi.org/10.17617/1.pa38-mh49; Ukrow and Renard, 2025) under the 3-Clause BSD License. The documentation is hosted on GitLab. The data used to train the segmentation models, the trained models, and example videos for the tutorial can be found at https://public.brain.mpg.de/Laurent/Chromas2025/. Code and example datasets used for the biological analyses reported in this study are provided in the “motor_units” folder at https://public.brain.mpg.de/ (under the project directory named after this paper). Electrophysiology videos and trace data are provided in a separate folder named “ephys”.

The following dataset was generated:

Renard MDM, Ukrow J, Elmaleh M, Evans DA, Wu Y, Liang X, Laurent G. 2026. Disentangling cephalopod chromatophores motor units with computer vision. Edmond.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 7 authors, 1 keyword, 4 MeSH terms, 2 funders, 36 references, 6 RRIDs.

Cite

This paper

Renard, M. D., Ukrow, J., Elmaleh, M., Evans, D. A., Wu, Y., Liang, X., & Laurent, G. (2026). Disentangling cephalopod chromatophores motor units with computer vision. eLife, 15, RP110074. https://doi.org/10.7554/elife.110074

BibTeX

@article{renard2026disentangling,
author = {Renard, Mathieu DM and Ukrow, Johann and Elmaleh, Margot and Evans, Dominic A and Wu, Yifan and Liang, Xitong and Laurent, Gilles},
title = {{Disentangling cephalopod chromatophores motor units with computer vision}},
journal = {eLife},
year = {2026},
month = jul,
volume = {15},
pages = {RP110074},
publisher = {eLife Sciences Publications, Ltd},
issn = {2050-084X},
doi = {10.7554/elife.110074},
url = {https://doi.org/10.7554/elife.110074},
pmid = {42418335},
pmcid = {PMC13345625}
}

RIS

TY - JOUR
AU - Renard, Mathieu DM
AU - Ukrow, Johann
AU - Elmaleh, Margot
AU - Evans, Dominic A
AU - Wu, Yifan
AU - Liang, Xitong
AU - Laurent, Gilles
TI - Disentangling cephalopod chromatophores motor units with computer vision
T2 - eLife
J2 - eLife
PY - 2026
DA - 2026/07/08
VL - 15
SP - RP110074
SN - 2050-084X
PB - eLife Sciences Publications, Ltd
DO - 10.7554/elife.110074
UR - https://doi.org/10.7554/elife.110074
LA - en
ER -

CSL-JSON

{
"id": "10.7554/elife.110074",
"type": "article-journal",
"title": "Disentangling cephalopod chromatophores motor units with computer vision",
"container-title": "eLife",
"author": [
{
"family": "Renard",
"given": "Mathieu DM"
},
{
"family": "Ukrow",
"given": "Johann"
},
{
"family": "Elmaleh",
"given": "Margot"
},
{
"family": "Evans",
"given": "Dominic A"
},
{
"family": "Wu",
"given": "Yifan"
},
{
"family": "Liang",
"given": "Xitong"
},
{
"family": "Laurent",
"given": "Gilles"
}
],
"container-title-short": "eLife",
"volume": "15",
"page": "RP110074",
"DOI": "10.7554/elife.110074",
"PMID": "42418335",
"PMCID": "PMC13345625",
"ISSN": "2050-084X",
"publisher": "eLife Sciences Publications, Ltd",
"URL": "https://doi.org/10.7554/elife.110074",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
8
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41593-026-02232-0 [code]
Entorhinal cortex represents task-relevant remote locations independently of CA1.
Journal: Nature neuroscience
In common: xarray, NetworkX, OpenCV, 8 other tools
[2] doi:10.7554/elife.107393 [code]
Chromosome-scale genome assembly of the European common cuttlefish &lt;i&gt;Sepia officinalis&lt;/i&gt;.
Journal: eLife
In common: pandas, SciPy, Matplotlib, 1 other tool, other, 2 references, author Gilles Laurent
[3] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: NetworkX, Plotly, OpenCV, 8 other tools
[4] doi:10.1016/j.isci.2026.116825 [code]
Social hierarchy shapes behavioral and transcriptional responses to chronic stress and ketamine in male mice.
Journal: iScience
In common: NetworkX, Plotly, OpenCV, 8 other tools
[5] doi:10.1038/s41598-026-57519-w [code]
Automated segmentation of neurons and spinal cord structures in immunofluorescence images using SpineDL.
Journal: Scientific reports
In common: NetworkX, Plotly, OpenCV, 8 other tools
[6] doi:10.3389/fnsys.2026.1822122 [code]
Convergence-divergence circuits for multimodal integration of innate and learned opponent valences.
Journal: Frontiers in systems neuroscience
In common: NetworkX, Plotly, scikit-image, 7 other tools, 1 reference
[7] doi:10.1371/journal.pcbi.1013441 [code]
Large vision model framework for automated C. elegans analysis: From static morphometry to dynamic neural activity.
Journal: PLoS computational biology
In common: NetworkX, OpenCV, scikit-image, 6 other tools, 2 references
[8] doi: [code]
Naturalistic behavior and self-generated neural activity predictive of self-correction
Journal: bioRxiv : the preprint server for biology
In common: xarray, NetworkX, OpenCV, 7 other tools
[9] doi:10.1038/s41467-026-76045-x [code]
A manufacturability-informed topology framework for AI-guided design of fibrous network materials.
Journal: Nature communications
In common: NetworkX, OpenCV, Pillow, 6 other tools, 2 references
[10] doi:10.1038/s41467-026-72057-9 [code]
Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior.
Journal: Nature communications
In common: xarray, OpenCV, scikit-image, 7 other tools

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.