Skip to content

Support for fisheye (instaX5). - #536

Merged
JanuszBedkowski merged 34 commits into
mainfrom
mp/equirectangular-camera-model
Sep 25, 2026
Merged

JanuszBedkowski merged 34 commits into
mainfrom
mp/equirectangular-camera-model

Conversation

@michalpelka

@michalpelka michalpelka commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Few rather large changes to work with camera.

  • Support for Mei Fisheye in hdmapping-camera_lidar_calibration (the Mei extrinsics solver currently needs Ceres, -DCALIB_ENABLE_CERES=ON)
  • Support for Mei Fisheye in hdmapping-camera_lidar_trajectory_viewer, including the ROS 2 export (CameraInfo with insta360_mei_v2)
  • Image masks for coloring in hdmapping-camera_lidar_trajectory_viewer, and an option to export only points that got their color from an image
  • Load timestamps from .meta.json in hdmapping-camera_lidar_trajectory_viewer for build-in camera.
  • Some changes for 360 calibration in hdmapping-mandeye_with_360_camera_manual_coloring: intensity view, extrinsics sliders, 2D overlay, reprojection error, --photo/--laz (i think it should be retired)
  • Command line tool session_to_mcap.
  • Fix trajectory CSV parsing of fractional timestamps.

Notes:

  • No equirectangular (360) camera model: calib_core, camera_lidar_calibration and camera_lidar_trajectory_viewer support Pinhole and Mei only.
  • The ROS 2 export no longer rectifies images. Frames go out as captured, and CameraInfo carries the real distortion.

🤖 Generated with Claude Code

michalpelka added a commit that referenced this pull request Sep 16, 2026
- App.cpp: drop a stray blank line before a closing brace that
  clang-format flags.
- test_camera.cpp: widen the epsilon on the two "top edge" pole
  assertions. asinf(-1) isn't correctly rounded on every platform's
  libm (macOS/AppleClang's included) -- the derivative of asin is
  infinite at the pole, so even a 1-ULP wobble there gets amplified
  through the pixel-height scale. doctest::Approx's default epsilon
  is an absolute tolerance too tight for that when comparing against
  0, which made these two checks flaky on macOS CI.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
michalpelka added a commit that referenced this pull request Sep 20, 2026
- Reformat session_to_mcap.cpp lambdas to match clang-format-21 output
  (multi-line lambda bodies, split argument lists).
- McapFileWriter now always registers a /tf channel alongside
  lidar/imu/sn, so McapFileReader::channels() returns 4 channels, not 3.
  Update the two "topic resolution" test assertions accordingly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LuAR1gPWFRZxRpCfzyRn9s
@Sau72

Sau72 commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Is here will be instruction, with using insta 360 camera? Which sync methods must using with insta camera?

@michalpelka

michalpelka commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

@Sau72 this work is still in progress

michalpelka added a commit that referenced this pull request Sep 23, 2026
- App.cpp: drop a stray blank line before a closing brace that
  clang-format flags.
- test_camera.cpp: widen the epsilon on the two "top edge" pole
  assertions. asinf(-1) isn't correctly rounded on every platform's
  libm (macOS/AppleClang's included) -- the derivative of asin is
  infinite at the pole, so even a 1-ULP wobble there gets amplified
  through the pixel-height scale. doctest::Approx's default epsilon
  is an absolute tolerance too tight for that when comparing against
  0, which made these two checks flaky on macOS CI.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
michalpelka added a commit that referenced this pull request Sep 23, 2026
- Reformat session_to_mcap.cpp lambdas to match clang-format-21 output
  (multi-line lambda bodies, split argument lists).
- McapFileWriter now always registers a /tf channel alongside
  lidar/imu/sn, so McapFileReader::channels() returns 4 channels, not 3.
  Update the two "topic resolution" test assertions accordingly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LuAR1gPWFRZxRpCfzyRn9s
@michalpelka
michalpelka force-pushed the mp/equirectangular-camera-model branch from 190d04c to d66bc00 Compare September 23, 2026 11:26
@michalpelka

Copy link
Copy Markdown
Contributor Author

rebased on top main

michalpelka and others added 22 commits September 24, 2026 14:33
…ntensity windows, 2D overlay, CLI, error display

- Fix broken intensity grayscale (was unnormalized glColor3f) and add a
  calibration window (min/max/gamma, auto-range) for it.
- Add an extrinsics calibration window with live-preview sliders and
  precise +/- angle nudge buttons for the camera-to-LiDAR pose.
- Add an optional 2D projection overlay (intensity or depth, jet
  colormap, alpha, decimation) clipped to the image viewport.
- Add --photo/--laz CLI flags to load files on startup.
- Show per-pair reprojection error and RMS in the point-pair lists, with
  a "remove pair" action that keeps the 2D/3D correspondence lists in sync.
- Add Space as a shortcut to toggle RGB <-> intensity in the 3D view.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…viewer

calib::Intrinsics gains a CameraModel tag (Pinhole | Equirectangular) plus the
image dimensions, which are what an equirectangular camera projects with in
place of fx/fy/cx/cy, and projectPoint branches on it. The change is purely
additive: projectPoint's signature is unchanged and the model defaults to
Pinhole, so camera_lidar_calibration keeps building and behaving identically.
It does not yet read or write the "model" key -- the comment on CameraModel
records that gap, the other pinhole-only spots in that app, and how the
vendored equirectangular observation equations drop into the solver when it
is picked up.

camera_lidar_trajectory_viewer wires the model up end to end:

- loadCalib reads a "model" key, at the top level or under "intrinsics".
- The image scanner accepts equirectangular_<ts>.jpg and bare <ts>.jpg
  alongside cam0_<ts>.jpg, and infers the model from the prefix when the
  calibration doesn't name one. Both loaders share one parse now instead of
  two copies, and the model is resolved in a single place because the
  calibration and the images arrive in either order.
- colorize() calls calib::projectPoint instead of duplicating the distortion
  math inline, over intrinsics scaled by a new image-scale control -- a chunk
  of 360 frames is ~2.2 GB at full size and multi-image coloring holds a whole
  chunk resident.
- Image dimensions come from the first scanned frame rather than a hardcoded
  4656x3496 that matched neither camera; they drive the ROI default, the
  frustums and COLMAP's cameras.txt.
- Frustums become a position marker and axis triad for a camera with no
  frustum; COLMAP export refuses, since its text model has no equirectangular
  type; the ROS CameraInfo reports "equirectangular" with no K rather than a
  pinhole that would mislead consumers, and rectification is skipped.

New calib_core/tests (doctest, following shared/tests) cover the seam wrap,
the poles, a bearing round trip, behind-the-camera points and the extrinsics,
and pin the pinhole path against the refactor with hand-computed values.

Also fixes a pre-existing include in RosExport.cpp that fails to compile
whenever CALIB_ENABLE_ROS_EXPORT is ON.

Co-Authored-By: Claude Opus 5 <[email protected]>
…r_calibration

calib::CameraModel gains a Mei enumerator for the Insta360 rig's per-lens
fisheye (distortion_model insta360_mei_v2). projectPoint does not re-derive the
unified-sphere math -- it wraps the existing MeiCamera (MeiCamera.h/.cpp), which
until now was an orphan: absent from every CMakeLists source list, and its own
quoted "MeiCamera.h" include didn't match this repo's include/CalibCore layout,
so it had never actually been compiled here. Intrinsics gains `xi`; k1/k2/k3 and
p1/p2 are reused as Mei's own (non-rational) polynomial, k4/k5/k6 unused.

calib_core therefore now compiles MeiCamera.cpp and picks up OpenCV's
header-only Point2d/Point3d and yaml-cpp (camera_info.yaml loading) for it.

camera_lidar_calibration is wired up end to end:

- loadIntrinsics detects a Mei camera_info.yaml by its distortion_model key and
  loads it via LoadMeiCamera; the OpenCV-YAML path now resets model/xi
  explicitly so a Mei calibration can't linger behind a pinhole one.
- load/saveCalibration round-trip "model" and "xi".
- Intrinsics naming a different resolution than the loaded image are auto-scaled
  (calib::scaleIntrinsics) rather than only warned about, in whichever order the
  image and the calibration arrive; saveCalibration records the resolution they
  apply to (width/height) so the next load can do the same.
- rebuildImageTexture skips initUndistortRectifyMap for non-pinhole models: it
  assumes OpenCV's rational pinhole model and would mis-warp a fisheye rather
  than rectify it, so a Mei image is always shown raw.
- Both GLSL shaders gain a Mei branch mirroring MeiCamera::Project, so the
  projection overlay and Camera-RGB coloring work against that raw image. The
  clip weight is Xs.z+xi, the model's own "in front of the camera" test (it
  reduces exactly to the pinhole z_cam when xi==0); using the range there
  instead let points behind the camera through the hardware clip.
- projectPoint applies that same Xs.z+xi>0 guard on the CPU side. MeiCamera::
  Project has no domain guard of its own and the projection isn't injective
  past its valid dome, so a point behind the camera could otherwise be scored
  as visible at a plausible-looking pixel.
- drawCameraFrustum becomes a position marker plus axis triad for non-pinhole
  models -- a rectangular pyramid misrepresents a fisheye's field of view --
  matching what camera_lidar_trajectory_viewer already does for equirectangular.
- The Intrinsics panel gains a model combo and an xi drag, and hides k4/k5/k6
  for Mei rather than showing dead controls.

New solveExtrinsicsMeiCeres: a Ceres-based extrinsics solver for Mei, since no
vendored analytic Jacobian exists for this model as it does for Pinhole. It is
gated behind -DCALIB_ENABLE_CERES, OFF by default because the README advertises
depending on nothing but Eigen for optimization; without it the function
compiles to a stub reporting why it is unavailable, so callers need no #ifdef of
their own. The Pinhole solver is untouched and stays analytic.

Tests check the Mei projection against MeiCamera::Project directly (so the
wrapper is verified as delegation, not as a re-implementation), the
behind-the-camera rejection for xi<1 alongside full-sphere coverage at xi>=1,
scaleIntrinsics for Mei, and the no-Ceres stub.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
(cherry picked from commit f78fe80 on fork/mp/work)

Adapted during the cherry-pick: the source branch carries the
fast-rotation image filter (#526), which this branch does not, and
f78fe80 touches the same lines. The filter's machinery -- poseAngSpeedDeg,
poseAngSpeedMax, filterFastImages, maxImageAngSpeedDeg, angFilteredImgs and
the angularSpeedDegAt()/computePoseAngularSpeedDeg() helpers -- was dropped
rather than carried along: the calls came across in the conflicted hunks but
their definitions did not, so keeping them would not have compiled. Bringing
the filter here is a matter for cherry-picking #526 on its own.

What the commit actually contributes is kept whole: the intensity drawing,
the Camera.cpp fold-back guard ported from #527, and the imageTimeOffsetMs
camera/LiDAR clock offset with its imageOffsetNs() plumbing.

Note that imageTimeOffsetMs has no writer -- no widget, CLI flag or
calibration-file key sets it, here or on the source branch -- so it stays 0
and the offset plumbing is inert until something wires it up.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The calibration loader reads "mei"/"insta360_mei_v2" and xi, the 5 cm near
clip now applies to Mei as well as pinhole (its depth is a range rather than
a z, but 5 cm means the same thing physically), and every non-pinhole model
draws an axis triad instead of a frustum -- a fisheye sees far more than the
pyramid fx/fy/cx/cy imply.

Rectification and the COLMAP export are gated on Pinhole:
initUndistortRectifyMap would mis-warp a fisheye rather than rectify it, and
no COLMAP camera type carries an xi. The ROS 2 export reports the rig's own
insta360_mei_v2 distortion tag rather than claiming plumb_bob, with xi
appended to d since CameraInfo has nowhere else to put it.

Also: image filenames parse as "<any prefix>_<timestamp_ns>.jpg", so the 360
rig's per-lens frames work without the parser knowing the list of rigs, and
the camera/LiDAR clock offset becomes timeOffsetSec (seconds, double),
applied to the stamps the ROS 2 bag is written with as well.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The mask is the free-form counterpart of the ROI: a point projecting onto a
masked-out pixel stays uncolored, which is what it takes to drop the operator
and the rig itself out of a 360 frame -- no rectangle can cut those out
without cutting out the scene with them. Loaded from any image OpenCV reads
(File > Open Image Mask..., the Calibration panel, or drag & drop) and
thresholded to a strict 0/255, so a hand-painted PNG and a jpeg with
compression noise behave the same. It is resampled to whatever size the
frames are actually read at, so its own resolution doesn't have to match:
one drawn over a downscaled copy of a frame works as well as a full-res one.
Rejected pixels are tinted red over the image preview, and the "In ROI"
point color mode becomes "In ROI / mask", painting what either filter
rejects.

The ROI had the scale mismatch the mask sidesteps: it is specified in
full-resolution pixels (calib::Roi) but was compared against pixels of the
downscaled images the colorizer reads, stretching it by 1/imgScale at any
Image scale below 1. calib::scaleRoi now scales it alongside the intrinsics.
It rounds both edges and subtracts, rather than scaling the width on its own,
so abutting rectangles cannot come back overlapping, and it never lets a
non-empty rectangle collapse to w/h == 0 -- the sentinel every caller reads
as "no ROI set", i.e. accept everything.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…ominator

The Mei domain guard was "Xs.z + xi > 0", which is the right limit only for
xi <= 1. For xi > 1 -- both rigs in use are calibrated at xi = 2.0 -- that
expression never goes non-positive (Xs.z + 2 stays in [1, 3] for every
direction on the sphere), so the guard never fired and every direction,
including straight backwards, produced a finite pixel.

It is worse than an unfiltered projection: r(theta) = sin/(cos + xi) is only
injective up to its turning point at cos(theta) = -1/xi, and past that the
radius shrinks again, folding far-off-axis directions back onto real pixels
rather than pushing them out of frame. At theta = 180 deg the radius is
exactly 0, so a point directly behind the camera lands on (cx, cy) -- dead
centre. That is the "masked points still get colours" symptom: geometry
behind the rig painting into the middle of the image.

The limit both regimes share:

    xi <= 1:  Xs.z > -xi     (blow-up; reduces to Pinhole's pc.z > 0 at xi = 0)
    xi  > 1:  Xs.z > -1/xi   (fold-back)

applied in calib::projectPoint and in both of camera_lidar_calibration's GLSL
shaders -- kProjVS's clip weight becomes the distance inside that dome rather
than the denominator, and kPointVS's fragCamDepth carries the same sign for
the Camera-RGB "seen" test.

For a real lens the calibrated image circle is tighter still (the 3840x3840
xi = 2.0 front lens reaches its edge at theta = 101.2 deg, i.e. ~202 deg FOV,
while the fold is at 120 deg), so the bounds check remains the binding
constraint in normal use; this only removes the directions that were bypassing
it entirely.

Tests replace a case that asserted the old behaviour outright ("xi >= 1 covers
the full sphere -- straight behind still succeeds") with the cutoff at
acos(-1/xi), acos(-xi) and the xi = 0 pinhole half-space. The helper building
those directions needs an explicit -> Eigen::Vector3f: with auto it deduces an
expression template holding a reference to the temporary, which dangles and
made the tests report nonsense.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…ry viewer

Upstream's fix for colorizing invalid points (#527) applies the rational
model's fold-back cutoff inline in TrajectoryViewer.cpp, against local
d_k1..d_k6 distortion variables. This branch had already moved that same
logic into calib_core when coloring switched to calib::projectPoint, deleting
those locals with the rest of the inline distortion math. Both sides merged
without a textual conflict, leaving a call to maxValidRadiusSq() with
arguments that no longer exist -- so the app did not compile after the rebase.

Removes the reinstated copy of the function and its call site. The cutoff
itself is unchanged in behaviour: calib::projectPoint runs it internally
(Camera.cpp's cachedMaxValidRadiusSq), which is where the equirectangular
commit deliberately put it so every caller is covered rather than this one
call site. rMaxSq had no other reader.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
build-ceres/ and rosbags.zip sat untracked and unignored, so `git add -A`
would sweep 424 MB of build output into a commit. /build-* follows the
anchored style of the existing /build2 and /build3 entries and covers
future variants; /rosbags.zip is anchored to the root so the tracked
rosbags/ source directory is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
saveCalibrationJson writes camera_pose.inverse(), which is the R_wc/C the
shared schema calls for, but loadCalibrationJson read those fields straight
back into camera_pose without inverting. The two are not the same thing: the
*.reg file stores camera_pose verbatim (the LiDAR-to-camera transform,
p_cam = M*p), while the JSON's camera_rotation_matrix_in_world and
camera_position_in_world_xyz are R_wc and C (p_cam = R_wc^T * (p - C)).
Reading them the same way left the pose inverted, which mis-projects
plausibly rather than failing, and meant loading a file this app had just
saved did not reproduce the pose it saved.

Load now converts, so the pair round-trips exactly.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…ribution

calib_core had picked up find_package(yaml-cpp REQUIRED), but nothing else in
the repo uses yaml-cpp, no CI workflow installs it, and Windows/macOS take
their dependencies from pre-downloaded binaries with no equivalent path for
it -- so the build configured only where it happened to be installed already.
cv::FileStorage cannot substitute (it rejects plain YAML without a %YAML:1.0
header), so LoadMeiCamera now parses this rig's flat camera_info.yaml
directly, covered by tests. MeiCamera::Project takes Eigen rather than
cv::Point, which drops OpenCV from calib_core's public interface and restores
its "nothing but Eigen/LASzip/std" rule.

Also:
- modelToString/modelFromString were declared static in Camera.h. At
  namespace scope that is internal linkage, so every includer warned about an
  unused function and the definitions in Camera.cpp were never exported --
  App.cpp's call sites would not have linked. Their -Wswitch rationale moved
  across with them.
- Removed MeiCamera::Unproject and AppState::imageTimeOffsetMs, neither of
  which had any caller; imageTimeOffsetMs also duplicated timeOffsetSec and
  documented behavior it did not have.
- Camera.h's CameraModel comment claimed camera_lidar_calibration neither
  reads nor writes the "model" key, which all three of loadIntrinsics,
  loadCalibration and saveCalibration now do.
- Fixed 55 clang-format 21 violations across App.cpp, TrajectoryViewer.cpp
  and manual_color.cpp; apps/ is clean again under the version CI runs.
- Camera.h's documentation is now doxygen, matching the //! and @PARAM style
  the rest of the codebase uses.
- Trimmed comments that restated the code or speculated about future work,
  and dropped a personal absolute path and a pointer to a file that does not
  exist.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Matches the //! and @param/@return/@note style Camera.h and the rest of the
codebase use, so calib_core's public headers no longer mix two conventions.
Struct members are documented individually, and the contracts that were
buried in prose -- what each function returns on failure, which pixel frame
each solver expects, that errorMessage is non-defaulted so a failure reason
cannot be dropped -- are now tagged.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Completes the conversion, so every documented declaration in calib_core's
public headers now uses //! with @param/@return/@note. Code is untouched --
these two predate the .clang-format brace style and calib_core is outside the
format check's scope, so only the comments changed.

Two contracts that were previously implicit are now written down:
Trajectory::nearest binary-searches, so it assumes poses are sorted and gives
an arbitrary answer otherwise, and it clamps to the first or last pose rather
than failing when the timestamp falls outside the trajectory.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
MeiCamera duplicated ten fields that Intrinsics already had, so the same
numbers were copied twice: once from the yaml into a MeiCamera and then into
an Intrinsics on load, and again from Intrinsics into a throwaway MeiCamera on
every projectPoint call.

The struct and its header are gone. loadMeiIntrinsics fills an Intrinsics
directly, and the unified-sphere projection now lives in Camera.cpp's Mei
branch beside the pinhole and equirectangular ones -- which is also where its
domain guard already was, so the math and the guard that protects it are no
longer split across two files. src/MeiCamera.cpp becomes src/MeiIntrinsics.cpp,
now purely the yaml reader.

frameId is dropped, nothing having read it. distortion_model is still checked
and still warns when it is not insta360_mei_v2, but stays internal to the
loader rather than being exposed as a field.

The test that checked projectPoint against MeiCamera::Project had nothing left
to compare against, so it is replaced by one pinning the projection to
reference values captured from the previous implementation -- they pass
unchanged, so this refactor is behavior-preserving.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Frames now go out exactly as captured and CameraInfo always carries the real
distortion, so consumers that want rectified images undistort from it.

Rectification only ever applied to Pinhole anyway -- OpenCV's
initUndistortRectifyMap has nothing to say about a 360 panorama, and Mei's
k1/k2/k3/p1/p2 are its own polynomial applied after a unit-sphere step a K/D
pair cannot express -- so it was a per-model special case guarded by a
checkbox that was disabled for two of the three models.

Removing it also removes the re-encode it forced: with nothing to rectify,
the compressed path copies the source jpeg verbatim in every case rather than
decoding and re-encoding it, so exported images no longer lose a generation
of jpeg quality. imageFiles is jpeg-only (see imageTsFromName), so the
verbatim copy is safe.

The undistortCamera option, the rectify maps, and the now-unused calib3d and
imgproc includes are gone with it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Cuts design history, narration of what other files do, and a block of
commented-out dynamic-subsampling code that git still has. Comment blocks of
five lines or more drop from 171 lines to 106, with none over 10.

File-scope declarations and AppState members now use //! (//!< for trailing
member comments), matching Camera.h and the rest of calib_core; @param/@return
are added where a function has an out-parameter or a non-obvious failure case.
Section dividers and in-body comments stay plain //.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Cuts the rationale that narrated step2's internals and the camera-framing
formula. Blocks of five lines or more are now 13 / 77 lines, down from 25 / 171
before this pass started.

Also fixes a misattributed doc block: the mask commit inserted actionOpenMask
directly beneath handleDroppedPath's comment, so actionOpenMask was documented
as the drag & drop handler while handleDroppedPath carried a near-duplicate of
the same text further down. actionOpenMask now describes itself, and the drag &
drop description survives once, on the function it belongs to.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
PointCloud.cpp includes nlohmann/json.hpp but calib_core never added
the bundled 3rdparty/json/include dir to its include paths, so the
header wasn't found on macOS/AppleClang builds.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
- App.cpp: drop a stray blank line before a closing brace that
  clang-format flags.
- test_camera.cpp: widen the epsilon on the two "top edge" pole
  assertions. asinf(-1) isn't correctly rounded on every platform's
  libm (macOS/AppleClang's included) -- the derivative of asin is
  infinite at the pole, so even a 1-ULP wobble there gets amplified
  through the pixel-height scale. doctest::Approx's default epsilon
  is an absolute tolerance too tight for that when comparing against
  0, which made these two checks flaky on macOS CI.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…, tf)

New console tool that writes a processed lidar_odometry_step_1 session
(session.json) to an MCAP file containing already-undistorted lidar points
placed in the map frame, a /tf stream of map->lidar transforms taken from
each chunk's local_trajectory, and optionally /imu re-read from the
original mandeye recording directory (a session keeps no raw IMU samples).

Extends rosbags::McapWriter with tf2_msgs/msg/TFMessage support (hand-rolled
ros2msg/CDR encoding, same approach as the existing PointCloud2/Imu channels,
no real ROS dependency) and a decoupled point-cloud frame_id so a cloud can
be published in a different frame than the Imu/tf child frame.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
michalpelka and others added 3 commits September 24, 2026 14:33
- Reformat session_to_mcap.cpp lambdas to match clang-format-21 output
  (multi-line lambda bodies, split argument lists).
- McapFileWriter now always registers a /tf channel alongside
  lidar/imu/sn, so McapFileReader::channels() returns 4 channels, not 3.
  Update the two "topic resolution" test assertions accordingly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LuAR1gPWFRZxRpCfzyRn9s
Replaces the hand-rolled FRAME_WALL_CLOCK text scan in Camera.cpp with
proper JSON parsing via nlohmann::json (already a calib_core dependency
via PointCloud.cpp), and drops the now-unused <cstdlib>/<map> includes.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
lidar_odometry_step_1 writes timestamp_nanoseconds as a double
(seconds * 1e9), so rows can carry a fraction such as
"548348730189.99993896". Trajectory::loadCSV read it into an int64,
stopping at the '.', which shifted every pose column by one. In the
TrajectoryViewer this interleaved garbage poses with correct ones,
drawing filled wedges along the path and a bogus angular-speed peak.

Read the timestamp as a double and round it, in loadCSV and in the
viewer's chunk time-range read. Add a regression test.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@michalpelka
michalpelka force-pushed the mp/equirectangular-camera-model branch from 0fb5546 to b85d4bc Compare September 24, 2026 12:38
michalpelka and others added 6 commits September 24, 2026 15:33
…y LAZ export

ColorPt gains validColor, set when an image colored the point (vs the
intensity-gray fallback). A new Export-panel checkbox makes the LAS/LAZ
export skip points without valid color; header count and bounds cover
only the written points.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Drops CameraModel::Equirectangular from calib_core, leaving Pinhole and
Mei: its projectPoint branch, its model-name mapping and its tests. The
Mei scaleIntrinsics test now covers width/height scaling instead.

camera_lidar_trajectory_viewer loses the "Load as equirectangular"
checkbox and the model resolution behind it -- loadCalib now sets
K.model straight from the "model" key -- along with the seam wrapping in
colorize and the intensity projection, and the equirectangular cases in
the COLMAP refusal and the calibration panel. The ROS export no longer
publishes an "equirectangular" CameraInfo.

Intrinsics::width/height stay: Mei's yaml sets them and the intensity
projection sizes its output from them. manual_color is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds CameraModel::Fisheye to calib_core: cv::fisheye's projection, with
k1..k4 applied to the incidence angle. theta comes from atan2 rather
than OpenCV's atan(r), so it matches cv::fisheye::projectPoints in front
of the camera and still projects past 90 deg for lenses wider than 180.
fisheyeMaxTheta finds where the theta polynomial stops increasing, and
projectPoint rejects directions past it, as it does Mei's fold-back.
Written as "fisheye" in the calibration JSON; "equidistant" is read too.

The Ceres extrinsics solver now handles Mei and Fisheye, so it is
renamed solveExtrinsicsCeres (CameraCalibrationSolverCeres.cpp) and
refuses any other model.

camera_lidar_calibration offers Fisheye in the model combo, shows its
image raw like Mei's, projects it in both GLSL shaders, and solves it
with the Ceres solver. An OpenCV/ROS YAML selects it only through
distortion_model: equidistant|fisheye, and needs exactly 4 coefficients.

camera_lidar_trajectory_viewer reads the model through modelFromString,
shows Fisheye in the calibration panel and exports it to COLMAP as
OPENCV_FISHEYE. The ROS export publishes it as an "equidistant"
CameraInfo.

Tests pin the projection against cv::fisheye::projectPoints and cover
the fold-back limit, >90 deg directions and a Ceres solve.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
camera_lidar_calibration read the flat camera_info.yaml layout that
insta360-to-images and insta360-test-calib write only for Mei. Any
other distortion_model went to the OpenCV/ROS parser, which expects a
camera_matrix: block, so an equidistant file from insta360-test-calib
failed with "camera_matrix needs 9 values".

calib_core's loadMeiIntrinsics becomes loadCameraInfoYaml
(CameraInfoYaml.cpp) and maps distortion_model onto Intrinsics:
insta360_mei_v2 to Mei, equidistant/fisheye to Fisheye, plumb_bob and
rational_polynomial to Pinhole in OpenCV's coefficient order. An unknown
distortion_model now fails instead of loading as another model, and an
equidistant file needs exactly 4 coefficients. Files the Mei loader
accepted still load as Mei, including xi with no distortion_model.

The app sends any YAML with a top-level fx: key to it and keeps
parseOpenCVYaml for ROS camera_matrix: files.

Also drops a stale note on LoadTimestampFromSideCar, which has parsed
JSON since 12fade8.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Michał Pełka <[email protected]>
@michalpelka
michalpelka marked this pull request as ready for review September 25, 2026 14:04
michalpelka and others added 3 commits September 25, 2026 16:53
Signed-off-by: Michał Pełka <[email protected]>
- camera_lidar_calibration: loadCalibration read serial/model/firmware/
  frame_id from the top level, but saveCalibration writes them under
  "camera", so a save/load round trip wiped the camera identity.
- core: build core_no_gui when BUILD_WITH_CLI_TOOLS is on; session_to_mcap
  links it, and with testing and pybind off the target did not exist.
- deploy_mandeye.bat: drop the retired manual coloring tools and single
  session viewer from the expected list, add session_to_mcap.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Michał Pełka <[email protected]>
This reverts commit b516813.

Also restores mandeye_single_session_viewer.exe in deploy_mandeye.bat's
expected list, which 8a82965 dropped after the app was retired.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Michał Pełka <[email protected]>
@JanuszBedkowski
JanuszBedkowski merged commit a849bd0 into main Sep 25, 2026
7 checks passed
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.

3 participants