14.26: adaptive Contrast, Highlights and Shadows; Dehaze, Texture, Clarity and Sharpening

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 13:33:45 -04:00
1 parent a1453df517
commit 32f6362787
12 files changed
+289 -153

No files matched your search

+120 -51
View File
@@ -201,6 +201,16 @@ static double camera_clamp(double value) {
return value;
}
static double srgb_decode(double encoded) {
return encoded <= 0.04045 ? encoded / 12.92 : pow((encoded + 0.055) / 1.055, 2.4);
}
static double srgb_encode(double linear) {
if (linear <= 0) return 0;
if (linear >= 1) return 1;
return linear <= 0.0031308 ? linear * 12.92 : 1.055 * pow(linear, 1.0 / 2.4) - 0.055;
}
static double rec709(double r, double g, double b) {
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
@@ -221,28 +231,6 @@ static void scale_luminance(double *r, double *g, double *b, double target) {
*b = camera_clamp(*b * scale);
}
// A bump over a range of tones, 0 at both ends: u(1 - 2u)², largest a sixth of the way in. Its slope runs from 1
// down to -1/3, so a curve adding up to three times it (or taking away up to once it) keeps rising: tones never swap
// places.
static double tone_bump(double u) {
if (u <= 0 || u >= 0.5) return 0;
double rest = 1.0 - 2.0 * u;
return u * rest * rest;
}
// Shadows lifts (or deepens) the dark tones, most a third of the way up to middle gray, leaving black at black and
// middle gray where it is. It once lifted black itself to middle gray, above the tones just over it, so the darkest,
// least certain pixels were the ones stretched furthest: a lifted dark area broke into blotches of color.
static double tone_shadows(double y, double amount) {
return camera_clamp(y + amount * (amount >= 0 ? 2.5 : 1.0) * tone_bump(y));
}
// Highlights, the same in the light tones: white stays white, and pulling them down no longer drops white below the
// tones under it.
static double tone_highlights(double y, double amount) {
return camera_clamp(y + amount * (amount >= 0 ? 1.0 : 2.5) * tone_bump(1.0 - y));
}
// A measured table's color for an sRGB color (0…1), between its eight nearest entries.
static void table_lookup(const uint8_t *table, int size, const double in[3], double out[3]) {
double f[3];
@@ -324,10 +312,30 @@ static void write_premultiplied(uint8_t *p, double r, double g, double b, double
p[2] = (uint8_t)fmin(alpha, fmax(0.0, round(b * alpha)));
}
void adjust_camera_raw(uint8_t *rgba, size_t width, size_t height, size_t stride, const float *before,
const float *after, int grid, double highlights, double shadows, int clipping) {
double highlightAmount = highlights / 100.0;
double shadowAmount = shadows / 100.0;
void camera_raw_statistics(const uint8_t *rgba, size_t width, size_t height, size_t stride, double *out) {
double brightest = 0, luminance = 0, logBrightest = 0, weight = 0;
for (size_t y = 0; y < height; ++y) {
const uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
const uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
double r = srgb_decode(fmin(1.0, p[0] / alpha)), g = srgb_decode(fmin(1.0, p[1] / alpha));
double b = srgb_decode(fmin(1.0, p[2] / alpha)), top = fmax(r, fmax(g, b)), w = alpha / 255.0;
brightest += w * top;
luminance += w * rec709(r, g, b);
logBrightest += w * log(top + 3e-4);
weight += w;
}
}
if (weight <= 0) { out[0] = out[1] = out[2] = 0.5; return; }
out[0] = srgb_encode(brightest / weight);
out[1] = srgb_encode(luminance / weight);
out[2] = srgb_encode(exp(logBrightest / weight) - 3e-4);
}
void adjust_camera_raw(uint8_t *rgba, size_t width, size_t height, size_t stride, const float *table, int grid,
int clipping) {
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
@@ -337,10 +345,7 @@ void adjust_camera_raw(uint8_t *rgba, size_t width, size_t height, size_t stride
double r = fmin(255.0, p[0] * 255.0 / alpha) / 255.0;
double g = fmin(255.0, p[1] * 255.0 / alpha) / 255.0;
double b = fmin(255.0, p[2] * 255.0 / alpha) / 255.0;
if (before) composed_lookup(before, grid, &r, &g, &b);
if (highlightAmount != 0) scale_luminance(&r, &g, &b, tone_highlights(rec709(r, g, b), highlightAmount));
if (shadowAmount != 0) scale_luminance(&r, &g, &b, tone_shadows(rec709(r, g, b), shadowAmount));
if (after) composed_lookup(after, grid, &r, &g, &b);
if (table) composed_lookup(table, grid, &r, &g, &b);
if (clipping == 1) {
int rc = r >= 254.5 / 255.0, gc = g >= 254.5 / 255.0, bc = b >= 254.5 / 255.0;
r = rc ? 1 : 0;
@@ -398,6 +403,30 @@ static int box_blur_plane(const float *src, float *dst, size_t width, size_t hei
return 1;
}
// An edge-aware blur (He's guided filter, the plane guiding itself): smooth within regions, stopping at edges, so
// the detail taken against it has no halos. `epsilon` is the variance an edge needs to be kept.
static int guided_filter_plane(const float *src, float *dst, size_t width, size_t height, int radius, float epsilon) {
size_t count = width * height;
float *mean = malloc(count * sizeof(float)), *square = malloc(count * sizeof(float));
float *a = malloc(count * sizeof(float)), *b = malloc(count * sizeof(float));
int ok = mean && square && a && b && box_blur_plane(src, mean, width, height, radius);
if (ok) {
for (size_t i = 0; i < count; ++i) a[i] = src[i] * src[i];
ok = box_blur_plane(a, square, width, height, radius);
}
if (ok) {
for (size_t i = 0; i < count; ++i) {
float variance = square[i] - mean[i] * mean[i];
a[i] = variance / (variance + epsilon);
b[i] = mean[i] - a[i] * mean[i];
}
ok = box_blur_plane(a, mean, width, height, radius) && box_blur_plane(b, square, width, height, radius);
}
if (ok) for (size_t i = 0; i < count; ++i) dst[i] = mean[i] * src[i] + square[i];
free(mean); free(square); free(a); free(b);
return ok;
}
static int effects_radius(double base, double scale) {
double radius = base * (scale > 0 ? scale : 1);
if (radius < 1) radius = 1;
@@ -405,20 +434,51 @@ static int effects_radius(double base, double scale) {
return (int)lround(radius);
}
static void effects_dehaze(double *r, double *g, double *b, double amount) {
double d = amount / 100.0;
double y = rec709(*r, *g, *b);
double contrast = 1.0 + 0.8 * d;
double pivot = 0.45 - 0.1 * (d > 0 ? d : 0);
double y2 = camera_clamp(pivot + (y - 0.45) * contrast);
if (d < 0) y2 = camera_clamp(y2 + (-d) * (1.0 - y2) * 0.45);
else y2 = camera_clamp(y2 - d * fmax(0.0, 0.4 - y2));
scale_luminance(r, g, b, y2);
y2 = rec709(*r, *g, *b);
double sat = 1.0 + 0.7 * d;
*r = camera_clamp(y2 + (*r - y2) * sat);
*g = camera_clamp(y2 + (*g - y2) * sat);
*b = camera_clamp(y2 + (*b - y2) * sat);
// Dehaze, after Camera Raw's: `haze` is the picture's haze color, its brightest one percent. Taking haze away
// (positive) removes it where a pixel's darkest channel, against the haze, says it lies; adding it (negative) veils the
// picture in it. Measured against Photoshop's Camera Raw Filter.
static void effects_dehaze(double *r, double *g, double *b, double amount, const double haze[3]) {
double d = amount / 100.0, c[3] = {*r, *g, *b};
if (d < 0) {
double veil = 0.3 * pow(-d, 1.3);
for (int k = 0; k < 3; ++k) c[k] = c[k] * (1 - veil) + haze[k] * veil;
} else {
double dark = 1;
for (int k = 0; k < 3; ++k) dark = fmin(dark, c[k] / fmax(haze[k], 1e-3));
double transmission = fmax(0.5, 1 - 0.7 * d * dark);
for (int k = 0; k < 3; ++k) c[k] = (c[k] - haze[k]) / transmission + haze[k];
}
*r = camera_clamp(c[0]);
*g = camera_clamp(c[1]);
*b = camera_clamp(c[2]);
}
// The picture's haze color for Dehaze: the mean of its brightest one percent of pixels by luminance. A picture of
// one color is all haze, so Dehaze leaves it be, as Photoshop's does.
static void dehaze_haze(const uint8_t *rgba, size_t width, size_t height, size_t stride, double haze[3]) {
double histogram[256] = {0};
for (size_t y = 0; y < height; ++y)
for (size_t x = 0; x < width; ++x) {
const uint8_t *p = rgba + y * stride + x * 4;
if (!p[3]) continue;
int level = (int)lround(255 * rec709(fmin(1.0, (double)p[0] / p[3]), fmin(1.0, (double)p[1] / p[3]), fmin(1.0, (double)p[2] / p[3])));
histogram[level] += p[3] / 255.0;
}
double total = 0;
for (int i = 0; i < 256; ++i) total += histogram[i];
int threshold = 255;
for (double above = 0; threshold > 0 && above + histogram[threshold] < total * 0.01; --threshold) above += histogram[threshold];
double sum[3] = {0, 0, 0}, weight = 0;
for (size_t y = 0; y < height; ++y)
for (size_t x = 0; x < width; ++x) {
const uint8_t *p = rgba + y * stride + x * 4;
if (!p[3]) continue;
double c[3] = {fmin(1.0, (double)p[0] / p[3]), fmin(1.0, (double)p[1] / p[3]), fmin(1.0, (double)p[2] / p[3])};
if (lround(255 * rec709(c[0], c[1], c[2])) < threshold) continue;
for (int k = 0; k < 3; ++k) sum[k] += c[k] * p[3];
weight += p[3];
}
for (int k = 0; k < 3; ++k) haze[k] = weight > 0 ? sum[k] / weight : 1;
}
/// The vignette's strength at a point `px`, `py` of a `width` × `height` frame (0 at its middle, 1 past its edges).
@@ -561,13 +621,18 @@ void adjust_camera_raw_effects(uint8_t *rgba, size_t width, size_t height, size_
luma[y * width + x] = (float)rec709(r, g, b);
}
}
// Radii in proportion to the picture, as fitted to Photoshop's on 1456-pixel-wide photos (8 and 32 there), so
// a downscaled preview matches the full-size result.
double side = (double)(width > height ? width : height);
if (texture != 0) {
fine = malloc(count * sizeof(float));
if (!fine || !box_blur_plane(luma, fine, width, height, effects_radius(1, scale))) failed = 1;
int radius = (int)fmax(1, lround(side * 8 / 1456));
if (!fine || !guided_filter_plane(luma, fine, width, height, radius, 0.01f)) failed = 1;
}
if (!failed && clarity != 0) {
coarse = malloc(count * sizeof(float));
if (!coarse || !box_blur_plane(luma, coarse, width, height, effects_radius(4, scale))) failed = 1;
int radius = (int)fmax(2, lround(side * 32 / 1456));
if (!coarse || !guided_filter_plane(luma, coarse, width, height, radius, 0.01f)) failed = 1;
}
if (!failed && glow > 0) {
double spread = glowSpread / 100.0;
@@ -597,6 +662,8 @@ void adjust_camera_raw_effects(uint8_t *rgba, size_t width, size_t height, size_
free(luma); free(fine); free(coarse); free(glowPlane);
return;
}
double haze[3] = {1, 1, 1};
if (dehaze != 0) dehaze_haze(rgba, width, height, stride, haze);
double warmth = glowWarmth / 100.0;
double glowRed, glowGreen, glowBlue, glowGain;
if (glowStyle == 2) {
@@ -624,11 +691,11 @@ void adjust_camera_raw_effects(uint8_t *rgba, size_t width, size_t height, size_
if (fine || coarse) {
double tone = rec709(r, g, b);
double detail = 0;
if (fine) detail += (texture / 100.0) * (tone - fine[index]);
if (coarse) detail += (clarity / 100.0) * (tone - coarse[index]);
if (fine) detail += (texture / 100.0) * (texture > 0 ? 0.6 : 0.5) * (tone - fine[index]);
if (coarse) detail += (clarity / 100.0) * (clarity > 0 ? 0.85 : 0.5) * (tone - coarse[index]);
if (detail != 0) scale_luminance(&r, &g, &b, camera_clamp(tone + detail));
}
if (dehaze != 0) effects_dehaze(&r, &g, &b, dehaze);
if (dehaze != 0) effects_dehaze(&r, &g, &b, dehaze, haze);
if (glowPlane && glow > 0) {
double add = glowPlane[index] * (glow / 100.0) * glowGain;
r = camera_clamp(r + add * glowRed);
@@ -1035,9 +1102,11 @@ void adjust_camera_raw_detail(uint8_t *rgba, size_t width, size_t height, size_t
if (!alpha) continue;
size_t index = y * width + x;
float edge = sharpen_edge_at(luma, width, height, x, y, radius);
double mask = camera_clamp((edge * (0.5 + detailMix) - threshold) / fmax(0.04, 0.35 - threshold * 0.5));
// Masking at 0 sharpens everywhere, as in Photoshop; raising it keeps the sharpening to edges.
double mask = threshold > 0 ? camera_clamp((edge * (0.5 + detailMix) - threshold) / fmax(0.04, 0.35 - threshold * 0.5)) : 1;
double high = luma[index] - work[index];
double sharpened = camera_clamp(luma[index] + high * amount * mask * (0.5 + detailMix));
// 1.9 × (0.5 + Detail) is Photoshop's strength: 1.4 at its default Detail of 25, measured on photos.
double sharpened = camera_clamp(luma[index] + high * amount * mask * 1.9 * (0.5 + detailMix));
double r = fmin(1.0, p[0] / alpha), g = fmin(1.0, p[1] / alpha), b = fmin(1.0, p[2] / alpha);
scale_luminance(&r, &g, &b, sharpened);
write_premultiplied(p, r, g, b, alpha);