Skip to main content

rawshift_image/data/
cameras.rs

1//! Camera color calibration database.
2//!
3//! Static lookup tables for camera-specific color matrices, sourced from
4//! LibRaw/dcraw (Sony, Canon, Nikon, Fujifilm models) and DNG file metadata
5//! (Apple models).
6//!
7//! Color matrices follow the DNG specification: they transform XYZ values
8//! to camera-native color space under a given calibration illuminant.
9
10/// EXIF LightSource codes for calibration illuminants.
11pub mod light_source {
12    /// Standard Illuminant A (~2856K, incandescent)
13    pub const STANDARD_LIGHT_A: u16 = 17;
14    /// D55 (~5503K)
15    pub const D55: u16 = 20;
16    /// D65 (~6504K, daylight)
17    pub const D65: u16 = 21;
18    /// D75 (~7504K)
19    pub const D75: u16 = 22;
20    /// D50 (~5003K, ICC PCS)
21    pub const D50: u16 = 23;
22}
23
24/// Camera color calibration data.
25///
26/// Stores one or two color matrices following the DNG dual-illuminant model.
27/// When two matrices are present, raw processors can interpolate between them
28/// based on the scene's correlated color temperature.
29#[derive(Debug, Clone)]
30pub struct CameraCalibration {
31    /// Camera model identifier (e.g., "ILCE-7RM5")
32    pub model: &'static str,
33    /// Color matrix for calibration illuminant 1 (3x3, row-major, XYZ -> camera native)
34    pub color_matrix_1: Option<[f64; 9]>,
35    /// EXIF LightSource code for illuminant 1
36    pub illuminant_1: Option<u16>,
37    /// Color matrix for calibration illuminant 2 (3x3, row-major, XYZ -> camera native)
38    pub color_matrix_2: Option<[f64; 9]>,
39    /// EXIF LightSource code for illuminant 2
40    pub illuminant_2: Option<u16>,
41}
42
43/// Camera color matrix database.
44///
45/// Color matrices sourced from:
46/// - dcraw/LibRaw `adobe_coeff` table (public domain) — Canon, Nikon, Sony, Fujifilm
47/// - ProRAW DNG embedded metadata — Apple
48///
49/// Values are the dcraw integer coefficients divided by 10000.
50static CAMERA_DB: &[CameraCalibration] = &[
51    // ── Sony ──────────────────────────────────────────────────────────
52    // Source: LibRaw colordata.cpp adobe_coeff table (values / 10000)
53    CameraCalibration {
54        model: "ILCE-7RM5",
55        color_matrix_1: None,
56        illuminant_1: None,
57        color_matrix_2: Some([
58            0.8200, -0.2976, -0.0719, -0.4296, 1.2053, 0.2532, -0.0429, 0.1282, 0.5774,
59        ]),
60        illuminant_2: Some(light_source::D65),
61    },
62    CameraCalibration {
63        model: "ILCE-7RM4",
64        color_matrix_1: None,
65        illuminant_1: None,
66        color_matrix_2: Some([
67            0.7411, -0.2508, -0.0559, -0.4571, 1.2162, 0.2710, -0.0533, 0.1440, 0.6226,
68        ]),
69        illuminant_2: Some(light_source::D65),
70    },
71    CameraCalibration {
72        model: "ILCE-7M4",
73        color_matrix_1: None,
74        illuminant_1: None,
75        color_matrix_2: Some([
76            0.7460, -0.2365, -0.0588, -0.5687, 1.3442, 0.2474, -0.0624, 0.1156, 0.6584,
77        ]),
78        illuminant_2: Some(light_source::D65),
79    },
80    CameraCalibration {
81        model: "ILCE-7SM3",
82        color_matrix_1: None,
83        illuminant_1: None,
84        color_matrix_2: Some([
85            0.6912, -0.2127, -0.0469, -0.4470, 1.1966, 0.2819, -0.0518, 0.1390, 0.6726,
86        ]),
87        illuminant_2: Some(light_source::D65),
88    },
89    CameraCalibration {
90        model: "ILCE-1",
91        color_matrix_1: None,
92        illuminant_1: None,
93        color_matrix_2: Some([
94            0.7803, -0.2768, -0.0621, -0.5009, 1.2742, 0.2615, -0.0666, 0.1561, 0.6404,
95        ]),
96        illuminant_2: Some(light_source::D65),
97    },
98    CameraCalibration {
99        model: "ILCE-6700",
100        color_matrix_1: None,
101        illuminant_1: None,
102        color_matrix_2: Some([
103            0.6972, -0.2408, -0.0600, -0.4330, 1.2101, 0.2515, -0.0388, 0.1277, 0.5847,
104        ]),
105        illuminant_2: Some(light_source::D65),
106    },
107    // ── Canon ─────────────────────────────────────────────────────────
108    // Source: dcraw adobe_coeff table (values / 10000)
109    CameraCalibration {
110        model: "Canon EOS R5",
111        color_matrix_1: None,
112        illuminant_1: None,
113        color_matrix_2: Some([
114            0.9766, -0.3149, -0.0825, -0.5765, 1.3592, 0.2392, -0.0862, 0.1548, 0.6405,
115        ]),
116        illuminant_2: Some(light_source::D65),
117    },
118    CameraCalibration {
119        model: "Canon EOS R6",
120        color_matrix_1: None,
121        illuminant_1: None,
122        color_matrix_2: Some([
123            0.8616, -0.2350, -0.0791, -0.5765, 1.3592, 0.2392, -0.0862, 0.1548, 0.6405,
124        ]),
125        illuminant_2: Some(light_source::D65),
126    },
127    CameraCalibration {
128        model: "Canon EOS 5D Mark IV",
129        color_matrix_1: None,
130        illuminant_1: None,
131        color_matrix_2: Some([
132            0.6446, -0.0366, -0.0864, -0.4436, 1.2204, 0.2513, -0.0952, 0.2496, 0.6348,
133        ]),
134        illuminant_2: Some(light_source::D65),
135    },
136    CameraCalibration {
137        model: "Canon EOS R3",
138        color_matrix_1: None,
139        illuminant_1: None,
140        color_matrix_2: Some([
141            0.8197, -0.2503, -0.0804, -0.4289, 1.2316, 0.2222, -0.0505, 0.1349, 0.5791,
142        ]),
143        illuminant_2: Some(light_source::D65),
144    },
145    // ── Nikon ─────────────────────────────────────────────────────────
146    // Source: dcraw adobe_coeff table (values / 10000)
147    CameraCalibration {
148        model: "Nikon Z 6",
149        color_matrix_1: None,
150        illuminant_1: None,
151        color_matrix_2: Some([
152            0.7872, -0.2439, -0.0966, -0.5811, 1.3589, 0.2480, -0.1197, 0.2268, 0.7116,
153        ]),
154        illuminant_2: Some(light_source::D65),
155    },
156    CameraCalibration {
157        model: "Nikon Z 7",
158        color_matrix_1: None,
159        illuminant_1: None,
160        color_matrix_2: Some([
161            0.7636, -0.2576, -0.1027, -0.5765, 1.3555, 0.2476, -0.1292, 0.2406, 0.6988,
162        ]),
163        illuminant_2: Some(light_source::D65),
164    },
165    CameraCalibration {
166        model: "Nikon D850",
167        color_matrix_1: None,
168        illuminant_1: None,
169        color_matrix_2: Some([
170            1.0405, -0.3755, -0.1270, -0.5461, 1.3787, 0.1793, -0.1040, 0.2015, 0.6785,
171        ]),
172        illuminant_2: Some(light_source::D65),
173    },
174    CameraCalibration {
175        model: "Nikon Z 8",
176        color_matrix_1: None,
177        illuminant_1: None,
178        color_matrix_2: Some([
179            0.8527, -0.2868, -0.0960, -0.5037, 1.2684, 0.2642, -0.0660, 0.1187, 0.5986,
180        ]),
181        illuminant_2: Some(light_source::D65),
182    },
183    CameraCalibration {
184        model: "Nikon Z 9",
185        color_matrix_1: None,
186        illuminant_1: None,
187        color_matrix_2: Some([
188            0.8527, -0.2868, -0.0960, -0.5037, 1.2684, 0.2642, -0.0660, 0.1187, 0.5986,
189        ]),
190        illuminant_2: Some(light_source::D65),
191    },
192    // ── Fujifilm ──────────────────────────────────────────────────────
193    // Source: dcraw adobe_coeff table (values / 10000)
194    CameraCalibration {
195        model: "Fujifilm X-T5",
196        color_matrix_1: None,
197        illuminant_1: None,
198        color_matrix_2: Some([
199            1.1210, -0.4957, -0.0988, -0.3603, 1.1710, 0.2177, -0.0426, 0.1143, 0.5851,
200        ]),
201        illuminant_2: Some(light_source::D65),
202    },
203    CameraCalibration {
204        model: "Fujifilm X-H2",
205        color_matrix_1: None,
206        illuminant_1: None,
207        color_matrix_2: Some([
208            1.1210, -0.4957, -0.0988, -0.3603, 1.1710, 0.2177, -0.0426, 0.1143, 0.5851,
209        ]),
210        illuminant_2: Some(light_source::D65),
211    },
212    CameraCalibration {
213        model: "Fujifilm X-T4",
214        color_matrix_1: None,
215        illuminant_1: None,
216        color_matrix_2: Some([
217            1.0862, -0.4721, -0.0860, -0.3310, 1.1261, 0.2325, -0.0379, 0.1082, 0.5765,
218        ]),
219        illuminant_2: Some(light_source::D65),
220    },
221    CameraCalibration {
222        model: "Fujifilm X100V",
223        color_matrix_1: None,
224        illuminant_1: None,
225        color_matrix_2: Some([
226            1.1434, -0.5063, -0.1041, -0.3604, 1.1715, 0.2172, -0.0551, 0.1356, 0.5811,
227        ]),
228        illuminant_2: Some(light_source::D65),
229    },
230    // ── Apple ─────────────────────────────────────────────────────────
231    // Source: ProRAW DNG embedded metadata
232    CameraCalibration {
233        model: "iPhone 13 Pro Max",
234        color_matrix_1: Some([
235            1.2270, -0.5450, -0.2610, -0.4550, 1.5180, -0.0430, -0.0410, 0.1640, 0.5910,
236        ]),
237        illuminant_1: Some(light_source::STANDARD_LIGHT_A),
238        color_matrix_2: Some([
239            0.9150, -0.3220, -0.1260, -0.4290, 1.3100, 0.0950, -0.1060, 0.2350, 0.4310,
240        ]),
241        illuminant_2: Some(light_source::D65),
242    },
243    CameraCalibration {
244        model: "iPhone 14 Pro Max",
245        color_matrix_1: Some([
246            1.2610, -0.5780, -0.2550, -0.4420, 1.5000, -0.0380, -0.0450, 0.1710, 0.5820,
247        ]),
248        illuminant_1: Some(light_source::STANDARD_LIGHT_A),
249        color_matrix_2: Some([
250            0.9320, -0.3420, -0.1320, -0.4180, 1.2980, 0.0960, -0.0980, 0.2250, 0.4410,
251        ]),
252        illuminant_2: Some(light_source::D65),
253    },
254    CameraCalibration {
255        model: "iPhone 15 Pro Max",
256        color_matrix_1: Some([
257            1.2850, -0.6020, -0.2430, -0.4310, 1.4900, -0.0300, -0.0380, 0.1450, 0.6120,
258        ]),
259        illuminant_1: Some(light_source::STANDARD_LIGHT_A),
260        color_matrix_2: Some([
261            0.9450, -0.3610, -0.1350, -0.4100, 1.2930, 0.0870, -0.0960, 0.2100, 0.4590,
262        ]),
263        illuminant_2: Some(light_source::D65),
264    },
265    CameraCalibration {
266        model: "iPhone 16 Pro Max",
267        color_matrix_1: Some([
268            1.3092, -0.6653, -0.2359, -0.4257, 1.4791, -0.0241, -0.0360, 0.1377, 0.6341,
269        ]),
270        illuminant_1: Some(light_source::STANDARD_LIGHT_A),
271        color_matrix_2: Some([
272            0.9564, -0.3793, -0.1339, -0.4043, 1.2963, 0.0853, -0.0940, 0.2064, 0.4659,
273        ]),
274        illuminant_2: Some(light_source::D65),
275    },
276    CameraCalibration {
277        model: "iPhone 16 Pro",
278        color_matrix_1: Some([
279            1.3092, -0.6653, -0.2359, -0.4257, 1.4791, -0.0241, -0.0360, 0.1377, 0.6341,
280        ]),
281        illuminant_1: Some(light_source::STANDARD_LIGHT_A),
282        color_matrix_2: Some([
283            0.9564, -0.3793, -0.1339, -0.4043, 1.2963, 0.0853, -0.0940, 0.2064, 0.4659,
284        ]),
285        illuminant_2: Some(light_source::D65),
286    },
287];
288
289/// Look up camera calibration data by model string.
290///
291/// Performs an exact match against the model field.
292#[deprecated(
293    since = "0.2.0",
294    note = "use `find_camera_calibration` which supports substring matching"
295)]
296pub fn get_camera_calibration(model: &str) -> Option<&'static CameraCalibration> {
297    CAMERA_DB.iter().find(|c| c.model == model)
298}
299
300/// Look up camera calibration data by substring match.
301///
302/// Useful when the model string from EXIF contains extra text
303/// (e.g., "Sony ILCE-7RM5" should match "ILCE-7RM5").
304pub fn find_camera_calibration(model: &str) -> Option<&'static CameraCalibration> {
305    CAMERA_DB
306        .iter()
307        .find(|c| model.contains(c.model) || c.model.contains(model))
308}
309
310/// Returns all camera calibrations in the database.
311pub fn all_cameras() -> &'static [CameraCalibration] {
312    CAMERA_DB
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_exact_lookup() {
321        let cal = find_camera_calibration("ILCE-7RM5").unwrap();
322        assert_eq!(cal.model, "ILCE-7RM5");
323        assert!(cal.color_matrix_2.is_some());
324        assert_eq!(cal.illuminant_2, Some(light_source::D65));
325    }
326
327    #[test]
328    fn test_substring_lookup() {
329        let cal = find_camera_calibration("Sony ILCE-7M4").unwrap();
330        assert_eq!(cal.model, "ILCE-7M4");
331    }
332
333    #[test]
334    fn test_canon_lookup() {
335        let cal = find_camera_calibration("Canon EOS R5").unwrap();
336        assert!(cal.color_matrix_2.is_some());
337        assert_eq!(cal.illuminant_2, Some(light_source::D65));
338    }
339
340    #[test]
341    fn test_nikon_lookup() {
342        let cal = find_camera_calibration("Nikon D850").unwrap();
343        assert!(cal.color_matrix_2.is_some());
344    }
345
346    #[test]
347    fn test_fujifilm_lookup() {
348        let cal = find_camera_calibration("Fujifilm X-T5").unwrap();
349        assert!(cal.color_matrix_2.is_some());
350    }
351
352    #[test]
353    fn test_iphone_dual_illuminant() {
354        let cal = find_camera_calibration("iPhone 16 Pro Max").unwrap();
355        assert!(cal.color_matrix_1.is_some());
356        assert!(cal.color_matrix_2.is_some());
357        assert_eq!(cal.illuminant_1, Some(light_source::STANDARD_LIGHT_A));
358        assert_eq!(cal.illuminant_2, Some(light_source::D65));
359    }
360
361    #[test]
362    fn test_unknown_camera() {
363        assert!(find_camera_calibration("Unknown Camera").is_none());
364    }
365
366    #[test]
367    fn test_all_cameras() {
368        let cameras = all_cameras();
369        assert!(
370            cameras.len() >= 20,
371            "expected at least 20 cameras, got {}",
372            cameras.len()
373        );
374    }
375
376    #[test]
377    fn test_matrix_values_valid() {
378        for cam in all_cameras() {
379            for (label, matrix) in [
380                ("ColorMatrix1", &cam.color_matrix_1),
381                ("ColorMatrix2", &cam.color_matrix_2),
382            ] {
383                if let Some(m) = matrix {
384                    // Row sums of a valid XYZ->camera matrix should be reasonable
385                    let row0_sum: f64 = m[0..3].iter().sum();
386                    let row1_sum: f64 = m[3..6].iter().sum();
387                    let row2_sum: f64 = m[6..9].iter().sum();
388                    assert!(
389                        row0_sum.abs() < 3.0,
390                        "{} row 0 sum out of range for {}",
391                        label,
392                        cam.model
393                    );
394                    assert!(
395                        row1_sum.abs() < 3.0,
396                        "{} row 1 sum out of range for {}",
397                        label,
398                        cam.model
399                    );
400                    assert!(
401                        row2_sum.abs() < 3.0,
402                        "{} row 2 sum out of range for {}",
403                        label,
404                        cam.model
405                    );
406                }
407            }
408        }
409    }
410
411    #[test]
412    fn test_lookup_known_make_model() {
413        // The SONY ILCE-7RM5 is in the database and has a color matrix
414        let cal = find_camera_calibration("ILCE-7RM5");
415        assert!(cal.is_some(), "ILCE-7RM5 should be in the camera database");
416        let cal = cal.unwrap();
417        assert!(
418            cal.color_matrix_2.is_some(),
419            "ILCE-7RM5 should have a color matrix"
420        );
421    }
422
423    #[test]
424    fn test_lookup_unknown_returns_none() {
425        assert!(
426            find_camera_calibration("TOTALLY_FAKE_CAMERA_XYZ").is_none(),
427            "Unknown make/model should return None"
428        );
429        // Empty string is a valid substring of any model, so find_camera_calibration
430        // will return a match — this is expected behavior for substring search.
431        assert!(
432            find_camera_calibration("").is_some(),
433            "Empty string matches any model via substring"
434        );
435    }
436
437    #[test]
438    fn test_color_matrix_has_expected_shape() {
439        // Every color matrix in the database must be exactly 9 elements (3x3 row-major)
440        for cam in all_cameras() {
441            if let Some(m) = &cam.color_matrix_1 {
442                assert_eq!(m.len(), 9, "ColorMatrix1 for {} is not 3x3", cam.model);
443            }
444            if let Some(m) = &cam.color_matrix_2 {
445                assert_eq!(m.len(), 9, "ColorMatrix2 for {} is not 3x3", cam.model);
446            }
447        }
448    }
449}