Skip to content

Output video improvements - #71

Open
jasper-tms wants to merge 19 commits into
masterfrom
output-video-improvements
Open

Output video improvements#71
jasper-tms wants to merge 19 commits into
masterfrom
output-video-improvements

Conversation

@jasper-tms

Copy link
Copy Markdown
Member

No description provided.

jasper-tms added 19 commits May 18, 2026 15:13
These files implement the pictorial-structures pose correction described
in Figure 10 of the 2019 DeepFly3D eLife paper. They were deleted in:

  - 6214d05 (2021-12-19) deepfly/belief_propagation.py
  - d1798cb (2021-12-20) deepfly/optim_util.py
  - a073020 (2021-12-20) deepfly/cv_util.py

Restored verbatim from the parent of each deletion commit, into df3d/
rather than the old deepfly/ package. Imports still reference
`from deepfly.*` and will not run as-is; subsequent commits will adapt:

  - deepfly.Config       -> df3d.config
  - deepfly.Camera       -> pyba.Camera (API differs)
  - deepfly.cv_util      -> df3d.cv_util (kept local)
  - deepfly.optim_util   -> df3d.optim_util
  - deepfly.logger       -> df3d.logger
The pre-refactor implementation called self.get_joint_reprojection_error
against self.camNetLeft / self.camNetRight (a separate CameraNetwork per
half of the camera ring). Those attributes have not existed since the
2021 deletion of deepfly/Camera.py and deepfly/CameraNetwork.py, so the
helper raised AttributeError at the first call from next_error_in_range
-- but the GUI buttons that drove that path were commented out, hiding
the breakage.

Replace with a cached per-camera per-frame per-joint reprojection error
norm tensor derived from pyba.CameraNetwork.reprojection_error(reduce=
False). Max across cameras is compared to the per-joint threshold in
config['reproj_thr'] (default 40 px). Mirrors original behaviour minus
the left/right split, which has no analogue in the current single-
CameraNetwork pipeline.
The launcher had three bugs that prevented df3d-gui from starting:

1. parse_cli_args double-used argv[2] as both num_images_max (int) and
   output_subfolder (string).
2. The startup assertion looked for df3d_result*.pkl in input_folder/
   output_subfolder, but df3d-cli writes results to <input_folder>_df3d
   (a sibling, not a subfolder).
3. Core's constructor's second positional argument is output_folder
   (full path), not output_subfolder; the GUI passed a bare subfolder
   name like 'df3d' instead, breaking save_path.

Switch to argparse, mirror df3d-cli's --output-folder flag and default
(input_folder + '_df3d'), and drop the now-unused output-subdir prompt.
These were commented out because Core.joint_has_error was broken (called
attributes that no longer exist post-2021 refactor). With that helper
repaired in the previous commit, the buttons can be wired back up.

The checkbox_solve_bp line is intentionally still disabled -- belief-
propagation pose correction itself is not yet plumbed back through Core,
so the auto-correct toggle would have nothing to call.
Mechanical adaptations applied to the verbatim-resurrected modules so
they can at least be loaded by the current df3d/pyba layout:

* Renamed imports: deepfly.Config -> df3d.config, deepfly.cv_util ->
  df3d.cv_util, deepfly.optim_util -> df3d.optim_util, deepfly.Camera
  -> pyba.Camera, deepfly.logger -> df3d.logger.
* Deferred config['image_shape'] / config['num_peak'] / etc. defaults
  from kwarg-definition time to function-body time; image_shape is only
  populated by df3d.core.Core at runtime, so import-time evaluation of
  those defaults raised KeyError.
* Replaced numpy aliases removed in modern numpy: np.int -> int,
  np.float -> float.
* Added a single-point project shim (_project_single) so error_reprojection
  and project_on_last keep working against pyba.Camera.project's batched
  (T, J, 3) -> (T, J, 2) signature.
* Documented the three remaining API gaps to pyba.Camera at the top of
  belief_propagation.py: cam.cam_id, cam.get_heatmap, Camera.hm_to_pred.
  Filling these is part of the heatmap-plumbing work, not this commit.
Adds the data-collection side of the pictorial-structures pose-correction
restoration: Core.pose2d_estimation can now optionally fetch the top-K
per-channel heatmap peaks (via the new df2d.heatmap_peaks plumbing) and
store them in the result pkl under 'top_k_peaks', shape
[n_cameras, n_frames, 38, K, 3] with last axis (y, x, score).

Peaks are expanded from 19 to 38 joints mirroring points2d, including
the right-side camera horizontal-flip (applied only to real peaks; padded
slots stay (0, 0, 0) so the score==0 sentinel still distinguishes them).

K defaults to config['num_peak'] (=10) -- one of the orphaned BP config
parameters that has been sitting unused since 2021.

Opt-in only: the flag is off by default, so the existing pkl schema is
unchanged for users who do not want belief propagation. Sanity-checked
end-to-end against tests/data/reference: peak[0] coordinates match the
existing points2d argmax for every cam/frame/joint, and the highest peak
score matches heatmap_confidence.
…am.heatmaps

belief_propagation.py:
  Use df2d.util.heatmap_peaks for top-K peak extraction (the
  pyba.Camera dependency is gone). Added _top_k_peaks_xy_normalized
  which wraps heatmap_peaks and swaps (y, x) -> (x, y) at the
  convention boundary (BP uses pyba's (x, y) order internally).

  Looking up heatmaps now goes through cam.heatmaps[img_id, j_id]
  directly -- df3d.core.Core will attach the network's heatmaps to
  each pyba.Camera before invoking BP.

  Updated module docstring to drop the API-gap warnings; all three
  blockers (cam_id, get_heatmap, hm_to_pred) are resolved.

optim_util.py:
  Fixed a latent coord-order bug: config['heatmap_shape'] is stored
  as (H, W) to match the network's row-major axes, but the code
  multiplied (x, y) points by it -- which would put y in [0, H) and x
  in [0, W) ranges that disagree with hm[y, x] indexing. Now reverse
  to (W, H) before multiplication. image_shape is already (W, H) in
  config so it stays as-is.

  probability_heatmap now reads cam.heatmaps[img_id, j_id] instead of
  the deleted cam.get_heatmap method.
Adds Core.run_belief_propagation() and a corresponding --belief-propagation
CLI flag. When the flag is set:

* pose2d_estimation retains the full per-joint heatmaps from inference
  (keep_heatmaps=True) -- expanded to 38-joint padded form and with the
  right-side cameras' heatmaps horizontally flipped to match the saved
  points2d's un-flipped image coordinates. About 8.7 GB / 1000 frames;
  not persisted.
* After calibrate_calc(), each pyba.Camera in self.camNet is given its
  heatmaps, BP runs per frame, and corrected (x_norm, y_norm) points are
  swapped back to df3d's (y_norm, x_norm) convention and written into
  both self.points2d and each cam.points2d (so the subsequent save()
  triangulates from corrected detections).
* The middle camera is excluded from BP because the current pipeline
  routes no per-joint heatmaps to it -- including it would empty the
  itertools.product over per-camera candidate peaks.
* Cameras that don't see a given joint (or whose peak list is empty) now
  contribute a single placeholder peak to keep the product non-empty;
  placeholders are filtered out via camera_see_joint downstream.

Verified end-to-end on tests/data/reference (15 frames): BP changes 20%
of the per-cam per-joint slots, mean reprojection error barely moves
(1.87 -> 1.86 px) but the worst outlier drops from 43.94 to 16.08 px --
which is exactly the failure mode (a few egregious 2D outliers) that BP
is supposed to fix via cross-view geometric consistency.

Also: silenced a debug 'not float' print in optim_util.prob_from_heatmap
that fired hundreds of times per frame during the BP loop.
The saved points3d was in an arbitrary procrustes-template frame (~94 deg
off the fly body), so a coordinate like "tarsus-tip x" mixed anterior-
posterior, medial-lateral and dorsal-ventral motion. New df3d.body_align
computes a body frame per recording from the six thorax-coxa landmarks and
the dorsal stripe, and rotates every point into it:

  x = anterior-posterior (+x anterior)
  y = medial-lateral      (+y the fly's left)
  z = dorsal-ventral      (+z dorsal / leg lift)

Applied in Core.save() and Core.get_points3d(), gated by the config key
align_body_axes (default True); points3d is stamped body_axis_aligned so
downstream loaders can align legacy results without rotating twice.
points3d_wo_procrustes keeps the raw frame. The transform is a proper rigid
rotation (det +1, no reflection) about the coxae centroid, so joint angles
and all relative geometry are unchanged (verified bit-for-bit against
df3dPostProcessing).

The ~94 deg offset came from the procrustes template, data/df3d_result.pkl,
an old result stored in its own camera frame that every recording is aligned
to. That file is now rotated into the body frame too, so procrustes output
arrives 0.5 deg from aligned instead of 96 deg, and callers get roughly
correct axes even with align_body_axes off. The explicit rotation stays, so
the axes are still guaranteed for any other template. Rotating the template
only re-orients the output: procrustes runs with scaling=False, so it
applies an orthogonal transform whatever it fits, and every within-side
inter-joint distance is unchanged to 4e-15. The test_calibration reference
moved by the same transform; points3d_wo_procrustes predates procrustes and
is untouched.

Also stop applying rotate_points3d() in get_points3d(). That transform swaps
and negates y and z, which has determinant -1: it is a reflection, not a
rotation, and existed only to make the old arbitrary template frame display
upright. It mirrored the fly, so +y meant the fly's right in get_points3d()
but its left in save(). With the template upright it is simply wrong, so it
is gone, and its docstring now warns against reintroducing it.

test_body_axis_alignment covers the feature, asserts both code paths agree
on the medial-lateral sign, and asserts the shipped template is still
body-aligned. test_video_3d remains failing: it already failed on clean
master from a matplotlib version mismatch, and its reference is now stale on
content too since get_points3d() no longer reflects. Regenerating it needs
an environment that reproduces the original rendering.
Add a bone_lines kwarg to plot_drosophila_3d and _compute_3d_img: when
given, the existing per-bone Line3D artists are updated via
set_data_3d (and the associated figure reused) instead of rebuilding
a fresh Figure/Axes3D/lines every frame. make_pose3d_video now builds
one canvas per camera via _make_3d_canvas and passes bone_lines into
every per-frame _compute_3d_img call. Callers that pass no bone_lines
get the original create+close-fig behavior.
Adds a boolean (default in config['only_render_legs']) that, when set,
zeroes antenna and stripe joints in each camera's stored 2D points
(after bundle adjustment) so pyba skips drawing them, and filters
those joints out of the 3D plot via draw_joints.
Previously every 2d thumbnail was force-resized to a fixed 200x100 cell,
which stretched non-2:1 input frames. The whole stacked frame was then
upsampled to 5000px wide, leaving images visibly pixelated.

Each row now has 3 cells of cell_width px (= video_width // 3). 2d
thumbnails preserve source aspect; 3d figures rasterize directly at
cell_width square. No final cv2 upsample. Module-level constants are
in pixels — no more dpi/inches. Default video width is 1920 (1080p).

Reference test videos regenerated for the new dimensions.
When camera_{cam_id}.mp4 (or .avi) exists in input_folder, pass that
path to pyba.CameraNetwork instead of the per-frame jpg template.
pyba's Camera.get_image then streams frames via cv2.VideoCapture,
which can be up to ~10x faster than per-frame cv2.imread over a network
mounted server.

Requires the matching pyba change that taught Camera to accept a video
file as image_path.

Also label the 2D and 3D render tqdm bars with a desc + total so
their progress is distinguishable and shows an ETA.
Replace the hardcoded video_width=1920 with output_video_downsampling
(default 2) and derive per-cell width from the source frame width at
render time. _make_3d_figure / _make_3d_canvas now take cell_width
explicitly; _compute_3d_img gets a cell_width keyword arg (default
480) for standalone use of its bone_lines=None fallback.
only_render_legs (now default True) previously hid antenna/stripe
joints by zeroing them directly on the camera network before
triangulation/save. That corrupted their saved points3d to (0, 0, 0),
which in turn broke df3d.body_align's dorsal-ventral sign
disambiguation (it reads the stripe's 3D position to tell which way is
"up"), flipping the whole 3D skeleton upside down. Replaced the mutating
mask with Core.hidden_joints(), a pure query consumed only by plot_2d's
new hidden_joints= parameter (paired with a PyBundleAdjustment commit
adding that parameter to Camera.plot_2d/plot_reprojections) -- rendering
can now hide joints without touching any data calibration, triangulation
or alignment depend on.
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.

1 participant