Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
segmentation.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel 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 Ansel 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// Connected-component segmentation of the clipped regions (host, both paths). (implementation; see segmentation.h
20// for the public API.)
21
22#include "common/darktable.h"
23#include "develop/imageop.h"
26#include <stdlib.h>
27#include <string.h>
28
29int _segment_clipped_regions(const uint8_t *const restrict maskb, const float *const restrict depth,
30 const int width, const int height, const float pad_factor, const int pad_min,
31 const int pad_max, _hl_region_t **regions_out)
32{
33 const size_t npix = (size_t)width * height;
34 int *const restrict label = calloc(npix, sizeof(int)); // 0 = background / unvisited
35 int *const restrict stack = malloc(npix * sizeof(int)); // flood-fill work stack
36 if(!label || !stack)
37 {
38 free(label);
39 free(stack);
40 *regions_out = NULL;
41 return 0;
42 }
43
44 int capacity = 64, count = 0;
45 _hl_region_t *regions = malloc((size_t)capacity * sizeof(_hl_region_t));
46 if(!regions)
47 {
48 free(label);
49 free(stack);
50 *regions_out = NULL;
51 return 0;
52 }
53
54 for(size_t pixel_index = 0; pixel_index < npix; pixel_index++)
55 {
56 // seed on the REAL feather support: the 5x5 box mean's genuine values are >= 1/25, while
57 // the CPU running-sum blur leaves ~1e-7 cancellation residue on millions of pixels whose
58 // true value is zero -- seeding on > 0 made the region topology depend on float noise
59 // (and differ between the CPU and OpenCL gathers, which compute exact zeros)
60 if(label[pixel_index] || !maskb[pixel_index]) continue;
61 int stack_top = 0;
62 stack[stack_top++] = (int)pixel_index;
63 label[pixel_index] = count + 1;
64 // bounding box of the region, grown pixel by pixel as the flood fill visits them
65 int x_min = (int)(pixel_index % (size_t)width);
66 int x_max = x_min;
67 int y_min = (int)(pixel_index / (size_t)width);
68 int y_max = y_min;
69 float rmax = depth[pixel_index]; // reconstruction radius = deepest clip-to-valid distance in the region
70 while(stack_top > 0)
71 {
72 const int visited_index = stack[--stack_top];
73 const int visited_x = visited_index % width;
74 const int visited_y = visited_index / width;
75
76 // grow the region's bounding box to include the visited pixel, and keep the deepest
77 // clip-to-valid distance seen so far (it becomes the region's reconstruction radius)
78 if(visited_x < x_min) x_min = visited_x;
79 if(visited_x > x_max) x_max = visited_x;
80 if(visited_y < y_min) y_min = visited_y;
81 if(visited_y > y_max) y_max = visited_y;
82 if(depth[visited_index] > rmax) rmax = depth[visited_index];
83
84 // push every in-bounds, still-unlabelled clipped neighbour (8-connectivity) onto the
85 // flood-fill stack, so connected clipped pixels end up in the same region
86 for(int delta_y = -1; delta_y <= 1; delta_y++)
87 for(int delta_x = -1; delta_x <= 1; delta_x++)
88 {
89 if(!delta_x && !delta_y) continue;
90
91 const int neighbour_x = visited_x + delta_x;
92 const int neighbour_y = visited_y + delta_y;
93 if(neighbour_x < 0 || neighbour_y < 0 || neighbour_x >= width || neighbour_y >= height) continue;
94
95 const size_t neighbour_index = (size_t)neighbour_y * width + neighbour_x;
96 if(label[neighbour_index] || !maskb[neighbour_index]) continue;
97
98 label[neighbour_index] = count + 1;
99 stack[stack_top++] = (int)neighbour_index;
100 }
101 }
102 if(count >= capacity)
103 {
104 capacity *= 2;
105 _hl_region_t *const tmp = realloc(regions, (size_t)capacity * sizeof(_hl_region_t));
106 if(!tmp)
107 {
108 free(regions);
109 free(label);
110 free(stack);
111 *regions_out = NULL;
112 return 0;
113 }
114 regions = tmp;
115 }
116 // radius = deepest clip-to-valid distance (distance transform), padded by pad_factor of it
117 const int pad = CLAMP((int)(pad_factor * rmax + 0.5f), pad_min, pad_max); // pad = clamp(ceil(pad_factor * R))
118 regions[count].x0 = x_min; // clipped-pixel bbox (accumulated over the flood fill)
119 regions[count].y0 = y_min;
120 regions[count].x1 = x_max;
121 regions[count].y1 = y_max;
122 regions[count].pad = pad;
123 regions[count].radius = rmax; // reconstruction radius R = max_{x in Omega} delta(x)
124 regions[count].rx0 = MAX(x_min - pad, 0);
125 regions[count].ry0 = MAX(y_min - pad, 0);
126 regions[count].rx1 = MIN(x_max + pad, width - 1);
127 regions[count].ry1 = MIN(y_max + pad, height - 1);
128 count++;
129 }
130 free(label);
131 free(stack);
132
133 // Merge regions whose padded READ boxes overlap. Such regions share reconstruction context, so
134 // processing them separately is redundant and leaves a seam where their fills meet (each sees the
135 // other only as unreconstructed clip values). Union-find on padded-box intersection, then rebuild
136 // one region per group: union of the member CLIPPED bboxes, re-padded from the merged extent.
137 if(count > 1)
138 {
139 int *const restrict parent = malloc((size_t)count * sizeof(int));
140 _hl_region_t *const restrict merged = malloc((size_t)count * sizeof(_hl_region_t));
141 int *const restrict map = malloc((size_t)count * sizeof(int));
142 if(!parent || !merged || !map)
143 {
144 free(parent);
145 free(merged);
146 free(map);
147 *regions_out = regions;
148 return count;
149 }
150 for(int i = 0; i < count; i++) parent[i] = i;
151
152 // union every pair whose padded read boxes intersect (they share reconstruction context)
153 for(int i = 0; i < count; i++)
154 {
155 for(int j = i + 1; j < count; j++)
156 {
157 // skip disjoint padded boxes
158 if(regions[i].rx0 > regions[j].rx1 || regions[j].rx0 > regions[i].rx1) continue;
159 if(regions[i].ry0 > regions[j].ry1 || regions[j].ry0 > regions[i].ry1) continue;
160
161 // find the root of i (path halving)
162 int root_i = i;
163 while(parent[root_i] != root_i)
164 {
165 parent[root_i] = parent[parent[root_i]];
166 root_i = parent[root_i];
167 }
168
169 // find the root of j (path halving)
170 int root_j = j;
171 while(parent[root_j] != root_j)
172 {
173 parent[root_j] = parent[parent[root_j]];
174 root_j = parent[root_j];
175 }
176
177 // link the two components
178 if(root_i != root_j) parent[root_j] = root_i;
179 }
180 }
181
182 for(int i = 0; i < count; i++) map[i] = -1;
183
184 // fold each component into its root: union the clipped bboxes, keep the group's MAX padding
185 int mcount = 0;
186 for(int i = 0; i < count; i++)
187 {
188 // find the root (path halving)
189 int root_i = i;
190 while(parent[root_i] != root_i)
191 {
192 parent[root_i] = parent[parent[root_i]];
193 root_i = parent[root_i];
194 }
195
196 if(map[root_i] < 0)
197 {
198 // first member of this group: seed the merged region with it
199 map[root_i] = mcount;
200 merged[mcount] = regions[i];
201 mcount++;
202 }
203 else
204 {
205 // grow the group's bbox and keep the largest reconstruction radius in the group, so the
206 // smaller holes inherit enough context (per the merge rule)
207 _hl_region_t *const merged_region = &merged[map[root_i]];
208 merged_region->x0 = MIN(merged_region->x0, regions[i].x0);
209 merged_region->y0 = MIN(merged_region->y0, regions[i].y0);
210 merged_region->x1 = MAX(merged_region->x1, regions[i].x1);
211 merged_region->y1 = MAX(merged_region->y1, regions[i].y1);
212 merged_region->pad = MAX(merged_region->pad, regions[i].pad);
213 merged_region->radius = fmaxf(merged_region->radius, regions[i].radius);
214 }
215 }
216
217 // pad every merged region by the group's largest radius, clamped to the image
218 for(int merged_region = 0; merged_region < mcount; merged_region++)
219 {
220 const int pad = merged[merged_region].pad;
221 merged[merged_region].rx0 = MAX(merged[merged_region].x0 - pad, 0);
222 merged[merged_region].ry0 = MAX(merged[merged_region].y0 - pad, 0);
223 merged[merged_region].rx1 = MIN(merged[merged_region].x1 + pad, width - 1);
224 merged[merged_region].ry1 = MIN(merged[merged_region].y1 + pad, height - 1);
225 }
226 free(parent);
227 free(map);
228 free(regions);
229 *regions_out = merged;
230 return mcount;
231 }
232
233 *regions_out = regions;
234 return count;
235}
int width
Definition bilateral.h:1
int height
Definition bilateral.h:1
int _segment_clipped_regions(const uint8_t *const restrict maskb, const float *const restrict depth, const int width, const int height, const float pad_factor, const int pad_min, const int pad_max, _hl_region_t **regions_out)
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29