Skip to content

Commit 21cbb48

Browse files
authored
Merge pull request darktable-org#20198 from darktable-org/po/bitmap-vectorisation
Bitmap vectorisation - prerequisit for AI masking
2 parents 0927dd1 + 5562222 commit 21cbb48

File tree

12 files changed

+444
-45
lines changed

12 files changed

+444
-45
lines changed

.ci/Brewfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ brew 'openexr'
4949
brew 'openjpeg'
5050
brew 'osm-gps-map'
5151
brew 'portmidi'
52+
brew 'potrace'
5253
brew 'pugixml'
5354
brew 'sdl2'
5455
brew 'curl'

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ jobs:
140140
libpango1.0-dev \
141141
libpng-dev \
142142
libportmidi-dev \
143+
libpotrace-dev \
143144
libpugixml-dev \
144145
libraw-dev \
145146
librsvg2-dev \
@@ -262,6 +263,7 @@ jobs:
262263
openjpeg2:p
263264
osm-gps-map:p
264265
portmidi:p
266+
potrace:p
265267
pugixml:p
266268
SDL2:p
267269
sqlite3:p

.github/workflows/nightly.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ jobs:
7676
libpango1.0-dev \
7777
libpng-dev \
7878
libportmidi-dev \
79+
libpotrace-dev \
7980
libpugixml-dev \
8081
librsvg2-dev \
8182
libsaxon-java \
@@ -249,6 +250,7 @@ jobs:
249250
openjpeg2:p
250251
osm-gps-map:p
251252
portmidi:p
253+
potrace:p
252254
pugixml:p
253255
SDL2:p
254256
sqlite3:p

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ Required dependencies (minimum version):
176176
* libcurl 7.56
177177
* libpng 1.5.0 *(for PNG import & export, also for reading LUT files in PNG format)*
178178
* Exiv2 0.27.2 *(but at least 0.27.4 built with ISO BMFF support needed for Canon CR3 raw import)*
179+
* potrace 1.16
179180
* pugixml 1.8
180181

181182
Required dependencies (no version requirement):

cmake/modules/FindPotrace.cmake

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
find_path(POTRACE_INCLUDE_DIR
2+
NAMES potracelib.h
3+
PATH_SUFFIXES include
4+
)
5+
6+
find_library(POTRACE_LIBRARY
7+
NAMES potrace libpotrace
8+
PATH_SUFFIXES lib
9+
)
10+
11+
include(FindPackageHandleStandardArgs)
12+
find_package_handle_standard_args(Potrace
13+
REQUIRED_VARS POTRACE_LIBRARY POTRACE_INCLUDE_DIR
14+
)
15+
16+
if(Potrace_FOUND)
17+
add_library(Potrace::Potrace UNKNOWN IMPORTED)
18+
set_target_properties(Potrace::Potrace PROPERTIES
19+
IMPORTED_LOCATION ${POTRACE_LIBRARY}
20+
INTERFACE_INCLUDE_DIRECTORIES ${POTRACE_INCLUDE_DIR}
21+
)
22+
endif()

packaging/macosx/1_install_hb_dependencies.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ hbDependencies="adwaita-icon-theme \
5959
perl \
6060
po4a \
6161
portmidi \
62+
potrace \
6263
pugixml \
6364
sdl2 \
6465
webp"

src/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ FILE(GLOB SOURCE_FILES
7474
"common/presets.c"
7575
"common/pwstorage/backend_kwallet.c"
7676
"common/pwstorage/pwstorage.c"
77+
"common/ras2vect.c"
7778
"common/ratings.c"
7879
"common/resource_limits.c"
7980
"common/selection.c"
@@ -307,6 +308,10 @@ include_directories(SYSTEM ${LIBXML2_INCLUDE_DIR})
307308
list(APPEND LIBS ${LIBXML2_LIBRARIES})
308309
add_definitions(${LIBXML2_DEFINITIONS})
309310

311+
# Check for potrace
312+
find_package(Potrace REQUIRED)
313+
list(APPEND LIBS ${POTRACE_LIBRARY})
314+
310315
if(USE_CAMERA_SUPPORT)
311316
find_package(Gphoto2 2.5)
312317
if(Gphoto2_FOUND)

src/common/ras2vect.c

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
This file is part of darktable,
3+
Copyright (C) 2026 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 <stddef.h>
20+
#include <potracelib.h>
21+
22+
#include "develop/masks.h"
23+
24+
#ifdef _OPENMP
25+
#include <omp.h>
26+
#endif
27+
28+
/* Macros as in Inkscape */
29+
#define BM_WORDBITS (8 * (int)sizeof(potrace_word))
30+
#define BM_HIBIT ((potrace_word)1 << (BM_WORDBITS-1))
31+
32+
#define bm_scanline(bm, y) ((bm)->map + (ptrdiff_t)(y) * (ptrdiff_t)(bm)->dy)
33+
#define bm_index(bm,x,y) (&bm_scanline(bm,y)[ (x) / BM_WORDBITS ])
34+
#define bm_mask(x) (BM_HIBIT >> ((x) & (BM_WORDBITS-1)))
35+
36+
#define BM_USET(bm,x,y) (*bm_index(bm,x,y) |= bm_mask(x))
37+
#define BM_UCLR(bm,x,y) (*bm_index(bm,x,y) &= ~bm_mask(x))
38+
39+
static potrace_bitmap_t *_bm_new(const int w,
40+
const int h)
41+
{
42+
potrace_bitmap_t *bm = calloc(1, sizeof(*bm));
43+
if(!bm) return NULL;
44+
45+
bm->w = w;
46+
bm->h = h;
47+
bm->dy = (w + BM_WORDBITS - 1) / BM_WORDBITS; /* words per scanline */
48+
const size_t total_words = (size_t)bm->dy * h;
49+
bm->map = calloc(total_words, sizeof(potrace_word));
50+
51+
if(!bm->map)
52+
{
53+
free(bm);
54+
return NULL;
55+
}
56+
57+
return bm;
58+
}
59+
60+
static void _bm_free(potrace_bitmap_t *bm)
61+
{
62+
if(bm)
63+
{
64+
free(bm->map);
65+
free(bm);
66+
}
67+
}
68+
69+
#define SET_THRESHOLD 0.6f
70+
71+
static inline void _scale_point(float p[2],
72+
const float xscale,
73+
const float yscale,
74+
const float cx,
75+
const float cy,
76+
const float iwidth,
77+
const float iheight)
78+
{
79+
p[0] = ((p[0] * xscale) + cx) / iwidth;
80+
p[1] = ((p[1] * yscale) + cy) / iheight;
81+
}
82+
83+
static void _add_point(dt_masks_form_t *form,
84+
const dt_image_t *const image,
85+
const float width,
86+
const float height,
87+
const float x,
88+
const float y,
89+
const float ctl1_x,
90+
const float ctl1_y,
91+
const float ctl2_x,
92+
const float ctl2_y)
93+
{
94+
dt_masks_point_path_t *bzpt = calloc(1, sizeof(dt_masks_point_path_t));
95+
96+
bzpt->corner[0] = x;
97+
bzpt->corner[1] = y;
98+
99+
// set the control points if defined
100+
if(ctl1_x > 0)
101+
{
102+
bzpt->ctrl1[0] = ctl1_x;
103+
bzpt->ctrl1[1] = ctl1_y;
104+
bzpt->ctrl2[0] = ctl2_x;
105+
bzpt->ctrl2[1] = ctl2_y;
106+
}
107+
else
108+
{
109+
bzpt->ctrl1[0] = x;
110+
bzpt->ctrl1[1] = y;
111+
bzpt->ctrl2[0] = x;
112+
bzpt->ctrl2[1] = y;
113+
}
114+
115+
if(image)
116+
{
117+
const float iwidth = image->width;
118+
const float iheight = image->height;
119+
const float pwidth = image->p_width;
120+
const float pheight = image->p_height;
121+
const float cx = image->crop_x;
122+
const float cy = image->crop_y;
123+
124+
const float xscale = pwidth / (float)width;
125+
const float yscale = pheight / (float)height;
126+
127+
_scale_point(bzpt->corner, xscale, yscale, cx, cy, iwidth, iheight);
128+
_scale_point(bzpt->ctrl1, xscale, yscale, cx, cy, iwidth, iheight);
129+
_scale_point(bzpt->ctrl2, xscale, yscale, cx, cy, iwidth, iheight);
130+
}
131+
132+
bzpt->state = DT_MASKS_POINT_STATE_USER;
133+
bzpt->border[0] = bzpt->border[1] = 0.f;
134+
135+
form->points = g_list_append(form->points, bzpt);
136+
}
137+
138+
static uint32_t formnb = 0;
139+
140+
GList *ras2forms(const float *mask,
141+
const int width,
142+
const int height,
143+
const dt_image_t *const image)
144+
{
145+
GList *forms = NULL;
146+
147+
// create bitmap mask for potrace
148+
149+
potrace_bitmap_t *bm = _bm_new(width, height);
150+
151+
DT_OMP_FOR()
152+
for(int y=0; y < height; y++)
153+
{
154+
for(int x=0; x < width; x++)
155+
{
156+
const int index = x + y * width;
157+
if(mask[index] < SET_THRESHOLD)
158+
{
159+
// black enough to be a point of the form
160+
BM_USET(bm, x, y);
161+
}
162+
else
163+
{
164+
BM_UCLR(bm, x, y);
165+
}
166+
}
167+
}
168+
169+
potrace_param_t *param = potrace_param_default();
170+
// finer path possible
171+
param->alphamax = 0.0f;
172+
173+
potrace_state_t *st = potrace_trace(param, bm);
174+
175+
// get all paths, create corresponding path form
176+
177+
for(const potrace_path_t *p = st->plist;
178+
p;
179+
p = p->next)
180+
{
181+
const potrace_curve_t *cv = &p->curve;
182+
const int n = cv->n;
183+
184+
// Start = end of last segment
185+
const potrace_dpoint_t start = cv->c[n-1][2];
186+
187+
dt_masks_form_t *form = dt_masks_create(DT_MASKS_PATH);
188+
snprintf(form->name, sizeof(form->name), "path raster %d", ++formnb);
189+
190+
_add_point(form, image, width, height, start.x, start.y, -1, -1, -1, -1);
191+
192+
for(int i = 0; i < n; i++)
193+
{
194+
if(cv->tag[i] == POTRACE_CURVETO)
195+
{
196+
const potrace_dpoint_t c0 = cv->c[i][0];
197+
const potrace_dpoint_t c1 = cv->c[i][1];
198+
const potrace_dpoint_t e = cv->c[i][2];
199+
200+
_add_point(form, image, width, height, e.x, e.y, c0.x, c0.y, c1.x, c1.y);
201+
}
202+
else // POTRACE_CORNER
203+
{
204+
const potrace_dpoint_t v = cv->c[i][1];
205+
const potrace_dpoint_t e = cv->c[i][2];
206+
207+
_add_point(form, image, width, height, v.x, v.y, -1, -1, -1, -1);
208+
_add_point(form, image, width, height, e.x, e.y, -1, -1, -1, -1);
209+
}
210+
}
211+
212+
forms = g_list_prepend(forms, form);
213+
}
214+
215+
potrace_state_free(st);
216+
potrace_param_free(param);
217+
_bm_free(bm);
218+
219+
return forms;
220+
}
221+
222+
// clang-format off
223+
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
224+
// vim: shiftwidth=2 expandtab tabstop=2 cindent
225+
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
226+
// clang-format on

src/common/ras2vect.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
This file is part of darktable,
3+
Copyright (C) 2026 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+
#pragma once
20+
21+
/* Returns a list of path forms after having vectorized the raster mask.
22+
The coordinates are either on mask space: (0 x 0) -> (width x height)
23+
or if image is set (not NULL) on image spaces making the masks directly
24+
usable on the corresponding image.
25+
26+
*/
27+
GList *ras2forms(const float *mask,
28+
const int width,
29+
const int height,
30+
const dt_image_t *const image);
31+
32+
// clang-format off
33+
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
34+
// vim: shiftwidth=2 expandtab tabstop=2 cindent
35+
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
36+
// clang-format on

src/develop/masks.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
This file is part of darktable,
3-
Copyright (C) 2013-2025 darktable developers.
3+
Copyright (C) 2013-2026 darktable developers.
44
55
darktable is free software: you can redistribute it and/or modify
66
it under the terms of the GNU General Public License as published by
@@ -536,6 +536,9 @@ void dt_masks_replace_current_forms(dt_develop_t *dev, GList *forms);
536536
dt_masks_form_t *dt_masks_get_from_id_ext(GList *forms, dt_mask_id_t id);
537537
/** returns a form with formid == id from dev->forms */
538538
dt_masks_form_t *dt_masks_get_from_id(const dt_develop_t *dev, dt_mask_id_t id);
539+
/** register forms into the mask manager */
540+
void dt_masks_register_forms(dt_develop_t *dev,
541+
GList *forms);
539542

540543
/** read the forms from the db */
541544
void dt_masks_read_masks_history(dt_develop_t *dev, const dt_imgid_t imgid);

0 commit comments

Comments
 (0)