Camera Raw filter's model and pixel kernels (12.8a)

This commit is contained in:
雷电芽衣 committed 2026-09-27 19:55:59 -04:00
1 parent 4291f4249a
commit b544e12b1d
33 files changed
+4438 -27

No files matched your search

+888 -14
View File
@@ -1,5 +1,8 @@
#include "AdjustPixels.h"
#include "LensPixels.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
void adjust_gradient_map(uint8_t *rgba, size_t width, size_t height, size_t stride, const uint8_t *table) {
for (size_t y = 0; y < height; y++) {
@@ -41,6 +44,21 @@ static inline float lattice(int64_t ix, int64_t iy, uint32_t seed) {
return (float)(h & 0xFFFFU) / 65535.0f + (float)(h >> 16) / 65535.0f - 1.0f;
}
// Smooth seeded noise whose features follow `scale` document pixels. Keeping both the broad and
// detailed patterns relative to the requested grain size makes Size remain visible at any Roughness.
static inline float grain_field(double u, double v, double scale, uint32_t seed) {
double cellX = floor(u / scale), cellY = floor(v / scale);
float tx = (float)(u / scale - cellX), ty = (float)(v / scale - cellY);
tx = tx * tx * (3.0f - 2.0f * tx);
ty = ty * ty * (3.0f - 2.0f * ty);
int64_t ix = (int64_t)cellX, iy = (int64_t)cellY;
float n00 = lattice(ix, iy, seed), n10 = lattice(ix + 1, iy, seed);
float n01 = lattice(ix, iy + 1, seed), n11 = lattice(ix + 1, iy + 1, seed);
float top = n00 + (n10 - n00) * tx, bottom = n01 + (n11 - n01) * tx;
// Blending neighboring lattice values narrows the spread; restore approximately its original range.
return (top + (bottom - top) * ty) * 1.6f;
}
static inline float clamp255(float value) { return value < 0 ? 0 : value > 255 ? 255 : value; }
void adjust_grain(uint8_t *rgba, size_t width, size_t height, size_t stride, double amount, double size,
@@ -50,27 +68,18 @@ void adjust_grain(uint8_t *rgba, size_t width, size_t height, size_t stride, dou
float strength = (float)(amount > 100 ? 1.0 : amount / 100.0) * 0.35f * 255.0f;
float rough = (float)(roughness < 0 ? 0.0 : roughness > 100 ? 1.0 : roughness / 100.0);
uint32_t fineSeed = mix32(seed ^ 0xA511E9B3U);
// Roughness adds smaller, less regular particles, as in Photoshop, but their size remains
// proportional to the Size control instead of collapsing to fixed one-pixel noise.
double detailSize = fmax(0.5, size * 0.35);
for (size_t y = 0; y < height; y++) {
double v = originY + ((double)y + 0.5) * unitsPerPixel;
double cellY = floor(v / size);
float ty = (float)(v / size - cellY);
ty = ty * ty * (3.0f - 2.0f * ty);
int64_t iy = (int64_t)cellY, fineY = (int64_t)floor(v);
uint8_t *p = rgba + y * stride;
for (size_t x = 0; x < width; x++, p += 4) {
unsigned a = p[3];
if (a == 0) continue;
double u = originX + ((double)x + 0.5) * unitsPerPixel;
double cellX = floor(u / size);
float tx = (float)(u / size - cellX);
tx = tx * tx * (3.0f - 2.0f * tx);
int64_t ix = (int64_t)cellX;
float n00 = lattice(ix, iy, seed), n10 = lattice(ix + 1, iy, seed);
float n01 = lattice(ix, iy + 1, seed), n11 = lattice(ix + 1, iy + 1, seed);
float top = n00 + (n10 - n00) * tx, bottom = n01 + (n11 - n01) * tx;
// Blending neighbors narrows the spread; scaling restores about the lattice's own.
float smooth = (top + (bottom - top) * ty) * 1.6f;
float fine = lattice((int64_t)floor(u), fineY, fineSeed);
float smooth = grain_field(u, v, size, seed);
float fine = grain_field(u, v, detailSize, fineSeed);
float noise = smooth + (fine - smooth) * rough;
float unpremultiply = a == 255 ? 1.0f : 255.0f / (float)a;
float r = p[0] * unpremultiply, g = p[1] * unpremultiply, b = p[2] * unpremultiply;
@@ -185,3 +194,868 @@ void adjust_color_balance(uint8_t *rgba, size_t width, size_t height, size_t str
}
}
}
static double camera_clamp(double value) {
if (value < 0) return 0;
if (value > 1) return 1;
return value;
}
static double srgb_to_linear(double encoded) {
if (encoded <= 0.04045) return encoded / 12.92;
return pow((encoded + 0.055) / 1.055, 2.4);
}
static double linear_to_srgb(double linear) {
if (linear <= 0) return 0;
if (linear >= 1) return 1;
if (linear <= 0.0031308) return linear * 12.92;
return 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;
}
// Moves r, g, b so their Rec. 709 luminance becomes `target`, keeping the hue. Pure black cannot
// be scaled, so a lift paints neutral light of that luminance.
static void scale_luminance(double *r, double *g, double *b, double target) {
target = camera_clamp(target);
double y = rec709(*r, *g, *b);
if (fabs(target - y) < 1e-8) return;
if (y < 1e-8) {
if (target > y) *r = *g = *b = target;
return;
}
double scale = target / y;
*r = camera_clamp(*r * scale);
*g = camera_clamp(*g * scale);
*b = camera_clamp(*b * scale);
}
static double tone_highlights(double y, double amount) {
double t = camera_clamp((y - 0.5) / 0.5);
double weight = t * t;
if (amount >= 0) return camera_clamp(y + amount * weight * (1.0 - y));
return camera_clamp(y + amount * weight * (y - 0.5));
}
static double tone_shadows(double y, double amount) {
double t = camera_clamp((0.5 - y) / 0.5);
double weight = t * t;
if (amount >= 0) return camera_clamp(y + amount * weight * (0.5 - y));
return camera_clamp(y + amount * weight * y);
}
// The top quarter is the white point: +1 maps 0.875 to 1, −1 pulls everything above 0.75 down to 0.75.
static double tone_whites(double y, double amount) {
if (y <= 0.75) return y;
return camera_clamp(0.75 + (y - 0.75) * (1.0 + amount));
}
// The bottom quarter is the black point. Negative amounts crush toward 0; positive ones lift toward 0.25.
static double tone_blacks(double y, double amount) {
if (y >= 0.25) return y;
return camera_clamp(0.25 + (y - 0.25) * (1.0 - amount));
}
static void vibrance_and_saturation(double *r, double *g, double *b, double vibrance, double saturation) {
double lum = rec709(*r, *g, *b);
double maxc = fmax(*r, fmax(*g, *b));
double minc = fmin(*r, fmin(*g, *b));
double chroma = maxc - minc;
double sat = maxc <= 1e-8 ? 0 : chroma / maxc;
double hue = 0;
if (chroma > 1e-8) {
if (*r >= *g && *r >= *b) hue = 60.0 * fmod((*g - *b) / chroma, 6.0);
else if (*g >= *r && *g >= *b) hue = 60.0 * ((*b - *r) / chroma + 2.0);
else hue = 60.0 * ((*r - *g) / chroma + 4.0);
if (hue < 0) hue += 360.0;
}
double skin = 0;
if (hue >= 10.0 && hue <= 50.0) {
skin = hue <= 30.0 ? (hue - 10.0) / 20.0 : (50.0 - hue) / 20.0;
skin *= camera_clamp((sat - 0.15) / 0.35);
}
double amount = vibrance * (1.0 - sat);
if (vibrance > 0) amount *= 1.0 - 0.7 * skin;
double factor = 1.0 + amount;
*r = camera_clamp(lum + (*r - lum) * factor);
*g = camera_clamp(lum + (*g - lum) * factor);
*b = camera_clamp(lum + (*b - lum) * factor);
lum = rec709(*r, *g, *b);
factor = 1.0 + saturation;
*r = camera_clamp(lum + (*r - lum) * factor);
*g = camera_clamp(lum + (*g - lum) * factor);
*b = camera_clamp(lum + (*b - lum) * factor);
}
static void write_premultiplied(uint8_t *p, double r, double g, double b, double alpha) {
p[0] = (uint8_t)fmin(alpha, fmax(0.0, round(r * alpha)));
p[1] = (uint8_t)fmin(alpha, fmax(0.0, round(g * alpha)));
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,
double redGain, double greenGain, double blueGain, double exposure, double contrast,
double highlights, double shadows, double whites, double blacks,
double vibrance, double saturation, int clipping) {
double light = exp2(exposure);
double contrastScale = 1.0 + contrast / 100.0;
double highlightAmount = highlights / 100.0;
double shadowAmount = shadows / 100.0;
double whiteAmount = whites / 100.0;
double blackAmount = blacks / 100.0;
double vibranceAmount = vibrance / 100.0;
double saturationAmount = saturation / 100.0;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
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;
r = camera_clamp(srgb_to_linear(r) * redGain * light);
g = camera_clamp(srgb_to_linear(g) * greenGain * light);
b = camera_clamp(srgb_to_linear(b) * blueGain * light);
r = camera_clamp(0.5 + (linear_to_srgb(r) - 0.5) * contrastScale);
g = camera_clamp(0.5 + (linear_to_srgb(g) - 0.5) * contrastScale);
b = camera_clamp(0.5 + (linear_to_srgb(b) - 0.5) * contrastScale);
scale_luminance(&r, &g, &b, tone_highlights(rec709(r, g, b), highlightAmount));
scale_luminance(&r, &g, &b, tone_shadows(rec709(r, g, b), shadowAmount));
scale_luminance(&r, &g, &b, tone_whites(rec709(r, g, b), whiteAmount));
scale_luminance(&r, &g, &b, tone_blacks(rec709(r, g, b), blackAmount));
vibrance_and_saturation(&r, &g, &b, vibranceAmount, saturationAmount);
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;
g = gc ? 1 : 0;
b = bc ? 1 : 0;
} else if (clipping == 2) {
int rc = r <= 0.5 / 255.0, gc = g <= 0.5 / 255.0, bc = b <= 0.5 / 255.0;
if (rc || gc || bc) {
r = rc ? 0 : 1;
g = gc ? 0 : 1;
b = bc ? 0 : 1;
} else {
r = g = b = 1;
}
}
write_premultiplied(p, r, g, b, alpha);
}
}
}
static size_t clamped_index(int index, size_t limit) {
if (index < 0) return 0;
if ((size_t)index >= limit) return limit - 1;
return (size_t)index;
}
// Edge-clamped box blur. `dst` may not alias `src`. Returns 0 when the temporary row buffer cannot be allocated.
static int box_blur_plane(const float *src, float *dst, size_t width, size_t height, int radius) {
if (radius < 1) {
memcpy(dst, src, width * height * sizeof(float));
return 1;
}
float *temp = malloc(width * height * sizeof(float));
if (!temp) return 0;
int window = radius * 2 + 1;
for (size_t y = 0; y < height; ++y) {
double sum = 0;
for (int k = -radius; k <= radius; ++k) sum += src[y * width + clamped_index(k, width)];
for (size_t x = 0; x < width; ++x) {
temp[y * width + x] = (float)(sum / window);
sum += src[y * width + clamped_index((int)x + radius + 1, width)];
sum -= src[y * width + clamped_index((int)x - radius, width)];
}
}
for (size_t x = 0; x < width; ++x) {
double sum = 0;
for (int k = -radius; k <= radius; ++k) sum += temp[clamped_index(k, height) * width + x];
for (size_t y = 0; y < height; ++y) {
dst[y * width + x] = (float)(sum / window);
sum += temp[clamped_index((int)y + radius + 1, height) * width + x];
sum -= temp[clamped_index((int)y - radius, height) * width + x];
}
}
free(temp);
return 1;
}
static int effects_radius(double base, double scale) {
double radius = base * (scale > 0 ? scale : 1);
if (radius < 1) radius = 1;
if (radius > 64) radius = 64;
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);
}
static void effects_vignette(double *r, double *g, double *b, size_t x, size_t y, size_t width, size_t height,
double amount, double midpoint, double roundness, double feather, double highlights,
int style) {
if (amount == 0 || width == 0 || height == 0) return;
double nx = ((double)x + 0.5) / (double)width * 2.0 - 1.0;
double ny = ((double)y + 0.5) / (double)height * 2.0 - 1.0;
double square = fmax(fabs(nx), fabs(ny));
double circle = hypot(nx, ny) / sqrt(2.0);
double shape = (1.0 - roundness / 100.0) * 0.5;
double dist = circle + (square - circle) * shape;
double start = (midpoint / 100.0) * 0.85;
double soft = feather / 100.0;
if (soft < 0.05) soft = 0.05;
double t = (dist - start) / soft;
t = camera_clamp(t);
double mask = t * t * (3.0 - 2.0 * t);
double effect = (amount / 100.0) * mask;
// Highlight Priority eases a darkening vignette off bright pixels. The other styles do not.
if (effect < 0 && style == 0) {
double bright = camera_clamp((rec709(*r, *g, *b) - 0.45) / 0.55);
effect *= 1.0 - (highlights / 100.0) * bright;
}
if (effect < 0) {
double factor = 1.0 + effect;
*r *= factor; *g *= factor; *b *= factor;
} else if (effect > 0) {
*r = *r + (1.0 - *r) * effect;
*g = *g + (1.0 - *g) * effect;
*b = *b + (1.0 - *b) * effect;
}
if (style == 1 && mask > 0) {
double lum = rec709(*r, *g, *b);
double sat = 1.0 - 0.75 * mask * fabs(amount / 100.0);
*r = camera_clamp(lum + (*r - lum) * sat);
*g = camera_clamp(lum + (*g - lum) * sat);
*b = camera_clamp(lum + (*b - lum) * sat);
}
}
void adjust_camera_raw_clip_overlay(uint8_t *rgba, size_t width, size_t height, size_t stride, int shadows, int highlights) {
if (!shadows && !highlights) return;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
double r = fmin(1.0, p[0] / alpha);
double g = fmin(1.0, p[1] / alpha);
double b = fmin(1.0, p[2] / alpha);
if (shadows && (r <= 0.5 / 255.0 || g <= 0.5 / 255.0 || b <= 0.5 / 255.0)) {
r *= 0.35; g *= 0.35; b = b * 0.35 + 0.65;
}
if (highlights && (r >= 254.5 / 255.0 || g >= 254.5 / 255.0 || b >= 254.5 / 255.0)) {
r = r * 0.35 + 0.65; g *= 0.35; b *= 0.35;
}
write_premultiplied(p, r, g, b, alpha);
}
}
}
void adjust_camera_raw_effects(uint8_t *rgba, size_t width, size_t height, size_t stride,
double texture, double clarity, double dehaze,
double glow, int glowStyle, double glowRange, double glowSpread, double glowWarmth,
double vignetteAmount, double vignetteMidpoint, double vignetteRoundness,
double vignetteFeather, double vignetteHighlights, int vignetteStyle,
double scale) {
if (width == 0 || height == 0) return;
if (texture == 0 && clarity == 0 && dehaze == 0 && !(glow > 0) && vignetteAmount == 0) return;
size_t count = width * height;
float *luma = NULL, *fine = NULL, *coarse = NULL, *glowPlane = NULL;
int glowRadius = 1;
int failed = 0;
if (texture != 0 || clarity != 0 || glow > 0) {
luma = malloc(count * sizeof(float));
if (!luma) return;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) { luma[y * width + x] = 0; continue; }
double r = fmin(1.0, p[0] / alpha);
double g = fmin(1.0, p[1] / alpha);
double b = fmin(1.0, p[2] / alpha);
luma[y * width + x] = (float)rec709(r, g, b);
}
}
if (texture != 0) {
fine = malloc(count * sizeof(float));
if (!fine || !box_blur_plane(luma, fine, width, height, effects_radius(1, scale))) 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;
}
if (!failed && glow > 0) {
double spread = glowSpread / 100.0;
double base = glowStyle == 1 ? 2.0 : 5.0;
double widened = base * (1.0 + spread);
if (widened < 1) widened = 1;
glowRadius = effects_radius(widened, scale);
float threshold = (float)(0.55 + 0.4 * (glowRange / 100.0));
glowPlane = malloc(count * sizeof(float));
float *source = malloc(count * sizeof(float));
if (!glowPlane || !source) failed = 1;
else {
float denom = 1.0f - threshold;
if (denom < 0.05f) denom = 0.05f;
for (size_t i = 0; i < count; ++i) {
float t = (luma[i] - threshold) / denom;
if (t < 0) t = 0;
if (t > 1) t = 1;
source[i] = t;
}
failed = !box_blur_plane(source, glowPlane, width, height, glowRadius);
}
free(source);
}
}
if (failed) {
free(luma); free(fine); free(coarse); free(glowPlane);
return;
}
double warmth = glowWarmth / 100.0;
double glowRed, glowGreen, glowBlue, glowGain;
if (glowStyle == 2) {
// Halation's fringe is red. Warmth pushes it further that way, rather than toward yellow or blue.
glowRed = 1;
glowGreen = 0.35 - 0.3 * warmth;
glowBlue = 0.2 - 0.2 * warmth;
glowGain = 1;
} else {
glowRed = 0.75 + 0.25 * warmth;
glowGreen = 0.6 + 0.2 * warmth;
glowBlue = 0.75 - 0.6 * warmth;
glowGain = glowStyle == 1 ? 1.4 : 1;
}
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
size_t index = y * width + x;
double r = fmin(1.0, p[0] / alpha);
double g = fmin(1.0, p[1] / alpha);
double b = fmin(1.0, p[2] / alpha);
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 (detail != 0) scale_luminance(&r, &g, &b, camera_clamp(tone + detail));
}
if (dehaze != 0) effects_dehaze(&r, &g, &b, dehaze);
if (glowPlane && glow > 0) {
double add = glowPlane[index] * (glow / 100.0) * glowGain;
r = camera_clamp(r + add * glowRed);
g = camera_clamp(g + add * glowGreen);
b = camera_clamp(b + add * glowBlue);
}
effects_vignette(&r, &g, &b, x, y, width, height, vignetteAmount, vignetteMidpoint, vignetteRoundness,
vignetteFeather, vignetteHighlights, vignetteStyle);
write_premultiplied(p, r, g, b, alpha);
}
}
free(luma);
free(fine);
free(coarse);
free(glowPlane);
}
static double lut_at(const float *lut, double value) {
double scaled = camera_clamp(value) * 255.0;
int lo = (int)scaled;
int hi = lo < 255 ? lo + 1 : 255;
double t = scaled - lo;
return lut[lo] + (lut[hi] - lut[lo]) * t;
}
static void rgb_to_hsl(double r, double g, double b, double *h, double *s, double *l) {
double maxc = fmax(r, fmax(g, b)), minc = fmin(r, fmin(g, b));
*l = (maxc + minc) * 0.5;
double d = maxc - minc;
if (d < 1e-6) { *h = 0; *s = 0; return; }
*s = d / (1.0 - fabs(2.0 * *l - 1.0));
if (maxc == r) *h = fmod((g - b) / d, 6.0);
else if (maxc == g) *h = (b - r) / d + 2.0;
else *h = (r - g) / d + 4.0;
*h /= 6.0;
if (*h < 0) *h += 1;
}
static double hue_to_rgb(double p, double q, double t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1.0 / 6) return p + (q - p) * 6 * t;
if (t < 0.5) return q;
if (t < 2.0 / 3) return p + (q - p) * (2.0 / 3 - t) * 6;
return p;
}
static void hsl_to_rgb(double h, double s, double l, double *r, double *g, double *b) {
if (s <= 1e-6) { *r = *g = *b = l; return; }
double q = l < 0.5 ? l * (1 + s) : l + s - l * s;
double p = 2 * l - q;
*r = hue_to_rgb(p, q, h + 1.0 / 3);
*g = hue_to_rgb(p, q, h);
*b = hue_to_rgb(p, q, h - 1.0 / 3);
}
static double circular_distance(double a, double b) {
double d = fabs(a - b);
return d > 0.5 ? 1 - d : d;
}
static const double mixer_centers[8] = {0, 30.0 / 360, 60.0 / 360, 120.0 / 360, 180.0 / 360, 240.0 / 360, 270.0 / 360, 300.0 / 360};
static double point_weight(double h, double s, double l, const float *point) {
double hueHalf = point[6] > 0.01f ? point[6] : 0.01f;
double satHalf = point[7] > 0.01f ? point[7] : 0.01f;
double lumHalf = point[8] > 0.01f ? point[8] : 0.01f;
double hueW = 1 - circular_distance(h, point[0]) / hueHalf;
double satW = 1 - fabs(s - point[1]) / satHalf;
double lumW = 1 - fabs(l - point[2]) / lumHalf;
if (hueW < 0 || satW < 0 || lumW < 0) return 0;
return hueW * satW * lumW;
}
void adjust_camera_raw_curve_color(uint8_t *rgba, size_t width, size_t height, size_t stride,
const float *lumaLut, const float *redLut, const float *greenLut, const float *blueLut,
double refineSaturation, const float *mixer, int pointCount, const float *points,
const float *grade, double blending, double balance, int visualize) {
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
double r = fmin(1.0, p[0] / alpha), g = fmin(1.0, p[1] / alpha), b = fmin(1.0, p[2] / alpha);
double tone = rec709(r, g, b);
double mapped = lut_at(lumaLut, tone);
scale_luminance(&r, &g, &b, mapped);
if (refineSaturation != 0 && tone > 1e-4) {
double factor = 1 + refineSaturation * (mapped / tone - 1);
double lum = rec709(r, g, b);
r = camera_clamp(lum + (r - lum) * factor);
g = camera_clamp(lum + (g - lum) * factor);
b = camera_clamp(lum + (b - lum) * factor);
}
r = lut_at(redLut, r); g = lut_at(greenLut, g); b = lut_at(blueLut, b);
double h, s, l;
rgb_to_hsl(r, g, b, &h, &s, &l);
double sourceHue = h, sourceSat = s, sourceLum = l;
double hueDelta = 0, satDelta = 0, lumDelta = 0, weightSum = 0;
for (int i = 0; i < 8; ++i) {
double dist = circular_distance(h, mixer_centers[i]);
double w = 1 - dist / (40.0 / 360);
if (w <= 0) continue;
hueDelta += mixer[i] * w * (30.0 / 360);
satDelta += mixer[8 + i] * w;
lumDelta += mixer[16 + i] * w * 0.25;
weightSum += w;
}
if (weightSum > 1) { hueDelta /= weightSum; satDelta /= weightSum; lumDelta /= weightSum; }
h += hueDelta; if (h < 0) h += 1; if (h >= 1) h -= 1;
s = camera_clamp(s * (1 + satDelta));
l = camera_clamp(l + lumDelta);
for (int i = 0; i < pointCount; ++i) {
const float *point = points + i * 9;
double w = point_weight(h, s, l, point);
if (w <= 0) continue;
h += point[3] * w * (30.0 / 360);
s = camera_clamp(s * (1 + point[4] * w));
l = camera_clamp(l + point[5] * w * 0.25);
}
if (h < 0) h += 1; if (h >= 1) h -= 1;
hsl_to_rgb(h, s, l, &r, &g, &b);
// Balance moves the crossover between the shadow and highlight wheels. Toward highlights
// it has to move down, so more of the picture counts as highlight and the shadow wheel
// loses its hold; the other sign strengthened the shadow tint it was meant to weaken.
double split = 0.5 - balance * 0.2;
double reach = 0.12 + blending * 0.38;
double shadowW = camera_clamp((split + reach - rec709(r, g, b)) / fmax(0.05, reach * 2));
double highlightW = camera_clamp((rec709(r, g, b) - (split - reach)) / fmax(0.05, reach * 2));
double midW = camera_clamp(1 - fabs(rec709(r, g, b) - split) / (0.35 + reach));
double sum = shadowW + midW + highlightW;
if (sum > 1e-4) { shadowW /= sum; midW /= sum; highlightW /= sum; }
double weights[4] = {shadowW, midW, highlightW, 1};
for (int wheel = 0; wheel < 4; ++wheel) {
double wh = grade[wheel * 3], ws = grade[wheel * 3 + 1], wl = grade[wheel * 3 + 2];
double w = weights[wheel];
if (w <= 0 || (ws <= 0 && wl == 0)) continue;
double cr, cg, cb;
hsl_to_rgb(wh, 1, 0.5, &cr, &cg, &cb);
r = camera_clamp(r + (cr - 0.5) * ws * w * 0.85);
g = camera_clamp(g + (cg - 0.5) * ws * w * 0.85);
b = camera_clamp(b + (cb - 0.5) * ws * w * 0.85);
if (wl != 0) scale_luminance(&r, &g, &b, camera_clamp(rec709(r, g, b) + wl * 0.25 * w));
}
if (visualize >= 0 && visualize < pointCount && point_weight(sourceHue, sourceSat, sourceLum, points + visualize * 9) <= 0.05) {
r *= 0.35; g *= 0.35; b *= 0.35;
}
write_premultiplied(p, r, g, b, alpha);
}
}
}
static double detail_radius(double slider, double scale) {
double base = 0.5 + (slider / 100.0) * 2.5;
double radius = base * (scale > 0 ? scale : 1);
if (radius < 0.5) radius = 0.5;
if (radius > 64) radius = 64;
return radius;
}
static double pixel_hue_deg(double r, double g, double b) {
double maxc = fmax(r, fmax(g, b)), minc = fmin(r, fmin(g, b));
double chroma = maxc - minc;
if (chroma < 1e-6) return 0;
double hue;
if (maxc == r) hue = fmod((g - b) / chroma, 6.0);
else if (maxc == g) hue = (b - r) / chroma + 2.0;
else hue = (r - g) / chroma + 4.0;
hue = hue * 60.0;
if (hue < 0) hue += 360.0;
return hue;
}
static int hue_in_range(double hue, double low, double high) {
if (low <= high) return hue >= low && hue <= high;
return hue >= low || hue <= high;
}
static float sharpen_edge_at(const float *luma, size_t width, size_t height, size_t x, size_t y, int radius) {
if (radius < 1) radius = 1;
float center = luma[y * width + x];
float sum = 0;
int count = 0;
for (int dy = -radius; dy <= radius; dy += radius) {
for (int dx = -radius; dx <= radius; dx += radius) {
if (dx == 0 && dy == 0) continue;
long sx = (long)x + dx, sy = (long)y + dy;
if (sx < 0 || sy < 0 || (size_t)sx >= width || (size_t)sy >= height) continue;
sum += fabsf(luma[sy * width + sx] - center);
count++;
}
}
return count ? sum / count : 0;
}
void adjust_camera_raw_sharpen_mask_overlay(uint8_t *rgba, size_t width, size_t height, size_t stride,
double sharpenRadius, double sharpenDetail, double sharpenMasking, double scale) {
if (width == 0 || height == 0) return;
size_t count = width * height;
float *luma = malloc(count * sizeof(float));
if (!luma) return;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) { luma[y * width + x] = 0; continue; }
double r = fmin(1.0, p[0] / alpha), g = fmin(1.0, p[1] / alpha), b = fmin(1.0, p[2] / alpha);
luma[y * width + x] = (float)rec709(r, g, b);
}
}
int radius = effects_radius(detail_radius(sharpenRadius, scale), 1);
double threshold = (sharpenMasking / 100.0) * 0.35;
double detailBoost = 0.5 + sharpenDetail / 100.0;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
float edge = sharpen_edge_at(luma, width, height, x, y, radius);
double mask = camera_clamp((edge * detailBoost - threshold) / fmax(0.04, 0.35 - threshold * 0.5));
uint8_t gray = (uint8_t)lround(mask * alpha);
p[0] = p[1] = p[2] = gray;
}
}
free(luma);
}
void adjust_camera_raw_detail(uint8_t *rgba, size_t width, size_t height, size_t stride,
double sharpenAmount, double sharpenRadius, double sharpenDetail, double sharpenMasking,
double noiseLuminance, double noiseLuminanceDetail, double noiseLuminanceContrast,
double noiseColor, double noiseColorDetail, double noiseColorSmoothness, double scale) {
if (width == 0 || height == 0) return;
if (sharpenAmount == 0 && noiseLuminance == 0 && noiseColor == 0) return;
size_t count = width * height;
float *luma = malloc(count * sizeof(float));
float *work = malloc(count * sizeof(float));
if (!luma || !work) { free(luma); free(work); return; }
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) { luma[y * width + x] = 0; continue; }
double r = fmin(1.0, p[0] / alpha), g = fmin(1.0, p[1] / alpha), b = fmin(1.0, p[2] / alpha);
luma[y * width + x] = (float)rec709(r, g, b);
}
}
if (noiseLuminance > 0) {
int radius = effects_radius(1.0 + noiseLuminance / 50.0, scale);
if (!box_blur_plane(luma, work, width, height, radius)) { free(luma); free(work); return; }
double strength = noiseLuminance / 100.0;
double preserve = noiseLuminanceDetail / 100.0;
double contrast = noiseLuminanceContrast / 100.0;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
size_t index = y * width + x;
float edge = sharpen_edge_at(luma, width, height, x, y, 1);
double local = strength * (1.0 - preserve * fmin(1.0, edge * 6.0));
float blurred = work[index];
float target = (float)(luma[index] * (1.0 - local) + blurred * local);
if (contrast != 0) target = (float)(target + contrast * 0.25 * (luma[index] - blurred));
luma[index] = target;
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, target);
write_premultiplied(p, r, g, b, alpha);
}
}
}
if (noiseColor > 0) {
int radius = effects_radius(1.0 + noiseColorSmoothness / 40.0, scale);
float *chroma = malloc(count * sizeof(float));
float *chromaBlur = malloc(count * sizeof(float));
if (!chroma || !chromaBlur) { free(chroma); free(chromaBlur); free(luma); free(work); return; }
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
double r = fmin(1.0, p[0] / alpha), g = fmin(1.0, p[1] / alpha), b = fmin(1.0, p[2] / alpha);
double h, s, l;
rgb_to_hsl(r, g, b, &h, &s, &l);
chroma[y * width + x] = (float)s;
}
}
if (!box_blur_plane(chroma, chromaBlur, width, height, radius)) {
free(chroma); free(chromaBlur); free(luma); free(work); return;
}
double strength = noiseColor / 100.0;
double preserve = noiseColorDetail / 100.0;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
size_t index = y * width + x;
float edge = fabsf(chroma[index] - chromaBlur[index]);
double local = strength * (1.0 - preserve * fmin(1.0, edge * 4.0));
float sat = chroma[index] * (float)(1.0 - local) + chromaBlur[index] * (float)local;
double r = fmin(1.0, p[0] / alpha), g = fmin(1.0, p[1] / alpha), b = fmin(1.0, p[2] / alpha);
double h, s, l;
rgb_to_hsl(r, g, b, &h, &s, &l);
s = sat;
hsl_to_rgb(h, s, l, &r, &g, &b);
write_premultiplied(p, r, g, b, alpha);
}
}
free(chroma);
free(chromaBlur);
}
if (sharpenAmount > 0) {
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
double r = fmin(1.0, p[0] / alpha), g = fmin(1.0, p[1] / alpha), b = fmin(1.0, p[2] / alpha);
luma[y * width + x] = (float)rec709(r, g, b);
}
}
int radius = effects_radius(detail_radius(sharpenRadius, scale), 1);
if (!box_blur_plane(luma, work, width, height, radius)) { free(luma); free(work); return; }
double amount = sharpenAmount / 100.0;
double detailMix = sharpenDetail / 100.0;
double threshold = (sharpenMasking / 100.0) * 0.35;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
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));
double high = luma[index] - work[index];
double sharpened = camera_clamp(luma[index] + high * amount * mask * (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);
}
}
}
free(luma);
free(work);
}
static void optics_defringe(double *r, double *g, double *b, double purpleAmount, double purpleLow, double purpleHigh,
double greenAmount, double greenLow, double greenHigh) {
double hue = pixel_hue_deg(*r, *g, *b);
double maxc = fmax(*r, fmax(*g, *b)), minc = fmin(*r, fmin(*g, *b));
double chroma = maxc - minc;
if (chroma < 1e-6) return;
double sat = chroma / maxc;
double reduce = 0;
if (purpleAmount > 0 && hue_in_range(hue, purpleLow, purpleHigh)) reduce = fmax(reduce, purpleAmount / 100.0);
if (greenAmount > 0 && hue_in_range(hue, greenLow, greenHigh)) reduce = fmax(reduce, greenAmount / 100.0);
if (reduce <= 0) return;
double lum = rec709(*r, *g, *b);
double factor = 1.0 - reduce * sat;
*r = camera_clamp(lum + (*r - lum) * factor);
*g = camera_clamp(lum + (*g - lum) * factor);
*b = camera_clamp(lum + (*b - lum) * factor);
}
static void optics_chromatic(uint8_t *rgba, size_t width, size_t height, size_t stride, double strength) {
if (strength <= 0) return;
uint8_t *copy = malloc(height * stride);
if (!copy) return;
for (size_t y = 0; y < height; ++y) memcpy(copy + y * stride, rgba + y * stride, width * 4);
double cx = width * 0.5, cy = height * 0.5;
double maxR = hypot(cx, cy);
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
const uint8_t *srcRow = copy + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
double dx = x + 0.5 - cx, dy = y + 0.5 - cy;
double radial = hypot(dx, dy) / maxR;
double shift = strength * radial * radial * 2.5;
long rx = (long)lround(x - shift), bx = (long)lround(x + shift);
const uint8_t *pr = srcRow + clamped_index(rx, width) * 4;
const uint8_t *pb = srcRow + clamped_index(bx, width) * 4;
double g = fmin(1.0, srcRow[x * 4 + 1] / alpha);
double r = fmin(1.0, pr[0] / fmax(1.0, pr[3]));
double b = fmin(1.0, pb[2] / fmax(1.0, pb[3]));
write_premultiplied(p, r, g, b, alpha);
}
}
free(copy);
}
static void optics_vignette_correct(double *r, double *g, double *b, size_t x, size_t y, size_t width, size_t height,
double amount, double midpoint) {
if (amount == 0 || width == 0 || height == 0) return;
double nx = ((double)x + 0.5) / (double)width * 2.0 - 1.0;
double ny = ((double)y + 0.5) / (double)height * 2.0 - 1.0;
double dist = hypot(nx, ny) / sqrt(2.0);
double start = (midpoint / 100.0) * 0.85;
double t = camera_clamp((dist - start) / 0.35);
double mask = t * t * (3.0 - 2.0 * t);
double lift = (amount / 100.0) * mask;
if (lift > 0) {
*r = camera_clamp(*r + (1.0 - *r) * lift);
*g = camera_clamp(*g + (1.0 - *g) * lift);
*b = camera_clamp(*b + (1.0 - *b) * lift);
} else {
double factor = 1.0 + lift;
*r *= factor; *g *= factor; *b *= factor;
}
}
void adjust_camera_raw_optics(uint8_t *rgba, size_t width, size_t height, size_t stride,
int removeChromatic, int lensProfile, double profileDistortion, double profileVignetting,
double distortionK, double purpleAmount, double purpleHueLow, double purpleHueHigh,
double greenAmount, double greenHueLow, double greenHueHigh,
double vignetteAmount, double vignetteMidpoint, double scale) {
if (width == 0 || height == 0) return;
double profileVignette = lensProfile ? profileVignetting / 100.0 : 0;
double vignette = vignetteAmount + profileVignette * 35.0;
if (distortionK != 0) {
size_t bytes = height * stride;
uint8_t *copy = malloc(bytes);
if (!copy) return;
memcpy(copy, rgba, bytes);
lens_distort(copy, rgba, width, height, stride, distortionK);
free(copy);
}
if (removeChromatic) optics_chromatic(rgba, width, height, stride, 0.45);
if (purpleAmount == 0 && greenAmount == 0 && vignette == 0) return;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
double r = fmin(1.0, p[0] / alpha), g = fmin(1.0, p[1] / alpha), b = fmin(1.0, p[2] / alpha);
optics_defringe(&r, &g, &b, purpleAmount, purpleHueLow, purpleHueHigh, greenAmount, greenHueLow, greenHueHigh);
optics_vignette_correct(&r, &g, &b, x, y, width, height, vignette, vignetteMidpoint);
write_premultiplied(p, r, g, b, alpha);
}
}
}
void adjust_camera_raw_calibration(uint8_t *rgba, size_t width, size_t height, size_t stride,
double shadowTint, double redHue, double redSaturation,
double greenHue, double greenSaturation, double blueHue, double blueSaturation,
int processVersion) {
if (width == 0 || height == 0) return;
double versionScale = processVersion <= 1 ? 0.55 : processVersion == 2 ? 0.65 : processVersion == 3 ? 0.75
: processVersion == 4 ? 0.85 : processVersion == 5 ? 0.92 : 1.0;
double tint = shadowTint / 100.0 * versionScale;
double rh = redHue / 100.0 * (15.0 / 360.0) * versionScale;
double rs = redSaturation / 100.0 * 0.45 * versionScale;
double gh = greenHue / 100.0 * (15.0 / 360.0) * versionScale;
double gs = greenSaturation / 100.0 * 0.45 * versionScale;
double bh = blueHue / 100.0 * (15.0 / 360.0) * versionScale;
double bs = blueSaturation / 100.0 * 0.45 * versionScale;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
double alpha = p[3];
if (!alpha) continue;
double r = fmin(1.0, p[0] / alpha), g = fmin(1.0, p[1] / alpha), b = fmin(1.0, p[2] / alpha);
double h, s, l;
rgb_to_hsl(r, g, b, &h, &s, &l);
if (l < 0.35 && tint != 0) {
h += tint * 0.06;
if (h < 0) h += 1;
if (h >= 1) h -= 1;
}
double maxc = fmax(r, fmax(g, b)), minc = fmin(r, fmin(g, b));
if (maxc - minc > 1e-5) {
if (r >= g && r >= b) { h += rh; s = camera_clamp(s * (1 + rs)); }
else if (g >= r && g >= b) { h += gh; s = camera_clamp(s * (1 + gs)); }
else { h += bh; s = camera_clamp(s * (1 + bs)); }
if (h < 0) h += 1;
if (h >= 1) h -= 1;
}
hsl_to_rgb(h, s, l, &r, &g, &b);
write_premultiplied(p, r, g, b, alpha);
}
}
}
+52 -1
View File
@@ -8,7 +8,7 @@
void adjust_gradient_map(uint8_t *rgba, size_t width, size_t height, size_t stride, const uint8_t *table);
// Film grain on premultiplied RGBA pixels: the same brightness change on all three channels, strongest
// in the midtones. `amount` is 0–100, `size` the grain's scale in document units, and `roughness`
// (0–100) mixes in per-unit noise. Pixel (x, y) sits at (originX + (x + 0.5) × unitsPerPixel,
// (0–100) adds smaller irregular particles whose scale remains relative to `size`. Pixel (x, y) sits at (originX + (x + 0.5) × unitsPerPixel,
// originY + (y + 0.5) × unitsPerPixel), and its grain depends only on that position and `seed`, so a
// piece of an image gets the same grain as that part of the whole.
void adjust_grain(uint8_t *rgba, size_t width, size_t height, size_t stride, double amount, double size,
@@ -30,4 +30,55 @@ void adjust_color_balance(uint8_t *rgba, size_t width, size_t height, size_t str
// After resampling with a filter that rings (Lanczos), premultiplied RGBA colors can exceed their alpha;
// this clamps each channel back to its pixel's alpha. `count` is the number of pixels.
void rgba_clamp_premultiplied(uint8_t *rgba, size_t count);
// Camera Raw's Light and Color groups on premultiplied RGBA pixels, in this order: white balance
// (the three channel gains), exposure in stops of linear light, contrast about mid gray, highlights,
// shadows, whites, blacks, vibrance, then saturation. Temperature and tint are relative, so the gains
// are computed by the caller. Amounts are Camera Raw's own ranges (exposure −5…5, the rest −100…100).
// `clipping` 0 renders the grade; 1 replaces it with a highlight-clip view (clipped channels lit on
// black); 2 replaces it with a shadow-clip view (clipped channels dark on white). Alpha is kept.
void adjust_camera_raw(uint8_t *rgba, size_t width, size_t height, size_t stride,
double redGain, double greenGain, double blueGain, double exposure, double contrast,
double highlights, double shadows, double whites, double blacks,
double vibrance, double saturation, int clipping);
// Camera Raw Effects after Light and Color. Texture is a fine local contrast, Clarity a broader one.
// Dehaze raises contrast and saturation when positive and lifts the shadows when negative. Glow, its
// range, spread and warmth do nothing until `glow` is above zero: styles are 0 diffusion, 1 bloom,
// 2 halation. Vignette styles are 0 highlight priority, 1 color priority, 2 paint overlay; Highlights
// protects bright pixels only while the amount darkens. `scale` is preview pixels per layer pixel, so
// the radii match a full-size render. Grain is applied separately. Alpha is kept.
// Blue over clipped shadows and red over clipped highlights, on top of the grade. Preview only.
void adjust_camera_raw_clip_overlay(uint8_t *rgba, size_t width, size_t height, size_t stride, int shadows, int highlights);
// Curve, Color Mixer, and Color Grading after the basic grade. `lumaLut` and the channel LUTs are 256
// entries. `mixer` is 24 floats: hue, saturation, luminance for eight families, −1…1. Each point color is
// 9 floats (hue, saturation, luminance, three shifts −1…1, three range half-widths). `grade` is four wheels
// of hue turns, saturation 0…1, and luminance −1…1. `visualize` darkens pixels outside that point color.
void adjust_camera_raw_curve_color(uint8_t *rgba, size_t width, size_t height, size_t stride,
const float *lumaLut, const float *redLut, const float *greenLut, const float *blueLut,
double refineSaturation, const float *mixer, int pointCount, const float *points,
const float *grade, double blending, double balance, int visualize);
void adjust_camera_raw_effects(uint8_t *rgba, size_t width, size_t height, size_t stride,
double texture, double clarity, double dehaze,
double glow, int glowStyle, double glowRange, double glowSpread, double glowWarmth,
double vignetteAmount, double vignetteMidpoint, double vignetteRoundness,
double vignetteFeather, double vignetteHighlights, int vignetteStyle,
double scale);
// Manual noise reduction, then sharpening. `scale` maps radius to preview pixels. Applied after the creative grade.
void adjust_camera_raw_detail(uint8_t *rgba, size_t width, size_t height, size_t stride,
double sharpenAmount, double sharpenRadius, double sharpenDetail, double sharpenMasking,
double noiseLuminance, double noiseLuminanceDetail, double noiseLuminanceContrast,
double noiseColor, double noiseColorDetail, double noiseColorSmoothness, double scale);
// Preview only: white where sharpening would land, black where masking protects. Uses the current sharpen sliders.
void adjust_camera_raw_sharpen_mask_overlay(uint8_t *rgba, size_t width, size_t height, size_t stride,
double sharpenRadius, double sharpenDetail, double sharpenMasking, double scale);
// Chromatic aberration, lens distortion, defringe, and lens-vignetting correction. `distortionK` matches `lens_distort`.
void adjust_camera_raw_optics(uint8_t *rgba, size_t width, size_t height, size_t stride,
int removeChromatic, int lensProfile, double profileDistortion, double profileVignetting,
double distortionK, double purpleAmount, double purpleHueLow, double purpleHueHigh,
double greenAmount, double greenHueLow, double greenHueHigh,
double vignetteAmount, double vignetteMidpoint, double scale);
// Camera calibration before the main grade. Primary hue and saturation shifts are −100…100; shadow tint is green/magenta.
void adjust_camera_raw_calibration(uint8_t *rgba, size_t width, size_t height, size_t stride,
double shadowTint, double redHue, double redSaturation,
double greenHue, double greenSaturation, double blueHue, double blueSaturation,
int processVersion);
#endif