mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-03 06:59:53 -06:00
Use crw craftlib (#3503)
* CRW-LIB: decode Canon CRW through the shared craft-crw crate LightCraft decodes Canon CRW (the CIFF raws of the EOS D30, D60, 10D, 300D and the PowerShot G1/G6) through craft-libs' `craft-crw`, the clean-room decoder derived and verified on the `clean-room-crw-format-decode` branch and moved there with its frozen tables and derivation record. `crates/raw/src/vendor/crw.rs` is now the adapter: it maps the crate's header and samples onto `RawImage` (CFA, levels, active area, orientation, as-shot white balance, the make/model/time records a CRW keeps instead of Exif) and converts the capture time through chrono. The dependency is a local path (`../craft-libs/crates/crw`) until the craft-libs branch merges; then it becomes a git revision. Everything LightCraft-side from that branch comes along unchanged: `probe_bytes` fills metadata gaps from the decoder's records so CRW imports name their camera; the EOS D60 bundled camera profile and its chart-measured dual-illuminant matrices (`chart_table.rs`, `spectral::Camera` carries its illuminant pair) give the D60 a Kelvin white-balance scale; CRW joins the JPEG-fitted starting look, the catalog's raw formats and `lightcraft-cli calibrate`; `RENDER_CACHE_VERSION` 71; the CC0 CRW corpus entries for `cargo xtask corpus`; coverage, parity, gaps, raw-parity, target-app-parity and ROADMAP updates. The G5 stays preview-only (the crate refuses it with the reason) and the robustness test keeps its CRW sample. What does not come along: the decoder, its tables and `crw_oracle.rs` (now craft-crw's digest-pinned sample test) and the derivation record, which lives in craft-libs `crates/crw/clean-room/`. `clean-room/crw/` here keeps only the EOS D60 chart-calibration document and its capture list, with a README pointing at the record. Gates: fmt, clippy -D warnings (raw all targets; engine, catalog, cli), lightcraft-raw tests (475 + 134, the corpus files decoding through the adapter), catalog tests, engine lib tests (1,300 pass; the two failures, `tests_import::recursive_discovery_…` and `originals::tests::write_targets_pin_absent_paths_…`, are in files unchanged from main), `cargo xtask parity`, `assets`, `layers`, `coverage.py --check`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * no message * no message * Update documentation after moving code to craft-libs * Review coment fixes * CRW: pin craft-crw f3b62ec, a truncated entropy stream is Corrupt craft-libs #472 makes craft_crw::decode refuse a stream that ends more than Limits::max_stream_end_fill (64 samples, one block) before the frame does, instead of filling the rest with predictor values and returning success. The adapter already maps craft_crw::Error::Corrupt to RawError::Corrupt; a regression test proves a truncated CRW now fails at this boundary (it decoded as a flat frame on the old pin) while its header still reads. Only craft-crw moves; craft-ui/docpath/update/window stay on c5a1cbf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Code review comment fix * Add some tests --------- Co-authored-by: Robert Woldberg <314159+rwoldberg@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Robert Woldberg
parent
32d569ad5d
commit
06c1b9ae07
Generated
+10
@@ -1181,6 +1181,14 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-crw"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=f3b62ec61f45534392376492fc5ad33047375205#f3b62ec61f45534392376492fc5ad33047375205"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-docpath"
|
||||
version = "0.1.0"
|
||||
@@ -3902,6 +3910,8 @@ dependencies = [
|
||||
name = "lightcraft-raw"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"craft-crw",
|
||||
"dhat",
|
||||
"jpeg-encoder",
|
||||
"jxl-oxide",
|
||||
|
||||
@@ -12,6 +12,8 @@ repository = "https://github.com/storytold/lightcraft"
|
||||
[workspace.dependencies]
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["serde"] }
|
||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7" }
|
||||
# Canon CRW decoder shared with the other Craft apps
|
||||
craft-crw = { git = "https://github.com/storytold/craft-libs", rev = "f3b62ec61f45534392376492fc5ad33047375205" }
|
||||
# craft-update: app-specific pretend versions apply to startup and manual checks.
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["egui"] }
|
||||
craft-window = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["egui"] }
|
||||
|
||||
@@ -349,7 +349,7 @@ LightCraft is young and moving fast: **alpha**. **Where we honestly stand** (det
|
||||
casual use.** It's great for JPEG/DNG and most Sony / Panasonic /
|
||||
Fujifilm / Nikon / older-Canon raws on one machine; see [docs/raw-parity.md](docs/raw-parity.md) for your camera.
|
||||
- **The biggest gaps:**
|
||||
- **camera colour calibration:** Sony, Nikon, Panasonic, Fujifilm and Canon CR3 raws have guarded estimates from their camera JPEGs, with built-in ILCA-99M2, ILCE-7CR, ILCE-7M4, ILCE-7RM5, X-H2S, X-T4, Nikon Z 6II, Z 8 and D5300 profiles; measured calibration is missing, and other raws or rejected fits retain a neutral matrix;
|
||||
- **camera colour calibration:** Sony, Nikon, Panasonic, Fujifilm and Canon CR3 and CRW raws have guarded estimates from their camera JPEGs, with built-in Canon EOS D60, ILCA-99M2, ILCE-7CR, ILCE-7M4, ILCE-7RM5, X-H2S, X-T4, Nikon Z 6II, Z 8 and D5300 profiles; measured calibration covers one model so far (chart-measured matrices for the Canon EOS D60), and other raws or rejected fits retain a neutral matrix;
|
||||
- **compressed Olympus raws and unsupported CR3 variants:** these use embedded JPEG previews when present. Fujifilm lossless/lossy compressed RAF now decodes sensor data; [verification and existing-library reload instructions](docs/raf-compression.md);
|
||||
- **AI quality and model setup:** Subject and Sky selection are classical heuristics; Object/Describe use an
|
||||
optional SAM 3 model. AI Denoise supports Bayer RAW with separately installed weights, while X-Trans and
|
||||
|
||||
+9
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~14 points and ~340–600 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (gap 9: CI runs the tests on Linux x64 since #2667; macOS, Windows and the GPU equivalence tests still don't; the lens profile database priority links its tracking issue #329; Print: Japanese photo and postcard paper sizes — L判, 2L判, KG判, はがき, 往復はがき, 六切, 四切, 名刺; Minolta MRW decoded, 6 bodies verified; RAW: 83% of Adobe's models decode, 38% verified after the camera-coverage sweep #1327; German catalog 91% → 97%: interface strings and What's New lines translated) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (Canon CRW decoded clean-room, 6 bodies verified, 557 models verified; gap 9: CI runs the tests on Linux x64 since #2667; macOS, Windows and the GPU equivalence tests still don't; the lens profile database priority links its tracking issue #329; Print: Japanese photo and postcard paper sizes — L判, 2L判, KG判, はがき, 往復はがき, 六切, 四切, 名刺; Minolta MRW decoded, 6 bodies verified; RAW: 85% of Adobe's models decode, 39% verified after the camera-coverage sweep #1327; German catalog 91% → 97%: interface strings and What's New lines translated) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
|
||||
One-page summary of how close LightCraft is to Adobe Lightroom. Agents: read this, then pick work
|
||||
from [docs/gaps.md](docs/gaps.md). The full assessment, method and evidence are in
|
||||
@@ -45,7 +45,7 @@ Hours are calibrated on this repo's PR history; see
|
||||
| Dimension | Ready | Breadth | Remaining | Doc |
|
||||
|---|---:|---:|---:|---|
|
||||
| Features | ~61% | ~77% (checklist 81.1%) | 600–1,100 h | [target-app-parity](docs/target-app-parity.md#features) |
|
||||
| ↳ RAW cameras, colour, lens profiles | ~60% | 83% of 1,446 Adobe models decode, 38% verified | 255–480 h | [raw-parity](docs/raw-parity.md) |
|
||||
| ↳ RAW cameras, colour, lens profiles | ~60% | 85% of 1,446 Adobe models decode, 39% verified | 255–480 h | [raw-parity](docs/raw-parity.md) |
|
||||
| UI/UX fidelity | ~70% | ~85% | 32–54 h | [ui-parity](docs/ui-parity.md) |
|
||||
| File formats (non-raw) | ~70% | ~85% | 30–55 h | [file-format-parity](docs/file-format-parity.md) |
|
||||
| Hardware | ~40% | ~50% | 40–80 h | [hardware-parity](docs/hardware-parity.md) |
|
||||
@@ -119,6 +119,12 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
|
||||
六切 (203 × 254), 四切 (254 × 305) and 名刺 (55 × 91) — at their exact metric sizes. The Print dialog
|
||||
and `print.set` take their customary names, and names are matched case-, space- and
|
||||
punctuation-insensitively (`L判`, `l`, `L-size` are one paper).
|
||||
- 2026-10-13: Canon CRW (the 2000–2004 CIFF bodies) decodes from sensor data: compression
|
||||
derived clean-room against a black-box reference and frozen (`craft-libs crates/crw/clean-room/02-crw-format.md`);
|
||||
EOS D30/D60/10D/300D and PowerShot G1/G6 verified pixel-exact on 28 of 29 corpus files (18 of
|
||||
them the maintainer's D60 shots); as-shot white balance, black level, rotation and capture time
|
||||
from the CIFF records; the PowerShot G5's anomalous coding stays preview-only. With the
|
||||
camera-coverage sweep and MRW merged, 557 models verified; Canon bucket 95% → 98%.
|
||||
- 2026-10-14: Camera-coverage sweep verifies 30 more models on 40 CC0 files, with file and sensor
|
||||
hashes pinned (`docs/raw/coverage.py`: 515 → 545 verified). Eleven candidates excluded after rechecking
|
||||
identity, provenance and camera-image agreement. Phone and drone DNG tone differences remain tracked in #1277.
|
||||
@@ -194,6 +200,7 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
|
||||
| 2026-10-24 | patch | Alpha rationale: tests run in CI on Linux x64 (#2667) and FreeBSD nightly, not on macOS or Windows; ranked item 8 now reads "Tests in CI on macOS and Windows, and the GPU tests in CI"; percentages and hours unchanged |
|
||||
| 2026-10-22 | minor | Print: Japanese photo and postcard paper sizes (L判, 2L判, KG判, はがき, 往復はがき, 六切, 四切, 名刺) in the page setup, Print dialog and PDF output; headline numbers unchanged |
|
||||
| 2026-10-19 | minor | Indonesian interface (`id`): every plain and formats message, plus 69 command labels, locale, menu command and tests; What's New left in English; Localization ~55% → ~64% (mean of the 12 key languages), 8 of 12 present |
|
||||
| 2026-10-13 | minor | Canon CRW decoded (clean-room): 6 bodies verified pixel-exact, G5 preview; 551 models verified after merging the coverage sweep |
|
||||
| 2026-10-12 | minor | Italian interface (`it`, #1192): 2,201 of 2,398 messages, locale, menu command and tests; headline numbers unchanged |
|
||||
| 2026-10-13 | minor | Sony ILCA-99M2 camera colour profile in the log; numbers unchanged |
|
||||
| 2026-10-14 | minor | Localization ~52% → ~55% (mean of the 12 key languages): de, es, fr, ja and uk catalogs filled |
|
||||
|
||||
@@ -106,7 +106,7 @@ USAGE:
|
||||
lightcraft-cli controls [--json] list every develop control id with its range
|
||||
lightcraft-cli calibrate [--max N] [--out DIR] FOLDERS/FILES…
|
||||
Fit a colour profile per camera model from raw files and their embedded camera JPEGs
|
||||
(Sony ARW, Nikon NEF, Fujifilm RAF): up to N files spread over the folders (default 300; 0 = all), pooled per
|
||||
(Sony ARW, Nikon NEF, Fujifilm RAF, Canon CRW): up to N files spread over the folders (default 300; 0 = all), pooled per
|
||||
model, written as <model>.json to DIR (default: the profiles folder LightCraft reads,
|
||||
<config>/camera-profiles, or $LIGHTCRAFT_CAMERA_PROFILES). Raws of a profiled model then
|
||||
take their colour from the profile and only their tone from their own JPEG.
|
||||
@@ -197,8 +197,9 @@ fn main() -> ExitCode {
|
||||
|
||||
/// Raw files below `path` (or `path` itself), skipping hidden and NAS metadata folders.
|
||||
fn raw_files(path: &Path, out: &mut Vec<std::path::PathBuf>, depth: usize) {
|
||||
let is_raw =
|
||||
|p: &Path| p.extension().and_then(|e| e.to_str()).is_some_and(|e| ["arw", "nef", "nrw", "raf"].iter().any(|x| e.eq_ignore_ascii_case(x)));
|
||||
let is_raw = |p: &Path| {
|
||||
p.extension().and_then(|e| e.to_str()).is_some_and(|e| ["arw", "nef", "nrw", "raf", "crw"].iter().any(|x| e.eq_ignore_ascii_case(x)))
|
||||
};
|
||||
if path.is_file() {
|
||||
if is_raw(path) {
|
||||
out.push(path.to_path_buf());
|
||||
|
||||
@@ -19,11 +19,13 @@ covers a whole directory), or if a licence file referenced in the Licence column
|
||||
| `assets/camera-profiles/ILCA-99M2.json` | Sony ILCA-99M2 camera colour profile (matrix + hue/saturation/value table), built into LightCraft | LightCraft contributors; source photographs supplied by @PiMinusMax | original work: fitted by `lightcraft-cli calibrate` to 144 usable ILCA-99M2 ARWs (of 150, seven shooting days, Standard creative style) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-11 | none |
|
||||
| `assets/camera-profiles/ILCE-7CR.json` | Sony ILCE-7CR camera colour profile (matrix + hue/saturation/value table), built into LightCraft | LightCraft contributors; source photographs supplied by @AlexChambers | original work: fitted by `lightcraft-cli calibrate` to 125 usable ILCE-7CR ARWs (of 131, September 2025 to September 2026, Standard creative style) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-09 | none |
|
||||
| `assets/camera-profiles/X-H2S.json`, `assets/camera-profiles/X-T4.json` | Fujifilm camera colour profiles (matrix + hue/saturation/value table), built into LightCraft | LightCraft contributors; source photographs supplied by Sebastian Pfluegelmeier | original work: fitted by `lightcraft-cli calibrate` to 201 X-H2S and 545 X-T4 RAFs and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-08 | none |
|
||||
| `assets/camera-profiles/Canon_EOS_D60.json` | Canon EOS D60 camera colour profile (matrix + hue/saturation/value table), built into LightCraft | LightCraft contributors; source photographs supplied by Robert Woldberg | original work: fitted by `lightcraft-cli calibrate` to 1,424 usable EOS D60 CRWs (of 1,520, 2005 to 2014, Auto white balance) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-13 | none |
|
||||
| `assets/camera-profiles/NIKON_Z_6_2.json` | Nikon Z 6II camera colour profile (matrix + hue/saturation/value table), built into LightCraft | Michael Anderson (@endthestart) | original work: fitted by `lightcraft-cli calibrate` to 296 usable Z 6II NEFs of my own (of 299, 2024 to 2026, Standard and Auto picture controls; NIKKOR Z 20mm f/1.8 S, Z 24-70mm f/4 S and AF-S 70-200mm f/2.8G on the FTZ) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-13 | none |
|
||||
| `assets/camera-profiles/NIKON_Z_8.json` | Nikon Z 8 camera colour profile (matrix + hue/saturation/value table), built into LightCraft | Michael Anderson (@endthestart) | original work: fitted by `lightcraft-cli calibrate` to 261 usable Z 8 NEFs of my own (of 278, 2023 and 2026, nearly all Standard picture control, lossless 14-bit; NIKKOR Z 24-120mm f/4 S and AF-S 70-200mm f/2.8G on the FTZ) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-13 | none |
|
||||
| `assets/camera-profiles/NIKON_D5300.json` | Nikon D5300 camera colour profile (matrix + hue/saturation/value table), built into LightCraft | Michael Anderson (@endthestart) | original work: fitted by `lightcraft-cli calibrate` to 264 usable D5300 NEFs of my own (of 273, 2014 to 2017, mostly Standard picture control, lossy 12-bit, mostly the AF-S DX 35mm f/1.8G) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-13 | none |
|
||||
| `assets/camera-profiles/ILCE-7RM5.json` | Sony ILCE-7RM5 camera colour profile (matrix + hue/saturation/value table), built into LightCraft | Yule (Brian) Wang (@peaperfish) | original work: fitted by `lightcraft-cli calibrate` to 220 usable ILCE-7RM5 ARWs of my own (of 241, 2025 to 2026, Standard creative style; Sigma 24-70mm F2.8 DG DN II Art and Sony FE 200-600mm F5.6-6.3 G OSS) and their in-camera JPEGs; aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-26 | none |
|
||||
| `crates/raw/src/spectral_table.rs` | Camera colour matrices (DNG ColorMatrix1/2 and ForwardMatrix1/2 for Standard Illuminant A and D65) for 52 camera models, and the model alias list | Derived by LightCraft contributors from rawtoaces-data (Academy Software Foundation): camera spectral sensitivities measured by Weta Digital (44 cameras), scitech (7) and Jonathon Irons at the National Physical Laboratory (Nikon D5100); training reflectances; CIE 1931 colour-matching functions (CIE, CC-BY-4.0, as shipped in rawtoaces-data) | https://github.com/AcademySoftwareFoundation/rawtoaces-data, tag v1.1.0, commit 5cfa53707b053594b2d0d48e8019fb56e6582d6c | Apache-2.0 (`LICENSE-APACHE`); the colour-matching functions CC-BY-4.0 | 2026-10-08 | generated by `tools/spectral_camera_table.py`: a 3×3 matrix per illuminant fitted to the spectral data (no measurement copied verbatim); aliases copied from `data/aliases.json`; no Adobe or third-party camera matrix |
|
||||
| `crates/raw/src/chart_table.rs` | Chart-measured camera colour matrices (DNG ColorMatrix1/2 and ForwardMatrix1/2 for Standard Illuminant A and D50) for the Canon EOS D60 | LightCraft contributors; ColorChecker photographs taken by Robert Woldberg on the maintainer's EOS D60 | original work: fitted to our own CRWs of a ColorChecker Classic (post-November-2014 formulation) under a ~2,900 K tungsten bulb (11 frames) and direct sun (~4,850 K, 20 frames); reference colours: X-Rite's published L*a*b* (D50) for the chart, as packaged by [colour-science](https://github.com/colour-science/colour) (BSD-3-Clause), used for fitting only and not shipped; method in `clean-room/crw/10-chart-calibration.md` | MIT OR Apache-2.0 | 2026-10-25 | kept by hand: a 3×3 matrix per illuminant fitted on chromaticity with neutral held at D50; no Adobe or third-party camera matrix, no number from a GPL raw decoder |
|
||||
| `assets/camera-tone/sony-dro-off.json` | Sony global tone curve with the Dynamic Range Optimizer off (32 knots), built into LightCraft for ARWs shot with DRO on | LightCraft contributors; source photographs by raw.pixls.us contributors | original work: tone curve quantile-matched over the pooled pixel pairs of 12 CC0 raw.pixls.us ARWs (DRO off, Standard creative style; ILCE-3500, -5000, -6600, -7CR, -7M3 ×3, -7S, -9M2, ILCA-77M2, SLT-A57, NEX-F3) and their embedded camera JPEGs; the file lists each source URL and SHA-256 and how the curve was derived (see also `docs/camera-preview-colour.md`); aggregate tone statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 (sources CC0) | 2026-10-09 | none |
|
||||
| `crates/scenes/` (generated images) | Procedural demo photographs | LightCraft contributors | original work (generated at runtime, no source imagery) | MIT OR Apache-2.0 | 2026-09-30 | n/a |
|
||||
| `crates/ui-egui/src/icons.rs` | UI icons drawn as vector paths in code | LightCraft contributors | original work | MIT OR Apache-2.0 | 2026-09-30 | n/a |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,97 @@
|
||||
# EOS D60 colour matrices from a ColorChecker (2026-10-25)
|
||||
|
||||
The Canon EOS D60 has no published spectral sensitivities, so it is not in `spectral_table.rs`.
|
||||
These are its DNG-style matrices (`ColorMatrix1/2`, `ForwardMatrix1/2`), fitted to our own
|
||||
photographs of a ColorChecker Classic under two lights. They are in
|
||||
`crates/raw/src/chart_table.rs`. Measurements come from the maintainer's own captures (names,
|
||||
sizes and SHA-256 in `logs/chart-captures.tsv`; no image content recorded) and the chart's
|
||||
published reference colours (craft-libs `crates/crw/clean-room/sources.md`). Scripts and measurements are local-only, in
|
||||
`plan/crw/chart/`.
|
||||
|
||||
## Captures
|
||||
|
||||
| Set | Frames used | Light | Notes |
|
||||
|---|---:|---|---|
|
||||
| `SunlightColorCard/` | 20 of 22 | Direct afternoon sun, ISO 100 | Calibrite ColorChecker Classic (50th-anniversary edition, post-2014 formulation) leaning on a wall, filling most of the frame. Auto and Daylight white balance, a shutter bracket. Two frames dropped: a white-patch sample ≥ 3900 (clip ≈ 4000 at ISO 100, craft-libs `crates/crw/clean-room/04-color.md` §2). |
|
||||
| `TungstonColorChart/` 2696–2706 | 11 | One household tungsten bulb, ISO 100 | Chart flat on a wall, a shutter bracket. Brightest white patch 2657 of ~4000. |
|
||||
| `TungstonColorChart/` 2688–2691 | not used | Same bulb, 8 minutes earlier | Steady within the set, but its neutral reads ~3,040 K against ~2,900 K for the later set: the light changed (window light or bulb warm-up). The later, warmer and better-exposed set is used. |
|
||||
|
||||
All 37 frames decode sample-exact against the black-box reference (`craft-crw`'s digest-pinned sample test, formerly `crw_oracle.rs` here).
|
||||
|
||||
An earlier set on a ColorChecker-layout clone (Pixel Perfect "24 Standard Color Calibration
|
||||
Chart", 2026-10-15) was not used. The maker publishes no reference values, and its grey patches
|
||||
are not neutral: against an 18 % grey board they read R/G 0.88–0.94 in shade and B/G up to
|
||||
1.29 under warm LED light, the darker patches drifting most. That is the sign of greys printed
|
||||
from coloured inks.
|
||||
|
||||
## Measurement
|
||||
|
||||
- Patches are located on the embedded JPEG (four corner patch centres, bilinear grid), tracked
|
||||
per frame by phase correlation, and checked visually on drawn boxes. The tungsten-set chart
|
||||
was upside down in the first four frames; patch order is mapped accordingly.
|
||||
- Each patch is measured in a central box of about 40 % of its width in the linear raw (black
|
||||
from the far masked border per CFA site; R, mean of both G, B per 2×2 site). Uniformity
|
||||
across the box (3×3 sub-blocks): ≤ 4 % in sun, ≤ 7 % under the bulb (light falloff across
|
||||
the closer chart).
|
||||
- Frames are normalised by the four middle neutrals (patches 20–23), averaged, and white
|
||||
balanced on those neutrals. White balance (R, B multipliers, G = 1): sun 2.362, 1.298; bulb
|
||||
1.353, 2.213. The neutrals agree on it to ±2 %.
|
||||
|
||||
## Fit
|
||||
|
||||
- The reference is X-Rite's published L\*a\*b\* (D50) for charts made after November 2014.
|
||||
- The model is a 3×3 matrix from white-balanced camera RGB to XYZ (D50), with neutral (1, 1, 1)
|
||||
held exactly at the engine's D50 white (`lightcraft_color::D50`): the third column is D50
|
||||
minus the first two. That is a DNG `ForwardMatrix`.
|
||||
- **Chromaticity only.** Each prediction is scaled to the patch's reference luminance before
|
||||
the CIEDE2000 residual. In the sun the chart warped as it heated: the grey row's brightness
|
||||
slope swung from +4 % to −7 % across the 70 s of shooting, in time order, and the per-patch
|
||||
brightness needed was not smooth across the chart. A fit including lightness would have
|
||||
absorbed that into the matrix. Chromaticity still determines the whole 3×3 up to scale,
|
||||
which the white constraint fixes. The cost is that luminance of saturated blues and purples
|
||||
rests on the model, not the measurement.
|
||||
- `ColorMatrix` per light = diag(1 / multipliers) · ForwardMatrix⁻¹ · Bradford(light → D50),
|
||||
scaled so the light's white maps to camera values with maximum 1. By construction it maps
|
||||
the filed illuminant's white to the measured camera neutral.
|
||||
|
||||
| Light | CIEDE2000 on the 24 patches | Leaving each patch out | Worst |
|
||||
|---|---|---|---|
|
||||
| Sun (filed as D50) | mean 1.70, median 1.46 | mean 1.97 | white 4.5 |
|
||||
| Bulb (filed as A) | mean 2.42, median 1.79 | mean 2.74 | bluish green 6.7 |
|
||||
|
||||
- One matrix does not serve both lights: the sun's matrix on the bulb's chart gives mean 3.6
|
||||
(cyan 9.0), the bulb's on the sun's 3.4 (red 7.7). Hence the dual-illuminant entry.
|
||||
- The neutral row reads a\* within ±0.4 for the middle greys; white and the darkest patches
|
||||
are off by up to 2 (white patch sheen, flare).
|
||||
|
||||
## Which illuminants
|
||||
|
||||
The two lights' colour temperatures come from the camera's own white-balance presets
|
||||
(ColorBalance slots, craft-libs `crates/crw/clean-room/04-color.md` §3) at the temperatures Canon's manuals give them (Daylight
|
||||
5200 K, Tungsten 3200 K, Fluorescent 4000 K), interpolated in mired on ln(R/B): sun ~4,850 K,
|
||||
bulb ~2,900 K. They are filed under the nearest DNG illuminants the engine knows, **D50
|
||||
(5003 K) and Standard Illuminant A (2856 K)**, so `Camera` now carries its own illuminant
|
||||
pair (spectral rows: A and D65).
|
||||
|
||||
## Checks in the app
|
||||
|
||||
- `spectral::tests::the_chart_measured_eos_d60_reads_its_presets_in_kelvin`: the camera's
|
||||
presets, which the fit never saw, read through the matrices as Tungsten 3,264 K, Daylight
|
||||
5,603 K, Cloudy 6,613 K, Flash 7,085 K, near the locus. Canon's nominal values are 3200, 5200,
|
||||
6000 and 6000 K. Order and spacing hold; the daylight presets read 8–15 % high, as Sony's
|
||||
dial does against its spectral matrices (#730).
|
||||
- Real files through the headless app (As Shot): WBModes Fluorescent 4,103 K, tint +26 (green
|
||||
compensation); LED-lit room, Auto 3,761 K; open shade, Auto 6,596 K; sunlit chart, Auto
|
||||
4,762 K.
|
||||
- `files::tests::crw_probes_name_their_camera_from_the_ciff_records`: CRW imports now take make,
|
||||
model and capture time from the decoder's CIFF records (no Exif in a CRW; the catalog had an
|
||||
empty camera, so a D60 stayed on the relative white-balance scale).
|
||||
|
||||
## Limits
|
||||
|
||||
- One body, one bulb, one afternoon. The chart is 24 patches; a 3×3 matrix cannot follow the
|
||||
sensor's departures from the CIE observer (the bulb fit's worst patches are 5–7 ΔE).
|
||||
- The lights are filed under standard illuminants they only approximate (~50 K for the bulb,
|
||||
~150 K for the sun).
|
||||
- The D60's look is still the per-file JPEG fit and the pooled profile; these matrices give the
|
||||
white-balance scale and the fallback colour only.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Canon CRW: where the record is
|
||||
|
||||
The Canon CRW (CIFF) decoder and its clean-room record (the frozen format, the verification
|
||||
summary, the colour, geometry and metadata notes, the private-corpus summary and the source log)
|
||||
live in craft-libs:
|
||||
[`crates/crw/clean-room/`](https://github.com/storytold/craft-libs/tree/main/crates/crw/clean-room).
|
||||
The derivation trail and the frozen table-generation materials stay in LightCraft PR #3021.
|
||||
LightCraft uses the crate `craft-crw` through the adapter `crates/raw/src/vendor/crw.rs`.
|
||||
|
||||
What stays here is LightCraft's own colour work on these cameras:
|
||||
|
||||
- [`10-chart-calibration.md`](10-chart-calibration.md): the EOS D60 colour matrices fitted to
|
||||
ColorChecker captures, implemented in `crates/raw/src/chart_table.rs`.
|
||||
- [`logs/chart-captures.tsv`](logs/chart-captures.tsv): names, sizes and SHA-256 of the chart
|
||||
frames (no image content).
|
||||
|
||||
The capture statistics those documents cite (`04-color.md`, `05-private-corpus.md`, `sources.md`)
|
||||
are in the craft-libs record.
|
||||
@@ -0,0 +1,39 @@
|
||||
# EOS D60 ColorChecker Classic captures (maintainer, local only; never committed). Names, sizes, SHA-256; no image content. Generated 2026-10-25.
|
||||
file bytes sha256
|
||||
SunlightColorCard/CRW_2666.CRW 7791628 371d8e18c12801c72754dd7b277163a866afc422c8edf6ad1ea822009e4170f8
|
||||
SunlightColorCard/CRW_2667.CRW 6638578 188d625c31ab44475a6cf54a5dae00fd861e26aafc90f4b5d18a1905c0a3cef5
|
||||
SunlightColorCard/CRW_2668.CRW 6832770 1189b8dc79642c57a8c9496b5629e36a1741c545d2b0b864d6e6e20d901510c1
|
||||
SunlightColorCard/CRW_2669.CRW 7079102 baf1686b27a9e0a04bf87c0b6cf55168f398d85247f8115a0af69b5e9ea1884c
|
||||
SunlightColorCard/CRW_2670.CRW 7225298 067ba3f4d3026b8009cfe4babb5e4c3b28f988d95c48024a75c6715de32afccf
|
||||
SunlightColorCard/CRW_2671.CRW 7504700 cf81cbb96389624318e890b5506221d49f5fba8e2d45447d87e0f3ff3e9bdc72
|
||||
SunlightColorCard/CRW_2672.CRW 7830852 7a59446bfb1f00d6eabd0115b8e5451c34f5d3d48f483cb27608ce9508e43ee5
|
||||
SunlightColorCard/CRW_2673.CRW 8053180 feeb8f818325d6ddb4e88b5eabe4a74f4cb5e1da9f70135ae3f85938c86695dc
|
||||
SunlightColorCard/CRW_2674.CRW 8312060 45e7402c888582adadb607599eae80644d657d61c9607b426c411ee306e42daf
|
||||
SunlightColorCard/CRW_2675.CRW 8565950 be28d2fa049aa4158ae9e3ec49dd28eb56fcd0ffa7af060235b8bda0d89dfd0a
|
||||
SunlightColorCard/CRW_2676.CRW 7795508 76f7418a82e45e1034b3adacacf4bc756cb65a0d683e5bb3631992784e654f93
|
||||
SunlightColorCard/CRW_2677.CRW 7747450 a5e7bc618e6b80ea70f9bad6a1bf41ee63eadbc51df28b82d0864a73434f7964
|
||||
SunlightColorCard/CRW_2678.CRW 6815054 28824f0f64b087488d8dc5a66a8bdf717e83a4d4e1660676d07991bf32dffb96
|
||||
SunlightColorCard/CRW_2679.CRW 7050606 b5c915fa89542265deb4fa16da3ca7ffda01b1d961903836b0059d024316673a
|
||||
SunlightColorCard/CRW_2680.CRW 7231410 a5780dfe99a2df2a3ed9de1888e91baa614b2e009a2b6e0a84d442b6bf1af9fd
|
||||
SunlightColorCard/CRW_2681.CRW 7438088 eb4475919c2770f816d47787656ab7ecb5396f30d2c03628c3ab167ebbc47c65
|
||||
SunlightColorCard/CRW_2682.CRW 7584262 a6fe417b90ce41b03552963dee6b21e4f02b59a5c51148edc04af7224e8c59e9
|
||||
SunlightColorCard/CRW_2683.CRW 7811474 5402ad40e2689d78ade083d8855ecced5caea9368b9d77ecc4a61a16dc55aa36
|
||||
SunlightColorCard/CRW_2684.CRW 8043372 bbef5da457c0b6d6c45c74a99f3e318e6c80a7d3a0ad5c7cb7e13d24205f64fa
|
||||
SunlightColorCard/CRW_2685.CRW 8242344 8d68d592a5c8933f0f35630114fe7017be808b007867beaf1a8d6d2bd1441f70
|
||||
SunlightColorCard/CRW_2686.CRW 8462380 79f7db32b18a834d5408500185c65d316387d1440aa61e26a11be85883fcc382
|
||||
SunlightColorCard/CRW_2687.CRW 7690732 0333d8d18390e77128f186239e6de71c0432ab6ee9669a71c210255e4d915d19
|
||||
TungstonColorChart/CRW_2688.CRW 6269390 d9c4334edfe3636a8a7e69f29c1d1e89f820b7b704697684d7996faee41f5d43
|
||||
TungstonColorChart/CRW_2689.CRW 6001348 1e8b47645d76a71fd9fb4111c4dce805d6dcd41ac82e6b4ce52debbf71565455
|
||||
TungstonColorChart/CRW_2690.CRW 6114056 f63fe49a9ed13af33ddb8d8ed8d75de4e304a3bc2c1f1d4275ba7edbc4f459f3
|
||||
TungstonColorChart/CRW_2691.CRW 6299730 ad8a2844c00965fb6a12e8fc85a6e8bfbdf1924e6d1a68b55472701cb8ffa115
|
||||
TungstonColorChart/CRW_2696.CRW 6169684 105ee515c94da78dedc1f8d777982c4a1e62781b160428524fbe922f62b3ef5c
|
||||
TungstonColorChart/CRW_2697.CRW 5565982 327eebf702185127feb8a5f4bfac5d7ebad5532f7417b302997b19b97d7ff4a8
|
||||
TungstonColorChart/CRW_2698.CRW 5709092 f0fca20f8dcd51046e53bebf9fb9f624677ee674ad739622cdb2578977669705
|
||||
TungstonColorChart/CRW_2699.CRW 5873492 57724a1ff507cf4e78c80a34cd4e0b77eac397322228d32e5215fc3869853dd1
|
||||
TungstonColorChart/CRW_2700.CRW 5988146 6c97ae2c72114034e2fbe6aed0ef97f475696e43b56ed62273eccbfbba4cbaa0
|
||||
TungstonColorChart/CRW_2701.CRW 6202756 c4032e46b753a718d8748e269a9da5d124891d07912542368ea867529143aa62
|
||||
TungstonColorChart/CRW_2702.CRW 6409582 89b65c1c07e0685841046db85923b0bf80e747fed141ddf0335ba12dc75f3f3c
|
||||
TungstonColorChart/CRW_2703.CRW 6407270 50179d0a330c967a74d76bc8342eea1052689c5c544aa1a913769b241ac6f736
|
||||
TungstonColorChart/CRW_2704.CRW 6483154 6bac4d8229a8d0fff1445d212ca63e53911dc7bda988743215009257b87787fa
|
||||
TungstonColorChart/CRW_2705.CRW 7170488 629f94131e7f598964d2635e3039a20c441563201c4f3cbf177d83ad6b061f78
|
||||
TungstonColorChart/CRW_2706.CRW 6383026 67657e7a21f6fd8580012fccd2210afccf285e747bc4d39b8ea775811528ce83
|
||||
|
@@ -332,7 +332,9 @@ pub struct Analysis {
|
||||
/// file's white balance is edited relative to its as-shot look or in Kelvin depends on the camera:
|
||||
/// [`relative_wb_camera`].
|
||||
pub fn relative_wb_format(format: &str) -> bool {
|
||||
["ARW", "NEF", "NRW", "RW2", "RWL", "RAW", "RAF", "CR3", "CR2", "PEF", "SRW", "ORF", "ERF", "MRW"].iter().any(|f| format.eq_ignore_ascii_case(f))
|
||||
["ARW", "NEF", "NRW", "RW2", "RWL", "RAW", "RAF", "CR3", "CR2", "PEF", "SRW", "ORF", "ERF", "MRW", "CRW"]
|
||||
.iter()
|
||||
.any(|f| format.eq_ignore_ascii_case(f))
|
||||
}
|
||||
|
||||
/// Whether a raw of `format` shot on `camera` (Exif make and model, as [`Meta::camera`] keeps them) is developed
|
||||
|
||||
@@ -27,6 +27,7 @@ pub(crate) fn file_local_look(format: RawFormat) -> bool {
|
||||
| RawFormat::Pef
|
||||
| RawFormat::Srw
|
||||
| RawFormat::Orf
|
||||
| RawFormat::Crw
|
||||
| RawFormat::Mrw
|
||||
| RawFormat::Erf
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Camera colour profiles of our own, pooled from many photos of one camera model.
|
||||
//!
|
||||
//! A raw file without colour matrices (Sony ARW, Nikon NEF, Fujifilm RAF) gets its look fitted to its own embedded camera
|
||||
//! A raw file without colour matrices (Sony ARW, Nikon NEF, Fujifilm RAF, Canon CRW) gets its look fitted to its own embedded camera
|
||||
//! JPEG (`camera_preview`), but one photo shows too little of some colours: a lime shirt covering a
|
||||
//! few dozen proxy pixels next to a hillside of foliage at the same hue. `lightcraft-cli calibrate`
|
||||
//! pools the colour pairs of many photos per model and fits one matrix and hue/saturation/value
|
||||
@@ -25,6 +25,7 @@ const MAX_FILE: u64 = 4 << 20;
|
||||
/// Profiles built into LightCraft (`assets/camera-profiles/`, see `assets/ATTRIBUTION.md`):
|
||||
/// `(model, JSON)`.
|
||||
pub const BUNDLED: &[(&str, &str)] = &[
|
||||
("Canon EOS D60", include_str!("../../../assets/camera-profiles/Canon_EOS_D60.json")),
|
||||
("ILCA-99M2", include_str!("../../../assets/camera-profiles/ILCA-99M2.json")),
|
||||
("ILCE-7CR", include_str!("../../../assets/camera-profiles/ILCE-7CR.json")),
|
||||
("ILCE-7M4", include_str!("../../../assets/camera-profiles/ILCE-7M4.json")),
|
||||
@@ -295,6 +296,9 @@ mod tests {
|
||||
assert_eq!(bundled(model), Some(p));
|
||||
}
|
||||
assert!(bundled("No Such Camera").is_none());
|
||||
// a model name with spaces is stored under its safe file name
|
||||
assert!(bundled("Canon EOS D60").is_some_and(|p| p.files > 1000));
|
||||
assert_eq!(file_name("Canon EOS D60"), "Canon_EOS_D60.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -149,6 +149,13 @@ pub fn probe_bytes(name: &str, bytes: &[u8]) -> Result<ProbeInfo, String> {
|
||||
}
|
||||
Err(Err(e)) => return Err(e),
|
||||
};
|
||||
// what the Exif reader can't see, the decoder may have read from the raw's own records (a CRW keeps make,
|
||||
// model and capture time in CIFF records, no Exif): it fills the gaps, so the catalog knows the camera
|
||||
let (meta, captured) = {
|
||||
let mut m = m.clone();
|
||||
m.fill_missing(&raw.metadata);
|
||||
meta_of(&m)
|
||||
};
|
||||
let (w, h) = raw.developed_size();
|
||||
let (mut w, mut h) = (w.max(1) as u32, h.max(1) as u32);
|
||||
if raw.orientation.swaps_axes() {
|
||||
|
||||
@@ -44,6 +44,30 @@ fn psd_probe_reads_embedded_capture_time_gps_and_title() {
|
||||
use super::*;
|
||||
use lightcraft_codecs::{ChromaSubsampling, EncodeImage, EncodeMeta, Samples, encode_jpeg};
|
||||
|
||||
#[test]
|
||||
fn crw_probes_name_their_camera_from_the_ciff_records() {
|
||||
// no Exif in a CRW: the camera and capture time come from the decoder's records, and the catalog's camera
|
||||
// string decides the white-balance scale (the D60 has chart-measured matrices, the 10D none)
|
||||
let dir = std::env::var_os("LIGHTCRAFT_CORPUS")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"))
|
||||
.join("raw");
|
||||
for (name, camera, kelvin) in [("crw-canon-eos-d60.crw", "Canon Canon EOS D60", true), ("crw-canon-eos-10d-a.crw", "Canon Canon EOS 10D", false)]
|
||||
{
|
||||
let Ok(bytes) = std::fs::read(dir.join(name)) else {
|
||||
lightcraft_testkit::skip!(Corpus, "{name} absent");
|
||||
continue;
|
||||
};
|
||||
assert!(lightcraft_meta::extract(&bytes).model.is_none(), "{name}: no Exif to read");
|
||||
let info = probe_bytes(name, &bytes).unwrap();
|
||||
assert_eq!(info.meta.camera, camera, "{name}");
|
||||
assert!(info.captured.is_some(), "{name}");
|
||||
assert_eq!(lightcraft_catalog::relative_wb_camera(&info.format, &info.meta.camera), !kelvin, "{name}");
|
||||
let (t, _) = info.as_shot_wb.unwrap();
|
||||
assert_eq!(t != 6500.0, kelvin, "{name}: {t}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Which white-balance scale a raw gets (issue #730): a file with matrices of its own is absolute from them; a
|
||||
/// fitted-look file is absolute through its camera's measured matrices, which only read the as-shot
|
||||
/// multipliers as Kelvin; any other raw stays relative (6500 / 0 = as shot). The catalog's decision from the
|
||||
@@ -110,6 +134,17 @@ fn white_balance_scale_follows_the_files_matrices_then_the_cameras() {
|
||||
assert!(other.relative && other.scale_white.is_none());
|
||||
let unknown = WbScaleOf::new(RawFormat::Nef, &ColorData::default(), mul, &lightcraft_meta::Metadata::default());
|
||||
assert!(unknown.relative && unknown.as_shot(d65) == (6500.0, 0.0));
|
||||
// CRW: no matrices in the file. The EOS D60 has chart-measured matrices: Kelvin through them, its multipliers
|
||||
// for the sun its D50 matrix was measured under reading as D50; another body stays relative, so its
|
||||
// JPEG-fitted look applies
|
||||
let d60 = WbScaleOf::new(RawFormat::Crw, &ColorData::default(), Some([2.362, 1.0, 1.298]), &meta("Canon", "Canon EOS D60"));
|
||||
assert!(!d60.own_matrix && !d60.relative && d60.legacy_relative && d60.scale_white.is_some());
|
||||
let ((t, tint), (t50, tint50)) = (d60.as_shot(d65), xy_to_temp_tint(lightcraft_color::D50));
|
||||
assert!((t - t50).abs() < 50.0 && (tint - tint50).abs() < 2.0, "{t} {tint} vs D50 {t50} {tint50}");
|
||||
assert!(!lightcraft_catalog::relative_wb_camera("CRW", "Canon Canon EOS D60"));
|
||||
let crw = WbScaleOf::new(RawFormat::Crw, &ColorData::default(), mul, &meta("Canon", "Canon EOS 10D"));
|
||||
assert!(crw.relative && !crw.legacy_relative && crw.scale_white.is_none());
|
||||
assert!(lightcraft_catalog::relative_wb_camera("CRW", "Canon Canon EOS 10D"));
|
||||
// a file with matrices of its own (DNG): absolute from them, no matter whose camera
|
||||
let own = ColorData { illuminant: [21, 0], color_matrix: [Some(lightcraft_color::Mat3::IDENTITY), None], ..Default::default() };
|
||||
let dng = WbScaleOf::new(RawFormat::Dng, &own, mul, &meta("SONY", "ILCE-7M3"));
|
||||
|
||||
@@ -1139,7 +1139,7 @@ pub(crate) fn apply_prepared(s: &mut crate::Session, mut data: CatalogImport, co
|
||||
number(&src.image, "fileHeight").max(0) as u32,
|
||||
&now,
|
||||
);
|
||||
if ["RAW", "DNG", "ARW", "CR2", "CR3", "NEF", "NRW", "RAF", "ORF", "RW2", "PEF"].contains(&p.format.to_uppercase().as_str()) {
|
||||
if ["RAW", "DNG", "ARW", "CR2", "CR3", "CRW", "NEF", "NRW", "RAF", "ORF", "RW2", "PEF"].contains(&p.format.to_uppercase().as_str()) {
|
||||
p.kind = lightcraft_catalog::MediaKind::Raw;
|
||||
}
|
||||
p
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{Arc, Deserialize, DevelopSettings, MediaKind, Photo, Rgb32f, Rgba8, Serialize, SourceInfo};
|
||||
|
||||
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
|
||||
pub const RENDER_CACHE_VERSION: u64 = 72;
|
||||
pub const RENDER_CACHE_VERSION: u64 = 73;
|
||||
|
||||
/// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache).
|
||||
pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];
|
||||
|
||||
@@ -15,6 +15,8 @@ lightcraft-raster = { workspace = true }
|
||||
lightcraft-color = { workspace = true }
|
||||
lightcraft-geom = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
craft-crw = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
miniz_oxide = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Camera colour matrices fitted to our own photographs of a ColorChecker Classic (post-November-2014 formulation),
|
||||
// for cameras with no published spectral sensitivities. Kept by hand: each row's capture, fit and checks are in
|
||||
// clean-room/crw/10-chart-calibration.md (scripts local-only). Reference colours: X-Rite's published L*a*b* (D50) for
|
||||
// the chart, as packaged by colour-science (BSD-3-Clause). No number comes from Adobe or from a GPL raw decoder.
|
||||
use super::Camera;
|
||||
use lightcraft_color::Mat3;
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub(super) const CAMERAS: &[Camera] = &[
|
||||
// Canon EOS D60 (CRW), the maintainer's body, ISO 100. Matrix 1: one ~2,900 K tungsten bulb, 11 frames, filed as
|
||||
// Standard Illuminant A; matrix 2: direct afternoon sun, ~4,850 K, 20 frames, filed as D50 (both temperatures from
|
||||
// the camera's own white-balance presets). Fitted on chromaticity with neutral held at D50 (the sunlit chart was
|
||||
// unevenly lit): CIEDE2000 mean 2.42 / 1.70 on the 24 patches, 2.74 / 1.97 leaving each patch out.
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS D60",
|
||||
illuminant: [17, 23],
|
||||
color_matrix: [Mat3([[0.950961, -0.345297, 0.112165], [-0.545579, 1.400621, 0.558365], [-0.123107, 0.222004, 1.026236]]), Mat3([[0.609694, -0.127035, -0.045387], [-0.461201, 1.16644, 0.337283], [-0.115992, 0.229363, 0.791647]])],
|
||||
forward_matrix: [Mat3([[0.763921, 0.183562, 0.016813], [0.268545, 0.944022, -0.212567], [0.100742, -0.431647, 1.15601]]), Mat3([[0.757277, 0.190573, 0.016446], [0.291782, 1.009128, -0.300909], [0.026419, -0.264451, 1.063137]])],
|
||||
},
|
||||
];
|
||||
@@ -15,7 +15,8 @@
|
||||
//!
|
||||
//! Formats: DNG (uncompressed, lossless JPEG, lossy JPEG (Smart Previews), Deflate incl. floating point, JPEG XL (DNG 1.7,
|
||||
//! `jxl` feature, on by default), tiled/stripped, CFA and LinearRaw),
|
||||
//! Canon CR2 / CR3 (lossless CRX Bayer and version 0x100/0x200 C-RAW), Nikon NEF/NRW (uncompressed, Huffman lossless / lossy compressed), Sony ARW (uncompressed, ARW2, lossless), Fujifilm RAF (uncompressed Bayer
|
||||
//! Canon CR2 / CR3 (lossless CRX Bayer and version 0x100/0x200 C-RAW) / CRW (the CIFF bodies of 2000–2004; clean-room-derived
|
||||
//! compression, `vendor::crw`; the PowerShot G5's anomalous coding is refused), Nikon NEF/NRW (uncompressed, Huffman lossless / lossy compressed), Sony ARW (uncompressed, ARW2, lossless), Fujifilm RAF (uncompressed Bayer
|
||||
//! and X-Trans, lossless and lossy compressed), Panasonic RW2 / Leica RWL / Panasonic RAW (every raw format: compressed 4 and 6, the prefix-coded strips of 8,
|
||||
//! packed 2/5/7, the 16-bit words of the oldest bodies), Pentax PEF (uncompressed, Huffman), Olympus ORF (uncompressed: 16-bit words, 12-bit XZ-2 words, 12-bit 16-byte blocks of the E-300/E-330/E-500).
|
||||
//! [`embedded_preview`] covers these containers' JPEG previews. Variants we can't decode yet (compressed ORF, CR3 unverified marker families / C-RAW configurations) return [`RawError::Unsupported`]; each vendor module documents its sources
|
||||
@@ -140,6 +141,7 @@ impl RawFormat {
|
||||
RawFormat::Dng
|
||||
| RawFormat::Cr2
|
||||
| RawFormat::Cr3
|
||||
| RawFormat::Crw
|
||||
| RawFormat::Nef
|
||||
| RawFormat::Nrw
|
||||
| RawFormat::Arw
|
||||
@@ -413,6 +415,7 @@ fn decode_with(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
RawFormat::Dng => dng::decode(bytes, mode).map(lift_clipped),
|
||||
RawFormat::Cr2 => vendor::cr2::decode(bytes, mode),
|
||||
RawFormat::Cr3 => vendor::cr3::decode(bytes, mode),
|
||||
RawFormat::Crw => vendor::crw::decode(bytes, mode),
|
||||
RawFormat::Nef | RawFormat::Nrw => vendor::nef::decode(bytes, mode),
|
||||
RawFormat::Arw => vendor::arw::decode(bytes, mode),
|
||||
RawFormat::Raf => vendor::raf::decode(bytes, mode),
|
||||
|
||||
@@ -485,8 +485,8 @@ fn scanned_containers_give_the_largest_colour_jpeg() {
|
||||
f.extend_from_slice(part);
|
||||
f.extend_from_slice(&[0u8; 7]);
|
||||
}
|
||||
// MRW decodes; its preview is still found by the scan
|
||||
assert!(crate::probe(&f).is_some_and(|p| !p.is_supported() || p == crate::RawFormat::Mrw), "{head:?}");
|
||||
// CRW and MRW decode; their previews are still found by the scan, the others stay preview-only containers
|
||||
assert!(crate::probe(&f).is_some_and(|p| !p.is_supported() || p == crate::RawFormat::Crw || p == crate::RawFormat::Mrw), "{head:?}");
|
||||
assert_eq!(embedded_preview(&f).as_deref(), Some(&big[..]), "{head:?}");
|
||||
for n in (0..f.len()).step_by(11) {
|
||||
let _ = embedded_preview(&f[..n]);
|
||||
|
||||
+52
-17
@@ -1,28 +1,34 @@
|
||||
//! Camera colour matrices from measured spectral sensitivities, for raws that carry no colour matrix.
|
||||
//! Measured camera colour matrices, for raws that carry no colour matrix.
|
||||
//!
|
||||
//! [`find`] looks a camera up by its Exif make and model in a table generated by
|
||||
//! `tools/spectral_camera_table.py` from rawtoaces-data (Academy Software Foundation, Apache-2.0): per model, DNG
|
||||
//! `ColorMatrix1/2` and `ForwardMatrix1/2` for Standard Illuminant A and D65, fitted to the camera's measured spectral
|
||||
//! sensitivities. [`Camera::fill`] writes them into a file's [`ColorData`], and [`crate::color`]'s DNG model does the
|
||||
//! rest. Other names of a covered camera (Rebel T3i for EOS 600D) come from rawtoaces-data's alias list, and other
|
||||
//! bodies built on a covered camera's sensor from [`SAME_SENSOR`]. No number
|
||||
//! [`find`] looks a camera up by its Exif make and model in two tables, per model DNG `ColorMatrix1/2` and
|
||||
//! `ForwardMatrix1/2` for two calibration illuminants:
|
||||
//! - generated by `tools/spectral_camera_table.py` from rawtoaces-data (Academy Software Foundation, Apache-2.0),
|
||||
//! fitted to the camera's measured spectral sensitivities, for Standard Illuminant A and D65. Other names of a
|
||||
//! covered camera (Rebel T3i for EOS 600D) come from rawtoaces-data's alias list, and other bodies built on a
|
||||
//! covered camera's sensor from [`SAME_SENSOR`];
|
||||
//! - `chart_table.rs`: fitted to our own photographs of a ColorChecker Classic under two lights, for cameras with no
|
||||
//! published sensitivities (`clean-room/crw/10-chart-calibration.md`).
|
||||
//!
|
||||
//! [`Camera::fill`] writes them into a file's [`ColorData`], and [`crate::color`]'s DNG model does the rest. No number
|
||||
//! comes from Adobe or from a GPL raw decoder. Which colour a raw starts from is the engine's choice
|
||||
//! (`docs/camera-preview-colour.md`).
|
||||
use crate::ColorData;
|
||||
use lightcraft_color::{Mat3, Xy};
|
||||
|
||||
#[path = "chart_table.rs"]
|
||||
mod chart;
|
||||
#[path = "spectral_table.rs"]
|
||||
mod table;
|
||||
|
||||
/// DNG `CalibrationIlluminant` codes of the two matrices: Standard Illuminant A, D65.
|
||||
const ILLUMINANTS: [u16; 2] = [17, 21];
|
||||
|
||||
/// One camera's colour matrices.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Camera {
|
||||
pub make: &'static str,
|
||||
pub model: &'static str,
|
||||
/// XYZ → camera under Standard Illuminant A and D65 (`ColorMatrix1/2`).
|
||||
/// DNG `CalibrationIlluminant` codes of the two matrices: Standard Illuminant A (17) and D65 (21) for the spectral
|
||||
/// table, A and D50 (23) for a chart-measured camera.
|
||||
pub illuminant: [u16; 2],
|
||||
/// XYZ → camera under the two illuminants (`ColorMatrix1/2`).
|
||||
pub color_matrix: [Mat3; 2],
|
||||
/// White-balanced camera → XYZ D50 (`ForwardMatrix1/2`).
|
||||
pub forward_matrix: [Mat3; 2],
|
||||
@@ -34,7 +40,7 @@ impl Camera {
|
||||
if crate::color::has_matrix(color) {
|
||||
return false;
|
||||
}
|
||||
color.illuminant = ILLUMINANTS;
|
||||
color.illuminant = self.illuminant;
|
||||
color.color_matrix = self.color_matrix.map(Some);
|
||||
color.forward_matrix = self.forward_matrix.map(Some);
|
||||
true
|
||||
@@ -88,7 +94,7 @@ pub fn find(make: Option<&str>, model: &str) -> Option<&'static Camera> {
|
||||
};
|
||||
match table::ALIASES.iter().chain(SAME_SENSOR).find(|(m, alias, _)| same(m, alias)) {
|
||||
Some((m, _, target)) => table::CAMERAS.iter().find(|c| brand(c.make) == brand(m) && c.model == *target),
|
||||
None => table::CAMERAS.iter().find(|c| same(c.make, c.model)),
|
||||
None => cameras().find(|c| same(c.make, c.model)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +126,9 @@ pub fn as_shot_white_through(camera: &Camera, color: &ColorData, wb_multipliers:
|
||||
Some(crate::color::as_shot_white_of(&c, wb_multipliers))
|
||||
}
|
||||
|
||||
/// Every camera in the table.
|
||||
pub fn cameras() -> &'static [Camera] {
|
||||
table::CAMERAS
|
||||
/// Every camera in the tables: the spectral one, then the chart-measured one.
|
||||
pub fn cameras() -> impl Iterator<Item = &'static Camera> {
|
||||
table::CAMERAS.iter().chain(chart::CAMERAS)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -240,7 +246,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_row_is_a_usable_dng_profile() {
|
||||
assert_eq!(cameras().len(), 52);
|
||||
assert_eq!(cameras().count(), 53);
|
||||
for c in cameras() {
|
||||
assert_eq!(find(Some(c.make), c.model), Some(c), "{} {}", c.make, c.model);
|
||||
for m in c.color_matrix.iter().chain(&c.forward_matrix) {
|
||||
@@ -253,7 +259,36 @@ mod tests {
|
||||
let d50 = lightcraft_color::D50.to_xyz();
|
||||
assert!((0..3).all(|i| (w[i] - d50[i]).abs() < 1e-4), "{} {w:?}", c.model);
|
||||
}
|
||||
assert!(c.illuminant.iter().all(|&i| crate::color::illuminant_temperature(i).is_some()), "{}", c.model);
|
||||
}
|
||||
assert!(table::CAMERAS.iter().all(|c| c.illuminant == [17, 21]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_chart_measured_eos_d60_reads_its_presets_in_kelvin() {
|
||||
// Exif "Canon" / "Canon EOS D60" (CRW), and the catalog's joined string; not the Nikon D60
|
||||
let c = find(Some("Canon"), "Canon EOS D60").unwrap();
|
||||
assert_eq!((c.model, c.illuminant), ("EOS D60", [17, 23]));
|
||||
assert_eq!(find_camera("Canon Canon EOS D60"), Some(c));
|
||||
assert_eq!(find(Some("NIKON CORPORATION"), "NIKON D60"), None);
|
||||
// the camera's own white-balance presets (ColorBalance multipliers of a D60 file), which the fit never saw,
|
||||
// land near the temperatures Canon names for them, on the locus
|
||||
let kelvin = |r: f32, b: f32| {
|
||||
let xy = as_shot_white_through(c, &ColorData::default(), Some([r, 1.0, b])).unwrap();
|
||||
lightcraft_color::cct::xy_to_temp_tint(xy)
|
||||
};
|
||||
let (tungsten, tint) = kelvin(1.568, 1.918);
|
||||
assert!((2900.0..3500.0).contains(&tungsten) && tint.abs() < 15.0, "Tungsten {tungsten} {tint}");
|
||||
let (daylight, tint) = kelvin(2.541, 1.219);
|
||||
assert!((4800.0..5800.0).contains(&daylight) && tint.abs() < 15.0, "Daylight {daylight} {tint}");
|
||||
let (cloudy, _) = kelvin(2.799, 1.131);
|
||||
let (flash, _) = kelvin(2.911, 1.103);
|
||||
eprintln!("EOS D60 presets: Tungsten {tungsten:.0} K, Daylight {daylight:.0} K, Cloudy {cloudy:.0} K, Flash {flash:.0} K");
|
||||
assert!(cloudy > daylight + 300.0 && flash > daylight + 300.0, "{daylight} {cloudy} {flash}");
|
||||
// the two calibration lights read as the illuminants they were filed under
|
||||
let (bulb, _) = kelvin(1.353, 2.213);
|
||||
let (sun, _) = kelvin(2.362, 1.298);
|
||||
assert!((bulb - 2856.0).abs() < 30.0 && (sun - 5003.0).abs() < 50.0, "{bulb} {sun}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -9,312 +9,364 @@ pub(super) const CAMERAS: &[Camera] = &[
|
||||
Camera {
|
||||
make: "ARRI",
|
||||
model: "D21",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.933065, -0.03997, 0.042177], [-0.321253, 0.916394, 0.250116], [-0.047929, 0.258088, 0.770888]]), Mat3([[0.841118, 0.11685, -0.05716], [-0.416513, 1.06087, 0.169491], [-0.121062, 0.361236, 0.692345]])],
|
||||
forward_matrix: [Mat3([[0.913988, -0.102353, 0.152661], [0.315506, 0.849769, -0.165276], [-0.134707, -0.698389, 1.6582]]), Mat3([[1.01361, -0.108757, 0.059444], [0.405694, 0.811177, -0.216871], [-0.027612, -0.326433, 1.17915]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Arashi Vision",
|
||||
model: "Insta360 X5",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.0899, -0.436896, 0.023525], [-0.482767, 1.39879, 0.369626], [-0.056659, 0.169421, 0.72407]]), Mat3([[0.700283, -0.158934, -0.077847], [-0.543955, 1.30291, 0.196622], [-0.148964, 0.25165, 0.516509]])],
|
||||
forward_matrix: [Mat3([[0.710808, 0.160362, 0.093126], [0.231838, 0.891164, -0.123002], [0.020454, -0.498248, 1.3029]]), Mat3([[0.706099, 0.209581, 0.048616], [0.287186, 0.8971, -0.184286], [0.044158, -0.279999, 1.06095]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "Digital Rebel XTi",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.984721, -0.317618, -0.03823], [-0.536205, 1.41139, 0.499196], [-0.019493, 0.0898, 0.993219]]), Mat3([[0.654378, -0.152128, -0.077165], [-0.504141, 1.17532, 0.279049], [-0.071499, 0.132862, 0.672873]])],
|
||||
forward_matrix: [Mat3([[0.735947, 0.130604, 0.097745], [0.271991, 0.870186, -0.142177], [-0.013289, -0.229681, 1.06808]]), Mat3([[0.693623, 0.247958, 0.022715], [0.295094, 0.978904, -0.273998], [0.00951, -0.117049, 0.932644]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS-1D X Mark II",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.841821, -0.109388, -0.029826], [-0.469467, 1.34341, 0.484215], [-0.074495, 0.204633, 0.843881]]), Mat3([[0.559829, 0.012974, -0.076841], [-0.474068, 1.17573, 0.252424], [-0.137973, 0.263217, 0.557436]])],
|
||||
forward_matrix: [Mat3([[0.867342, -0.069209, 0.166163], [0.282942, 0.879178, -0.162119], [0.042308, -0.560464, 1.34326]]), Mat3([[0.866644, -0.0379, 0.135551], [0.340507, 0.913959, -0.254466], [0.035971, -0.324047, 1.11318]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS-1Ds Mark III",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.933464, -0.245919, -0.026807], [-0.571496, 1.49286, 0.379177], [-0.048229, 0.100118, 0.927528]]), Mat3([[0.623037, -0.073582, -0.085177], [-0.54689, 1.28786, 0.213006], [-0.105037, 0.156191, 0.659429]])],
|
||||
forward_matrix: [Mat3([[0.785005, 0.07525, 0.10404], [0.285685, 0.799015, -0.084701], [0.044611, -0.231741, 1.01223]]), Mat3([[0.764142, 0.106772, 0.093381], [0.318478, 0.835999, -0.154477], [0.03106, -0.128764, 0.922809]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS-1Ds Mark II",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.945891, -0.245875, -0.023592], [-0.58518, 1.49586, 0.412994], [-0.049647, 0.124624, 1.04996]]), Mat3([[0.633076, -0.080316, -0.073882], [-0.552992, 1.27833, 0.227086], [-0.10768, 0.180208, 0.730732]])],
|
||||
forward_matrix: [Mat3([[0.78531, 0.073316, 0.10567], [0.29637, 0.804671, -0.101041], [0.021036, -0.254656, 1.05872]]), Mat3([[0.780605, 0.119757, 0.063934], [0.332784, 0.851961, -0.184745], [0.021321, -0.137163, 0.940946]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS 100D",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.823379, -0.153284, -0.011827], [-0.451822, 1.34611, 0.422153], [-0.063154, 0.17914, 0.866791]]), Mat3([[0.537713, -0.001778, -0.071702], [-0.479729, 1.20511, 0.230384], [-0.135862, 0.245077, 0.608091]])],
|
||||
forward_matrix: [Mat3([[0.839709, -0.013024, 0.137611], [0.260264, 0.869968, -0.130232], [0.040567, -0.473274, 1.25781]]), Mat3([[0.852444, -0.005311, 0.117164], [0.33135, 0.880249, -0.211599], [0.038232, -0.260159, 1.04703]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS 200D",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.882798, -0.11491, -0.016841], [-0.443662, 1.31235, 0.491834], [-0.062151, 0.206192, 0.798346]]), Mat3([[0.570466, 0.029473, -0.079371], [-0.461712, 1.15876, 0.257222], [-0.118587, 0.24972, 0.538517]])],
|
||||
forward_matrix: [Mat3([[0.867592, -0.074026, 0.17073], [0.27684, 0.904905, -0.181745], [0.007997, -0.615366, 1.43247]]), Mat3([[0.884048, -0.067045, 0.147293], [0.347584, 0.917256, -0.26484], [0.021454, -0.322825, 1.12647]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS 200D II",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.820814, -0.08112, -0.025232], [-0.449049, 1.31524, 0.500348], [-0.063522, 0.20839, 0.807016]]), Mat3([[0.534782, 0.039491, -0.075304], [-0.461016, 1.15287, 0.26203], [-0.117011, 0.248633, 0.539767]])],
|
||||
forward_matrix: [Mat3([[0.879874, -0.096427, 0.180849], [0.283378, 0.900453, -0.183831], [0.006798, -0.61219, 1.4305]]), Mat3([[0.897644, -0.089573, 0.156225], [0.354938, 0.914467, -0.269405], [0.020307, -0.323555, 1.12835]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS 5DS",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.784071, -0.14278, -0.018109], [-0.493528, 1.36672, 0.49299], [-0.067845, 0.178082, 0.792129]]), Mat3([[0.501948, -0.005161, -0.069927], [-0.500698, 1.19504, 0.25793], [-0.125608, 0.228657, 0.525639]])],
|
||||
forward_matrix: [Mat3([[0.838414, -0.019978, 0.145859], [0.285256, 0.874752, -0.160008], [0.038705, -0.507361, 1.29376]]), Mat3([[0.840997, -0.003166, 0.126464], [0.342942, 0.904091, -0.247033], [0.034419, -0.289165, 1.07985]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS 5D",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.961758, -0.26807, -0.006664], [-0.565407, 1.47679, 0.405541], [-0.067833, 0.12912, 0.951998]]), Mat3([[0.616895, -0.064661, -0.085183], [-0.554321, 1.28625, 0.22097], [-0.137616, 0.197613, 0.656267]])],
|
||||
forward_matrix: [Mat3([[0.79062, 0.078103, 0.095573], [0.287453, 0.812941, -0.100394], [0.068474, -0.279714, 1.03635]]), Mat3([[0.77434, 0.091394, 0.098562], [0.324069, 0.841224, -0.165293], [0.044245, -0.169979, 0.950838]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS 5D Mark III",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.798227, -0.156522, -0.024001], [-0.479014, 1.37953, 0.412187], [-0.067815, 0.178094, 0.795854]]), Mat3([[0.536462, -0.020759, -0.074508], [-0.493492, 1.22724, 0.222071], [-0.136505, 0.242956, 0.547031]])],
|
||||
forward_matrix: [Mat3([[0.829015, -0.007278, 0.142558], [0.266702, 0.857593, -0.124295], [0.048011, -0.488746, 1.26584]]), Mat3([[0.818929, 0.023372, 0.121994], [0.319742, 0.879191, -0.198933], [0.040465, -0.281513, 1.06615]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS 5D Mark II",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.765281, -0.126067, -0.028016], [-0.539891, 1.44802, 0.407637], [-0.058008, 0.128461, 0.861097]]), Mat3([[0.508976, -0.002163, -0.072663], [-0.524812, 1.25489, 0.224018], [-0.11344, 0.183978, 0.599963]])],
|
||||
forward_matrix: [Mat3([[0.84396, -0.009524, 0.12986], [0.296711, 0.799464, -0.096175], [0.052873, -0.321603, 1.09383]]), Mat3([[0.8407, 0.001511, 0.122085], [0.343565, 0.82753, -0.171095], [0.035331, -0.182192, 0.971966]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS 5D Mark IV",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.763394, -0.085035, -0.016567], [-0.442907, 1.31347, 0.486351], [-0.080725, 0.247335, 0.884784]]), Mat3([[0.49672, 0.034424, -0.068025], [-0.461551, 1.16055, 0.255439], [-0.154521, 0.307914, 0.579555]])],
|
||||
forward_matrix: [Mat3([[0.880649, -0.094084, 0.177732], [0.27666, 0.906137, -0.182797], [0.024429, -0.659213, 1.45989]]), Mat3([[0.898336, -0.087734, 0.153694], [0.347611, 0.924078, -0.271689], [0.035769, -0.378779, 1.16812]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS 600D",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.800214, -0.150575, -0.010842], [-0.462694, 1.36091, 0.41413], [-0.054076, 0.17327, 0.853835]]), Mat3([[0.528239, -0.007001, -0.067784], [-0.485979, 1.21691, 0.225002], [-0.13144, 0.244539, 0.592949]])],
|
||||
forward_matrix: [Mat3([[0.836952, -0.011114, 0.138458], [0.267123, 0.859868, -0.12699], [0.017395, -0.463764, 1.27147]]), Mat3([[0.849461, 0.005947, 0.108888], [0.332065, 0.875561, -0.207627], [0.033552, -0.262503, 1.05405]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS M",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.858133, -0.188655, -0.015316], [-0.477476, 1.37657, 0.415737], [-0.066593, 0.173487, 0.870331]]), Mat3([[0.559337, -0.024286, -0.076489], [-0.498439, 1.22646, 0.227107], [-0.13663, 0.237788, 0.612213]])],
|
||||
forward_matrix: [Mat3([[0.818185, 0.015661, 0.130449], [0.263684, 0.85776, -0.121444], [0.046738, -0.442459, 1.22083]]), Mat3([[0.820106, 0.031315, 0.112875], [0.32549, 0.874009, -0.199499], [0.03847, -0.243276, 1.02991]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS R10",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.900395, -0.120204, -0.023257], [-0.450784, 1.32079, 0.490112], [-0.060508, 0.207987, 0.757971]]), Mat3([[0.586274, 0.022629, -0.081629], [-0.465757, 1.16444, 0.255537], [-0.114546, 0.247787, 0.505639]])],
|
||||
forward_matrix: [Mat3([[0.860416, -0.07424, 0.178119], [0.279245, 0.90614, -0.185385], [-0.005252, -0.64772, 1.47808]]), Mat3([[0.874599, -0.057155, 0.146852], [0.346428, 0.920831, -0.267259], [0.018292, -0.341711, 1.14852]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS R5",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.906792, -0.181028, -0.039982], [-0.497007, 1.37995, 0.46653], [-0.056558, 0.135904, 0.790883]]), Mat3([[0.599617, -0.03146, -0.086837], [-0.500836, 1.20715, 0.246929], [-0.122485, 0.204473, 0.529195]])],
|
||||
forward_matrix: [Mat3([[0.825544, 0.000874, 0.137878], [0.27921, 0.851443, -0.130653], [0.053855, -0.393303, 1.16455]]), Mat3([[0.80269, 0.040879, 0.120727], [0.321664, 0.899552, -0.221215], [0.039913, -0.245426, 1.03062]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS R5m2",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.925997, -0.201878, -0.03247], [-0.514263, 1.39694, 0.472057], [-0.067213, 0.159605, 0.932813]]), Mat3([[0.603973, -0.037874, -0.08705], [-0.520173, 1.22523, 0.247204], [-0.147012, 0.240988, 0.619754]])],
|
||||
forward_matrix: [Mat3([[0.816853, 0.016859, 0.130583], [0.284699, 0.847793, -0.132492], [0.049678, -0.384186, 1.15961]]), Mat3([[0.796684, 0.049994, 0.117618], [0.326624, 0.8894, -0.216024], [0.040655, -0.243309, 1.02776]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS R6",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.887235, -0.176902, -0.018642], [-0.462429, 1.33965, 0.473061], [-0.070097, 0.19962, 0.862666]]), Mat3([[0.571385, -0.014759, -0.079163], [-0.485367, 1.18708, 0.251862], [-0.145333, 0.265796, 0.580543]])],
|
||||
forward_matrix: [Mat3([[0.830203, -0.011614, 0.145706], [0.268315, 0.891706, -0.16002], [0.03391, -0.533608, 1.3248]]), Mat3([[0.829826, 0.011021, 0.123449], [0.329107, 0.920067, -0.249174], [0.037653, -0.307101, 1.09455]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS R6m2",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.904651, -0.185945, -0.032292], [-0.490303, 1.37329, 0.46456], [-0.076482, 0.19622, 0.939437]]), Mat3([[0.595351, -0.032062, -0.086354], [-0.499687, 1.20659, 0.246445], [-0.151602, 0.266204, 0.633694]])],
|
||||
forward_matrix: [Mat3([[0.822515, -0.0028, 0.14458], [0.275534, 0.86495, -0.140483], [0.04532, -0.468236, 1.24802]]), Mat3([[0.80207, 0.038555, 0.123671], [0.322259, 0.905003, -0.227262], [0.03728, -0.271382, 1.05921]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS RP",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.843269, -0.158656, -0.026198], [-0.497933, 1.37758, 0.476056], [-0.070882, 0.179269, 0.839769]]), Mat3([[0.548578, -0.014251, -0.077893], [-0.504763, 1.20691, 0.250579], [-0.139008, 0.243991, 0.560854]])],
|
||||
forward_matrix: [Mat3([[0.836529, -0.017241, 0.145008], [0.285362, 0.860694, -0.146057], [0.049163, -0.481077, 1.25702]]), Mat3([[0.825922, 0.011858, 0.126516], [0.334357, 0.898585, -0.232942], [0.038621, -0.283664, 1.07015]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "EOS R",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.732886, -0.080769, -0.010303], [-0.454757, 1.32829, 0.481287], [-0.083062, 0.251827, 0.895746]]), Mat3([[0.473635, 0.038459, -0.063761], [-0.474141, 1.17757, 0.250797], [-0.157456, 0.313293, 0.589901]])],
|
||||
forward_matrix: [Mat3([[0.884185, -0.093978, 0.174089], [0.284166, 0.893986, -0.178152], [0.023242, -0.654769, 1.45663]]), Mat3([[0.908841, -0.095987, 0.151442], [0.356864, 0.902145, -0.259009], [0.034581, -0.371735, 1.16226]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Canon",
|
||||
model: "PowerShot S90",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.37542, -0.592063, 0.037432], [-0.219062, 1.12456, 0.326212], [-0.021707, 0.09788, 0.58587]]), Mat3([[0.90732, -0.244506, -0.087152], [-0.314903, 1.06475, 0.215416], [-0.11223, 0.179775, 0.439924]])],
|
||||
forward_matrix: [Mat3([[0.67794, 0.203129, 0.083227], [0.105177, 1.01192, -0.1171], [0.055684, -0.464622, 1.23404]]), Mat3([[0.655349, 0.321862, -0.012915], [0.180096, 1.10102, -0.281111], [0.064028, -0.26917, 1.03025]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Fujifilm",
|
||||
model: "GFX 100",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.37208, -0.633889, 0.081365], [-0.430548, 1.32101, 0.427017], [-0.039908, 0.114944, 0.782345]]), Mat3([[0.834968, -0.233255, -0.07589], [-0.475844, 1.18948, 0.241344], [-0.126239, 0.194812, 0.541708]])],
|
||||
forward_matrix: [Mat3([[0.673446, 0.25104, 0.039809], [0.200798, 0.950696, -0.151494], [0.031013, -0.340478, 1.13457]]), Mat3([[0.679917, 0.304542, -0.020163], [0.261808, 1.00662, -0.268426], [0.044444, -0.214012, 0.994673]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Fujifilm",
|
||||
model: "X-T3",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.29933, -0.598524, 0.075033], [-0.403354, 1.28265, 0.450876], [-0.037817, 0.112326, 0.810909]]), Mat3([[0.785701, -0.221043, -0.071522], [-0.456611, 1.15627, 0.255052], [-0.124341, 0.191243, 0.562458]])],
|
||||
forward_matrix: [Mat3([[0.671025, 0.255703, 0.037568], [0.190434, 0.97306, -0.163493], [0.03182, -0.334086, 1.12737]]), Mat3([[0.676828, 0.316449, -0.028982], [0.256753, 1.03712, -0.293871], [0.04319, -0.207191, 0.989106]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Fujifilm",
|
||||
model: "X-T4",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.27406, -0.587718, 0.074639], [-0.371617, 1.24808, 0.450058], [-0.038746, 0.113215, 0.780014]]), Mat3([[0.771728, -0.215706, -0.070731], [-0.439625, 1.14183, 0.253491], [-0.125229, 0.192512, 0.542394]])],
|
||||
forward_matrix: [Mat3([[0.668116, 0.258486, 0.037694], [0.175163, 0.992049, -0.167213], [0.040145, -0.355105, 1.14006]]), Mat3([[0.675162, 0.317733, -0.028599], [0.248581, 1.05006, -0.298641], [0.04702, -0.219918, 0.998002]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Hasselblad",
|
||||
model: "L1D-20c",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.697908, -0.225515, 0.018865], [-0.403519, 1.29608, 0.413637], [-0.048508, 0.167167, 0.74727]]), Mat3([[0.466971, -0.076456, -0.040872], [-0.471572, 1.21777, 0.21163], [-0.133006, 0.246905, 0.516655]])],
|
||||
forward_matrix: [Mat3([[0.754419, 0.110524, 0.099352], [0.21061, 0.943846, -0.154456], [0.016488, -0.523808, 1.33242]]), Mat3([[0.780313, 0.168783, 0.0152], [0.295267, 0.942833, -0.2381], [0.041, -0.301041, 1.08515]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Hasselblad",
|
||||
model: "L2D-20c",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.787283, -0.249155, 0.044752], [-0.415734, 1.29586, 0.451949], [-0.060535, 0.144901, 0.709626]]), Mat3([[0.506449, -0.071884, -0.038243], [-0.435817, 1.14056, 0.251334], [-0.129637, 0.209508, 0.474062]])],
|
||||
forward_matrix: [Mat3([[0.776836, 0.120396, 0.067064], [0.218196, 0.951802, -0.169998], [0.071832, -0.4598, 1.21307]]), Mat3([[0.807201, 0.167509, -0.010414], [0.291699, 1.01528, -0.306978], [0.062694, -0.296911, 1.05932]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "D200",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.37569, -0.560621, 0.01677], [-0.606594, 1.52996, 0.38327], [-0.024329, 0.069398, 0.99135]]), Mat3([[0.898945, -0.249527, -0.086342], [-0.568281, 1.29839, 0.222009], [-0.098369, 0.139456, 0.687094]])],
|
||||
forward_matrix: [Mat3([[0.708545, 0.18748, 0.068271], [0.274935, 0.818921, -0.093855], [0.012224, -0.169279, 0.98216]]), Mat3([[0.685079, 0.274473, 0.004744], [0.294469, 0.898619, -0.193087], [0.026107, -0.10149, 0.900488]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "D3300",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.01047, -0.365965, 0.032301], [-0.473579, 1.3513, 0.474736], [-0.075188, 0.164522, 1.02012]]), Mat3([[0.644589, -0.136221, -0.056505], [-0.47509, 1.16499, 0.26318], [-0.15352, 0.237111, 0.681108]])],
|
||||
forward_matrix: [Mat3([[0.738138, 0.162349, 0.063809], [0.234836, 0.925124, -0.15996], [0.059989, -0.351996, 1.11711]]), Mat3([[0.742449, 0.23561, -0.013763], [0.290358, 1.00587, -0.29623], [0.04507, -0.217376, 0.99741]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "D5100",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.34772, -0.48474, 0.012024], [-0.466856, 1.29899, 0.44042], [-0.076248, 0.161652, 0.981853]]), Mat3([[0.929942, -0.213704, -0.081538], [-0.481178, 1.17186, 0.262186], [-0.14942, 0.233631, 0.699016]])],
|
||||
forward_matrix: [Mat3([[0.732398, 0.159354, 0.072543], [0.240376, 0.912366, -0.152741], [0.06111, -0.349037, 1.11303]]), Mat3([[0.727572, 0.254229, -0.017505], [0.287762, 1.00467, -0.292432], [0.040205, -0.206009, 0.990908]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "D5300",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.943297, -0.302977, 0.012055], [-0.489398, 1.36679, 0.480036], [-0.079912, 0.172098, 1.02217]]), Mat3([[0.613661, -0.114638, -0.052652], [-0.475392, 1.16153, 0.266621], [-0.147109, 0.232899, 0.680179]])],
|
||||
forward_matrix: [Mat3([[0.75961, 0.127618, 0.077067], [0.24786, 0.911605, -0.159465], [0.062139, -0.362458, 1.12542]]), Mat3([[0.765013, 0.208344, -0.009061], [0.301196, 0.998195, -0.299391], [0.042143, -0.217084, 1.00005]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "D7000",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.0579, -0.370378, 0.024968], [-0.50908, 1.38998, 0.475636], [-0.075079, 0.158561, 1.00866]]), Mat3([[0.675661, -0.143226, -0.053077], [-0.488456, 1.16985, 0.270384], [-0.137868, 0.21334, 0.681191]])],
|
||||
forward_matrix: [Mat3([[0.744976, 0.150244, 0.069076], [0.251735, 0.900835, -0.15257], [0.058406, -0.336086, 1.10278]]), Mat3([[0.755556, 0.237153, -0.028413], [0.304411, 0.998446, -0.302857], [0.039148, -0.192875, 0.978832]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "D700",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.25876, -0.491229, 0.017162], [-0.619342, 1.54791, 0.372191], [-0.050936, 0.098145, 1.01122]]), Mat3([[0.835285, -0.217528, -0.074624], [-0.583051, 1.32613, 0.209425], [-0.121368, 0.168895, 0.708639]])],
|
||||
forward_matrix: [Mat3([[0.722062, 0.17127, 0.070964], [0.279306, 0.809472, -0.088779], [0.042476, -0.200528, 0.983157]]), Mat3([[0.707886, 0.252255, 0.004155], [0.304416, 0.875438, -0.179854], [0.032977, -0.118624, 0.910752]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "D70",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.95752, -0.326533, -0.000865], [-0.654123, 1.5628, 0.437699], [-0.01723, 0.05324, 0.899754]]), Mat3([[0.621434, -0.137207, -0.067353], [-0.59081, 1.28888, 0.250406], [-0.066923, 0.098053, 0.633702]])],
|
||||
forward_matrix: [Mat3([[0.74958, 0.134109, 0.080606], [0.313007, 0.787492, -0.100498], [0.005453, -0.15183, 0.971482]]), Mat3([[0.722318, 0.215673, 0.026305], [0.326263, 0.879351, -0.205614], [0.017282, -0.077471, 0.885294]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "D800E",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.981723, -0.320813, 0.025417], [-0.500428, 1.37669, 0.486261], [-0.07946, 0.169419, 1.04548]]), Mat3([[0.629114, -0.117451, -0.046422], [-0.483155, 1.16337, 0.27171], [-0.147278, 0.230496, 0.696276]])],
|
||||
forward_matrix: [Mat3([[0.760723, 0.133423, 0.07015], [0.25382, 0.906695, -0.160515], [0.059962, -0.34837, 1.11351]]), Mat3([[0.780492, 0.212338, -0.028534], [0.311949, 0.998057, -0.310006], [0.041771, -0.208304, 0.991637]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "D810",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.983274, -0.318832, 0.030267], [-0.51358, 1.39278, 0.481653], [-0.076666, 0.176366, 1.07197]]), Mat3([[0.625688, -0.110113, -0.045912], [-0.497828, 1.18171, 0.267671], [-0.149244, 0.239807, 0.71615]])],
|
||||
forward_matrix: [Mat3([[0.763991, 0.129679, 0.070625], [0.262526, 0.897486, -0.160012], [0.045849, -0.35404, 1.13329]]), Mat3([[0.791151, 0.196615, -0.023471], [0.322368, 0.977225, -0.299593], [0.038389, -0.209045, 0.995761]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "D850",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.17359, -0.461087, -0.011934], [-0.607581, 1.52285, 0.4063], [-0.096127, 0.196063, 1.00005]]), Mat3([[0.773836, -0.196466, -0.097677], [-0.609077, 1.35526, 0.205388], [-0.190117, 0.290252, 0.676129]])],
|
||||
forward_matrix: [Mat3([[0.713588, 0.158895, 0.091813], [0.273041, 0.838318, -0.111359], [0.055293, -0.367196, 1.13701]]), Mat3([[0.669707, 0.227388, 0.0672], [0.291703, 0.871502, -0.163205], [0.042827, -0.229884, 1.01216]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Nikon",
|
||||
model: "Z f",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[1.16951, -0.415422, 0.033906], [-0.516721, 1.41065, 0.441119], [-0.080466, 0.180954, 1.07672]]), Mat3([[0.749962, -0.151755, -0.063228], [-0.504317, 1.20913, 0.248149], [-0.162888, 0.256933, 0.729432]])],
|
||||
forward_matrix: [Mat3([[0.744024, 0.151119, 0.069153], [0.253649, 0.888649, -0.142297], [0.049891, -0.351314, 1.12653]]), Mat3([[0.756117, 0.216534, -0.008355], [0.304737, 0.961046, -0.265784], [0.041615, -0.212408, 0.995897]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Panasonic",
|
||||
model: "DC-GX9",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.820911, -0.254983, 0.035979], [-0.372553, 1.25333, 0.438192], [-0.054294, 0.146795, 0.747319]]), Mat3([[0.534195, -0.07728, -0.038219], [-0.400358, 1.10647, 0.251688], [-0.12982, 0.216241, 0.50214]])],
|
||||
forward_matrix: [Mat3([[0.772645, 0.116685, 0.074966], [0.196365, 0.972505, -0.16887], [0.06062, -0.463162, 1.22765]]), Mat3([[0.805529, 0.178696, -0.01993], [0.275218, 1.04725, -0.322465], [0.060973, -0.29745, 1.06158]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "DSC-RX100M4",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.783848, -0.284096, 0.031033], [-0.411895, 1.30629, 0.410807], [-0.047532, 0.165091, 0.725564]]), Mat3([[0.507376, -0.094837, -0.042497], [-0.488786, 1.23246, 0.213166], [-0.137082, 0.247475, 0.503834]])],
|
||||
forward_matrix: [Mat3([[0.733625, 0.143965, 0.086705], [0.210364, 0.945779, -0.156144], [0.012886, -0.528963, 1.34118]]), Mat3([[0.767183, 0.193058, 0.004054], [0.296288, 0.94455, -0.240838], [0.043397, -0.304922, 1.08663]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "ILCE-6400",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.686447, -0.204525, 0.026311], [-0.43013, 1.31387, 0.445779], [-0.061279, 0.132669, 0.791917]]), Mat3([[0.44557, -0.064452, -0.027734], [-0.437513, 1.13079, 0.261784], [-0.122323, 0.190304, 0.552427]])],
|
||||
forward_matrix: [Mat3([[0.782555, 0.106676, 0.075064], [0.224677, 0.924591, -0.149268], [0.078407, -0.377222, 1.12392]]), Mat3([[0.821757, 0.17479, -0.032251], [0.30346, 1.00862, -0.31208], [0.052261, -0.224433, 0.997277]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "ILCE-7CM2",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.758033, -0.221503, 0.036455], [-0.492549, 1.36195, 0.503357], [-0.065665, 0.148645, 0.906293]]), Mat3([[0.482991, -0.070139, -0.027429], [-0.478804, 1.14702, 0.282926], [-0.119226, 0.194381, 0.616628]])],
|
||||
forward_matrix: [Mat3([[0.788283, 0.10759, 0.068422], [0.261923, 0.909809, -0.171732], [0.053026, -0.362705, 1.13478]]), Mat3([[0.83437, 0.173346, -0.043421], [0.336938, 0.996333, -0.333271], [0.037073, -0.203664, 0.991696]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "ILCE-7M3",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.759047, -0.22787, 0.042531], [-0.502563, 1.37732, 0.491076], [-0.066877, 0.150947, 0.900094]]), Mat3([[0.482867, -0.070899, -0.024574], [-0.489531, 1.16667, 0.274244], [-0.123988, 0.201218, 0.610148]])],
|
||||
forward_matrix: [Mat3([[0.787201, 0.113043, 0.064052], [0.265374, 0.902211, -0.167585], [0.052744, -0.364541, 1.1369]]), Mat3([[0.840001, 0.174345, -0.05005], [0.340881, 0.981246, -0.322127], [0.039232, -0.209817, 0.995689]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "ILCE-7M4",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.763892, -0.227658, 0.033049], [-0.512687, 1.38412, 0.503225], [-0.063774, 0.141899, 0.91715]]), Mat3([[0.485445, -0.074907, -0.026973], [-0.491937, 1.15934, 0.283071], [-0.115422, 0.185993, 0.622862]])],
|
||||
forward_matrix: [Mat3([[0.784064, 0.112194, 0.068037], [0.2698, 0.896428, -0.166229], [0.050008, -0.338826, 1.11392]]), Mat3([[0.830062, 0.181159, -0.046925], [0.341401, 0.985843, -0.327244], [0.034749, -0.188826, 0.979182]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "ILCE-7RM2",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.775308, -0.25358, 0.026979], [-0.524545, 1.40154, 0.490876], [-0.060608, 0.132591, 0.920838]]), Mat3([[0.492753, -0.089554, -0.036484], [-0.505928, 1.18284, 0.273705], [-0.116121, 0.182519, 0.626106]])],
|
||||
forward_matrix: [Mat3([[0.76369, 0.133034, 0.067571], [0.267238, 0.888036, -0.155274], [0.048003, -0.313698, 1.0908]]), Mat3([[0.787687, 0.201628, -0.025019], [0.326839, 0.971594, -0.298433], [0.034173, -0.177854, 0.968786]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "ILCE-7RM3",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.742519, -0.22996, 0.032883], [-0.500197, 1.37662, 0.485744], [-0.063378, 0.142467, 0.898759]]), Mat3([[0.473388, -0.075142, -0.031025], [-0.487453, 1.16732, 0.271829], [-0.119917, 0.192619, 0.610537]])],
|
||||
forward_matrix: [Mat3([[0.776802, 0.119664, 0.06783], [0.260471, 0.900308, -0.160779], [0.051869, -0.347156, 1.12039]]), Mat3([[0.813753, 0.181916, -0.031373], [0.32902, 0.979656, -0.308676], [0.037764, -0.198487, 0.985828]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "ILCE-7RM4",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.732028, -0.220808, 0.020118], [-0.501296, 1.37502, 0.49365], [-0.061615, 0.13838, 0.885461]]), Mat3([[0.472151, -0.07908, -0.031322], [-0.483014, 1.15528, 0.279019], [-0.111192, 0.18072, 0.604588]])],
|
||||
forward_matrix: [Mat3([[0.775623, 0.114594, 0.074079], [0.260863, 0.901608, -0.162471], [0.050735, -0.342927, 1.1173]]), Mat3([[0.806038, 0.193021, -0.034763], [0.32686, 0.992356, -0.319216], [0.033831, -0.189102, 0.980375]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "ILCE-7SM2",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.615775, -0.137144, 0.06099], [-0.408209, 1.28402, 0.462018], [-0.084205, 0.196387, 0.85663]]), Mat3([[0.395982, -0.016098, -0.005517], [-0.420931, 1.11058, 0.265879], [-0.162165, 0.267589, 0.576945]])],
|
||||
forward_matrix: [Mat3([[0.854583, 0.041834, 0.067878], [0.234125, 0.948414, -0.182539], [0.097857, -0.530195, 1.25744]]), Mat3([[0.948357, 0.092307, -0.076369], [0.33668, 1.02973, -0.366408], [0.073834, -0.330955, 1.08223]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "ILCE-7SM3",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.749715, -0.228299, 0.030188], [-0.512062, 1.38442, 0.500457], [-0.072776, 0.169469, 0.933545]]), Mat3([[0.476058, -0.07383, -0.031403], [-0.495874, 1.16904, 0.277606], [-0.134473, 0.222664, 0.625349]])],
|
||||
forward_matrix: [Mat3([[0.779691, 0.113899, 0.070706], [0.268632, 0.905927, -0.174559], [0.04639, -0.390929, 1.16964]]), Mat3([[0.816745, 0.179015, -0.031464], [0.334824, 0.988848, -0.323672], [0.037938, -0.229363, 1.01653]])],
|
||||
},
|
||||
Camera {
|
||||
make: "Sony",
|
||||
model: "ILCE-9",
|
||||
illuminant: [17, 21],
|
||||
color_matrix: [Mat3([[0.747492, -0.216385, 0.049086], [-0.499768, 1.37142, 0.499037], [-0.071324, 0.164705, 0.951496]]), Mat3([[0.473639, -0.063703, -0.016557], [-0.488372, 1.1591, 0.280181], [-0.129154, 0.213406, 0.651]])],
|
||||
forward_matrix: [Mat3([[0.796409, 0.106147, 0.061739], [0.26822, 0.906732, -0.174952], [0.050574, -0.378333, 1.15286]]), Mat3([[0.868551, 0.167059, -0.071314], [0.354491, 0.985971, -0.340462], [0.037779, -0.211149, 0.998474]])],
|
||||
},
|
||||
|
||||
@@ -258,9 +258,12 @@ fn private_containers_are_recognised_by_their_magic() {
|
||||
assert_eq!(probe(b"II\x1a\0\0\0HEAPCCDR\0\0"), Some(RawFormat::Crw));
|
||||
assert_eq!(probe(b"\0MRM\0\x01\0\0"), Some(RawFormat::Mrw));
|
||||
assert_eq!(probe(b"FOVb\x02\0\x02\0"), Some(RawFormat::X3f));
|
||||
for f in [RawFormat::Crw, RawFormat::X3f] {
|
||||
assert!(!f.is_supported());
|
||||
for f in [RawFormat::Crw, RawFormat::Mrw] {
|
||||
assert!(f.is_supported());
|
||||
}
|
||||
assert!(!RawFormat::X3f.is_supported());
|
||||
// a CRW signature with no heap behind it is corrupt, not a panic
|
||||
assert!(matches!(decode(b"II\x1a\0\0\0HEAPCCDR\0\0"), Err(RawError::Corrupt(_))));
|
||||
assert!(matches!(decode(b"FOVb\x02\0\x02\0"), Err(RawError::Unsupported(w)) if w.contains("X3f")));
|
||||
// too short for the CIFF signature
|
||||
assert_eq!(probe(b"II\x1a\0\0\0HEAP"), None);
|
||||
|
||||
@@ -9,6 +9,7 @@ mod arw_lens;
|
||||
pub(crate) mod arw_lens_rm6;
|
||||
pub mod cr2;
|
||||
pub mod cr3;
|
||||
pub mod crw;
|
||||
pub mod crx;
|
||||
pub(crate) mod crx_wavelet;
|
||||
pub mod erf;
|
||||
|
||||
Vendored
+255
@@ -0,0 +1,255 @@
|
||||
//! Canon CRW (CIFF heap) raw decoding through the shared `craft-crw` crate (craft-libs,
|
||||
//! `crates/crw`): the bodies before the CR2 era (EOS D30/D60/10D/300D, PowerShot G-series).
|
||||
//!
|
||||
//! The decoder, its frozen code tables and its clean-room record live in
|
||||
//! craft-libs (`crates/crw/clean-room/`: the frozen format `02-crw-format.md`, the verification
|
||||
//! `03-verification.md`, 28 of 29 corpus files and 1,516 EOS D60 files sample-exact against a
|
||||
//! black-box reference). This module maps its output onto [`RawImage`]: CFA phase, black and white
|
||||
//! levels, active area, orientation, as-shot white balance and the make, model and capture-time
|
||||
//! records (a CRW carries no Exif block, so this is where the catalog learns the camera).
|
||||
//!
|
||||
//! Known gaps (documented in the freeze): the PowerShot G5's entropy coding diverges from the
|
||||
//! model and is refused (its embedded JPEG is shown instead); PowerShot bodies without CC0 samples
|
||||
//! (G2/G3/Pro1/S-series) decode through the same model unverified. Colour matrices for these
|
||||
//! bodies are LightCraft's own (`chart_table`, the EOS D60); the file carries none.
|
||||
|
||||
use crate::{BlackLevel, Cfa, ColorData, Metadata, Mode, OpcodeLists, Orientation, RawData, RawError, RawFormat, RawImage, Rect, Result};
|
||||
use lightcraft_meta::DateTime;
|
||||
|
||||
fn map_err(e: craft_crw::Error) -> RawError {
|
||||
match e {
|
||||
craft_crw::Error::NotCrw => RawError::NotRaw,
|
||||
craft_crw::Error::Corrupt(why) => RawError::Corrupt(format!("CRW: {why}")),
|
||||
craft_crw::Error::Unsupported(why) => RawError::Unsupported(format!("CRW: {why}")),
|
||||
craft_crw::Error::LimitExceeded(what) => RawError::Limit(what),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
// the crate's own cap (2²³ samples; the largest CRW frame is 7.4 million): its decode holds
|
||||
// about three `u16` buffers of the declared size, so the raw crate's general `MAX_SAMPLES`
|
||||
// (2³⁰) would let a malformed file ask for gigabytes. No sample may be filled after the
|
||||
// entropy stream ends (the crate allows one 64-sample block by default): every corpus file
|
||||
// ends exactly on its frame, so a shorter stream is a truncated file and is `Corrupt`.
|
||||
let mut limits = craft_crw::Limits::default();
|
||||
limits.max_stream_end_fill = 0;
|
||||
let full = mode == Mode::Full;
|
||||
let (h, samples, black, white) = if full {
|
||||
let img = craft_crw::decode(bytes, &limits).map_err(map_err)?;
|
||||
let black = BlackLevel { repeat_rows: 2, repeat_cols: 2, values: img.black.values.to_vec(), delta_h: vec![], delta_v: vec![] };
|
||||
(img.header, img.samples, black, img.white)
|
||||
} else {
|
||||
let h = craft_crw::header(bytes, &limits).map_err(map_err)?;
|
||||
let white = ((1u32 << h.bits.clamp(1, 16)) - 1) as f32;
|
||||
(h, Vec::new(), BlackLevel::uniform(0.0), white)
|
||||
};
|
||||
let active = Rect::new(h.active_area.x, h.active_area.y, h.active_area.width, h.active_area.height);
|
||||
let orientation = Orientation::from_exif(h.exif_orientation as _);
|
||||
let metadata = Metadata {
|
||||
make: h.make,
|
||||
model: h.model,
|
||||
capture_time: h.capture_unix.and_then(datetime_from_unix),
|
||||
width: Some(active.width as u32),
|
||||
height: Some(active.height as u32),
|
||||
orientation: Some(orientation),
|
||||
..Metadata::default()
|
||||
};
|
||||
let img = RawImage {
|
||||
format: RawFormat::Crw,
|
||||
width: h.width,
|
||||
height: h.height,
|
||||
cpp: 1,
|
||||
data: RawData::U16(samples),
|
||||
cfa: Some(Cfa::bayer_static(h.cfa.name())),
|
||||
bits: h.bits,
|
||||
black,
|
||||
white: vec![white],
|
||||
active_area: active,
|
||||
crop: Rect::new(0, 0, active.width, active.height),
|
||||
orientation,
|
||||
color: ColorData::default(),
|
||||
wb_multipliers: h.wb_multipliers,
|
||||
linearized: false,
|
||||
pixel_aspect: 1.0,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata,
|
||||
lens_metadata: Default::default(),
|
||||
};
|
||||
img.validate_for(mode)?;
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
/// Civil date-time from the TimeStamp record's Unix seconds (the camera's local clock, no UTC
|
||||
/// offset; inverse of [`DateTime::unix_seconds`]). Before 1970 or after 2200 is a garbage clock,
|
||||
/// not a capture time.
|
||||
fn datetime_from_unix(t: i64) -> Option<DateTime> {
|
||||
use chrono::{Datelike, Timelike};
|
||||
let c = chrono::DateTime::from_timestamp(t, 0)?.naive_utc();
|
||||
let dt = DateTime {
|
||||
year: u16::try_from(c.year()).ok()?,
|
||||
month: u8::try_from(c.month()).ok()?,
|
||||
day: u8::try_from(c.day()).ok()?,
|
||||
hour: u8::try_from(c.hour()).ok()?,
|
||||
minute: u8::try_from(c.minute()).ok()?,
|
||||
second: u8::try_from(c.second()).ok()?,
|
||||
millis: 0,
|
||||
offset_minutes: None,
|
||||
};
|
||||
((1970..=2200).contains(&dt.year)).then_some(dt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const W: usize = 48;
|
||||
const H: usize = 20;
|
||||
|
||||
/// A CIFF file with the records the decoder reads and a pseudo-random payload: enough for the
|
||||
/// header path; the entropy data decodes or is refused, never panics.
|
||||
fn synthetic_crw(model: &str) -> Vec<u8> {
|
||||
synthetic_crw_sized(model, W, H)
|
||||
}
|
||||
|
||||
fn synthetic_crw_sized(model: &str, w: usize, h: usize) -> Vec<u8> {
|
||||
let payload: Vec<u8> = (0..514 + 256).map(|i| (i * 41 % 253) as u8).collect();
|
||||
synthetic_crw_with_payload(model, w, h, payload)
|
||||
}
|
||||
|
||||
fn synthetic_crw_with_payload(model: &str, w: usize, h: usize, payload: Vec<u8>) -> Vec<u8> {
|
||||
let le16 = |v: u16| v.to_le_bytes();
|
||||
let le32 = |v: u32| v.to_le_bytes();
|
||||
let heap = |records: &[(u16, Vec<u8>)]| -> Vec<u8> {
|
||||
let mut b = Vec::new();
|
||||
let mut entries = Vec::new();
|
||||
for (tc, v) in records {
|
||||
entries.push((*tc, v.len() as u32, b.len() as u32));
|
||||
b.extend_from_slice(v);
|
||||
if b.len() % 2 == 1 {
|
||||
b.push(0);
|
||||
}
|
||||
}
|
||||
let tbl = b.len() as u32;
|
||||
b.extend_from_slice(&le16(entries.len() as u16));
|
||||
for (tc, l, off) in entries {
|
||||
b.extend_from_slice(&le16(tc));
|
||||
b.extend_from_slice(&le32(l));
|
||||
b.extend_from_slice(&le32(off));
|
||||
}
|
||||
b.extend_from_slice(&le32(tbl));
|
||||
b
|
||||
};
|
||||
let sensor: Vec<u8> = [34u16, w as u16, h as u16, 0, 0, 2, 2, w as u16 - 3, h as u16 - 3].iter().flat_map(|&v| le16(v)).collect();
|
||||
let dtable: Vec<u8> = [0u32, 0, 514, 0].iter().flat_map(|&v| le32(v)).collect();
|
||||
let image_info: Vec<u8> = [w as u32, h as u32, 0, 270, 12, 12, 0].iter().flat_map(|&v| le32(v)).collect();
|
||||
let mut color_balance: Vec<u16> = vec![66];
|
||||
color_balance.extend([2000u16, 800, 800, 960]);
|
||||
color_balance.extend((0..7).flat_map(|_| [1500u16, 900, 900, 1500]));
|
||||
let color_balance: Vec<u8> = color_balance.iter().flat_map(|&v| le16(v)).collect();
|
||||
let mut make_model = b"Canon\0".to_vec();
|
||||
make_model.extend_from_slice(model.as_bytes());
|
||||
make_model.push(0);
|
||||
let props = heap(&[
|
||||
(0x1031, sensor),
|
||||
(0x1835, dtable),
|
||||
(0x080a, make_model),
|
||||
(0x1810, image_info),
|
||||
(0x180e, [1083410638u32, 0, 0].iter().flat_map(|&v| le32(v)).collect()),
|
||||
(0x10a9, color_balance),
|
||||
]);
|
||||
let root = heap(&[(0x2005, payload), (0x300a, props)]);
|
||||
let mut f = b"II".to_vec();
|
||||
f.extend_from_slice(&le32(26));
|
||||
f.extend_from_slice(b"HEAPCCDR");
|
||||
f.extend_from_slice(&le32(0x0001_0002));
|
||||
f.extend_from_slice(&[0; 8]);
|
||||
f.extend_from_slice(&root);
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_mode_maps_the_records_onto_raw_image() {
|
||||
let f = synthetic_crw("Canon EOS D60");
|
||||
let img = decode(&f, Mode::Header).expect("header decodes");
|
||||
assert_eq!((img.format, img.width, img.height, img.bits, img.cpp), (RawFormat::Crw, W, H, 10, 1));
|
||||
assert_eq!(img.data.len(), 0);
|
||||
assert_eq!(img.active_area, Rect::new(2, 2, W - 4, H - 4));
|
||||
assert_eq!(img.crop, Rect::new(0, 0, W - 4, H - 4));
|
||||
assert_eq!(img.cfa.as_ref().map(|c| c.name()).as_deref(), Some("RGGB"));
|
||||
assert_eq!(img.orientation, Orientation::from_exif(8));
|
||||
assert_eq!(img.metadata.make.as_deref(), Some("Canon"));
|
||||
assert_eq!(img.metadata.model.as_deref(), Some("Canon EOS D60"));
|
||||
assert_eq!(img.metadata.orientation, Some(Orientation::from_exif(8)));
|
||||
assert_eq!(img.metadata.capture_time.map(|t| t.unix_seconds()), Some(1083410638));
|
||||
let wb = img.wb_multipliers.expect("ColorBalance slot 0");
|
||||
assert!((wb[0] - 2.5).abs() < 1e-3 && wb[1] == 1.0 && (wb[2] - 1.2).abs() < 1e-3, "{wb:?}");
|
||||
assert_eq!(img.white, vec![1023.0]);
|
||||
assert_eq!(img.black, BlackLevel::uniform(0.0));
|
||||
// the entropy data is pseudo-random: decodes or errors, never panics
|
||||
let _ = decode(&f, Mode::Full);
|
||||
let g1 = decode(&synthetic_crw("Canon PowerShot G1"), Mode::Header).expect("header decodes");
|
||||
assert_eq!(g1.cfa.as_ref().map(|c| c.name()).as_deref(), Some("GBRG"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_map_onto_raw_error() {
|
||||
assert!(matches!(decode(b"II*\0\x08\0\0\0", Mode::Full), Err(RawError::NotRaw)));
|
||||
assert!(matches!(decode(b"II HEAPCCDR ", Mode::Full), Err(RawError::Corrupt(w)) if w.starts_with("CRW: ")));
|
||||
let Err(RawError::Unsupported(why)) = decode(&synthetic_crw("Canon PowerShot G5"), Mode::Header) else { panic!("expected Unsupported") };
|
||||
assert!(why.contains("PowerShot G5"), "{why}");
|
||||
assert_eq!(map_err(craft_crw::Error::LimitExceeded("image too large")), RawError::Limit("image too large"));
|
||||
}
|
||||
|
||||
/// A file declaring more samples than any CRW camera has is refused before anything is
|
||||
/// allocated: 4096 × 4096 (16.8 million) passed when the adapter raised the crate's cap of 2²³
|
||||
/// to the raw crate's 2³⁰.
|
||||
#[test]
|
||||
fn an_oversized_crw_is_refused_by_the_crates_own_limit() {
|
||||
let f = synthetic_crw_sized("Canon EOS D60", 4096, 4096);
|
||||
for mode in [Mode::Header, Mode::Full] {
|
||||
assert_eq!(decode(&f, mode).err(), Some(RawError::Limit("image too large")), "{mode:?}");
|
||||
}
|
||||
// the largest real frame (the 10D's 3152 × 2068) is within it
|
||||
assert!(decode(&synthetic_crw_sized("Canon EOS D60", 3152, 2068), Mode::Header).is_ok());
|
||||
}
|
||||
|
||||
/// A stream cut short is `Corrupt`, not a smeared image: the crate once filled every sample
|
||||
/// after the entropy stream ended with its predictor value and returned success, so a CRW
|
||||
/// truncated just past its header rendered and exported as a flat frame.
|
||||
#[test]
|
||||
fn a_truncated_entropy_stream_is_corrupt() {
|
||||
// the 514-byte header and two bytes of entropy: almost all of the 960-sample frame unread
|
||||
let f = synthetic_crw_with_payload("Canon EOS D60", W, H, vec![0; 516]);
|
||||
let Err(RawError::Corrupt(why)) = decode(&f, Mode::Full) else { panic!("a truncated stream must be refused") };
|
||||
assert!(why.starts_with("CRW: ") && why.contains("before the frame does"), "{why}");
|
||||
// the header alone still reads, so the library can list the file and show its preview
|
||||
assert!(decode(&f, Mode::Header).is_ok());
|
||||
}
|
||||
|
||||
/// A stream that ends a few samples short is `Corrupt` too: the crate's default limit fills up
|
||||
/// to one 64-sample block silently, and no real CRW fills any.
|
||||
#[test]
|
||||
fn a_stream_a_few_samples_short_is_corrupt() {
|
||||
// an all-zero entropy stream of 842 bytes decodes the 960-sample frame exactly
|
||||
let payload = |len: usize| vec![0u8; 514 + len];
|
||||
let whole = synthetic_crw_with_payload("Canon EOS D60", W, H, payload(842));
|
||||
assert!(decode(&whole, Mode::Full).is_ok());
|
||||
// six bytes fewer leave 10 samples unread, within the crate's default of 64
|
||||
let short = synthetic_crw_with_payload("Canon EOS D60", W, H, payload(836));
|
||||
let filled = craft_crw::decode(&short, &craft_crw::Limits::default()).map(|i| i.stream_end_fill);
|
||||
assert_eq!(filled, Ok(10));
|
||||
let Err(RawError::Corrupt(why)) = decode(&short, Mode::Full) else { panic!("a stream 10 samples short must be refused") };
|
||||
assert!(why.contains("10 samples before the frame does"), "{why}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unix_datetime_inverse_matches_unix_seconds() {
|
||||
for t in [0i64, 1, 86399, 86400, 951867295, 1083410638, 1600000000, 4102444799] {
|
||||
let dt = datetime_from_unix(t).expect("in range");
|
||||
assert_eq!(dt.unix_seconds(), t, "{t}");
|
||||
}
|
||||
// Before 1970 or after 2200 is a garbage clock, not a capture time.
|
||||
assert!(datetime_from_unix(-1).is_none());
|
||||
assert!(datetime_from_unix(i64::MAX).is_none());
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,11 @@ fn corpus_root() -> PathBuf {
|
||||
|
||||
/// Variants known not to decode yet (see the crate docs): matched against the lower-case file name.
|
||||
const KNOWN_UNSUPPORTED: &[&str] = &[
|
||||
"cr3-", // Unverified CRX coding variants; exact supported cases live in cr3_corpus.rs.
|
||||
"orf-olympus-em", // Olympus compressed ORF
|
||||
"-he.nef", // Nikon High Efficiency (HE / HE★) NEF, issue #193
|
||||
"sraw", // Canon sRAW / mRAW
|
||||
"cr3-", // Unverified CRX coding variants; exact supported cases live in cr3_corpus.rs.
|
||||
"orf-olympus-em", // Olympus compressed ORF
|
||||
"-he.nef", // Nikon High Efficiency (HE / HE★) NEF, issue #193
|
||||
"sraw", // Canon sRAW / mRAW
|
||||
"crw-canon-powershot-g5.crw", // Canon PowerShot G5 CRW: entropy coding anomaly (craft-libs crates/crw/clean-room/02-crw-format.md §4)
|
||||
];
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//! Canon CRW through the shared craft-crw decoder, on the CC0 corpus files (URLs and SHA-256 in
|
||||
//! `xtask/src/raw_samples.rs`; downloaded into the gitignored corpus/raw by `cargo xtask corpus --download`).
|
||||
//!
|
||||
//! The FNV-1a hashes over the decoded u16 samples (row-major, little-endian, masked borders
|
||||
//! included) were computed from LightCraft's earlier in-tree clean-room CRW decoder (commit
|
||||
//! dd02f12f, branch `clean-room-crw-format-decode`) before it moved to craft-libs: the shared
|
||||
//! decoder reproduces it bit for bit on all ten files, six bodies. Black and white levels are not
|
||||
//! pinned: craft-crw changed both on purpose (border padding no longer lowers the black level; the
|
||||
//! white level follows these bodies' low-ISO clip near 4000), so they are only checked for sanity.
|
||||
|
||||
use lightcraft_raw::{RawData, RawError, decode};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn corpus() -> PathBuf {
|
||||
std::env::var_os("LIGHTCRAFT_CORPUS").map(PathBuf::from).unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"))
|
||||
}
|
||||
|
||||
fn fnv1a(samples: &[u16]) -> u64 {
|
||||
samples.iter().flat_map(|v| v.to_le_bytes()).fold(0xcbf2_9ce4_8422_2325u64, |h, byte| (h ^ u64::from(byte)).wrapping_mul(0x100_0000_01b3))
|
||||
}
|
||||
|
||||
/// (file, sensor width × height, FNV-1a of the samples from the earlier in-tree decoder)
|
||||
const CASES: &[(&str, (usize, usize), u64)] = &[
|
||||
("crw-canon-eos-10d-a.crw", (3152, 2068), 0xb8a6_7781_fa37_b1df),
|
||||
("crw-canon-eos-10d-b.crw", (3152, 2068), 0xeeea_3c68_2fb5_62ae),
|
||||
("crw-canon-eos-10d-c.crw", (3152, 2068), 0x060d_d320_55aa_d79e),
|
||||
("crw-canon-eos-300d-a.crw", (3152, 2068), 0xa9a0_5cc0_ca19_01bd),
|
||||
("crw-canon-eos-300d-b.crw", (3152, 2068), 0xf0c3_98e8_be78_58cc),
|
||||
("crw-canon-eos-d30.crw", (2224, 1456), 0x1394_ca5c_1cb8_816d),
|
||||
("crw-canon-eos-d60.crw", (3152, 2068), 0x63e2_4897_3fe4_1cef),
|
||||
("crw-canon-eos-digital-rebel.crw", (3152, 2068), 0x0db1_b751_a769_aa32),
|
||||
("crw-canon-powershot-g1.crw", (2144, 1560), 0x29e5_4008_46b7_a54b),
|
||||
("crw-canon-powershot-g6.crw", (3160, 2344), 0xa7fa_be95_3036_5b64),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn crw_samples_match_the_earlier_in_tree_decoder() {
|
||||
let mut seen = 0;
|
||||
for &(name, sensor, hash) in CASES {
|
||||
let path = corpus().join("raw").join(name);
|
||||
let Ok(bytes) = std::fs::read(&path) else { continue };
|
||||
let raw = decode(&bytes).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
raw.validate().unwrap();
|
||||
assert_eq!((raw.width, raw.height, raw.cpp), (sensor.0, sensor.1, 1), "{name}: sensor size");
|
||||
let RawData::U16(samples) = &raw.data else { panic!("{name}: integer sensor data expected") };
|
||||
assert_eq!(fnv1a(samples), hash, "{name}: sensor samples differ from the earlier decoder");
|
||||
assert!(raw.white_at(0) > raw.black.mean() && raw.black.mean() > 0.0, "{name}: invalid black/white levels");
|
||||
seen += 1;
|
||||
}
|
||||
if seen == 0 {
|
||||
lightcraft_testkit::skip!(Corpus, "no CRW files in {}", corpus().join("raw").display());
|
||||
}
|
||||
}
|
||||
|
||||
/// A real CRW damaged two ways is an error, never a panic or a silently smeared frame.
|
||||
#[test]
|
||||
fn a_damaged_real_crw_is_an_error() {
|
||||
let path = corpus().join("raw").join("crw-canon-eos-d60.crw");
|
||||
let Ok(bytes) = std::fs::read(&path) else {
|
||||
lightcraft_testkit::skip!(Corpus, "{} absent", path.display());
|
||||
return;
|
||||
};
|
||||
// cut in half: the CIFF heap's record table sits at the end, so the file no longer parses
|
||||
let cut = &bytes[..bytes.len() / 2];
|
||||
assert!(matches!(decode(cut), Err(RawError::Corrupt(_) | RawError::NotRaw)), "{:?}", decode(cut).err());
|
||||
// the length kept but a run of the sensor data overwritten with an invalid code
|
||||
let mut garbled = bytes.clone();
|
||||
let mid = garbled.len() / 2;
|
||||
garbled[mid..mid + 4096].fill(0xff);
|
||||
assert!(matches!(decode(&garbled), Err(RawError::Corrupt(_))), "{:?}", decode(&garbled).err());
|
||||
}
|
||||
@@ -7,6 +7,7 @@ mod corpus;
|
||||
mod coverage_sweep_corpus;
|
||||
mod cr3_corpus;
|
||||
mod cr3_variants;
|
||||
mod crw_corpus;
|
||||
mod crx_properties;
|
||||
mod dng;
|
||||
mod lossy_dng;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Malformed vendor raw files (NEF, ARW, PEF, ORF, RW2 of every raw format, CR2, RAF) must decode to an error, never panic.
|
||||
//! Malformed vendor raw files (NEF, ARW, PEF, ORF, RW2 of every raw format, CR2, RAF, CRW) must decode to an error, never panic.
|
||||
|
||||
use lightcraft_tiff::tags as t;
|
||||
use lightcraft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter, Value};
|
||||
@@ -226,8 +226,48 @@ fn raf() -> Vec<u8> {
|
||||
raf_file(W, H, 16, strip, Some(layout), b"\xff\xd8\xff\xd9")
|
||||
}
|
||||
|
||||
/// A Canon CRW: CIFF header, a raw-data payload of pseudo-random bytes, and a nested properties
|
||||
/// heap with the records the decoder reads.
|
||||
fn crw() -> Vec<u8> {
|
||||
let le16 = |v: u16| v.to_le_bytes();
|
||||
let le32 = |v: u32| v.to_le_bytes();
|
||||
let heap = |records: &[(u16, Vec<u8>)]| -> Vec<u8> {
|
||||
let mut b = Vec::new();
|
||||
let mut entries = Vec::new();
|
||||
for (tc, v) in records {
|
||||
entries.push((*tc, v.len() as u32, b.len() as u32));
|
||||
b.extend_from_slice(v);
|
||||
if b.len() % 2 == 1 {
|
||||
b.push(0);
|
||||
}
|
||||
}
|
||||
let tbl = b.len() as u32;
|
||||
b.extend_from_slice(&le16(entries.len() as u16));
|
||||
for (tc, l, off) in entries {
|
||||
b.extend_from_slice(&le16(tc));
|
||||
b.extend_from_slice(&le32(l));
|
||||
b.extend_from_slice(&le32(off));
|
||||
}
|
||||
b.extend_from_slice(&le32(tbl));
|
||||
b
|
||||
};
|
||||
let sensor: Vec<u8> = [34u16, W as u16, H as u16, 0, 0, 2, 2, W as u16 - 3, H as u16 - 3].iter().flat_map(|&v| le16(v)).collect();
|
||||
let dtable: Vec<u8> = [0u32, 0, 514, 0].iter().flat_map(|&v| le32(v)).collect();
|
||||
let props = heap(&[(0x1031, sensor), (0x1835, dtable), (0x080a, b"Canon\0Canon EOS D60\0".to_vec())]);
|
||||
let payload: Vec<u8> = (0..514 + 256).map(|i| (i * 41 % 253) as u8).collect();
|
||||
let root = heap(&[(0x2005, payload), (0x300a, props)]);
|
||||
let mut f = b"II".to_vec();
|
||||
f.extend_from_slice(&le32(26));
|
||||
f.extend_from_slice(b"HEAPCCDR");
|
||||
f.extend_from_slice(&le32(0x0001_0002));
|
||||
f.extend_from_slice(&[0; 8]);
|
||||
f.extend_from_slice(&root);
|
||||
f
|
||||
}
|
||||
|
||||
fn samples() -> Vec<Vec<u8>> {
|
||||
vec![
|
||||
crw(),
|
||||
cfa_tiff("NIKON CORPORATION", 1, 12, ByteOrder::Big),
|
||||
cfa_tiff("NIKON CORPORATION", 34713, 14, ByteOrder::Big),
|
||||
cfa_tiff("SONY", 32767, 12, ByteOrder::Little),
|
||||
@@ -283,7 +323,7 @@ proptest! {
|
||||
#![proptest_config(ProptestConfig { cases: 2000, .. ProptestConfig::default() })]
|
||||
|
||||
#[test]
|
||||
fn mutated_vendor_files_never_panic(kind in 0usize..18, flips in proptest::collection::vec((any::<usize>(), any::<u8>()), 1..16), cut in any::<usize>()) {
|
||||
fn mutated_vendor_files_never_panic(kind in 0usize..19, flips in proptest::collection::vec((any::<usize>(), any::<u8>()), 1..16), cut in any::<usize>()) {
|
||||
let mut data = samples().swap_remove(kind);
|
||||
let n = data.len();
|
||||
for (i, v) in flips {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3/CR2, Pentax PEF, Samsung SRW, Olympus ORF, Epson ERF and Minolta MRW starting look
|
||||
# Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3/CR2/CRW, Pentax PEF, Samsung SRW, Olympus ORF, Epson ERF and Minolta MRW starting look
|
||||
|
||||
ARW, NEF, RW2, RAF, CR3, CR2, PEF, SRW, ORF, ERF and MRW decoding supply sensor data and camera white-balance multipliers, but currently no measured camera colour matrix. Treating camera RGB as linear sRGB produces a dark, muted default rendering. The loader now estimates a file-local starting look from that file's embedded camera JPEG, only when a usable camera matrix is absent. All output pixels still come from the RAW sensor data; there is no JPEG replacement or uniform saturation boost.
|
||||
ARW, NEF, RW2, RAF, CR3, CR2, CRW, PEF, SRW, ORF, ERF and MRW decoding supply sensor data and camera white-balance multipliers (CRW only on the D60/10D/300D; on the other CRW bodies the fitted matrix absorbs the white balance), but currently no measured camera colour matrix — with one exception: the Canon EOS D60, whose matrices we measured on a ColorChecker (`crates/raw/src/chart_table.rs`). They give a D60 CRW its colour when its JPEG look cannot be fitted, and its white balance in Kelvin (below); an accepted fit still wins. Treating camera RGB as linear sRGB produces a dark, muted default rendering. The loader now estimates a file-local starting look from that file's embedded camera JPEG, only when a usable camera matrix is absent. All output pixels still come from the RAW sensor data; there is no JPEG replacement or uniform saturation boost.
|
||||
|
||||
## Colour and tone are separate
|
||||
|
||||
@@ -48,7 +48,7 @@ Previously `SourceRef::load()` discarded the decoder's `SourceInfo`, and render
|
||||
|
||||
Vendor RGB WB multipliers alone do **not** identify an absolute illuminant without camera calibration. The generic model inferred approximately 6829 K / −127 tint for one Sony sample; changing only temperature retained that spurious tint and caused a pink cast. Uncalibrated ARWs, NEFs, RW2s and RAFs keep the file's vendor WB at decode and expose **relative** WB controls around the as-shot look. 6500/0 is the internal neutral reference of that relative scale, not a claim about the capture's measured Kelvin temperature. Presets, picker/auto and manual controls use this reference consistently. Switching from As Shot to Custom first resolves both controls; changing one cannot inherit stale catalog tint. Reset restores the current as-shot reference. Old As Shot numbers in a catalog do not mark an otherwise unchanged photo as edited; Custom WB remains an edit.
|
||||
|
||||
**Kelvin through the camera's measured matrices (issue #730).** A raw of a camera in `lightcraft_raw::spectral` (52 bodies: ILCE-7M3, ILCE-7M4, ILCE-7RM3, D850, EOS 5D Mark III…) edits white balance in Kelvin like a DNG, whichever look it got: the matrices read its as-shot multipliers as a temperature and tint (`spectral::as_shot_white_through`, the DNG `AsShotNeutral` reading with the model's `ColorMatrix1/2`) and the Temp slider runs 2000–50000 K with As Shot at that reading. The matrices do nothing else: the colour look stays the file's JPEG fit (or the profile, or the spectral fallback), and the picture is adapted from the as-shot white to the chosen one with Bradford, as every fitted look is — not re-developed in camera space as a DNG is. That keeps one adaptation model per camera whether or not a photo's fit was accepted, and it is what makes the upgrade exact: white balance written on the relative scale before this (no `scale` in the settings' `wb`, `WbScale::Legacy`) renders as the composition of the two Bradford adaptations, which is again one Bradford adaptation (`lightcraft_pipeline::local::legacy_white`), so existing edits are pixel-identical; they are converted to Custom Kelvin values the first time they are edited (`normalize_wb`). The decision is one function on both sides — `lightcraft_catalog::relative_wb_camera(format, camera)` from the catalog's camera string and `files::WbScaleOf` from the file — and a test keeps them in step. A photo imported before this keeps the relative scale's 6500 / 0 as its recorded as-shot white until its file is next decoded for display or reloaded (`Op::SetAsShotWb`, bookkeeping); only the readout waits, not the look. Raws of cameras without measured matrices stay relative, unless their own white-balance presets give the scale (next).
|
||||
**Kelvin through the camera's measured matrices (issue #730).** A raw of a camera in `lightcraft_raw::spectral` (52 bodies with spectral matrices: ILCE-7M3, ILCE-7M4, ILCE-7RM3, D850, EOS 5D Mark III…, and the Canon EOS D60, whose matrices we measured on a ColorChecker under tungsten and sun, `chart_table.rs`, `clean-room/crw/10-chart-calibration.md`) edits white balance in Kelvin like a DNG, whichever look it got: the matrices read its as-shot multipliers as a temperature and tint (`spectral::as_shot_white_through`, the DNG `AsShotNeutral` reading with the model's `ColorMatrix1/2`) and the Temp slider runs 2000–50000 K with As Shot at that reading. The matrices do nothing else: the colour look stays the file's JPEG fit (or the profile, or the spectral fallback), and the picture is adapted from the as-shot white to the chosen one with Bradford, as every fitted look is — not re-developed in camera space as a DNG is. That keeps one adaptation model per camera whether or not a photo's fit was accepted, and it is what makes the upgrade exact: white balance written on the relative scale before this (no `scale` in the settings' `wb`, `WbScale::Legacy`) renders as the composition of the two Bradford adaptations, which is again one Bradford adaptation (`lightcraft_pipeline::local::legacy_white`), so existing edits are pixel-identical; they are converted to Custom Kelvin values the first time they are edited (`normalize_wb`). The decision is one function on both sides — `lightcraft_catalog::relative_wb_camera(format, camera)` from the catalog's camera string and `files::WbScaleOf` from the file — and a test keeps them in step. A photo imported before this keeps the relative scale's 6500 / 0 as its recorded as-shot white until its file is next decoded for display or reloaded (`Op::SetAsShotWb`, bookkeeping); only the readout waits, not the look. Raws of cameras without measured matrices stay relative, unless their own white-balance presets give the scale (next).
|
||||
|
||||
**Kelvin through the camera's white-balance presets.** Sony ARWs store, in the encrypted `SR2SubIFD`, the multipliers of every white-balance preset, five of them at a stated temperature (2500, 3200, 4500, 6000 and 8500 K). For a camera without measured matrices whose files carry them (`lightcraft_raw::kelvin_presets`: Sony ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S) they take the matrices' place in everything above: the as-shot multipliers are placed on the curve through the five presets (mired linear along it, in log red/green and blue/green), an offset from the curve is the tint, and the decision, the Bradford adaptation, the pixel-identical upgrade and `Op::SetAsShotWb` are the same. The scale is Sony's own: files shot on the Kelvin setting read back what the dial said (ILCA-77M2 3400 K → 3396 K and 5200 K → 5193 K, SLT-A33 4700 K → 4708 K, the public ILCE-3500 sample's 5600 K → 5594 K; with the colour filter one step towards magenta, the SLT-A33's 3400 K reads 3473 K / +11), and the Shade preset reads 7493 K on the public ILCE-7S sample. Off the curve, two constants come from the 14 Sony bodies that have both measured matrices and presets: the tint is the offset scaled by the locus's own spacing (the matrices give 0.90–1.04 of it), and the isotherms lean against the curve's normal (`ISOTHERM_TILT` 0.74, measured 0.59–0.84). On 150 ILCE-7SM2 photos read both ways, the presets' temperature tracks the matrices' to ±4 mired around Sony's 8 % higher scale, and the tint to −9 ± 4. A listed camera whose file has no presets keeps the Kelvin scale with As Shot at D65, as a covered camera without multipliers does. Bodies with measured matrices keep reading through them.
|
||||
|
||||
|
||||
+6
-4
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+24
-18
@@ -1,6 +1,6 @@
|
||||
# RAW parity: cameras, formats, demosaic, colour and lens profiles
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (Sony ILCE-6700 in-file distortion; Minolta MRW decoded, 6 bodies verified; lossy JPEG XL DNG tiles with a non-linear declared transfer curve decode; Canon ColorData Kelvin; X-Trans CMOS 4 bodies take the X-T3's spectral matrices; AHD no longer turns flat noise into a maze pattern) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (Canon CRW decoder shared via craft-libs `craft-crw`; Canon EOS D60: chart-measured colour matrices, white balance in Kelvin; Canon CRW merged with MRW: 557 verified; CRW variant rows split by verification status; Sony ILCE-6700 in-file distortion; Minolta MRW decoded, 6 bodies verified; lossy JPEG XL DNG tiles with a non-linear declared transfer curve decode; Canon ColorData Kelvin; X-Trans CMOS 4 bodies take the X-T3's spectral matrices; AHD no longer turns flat noise into a maze pattern) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
|
||||
|
||||
Raw processing is the deepest single dimension of Lightroom parity: a photographer whose camera
|
||||
does not decode, or decodes with the wrong colour, cannot switch. This document measures it model by
|
||||
@@ -15,14 +15,14 @@ ranked in [gaps.md](gaps.md). Details per format: [cr3.md](cr3.md),
|
||||
| Measure | Value | Kind |
|
||||
|---|---:|---|
|
||||
| Adobe-listed camera models | **1,446** | measured: [raw/adobe-cameras.tsv](raw/adobe-cameras.tsv) |
|
||||
| Decoded from sensor data and **verified** on a real file | **551** (38%) | measured: [raw/evidence.tsv](raw/evidence.tsv) |
|
||||
| Decoder path should handle it, **unverified** | **656** (45%) | measured from the format rules (631 of them DNG, mostly phones) |
|
||||
| **Preview-only** (embedded JPEG) | **201** (14%) | measured |
|
||||
| **Unsupported** (not imported, refused or decodes wrongly) | **38** (3%) | measured |
|
||||
| Decoded from sensor data and **verified** on a real file | **557** (39%) | measured: [raw/evidence.tsv](raw/evidence.tsv) |
|
||||
| Decoder path should handle it, **unverified** | **665** (46%) | measured from the format rules (631 of them DNG, mostly phones) |
|
||||
| **Preview-only** (embedded JPEG) | **187** (13%) | measured |
|
||||
| **Unsupported** (not imported, refused or decodes wrongly) | **37** (3%) | measured |
|
||||
| Models added since 2017 (Camera Raw 10+): verified / unverified / preview / unsupported | 182 / 542 / 22 / 8 of 754 | measured |
|
||||
| **Share of photographers whose camera fully works** (decodes from sensor data), use-weighted | **~94%** | estimated: measured classes × written usage weights × per-format confidence |
|
||||
| …of which on a body we have verified | **~85%** | estimated, same weights |
|
||||
| Camera colour: Adobe profiles vs ours | 1,500 Adobe Standard profiles, 481 models with Camera Matching profiles vs **9 bundled profiles** + per-file fits + 52 spectral matrices (plus same-sensor aliases) | measured (bundle listing, Adobe list, `assets/camera-profiles`) |
|
||||
| …of which on a body we have verified | **~86%** | estimated, same weights |
|
||||
| Camera colour: Adobe profiles vs ours | 1,500 Adobe Standard profiles, 481 models with Camera Matching profiles vs **9 bundled profiles** + per-file fits + 52 spectral matrices (plus same-sensor aliases) + chart-measured matrices (1 model, the EOS D60) | measured (bundle listing, Adobe list, `assets/camera-profiles`) |
|
||||
| Lens profiles: Adobe vs ours | **3,635 vs 0** (in-file corrections only) | measured |
|
||||
| **RAW readiness** (decoding 60%, colour 25%, lens 15%) | **~60%** (range 54–64%) | estimated, see [Scores](#scores) |
|
||||
|
||||
@@ -86,7 +86,7 @@ unsupported to preview-only in the counts; sensor-decoding readiness is unchange
|
||||
| Maker | Adobe models | verified | unverified | preview | unsupported | Main reason for preview / unsupported |
|
||||
|---|---:|---:|---:|---:|---:|---|
|
||||
| Sony | 149 | 94 | 43 | 10 | 2 | DSLR-A200…A390 and A850 12-bit compressed ARW (preview, #535); A100 remains classified unsupported in the recorded evidence; DSC-F828 SRF imports as preview-only; DSC-V3 preview not verified |
|
||||
| Canon | 134 | 95 | 16 | 21 | 2 | CRW (2000–2004 bodies) preview; EOS-1D / 1Ds TIFF raws not imported |
|
||||
| Canon | 134 | 101 | 25 | 6 | 2 | PowerShot G5 CRW anomaly and pre-CIFF compacts preview; EOS-1D / 1Ds TIFF raws not imported |
|
||||
| Samsung | 128 | 10 | 109 | 9 | 0 | compressed SRW (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini, Galaxy NX) |
|
||||
| Apple | 113 | 4 | 109 | 0 | 0 | — (DNG / ProRAW) |
|
||||
| Panasonic | 104 | 93 | 11 | 0 | 0 | — |
|
||||
@@ -103,13 +103,13 @@ unsupported to preview-only in the counts; sensor-decoding readiness is unchange
|
||||
| DJI | 21 | 4 | 17 | 0 | 0 | DNG |
|
||||
| OM Digital Solutions | 7 | 0 | 0 | 7 | 0 | **compressed ORF** |
|
||||
| Kodak, Konica Minolta, Epson, Sigma, GoPro, Contax, others | 56 | 12 | 16 | 13 | 15 | DCR, GPR not imported; KDC, X3F preview (Epson ERF and Minolta MRW decode) |
|
||||
| **All** | **1,446** | **551 (38%)** | **656 (45%)** | **201 (14%)** | **38 (3%)** | |
|
||||
| **All** | **1,446** | **557 (39%)** | **665 (46%)** | **187 (13%)** | **37 (3%)** | |
|
||||
|
||||
## By use (estimated)
|
||||
|
||||
| Bucket | Weight | Camera fully works | On a verified body | Why |
|
||||
|---|---:|---:|---:|---|
|
||||
| Canon | 33% | 95% | 93% | 18 of the 22 popular bodies write CR3; 26 CR3 bodies verified (raw.pixls.us has a CC0 file for each), the rest at 0.6 |
|
||||
| Canon | 33% | 98% | 94% | 18 of the 22 popular bodies write CR3; 26 CR3 bodies verified (raw.pixls.us has a CC0 file for each), the rest at 0.6; CRW decoded |
|
||||
| Sony | 27% | 98% | 92% | bulk-checked |
|
||||
| Nikon | 16% | 98% | 89% | decoders cover every NEF coding but HE; 77 bodies verified |
|
||||
| Fujifilm | 10% | 94% | 92% | |
|
||||
@@ -120,9 +120,9 @@ unsupported to preview-only in the counts; sensor-decoding readiness is unchange
|
||||
| Leica | 1% | 99% | 72% | the Q3 verified, 13 more bodies pinned on CC0 files |
|
||||
| DJI | 1% | 96% | 19% | |
|
||||
| Medium format and others | 0.5% | 29% | 8% | IIQ, 3FR, X3F |
|
||||
| **Weighted** | 100% | **94%** | **85%** | |
|
||||
| **Weighted** | 100% | **94%** | **86%** | |
|
||||
|
||||
About 19 in 20 raw photographers' cameras decode, and about 5 in 6 use a body we have actually
|
||||
About 19 in 20 raw photographers' cameras decode, and about 6 in 7 use a body we have actually
|
||||
checked (the per-model CC0 sweeps of October 2026); compressed ORF (OM / Olympus) is the largest gap left.
|
||||
|
||||
## Variant matrix (depth per format)
|
||||
@@ -141,7 +141,9 @@ checked (the per-model CC0 sweeps of October 2026); compressed ORF (OM / Olympus
|
||||
| CR3 | sRAW / mRAW | ⬜ | 4-plane Bayer only |
|
||||
| CR3 | HEIF / HDR PQ | ⬜ | HEVC tracks recognised, not decoded |
|
||||
| CR3 | dual-pixel raw | ⬜ | never substituted |
|
||||
| CRW | all | ⬜ | preview |
|
||||
| CRW | CIFF: EOS D30/D60/10D/300D, PowerShot G1/G6 | ✅ | clean-room derivation frozen in `craft-libs crates/crw/clean-room/02-crw-format.md`; 28 of 29 files pixel-exact vs a black-box reference (8 CC0 EOS, G1, G6, 18 private D60); as-shot WB from ColorBalance; EOS D60 colour matrices measured on a ColorChecker (Kelvin white balance) |
|
||||
| CRW | CIFF: PowerShot G2/G3/G4, Pro1, S-series | 🟡 | same decoder and tables; no sample file checked |
|
||||
| CRW | CIFF: PowerShot G5 | ⬜ | refused (entropy anomaly, `craft-libs crates/crw/clean-room/02-crw-format.md`); preview |
|
||||
| NEF | uncompressed, lossless, lossy type 1 / 2, lossy after split | ✅ | D5100, D7000, D7500, Z f corpus; ~60 NEFs by hand; #633; D4, D90, D5000, D600 and D610 image area measured against the camera JPEG (white level too for the D4, D90 and D5000) |
|
||||
| NEF | packed 14-bit stored under a compressed tag (Z 6II uncompressed) | ⬜ | #769; fix in PR #671 (not merged) |
|
||||
| NEF | High Efficiency / HE★ (JPEG XS) | ⬜ | named, preview (#193); needs a JPEG XS decoder |
|
||||
@@ -214,11 +216,11 @@ sample files. A pointer for contributors, not legal advice.
|
||||
|
||||
| Item | Adobe | LightCraft |
|
||||
|---|---|---|
|
||||
| Default profile per model | Adobe Standard for every model (1,500 profile names in the bundle) | the DNG's own matrices; else a pooled profile (**9 bundled**: ILCA-99M2, ILCE-7CR, ILCE-7M4, ILCE-7RM5, X-H2S, X-T4, Nikon Z 6II, Z 8 and D5300); else a guarded per-file fit to the camera's embedded JPEG; else spectral matrices (**52 models**, rawtoaces-data, and the bodies sharing their sensors: 6 Fujifilm bodies take the X-T3's (X-T30, X-T30 II, X-Pro3, X100V, X-E4, X-M5) and 2 take the X-T4's (X-S10, X-S20)); else a neutral matrix |
|
||||
| Default profile per model | Adobe Standard for every model (1,500 profile names in the bundle) | the DNG's own matrices; else a pooled profile (**10 bundled**: EOS D60, ILCA-99M2, ILCE-7CR, ILCE-7M4, ILCE-7RM5, X-H2S, X-T4, Nikon Z 6II, Z 8 and D5300); else a guarded per-file fit to the camera's embedded JPEG; else spectral matrices (**52 models**, rawtoaces-data, and the bodies sharing their sensors: 6 Fujifilm bodies take the X-T3's (X-T30, X-T30 II, X-Pro3, X100V, X-E4, X-M5) and 2 take the X-T4's (X-S10, X-S20)) or chart-measured ones (**1 model**, the EOS D60); else a neutral matrix |
|
||||
| Camera Matching looks | 481 models (plus phone-maker looks) | none (the per-file JPEG fit imitates the camera's own look) |
|
||||
| Creative profiles | Adobe Color, Portrait, Landscape, Vivid, Monochrome… | our own looks (Color, Neutral, Vivid, Landscape, Portrait, Monochrome) |
|
||||
| DCP / dual-illuminant model | ✅ | dual-illuminant DNG colour (ColorMatrix / ForwardMatrix 1-2, CameraCalibration, AnalogBalance), HueSatMap, LookTable, ProfileToneCurve; external `.dcp` never loaded (clean-room) |
|
||||
| White balance in Kelvin | ✅ for every model | absolute Kelvin where the camera has colour matrices (its own, or one of the 52 spectral rows), also for files whose look is a per-file fit (#730), or where the camera's white-balance presets in the file give the scale (6 Sony bodies and the Canon PowerShot G1 X Mark III, `kelvin_presets`), or the neutral pair a Fujifilm X-Trans III RAF stores (6 bodies, #730); other raws keep the relative scale (#296) |
|
||||
| White balance in Kelvin | ✅ for every model | absolute Kelvin where the camera has colour matrices (its own, one of the 52 spectral rows or the chart-measured EOS D60), also for files whose look is a per-file fit (#730), or where the camera's white-balance presets in the file give the scale (6 Sony bodies and the Canon PowerShot G1 X Mark III, `kelvin_presets`), or the neutral pair a Fujifilm X-Trans III RAF stores (6 bodies, #730); other raws keep the relative scale (#296) |
|
||||
|
||||
**How far the default rendering is from Adobe's** (CIE ΔE; measured where stated):
|
||||
|
||||
@@ -280,9 +282,8 @@ works, from [raw/usage-weights.tsv](raw/usage-weights.tsv).
|
||||
| 5 | Sony DSLR-A200…A390 / A850 12-bit compressed ARW | +0.4 | 3–6 | ~0.08 | samples on raw.pixls.us |
|
||||
| 6 | Pre-X FinePix RAF | +0.6 | 4–8 | ~0.08 | samples |
|
||||
| 7 | Sony A100 regression (#702) and DSC-F828 sensor decoding (preview-only) | +0.1 | 1–2 | ~0.07 | samples on raw.pixls.us |
|
||||
| 8 | Canon CRW (2000–2004 bodies) | +1.2 by the uniform tail weight, far less by real use | 6–12 | ≤ 0.1 | samples |
|
||||
| 9 | Nikon HE / HE★ NEF (Z 8, Z 9, Z 6III, Z f, Z50II, Z5II set to HE) | +1.1 | 20–40 | ~0.04 | a pure-Rust JPEG XS decoder |
|
||||
| 10 | Compressed SRW, Hasselblad 3FR, IIQ, X3F, KDC, DCR, GPR (ERF, MEF, MRW done) | +0.6 together | 45–85 | ~0.01 | samples; low use |
|
||||
| 8 | Nikon HE / HE★ NEF (Z 8, Z 9, Z 6III, Z f, Z50II, Z5II set to HE) | +1.1 | 20–40 | ~0.04 | a pure-Rust JPEG XS decoder |
|
||||
| 9 | Compressed SRW, Hasselblad 3FR, IIQ, X3F, KDC, DCR, GPR (ERF, MEF, MRW done) | +0.6 together | 45–85 | ~0.01 | samples; low use |
|
||||
|
||||
Beyond decoding:
|
||||
|
||||
@@ -299,6 +300,8 @@ Beyond decoding:
|
||||
|---|---|---|
|
||||
| 2026-10-26 | patch | Bundled camera profile count 5 → 9 (Nikon Z 6II, Z 8 and D5300, ILCE-7RM5); counts only |
|
||||
| 2026-10-23 | minor | Lens profiles: Sony ILCE-6700 (APS-C) in-file distortion through the ILCE-7RM6 conversion, knots spanning its own crop; lens score unchanged (~10%: one more body) |
|
||||
| 2026-10-25 | patch | CRW variant rows split by verification status (EOS and G1/G6 verified; G2/G3/G4/Pro1/S-series unverified; G5 preview); the CIFF heap walk is bounded by a per-file record budget, so a block of sub-heap records pointing back at their parent no longer hangs decode |
|
||||
| 2026-10-13 | minor | Canon CRW decoded (clean-room derivation, craft-libs `crates/crw/clean-room/`): EOS D30/D60/10D/300D, PowerShot G1/G6 verified pixel-exact (28 of 29 corpus files); G5 preview (entropy anomaly); as-shot WB from ColorBalance 0x10a9; 243 verified, 950 unverified, 217 preview; Canon bucket 76% (21% verified) |
|
||||
| 2026-10-18 | patch | X-T30, X-T30 II, X-Pro3, X100V, X-S10 and X-E4 take the X-T3's spectral matrices (same 26.1 MP X-Trans CMOS 4 sensor): an X-T30 raw with a black-and-white (Acros) camera JPEG no longer opens in the muted neutral fallback |
|
||||
| 2026-10-16 | minor | Kelvin white balance from Canon CR2 `ColorData` presets (PowerShot G1 X Mark III) |
|
||||
| 2026-10-15 | minor | DNGs with a look table and no tone curve (Lightroom-written, Adobe Standard) fit their tone to the embedded preview, also Lightroom's 1024 px one: two lossy ILCE-7CM2 DNGs no longer render 0.8 EV dark and flat |
|
||||
@@ -330,6 +333,9 @@ Beyond decoding:
|
||||
| 2026-10-14 | minor | 97 Canon CR2, Pentax and Ricoh models verified (one CC0 raw.pixls.us file each, pinned, checked against the camera JPEG; `canon_pentax_corpus.rs`): 334 verified, 844 unverified, 232 preview; 50% of photographers on a verified body; use-weighted 85% unchanged |
|
||||
| 2026-10-14 | minor | Integration batch 36: CC0 per-model verification for 117 Nikon/Fujifilm, 97 Canon CR2/Pentax/Ricoh, 21 Sony/Leica and 23 Olympus/Samsung bodies, 22 more CR3 bodies and 30 more models; Epson ERF and Mamiya MEF decode; measured Nikon image areas (#1053, #1770): 515 verified, 675 unverified, 218 preview, 38 unsupported (re-measured with `coverage.py`) |
|
||||
| 2026-10-15 | minor | By-maker and by-use tables re-measured with `coverage.py` after batches 36–39 (545 verified, 651 unverified, 212 preview, 38 unsupported; ~94% of photographers' cameras fully work, ~85% on a verified body); rows the per-model PRs had updated are current again |
|
||||
| 2026-10-15 | minor | Canon CRW merged with batches 36–39 and re-measured with `coverage.py`: 551 verified, 660 unverified, 197 preview, 38 unsupported; Canon 98% (94% on verified bodies); use-weighted 94% and 86% |
|
||||
| 2026-10-25 | minor | Canon EOS D60: dual-illuminant matrices fitted to our own ColorChecker Classic shots (tungsten, sun; `crates/raw/src/chart_table.rs`), the first chart-calibrated model: its CRWs edit white balance in Kelvin; counts unchanged |
|
||||
| 2026-10-18 | minor | Minolta MRW decoded (`vendor/mrw.rs`): DiMAGE 7, 7Hi, A1, A2, A200 and Dynax / Maxxum 7D verified; 551 verified, 656 unverified, 201 preview (re-measured with `coverage.py`) |
|
||||
| 2026-10-23 | minor | AHD blends its horizontal and vertical candidates where their homogeneity is close, so flat noise no longer becomes a maze pattern; Bayer demosaic row notes the pre-white-balance demosaic (#1328); scores unchanged |
|
||||
| 2026-10-30 | patch | RAF camera look fitted on the vignetting-corrected sensor (`LOOK_VERSION` 13): renders were up to 0.3 EV too bright in midtones and 0.6 EV in shadows; X-S10 mean ΔE00 vs the camera JPEG 2.71 → 2.55 on 60 photos |
|
||||
| 2026-10-26 | minor | Canon CRW decoding moved to the shared craft-libs crate `craft-crw` (local path dependency until that branch merges); `vendor/crw.rs` is now the adapter onto `RawImage`, and the clean-room derivation record lives in craft-libs `crates/crw/clean-room/`; counts unchanged |
|
||||
|
||||
+12
-1
@@ -32,6 +32,10 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ORF_DECODED = re.compile(r"^(E-1|E-10|E-20|EVOLT E-300|EVOLT E-330|EVOLT E-400|EVOLT E-500|E-300|E-330|E-500|Camedia C-\S.*|Stylus XZ-2 iHS)$")
|
||||
# Samsung SRW bodies with compressed raw data (crates/raw/src/vendor/srw.rs refuses 32770 / 32772 / 32773).
|
||||
SRW_COMPRESSED = re.compile(r"(^|\W)(NX1|NX30|NX300|NX300M|NX500|NX2000|NX3000|NX3300|NX mini|EK-GN\d+|EK-KN\d+)(\W|$)")
|
||||
|
||||
# CIFF-container CRW bodies (crates/raw/src/vendor/crw.rs): every CRW EOS and the CIFF PowerShots
|
||||
# (the G and S series and the Pro 1).
|
||||
CRW_DECODED = re.compile(r"^EOS |^PowerShot (G[1-6]$|Pro ?1$|S[34567]0$|S45$)")
|
||||
# Fujifilm bodies before the X series whose RAF has no raw IFD (crates/raw/src/vendor/raf.rs).
|
||||
RAF_OLD = re.compile(r"^(FinePix (E|F|S\d|S\d{3,4}|IS)|IS Pro|S5Pro|S3Pro|S2Pro)", re.I)
|
||||
|
||||
@@ -89,9 +93,16 @@ def classify(maker, camera, ext_field):
|
||||
return "unverified", "MEF", "MEF decoder (uncompressed 12-bit)"
|
||||
if e == "ERF":
|
||||
return "unverified", "ERF", "ERF decoder (12-bit 16-byte blocks)"
|
||||
if e == "CRW":
|
||||
# CIFF-era bodies decode (craft-libs crates/crw/clean-room/02-crw-format.md): EOS D30/D60/10D/300D and
|
||||
# the CIFF PowerShots; the pre-CIFF compacts (PowerShot 600, A5, A50, Pro70) and the
|
||||
# unprobed Pro90 IS stay preview. The G5 is preview by evidence (entropy anomaly).
|
||||
if CRW_DECODED.search(camera):
|
||||
return "unverified", "CRW", "CRW decoder (CIFF; selector 0-2 tables)"
|
||||
return "preview", "CRW", "pre-CIFF CRW is not decoded"
|
||||
if e == "MRW":
|
||||
return "unverified", "MRW", "MRW decoder (12-bit packed, 16-bit words)"
|
||||
if e in ("CRW", "3FR", "FFF", "IIQ", "MOS", "KDC", "X3F", "RAW", "ORI"):
|
||||
if e in ("3FR", "FFF", "IIQ", "MOS", "KDC", "X3F", "RAW", "ORI"):
|
||||
return "preview", e, f"{e} is opened from its embedded JPEG only"
|
||||
return "unsupported", e, f"{e or 'unknown'} is not imported"
|
||||
|
||||
|
||||
+21
-21
@@ -135,7 +135,7 @@ Canon EOS 6D CR2 2012 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_
|
||||
Canon EOS 6D Mark II CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon EOS 7D CR2 2008 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES)
|
||||
Canon EOS 7D Mark II CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon EOS 10D CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon EOS 10D CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES; 3 files)
|
||||
Canon EOS 20D CR2 2003 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon EOS 20Da CR2 2005 unverified CR2 lossless JPEG decoder
|
||||
Canon EOS 30D CR2 2005 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
@@ -149,9 +149,9 @@ Canon EOS 80D CR2 2015 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW
|
||||
Canon EOS 90D CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md)
|
||||
Canon EOS 200D (EOS Kiss X9, EOS Rebel SL2) CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon EOS 250D (EOS Rebel SL3, EOS 200D II, EOS Kiss X10) CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md)
|
||||
Canon EOS D30 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon EOS D60 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon EOS 300D (EOS Digital Rebel, EOS Kiss Digital) CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon EOS D30 CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES)
|
||||
Canon EOS D60 CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs; 18 maintainer files exact (craft-libs crates/crw/clean-room/03-verification.md)
|
||||
Canon EOS 300D (EOS Digital Rebel, EOS Kiss Digital) CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES; 3 files)
|
||||
Canon EOS 100D (EOS Digital Rebel SL1, EOS Kiss X7) CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon EOS 500D (EOS Digital Rebel T1i, EOS Kiss X3 Digital) CR2 2008 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon EOS 550D (EOS Digital Rebel T2i, EOS Kiss X4 Digital) CR2 2010 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES)
|
||||
@@ -202,20 +202,20 @@ Canon EOS R8 CR3 2022 verified corpus: crates/raw/tests/it/cr3_corpus.rs (exact
|
||||
Canon EOS R50 CR3 2022 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md)
|
||||
Canon EOS R50 V CR3 2024 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md)
|
||||
Canon EOS R100 CR3 2022 verified corpus: crates/raw/tests/it/cr3_corpus.rs (exact sensor hashes, docs/cr3.md)
|
||||
Canon PowerShot 600 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot A5 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot A50 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot G1 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot 600 CRW 2003 preview pre-CIFF CRW is not decoded
|
||||
Canon PowerShot A5 CRW 2003 preview pre-CIFF CRW is not decoded
|
||||
Canon PowerShot A50 CRW 2003 preview pre-CIFF CRW is not decoded
|
||||
Canon PowerShot G1 CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES)
|
||||
Canon PowerShot G1 X CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot G1 X Mark II CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot G1 X Mark III CR2 2017 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot G2 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot G3 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot G2 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables)
|
||||
Canon PowerShot G3 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables)
|
||||
Canon PowerShot G3 X CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot G5 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot G5 CRW 2003 preview CRW entropy anomaly (craft-libs crates/crw/clean-room/02-crw-format.md section 4); embedded JPEG
|
||||
Canon PowerShot G5 X CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot G5 X Mark II CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md)
|
||||
Canon PowerShot G6 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot G6 CRW 2003 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES)
|
||||
Canon PowerShot G7 X CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot G7 X Mark II CR2 2015 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot G7 X Mark III CR3 2018 verified corpus: crates/raw/tests/it/cr3_corpus.rs (pinned sensor hash, camera-JPEG check, docs/cr3.md)
|
||||
@@ -227,17 +227,17 @@ Canon PowerShot G11 CR2 2008 verified corpus: crates/raw/tests/it/body_corpus.rs
|
||||
Canon PowerShot G12 CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot G15 CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot G16 CR2 2013 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot Pro 1 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot Pro70 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot Pro90 IS CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot S30 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot S40 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot S45 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot S50 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot S60 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot Pro 1 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables)
|
||||
Canon PowerShot Pro70 CRW 2003 preview pre-CIFF CRW is not decoded
|
||||
Canon PowerShot Pro90 IS CRW 2003 preview pre-CIFF CRW is not decoded
|
||||
Canon PowerShot S30 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables)
|
||||
Canon PowerShot S40 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables)
|
||||
Canon PowerShot S45 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables)
|
||||
Canon PowerShot S50 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables)
|
||||
Canon PowerShot S60 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables)
|
||||
Canon PowerShot S90 CR2 2008 unverified CR2 lossless JPEG decoder
|
||||
Canon PowerShot S95 CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot S70 CRW 2003 preview CRW is opened from its embedded JPEG only
|
||||
Canon PowerShot S70 CRW 2003 unverified CRW decoder (CIFF; selector 0-2 tables)
|
||||
Canon PowerShot S100 CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot S100V CR2 2010 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
Canon PowerShot S110 CR2 2012 verified corpus: crates/raw/tests/it/body_corpus.rs (pinned sensor hash, camera-JPEG check)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 86 and column 19.
|
@@ -569,3 +569,10 @@ Konica Minolta DiMAGE A200 verified corpus: crates/raw/tests/it/corpus.rs (xtask
|
||||
Konica Minolta Maxxum 7D Dynax 7D verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES)
|
||||
Konica Minolta DiMAGE 7 verified raw.pixls.us check: CC0 sample decoded and its WB / CFA matched to the embedded preview (vendor/mrw.rs)
|
||||
Konica Minolta DiMAGE A2 verified raw.pixls.us check: CC0 sample decoded and its WB / CFA matched to the embedded preview (vendor/mrw.rs)
|
||||
Canon EOS D30 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES)
|
||||
Canon EOS D60 verified corpus: crates/raw/tests/it/corpus.rs; 18 maintainer files exact (craft-libs crates/crw/clean-room/03-verification.md)
|
||||
Canon EOS 10D verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES; 3 files)
|
||||
Canon EOS 300D (EOS Digital Rebel, EOS Kiss Digital) verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES; 3 files)
|
||||
Canon PowerShot G1 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES)
|
||||
Canon PowerShot G6 verified corpus: crates/raw/tests/it/corpus.rs (xtask RAW_SAMPLES)
|
||||
Canon PowerShot G5 preview CRW entropy anomaly (craft-libs crates/crw/clean-room/02-crw-format.md section 4); embedded JPEG
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 3.
|
@@ -1,6 +1,6 @@
|
||||
# Target-app parity: LightCraft vs Adobe Lightroom
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (gap 9: CI runs the tests on Linux x64 since #2667; macOS, Windows and the GPU equivalence tests still don't; Minolta MRW decoded, 6 bodies verified; RAW: 83% of Adobe's models decode, 38% verified after the camera-coverage sweep #1327; Platforms: the Windows x64 installer verified on Windows 11) · **Target:** Adobe Lightroom Classic 15.5 (August 2026) and Lightroom 9.6 (desktop)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (Canon CRW decoder shared via craft-libs `craft-crw`; Canon CRW decoded clean-room, 6 bodies verified; gap 9: CI runs the tests on Linux x64 since #2667; macOS, Windows and the GPU equivalence tests still don't; Minolta MRW decoded, 6 bodies verified; RAW: 85% of Adobe's models decode, 39% verified after the camera-coverage sweep #1327; Platforms: the Windows x64 installer verified on Windows 11) · **Target:** Adobe Lightroom Classic 15.5 (August 2026) and Lightroom 9.6 (desktop)
|
||||
|
||||
The authoritative assessment of how close LightCraft is to Lightroom. The row-by-row checklist is
|
||||
[parity-checklist.md](parity-checklist.md); the ranked work list is [gaps.md](gaps.md); deep areas
|
||||
@@ -47,7 +47,7 @@ phase #85, duplicate Local entries #22, black GPU exports on an Intel iGPU #78).
|
||||
| Dimension | Breadth | Ready for real work | Remaining | Doc |
|
||||
|---|---:|---:|---:|---|
|
||||
| Features (weighted areas below) | ~77% (checklist 81.1%, measured) | ~61% | 600–1,100 h | [Features](#features) |
|
||||
| ↳ RAW cameras, colour and lens profiles (part of features) | 83% of Adobe's 1,446 models decode (38% verified), measured | ~56% | 255–480 h | [raw-parity.md](raw-parity.md) |
|
||||
| ↳ RAW cameras, colour and lens profiles (part of features) | 85% of Adobe's 1,446 models decode (39% verified), measured | ~56% | 255–480 h | [raw-parity.md](raw-parity.md) |
|
||||
| UI/UX fidelity | ~85% | ~70% | 32–54 h | [ui-parity.md](ui-parity.md) |
|
||||
| File formats (non-raw: images, sidecars, presets, catalogs) | ~85% | ~70% | 30–55 h | [file-format-parity.md](file-format-parity.md) |
|
||||
| Hardware | ~50% | ~40% | 40–80 h (+ tethering and printing, counted under Classic modules) | [hardware-parity.md](hardware-parity.md) |
|
||||
@@ -68,7 +68,7 @@ from the checklist sections and the deep-dive documents.
|
||||
|---|---:|---:|---:|---:|---|
|
||||
| Import and library (import, folders, albums, smart albums, keywords, metadata, search, faces) | 15% | 90% | 80% | 25–45 h | checklist A–E 83–92%; robust journal; 85k-photo libraries |
|
||||
| RAW decoding and camera coverage | 15% | 81% | 77% | 120–220 h | [raw-parity.md](raw-parity.md), measured model by model: 85% of photographers' cameras decode (use-weighted), × 0.9 for per-body bugs; CR3 variants, compressed ORF, HE NEF |
|
||||
| Colour, camera profiles, render fidelity | 10% | 65% | 35% | 80–150 h | 9 bundled profiles vs Adobe's 1,500; per-file fits median ΔE ~5 vs the camera JPEG, ~10% far off; no fidelity suite ([raw-parity.md](raw-parity.md#colour)) |
|
||||
| Colour, camera profiles, render fidelity | 10% | 65% | 35% | 80–150 h | 10 bundled profiles vs Adobe's 1,500; per-file fits median ΔE ~5 vs the camera JPEG, ~10% far off; no fidelity suite ([raw-parity.md](raw-parity.md#colour)) |
|
||||
| Global develop adjustments (light, colour, curves, grading, effects, detail) | 15% | 93% | 70% | 30–60 h | checklist F 91.7%; character tuned by eye |
|
||||
| Optics, lens profiles, crop and geometry | 6% | 80% | 50% | 40–80 h | no lens database; crop / Upright complete |
|
||||
| Masking | 8% | 67% | 45% | 60–110 h | checklist K 67.4%; AI masks heuristic or SAM 3 without mirror |
|
||||
@@ -301,6 +301,7 @@ P2 items 12–25 h; look tuning, performance, packaging, hardening 25–45 h.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-24 | patch | Stability, the stability discount, the Beta-needs list and the estimate history: tests run in CI on Linux x64 since #2667 (FreeBSD nightly), not on macOS or Windows (gap 9); percentages and hours unchanged |
|
||||
| 2026-10-13 | minor | Canon CRW decoded (clean-room, craft-libs `crates/crw/clean-room/`): 551 models verified with the coverage sweep (557 with MRW after merging main), Canon preview rows 21 → 6; RAW decoding unchanged |
|
||||
| 2026-10-19 | patch | Stability note: tests run in CI on Linux x64 (every PR) as well as FreeBSD; macOS and Windows remain (gap 9) |
|
||||
| 2026-10-19 | minor | Localization ~55% → ~64% (mean of the 12 key languages): Indonesian (`id`) catalog, every message but What's New; full target 61.2% → 61.5% (still ~61%) |
|
||||
| 2026-10-19 | patch | Platforms: the Windows installer is no longer listed as unverified (the 0.9.1 x64 MSI is tested on Windows 11; a confirmation after uninstalling from Settings → Apps, #892, remains); #790 noted as fixed in 0.9.1; percentages and hours unchanged |
|
||||
@@ -320,3 +321,4 @@ P2 items 12–25 h; look tuning, performance, packaging, hardening 25–45 h.
|
||||
| 2026-10-10 | minor | Stage re-checked against the core-workflow alpha gate: passes, stays alpha |
|
||||
| 2026-10-10 | major | Created as progress docs: full re-measure (checklist recount, installed Lightroom 9.6 bundle listing, code counts, catalogs, CI), feature areas with weights, dimensions, AI, stage, calibration; merged ROADMAP's *Where we stand* and *Parity estimate* |
|
||||
| 2026-10-14 | minor | RAW: 82% of Adobe's models decode, 36% verified (integration batch 36) |
|
||||
| 2026-10-26 | minor | Canon CRW decoder moved to craft-libs as `craft-crw` (shared with the other Craft apps); LightCraft keeps the adapter, the EOS D60 colour work and its records; numbers unchanged |
|
||||
|
||||
@@ -129,6 +129,7 @@ def main(root, out):
|
||||
print(f"{f}: training dE76 A {e1:.3f}, D65 {e2:.3f}", file=sys.stderr)
|
||||
rows.append(
|
||||
f' Camera {{\n make: "{rust_lit(header["manufacturer"])}",\n model: "{rust_lit(header["model"])}",\n'
|
||||
" illuminant: [17, 21],\n"
|
||||
f" color_matrix: [{mat(cm1)}, {mat(cm2)}],\n forward_matrix: [{mat(fm1)}, {mat(fm2)}],\n }},"
|
||||
)
|
||||
aliases = json.load(open(os.path.join(root, "data/aliases.json")))["data"]["camera"]["model"]
|
||||
|
||||
@@ -926,6 +926,63 @@ pub(crate) const RAW_SAMPLES: &[(&str, &str, &str)] = &[
|
||||
"https://raw.pixls.us/getfile.php/4204/nice/Leica%20-%20D-Lux%207%20-%204:3.RWL",
|
||||
"4f6db6492da83f7f2a5012591e370557249d1ed44d4c948784016605b799327c",
|
||||
),
|
||||
// Canon CRW (CIFF): the clean-room decode's verification corpus (craft-libs crates/crw/clean-room/03-verification.md);
|
||||
// the PowerShot G5 is the documented anomaly the decoder refuses (embedded JPEG preview)
|
||||
(
|
||||
"crw-canon-eos-d30.crw",
|
||||
"https://raw.pixls.us/getfile.php/1307/nice/Canon%20-%20EOS%20D30%20-%20RAW%20(3:2).CRW",
|
||||
"4ee482f2b6f9f1a8d370703ea04e9f2abaa49356973a7f56b229af93a78e8a2e",
|
||||
),
|
||||
(
|
||||
"crw-canon-eos-d60.crw",
|
||||
"https://raw.pixls.us/getfile.php/2010/nice/Canon%20-%20EOS%20D60%20-%20RAW%20(3:2).CRW",
|
||||
"2b98659ecff170af1dfe44f89b493139e75e995b2979686f6ff0847cfb56b97f",
|
||||
),
|
||||
(
|
||||
"crw-canon-eos-10d-a.crw",
|
||||
"https://raw.pixls.us/getfile.php/2027/nice/Canon%20-%20EOS%2010D%20-%20RAW%20(3:2).CRW",
|
||||
"b0730eb3a67cc2281a32e0b2ee104199246928d954a68b7e5b72366514c73d02",
|
||||
),
|
||||
(
|
||||
"crw-canon-eos-10d-b.crw",
|
||||
"https://raw.pixls.us/getfile.php/3164/nice/Canon%20-%20EOS%2010D%20-%20RAW%20(3:2).crw",
|
||||
"e879c94afdc64dabc88b01ce2d02628aa225259303ec71dd61f7fb0322bfea2d",
|
||||
),
|
||||
(
|
||||
"crw-canon-eos-10d-c.crw",
|
||||
"https://raw.pixls.us/getfile.php/3165/nice/Canon%20-%20EOS%2010D%20-%20RAW%20(3:2).crw",
|
||||
"58144118854657df5cd2185df40fecf6b665fc6754fef0416c2f222b68b025c6",
|
||||
),
|
||||
(
|
||||
"crw-canon-eos-300d-a.crw",
|
||||
"https://raw.pixls.us/getfile.php/1983/nice/Canon%20-%20EOS%20300D%20-%20RAW%20(3:2).crw",
|
||||
"47da16e1668af5500eea63cf42cf2992d9cb3d32180b8d786f17f1e2a57f39f4",
|
||||
),
|
||||
(
|
||||
"crw-canon-eos-300d-b.crw",
|
||||
"https://raw.pixls.us/getfile.php/3162/nice/Canon%20-%20EOS%20300D%20-%20RAW%20(3:2).CRW",
|
||||
"9e7f62239fe728046af28d0d4a8a722d35d40146a79794a5bda1775306af4383",
|
||||
),
|
||||
(
|
||||
"crw-canon-eos-digital-rebel.crw",
|
||||
"https://raw.pixls.us/getfile.php/1308/nice/Canon%20-%20EOS%20Digital%20Rebel%20-%20RAW%20(3:2).CRW",
|
||||
"d6a3131bfac390fde01b96882ec378211f04570c3252708b58f0d05b33a9fdfc",
|
||||
),
|
||||
(
|
||||
"crw-canon-powershot-g1.crw",
|
||||
"https://raw.pixls.us/getfile.php/2073/nice/Canon%20-%20PowerShot%20G1%20-%20RAW%20(4:3).crw",
|
||||
"f39be5acf14057ce04527fd8d0574e57ec8ed5fed082d7bfaff763075a44ee8a",
|
||||
),
|
||||
(
|
||||
"crw-canon-powershot-g5.crw",
|
||||
"https://raw.pixls.us/getfile.php/1746/nice/Canon%20-%20PowerShot%20G5%20-%20RAW%20(4:3).CRW",
|
||||
"bb084223d714871b99de4f549eaf00513e30a7080feeead700252c2008060196",
|
||||
),
|
||||
(
|
||||
"crw-canon-powershot-g6.crw",
|
||||
"https://raw.pixls.us/getfile.php/4226/nice/Canon%20-%20PowerShot%20G6%20-%20RAW%20(4:3).CRW",
|
||||
"244378e1f5660b6cc6cefc93a75ed4928d0b28aa6304ff327b61c748538f4fef",
|
||||
),
|
||||
(
|
||||
"mrw-minolta-dimage7hi-words16.mrw",
|
||||
"https://raw.pixls.us/data/Minolta/DiMAGE%207Hi/RAW_MINOLTA_DIMAGE_7HI.MRW",
|
||||
|
||||
Reference in New Issue
Block a user