Skip to content

Commit 0b61727

Browse files
committed
add --l2 option to plot command
1 parent 866b7ca commit 0b61727

File tree

3 files changed

+233
-34
lines changed

3 files changed

+233
-34
lines changed

src/commands/plot.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@ pub struct PlotArgs {
3939

4040
#[arg(long, short = 'e', help = "Set frame range end (inclusive)")]
4141
pub end: Option<usize>,
42+
43+
#[arg(long, help = "Plot L2 trims metadata instead of L1 dynamic brightness")]
44+
pub l2: bool,
4245
}

src/dovi/plotter.rs

Lines changed: 151 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ use plotters::coord::ranged1d::{KeyPointHint, NoDefaultFormatting, Ranged, Value
1010
use plotters::coord::types::RangedCoordusize;
1111
use plotters::prelude::{
1212
AreaSeries, BitMapBackend, Cartesian2d, ChartBuilder, ChartContext, IntoDrawingArea,
13-
PathElement, SeriesLabelPosition, WHITE,
13+
LineSeries, PathElement, SeriesLabelPosition, WHITE,
1414
};
1515
use plotters::style::{BLACK, Color, IntoTextStyle, RGBColor, ShapeStyle};
1616

1717
use dolby_vision::rpu::utils::parse_rpu_file;
1818
use dolby_vision::utils::{nits_to_pq, pq_to_nits};
1919

2020
use super::input_from_either;
21-
use super::rpu_info::RpusListSummary;
21+
use super::rpu_info::{L2Data, RpusListSummary};
2222
use crate::commands::PlotArgs;
2323

2424
#[cfg(not(feature = "system-font"))]
@@ -56,10 +56,13 @@ impl Plotter {
5656
title,
5757
start: start_arg,
5858
end: end_arg,
59+
l2,
5960
} = args;
6061

61-
let output = output.unwrap_or(PathBuf::from("L1_plot.png"));
62-
let title = title.unwrap_or(String::from("Dolby Vision L1 plot"));
62+
let (level, y_desc) = if l2 { (2, "") } else { (1, "nits (cd/m²)") };
63+
64+
let output = output.unwrap_or(PathBuf::from(format!("L{level}_plot.png")));
65+
let title = title.unwrap_or(format!("Dolby Vision L{level} plot"));
6366

6467
let input = input_from_either("info", input, input_pos)?;
6568
let plotter = Plotter { input };
@@ -81,13 +84,17 @@ impl Plotter {
8184
.titled(&title, ("sans-serif", 40))?;
8285

8386
println!("Plotting...");
84-
let summary = RpusListSummary::new(rpus)?;
87+
let summary = if l2 {
88+
RpusListSummary::with_l2_data(rpus)?
89+
} else {
90+
RpusListSummary::new(rpus)?
91+
};
8592

8693
let mut chart = ChartBuilder::on(&root)
8794
.x_label_area_size(60)
8895
.y_label_area_size(60)
8996
.margin_top(90)
90-
.build_cartesian_2d(x_spec, PqCoord {})?;
97+
.build_cartesian_2d(x_spec, PqCoord::for_level(level))?;
9198

9299
chart
93100
.configure_mesh()
@@ -98,10 +105,15 @@ impl Plotter {
98105
.x_desc("frames")
99106
.x_max_light_lines(1)
100107
.x_labels(24)
101-
.y_desc("nits (cd/m²)")
108+
.y_desc(y_desc)
102109
.draw()?;
103110

104-
Self::draw_l1_series(&mut chart, &summary)?;
111+
if l2 {
112+
Self::draw_l2_series(&mut chart, &summary)?;
113+
} else {
114+
Self::draw_l1_series(&mut chart, &summary)?;
115+
}
116+
105117
chart
106118
.configure_series_labels()
107119
.border_style(BLACK)
@@ -239,49 +251,155 @@ impl Plotter {
239251

240252
Ok(())
241253
}
254+
255+
fn draw_l2_series(
256+
chart: &mut ChartContext<BitMapBackend, Cartesian2d<RangedCoordusize, PqCoord>>,
257+
summary: &RpusListSummary,
258+
) -> Result<()> {
259+
let data = summary.l2_data.as_ref().unwrap();
260+
let l2_stats = summary.l2_stats.as_ref().unwrap();
261+
262+
type Series = (&'static str, fn(&L2Data) -> f64, (f64, f64, f64), RGBColor);
263+
let series: [Series; 6] = [
264+
(
265+
"slope (gain)",
266+
|e| e.0 as f64,
267+
l2_stats.slope,
268+
RGBColor(96, 158, 232), // blue
269+
),
270+
(
271+
"offset (lift)",
272+
|e| e.1 as f64,
273+
l2_stats.offset,
274+
RGBColor(230, 110, 132), // pink
275+
),
276+
(
277+
"power (gamma)",
278+
|e| e.2 as f64,
279+
l2_stats.power,
280+
RGBColor(236, 162, 75), // orange
281+
),
282+
(
283+
"chroma (weight)",
284+
|e| e.3 as f64,
285+
l2_stats.chroma,
286+
RGBColor(115, 187, 190), // cyan
287+
),
288+
(
289+
"saturation (gain)",
290+
|e| e.4 as f64,
291+
l2_stats.saturation,
292+
RGBColor(144, 106, 252), // purple
293+
),
294+
(
295+
"ms (weight)",
296+
|e| e.5 as f64,
297+
l2_stats.ms_weight,
298+
RGBColor(243, 205, 95), // yellow
299+
),
300+
];
301+
302+
for (name, field_extractor, stats, color) in series.iter() {
303+
let label = format!(
304+
"{name} (min: {:.0}, max: {:.0}, avg: {:.0})",
305+
stats.0, stats.1, stats.2
306+
);
307+
let series = LineSeries::new(
308+
(0..).zip(data.iter()).map(|(x, y)| (x, field_extractor(y))),
309+
color,
310+
);
311+
let shape_style = ShapeStyle {
312+
color: color.to_rgba(),
313+
filled: false,
314+
stroke_width: 2,
315+
};
316+
317+
chart
318+
.draw_series(series)?
319+
.label(label)
320+
.legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], shape_style));
321+
}
322+
323+
Ok(())
324+
}
325+
}
326+
327+
pub struct PqCoord {
328+
key_points: Vec<f64>,
329+
range: Range<f64>,
330+
mapper: fn(&f64, (i32, i32)) -> i32,
331+
formatter: fn(&f64) -> String,
242332
}
243333

244-
pub struct PqCoord {}
334+
impl PqCoord {
335+
pub fn for_level(level: u8) -> PqCoord {
336+
if level == 2 {
337+
PqCoord {
338+
key_points: vec![
339+
0.0, 512.0, 1024.0, 1536.0, 2048.0, 2560.0, 3072.0, 3584.0, 4096.0,
340+
],
341+
range: 0_f64..4096.0_f64,
342+
mapper: |value, limit| {
343+
let norm = value / 4096.0;
344+
let size = limit.1 - limit.0;
345+
(norm * size as f64).round() as i32 + limit.0
346+
},
347+
formatter: |value| format!("{value}"),
348+
}
349+
} else {
350+
PqCoord {
351+
key_points: vec![
352+
nits_to_pq(0.01),
353+
nits_to_pq(0.1),
354+
nits_to_pq(0.5),
355+
nits_to_pq(1.0),
356+
nits_to_pq(2.5),
357+
nits_to_pq(5.0),
358+
nits_to_pq(10.0),
359+
nits_to_pq(25.0),
360+
nits_to_pq(50.0),
361+
nits_to_pq(100.0),
362+
nits_to_pq(200.0),
363+
nits_to_pq(400.0),
364+
nits_to_pq(600.0),
365+
nits_to_pq(1000.0),
366+
nits_to_pq(2000.0),
367+
nits_to_pq(4000.0),
368+
nits_to_pq(10000.0),
369+
],
370+
range: 0_f64..1.0_f64,
371+
mapper: |value, limit| {
372+
let size = limit.1 - limit.0;
373+
(*value * size as f64) as i32 + limit.0
374+
},
375+
formatter: |value| {
376+
let nits = (pq_to_nits(*value) * 1000.0).round() / 1000.0;
377+
format!("{nits}")
378+
},
379+
}
380+
}
381+
}
382+
}
245383

246384
impl Ranged for PqCoord {
247385
type FormatOption = NoDefaultFormatting;
248386
type ValueType = f64;
249387

250388
fn map(&self, value: &f64, limit: (i32, i32)) -> i32 {
251-
let size = limit.1 - limit.0;
252-
(*value * size as f64) as i32 + limit.0
389+
(self.mapper)(value, limit)
253390
}
254391

255392
fn key_points<Hint: KeyPointHint>(&self, _hint: Hint) -> Vec<f64> {
256-
vec![
257-
nits_to_pq(0.01),
258-
nits_to_pq(0.1),
259-
nits_to_pq(0.5),
260-
nits_to_pq(1.0),
261-
nits_to_pq(2.5),
262-
nits_to_pq(5.0),
263-
nits_to_pq(10.0),
264-
nits_to_pq(25.0),
265-
nits_to_pq(50.0),
266-
nits_to_pq(100.0),
267-
nits_to_pq(200.0),
268-
nits_to_pq(400.0),
269-
nits_to_pq(600.0),
270-
nits_to_pq(1000.0),
271-
nits_to_pq(2000.0),
272-
nits_to_pq(4000.0),
273-
nits_to_pq(10000.0),
274-
]
393+
self.key_points.clone()
275394
}
276395

277396
fn range(&self) -> Range<f64> {
278-
0_f64..1.0_f64
397+
self.range.clone()
279398
}
280399
}
281400

282401
impl ValueFormatter<f64> for PqCoord {
283402
fn format_ext(&self, value: &f64) -> String {
284-
let nits = (pq_to_nits(*value) * 1000.0).round() / 1000.0;
285-
format!("{nits}")
403+
(self.formatter)(value)
286404
}
287405
}

src/dovi/rpu_info.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ use itertools::Itertools;
77

88
use dolby_vision::rpu::dovi_rpu::DoviRpu;
99
use dolby_vision::rpu::extension_metadata::blocks::{
10-
ExtMetadataBlock, ExtMetadataBlockLevel1, ExtMetadataBlockLevel6,
10+
ExtMetadataBlock, ExtMetadataBlockLevel1, ExtMetadataBlockLevel2, ExtMetadataBlockLevel6,
1111
};
1212
use dolby_vision::rpu::utils::parse_rpu_file;
1313
use dolby_vision::utils::pq_to_nits;
1414

1515
use super::input_from_either;
1616
use crate::commands::InfoArgs;
1717

18+
pub type L2Data = (u16, u16, u16, u16, u16, i16);
19+
1820
pub struct RpuInfo {
1921
input: PathBuf,
2022
}
@@ -31,6 +33,8 @@ pub struct RpusListSummary {
3133
pub l1_data: Vec<(f64, f64, f64)>,
3234
pub l1_stats: SummaryL1Stats,
3335
pub l2_trims: Vec<String>,
36+
pub l2_data: Option<Vec<L2Data>>,
37+
pub l2_stats: Option<SummaryL2Stats>,
3438
}
3539

3640
pub struct SummaryL1Stats {
@@ -43,6 +47,15 @@ pub struct SummaryL1Stats {
4347
pub max_min_nits: f64,
4448
}
4549

50+
pub struct SummaryL2Stats {
51+
pub slope: (f64, f64, f64),
52+
pub offset: (f64, f64, f64),
53+
pub power: (f64, f64, f64),
54+
pub chroma: (f64, f64, f64),
55+
pub saturation: (f64, f64, f64),
56+
pub ms_weight: (f64, f64, f64),
57+
}
58+
4659
impl RpuInfo {
4760
pub fn info(args: InfoArgs) -> Result<()> {
4861
let InfoArgs {
@@ -344,6 +357,71 @@ impl RpusListSummary {
344357
l1_data,
345358
l1_stats,
346359
l2_trims,
360+
l2_data: None,
361+
l2_stats: None,
347362
})
348363
}
364+
365+
pub fn with_l2_data(rpus: &[DoviRpu]) -> Result<Self> {
366+
let mut summary = Self::new(rpus)?;
367+
368+
let default_l2_for_missing = ExtMetadataBlock::Level2(ExtMetadataBlockLevel2::default());
369+
370+
let l2_data: Vec<_> = rpus
371+
.iter()
372+
.map(|rpu| {
373+
let block = rpu
374+
.vdr_dm_data
375+
.as_ref()
376+
.and_then(|dm| dm.get_block(2))
377+
.unwrap_or(&default_l2_for_missing);
378+
379+
if let ExtMetadataBlock::Level2(l2) = block {
380+
(
381+
l2.trim_slope,
382+
l2.trim_offset,
383+
l2.trim_power,
384+
l2.trim_chroma_weight,
385+
l2.trim_saturation_gain,
386+
l2.ms_weight,
387+
)
388+
} else {
389+
unreachable!();
390+
}
391+
})
392+
.collect();
393+
394+
fn min_max_avg<F>(data: &[L2Data], field_extractor: F) -> (f64, f64, f64)
395+
where
396+
F: Fn(&L2Data) -> f64,
397+
{
398+
let mut iter = data.iter().map(field_extractor);
399+
let first = iter.next().unwrap();
400+
let (mut min, mut max, mut sum) = (first, first, first);
401+
402+
for v in iter {
403+
if v < min {
404+
min = v;
405+
}
406+
if v > max {
407+
max = v;
408+
}
409+
sum += v;
410+
}
411+
412+
(min, max, sum / data.len() as f64)
413+
}
414+
415+
summary.l2_stats = Some(SummaryL2Stats {
416+
slope: min_max_avg(&l2_data, |e| e.0 as f64),
417+
offset: min_max_avg(&l2_data, |e| e.1 as f64),
418+
power: min_max_avg(&l2_data, |e| e.2 as f64),
419+
chroma: min_max_avg(&l2_data, |e| e.3 as f64),
420+
saturation: min_max_avg(&l2_data, |e| e.4 as f64),
421+
ms_weight: min_max_avg(&l2_data, |e| e.5 as f64),
422+
});
423+
summary.l2_data = Some(l2_data);
424+
425+
Ok(summary)
426+
}
349427
}

0 commit comments

Comments
 (0)