|
| 1 | +/* |
| 2 | + This file is part of darktable, |
| 3 | + Copyright (C) 2025 darktable developers. |
| 4 | +
|
| 5 | + darktable is free software: you can redistribute it and/or modify |
| 6 | + it under the terms of the GNU General Public License as published by |
| 7 | + the Free Software Foundation, either version 3 of the License, or |
| 8 | + (at your option) any later version. |
| 9 | +
|
| 10 | + darktable is distributed in the hope that it will be useful, |
| 11 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | + GNU General Public License for more details. |
| 14 | +
|
| 15 | + You should have received a copy of the GNU General Public License |
| 16 | + along with darktable. If not, see <http://www.gnu.org/licenses/>. |
| 17 | +*/ |
| 18 | + |
| 19 | +#include "common.h" |
| 20 | +#include "colorspace.h" |
| 21 | + |
| 22 | +#define _epsilon 1E-6f |
| 23 | + |
| 24 | +// Must match tone_mapping_params_t in C code |
| 25 | +typedef struct dt_iop_agx_tone_mapping_params_t |
| 26 | +{ |
| 27 | + float min_ev; |
| 28 | + float max_ev; |
| 29 | + float range_in_ev; |
| 30 | + float curve_gamma; |
| 31 | + float pivot_x; |
| 32 | + float pivot_y; |
| 33 | + float target_black; |
| 34 | + float toe_power; |
| 35 | + float toe_transition_x; |
| 36 | + float toe_transition_y; |
| 37 | + float toe_scale; |
| 38 | + int need_convex_toe; |
| 39 | + float toe_fallback_coefficient; |
| 40 | + float toe_fallback_power; |
| 41 | + float slope; |
| 42 | + float intercept; |
| 43 | + float target_white; |
| 44 | + float shoulder_power; |
| 45 | + float shoulder_transition_x; |
| 46 | + float shoulder_transition_y; |
| 47 | + float shoulder_scale; |
| 48 | + int need_concave_shoulder; |
| 49 | + float shoulder_fallback_coefficient; |
| 50 | + float shoulder_fallback_power; |
| 51 | + float look_offset; |
| 52 | + float look_slope; |
| 53 | + float look_power; |
| 54 | + float look_saturation; |
| 55 | + float look_original_hue_mix_ratio; |
| 56 | + int look_tuned; |
| 57 | + int restore_hue; |
| 58 | +} dt_iop_agx_tone_mapping_params_t; |
| 59 | + |
| 60 | +static inline void _agx_compress_into_gamut(float4 *pixel) |
| 61 | +{ |
| 62 | + const float luminance_coeffs[] = { 0.2658180370250449f, 0.59846986045365f, 0.1357121025213052f }; |
| 63 | + const float input_y = pixel->x * luminance_coeffs[0] + pixel->y * luminance_coeffs[1] + pixel->z * luminance_coeffs[2]; |
| 64 | + const float max_rgb = fmax(pixel->x, fmax(pixel->y, pixel->z)); |
| 65 | + |
| 66 | + float4 opponent_rgb = max_rgb - (*pixel); |
| 67 | + const float opponent_y = opponent_rgb.x * luminance_coeffs[0] + opponent_rgb.y * luminance_coeffs[1] + opponent_rgb.z * luminance_coeffs[2]; |
| 68 | + const float max_opponent = fmax(opponent_rgb.x, fmax(opponent_rgb.y, opponent_rgb.z)); |
| 69 | + const float y_compensate_negative = max_opponent - opponent_y + input_y; |
| 70 | + |
| 71 | + const float min_rgb = fmin(pixel->x, fmin(pixel->y, pixel->z)); |
| 72 | + const float offset = fmax(-min_rgb, 0.0f); |
| 73 | + float4 rgb_offset = (*pixel) + (float4)(offset, offset, offset, 0.0f); |
| 74 | + |
| 75 | + const float max_of_rgb_offset = fmax(rgb_offset.x, fmax(rgb_offset.y, rgb_offset.z)); |
| 76 | + float4 opponent_rgb_offset = max_of_rgb_offset - rgb_offset; |
| 77 | + |
| 78 | + const float max_inverse_rgb_offset = fmax(opponent_rgb_offset.x, fmax(opponent_rgb_offset.y, opponent_rgb_offset.z)); |
| 79 | + const float y_inverse_rgb_offset = opponent_rgb_offset.x * luminance_coeffs[0] + opponent_rgb_offset.y * luminance_coeffs[1] + opponent_rgb_offset.z * luminance_coeffs[2]; |
| 80 | + float y_new = rgb_offset.x * luminance_coeffs[0] + rgb_offset.y * luminance_coeffs[1] + rgb_offset.z * luminance_coeffs[2]; |
| 81 | + y_new = max_inverse_rgb_offset - y_inverse_rgb_offset + y_new; |
| 82 | + |
| 83 | + const float luminance_ratio = |
| 84 | + (y_new > y_compensate_negative && y_new > _epsilon) |
| 85 | + ? y_compensate_negative / y_new |
| 86 | + : 1.f; |
| 87 | + *pixel = luminance_ratio * rgb_offset; |
| 88 | +} |
| 89 | + |
| 90 | +static inline float _agx_apply_log_encoding(const float x, const float range_in_ev, const float min_ev) |
| 91 | +{ |
| 92 | + const float x_relative = fmax(_epsilon, x / 0.18f); |
| 93 | + const float mapped = (log2(fmax(x_relative, 0.0f)) - min_ev) / range_in_ev; |
| 94 | + return clipf(mapped); |
| 95 | +} |
| 96 | + |
| 97 | +static inline float _agx_sigmoid(const float x, const float power) |
| 98 | +{ |
| 99 | + return x / dtcl_pow(1.0f + dtcl_pow(x, power), 1.0f / power); |
| 100 | +} |
| 101 | + |
| 102 | +static inline float _agx_scaled_sigmoid(const float x, const float scale, const float slope, const float power, const float transition_x, const float transition_y) |
| 103 | +{ |
| 104 | + return scale * _agx_sigmoid(slope * (x - transition_x) / scale, power) + transition_y; |
| 105 | +} |
| 106 | + |
| 107 | +static inline float _agx_fallback_toe(const float x, const dt_iop_agx_tone_mapping_params_t *params) |
| 108 | +{ |
| 109 | + return x < 0.0f ? params->target_black : params->target_black + fmax(0.0f, params->toe_fallback_coefficient * dtcl_pow(x, params->toe_fallback_power)); |
| 110 | +} |
| 111 | + |
| 112 | +static inline float _agx_fallback_shoulder(const float x, const dt_iop_agx_tone_mapping_params_t *params) |
| 113 | +{ |
| 114 | + return x >= 1.0f ? params->target_white : params->target_white - fmax(0.0f, params->shoulder_fallback_coefficient * dtcl_pow(1.0f - x, params->shoulder_fallback_power)); |
| 115 | +} |
| 116 | + |
| 117 | +static inline float _agx_apply_curve(const float x, const dt_iop_agx_tone_mapping_params_t *params) |
| 118 | +{ |
| 119 | + float result = 0.0f; |
| 120 | + if(x < params->toe_transition_x) |
| 121 | + { |
| 122 | + result = params->need_convex_toe ? _agx_fallback_toe(x, params) : _agx_scaled_sigmoid(x, params->toe_scale, params->slope, params->toe_power, params->toe_transition_x, params->toe_transition_y); |
| 123 | + } |
| 124 | + else if(x <= params->shoulder_transition_x) |
| 125 | + { |
| 126 | + result = params->slope * x + params->intercept; |
| 127 | + } |
| 128 | + else |
| 129 | + { |
| 130 | + result = params->need_concave_shoulder ? _agx_fallback_shoulder(x, params) : _agx_scaled_sigmoid(x, params->shoulder_scale, params->slope, params->shoulder_power, params->shoulder_transition_x, params->shoulder_transition_y); |
| 131 | + } |
| 132 | + return clamp(result, params->target_black, params->target_white); |
| 133 | +} |
| 134 | + |
| 135 | +static inline float _agx_apply_slope_offset(const float x, const float slope, const float offset) |
| 136 | +{ |
| 137 | + const float m = slope / (1.0f + offset); |
| 138 | + const float b = offset * m; |
| 139 | + return m * x + b; |
| 140 | +} |
| 141 | + |
| 142 | +static inline float _agx_luminance_from_matrix(const float4 pixel, constant float *rendering_to_xyz) |
| 143 | +{ |
| 144 | + float4 xyz = matrix_product_float4(pixel, rendering_to_xyz); |
| 145 | + return xyz.y; |
| 146 | +} |
| 147 | + |
| 148 | +static inline void _agx_look(float4 *pixel, const dt_iop_agx_tone_mapping_params_t *params, constant float *rendering_to_xyz) |
| 149 | +{ |
| 150 | + const float slope = params->look_slope; |
| 151 | + const float offset = params->look_offset; |
| 152 | + const float power = params->look_power; |
| 153 | + const float sat = params->look_saturation; |
| 154 | + |
| 155 | + float4 temp; |
| 156 | + temp.x = _agx_apply_slope_offset(pixel->x, slope, offset); |
| 157 | + temp.y = _agx_apply_slope_offset(pixel->y, slope, offset); |
| 158 | + temp.z = _agx_apply_slope_offset(pixel->z, slope, offset); |
| 159 | + |
| 160 | + pixel->x = temp.x > 0.0f ? dtcl_pow(temp.x, power) : temp.x; |
| 161 | + pixel->y = temp.y > 0.0f ? dtcl_pow(temp.y, power) : temp.y; |
| 162 | + pixel->z = temp.z > 0.0f ? dtcl_pow(temp.z, power) : temp.z; |
| 163 | + |
| 164 | + const float luma = _agx_luminance_from_matrix(*pixel, rendering_to_xyz); |
| 165 | + |
| 166 | + pixel->x = luma + sat * (pixel->x - luma); |
| 167 | + pixel->y = luma + sat * (pixel->y - luma); |
| 168 | + pixel->z = luma + sat * (pixel->z - luma); |
| 169 | +} |
| 170 | + |
| 171 | +static inline float _agx_lerp_hue(const float original_hue, const float processed_hue, const float mix) |
| 172 | +{ |
| 173 | + const float shortest_distance = processed_hue - original_hue - rint(processed_hue - original_hue); |
| 174 | + const float mixed_hue = (1.0f - mix) * shortest_distance + original_hue; |
| 175 | + return mixed_hue - floor(mixed_hue); |
| 176 | +} |
| 177 | + |
| 178 | +static inline void _agx_tone_mapping(float4 *rgb_in_out, const dt_iop_agx_tone_mapping_params_t *params, constant float *rendering_to_xyz) |
| 179 | +{ |
| 180 | + float4 hsv_pixel = (float4)(0.0f, 0.0f, 0.0f, 0.0f); |
| 181 | + if(params->restore_hue) |
| 182 | + { |
| 183 | + hsv_pixel = RGB_2_HSV(*rgb_in_out); |
| 184 | + } |
| 185 | + const float h_before = hsv_pixel.x; |
| 186 | + |
| 187 | + float4 transformed_pixel; |
| 188 | + transformed_pixel.x = _agx_apply_curve(_agx_apply_log_encoding(rgb_in_out->x, params->range_in_ev, params->min_ev), params); |
| 189 | + transformed_pixel.y = _agx_apply_curve(_agx_apply_log_encoding(rgb_in_out->y, params->range_in_ev, params->min_ev), params); |
| 190 | + transformed_pixel.z = _agx_apply_curve(_agx_apply_log_encoding(rgb_in_out->z, params->range_in_ev, params->min_ev), params); |
| 191 | + transformed_pixel.w = rgb_in_out->w; |
| 192 | + |
| 193 | + if(params->look_tuned) |
| 194 | + { |
| 195 | + _agx_look(&transformed_pixel, params, rendering_to_xyz); |
| 196 | + } |
| 197 | + |
| 198 | + transformed_pixel.x = dtcl_pow(fmax(0.0f, transformed_pixel.x), params->curve_gamma); |
| 199 | + transformed_pixel.y = dtcl_pow(fmax(0.0f, transformed_pixel.y), params->curve_gamma); |
| 200 | + transformed_pixel.z = dtcl_pow(fmax(0.0f, transformed_pixel.z), params->curve_gamma); |
| 201 | + |
| 202 | + if(params->restore_hue) |
| 203 | + { |
| 204 | + hsv_pixel = RGB_2_HSV(transformed_pixel); |
| 205 | + float h_after = hsv_pixel.x; |
| 206 | + h_after = _agx_lerp_hue(h_before, h_after, params->look_original_hue_mix_ratio); |
| 207 | + hsv_pixel.x = h_after; |
| 208 | + *rgb_in_out = HSV_2_RGB(hsv_pixel); |
| 209 | + } |
| 210 | + else |
| 211 | + { |
| 212 | + *rgb_in_out = transformed_pixel; |
| 213 | + } |
| 214 | +} |
| 215 | + |
| 216 | +__kernel void kernel_agx( |
| 217 | + read_only image2d_t input, |
| 218 | + write_only image2d_t output, |
| 219 | + const int width, |
| 220 | + const int height, |
| 221 | + const dt_iop_agx_tone_mapping_params_t params, |
| 222 | + constant float *pipe_to_base, |
| 223 | + constant float *base_to_rendering, |
| 224 | + constant float *rendering_to_pipe, |
| 225 | + constant float *rendering_to_xyz, |
| 226 | + const int base_working_same_profile |
| 227 | +) |
| 228 | +{ |
| 229 | + const int i = get_global_id(0); |
| 230 | + const int j = get_global_id(1); |
| 231 | + if(i >= width || j >= height) return; |
| 232 | + |
| 233 | + const int2 pos = (int2)(i, j); |
| 234 | + float4 in_pixel = read_imagef(input, sampleri, pos); |
| 235 | + |
| 236 | + // sanitize input range and get rid of NaNs |
| 237 | + in_pixel = select(clamp(in_pixel, -1e6f, 1e6f), (float4)(0.0f), isnan(in_pixel)); |
| 238 | + |
| 239 | + float4 base_rgb; |
| 240 | + if(base_working_same_profile) |
| 241 | + { |
| 242 | + base_rgb = in_pixel; |
| 243 | + } |
| 244 | + else |
| 245 | + { |
| 246 | + base_rgb = matrix_product_float4(in_pixel, pipe_to_base); |
| 247 | + } |
| 248 | + |
| 249 | + _agx_compress_into_gamut(&base_rgb); |
| 250 | + |
| 251 | + float4 rendering_rgb = matrix_product_float4(base_rgb, base_to_rendering); |
| 252 | + |
| 253 | + _agx_tone_mapping(&rendering_rgb, ¶ms, rendering_to_xyz); |
| 254 | + |
| 255 | + float4 out_pixel = matrix_product_float4(rendering_rgb, rendering_to_pipe); |
| 256 | + |
| 257 | + out_pixel.w = in_pixel.w; |
| 258 | + write_imagef(output, pos, out_pixel); |
| 259 | +} |
0 commit comments