OSCR

A dataset of fine-grained zebrafish interactions in health and disease.

Code ↔ Paper

11 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 11 matches · 1 of them tie a paragraph to a whole file, not to given lines: a weak match, whose lines are not tinted
  1. [1] § Technical Validation › Quality assurance ↔ base_workflow/track_experiment.py, lines 361–368 · score 0.79 · pec tail distance, head pec distance, pec tail threshold, head pec threshold, tracking experiment
  2. [2] § Methods › Postprocessing ↔ lib/sleap_idtracker_merge/post_processing.py, lines 42–119 · score 0.76 · Savitzky Golay filter, post processing, body point, gaps, velocity, window
  3. [3] § Methods › Experimental set-up ↔ campy/campy.py, lines 1–25 · score 0.72 · machine vision cameras, cameras recorded, campy, compressed, PYTHON, FFmpeg
  4. [4] § Methods › Postprocessing ↔ lib/tracking/fix_swaps.py, lines 4–48 · score 0.66 · Single fish jump, body point jump, swap, velocity, window, frames
  5. [5] § Methods › 3D posture tracking ↔ lib/tracking/tracking_functions.py, the whole file · a weak match · score 0.62 · best permutation, SLEAP skeleton, single frame, matching, thresholds, body
  6. [6] § Technical Validation › Quality assurance ↔ lib/sleap_idtracker_merge/threshold_filters.py, lines 43–80 · score 0.62 · pec tail threshold, head pec threshold, distance, skeletons, idtracker, SLEAP
  7. [7] § Methods › 3D posture tracking ↔ base_workflow/track_experiment.py, lines 468–537 · score 0.60 · idtracker identities, pec point, centroid, match, XY, skeleton
  8. [8] § Methods › Experimental set-up ↔ campy/configurator.py, lines 9–61 · score 0.59 · campy, NVIDIA, GPU, codec, compressed, Arduino
  9. [9] § Methods › 3D posture tracking ↔ base_workflow/track_experiment.py, lines 1–51 · score 0.56 · cross camera registration, SLEAP detections, match, skeletons, identities, frame
  10. [10] § Methods › Calibration ↔ base_workflow/make_calibration_models.py, lines 1–41 · score 0.54 · calibration board, polynomial, XYZ, XZ, modeled, camera
  11. [11] § Methods › 3D posture tracking ↔ base_workflow/track_experiment.py, lines 361–368 · score 0.54 · Pec tail distance, Head pec distance, thresholds, tracking

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 · 749 lines · 35 KB · MIT · 4 matches

  1. """
  2. 3D Fish Tracking Experiment
  3. This script processes SLEAP and idTracker.ai data to create 3D tracks of fish movement.
  4. The workflow includes:
  5. 1. Cross-camera registration of SLEAP detections
  6. 2. Applying registration and skeleton size thresholds
  7. 3. Matching idTracker.ai identities with 3D skeletons
  8. 4. Tracking segments in 3D
  9. 5. Post-processing trajectories (cleaning, interpolation, smoothing)
  10. 6. Saving results to H5 and CSV files
  11. Usage:
  12. python track_experiment.py <expName> <mean_reg_skel_thresh> <idtracks_to_sleap_thresh> <start_frame_in_video> [options]
  13. Arguments:
  14. expName: Name of the experiment
  15. mean_reg_skel_thresh: Threshold for skeleton registration
  16. idtracks_to_sleap_thresh: Threshold for matching idTracker.ai with SLEAP
  17. start_frame_in_video: Start frame in the video
  18. """
  19. import argparse
  20. import os
  21. import time
  22. from multiprocessing import Pool, RawArray
  23. import h5py
  24. import numpy as np
  25. from matplotlib import pyplot as plt
  26. from matplotlib.cbook import contiguous_regions
  27. from scipy.optimize import linear_sum_assignment
  28. from lib.calibration.calibration import Calibration
  29. from lib.registration.skeleton_registration_costs import plot_registration_costs
  30. from lib.sleap_idtracker_merge.plot_utils import plot_idtracker_sleap_assignment_costs, plot_bodypoints_distances
  31. from lib.sleap_idtracker_merge.post_processing import interpolate_over_small_gaps, \
  32. get_smooth_timeseries_and_derivatives_using_savgol
  33. from lib.sleap_idtracker_merge.track_3d import track_segment_in_3D_if_possible
  34. from lib.sleap_idtracker_merge.various import prepend_nan_to_idtracker_data, prepend_nan_to_results_array, \
  35. prepend_nan_to_im_coords_array
  36. from lib.tracking.data_cleaning import clean_data_from_jumps
  37. from lib.tracking.frame_registration import find_mean_bodypoint_registration_costs_array
  38. from lib.tracking.io import save_tracks_3D_to_csv_and_return_dataFrame
  39. from lib.tracking.track_utils import create_array_of_start_stop_frames_for_parallelization
  40. from lib.tracking.tracking_functions import get_3D_positions_from_sleap_imcoords
  41. from lib.tracking_quality.tracking_quality import get_percentage_frames_both_fish_full_info, \
  42. get_percentage_frames_no_info, get_percentage_frames_no_idtracker_info
  43. from lib.various.filesystem import find_calibration_folder
  44. from lib.various.geometric import get_distances_from_locations_array
  45. # Constants for paths
  46. BUCKET_BASE_PATH = "/bucket/StephensU/fish3D"
  47. RESULTS_TRACKING_PATH = "./results/tracking"
  48. RESULTS_INFERENCE_PATH = "./results/inference"
  49. # -------------------- Parse Arguments -------------------- #
  50. parser = argparse.ArgumentParser()
  51. parser.add_argument("expName", type=str)
  52. parser.add_argument("mean_reg_skel_thresh", type=float)
  53. parser.add_argument("idtracks_to_sleap_thresh", type=float)
  54. parser.add_argument("start_frame_in_video", type=int)
  55. parser.add_argument("--head_pec_thresh", type=float, default=1.4)
  56. parser.add_argument("--pec_tail_thresh", type=float, default=2.5)
  57. parser.add_argument("--pair_swap_velocity_threshold", type=float, default=30)
  58. parser.add_argument("--parStep", type=int, default=1000)
  59. parser.add_argument("--numProcessors", type=int, default=20)
  60. parser.add_argument("--interp_polyOrd", type=int, default=1)
  61. parser.add_argument("--interp_limit", type=int, default=7)
  62. parser.add_argument("--savgol_win", type=int, default=9)
  63. parser.add_argument("--savgol_ord", type=int, default=2)
  64. parser.add_argument("--fps", type=int, default=140)
  65. args = parser.parse_args()
  66. print()
  67. print('------- Tracking {0} -------'.format(args.expName))
  68. print()
  69. print('-- inputs --')
  70. print('expName: {0}'.format(args.expName))
  71. print('parStep: {0}'.format(args.parStep))
  72. print('numProcessors: {0}'.format(args.numProcessors))
  73. print('mean_reg_skel_thresh: {0}'.format(args.mean_reg_skel_thresh))
  74. print('head_pec_thresh: {0}'.format(args.head_pec_thresh))
  75. print('pec_tail_thresh: {0}'.format(args.pec_tail_thresh))
  76. print('idtracks_to_sleap_thresh: {0}'.format(args.idtracks_to_sleap_thresh))
  77. print('interp_polyOrd: {0}'.format(args.interp_polyOrd))
  78. print('interp_limit: {0}'.format(args.interp_limit))
  79. print('savgol_win: {0}'.format(args.savgol_win))
  80. print('savgol_ord: {0}'.format(args.savgol_ord))
  81. print('fps: {0}'.format(args.fps))
  82. print('start frame in video: {0}'.format(args.start_frame_in_video))
  83. print('pair swap velocity threshold: {0}'.format(args.pair_swap_velocity_threshold))
  84. print()
  85. # -------------------- Setup Paths and Parameters -------------------- #
  86. input_path = os.path.join(RESULTS_TRACKING_PATH, "sleap_and_idtracker_data.h5")
  87. saveFile_h5_path = os.path.join(RESULTS_TRACKING_PATH, "full_output.h5")
  88. saveFile_csv_path = os.path.join(RESULTS_TRACKING_PATH, "tracks.csv")
  89. saveFile_aligned_h5 = os.path.join(RESULTS_TRACKING_PATH, "tracks.h5")
  90. prepend_results_path = RESULTS_TRACKING_PATH
  91. dt = 1 / args.fps
  92. main_recordings_folder = os.path.join(BUCKET_BASE_PATH, args.expName)
  93. calibration_sub_folder_path = find_calibration_folder(main_recordings_folder)
  94. calibration_folder_path = os.path.join(calibration_sub_folder_path, "auto_calibration_results")
  95. movie_filepath_xy = os.path.join(main_recordings_folder, "xy", "fishfight.mp4")
  96. movie_filepath_xz = os.path.join(main_recordings_folder, "xz", "fishfight.mp4")
  97. movie_filepath_yz = os.path.join(main_recordings_folder, "yz", "fishfight.mp4")
  98. movie_filepaths = [movie_filepath_xz, movie_filepath_xy, movie_filepath_yz]
  99. # -------------------- Load Tracking Data -------------------- #
  100. with h5py.File(input_path, 'r') as hf:
  101. sleap_data = hf['sleap_data'][:]
  102. idtracker_data = hf['idtracker_data'][:]
  103. # sleap_data = sleap_data[:, 0:260000]
  104. # parse some shapes
  105. numCams, numFrames, numFish, numBodyPoints, coordinates = sleap_data.shape
  106. print("The shape of SLEAP data is: {0}".format(sleap_data.shape))
  107. print("The shape of idtracker data is: {0}".format(idtracker_data.shape))
  108. # -------------------- Initialize Calibration -------------------- #
  109. cal = Calibration(calibration_folder_path)
  110. # -------------------- Setup Parallelization -------------------- #
  111. # parse frames up into chunks
  112. parallelization_start_stop_frms = create_array_of_start_stop_frames_for_parallelization(numFrames,
  113. step=args.parStep)
  114. # the list of jobIdxs to map over
  115. job_idxs = [i for i in range(parallelization_start_stop_frms.shape[0])]
  116. ##############################
  117. # We are going to use a trick with a global dictionary
  118. # to allow multiple processes to access the same array in parallel.
  119. # This does require some uglyness/boilerplate,
  120. # which is contained below
  121. # First, define an instrically parallel function for mapping over,
  122. # to cross calibrate different chunks of frames in parallel.
  123. # This function will want access to the global dictionary
  124. def get_imcoord_positions_and_3D_positions_parallel(i):
  125. ''' Perform the cross camera registration for a section of frames.
  126. NB: THIS FUNCTION IS INTRINSICALLY PARALLEL
  127. It needs to be run with the below parallel boilerplate code.
  128. See: register_sleap_instances() in the lib file,
  129. for a non-parallel version of this function, which can be used
  130. without the global var_dict
  131. --- args ---
  132. i: int, an index used for parsing lists and global arrays to get the data
  133. for this particular process
  134. --- returns ---
  135. frame_methodIdxs : array, shape (numFrames,),
  136. the methodIdx of the method used to solve the frame
  137. frame_positions_imageCoordinates : array, shape (numCams, numFrames, numFish, numBodyPoints, 2),
  138. the image coordinates of the skeletons registered across cams
  139. frame_positions_3DBps : array, shape (numFrames, numFish, numBodyPoints, 3),
  140. the 3D coordinates of the skeletons registered across cams
  141. frame_registration_costs : array, shape (numFrames, numFish, numBodyPoints),
  142. the cross-camera registration costs for the frame
  143. '''
  144. # make the calibration object
  145. calOb = Calibration(var_dict['calibrationFolderPath'])
  146. # get numpy versions of the idTracker data
  147. complete_sleap_data = np.frombuffer(var_dict['sleap_data']).reshape(var_dict['sleap_data_shape'])
  148. # parse the inputs
  149. numCams, numFrames, numFish, numBodyPoints, _ = complete_sleap_data.shape
  150. # make the parsing array
  151. parStep = var_dict['parStep']
  152. start_stop_frms = create_array_of_start_stop_frames_for_parallelization(numFrames, step=parStep)
  153. # get the data for this jobID
  154. jobF0, jobFE = start_stop_frms[i]
  155. jobNumFrames = jobFE - jobF0
  156. job_sleap_data = complete_sleap_data[:, jobF0:jobFE]
  157. # ----- preallocate -------#
  158. frame_methodIdxs = np.ones((jobNumFrames,)) * np.NaN
  159. frame_positions_imageCoordinates = np.ones((numCams, jobNumFrames, numFish, numBodyPoints, 2)) * np.NaN
  160. frame_positions_3DBps = np.ones((jobNumFrames, numFish, numBodyPoints, 3)) * np.NaN
  161. frame_registration_costs = np.ones((jobNumFrames, numFish, numBodyPoints)) * np.NaN
  162. for fIdx in range(jobNumFrames):
  163. # get the frame loaded_imcoords
  164. frame_instances = np.copy(job_sleap_data[:, fIdx, :, :, :])
  165. # register the skeletons across camera views
  166. methodIdx, \
  167. positions_imageCoordinates, \
  168. positions_3DBps, \
  169. positions_registration_costs, \
  170. debug_vals = get_3D_positions_from_sleap_imcoords(frame_instances, calOb, debug=False)
  171. # record
  172. frame_methodIdxs[fIdx] = methodIdx
  173. frame_positions_imageCoordinates[:, fIdx] = positions_imageCoordinates
  174. frame_positions_3DBps[fIdx] = positions_3DBps
  175. frame_registration_costs[fIdx] = positions_registration_costs
  176. # finish up
  177. return frame_methodIdxs, frame_positions_imageCoordinates, frame_positions_3DBps, frame_registration_costs
  178. # Now make the global dictionary
  179. # This is a formality to allow multiple processes to share access to arrays
  180. sleap_data_RARR = RawArray('d', int(np.prod(sleap_data.shape)))
  181. sleap_data_np = np.frombuffer(sleap_data_RARR).reshape(sleap_data.shape)
  182. np.copyto(sleap_data_np, sleap_data)
  183. # A global dictionary storing the variables passed from the initializer
  184. # This dictionary holds variables that each process will share access to, instead of making copies
  185. var_dict = {}
  186. # This function initializes the shared data in each job process
  187. def init_worker(sleap_data, sleap_data_shape, calibrationFolderPath, parStep, mean_reg_skel_thresh):
  188. var_dict['sleap_data'] = sleap_data
  189. var_dict['sleap_data_shape'] = sleap_data_shape
  190. var_dict['calibrationFolderPath'] = calibrationFolderPath
  191. var_dict['parStep'] = parStep
  192. var_dict['mean_reg_skel_thresh'] = mean_reg_skel_thresh
  193. ##############################
  194. # -------------------- Perform Cross-Camera Registration -------------------- #
  195. t0 = time.time()
  196. print()
  197. print('Launching cross-camera registration...')
  198. # map the function
  199. with Pool(processes=args.numProcessors, initializer=init_worker,
  200. initargs=(sleap_data_RARR,
  201. sleap_data.shape,
  202. calibration_folder_path,
  203. args.parStep,
  204. args.mean_reg_skel_thresh)) as pool:
  205. outputs = pool.map(get_imcoord_positions_and_3D_positions_parallel, job_idxs)
  206. # ---- parse the output ---- #
  207. methodIdxs = []
  208. positions_imageCoordinates = []
  209. positions_3D = []
  210. registration_costs = []
  211. for job_results in outputs:
  212. job_frame_methodIdxs, job_frame_positions_imageCoordinates, job_frame_positions_3DBps, job_frame_registration_costs = job_results
  213. methodIdxs.append(job_frame_methodIdxs)
  214. positions_imageCoordinates.append(job_frame_positions_imageCoordinates)
  215. positions_3D.append(job_frame_positions_3DBps)
  216. registration_costs.append(job_frame_registration_costs)
  217. methodIdxs = np.concatenate(methodIdxs)
  218. positions_imageCoordinates = np.concatenate(positions_imageCoordinates, axis=1)
  219. positions_3D = np.concatenate(positions_3D, axis=0)
  220. registration_costs = np.concatenate(registration_costs)
  221. # ----- finish up -----#
  222. print()
  223. print(f'Cross-camera registration completed in {time.time() - t0:.2f} seconds')
  224. # -- preallocate main outputs -- #
  225. positions_imageCoordinates_processed = np.copy(positions_imageCoordinates)
  226. positions_3D_processed = np.copy(positions_3D)
  227. tracks_3D = np.ones_like(positions_3D_processed) * np.NaN
  228. tracks_imCoords = np.ones_like(positions_imageCoordinates_processed) * np.NaN
  229. # -------- Step 1 ------------#
  230. # Apply the reg_thresh to positions
  231. print('Applying the registration threshold...')
  232. # find the registration costs averaged along bodypoints
  233. registration_costs_mean_bp = find_mean_bodypoint_registration_costs_array(registration_costs)
  234. # find a mask, which shows 'True' frame-fish pairs that don't pass the threshold
  235. bad_registration_mask = registration_costs_mean_bp > args.mean_reg_skel_thresh
  236. # use the mask to NaN positions which don't pass the threshold
  237. # (reg_thresh_removal_info will keep track of which fish in which frame we discard)
  238. reg_thresh_removal_info = []
  239. for fIdx in range(positions_3D.shape[0]):
  240. for fishIdx in range(positions_3D.shape[1]):
  241. if bad_registration_mask[fIdx, fishIdx]:
  242. reg_thresh_removal_info.append([fIdx, fishIdx])
  243. positions_imageCoordinates_processed[:, fIdx, fishIdx, :, :] = np.NaN
  244. positions_3D_processed[fIdx, fishIdx, :, :] = np.NaN
  245. reg_thresh_removal_info = np.array(reg_thresh_removal_info)
  246. tE = time.time()
  247. print(f'Registration threshold applied in {tE - t0:.2f} seconds')
  248. # save cross-registration costs as a figure (only for frames where fish are distant)
  249. distances = get_distances_from_locations_array(sleap_data, 0)
  250. index_xz = distances[0, :] > 100
  251. index_xy = distances[1, :] > 100
  252. index_yz = distances[2, :] > 100
  253. long_distances_index = np.logical_and(np.logical_and(index_xz, index_xy), index_yz)
  254. all_registration_costs_mean_distant = registration_costs_mean_bp[long_distances_index]
  255. fig = plot_registration_costs(all_registration_costs_mean_distant, max_x=40,
  256. mean_reg_skel_thresh=args.mean_reg_skel_thresh)
  257. plt.savefig(os.path.join(prepend_results_path, 'all_registration_costs_100_distant.jpg'), dpi=300)
  258. plt.show()
  259. # -------- Step 2 ------------#
  260. # apply the size thresholds to positions
  261. print('Applying the skeleton 3D size threshold...')
  262. # Calculate the sizes
  263. head_pec_dists = np.ones((numFrames, numFish)) * np.NaN
  264. pec_tail_dists = np.ones((numFrames, numFish)) * np.NaN
  265. for fishIdx in range(numFish):
  266. for fIdx in range(numFrames):
  267. fishData = np.copy(positions_3D_processed[fIdx, fishIdx])
  268. head_pec_dists[fIdx, fishIdx] = np.linalg.norm(fishData[0] - fishData[1])
  269. pec_tail_dists[fIdx, fishIdx] = np.linalg.norm(fishData[1] - fishData[2])
  270. # remove any fish that are too big
  271. size_thresh_removal_info = []
  272. for fIdx in range(positions_3D.shape[0]):
  273. for fishIdx in range(positions_3D.shape[1]):
  274. if head_pec_dists[fIdx, fishIdx] > args.head_pec_thresh:
  275. size_thresh_removal_info.append([fIdx, fishIdx, 0])
  276. positions_imageCoordinates_processed[:, fIdx, fishIdx, :, :] = np.NaN
  277. positions_3D_processed[fIdx, fishIdx, :, :] = np.NaN
  278. elif pec_tail_dists[fIdx, fishIdx] > args.pec_tail_thresh:
  279. size_thresh_removal_info.append([fIdx, fishIdx, 1])
  280. positions_imageCoordinates_processed[:, fIdx, fishIdx, :, :] = np.NaN
  281. positions_3D_processed[fIdx, fishIdx, :, :] = np.NaN
  282. else:
  283. continue
  284. size_thresh_removal_info = np.array(size_thresh_removal_info)
  285. frames_removed_size_threshold = len([element[0] for element in size_thresh_removal_info])
  286. print(f"Total frames removed due to fish size threshold: {frames_removed_size_threshold}")
  287. # plot stats for head-pec and pec-tail distances
  288. fig = plot_bodypoints_distances(head_pec_dists, args.head_pec_thresh, title="Head-pec distances")
  289. plt.savefig(os.path.join(prepend_results_path, 'head_pec_distances.jpg'), dpi=300)
  290. fig = plot_bodypoints_distances(pec_tail_dists, args.pec_tail_thresh, title="Pec-tail distances")
  291. plt.savefig(os.path.join(prepend_results_path, 'pec_tail_distances.jpg'), dpi=300)
  292. tE = time.time()
  293. print(f'Size threshold applied in {tE - t0:.2f} seconds')
  294. # -------- Step 3 ------------#
  295. # Match idTracker info with 3D skeletons to start the trajectories array
  296. # In this step, we only work with frames where we have pec points and idtracks for all fish
  297. print('Performing Hungarian matching of 3D skeletons with idTracker.ai identities...')
  298. # make an array to hold information on if data has been assigned after this step
  299. tracks_available_post_initial_id_assignments = np.zeros((numFrames, numFish), dtype=int)
  300. # find frames with both idTracker.ai centroids for all fish, and xy cam pec image coordinates for both fish
  301. fIdxs_with_idtracker_info_for_all_fish = np.where(~np.any(np.isnan(idtracker_data), axis=(1, 2)))[0]
  302. xyCamIdx = 1
  303. pecBpIdx = 1
  304. fIdxs_with_xyPec_info_for_all_fish = \
  305. np.where(~np.any(np.isnan(positions_imageCoordinates_processed[xyCamIdx, :, :, pecBpIdx, :]),
  306. axis=(1, 2))
  307. )[0]
  308. fIdxs_with_idtracker_and_xyPec_info_for_all_fish = np.intersect1d(fIdxs_with_idtracker_info_for_all_fish,
  309. fIdxs_with_xyPec_info_for_all_fish)
  310. # do the idtracker.ai <-> sLEAP matching to assign identities for these frame
  311. # Note: registrations must pass the threshold
  312. # this array will keep track of costs of assignments that we make
  313. idtracker_sleap_assignment_costs = np.zeros(
  314. (fIdxs_with_idtracker_and_xyPec_info_for_all_fish.shape[0], numFish)) * np.NaN
  315. idtracker_sleap_assignment_nothresh_costs = np.zeros(
  316. (fIdxs_with_idtracker_and_xyPec_info_for_all_fish.shape[0], numFish)) * np.NaN
  317. # test all frames with idtracks and xypecs
  318. for ii, fIdx in enumerate(fIdxs_with_idtracker_and_xyPec_info_for_all_fish):
  319. # get the image coordinates of interest
  320. frame_pec_points = positions_imageCoordinates_processed[xyCamIdx, fIdx, :, pecBpIdx, :]
  321. frame_idTracker_centroids = idtracker_data[fIdx, :]
  322. # create the cost matrix for the hungarian sorting
  323. frame_cost_mat = np.zeros((numFish, numFish))
  324. for idtracker_idx in range(numFish):
  325. for sleap_idx in range(numFish):
  326. frame_cost_mat[idtracker_idx, sleap_idx] = np.linalg.norm(frame_idTracker_centroids[idtracker_idx] -
  327. frame_pec_points[sleap_idx])
  328. # sort the cost matrix record the identity assignment for this frame
  329. row_ind, col_ind = linear_sum_assignment(frame_cost_mat)
  330. # record the identity assignment, and cost, for this frame if they past the threshold
  331. for fishIdx in range(numFish):
  332. id_assignment_cost = frame_cost_mat[row_ind, col_ind][fishIdx]
  333. # record this, for debugging purposes
  334. idtracker_sleap_assignment_nothresh_costs[ii, fishIdx] = id_assignment_cost
  335. if id_assignment_cost < args.idtracks_to_sleap_thresh:
  336. # update the tracks arrays
  337. tracks_3D[fIdx, fishIdx] = np.copy(positions_3D_processed[fIdx, col_ind[fishIdx]])
  338. tracks_imCoords[:, fIdx, fishIdx] = np.copy(positions_imageCoordinates_processed[:, fIdx, col_ind[fishIdx]])
  339. # record some other data
  340. tracks_available_post_initial_id_assignments[fIdx, fishIdx] = 1
  341. idtracker_sleap_assignment_costs[ii, fishIdx] = id_assignment_cost
  342. else:
  343. continue
  344. fig = plot_idtracker_sleap_assignment_costs(idtracker_sleap_assignment_nothresh_costs,
  345. threshold=args.idtracks_to_sleap_thresh)
  346. plt.savefig(os.path.join(prepend_results_path, 'idtracker_sleap_assignment_costs.jpg'), dpi=300)
  347. tE = time.time()
  348. print(f'Hungarian matching completed in {tE - t0:.2f} seconds')
  349. # -------- Step 4 ------------#
  350. # Match idTracker info with 3D skeletons in all other frames where possible.
  351. # In these frames, we cant do a hungarian sorting, because we don't 2 xypec points and 2 idtracks
  352. # (by definition of these frame, they are the complement of the above frames).
  353. # So in these frames, we assign identities to any fish we can assignment passes the threshold
  354. # t0 = time.time()
  355. print('Matching individual 3D skeletons with idTracker.ai identities...')
  356. # find the indexs of frames we havent looked at yet
  357. # (all frame numbers less (set less) the frame numbers we looked at already)
  358. fIdxs_withOUT_idtracker_and_xyPec_info_for_all_fish = np.setdiff1d(np.arange(0, numFrames, 1),
  359. fIdxs_with_idtracker_and_xyPec_info_for_all_fish)
  360. # make an array to hold information on if data has been assigned after this step
  361. # start with a copy of what we already have, and we will add to this copy here
  362. tracks_available_post_pass2_id_assignments = np.copy(tracks_available_post_initial_id_assignments)
  363. # -- test all frames with idtracks and xypecs -- #
  364. # this list will hold [fIdx, fishIdx, costs] values for
  365. # where we add data to the tracks arrays in this pass
  366. pass2_added_data_info_from_ids = []
  367. pass2_added_data_info_from_assigning_remaining_detections = []
  368. for ii, fIdx in enumerate(fIdxs_withOUT_idtracker_and_xyPec_info_for_all_fish):
  369. # get the image coordinates of interest
  370. frame_pec_points = positions_imageCoordinates_processed[xyCamIdx, fIdx, :, pecBpIdx, :]
  371. frame_idTracker_centroids = idtracker_data[fIdx, :]
  372. # test idtracks to see if we can make matches with pec points
  373. for fishIdx in range(numFish):
  374. idtrack_centroid = frame_idTracker_centroids[fishIdx]
  375. # count the number of xyPec points that pass the threshold with this idtrack coord
  376. candidate_match_idxs = []
  377. candidate_match_costs = []
  378. for dummyFishIdx in range(numFish):
  379. pec_point = frame_pec_points[dummyFishIdx]
  380. assign_cost = np.linalg.norm(idtrack_centroid - pec_point)
  381. if assign_cost < args.idtracks_to_sleap_thresh:
  382. candidate_match_idxs.append(dummyFishIdx)
  383. candidate_match_costs.append(assign_cost)
  384. else:
  385. continue
  386. # if we have exactly one match, use it.
  387. # update the tracks, then try to assign ids to image_cooridinate positions left over
  388. if len(candidate_match_idxs) == 1:
  389. # update the tracks arrays
  390. tracks_3D[fIdx, fishIdx] = np.copy(positions_3D_processed[fIdx, candidate_match_idxs[0]])
  391. tracks_imCoords[:, fIdx, fishIdx] = np.copy(
  392. positions_imageCoordinates_processed[:, fIdx, candidate_match_idxs[0]])
  393. # record some other data
  394. tracks_available_post_pass2_id_assignments[fIdx, fishIdx] = 1
  395. pass2_added_data_info_from_ids.append([fIdx, fishIdx, candidate_match_costs[0]])
  396. # Is there a 3D skeleton for this frame that doesn't have an identity from idtracker.ai?
  397. # If there is, since we just assigned an identity to one individual,
  398. # then this 3D skeleton can get the identity of the remaining fish.
  399. index_used_to_assign_id = candidate_match_idxs[0]
  400. other_index = np.mod(candidate_match_idxs[0] + 1, 2) # NB: only works for numFish=2
  401. skeleton_just_used = np.copy(positions_3D_processed[fIdx, index_used_to_assign_id])
  402. other_skeleton = np.copy(positions_3D_processed[fIdx, other_index])
  403. # get the index of the other individual we are trying to assign data to now
  404. other_individual_fishIdx = np.mod(fishIdx + 1, 2) # NB: only works for numFish=2
  405. # check that we don't already have something in tracks_3D for this individual
  406. if np.all(np.isnan(tracks_3D[fIdx, other_individual_fishIdx])):
  407. # check that the unidentified (remaining) 3D skeleton is not entirely empty
  408. if ~np.all(np.isnan(other_skeleton)):
  409. tracks_3D[fIdx, other_individual_fishIdx] = np.copy(other_skeleton)
  410. tracks_imCoords[:, fIdx, other_individual_fishIdx] = np.copy(
  411. positions_imageCoordinates_processed[:, fIdx, other_index])
  412. # record some other data (the cost is NaN since we have no idtracker identity)
  413. tracks_available_post_pass2_id_assignments[fIdx, other_individual_fishIdx] = 1
  414. pass2_added_data_info_from_assigning_remaining_detections.append(
  415. [fIdx, other_individual_fishIdx, np.NaN])
  416. else:
  417. continue
  418. pass2_added_data_info_from_ids = np.array(pass2_added_data_info_from_ids)
  419. pass2_added_data_info_from_assigning_remaining_detections = np.array(
  420. pass2_added_data_info_from_assigning_remaining_detections)
  421. tE = time.time()
  422. print(f'Individual matching completed in {tE - t0:.2f} seconds')
  423. # -------- Step 5 ------------#
  424. # Fill-in gaps in trajectories by propagating idx in 3D,
  425. # only accepting cases where we get it right
  426. print('Tracking segments in time with 3D skeletons but without idtracker.ai matches...')
  427. # find contiguous regions that don't have tracks available for both fish
  428. frames_without_numFish_3D_tracks_yet = np.zeros((numFrames,))
  429. frames_without_numFish_3D_tracks_yet[np.sum(tracks_available_post_pass2_id_assignments, axis=1) != numFish] = 1
  430. regions_without_numFish_3D_tracks_yet = contiguous_regions(frames_without_numFish_3D_tracks_yet)
  431. # ---- track in 3D ------- #
  432. # prepare containers to grab results
  433. successful_tracking = []
  434. for regIdx in range(len(regions_without_numFish_3D_tracks_yet)):
  435. # prepare the args
  436. regF0, regFE = regions_without_numFish_3D_tracks_yet[regIdx]
  437. # Test:
  438. # If regF0=0, that means the first region starts at the first frame,
  439. # i.e. we have no idtracker.ai results to start us off.
  440. # We do not have a concept of 'start from known positions and going towards known positions'
  441. # for this region, so we cannot track it, hence we skip it
  442. if regF0 == 0:
  443. was_successfull = False
  444. successful_tracking.append(was_successfull)
  445. continue
  446. # Test End:
  447. # Test:
  448. # If regFE=tracks_3D.shape[0], that means we don't have last known positions contained within the experiment data
  449. # So we will skip this region
  450. if regFE == tracks_3D.shape[0]:
  451. was_successfull = False
  452. successful_tracking.append(was_successfull)
  453. continue
  454. # Test End:
  455. existing_tracks_3D_for_segment = np.copy(tracks_3D[regF0:regFE + 1])
  456. existing_imCoords_3D_for_segment = np.copy(tracks_imCoords[:, regF0:regFE + 1])
  457. positions_3D_processed_for_segment = np.copy(positions_3D_processed[regF0:regFE + 1])
  458. positions_imageCoordinates_processed_for_segment = np.copy(positions_imageCoordinates_processed[:, regF0:regFE + 1])
  459. last_known_positions = np.copy(tracks_3D[regF0 - 1])
  460. final_known_positions = np.copy(tracks_3D[regFE])
  461. tracks_available_post_pass2_id_assignments_for_segment = np.copy(
  462. tracks_available_post_pass2_id_assignments[regF0:regFE + 1])
  463. # Track segment
  464. track_outs = track_segment_in_3D_if_possible(existing_tracks_3D_for_segment,
  465. existing_imCoords_3D_for_segment,
  466. positions_3D_processed_for_segment,
  467. positions_imageCoordinates_processed_for_segment,
  468. last_known_positions,
  469. final_known_positions,
  470. tracks_available_post_pass2_id_assignments_for_segment)
  471. # parse the output
  472. [was_successfull, segment_tracks_3D, segment_tracks_imCoords, seg_track_method,
  473. seg_data_available_array] = track_outs
  474. # update tracks 3D if we can
  475. if was_successfull:
  476. # the :-1 is to remove the final frame from the segment_tracks, which is one
  477. # longer than the segment itself, since we included the frame with the
  478. # final_known_position information
  479. tracks_3D[regF0:regFE] = np.copy(segment_tracks_3D[:-1])
  480. tracks_imCoords[:, regF0:regFE] = np.copy(segment_tracks_imCoords[:, :-1])
  481. # record outputs
  482. successful_tracking.append(was_successfull)
  483. successful_tracking = np.array(successful_tracking)
  484. # -------- Step 6 ------------#
  485. # post process the trajectories, and save
  486. # post processing
  487. print()
  488. print('Post-processing trajectories...')
  489. tracks_3D_tracked = np.copy(tracks_3D)
  490. # Clean up pair swaps, sudden jumps etc...
  491. tracks_3D, pair_switch_removed_frames, velocity_outlier_removed_frames = clean_data_from_jumps(tracks_3D,
  492. tracks_imCoords, dt,
  493. threshold=args.pair_swap_velocity_threshold)
  494. # interpolate
  495. tracks_3D_interpd = interpolate_over_small_gaps(tracks_3D,
  496. limit=args.interp_limit,
  497. polyord=args.interp_polyOrd)
  498. # use sav-gol filter
  499. outs = get_smooth_timeseries_and_derivatives_using_savgol(tracks_3D_interpd,
  500. win_len=args.savgol_win,
  501. polyOrd=args.savgol_ord,
  502. dt=dt)
  503. tracks_3D_smooth, tracks_3D_vel_smooth, tracks_3D_speed_smooth, tracks_3D_accvec_smooth, tracks_3D_accmag_smooth = outs
  504. print('Post-processing completed successfully')
  505. # save the results, as they are, unsynchronized with video
  506. print('Saving results to H5 file...')
  507. with h5py.File(saveFile_h5_path, 'w') as hf:
  508. hf.create_dataset('tracks_3D_tracked', data=tracks_3D_tracked)
  509. hf.create_dataset('tracks_3D_raw', data=tracks_3D)
  510. hf.create_dataset('tracks_imCoords_raw', data=tracks_imCoords)
  511. hf.create_dataset('tracks_3D_smooth', data=tracks_3D_smooth)
  512. # save debugging info
  513. hf.create_dataset('methodIdxs', data=methodIdxs)
  514. hf.create_dataset('positions_imageCoordinates', data=positions_imageCoordinates)
  515. hf.create_dataset('registration_costs', data=registration_costs)
  516. hf.create_dataset('reg_thresh_removal_info', data=reg_thresh_removal_info)
  517. hf.create_dataset('size_thresh_removal_info', data=size_thresh_removal_info)
  518. hf.create_dataset('tracks_available_post_initial_id_assignments', data=tracks_available_post_initial_id_assignments)
  519. hf.create_dataset('pass2_added_data_info_from_ids', data=pass2_added_data_info_from_ids)
  520. hf.create_dataset('pass2_added_data_info_from_assigning_remaining_detections',
  521. data=pass2_added_data_info_from_assigning_remaining_detections)
  522. hf.create_dataset('successful_tracking', data=successful_tracking)
  523. hf.create_dataset('pair_swap_removed_frames', data=pair_switch_removed_frames)
  524. hf.create_dataset('single_fish_jump_removed_frames', data=velocity_outlier_removed_frames)
  525. # extra stuff to save, if you like
  526. hf.create_dataset('idtracker_sleap_assignment_nothresh_costs', data=idtracker_sleap_assignment_nothresh_costs)
  527. hf.create_dataset('idtracker_data', data=idtracker_data)
  528. hf.create_dataset("registration_costs_mean_distant", data=all_registration_costs_mean_distant)
  529. hf.create_dataset("head_pec_dist", data=head_pec_dists)
  530. hf.create_dataset("pec_tail_dist", data=pec_tail_dists)
  531. # -- strings -- #
  532. string_type = h5py.special_dtype(vlen=str)
  533. tracks_dim_names = np.array(['numFrames', 'numFish', 'numBodyPoints', 'XYZ'], dtype=string_type)
  534. hf.create_dataset('tracks_dim_names', data=tracks_dim_names)
  535. savgol_info = np.array(['win_len={0}'.format(args.savgol_win),
  536. 'polyOrd={0}'.format(args.savgol_ord)], dtype=string_type)
  537. hf.create_dataset('savgol_info', data=savgol_info)
  538. ## NB: HARDCODED
  539. interp_info = np.array(['method=polynomial', 'order={0}'.format(args.interp_polyOrd),
  540. 'limit_direction=both', 'limit={0}'.format(args.interp_limit),
  541. 'inplace=False'], dtype=string_type)
  542. hf.create_dataset('interp_info', data=interp_info)
  543. print('H5 file saved successfully')
  544. # Log stats on tracking quality
  545. print("Stats on tracking quality in smooth data:")
  546. print("Frames with full SLEAP detections in camera 0: {0}%".format(
  547. get_percentage_frames_both_fish_full_info(sleap_data[0])))
  548. print("Frames with full SLEAP detections in camera 1: {0}%".format(
  549. get_percentage_frames_both_fish_full_info(sleap_data[1])))
  550. print("Frames with full SLEAP detections in camera 2: {0}%".format(
  551. get_percentage_frames_both_fish_full_info(sleap_data[2])))
  552. print("Frames with no SLEAP detections in camera 0: {0}%".format(get_percentage_frames_no_info(sleap_data[0])))
  553. print("Frames with no SLEAP detections in camera 1: {0}%".format(get_percentage_frames_no_info(sleap_data[1])))
  554. print("Frames with no SLEAP detections in camera 2: {0}%".format(get_percentage_frames_no_info(sleap_data[2])))
  555. print("Frames with no idtrackerid information: {0}%".format(get_percentage_frames_no_idtracker_info(idtracker_data)))
  556. print("Tracked data: Frames with full information: {0}%".format(
  557. get_percentage_frames_both_fish_full_info(tracks_3D_tracked)))
  558. print("Tracked data: Frames with no information: {0}%".format(get_percentage_frames_no_info(tracks_3D_tracked)))
  559. print("Raw data: Frames with full information: {0}%".format(get_percentage_frames_both_fish_full_info(tracks_3D)))
  560. print("Raw data: Frames with no information: {0}%".format(get_percentage_frames_no_info(tracks_3D)))
  561. print("Smooth data: Frames with full information: {0}%".format(
  562. get_percentage_frames_both_fish_full_info(tracks_3D_smooth)))
  563. print("Smooth data: Frames with no information: {0}%".format(get_percentage_frames_no_info(tracks_3D_smooth)))
  564. # before saving the track data, append NaN to the data until the start_frame value.
  565. # This has the benefit that it "synchronizes" the data generated with the full video, even
  566. # if we did the tracking for only a part of the video (i.e. started tracking after X frames).
  567. tracks_3D = prepend_nan_to_results_array(tracks_3D, args.start_frame_in_video)
  568. tracks_3D_tracked = prepend_nan_to_results_array(tracks_3D_tracked, args.start_frame_in_video)
  569. tracks_imCoords = prepend_nan_to_im_coords_array(tracks_imCoords, args.start_frame_in_video)
  570. tracks_3D_smooth = prepend_nan_to_results_array(tracks_3D_smooth, args.start_frame_in_video)
  571. idtracker_data = prepend_nan_to_idtracker_data(idtracker_data, args.start_frame_in_video)
  572. # save the tracks only, this time they are aligned with the video
  573. print('saving ... ')
  574. with h5py.File(saveFile_aligned_h5, 'w') as hf:
  575. hf.create_dataset('tracks_3D_raw', data=tracks_3D)
  576. hf.create_dataset('tracks_imCoords_raw', data=tracks_imCoords)
  577. hf.create_dataset('tracks_3D_smooth', data=tracks_3D_smooth)
  578. hf.create_dataset('tracks_3D_tracked', data=tracks_3D_tracked)
  579. hf.create_dataset('idtracker_data', data=idtracker_data)
  580. print('saved')
  581. print()
  582. tE = time.time()
  583. print()
  584. print('Finished post processing and saving h5 file')
  585. print()
  586. # -------------------- Finish Up and Save Results -------------------- #
  587. tE = time.time()
  588. print('saving csv file ...')
  589. _ = save_tracks_3D_to_csv_and_return_dataFrame(tracks_3D_smooth, saveFile_csv_path)
  590. print()
  591. print('Finished!')
  592. print("Exporting a sample video...")
  593. print()
  594. print('------------')
  595. print('Completely finished!')
  596. print('total time: t = ', (tE - t0) / 60, ' mins')

track_experiment.py at commit be38d43, under MIT · at the source

Overview

Authors: Kosmas Deligkaris1, Radmila Neiman1, Makoto Hiroi1, Tatsuo Izawa1, Liam O’Shaughnessy2, Luis Carretero Rodriguez1, Ichiro Masai1, Greg J Stephens1,2
  1. Okinawa Institute of Science and Technology Graduate University, Okinawa, Japan
  2. Department of Physics and Astronomy, Vrije Universiteit Amsterdam, Amsterdam, The Netherlands
Journal: Scientific data, volume 13, issue 1, article 583
Dates: received 31 October 2025; accepted 23 February 2026; published online 3 March 2026
Type: Data paper · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41597-026-06953-6 · PMID 41775730 · PMCID PMC13066468 · OpenAlex W7133354171
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: zebrafish (organism), methods / tools (subfield)
Methods: Statistics, Smoothing, state filtering, decompositions
MeSH: Behavior, Animal*, Social Behavior*, Zebrafish*, Animals, Datasets as Topic, Female, Male (* major topic)
Topic: Zebrafish Biomedical Research Applications (Cell Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: OIST Graduate University; Human Frontiers Science Program (RGP0055); Vrije Universiteit Amsterdam
Citations: cited by 1 paper (Europe PMC); 30 references in the paper

Abstract

Zebrafish (Danio rerio) has emerged as a valuable vertebrate model organism for studies of social interactions. While previous multiple-animal research has focused on gross movement, here we present a machine vision workflow to capture and analyze fine-grained social interactions through the tracking of three anatomical landmarks (at the head, pectoral fins, and tail) as well as the identity of the fish in 3D. We release a dataset of N = 173 five-hour recordings of adult zebrafish dyads, including male/male and female/female wild-type pairs and disease-model mutants, sampling complex behaviors such as dominance contests and aggressive/submissive motifs. The recordings are of high temporal resolution (fs = 140 Hz) from a large imaging volume ~ 10 body lengths per linear dimension, and include both square and cylindrical arenas. This dataset offers a critical resource for biologists seeking to understand the neural basis of social behavior, for machine learning researchers working to improve posture tracking, and for the broader quantitative understanding of natural behavior.

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

Repositories

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

ksseverson57/campy

License: MIT
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 5c4ea44708bde03ef42aa7db2084823857a30438, 24 November 2025
Languages: Python (20)
Size: 36 files, 20 scripts
Software Heritage: archived
Found in: the text, “Experimental set-up”
Holds: README, license file, environment (setup.py)
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (7 files), imageio (4 files), Matplotlib (2 files), SciPy (2 files), scikit-image (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
22 files

deligkarisk/Zebrafish_3D_Tracking_Workflow

License: MIT
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: be38d43be63b34665bf016559d539891bbad6c51, 12 February 2026
Languages: Python (34)
Size: 37 files, 34 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, environment (environment.yml)
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (24 files), Matplotlib (5 files), SciPy (4 files), h5py (3 files), pandas (3 files), scikit-learn (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
35 files

Code availability

Code is available on GitHub at the following address: https://github.com/deligkarisk/Zebrafish_3D_Tracking_Workflow.

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

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:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 54 scripts, each with its path and the digest of its content;
  • 11 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

Datasets cited

Data availability

All tracked experiments, as well as the corresponding metadata and sample video files, are available in Zenodo21 (10.5281/zenodo.17190142). Further details are provided in the Data Records section.

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, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 8 authors, 7 MeSH terms, 3 funders, 29 references.

Cite

This paper

Deligkaris, K., Neiman, R., Hiroi, M., Izawa, T., O’Shaughnessy, L., Rodriguez, L. C., Masai, I., & Stephens, G. J. (2026). A dataset of fine-grained zebrafish interactions in health and disease. Scientific data, 13(1), 583. https://doi.org/10.1038/s41597-026-06953-6

BibTeX

@article{deligkaris2026dataset,
author = {Deligkaris, Kosmas and Neiman, Radmila and Hiroi, Makoto and Izawa, Tatsuo and O’Shaughnessy, Liam and Rodriguez, Luis Carretero and Masai, Ichiro and Stephens, Greg J},
title = {{A dataset of fine-grained zebrafish interactions in health and disease}},
journal = {Scientific data},
year = {2026},
month = mar,
volume = {13},
number = {1},
pages = {583},
publisher = {Nature Publishing Group},
issn = {2052-4463},
doi = {10.1038/s41597-026-06953-6},
url = {https://doi.org/10.1038/s41597-026-06953-6},
pmid = {41775730},
pmcid = {PMC13066468}
}

RIS

TY - JOUR
AU - Deligkaris, Kosmas
AU - Neiman, Radmila
AU - Hiroi, Makoto
AU - Izawa, Tatsuo
AU - O’Shaughnessy, Liam
AU - Rodriguez, Luis Carretero
AU - Masai, Ichiro
AU - Stephens, Greg J
TI - A dataset of fine-grained zebrafish interactions in health and disease
T2 - Scientific data
J2 - Sci Data
PY - 2026
DA - 2026/03/03
VL - 13
IS - 1
SP - 583
SN - 2052-4463
PB - Nature Publishing Group
DO - 10.1038/s41597-026-06953-6
UR - https://doi.org/10.1038/s41597-026-06953-6
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41597-026-06953-6",
"type": "article-journal",
"title": "A dataset of fine-grained zebrafish interactions in health and disease",
"container-title": "Scientific data",
"author": [
{
"family": "Deligkaris",
"given": "Kosmas"
},
{
"family": "Neiman",
"given": "Radmila"
},
{
"family": "Hiroi",
"given": "Makoto"
},
{
"family": "Izawa",
"given": "Tatsuo"
},
{
"family": "O’Shaughnessy",
"given": "Liam"
},
{
"family": "Rodriguez",
"given": "Luis Carretero"
},
{
"family": "Masai",
"given": "Ichiro"
},
{
"family": "Stephens",
"given": "Greg J"
}
],
"container-title-short": "Sci Data",
"volume": "13",
"issue": "1",
"page": "583",
"DOI": "10.1038/s41597-026-06953-6",
"PMID": "41775730",
"PMCID": "PMC13066468",
"ISSN": "2052-4463",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41597-026-06953-6",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
3
]
]
}
}

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/s41586-026-10679-1 [code]
Cortical development dynamics across autism spectrum disorder mouse models.
Journal: Nature
In common: imageio, scikit-image, h5py, 5 other tools, 1 reference
[2] doi:10.1038/s41593-026-02262-8 [code]
Cheese3D enables sensitive detection and analysis of whole-face movement in mice.
Journal: Nature neuroscience
In common: imageio, scikit-image, h5py, 5 other tools, 1 reference
[3] 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: imageio, scikit-image, h5py, 5 other tools, methods / tools
[4] doi:10.1016/j.isci.2026.116168 [code]
See the small lesions: Frequency-guided spatial debiasing GAN for multimodal medical image fusion.
Journal: iScience
In common: imageio, scikit-image, h5py, 5 other tools, methods / tools
[5] doi:10.1371/journal.pcbi.1013499 [code]
VesiclePy: A machine learning vesicle analysis toolbox for volume electron microscopy.
Journal: PLoS computational biology
In common: imageio, scikit-image, h5py, 5 other tools, methods / tools
[6] doi:10.1038/s41597-026-07248-6 [code]
A large-scale fMRI dataset for vision-language semantic association.
Journal: Scientific data
In common: imageio, scikit-image, h5py, 5 other tools, methods / tools
[7] doi:10.3389/frai.2026.1771088 [code]
Few-shot deployment of pretrained MRI transformers in brain imaging tasks.
Journal: Frontiers in artificial intelligence
In common: imageio, scikit-image, h5py, 5 other tools, methods / tools
[8] doi: [code]
Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement control
Journal: eLife
In common: imageio, scikit-image, h5py, 5 other tools
[9] doi:10.1364/boe.605322 [code]
Generalized plaque digitization framework for multi-dimensional mesoscopic images.
Journal: Biomedical optics express
In common: imageio, scikit-image, h5py, 5 other tools
[10] doi:10.7554/elife.109717 [code]
Retrosplenial cortex enables context-dependent goal-directed sensorimotor transformation.
Journal: eLife
In common: imageio, scikit-image, h5py, 5 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.