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