From 706578ecc803fbffed9eaab60f0d15501281a2b4 Mon Sep 17 00:00:00 2001 From: Naushir Patuck Date: Fri, 10 May 2024 11:02:07 +0100 Subject: ipa: rpi: Add HDR support Add support for the following HDR modes in the Raspberry Pi IPA: - Night mode - Single exposure mode - Multi-exposure (merged and unmerged) The algorithm is updated to expect the HDR short channel to meter explicitly for highlights. This means that it will not in general under-expose the short channel more than is actually necessary. When images don't have much saturation, it's good to detect this so that some of the boost we want to apply to the dark areas can be implemented as regular gain. This means we can then adjust the tone curve less, leading to less flat looking images. The impact on the HDR algorithm is then that this determines how we build tonemaps dynamically. The highlights are more-or-less correct now, so we have to build a power-type curve that gives us the appropriately configured targets in the lower part of the histogram. We allow the tuning file to supply the maximum spatial gain value, rather than the whole curve (though it can supply this if it wants). Some parameter defaults are tweaked to be generally better across the range of our cameras. Signed-off-by: David Plowman Signed-off-by: Naushir Patuck Reviewed-by: David Plowman Reviewed-by: Naushir Patuck Acked-by: Jacopo Mondi Signed-off-by: Kieran Bingham --- src/ipa/rpi/controller/rpi/hdr.cpp | 112 +++++++++++++++++++++++++++++++------ src/ipa/rpi/controller/rpi/hdr.h | 10 +++- 2 files changed, 105 insertions(+), 17 deletions(-) (limited to 'src/ipa/rpi/controller') diff --git a/src/ipa/rpi/controller/rpi/hdr.cpp b/src/ipa/rpi/controller/rpi/hdr.cpp index 0019d508..34cf360e 100644 --- a/src/ipa/rpi/controller/rpi/hdr.cpp +++ b/src/ipa/rpi/controller/rpi/hdr.cpp @@ -7,6 +7,8 @@ #include "hdr.h" +#include + #include #include "../agc_status.h" @@ -39,25 +41,52 @@ void HdrConfig::read(const libcamera::YamlObject ¶ms, const std::string &mod channelMap[v.get().value()] = k; /* Lens shading related parameters. */ - if (params.contains("spatial_gain")) { - spatialGain.read(params["spatial_gain"]); - diffusion = params["diffusion"].get(3); - /* Clip to an arbitrary limit just to stop typos from killing the system! */ - const unsigned int MAX_DIFFUSION = 15; - if (diffusion > MAX_DIFFUSION) { - diffusion = MAX_DIFFUSION; - LOG(RPiHdr, Warning) << "Diffusion value clipped to " << MAX_DIFFUSION; - } + if (params.contains("spatial_gain_curve")) { + spatialGainCurve.read(params["spatial_gain_curve"]); + } else if (params.contains("spatial_gain")) { + double spatialGain = params["spatial_gain"].get(2.0); + spatialGainCurve.append(0.0, spatialGain); + spatialGainCurve.append(0.01, spatialGain); + spatialGainCurve.append(0.06, 1.0); /* maybe make this programmable? */ + spatialGainCurve.append(1.0, 1.0); + } + + diffusion = params["diffusion"].get(3); + /* Clip to an arbitrary limit just to stop typos from killing the system! */ + const unsigned int MAX_DIFFUSION = 15; + if (diffusion > MAX_DIFFUSION) { + diffusion = MAX_DIFFUSION; + LOG(RPiHdr, Warning) << "Diffusion value clipped to " << MAX_DIFFUSION; } /* Read any tonemap parameters. */ tonemapEnable = params["tonemap_enable"].get(0); - detailConstant = params["detail_constant"].get(50); - detailSlope = params["detail_slope"].get(8.0); + detailConstant = params["detail_constant"].get(0); + detailSlope = params["detail_slope"].get(0.0); iirStrength = params["iir_strength"].get(8.0); strength = params["strength"].get(1.5); if (tonemapEnable) tonemap.read(params["tonemap"]); + speed = params["speed"].get(1.0); + if (params.contains("hi_quantile_targets")) { + hiQuantileTargets = params["hi_quantile_targets"].getList().value(); + if (hiQuantileTargets.empty() || hiQuantileTargets.size() % 2) + LOG(RPiHdr, Fatal) << "hi_quantile_targets much be even and non-empty"; + } else + hiQuantileTargets = { 0.95, 0.65, 0.5, 0.28, 0.3, 0.25 }; + hiQuantileMaxGain = params["hi_quantile_max_gain"].get(1.6); + if (params.contains("quantile_targets")) { + quantileTargets = params["quantile_targets"].getList().value(); + if (quantileTargets.empty() || quantileTargets.size() % 2) + LOG(RPiHdr, Fatal) << "quantile_targets much be even and non-empty"; + } else + quantileTargets = { 0.2, 0.03, 1.0, 0.15 }; + powerMin = params["power_min"].get(0.65); + powerMax = params["power_max"].get(1.0); + if (params.contains("contrast_adjustments")) { + contrastAdjustments = params["contrast_adjustments"].getList().value(); + } else + contrastAdjustments = { 0.5, 0.75 }; /* Read any stitch parameters. */ stitchEnable = params["stitch_enable"].get(0); @@ -159,7 +188,7 @@ void Hdr::prepare(Metadata *imageMetadata) } HdrConfig &config = it->second; - if (config.spatialGain.empty()) + if (config.spatialGainCurve.empty()) return; AlscStatus alscStatus{}; /* some compilers seem to require the braces */ @@ -205,10 +234,61 @@ bool Hdr::updateTonemap([[maybe_unused]] StatisticsPtr &stats, HdrConfig &config return true; /* - * If we wanted to build or adjust tonemaps dynamically, this would be the place - * to do it. But for now we seem to be getting by without. + * Create a tonemap dynamically. We have three ingredients. + * + * 1. We have a list of "hi quantiles" and "targets". We use these to judge if + * the image does seem to be reasonably saturated. If it isn't, we calculate + * a gain that we will feed as a linear factor into the tonemap generation. + * This prevents unsaturated images from beoming quite so "flat". + * + * 2. We have a list of quantile/target pairs for the bottom of the histogram. + * We use these to calculate how much gain we must apply to the bottom of the + * tonemap. We apply this gain as a power curve so as not to blow out the top + * end. + * + * 3. Finally, when we generate the tonemap, we have some contrast adjustments + * for the bottom because we know that power curves can start quite steeply and + * cause a washed-out look. */ + /* Compute the linear gain from the headroom for saturation at the top. */ + double gain = 10; /* arbitrary, but hiQuantileMaxGain will clamp it later */ + for (unsigned int i = 0; i < config.hiQuantileTargets.size(); i += 2) { + double quantile = config.hiQuantileTargets[i]; + double target = config.hiQuantileTargets[i + 1]; + double value = stats->yHist.interQuantileMean(quantile, 1.0) / 1024.0; + double newGain = target / (value + 0.01); + gain = std::min(gain, newGain); + } + gain = std::clamp(gain, 1.0, config.hiQuantileMaxGain); + + /* Compute the power curve from the amount of gain needed at the bottom. */ + double min_power = 2; /* arbitrary, but config.powerMax will clamp it later */ + for (unsigned int i = 0; i < config.quantileTargets.size(); i += 2) { + double quantile = config.quantileTargets[i]; + double target = config.quantileTargets[i + 1]; + double value = stats->yHist.interQuantileMean(0, quantile) / 1024.0; + value = std::min(value * gain, 1.0); + double power = log(target + 1e-6) / log(value + 1e-6); + min_power = std::min(min_power, power); + } + double power = std::clamp(min_power, config.powerMin, config.powerMax); + + /* Generate the tonemap, including the contrast adjustment factors. */ + Pwl tonemap; + tonemap.append(0, 0); + for (unsigned int i = 0; i <= 6; i++) { + double x = 1 << (i + 9); /* x loops from 512 to 32768 inclusive */ + double y = pow(std::min(x * gain, 65535.0) / 65536.0, power) * 65536; + if (i < config.contrastAdjustments.size()) + y *= config.contrastAdjustments[i]; + if (!tonemap_.empty()) + y = y * config.speed + tonemap_.eval(x) * (1 - config.speed); + tonemap.append(x, y); + } + tonemap.append(65535, 65535); + tonemap_ = tonemap; + return true; } @@ -255,7 +335,7 @@ static void averageGains(std::vector &src, std::vector &dst, con void Hdr::updateGains(StatisticsPtr &stats, HdrConfig &config) { - if (config.spatialGain.empty()) + if (config.spatialGainCurve.empty()) return; /* When alternating exposures, only compute these gains for the short frame. */ @@ -270,7 +350,7 @@ void Hdr::updateGains(StatisticsPtr &stats, HdrConfig &config) double g = region.val.gSum / counted; double b = region.val.bSum / counted; double brightness = std::max({ r, g, b }) / 65535; - gains_[0][i] = config.spatialGain.eval(brightness); + gains_[0][i] = config.spatialGainCurve.eval(brightness); } /* Ping-pong between the two gains_ buffers. */ diff --git a/src/ipa/rpi/controller/rpi/hdr.h b/src/ipa/rpi/controller/rpi/hdr.h index 44272cb8..9b7327f8 100644 --- a/src/ipa/rpi/controller/rpi/hdr.h +++ b/src/ipa/rpi/controller/rpi/hdr.h @@ -26,7 +26,7 @@ struct HdrConfig { std::map channelMap; /* Lens shading related parameters. */ - Pwl spatialGain; /* Brightness to gain curve for different image regions. */ + Pwl spatialGainCurve; /* Brightness to gain curve for different image regions. */ unsigned int diffusion; /* How much to diffuse the gain spatially. */ /* Tonemap related parameters. */ @@ -36,6 +36,14 @@ struct HdrConfig { double iirStrength; double strength; Pwl tonemap; + /* These relate to adaptive tonemap calculation. */ + double speed; + std::vector hiQuantileTargets; /* quantiles to check for unsaturated images */ + double hiQuantileMaxGain; /* the max gain we'll apply when unsaturated */ + std::vector quantileTargets; /* target values for histogram quantiles */ + double powerMin; /* minimum tonemap power */ + double powerMax; /* maximum tonemap power */ + std::vector contrastAdjustments; /* any contrast adjustment factors */ /* Stitch related parameters. */ bool stitchEnable; -- cgit v1.2.1