Skip to content

Commit c617194

Browse files
authored
Merge pull request #16 from awxkee/dev
A bit changes
2 parents fca49f3 + 5e9b789 commit c617194

File tree

8 files changed

+269
-93
lines changed

8 files changed

+269
-93
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ workspace = { members = ["app"] }
22

33
[package]
44
name = "gainforge"
5-
version = "0.3.7"
5+
version = "0.4.0"
66
edition = "2021"
77
description = "HDR tonemapping library"
88
readme = "README.md"

app/src/main.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ fn extract_images(file_path: &str) -> GainMapAssociationGroup {
164164
}
165165

166166
fn main() {
167-
let img = image::ImageReader::open("./assets/03.jpg")
167+
let img = image::ImageReader::open("./assets/base.jpeg")
168168
.unwrap()
169169
.decode()
170170
.unwrap();
@@ -186,16 +186,16 @@ fn main() {
186186
// saturation: Rgb::new(1.4, 1.4, 1.4),
187187
// offset: Rgb::default(),
188188
// })),
189-
ToneMappingMethod::Rec2408(GainHdrMetadata::new(2000f32, 203.)),
189+
ToneMappingMethod::TunedReinhard(GainHdrMetadata::new(2000., 250.)),
190190
// ToneMappingMethod::ExtendedReinhard,
191-
MappingColorSpace::Rgb(RgbToneMapperParameters {
192-
gamut_clipping: GamutClipping::NoClip,
193-
exposure: 1f32,
194-
}),
195-
// MappingColorSpace::YRgb(CommonToneMapperParameters {
196-
// exposure: 1f32,
191+
// MappingColorSpace::Rgb(RgbToneMapperParameters {
197192
// gamut_clipping: GamutClipping::NoClip,
193+
// exposure: 1f32,
198194
// }),
195+
MappingColorSpace::Yrg(CommonToneMapperParameters {
196+
exposure: 1f32,
197+
gamut_clipping: GamutClipping::NoClip,
198+
}),
199199
// MappingColorSpace::Jzazbz(JzazbzToneMapperParameters {
200200
// content_brightness: 2000.,
201201
// exposure: 1f32,
@@ -247,7 +247,7 @@ fn main() {
247247
// let compressed = dst.iter().map(|&x| (x >> 8) as u8).collect::<Vec<_>>();
248248

249249
image::save_buffer(
250-
"clamp_agx_t.jpg",
250+
"tuned_reinhard.jpg",
251251
&dst,
252252
img.width(),
253253
img.height(),

src/err.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub struct MismatchedSize {
3636
pub received: usize,
3737
}
3838

39-
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
39+
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
4040
pub enum ForgeError {
4141
LaneSizeMismatch,
4242
LaneMultipleOfChannels,
@@ -52,6 +52,7 @@ pub enum ForgeError {
5252
InvalidGainMapConfiguration,
5353
ImageSizeMismatch,
5454
ZeroBaseSize,
55+
UnsupportedToneMappingConfiguration(String),
5556
MinimumSliceSizeMismatch(MismatchedSize),
5657
MinimumStrideSizeMismatch(MismatchedSize),
5758
UnknownError,
@@ -90,6 +91,7 @@ impl Display for ForgeError {
9091
"Minimum stride must have size at least {} but it is {}",
9192
size.expected, size.received
9293
)),
94+
ForgeError::UnsupportedToneMappingConfiguration(str) => f.write_str(str),
9395
ForgeError::UnknownError => f.write_str("Unknown error"),
9496
}
9597
}

src/gamma.rs

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -171,16 +171,27 @@ pub(crate) fn gamma2p8_to_linear(gamma: f32) -> f32 {
171171

172172
#[inline(always)]
173173
/// Linear transfer function for PQ
174-
pub(crate) fn pq_to_linear(gamma: f32) -> f32 {
174+
pub(crate) fn pq_to_linear(gamma: f32, reference_display: f32) -> f32 {
175175
if gamma > 0.0 {
176176
let pow_gamma = f_powf(gamma, 1.0 / 78.84375);
177177
let num = (pow_gamma - 0.8359375).max(0.);
178178
let den = (18.8515625 - 18.6875 * pow_gamma).max(f32::MIN);
179179
let linear = f_powf(num / den, 1.0 / 0.1593017578125);
180180
// Scale so that SDR white is 1.0 (extended SDR).
181-
const PQ_MAX_NITS: f32 = 10000.;
182-
const SDR_WHITE_NITS: f32 = 203.;
183-
linear * PQ_MAX_NITS / SDR_WHITE_NITS
181+
linear * reference_display
182+
} else {
183+
0.0
184+
}
185+
}
186+
187+
#[inline(always)]
188+
/// Linear transfer function for PQ
189+
pub(crate) fn pq_to_linear_unscaled(gamma: f32) -> f32 {
190+
if gamma > 0.0 {
191+
let pow_gamma = f_powf(gamma, 1.0 / 78.84375);
192+
let num = (pow_gamma - 0.8359375).max(0.);
193+
let den = (18.8515625 - 18.6875 * pow_gamma).max(f32::MIN);
194+
f_powf(num / den, 1.0 / 0.1593017578125)
184195
} else {
185196
0.0
186197
}
@@ -195,7 +206,22 @@ const HLG_WHITE_NITS: f32 = 1000.;
195206
pub(crate) fn pq_from_linear(linear: f32) -> f32 {
196207
if linear > 0.0 {
197208
// Scale from extended SDR range to [0.0, 1.0].
198-
let linear = (linear * SDR_REFERENCE_DISPLAY / PQ_MAX_NITS).clamp(0., 1.);
209+
let linear = (linear * (SDR_REFERENCE_DISPLAY / PQ_MAX_NITS)).clamp(0., 1.);
210+
let pow_linear = f_powf(linear, 0.1593017578125);
211+
let num = 0.1640625 * pow_linear - 0.1640625;
212+
let den = 1.0 + 18.6875 * pow_linear;
213+
f_powf(1.0 + num / den, 78.84375)
214+
} else {
215+
0.0
216+
}
217+
}
218+
219+
#[inline(always)]
220+
/// Gamma transfer function for PQ
221+
pub(crate) fn pq_from_linear_with_reference_display(linear: f32, reference_display: f32) -> f32 {
222+
if linear > 0.0 {
223+
// Scale from extended SDR range to [0.0, 1.0].
224+
let linear = (linear * (reference_display * (1. / PQ_MAX_NITS))).clamp(0., 1.);
199225
let pow_linear = f_powf(linear, 0.1593017578125);
200226
let num = 0.1640625 * pow_linear - 0.1640625;
201227
let den = 1.0 + 18.6875 * pow_linear;
@@ -286,7 +312,7 @@ impl From<u8> for TransferFunction {
286312

287313
impl TransferFunction {
288314
#[inline(always)]
289-
pub fn linearize(&self, v: f32) -> f32 {
315+
pub fn linearize(&self, v: f32, reference_display: f32) -> f32 {
290316
match self {
291317
TransferFunction::Srgb => srgb_to_linear(v),
292318
TransferFunction::Rec709 => rec709_to_linear(v),
@@ -296,7 +322,7 @@ impl TransferFunction {
296322
TransferFunction::Bt1361 => bt1361_to_linear(v),
297323
TransferFunction::Linear => trc_linear(v),
298324
TransferFunction::HybridLogGamma => hlg_to_linear(v),
299-
TransferFunction::PerceptualQuantizer => pq_to_linear(v),
325+
TransferFunction::PerceptualQuantizer => pq_to_linear(v, reference_display),
300326
}
301327
}
302328

@@ -332,20 +358,24 @@ impl TransferFunction {
332358
table
333359
}
334360

335-
pub(crate) fn generate_linear_table_u16(&self, bit_depth: usize) -> Box<[f32; 65536]> {
361+
pub(crate) fn generate_linear_table_u16(
362+
&self,
363+
bit_depth: usize,
364+
reference_display: f32,
365+
) -> Box<[f32; 65536]> {
336366
let mut table = Box::new([0.; 65536]);
337367
let max_bp = (1 << bit_depth as u32) - 1;
338368
let max_scale = 1f32 / max_bp as f32;
339369
for (i, value) in table.iter_mut().take(max_bp).enumerate() {
340-
*value = self.linearize(i as f32 * max_scale);
370+
*value = self.linearize(i as f32 * max_scale, reference_display);
341371
}
342372
table
343373
}
344374

345-
pub(crate) fn generate_linear_table_u8(&self) -> Box<[f32; 256]> {
375+
pub(crate) fn generate_linear_table_u8(&self, reference_display: f32) -> Box<[f32; 256]> {
346376
let mut table = Box::new([0.; 256]);
347377
for (i, value) in table.iter_mut().enumerate() {
348-
*value = self.linearize(i as f32 / 255.);
378+
*value = self.linearize(i as f32 * (1. / 255.), reference_display);
349379
}
350380
table
351381
}

src/lib.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ pub use iso_gain_map::{
257257
UhdrDirectorySeq, UhdrItem, UhdrItemContainerLi, UhdrItemResource,
258258
};
259259
pub use mappers::{AgxCustomLook, AgxLook, ToneMappingMethod};
260-
use num_traits::Num;
260+
use num_traits::{Float, Num};
261261
pub use spline::FilmicSplineParameters;
262262
pub use tonemapper::{
263263
create_tone_mapper_rgb, create_tone_mapper_rgb10, create_tone_mapper_rgb12,
@@ -269,13 +269,6 @@ pub use tonemapper::{
269269
};
270270

271271
#[inline]
272-
pub(crate) fn m_clamp<T: Num + PartialOrd>(a: T, min: T, max: T) -> T {
273-
if a > max {
274-
max
275-
} else if a >= min {
276-
a
277-
} else {
278-
// a < min or a is NaN
279-
min
280-
}
272+
pub(crate) fn m_clamp<T: Num + Float>(a: T, min: T, max: T) -> T {
273+
a.min(max).max(min)
281274
}

src/mappers.rs

Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
* // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2727
* // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2828
*/
29+
use crate::gamma::{pq_from_linear_with_reference_display, pq_to_linear_unscaled};
2930
use crate::mlaf::{fmla, mlaf};
3031
use crate::spline::FilmicSplineParameters;
3132
use crate::GainHdrMetadata;
@@ -49,8 +50,10 @@ pub enum AgxLook {
4950
/// many of the supported tone mapping methods.
5051
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
5152
pub enum ToneMappingMethod {
53+
/// Fast and accurate tuned Reinhard, preferred
54+
TunedReinhard(GainHdrMetadata),
5255
/// ITU-R broadcasting TV [recommendation 2408](https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2408-4-2021-PDF-E.pdf)
53-
Rec2408(GainHdrMetadata),
56+
Itu2408(GainHdrMetadata),
5457
/// The ['Uncharted 2' filmic](https://www.gdcvault.com/play/1012351/Uncharted-2-HDR)
5558
/// tone mapping method.
5659
Filmic,
@@ -84,12 +87,146 @@ pub(crate) trait ToneMap {
8487

8588
#[derive(Debug, Clone, Copy)]
8689
pub(crate) struct Rec2408ToneMapper<const CN: usize> {
90+
content_max_brightness: f32,
91+
display_max_brightness: f32,
92+
primaries: [f32; 3],
93+
content_min_luminance: f32,
94+
content_luminance_range: f32,
95+
inv_pq_mastering_range: f32,
96+
min_lum: f32,
97+
max_lum: f32,
98+
ks: f32,
99+
inv_one_minus_ks: f32,
100+
one_minus_ks: f32,
101+
normalizer: f32,
102+
inv_target_peak: f32,
103+
}
104+
105+
impl<const CN: usize> Rec2408ToneMapper<CN> {
106+
pub(crate) fn new(
107+
content_max_brightness: f32,
108+
display_max_brightness: f32,
109+
primaries: [f32; 3],
110+
) -> Self {
111+
let content_luminance_black = pq_from_linear_with_reference_display(0., 10000.0);
112+
let content_luminance_white =
113+
pq_from_linear_with_reference_display(content_max_brightness / 10000.0, 10000.0);
114+
let content_luminance_range = content_luminance_white - content_luminance_black;
115+
let inv_pq_mastering_range = 1.0 / content_luminance_range;
116+
117+
let min_lum = (pq_from_linear_with_reference_display(0., 10000.0)
118+
- content_luminance_black)
119+
* inv_pq_mastering_range;
120+
let max_lum =
121+
(pq_from_linear_with_reference_display(display_max_brightness / 10000.0, 10000.0)
122+
- content_luminance_black)
123+
* inv_pq_mastering_range;
124+
let ks = 1.5 * max_lum - 0.5;
125+
126+
Self {
127+
content_max_brightness,
128+
display_max_brightness,
129+
primaries,
130+
content_min_luminance: content_luminance_black,
131+
content_luminance_range,
132+
inv_pq_mastering_range,
133+
min_lum,
134+
max_lum,
135+
ks,
136+
inv_one_minus_ks: 1.0 / (1.0 - ks).max(1e-6),
137+
one_minus_ks: 1.0 - ks,
138+
normalizer: content_max_brightness / display_max_brightness,
139+
inv_target_peak: 1.0 / display_max_brightness,
140+
}
141+
}
142+
}
143+
144+
impl<const CN: usize> Rec2408ToneMapper<CN> {
145+
#[inline(always)]
146+
fn t(&self, a: f32) -> f32 {
147+
(a - self.ks) * self.inv_one_minus_ks
148+
}
149+
150+
#[inline]
151+
fn hermite_spline(&self, b: f32) -> f32 {
152+
let t_b = self.t(b);
153+
let t_b_2 = t_b * t_b;
154+
let t_b_3 = t_b_2 * t_b;
155+
fmla(2.0, t_b_3, fmla(-3.0, t_b_2, 1.0)) * self.ks
156+
+ fmla(-2.0, t_b_2, t_b_3 + t_b) * self.one_minus_ks
157+
+ fmla(-2.0, t_b_3, 3.0 * t_b_2) * self.max_lum
158+
}
159+
160+
#[inline(always)]
161+
fn make_luma_scale(&self, luma: f32) -> f32 {
162+
let s = pq_from_linear_with_reference_display(luma / 10000.0, 10000.0);
163+
let normalized_pq =
164+
((s - self.content_min_luminance) * self.inv_pq_mastering_range).min(1.0);
165+
166+
let e2 = if normalized_pq < self.ks {
167+
normalized_pq
168+
} else {
169+
self.hermite_spline(normalized_pq)
170+
};
171+
172+
let one_minus_e2 = 1.0 - e2;
173+
let one_minus_e2_2 = one_minus_e2 * one_minus_e2;
174+
let one_minus_e2_4 = one_minus_e2_2 * one_minus_e2_2;
175+
let e3 = fmla(self.min_lum, one_minus_e2_4, e2);
176+
let e4 = e3 * self.content_luminance_range + self.content_min_luminance;
177+
let d4 = pq_to_linear_unscaled(e4) * 10000.0;
178+
let new_luminance = d4.min(self.display_max_brightness).max(0.);
179+
180+
let min_luminance = 1e-6;
181+
let use_limit = luma <= min_luminance;
182+
let ratio = new_luminance / luma.max(min_luminance);
183+
let limit = new_luminance * self.inv_target_peak;
184+
let scale = ratio * self.normalizer;
185+
if use_limit {
186+
limit
187+
} else {
188+
scale
189+
}
190+
}
191+
}
192+
193+
impl<const CN: usize> ToneMap for Rec2408ToneMapper<CN> {
194+
fn process_lane(&self, in_place: &mut [f32]) {
195+
for chunk in in_place.chunks_exact_mut(CN) {
196+
let luma = fmla(
197+
chunk[0],
198+
self.primaries[0],
199+
fmla(chunk[1], self.primaries[1], chunk[2] * self.primaries[2]),
200+
) * self.content_max_brightness;
201+
if luma == 0. {
202+
chunk[0] = 0.;
203+
chunk[1] = 0.;
204+
chunk[2] = 0.;
205+
continue;
206+
}
207+
let scale = self.make_luma_scale(luma);
208+
chunk[0] *= scale;
209+
chunk[1] *= scale;
210+
chunk[2] *= scale;
211+
}
212+
}
213+
214+
fn process_luma_lane(&self, chunk: &mut [f32]) {
215+
for chunk in chunk.chunks_exact_mut(CN) {
216+
let scale = self.make_luma_scale(chunk[0] * self.content_max_brightness);
217+
chunk[0] *= scale;
218+
}
219+
}
220+
}
221+
222+
#[derive(Debug, Clone, Copy)]
223+
pub(crate) struct TunedReinhardToneMapper<const CN: usize> {
87224
w_a: f32,
88225
w_b: f32,
89226
primaries: [f32; 3],
90227
}
91228

92-
impl<const CN: usize> Rec2408ToneMapper<CN> {
229+
impl<const CN: usize> TunedReinhardToneMapper<CN> {
93230
pub(crate) fn new(
94231
content_max_brightness: f32,
95232
display_max_brightness: f32,
@@ -107,14 +244,14 @@ impl<const CN: usize> Rec2408ToneMapper<CN> {
107244
}
108245
}
109246

110-
impl<const CN: usize> Rec2408ToneMapper<CN> {
247+
impl<const CN: usize> TunedReinhardToneMapper<CN> {
111248
#[inline(always)]
112249
fn tonemap(&self, luma: f32) -> f32 {
113250
mlaf(1f32, self.w_a, luma) / mlaf(1f32, self.w_b, luma)
114251
}
115252
}
116253

117-
impl<const CN: usize> ToneMap for Rec2408ToneMapper<CN> {
254+
impl<const CN: usize> ToneMap for TunedReinhardToneMapper<CN> {
118255
fn process_lane(&self, in_place: &mut [f32]) {
119256
for chunk in in_place.chunks_exact_mut(CN) {
120257
let luma = fmla(

0 commit comments

Comments
 (0)