Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
heal.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2017 Edgardo Hoszowski.
4 Copyright (C) 2019 Andreas Schneider.
5 Copyright (C) 2020 Hubert Kowalski.
6 Copyright (C) 2020-2022 Pascal Obry.
7 Copyright (C) 2021 parafin.
8 Copyright (C) 2021-2022 Ralf Brown.
9 Copyright (C) 2022 Hanno Schwalm.
10 Copyright (C) 2022 Martin Bařinka.
11 Copyright (C) 2024 Alynx Zhou.
12 Copyright (C) 2026 Aurélien PIERRE.
13
14 darktable is free software: you can redistribute it and/or modify
15 it under the terms of the GNU General Public License as published by
16 the Free Software Foundation, either version 3 of the License, or
17 (at your option) any later version.
18
19 darktable is distributed in the hope that it will be useful,
20 but WITHOUT ANY WARRANTY; without even the implied warranty of
21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 GNU General Public License for more details.
23
24 You should have received a copy of the GNU General Public License
25 along with darktable. If not, see <http://www.gnu.org/licenses/>.
26*/
27
28#include "system/macros.h"
29#include "system/openmp.h"
30#include "system/mem_alloc.h"
31#include "system/simd.h"
33#include "develop/imageop.h"
34#include "math/openmp_maths.h"
35#include "heal.h"
36
37/* Based on the original source code of GIMP's Healing Tool, by Jean-Yves Couleaud
38 *
39 * http://www.gimp.org/
40 *
41 * */
42
43/* NOTES
44 *
45 * The method used here is similar to the lighting invariant correction
46 * method but slightly different: we do not divide the RGB components,
47 * but subtract them I2 = I0 - I1, where I0 is the sample image to be
48 * corrected, I1 is the reference pattern. Then we solve DeltaI=0
49 * (Laplace) with I2 Dirichlet conditions at the borders of the
50 * mask. The solver is a red/black checker Gauss-Seidel with over-relaxation.
51 * It could benefit from a multi-grid evaluation of an initial solution
52 * before the main iteration loop.
53 *
54 * I reduced the convergence criteria to 0.1% (0.001) as we are
55 * dealing here with RGB integer components, more is overkill.
56 *
57 * Jean-Yves Couleaud cjyves@free.fr
58 */
59
60
61// Subtract bottom from top and store in result as a float; separate 'red' and 'black' pixels into
62// two contiguous regions
63static void _heal_sub(const float *const top_buffer, const float *const bottom_buffer,
64 float *const restrict red_buffer, float *const restrict black_buffer,
65 const size_t width, const size_t height)
66{
67 // how many red or black pixels per line? For consistency, we need the larger of the two, so round up
68 const size_t res_stride = 4 * ((width + 1) / 2);
70 for(size_t row = 0; row < height; row++)
71 {
72 const int parity = row & 1;
73 const size_t row_start = (row+1) * res_stride;
74 float *const buf1 = parity ? red_buffer + row_start : black_buffer + row_start;
75 float *const buf2 = parity ? black_buffer + row_start : red_buffer + row_start;
76 // handle the pixels of the row pairwise, one red and one black at a time
77 for(size_t col = 0; col < width/2; col++)
78 {
79 const size_t idx = 4 * (row * width + 2*col);
81 {
82 buf1[4*col + c] = top_buffer[idx + c] - bottom_buffer[idx + c];
83 buf2[4*col + c] = top_buffer[idx+4 + c] - bottom_buffer[idx+4 + c];
84 }
85 }
86 if(width & 1)
87 {
88 // Handle the left-over pixel when the total width is odd. Its color will always be the same as the
89 // left-most pixel, so it goes into buf1
90 const size_t res_idx = (width-1)/2;
91 const size_t idx = 4 * (row * width + (width-1));
93 {
94 buf1[4*res_idx + c] = top_buffer[idx + c] - bottom_buffer[idx + c];
95 buf2[4*res_idx + c] = 0.0f;
96 }
97 }
98 }
99 // clear the top and bottom rows, used for padding
100 memset(red_buffer, 0, res_stride * sizeof(float));
101 memset(red_buffer + (height+1)*res_stride, 0, res_stride * sizeof(float));
102 memset(black_buffer, 0, res_stride * sizeof(float));
103 memset(black_buffer + (height+1)*res_stride, 0, res_stride * sizeof(float));
104}
105
106// Add first to second and store in result, re-interleaving the 'red' and 'black' pixels
107static void _heal_add(const float *const restrict red_buffer, const float *const black_buffer,
108 const float *const restrict second_buffer, float *const restrict result_buffer,
109 const size_t width, const size_t height)
110{
111 // how many red or black pixels per line? For consistency, we need the larger of the two, so round up, then
112 // add one to ensure a padding pixel on the right
113 const size_t res_stride = 4 * ((width + 1) / 2);
115 for(size_t row = 0; row < height; row++)
116 {
117 const int parity = row & 1;
118 const size_t row_start = (row+1) * res_stride;
119 const float *const restrict buf1 = parity ? red_buffer + row_start : black_buffer + row_start;
120 const float *const restrict buf2 = parity ? black_buffer + row_start : red_buffer + row_start;
121 // handle the pixels of the row pairwise, one red and one black at a time
122 for(size_t col = 0; col < width/2; col++)
123 {
124 const size_t idx = 4 * (row * width + 2*col);
126 {
127 result_buffer[idx + c] = buf1[4*col + c] + second_buffer[idx + c];
128 result_buffer[idx + 4 + c] = buf2[4*col + c] + second_buffer[idx + 4 + c];
129 }
130 }
131 if(width & 1)
132 {
133 // handle the left-over pixel when the total width is odd
134 const size_t res_idx = (width-1)/2;
135 const size_t idx = 4 * (row * width + (width-1));
137 result_buffer[idx + c] = buf1[4*res_idx + c] + second_buffer[idx + c];
138 }
139 }
140}
141
142// define a custom reduction operation to handle a 3-vector of floats
143// we can't return an array from a function, so wrap the array type in a struct
145#ifdef _OPENMP
146static inline _aligned_pixel _add_float4(_aligned_pixel acc, _aligned_pixel newval)
147{
148 for_each_channel(c) acc.v[c] += newval.v[c];
149 return acc;
150}
151#pragma omp declare reduction(vsum:_aligned_pixel:omp_out=_add_float4(omp_out,omp_in)) \
152 initializer(omp_priv = { { 0.0f, 0.0f, 0.0f, 0.0f } })
153#endif
154
155// Perform one iteration of Gauss-Seidel, and return the sum squared residual.
156static float _heal_laplace_iteration(float *const restrict active_pixels,
157 const float *const restrict neighbor_pixels,
158 const size_t height, const size_t width, const unsigned *const restrict runs,
159 const size_t num_runs, const size_t start_parity, const float w)
160{
161 _aligned_pixel err = { { 0.f } };
162
163 // on each iteration, we adjust each cell of the current color by a weighted fraction of the difference between
164 // it and the sum of the adjacenct cells of the opposite color. Because we've split the cells by color, this
165 // leads to a somewhat different computation of neighbors.
166 // Rearranged, as provided to this function:
167 // r00 r01 r02 ... b00 b01 b02 ...
168 // r10 r11 r12 b10 b11 b12
169 // r20 r21 r22 b20 b21 b22
170 // ... ...
171 // Original layout, from which we need to get the neighbors:
172 // r00 b00 r01 b01 r02 b02 ...
173 // b10 r10 b11 r11 b12 r12
174 // r20 b20 r21 b21 r22 b22
175 // b30 r30 b31 r31 b32 r32
176 // ...
177 // As can be seen, the 'above' and 'below' neighbors of r(i)(j) are always b(i-1)(j) and b(i+1)(j). The
178 // left and right neighbors depend on which color the row starts with: if red, they are b(i)(j-1) and b(i)(j);
179 // if black, they are b(i)(j) and b(i)(j+1). All of the above holds when colors are swapped.
180#if !(defined(__apple_build_version__) && __apple_build_version__ < 11030000) //makes Xcode 11.3.1 compiler crash
181__OMP_PARALLEL_FOR__(reduction(vsum : err)) /* _OPENMP */
182#endif
183 for(size_t i = 0; i < num_runs; i++)
184 {
185 const size_t idx = runs[2*i];
186 const unsigned count = runs[2*i+1];
187 const size_t index = (size_t)4 * idx;
188 const size_t row = idx / width;
189 float a = 4.0f; // four neighboring pixels except at the edges
190 if(row == 1) a -= 1.0f; // we added a padding row at top and bottom
191 if(row == height) a -= 1.0f;
192 const size_t vert_offset = 4 * width;
193 const size_t lroffset = 4 * (start_parity ^ (row & 1)); // how many floats to offset the left/right neighbors
194 if(count == 1)
195 {
196 const size_t col = idx % width;
197 float aa = a;
198 dt_aligned_pixel_t left = { 0.0f };
199 dt_aligned_pixel_t right = { 0.0f };
200 if(col > 0 || lroffset) // first pixel in original stamp?
201 for_each_channel(c) left[c] = neighbor_pixels[index - 4 + lroffset + c];
202 else
203 aa -= 1.0f;
204 if(col + 1 < width || lroffset == 0) // last pixel in original stamp?
205 for_each_channel(c) right[c] = neighbor_pixels[index + lroffset + c];
206 else
207 aa -= 1.0f;
208
210 for_each_channel(c, aligned(active_pixels, neighbor_pixels))
211 {
212 diff[c] = w * ((aa * active_pixels[index+c])
213 - (neighbor_pixels[index - vert_offset + c] + neighbor_pixels[index + vert_offset + c]
214 + left[c] + right[c]));
215 active_pixels[index + c] -= diff[c];
216 err.v[c] += (diff[c] * diff[c]);
217 }
218 continue;
219 }
221 copy_pixel(left, neighbor_pixels + index - 4 + lroffset);
222 for(size_t j = 0; j < count; j++)
223 {
224 const size_t pixidx = index + 4*j;
226 dt_aligned_pixel_t right;
227 for_each_channel(c, aligned(active_pixels,neighbor_pixels))
228 {
229 right[c] = neighbor_pixels[pixidx + lroffset + c];
230 diff[c] = w * (a * active_pixels[pixidx+c]
231 - (neighbor_pixels[pixidx - vert_offset + c] + neighbor_pixels[pixidx + vert_offset + c]
232 + left[c] + right[c]));
233 active_pixels[pixidx + c] -= diff[c];
234 err.v[c] += (diff[c] * diff[c]);
235 left[c] = right[c];
236 }
237 }
238 }
239 return err.v[0] + err.v[1] + err.v[2];
240}
241
242// convert alternating pixels of one row of the opacity mask into a set of runs of opaque pixels of the
243// form (start_index, count), and return the updated number of runs
244static size_t _collect_color_runs(const float *const restrict mask, const size_t start_index,
245 size_t start, const size_t width,
246 unsigned *const restrict runs, size_t count, size_t *nmask)
247{
248 size_t masked = 0;
249 // handle the first and last pixels of the row specially so that we don't need checks on every pixel in the
250 // adaptation loop (which will be run hundreds of times for any stamp large enough that execution time is
251 // non-negligible)
252 if(start == 0 && mask[start])
253 {
254 runs[2*count] = start_index;
255 runs[2*count+1] = 1;
256 count++;
257 masked++;
258 start += 2; // we've processed the first pixel
259 }
260 gboolean in_run = FALSE;
261 unsigned run_start = 0;
262 size_t col;
263 for(col = start; col < width; col += 2)
264 {
265 if(mask[col])
266 {
267 masked++;
268 if(!in_run)
269 {
270 run_start = col;
271 in_run = TRUE;
272 }
273 }
274 else if(in_run)
275 {
276 runs[2*count] = start_index + run_start / 2;
277 runs[2*count + 1] = (col - run_start) / 2;
278 count++;
279 in_run = FALSE;
280 }
281 }
282 if(in_run) // finish off a run that doesn't have a zero pixel to its right
283 {
284 runs[2*count] = start_index + run_start / 2;
285 const unsigned runlen = (col - run_start) / 2;
286 runs[2*count + 1] = runlen;
287 if(runlen > 1 && col > width)
288 {
289 // split off the final pixel into its own run
290 runs[2*count + 1]--;
291 runs[2*count + 2] = runs[2*count] + runs[2*count+1];
292 runs[2*count + 3] = 1;
293 count++;
294 }
295 count++;
296 }
297 *nmask += masked;
298 return count;
299}
300
301// convert one row of the opacity mask into a set of runs of opaque pixels of the form (start_index, count)
302static void collect_runs(const int start, const float *const restrict mask, const size_t width, const size_t height,
303 const size_t subwidth, unsigned *const restrict runs, size_t *count, size_t *nmask)
304{
305 for(size_t row = 0; row < height; row++)
306 {
307 const int parity = start ^ (row & 1);
308 const size_t index = (row + 1) * subwidth;
309 const size_t mask_index = row * width;
310 *count = _collect_color_runs(mask + mask_index, index, parity, width, runs, *count, nmask);
311 }
312}
313
314// Solve the laplace equation for pixels and store the result in-place.
315static void _heal_laplace_loop(float *const restrict red_pixels, float *const restrict black_pixels,
316 const size_t width, const size_t height,
317 const float *const restrict mask, const int max_iter)
318{
319 // we start by converting the opacity mask into runs of nonzero positions, handling the 'red' and 'black'
320 // checkerboarded pixels separately
321 // the worst case is when consecutive red pixels alternate between being in the mask and out (same for black),
322 // in which case we will need exactly as many values in the run-length encoding as pixels; any other
323 // arrangement will yield fewer runs. For any mask a user would have the patience to draw, there will only be
324 // a handful of runs in each row of pixels.
325 // Note that using `unsigned` instead of size_t, the stamp is limited to ~8 gigapixels (the main image can be larger)
326 const size_t subwidth = (width+1)/2; // round up to be able to handle odd widths
327 unsigned *const restrict red_runs = dt_pixelpipe_cache_alloc_align_cache(
328 sizeof(unsigned) * subwidth * (height + 2),
329 0);
330 unsigned *const restrict black_runs = dt_pixelpipe_cache_alloc_align_cache(
331 sizeof(unsigned) * subwidth * (height + 2),
332 0);
333 if(IS_NULL_PTR(red_runs) || IS_NULL_PTR(black_runs))
334 {
335 fprintf(stderr, "_heal_laplace_loop: error allocating memory for healing\n");
336 goto cleanup;
337 }
338
339 size_t num_red = 0;
340 size_t num_black = 0;
341 size_t nmask_red = 0;
342 size_t nmask_black = 0;
343
344#ifdef _OPENMP
345#pragma omp parallel sections
346#endif
347 {
348 collect_runs(1, mask, width, height, subwidth, red_runs, &num_red, &nmask_red);
349 #ifdef _OPENMP
350 #pragma omp section
351 #endif
352 collect_runs(0, mask, width, height, subwidth, black_runs, &num_black, &nmask_black);
353 }
354 const size_t nmask = nmask_red + nmask_black;
355
356 /* Empirically optimal over-relaxation factor. (Benchmarked on
357 * round brushes, at least. I don't know whether aspect ratio
358 * affects it.)
359 */
360 const float w = ((2.0f - 1.0f / (0.1575f * sqrtf(nmask) + 0.8f)) * .25f);
361
362 const float epsilon = (0.1 / 255);
363 const float err_exit = epsilon * epsilon * w * w;
364
365 /* Gauss-Seidel with successive over-relaxation */
366 for(int iter = 0; iter < max_iter; iter++)
367 {
368 // process red/black cells separately
369 float err = _heal_laplace_iteration(black_pixels, red_pixels, height, subwidth, black_runs, num_black, 1, w);
370 err += _heal_laplace_iteration(red_pixels, black_pixels, height, subwidth, red_runs, num_red, 0, w);
371
372 if(err < err_exit) break;
373 }
374
375cleanup:
378}
379
380
381/* Original Algorithm Design:
382 *
383 * T. Georgiev, "Photoshop Healing Brush: a Tool for Seamless Cloning
384 * http://www.tgeorgiev.net/Photoshop_Healing.pdf
385 */
386void dt_heal(const float *const src_buffer, float *dest_buffer, const float *const mask_buffer, const int width,
387 const int height, const int ch, const int max_iter)
388{
389 if(ch != 4)
390 {
391 fprintf(stderr,"dt_heal: full-color image required\n");
392 return;
393 }
394 const size_t subwidth = 4 * ((width+1)/2); // round up to be able to handle odd widths
395 float *const restrict red_buffer = dt_pixelpipe_cache_alloc_align_float_cache(subwidth * (height + 2), 0);
396 float *const restrict black_buffer = dt_pixelpipe_cache_alloc_align_float_cache(subwidth * (height + 2), 0);
397 if(IS_NULL_PTR(red_buffer) || IS_NULL_PTR(black_buffer))
398 {
399 fprintf(stderr, "dt_heal: error allocating memory for healing\n");
400 goto cleanup;
401 }
402
403 /* subtract pattern from image and store the result split by 'red' and 'black' positions */
404 _heal_sub(dest_buffer, src_buffer, red_buffer, black_buffer, width, height);
405
406 _heal_laplace_loop(red_buffer, black_buffer, width, height, mask_buffer, max_iter);
407
408 /* add solution to original image and store in dest */
409 _heal_add(red_buffer, black_buffer, src_buffer, dest_buffer, width, height);
410
411cleanup:
413 dt_pixelpipe_cache_free_align(black_buffer);
414}
415
416#ifdef HAVE_OPENCL
417
418/* The kernels this subsystem compiles, owned HERE. They used to be handed to
419 * common/opencl.c, parked on the application-wide dt_opencl_t, and read back from it --
420 * a round trip through a god-struct that added nothing but an ordering. opencl.c still
421 * calls init/free, because the kernels must be built after the devices exist, but the
422 * pointer never leaves this file. */
424
431
433{
435 _heal_cl_global = NULL;
436 if(IS_NULL_PTR(g)) return;
437
438 dt_free(g);
439}
440
442{
443
445 if(IS_NULL_PTR(p)) return NULL;
446
448 p->devid = devid;
449
450 return p;
451}
452
454{
455 if(IS_NULL_PTR(p)) return;
456 dt_free(p);
457}
458
459cl_int dt_heal_cl(heal_params_cl_t *p, cl_mem dev_src, cl_mem dev_dest, const float *const mask_buffer,
460 const int width, const int height, const int max_iter)
461{
462 cl_int err = CL_SUCCESS;
463
464 const int ch = 4;
465
466 float *src_buffer = NULL;
467 float *dest_buffer = NULL;
468
469 src_buffer = dt_pixelpipe_cache_alloc_align_float_cache((size_t)ch * width * height, 0);
470 if(IS_NULL_PTR(src_buffer))
471 {
472 fprintf(stderr, "dt_heal_cl: error allocating memory for healing\n");
474 goto cleanup;
475 }
476
477 dest_buffer = dt_pixelpipe_cache_alloc_align_float_cache((size_t)ch * width * height, 0);
478 if(IS_NULL_PTR(dest_buffer))
479 {
480 fprintf(stderr, "dt_heal_cl: error allocating memory for healing\n");
482 goto cleanup;
483 }
484
485 err = dt_opencl_read_buffer_from_device(p->devid, (void *)src_buffer, dev_src, 0,
486 (size_t)width * height * ch * sizeof(float), CL_TRUE);
487 if(err != CL_SUCCESS)
488 {
489 goto cleanup;
490 }
491
492 err = dt_opencl_read_buffer_from_device(p->devid, (void *)dest_buffer, dev_dest, 0,
493 (size_t)width * height * ch * sizeof(float), CL_TRUE);
494 if(err != CL_SUCCESS)
495 {
496 goto cleanup;
497 }
498
499 // I couldn't make it run fast on opencl (the reduction takes forever), so just call the cpu version
500 dt_heal(src_buffer, dest_buffer, mask_buffer, width, height, ch, max_iter);
501
502 err = dt_opencl_write_buffer_to_device(p->devid, dest_buffer, dev_dest, 0, sizeof(float) * width * height * ch, CL_TRUE);
503 if(err != CL_SUCCESS)
504 {
505 goto cleanup;
506 }
507
508cleanup:
511
512 return err;
513}
514
515#endif
516// clang-format off
517// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
518// vim: shiftwidth=2 expandtab tabstop=2 cindent
519// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
520// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void cleanup(dt_imageio_module_format_t *self)
Definition avif.c:170
static const int row
static size_t _collect_color_runs(const float *const restrict mask, const size_t start_index, size_t start, const size_t width, unsigned *const restrict runs, size_t count, size_t *nmask)
Definition heal.c:244
void dt_heal_free_cl_global(void)
Definition heal.c:432
void dt_heal_free_cl(heal_params_cl_t *p)
Definition heal.c:453
void dt_heal(const float *const src_buffer, float *dest_buffer, const float *const mask_buffer, const int width, const int height, const int ch, const int max_iter)
Definition heal.c:386
static dt_heal_cl_global_t * _heal_cl_global
Definition heal.c:423
static void collect_runs(const int start, const float *const restrict mask, const size_t width, const size_t height, const size_t subwidth, unsigned *const restrict runs, size_t *count, size_t *nmask)
Definition heal.c:302
void dt_heal_init_cl_global(void)
Definition heal.c:425
cl_int dt_heal_cl(heal_params_cl_t *p, cl_mem dev_src, cl_mem dev_dest, const float *const mask_buffer, const int width, const int height, const int max_iter)
Definition heal.c:459
heal_params_cl_t * dt_heal_init_cl(const int devid)
Definition heal.c:441
static void _heal_add(const float *const restrict red_buffer, const float *const black_buffer, const float *const restrict second_buffer, float *const restrict result_buffer, const size_t width, const size_t height)
Definition heal.c:107
static void _heal_laplace_loop(float *const restrict red_pixels, float *const restrict black_pixels, const size_t width, const size_t height, const float *const restrict mask, const int max_iter)
Definition heal.c:315
static void _heal_sub(const float *const top_buffer, const float *const bottom_buffer, float *const restrict red_buffer, float *const restrict black_buffer, const size_t width, const size_t height)
Definition heal.c:63
static float _heal_laplace_iteration(float *const restrict active_pixels, const float *const restrict neighbor_pixels, const size_t height, const size_t width, const unsigned *const restrict runs, const size_t num_runs, const size_t start_parity, const float w)
Definition heal.c:156
float *const restrict const size_t const size_t ch
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
int dt_opencl_write_buffer_to_device(const int devid, void *host, void *device, const size_t offset, const size_t size, const int blocking)
Definition opencl.c:2738
int dt_opencl_read_buffer_from_device(const int devid, void *host, void *device, const size_t offset, const size_t size, const int blocking)
Definition opencl.c:2727
#define DT_OPENCL_SYSMEM_ALLOCATION
Definition opencl.h:62
#define __OMP_PARALLEL_FOR__(...)
Definition openmp.h:95
#define dt_pixelpipe_cache_alloc_align_float_cache(pixels, id)
#define dt_pixelpipe_cache_alloc_align_cache(size, id)
#define dt_pixelpipe_cache_free_align(mem)
static void copy_pixel(float *const __restrict__ out, const float *const __restrict__ in)
Definition simd.h:217
DT_ALIGNED_PIXEL float dt_aligned_pixel_t[4]
Definition simd.h:53
#define for_each_channel(_var,...)
Definition simd.h:87
dt_aligned_pixel_t v
Definition eaw.c:199
dt_heal_cl_global_t * global
Definition heal.h:40