Raw-rs: Add preprocessing and demosaicing steps (#1796)

* add subtract black step

* add scale colors step

* add raw to image step

* implement linear demosiacing and fix errors in previous code

* fix missing variable

* make dependencies of tests optional

* fix error in raw-rs tests

* fix typo in "demosiacing"

* use camera data from ADC and remove downloader

* cargo fmt

* use file_stem instead of file_name

* remove old camera data

* use equality instead of subtring to find model

* store camera_to_xyz in decimal form

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Elbert Ronnie 2024-08-11 14:06:50 +05:30 committed by GitHub
parent 1b6d45ad30
commit a9cfeeb219
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 688 additions and 56 deletions

64
Cargo.lock generated
View File

@ -721,12 +721,28 @@ dependencies = [
"serde",
]
[[package]]
name = "build-camera-data"
version = "0.0.1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.72",
"toml 0.8.15",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "by_address"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
[[package]]
name = "bytemuck"
version = "1.16.1"
@ -1396,19 +1412,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "downloader"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05213e96f184578b5f70105d4d0a644a168e99e12d7bea0b200c15d67b5c182"
dependencies = [
"futures",
"rand 0.8.5",
"reqwest 0.11.27",
"thiserror",
"tokio",
]
[[package]]
name = "dtoa"
version = "1.0.9"
@ -1592,6 +1595,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "fast-srgb8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]]
name = "fastnoise-lite"
version = "1.1.1"
@ -4188,6 +4197,30 @@ dependencies = [
"ttf-parser 0.24.0",
]
[[package]]
name = "palette"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
dependencies = [
"approx",
"fast-srgb8",
"palette_derive",
"phf 0.11.2",
]
[[package]]
name = "palette_derive"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
dependencies = [
"by_address",
"proc-macro2",
"quote",
"syn 2.0.72",
]
[[package]]
name = "pango"
version = "0.15.10"
@ -4784,9 +4817,12 @@ name = "raw-rs"
version = "0.0.1"
dependencies = [
"bitstream-io",
"downloader",
"build-camera-data",
"image 0.25.2",
"libraw-rs",
"num_enum 0.7.2",
"palette",
"reqwest 0.12.5",
"tag-derive",
"thiserror",
]

View File

@ -19,6 +19,7 @@ members = [
"libraries/bezier-rs",
"libraries/raw-rs",
"libraries/raw-rs/tag-derive",
"libraries/raw-rs/build-camera-data",
"website/other/bezier-rs-demos/wasm",
]
exclude = ["node-graph/gpu-compiler"]

View File

@ -13,20 +13,24 @@ repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/r
documentation = "https://docs.rs/raw-rs"
[features]
raw-rs-tests = ["libraw-rs"]
raw-rs-tests = ["dep:image", "dep:libraw-rs", "dep:palette", "dep:reqwest"]
[dependencies]
# Local dependencies
tag-derive = { path = "tag-derive" }
build-camera-data = { path = "build-camera-data" }
# Workspace dependencies
thiserror = { workspace = true }
# Required dependencies
bitstream-io = "2.3.0"
num_enum = "0.7.2"
thiserror = "1.0"
# Optional dependencies
libraw-rs = { version = "0.0.4", optional = true } # Should be a dev dependency, but Cargo currently doesn't allow optional dev dependencies
# Optional workspace dependencies
image = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true }
[dev-dependencies]
# Required dependencies
downloader = "0.2.7"
# Optional dependencies (should be dev dependencies, but Cargo currently doesn't allow optional dev dependencies)
libraw-rs = { version = "0.0.4", optional = true }
palette = { version = "0.7.6", optional = true }

View File

@ -0,0 +1,21 @@
[package]
name = "build-camera-data"
version = "0.0.1"
publish = false
edition = "2021"
authors = ["Graphite Authors <contact@graphite.rs>"]
description = "Procedural macro to build the camera data into rust code"
license = "MIT OR Apache-2.0"
repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/raw-rs/build-camera-data"
[lib]
proc-macro = true
[dependencies]
# Workspace dependencies
quote = { workspace = true }
syn = { workspace = true }
proc-macro2 = { workspace = true }
# Required dependencies
toml = "0.8.15"

View File

@ -0,0 +1,101 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use toml::{Table, Value};
use std::fs;
use std::path::Path;
enum CustomValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<CustomValue>),
}
impl ToTokens for CustomValue {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self {
CustomValue::String(x) => x.to_tokens(tokens),
CustomValue::Integer(x) => {
let x: proc_macro2::TokenStream = format!("{:?}", x).parse().unwrap();
x.to_tokens(tokens)
}
CustomValue::Float(x) => {
let x: proc_macro2::TokenStream = format!("{:?}", x).parse().unwrap();
x.to_tokens(tokens)
}
CustomValue::Boolean(x) => x.to_tokens(tokens),
CustomValue::Array(x) => quote! { [ #( #x ),* ] }.to_tokens(tokens),
}
}
}
impl From<Value> for CustomValue {
fn from(value: Value) -> Self {
match value {
Value::String(x) => CustomValue::String(x),
Value::Integer(x) => CustomValue::Integer(x),
Value::Float(x) => CustomValue::Float(x),
Value::Boolean(x) => CustomValue::Boolean(x),
Value::Array(x) => CustomValue::Array(x.into_iter().map(|x| x.into()).collect()),
_ => panic!("Unsuported data type"),
}
}
}
#[proc_macro]
pub fn build_camera_data(_: TokenStream) -> TokenStream {
let mut camera_data: Vec<(String, Table)> = Vec::new();
let mut path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).to_path_buf();
path.push("camera_data");
fs::read_dir(path).unwrap().for_each(|entry| {
let company_name_path = entry.unwrap().path();
if !company_name_path.is_dir() {
panic!("camera_data should only contain folders of company names")
}
let company_name = company_name_path.file_name().unwrap().to_str().unwrap().to_string();
fs::read_dir(company_name_path).unwrap().for_each(|entry| {
let model_path = entry.unwrap().path();
if !model_path.is_file() || model_path.extension().unwrap() != "toml" {
panic!("The folders within camera_data should only contain toml files")
}
let name = company_name.clone() + " " + model_path.file_stem().unwrap().to_str().unwrap();
let mut values: Table = toml::from_str(&fs::read_to_string(model_path).unwrap()).unwrap();
if let Some(val) = values.get_mut("camera_to_xyz") {
*val = Value::Array(val.as_array().unwrap().iter().map(|x| Value::Integer((x.as_float().unwrap() * 10_000.) as i64)).collect());
}
camera_data.push((name, values))
});
});
let x: Vec<_> = camera_data
.iter()
.map(|(name, camera_data)| {
let keys: Vec<_> = camera_data.keys().map(|key| syn::Ident::new(key, proc_macro2::Span::call_site())).collect();
let values: Vec<CustomValue> = camera_data.values().cloned().map(|x| x.into()).collect();
quote! {
(
#name,
CameraData {
#( #keys: #values, )*
..CameraData::DEFAULT
}
)
}
})
.collect();
quote!([ #(#x),* ]).into()
}

View File

@ -0,0 +1 @@
camera_to_xyz = [0.9437, -0.2812, -0.0774, -0.8405, 1.6215, 0.2291, -0.0709, 0.0596, 0.7181]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.9847, -0.3091, -0.0929, -0.8485, 1.6346, 0.2225, -0.0714, 0.0595, 0.7103]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.9847, -0.3091, -0.0929, -0.8485, 1.6346, 0.2225, -0.0714, 0.0595, 0.7103]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.9847, -0.3091, -0.0929, -0.8485, 1.6346, 0.2225, -0.0714, 0.0595, 0.7103]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6038, -0.1484, -0.0579, -0.9145, 1.6746, 0.2512, -0.0875, 0.0746, 0.7218]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.4950, -0.0580, -0.0103, -0.5228, 1.2542, 0.3029, -0.0709, 0.1435, 0.7371]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.5775, -0.0805, -0.0359, -0.8573, 1.6294, 0.2391, -0.1943, 0.2342, 0.7249]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.5413, -0.1162, -0.0365, -0.5665, 1.3098, 0.2866, -0.0608, 0.1179, 0.8440]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.5209, -0.1072, -0.0397, -0.8845, 1.6121, 0.2919, -0.1618, 0.1802, 0.8654]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6435, -0.1903, -0.0536, -0.4722, 1.2449, 0.2550, -0.0663, 0.1363, 0.6517]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6660, -0.1918, -0.0471, -0.4613, 1.2398, 0.2485, -0.0649, 0.1433, 0.6447]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.8161, -0.2947, -0.0739, -0.4811, 1.2668, 0.2389, -0.0437, 0.1229, 0.6524]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.5991, -0.1456, -0.0455, -0.4764, 1.2135, 0.2980, -0.0707, 0.1425, 0.6701]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.5991, -0.1456, -0.0455, -0.4764, 1.2135, 0.2980, -0.0707, 0.1425, 0.6701]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.7657, -0.2847, -0.0607, -0.4083, 1.1966, 0.2389, -0.0684, 0.1418, 0.5844]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.5973, -0.1695, -0.0419, -0.3826, 1.1797, 0.2293, -0.0639, 0.1398, 0.5789]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.7657, -0.2847, -0.0607, -0.4083, 1.1966, 0.2389, -0.0684, 0.1418, 0.5844]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.5973, -0.1695, -0.0419, -0.3826, 1.1797, 0.2293, -0.0639, 0.1398, 0.5789]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.7657, -0.2847, -0.0607, -0.4083, 1.1966, 0.2389, -0.0684, 0.1418, 0.5844]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6972, -0.2408, -0.0600, -0.4330, 1.2101, 0.2515, -0.0388, 0.1277, 0.5847]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.7460, -0.2365, -0.0588, -0.5687, 1.3442, 0.2474, -0.0624, 0.1156, 0.6584]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.8200, -0.2976, -0.0719, -0.4296, 1.2053, 0.2532, -0.0429, 0.1282, 0.5774]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.5271, -0.0712, -0.0347, -0.6153, 1.3653, 0.2763, -0.1601, 0.2366, 0.7242]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.7374, -0.2389, -0.0551, -0.5435, 1.3162, 0.2519, -0.1006, 0.1795, 0.6552]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6640, -0.1847, -0.0503, -0.5238, 1.3010, 0.2474, -0.0993, 0.1673, 0.6527]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.7662, -0.2686, -0.0660, -0.5240, 1.2965, 0.2530, -0.0796, 0.1508, 0.6167]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.8200, -0.2976, -0.0719, -0.4296, 1.2053, 0.2532, -0.0429, 0.1282, 0.5774]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.5838, -0.1430, -0.0246, -0.3497, 1.1477, 0.2297, -0.0748, 0.1885, 0.5778]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6389, -0.1703, -0.0378, -0.4562, 1.2265, 0.2587, -0.0670, 0.1489, 0.6550]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6389, -0.1703, -0.0378, -0.4562, 1.2265, 0.2587, -0.0670, 0.1489, 0.6550]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.9811, -0.3908, -0.0752, -0.3704, 1.1577, 0.2417, -0.0073, 0.0950, 0.5980]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6549, -0.1550, -0.0436, -0.4880, 1.2435, 0.2753, -0.0854, 0.1868, 0.6976]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6129, -0.1545, -0.0418, -0.4930, 1.2490, 0.2743, -0.0977, 0.1693, 0.6615]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6129, -0.1545, -0.0418, -0.4930, 1.2490, 0.2743, -0.0977, 0.1693, 0.6615]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6129, -0.1545, -0.0418, -0.4930, 1.2490, 0.2743, -0.0977, 0.1693, 0.6615]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.5491, -0.1192, -0.0363, -0.4951, 1.2342, 0.2948, -0.0911, 0.1722, 0.7192]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.8280, -0.2987, -0.0703, -0.3531, 1.1645, 0.2133, -0.0550, 0.1542, 0.5312]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.8280, -0.2987, -0.0703, -0.3531, 1.1645, 0.2133, -0.0550, 0.1542, 0.5312]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6912, -0.2127, -0.0469, -0.4470, 1.2175, 0.2587, -0.0398, 0.1478, 0.6492]

View File

@ -0,0 +1 @@
camera_to_xyz = [0.6355, -0.2067, -0.0490, -0.3653, 1.1542, 0.2400, -0.0406, 0.1258, 0.5506]

View File

@ -1,7 +1,7 @@
use crate::tiff::file::TiffRead;
use crate::tiff::tags::SonyDataOffset;
use crate::tiff::Ifd;
use crate::RawImage;
use crate::{RawImage, SubtractBlack};
use bitstream_io::{BitRead, BitReader, Endianness, BE};
use std::io::{Read, Seek};
@ -22,6 +22,10 @@ pub fn decode_a100<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage
data: image,
width: image_width,
height: image_height,
cfa_pattern: todo!(),
maximum: (1 << 12) - 1,
black: SubtractBlack::None,
camera_to_xyz: None,
}
}

View File

@ -2,7 +2,7 @@ use crate::tiff::file::{Endian, TiffRead};
use crate::tiff::tags::{BitsPerSample, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, SonyToneCurve, StripByteCounts, StripOffsets, Tag};
use crate::tiff::values::CurveLookupTable;
use crate::tiff::{Ifd, TiffError};
use crate::RawImage;
use crate::{RawImage, SubtractBlack};
use std::io::{Read, Seek};
use tag_derive::Tag;
@ -30,7 +30,9 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
let image_width: usize = ifd.image_width.try_into().unwrap();
let image_height: usize = ifd.image_height.try_into().unwrap();
let _bits_per_sample: usize = ifd.bits_per_sample.into();
let bits_per_sample: usize = ifd.bits_per_sample.into();
assert!(bits_per_sample == 12);
let [cfa_pattern_width, cfa_pattern_height] = ifd.cfa_pattern_dim;
assert!(cfa_pattern_width == 2 && cfa_pattern_height == 2);
@ -44,6 +46,10 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
data: image,
width: image_width,
height: image_height,
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
maximum: (1 << 14) - 1,
black: SubtractBlack::CfaGrid([512, 512, 512, 512]), // TODO: Find the correct way to do this
camera_to_xyz: None,
}
}

View File

@ -1,7 +1,8 @@
use crate::tiff::file::TiffRead;
use crate::tiff::tags::{BitsPerSample, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, RowsPerStrip, StripByteCounts, StripOffsets, Tag};
use crate::tiff::tags::{BitsPerSample, BlackLevel, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, RowsPerStrip, StripByteCounts, StripOffsets, Tag};
use crate::tiff::{Ifd, TiffError};
use crate::RawImage;
use crate::{RawImage, SubtractBlack};
use std::io::{Read, Seek};
use tag_derive::Tag;
@ -13,6 +14,7 @@ struct ArwUncompressedIfd {
rows_per_strip: RowsPerStrip,
bits_per_sample: BitsPerSample,
compression: Compression,
black_level: BlackLevel,
cfa_pattern: CfaPattern,
cfa_pattern_dim: CfaPatternDim,
strip_offsets: StripOffsets,
@ -29,7 +31,7 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
let image_width: usize = ifd.image_width.try_into().unwrap();
let image_height: usize = ifd.image_height.try_into().unwrap();
let rows_per_strip: usize = ifd.rows_per_strip.try_into().unwrap();
let _bits_per_sample: usize = ifd.bits_per_sample.into();
let bits_per_sample: usize = ifd.bits_per_sample.into();
let [cfa_pattern_width, cfa_pattern_height] = ifd.cfa_pattern_dim;
assert!(cfa_pattern_width == 2 && cfa_pattern_height == 2);
@ -52,5 +54,9 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
data: image,
width: image_width,
height: image_height,
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
maximum: if bits_per_sample == 16 { u16::MAX } else { (1 << bits_per_sample) - 1 },
black: SubtractBlack::CfaGrid(ifd.black_level),
camera_to_xyz: None,
}
}

View File

@ -0,0 +1,77 @@
use crate::{Image, RawImage};
fn average(data: &[u16], indexes: impl Iterator<Item = i64>) -> u16 {
let mut sum = 0;
let mut count = 0;
for index in indexes {
if index >= 0 && (index as usize) < data.len() {
sum += data[index as usize] as u32;
count += 1;
}
}
(sum / count) as u16
}
pub fn linear_demosaic(raw_image: RawImage) -> Image<u16> {
match raw_image.cfa_pattern {
[0, 1, 1, 2] => linear_demosaic_rggb(raw_image),
_ => todo!(),
}
}
fn linear_demosaic_rggb(mut raw_image: RawImage) -> Image<u16> {
let width = raw_image.width as i64;
let height = raw_image.height as i64;
for row in 0..height {
let row_by_width = row * width;
for col in 0..width {
let pixel_index = row_by_width + col;
let vertical_indexes = [pixel_index + width, pixel_index - width];
let horizontal_indexes = [pixel_index + 1, pixel_index - 1];
let cross_indexes = [pixel_index + width, pixel_index - width, pixel_index + 1, pixel_index - 1];
let diagonal_indexes = [pixel_index + width + 1, pixel_index - width + 1, pixel_index + width - 1, pixel_index - width - 1];
match (row % 2 == 0, col % 2 == 0) {
(true, true) => {
let indexes = cross_indexes.iter().map(|x| 3 * x + 1);
raw_image.data[3 * (pixel_index as usize) + 1] = average(&raw_image.data, indexes);
let indexes = diagonal_indexes.iter().map(|x| 3 * x + 2);
raw_image.data[3 * (pixel_index as usize) + 2] = average(&raw_image.data, indexes);
}
(true, false) => {
let indexes = horizontal_indexes.iter().map(|x| 3 * x);
raw_image.data[3 * (pixel_index as usize)] = average(&raw_image.data, indexes);
let indexes = vertical_indexes.iter().map(|x| 3 * x + 2);
raw_image.data[3 * (pixel_index as usize) + 2] = average(&raw_image.data, indexes);
}
(false, true) => {
let indexes = vertical_indexes.iter().map(|x| 3 * x);
raw_image.data[3 * (pixel_index as usize)] = average(&raw_image.data, indexes);
let indexes = horizontal_indexes.iter().map(|x| 3 * x + 2);
raw_image.data[3 * (pixel_index as usize) + 2] = average(&raw_image.data, indexes);
}
(false, false) => {
let indexes = cross_indexes.iter().map(|x| 3 * x + 1);
raw_image.data[3 * (pixel_index as usize) + 1] = average(&raw_image.data, indexes);
let indexes = diagonal_indexes.iter().map(|x| 3 * x);
raw_image.data[3 * (pixel_index as usize)] = average(&raw_image.data, indexes);
}
}
}
}
Image {
channels: 3,
data: raw_image.data,
width: raw_image.width,
height: raw_image.height,
}
}

View File

@ -0,0 +1 @@
pub mod linear_demosaicing;

View File

@ -1,18 +1,33 @@
pub mod decoder;
pub mod demosaicing;
pub mod metadata;
pub mod preprocessing;
pub mod tiff;
use crate::preprocessing::camera_data::camera_to_xyz;
use tag_derive::Tag;
use tiff::file::TiffRead;
use tiff::tags::{Compression, ImageLength, ImageWidth, Model, StripByteCounts, SubIfd, Tag};
use tiff::tags::{Compression, ImageLength, ImageWidth, StripByteCounts, SubIfd, Tag};
use tiff::{Ifd, TiffError};
use std::io::{Read, Seek};
use thiserror::Error;
pub enum SubtractBlack {
None,
Value(u16),
CfaGrid([u16; 4]),
}
pub struct RawImage {
pub data: Vec<u16>,
pub width: usize,
pub height: usize,
pub cfa_pattern: [u8; 4],
pub maximum: u16,
pub black: SubtractBlack,
pub camera_to_xyz: Option<[f64; 9]>,
}
pub struct Image<T> {
@ -35,28 +50,41 @@ pub fn decode<R: Read + Seek>(reader: &mut R) -> Result<RawImage, DecoderError>
let mut file = TiffRead::new(reader)?;
let ifd = Ifd::new_first_ifd(&mut file)?;
// TODO: This is only for the tests to pass for now. Replace this with the correct implementation when the decoder is complete.
let model = ifd.get_value::<Model, _>(&mut file)?;
let camera_model = metadata::identify::identify_camera_model(&ifd, &mut file).unwrap();
if model == "DSLR-A100" {
Ok(decoder::arw1::decode_a100(ifd, &mut file))
let mut raw_image = if camera_model.model == "DSLR-A100" {
decoder::arw1::decode_a100(ifd, &mut file)
} else {
let sub_ifd = ifd.get_value::<SubIfd, _>(&mut file)?;
let arw_ifd = sub_ifd.get_value::<ArwIfd, _>(&mut file)?;
if arw_ifd.compression == 1 {
Ok(decoder::uncompressed::decode(sub_ifd, &mut file))
decoder::uncompressed::decode(sub_ifd, &mut file)
} else if arw_ifd.strip_byte_counts[0] == arw_ifd.image_width * arw_ifd.image_height {
Ok(decoder::arw2::decode(sub_ifd, &mut file))
decoder::arw2::decode(sub_ifd, &mut file)
} else {
// TODO: implement for arw 1.
todo!()
}
}
};
raw_image.camera_to_xyz = camera_to_xyz(&camera_model);
Ok(raw_image)
}
pub fn process_8bit(_image: RawImage) -> Image<u8> {
todo!()
pub fn process_8bit(raw_image: RawImage) -> Image<u8> {
let raw_image = crate::preprocessing::subtract_black::subtract_black(raw_image);
let raw_image = crate::preprocessing::raw_to_image::raw_to_image(raw_image);
let raw_image = crate::preprocessing::scale_colors::scale_colors(raw_image);
let image = crate::demosaicing::linear_demosaicing::linear_demosaic(raw_image);
Image {
channels: image.channels,
data: image.data.iter().map(|x| (x >> 8) as u8).collect(),
width: image.width,
height: image.height,
}
}
pub fn process_16bit(_image: RawImage) -> Image<u16> {

View File

@ -0,0 +1,60 @@
use crate::tiff::file::TiffRead;
use crate::tiff::tags::{Make, Model, Tag};
use crate::tiff::{Ifd, TiffError};
use std::io::{Read, Seek};
use tag_derive::Tag;
const COMPANY_NAMES: [&str; 22] = [
"AgfaPhoto",
"Canon",
"Casio",
"Epson",
"Fujifilm",
"Mamiya",
"Minolta",
"Motorola",
"Kodak",
"Konica",
"Leica",
"Nikon",
"Nokia",
"Olympus",
"Ricoh",
"Pentax",
"Phase One",
"Samsung",
"Sigma",
"Sinar",
"Sony",
"YI",
];
#[allow(dead_code)]
#[derive(Tag)]
struct CameraModelIfd {
make: Make,
model: Model,
}
pub struct CameraModel {
pub make: String,
pub model: String,
}
pub fn identify_camera_model<R: Read + Seek>(ifd: &Ifd, file: &mut TiffRead<R>) -> Option<CameraModel> {
let mut ifd = ifd.get_value::<CameraModelIfd, _>(file).unwrap();
ifd.make.make_ascii_lowercase();
for company_name in COMPANY_NAMES {
let lowercase_company_name = company_name.to_ascii_lowercase();
if ifd.make.contains(&lowercase_company_name) {
return Some(CameraModel {
make: company_name.to_string(),
model: ifd.model,
});
}
}
None
}

View File

@ -0,0 +1 @@
pub mod identify;

View File

@ -0,0 +1,26 @@
use crate::metadata::identify::CameraModel;
use build_camera_data::build_camera_data;
pub struct CameraData {
pub black: u16,
pub maximum: u16,
pub camera_to_xyz: [i16; 9],
}
impl CameraData {
const DEFAULT: CameraData = CameraData {
black: 0,
maximum: 0,
camera_to_xyz: [0; 9],
};
}
const CAMERA_DATA: [(&str, CameraData); 40] = build_camera_data!();
pub fn camera_to_xyz(camera_model: &CameraModel) -> Option<[f64; 9]> {
let camera_name_needle = camera_model.make.to_owned() + " " + &camera_model.model;
CAMERA_DATA
.iter()
.find(|(camera_name_haystack, _)| camera_name_needle == *camera_name_haystack)
.map(|(_, data)| data.camera_to_xyz.map(|x| (x as f64) / 10_000.))
}

View File

@ -0,0 +1,4 @@
pub mod camera_data;
pub mod raw_to_image;
pub mod scale_colors;
pub mod subtract_black;

View File

@ -0,0 +1,17 @@
use crate::RawImage;
pub fn raw_to_image(mut raw_image: RawImage) -> RawImage {
let mut image = Vec::with_capacity(raw_image.width * raw_image.height * 3);
for row in 0..raw_image.height {
for col in 0..raw_image.width {
let mut pixel = [0_u16; 3];
let color_index = raw_image.cfa_pattern[2 * (row % 2) + (col % 2)];
pixel[color_index as usize] = raw_image.data[row * raw_image.width + col];
image.extend_from_slice(&pixel);
}
}
raw_image.data = image;
raw_image
}

View File

@ -0,0 +1,46 @@
use crate::RawImage;
const XYZ_TO_RGB: [[f64; 3]; 3] = [[0.412453, 0.357580, 0.180423], [0.212671, 0.715160, 0.072169], [0.019334, 0.119193, 0.950227]];
pub fn scale_colors(mut raw_image: RawImage) -> RawImage {
if let Some(camera_to_xyz) = raw_image.camera_to_xyz {
let mut camera_to_rgb = [[0.; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
camera_to_rgb[i][j] += camera_to_xyz[i * 3 + k] * XYZ_TO_RGB[k][j];
}
}
}
let mut white_balance_multiplier = camera_to_rgb.map(|x| 1. / x.iter().sum::<f64>());
if white_balance_multiplier[1] == 0. {
white_balance_multiplier[1] = 1.;
}
// TODO: Move this at its correct location when highlights are implemented correctly.
let highlight = 0;
let normalize_white_balance = if highlight == 0 {
white_balance_multiplier.iter().fold(f64::INFINITY, |a, &b| a.min(b))
} else {
white_balance_multiplier.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b))
};
let final_multiplier = if normalize_white_balance > 0.00001 && raw_image.maximum > 0 {
let scale_to_16bit_multiplier = u16::MAX as f64 / raw_image.maximum as f64;
white_balance_multiplier.map(|x| x / normalize_white_balance * scale_to_16bit_multiplier)
} else {
[1., 1., 1.]
};
for i in 0..(raw_image.height * raw_image.width) {
for (c, multiplier) in final_multiplier.iter().enumerate() {
raw_image.data[3 * i + c] = ((raw_image.data[3 * i + c] as f64) * multiplier).min(u16::MAX as f64).max(0.) as u16;
}
}
}
raw_image
}

View File

@ -0,0 +1,30 @@
use crate::{RawImage, SubtractBlack};
pub fn subtract_black(raw_image: RawImage) -> RawImage {
let mut raw_image = match raw_image.black {
SubtractBlack::None => raw_image,
SubtractBlack::Value(_) => todo!(),
SubtractBlack::CfaGrid(_) => subtract_black_cfa_grid(raw_image),
};
raw_image.black = SubtractBlack::None;
raw_image
}
pub fn subtract_black_cfa_grid(mut raw_image: RawImage) -> RawImage {
let width = raw_image.width;
let black_level = match raw_image.black {
SubtractBlack::CfaGrid(x) => x,
_ => unreachable!(),
};
for row in 0..raw_image.height {
for col in 0..width {
raw_image.data[row * width + col] = raw_image.data[row * width + col].saturating_sub(black_level[2 * (row % 2) + (col % 2)]);
}
}
raw_image.maximum -= black_level.iter().max().unwrap();
raw_image
}

View File

@ -29,8 +29,11 @@ pub enum TagId {
JpegOffset = 0x201,
JpegLength = 0x202,
SonyToneCurve = 0x7010,
BlackLevel = 0x7310,
CfaPatternDim = 0x828d,
CfaPattern = 0x828e,
ColorMatrix1 = 0xc621,
ColorMatrix2 = 0xc622,
#[num_enum(catch_all)]
Unknown(u16),

View File

@ -1,4 +1,4 @@
use super::types::{Array, ConstArray, TagType, TypeByte, TypeIfd, TypeLong, TypeNumber, TypeShort, TypeSonyToneCurve, TypeString};
use super::types::{Array, ConstArray, TagType, TypeByte, TypeIfd, TypeLong, TypeNumber, TypeSRational, TypeShort, TypeSonyToneCurve, TypeString};
use super::{Ifd, TagId, TiffError, TiffRead};
use std::io::{Read, Seek};
@ -24,10 +24,13 @@ pub struct StripByteCounts;
pub struct SubIfd;
pub struct JpegOffset;
pub struct JpegLength;
pub struct CfaPatternDim;
pub struct CfaPattern;
pub struct SonyDataOffset;
pub struct SonyToneCurve;
pub struct BlackLevel;
pub struct CfaPatternDim;
pub struct CfaPattern;
pub struct ColorMatrix1;
pub struct ColorMatrix2;
impl SimpleTag for ImageWidth {
type Type = TypeNumber;
@ -141,6 +144,20 @@ impl SimpleTag for CfaPattern {
const NAME: &'static str = "CFA Pattern";
}
impl SimpleTag for ColorMatrix1 {
type Type = Array<TypeSRational>;
const ID: TagId = TagId::ColorMatrix1;
const NAME: &'static str = "Color Matrix 1";
}
impl SimpleTag for ColorMatrix2 {
type Type = Array<TypeSRational>;
const ID: TagId = TagId::ColorMatrix2;
const NAME: &'static str = "Color Matrix 2";
}
impl SimpleTag for SonyDataOffset {
type Type = TypeLong;
@ -155,6 +172,12 @@ impl SimpleTag for SonyToneCurve {
const NAME: &'static str = "Sony Tone Curve";
}
impl SimpleTag for BlackLevel {
const ID: TagId = TagId::BlackLevel;
type Type = ConstArray<TypeShort, 4>;
const NAME: &'static str = "Black Level";
}
pub trait Tag {
type Output;

View File

@ -1,8 +1,30 @@
pub struct Rational<T> {
pub trait ToFloat {
fn to_float(&self) -> f64;
}
impl ToFloat for u32 {
fn to_float(&self) -> f64 {
*self as f64
}
}
impl ToFloat for i32 {
fn to_float(&self) -> f64 {
*self as f64
}
}
pub struct Rational<T: ToFloat> {
pub numerator: T,
pub denominator: T,
}
impl<T: ToFloat> ToFloat for Rational<T> {
fn to_float(&self) -> f64 {
self.numerator.to_float() / self.denominator.to_float()
}
}
pub struct CurveLookupTable {
table: Vec<u16>,
}

View File

@ -1,13 +1,18 @@
// Only compile this file if the feature "raw-rs-tests" is enabled
#![cfg(feature = "raw-rs-tests")]
use std::collections::HashMap;
use std::fmt::Write;
use std::fs::{read_dir, File};
use std::io::{Cursor, Read};
use std::path::Path;
use raw_rs::RawImage;
use downloader::{Download, Downloader};
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{ColorType, ImageEncoder};
use libraw::Processor;
use palette::{LinSrgb, Srgb};
use std::collections::HashMap;
use std::fmt::Write;
use std::fs::{read_dir, File};
use std::io::{BufWriter, Cursor, Read};
use std::path::{Path, PathBuf};
use std::time::Duration;
const TEST_FILES: [&str; 3] = ["ILCE-7M3-ARW2.3.5-blossoms.arw", "ILCE-7RM4-ARW2.3.5-kestrel.arw", "ILCE-6000-ARW2.3.1-windsock.arw"];
const BASE_URL: &str = "https://static.graphite.rs/test-data/libraries/raw-rs/";
@ -30,7 +35,7 @@ fn test_images_match_with_libraw() {
print!("{} => ", path.display());
let _raw_image = match test_raw_data(&content) {
let raw_image = match test_raw_data(&content) {
Err(err_msg) => {
failed_tests += 1;
return println!("{}", err_msg);
@ -47,6 +52,15 @@ fn test_images_match_with_libraw() {
// };
println!("Passed");
// TODO: Remove this later
let mut image = raw_rs::process_8bit(raw_image);
store_image(&path, "raw_rs", &mut image.data, image.width, image.height);
let processor = Processor::new();
let libraw_image = processor.process_8bit(&content).unwrap();
let mut data = Vec::from_iter(libraw_image.iter().copied());
store_image(&path, "libraw_rs", &mut data[..], libraw_image.width() as usize, libraw_image.height() as usize);
});
if failed_tests != 0 {
@ -54,24 +68,47 @@ fn test_images_match_with_libraw() {
}
}
fn store_image(path: &Path, suffix: &str, data: &mut [u8], width: usize, height: usize) {
if suffix == "raw_rs" {
for pixel in data.chunks_mut(3) {
let lin_srgb: LinSrgb<f64> = LinSrgb::new(pixel[0], pixel[1], pixel[2]).into_format();
let output: Srgb<u8> = Srgb::from_linear(lin_srgb);
pixel[0] = output.red;
pixel[1] = output.green;
pixel[2] = output.blue;
}
}
let mut output_path = PathBuf::new();
if let Some(parent) = path.parent() {
output_path.push(parent);
}
output_path.push("output");
if let Some(filename) = path.file_stem() {
let new_filename = format!("{}_{}.{}", filename.to_string_lossy(), suffix, "png");
output_path.push(new_filename);
}
output_path.set_extension("png");
let file = BufWriter::new(File::create(output_path).unwrap());
let png_encoder = PngEncoder::new_with_quality(file, CompressionType::Best, FilterType::Adaptive);
png_encoder.write_image(data, width as u32, height as u32, ColorType::Rgb8.into()).unwrap();
}
fn download_images() {
let mut path = Path::new(BASE_PATH).to_owned();
let mut downloads: Vec<Download> = Vec::new();
let client = reqwest::blocking::Client::builder().timeout(Duration::from_secs(60 * 5)).build().unwrap();
for filename in TEST_FILES {
path.push(filename);
if !path.exists() {
let url = BASE_URL.to_owned() + filename;
downloads.push(Download::new(&url).file_name(Path::new(filename)));
let mut response = client.get(url).send().unwrap();
let mut file = File::create(BASE_PATH.to_owned() + filename).unwrap();
std::io::copy(&mut response, &mut file).unwrap();
}
path.pop();
}
let mut downloader = Downloader::builder().download_folder(Path::new(BASE_PATH)).build().unwrap();
for download_summary in downloader.download(&downloads).unwrap() {
download_summary.unwrap();
}
}
fn test_raw_data(content: &[u8]) -> Result<RawImage, String> {
@ -269,3 +306,41 @@ fn _test_final_image(content: &[u8], raw_image: RawImage) -> Result<(), String>
Ok(())
}
// #[test]
fn extract_data_from_dng_images() {
read_dir(BASE_PATH)
.unwrap()
.map(|dir_entry| dir_entry.unwrap().path())
.filter(|path| path.is_file() && path.file_name().map(|file_name| file_name != ".gitkeep").unwrap_or(false))
.for_each(|path| {
extract_data_from_dng_image(&path);
});
}
fn extract_data_from_dng_image(path: &Path) {
use raw_rs::tiff::file::TiffRead;
use raw_rs::tiff::tags::{ColorMatrix2, Make, Model};
use raw_rs::tiff::values::ToFloat;
use raw_rs::tiff::Ifd;
use std::io::{BufReader, Write};
let reader = BufReader::new(File::open(path).unwrap());
let mut file = TiffRead::new(reader).unwrap();
let ifd = Ifd::new_first_ifd(&mut file).unwrap();
let make = ifd.get_value::<Make, _>(&mut file).unwrap();
let model = ifd.get_value::<Model, _>(&mut file).unwrap();
let matrix = ifd.get_value::<ColorMatrix2, _>(&mut file).unwrap();
if model == "MODEL-NAME" {
println!("{}", path.display());
return;
}
let output_folder = path.parent().unwrap().join(make);
std::fs::create_dir_all(&output_folder).unwrap();
let mut output_file = File::create(output_folder.join(model + ".toml")).unwrap();
let matrix: Vec<_> = matrix.iter().map(|x| x.to_float()).collect();
writeln!(output_file, "camera_to_xyz = {:.4?}", matrix).unwrap();
}