Skip to content

Commit 8087365

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 8087365

File tree

4 files changed

+409
-88
lines changed

4 files changed

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

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)