/* This file is part of darktable, Copyright (C) 2014-2026 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 . */ #include "bauhaus/bauhaus.h" #include "common/interpolation.h" #include "common/opencl.h" #include "develop/imageop.h" #include "develop/imageop_gui.h" #include "develop/tiling.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "iop/iop_api.h" #include #include DT_MODULE_INTROSPECTION(1, dt_iop_scalepixels_params_t) typedef struct dt_iop_scalepixels_params_t { // Aspect ratio of the pixels, usually 1 but some cameras need scaling // <1 means the image needs to be stretched vertically, (0.5 means 2x) // >1 means the image needs to be stretched horizontally (2 mean 2x) float pixel_aspect_ratio; // $MIN: 0.5f $MAX: 2.0f $DEFAULT: 1.0f $DESCRIPTION: "pixel aspect ratio" } dt_iop_scalepixels_params_t; typedef struct dt_iop_scalepixels_gui_data_t { } dt_iop_scalepixels_gui_data_t; typedef struct dt_iop_scalepixels_data_t { float pixel_aspect_ratio; float x_scale; float y_scale; } dt_iop_scalepixels_data_t; typedef struct dt_iop_scalepixels_global_data_t { int kernel_scalepixels; } dt_iop_scalepixels_global_data_t; const char *name() { return C_("modulename", "scale pixels"); } int flags() { return IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_TILING_FULL_ROI | IOP_FLAGS_ONE_INSTANCE | IOP_FLAGS_UNSAFE_COPY; } int default_group() { return IOP_GROUP_CORRECT | IOP_GROUP_TECHNICAL; } int operation_tags() { return IOP_TAG_DISTORT; } dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { return IOP_CS_RGB; } const char **description(dt_iop_module_t *self) { return dt_iop_set_description(self, _("module for setting pixel aspect ratio\n\n" "useful for certain sensor types\n" "and anamorphic desqueeze"), _("corrective"), _("linear, RGB, scene-referred"), _("linear, RGB"), _("linear, RGB, scene-referred")); } static void _transform(const dt_dev_pixelpipe_iop_t *const piece, float *p) { dt_iop_scalepixels_data_t *d = piece->data; if(d->pixel_aspect_ratio < 1.0f) { p[1] /= d->pixel_aspect_ratio; } else { p[0] *= d->pixel_aspect_ratio; } } static void _backtransform(const dt_dev_pixelpipe_iop_t *const piece, float *p) { dt_iop_scalepixels_data_t *d = piece->data; if(d->pixel_aspect_ratio < 1.0f) { p[1] *= d->pixel_aspect_ratio; } else { p[0] /= d->pixel_aspect_ratio; } } // map like modify_roi_out(): d->x_scale and d->y_scale are relative // to the upscaled input requested by modify_roi_in() gboolean distort_transform(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, float *points, size_t points_count) { for(size_t i = 0; i < points_count * 2; i += 2) _transform(piece, points + i); return TRUE; } gboolean distort_backtransform(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, float *points, size_t points_count) { for(size_t i = 0; i < points_count * 2; i += 2) _backtransform(piece, points + i); return TRUE; } 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) { const dt_interpolation_t *iter = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP); const int iw = roi_in->width; const int ih = roi_in->height; const size_t ow = roi_out->width; // Compute scale factors locally from the actual roi_in/roi_out to ensure // consistency, rather than relying on d->x_scale/d->y_scale which may have // been overwritten by modify_roi_in() with different ROI dimensions. const float x_scale = (roi_in->width * 1.0f) / (roi_out->width * 1.0f); const float y_scale = (roi_in->height * 1.0f) / (roi_out->height * 1.0f); DT_OMP_FOR(collapse(2)) for(int row = 0; row < roi_out->height; row++) { for(int col = 0; col < roi_out->width; col++) { const float x = col * x_scale; const float y = row * y_scale; out[ow * row + col] = CLIP(dt_interpolation_compute_sample(iter, in, x, y, iw, ih, 1, iw)); } } } void modify_roi_out(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, dt_iop_roi_t *roi_out, const dt_iop_roi_t *const roi_in) { *roi_out = *roi_in; float xy[2] = { roi_out->x, roi_out->y }; float wh[2] = { roi_out->width, roi_out->height }; _transform(piece, xy); _transform(piece, wh); roi_out->x = (int)floorf(xy[0]); roi_out->y = (int)floorf(xy[1]); roi_out->width = (int)ceilf(wh[0]); roi_out->height = (int)ceilf(wh[1]); // sanity check. if(roi_out->x < 0) roi_out->x = 0; if(roi_out->y < 0) roi_out->y = 0; if(roi_out->width < 1) roi_out->width = 1; if(roi_out->height < 1) roi_out->height = 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; // If possible try to get an image that's strictly larger than what we want to output float hw[2] = {roi_out->height, roi_out->width}; _transform(piece, hw); // _transform() is used reversed here intentionally roi_in->height = hw[0]; roi_in->width = hw[1]; dt_iop_scalepixels_data_t *d = piece->data; d->x_scale = (roi_in->width * 1.0f) / (roi_out->width * 1.0f); d->y_scale = (roi_in->height * 1.0f) / (roi_out->height * 1.0f); roi_in->scale = roi_out->scale * MAX(d->x_scale, d->y_scale); roi_in->x = roi_out->x * d->x_scale; roi_in->y = roi_out->y * d->y_scale; // a tile at the far edge of the whole roi gets the input up to its far // edge: the rounding above can leave it one input row or column short, and // the last output row or column would then mirror the border too early const dt_iop_roi_t *const full_in = &piece->processed_roi_in; const dt_iop_roi_t *const full_out = &piece->processed_roi_out; if(piece->pipe->tiling && full_out->width > 0 && full_in->width > 0) { if(roi_out->x + roi_out->width >= full_out->x + full_out->width) roi_in->width = MAX(roi_in->width, full_in->x + full_in->width - roi_in->x); if(roi_out->y + roi_out->height >= full_out->y + full_out->height) roi_in->height = MAX(roi_in->height, full_in->y + full_in->height - roi_in->y); } } // output pixel (i, j) reads the input at ((i, j) + shift) * scale - offset, on // the grid of the whole roi: a tile's own rois have slightly different ratios, // which would shift each tile. Untiled, shift and offset are 0 static void _sampling_grid(const dt_dev_pixelpipe_iop_t *const piece, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out, float scale[2], int shift[2], float offset[2]) { const gboolean whole = piece->processed_roi_out.width > 0 && piece->processed_roi_in.width > 0; const dt_iop_roi_t *const full_in = whole ? &piece->processed_roi_in : roi_in; const dt_iop_roi_t *const full_out = whole ? &piece->processed_roi_out : roi_out; scale[0] = (full_in->width * 1.0f) / (full_out->width * 1.0f); scale[1] = (full_in->height * 1.0f) / (full_out->height * 1.0f); shift[0] = roi_out->x - full_out->x; shift[1] = roi_out->y - full_out->y; offset[0] = roi_in->x - full_in->x; offset[1] = roi_in->y - full_in->y; } 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 int ch = piece->colors; const int ch_width = ch * roi_in->width; const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF); float scale[2], offset[2]; int shift[2]; _sampling_grid(piece, roi_in, roi_out, scale, shift, offset); DT_OMP_FOR() // (slow) point-by-point transformation. // TODO: optimize with scanlines and linear steps between? for(int j = 0; j < roi_out->height; j++) { float *out = ((float *)ovoid) + (size_t)4 * j * roi_out->width; for(int i = 0; i < roi_out->width; i++, out += 4) { const float x = (i + shift[0]) * scale[0] - offset[0]; const float y = (j + shift[1]) * scale[1] - offset[1]; dt_interpolation_compute_pixel4c(interpolation, (float *)ivoid, out, x, y, roi_in->width, roi_in->height, ch_width); } } } #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) { const dt_iop_scalepixels_global_data_t *gd = self->global_data; const int devid = piece->pipe->devid; const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF); float scale[2], offset[2]; int shift[2]; _sampling_grid(piece, roi_in, roi_out, scale, shift, offset); const int itype = interpolation->id; const int iwidth = interpolation->width; return dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_scalepixels, roi_out->width, roi_out->height, CLARG(dev_in), CLARG(dev_out), CLARG(roi_out->width), CLARG(roi_out->height), CLARG(roi_in->width), CLARG(roi_in->height), CLARG(scale), CLARG(shift), CLARG(offset), CLARG(itype), CLARG(iwidth)); } #endif void init_global(dt_iop_module_so_t *self) { const int program = 2; // basic.cl, from programs.conf dt_iop_scalepixels_global_data_t *gd = malloc(sizeof(dt_iop_scalepixels_global_data_t)); self->data = gd; gd->kernel_scalepixels = dt_opencl_create_kernel(program, "scalepixels"); } void cleanup_global(dt_iop_module_so_t *self) { dt_iop_scalepixels_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_scalepixels); free(self->data); self->data = NULL; } void commit_params(dt_iop_module_t *self, dt_iop_params_t *params, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { const dt_iop_scalepixels_params_t *p = params; dt_iop_scalepixels_data_t *d = piece->data; d->pixel_aspect_ratio = p->pixel_aspect_ratio; d->x_scale = 1.0f; d->y_scale = 1.0f; if(dt_isnan(p->pixel_aspect_ratio) || p->pixel_aspect_ratio <= 0.0f || p->pixel_aspect_ratio == 1.0f) piece->enabled = FALSE; } void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { piece->data = calloc(1, sizeof(dt_iop_scalepixels_data_t)); } void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { free(piece->data); piece->data = NULL; } void gui_init(dt_iop_module_t *self) { GtkWidget *w = dt_bauhaus_slider_from_params(self, "pixel_aspect_ratio"); gtk_widget_set_tooltip_text(w, _("adjust pixel aspect ratio")); } // 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