{"id":50194081,"url":"https://github.com/kmatzen/chromacal","last_synced_at":"2026-05-25T16:06:32.853Z","repository":{"id":350775329,"uuid":"1208214124","full_name":"kmatzen/chromacal","owner":"kmatzen","description":"ColorChecker camera calibration: detect, solve, apply. Log-polynomial tone curve + CCM via Ceres, OCIO 3D LUT export. C++ and Python.","archived":false,"fork":false,"pushed_at":"2026-05-22T06:42:37.000Z","size":4171,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T07:04:53.678Z","etag":null,"topics":["camera-calibration","ceres-solver","color-calibration","color-correction","color-science","colorchecker","cpp","opencolorio","opencv","python"],"latest_commit_sha":null,"homepage":null,"language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kmatzen.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-12T01:13:44.000Z","updated_at":"2026-05-21T20:23:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kmatzen/chromacal","commit_stats":null,"previous_names":["kmatzen/chromacal"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/kmatzen/chromacal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kmatzen%2Fchromacal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kmatzen%2Fchromacal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kmatzen%2Fchromacal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kmatzen%2Fchromacal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kmatzen","download_url":"https://codeload.github.com/kmatzen/chromacal/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kmatzen%2Fchromacal/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33455052,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-24T19:21:36.376Z","status":"ssl_error","status_checked_at":"2026-05-24T19:21:10.562Z","response_time":57,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["camera-calibration","ceres-solver","color-calibration","color-correction","color-science","colorchecker","cpp","opencolorio","opencv","python"],"created_at":"2026-05-25T16:06:31.863Z","updated_at":"2026-05-25T16:06:32.844Z","avatar_url":"https://github.com/kmatzen.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# chromacal\n\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\nColorChecker camera calibration with correct color science. Detect the chart, solve for a color profile, apply it — three functions, one library.\n\n## What it does\n\nchromacal fits a **log-polynomial tone curve + 3x3 color correction matrix** to ColorChecker patch measurements using non-linear optimization. Unlike OpenCV's basic `ccm` module, chromacal:\n\n- **Jointly fits tone curve and CCM** — no need to manually linearize first\n- **Weights by measurement uncertainty** — per-patch covariance (Mahalanobis distance) so noisy patches matter less\n- **Down-weights bad patches** — a robust per-patch reliability score (outlier fraction vs. the chi-square expectation) lets the solver gracefully discount patches with specular reflections or occlusions, without discarding them; an optional normality filter is also available for pristine captures\n- **Perceptually weights** — darker and more saturated colors get higher weight (where cameras struggle most)\n- **Generates OCIO 3D LUTs** — apply calibration via OpenColorIO for GPU-accelerated or batch processing\n\n## Example\n\nBefore and after calibration on a GoPro Hero10 frame (ColorChecker visible in scene):\n\n| Before | After |\n|--------|-------|\n| ![Before calibration](docs/before.png) | ![After calibration](docs/after.png) |\n\n**Detected patches:** 24 of 24\n\n**Fitted tone curve coefficients:** `[1.377, 3.479, 0.739, 0.072]`\n\n**Fitted color correction matrix:**\n```\n[[ 1.586  -0.638  -0.214]\n [-0.303   1.826  -0.294]\n [-0.014  -0.449   3.000]]\n```\n\nThe off-diagonal entries show the GoPro sensor has significant blue-channel crosstalk that the CCM corrects. The tone curve coefficients (far from the identity `[0, 1, 0, 0]`) indicate the camera's built-in processing applies a heavy tonal response.\n\n### Patch comparison\n\nDetected patch colors before and after calibration, compared to ground truth:\n\n![Patch comparison](docs/patches.png)\n\nThe \"after\" row closely matches the reference — saturated colors are recovered and the grayscale ramp is neutral.\n\n### Re-detection identity check\n\nDetecting the ColorChecker in the *corrected* image and solving again should produce a near-identity transform, confirming the calibration was applied correctly:\n\n```python\n# Apply calibration\ncorrected = solver.infer(rgb_image)  # linear RGB float64\n\n# detect() requires uint8, so we must quantize. Linear values in 8-bit\n# have poor precision in the darks (causing patch detection failures),\n# so we apply the sRGB transfer function before quantizing.\ncorrected_srgb = linear_to_srgb(corrected)\ncorrected_8bit = (corrected_srgb * 255).astype(np.uint8)\n\n# Re-detect and re-solve on the corrected image\npatches2 = chromacal.detect(cv2.cvtColor(corrected_8bit, cv2.COLOR_RGB2BGR))\nsolver2 = chromacal.Solver()\nsolver2.solve(patches2)\n\nprint(solver2.get_ccm())        # near identity\nprint(solver2.get_luma_params()) # absorbs sRGB transfer function\n```\n\n**Re-detection CCM (24/24 patches):**\n```\n[[ 1.006  -0.011   0.013]\n [-0.003   0.999   0.010]\n [ 0.005  -0.001   0.999]]\n```\n\n**Re-detection tone curve:** `[0.013, 2.316, 0.116, 0.018]`\n\nThe CCM is within 1.3% of the identity matrix — the color correction has already been applied. The tone curve departs from `[0, 1, 0, 0]` because it absorbs the sRGB transfer function applied during the uint8 quantization step.\n\n## Usage\n\n### Python\n\n```python\nimport chromacal\nimport cv2\n\nimage = cv2.imread(\"colorchecker.jpg\")\n\n# 1. Detect — find the chart and extract patch statistics\npatches = chromacal.detect(image)\n# (optional) cull patches up front on pristine captures:\n#   patches = chromacal.filter_normal(patches)\n\n# 2. Solve — fit the color profile (down-weights unreliable patches internally)\nsolver = chromacal.Solver()\nsolver.solve(patches)\nsolver.save(\"calibration.yml\")\n\n# 3. Apply — correct any image from this camera\nlut = chromacal.create_lut(solver)\ncorrected = chromacal.apply_lut(image, lut)  # float32 RGB\n```\n\n### C++\n\n```cpp\n#include \u003cchromacal/chromacal.h\u003e\n\ncv::Mat image = cv::imread(\"colorchecker.jpg\");\n\n// Detect\nauto patches = chromacal::detect(image);\n// (optional) patches = chromacal::filter_normal(patches);\n\n// Solve — down-weights unreliable patches internally\nchromacal::Solver solver;\nsolver.solve(patches);\nsolver.save(\"calibration.yml\");\n\n// Apply via OCIO 3D LUT\nauto lut = chromacal::create_lut(solver);\ncv::Mat corrected = chromacal::apply_lut(rgb_image, lut);\n```\n\n### As a CMake dependency\n\n```cmake\ninclude(FetchContent)\nFetchContent_Declare(\n    chromacal\n    GIT_REPOSITORY https://github.com/kmatzen/chromacal.git\n    GIT_TAG main\n)\nFetchContent_MakeAvailable(chromacal)\n\ntarget_link_libraries(your_target PRIVATE chromacal::chromacal)\n```\n\n## The algorithm\n\n1. **Detection**: OpenCV's MCC24 detector locates the ColorChecker. Per-patch pixel statistics (mean, covariance) are computed after rejecting saturated pixels.\n\n2. **Reliability scoring**: Each patch gets a robust reliability weight from the fraction of its pixels that are multivariate outliers (Mahalanobis distance beyond the chi-square expectation). This flags genuine contamination (specular highlights, occlusions) while tolerating mild gradients and 8-bit quantization, and — unlike a normality hypothesis test — it is stable with respect to the number of pixels in a patch. (An optional `filter_normal` step using Shapiro-Francia / Mardia / Henze-Zirkler tests is available for pristine, high-bit-depth captures.)\n\n3. **Optimization**: Ceres Solver minimizes the perceptually-weighted Mahalanobis distance between predicted and reference colors (CIE Lab D50). The model has 13 parameters: 4 log-polynomial tone curve coefficients + 9 CCM entries. Huber loss reduces outlier influence, and each patch's residual is additionally scaled by its reliability weight. Neutral patches (18-23) get an auxiliary white balance constraint.\n\n4. **Application**: The solver is baked into a 129^3 OCIO 3D LUT for fast per-pixel application. Input: gamma-encoded RGB. Output: linear RGB at reference exposure.\n\n## Requirements\n\n- C++17 compiler\n- OpenCV 4.x (with `mcc` contrib module)\n- [Ceres Solver](http://ceres-solver.org/)\n- [OpenColorIO](https://opencolorio.org/)\n- Eigen3\n- pybind11 (optional, for Python bindings)\n\n## Building\n\n```bash\n# C++ only\ncmake -B build -DCMAKE_BUILD_TYPE=Release\ncmake --build build\n\n# With Python bindings\ncmake -B build -DCHROMACAL_BUILD_PYTHON=ON\ncmake --build build\n```\n\n## API\n\n### `detect(image, exposure=1.0)`\n\nDetect a ColorChecker in a BGR image and return patch statistics. Each returned\npatch carries a `reliability` weight in (0, 1]; `Solver.solve` uses it to\ndown-weight contaminated patches automatically, so no explicit filtering step\nis required.\n\n### `filter_normal(patches)` *(optional)*\n\nRemove patches that fail multivariate normality tests (Shapiro-Francia,\nMardia, Henze-Zirkler). This is an opt-in, aggressive cull intended for\npristine, high-bit-depth captures with flat, evenly-lit patches. Because\ngoodness-of-fit tests grow more sensitive with sample size, patches with many\npixels — or any 8-bit/compressed/downscaled image (such as the web-sized\n`docs/before.png` example) — are frequently rejected as non-Gaussian even when\ntheir mean color is perfectly usable. Prefer the built-in `reliability`\nweighting (above) for most workflows.\n\n### `Solver`\n\n| Method | Description |\n|--------|-------------|\n| `solve(patches)` | Fit the tone curve + CCM to patch data |\n| `infer(image)` | Apply calibration to an image (CV_64FC3 RGB) |\n| `get_ccm()` | Get the 3x3 color correction matrix |\n| `get_luma_params()` | Get the 4 log-polynomial coefficients |\n| `save(path)` / `load(path)` | Serialize to/from YAML |\n\n### `create_lut(solver, lut_size=129)`\n\nBake the calibration into an OCIO 3D LUT for fast application.\n\n### `apply_lut(image, lut)`\n\nApply the 3D LUT to an image. Returns float32 RGB.\n\n### `write_cube(solver, path, lut_size=33, title=\"chromacal\")`\n\nWrite the calibration as an Iridas/Resolve `.cube` 3D LUT file, independent of\nOpenColorIO. Drop the result into a DaVinci Resolve or Premiere/Lumetri LUT slot.\nInput domain is gamma-encoded RGB in `[0, 1]`; output is linear RGB at the\nreference exposure (written unclamped, so values may exceed `[0, 1]`).\n\n## Premiere Pro / After Effects plugin\n\nA native **video effect** ([`plugin/`](plugin/README.md)) brings Resolve-style\nColorChecker calibration to Premiere: an After Effects-style effect with an\n*Analyze* button that detects the chart and applies the tone curve + CCM live in\nthe render pipeline. It can also **Export a `.cube`** for Lumetri's Input LUT\n(the effect's exact transform; Premiere applies an Input LUT directly, so it\nmatches the effect — measured ~0.5% mean over the chart).\n\nThe SDK-free engine and a headless CLI build with no Adobe SDK at all:\n\n```bash\ncmake -B build -DCHROMACAL_BUILD_PPRO=ON\ncmake --build build --target chromacal_solve\n./build/plugin/chromacal_solve frame.png out.cube\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkmatzen%2Fchromacal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkmatzen%2Fchromacal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkmatzen%2Fchromacal/lists"}