Skip to content

Add noise-aware stitching, min_rows, concat, and calcium imaging config#155

Merged
sneakers-the-rat merged 22 commits into
miniscope:mainfrom
MarcelMB:feat/timestamp-stitch-and-min-rows
Apr 24, 2026
Merged

Add noise-aware stitching, min_rows, concat, and calcium imaging config#155
sneakers-the-rat merged 22 commits into
miniscope:mainfrom
MarcelMB:feat/timestamp-stitch-and-min-rows

Conversation

@MarcelMB
Copy link
Copy Markdown
Collaborator

@MarcelMB MarcelMB commented Mar 16, 2026

Summary

Adds several features for processing dual-DAQ wireless miniscope recordings, motivated by a 9-hour calcium imaging session with two DAQs.

Noise-aware frame selection (--selection-mode noise_aware)

  • During stitching, runs mio's existing InvalidFrameDetector (gradient + black_area) on each candidate frame
  • If one DAQ's frame is noisy and the other is clean → pick the clean one
  • If both are clean → pick the first (no expensive Sobel computation)
  • If both are noisy → skip the frame entirely (no corrupted data in output)
  • Produces terminal summary with per-DAQ noise percentages and both-broken stats
  • Generates noise_report.png (timeline, run length distribution, drop density plots)
  • Writes both_broken.avi for manual inspection of skipped frames

min_rows parameter for BlackAreaDetector

  • New min_rows field on BlackAreaDetectorConfig (default=1, preserves existing behavior)
  • Requires N flagged rows before marking a frame as invalid
  • Eliminates false positives from naturally dark regions in calcium imaging (recommended: min_rows=10)

Vectorized BlackAreaDetector

  • Replaced pixel-by-pixel Python for-loop with numpy cumsum sliding window
  • ~100x speedup (enables real-time noise detection during stitching at ~170 fps)

Timestamp-based frame matching (--match-by timestamp)

  • Matches frames across DAQs by nearest buffer_recv_unix_time within configurable threshold (default 25ms)
  • Handles DAQs starting/stopping at different times naturally
  • Alternative to existing frame_num matching (which remains the default)

mio process concat command

  • Concatenates sequential recording segments from one DAQ into a single AVI + CSV
  • Discovers all AVIs in a directory with natural numeric sort
  • Fuzzy CSV matching handles mismatched filenames (e.g., long-8-002.avilong-8.csv)
  • Renumbers reconstructed_frame_index to be contiguous across segments

--max-frames flag

  • Limits frame processing during stitching for quick test runs

denoise_calcium_imaging config

  • New example config with recommended calcium imaging parameters
  • consecutive_threshold: 30, value_threshold: 0, min_rows: 10

New output files

These are additional outputs created by the new features (on top of mio's existing outputs):

File Created by Description
noise_report.png stitch --selection-mode noise_aware Per-DAQ noise timeline, run-length distribution, and drop density plots
both_broken.avi stitch --selection-mode noise_aware All candidate frames from frame pairs where both DAQs were noisy (skipped from output)
<dir>_combined.avi concat Single AVI merging all recording segments from one DAQ directory
<dir>_combined.csv concat Merged metadata CSV with contiguous reconstructed_frame_index

Example usage

# Concatenate segments per DAQ
mio process concat -d /path/to/neural_DAQ1/
mio process concat -d /path/to/neural_DAQ2/

# Full workflow: noise-aware stitch → denoise
mio process workflow \
  -i DAQ1_combined.avi \
  -i DAQ2_combined.avi \
  --match-by timestamp \
  --selection-mode noise_aware \
  -c denoise_calcium_imaging

# Quick test (first 50k frames only)
mio process workflow \
  -i DAQ1_combined.avi \
  -i DAQ2_combined.avi \
  --match-by timestamp \
  --selection-mode noise_aware \
  -c denoise_calcium_imaging \
  --max-frames 50000

Backward compatibility

  • All new features are opt-in via CLI flags
  • Default matching remains frame_num, default selection remains metadata
  • min_rows defaults to 1 (existing behavior)
  • Existing tests unaffected

Test plan

  • pytest tests/test_process/test_frame_helper.py -v — min_rows + vectorized detection
  • pytest tests/test_process/test_stitch.py -v — timestamp matching + concat
  • Manual test on dual-DAQ recording with --selection-mode noise_aware

🤖 Generated with Claude Code

MarcelMB and others added 6 commits March 16, 2026 15:24
BlackAreaDetector now supports a min_rows parameter (default=1, preserving
backward compatibility) that requires multiple flagged rows before marking
a frame as invalid. This eliminates false positives from naturally dark
regions in calcium imaging data.

Stitch now supports --match-by=timestamp which matches frames across
recordings by nearest buffer_recv_unix_time (within configurable threshold),
instead of by device frame_num. This handles DAQs with offset frame
numbering, different start/stop times, and mid-recording gaps automatically.

Also adds denoise_calcium_imaging.yml example config with recommended
parameters for calcium imaging (consecutive_threshold=30, value_threshold=0,
min_rows=10).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New `mio process concat -d /path/to/daq1/` command discovers all .avi
files (with companion .csv) in a directory, sorts by filename, and
concatenates them into a single video + CSV with contiguous
reconstructed_frame_index. This is needed when a DAQ produces multiple
segment files that must be combined before cross-DAQ stitching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Alphabetical sort puts long-10 before long-2. Natural sort correctly
orders segments by their numeric suffix (2, 3, ..., 9, 10, 12, 13).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AVI files like long-8-002.avi have CSVs named long-8.csv (without the
extra numeric suffix). The concat command now strips trailing -NNN
suffixes when looking for companion CSVs, and shows the mapping in
output. Also quiets per-frame debug messages to logger only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…masking

The FrequencyMaskingConfig requires id, mio_model, and mio_version fields
from its MiniscopeConfig base class. Without these, the config fails
Pydantic validation at runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tch diagnostics

- Add --selection-mode noise_aware to stitch/workflow: uses InvalidFrameDetector
  during stitching to pick clean frames and skip both-broken pairs
- Vectorize BlackAreaDetector with numpy cumsum sliding window (~100x faster)
- Skip Sobel edge scoring in noise_aware mode for additional speedup
- Add --max-frames flag for quick test runs
- Add terminal noise summary (per-DAQ noisy counts, both-broken percentage)
- Add noise_report.png (timeline, run length distribution, drop density)
- Add both_broken.avi debug output for manual inspection of skipped frames
- Fix denoise_calcium_imaging.yml: add interactive_display section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@MarcelMB MarcelMB changed the title Add min_rows to BlackAreaDetector and timestamp-based matching to stitch Add noise-aware stitching, min_rows, concat, and calcium imaging config Mar 17, 2026
@t-sasatani
Copy link
Copy Markdown
Collaborator

Nice that it seems to do what we want. But I also think some parts are a bit redundant/excessive for what we want to do.

Some quick thoughts before looking into the details of the code:

  • The stitching part is the first step for all preprocessing, and this module originally had no dependency on user-defined parameters (so this is a one-time thing with no need for iterative tuning). The noise-aware frame selection part introduces user-parameter-dependent stuff in this step, which is handled later anyway in the process. I rather think isolating these steps is better.
  • Isn't setting the min_rows the same as setting the consecutive_threshold parameter higher than the number of pixels per line? Same question about the need for the denoise_calcium_imaging config.
  • Vectorized BlackAreaDetector seems to be an improvement for free. But this number should also be in the metadata now, so we could shift to something more deterministic.
  • mio process concat command -> Shouldn't this be one specific case within the timestamp-based stitch? I think this is what (should) happen when you input two streams in series into the stitch command, so I'm unsure why to isolate this to another command.
  • Timestamp-based frame matching is rather fuzzy, so I'm a bit afraid of frames getting fused when the time gets shifted a bit. I think adding another guard based on stricter parameters, such as frame_num (and possibly loosening the timestamp threshold), would improve data integrity without losing anything.

@MarcelMB
Copy link
Copy Markdown
Collaborator Author

  • I had this earlier in two steps: stitching (parameter-free) and then noise detection(user parameter), and I see your point about how it's cleaner. I remember changing it because of speed for the 9h recording, specifically: don't have to read the video streams twice, but do it all in one go
  • denoise_calcium_imaging was just for me for testing, shouldn't go to main, and yeah min_rows is the same as making the threshold longer
  • Vectorization made it so much faster. I don't think the black detection was vectorized before?!
  • concat was just used here because what I build needed a one file submission not many .avi's so a lot of this is an artefact of I made it work outside of mio for my purpose of getting a 10 min fast stitching but than integrating it into mio is actually the hardest part for me
  • well for timestamps, I think we synchronize both DAQs with a maximal difference of 10 m,s an you checked the drift for at least 1 h recording chunks, and this was fine. So that's why I chose it, the hardware we choose seems to be in a low jitter range, but I see how frame_number can be more accurate if the MCU is working properly and the firmware works well

so good comments. Makes total sense. Thanks!!!

@MarcelMB
Copy link
Copy Markdown
Collaborator Author

To-DO safeguard:

  • adding frum_number to unix_timestamp matching (marcel approach) can make sense

  • concat also should look into unix_time, not just file order (get first unix timestamp and latest), so also a guard

-seperate two steps noise filter and stitch as discussed above

Copy link
Copy Markdown
Collaborator

@sneakers-the-rat sneakers-the-rat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's merge #133 first and then change the base of this to main. I'll handle restructuring this to match the changes i made there. a few questions and notes, but i get the need for this and can handle tidying up

Comment thread mio/cli/process.py Outdated
Comment thread mio/cli/process.py Outdated
Comment thread mio/cli/process.py Outdated
Comment thread mio/cli/process.py Outdated
Comment thread mio/cli/process.py Outdated
Comment thread mio/process/stitch.py Outdated
Comment thread mio/process/stitch.py Outdated
Comment thread mio/process/stitch.py Outdated
Comment thread mio/process/stitch.py Outdated
Comment thread tests/test_process/test_stitch.py Outdated
@sneakers-the-rat sneakers-the-rat changed the base branch from feat-stitch-video to main April 22, 2026 23:49
@sneakers-the-rat
Copy link
Copy Markdown
Collaborator

sneakers-the-rat commented Apr 23, 2026

OK pushing what i have for the day. things i still need to do

  • figure out where the new stitching logic differs from the prior stitching logic in the tests
  • validate the new black pixel counting method
  • incorporate the timestamp-based alignment
  • final cleanup

the diff got extremely fucked up from the removal of the MSE detector, but basically i simplified the noise-aware part of the stitching into that score_noise function, and so now that just creates another csv table alongside the video before stitching.

still TODO is to figure out what to do with the denoise processor classes, because they don't use the _noise.csv premade measurements at all - need to figure out which intermediate outputs we want to keep and then clean that all up - #173

@sneakers-the-rat
Copy link
Copy Markdown
Collaborator

ok! so checking out the changes to the stitched video, they are correct! this PR differs from main's stitching decisions on two frames:

  • 2542: frame index on video 1=29, video 2=25
  • 2544: frame index on video 1=31, video 2=27

this PR chooses video 2 for both of those where main chooses video 1.

It's because video 1 has big black missing areas in those frames! It has all 8 buffers, and the black_padding_px are both zero, so it just defaulted to video 1.

the indexes below are 0-indexed, so they are off by one from the frame indexes in the screenshots (which are 1-indexed)

frame_num video 1 index video 2 index video_1.avi video_2.avi
2542 29 25 Screenshot 2026-04-23 at 4 55 13 PM Screenshot 2026-04-23 at 4 56 27 PM
2544 31 27 Screenshot 2026-04-23 at 4 55 38 PM Screenshot 2026-04-23 at 4 56 54 PM

so hooray! good test failures!

@sneakers-the-rat sneakers-the-rat merged commit d1a0b07 into miniscope:main Apr 24, 2026
16 checks passed
@coveralls
Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 24915631375

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage decreased (-0.6%) to 83.793%

Details

  • Coverage decreased (-0.6%) from the base build.
  • Patch coverage: 14 uncovered changes across 5 files (163 of 177 lines covered, 92.09%).
  • 27 coverage regressions across 4 files.

Uncovered Changes

File Changed Covered %
mio/cli/process.py 19 11 57.89%
mio/models/dataset.py 18 16 88.89%
mio/process/frame_helper.py 33 31 93.94%
mio/process/stitch.py 81 80 98.77%
mio/utils.py 2 1 50.0%

Coverage Regressions

27 previously-covered lines in 4 files lost coverage.

File Lines Losing Coverage Coverage
mio/process/video.py 15 85.16%
mio/process/frame_helper.py 7 89.52%
mio/models/stream.py 4 94.74%
mio/process/stitch.py 1 97.62%

Coverage Stats

Coverage Status
Relevant Lines: 3227
Covered Lines: 2704
Line Coverage: 83.79%
Coverage Strength: 9.79 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants