Skip to content

Commit a7840b2

Browse files
elbertronnieKeavon
andauthored
Raw-rs: add post-processing steps (#1923)
* add convert_to_rgb step * add code to generate gamma correction curve * add gamma correction step * fix clippy warnings and cargo fmt * remove unnecessary dependencies * Code review 1 * Code review 2 * fix the order of operations * Code review 3 --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent 40fd447 commit a7840b2

File tree

13 files changed

+284
-102
lines changed

13 files changed

+284
-102
lines changed

Cargo.lock

Lines changed: 0 additions & 37 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libraries/raw-rs/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/r
1313
documentation = "https://docs.rs/raw-rs"
1414

1515
[features]
16-
raw-rs-tests = ["dep:image", "dep:libraw-rs", "dep:palette", "dep:reqwest"]
16+
raw-rs-tests = ["dep:image", "dep:libraw-rs", "dep:reqwest"]
1717

1818
[dependencies]
1919
# Local dependencies
@@ -33,4 +33,3 @@ reqwest = { workspace = true, optional = true }
3333

3434
# Optional dependencies (should be dev dependencies, but Cargo currently doesn't allow optional dev dependencies)
3535
libraw-rs = { version = "0.0.4", optional = true }
36-
palette = { version = "0.7.6", optional = true }

libraries/raw-rs/src/decoder/arw1.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ pub fn decode_a100<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage
2626
#[allow(unreachable_code)]
2727
maximum: (1 << 12) - 1,
2828
black: SubtractBlack::None,
29-
camera_to_xyz: None,
29+
camera_model: None,
30+
white_balance_multiplier: None,
31+
camera_to_rgb: None,
32+
rgb_to_camera: None,
3033
}
3134
}
3235

libraries/raw-rs/src/decoder/arw2.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
4949
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
5050
maximum: (1 << 14) - 1,
5151
black: SubtractBlack::CfaGrid([512, 512, 512, 512]), // TODO: Find the correct way to do this
52-
camera_to_xyz: None,
52+
camera_model: None,
53+
white_balance_multiplier: None,
54+
camera_to_rgb: None,
55+
rgb_to_camera: None,
5356
}
5457
}
5558

libraries/raw-rs/src/decoder/uncompressed.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
5757
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
5858
maximum: if bits_per_sample == 16 { u16::MAX } else { (1 << bits_per_sample) - 1 },
5959
black: SubtractBlack::CfaGrid(ifd.black_level),
60-
camera_to_xyz: None,
60+
camera_model: None,
61+
white_balance_multiplier: None,
62+
camera_to_rgb: None,
63+
rgb_to_camera: None,
6164
}
6265
}

libraries/raw-rs/src/demosaicing/linear_demosaicing.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,7 @@ fn linear_demosaic_rggb(mut raw_image: RawImage) -> Image<u16> {
7373
data: raw_image.data,
7474
width: raw_image.width,
7575
height: raw_image.height,
76+
rgb_to_camera: raw_image.rgb_to_camera,
77+
histogram: None,
7678
}
7779
}

libraries/raw-rs/src/lib.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
pub mod decoder;
22
pub mod demosaicing;
33
pub mod metadata;
4+
pub mod postprocessing;
45
pub mod preprocessing;
56
pub mod tiff;
67

7-
use crate::preprocessing::camera_data::camera_to_xyz;
8+
use crate::metadata::identify::CameraModel;
89

910
use tag_derive::Tag;
1011
use tiff::file::TiffRead;
@@ -27,14 +28,21 @@ pub struct RawImage {
2728
pub cfa_pattern: [u8; 4],
2829
pub maximum: u16,
2930
pub black: SubtractBlack,
30-
pub camera_to_xyz: Option<[f64; 9]>,
31+
pub camera_model: Option<CameraModel>,
32+
pub white_balance_multiplier: Option<[f64; 3]>,
33+
pub camera_to_rgb: Option<[[f64; 3]; 3]>,
34+
pub rgb_to_camera: Option<[[f64; 3]; 3]>,
3135
}
3236

3337
pub struct Image<T> {
3438
pub data: Vec<T>,
3539
pub width: usize,
3640
pub height: usize,
41+
/// We can assume this will be 3 for all non-obscure, modern cameras.
42+
/// See <https://github.com/GraphiteEditor/Graphite/pull/1923#discussion_r1725070342> for more information.
3743
pub channels: u8,
44+
pub rgb_to_camera: Option<[[f64; 3]; 3]>,
45+
pub histogram: Option<[[usize; 0x2000]; 3]>,
3846
}
3947

4048
#[allow(dead_code)]
@@ -68,27 +76,32 @@ pub fn decode<R: Read + Seek>(reader: &mut R) -> Result<RawImage, DecoderError>
6876
}
6977
};
7078

71-
raw_image.camera_to_xyz = camera_to_xyz(&camera_model);
79+
raw_image.camera_model = Some(camera_model);
7280

7381
Ok(raw_image)
7482
}
7583

7684
pub fn process_8bit(raw_image: RawImage) -> Image<u8> {
77-
let raw_image = crate::preprocessing::subtract_black::subtract_black(raw_image);
78-
let raw_image = crate::preprocessing::raw_to_image::raw_to_image(raw_image);
79-
let raw_image = crate::preprocessing::scale_colors::scale_colors(raw_image);
80-
let image = crate::demosaicing::linear_demosaicing::linear_demosaic(raw_image);
85+
let image = process_16bit(raw_image);
8186

8287
Image {
8388
channels: image.channels,
8489
data: image.data.iter().map(|x| (x >> 8) as u8).collect(),
8590
width: image.width,
8691
height: image.height,
92+
rgb_to_camera: image.rgb_to_camera,
93+
histogram: image.histogram,
8794
}
8895
}
8996

90-
pub fn process_16bit(_image: RawImage) -> Image<u16> {
91-
todo!()
97+
pub fn process_16bit(raw_image: RawImage) -> Image<u16> {
98+
let raw_image = crate::preprocessing::camera_data::calculate_conversion_matrices(raw_image);
99+
let raw_image = crate::preprocessing::subtract_black::subtract_black(raw_image);
100+
let raw_image = crate::preprocessing::raw_to_image::raw_to_image(raw_image);
101+
let raw_image = crate::preprocessing::scale_colors::scale_colors(raw_image);
102+
let image = crate::demosaicing::linear_demosaicing::linear_demosaic(raw_image);
103+
let image = crate::postprocessing::convert_to_rgb::convert_to_rgb(image);
104+
crate::postprocessing::gamma_correction::gamma_correction(image)
92105
}
93106

94107
#[derive(Error, Debug)]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use crate::Image;
2+
3+
const CHANNELS_IN_RGB: usize = 3;
4+
5+
pub fn convert_to_rgb(mut image: Image<u16>) -> Image<u16> {
6+
let Some(rgb_to_camera) = image.rgb_to_camera else { return image };
7+
8+
// Rarely this might be 4 instead of 3 if an obscure Bayer filter is used, such as RGBE or CYGM, instead of the typical RGGB.
9+
// See: <https://github.com/GraphiteEditor/Graphite/pull/1923#discussion_r1725070342>.
10+
let channels = image.channels as usize;
11+
let mut data = Vec::with_capacity(CHANNELS_IN_RGB * image.width * image.height);
12+
let mut histogram = [[0; 0x2000]; CHANNELS_IN_RGB];
13+
14+
for i in 0..(image.height * image.width) {
15+
let start = i * channels;
16+
let end = start + channels;
17+
let input_pixel = &mut image.data[start..end];
18+
19+
let mut output_pixel = [0.; CHANNELS_IN_RGB];
20+
for (channel, &value) in input_pixel.iter().enumerate() {
21+
output_pixel[0] += rgb_to_camera[0][channel] * value as f64;
22+
output_pixel[1] += rgb_to_camera[1][channel] * value as f64;
23+
output_pixel[2] += rgb_to_camera[2][channel] * value as f64;
24+
}
25+
26+
for (output_pixel_channel, histogram_channel) in output_pixel.iter().zip(histogram.iter_mut()) {
27+
let final_sum = (*output_pixel_channel as u16).clamp(0, u16::MAX);
28+
29+
histogram_channel[final_sum as usize >> CHANNELS_IN_RGB] += 1;
30+
31+
data.push(final_sum);
32+
}
33+
}
34+
35+
image.data = data;
36+
image.histogram = Some(histogram);
37+
image.channels = CHANNELS_IN_RGB as u8;
38+
39+
image
40+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use crate::Image;
2+
use std::f64::consts::E;
3+
4+
pub fn gamma_correction(mut image: Image<u16>) -> Image<u16> {
5+
let Some(histogram) = image.histogram else { return image };
6+
7+
let percentage = image.width * image.height;
8+
9+
let mut white = 0;
10+
for channel_histogram in histogram {
11+
let mut total = 0;
12+
for i in (0x20..0x2000).rev() {
13+
total += channel_histogram[i] as u64;
14+
15+
if total * 100 > percentage as u64 {
16+
white = white.max(i);
17+
break;
18+
}
19+
}
20+
}
21+
22+
let curve = generate_gamma_curve(0.45, 4.5, (white << 3) as f64);
23+
24+
for value in image.data.iter_mut() {
25+
*value = curve[*value as usize];
26+
}
27+
image.histogram = None;
28+
29+
image
30+
}
31+
32+
/// `max_intensity` must be non-zero.
33+
fn generate_gamma_curve(power: f64, threshold: f64, max_intensity: f64) -> Vec<u16> {
34+
debug_assert!(max_intensity != 0.);
35+
36+
let (mut bound_start, mut bound_end) = if threshold >= 1. { (0., 1.) } else { (1., 0.) };
37+
38+
let mut transition_point = 0.;
39+
let mut transition_ratio = 0.;
40+
let mut curve_adjustment = 0.;
41+
42+
if threshold != 0. && (threshold - 1.) * (power - 1.) <= 0. {
43+
for _ in 0..48 {
44+
transition_point = (bound_start + bound_end) / 2.;
45+
46+
if power != 0. {
47+
let temp_transition_ratio = transition_point / threshold;
48+
let exponential_power = temp_transition_ratio.powf(-power);
49+
let normalized_exponential_power = (exponential_power - 1.) / power;
50+
let comparison_result = normalized_exponential_power - (1. / transition_point);
51+
52+
let bound_to_update = if comparison_result > -1. { &mut bound_end } else { &mut bound_start };
53+
*bound_to_update = transition_point;
54+
} else {
55+
let adjusted_transition_point = E.powf(1. - 1. / transition_point);
56+
let transition_point_ratio = transition_point / adjusted_transition_point;
57+
58+
let bound_to_update = if transition_point_ratio < threshold { &mut bound_end } else { &mut bound_start };
59+
*bound_to_update = transition_point;
60+
}
61+
}
62+
63+
transition_ratio = transition_point / threshold;
64+
65+
if power != 0. {
66+
curve_adjustment = transition_point * ((1. / power) - 1.);
67+
}
68+
}
69+
70+
let mut curve = vec![0xffff; 0x1_0000];
71+
let length = curve.len() as f64;
72+
73+
for (i, entry) in curve.iter_mut().enumerate() {
74+
let ratio = (i as f64) / max_intensity;
75+
if ratio < 1. {
76+
let altered_ratio = if ratio < transition_ratio {
77+
ratio * threshold
78+
} else if power != 0. {
79+
ratio.powf(power) * (1. + curve_adjustment) - curve_adjustment
80+
} else {
81+
ratio.ln() * transition_point + 1.
82+
};
83+
84+
*entry = (length * altered_ratio) as u16;
85+
}
86+
}
87+
88+
curve
89+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod convert_to_rgb;
2+
pub mod gamma_correction;

0 commit comments

Comments
 (0)