Conversation
romleiaj
force-pushed
the
dev/multicam-time-offset
branch
from
September 28, 2026 15:12
5e036dc to
d80e8e8
Compare
The worker image sets VIRTUAL_ENV to DIVE's own Python 3.11 venv, and `uv run` re-exports it to the Celery process, so every pipeline subprocess inherited it. The VIAME image's kwiver ships a second Python plugin loader (plugins_from_python) that reads VIRTUAL_ENV and edits sys.path via PySys_GetObject without holding the GIL after modules_python has already started the interpreter. The result is a SIGSEGV at plugin load for every `viame runner` invocation, before any pipeline process runs, with nothing useful on stderr. get_gpu_environment already strips the venv from PATH for the same reason; now it also drops VIRTUAL_ENV and the UV_PYTHON* variables. The variable has to be absent rather than empty because kwiver only checks getenv for null. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ
VIAME renamed its DIVE registration reader on 2026-08-25 (b540a933b,
"Rename dive_transform_io to homography_json_io"): the transform_2d_io
implementation is registered as "homography_json" now, not "dive".
Every VIAME built since then, including the web worker image, refuses
the warp pipelines with
Could not find implementation "dive" for
"kwiver::vital::algo::transform_2d_io" from key "transform_reader:type"
viame: Caught unhandled std::exception: Unable to create transform_reader
Send the new name from both the web task and the Desktop backend.
kwiver has no fallback for an algorithm type, so a VIAME older than the
rename needs rebuilding before its 2-cam/3-cam warp pipes run again.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ
DIVE lets the cameras of a rig differ in frame count, but the 2-cam
and 3-cam pipes run in lockstep and kwiver's detected_object_output
grabs its optional image_file_name port unconditionally. When one
camera ran out first the writer for the other tried to read the
"complete" marker as a string:
RuntimeError: Failed to cast datum of type 'complete' into
std::string: bad any_cast.
Before launching, take the native frame count of each video camera
from its folder's ffprobe metadata and the list length of each image
camera, and when they disagree cap every camera at the minimum:
image lists are truncated, video readers get
vidl_ffmpeg:stop_after_frame (1-based, so exactly that many frames).
A registration frame subset already pairs row for row and is left
alone. The job log says when the cap is applied.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ
buildAlignedTimeline needs a timestamp on every frame, which only image sequences carry (parsed from filenames). Video panes never qualify, so they scrub in raw-index lockstep and a recording start offset between two cameras is baked into the review -- a fixed EO/IR rig whose encoders start a fraction of a second apart has no way to express that today. On a fixed rig the offset is one constant per camera, so buildOffsetTimeline emits the same slot structure buildAlignedTimeline does from just that number. Everything downstream (pane seek, gap indication, cross-camera frame translation) then behaves exactly as it does for a timestamp-aligned dataset. Slots span the union of coverage, so the non-overlapping ends blank through the existing gap handling rather than being trimmed away. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs (cherry picked from commit 821a3469561e4180d612a136ef22e1d16d48885f)
Holds the offsets on CameraRegistrationStore, beside the spatial transform
-- they are the other half of how two cameras line up, and persist with the
same dataset save. The viewer now falls back to buildOffsetTimeline when no
frame carries a timestamp, which is always the case for video panes, so a
fixed rig whose encoders started a fraction of a second apart can finally
express that. An all-zero offset still yields { aligned: false }, keeping
uncorrected datasets on the existing positional path.
Also extracts the per-camera frame count the auto-register service was
computing inline, since the timeline needs the same video-aware lookup, and
moves the registration store's construction above the timeline watch that
now reads it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs
(cherry picked from commit 149942d72d69044789d7ace263e2d7f33a574cba)
Sits under Overlay Warp because the warp is how the value gets judged: scrub with the overlay on and only the correct offset makes moving subjects line up. The fit is no guide -- a static background matches at every offset, so nothing in the registration stats can confirm it. Slider plus single-frame nudge buttons, bounded at +/-3s of the dataset's frame rate, with a frames-and-seconds readout. Only the right camera moves; the left stays the time reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs (cherry picked from commit e7de7c1d888d1cfd1e482ce85faef0f16f8c6391)
Adds cameraFrameOffsets to the mutable dataset config (client type, allowlisted mutable keys, and the server metadata model) and threads it through the two save paths and all three hydrate paths, so an offset dialed in during review survives a reload and travels with the dataset the same way the registration does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs (cherry picked from commit ee4d66b8b36e81012d9649a5d8949bf97d2f41c2)
Adds an optional per-pair frameOffset to the portable registration JSON: the right camera's start offset in its own frames, relative to the left. A producer that measured it (the batch registration driver) now hands it over with the transform instead of leaving the reviewer to rediscover it, and a correction made in the panel travels back out on export. Read on file load and through the multicam import seed, merged per camera so a later file wins one camera's offset without clearing another's, and omitted from written files when a camera is in step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs (cherry picked from commit 8b8191ed0ee3c3dd3b6c719234989a22e632ea0f)
Pipeline inputs pair positionally -- row i of one camera's list with row i of every other's -- so a rig whose recorders started at different times fed a 2-cam/3-cam detector mismatched instants, and every cross-camera association it made was quietly wrong. Only the auto-register path was safe, because it builds its imagePairs subset from aligned-timeline slots; every ordinary run took the raw-index path. Image-sequence cameras now drop the frames before their first paired instant and stop where the shortest camera does. Video cameras get the same correction as a seek (video_input's start_at_frame) rather than being extracted. Datasets with no offset set are untouched, taking exactly the previous path, and offsets that leave no overlapping span raise a clear error instead of writing empty input lists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs (cherry picked from commit 5b544c2568ed7f73599e23b3b2cc73643b489f7d)
The offset correction seeked each video to its first paired frame but left the tail open, so unequal-length recordings still ran on past each other -- which for video is not merely wasted work: one input completing while another still has frames desynchronizes the pipeline, and a writer takes a 'complete' datum where it expects data. Each video input now also carries stop_after_frame. Bounding the tail needs a frame count per camera, which a video only knows after a probe, so pairedStartFrames now takes counts rather than digging them out of the image list. That also fixes an all-video rig computing a zero-length span from image counts that were never there -- the case the previous commit would have wrongly refused to run. The probe reads the container header rather than decoding: counting frames for real would add tens of seconds per camera to every launch, and it only runs when an offset is actually set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs (cherry picked from commit fb9600090241b4809df1d386caa914ea55e4a260)
The time-offset branch corrected Desktop pipeline inputs but never the web task, so a web run of a 2-cam/3-cam pipe still paired frame i of each camera regardless of the stored cameraFrameOffsets. Port the same arithmetic: each camera skips to its first paired frame and every camera stops where the shortest does, sliced for image lists and set as start_at_frame/stop_after_frame on the video reader. Registration frame subsets are left alone since they already pair row for row. A dataset with no offset keeps the plain shortest-camera cap. The Desktop side set start_at_frame and stop_after_frame on the video_input process, which has no such keys, so the seek was silently ignored. Address the reader (video_reader:vidl_ffmpeg) instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ
Installing an aligned-timeline resolver always seeked to slot 0. With a camera time offset the timeline starts with the earlier camera's lead, where the other camera has no frame yet, and every Time Offset nudge rebuilds the timeline -- so each nudge jumped the viewer to the start and blanked a pane with "No frame at this instant", which made the slider unusable for the very judgement it exists for. When a resolver replaces another, take the first camera's displayed local frame and seek to its slot in the new timeline instead. A fresh install still starts at slot 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ (cherry picked from commit 38e5a499554beefca1c959d21825199cdeae5efe)
The offset slider lived in the Camera Registration panel, tied to that panel's left/right pair selection. It now sits in Multi Camera Tools with one control per non-reference camera (the first camera in display order is the reference, as in registration), so it no longer depends on which pair is picked. A "Save annotations" button moves every annotation on a shifted camera by its offset so the boxes line up with that camera's own video, then saves. The part of each offset already applied is recorded as cameraFrameOffsetsApplied, so a later nudge only shifts by the difference and the button disables when nothing is pending. Features that would land before frame 0 are dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ
Nudging the Time Offset moved a camera's video and its annotations together, so the boxes never disagreed with the frame under them and there was nothing to line up. Each camera pane now reads its annotations at `frame` minus that camera's unapplied offset, so while the reviewer dials the offset the boxes stay at the instant the reference camera shows and only the video underneath moves; Save annotations then bakes the offset in and the two coincide again. The first nudge on an uncorrected dataset also still jumped to frame 0: the earlier keep-the-instant fix only covered replacing one timeline with another, and a dataset with no offset has none, so a fresh install started at slot 0. Take the frame positional playback was showing in that case. Widen the slider to +/-10 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ
Two playback problems with a camera time offset on a video rig. Playing fought itself. play() started every <video> element and, with an aligned timeline installed, the aggregate tick also re-seeked both videos every frame. Two free-running decoders yanked around by async seeks at 30 Hz cannot stay in step, which is the "not in lockstep" scrubbing and playback. When every camera is a video, play now aligns each pane to the slot once and lets the elements free-run; the global slot follows the reference camera's own clock, and pause snaps every pane back onto the slot so any drift is gone. Mixed image/video rigs keep the centralized tick. Scrubbing froze the picture while the boxes moved. geojs skips rendering a video quad while any of its videos is seeking (delayRenderWhenSeeking, default true), so under a continuous scrub two decoding streams never got a render until the scrub stopped, and a zoom was the next thing to force one. Both the native video quad and the aligned-view warp now render whatever frame is decoded. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ
onResize refit every pane to its native bounds whenever its container changed size, so any change in the strip under the panes -- the controls growing a row, a gap indicator appearing on an offset timeline -- threw away the reviewer's zoom on the camera they were inspecting. Only a pane still at its fitted view is refit now; a pane the user has zoomed or panned keeps that view across the resize. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ
This reverts commit 93debc9.
Turning delayRenderWhenSeeking off was a mistake: geojs's canvas renderer then paints immediately, and its video quad draw skips a video that is still seeking, so a seek cleared the pane and nothing retried -- a black or stale frame until a pan or zoom forced a draw. With the default back on, the renderer retries every animation frame until the seek lands and paints the new frame. That retry is also why a continuous scrub of two videos never painted: every slider event issued a new seek before the previous one finished, so a quad was always seeking. The video annotator now holds the newest request while a seek is in flight and issues it on 'seeked', so each landed seek paints before the next goes out. The annotation frame and shared time still follow the slider immediately. The resize handler also ignores sub-pixel differences between the pane's box and the geojs map size, since a "resize" refits the pane and discards the reviewer's zoom. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ
warp_detections maps each box's corners through the homography and keeps the bound, with no notion of the target frame, so whatever EO sees outside IR's narrower field of view came back at negative or oversized coordinates and hung off the IR pane. After a warp pipeline finishes, the web task now clamps every box in each camera's output CSV to that camera's frame and drops rows with less than a quarter of their area inside (a zero-area box must lie inside). Frame size comes from the video's ffprobe metadata or the first image of a sequence; when neither is known the CSV is uploaded untouched with a warning in the job log. Only runs that handed the pipe a registration are affected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKg5t3obzg2v7yrjCR47VZ
Applying a time offset used to rewrite every track on the shifted camera
in the browser: a synchronous remove/rebuild/insert loop that is quadratic
in track count (each store removal scans and splices the reactive id
array), followed by an upload of the entire dataset as pending upserts and
a full pydantic re-validation on the server. On detection-per-track
datasets that blocked the page long enough for the browser's "wait or
exit" dialog.
The rewrite now happens where the data lives. A new
PATCH /dive_dataset/{id}/camera_frame_offset takes a camera and its offset,
shifts only the not-yet-applied part of that camera's tracks and groups
across every annotation set as a new revision, and writes the offset and
its applied record in the same update so a failed shift never leaves the
record ahead of the data. The desktop Express server gains the same route
over its track file, and desktop config save now persists the offset keys,
which it had been dropping. The client sends one number per camera, saves
pending edits first so the shift sees current data, and reloads just that
camera through the same yielding bulk insert the initial load uses.
Track shifting rebounds begin/end to the surviving features instead of
clamping at zero, since the server's Track validator requires begin and
end to match the first and last feature.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A multicamera dataset loaded one camera after another: config, then annotations, then track insertion with a hard 500 ms sleep every 4000 tracks, and only once every camera was through did the annotator panes mount and the videos begin to download. Two large cameras spent seconds sleeping and seconds waiting on serialized requests before either video had fetched a byte. Every camera's config and annotations are now requested at once, the media is applied for all cameras first, and a new progress.mediaLoaded flag mounts the panes at that point so the videos fetch their metadata while the annotations are still being inserted. The insertion loop yields with a zero-delay timeout instead of sleeping; the yield was only ever there to let the page paint. A progress ring overlays the panes until the annotations are in, and the pane keybindings stay off until then. Both transcodes also write the MP4 index up front (faststart), so a browser can read the video's metadata from the first bytes instead of reaching to the end of the file first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The aligned timeline tried filename timestamps first and consulted the reviewer's per-camera offsets only when no timestamps were usable. On an image sequence with a timestamp on every frame that meant the Time Offset slider moved the annotations but never the images: the panes stayed lined up by capture time while the boxes drifted, which reads as the annotations being wrong. Pipelines pair cameras by index, not by capture time, so annotations warped across a rig are index-aligned regardless of what the filenames say; the offset exists to correct exactly that. Prefer the offset timeline whenever any camera's offset is nonzero, and keep the timestamp timeline, with its gap handling, for datasets that have no offset set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Applying a camera time offset shifts that camera's tracks on the server and then reloads only that camera through reloadCameraAnnotations. The undo history kept its snapshots from before the shift, so Ctrl+Z put a track back at its unshifted frames, and the next save wrote it to the server that way while the dataset still recorded the offset as applied. The first edit after the reload had the same problem, because its "before" snapshot came from the stale baseline. Re-baseline the history once the reloaded tracks and groups are in, the same way the full reload path already does. MultiCamTools saves pending edits before applying an offset, so no unsaved work is lost with the history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reformat two multi-context `with` statements the way black 26.5.1 now wants them, sort the multicam_pipeline test imports, and drop an unused MagicMock import from the camera frame offset tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Until "Apply to annotations" runs, a shifted camera's pane draws its annotations at the video frame minus the offset, but the mode manager keys edits by the video frame (selectedCameraFrame). A box drawn there landed on a different frame than the one on screen, seemed to vanish, and was then shifted a second time when the offset was applied. Some edits went through LayerManager with the displayed frame and others through the video frame, so one session could write to both. Treat the dataset as read-only for editing while any camera has an unapplied offset. The lock covers every camera, not just the shifted one, because cross-camera writes (stereo copies, extending a detection to another camera) reach the shifted camera from the others. Saving is left on the plain readonlyState, since applying an offset first saves pending edits. A new offsetEditLock provide lets Multi Camera Tools keep "Apply to annotations" enabled under this lock while still disabling it in a truly read-only view, and it tells the user why editing is paused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stepping a video forward updated `frame` right away, and LayerManager drew that frame's annotations over the picture still showing the old frame until the seek landed. The boxes visibly jumped ahead of the video on every step. Batching seeks behind the in-flight one can make that window longer. Annotations now draw at `syncedFrame`, which a video annotator only advances on 'seeked'. The annotator records which frame each seek is for rather than recomputing it from currentTime, because kwiverSeek times do not always round back to the requested frame when the dataset's frame rate differs from the video's. Image annotators already set syncedFrame together with frame, so their behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The timeline's track bars, keyframe markers and count chart took each track's begin/end and feature frames straight from the cameras' stored frame numbers, while the playhead moves through aligned-timeline slots. Once a time offset shifts the timeline, those numberings disagree: on an EO/IR/UV sequence with one camera nudged by a frame, a detection that appeared at slot 1 was charted on frames 2-3. Under an aligned timeline, the Viewer now maps each camera's replica of a track through cameraFrameToSlot and hands the charts the resulting range and features; without one, the charts read the stored frames as before. The group chart is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The track list showed each track's begin/end as the minimum/maximum of its stored frames across cameras, in the cameras' own numberings, while the playhead and timeline count aligned slots. With a time offset set, the list disagreed with the timeline, and seeking to a track's start translated that mixed number through the selected camera, landing off by the offset. A trackTimeline provide gives each list row the track's slot range and a seek straight to a slot. The bottom list shows those begin/end frames and timestamps, both lists seek to them from the buttons and home/end keys, and start/end sorting follows the same range. Without an aligned timeline the range is null and rows use the stored frames and the existing seek, as before. Keyframe navigation and editing still use the selected camera's own frame, which is what useTime reports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Annotations draw at syncedFrame, but the image annotator advanced it the moment a frame was requested, while an uncached image loads asynchronously. Stepping mostly hits cached frames, so the gap was invisible; scrubbing requests uncached ones, and the next frame's box (often interpolated to a different size) sat over the previous image until the new one loaded, then snapped. syncedFrame now advances when the frame's image is drawn: at once for a cached image, after onloadPromise otherwise. onloadPromise also resolves on a load error, so a failed image cannot leave annotations stuck. LargeImageAnnotator is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pane draws its annotations at syncedFrame minus the camera's unapplied shift. Moving the slider raised the shift at once, but the video only reaches the matching frame when the seek the rebuilt timeline issues lands. In between, the new shift was subtracted from the old on-screen frame, so the box was drawn from the wrong frame, a different size on an interpolated track, until the seek landed and it snapped back. The +/- buttons seek once and land quickly; dragging queues seeks, so the mismatch lasted long enough to see. Each camera now adopts a changed shift only once its on-screen frame equals its requested frame, i.e. once the seek paired with that shift has landed. The watch runs after the timeline rebuild in the same tick has updated the requested frame, so it cannot adopt the new shift early. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'seeked' only means the seek finished; the browser can fire it before the decoded frame is presented, so on a fast scrub the boxes moved to the new frame while the picture still showed the old one. The landed frame now becomes syncedFrame from requestVideoFrameCallback, which fires when that frame is presented. A seek onto the frame already shown presents nothing new, so a 250 ms timeout applies it instead, and a landing on the frame already synced applies at once. Only the newest landing applies, and playback cancels a pending one since it drives syncedFrame itself. The aligned-view warp's imageRevision bump moves with it, so the warp also snapshots the painted frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ainted" This reverts commit 22cd23a. Waiting for the painted frame still left the boxes a paint behind GeoJS's own video redraw and made scrubbing stutter; video goes back to drawing at the requested frame instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holding a video's boxes until the seek landed kept them in step with the picture in principle, but GeoJS repaints the video quad on its own animation loop, so the boxes still trailed or jumped a paint apart and scrubbing felt awkward. Main draws a video's annotations at the requested frame the moment a seek is issued: the boxes lead the picture by the seek latency but move smoothly with the scrubber. A video pane's annotationFrame is now its requested frame minus the camera's unapplied shift, as on main. A Time Offset nudge changes the requested frame and the shift in the same tick, so the slider stays steady without waiting for the seek. Image panes keep drawing at syncedFrame with the settled shift, so their boxes still wait for their image. Seek coalescing stays, since it is what lets two offset videos paint during a continuous scrub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Video annotations draw at the requested frame, leading the picture while a seek lands. The new User Settings switch "Experimental: sync video annotations to the painted frame" (annotatorPreferences.videoPaintSync, on by default) draws them with the frame the browser has actually painted instead; switching it off restores main's behavior. With it on, every seek the video annotator issues watches for the next painted frame through requestVideoFrameCallback. The callback maps the frame's mediaTime back to a DIVE frame (videoTimeToFrame, the inverse of kwiverSeek, preferring the requested frame when several DIVE frames show the same video frame), sets syncedFrame, and redraws the video quad, so the annotation layers and the video paint in the same GeoJS animation frame. It keeps watching while seeks are in flight, so intermediate frames shown mid-scrub get their own boxes. A landing that paints nothing new settles after 250 ms, and playback ignores the callback since it drives syncedFrame itself. Browsers without requestVideoFrameCallback fall back to syncing when the seek lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit de54564. Syncing video annotations to the painted frame did not look right in testing; video goes back to drawing at the requested frame, as main does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
romleiaj
force-pushed
the
dev/multicam-time-offset
branch
from
September 28, 2026 20:37
de54564 to
6692169
Compare
onResize reset every pane whose container changed size, so opening a context panel such as Multi Camera Tools, or a Time Offset nudge that changes the timeline's gap row and with it the controls' height, threw away the zoom the reviewer had set, including under Align View. Each pane now remembers the view its last reset produced: the per-pane native fit, or the Align View reference fit when the aggregate reset goes through the aligned override. On a resize, a pane still at that view refits to the new size as before, and a pane the user has zoomed or panned keeps its zoom and center. Under Align View the reference pane's kept view is then copied to the others by the existing resizeTrigger re-link. An earlier version (59e8612, reverted in 7423a86 without a recorded reason) judged "fitted" against the pane's native bounds only, which is not where Align View leaves a pane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turning Align View on fits the reference frame and arms a one-shot watch on each camera's imageRevision, so imagery that lands late gets a second fit. A video pane only bumps imageRevision when a seek lands, so after turning Align on and zooming without seeking, the watch stayed armed: the first seek afterward, such as the first Time Offset nudge, fired it and threw the zoom away. Later nudges kept the zoom because the watch had already been used up. snapFromReference now records the reference view it produced, and the late re-fits (the nextTick/animation-frame follow-ups and the imageRevision watch) run only while the reference pane still shows that view. A late image still re-fits an untouched Align-on view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Puts three changes that reach beyond multicam back to main's behavior, to judge the branch without them: - VideoAnnotator sets currentTime on every seek again instead of holding a new seek behind the one in flight, and syncedFrame is read back from currentTime on 'seeked'. - ImageAnnotator sets syncedFrame as soon as a frame is requested again, so an image pane draws the new frame's annotations while its image is still loading. - The camera panes mount once annotations are loaded (progress.loaded), with the full-page loading indicator until then, rather than at progress.mediaLoaded with a ring over the panes. Cameras still load in parallel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening a multicam dataset fetched every camera's config and annotations all at once. The total server work is unchanged, but the annotation request is the heavy one (every track read from Mongo and serialized), so a large rig put several of those in flight per open, sharpening peak load when many users open datasets together. Configs are small and still load in parallel. Annotations now load one camera at a time, alongside the configs, so each open holds at most one annotation request in flight, as on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
romleiaj
requested review from
BryonLewis
and removed request for
BryonLewis
September 29, 2026 16:57
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the aligned multicam timeline work for video as well as image sequences, and fixes several multicam pipeline bugs. Each camera can now carry a time offset, set from a slider in Multi Camera Tools and
saved with the dataset. The offset lines the cameras up for review and playback, can be applied to that camera's annotations on the server, and pairs 2-cam/3-cam pipeline inputs at matching instants. While an
offset hasn't been applied yet, annotation editing is paused, because edits would land on a different frame than the one shown.
Under an aligned timeline, the timeline chart and the track list show timeline slots, so their numbers match the playhead. Zoom is now kept when panels open, when the offset changes, and when Align View's
delayed re-fit runs. Apart from the notes below, datasets with no offset behave as before.
Also fixed: VIAME no longer crashes when VIRTUAL_ENV is set, warp pipelines request the renamed homography_json reader, multicam pipeline inputs are capped at the shortest camera, warped boxes are clipped to
the target camera's frame, annotation undo is reset after an offset is applied, and multicam datasets load faster (camera configs load in parallel, annotations still one camera at a time, and track insertion
no longer sleeps).
Things to watch: