mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 11:17:05 -04:00
M11.4: raws from 52 camera models start from matrices fitted to measured spectral sensitivities
A raw without colour matrices of its own now gets DNG ColorMatrix1/2 and ForwardMatrix1/2 for its model when the model is one of the 52 cameras in rawtoaces-data (Academy Software Foundation, Apache-2.0): 22 Canon, 11 Nikon, 11 Sony, 3 Fujifilm and 5 others, plus the other names its alias list gives (Rebel T3i and Kiss X5 for the EOS 600D; bodies it lists as sharing a sensor). tools/spectral_camera_table.py fits, under Standard Illuminant A and D65, a white-preserving 3x3 to the measured spectral sensitivities over rawtoaces-data's 190 training reflectances and writes crates/raw/src/spectral_table.rs. No number comes from Adobe or from a raw decoder. lightcraft_raw::spectral looks a camera up by Exif make and model; the DNG colour model does the rest. camera_preview::starting_colour takes the first available of: the file's own matrices, a camera profile for the model (fitted to the JPEG for tone), the spectral matrices, the file-local JPEG fit, the neutral fallback. A profiled model whose profile fit fails takes its spectral matrices. White balance stays relative to the as-shot look for every raw without matrices of its own, so Photo::relative_wb and the controls are unchanged; calibrate still pools covered models. Tests: one per precedence step (synthetic), the table's lookup, aliases and DNG shape, and a corpus test of the order on ILCE-7M4 (profile), EOS 5D Mark III and D5100 (spectral), EOS 7D (own fit) and a DNG. The ILCE-7RM2 and GFX100S corpus tests now expect the spectral matrices. Attribution in assets/ATTRIBUTION.md and NOTICE. RENDER_CACHE_VERSION 23. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
88ea1a804b
commit
82ff68d4f3
14 files changed
+879
-27
No files matched your search
@@ -15,6 +15,13 @@ list (author, source, license) is in assets/ATTRIBUTION.md.
|
|||||||
Inter (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt.
|
Inter (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt.
|
||||||
Showcase photographs by Ansel Adams, Dorothea Lange and NASA (docs/images/) — public domain. See assets/ATTRIBUTION.md.
|
Showcase photographs by Ansel Adams, Dorothea Lange and NASA (docs/images/) — public domain. See assets/ATTRIBUTION.md.
|
||||||
|
|
||||||
|
crates/raw/src/spectral_table.rs (lightcraft-raw) holds camera colour matrices derived from the
|
||||||
|
camera spectral sensitivities, training reflectances and model alias list of rawtoaces-data
|
||||||
|
(https://github.com/AcademySoftwareFoundation/rawtoaces-data, v1.1.0), an Academy Software
|
||||||
|
Foundation project licensed under the Apache License 2.0, and from the CIE 1931 colour-matching
|
||||||
|
functions it ships (CIE, CC-BY-4.0). The matrices were fitted by the LightCraft contributors
|
||||||
|
(2026) with tools/spectral_camera_table.py. See assets/ATTRIBUTION.md.
|
||||||
|
|
||||||
crates/segment (lightcraft-segment) ports the SAM 3 model code of Hugging Face Transformers
|
crates/segment (lightcraft-segment) ports the SAM 3 model code of Hugging Face Transformers
|
||||||
(https://github.com/huggingface/transformers, src/transformers/models/sam3 and sam3_tracker),
|
(https://github.com/huggingface/transformers, src/transformers/models/sam3 and sam3_tracker),
|
||||||
Copyright The HuggingFace Team and Meta Platforms, Inc., licensed under the Apache License 2.0.
|
Copyright The HuggingFace Team and Meta Platforms, Inc., licensed under the Apache License 2.0.
|
||||||
|
|||||||
+4
-3
@@ -24,7 +24,7 @@ duplicate Local entries #22, black GPU exports on an Intel iGPU #78).
|
|||||||
|---|---:|---|---|
|
|---|---:|---|---|
|
||||||
| **Feature checklist** | 79.7% | P0 core and P1 nearly complete: import (Add / Copy / Move, templates, devices), library, grid/loupe/compare/survey, every Edit slider, curves, colour grading, masking tools, crop/Upright, heal/clone, presets/profiles, versions/history, sync, export, menus, shortcuts | P0: full-resolution zoom detail; P1: lens-profile database, content-aware fill (patch synthesis), video playback/trim |
|
| **Feature checklist** | 79.7% | P0 core and P1 nearly complete: import (Add / Copy / Move, templates, devices), library, grid/loupe/compare/survey, every Edit slider, curves, colour grading, masking tools, crop/Upright, heal/clone, presets/profiles, versions/history, sync, export, menus, shortcuts | P0: full-resolution zoom detail; P1: lens-profile database, content-aware fill (patch synthesis), video playback/trim |
|
||||||
| **RAW coverage** (formats people shoot) | ~55% | DNG (all kinds), CR2, CR3 lossless Bayer and version 0x100/0x200 C-RAW (M50/R100/R8 pixel-exact corpus), ARW, NEF (uncompressed + Huffman lossless/lossy), RAF (uncompressed, lossless/lossy compressed Bayer and X-Trans), uncompressed ORF, uncompressed Samsung SRW (NX5 to NX1100, EX1, WB2000), RW2 / RWL / Panasonic RAW (every raw format, checked on 178 files from 118 Panasonic and Leica bodies), PEF; every container's embedded preview (incl. CR3) | **CR3** (every Canon since ~2018), compressed ORF, compressed Samsung SRW (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini), Nikon lossy-after-split, Canon sRAW, HEIC/AVIF. NEFs labelled compressed but stored uncompressed (Z 6 packed 14-bit, D850 12-bit uncompressed). Per-model verification is thin (~55 corpus files vs >1,000 models) |
|
| **RAW coverage** (formats people shoot) | ~55% | DNG (all kinds), CR2, CR3 lossless Bayer and version 0x100/0x200 C-RAW (M50/R100/R8 pixel-exact corpus), ARW, NEF (uncompressed + Huffman lossless/lossy), RAF (uncompressed, lossless/lossy compressed Bayer and X-Trans), uncompressed ORF, uncompressed Samsung SRW (NX5 to NX1100, EX1, WB2000), RW2 / RWL / Panasonic RAW (every raw format, checked on 178 files from 118 Panasonic and Leica bodies), PEF; every container's embedded preview (incl. CR3) | **CR3** (every Canon since ~2018), compressed ORF, compressed Samsung SRW (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini), Nikon lossy-after-split, Canon sRAW, HEIC/AVIF. NEFs labelled compressed but stored uncompressed (Z 6 packed 14-bit, D850 12-bit uncompressed). Per-model verification is thin (~55 corpus files vs >1,000 models) |
|
||||||
| **Colour & image quality** | ~55–65% | Pipeline is complete and fast; GPU path CPU-exact within 1/255 | **No measured camera calibration database**: ARW, NEF, RW2, RAF, CR3, CR2 and PEF have guarded camera-JPEG colour estimates, with bundled ILCE-7M4, X-H2S and X-T4 profiles (docs/camera-preview-colour.md); other non-DNG raws and rejected estimates use a neutral matrix. Colour fidelity remains incomplete. No lens-profile database. No measured fidelity against Lightroom (tone, highlights, texture/clarity, NR, sharpening are tuned by eye) |
|
| **Colour & image quality** | ~55–65% | Pipeline is complete and fast; GPU path CPU-exact within 1/255 | **No measured camera calibration database**: ARW, NEF, RW2, RAF, CR3, CR2 and PEF have guarded camera-JPEG colour estimates, with bundled ILCE-7M4, X-H2S and X-T4 profiles (docs/camera-preview-colour.md); 52 models start from matrices fitted to measured spectral sensitivities; other non-DNG raws and rejected estimates use a neutral matrix. Colour fidelity remains incomplete. No lens-profile database. No measured fidelity against Lightroom (tone, highlights, texture/clarity, NR, sharpening are tuned by eye) |
|
||||||
| **AI & computational** | ~15–20% | Assisted culling (focus, bursts), auto tone, HDR/panorama merge; subject/sky/background masks as classical heuristics, or the photo's own segmentation mattes when it ships them (DNG semantic masks, e.g. iPhone ProRAW's sky matte); non-destructive Bayer RAW denoise with opt-in weights on pure-Rust CPU/GPU; **faces (MVP, [docs/faces.md](docs/faces.md))**: a pure-Rust detector (a 232 KB opt-in download), opt-in recognition models (one-click download or add a file; they run on the shared pure-Rust CPU interpreter; pinned SFace/AuraFace embeddings agree with an independent reference runtime on 40 public crops; one-thread medians 34–36 / 233–252 ms (AuraFace remains expensive)), a background scan that finds and embeds faces, name suggestions, a person's page with look-alike faces to confirm, an Unnamed faces section (look-alikes together, select a group and name it at once) | Real segmentation masks (subject, sky, people, objects, landscape, depth), denoise model policy / linear RGB / quality verification, super resolution, lens blur, generative remove, natural-language search; for faces: clusters with separators between groups, small faces in large photos (the detector looks at 640 px), thresholds calibrated on live faces, writing face regions back to XMP. **Blocked on a model strategy** (licensable weights or our own training; pure-Rust inference is feasible) |
|
| **AI & computational** | ~15–20% | Assisted culling (focus, bursts), auto tone, HDR/panorama merge; subject/sky/background masks as classical heuristics, or the photo's own segmentation mattes when it ships them (DNG semantic masks, e.g. iPhone ProRAW's sky matte); non-destructive Bayer RAW denoise with opt-in weights on pure-Rust CPU/GPU; **faces (MVP, [docs/faces.md](docs/faces.md))**: a pure-Rust detector (a 232 KB opt-in download), opt-in recognition models (one-click download or add a file; they run on the shared pure-Rust CPU interpreter; pinned SFace/AuraFace embeddings agree with an independent reference runtime on 40 public crops; one-thread medians 34–36 / 233–252 ms (AuraFace remains expensive)), a background scan that finds and embeds faces, name suggestions, a person's page with look-alike faces to confirm, an Unnamed faces section (look-alikes together, select a group and name it at once) | Real segmentation masks (subject, sky, people, objects, landscape, depth), denoise model policy / linear RGB / quality verification, super resolution, lens blur, generative remove, natural-language search; for faces: clusters with separators between groups, small faces in large photos (the detector looks at 640 px), thresholds calibrated on live faces, writing face regions back to XMP. **Blocked on a model strategy** (licensable weights or our own training; pure-Rust inference is feasible) |
|
||||||
| **Workflow & library** | ~85% (single machine) | Robust catalog (journal + snapshots, background compaction, crash-tested), 85k-photo libraries stay responsive, Local browsing with automatic cleanup, XMP interop, keywords, smart albums, Move import | Opening an 85k library takes 1.7–4.7 s; no cloud sync (out of scope), no tablet companion (#74, roadmap), shared albums, publish services, tethering |
|
| **Workflow & library** | ~85% (single machine) | Robust catalog (journal + snapshots, background compaction, crash-tested), 85k-photo libraries stay responsive, Local browsing with automatic cleanup, XMP interop, keywords, smart albums, Move import | Opening an 85k library takes 1.7–4.7 s; no cloud sync (out of scope), no tablet companion (#74, roadmap), shared albums, publish services, tethering |
|
||||||
| **Classic modules** | ~30% | Geotagging from GPX track logs, soft proofing (partial), slideshow (basic) | **Map view, Book, Print, Slideshow module, Web, publish services**: ~40 tracker rows ⬜ |
|
| **Classic modules** | ~30% | Geotagging from GPX track logs, soft proofing (partial), slideshow (basic) | **Map view, Book, Print, Slideshow module, Web, publish services**: ~40 tracker rows ⬜ |
|
||||||
@@ -49,7 +49,8 @@ Priorities, in order. Each points at tracker rows in [`docs/parity.md`](docs/par
|
|||||||
1. **Camera colour calibration of our own** (LR-PROF-CAMERACOLOR, P0): fit each camera to its own embedded JPEG, use
|
1. **Camera colour calibration of our own** (LR-PROF-CAMERACOLOR, P0): fit each camera to its own embedded JPEG, use
|
||||||
matrices the files carry themselves, then chart shots. Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3/CR2,
|
matrices the files carry themselves, then chart shots. Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3/CR2,
|
||||||
Pentax PEF and Samsung SRW have the file-local fit (matrix + tone curve from their own JPEG; CR2 and PEF since issue #310, not yet
|
Pentax PEF and Samsung SRW have the file-local fit (matrix + tone curve from their own JPEG; CR2 and PEF since issue #310, not yet
|
||||||
checked on the corpus); then ORF, then validate fidelity.
|
checked on the corpus); 52 models start from matrices fitted to measured spectral sensitivities (rawtoaces-data);
|
||||||
|
then ORF, then validate fidelity.
|
||||||
2. **Raw formats, clean-room** (LR-IMP-FORMATS, P0): Remaining **unverified CR3 CRX** variants, compressed ORF, NEF
|
2. **Raw formats, clean-room** (LR-IMP-FORMATS, P0): Remaining **unverified CR3 CRX** variants, compressed ORF, NEF
|
||||||
lossy-after-split, sRAW. Decided 2026-10-05: write our own decoders from prose descriptions (never decoder source,
|
lossy-after-split, sRAW. Decided 2026-10-05: write our own decoders from prose descriptions (never decoder source,
|
||||||
no LGPL dependency); compressed NEF (#86) is the template.
|
no LGPL dependency); compressed NEF (#86) is the template.
|
||||||
@@ -188,7 +189,7 @@ Not decoded yet — preview only (no permissively licensed description; black-bo
|
|||||||
alignment or reset predictors at the split row). The other Nikon Huffman variants are decoded (`crates/raw/src/vendor/nefc.rs` documents the analysis).
|
alignment or reset predictors at the split row). The other Nikon Huffman variants are decoded (`crates/raw/src/vendor/nefc.rs` documents the analysis).
|
||||||
- **Olympus compressed ORF**, **Canon CR3 unverified CRX variants** (M11.1), **Canon sRAW/mRAW**.
|
- **Olympus compressed ORF**, **Canon CR3 unverified CRX variants** (M11.1), **Canon sRAW/mRAW**.
|
||||||
|
|
||||||
**Camera colour matrices:** ARW, NEF, RW2, RAF, CR3, CR2, PEF and Samsung SRW files can use guarded, separate chromaticity and tone estimates from their own embedded JPEG (see `docs/camera-preview-colour.md`); this is a per-file camera-look estimate with relative WB, not measured calibration or absolute-Kelvin WB. Other non-DNG raws and rejected fits use the documented neutral fallback (camera RGB ≈ linear sRGB, flagged
|
**Camera colour matrices:** ARW, NEF, RW2, RAF, CR3, CR2, PEF and Samsung SRW files can use guarded, separate chromaticity and tone estimates from their own embedded JPEG (see `docs/camera-preview-colour.md`); this is a per-file camera-look estimate with relative WB, not measured calibration or absolute-Kelvin WB. 52 models without a camera profile start from matrices fitted to their measured spectral sensitivities instead (rawtoaces-data, Apache-2.0; `crates/raw/src/spectral.rs`). Other non-DNG raws and rejected fits use the documented neutral fallback (camera RGB ≈ linear sRGB, flagged
|
||||||
`matrix_is_fallback`) with the file's as-shot white-balance multipliers. Clean sources to evaluate next: manufacturer
|
`matrix_is_fallback`) with the file's as-shot white-balance multipliers. Clean sources to evaluate next: manufacturer
|
||||||
matrices stored in the files themselves (Olympus ImageProcessing `ColorMatrix`, Pentax equivalents; Panasonic RW2 files carry none) and our
|
matrices stored in the files themselves (Olympus ImageProcessing `ColorMatrix`, Pentax equivalents; Panasonic RW2 files carry none) and our
|
||||||
own chart-based calibration (M11.4). Adobe matrices are never used.
|
own chart-based calibration (M11.4). Adobe matrices are never used.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ covers a whole directory), or if a licence file referenced in the Licence column
|
|||||||
| `assets/camera-profiles/*.json` | Camera colour profiles (matrix + hue/saturation/value table), built into LightCraft | ILCE-7M4: Harald Hoyer | original work: fitted by `lightcraft-cli calibrate` to the contributor's own raw photos and their in-camera JPEGs (597 ILCE-7M4 photos, 2026); aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-07 | none |
|
| `assets/camera-profiles/*.json` | Camera colour profiles (matrix + hue/saturation/value table), built into LightCraft | ILCE-7M4: Harald Hoyer | original work: fitted by `lightcraft-cli calibrate` to the contributor's own raw photos and their in-camera JPEGs (597 ILCE-7M4 photos, 2026); aggregate colour statistics only, no image content; no Adobe or third-party profile data | MIT OR Apache-2.0 | 2026-10-07 | none |
|
||||||
| `packaging/macos/dmg/` (`background.svg`, the `background.tiff` rendered from it by `generate.py`, and the `dmg-layout.DS_Store` layout file) | LightCraft macOS DMG window background | @XusBadia | original work for this repository, derived from the LightCraft app icon (`assets/app-icon/lightcraft.svg`, referenced via `<image>`, not copied); text is outlined Inter / JetBrains Mono glyph paths | MIT OR Apache-2.0 | 2026-10-08 | none (original); used for the Finder window of the macOS DMG, `packaging/macos/dmg/generate.py` renders it to `background.tiff` |
|
| `packaging/macos/dmg/` (`background.svg`, the `background.tiff` rendered from it by `generate.py`, and the `dmg-layout.DS_Store` layout file) | LightCraft macOS DMG window background | @XusBadia | original work for this repository, derived from the LightCraft app icon (`assets/app-icon/lightcraft.svg`, referenced via `<image>`, not copied); text is outlined Inter / JetBrains Mono glyph paths | MIT OR Apache-2.0 | 2026-10-08 | none (original); used for the Finder window of the macOS DMG, `packaging/macos/dmg/generate.py` renders it to `background.tiff` |
|
||||||
| `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/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 |
|
||||||
|
| `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/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/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 |
|
| `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 |
|
||||||
| `docs/images/*-tetons.jpg`, `docs/images/grid-pd.jpg` (screenshots containing it) | "The Tetons and the Snake River" | Ansel Adams (1942), U.S. National Archives | https://commons.wikimedia.org/wiki/File:Adams_The_Tetons_and_the_Snake_River.jpg | Public domain (U.S. federal government work) | 2026-09-30 | developed in LightCraft; shown inside app screenshots |
|
| `docs/images/*-tetons.jpg`, `docs/images/grid-pd.jpg` (screenshots containing it) | "The Tetons and the Snake River" | Ansel Adams (1942), U.S. National Archives | https://commons.wikimedia.org/wiki/File:Adams_The_Tetons_and_the_Snake_River.jpg | Public domain (U.S. federal government work) | 2026-09-30 | developed in LightCraft; shown inside app screenshots |
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
//! A global matrix can't follow the camera's hue-dependent rendering (the best matrix rendered a
|
//! A global matrix can't follow the camera's hue-dependent rendering (the best matrix rendered a
|
||||||
//! lime shirt olive that the camera kept lime): a hue/saturation table fitted to the residuals
|
//! lime shirt olive that the camera kept lime): a hue/saturation table fitted to the residuals
|
||||||
//! (applied like a DNG `ProfileHueSatMap`) corrects that when it also improves the held-out pixels.
|
//! (applied like a DNG `ProfileHueSatMap`) corrects that when it also improves the held-out pixels.
|
||||||
use lightcraft_color::{D50, D65, Mat3, PROPHOTO, REC2020, bradford, luminance_2020};
|
use crate::camera_profiles::CameraProfile;
|
||||||
|
use lightcraft_color::{D50, D65, Mat3, PROPHOTO, REC2020, Xy, bradford, luminance_2020};
|
||||||
use lightcraft_pipeline::tone::{CameraTone, ToneMap};
|
use lightcraft_pipeline::tone::{CameraTone, ToneMap};
|
||||||
use lightcraft_raster::{
|
use lightcraft_raster::{
|
||||||
Rgb32f,
|
Rgb32f,
|
||||||
@@ -45,16 +46,52 @@ pub(crate) fn file_local_look(format: RawFormat) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fit_preview(raw: &RawImage, bytes: &[u8], transform: &CameraTransform) -> Option<CameraLook> {
|
/// The colour a raw starts from, first available first: the file's own colour matrices (DNG); the camera profile for
|
||||||
if !transform.matrix_is_fallback || !file_local_look(raw.format) {
|
/// its model (bundled or local, `camera_profiles`), with tone and chroma fitted to the file's JPEG; the model's colour
|
||||||
|
/// matrices from measured spectral sensitivities (`lightcraft_raw::spectral`); a fit to the file's own JPEG; the
|
||||||
|
/// neutral fallback. The spectral matrices are written into `raw.color`. Returns the as-shot white, the camera
|
||||||
|
/// transform at it and the fitted look.
|
||||||
|
pub(crate) fn starting_colour(raw: &mut RawImage, bytes: &[u8]) -> (Xy, CameraTransform, Option<CameraLook>) {
|
||||||
|
let start = |raw: &RawImage| {
|
||||||
|
let xy = lightcraft_raw::color::as_shot_white_xy(raw);
|
||||||
|
(xy, lightcraft_raw::color::camera_transform(raw, xy))
|
||||||
|
};
|
||||||
|
let (xy, t) = start(raw);
|
||||||
|
let profile = raw.metadata.model.as_deref().and_then(crate::camera_profiles::get);
|
||||||
|
let spectral = match raw.metadata.model.as_deref() {
|
||||||
|
Some(model) if t.matrix_is_fallback => lightcraft_raw::spectral::find(raw.metadata.make.as_deref(), model),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let Some(camera) = spectral else { return (xy, t, fit_preview(raw, bytes, &t, profile.as_deref(), true)) };
|
||||||
|
if let Some(look) = fit_preview(raw, bytes, &t, profile.as_deref(), false) {
|
||||||
|
return (xy, t, Some(look));
|
||||||
|
}
|
||||||
|
camera.fill(&mut raw.color);
|
||||||
|
if lightcraft_pipeline::profiling() {
|
||||||
|
eprintln!("[profile] {:?} camera colour from spectral sensitivities: {} {}", raw.format, camera.make, camera.model);
|
||||||
|
}
|
||||||
|
let (xy, t) = start(raw);
|
||||||
|
(xy, t, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The look fitted to the file's JPEG: with `profile`'s colour when there is one, else (or when that fails the
|
||||||
|
/// gates, if `own_colour`) with colour fitted to this photo alone.
|
||||||
|
fn fit_preview(raw: &RawImage, bytes: &[u8], transform: &CameraTransform, profile: Option<&CameraProfile>, own_colour: bool) -> Option<CameraLook> {
|
||||||
|
if !transform.matrix_is_fallback || !file_local_look(raw.format) || (profile.is_none() && !own_colour) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// A camera profile pooled from many photos knows colours this photo shows too little of;
|
||||||
|
// try its colour first and fit tone/chroma per photo (DRO and picture styles vary).
|
||||||
|
let colour = profile.and_then(|p| Some((p.matrix().mul(&transform.matrix.inverse()?), p.hue_sat.clone())));
|
||||||
|
if colour.is_none() && !own_colour {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let (sensor, reference) = proxies(raw, bytes, transform, PROXY)?;
|
let (sensor, reference) = proxies(raw, bytes, transform, PROXY)?;
|
||||||
// A camera profile pooled from many photos knows colours this photo shows too little of;
|
let look = if own_colour {
|
||||||
// try its colour first and fit tone/chroma per photo (DRO and picture styles vary).
|
fit_pairs_with(&sensor, &reference, colour)?
|
||||||
let profile = raw.metadata.model.as_deref().and_then(crate::camera_profiles::get);
|
} else {
|
||||||
let colour = profile.as_ref().and_then(|p| Some((p.matrix().mul(&transform.matrix.inverse()?), p.hue_sat.clone())));
|
TONE_FITS.iter().find_map(|&tone| fit_candidate(&sensor, &reference, colour.clone(), tone))?
|
||||||
let look = fit_pairs_with(&sensor, &reference, colour)?;
|
};
|
||||||
if lightcraft_pipeline::profiling() {
|
if lightcraft_pipeline::profiling() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[profile] {:?} camera look: {:?}, {:?}, hue/sat table {}, camera profile available {}",
|
"[profile] {:?} camera look: {:?}, {:?}, hue/sat table {}, camera profile available {}",
|
||||||
@@ -410,9 +447,12 @@ const EDGE_CONTRAST: f32 = 3.0;
|
|||||||
/// it is tried once more on the pixels away from edges (same gates): there colour pairs stay
|
/// it is tried once more on the pixels away from edges (same gates): there colour pairs stay
|
||||||
/// valid when the camera JPEG's geometry differs slightly from the raw's.
|
/// valid when the camera JPEG's geometry differs slightly from the raw's.
|
||||||
fn fit_pairs_with(sensor: &Rgb32f, reference: &Rgb32f, colour: Option<(Mat3, Option<HsvTable>)>) -> Option<CameraLook> {
|
fn fit_pairs_with(sensor: &Rgb32f, reference: &Rgb32f, colour: Option<(Mat3, Option<HsvTable>)>) -> Option<CameraLook> {
|
||||||
fit_pairs_ordered(sensor, reference, colour, &[ToneFit::Quantile, ToneFit::ConditionalMedian])
|
fit_pairs_ordered(sensor, reference, colour, &TONE_FITS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The tone fits a look tries, in order (see [`fit_pairs_ordered`]).
|
||||||
|
const TONE_FITS: [ToneFit; 2] = [ToneFit::Quantile, ToneFit::ConditionalMedian];
|
||||||
|
|
||||||
/// [`fit_pairs_with`] with the tone fits to try, in order: the first whose look passes the
|
/// [`fit_pairs_with`] with the tone fits to try, in order: the first whose look passes the
|
||||||
/// acceptance gates is used. Quantile matching comes first because it cannot produce flat steps;
|
/// acceptance gates is used. Quantile matching comes first because it cannot produce flat steps;
|
||||||
/// the conditional-median fit follows because on a few photos (compacts, early Micro Four Thirds)
|
/// the conditional-median fit follows because on a few photos (compacts, early Micro Four Thirds)
|
||||||
@@ -1102,6 +1142,84 @@ mod tests {
|
|||||||
assert!(fit_hue_sat(&[], &Mat3::IDENTITY).is_none(), "too few samples");
|
assert!(fit_hue_sat(&[], &Mat3::IDENTITY).is_none(), "too few samples");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn raw_of(format: RawFormat, make: &str, model: &str, color: lightcraft_raw::ColorData) -> RawImage {
|
||||||
|
let area = lightcraft_raw::Rect::new(0, 0, 2, 2);
|
||||||
|
RawImage {
|
||||||
|
format,
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
cpp: 1,
|
||||||
|
data: lightcraft_raw::RawData::U16(vec![0; 4]),
|
||||||
|
cfa: None,
|
||||||
|
bits: 14,
|
||||||
|
black: Default::default(),
|
||||||
|
white: vec![16383.0],
|
||||||
|
active_area: area,
|
||||||
|
crop: area,
|
||||||
|
orientation: Default::default(),
|
||||||
|
color,
|
||||||
|
wb_multipliers: Some([2.0, 1.0, 1.5]),
|
||||||
|
linearized: false,
|
||||||
|
opcodes: Default::default(),
|
||||||
|
metadata: lightcraft_raw::Metadata { make: Some(make.to_owned()), model: Some(model.to_owned()), ..Default::default() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step 1: a file's own colour matrices win over everything, including a spectral row for its model.
|
||||||
|
#[test]
|
||||||
|
fn a_files_own_matrices_come_first() {
|
||||||
|
let own = Mat3([[0.6, -0.1, -0.05], [-0.4, 1.3, 0.1], [-0.1, 0.2, 0.7]]);
|
||||||
|
let color = lightcraft_raw::ColorData { illuminant: [21, 0], color_matrix: [Some(own), None], ..Default::default() };
|
||||||
|
let mut raw = raw_of(RawFormat::Cr2, "Canon", "Canon EOS 600D", color.clone());
|
||||||
|
let (_, t, look) = starting_colour(&mut raw, &[]);
|
||||||
|
assert_eq!(raw.color, color);
|
||||||
|
assert!(!t.matrix_is_fallback && look.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step 2: a camera profile comes before the spectral matrices; when its fit can't run (here no camera JPEG) the
|
||||||
|
/// spectral matrices follow. The ILCE-7M4 has a bundled profile and a spectral row.
|
||||||
|
#[test]
|
||||||
|
fn a_camera_profile_is_tried_before_the_spectral_matrices() {
|
||||||
|
assert!(crate::camera_profiles::get("ILCE-7M4").is_some());
|
||||||
|
let camera = lightcraft_raw::spectral::find(Some("SONY"), "ILCE-7M4").unwrap();
|
||||||
|
let mut raw = raw_of(RawFormat::Arw, "SONY", "ILCE-7M4", Default::default());
|
||||||
|
let (_, t, look) = starting_colour(&mut raw, &[]);
|
||||||
|
assert!(look.is_none());
|
||||||
|
assert_eq!(raw.color.color_matrix, camera.color_matrix.map(Some));
|
||||||
|
assert!(!t.matrix_is_fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step 3: a camera without a profile takes its spectral matrices, and no JPEG fit is tried: the look is `None`
|
||||||
|
/// and the transform is the DNG model's, at the as-shot white the matrices give the vendor multipliers.
|
||||||
|
#[test]
|
||||||
|
fn spectral_matrices_come_before_the_photos_own_fit() {
|
||||||
|
let camera = lightcraft_raw::spectral::find(Some("Canon"), "Canon EOS 600D").unwrap();
|
||||||
|
let mut raw = raw_of(RawFormat::Cr2, "Canon", "Canon EOS 600D", Default::default());
|
||||||
|
let (xy, t, look) = starting_colour(&mut raw, &[]);
|
||||||
|
assert!(look.is_none());
|
||||||
|
assert_eq!(raw.color.illuminant, [17, 21]);
|
||||||
|
assert_eq!(raw.color.forward_matrix, camera.forward_matrix.map(Some));
|
||||||
|
assert!(!t.matrix_is_fallback);
|
||||||
|
assert_eq!(xy, lightcraft_raw::color::as_shot_white_xy(&raw));
|
||||||
|
// white-balanced white stays white
|
||||||
|
let w = t.matrix.apply([1.0; 3]);
|
||||||
|
assert!(w.iter().all(|v| (v - 1.0).abs() < 1e-4), "{w:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Steps 4 and 5: a camera outside the table keeps today's path, the photo's own JPEG fit and else the neutral
|
||||||
|
/// fallback (here: no camera JPEG, so the fallback).
|
||||||
|
#[test]
|
||||||
|
fn a_camera_outside_the_table_keeps_the_fit_or_the_fallback() {
|
||||||
|
let mut raw = raw_of(RawFormat::Cr2, "Canon", "Canon EOS 7D", Default::default());
|
||||||
|
let (_, t, look) = starting_colour(&mut raw, &[]);
|
||||||
|
assert!(look.is_none() && t.matrix_is_fallback);
|
||||||
|
assert_eq!(raw.color, lightcraft_raw::ColorData::default());
|
||||||
|
// without a model there is no table row
|
||||||
|
let mut raw = raw_of(RawFormat::Cr2, "Canon", "", Default::default());
|
||||||
|
raw.metadata.model = None;
|
||||||
|
assert!(starting_colour(&mut raw, &[]).1.matrix_is_fallback);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn supported_raws_get_a_file_local_look() {
|
fn supported_raws_get_a_file_local_look() {
|
||||||
// every decoded raw without a colour matrix of its own (issue #310: CR2 rendered flat with the fallback)
|
// every decoded raw without a colour matrix of its own (issue #310: CR2 rendered flat with the fallback)
|
||||||
@@ -1172,8 +1290,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(raw.format, RawFormat::Cr3);
|
assert_eq!(raw.format, RawFormat::Cr3);
|
||||||
assert_eq!(raw.info().developed_size(), (6000, 4000));
|
assert_eq!(raw.info().developed_size(), (6000, 4000));
|
||||||
let transform = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
|
let (_, _, look) = starting_colour(&mut raw.clone(), &bytes);
|
||||||
let look = fit_preview(&raw, &bytes, &transform);
|
|
||||||
eprintln!("R100 guarded colour fit accepted: {}", look.is_some());
|
eprintln!("R100 guarded colour fit accepted: {}", look.is_some());
|
||||||
assert!(look.is_some(), "this R100 corpus photo has enough matching colour after camera-framing alignment");
|
assert!(look.is_some(), "this R100 corpus photo has enough matching colour after camera-framing alignment");
|
||||||
let (_, info) = crate::files::load_bytes(&bytes, 400).unwrap();
|
let (_, info) = crate::files::load_bytes(&bytes, 400).unwrap();
|
||||||
@@ -1219,7 +1336,8 @@ mod tests {
|
|||||||
|
|
||||||
/// Issue #232 (skipped without the corpus): the public ILCE-7RM2 sample's camera JPEG is
|
/// Issue #232 (skipped without the corpus): the public ILCE-7RM2 sample's camera JPEG is
|
||||||
/// distortion-corrected, and on its glass façade the fit on all pixels misses the gate
|
/// distortion-corrected, and on its glass façade the fit on all pixels misses the gate
|
||||||
/// (held-out RMS 0.113): it used to open grey with the neutral fallback.
|
/// (held-out RMS 0.113): it used to open grey with the neutral fallback. The ILCE-7RM2 now starts from its
|
||||||
|
/// spectral matrices; the fit away from edges is still what a camera outside that table gets.
|
||||||
#[test]
|
#[test]
|
||||||
fn corpus_arw_with_a_lens_corrected_preview_gets_a_camera_look() {
|
fn corpus_arw_with_a_lens_corrected_preview_gets_a_camera_look() {
|
||||||
let path = std::env::var_os("LIGHTCRAFT_CORPUS")
|
let path = std::env::var_os("LIGHTCRAFT_CORPUS")
|
||||||
@@ -1230,8 +1348,49 @@ mod tests {
|
|||||||
eprintln!("skip: {} absent", path.display());
|
eprintln!("skip: {} absent", path.display());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let mut raw = lightcraft_raw::decode(&bytes).unwrap();
|
||||||
|
raw.opcodes.list3.retain(|op| !op.is_lens_correction());
|
||||||
|
let transform = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
|
||||||
|
assert!(fit_preview(&raw, &bytes, &transform, None, true).is_some(), "no camera look fitted");
|
||||||
let (_, info) = crate::files::load_bytes(&bytes, 400).unwrap();
|
let (_, info) = crate::files::load_bytes(&bytes, 400).unwrap();
|
||||||
assert!(info.camera_tone.is_some(), "no camera look fitted");
|
assert!(info.camera_tone.is_none(), "the spectral matrices come before the photo's own fit");
|
||||||
|
assert!(info.relative_wb && info.as_shot_temp == 6500.0 && info.as_shot_tint == 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The colour precedence on public raws (each skipped without its corpus file): the ILCE-7M4 has a bundled
|
||||||
|
/// profile, fitted for tone to its JPEG; the EOS 5D Mark III and D5100 have no profile and take their spectral
|
||||||
|
/// matrices; the EOS 7D is in neither and keeps its own fit; a DNG keeps its own matrices. White balance stays
|
||||||
|
/// relative to the as-shot look for every raw without matrices of its own.
|
||||||
|
#[test]
|
||||||
|
fn corpus_colour_precedence() {
|
||||||
|
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");
|
||||||
|
// (file, spectral matrices used, camera look fitted)
|
||||||
|
for (name, spectral, look) in [
|
||||||
|
("arw-sony-a7m4-14bit.arw", false, true),
|
||||||
|
("cr2-canon-5d3.cr2", true, false),
|
||||||
|
("nef-nikon-d5100-lossless.nef", true, false),
|
||||||
|
("cr2-canon-7d.cr2", false, true),
|
||||||
|
("dng-canon-5d3-16bit.dng", false, false),
|
||||||
|
] {
|
||||||
|
let Ok(bytes) = std::fs::read(dir.join(name)) else {
|
||||||
|
eprintln!("skip: {name} absent");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut raw = lightcraft_raw::decode(&bytes).unwrap();
|
||||||
|
raw.opcodes.list3.retain(|op| !op.is_lens_correction());
|
||||||
|
let own = lightcraft_raw::color::has_matrix(&raw.color);
|
||||||
|
let before = raw.color.clone();
|
||||||
|
let (_, t, fitted) = starting_colour(&mut raw, &bytes);
|
||||||
|
assert_eq!(raw.color != before, spectral, "{name}");
|
||||||
|
assert_eq!(fitted.is_some(), look, "{name}");
|
||||||
|
assert_eq!(t.matrix_is_fallback, !own && !spectral, "{name}");
|
||||||
|
let (_, info) = crate::files::load_bytes(&bytes, 400).unwrap();
|
||||||
|
assert_eq!(info.camera_tone.is_some(), look, "{name}");
|
||||||
|
assert_eq!(info.relative_wb, !own, "{name}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Issue #232: an ILCE-7RM2 camera JPEG is lens-corrected ("Distortion Comp.: Auto"), so on a
|
/// Issue #232: an ILCE-7RM2 camera JPEG is lens-corrected ("Distortion Comp.: Auto"), so on a
|
||||||
@@ -1289,7 +1448,9 @@ mod tests {
|
|||||||
.map(std::path::PathBuf::from)
|
.map(std::path::PathBuf::from)
|
||||||
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"))
|
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"))
|
||||||
.join("raw");
|
.join("raw");
|
||||||
for (name, accepted) in [("raf-fuji-xt2-865.raf", true), ("raf-fuji-gfx100s-4503.raf", true), ("raf-fuji-xt20-compressed.raf", true)] {
|
// The GFX100S shares the GFX 100's sensor (rawtoaces-data's alias list), so it starts from those spectral
|
||||||
|
// matrices instead of a look fitted to its JPEG.
|
||||||
|
for (name, accepted) in [("raf-fuji-xt2-865.raf", true), ("raf-fuji-gfx100s-4503.raf", false), ("raf-fuji-xt20-compressed.raf", true)] {
|
||||||
let Ok(bytes) = std::fs::read(dir.join(name)) else { continue };
|
let Ok(bytes) = std::fs::read(dir.join(name)) else { continue };
|
||||||
let header = crate::files::probe_bytes(name, &bytes).unwrap();
|
let header = crate::files::probe_bytes(name, &bytes).unwrap();
|
||||||
assert_eq!(header.as_shot_wb, Some((6500.0, 0.0)), "{name}");
|
assert_eq!(header.as_shot_wb, Some((6500.0, 0.0)), "{name}");
|
||||||
|
|||||||
@@ -275,12 +275,13 @@ fn load_bytes_now(
|
|||||||
};
|
};
|
||||||
// Embedded lens corrections are applied by the pipeline ("Enable Profile Corrections"), not baked in
|
// Embedded lens corrections are applied by the pipeline ("Enable Profile Corrections"), not baked in
|
||||||
// (removed before the camera look's binned sensor proxy too, which needs an empty `OpcodeList3`).
|
// (removed before the camera look's binned sensor proxy too, which needs an empty `OpcodeList3`).
|
||||||
let lens = embedded_lens(&raw.info());
|
let info = raw.info();
|
||||||
|
let lens = embedded_lens(&info);
|
||||||
raw.opcodes.list3.retain(|op| !op.is_lens_correction());
|
raw.opcodes.list3.retain(|op| !op.is_lens_correction());
|
||||||
let xy = lightcraft_raw::color::as_shot_white_xy(&raw);
|
// Without colour matrices of its own, white balance is relative to the as-shot look (`Photo::relative_wb`).
|
||||||
let t = lightcraft_raw::color::camera_transform(&raw, xy);
|
let own_matrix = lightcraft_raw::color::has_matrix(&raw.color);
|
||||||
// the source's segmentation mattes (DNG semantic masks), read while the preview is fitted
|
// the source's segmentation mattes (DNG semantic masks), read while the starting colour is fitted
|
||||||
let (camera_look, mattes) = rayon::join(|| crate::camera_preview::fit_preview(&raw, &bytes, &t), || dng_mattes(&bytes, &raw));
|
let ((xy, t, camera_look), mattes) = rayon::join(|| crate::camera_preview::starting_colour(&mut raw, &bytes), || dng_mattes(&bytes, &info));
|
||||||
drop(bytes);
|
drop(bytes);
|
||||||
// Previews and thumbnails bin the mosaic straight to (about) the size they need; only
|
// Previews and thumbnails bin the mosaic straight to (about) the size they need; only
|
||||||
// larger levels (exports, 1:1) demosaic the whole sensor.
|
// larger levels (exports, 1:1) demosaic the whole sensor.
|
||||||
@@ -350,7 +351,7 @@ fn load_bytes_now(
|
|||||||
}
|
}
|
||||||
let twin = twin.map(|picture| finish(picture).0);
|
let twin = twin.map(|picture| finish(picture).0);
|
||||||
let (temp, tint) = xy_to_temp_tint(xy);
|
let (temp, tint) = xy_to_temp_tint(xy);
|
||||||
let relative = crate::camera_preview::file_local_look(raw.format) && t.matrix_is_fallback;
|
let relative = crate::camera_preview::file_local_look(raw.format) && !own_matrix;
|
||||||
// White balance re-evaluates the file's own colour model (when it has one and no
|
// White balance re-evaluates the file's own colour model (when it has one and no
|
||||||
// file-local look matrix sits on top of it)
|
// file-local look matrix sits on top of it)
|
||||||
let camera_color = (!t.matrix_is_fallback && camera_look.is_none()).then(|| {
|
let camera_color = (!t.matrix_is_fallback && camera_look.is_none()).then(|| {
|
||||||
@@ -398,7 +399,7 @@ fn local_tone(raw: &lightcraft_raw::RawImage) -> Option<Arc<lightcraft_pipeline:
|
|||||||
/// The semantic masks of a DNG that AI masks understand, over the developed image (default crop,
|
/// The semantic masks of a DNG that AI masks understand, over the developed image (default crop,
|
||||||
/// oriented like it). Person mattes without a confidently selected pixel are left out (the
|
/// oriented like it). Person mattes without a confidently selected pixel are left out (the
|
||||||
/// Subject mask then falls back to its heuristic); a sky matte counts even when empty (no sky).
|
/// Subject mask then falls back to its heuristic); a sky matte counts even when empty (no sky).
|
||||||
fn dng_mattes(bytes: &[u8], raw: &lightcraft_raw::RawImage) -> Option<Arc<lightcraft_pipeline::masks::Mattes>> {
|
fn dng_mattes(bytes: &[u8], raw: &lightcraft_raw::RawInfo) -> Option<Arc<lightcraft_pipeline::masks::Mattes>> {
|
||||||
use lightcraft_pipeline::masks::{MatteKind, Mattes};
|
use lightcraft_pipeline::masks::{MatteKind, Mattes};
|
||||||
if raw.format != lightcraft_raw::RawFormat::Dng {
|
if raw.format != lightcraft_raw::RawFormat::Dng {
|
||||||
return None;
|
return None;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ impl SettingsHashes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
|
/// Bump when the pipeline's output changes, to invalidate cached thumbnails.
|
||||||
pub const RENDER_CACHE_VERSION: u64 = 22;
|
pub const RENDER_CACHE_VERSION: u64 = 23;
|
||||||
|
|
||||||
/// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache).
|
/// 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];
|
pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
//! [`embedded_preview`] covers these containers' JPEG previews. Variants we can't decode yet (Nikon "lossy after split" NEF,
|
//! [`embedded_preview`] covers these containers' JPEG previews. Variants we can't decode yet (Nikon "lossy after split" NEF,
|
||||||
//! compressed ORF, CR3 unverified marker families / C-RAW configurations) return [`RawError::Unsupported`]; each vendor module documents its sources
|
//! compressed ORF, CR3 unverified marker families / C-RAW configurations) return [`RawError::Unsupported`]; each vendor module documents its sources
|
||||||
//! (public specifications, tag-name documentation, black-box analysis of CC0 samples) and gaps. Non-DNG files carry no
|
//! (public specifications, tag-name documentation, black-box analysis of CC0 samples) and gaps. Non-DNG files carry no
|
||||||
//! colour matrix: [`color`] falls back to a documented neutral model. The decoders never panic on malformed input.
|
//! colour matrix: [`spectral`] has matrices fitted to measured spectral sensitivities for 52 models, and
|
||||||
|
//! otherwise [`color`] falls back to a documented neutral model. The decoders never panic on malformed input.
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ pub mod opcodes;
|
|||||||
mod preview;
|
mod preview;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod semantic;
|
pub mod semantic;
|
||||||
|
pub mod spectral;
|
||||||
mod tiffraw;
|
mod tiffraw;
|
||||||
mod unpack;
|
mod unpack;
|
||||||
mod vendor;
|
mod vendor;
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
//! Camera colour matrices from measured spectral sensitivities, 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. 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;
|
||||||
|
|
||||||
|
#[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`).
|
||||||
|
pub color_matrix: [Mat3; 2],
|
||||||
|
/// White-balanced camera → XYZ D50 (`ForwardMatrix1/2`).
|
||||||
|
pub forward_matrix: [Mat3; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Camera {
|
||||||
|
/// Give `color` this camera's matrices, when it has no colour matrix of its own. Returns whether it did.
|
||||||
|
pub fn fill(&self, color: &mut ColorData) -> bool {
|
||||||
|
if crate::color::has_matrix(color) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
color.illuminant = ILLUMINANTS;
|
||||||
|
color.color_matrix = self.color_matrix.map(Some);
|
||||||
|
color.forward_matrix = self.forward_matrix.map(Some);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Model names compared without case, the brand, a leading `EOS` or a trailing `DIGITAL`
|
||||||
|
/// (Exif "Canon EOS 400D DIGITAL" is the table's "EOS 400D").
|
||||||
|
fn key(brand: &str, model: &str) -> String {
|
||||||
|
let mut words: Vec<String> = model.split_whitespace().map(str::to_uppercase).collect();
|
||||||
|
if words.first().is_some_and(|w| *w == brand) {
|
||||||
|
words.remove(0);
|
||||||
|
}
|
||||||
|
if words.first().is_some_and(|w| w == "EOS") {
|
||||||
|
words.remove(0);
|
||||||
|
}
|
||||||
|
if words.len() > 1 && words.last().is_some_and(|w| w == "DIGITAL") {
|
||||||
|
words.pop();
|
||||||
|
}
|
||||||
|
words.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// First word of a make, upper case ("NIKON CORPORATION" → "NIKON").
|
||||||
|
fn brand(make: &str) -> String {
|
||||||
|
make.split_whitespace().next().unwrap_or_default().to_uppercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The table's camera for a file's Exif make and model, directly or through an alias. Without a make, the model
|
||||||
|
/// alone must match.
|
||||||
|
pub fn find(make: Option<&str>, model: &str) -> Option<&'static Camera> {
|
||||||
|
let make = make.map(brand).filter(|b| !b.is_empty());
|
||||||
|
let same = |row_make: &str, row_model: &str| {
|
||||||
|
let row_brand = brand(row_make);
|
||||||
|
let wanted = key(&row_brand, model);
|
||||||
|
make.as_ref().is_none_or(|m| *m == row_brand) && !wanted.is_empty() && key(&row_brand, row_model) == wanted
|
||||||
|
};
|
||||||
|
match table::ALIASES.iter().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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every camera in the table.
|
||||||
|
pub fn cameras() -> &'static [Camera] {
|
||||||
|
table::CAMERAS
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exif_names_find_their_rows() {
|
||||||
|
for (make, model, row) in [
|
||||||
|
("Canon", "Canon EOS 5D Mark III", "EOS 5D Mark III"),
|
||||||
|
("Canon", "Canon EOS 600D", "EOS 600D"),
|
||||||
|
("Canon", "Canon EOS-1Ds Mark III", "EOS-1Ds Mark III"),
|
||||||
|
("Canon", "Canon EOS DIGITAL REBEL XTi", "Digital Rebel XTi"),
|
||||||
|
("Canon", "Canon EOS R5m2", "EOS R5m2"),
|
||||||
|
("Canon", "Canon PowerShot S90", "PowerShot S90"),
|
||||||
|
("NIKON CORPORATION", "NIKON D850", "D850"),
|
||||||
|
("NIKON CORPORATION", "NIKON Z f", "Z f"),
|
||||||
|
("SONY", "ILCE-7M3", "ILCE-7M3"),
|
||||||
|
("FUJIFILM", "X-T4", "X-T4"),
|
||||||
|
("Panasonic", "DC-GX9", "DC-GX9"),
|
||||||
|
] {
|
||||||
|
assert_eq!(find(Some(make), model).map(|c| c.model), Some(row), "{make} {model}");
|
||||||
|
}
|
||||||
|
assert_eq!(find(None, "NIKON D850").map(|c| c.model), Some("D850"));
|
||||||
|
assert_eq!(find(None, "ILCE-7M3").map(|c| c.model), Some("ILCE-7M3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aliases_reach_the_camera_they_name() {
|
||||||
|
for (make, model, row) in [
|
||||||
|
("Canon", "Canon EOS 400D DIGITAL", "Digital Rebel XTi"),
|
||||||
|
("Canon", "Canon EOS Kiss Digital X", "Digital Rebel XTi"),
|
||||||
|
("Canon", "Canon EOS REBEL T3i", "EOS 600D"),
|
||||||
|
("Canon", "Canon EOS Kiss X5", "EOS 600D"),
|
||||||
|
("Canon", "Canon EOS R5 Mark II", "EOS R5m2"),
|
||||||
|
("NIKON CORPORATION", "NIKON D800", "D800E"),
|
||||||
|
("NIKON CORPORATION", "NIKON Z 6_2", "Z f"),
|
||||||
|
("SONY", "ILCE-7RM4A", "ILCE-7RM4"),
|
||||||
|
("FUJIFILM", "GFX100S", "GFX 100"),
|
||||||
|
] {
|
||||||
|
assert_eq!(find(Some(make), model).map(|c| c.model), Some(row), "{make} {model}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn other_cameras_and_other_makes_find_nothing() {
|
||||||
|
assert_eq!(find(Some("Canon"), "Canon EOS 7D"), None);
|
||||||
|
assert_eq!(find(Some("SONY"), "ILCE-7M2"), None);
|
||||||
|
assert_eq!(find(Some("OLYMPUS IMAGING CORP."), "E-M5"), None);
|
||||||
|
// the model of another make
|
||||||
|
assert_eq!(find(Some("Canon"), "D850"), None);
|
||||||
|
assert_eq!(find(Some("SONY"), "X-T4"), None);
|
||||||
|
assert_eq!(find(None, ""), None);
|
||||||
|
assert_eq!(find(Some(""), ""), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_row_is_a_usable_dng_profile() {
|
||||||
|
assert_eq!(cameras().len(), 52);
|
||||||
|
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) {
|
||||||
|
assert!(m.0.iter().flatten().all(|v| v.is_finite() && v.abs() < 4.0), "{}", c.model);
|
||||||
|
assert!(m.inverse().is_some(), "{}", c.model);
|
||||||
|
}
|
||||||
|
// a forward matrix maps white-balanced white to D50 (DNG spec, chapter 6)
|
||||||
|
for fm in &c.forward_matrix {
|
||||||
|
let w = fm.apply([1.0; 3]);
|
||||||
|
let d50 = lightcraft_color::D50.to_xyz();
|
||||||
|
assert!((0..3).all(|i| (w[i] - d50[i]).abs() < 1e-4), "{} {w:?}", c.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fill_keeps_a_files_own_matrices() {
|
||||||
|
let c = find(Some("Canon"), "Canon EOS 600D").unwrap();
|
||||||
|
let mut color = ColorData::default();
|
||||||
|
assert!(c.fill(&mut color));
|
||||||
|
assert_eq!(color.illuminant, [17, 21]);
|
||||||
|
assert_eq!(color.color_matrix, [Some(c.color_matrix[0]), Some(c.color_matrix[1])]);
|
||||||
|
assert_eq!(color.forward_matrix, [Some(c.forward_matrix[0]), Some(c.forward_matrix[1])]);
|
||||||
|
|
||||||
|
let own = Mat3([[0.5, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.7]]);
|
||||||
|
let mut color = ColorData { illuminant: [21, 0], color_matrix: [Some(own), None], ..Default::default() };
|
||||||
|
let before = color.clone();
|
||||||
|
assert!(!c.fill(&mut color));
|
||||||
|
assert_eq!(color, before);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
// Generated by tools/spectral_camera_table.py from rawtoaces-data (https://github.com/AcademySoftwareFoundation/rawtoaces-data, commit 5cfa53707b053594b2d0d48e8019fb56e6582d6c).
|
||||||
|
// Derived from the camera spectral sensitivities of rawtoaces-data (Academy Software Foundation), licensed under
|
||||||
|
// the Apache License 2.0 (LICENSE-APACHE); see NOTICE and assets/ATTRIBUTION.md. Do not edit by hand.
|
||||||
|
use super::Camera;
|
||||||
|
use lightcraft_color::Mat3;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub(super) const CAMERAS: &[Camera] = &[
|
||||||
|
Camera {
|
||||||
|
make: "ARRI",
|
||||||
|
model: "D21",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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]])],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/// `(make, model as sold, table model)`: other names of a camera in [`CAMERAS`], or of a camera with the same sensor.
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub(super) const ALIASES: &[(&str, &str, &str)] = &[
|
||||||
|
("Canon", "EOS 400D", "Digital Rebel XTi"), // same model, alternative name
|
||||||
|
("Canon", "Kiss Digital X", "Digital Rebel XTi"), // same model, alternative name
|
||||||
|
("Canon", "EOS Rebel SL1", "EOS 100D"), // same model, alternative name
|
||||||
|
("Canon", "Kiss X7", "EOS 100D"), // same model, alternative name
|
||||||
|
("Canon", "EOS Rebel SL2", "EOS 200D"), // same model, alternative name
|
||||||
|
("Canon", "Kiss X9", "EOS 200D"), // same model, alternative name
|
||||||
|
("Canon", "EOS 250D", "EOS 200D II"), // same model, alternative name
|
||||||
|
("Canon", "EOS Rebel SL3", "EOS 200D II"), // same model, alternative name
|
||||||
|
("Canon", "Kiss X10", "EOS 200D II"), // same model, alternative name
|
||||||
|
("Canon", "EOS Rebel T3i", "EOS 600D"), // same model, alternative name
|
||||||
|
("Canon", "Kiss X5", "EOS 600D"), // same model, alternative name
|
||||||
|
("Canon", "EOS R5 Mark II", "EOS R5m2"), // same model, alternative name
|
||||||
|
("Canon", "EOS 5DS R", "EOS 5DS"), // different model, same sensor
|
||||||
|
("Canon", "EOS 6D Mark II", "EOS RP"), // different model, same sensor
|
||||||
|
("Canon", "EOS R5 C", "EOS R5"), // different model, same sensor
|
||||||
|
("Canon", "EOS R50", "EOS R10"), // different model, same sensor
|
||||||
|
("Canon", "EOS R8", "EOS R6m2"), // different model, same sensor
|
||||||
|
("Nikon", "D800", "D800E"), // different model, same sensor
|
||||||
|
("Nikon", "Z 6_2", "Z f"), // different model, same sensor
|
||||||
|
("Sony", "ILCE-7RM5", "ILCE-7RM4"), // different model, same sensor
|
||||||
|
("Sony", "ILCE-7RM4A", "ILCE-7RM4"), // updated model, same sensor
|
||||||
|
("Sony", "ILCE-7RM3A", "ILCE-7RM3"), // updated model, same sensor
|
||||||
|
("Fujifilm", "GFX100S", "GFX 100"), // different model, same sensor
|
||||||
|
];
|
||||||
@@ -77,6 +77,34 @@ One photo can show too little of a colour for its own fit to learn it: in a seco
|
|||||||
Profiles are JSON files `<model>.json` in `$LIGHTCRAFT_CAMERA_PROFILES`, else `<config>/camera-profiles` (macOS `~/Library/Application Support/LightCraft/camera-profiles`); a local profile replaces a built-in one. Built-in profiles live in `assets/camera-profiles/` (listed in `assets/ATTRIBUTION.md`) and are compiled in, so the app, CLI and web build share them: ILCE-7M4, fitted to 597 photos (2.2 million colour pairs) shot in 2026, mostly with the Standard creative style and DRO Auto; X-H2S, 201 photos; X-T4, 545 photos. They hold aggregate colour statistics only. A photo of a profiled model takes its colour from the profile and fits only its own tone and chroma curves (DRO and picture styles vary per shot); the acceptance gates still apply. A rejected profile fit retries the photo's own colour and tone fit before falling back to neutral rendering. Files are read once per process and validated (version, bounded invertible matrix, table shape and finite data); a damaged file is ignored with a warning. The profiles folder's contents are part of the render cache keys, so thumbnails rendered before a profile existed are redone; smart previews built before keep their colour until rebuilt.
|
Profiles are JSON files `<model>.json` in `$LIGHTCRAFT_CAMERA_PROFILES`, else `<config>/camera-profiles` (macOS `~/Library/Application Support/LightCraft/camera-profiles`); a local profile replaces a built-in one. Built-in profiles live in `assets/camera-profiles/` (listed in `assets/ATTRIBUTION.md`) and are compiled in, so the app, CLI and web build share them: ILCE-7M4, fitted to 597 photos (2.2 million colour pairs) shot in 2026, mostly with the Standard creative style and DRO Auto; X-H2S, 201 photos; X-T4, 545 photos. They hold aggregate colour statistics only. A photo of a profiled model takes its colour from the profile and fits only its own tone and chroma curves (DRO and picture styles vary per shot); the acceptance gates still apply. A rejected profile fit retries the photo's own colour and tone fit before falling back to neutral rendering. Files are read once per process and validated (version, bounded invertible matrix, table shape and finite data); a damaged file is ignored with a warning. The profiles folder's contents are part of the render cache keys, so thumbnails rendered before a profile existed are redone; smart previews built before keep their colour until rebuilt.
|
||||||
|
|
||||||
|
|
||||||
|
## Spectral camera matrices and the order of precedence
|
||||||
|
|
||||||
|
For 52 camera models LightCraft has colour matrices fitted to the camera's measured spectral sensitivities
|
||||||
|
(`crates/raw/src/spectral.rs`, generated into `spectral_table.rs` by `tools/spectral_camera_table.py` from
|
||||||
|
[rawtoaces-data](https://github.com/AcademySoftwareFoundation/rawtoaces-data) v1.1.0, Apache-2.0): 22 Canon, 11 Nikon,
|
||||||
|
11 Sony, 3 Fujifilm and 5 others, plus the other names rawtoaces-data lists for them (EOS Rebel T3i and Kiss X5 for
|
||||||
|
the EOS 600D; bodies it lists as sharing a sensor, such as the GFX100S with the GFX 100). Per model the generator fits,
|
||||||
|
under CIE Standard Illuminant A and under D65, a white-preserving 3×3 from white-balanced camera RGB to XYZ by the
|
||||||
|
least mean CIE76 error over rawtoaces-data's 190 training reflectances, and writes DNG `ColorMatrix1/2` and
|
||||||
|
`ForwardMatrix1/2`. The existing DNG colour model then interpolates them for the file's as-shot white, which the
|
||||||
|
vendor white-balance multipliers give through the same matrices. No number comes from Adobe or from a raw decoder.
|
||||||
|
|
||||||
|
A raw starts from the first of these that is available (`camera_preview::starting_colour`):
|
||||||
|
|
||||||
|
1. the file's own colour matrices (DNG);
|
||||||
|
2. a camera profile for its model (bundled or local), with tone and chroma fitted to the file's JPEG;
|
||||||
|
3. the model's spectral matrices, with LightCraft's default tone curve and no JPEG fit;
|
||||||
|
4. the file-local fit to its own JPEG described above;
|
||||||
|
5. the neutral fallback.
|
||||||
|
|
||||||
|
A profiled model whose profile fit fails the gates (or that has no usable JPEG) takes its spectral matrices when it
|
||||||
|
has them. White balance stays relative to the as-shot look for every raw without matrices of its own, as before, so
|
||||||
|
the catalog's `Photo::relative_wb` and the controls are unchanged. `lightcraft-cli calibrate` still pools the
|
||||||
|
uncorrected camera colour of covered models, so a profile can be fitted for them and then takes precedence.
|
||||||
|
|
||||||
|
No colour-difference measurement of the spectral matrices against the camera JPEG, or against the file-local fit,
|
||||||
|
is recorded here yet. Moving step 3 after step 4 is a one-line change in `starting_colour`.
|
||||||
|
|
||||||
## Nikon crop and preview colour metadata
|
## Nikon crop and preview colour metadata
|
||||||
|
|
||||||
Nikon maker-note `CropArea` (0x0045) supplies the default `[left, top, width, height]` crop. The decoder validates the rectangle against the active sensor area and falls back to that area for missing or invalid values. The CFA origin is unchanged; cropping follows demosaicing.
|
Nikon maker-note `CropArea` (0x0045) supplies the default `[left, top, width, height]` crop. The decoder validates the rectangle against the active sensor area and falls back to that area for missing or invalid values. The CFA origin is unchanged; cropping follows demosaicing.
|
||||||
|
|||||||
+1
-1
@@ -296,7 +296,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
|||||||
| LR-PROF-ADOBE | Standard raw looks (own equivalents) | P0 | ✅ | `crates/engine/src/presets.rs` (`PROFILES`), `crates/pipeline/src/profiles.rs` | six own looks: Color, Neutral, Vivid, Landscape, Portrait, Monochrome |
|
| LR-PROF-ADOBE | Standard raw looks (own equivalents) | P0 | ✅ | `crates/engine/src/presets.rs` (`PROFILES`), `crates/pipeline/src/profiles.rs` | six own looks: Color, Neutral, Vivid, Landscape, Portrait, Monochrome |
|
||||||
| LR-PROF-ADAPTIVE | Adaptive profiles | P2 | ⬜ | | |
|
| LR-PROF-ADAPTIVE | Adaptive profiles | P2 | ⬜ | | |
|
||||||
| LR-PROF-CAMERA | Camera-matching looks | P2 | ⬜ | | |
|
| LR-PROF-CAMERA | Camera-matching looks | P2 | ⬜ | | |
|
||||||
| LR-PROF-CAMERACOLOR | Camera colour calibration (own) | P0 | 🟡 | `crates/raw/src/color.rs`, `crates/raw/src/profile.rs`, `crates/raw/src/gaintable.rs`, `crates/engine/src/camera_preview.rs` | DNG files use the colour matrices and the camera-profile look they carry (`ProfileHueSatMap`, `ProfileLookTable`, `ProfileToneCurve`, read from the file at run time per DNG spec ch. 6 — Lightroom-converted DNGs rendered flat and muted without them, issue #138). A DNG's `ProfileGainTableMap` / `ProfileGainTableMap2` (Apple ProRAW's local tone map; both versions, DNG 1.7 precedence, the file's byte order with a big-endian fallback) is read and written back by DNG export and not rendered by default, as Lightroom Classic renders ProRAW without it (measured on one CC0 iPhone 12 Pro ProRAW); the Profile option "Camera local tone mapping" (`profile.camera_local_tone`, per photo, off by default, `crates/pipeline/src/local_tone.rs`) renders it as the camera does, per output pixel in the scene-linear stage (CPU, also under the GPU path); Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3 and CR2, Pentax PEF and Samsung SRW get a guarded file-local fit to their own embedded JPEG — chromaticity matrix + hue/saturation/value table + tone and chroma curves, relative WB — or, with a bundled or local per-model profile pooled from many photos (`lightcraft-cli calibrate`, ARW, NEF and RAF so far, `crates/engine/src/camera_profiles.rs`), the profile's colour and only their own tone/chroma curves (on 29 held-out ILCE-7M4 photos mean ΔE vs the camera JPEG 3.54 → 3.10), kept with smart previews (see docs/camera-preview-colour.md); ARW: on 10 public raw.pixls.us samples from 8 bodies 7 fits were accepted, mean ΔE vs the camera JPEG 17–26 → 3–10; NEF: 13 of 13 decodable samples (D750, D780, D850, D7500, Z 50 and local D7500 shots) accepted, ΔE 13–46 → 3–8 (issue #150); RW2 / RWL: 140 of 174 public samples accepted, from 93 of the 114 bodies with a preview, median ΔE 5.0 with matching lightness (13 of the 14 corpus files with a preview: ΔE 9.6–27 → 2.8–8.9; most rejections are compacts and kit zooms whose camera JPEG is distortion-corrected); a fit that fails the gates on all pixels is retried away from edges, where a lens-corrected camera JPEG still lines up (issue #232: the six distortion-corrected public ILCE-7RM2 samples ΔE 16.6–16.9 → 3.7–3.8, the corpus GX80 19.1 → 7.9); RAF: bundled X-H2S/X-T4 profiles from 201/545 photos; on 150 withheld files mean ΔE improves slightly over per-file fitting (X-H2S 1.70 → 1.62, X-T4 2.43 → 2.34); 23 of 24 references from 13 bodies accepted, including the dark X-H2S samples (8.74 → 2.25) (`docs/camera-preview-colour.md`); this is not measured camera calibration; other raws and rejected fits use a neutral fallback (camera RGB ≈ linear sRGB, `matrix_is_fallback`) with as-shot white balance, so colours are muted and not accurate. Needs our own per-camera calibration: matrices the files carry themselves (e.g. Olympus `ColorMatrix`), fitting each camera to its own embedded JPEG, then chart shots. Adobe matrices / DCPs are never used. Biggest image-quality gap today |
|
| LR-PROF-CAMERACOLOR | Camera colour calibration (own) | P0 | 🟡 | `crates/raw/src/color.rs`, `crates/raw/src/profile.rs`, `crates/raw/src/gaintable.rs`, `crates/engine/src/camera_preview.rs` | DNG files use the colour matrices and the camera-profile look they carry (`ProfileHueSatMap`, `ProfileLookTable`, `ProfileToneCurve`, read from the file at run time per DNG spec ch. 6 — Lightroom-converted DNGs rendered flat and muted without them, issue #138). A DNG's `ProfileGainTableMap` / `ProfileGainTableMap2` (Apple ProRAW's local tone map; both versions, DNG 1.7 precedence, the file's byte order with a big-endian fallback) is read and written back by DNG export and not rendered by default, as Lightroom Classic renders ProRAW without it (measured on one CC0 iPhone 12 Pro ProRAW); the Profile option "Camera local tone mapping" (`profile.camera_local_tone`, per photo, off by default, `crates/pipeline/src/local_tone.rs`) renders it as the camera does, per output pixel in the scene-linear stage (CPU, also under the GPU path); Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3 and CR2, Pentax PEF and Samsung SRW get a guarded file-local fit to their own embedded JPEG — chromaticity matrix + hue/saturation/value table + tone and chroma curves, relative WB — or, with a bundled or local per-model profile pooled from many photos (`lightcraft-cli calibrate`, ARW, NEF and RAF so far, `crates/engine/src/camera_profiles.rs`), the profile's colour and only their own tone/chroma curves (on 29 held-out ILCE-7M4 photos mean ΔE vs the camera JPEG 3.54 → 3.10), kept with smart previews (see docs/camera-preview-colour.md); 52 models without a profile start from DNG-style matrices fitted to their measured spectral sensitivities instead of the file-local fit (`crates/raw/src/spectral.rs`, rawtoaces-data v1.1.0, Apache-2.0, aliases included; not yet measured against the camera JPEG); ARW: on 10 public raw.pixls.us samples from 8 bodies 7 fits were accepted, mean ΔE vs the camera JPEG 17–26 → 3–10; NEF: 13 of 13 decodable samples (D750, D780, D850, D7500, Z 50 and local D7500 shots) accepted, ΔE 13–46 → 3–8 (issue #150); RW2 / RWL: 140 of 174 public samples accepted, from 93 of the 114 bodies with a preview, median ΔE 5.0 with matching lightness (13 of the 14 corpus files with a preview: ΔE 9.6–27 → 2.8–8.9; most rejections are compacts and kit zooms whose camera JPEG is distortion-corrected); a fit that fails the gates on all pixels is retried away from edges, where a lens-corrected camera JPEG still lines up (issue #232: the six distortion-corrected public ILCE-7RM2 samples ΔE 16.6–16.9 → 3.7–3.8, the corpus GX80 19.1 → 7.9); RAF: bundled X-H2S/X-T4 profiles from 201/545 photos; on 150 withheld files mean ΔE improves slightly over per-file fitting (X-H2S 1.70 → 1.62, X-T4 2.43 → 2.34); 23 of 24 references from 13 bodies accepted, including the dark X-H2S samples (8.74 → 2.25) (`docs/camera-preview-colour.md`); this is not measured camera calibration; other raws and rejected fits use a neutral fallback (camera RGB ≈ linear sRGB, `matrix_is_fallback`) with as-shot white balance, so colours are muted and not accurate. Needs our own per-camera calibration: matrices the files carry themselves (e.g. Olympus `ColorMatrix`), fitting each camera to its own embedded JPEG, then chart shots. Adobe matrices / DCPs are never used. Biggest image-quality gap today |
|
||||||
| LR-PROF-CREATIVE | Creative profiles (own) | P2 | ✅ | `cmd:develop.profile`, `crates/pipeline/src/profiles.rs` | 16 own looks in Film / Cinematic / Muted / B&W (tone + point-curve fades, colour grading, mixer / B&W mix); scale with `ctl:profile.amount`; sliders untouched |
|
| LR-PROF-CREATIVE | Creative profiles (own) | P2 | ✅ | `cmd:develop.profile`, `crates/pipeline/src/profiles.rs` | 16 own looks in Film / Cinematic / Muted / B&W (tone + point-curve fades, colour grading, mixer / B&W mix); scale with `ctl:profile.amount`; sliders untouched |
|
||||||
| LR-PROF-LEGACY | Legacy profiles | P2 | ⬜ | | |
|
| LR-PROF-LEGACY | Legacy profiles | P2 | ⬜ | | |
|
||||||
| LR-PROF-NONRAW | Profiles for non-raw files | P0 | ✅ | `cmd:develop.profile` | same looks apply to JPEG/TIFF |
|
| LR-PROF-NONRAW | Profiles for non-raw files | P0 | ✅ | `cmd:develop.profile` | same looks apply to JPEG/TIFF |
|
||||||
|
|||||||
@@ -40,6 +40,10 @@
|
|||||||
- Samsung SRW files without compression now open as raws: NX5, NX10, NX11, NX20, NX200, NX210, NX1000, NX1100, EX1
|
- Samsung SRW files without compression now open as raws: NX5, NX10, NX11, NX20, NX200, NX210, NX1000, NX1100, EX1
|
||||||
and WB2000. The compressed ones (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini) still open from their
|
and WB2000. The compressed ones (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini) still open from their
|
||||||
camera JPEG. Photos already imported pick the change up on Reload.
|
camera JPEG. Photos already imported pick the change up on Reload.
|
||||||
|
- Raws from 52 camera models (22 Canon, 11 Nikon, 11 Sony, 3 Fujifilm and 5 others, plus their other names) start
|
||||||
|
from colour matrices fitted to the camera's measured spectral sensitivities, from the Academy Software
|
||||||
|
Foundation's rawtoaces-data. A camera profile for the model still comes first. See
|
||||||
|
[camera preview colour](camera-preview-colour.md#spectral-camera-matrices-and-the-order-of-precedence).
|
||||||
- Canon CR2 and Pentax PEF raws get the same starting look fitted to the camera's own JPEG as ARW, NEF, RW2, RAF
|
- Canon CR2 and Pentax PEF raws get the same starting look fitted to the camera's own JPEG as ARW, NEF, RW2, RAF
|
||||||
and CR3, instead of opening flat and desaturated (issue #310). Photos already imported pick it up when re-rendered.
|
and CR3, instead of opening flat and desaturated (issue #310). Photos already imported pick it up when re-rendered.
|
||||||
- JPEG XL compressed DNGs (DNG 1.7) now open: lossless tiles decode sample for sample (checked on synthetic files);
|
- JPEG XL compressed DNGs (DNG 1.7) now open: lossless tiles decode sample for sample (checked on synthetic files);
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Python packages for tools/spectral_camera_table.py (crates/raw/src/spectral_table.rs).
|
||||||
|
# python3 -m venv .venv-spectral && .venv-spectral/bin/pip install -r tools/requirements-spectral-camera-table.txt
|
||||||
|
numpy==2.5.3
|
||||||
|
scipy==1.18.1
|
||||||
|
colour-science==0.4.7
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Writes crates/raw/src/spectral_table.rs: DNG-shaped colour matrices per camera model, fitted to the
|
||||||
|
# measured spectral sensitivities in rawtoaces-data (Academy Software Foundation, Apache-2.0).
|
||||||
|
#
|
||||||
|
# git clone https://github.com/AcademySoftwareFoundation/rawtoaces-data ../rawtoaces-data
|
||||||
|
# git -C ../rawtoaces-data checkout v1.1.0
|
||||||
|
# python3 -m venv .venv-spectral && .venv-spectral/bin/pip install -r tools/requirements-spectral-camera-table.txt
|
||||||
|
# .venv-spectral/bin/python tools/spectral_camera_table.py ../rawtoaces-data crates/raw/src/spectral_table.rs
|
||||||
|
#
|
||||||
|
# Method (Academy P-2013-001, written from its description; no rawtoaces code is used): the camera and the CIE 1931
|
||||||
|
# observer both look at the 190 training reflectances of rawtoaces-data under CIE Standard Illuminant A and under D65.
|
||||||
|
# The camera values are white balanced to that illuminant, and a white-preserving 3x3 to XYZ is fitted by least
|
||||||
|
# mean CIE76 error in Lab. Per illuminant this gives ColorMatrix (XYZ -> camera, DNG CalibrationIlluminant 17 / 21)
|
||||||
|
# and ForwardMatrix (white-balanced camera -> XYZ D50). The aliases are rawtoaces-data's own `data/aliases.json`.
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import colour
|
||||||
|
import numpy as np
|
||||||
|
from scipy.optimize import minimize
|
||||||
|
|
||||||
|
WL = np.arange(380, 781, 5)
|
||||||
|
D50 = colour.xy_to_XYZ(colour.CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D50"])
|
||||||
|
|
||||||
|
|
||||||
|
def spectral(path, columns):
|
||||||
|
data = json.load(open(path))["spectral_data"]["data"]["main"]
|
||||||
|
return np.array([[data[str(w)][i] for i in range(columns)] for w in WL], dtype=float)
|
||||||
|
|
||||||
|
|
||||||
|
def illuminant(name):
|
||||||
|
sd = colour.SDS_ILLUMINANTS[name]
|
||||||
|
return np.array([sd[w] for w in WL], dtype=float)
|
||||||
|
|
||||||
|
|
||||||
|
def fit(cmf, refl, ssf, light, target_white):
|
||||||
|
"""White-balanced camera RGB -> XYZ adapted to `target_white`; maps (1, 1, 1) to `target_white`."""
|
||||||
|
w = ssf.T @ light
|
||||||
|
cam = (ssf.T @ (light[:, None] * refl)) / w[:, None]
|
||||||
|
k = 1.0 / (cmf[:, 1] @ light)
|
||||||
|
xyz = k * (cmf.T @ (light[:, None] * refl))
|
||||||
|
white = k * (cmf.T @ light)
|
||||||
|
cat = colour.adaptation.matrix_chromatic_adaptation_VonKries(white, target_white, transform="Bradford")
|
||||||
|
xyz = cat @ xyz
|
||||||
|
target_xy = colour.XYZ_to_xy(target_white)
|
||||||
|
lab = colour.XYZ_to_Lab(xyz.T, target_xy)
|
||||||
|
|
||||||
|
def build(p):
|
||||||
|
return np.stack([p[:3], p[3:], target_white - p[:3] - p[3:]], axis=1)
|
||||||
|
|
||||||
|
def loss(p):
|
||||||
|
return float(np.mean(np.linalg.norm(colour.XYZ_to_Lab((build(p) @ cam).T, target_xy) - lab, axis=1)))
|
||||||
|
|
||||||
|
m0 = xyz @ np.linalg.pinv(cam)
|
||||||
|
r = minimize(loss, np.concatenate([m0[:, 0], m0[:, 1]]), method="Nelder-Mead", options={"maxiter": 20000, "xatol": 1e-9, "fatol": 1e-9})
|
||||||
|
return build(r.x), loss(r.x)
|
||||||
|
|
||||||
|
|
||||||
|
def dng_tags(cmf, refl, ssf):
|
||||||
|
"""[(ColorMatrix, ForwardMatrix, mean training dE76)] for Standard Illuminant A, then D65."""
|
||||||
|
out = []
|
||||||
|
for name in ("A", "D65"):
|
||||||
|
light = illuminant(name)
|
||||||
|
forward, error = fit(cmf, refl, ssf, light, D50)
|
||||||
|
k = 1.0 / (cmf[:, 1] @ light)
|
||||||
|
white = k * (cmf.T @ light)
|
||||||
|
to_xyz, _ = fit(cmf, refl, ssf, light, white)
|
||||||
|
cam_from_xyz = np.diag(ssf.T @ light) @ np.linalg.inv(to_xyz)
|
||||||
|
cam_from_xyz = cam_from_xyz / (cam_from_xyz @ white).max()
|
||||||
|
out.append((cam_from_xyz.round(6), forward.round(6), error))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def mat(m):
|
||||||
|
return "Mat3([" + ", ".join("[" + ", ".join(f"{v:.6}" for v in row) + "]" for row in m) + "])"
|
||||||
|
|
||||||
|
|
||||||
|
def main(root, out):
|
||||||
|
commit = subprocess.run(["git", "-C", root, "rev-parse", "HEAD"], capture_output=True, text=True, check=True).stdout.strip()
|
||||||
|
cmf = spectral(os.path.join(root, "data/cmf/cmf_1931.json"), 3)
|
||||||
|
training = json.load(open(os.path.join(root, "data/training/training_spectral.json")))["spectral_data"]["data"]["main"]
|
||||||
|
refl = np.array([training[str(w)] for w in WL], dtype=float)
|
||||||
|
rows = []
|
||||||
|
for f in sorted(os.listdir(os.path.join(root, "data/camera"))):
|
||||||
|
if not f.endswith("_380_780_5.json"):
|
||||||
|
continue
|
||||||
|
path = os.path.join(root, "data/camera", f)
|
||||||
|
header = json.load(open(path))["header"]
|
||||||
|
if header.get("license") != "Apache-2.0":
|
||||||
|
sys.exit(f"{f}: licence {header.get('license')!r}, expected Apache-2.0")
|
||||||
|
(cm1, fm1, e1), (cm2, fm2, e2) = dng_tags(cmf, refl, spectral(path, 3))
|
||||||
|
print(f"{f}: training dE76 A {e1:.3f}, D65 {e2:.3f}", file=sys.stderr)
|
||||||
|
rows.append(
|
||||||
|
f' Camera {{\n make: "{header["manufacturer"]}",\n model: "{header["model"]}",\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"]
|
||||||
|
alias_rows = [
|
||||||
|
f' ("{make}", "{alias}", "{target["model"]}"), // {target.get("comments", "")}'
|
||||||
|
for make, models in aliases.items()
|
||||||
|
for alias, target in models.items()
|
||||||
|
]
|
||||||
|
rust = (
|
||||||
|
"// Generated by tools/spectral_camera_table.py from rawtoaces-data "
|
||||||
|
f"(https://github.com/AcademySoftwareFoundation/rawtoaces-data, commit {commit}).\n"
|
||||||
|
"// Derived from the camera spectral sensitivities of rawtoaces-data (Academy Software Foundation), licensed under\n"
|
||||||
|
"// the Apache License 2.0 (LICENSE-APACHE); see NOTICE and assets/ATTRIBUTION.md. Do not edit by hand.\n"
|
||||||
|
"use super::Camera;\n"
|
||||||
|
"use lightcraft_color::Mat3;\n\n"
|
||||||
|
"#[rustfmt::skip]\npub(super) const CAMERAS: &[Camera] = &[\n" + "\n".join(rows) + "\n];\n\n"
|
||||||
|
"/// `(make, model as sold, table model)`: other names of a camera in [`CAMERAS`], or of a camera with the same sensor.\n"
|
||||||
|
"#[rustfmt::skip]\npub(super) const ALIASES: &[(&str, &str, &str)] = &[\n" + "\n".join(alias_rows) + "\n];\n"
|
||||||
|
)
|
||||||
|
open(out, "w").write(rust)
|
||||||
|
print(f"{len(rows)} cameras, {len(alias_rows)} aliases -> {out}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
sys.exit("usage: spectral_camera_table.py <rawtoaces-data checkout> <output .rs>")
|
||||||
|
main(sys.argv[1], sys.argv[2])
|
||||||
Reference in new issue
Block a user