CV Save Camera Params (JSON)
Calibrate once, then never again
- camera_matrix
- dist_coeffs
Camera calibration is a chore you should only do once per lens-and-resolution combination. You shoot chessboards from a dozen angles, run CV Calibrate Camera (Chessboard), get a 3x3 intrinsic matrix and a fistful of distortion coefficients - and then, in every future session, you need them again. This node writes them to disk so the next graph starts from CV Load Camera Params (JSON) instead.
What it writes
camera_matrix is the 3x3 K and dist_coeffs is the distortion vector (k1, k2, p1, p2, k3…) - both straight off a calibration node or CV Camera Matrix. filename defaults to camera_params and gets .json appended if you forget it. It's a plain, human-readable JSON file in ComfyUI's output folder, with arrays as nested lists so the shapes survive a round trip. Open it in a text editor and read your own intrinsics; there's no binary blob and no proprietary format to decode later.
Three optional metadata fields - image_width, image_height, rms_error - are recorded alongside the numbers when you supply them, and omitted when you leave them at 0. The rms_error from the calibration is genuinely worth saving: it's the reprojection error in pixels, and it's the only number that tells future-you whether that calibration was any good.
One thing to know about the filename
Path components are stripped. The file always lands directly in the output folder, no subdirectories. That's deliberate and it's the right call: the node's UI stays honest about where things go, and nobody can accidentally write outside the output tree. It also means you can't organise calibrations into camera_params/fisheye/ - use descriptive filenames instead.
This is an output node with no outputs at all: nothing to wire downstream, and it still runs. The name of the file it wrote is displayed on the node, so you can see what happened without hunting through the output folder. That "is_output_node, runs even with nothing wired to it" pattern is how the pack handles every writer - the save node is the terminal node in the graph, exactly like Save Image.
Why this matters more than it sounds
Calibration inputs are annoyingly session-dependent. Intrinsics are tied to a specific sensor, lens and resolution - change any of them and the matrix is wrong - while distortion is a property of the lens, so a 5% different coefficient set can mean tens of pixels of error at the frame edge for a wide lens. In a graph where geometry flows into triangulation, pose recovery, stereo rectification or a reprojection to 3D, a stale K doesn't fail loudly. It produces a plausible cloud that's slightly, consistently wrong.
So the discipline this node enables is real: calibrate the lens, save the params, and make every downstream graph load them rather than re-derive them. Combine it with CV Build Info-style provenance habits and you have a rig description you can version, share, and diff.
Install
Ships in ComfyUI CV (bmad4ever/comfyui_cv), a GPL-3.0 fork of opencv-comfyui:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
# restart ComfyUI
Manager users: search the pack title. Python ≥ 3.12 and a ComfyUI on the V3 node API. This node is pure type-bridge work - the pack tags it TB - so no contrib-specific OpenCV calls are involved. The nodes on either side of it are the ones that need the contrib wheel.
Gotchas
Saving and loading is not the same as applying. CV Load Camera Params (JSON) gives you K and the coefficients back; you still have to wire them into whatever undistorts, rectifies or reprojects. The pack's workflows/50_camera_calibration_roundtrip.json is the whole loop in one graph - six synthetic chessboard views with baked-in distortion through the calibrator, out to JSON here, back in on the other branch, and into undistortion - and comparing the two branches is the fastest way to confirm your saved file is actually being used.
Output folder, not input folder. Files land in ComfyUI's output directory, which is a different tree from input. Loading works because the loader node knows where to look, but if you're scripting around ComfyUI, don't go hunting in input.
A perfect rms_error should make you suspicious. It's cheap to overfit calibration to too few views or a badly-printed pattern; a handful of views from very similar angles can produce low error and terrible geometry. The pack is upfront that its example workflows are demonstrations rather than production recipes, and calibration is the place where that caveat bites hardest.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| camera_matrix | NPARRAY | 3x3 intrinsic matrix K (e.g. from Calibrate Camera or CV Camera Matrix). | |
| dist_coeffs | NPARRAY | Distortion coefficients (k1, k2, p1, p2, k3...). | |
| filename | STRING | camera_params | Output file name; '.json' is appended if missing. Path components are stripped - the file always lands directly in the output folder. |
| image_widthopt | INT | 00–100000 | Calibration image width in px, recorded as metadata. 0 = omit. |
| image_heightopt | INT | 00–100000 | Calibration image height in px, recorded as metadata. 0 = omit. |
| rms_erroropt | FLOAT | 0.000–1000000000 | Reprojection RMS (px) from Calibrate Camera, recorded as metadata. 0 = omit. |
Outputs (0)
No outputs