use anyhow::{Context, Result};
use clap::{Args, Subcommand, ValueEnum};
use seiza_stretch::{ColorStrategy, GhsParams, StretchConfig, StretchModel, StretchParams};
use std::path::PathBuf;
#[derive(Args)]
pub(crate) struct StretchArgs {
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(long, value_enum, default_value = "linked")]
color_strategy: ColorStrategyArg,
#[arg(long, default_value_t = crate::preview::MAXIMUM_SAMPLES)]
max_analysis_samples: usize,
#[command(subcommand)]
model: StretchModelArgs,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum ColorStrategyArg {
Linked,
Unlinked,
LuminancePreserving,
}
impl From<ColorStrategyArg> for ColorStrategy {
fn from(value: ColorStrategyArg) -> Self {
match value {
ColorStrategyArg::Linked => Self::Linked,
ColorStrategyArg::Unlinked => Self::Unlinked,
ColorStrategyArg::LuminancePreserving => Self::LuminancePreserving,
}
}
}
impl ColorStrategyArg {
fn summary(self) -> &'static str {
match self {
Self::Linked => "linked channels",
Self::Unlinked => "unlinked channels",
Self::LuminancePreserving => "luminance-preserving",
}
}
}
#[derive(Subcommand)]
enum StretchModelArgs {
Identity,
Linear {
#[arg(long)]
black: f64,
#[arg(long)]
white: f64,
},
Asinh {
#[arg(long)]
black: f64,
#[arg(long)]
white: f64,
#[arg(long, default_value_t = 10.0)]
strength: f64,
},
PercentileAsinh {
#[arg(long, default_value_t = 0.01)]
black_percentile: f64,
#[arg(long, default_value_t = 0.995)]
white_percentile: f64,
#[arg(long, default_value_t = 10.0)]
strength: f64,
},
Mtf {
#[arg(long)]
shadows: f64,
#[arg(long)]
midtone: f64,
#[arg(long)]
highlights: f64,
},
Ghs {
#[arg(long, default_value_t = 1.0)]
stretch_factor: f64,
#[arg(long, default_value_t = 0.0, allow_negative_numbers = true)]
local_intensity: f64,
#[arg(long, default_value_t = 0.0)]
symmetry_point: f64,
#[arg(long, default_value_t = 0.0)]
protect_shadows: f64,
#[arg(long, default_value_t = 1.0)]
protect_highlights: f64,
#[arg(long, default_value_t = 0.0, allow_negative_numbers = true)]
black: f64,
#[arg(long, default_value_t = 1.0, allow_negative_numbers = true)]
white: f64,
},
AutoMtf {
#[arg(long, default_value_t = 0.2)]
target_median: f64,
#[arg(long, default_value_t = -2.8, allow_negative_numbers = true)]
shadows_clip: f64,
},
}
impl StretchModelArgs {
fn model(&self) -> StretchModel {
match *self {
Self::Identity => StretchModel::Identity,
Self::Linear { black, white } => StretchModel::Linear { black, white },
Self::Asinh {
black,
white,
strength,
} => StretchModel::Asinh {
black,
white,
strength,
},
Self::PercentileAsinh {
black_percentile,
white_percentile,
strength,
} => StretchModel::PercentileAsinh {
black_percentile,
white_percentile,
strength,
},
Self::Mtf {
shadows,
midtone,
highlights,
} => StretchModel::Mtf {
shadows,
midtone,
highlights,
},
Self::Ghs {
stretch_factor,
local_intensity,
symmetry_point,
protect_shadows,
protect_highlights,
black,
white,
} => StretchModel::Ghs(GhsParams {
stretch_factor,
local_intensity,
symmetry_point,
protect_shadows,
protect_highlights,
black,
white,
}),
Self::AutoMtf {
target_median,
shadows_clip,
} => StretchModel::AutoMtf(StretchParams {
target_median,
shadows_clip,
}),
}
}
fn summary(&self) -> String {
match *self {
Self::Identity => "identity clamp to [0, 1]".to_string(),
Self::Linear { black, white } => format!("linear black {black}, white {white}"),
Self::Asinh {
black,
white,
strength,
} => format!("asinh black {black}, white {white}, strength {strength}"),
Self::PercentileAsinh {
black_percentile,
white_percentile,
strength,
} => format!(
"percentile asinh black {black_percentile}, white {white_percentile}, strength {strength}"
),
Self::Mtf {
shadows,
midtone,
highlights,
} => format!("MTF shadows {shadows}, midtone {midtone}, highlights {highlights}"),
Self::Ghs {
stretch_factor,
local_intensity,
symmetry_point,
protect_shadows,
protect_highlights,
black,
white,
} => format!(
"generalized hyperbolic stretch factor {stretch_factor}, local intensity {local_intensity}, symmetry {symmetry_point}, shadow protection {protect_shadows}, highlight protection {protect_highlights}, black {black}, white {white}"
),
Self::AutoMtf {
target_median,
shadows_clip,
} => format!("auto MTF target median {target_median}, shadows clip {shadows_clip}"),
}
}
}
pub(crate) fn run(args: StretchArgs) -> Result<()> {
crate::provenance::validate_path_roles([
("stretch input".into(), args.input.as_path()),
("stretch output".into(), args.output.as_path()),
])?;
let frame = crate::common::open_frame(&args.input, "stretch input")?;
let config = StretchConfig {
model: args.model.model(),
color_strategy: args.color_strategy.into(),
max_analysis_samples: args.max_analysis_samples,
};
let plan = config
.resolve_for(&frame.image.data, frame.image.channels)
.context("could not resolve stretch model")?;
let pixels = plan
.apply_u8(&frame.image.data, frame.image.channels)
.context("could not apply stretch model")?;
crate::preview::write_display_image(
&args.output,
frame.image.width,
frame.image.height,
frame.image.channels,
pixels,
)?;
crate::common::wrote(
&args.output,
format_args!(
"{}, {}, display-referred",
args.model.summary(),
args.color_strategy.summary(),
),
);
Ok(())
}