diff --git a/src/common/iop_order.c b/src/common/iop_order.c index 67943405f534..32978c021c5a 100644 --- a/src/common/iop_order.c +++ b/src/common/iop_order.c @@ -1,6 +1,6 @@ /* This file is part of darktable, - Copyright (C) 2018-2024 darktable developers. + Copyright (C) 2018-2025 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -82,6 +82,7 @@ const dt_iop_order_entry_t legacy_order[] = { { { 1.0f }, "rawprepare", 0}, { { 2.0f }, "invert", 0}, { { 3.0f }, "temperature", 0}, + { { 3.5f }, "rastermaps", 0}, { { 4.0f }, "highlights", 0}, { { 5.0f }, "cacorrect", 0}, { { 6.0f }, "hotpixels", 0}, @@ -177,6 +178,7 @@ const dt_iop_order_entry_t v30_order[] = { { { 1.0 }, "rawprepare", 0}, { { 2.0 }, "invert", 0}, { { 3.0f }, "temperature", 0}, + { { 3.5f }, "rastermaps", 0}, { { 4.0f }, "highlights", 0}, { { 5.0f }, "cacorrect", 0}, { { 6.0f }, "hotpixels", 0}, @@ -293,6 +295,7 @@ const dt_iop_order_entry_t v50_order[] = { { { 1.0 }, "rawprepare", 0}, { { 2.0 }, "invert", 0}, { { 3.0f }, "temperature", 0}, + { { 3.5f }, "rastermaps", 0}, { { 4.0f }, "highlights", 0}, { { 5.0f }, "cacorrect", 0}, { { 6.0f }, "hotpixels", 0}, @@ -411,6 +414,7 @@ const dt_iop_order_entry_t v30_jpg_order[] = { { { 1.0 }, "rawprepare", 0 }, { { 2.0 }, "invert", 0 }, { { 3.0f }, "temperature", 0 }, + { { 3.5f }, "rastermaps", 0}, { { 4.0f }, "highlights", 0 }, { { 5.0f }, "cacorrect", 0 }, { { 6.0f }, "hotpixels", 0 }, @@ -530,6 +534,7 @@ const dt_iop_order_entry_t v50_jpg_order[] = { { { 1.0 }, "rawprepare", 0 }, { { 2.0 }, "invert", 0 }, { { 3.0f }, "temperature", 0 }, + { { 3.5f }, "rastermaps", 0}, { { 4.0f }, "highlights", 0 }, { { 5.0f }, "cacorrect", 0 }, { { 6.0f }, "hotpixels", 0 }, @@ -1173,6 +1178,7 @@ GList *dt_ioppr_get_iop_order_list(const dt_imgid_t imgid, _insert_before(iop_order_list, "nlmeans", "blurs"); _insert_before(iop_order_list, "filmicrgb", "sigmoid"); _insert_before(iop_order_list, "colorbalancergb", "colorequal"); + _insert_before(iop_order_list, "rastermaps", "highlights"); } } else if(version >= DT_IOP_ORDER_LEGACY diff --git a/src/iop/CMakeLists.txt b/src/iop/CMakeLists.txt index de7d695267fa..bf3610384ef7 100644 --- a/src/iop/CMakeLists.txt +++ b/src/iop/CMakeLists.txt @@ -154,6 +154,7 @@ add_iop(blurs "blurs.c") add_iop(sigmoid "sigmoid.c") add_iop(primaries "primaries.c") add_iop(colorequal "colorequal.c") +add_iop(rastermaps "rastermaps.c") if(Rsvg2_FOUND) add_iop(watermark "watermark.c") diff --git a/src/iop/rastermaps.c b/src/iop/rastermaps.c new file mode 100644 index 000000000000..bde6db065a6c --- /dev/null +++ b/src/iop/rastermaps.c @@ -0,0 +1,932 @@ +/* + This file is part of darktable, + Copyright (C) 2025 darktable developers. + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +*/ + + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include +#include +#include + +#include "bauhaus/bauhaus.h" +#include "develop/tiling.h" +#include "develop/develop.h" +#include "develop/imageop.h" +#include "develop/imageop_gui.h" +#include "develop/imageop_math.h" +#include "common/interpolation.h" +#include "common/fast_guided_filter.h" +#include "gui/accelerators.h" +#include "gui/gtk.h" + +#ifdef _OPENMP +#include +#endif + +DT_MODULE_INTROSPECTION(1, dt_iop_segmap_params_t) + +typedef enum dt_iop_segmap_model_t +{ + DT_SEGMAP_MODEL_VARIANCE = 0, // $DESCRIPTION: "local variance" + DT_SEGMAP_MODELS, +} dt_iop_segmap_model_t; + +/* For every defined model we require these statics + - model_hash reflects an internal version hash, the model algorithm must change it + in case of new internals like training data + - model_depth defines if the depth parameter will be visible + - model_level defines if the level parameter will be visible + - model_fbutton defines visibility of a file button + - model_file defines visibility of a file + - _depth_help provides the tooltip for depth parameter + - _level_help provides the tooltip for level parameter + - _model_help provides the tooltip for the selected model +*/ + +static dt_hash_t model_hash[DT_SEGMAP_MODELS] = { DT_INITHASH }; +static gboolean model_depth[DT_SEGMAP_MODELS] = { TRUE }; +static gboolean model_level[DT_SEGMAP_MODELS] = { TRUE }; +static gboolean model_fbutton[DT_SEGMAP_MODELS] = { FALSE }; +static gboolean model_file[DT_SEGMAP_MODELS] = { FALSE }; + +static char *_depth_help(const int model) +{ + switch(model) + { + case DT_SEGMAP_MODEL_VARIANCE: return _("circular radius of variance calculation"); + default: return _("unknown"); + } +} + +static char *_level_help(const int model) +{ + switch(model) + { + case DT_SEGMAP_MODEL_VARIANCE: return _("threshold of variance calculation"); + default: return _("unknown"); + } +} + +static char *_model_help(const int model) +{ + switch(model) + { + case DT_SEGMAP_MODEL_VARIANCE: return _("create local variance maps for each RGB channel"); + default: return _("unknown"); + } +} + +#define UNDEFINED_MOUSE_SEGMENT -2 +#define NO_MOUSE_SEGMENT -1 +#define SEGMAP_MAXSEGMENTS 128 + +typedef struct dt_segmentation_t +{ + dt_pthread_mutex_t lock; // all access to segmentation data is done in locked state + dt_hash_t hash; // the piece parameters hash + dt_iop_segmap_model_t model; // The UI mode requires this to avoid superfluos actions + int segments; // provided segment maps after the segmentation + int width, height; // dimension of each segment map + int threshold; // relevance threshold + uint8_t *map[SEGMAP_MAXSEGMENTS]; // a map per segment + + /* After scaling the map to rastermask we might do some extra work like deblurring. + If undefined _postprocess_default() is used. + If defined make sure the mask data are in 0->1 range + */ + void(*postprocess) (float *mask, int width, int height, int depth, int level); +} dt_segmentation_t; + +typedef struct dt_iop_segmap_params_t +{ + dt_iop_segmap_model_t model; // $DEFAULT: DT_SEGMAP_MODEL_VARIANCE $DESCRIPTION: "model" + int depth; // $MIN: 0 $MAX: 20 $DEFAULT: 2 $DESCRIPTION: "model depth" + int level; // $MIN: 0 $MAX: 20 $DEFAULT: 2 $DESCRIPTION: "model detail" + uint8_t id[SEGMAP_MAXSEGMENTS]; +} dt_iop_segmap_params_t; + +typedef struct dt_iop_segmap_data_t +{ + dt_iop_segmap_model_t model; + int depth; + int level; + uint8_t id[SEGMAP_MAXSEGMENTS]; +} dt_iop_segmap_data_t; + +typedef struct dt_iop_segmap_module_data_t +{ + dt_segmentation_t *segment; +} dt_iop_segmap_module_data_t; + +const char *name() +{ + return _("segment maps"); +} + +const char *aliases() +{ + return _("segmentation|raster|mask|map|AI"); +} + +const char **description(dt_iop_module_t *self) +{ + return dt_iop_set_description(self, + _("create and handle segment rastermasks"), + _("corrective or creative"), + _("linear, raw, scene-referred"), + _("linear, raw"), + _("linear, raw, scene-referred")); +} + +int default_group() +{ + return IOP_GROUP_BASIC | IOP_GROUP_TECHNICAL; +} + +int flags() +{ + return IOP_FLAGS_WRITE_RASTER; +} + +dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, + dt_dev_pixelpipe_t *pipe, + dt_dev_pixelpipe_iop_t *piece) +{ + return (pipe && !dt_image_is_raw(&pipe->image)) ? IOP_CS_RGB : IOP_CS_RAW; +} + +typedef struct dt_iop_segmap_gui_data_t +{ + GtkWidget *model; + GtkWidget *depth; + GtkWidget *level; + GtkWidget *fbutton; + GtkWidget *file; + int mouse_segment; + gboolean down; + gboolean dclick; +} dt_iop_segmap_gui_data_t; + +int legacy_params(dt_iop_module_t *self, + const void *const old_params, + const int old_version, + void **new_params, + int32_t *new_params_size, + int *new_version) +{ + return 1; +} + +void modify_roi_in(dt_iop_module_t *self, + dt_dev_pixelpipe_iop_t *piece, + const dt_iop_roi_t *const roi_out, + dt_iop_roi_t *roi_in) +{ + *roi_in = *roi_out; + roi_in->scale = 1.0f; + roi_in->x = 0; + roi_in->y = 0; + roi_in->width = piece->buf_in.width; + roi_in->height = piece->buf_in.height; +} + +// the default postprocess algorithm, some blurring for edges plus range limit safety. +static void _postprocess_default(float *mask, const int width, const int height, const int depth, const int level) +{ + const float sigma = 1.0f; + const float mmax[] = { 1.0f }; + const float mmin[] = { 0.0f }; + dt_gaussian_t *g = dt_gaussian_init(width, height, 1, mmax, mmin, sigma, 0); + if(g) + { + dt_gaussian_blur(g, mask, mask); + dt_gaussian_free(g); + } +} + +static void _variance_segment(float *in, + dt_segmentation_t *seg, + const int depth, + const int level, + const dt_iop_roi_t *roi) +{ + /* For many algorithms we might want to scale down for performance reasons, in addition + to that we might require some blurring or other preprocessing. + As the stored uint8_t maps are later bilinear interpolated when inserted into the pipe + we can effectively choose any size/ratio for the maps. + */ + const int width = roi->width / 2; + const int height = roi->height / 2; + float *rgb = dt_iop_image_alloc(width, height, 4); + if(!rgb) + { + dt_print(DT_DEBUG_ALWAYS, "can't provide variance segments because of low memory"); + dt_control_log(_("can't provide variance segments because of low memory")); + return; + } + + interpolate_bilinear(in, roi->width, roi->height, rgb, width, height, 4); + seg->postprocess = NULL; + seg->width = width; + seg->height = height; + seg->segments = 3; // for many algorithms the number of presented segments will depend on depth + seg->threshold = 4; + for(int i = 0; i < seg->segments; i++) + seg->map[i] = dt_calloc_align_type(uint8_t, (size_t)width * height); + + const int r = depth+1; + const int limit = r * r + 1; + const float power = 0.4f + 0.025f * level; + + DT_OMP_FOR() + for(ssize_t row = 0; row < height; row++) + { + for(ssize_t col = 0; col < width; col++) + { + float pix = 0.0f; // count the pixels inside the circle + dt_aligned_pixel_t av = { 0.0f, 0.0f, 0.0f, 0.0f }; + for(int y = MAX(0, row-r); y < MIN(height, row+r+1); y++) + { + for(int x = MAX(0, col-r); x < MIN(width, col+r+1); x++) + { + const int dx = x - col; + const int dy = y - row; + if((dx*dx + dy*dy) <= limit) + { + for_each_channel(c) av[c] += rgb[(size_t)4*(y*width + x) + c]; + pix += 1.0f; + } + } + } + for_each_channel(c) av[c] /= pix; + + dt_aligned_pixel_t sv = { 0.0f, 0.0f, 0.0f, 0.0f }; + for(int y = MAX(0, row-r); y < MIN(height, row+r+1); y++) + { + for(int x = MAX(0, col-r); x < MIN(width, col+r+1); x++) + { + const int dx = x - col; + const int dy = y - row; + if((dx*dx + dy*dy) <= limit) + { + for_each_channel(c) sv[c] += sqrf(rgb[(size_t)4*(y*width + x) + c] - av[c]); + } + } + } + for_each_channel(c) sv[c] /= (pix - 1.0f); + + for_three_channels(c) + if(seg->map[c]) seg->map[c][row*width + col] = CLIP(3.0f * powf(sv[c], power)) * 255.0f; + } + } + dt_print(DT_DEBUG_PIPE, "%d variance segments %dx%d provided hash=%"PRIx64, seg->segments, seg->width, seg->height, seg->hash); + dt_control_log(_("%d variance segments %dx%d provided"), seg->segments, seg->width, seg->height); + dt_free_align(rgb); +} + +static float *_dev_get_segmentation_mask(dt_dev_pixelpipe_iop_t *piece) +{ + dt_iop_module_t *self = piece->module; + dt_iop_segmap_data_t *d = piece->data; + dt_iop_segmap_module_data_t *md = self->data; + const dt_iop_roi_t *roi = &piece->processed_roi_in; + const dt_iop_roi_t *roo = &piece->processed_roi_out; + + dt_segmentation_t *seg = md->segment; + if(!seg) return NULL; + + float *src = dt_iop_image_alloc(seg->width, seg->height, 1); + if(!src) return NULL; + + float *tmp = NULL; + float *res = NULL; + const uint8_t *cmap = d->id; + DT_OMP_FOR() + for(size_t k = 0; k < (size_t)seg->width * seg->height; k++) + { + uint8_t val = 0; + for(int c = 0; c < seg->segments; c++) + { + if(cmap[c] && seg->map[c]) val = MAX(val, seg->map[c][k]); + } + src[k] = (float)val / 255.0f; + } + + tmp = dt_iop_image_alloc(roi->width, roi->height, 1); + if(!tmp) goto final; + + interpolate_bilinear(src, seg->width, seg->height, tmp, roi->width, roi->height, 1); + + if(seg->postprocess) + seg->postprocess(tmp, roi->width, roi->height, d->depth, d->level); + else + _postprocess_default(tmp, roi->width, roi->height, d->depth, d->level); + + dt_free_align(src); + src = NULL; + + res = dt_iop_image_alloc(roo->width, roo->height, 1);; + if(res) self->distort_mask(self, piece, tmp, res, roi, roo); + +final: + dt_free_align(src); + dt_free_align(tmp); + return res; +} + +static inline void _clean_segment(dt_segmentation_t *seg) +{ + for(int s = 0; s < SEGMAP_MAXSEGMENTS; s++) + { + dt_free_align(seg->map[s]); + seg->map[s] = NULL; + } + seg->segments = seg->width = seg->height = seg->threshold = 0; + seg->postprocess = NULL; + seg->hash = DT_INVALID_HASH; +} + +#ifdef HAVE_OPENCL +int process_cl(dt_iop_module_t *self, + dt_dev_pixelpipe_iop_t *piece, + cl_mem dev_in, + cl_mem dev_out, + const dt_iop_roi_t *const roi_in, + const dt_iop_roi_t *const roi_out) +{ + dt_dev_pixelpipe_t *pipe = piece->pipe; + const ssize_t ch = pipe->dsc.filters ? 1 : 4; + const gboolean fullpipe = pipe->type & DT_DEV_PIXELPIPE_FULL; + dt_iop_segmap_data_t *d = piece->data; + const dt_hash_t hash = dt_hash(dt_dev_pixelpipe_piece_hash(piece, NULL, TRUE), &model_hash[d->model], sizeof(dt_hash_t)); + const gboolean visual = fullpipe && dt_iop_has_focus(self); + dt_iop_segmap_module_data_t *md = piece->module->data; + dt_segmentation_t *seg = md->segment; + + dt_pthread_mutex_lock(&seg->lock); + const gboolean bad_hash = hash != seg->hash; + dt_pthread_mutex_unlock(&seg->lock); + const int devid = pipe->devid; + cl_int err = DT_OPENCL_PROCESS_CL; + + if(visual || bad_hash) + { + dt_print_pipe(DT_DEBUG_PIPE, bad_hash ? "rastermap hash BAD" : "rastermap hash GOOD", + pipe, self, devid, NULL, NULL, "piece hash=%"PRIx64" seg hash=%"PRIx64" CPU%s fallback", + hash, seg->hash, visual ? "visualizing " : ""); + return err; + } + + if(roi_out->scale != roi_in->scale && ch == 4) + err = dt_iop_clip_and_zoom_roi_cl(devid, dev_out, dev_in, roi_out, roi_in); + else + { + size_t iorigin[] = { roi_out->x, roi_out->y, 0 }; + size_t oorigin[] = { 0, 0, 0 }; + size_t region[] = { roi_out->width, roi_out->height, 1 }; + err = dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, iorigin, oorigin, region); + } + + if(dt_iop_is_raster_mask_used(piece->module, BLEND_RASTER_ID)) + { + float *mask = _dev_get_segmentation_mask(piece); + dt_iop_piece_set_raster(piece, mask, roi_in, roi_out); + } + else + dt_iop_piece_clear_raster(piece, NULL); + + return err; +} +#endif + +void process(dt_iop_module_t *self, + dt_dev_pixelpipe_iop_t *piece, + const void *const ivoid, + void *const ovoid, + const dt_iop_roi_t *const roi_in, + const dt_iop_roi_t *const roi_out) +{ + const float *const in = (float *)ivoid; + float *const out = (float *)ovoid; + dt_dev_pixelpipe_t *pipe = piece->pipe; + const uint32_t filters = pipe->dsc.filters; + const ssize_t ch = filters ? 1 : 4; + + if(roi_out->scale != roi_in->scale && ch == 4) + { + const dt_interpolation_t *itor = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP); + dt_interpolation_resample(itor, out, roi_out, in, roi_in); + } + else + dt_iop_copy_image_roi(out, in, ch, roi_in, roi_out); + + dt_iop_segmap_data_t *d = piece->data; + const dt_iop_segmap_gui_data_t *g = self->gui_data; + const uint8_t(*const xtrans)[6] = (const uint8_t(*const)[6])pipe->dsc.xtrans; + const gboolean fullpipe = pipe->type & DT_DEV_PIXELPIPE_FULL; + const dt_hash_t hash = dt_hash(dt_dev_pixelpipe_piece_hash(piece, NULL, TRUE), &model_hash[d->model], sizeof(dt_hash_t)); + const gboolean is_xtrans = filters == 9u; + const gboolean is_bayer = !is_xtrans && filters != 0; + const gboolean request = dt_iop_is_raster_mask_used(piece->module, BLEND_RASTER_ID); + const gboolean visual = fullpipe && dt_iop_has_focus(self); + dt_iop_segmap_module_data_t *md = piece->module->data; + dt_segmentation_t *seg = md->segment; + + dt_pthread_mutex_lock(&seg->lock); + const gboolean bad_hash = hash != seg->hash; + dt_print_pipe(DT_DEBUG_PIPE, bad_hash ? "rastermap hash BAD" : "rastermap hash GOOD", + pipe, self, DT_DEVICE_NONE, NULL, NULL, "piece hash=%"PRIx64" seg hash=%"PRIx64, + hash, seg->hash); + if(bad_hash) dt_iop_piece_clear_raster(piece, NULL); + + const gboolean provider = bad_hash && (pipe->type & (DT_DEV_PIXELPIPE_FULL | DT_DEV_PIXELPIPE_EXPORT)); + float *tmp = provider || visual ? dt_iop_image_alloc(roi_in->width, roi_in->height, 4) : NULL; + + if(provider) + { + _clean_segment(seg); + if(tmp) + { + if(is_xtrans) + dt_iop_clip_and_zoom_demosaic_third_size_xtrans_f(tmp, in, roi_in, roi_in, roi_in->width, roi_in->width, xtrans); + else if(is_bayer) + dt_iop_clip_and_zoom_demosaic_half_size_f(tmp, in, roi_in, roi_in, roi_in->width, roi_in->width, filters); + + dt_iop_image_scaled_copy(tmp, filters ? tmp : in, 1.0f / dt_iop_get_processed_maximum(piece), roi_in->width, roi_in->height, 4); + + seg->hash = hash; + seg->model = d->model; + seg->postprocess = NULL; + /* This is where any segmentation takes place. We do this within a locked seg struct. + + We provide the RGB input data (float *tmp) normalized to 0->1, + it's dimension (roi_in->width/height) and a desired segmentation depth and level. + The meaning of depth and level depend on the model, + for segmentation algorithms lower values should lead to less segments and detail, + other tools might use it otherwise. + + All algorithms *must* provide and set + - the dimension of the sementation maps. + Please note that you might chose a different aspect and downscaled input data + for the algorithm performance. + - a uint8_t map for every generated segment with above dimension. + The selected combination of these maps is + - first bilinear scaled to a full image mask and then + - distorted by all modules module->distort_mask functions up to target module + as requested by a raster mask receiving module. + - the number of provided segments + - possibly a threshold value used when in visualizing mode + + An **optional** postprocess function might be provided to correct problems resulting from + the uin8_t maps or scaling. + */ + switch(seg->model) + { + + default: _variance_segment(tmp, seg, d->depth, d->level, roi_in); + } + if(!visual) dt_free_align(tmp); + } + else + { + dt_print(DT_DEBUG_ALWAYS, "can't provide segmentation because of low memory"); + dt_control_log(_("can't provide %d model segmentation because of low memory"), d->model); + } + } + + if(visual) + { + pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU; + const uint8_t *mouse_map = g->mouse_segment > NO_MOUSE_SEGMENT ? seg->map[g->mouse_segment] : NULL; + const uint8_t *cmap = d->id; + + const ssize_t owidth = roi_out->width; + const ssize_t oheight = roi_out->height; + const ssize_t iwidth = roi_in->width; + const ssize_t iheight = roi_in->height; + + if(ch == 1) + { + dt_iop_image_copy(tmp, out, owidth * oheight); + dt_box_mean(tmp, oheight, owidth, 1, 3, 2); // simple blur to remove CFA colors + DT_OMP_FOR(collapse(2)) + for(ssize_t row = 0; row < oheight; row++) + { + for(ssize_t col = 0; col < owidth; col++) + { + const ssize_t k = owidth * row + col; + const ssize_t irow = row + roi_out->y - roi_in->y; + const ssize_t icol = col + roi_out->x - roi_in->x; + if(irow < iheight && icol < iwidth && icol >= 0 && irow >= 0) + { + const ssize_t srow = irow * (ssize_t)seg->height / iheight; + const ssize_t scol = icol * (ssize_t)seg->width / iwidth; + const ssize_t sk = srow * (ssize_t)seg->width + scol; + + out[k] = 0.4f * CLAMPF(sqrtf(tmp[k]), 0.0f, 0.5f); + const int color = is_xtrans ? FCxtrans(irow, icol, roi_in, xtrans) : FC(irow, icol, filters); + /* 1. Brighten every location that has at least one segment + 2. If the mouse is over a segment, all segment locations are shown red + 3. The combination of all selected segments is shown green + Note1: As we might have segment mask data with a mask value below a threshold + those are not visualized & tested. + Note2: We might do better via a false-color map ? + */ + + for(int c = 0; c < seg->segments; c++) + { + if(seg->map[c] && seg->map[c][sk] > seg->threshold) + { + out[k] += 0.3f; + break; + } + } + + if(color == 0 && mouse_map && mouse_map[sk] > seg->threshold) + out[k] += 1.0f; + + if(color == 1) + { + for(int c = 0; c < seg->segments; c++) + { + if(cmap[c] && seg->map[c] && seg->map[c][sk] > seg->threshold) + { + out[k] += 1.0f; + break; + } + } + } + } + } + } + } + else // 4 channels + { + DT_OMP_FOR(collapse(2)) + for(ssize_t row = 0; row < iheight; row++) + { + for(ssize_t col = 0; col < iwidth; col++) + { + const ssize_t k = ch * (iwidth * row + col); + const ssize_t srow = row * (ssize_t)seg->height / iheight; + const ssize_t scol = col * (ssize_t)seg->width / iwidth; + const ssize_t sk = srow * (ssize_t)seg->width + scol; + + tmp[k] = tmp[k+1] = tmp[k+2] = 0.4f * CLAMPF(sqrtf(0.33f * (in[k] + in[k+1]+ in[k+2])), 0.0f, 0.5f); + for(int c = 0; c < seg->segments; c++) + { + if(seg->map[c] && seg->map[c][sk] > seg->threshold) + { + for_three_channels(m) tmp[k+m] += 0.3f; + break; + } + } + + if(mouse_map && mouse_map[sk] > seg->threshold) + tmp[k] += 1.0f; + + for(int c = 0; c < seg->segments; c++) + { + if(cmap[c] && seg->map[c] && seg->map[c][sk] > seg->threshold) + { + tmp[k+1] += 1.0f; + break; + } + } + } + } + if(roi_out->scale != roi_in->scale) + { + const dt_interpolation_t *itor = dt_interpolation_new(DT_INTERPOLATION_BILINEAR); + dt_interpolation_resample(itor, out, roi_out, tmp, roi_in); + } + else + dt_iop_copy_image_roi(out, tmp, ch, roi_in, roi_out); + } + dt_free_align(tmp); + } + else // we are not in UI mode so we must update the raster mask + { + if(request) + { + float *mask = _dev_get_segmentation_mask(piece); + dt_iop_piece_set_raster(piece, mask, roi_in, roi_out); + } + else + dt_iop_piece_clear_raster(piece, NULL); + + if(fullpipe && provider) dt_dev_reprocess_preview(self->dev); + } + dt_pthread_mutex_unlock(&seg->lock); +} + +void commit_params(dt_iop_module_t *self, + dt_iop_params_t *p1, + dt_dev_pixelpipe_t *pipe, + dt_dev_pixelpipe_iop_t *piece) +{ + const dt_iop_segmap_params_t *p = (dt_iop_segmap_params_t *)p1; + dt_iop_segmap_data_t *d = piece->data; + + d->depth = p->depth; + d->level = p->level; + d->model = p->model; + memcpy(d->id, p->id, sizeof(uint8_t) * SEGMAP_MAXSEGMENTS); +} + +void tiling_callback(dt_iop_module_t *self, + dt_dev_pixelpipe_iop_t *piece, + const dt_iop_roi_t *roi_in, + const dt_iop_roi_t *roi_out, + dt_develop_tiling_t *tiling) +{ + tiling->maxbuf = 1.0f; + tiling->xalign = 1; + tiling->yalign = 1; + tiling->overhead = 0; // following have to be according to the chosen algorithm + tiling->factor = 4.0f; +} + +void reload_defaults(dt_iop_module_t *self) +{ + // we might be called from presets update infrastructure => there is no image + if(!self->dev || !dt_is_valid_imgid(self->dev->image_storage.id)) return; + + self->default_enabled = FALSE; + dt_iop_segmap_params_t *d = self->default_params; + memset(d->id, 0, sizeof(uint8_t) * SEGMAP_MAXSEGMENTS); +} + +void distort_mask(dt_iop_module_t *self, + dt_dev_pixelpipe_iop_t *piece, + const float *const in, + float *const out, + const dt_iop_roi_t *const roi_in, + const dt_iop_roi_t *const roi_out) +{ + if(roi_out->scale != roi_in->scale) + { + const dt_interpolation_t *itor = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP); + dt_interpolation_resample_1c(itor, out, roi_out, in, roi_in); + } + else + dt_iop_copy_image_roi(out, in, 1, roi_in, roi_out); +} + +void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) +{ + dt_iop_segmap_gui_data_t *g = self->gui_data; + dt_iop_segmap_params_t *p = self->params; + g->mouse_segment = UNDEFINED_MOUSE_SEGMENT; + + if(!w || w == g->model) + { + gtk_widget_set_tooltip_text(g->depth, _depth_help(p->model)); + gtk_widget_set_visible(g->depth, model_depth[p->model]); + + gtk_widget_set_tooltip_text(g->level, _level_help(p->model)); + gtk_widget_set_visible(g->level, model_level[p->model]); + + gtk_widget_set_tooltip_text(g->model, _model_help(p->model)); + + if(g->fbutton) gtk_widget_set_visible(g->fbutton, model_fbutton[p->model]); + if(g->file) gtk_widget_set_visible(g->file, model_file[p->model]); + } + if(!w) + dt_dev_reprocess_center(self->dev); +} + +void gui_update(dt_iop_module_t *self) +{ + gui_changed(self, NULL, NULL); +} + +void init(dt_iop_module_t *self) +{ + dt_iop_default_init(self); + + dt_iop_segmap_params_t *d = self->default_params; + memset(d->id, 0, sizeof(uint8_t) * SEGMAP_MAXSEGMENTS); + + dt_iop_segmap_module_data_t *md = calloc(1, sizeof(dt_iop_segmap_module_data_t)); + dt_segmentation_t *seg = calloc(1, sizeof(dt_segmentation_t)); + seg->hash = DT_INVALID_HASH; + dt_pthread_mutex_init(&seg->lock, NULL); + md->segment = seg; + self->data = md; +} + +void cleanup(dt_iop_module_t *self) +{ + dt_iop_default_cleanup(self); + + dt_iop_segmap_module_data_t *md = self->data; + dt_segmentation_t *seg = md->segment; + _clean_segment(seg); + dt_pthread_mutex_destroy(&seg->lock); + free(seg); + free(md); + self->data = NULL; +} + +static void _mouse_update(dt_iop_module_t *self) +{ + dt_dev_invalidate(self->dev); + dt_control_queue_redraw_center(); +} + +static inline size_t _get_seg_k(dt_iop_module_t *self, + dt_segmentation_t *seg, + const float x, + const float y) +{ + dt_develop_t *dev = self->dev; + dt_dev_pixelpipe_t *fpipe = dev->full.pipe; + + /* slightly more complicated than usual as we calculate maps from data provided after rawprepare + and scale to dimensions after that module. + */ + const int rp_order = dt_ioppr_get_iop_order(self->dev->iop_order_list, "rawprepare", 0); + float pts[2]= { x * (float)fpipe->processed_width, y * (float)fpipe->processed_height }; + dt_dev_distort_backtransform_plus(dev, fpipe, rp_order, DT_DEV_TRANSFORM_DIR_FORW_EXCL, pts, 1); + + const size_t sx = roundf((float)seg->width * CLIP(pts[0] / (float)dev->image_storage.p_width)); + const size_t sy = roundf((float)seg->height * CLIP(pts[1] / (float)dev->image_storage.p_height)); + return sy * seg->width + sx; +} + +int mouse_leave(dt_iop_module_t *self) +{ + dt_iop_segmap_gui_data_t *g = self->gui_data; + g->mouse_segment = UNDEFINED_MOUSE_SEGMENT; + _mouse_update(self); + return TRUE; +} + +int mouse_moved(dt_iop_module_t *self, float x, float y, double pressure, int which, float zoom_scale) +{ + if(darktable.gui->reset) return FALSE; + dt_iop_segmap_gui_data_t *g = self->gui_data; + dt_iop_segmap_module_data_t *md = self->data; + dt_segmentation_t *seg = md->segment; + + if(g->down || !dt_iop_has_focus(self) || !seg || x < 0.0f || y < 0.0f || y > 1.0f || x > 1.0f) + return FALSE; + + gboolean other = g->mouse_segment == UNDEFINED_MOUSE_SEGMENT; + + dt_pthread_mutex_lock(&seg->lock); + const gboolean available = seg->segments > 0; + if(available) + { + int over = NO_MOUSE_SEGMENT; + const size_t k = _get_seg_k(self, seg, x, y); + for(int s = 0; s < seg->segments; s++) + { + if(seg->map[s] && seg->map[s][k] > seg->threshold) + { + over = s; + break; + } + } + + if(g->mouse_segment != over) + { + g->mouse_segment = over; + other = TRUE; + } + } + dt_pthread_mutex_unlock(&seg->lock); + + if(available && other) + _mouse_update(self); + return TRUE; +} + +int button_pressed(dt_iop_module_t *self, const float x, const float y, const double pressure, + const int which, const int type, const uint32_t state, const float zoom_scale) +{ + if(darktable.gui->reset) return FALSE; + dt_iop_segmap_gui_data_t *g = self->gui_data; + g->down = TRUE; + // keep track about double clicks + g->dclick = (type == GDK_DOUBLE_BUTTON_PRESS); + return TRUE; +} + +int button_released(dt_iop_module_t *self, float x, float y, int which, uint32_t state, float zoom_scale) +{ + if(darktable.gui->reset) return FALSE; + dt_iop_segmap_gui_data_t *g = self->gui_data; + dt_iop_segmap_params_t *p = self->params; + g->down = FALSE; + + // A double click while being in UI visualizing mode will unfocus to keep darkroom behaviour + if(g->dclick && which == GDK_BUTTON_PRIMARY && dt_iop_has_focus(self)) + { + dt_iop_request_focus(NULL); + return TRUE; + } + + dt_iop_segmap_module_data_t *md = self->data; + dt_segmentation_t *seg = md->segment; + + /* we only accept single left or right button clicks with shift or nothing mode + and make sure we have focus and valid positions + */ + if(!seg || g->dclick || !dt_iop_has_focus(self) || x < 0.0f || y < 0.0f || y > 1.0f || x > 1.0f) + return FALSE; + if(!(which == GDK_BUTTON_PRIMARY || which == GDK_BUTTON_SECONDARY)) + return FALSE; + if(state & ~GDK_SHIFT_MASK) + return FALSE; + + gboolean update = FALSE; + if(state == GDK_SHIFT_MASK) + { + memset(p->id, which == GDK_BUTTON_PRIMARY ? 1 : 0, sizeof(uint8_t) * SEGMAP_MAXSEGMENTS); + update = TRUE; + } + else + { + dt_pthread_mutex_lock(&seg->lock); + if(seg->segments > 0) + { + const size_t k = _get_seg_k(self, seg, x, y); + if(which == GDK_BUTTON_PRIMARY) // add first found *disabled* map to selection + { + for(int s = 0; s < seg->segments; s++) + { + if(p->id[s] == 0 && seg->map[s] && seg->map[s][k] > seg->threshold) + { + p->id[s] = 1; + update = TRUE; + break; + } + } + } + else // remove the first found *enabled* map from selection + { + for(int s = 0; s < seg->segments; s++) + { + if(p->id[s] == 1 && seg->map[s] && seg->map[s][k] > seg->threshold) + { + p->id[s] = 0; + update = TRUE; + break; + } + } + } + } + dt_pthread_mutex_unlock(&seg->lock); + } + + if(update) + dt_dev_add_history_item(darktable.develop, self, FALSE); + return TRUE; +} + +void gui_focus(dt_iop_module_t *self, gboolean in) +{ + dt_iop_segmap_gui_data_t *g = self->gui_data; + g->mouse_segment = UNDEFINED_MOUSE_SEGMENT; + dt_dev_reprocess_center(self->dev); +} + +void gui_init(dt_iop_module_t *self) +{ + dt_iop_segmap_gui_data_t *g = IOP_GUI_ALLOC(segmap); + + self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_BAUHAUS_SPACE); + + g->model = dt_bauhaus_combobox_from_params(self, "model"); + g->depth = dt_bauhaus_slider_from_params(self, "depth"); + g->level = dt_bauhaus_slider_from_params(self, "level"); +} + +#undef UNDEFINED_MOUSE_SEGMENT +#undef NO_MOUSE_SEGMENT +#undef SEGMAP_MAXSEGMENTS + +// clang-format off +// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py +// vim: shiftwidth=2 expandtab tabstop=2 cindent +// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; +// clang-format on diff --git a/src/libs/modulegroups.c b/src/libs/modulegroups.c index c0af9af8c84e..892d4a95fa7f 100644 --- a/src/libs/modulegroups.c +++ b/src/libs/modulegroups.c @@ -1604,6 +1604,7 @@ void init_presets(dt_lib_module_t *self) AM("lens"); AM("liquify"); AM("nlmeans"); + AM("rastermaps"); AM("rawdenoise"); AM("retouch"); AM("rotatepixels"); @@ -1705,6 +1706,7 @@ void init_presets(dt_lib_module_t *self) AM("lens"); AM("retouch"); AM("liquify"); + AM("rastermaps"); AM("sharpen"); AM("nlmeans"); @@ -1756,6 +1758,7 @@ void init_presets(dt_lib_module_t *self) AM("lens"); AM("retouch"); AM("liquify"); + AM("rastermaps"); AM("sharpen"); AM("nlmeans");