Skip to content

Commit b9007bc

Browse files
Add reading pfm files as rastermasks to segment maps
Had been requested a few times, one was #12551 1. In preferences we have a root folder containing such mask pfm files. 2. The segment map module gets a "mask from file" mode. There we have a button to select the file and besides that a menu offering the files found there (we have the same in luts). Just *.pfm files are supported, depending on being gray or rgb we offer one or three segment maps afterwards.
1 parent 5a939e4 commit b9007bc

File tree

4 files changed

+406
-88
lines changed

4 files changed

+406
-88
lines changed

data/darktableconfig.xml.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3500,6 +3500,13 @@
35003500
<shortdescription>LUT 3D root folder</shortdescription>
35013501
<longdescription>this folder (and sub-folders) contains LUT files used by LUT 3D module. (restart required)</longdescription>
35023502
</dtconfig>
3503+
<dtconfig prefs="processing" section="general" restart="true">
3504+
<name>plugins/darkroom/segments/def_path</name>
3505+
<type>dir</type>
3506+
<default>$(home)</default>
3507+
<shortdescription>image masks root folder</shortdescription>
3508+
<longdescription>this folder (and sub-folders) contains mask pfm files used by segment maps module. (restart required)</longdescription>
3509+
</dtconfig>
35033510
<dtconfig prefs="processing" section="general">
35043511
<name>plugins/darkroom/workflow</name>
35053512
<type>

src/iop/rastermapping/filemap.c

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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+
#include <dirent.h>
19+
20+
#if defined (_WIN32)
21+
#include "win/getdelim.h"
22+
#include "win/scandir.h"
23+
#endif
24+
25+
static void _filemap_segment(dt_segmentation_t *seg, char *filename)
26+
{
27+
/* basically copy&paste from imageio_pfm.c
28+
inspect that code for details
29+
*/
30+
float *readbuf = NULL;
31+
if(!filename || filename[0] == 0) return;
32+
FILE *f = g_fopen(filename, "r");
33+
if(f == NULL) goto error;
34+
35+
seg->threshold = 32;
36+
int ret = 0;
37+
char head[2] = { 'X', 'X' };
38+
39+
ret = fscanf(f, "%c%c\n", head, head + 1);
40+
if(ret != 2 || head[0] != 'P') goto error;
41+
42+
if(head[1] == 'F') seg->segments = 3;
43+
else if(head[1] == 'f') seg->segments = 1;
44+
else goto error;
45+
46+
gboolean made_by_photoshop = TRUE;
47+
for(;;)
48+
{
49+
int read_byte = fgetc(f);
50+
if((read_byte == '\n') || (read_byte == EOF))
51+
break;
52+
if(read_byte < '0') // easy way to match all whitespaces
53+
{
54+
made_by_photoshop = FALSE; // if present, the file is not saved by Photoshop
55+
break;
56+
}
57+
}
58+
fseek(f, 3, SEEK_SET);
59+
60+
char width_string[10] = { 0 };
61+
char height_string[10] = { 0 };
62+
char scale_factor_string[64] = { 0 };
63+
ret = fscanf(f, "%9s %9s %63s%*[^\n]", width_string, height_string, scale_factor_string);
64+
if(ret != 3) goto error;
65+
66+
seg->width = strtol(width_string, NULL, 0);
67+
seg->height = strtol(height_string, NULL, 0);
68+
const float scale_factor = g_ascii_strtod(scale_factor_string, NULL);
69+
70+
if(seg->width <= 0 || seg->height <= 0) goto error;
71+
72+
ret = fread(&ret, sizeof(char), 1, f);
73+
if(ret != 1) goto error;
74+
75+
const int swap_byte_order = (scale_factor >= 0.0) ^ (G_BYTE_ORDER == G_BIG_ENDIAN);
76+
const size_t npixels = (size_t)seg->width * seg->height;
77+
78+
readbuf = dt_alloc_align_float(npixels * seg->segments);
79+
if(!readbuf) goto error;
80+
ret = fread(readbuf, sizeof(float) * seg->segments, npixels, f);
81+
if(ret != npixels) goto error;
82+
83+
for(int i = 0; i < seg->segments; i++)
84+
seg->map[i] = dt_calloc_align_type(uint8_t, (size_t)seg->width * seg->height);
85+
86+
union { float as_float; guint32 as_int; } value;
87+
88+
if(seg->segments == 3)
89+
{
90+
DT_OMP_FOR(collapse(2))
91+
for(size_t row = 0; row < seg->height; row++)
92+
{
93+
const size_t target_row = made_by_photoshop ? row : seg->height - 1 - row;
94+
for(size_t column = 0; column < seg->width; column++)
95+
{
96+
for_three_channels(c)
97+
{
98+
value.as_float = readbuf[3 * (target_row * seg->width + column) + c];
99+
if(swap_byte_order) value.as_int = GUINT32_SWAP_LE_BE(value.as_int);
100+
101+
if(seg->map[c])
102+
seg->map[c][row*seg->width + column] = CLIP(value.as_float) * 255.0f;
103+
}
104+
}
105+
}
106+
}
107+
else
108+
{
109+
DT_OMP_FOR(collapse(2))
110+
for(size_t row = 0; row < seg->height; row++)
111+
{
112+
const size_t target_row = made_by_photoshop ? row : seg->height - 1 - row;
113+
for(size_t column = 0; column < seg->width; column++)
114+
{
115+
value.as_float = readbuf[target_row * seg->width + column];
116+
if(swap_byte_order) value.as_int = GUINT32_SWAP_LE_BE(value.as_int);
117+
118+
if(seg->map[0])
119+
seg->map[0][row*seg->width + column] = CLIP(value.as_float) * 255.0f;
120+
}
121+
}
122+
}
123+
124+
fclose(f);
125+
dt_free_align(readbuf);
126+
127+
dt_print(DT_DEBUG_PIPE, "file='%s' %d map segments %dx%d provided hash=%"PRIx64,
128+
filename, seg->segments, seg->width, seg->height, seg->hash);
129+
dt_control_log(_("%d filemap segments %dx%d provided"), seg->segments, seg->width, seg->height);
130+
return;
131+
132+
error:
133+
dt_print(DT_DEBUG_ALWAYS, "can't read image map file '%s'", filename ? filename : "???");
134+
dt_control_log(_("can't read image map file '%s'"), filename ? filename : "???");
135+
if(f) fclose(f);
136+
137+
dt_free_align(readbuf);
138+
for(int i = 0; i < seg->segments; i++) dt_free_align(seg->map[i]);
139+
seg->width = seg->height = seg->segments = 0;
140+
}
141+
142+
static int _check_extension(const struct dirent *namestruct)
143+
{
144+
const char *filename = namestruct->d_name;
145+
int res = 0;
146+
if(!filename || !filename[0]) return res;
147+
char *p = g_strrstr(filename,".");
148+
if(!p) return res;
149+
char *fext = g_ascii_strdown(g_strdup(p), -1);
150+
if(!g_strcmp0(fext, ".pfm")) res = 1;
151+
g_free(fext);
152+
return res;
153+
}
154+
155+
static void _update_filepath(dt_iop_module_t *self)
156+
{
157+
dt_iop_segmap_gui_data_t *g = self->gui_data;
158+
dt_iop_segmap_params_t *p = self->params;
159+
if(!p->path[0] || !p->file[0])
160+
{
161+
dt_bauhaus_combobox_clear(g->file);
162+
return;
163+
}
164+
165+
if(!dt_bauhaus_combobox_set_from_text(g->file, p->file))
166+
{
167+
struct dirent **entries;
168+
const int numentries = scandir(p->path, &entries, _check_extension, alphasort);
169+
dt_bauhaus_combobox_clear(g->file);
170+
171+
for(int i = 0; i < numentries; i++)
172+
{
173+
const char *file = entries[i]->d_name;
174+
dt_bauhaus_combobox_add_aligned(g->file, file, DT_BAUHAUS_COMBOBOX_ALIGN_LEFT);
175+
free(entries[i]);
176+
}
177+
if(numentries != -1) free(entries);
178+
179+
if(!dt_bauhaus_combobox_set_from_text(g->file, p->file))
180+
{ // file may have disappeared - show it
181+
char *invalidfilepath = g_strconcat(" ??? ", p->file, NULL);
182+
dt_bauhaus_combobox_add_aligned(g->file, invalidfilepath, DT_BAUHAUS_COMBOBOX_ALIGN_LEFT);
183+
dt_bauhaus_combobox_set_from_text(g->file, invalidfilepath);
184+
g_free(invalidfilepath);
185+
}
186+
}
187+
}
188+
189+
static void _fbutton_clicked(GtkWidget *widget, dt_iop_module_t *self)
190+
{
191+
dt_iop_segmap_gui_data_t *g = self->gui_data;
192+
dt_iop_segmap_params_t *p = self->params;
193+
194+
gchar *mfolder = dt_conf_get_string("plugins/darkroom/segments/def_path");
195+
if(strlen(mfolder) == 0)
196+
{
197+
dt_print(DT_DEBUG_ALWAYS, "segment masks root folder not defined");
198+
dt_control_log(_("segment masks root folder not defined"));
199+
g_free(mfolder);
200+
return;
201+
}
202+
203+
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
204+
GtkFileChooserNative *filechooser = gtk_file_chooser_native_new(
205+
_("select segment mask file"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_OPEN,
206+
_("_select"), _("_cancel"));
207+
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(filechooser), FALSE);
208+
gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), mfolder);
209+
GtkFileFilter *filter = GTK_FILE_FILTER(gtk_file_filter_new());
210+
// only pfm files yet supported
211+
gtk_file_filter_add_pattern(filter, "*.pfm");
212+
gtk_file_filter_add_pattern(filter, "*.PFM");
213+
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(filechooser), filter);
214+
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(filechooser), filter);
215+
216+
if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
217+
{
218+
gchar *filepath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
219+
const gboolean within = (strlen(filepath) > strlen(mfolder))
220+
&& (memcmp(filepath, mfolder, strlen(mfolder)) == 0);
221+
if(within)
222+
{
223+
char *relativepath = g_path_get_dirname(filepath);
224+
const int rplen = strlen(relativepath);
225+
memcpy(p->path, relativepath, rplen);
226+
p->path[rplen] = '\0';
227+
g_free(relativepath);
228+
229+
const int flen = strlen(filepath) - rplen - 1;
230+
memcpy(p->file, filepath + rplen + 1, flen);
231+
p->file[flen] = '\0';
232+
233+
_update_filepath(self);
234+
dt_dev_add_history_item(darktable.develop, self, FALSE);
235+
}
236+
else
237+
{
238+
dt_print(DT_DEBUG_ALWAYS, "selected file not within masks root folder");
239+
dt_control_log(_("selected file not within masks root folder"));
240+
}
241+
g_free(filepath);
242+
gtk_widget_set_sensitive(g->file, p->path[0] && p->file[0]);
243+
}
244+
g_free(mfolder);
245+
g_object_unref(filechooser);
246+
}
247+
248+
static void _file_callback(GtkWidget *widget, dt_iop_module_t *self)
249+
{
250+
dt_iop_segmap_params_t *p = self->params;
251+
const gchar *select = dt_bauhaus_combobox_get_text(widget);
252+
g_strlcpy(p->file, select, sizeof(p->file));
253+
dt_dev_add_history_item(darktable.develop, self, FALSE);
254+
}

src/iop/rastermapping/variance.c

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
static void _variance_segment(float *in,
20+
dt_segmentation_t *seg,
21+
const int depth,
22+
const int level,
23+
const dt_iop_roi_t *roi)
24+
{
25+
/* For many algorithms we might want to scale down for performance reasons, in addition
26+
to that we might require some blurring or other preprocessing.
27+
As the stored uint8_t maps are later bilinear interpolated when inserted into the pipe
28+
we can effectively choose any size/ratio for the maps.
29+
*/
30+
const int width = roi->width / 2;
31+
const int height = roi->height / 2;
32+
float *rgb = dt_iop_image_alloc(width, height, 4);
33+
if(!rgb)
34+
{
35+
dt_print(DT_DEBUG_ALWAYS, "can't provide variance segments because of low memory");
36+
dt_control_log(_("can't provide variance segments because of low memory"));
37+
return;
38+
}
39+
40+
interpolate_bilinear(in, roi->width, roi->height, rgb, width, height, 4);
41+
seg->postprocess = NULL;
42+
seg->width = width;
43+
seg->height = height;
44+
seg->segments = 3; // for many algorithms the number of presented segments will depend on depth
45+
seg->threshold = 4;
46+
for(int i = 0; i < seg->segments; i++)
47+
seg->map[i] = dt_calloc_align_type(uint8_t, (size_t)width * height);
48+
49+
const int r = depth+1;
50+
const int limit = r * r + 1;
51+
const float power = 0.4f + 0.025f * level;
52+
53+
DT_OMP_FOR(collapse(2))
54+
for(ssize_t row = 0; row < height; row++)
55+
{
56+
for(ssize_t col = 0; col < width; col++)
57+
{
58+
float pix = 0.0f; // count the pixels inside the circle
59+
dt_aligned_pixel_t av = { 0.0f, 0.0f, 0.0f, 0.0f };
60+
for(int y = MAX(0, row-r); y < MIN(height, row+r+1); y++)
61+
{
62+
for(int x = MAX(0, col-r); x < MIN(width, col+r+1); x++)
63+
{
64+
const int dx = x - col;
65+
const int dy = y - row;
66+
if((dx*dx + dy*dy) <= limit)
67+
{
68+
for_each_channel(c) av[c] += rgb[(size_t)4*(y*width + x) + c];
69+
pix += 1.0f;
70+
}
71+
}
72+
}
73+
for_each_channel(c) av[c] /= pix;
74+
75+
dt_aligned_pixel_t sv = { 0.0f, 0.0f, 0.0f, 0.0f };
76+
for(int y = MAX(0, row-r); y < MIN(height, row+r+1); y++)
77+
{
78+
for(int x = MAX(0, col-r); x < MIN(width, col+r+1); x++)
79+
{
80+
const int dx = x - col;
81+
const int dy = y - row;
82+
if((dx*dx + dy*dy) <= limit)
83+
{
84+
for_each_channel(c) sv[c] += sqrf(rgb[(size_t)4*(y*width + x) + c] - av[c]);
85+
}
86+
}
87+
}
88+
for_each_channel(c) sv[c] /= (pix - 1.0f);
89+
90+
for_three_channels(c)
91+
if(seg->map[c]) seg->map[c][row*width + col] = CLIP(3.0f * powf(sv[c], power)) * 255.0f;
92+
}
93+
}
94+
dt_print(DT_DEBUG_PIPE, "%d variance segments %dx%d provided hash=%"PRIx64, seg->segments, seg->width, seg->height, seg->hash);
95+
dt_control_log(_("%d variance segments %dx%d provided"), seg->segments, seg->width, seg->height);
96+
dt_free_align(rgb);
97+
}

0 commit comments

Comments
 (0)