Skip to content

Commit f72ba00

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 f72ba00

File tree

4 files changed

+408
-88
lines changed

4 files changed

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

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)