/* -*- C++ -*- * * This file is part of RawTherapee. * * Copyright (c) 2019 Alberto Griggio * * RawTherapee 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. * * RawTherapee 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 RawTherapee. If not, see . */ using namespace std; // taken from darktable (src/iop/ashift.c) /* This file is part of darktable, copyright (c) 2016 Ulrich Pegelow. 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 . */ // Inspiration to this module comes from the program ShiftN (http://www.shiftn.de) by // Marcus Hebel. // Thanks to Marcus for his support when implementing part of the ShiftN functionality // to darktable. #define ROTATION_RANGE 10 // allowed min/max default range for rotation parameter #define ROTATION_RANGE_SOFT 20 // allowed min/max range for rotation parameter with manual adjustment #define LENSSHIFT_RANGE 0.5 // allowed min/max default range for lensshift parameters #define LENSSHIFT_RANGE_SOFT 1 // allowed min/max range for lensshift parameters with manual adjustment #define SHEAR_RANGE 0.2 // allowed min/max range for shear parameter #define SHEAR_RANGE_SOFT 0.5 // allowed min/max range for shear parameter with manual adjustment #define CAMERA_ANGLE_RANGE_SOFT 80 #define MIN_LINE_LENGTH 5 // the minimum length of a line in pixels to be regarded as relevant #define MAX_TANGENTIAL_DEVIATION 30 // by how many degrees a line may deviate from the +/-180 and +/-90 to be regarded as relevant #define LSD_SCALE 0.99 // LSD: scaling factor for line detection #define LSD_SIGMA_SCALE 0.6 // LSD: sigma for Gaussian filter is computed as sigma = sigma_scale/scale #define LSD_QUANT 2.0 // LSD: bound to the quantization error on the gradient norm #define LSD_ANG_TH 22.5 // LSD: gradient angle tolerance in degrees #define LSD_LOG_EPS 0.0 // LSD: detection threshold: -log10(NFA) > log_eps #define LSD_DENSITY_TH 0.7 // LSD: minimal density of region points in rectangle #define LSD_N_BINS 1024 // LSD: number of bins in pseudo-ordering of gradient modulus #define LSD_GAMMA 0.45 // gamma correction to apply on raw images prior to line detection #define RANSAC_RUNS 400 // how many iterations to run in ransac #define RANSAC_EPSILON 2 // starting value for ransac epsilon (in -log10 units) #define RANSAC_EPSILON_STEP 1 // step size of epsilon optimization (log10 units) #define RANSAC_ELIMINATION_RATIO 60 // percentage of lines we try to eliminate as outliers #define RANSAC_OPTIMIZATION_STEPS 5 // home many steps to optimize epsilon #define RANSAC_OPTIMIZATION_DRY_RUNS 50 // how man runs per optimization steps #define RANSAC_HURDLE 5 // hurdle rate: the number of lines below which we do a complete permutation instead of random sampling #define MINIMUM_FITLINES 4 // minimum number of lines needed for automatic parameter fit #define NMS_EPSILON 1e-3 // break criterion for Nelder-Mead simplex #define NMS_SCALE 1.0 // scaling factor for Nelder-Mead simplex #define NMS_ITERATIONS 400 // number of iterations for Nelder-Mead simplex #define NMS_CROP_EPSILON 100.0 // break criterion for Nelder-Mead simplex on crop fitting #define NMS_CROP_SCALE 0.5 // scaling factor for Nelder-Mead simplex on crop fitting #define NMS_CROP_ITERATIONS 100 // number of iterations for Nelder-Mead simplex on crop fitting #define NMS_ALPHA 1.0 // reflection coefficient for Nelder-Mead simplex #define NMS_BETA 0.5 // contraction coefficient for Nelder-Mead simplex #define NMS_GAMMA 2.0 // expansion coefficient for Nelder-Mead simplex #define DEFAULT_F_LENGTH 28.0 // focal length we assume if no exif data are available /* // define to get debugging output */ /* #undef ASHIFT_DEBUG */ #define SQR(a) ((a) * (a)) // For line detection we use the LSD algorithm as published by Rafael Grompone: // // "LSD: a Line Segment Detector" by Rafael Grompone von Gioi, // Jeremie Jakubowicz, Jean-Michel Morel, and Gregory Randall, // Image Processing On Line, 2012. DOI:10.5201/ipol.2012.gjmr-lsd // http://dx.doi.org/10.5201/ipol.2012.gjmr-lsd #include "ashift_lsd.c" // For parameter optimization we are using the Nelder-Mead simplex method // implemented by Michael F. Hutt. #include "ashift_nmsimplex.c" #include "homogeneouscoordinates.h" //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #ifdef near # undef near #endif #if 0 DT_MODULE_INTROSPECTION(4, dt_iop_ashift_params_t) const char *name() { return _("perspective correction"); } int flags() { return IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_TILING_FULL_ROI | IOP_FLAGS_ONE_INSTANCE; } int groups() { return dt_iop_get_group("perspective correction", IOP_GROUP_CORRECT); } int operation_tags() { return IOP_TAG_DISTORT; } int operation_tags_filter() { // switch off clipping and decoration, we want to see the full image. return IOP_TAG_DECORATION | IOP_TAG_CLIPPING; } #endif // if 0 //----------------------------------------------------------------------------- typedef enum dt_iop_ashift_homodir_t { ASHIFT_HOMOGRAPH_FORWARD, ASHIFT_HOMOGRAPH_INVERTED } dt_iop_ashift_homodir_t; //typedef enum dt_iop_ashift_linetype_t enum { ASHIFT_LINE_IRRELEVANT = 0, // the line is found to be not interesting // eg. too short, or not horizontal or vertical ASHIFT_LINE_RELEVANT = 1 << 0, // the line is relevant for us ASHIFT_LINE_DIRVERT = 1 << 1, // the line is (mostly) vertical, else (mostly) horizontal ASHIFT_LINE_SELECTED = 1 << 2, // the line is selected for fitting ASHIFT_LINE_VERTICAL_NOT_SELECTED = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_DIRVERT, ASHIFT_LINE_HORIZONTAL_NOT_SELECTED = ASHIFT_LINE_RELEVANT, ASHIFT_LINE_VERTICAL_SELECTED = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_DIRVERT | ASHIFT_LINE_SELECTED, ASHIFT_LINE_HORIZONTAL_SELECTED = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED, ASHIFT_LINE_MASK = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_DIRVERT | ASHIFT_LINE_SELECTED }; //dt_iop_ashift_linetype_t; typedef unsigned int dt_iop_ashift_linetype_t; typedef enum dt_iop_ashift_linecolor_t { ASHIFT_LINECOLOR_GREY = 0, ASHIFT_LINECOLOR_GREEN = 1, ASHIFT_LINECOLOR_RED = 2, ASHIFT_LINECOLOR_BLUE = 3, ASHIFT_LINECOLOR_YELLOW = 4 } dt_iop_ashift_linecolor_t; //typedef enum dt_iop_ashift_fitaxis_t enum { ASHIFT_FIT_NONE = 0, // none ASHIFT_FIT_ROTATION = 1 << 0, // flag indicates to fit rotation angle ASHIFT_FIT_LENS_VERT = 1 << 1, // flag indicates to fit vertical lens shift ASHIFT_FIT_LENS_HOR = 1 << 2, // flag indicates to fit horizontal lens shift ASHIFT_FIT_SHEAR = 1 << 3, // flag indicates to fit shear parameter ASHIFT_FIT_LINES_VERT = 1 << 4, // use vertical lines for fitting ASHIFT_FIT_LINES_HOR = 1 << 5, // use horizontal lines for fitting ASHIFT_FIT_LENS_BOTH = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR, ASHIFT_FIT_LINES_BOTH = ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR, ASHIFT_FIT_VERTICALLY = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LINES_VERT, ASHIFT_FIT_HORIZONTALLY = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_HOR, ASHIFT_FIT_BOTH = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR, ASHIFT_FIT_VERTICALLY_NO_ROTATION = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LINES_VERT, ASHIFT_FIT_HORIZONTALLY_NO_ROTATION = ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_HOR, ASHIFT_FIT_BOTH_NO_ROTATION = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR, ASHIFT_FIT_BOTH_SHEAR = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_SHEAR | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR, ASHIFT_FIT_ROTATION_VERTICAL_LINES = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LINES_VERT, ASHIFT_FIT_ROTATION_HORIZONTAL_LINES = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LINES_HOR, ASHIFT_FIT_ROTATION_BOTH_LINES = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR, ASHIFT_FIT_FLIP = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR }; //dt_iop_ashift_fitaxis_t; typedef unsigned int dt_iop_ashift_fitaxis_t; typedef enum dt_iop_ashift_nmsresult_t { NMS_SUCCESS = 0, NMS_NOT_ENOUGH_LINES = 1, NMS_DID_NOT_CONVERGE = 2, NMS_INSANE = 3 } dt_iop_ashift_nmsresult_t; typedef enum dt_iop_ashift_enhance_t { ASHIFT_ENHANCE_NONE = 0, ASHIFT_ENHANCE_EDGES = 1 << 0, ASHIFT_ENHANCE_DETAIL = 1 << 1, ASHIFT_ENHANCE_HORIZONTAL = 0x100, ASHIFT_ENHANCE_VERTICAL = 0x200 } dt_iop_ashift_enhance_t; typedef enum dt_iop_ashift_mode_t { ASHIFT_MODE_GENERIC = 0, ASHIFT_MODE_SPECIFIC = 1 } dt_iop_ashift_mode_t; typedef enum dt_iop_ashift_crop_t { ASHIFT_CROP_OFF = 0, ASHIFT_CROP_LARGEST = 1, ASHIFT_CROP_ASPECT = 2 } dt_iop_ashift_crop_t; typedef enum dt_iop_ashift_bounding_t { ASHIFT_BOUNDING_OFF = 0, ASHIFT_BOUNDING_SELECT = 1, ASHIFT_BOUNDING_DESELECT = 2 } dt_iop_ashift_bounding_t; typedef enum dt_iop_ashift_jobcode_t { ASHIFT_JOBCODE_NONE = 0, ASHIFT_JOBCODE_GET_STRUCTURE = 1, ASHIFT_JOBCODE_FIT = 2 } dt_iop_ashift_jobcode_t; typedef struct dt_iop_ashift_params1_t { float rotation; float lensshift_v; float lensshift_h; int toggle; } dt_iop_ashift_params1_t; typedef struct dt_iop_ashift_params2_t { float rotation; float lensshift_v; float lensshift_h; float f_length; float crop_factor; float orthocorr; float aspect; dt_iop_ashift_mode_t mode; int toggle; } dt_iop_ashift_params2_t; typedef struct dt_iop_ashift_params3_t { float rotation; float lensshift_v; float lensshift_h; float f_length; float crop_factor; float orthocorr; float aspect; dt_iop_ashift_mode_t mode; int toggle; dt_iop_ashift_crop_t cropmode; float cl; float cr; float ct; float cb; } dt_iop_ashift_params3_t; typedef struct dt_iop_ashift_params_t { float rotation; float lensshift_v; float lensshift_h; float shear; float f_length; float crop_factor; float orthocorr; float aspect; dt_iop_ashift_mode_t mode; int toggle; dt_iop_ashift_crop_t cropmode; float cl; float cr; float ct; float cb; float camera_pitch; float camera_yaw; } dt_iop_ashift_params_t; typedef struct dt_iop_ashift_line_t { float p1[3]; float p2[3]; float length; float width; float weight; dt_iop_ashift_linetype_t type; // homogeneous coordinates: float L[3]; } dt_iop_ashift_line_t; typedef struct dt_iop_ashift_points_idx_t { size_t offset; int length; int near; int bounded; dt_iop_ashift_linetype_t type; dt_iop_ashift_linecolor_t color; // bounding box: float bbx, bby, bbX, bbY; } dt_iop_ashift_points_idx_t; typedef struct dt_iop_ashift_fit_params_t { int params_count; dt_iop_ashift_linetype_t linetype; dt_iop_ashift_linetype_t linemask; dt_iop_ashift_line_t *lines; int lines_count; int width; int height; float weight; float f_length_kb; float orthocorr; float aspect; float rotation; float lensshift_v; float lensshift_h; float shear; float camera_pitch; float camera_yaw; float rotation_range; float lensshift_v_range; float lensshift_h_range; float shear_range; float camera_pitch_range; float camera_yaw_range; } dt_iop_ashift_fit_params_t; typedef struct dt_iop_ashift_cropfit_params_t { int width; int height; float x; float y; float alpha; float homograph[3][3]; float edges[4][3]; } dt_iop_ashift_cropfit_params_t; typedef struct dt_iop_ashift_gui_data_t { /* GtkWidget *rotation; */ /* GtkWidget *lensshift_v; */ /* GtkWidget *lensshift_h; */ /* GtkWidget *shear; */ /* GtkWidget *guide_lines; */ /* GtkWidget *cropmode; */ /* GtkWidget *mode; */ /* GtkWidget *f_length; */ /* GtkWidget *crop_factor; */ /* GtkWidget *orthocorr; */ /* GtkWidget *aspect; */ /* GtkWidget *fit_v; */ /* GtkWidget *fit_h; */ /* GtkWidget *fit_both; */ /* GtkWidget *structure; */ /* GtkWidget *clean; */ /* GtkWidget *eye; */ int lines_suppressed; int fitting; int isflipped; int show_guides; int isselecting; int isdeselecting; dt_iop_ashift_bounding_t isbounding; float near_delta; int selecting_lines_version; float rotation_range; float lensshift_v_range; float lensshift_h_range; float shear_range; float camera_pitch_range; float camera_yaw_range; dt_iop_ashift_line_t *lines; int lines_in_width; int lines_in_height; int lines_x_off; int lines_y_off; int lines_count; int vertical_count; int horizontal_count; int lines_version; float vertical_weight; float horizontal_weight; float *points; dt_iop_ashift_points_idx_t *points_idx; int points_lines_count; int points_version; float *buf; int buf_width; int buf_height; int buf_x_off; int buf_y_off; float buf_scale; uint64_t lines_hash; uint64_t grid_hash; uint64_t buf_hash; dt_iop_ashift_fitaxis_t lastfit; float lastx; float lasty; float crop_cx; float crop_cy; dt_iop_ashift_jobcode_t jobcode; int jobparams; /* dt_pthread_mutex_t lock; */ MyMutex lock; gboolean adjust_crop; } dt_iop_ashift_gui_data_t; typedef struct dt_iop_ashift_data_t { float rotation; float lensshift_v; float lensshift_h; float shear; float f_length_kb; float orthocorr; float aspect; float cl; float cr; float ct; float cb; } dt_iop_ashift_data_t; typedef struct dt_iop_ashift_global_data_t { int kernel_ashift_bilinear; int kernel_ashift_bicubic; int kernel_ashift_lanczos2; int kernel_ashift_lanczos3; } dt_iop_ashift_global_data_t; typedef struct dt_iop_module_t { dt_iop_ashift_gui_data_t *gui_data; int is_raw; } dt_iop_module_t; //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void *new_params, const int new_version) { if(old_version == 1 && new_version == 4) { const dt_iop_ashift_params1_t *old = old_params; dt_iop_ashift_params_t *new = new_params; new->rotation = old->rotation; new->lensshift_v = old->lensshift_v; new->lensshift_h = old->lensshift_h; new->shear = 0.0f; new->toggle = old->toggle; new->f_length = DEFAULT_F_LENGTH; new->crop_factor = 1.0f; new->orthocorr = 100.0f; new->aspect = 1.0f; new->mode = ASHIFT_MODE_GENERIC; new->cropmode = ASHIFT_CROP_OFF; new->cl = 0.0f; new->cr = 1.0f; new->ct = 0.0f; new->cb = 1.0f; return 0; } if(old_version == 2 && new_version == 4) { const dt_iop_ashift_params2_t *old = old_params; dt_iop_ashift_params_t *new = new_params; new->rotation = old->rotation; new->lensshift_v = old->lensshift_v; new->lensshift_h = old->lensshift_h; new->shear = 0.0f; new->toggle = old->toggle; new->f_length = old->f_length; new->crop_factor = old->crop_factor; new->orthocorr = old->orthocorr; new->aspect = old->aspect; new->mode = old->mode; new->cropmode = ASHIFT_CROP_OFF; new->cl = 0.0f; new->cr = 1.0f; new->ct = 0.0f; new->cb = 1.0f; return 0; } if(old_version == 3 && new_version == 4) { const dt_iop_ashift_params3_t *old = old_params; dt_iop_ashift_params_t *new = new_params; new->rotation = old->rotation; new->lensshift_v = old->lensshift_v; new->lensshift_h = old->lensshift_h; new->shear = 0.0f; new->toggle = old->toggle; new->f_length = old->f_length; new->crop_factor = old->crop_factor; new->orthocorr = old->orthocorr; new->aspect = old->aspect; new->mode = old->mode; new->cropmode = old->cropmode; new->cl = old->cl; new->cr = old->cr; new->ct = old->ct; new->cb = old->cb; return 0; } return 1; } void init_key_accels(dt_iop_module_so_t *self) { dt_accel_register_slider_iop(self, FALSE, NC_("accel", "rotation")); dt_accel_register_slider_iop(self, FALSE, NC_("accel", "lens shift (v)")); dt_accel_register_slider_iop(self, FALSE, NC_("accel", "lens shift (h)")); dt_accel_register_slider_iop(self, FALSE, NC_("accel", "shear")); } void connect_key_accels(dt_iop_module_t *self) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; dt_accel_connect_slider_iop(self, "rotation", GTK_WIDGET(g->rotation)); dt_accel_connect_slider_iop(self, "lens shift (v)", GTK_WIDGET(g->lensshift_v)); dt_accel_connect_slider_iop(self, "lens shift (h)", GTK_WIDGET(g->lensshift_h)); dt_accel_connect_slider_iop(self, "shear", GTK_WIDGET(g->shear)); } #endif // if 0 //----------------------------------------------------------------------------- // multiply 3x3 matrix with 3x1 vector // dst needs to be different from v static inline void mat3mulv(float *dst, const float *const mat, const float *const v) { for(int k = 0; k < 3; k++) { float x = 0.0f; for(int i = 0; i < 3; i++) x += mat[3 * k + i] * v[i]; dst[k] = x; } } // multiply two 3x3 matrices // dst needs to be different from m1 and m2 static inline void mat3mul(float *dst, const float *const m1, const float *const m2) { for(int k = 0; k < 3; k++) { for(int i = 0; i < 3; i++) { float x = 0.0f; for(int j = 0; j < 3; j++) x += m1[3 * k + j] * m2[3 * j + i]; dst[3 * k + i] = x; } } } // normalized product of two 3x1 vectors // dst needs to be different from v1 and v2 static inline void vec3prodn(float *dst, const float *const v1, const float *const v2) { const float l1 = v1[1] * v2[2] - v1[2] * v2[1]; const float l2 = v1[2] * v2[0] - v1[0] * v2[2]; const float l3 = v1[0] * v2[1] - v1[1] * v2[0]; // normalize so that l1^2 + l2^2 + l3^3 = 1 const float sq = sqrt(l1 * l1 + l2 * l2 + l3 * l3); const float f = sq > 0.0f ? 1.0f / sq : 1.0f; dst[0] = l1 * f; dst[1] = l2 * f; dst[2] = l3 * f; } // normalize a 3x1 vector so that x^2 + y^2 + z^2 = 1 // dst and v may be the same static inline void vec3norm(float *dst, const float *const v) { const float sq = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); // special handling for an all-zero vector const float f = sq > 0.0f ? 1.0f / sq : 1.0f; dst[0] = v[0] * f; dst[1] = v[1] * f; dst[2] = v[2] * f; } // normalize a 3x1 vector so that x^2 + y^2 = 1; a useful normalization for // lines in homogeneous coordinates // dst and v may be the same static inline void vec3lnorm(float *dst, const float *const v) { const float sq = sqrt(v[0] * v[0] + v[1] * v[1]); // special handling for a point vector of the image center const float f = sq > 0.0f ? 1.0f / sq : 1.0f; dst[0] = v[0] * f; dst[1] = v[1] * f; dst[2] = v[2] * f; } // scalar product of two 3x1 vectors static inline float vec3scalar(const float *const v1, const float *const v2) { return (v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]); } // check if 3x1 vector is (very close to) null static inline int vec3isnull(const float *const v) { const float eps = 1e-10f; return (fabs(v[0]) < eps && fabs(v[1]) < eps && fabs(v[2]) < eps); } #ifdef ASHIFT_DEBUG static void print_roi(const dt_iop_roi_t *roi, const char *label) { printf("{ %5d %5d %5d %5d %.6f } %s\n", roi->x, roi->y, roi->width, roi->height, roi->scale, label); } #endif #define MAT3SWAP(a, b) { float (*tmp)[3] = (a); (a) = (b); (b) = tmp; } /* static void homography(float *homograph, const float angle, const float shift_v, const float shift_h, const float shear, const float f_length_kb, const float orthocorr, const float aspect, const int width, const int height, dt_iop_ashift_homodir_t dir) */ static void homography(float *homograph, const float angle, const float shift_v, const float shift_h, const float shear, const float camera_pitch, const float camera_yaw, const float f_length_kb, const float orthocorr, const float aspect, const int width, const int height, dt_iop_ashift_homodir_t dir) { // calculate homograph that combines all translations, rotations // and warping into one single matrix operation. // this is heavily leaning on ShiftN where the homographic matrix expects // input in (y : x : 1) format. in the darktable world we want to keep the // (x : y : 1) convention. therefore we need to flip coordinates first and // make sure that output is in correct format after corrections are applied. const float u = width; const float v = height; const float rot = -M_PI * angle / 180.0f; const float pitch = M_PI * camera_pitch / 180.0f; const float yaw = M_PI * camera_yaw / 180.0f; /* const float phi = M_PI * angle / 180.0f; const float cosi = cos(phi); const float sini = sin(phi); const float ascale = sqrt(aspect); // most of this comes from ShiftN const float f_global = f_length_kb; const float horifac = 1.0f - orthocorr / 100.0f; const float exppa_v = exp(shift_v); const float fdb_v = f_global / (14.4f + (v / u - 1) * 7.2f); const float rad_v = fdb_v * (exppa_v - 1.0f) / (exppa_v + 1.0f); const float alpha_v = CLAMP(atan(rad_v), -1.5f, 1.5f); const float rt_v = sin(0.5f * alpha_v); const float r_v = fmax(0.1f, 2.0f * (horifac - 1.0f) * rt_v * rt_v + 1.0f); const float vertifac = 1.0f - orthocorr / 100.0f; const float exppa_h = exp(shift_h); const float fdb_h = f_global / (14.4f + (u / v - 1) * 7.2f); const float rad_h = fdb_h * (exppa_h - 1.0f) / (exppa_h + 1.0f); const float alpha_h = CLAMP(atan(rad_h), -1.5f, 1.5f); const float rt_h = sin(0.5f * alpha_h); const float r_h = fmax(0.1f, 2.0f * (vertifac - 1.0f) * rt_h * rt_h + 1.0f); */ const float f = f_length_kb * (sqrt(u*u + v*v) / sqrt(36.0*36.0 + 24.0*24.0)); // three intermediate buffers for matrix calculation ... float m1[3][3]/*, m2[3][3]*/, m3[3][3]; // ... and some pointers to handle them more intuitively float (*mwork)[3] = m1; //float (*minput)[3] = m2; float (*moutput)[3] = m3; /* // Step 1: flip x and y coordinates (see above) memset(minput, 0, 9 * sizeof(float)); minput[0][1] = 1.0f; minput[1][0] = 1.0f; minput[2][2] = 1.0f; // Step 2: rotation of image around its center memset(mwork, 0, 9 * sizeof(float)); mwork[0][0] = cosi; mwork[0][1] = -sini; mwork[1][0] = sini; mwork[1][1] = cosi; mwork[0][2] = -0.5f * v * cosi + 0.5f * u * sini + 0.5f * v; mwork[1][2] = -0.5f * v * sini - 0.5f * u * cosi + 0.5f * u; mwork[2][2] = 1.0f; // multiply mwork * minput -> moutput mat3mul((float *)moutput, (float *)mwork, (float *)minput); // Step 3: apply shearing memset(mwork, 0, 9 * sizeof(float)); mwork[0][0] = 1.0f; mwork[0][1] = shear; mwork[1][1] = 1.0f; mwork[1][0] = shear; mwork[2][2] = 1.0f; // moutput (of last calculation) -> minput MAT3SWAP(minput, moutput); // multiply mwork * minput -> moutput mat3mul((float *)moutput, (float *)mwork, (float *)minput); // Step 4: apply vertical lens shift effect memset(mwork, 0, 9 * sizeof(float)); mwork[0][0] = exppa_v; mwork[1][0] = 0.5f * ((exppa_v - 1.0f) * u) / v; mwork[1][1] = 2.0f * exppa_v / (exppa_v + 1.0f); mwork[1][2] = -0.5f * ((exppa_v - 1.0f) * u) / (exppa_v + 1.0f); mwork[2][0] = (exppa_v - 1.0f) / v; mwork[2][2] = 1.0f; // moutput (of last calculation) -> minput MAT3SWAP(minput, moutput); // multiply mwork * minput -> moutput mat3mul((float *)moutput, (float *)mwork, (float *)minput); // Step 5: horizontal compression memset(mwork, 0, 9 * sizeof(float)); mwork[0][0] = 1.0f; mwork[1][1] = r_v; mwork[1][2] = 0.5f * u * (1.0f - r_v); mwork[2][2] = 1.0f; // moutput (of last calculation) -> minput MAT3SWAP(minput, moutput); // multiply mwork * minput -> moutput mat3mul((float *)moutput, (float *)mwork, (float *)minput); // Step 6: flip x and y back again memset(mwork, 0, 9 * sizeof(float)); mwork[0][1] = 1.0f; mwork[1][0] = 1.0f; mwork[2][2] = 1.0f; // moutput (of last calculation) -> minput MAT3SWAP(minput, moutput); // multiply mwork * minput -> moutput mat3mul((float *)moutput, (float *)mwork, (float *)minput); // from here output vectors would be in (x : y : 1) format // Step 7: now we can apply horizontal lens shift with the same matrix format as above memset(mwork, 0, 9 * sizeof(float)); mwork[0][0] = exppa_h; mwork[1][0] = 0.5f * ((exppa_h - 1.0f) * v) / u; mwork[1][1] = 2.0f * exppa_h / (exppa_h + 1.0f); mwork[1][2] = -0.5f * ((exppa_h - 1.0f) * v) / (exppa_h + 1.0f); mwork[2][0] = (exppa_h - 1.0f) / u; mwork[2][2] = 1.0f; // moutput (of last calculation) -> minput MAT3SWAP(minput, moutput); // multiply mwork * minput -> moutput mat3mul((float *)moutput, (float *)mwork, (float *)minput); // Step 8: vertical compression memset(mwork, 0, 9 * sizeof(float)); mwork[0][0] = 1.0f; mwork[1][1] = r_h; mwork[1][2] = 0.5f * v * (1.0f - r_h); mwork[2][2] = 1.0f; // moutput (of last calculation) -> minput MAT3SWAP(minput, moutput); // multiply mwork * minput -> moutput mat3mul((float *)moutput, (float *)mwork, (float *)minput); // Step 9: apply aspect ratio scaling memset(mwork, 0, 9 * sizeof(float)); mwork[0][0] = 1.0f * ascale; mwork[1][1] = 1.0f / ascale; mwork[2][2] = 1.0f; // moutput (of last calculation) -> minput MAT3SWAP(minput, moutput); // multiply mwork * minput -> moutput mat3mul((float *)moutput, (float *)mwork, (float *)minput); */ rtengine::homogeneous::Vector center; center[0] = 0.0f; center[1] = 0.0f; center[2] = f; center[3] = 1.0f; using rtengine::operator*; // Location of image center after rotations. const rtengine::homogeneous::Vector camera_center_yaw_pitch = rtengine::homogeneous::rotationMatrix(pitch, rtengine::homogeneous::Axis::X) * rtengine::homogeneous::rotationMatrix(yaw, rtengine::homogeneous::Axis::Y) * center; const rtengine::homogeneous::Matrix matrix = // Perspective correction. rtengine::homogeneous::projectionMatrix(camera_center_yaw_pitch[2], rtengine::homogeneous::Axis::Z) * rtengine::homogeneous::rotationMatrix(yaw, rtengine::homogeneous::Axis::Y) * rtengine::homogeneous::rotationMatrix(pitch, rtengine::homogeneous::Axis::X) * // Rotation. rtengine::homogeneous::rotationMatrix(rot, rtengine::homogeneous::Axis::Z) * // Lens/sensor shift and move to z == camera_focal_length. rtengine::homogeneous::translationMatrix((0.01f * shift_h - 0.5f) * u, (-0.01f * shift_v - 0.5f) * v, f); m3[0][0] = matrix[0][0]; m3[0][1] = matrix[0][1]; m3[0][2] = matrix[0][3]; m3[1][0] = matrix[1][0]; m3[1][1] = matrix[1][1]; m3[1][2] = matrix[1][3]; m3[2][0] = matrix[3][0]; m3[2][1] = matrix[3][1]; m3[2][2] = matrix[3][3]; /* // Step 10: find x/y offsets and apply according correction so that // no negative coordinates occur in output vector float umin = FLT_MAX, vmin = FLT_MAX; // visit all four corners for(int y = 0; y < height; y += height - 1) for(int x = 0; x < width; x += width - 1) { float pi[3], po[3]; pi[0] = x; pi[1] = y; pi[2] = 1.0f; // moutput expects input in (x:y:1) format and gives output as (x:y:1) mat3mulv(po, (float *)moutput, pi); umin = fmin(umin, po[0] / po[2]); vmin = fmin(vmin, po[1] / po[2]); } memset(mwork, 0, 9 * sizeof(float)); mwork[0][0] = 1.0f; mwork[1][1] = 1.0f; mwork[2][2] = 1.0f; mwork[0][2] = -umin; mwork[1][2] = -vmin; // moutput (of last calculation) -> minput MAT3SWAP(minput, moutput); // multiply mwork * minput -> moutput mat3mul((float *)moutput, (float *)mwork, (float *)minput); */ // on request we either keep the final matrix for forward conversions // or produce an inverted matrix for backward conversions if(dir == ASHIFT_HOMOGRAPH_FORWARD) { // we have what we need -> copy it to the right place memcpy(homograph, moutput, 9 * sizeof(float)); } else { // generate inverted homograph (mat3inv function defined in colorspaces.c) if(mat3inv((float *)homograph, (float *)moutput)) { // in case of error we set to unity matrix memset(mwork, 0, 9 * sizeof(float)); mwork[0][0] = 1.0f; mwork[1][1] = 1.0f; mwork[2][2] = 1.0f; memcpy(homograph, mwork, 9 * sizeof(float)); } } } #undef MAT3SWAP // check if module parameters are set to all neutral values in which case the module's // output is identical to its input static inline int isneutral(dt_iop_ashift_data_t *data) { // values lower than this have no visible effect const float eps = 1.0e-4f; return(fabs(data->rotation) < eps && fabs(data->lensshift_v) < eps && fabs(data->lensshift_h) < eps && fabs(data->shear) < eps && fabs(data->aspect - 1.0f) < eps && data->cl < eps && 1.0f - data->cr < eps && data->ct < eps && 1.0f - data->cb < eps); } //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 int distort_transform(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, float *points, size_t points_count) { dt_iop_ashift_data_t *data = (dt_iop_ashift_data_t *)piece->data; // nothing to be done if parameters are set to neutral values if(isneutral(data)) return 1; float homograph[3][3]; homography((float *)homograph, data->rotation, data->lensshift_v, data->lensshift_h, data->shear, data->f_length_kb, data->orthocorr, data->aspect, piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_FORWARD); // clipping offset const float fullwidth = (float)piece->buf_out.width / (data->cr - data->cl); const float fullheight = (float)piece->buf_out.height / (data->cb - data->ct); const float cx = fullwidth * data->cl; const float cy = fullheight * data->ct; #ifdef _OPENMP #pragma omp parallel for schedule(static) shared(points, points_count, homograph) #endif for(size_t i = 0; i < points_count * 2; i += 2) { float pi[3] = { points[i], points[i + 1], 1.0f }; float po[3]; mat3mulv(po, (float *)homograph, pi); points[i] = po[0] / po[2] - cx; points[i + 1] = po[1] / po[2] - cy; } return 1; } int distort_backtransform(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, float *points, size_t points_count) { dt_iop_ashift_data_t *data = (dt_iop_ashift_data_t *)piece->data; // nothing to be done if parameters are set to neutral values if(isneutral(data)) return 1; float ihomograph[3][3]; homography((float *)ihomograph, data->rotation, data->lensshift_v, data->lensshift_h, data->shear, data->f_length_kb, data->orthocorr, data->aspect, piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_INVERTED); // clipping offset const float fullwidth = (float)piece->buf_out.width / (data->cr - data->cl); const float fullheight = (float)piece->buf_out.height / (data->cb - data->ct); const float cx = fullwidth * data->cl; const float cy = fullheight * data->ct; #ifdef _OPENMP #pragma omp parallel for schedule(static) shared(points, points_count, ihomograph) #endif for(size_t i = 0; i < points_count * 2; i += 2) { float pi[3] = { points[i] + cx, points[i + 1] + cy, 1.0f }; float po[3]; mat3mulv(po, (float *)ihomograph, pi); points[i] = po[0] / po[2]; points[i + 1] = po[1] / po[2]; } return 1; } void modify_roi_out(struct dt_iop_module_t *self, struct dt_dev_pixelpipe_iop_t *piece, dt_iop_roi_t *roi_out, const dt_iop_roi_t *roi_in) { dt_iop_ashift_data_t *data = (dt_iop_ashift_data_t *)piece->data; *roi_out = *roi_in; // nothing more to be done if parameters are set to neutral values if(isneutral(data)) return; float homograph[3][3]; homography((float *)homograph, data->rotation, data->lensshift_v, data->lensshift_h, data->shear, data->f_length_kb, data->orthocorr, data->aspect, piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_FORWARD); float xm = FLT_MAX, xM = -FLT_MAX, ym = FLT_MAX, yM = -FLT_MAX; // go through all four vertices of input roi and convert coordinates to output for(int y = 0; y < roi_in->height; y += roi_in->height - 1) { for(int x = 0; x < roi_in->width; x += roi_in->width - 1) { float pin[3], pout[3]; // convert from input coordinates to original image coordinates pin[0] = roi_in->x + x; pin[1] = roi_in->y + y; pin[0] /= roi_in->scale; pin[1] /= roi_in->scale; pin[2] = 1.0f; // apply homograph mat3mulv(pout, (float *)homograph, pin); // convert to output image coordinates pout[0] /= pout[2]; pout[1] /= pout[2]; pout[0] *= roi_out->scale; pout[1] *= roi_out->scale; xm = MIN(xm, pout[0]); xM = MAX(xM, pout[0]); ym = MIN(ym, pout[1]); yM = MAX(yM, pout[1]); } } float width = xM - xm + 1; float height = yM - ym + 1; // clipping adjustments width *= data->cr - data->cl; height *= data->cb - data->ct; roi_out->width = floorf(width); roi_out->height = floorf(height); #ifdef ASHIFT_DEBUG print_roi(roi_in, "roi_in (going into modify_roi_out)"); print_roi(roi_out, "roi_out (after modify_roi_out)"); #endif } void modify_roi_in(struct dt_iop_module_t *self, struct dt_dev_pixelpipe_iop_t *piece, const dt_iop_roi_t *const roi_out, dt_iop_roi_t *roi_in) { dt_iop_ashift_data_t *data = (dt_iop_ashift_data_t *)piece->data; *roi_in = *roi_out; // nothing more to be done if parameters are set to neutral values if(isneutral(data)) return; float ihomograph[3][3]; homography((float *)ihomograph, data->rotation, data->lensshift_v, data->lensshift_h, data->shear, data->f_length_kb, data->orthocorr, data->aspect, piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_INVERTED); const float orig_w = roi_in->scale * piece->buf_in.width; const float orig_h = roi_in->scale * piece->buf_in.height; // clipping offset const float fullwidth = (float)piece->buf_out.width / (data->cr - data->cl); const float fullheight = (float)piece->buf_out.height / (data->cb - data->ct); const float cx = roi_out->scale * fullwidth * data->cl; const float cy = roi_out->scale * fullheight * data->ct; float xm = FLT_MAX, xM = -FLT_MAX, ym = FLT_MAX, yM = -FLT_MAX; // go through all four vertices of output roi and convert coordinates to input for(int y = 0; y < roi_out->height; y += roi_out->height - 1) { for(int x = 0; x < roi_out->width; x += roi_out->width - 1) { float pin[3], pout[3]; // convert from output image coordinates to original image coordinates pout[0] = roi_out->x + x + cx; pout[1] = roi_out->y + y + cy; pout[0] /= roi_out->scale; pout[1] /= roi_out->scale; pout[2] = 1.0f; // apply homograph mat3mulv(pin, (float *)ihomograph, pout); // convert to input image coordinates pin[0] /= pin[2]; pin[1] /= pin[2]; pin[0] *= roi_in->scale; pin[1] *= roi_in->scale; xm = MIN(xm, pin[0]); xM = MAX(xM, pin[0]); ym = MIN(ym, pin[1]); yM = MAX(yM, pin[1]); } } const struct dt_interpolation *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF); roi_in->x = fmaxf(0.0f, xm - interpolation->width); roi_in->y = fmaxf(0.0f, ym - interpolation->width); roi_in->width = fminf(ceilf(orig_w) - roi_in->x, xM - roi_in->x + 1 + interpolation->width); roi_in->height = fminf(ceilf(orig_h) - roi_in->y, yM - roi_in->y + 1 + interpolation->width); // sanity check. roi_in->x = CLAMP(roi_in->x, 0, (int)floorf(orig_w)); roi_in->y = CLAMP(roi_in->y, 0, (int)floorf(orig_h)); roi_in->width = CLAMP(roi_in->width, 1, (int)floorf(orig_w) - roi_in->x); roi_in->height = CLAMP(roi_in->height, 1, (int)floorf(orig_h) - roi_in->y); #ifdef ASHIFT_DEBUG print_roi(roi_out, "roi_out (going into modify_roi_in)"); print_roi(roi_in, "roi_in (after modify_roi_in)"); #endif } #endif // if 0 //----------------------------------------------------------------------------- // simple conversion of rgb image into greyscale variant suitable for line segment detection // the lsd routines expect input as *double, roughly in the range [0.0; 256.0] static void rgb2grey256(const float *in, double *out, const int width, const int height) { const int ch = 4; #ifdef _OPENMP #pragma omp parallel for schedule(static) shared(in, out) #endif for(int j = 0; j < height; j++) { const float *inp = in + (size_t)ch * j * width; double *outp = out + (size_t)j * width; for(int i = 0; i < width; i++, inp += ch, outp++) { *outp = (0.3f * inp[0] + 0.59f * inp[1] + 0.11f * inp[2]) * 256.0; } } } // sobel edge enhancement in one direction static void edge_enhance_1d(const double *in, double *out, const int width, const int height, dt_iop_ashift_enhance_t dir) { // Sobel kernels for both directions const double hkernel[3][3] = { { 1.0, 0.0, -1.0 }, { 2.0, 0.0, -2.0 }, { 1.0, 0.0, -1.0 } }; const double vkernel[3][3] = { { 1.0, 2.0, 1.0 }, { 0.0, 0.0, 0.0 }, { -1.0, -2.0, -1.0 } }; const int kwidth = 3; const int khwidth = kwidth / 2; // select kernel const double *kernel = (dir == ASHIFT_ENHANCE_HORIZONTAL) ? (const double *)hkernel : (const double *)vkernel; #ifdef _OPENMP #pragma omp parallel for schedule(static) shared(in, out, kernel) #endif // loop over image pixels and perform sobel convolution for(int j = khwidth; j < height - khwidth; j++) { const double *inp = in + (size_t)j * width + khwidth; double *outp = out + (size_t)j * width + khwidth; for(int i = khwidth; i < width - khwidth; i++, inp++, outp++) { double sum = 0.0f; for(int jj = 0; jj < kwidth; jj++) { const int k = jj * kwidth; const int l = (jj - khwidth) * width; for(int ii = 0; ii < kwidth; ii++) { sum += inp[l + ii - khwidth] * kernel[k + ii]; } } *outp = sum; } } #ifdef _OPENMP #pragma omp parallel for schedule(static) shared(out) #endif // border fill in output buffer, so we don't get pseudo lines at image frame for(int j = 0; j < height; j++) for(int i = 0; i < width; i++) { double val = out[j * width + i]; if(j < khwidth) val = out[(khwidth - j) * width + i]; else if(j >= height - khwidth) val = out[(j - khwidth) * width + i]; else if(i < khwidth) val = out[j * width + (khwidth - i)]; else if(i >= width - khwidth) val = out[j * width + (i - khwidth)]; out[j * width + i] = val; // jump over center of image if(i == khwidth && j >= khwidth && j < height - khwidth) i = width - khwidth; } } // edge enhancement in both directions static int edge_enhance(const double *in, double *out, const int width, const int height) { double *Gx = NULL; double *Gy = NULL; Gx = (double *)malloc((size_t)width * height * sizeof(double)); if(Gx == NULL) goto error; Gy = (double *)malloc((size_t)width * height * sizeof(double)); if(Gy == NULL) goto error; // perform edge enhancement in both directions edge_enhance_1d(in, Gx, width, height, ASHIFT_ENHANCE_HORIZONTAL); edge_enhance_1d(in, Gy, width, height, ASHIFT_ENHANCE_VERTICAL); // calculate absolute values #ifdef _OPENMP #pragma omp parallel for schedule(static) shared(Gx, Gy, out) #endif for(size_t k = 0; k < (size_t)width * height; k++) { out[k] = sqrt(Gx[k] * Gx[k] + Gy[k] * Gy[k]); } free(Gx); free(Gy); return TRUE; error: if(Gx) free(Gx); if(Gy) free(Gy); return FALSE; } //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 // XYZ -> sRGB matrix static void XYZ_to_sRGB(const float *XYZ, float *sRGB) { sRGB[0] = 3.1338561f * XYZ[0] - 1.6168667f * XYZ[1] - 0.4906146f * XYZ[2]; sRGB[1] = -0.9787684f * XYZ[0] + 1.9161415f * XYZ[1] + 0.0334540f * XYZ[2]; sRGB[2] = 0.0719453f * XYZ[0] - 0.2289914f * XYZ[1] + 1.4052427f * XYZ[2]; } // sRGB -> XYZ matrix static void sRGB_to_XYZ(const float *sRGB, float *XYZ) { XYZ[0] = 0.4360747f * sRGB[0] + 0.3850649f * sRGB[1] + 0.1430804f * sRGB[2]; XYZ[1] = 0.2225045f * sRGB[0] + 0.7168786f * sRGB[1] + 0.0606169f * sRGB[2]; XYZ[2] = 0.0139322f * sRGB[0] + 0.0971045f * sRGB[1] + 0.7141733f * sRGB[2]; } #endif // if 0 //----------------------------------------------------------------------------- // detail enhancement via bilateral grid (function arguments in and out may represent identical buffers) static int detail_enhance(const float *in, float *out, const int width, const int height) { return TRUE; //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 const float sigma_r = 5.0f; const float sigma_s = fminf(width, height) * 0.02f; const float detail = 10.0f; int success = TRUE; // we need to convert from RGB to Lab first; // as colors don't matter we are safe to assume data to be sRGB // convert RGB input to Lab, use output buffer for intermediate storage #ifdef _OPENMP #pragma omp parallel for schedule(static) shared(in, out) #endif for(int j = 0; j < height; j++) { const float *inp = in + (size_t)4 * j * width; float *outp = out + (size_t)4 * j * width; for(int i = 0; i < width; i++, inp += 4, outp += 4) { float XYZ[3]; sRGB_to_XYZ(inp, XYZ); dt_XYZ_to_Lab(XYZ, outp); } } // bilateral grid detail enhancement dt_bilateral_t *b = dt_bilateral_init(width, height, sigma_s, sigma_r); if(b != NULL) { dt_bilateral_splat(b, out); dt_bilateral_blur(b); dt_bilateral_slice_to_output(b, out, out, detail); dt_bilateral_free(b); } else success = FALSE; // convert resulting Lab to RGB output #ifdef _OPENMP #pragma omp parallel for schedule(static) shared(out) #endif for(int j = 0; j < height; j++) { float *outp = out + (size_t)4 * j * width; for(int i = 0; i < width; i++, outp += 4) { float XYZ[3]; dt_Lab_to_XYZ(outp, XYZ); XYZ_to_sRGB(XYZ, outp); } } return success; #endif // if 0 //----------------------------------------------------------------------------- } // apply gamma correction to RGB buffer (function arguments in and out may represent identical buffers) static void gamma_correct(const float *in, float *out, const int width, const int height) { #ifdef _OPENMP #pragma omp parallel for schedule(static) shared(in, out) #endif for(int j = 0; j < height; j++) { const float *inp = in + (size_t)4 * j * width; float *outp = out + (size_t)4 * j * width; for(int i = 0; i < width; i++, inp += 4, outp += 4) { for(int c = 0; c < 3; c++) outp[c] = powf(inp[c], LSD_GAMMA); } } } // do actual line_detection based on LSD algorithm and return results according // to this module's conventions static int line_detect(float *in, const int width, const int height, const int x_off, const int y_off, const float scale, dt_iop_ashift_line_t **alines, int *lcount, int *vcount, int *hcount, float *vweight, float *hweight, dt_iop_ashift_enhance_t enhance, const int is_raw) { double *greyscale = NULL; double *lsd_lines = NULL; dt_iop_ashift_line_t *ashift_lines = NULL; int vertical_count = 0; int horizontal_count = 0; float vertical_weight = 0.0f; float horizontal_weight = 0.0f; // int lines_count; // we count the lines that we really want to use int lct = 0; // apply gamma correction if image is raw if(is_raw) { gamma_correct(in, in, width, height); } // if requested perform an additional detail enhancement step if(enhance & ASHIFT_ENHANCE_DETAIL) { (void)detail_enhance(in, in, width, height); } // allocate intermediate buffers greyscale = (double *)malloc((size_t)width * height * sizeof(double)); if(greyscale == NULL) goto error; // convert to greyscale image rgb2grey256(in, greyscale, width, height); // if requested perform an additional edge enhancement step if(enhance & ASHIFT_ENHANCE_EDGES) { (void)edge_enhance(greyscale, greyscale, width, height); } // call the line segment detector LSD; // LSD stores the number of found lines in lines_count. // it returns structural details as vector 'double lines[7 * lines_count]' lsd_lines = LineSegmentDetection(&lines_count, greyscale, width, height, LSD_SCALE, LSD_SIGMA_SCALE, LSD_QUANT, LSD_ANG_TH, LSD_LOG_EPS, LSD_DENSITY_TH, LSD_N_BINS, NULL, NULL, NULL); if(lines_count > 0) { // aggregate lines data into our own structures ashift_lines = (dt_iop_ashift_line_t *)malloc((size_t)lines_count * sizeof(dt_iop_ashift_line_t)); if(ashift_lines == NULL) goto error; for(int n = 0; n < lines_count; n++) { float x1 = lsd_lines[n * 7 + 0]; float y1 = lsd_lines[n * 7 + 1]; float x2 = lsd_lines[n * 7 + 2]; float y2 = lsd_lines[n * 7 + 3]; // check for lines running along image borders and skip them. // these would likely be false-positives which could result // from any kind of processing artifacts if((fabs(x1 - x2) < 1 && fmax(x1, x2) < 2) || (fabs(x1 - x2) < 1 && fmin(x1, x2) > width - 3) || (fabs(y1 - y2) < 1 && fmax(y1, y2) < 2) || (fabs(y1 - y2) < 1 && fmin(y1, y2) > height - 3)) continue; // line position in absolute coordinates float px1 = x_off + x1; float py1 = y_off + y1; float px2 = x_off + x2; float py2 = y_off + y2; // scale back to input buffer px1 /= scale; py1 /= scale; px2 /= scale; py2 /= scale; // store as homogeneous coordinates ashift_lines[lct].p1[0] = px1; ashift_lines[lct].p1[1] = py1; ashift_lines[lct].p1[2] = 1.0f; ashift_lines[lct].p2[0] = px2; ashift_lines[lct].p2[1] = py2; ashift_lines[lct].p2[2] = 1.0f; // calculate homogeneous coordinates of connecting line (defined by the two points) vec3prodn(ashift_lines[lct].L, ashift_lines[lct].p1, ashift_lines[lct].p2); // normalaze line coordinates so that x^2 + y^2 = 1 // (this will always succeed as L is a real line connecting two real points) vec3lnorm(ashift_lines[lct].L, ashift_lines[lct].L); // length and width of rectangle (see LSD) ashift_lines[lct].length = sqrt((px2 - px1) * (px2 - px1) + (py2 - py1) * (py2 - py1)); ashift_lines[lct].width = lsd_lines[n * 7 + 4] / scale; // ... and weight (= angle precision * length * width) const float weight = lsd_lines[n * 7 + 5] * ashift_lines[lct].length * ashift_lines[lct].width; ashift_lines[lct].weight = weight; const float angle = atan2(py2 - py1, px2 - px1) / M_PI * 180.0f; const int vertical = fabs(fabs(angle) - 90.0f) < MAX_TANGENTIAL_DEVIATION ? 1 : 0; const int horizontal = fabs(fabs(fabs(angle) - 90.0f) - 90.0f) < MAX_TANGENTIAL_DEVIATION ? 1 : 0; const int relevant = ashift_lines[lct].length > MIN_LINE_LENGTH ? 1 : 0; // register type of line dt_iop_ashift_linetype_t type = ASHIFT_LINE_IRRELEVANT; if(vertical && relevant) { type = ASHIFT_LINE_VERTICAL_SELECTED; vertical_count++; vertical_weight += weight; } else if(horizontal && relevant) { type = ASHIFT_LINE_HORIZONTAL_SELECTED; horizontal_count++; horizontal_weight += weight; } ashift_lines[lct].type = type; // the next valid line lct++; } } #ifdef ASHIFT_DEBUG printf("%d lines (vertical %d, horizontal %d, not relevant %d)\n", lines_count, vertical_count, horizontal_count, lct - vertical_count - horizontal_count); float xmin = FLT_MAX, xmax = FLT_MIN, ymin = FLT_MAX, ymax = FLT_MIN; for(int n = 0; n < lct; n++) { xmin = fmin(xmin, fmin(ashift_lines[n].p1[0], ashift_lines[n].p2[0])); xmax = fmax(xmax, fmax(ashift_lines[n].p1[0], ashift_lines[n].p2[0])); ymin = fmin(ymin, fmin(ashift_lines[n].p1[1], ashift_lines[n].p2[1])); ymax = fmax(ymax, fmax(ashift_lines[n].p1[1], ashift_lines[n].p2[1])); printf("x1 %.0f, y1 %.0f, x2 %.0f, y2 %.0f, length %.0f, width %f, X %f, Y %f, Z %f, type %d, scalars %f %f\n", ashift_lines[n].p1[0], ashift_lines[n].p1[1], ashift_lines[n].p2[0], ashift_lines[n].p2[1], ashift_lines[n].length, ashift_lines[n].width, ashift_lines[n].L[0], ashift_lines[n].L[1], ashift_lines[n].L[2], ashift_lines[n].type, vec3scalar(ashift_lines[n].p1, ashift_lines[n].L), vec3scalar(ashift_lines[n].p2, ashift_lines[n].L)); } printf("xmin %.0f, xmax %.0f, ymin %.0f, ymax %.0f\n", xmin, xmax, ymin, ymax); #endif // store results in provided locations *lcount = lct; *vcount = vertical_count; *vweight = vertical_weight; *hcount = horizontal_count; *hweight = horizontal_weight; *alines = ashift_lines; // free intermediate buffers free(lsd_lines); free(greyscale); return lct > 0 ? TRUE : FALSE; error: free(lsd_lines); free(greyscale); return FALSE; } // get image from buffer, analyze for structure and save results static int get_structure(dt_iop_module_t *module, dt_iop_ashift_enhance_t enhance) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)module->gui_data; float *buffer = NULL; int width = 0; int height = 0; int x_off = 0; int y_off = 0; float scale = 0.0f; { //dt_pthread_mutex_lock(&g->lock); MyMutex::MyLock lock(g->lock); // read buffer data if they are available if(g->buf != NULL) { width = g->buf_width; height = g->buf_height; x_off = g->buf_x_off; y_off = g->buf_y_off; scale = g->buf_scale; // create a temporary buffer to hold image data buffer = (float *)malloc((size_t)width * height * 4 * sizeof(float)); if(buffer != NULL) memcpy(buffer, g->buf, (size_t)width * height * 4 * sizeof(float)); } } /* dt_pthread_mutex_unlock(&g->lock); */ if(buffer == NULL) goto error; // get rid of old structural data g->lines_count = 0; g->vertical_count = 0; g->horizontal_count = 0; free(g->lines); g->lines = NULL; dt_iop_ashift_line_t *lines; int lines_count; int vertical_count; int horizontal_count; float vertical_weight; float horizontal_weight; // get new structural data if(!line_detect(buffer, width, height, x_off, y_off, scale, &lines, &lines_count, &vertical_count, &horizontal_count, &vertical_weight, &horizontal_weight, enhance, module->is_raw))//dt_image_is_raw(&module->dev->image_storage))) goto error; // save new structural data g->lines_in_width = width; g->lines_in_height = height; g->lines_x_off = x_off; g->lines_y_off = y_off; g->lines_count = lines_count; g->vertical_count = vertical_count; g->horizontal_count = horizontal_count; g->vertical_weight = vertical_weight; g->horizontal_weight = horizontal_weight; g->lines_version++; g->lines_suppressed = 0; g->lines = lines; free(buffer); return TRUE; error: free(buffer); return FALSE; } // swap two integer values static inline void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } // do complete permutations static int quickperm(int *a, int *p, const int N, int *i) { if(*i >= N) return FALSE; p[*i]--; int j = (*i % 2 == 1) ? p[*i] : 0; swap(&a[j], &a[*i]); *i = 1; while(p[*i] == 0) { p[*i] = *i; (*i)++; } return TRUE; } // Fisher-Yates shuffle static void shuffle(int *a, const int N) { for(int i = 0; i < N; i++) { int j = i + rand() % (N - i); swap(&a[j], &a[i]); } } // factorial function static int fact(const int n) { return (n == 1 ? 1 : n * fact(n - 1)); } // We use a pseudo-RANSAC algorithm to elminiate ouliers from our set of lines. The // original RANSAC works on linear optimization problems. Our model is nonlinear. We // take advantage of the fact that lines interesting for our model are vantage lines // that meet in one vantage point for each subset of lines (vertical/horizontal). // Strategy: we construct a model by (random) sampling within the subset of lines and // calculate the vantage point. Then we check the "distance" of all other lines to the // vantage point. The model that gives highest number of lines combined with the highest // total weight and lowest overall "distance" wins. // Disadvantage: compared to the original RANSAC we don't get any model parameters that // we could use for the following NMS fit. // Self-tuning: we optimize "epsilon", the hurdle rate to reject a line as an outlier, // by a number of dry runs first. The target average percentage value of lines to eliminate as // outliers (without judging on the quality of the model) is given by RANSAC_ELIMINATION_RATIO, // note: the actual percentage of outliers removed in the final run will be lower because we // will finally look for the best quality model with the optimized epsilon and that quality value also // encloses the number of good lines static void ransac(const dt_iop_ashift_line_t *lines, int *index_set, int *inout_set, const int set_count, const float total_weight, const int xmin, const int xmax, const int ymin, const int ymax) { if(set_count < 3) return; const size_t set_size = set_count * sizeof(int); int *best_set = (int *)malloc(set_size); memcpy(best_set, index_set, set_size); int *best_inout = (int *)calloc(1, set_size); float best_quality = 0.0f; // hurdle value epsilon for rejecting a line as an outlier will be self-tuning // in a number of dry runs float epsilon = pow(10.0f, -RANSAC_EPSILON); float epsilon_step = RANSAC_EPSILON_STEP; // some accounting variables for self-tuning int lines_eliminated = 0; int valid_runs = 0; // number of runs to optimize epsilon const int optiruns = RANSAC_OPTIMIZATION_STEPS * RANSAC_OPTIMIZATION_DRY_RUNS; // go for complete permutations on small set sizes, else for random sample consensus const int riter = (set_count > RANSAC_HURDLE) ? RANSAC_RUNS : fact(set_count); // some data needed for quickperm int *perm = (int *)malloc((set_count + 1) * sizeof(int)); for(int n = 0; n < set_count + 1; n++) perm[n] = n; int piter = 1; // inout holds good/bad qualification for each line int *inout = (int *)malloc(set_size); for(int r = 0; r < optiruns + riter; r++) { // get random or systematic variation of index set if(set_count > RANSAC_HURDLE || r < optiruns) shuffle(index_set, set_count); else (void)quickperm(index_set, perm, set_count, &piter); // summed quality evaluation of this run float quality = 0.0f; // we build a model ouf of the first two lines const float *L1 = lines[index_set[0]].L; const float *L2 = lines[index_set[1]].L; // get intersection point (ideally a vantage point) float V[3]; vec3prodn(V, L1, L2); // catch special cases: // a) L1 and L2 are identical -> V is NULL -> no valid vantage point // b) vantage point lies inside image frame (no chance to correct for this case) if(vec3isnull(V) || (fabs(V[2]) > 0.0f && V[0]/V[2] >= xmin && V[1]/V[2] >= ymin && V[0]/V[2] <= xmax && V[1]/V[2] <= ymax)) { // no valid model quality = 0.0f; } else { // valid model // normalize V so that x^2 + y^2 + z^2 = 1 vec3norm(V, V); // the two lines constituting the model are part of the set inout[0] = 1; inout[1] = 1; // go through all remaining lines, check if they are within the model, and // mark that fact in inout[]. // summarize a quality parameter for all lines within the model for(int n = 2; n < set_count; n++) { // L is normalized so that x^2 + y^2 = 1 const float *L3 = lines[index_set[n]].L; // we take the absolute value of the dot product of V and L as a measure // of the "distance" between point and line. Note that this is not the real euclidean // distance but - with the given normalization - just a pragmatically selected number // that goes to zero if V lies on L and increases the more V and L are apart const float d = fabs(vec3scalar(V, L3)); // depending on d we either include or exclude the point from the set inout[n] = (d < epsilon) ? 1 : 0; float q; if(inout[n] == 1) { // a quality parameter that depends 1/3 on the number of lines within the model, // 1/3 on their weight, and 1/3 on their weighted distance d to the vantage point q = 0.33f / (float)set_count + 0.33f * lines[index_set[n]].weight / total_weight + 0.33f * (1.0f - d / epsilon) * (float)set_count * lines[index_set[n]].weight / total_weight; } else { q = 0.0f; lines_eliminated++; } quality += q; } valid_runs++; } if(r < optiruns) { // on last run of each self-tuning step if((r % RANSAC_OPTIMIZATION_DRY_RUNS) == (RANSAC_OPTIMIZATION_DRY_RUNS - 1) && (valid_runs > 0)) { #ifdef ASHIFT_DEBUG printf("ransac self-tuning (run %d): epsilon %f", r, epsilon); #endif // average ratio of lines that we eliminated with the given epsilon float ratio = 100.0f * (float)lines_eliminated / ((float)set_count * valid_runs); // adjust epsilon accordingly if(ratio < RANSAC_ELIMINATION_RATIO) epsilon = pow(10.0f, log10(epsilon) - epsilon_step); else if(ratio > RANSAC_ELIMINATION_RATIO) epsilon = pow(10.0f, log10(epsilon) + epsilon_step); #ifdef ASHIFT_DEBUG printf(" (elimination ratio %f) -> %f\n", ratio, epsilon); #endif // reduce step-size for next optimization round epsilon_step /= 2.0f; lines_eliminated = 0; valid_runs = 0; } } else { // in the "real" runs check against the best model found so far if(quality > best_quality) { memcpy(best_set, index_set, set_size); memcpy(best_inout, inout, set_size); best_quality = quality; } } #ifdef ASHIFT_DEBUG // report some statistics int count = 0, lastcount = 0; for(int n = 0; n < set_count; n++) count += best_inout[n]; for(int n = 0; n < set_count; n++) lastcount += inout[n]; printf("ransac run %d: best qual %.6f, eps %.6f, line count %d of %d (this run: qual %.5f, count %d (%2f%%))\n", r, best_quality, epsilon, count, set_count, quality, lastcount, 100.0f * lastcount / (float)set_count); #endif } // store back best set memcpy(index_set, best_set, set_size); memcpy(inout_set, best_inout, set_size); free(inout); free(perm); free(best_inout); free(best_set); } // try to clean up structural data by eliminating outliers and thereby increasing // the chance of a convergent fitting static int remove_outliers(dt_iop_module_t *module) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)module->gui_data; const int width = g->lines_in_width; const int height = g->lines_in_height; const int xmin = g->lines_x_off; const int ymin = g->lines_y_off; const int xmax = xmin + width; const int ymax = ymin + height; // holds the index set of lines we want to work on int *lines_set = (int *)malloc(g->lines_count * sizeof(int)); // holds the result of ransac int *inout_set = (int *)malloc(g->lines_count * sizeof(int)); // some accounting variables int vnb = 0, vcount = 0; int hnb = 0, hcount = 0; // just to be on the safe side if(g->lines == NULL) goto error; // generate index list for the vertical lines for(int n = 0; n < g->lines_count; n++) { // is this a selected vertical line? if((g->lines[n].type & ASHIFT_LINE_MASK) != ASHIFT_LINE_VERTICAL_SELECTED) continue; lines_set[vnb] = n; inout_set[vnb] = 0; vnb++; } // it only makes sense to call ransac if we have more than two lines if(vnb > 2) ransac(g->lines, lines_set, inout_set, vnb, g->vertical_weight, xmin, xmax, ymin, ymax); // adjust line selected flag according to the ransac results for(int n = 0; n < vnb; n++) { const int m = lines_set[n]; if(inout_set[n] == 1) { g->lines[m].type |= ASHIFT_LINE_SELECTED; vcount++; } else g->lines[m].type &= ~ASHIFT_LINE_SELECTED; } // update number of vertical lines g->vertical_count = vcount; g->lines_version++; // now generate index list for the horizontal lines for(int n = 0; n < g->lines_count; n++) { // is this a selected horizontal line? if((g->lines[n].type & ASHIFT_LINE_MASK) != ASHIFT_LINE_HORIZONTAL_SELECTED) continue; lines_set[hnb] = n; inout_set[hnb] = 0; hnb++; } // it only makes sense to call ransac if we have more than two lines if(hnb > 2) ransac(g->lines, lines_set, inout_set, hnb, g->horizontal_weight, xmin, xmax, ymin, ymax); // adjust line selected flag according to the ransac results for(int n = 0; n < hnb; n++) { const int m = lines_set[n]; if(inout_set[n] == 1) { g->lines[m].type |= ASHIFT_LINE_SELECTED; hcount++; } else g->lines[m].type &= ~ASHIFT_LINE_SELECTED; } // update number of horizontal lines g->horizontal_count = hcount; g->lines_version++; free(inout_set); free(lines_set); return TRUE; error: free(inout_set); free(lines_set); return FALSE; } // utility function to map a variable in [min; max] to [-INF; + INF] static inline double logit(double x, double min, double max) { const double eps = 1.0e-6; // make sure p does not touch the borders of its definition area, // not critical for data accuracy as logit() is only used on initial fit parameters double p = CLAMP((x - min) / (max - min), eps, 1.0 - eps); return (2.0 * atanh(2.0 * p - 1.0)); } // inverted function to logit() static inline double ilogit(double L, double min, double max) { double p = 0.5 * (1.0 + tanh(0.5 * L)); return (p * (max - min) + min); } // helper function for simplex() return quality parameter for the given model // strategy: // * generate homography matrix out of fixed parameters and fitting parameters // * apply homography to all end points of affected lines // * generate new line out of transformed end points // * calculate scalar product s of line with perpendicular axis // * sum over weighted s^2 values static double model_fitness(double *params, void *data) { dt_iop_ashift_fit_params_t *fit = (dt_iop_ashift_fit_params_t *)data; // just for convenience: get shorter names dt_iop_ashift_line_t *lines = fit->lines; const int lines_count = fit->lines_count; const int width = fit->width; const int height = fit->height; const float f_length_kb = fit->f_length_kb; const float orthocorr = fit->orthocorr; const float aspect = fit->aspect; float rotation = fit->rotation; float lensshift_v = fit->lensshift_v; float lensshift_h = fit->lensshift_h; float shear = fit->shear; float camera_pitch = fit->camera_pitch; float camera_yaw = fit->camera_yaw; float rotation_range = fit->rotation_range; /* float lensshift_v_range = fit->lensshift_v_range; float lensshift_h_range = fit->lensshift_h_range; float shear_range = fit->shear_range; */ float camera_pitch_range = fit->camera_pitch_range; float camera_yaw_range = fit->camera_yaw_range; int pcount = 0; // fill in fit parameters from params[]. Attention: order matters!!! if(isnan(rotation)) { rotation = ilogit(params[pcount], -rotation_range, rotation_range); pcount++; } /* if(isnan(lensshift_v)) { lensshift_v = ilogit(params[pcount], -lensshift_v_range, lensshift_v_range); pcount++; } if(isnan(lensshift_h)) { lensshift_h = ilogit(params[pcount], -lensshift_h_range, lensshift_h_range); pcount++; } if(isnan(shear)) { shear = ilogit(params[pcount], -shear_range, shear_range); pcount++; } */ if(isnan(camera_pitch)) { camera_pitch = ilogit(params[pcount], -camera_pitch_range, camera_pitch_range); pcount++; } if(isnan(camera_yaw)) { camera_yaw = ilogit(params[pcount], -camera_yaw_range, camera_yaw_range); pcount++; } assert(pcount == fit->params_count); // the possible reference axes const float Av[3] = { 1.0f, 0.0f, 0.0f }; const float Ah[3] = { 0.0f, 1.0f, 0.0f }; // generate homograph out of the parameters float homograph[3][3]; homography((float *)homograph, rotation, lensshift_v, lensshift_h, shear, camera_pitch, camera_yaw, f_length_kb, orthocorr, aspect, width, height, ASHIFT_HOMOGRAPH_FORWARD); // accounting variables double sumsq_v = 0.0; double sumsq_h = 0.0; double weight_v = 0.0; double weight_h = 0.0; int count_v = 0; int count_h = 0; int count = 0; // iterate over all lines for(int n = 0; n < lines_count; n++) { // check if this is a line which we must skip if((lines[n].type & fit->linemask) != fit->linetype) continue; // the direction of this line (vertical?) const int isvertical = lines[n].type & ASHIFT_LINE_DIRVERT; // select the perpendicular reference axis const float *A = isvertical ? Ah : Av; // apply homographic transformation to the end points float P1[3], P2[3]; mat3mulv(P1, (float *)homograph, lines[n].p1); mat3mulv(P2, (float *)homograph, lines[n].p2); // get line connecting the two points float L[3]; vec3prodn(L, P1, P2); // normalize L so that x^2 + y^2 = 1; makes sure that // y^2 = 1 / (1 + m^2) and x^2 = m^2 / (1 + m^2) with m defining the slope of the line vec3lnorm(L, L); // get scalar product of line L with orthogonal axis A -> gives 0 if line is perpendicular float s = vec3scalar(L, A); // sum up weighted s^2 for both directions individually sumsq_v += isvertical ? (double)s * s * lines[n].weight : 0.0; weight_v += isvertical ? lines[n].weight : 0.0; count_v += isvertical ? 1 : 0; sumsq_h += !isvertical ? (double)s * s * lines[n].weight : 0.0; weight_h += !isvertical ? lines[n].weight : 0.0; count_h += !isvertical ? 1 : 0; count++; } const double v = weight_v > 0.0f && count > 0 ? sumsq_v / weight_v * (float)count_v / count : 0.0; const double h = weight_h > 0.0f && count > 0 ? sumsq_h / weight_h * (float)count_h / count : 0.0; double sum = sqrt(1.0 - (1.0 - v) * (1.0 - h)) * 1.0e6; //double sum = sqrt(v + h) * 1.0e6; #ifdef ASHIFT_DEBUG /* printf("fitness with rotation %f, lensshift_v %f, lensshift_h %f, shear %f -> lines %d, quality %10f\n", rotation, lensshift_v, lensshift_h, shear, count, sum); */ printf("fitness with rotation %f, camera_pitch %f, camera_yaw %f -> lines %d, quality %10f\n", rotation, camera_pitch, camera_yaw, count, sum); #endif return sum; } // setup all data structures for fitting and call NM simplex static dt_iop_ashift_nmsresult_t nmsfit(dt_iop_module_t *module, dt_iop_ashift_params_t *p, dt_iop_ashift_fitaxis_t dir, int min_line_count) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)module->gui_data; if(!g->lines) return NMS_NOT_ENOUGH_LINES; if(dir == ASHIFT_FIT_NONE) return NMS_SUCCESS; double params[4]; int pcount = 0; int enough_lines = TRUE; // initialize fit parameters dt_iop_ashift_fit_params_t fit; fit.lines = g->lines; fit.lines_count = g->lines_count; fit.width = g->lines_in_width; fit.height = g->lines_in_height; fit.f_length_kb = (p->mode == ASHIFT_MODE_GENERIC) ? (float)DEFAULT_F_LENGTH : p->f_length * p->crop_factor; fit.orthocorr = (p->mode == ASHIFT_MODE_GENERIC) ? 0.0f : p->orthocorr; fit.aspect = (p->mode == ASHIFT_MODE_GENERIC) ? 1.0f : p->aspect; fit.rotation = p->rotation; fit.lensshift_v = p->lensshift_v; fit.lensshift_h = p->lensshift_h; fit.shear = p->shear; fit.camera_pitch = p->camera_pitch; fit.camera_yaw = p->camera_yaw; fit.rotation_range = g->rotation_range; fit.lensshift_v_range = g->lensshift_v_range; fit.lensshift_h_range = g->lensshift_h_range; fit.shear_range = g->shear_range; fit.camera_pitch_range = g->camera_pitch_range; fit.camera_yaw_range = g->camera_yaw_range; fit.linetype = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED; fit.linemask = ASHIFT_LINE_MASK; fit.params_count = 0; fit.weight = 0.0f; // if the image is flipped and if we do not want to fit both lens shift // directions or none at all, then we need to change direction dt_iop_ashift_fitaxis_t mdir = dir; if((mdir & ASHIFT_FIT_LENS_BOTH) != ASHIFT_FIT_LENS_BOTH && (mdir & ASHIFT_FIT_LENS_BOTH) != 0) { // flip all directions mdir ^= g->isflipped ? ASHIFT_FIT_FLIP : 0; // special case that needs to be corrected mdir |= (mdir & ASHIFT_FIT_LINES_BOTH) == 0 ? ASHIFT_FIT_LINES_BOTH : 0; } // prepare fit structure and starting parameters for simplex fit. // note: the sequence of parameters in params[] needs to match the // respective order in dt_iop_ashift_fit_params_t. Parameters which are // to be fittet are marked with NAN in the fit structure. Non-NAN // parameters are assumed to be constant. if(mdir & ASHIFT_FIT_ROTATION) { // we fit rotation fit.params_count++; params[pcount] = logit(0, -fit.rotation_range, fit.rotation_range); pcount++; fit.rotation = NAN; } /* if(mdir & ASHIFT_FIT_LENS_VERT) { // we fit vertical lens shift fit.params_count++; params[pcount] = logit(fit.lensshift_v, -fit.lensshift_v_range, fit.lensshift_v_range); pcount++; fit.lensshift_v = NAN; } if(mdir & ASHIFT_FIT_LENS_HOR) { // we fit horizontal lens shift fit.params_count++; params[pcount] = logit(fit.lensshift_h, -fit.lensshift_h_range, fit.lensshift_h_range); pcount++; fit.lensshift_h = NAN; } if(mdir & ASHIFT_FIT_SHEAR) { // we fit the shear parameter fit.params_count++; params[pcount] = logit(fit.shear, -fit.shear_range, fit.shear_range); pcount++; fit.shear = NAN; } */ if(mdir & ASHIFT_FIT_LENS_VERT) { // we fit pitch fit.params_count++; params[pcount] = logit(0, -fit.camera_pitch_range, fit.camera_pitch_range); pcount++; fit.camera_pitch = NAN; } if(mdir & ASHIFT_FIT_LENS_HOR) { // we fit yaw fit.params_count++; params[pcount] = logit(0, -fit.camera_yaw_range, fit.camera_yaw_range); pcount++; fit.camera_yaw = NAN; } if(mdir & ASHIFT_FIT_LINES_VERT) { // we use vertical lines for fitting fit.linetype |= ASHIFT_LINE_DIRVERT; fit.weight += g->vertical_weight; enough_lines = enough_lines && (g->vertical_count >= min_line_count); } if(mdir & ASHIFT_FIT_LINES_HOR) { // we use horizontal lines for fitting fit.linetype |= 0; fit.weight += g->horizontal_weight; enough_lines = enough_lines && (g->horizontal_count >= min_line_count); } // this needs to come after ASHIFT_FIT_LINES_VERT and ASHIFT_FIT_LINES_HOR if((mdir & ASHIFT_FIT_LINES_BOTH) == ASHIFT_FIT_LINES_BOTH) { // if we use fitting in both directions we need to // adjust fit.linetype and fit.linemask to match all selected lines fit.linetype = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED; fit.linemask = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED; } // error case: we do not run simplex if there are not enough lines if(!enough_lines) { #ifdef ASHIFT_DEBUG printf("optimization not possible: insufficient number of lines\n"); #endif return NMS_NOT_ENOUGH_LINES; } // start the simplex fit int iter = simplex(model_fitness, params, fit.params_count, NMS_EPSILON, NMS_SCALE, NMS_ITERATIONS, NULL, (void*)&fit); // error case: the fit did not converge if(iter >= NMS_ITERATIONS) { #ifdef ASHIFT_DEBUG printf("optimization not successful: maximum number of iterations reached (%d)\n", iter); #endif return NMS_DID_NOT_CONVERGE; } // fit was successful: now consolidate the results (order matters!!!) pcount = 0; fit.rotation = isnan(fit.rotation) ? ilogit(params[pcount++], -fit.rotation_range, fit.rotation_range) : fit.rotation; /* fit.lensshift_v = isnan(fit.lensshift_v) ? ilogit(params[pcount++], -fit.lensshift_v_range, fit.lensshift_v_range) : fit.lensshift_v; fit.lensshift_h = isnan(fit.lensshift_h) ? ilogit(params[pcount++], -fit.lensshift_h_range, fit.lensshift_h_range) : fit.lensshift_h; fit.shear = isnan(fit.shear) ? ilogit(params[pcount++], -fit.shear_range, fit.shear_range) : fit.shear; */ fit.camera_pitch = isnan(fit.camera_pitch) ? ilogit(params[pcount++], -fit.camera_pitch_range, fit.camera_pitch_range) : fit.camera_pitch; fit.camera_yaw = isnan(fit.camera_yaw) ? ilogit(params[pcount++], -fit.camera_yaw_range, fit.camera_yaw_range) : fit.camera_yaw; #ifdef ASHIFT_DEBUG /* printf("params after optimization (%d iterations): rotation %f, lensshift_v %f, lensshift_h %f, shear %f\n", iter, fit.rotation, fit.lensshift_v, fit.lensshift_h, fit.shear); */ printf("params after optimization (%d iterations): rotation %f, camera_pitch %f, camera_yaw %f\n", iter, fit.rotation, fit.camera_pitch, fit.camera_yaw); #endif /* // sanity check: in case of extreme values the image gets distorted so strongly that it spans an insanely huge area. we check that // case and assume values that increase the image area by more than a factor of 4 as being insane. float homograph[3][3]; homography((float *)homograph, fit.rotation, fit.lensshift_v, fit.lensshift_h, fit.shear, fit.f_length_kb, fit.orthocorr, fit.aspect, fit.width, fit.height, ASHIFT_HOMOGRAPH_FORWARD); // visit all four corners and find maximum span float xm = FLT_MAX, xM = -FLT_MAX, ym = FLT_MAX, yM = -FLT_MAX; for(int y = 0; y < fit.height; y += fit.height - 1) for(int x = 0; x < fit.width; x += fit.width - 1) { float pi[3], po[3]; pi[0] = x; pi[1] = y; pi[2] = 1.0f; mat3mulv(po, (float *)homograph, pi); po[0] /= po[2]; po[1] /= po[2]; xm = fmin(xm, po[0]); ym = fmin(ym, po[1]); xM = fmax(xM, po[0]); yM = fmax(yM, po[1]); } if((xM - xm) * (yM - ym) > 4.0f * fit.width * fit.height) { #ifdef ASHIFT_DEBUG printf("optimization not successful: degenerate case with area growth factor (%f) exceeding limits\n", (xM - xm) * (yM - ym) / (fit.width * fit.height)); #endif return NMS_INSANE; } */ // now write the results into structure p p->rotation = fit.rotation; /* p->lensshift_v = fit.lensshift_v; p->lensshift_h = fit.lensshift_h; p->shear = fit.shear; */ p->camera_pitch = fit.camera_pitch; p->camera_yaw = fit.camera_yaw; return NMS_SUCCESS; } //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 #ifdef ASHIFT_DEBUG // only used in development phase. call model_fitness() with current parameters and // print some useful information static void model_probe(dt_iop_module_t *module, dt_iop_ashift_params_t *p, dt_iop_ashift_fitaxis_t dir) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)module->gui_data; if(!g->lines) return; if(dir == ASHIFT_FIT_NONE) return; double params[4]; int enough_lines = TRUE; // initialize fit parameters dt_iop_ashift_fit_params_t fit; fit.lines = g->lines; fit.lines_count = g->lines_count; fit.width = g->lines_in_width; fit.height = g->lines_in_height; fit.f_length_kb = (p->mode == ASHIFT_MODE_GENERIC) ? DEFAULT_F_LENGTH : p->f_length * p->crop_factor; fit.orthocorr = (p->mode == ASHIFT_MODE_GENERIC) ? 0.0f : p->orthocorr; fit.aspect = (p->mode == ASHIFT_MODE_GENERIC) ? 1.0f : p->aspect; fit.rotation = p->rotation; fit.lensshift_v = p->lensshift_v; fit.lensshift_h = p->lensshift_h; fit.shear = p->shear; fit.linetype = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED; fit.linemask = ASHIFT_LINE_MASK; fit.params_count = 0; fit.weight = 0.0f; // if the image is flipped and if we do not want to fit both lens shift // directions or none at all, then we need to change direction dt_iop_ashift_fitaxis_t mdir = dir; if((mdir & ASHIFT_FIT_LENS_BOTH) != ASHIFT_FIT_LENS_BOTH && (mdir & ASHIFT_FIT_LENS_BOTH) != 0) { // flip all directions mdir ^= g->isflipped ? ASHIFT_FIT_FLIP : 0; // special case that needs to be corrected mdir |= (mdir & ASHIFT_FIT_LINES_BOTH) == 0 ? ASHIFT_FIT_LINES_BOTH : 0; } if(mdir & ASHIFT_FIT_LINES_VERT) { // we use vertical lines for fitting fit.linetype |= ASHIFT_LINE_DIRVERT; fit.weight += g->vertical_weight; enough_lines = enough_lines && (g->vertical_count >= MINIMUM_FITLINES); } if(mdir & ASHIFT_FIT_LINES_HOR) { // we use horizontal lines for fitting fit.linetype |= 0; fit.weight += g->horizontal_weight; enough_lines = enough_lines && (g->horizontal_count >= MINIMUM_FITLINES); } // this needs to come after ASHIFT_FIT_LINES_VERT and ASHIFT_FIT_LINES_HOR if((mdir & ASHIFT_FIT_LINES_BOTH) == ASHIFT_FIT_LINES_BOTH) { // if we use fitting in both directions we need to // adjust fit.linetype and fit.linemask to match all selected lines fit.linetype = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED; fit.linemask = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED; } double quality = model_fitness(params, (void *)&fit); printf("model fitness: %.8f (rotation %f, lensshift_v %f, lensshift_h %f, shear %f)\n", quality, p->rotation, p->lensshift_v, p->lensshift_h, p->shear); } #endif #endif // if 0 //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- // RT: BEGIN COMMENT (no crop support yet) //----------------------------------------------------------------------------- #if 0 // function to keep crop fitting parameters within constraints static void crop_constraint(double *params, int pcount) { if(pcount > 0) params[0] = fabs(params[0]); if(pcount > 1) params[1] = fabs(params[1]); if(pcount > 2) params[2] = fabs(params[2]); if(pcount > 0 && params[0] > 1.0) params[0] = 1.0 - params[0]; if(pcount > 1 && params[1] > 1.0) params[1] = 1.0 - params[1]; if(pcount > 2 && params[2] > 0.5*M_PI) params[2] = 0.5*M_PI - params[2]; } // helper function for getting the best fitting crop area; // returns the negative area of the largest rectangle that fits within the // defined image with a given rectangle's center and its aspect angle; // the trick: the rectangle center coordinates are given in the input // image coordinates so we know for sure that it also lies within the image after // conversion to the output coordinates static double crop_fitness(double *params, void *data) { dt_iop_ashift_cropfit_params_t *cropfit = (dt_iop_ashift_cropfit_params_t *)data; const float wd = cropfit->width; const float ht = cropfit->height; // get variable and constant parameters, respectively const float x = isnan(cropfit->x) ? params[0] : cropfit->x; const float y = isnan(cropfit->y) ? params[1] : cropfit->y; const float alpha = isnan(cropfit->alpha) ? params[2] : cropfit->alpha; // the center of the rectangle in input image coordinates const float Pc[3] = { x * wd, y * ht, 1.0f }; // convert to the output image coordinates and normalize float P[3]; mat3mulv(P, (float *)cropfit->homograph, Pc); P[0] /= P[2]; P[1] /= P[2]; P[2] = 1.0f; // two auxiliary points (some arbitrary distance away from P) to construct the diagonals const float Pa[2][3] = { { P[0] + 10.0f * cos(alpha), P[1] + 10.0f * sin(alpha), 1.0f }, { P[0] + 10.0f * cos(alpha), P[1] - 10.0f * sin(alpha), 1.0f } }; // the two diagonals: D = P x Pa float D[2][3]; vec3prodn(D[0], P, Pa[0]); vec3prodn(D[1], P, Pa[1]); // find all intersection points of all four edges with both diagonals (I = E x D); // the shortest distance d2min of the intersection point I to the crop area center P determines // the size of the crop area that still fits into the image (for the given center and aspect angle) float d2min = FLT_MAX; for(int k = 0; k < 4; k++) for(int l = 0; l < 2; l++) { // the intersection point float I[3]; vec3prodn(I, cropfit->edges[k], D[l]); // special case: I is all null -> E and D are identical -> P lies on E -> d2min = 0 if(vec3isnull(I)) { d2min = 0.0f; break; } // special case: I[2] is 0.0f -> E and D are parallel and intersect at infinity -> no relevant point if(I[2] == 0.0f) continue; // the default case -> normalize I I[0] /= I[2]; I[1] /= I[2]; // calculate distance from I to P const float d2 = SQR(P[0] - I[0]) + SQR(P[1] - I[1]); // the minimum distance over all intersection points d2min = MIN(d2min, d2); } // calculate the area of the rectangle const float A = 2.0f * d2min * sin(2.0f * alpha); #ifdef ASHIFT_DEBUG printf("crop fitness with x %f, y %f, angle %f -> distance %f, area %f\n", x, y, alpha, d2min, A); #endif // and return -A to allow Nelder-Mead simplex to search for the minimum return -A; } #endif // if 0 //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 // strategy: for a given center of the crop area and a specific aspect angle // we calculate the largest crop area that still lies within the output image; // now we allow a Nelder-Mead simplex to search for the center coordinates // (and optionally the aspect angle) that delivers the largest overall crop area. static void do_crop(dt_iop_module_t *module, dt_iop_ashift_params_t *p) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)module->gui_data; // skip if fitting is still running if(g->fitting) return; // reset fit margins if auto-cropping is off if(p->cropmode == ASHIFT_CROP_OFF) { p->cl = 0.0f; p->cr = 1.0f; p->ct = 0.0f; p->cb = 1.0f; return; } g->fitting = 1; double params[3]; int pcount; // get parameters for the homograph const float f_length_kb = (p->mode == ASHIFT_MODE_GENERIC) ? DEFAULT_F_LENGTH : p->f_length * p->crop_factor; const float orthocorr = (p->mode == ASHIFT_MODE_GENERIC) ? 0.0f : p->orthocorr; const float aspect = (p->mode == ASHIFT_MODE_GENERIC) ? 1.0f : p->aspect; const float rotation = p->rotation; const float lensshift_v = p->lensshift_v; const float lensshift_h = p->lensshift_h; const float shear = p->shear; // prepare structure of constant parameters dt_iop_ashift_cropfit_params_t cropfit; cropfit.width = g->buf_width; cropfit.height = g->buf_height; homography((float *)cropfit.homograph, rotation, lensshift_v, lensshift_h, shear, f_length_kb, orthocorr, aspect, cropfit.width, cropfit.height, ASHIFT_HOMOGRAPH_FORWARD); const float wd = cropfit.width; const float ht = cropfit.height; // the four vertices of the image in input image coordinates const float Vc[4][3] = { { 0.0f, 0.0f, 1.0f }, { 0.0f, ht, 1.0f }, { wd, ht, 1.0f }, { wd, 0.0f, 1.0f } }; // convert the vertices to output image coordinates float V[4][3]; for(int n = 0; n < 4; n++) mat3mulv(V[n], (float *)cropfit.homograph, Vc[n]); // get width and height of output image for later use float xmin = FLT_MAX, ymin = FLT_MAX, xmax = FLT_MIN, ymax = FLT_MIN; for(int n = 0; n < 4; n++) { // normalize V V[n][0] /= V[n][2]; V[n][1] /= V[n][2]; V[n][2] = 1.0f; xmin = MIN(xmin, V[n][0]); xmax = MAX(xmax, V[n][0]); ymin = MIN(ymin, V[n][1]); ymax = MAX(ymax, V[n][1]); } const float owd = xmax - xmin; const float oht = ymax - ymin; // calculate the lines defining the four edges of the image area: E = V[n] x V[n+1] for(int n = 0; n < 4; n++) vec3prodn(cropfit.edges[n], V[n], V[(n + 1) % 4]); // initial fit parameters: crop area is centered and aspect angle is that of the original image // number of parameters: fit only crop center coordinates with a fixed aspect ratio, or fit all three variables if(p->cropmode == ASHIFT_CROP_LARGEST) { params[0] = 0.5; params[1] = 0.5; params[2] = atan2((float)cropfit.height, (float)cropfit.width); cropfit.x = NAN; cropfit.y = NAN; cropfit.alpha = NAN; pcount = 3; } else //(p->cropmode == ASHIFT_CROP_ASPECT) { params[0] = 0.5; params[1] = 0.5; cropfit.x = NAN; cropfit.y = NAN; cropfit.alpha = atan2((float)cropfit.height, (float)cropfit.width); pcount = 2; } // start the simplex fit const int iter = simplex(crop_fitness, params, pcount, NMS_CROP_EPSILON, NMS_CROP_SCALE, NMS_CROP_ITERATIONS, crop_constraint, (void*)&cropfit); float A; // RT float d; // RT float Pc[3] = { 0.f, 0.f, 1.f }; // RT // in case the fit did not converge -> failed if(iter >= NMS_CROP_ITERATIONS) goto failed; // the fit did converge -> get clipping margins out of params: cropfit.x = isnan(cropfit.x) ? params[0] : cropfit.x; cropfit.y = isnan(cropfit.y) ? params[1] : cropfit.y; cropfit.alpha = isnan(cropfit.alpha) ? params[2] : cropfit.alpha; // the area of the best fitting rectangle /*RT const float*/ A = fabs(crop_fitness(params, (void*)&cropfit)); // unlikely to happen but we need to catch this case if(A == 0.0f) goto failed; // we need the half diagonal of that rectangle (this is in output image dimensions); // no need to check for division by zero here as this case implies A == 0.0f, caught above /*RT const float*/ d = sqrt(A / (2.0f * sin(2.0f * cropfit.alpha))); // the rectangle's center in input image (homogeneous) coordinates // RT const float Pc[3] = { cropfit.x * wd, cropfit.y * ht, 1.0f }; Pc[0] = cropfit.x * wd; // RT Pc[1] = cropfit.y * ht; // RT // convert rectangle center to output image coordinates and normalize float P[3]; mat3mulv(P, (float *)cropfit.homograph, Pc); P[0] /= P[2]; P[1] /= P[2]; // calculate clipping margins relative to output image dimensions p->cl = CLAMP((P[0] - d * cos(cropfit.alpha)) / owd, 0.0f, 1.0f); p->cr = CLAMP((P[0] + d * cos(cropfit.alpha)) / owd, 0.0f, 1.0f); p->ct = CLAMP((P[1] - d * sin(cropfit.alpha)) / oht, 0.0f, 1.0f); p->cb = CLAMP((P[1] + d * sin(cropfit.alpha)) / oht, 0.0f, 1.0f); // final sanity check if(p->cr - p->cl <= 0.0f || p->cb - p->ct <= 0.0f) goto failed; g->fitting = 0; #ifdef ASHIFT_DEBUG printf("margins after crop fitting: iter %d, x %f, y %f, angle %f, crop area (%f %f %f %f), width %f, height %f\n", iter, cropfit.x, cropfit.y, cropfit.alpha, p->cl, p->cr, p->ct, p->cb, wd, ht); #endif //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 dt_control_queue_redraw_center(); #endif // if 0 //----------------------------------------------------------------------------- return; failed: // in case of failure: reset clipping margins, set "automatic cropping" parameter // to "off" state, and display warning message p->cl = 0.0f; p->cr = 1.0f; p->ct = 0.0f; p->cb = 1.0f; p->cropmode = ASHIFT_CROP_OFF; //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 dt_bauhaus_combobox_set(g->cropmode, p->cropmode); #endif // if 0 //----------------------------------------------------------------------------- g->fitting = 0; dt_control_log(_("automatic cropping failed")); return; } #endif // if 0 //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- // RT: BEGIN COMMENT (no crop support yet) //----------------------------------------------------------------------------- #if 0 // manually adjust crop area by shifting its center static void crop_adjust(dt_iop_module_t *module, dt_iop_ashift_params_t *p, const float newx, const float newy) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)module->gui_data; // skip if fitting is still running if(g->fitting) return; // get parameters for the homograph const float f_length_kb = (p->mode == ASHIFT_MODE_GENERIC) ? DEFAULT_F_LENGTH : p->f_length * p->crop_factor; const float orthocorr = (p->mode == ASHIFT_MODE_GENERIC) ? 0.0f : p->orthocorr; const float aspect = (p->mode == ASHIFT_MODE_GENERIC) ? 1.0f : p->aspect; const float rotation = p->rotation; const float lensshift_v = p->lensshift_v; const float lensshift_h = p->lensshift_h; const float shear = p->shear; const float wd = g->buf_width; const float ht = g->buf_height; const float alpha = atan2(ht, wd); float homograph[3][3]; homography((float *)homograph, rotation, lensshift_v, lensshift_h, shear, f_length_kb, orthocorr, aspect, wd, ht, ASHIFT_HOMOGRAPH_FORWARD); // the four vertices of the image in input image coordinates const float Vc[4][3] = { { 0.0f, 0.0f, 1.0f }, { 0.0f, ht, 1.0f }, { wd, ht, 1.0f }, { wd, 0.0f, 1.0f } }; // convert the vertices to output image coordinates float V[4][3]; for(int n = 0; n < 4; n++) mat3mulv(V[n], (float *)homograph, Vc[n]); // get width and height of output image float xmin = FLT_MAX, ymin = FLT_MAX, xmax = FLT_MIN, ymax = FLT_MIN; for(int n = 0; n < 4; n++) { // normalize V V[n][0] /= V[n][2]; V[n][1] /= V[n][2]; V[n][2] = 1.0f; xmin = MIN(xmin, V[n][0]); xmax = MAX(xmax, V[n][0]); ymin = MIN(ymin, V[n][1]); ymax = MAX(ymax, V[n][1]); } const float owd = xmax - xmin; const float oht = ymax - ymin; // calculate the lines defining the four edges of the image area: E = V[n] x V[n+1] float E[4][3]; for(int n = 0; n < 4; n++) vec3prodn(E[n], V[n], V[(n + 1) % 4]); // the center of the rectangle in output image coordinates const float P[3] = { newx * owd, newy * oht, 1.0f }; // two auxiliary points (some arbitrary distance away from P) to construct the diagonals const float Pa[2][3] = { { P[0] + 10.0f * cos(alpha), P[1] + 10.0f * sin(alpha), 1.0f }, { P[0] + 10.0f * cos(alpha), P[1] - 10.0f * sin(alpha), 1.0f } }; // the two diagonals: D = P x Pa float D[2][3]; vec3prodn(D[0], P, Pa[0]); vec3prodn(D[1], P, Pa[1]); // find all intersection points of all four edges with both diagonals (I = E x D); // the shortest distance d2min of the intersection point I to the crop area center P determines // the size of the crop area that still fits into the image (for the given center and aspect angle) float d2min = FLT_MAX; for(int k = 0; k < 4; k++) for(int l = 0; l < 2; l++) { // the intersection point float I[3]; vec3prodn(I, E[k], D[l]); // special case: I is all null -> E and D are identical -> P lies on E -> d2min = 0 if(vec3isnull(I)) { d2min = 0.0f; break; } // special case: I[2] is 0.0f -> E and D are parallel and intersect at infinity -> no relevant point if(I[2] == 0.0f) continue; // the default case -> normalize I I[0] /= I[2]; I[1] /= I[2]; // calculate distance from I to P const float d2 = SQR(P[0] - I[0]) + SQR(P[1] - I[1]); // the minimum distance over all intersection points d2min = MIN(d2min, d2); } const float d = sqrt(d2min); // do not allow crop area to drop below 1% of input image area const float A = 2.0f * d * d * sin(2.0f * alpha); if(A < 0.01f * wd * ht) return; // calculate clipping margins relative to output image dimensions p->cl = CLAMP((P[0] - d * cos(alpha)) / owd, 0.0f, 1.0f); p->cr = CLAMP((P[0] + d * cos(alpha)) / owd, 0.0f, 1.0f); p->ct = CLAMP((P[1] - d * sin(alpha)) / oht, 0.0f, 1.0f); p->cb = CLAMP((P[1] + d * sin(alpha)) / oht, 0.0f, 1.0f); #ifdef ASHIFT_DEBUG printf("margins after crop adjustment: x %f, y %f, angle %f, crop area (%f %f %f %f), width %f, height %f\n", 0.5f * (p->cl + p->cr), 0.5f * (p->ct + p->cb), alpha, p->cl, p->cr, p->ct, p->cb, wd, ht); #endif dt_control_queue_redraw_center(); return; } #endif // if 0 //----------------------------------------------------------------------------- // helper function to start analysis for structural data and report about errors static int do_get_structure(dt_iop_module_t *module, dt_iop_ashift_params_t *p, dt_iop_ashift_enhance_t enhance) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)module->gui_data; if(g->fitting) return FALSE; g->fitting = 1; float *b = NULL; { MyMutex::MyLock lock(g->lock); b = g->buf; } /* dt_pthread_mutex_lock(&g->lock); */ /* float *b = g->buf; */ /* dt_pthread_mutex_unlock(&g->lock); */ if(b == NULL) { dt_control_log(_("data pending - please repeat")); goto error; } if(!get_structure(module, enhance)) { dt_control_log(_("could not detect structural data in image")); #ifdef ASHIFT_DEBUG // find out more printf("do_get_structure: buf %p, buf_hash %lu, buf_width %d, buf_height %d, lines %p, lines_count %d\n", g->buf, g->buf_hash, g->buf_width, g->buf_height, g->lines, g->lines_count); #endif goto error; } if(!remove_outliers(module)) { dt_control_log(_("could not run outlier removal")); #ifdef ASHIFT_DEBUG // find out more printf("remove_outliers: buf %p, buf_hash %lu, buf_width %d, buf_height %d, lines %p, lines_count %d\n", g->buf, g->buf_hash, g->buf_width, g->buf_height, g->lines, g->lines_count); #endif goto error; } g->fitting = 0; return TRUE; error: g->fitting = 0; return FALSE; } //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 // helper function to clean structural data static int do_clean_structure(dt_iop_module_t *module, dt_iop_ashift_params_t *p) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)module->gui_data; if(g->fitting) return FALSE; g->fitting = 1; g->lines_count = 0; g->vertical_count = 0; g->horizontal_count = 0; free(g->lines); g->lines = NULL; g->lines_version++; g->lines_suppressed = 0; g->fitting = 0; return TRUE; } #endif // if 0 //----------------------------------------------------------------------------- // helper function to start parameter fit and report about errors static int do_fit(dt_iop_module_t *module, dt_iop_ashift_params_t *p, dt_iop_ashift_fitaxis_t dir, int min_line_count = MINIMUM_FITLINES) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)module->gui_data; dt_iop_ashift_nmsresult_t res; if(g->fitting) return FALSE; // if no structure available get it if(g->lines == NULL) if(!do_get_structure(module, p, ASHIFT_ENHANCE_NONE)) goto error; g->fitting = 1; res = nmsfit(module, p, dir, min_line_count); switch(res) { case NMS_NOT_ENOUGH_LINES: dt_control_log(_("not enough structure for automatic correction")); goto error; break; case NMS_DID_NOT_CONVERGE: case NMS_INSANE: dt_control_log(_("automatic correction failed, please correct manually")); goto error; break; case NMS_SUCCESS: default: break; } g->fitting = 0; /* // finally apply cropping do_crop(module, p); */ return TRUE; error: g->fitting = 0; return FALSE; } //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 //----------------------------------------------------------------------------- void process(struct 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) { dt_iop_ashift_data_t *data = (dt_iop_ashift_data_t *)piece->data; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; const int ch = piece->colors; const int ch_width = ch * roi_in->width; // only for preview pipe: collect input buffer data and do some other evaluations if(self->dev->gui_attached && g && piece->pipe->type == DT_DEV_PIXELPIPE_PREVIEW) { // we want to find out if the final output image is flipped in relation to this iop // so we can adjust the gui labels accordingly const int width = roi_in->width; const int height = roi_in->height; const int x_off = roi_in->x; const int y_off = roi_in->y; const float scale = roi_in->scale; // origin of image and opposite corner as reference points float points[4] = { 0.0f, 0.0f, (float)piece->buf_in.width, (float)piece->buf_in.height }; float ivec[2] = { points[2] - points[0], points[3] - points[1] }; float ivecl = sqrt(ivec[0] * ivec[0] + ivec[1] * ivec[1]); // where do they go? dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->priority + 1, 9999999, points, 2); float ovec[2] = { points[2] - points[0], points[3] - points[1] }; float ovecl = sqrt(ovec[0] * ovec[0] + ovec[1] * ovec[1]); // angle between input vector and output vector float alpha = acos(CLAMP((ivec[0] * ovec[0] + ivec[1] * ovec[1]) / (ivecl * ovecl), -1.0f, 1.0f)); // we are interested if |alpha| is in the range of 90° +/- 45° -> we assume the image is flipped int isflipped = fabs(fmod(alpha + M_PI, M_PI) - M_PI / 2.0f) < M_PI / 4.0f ? 1 : 0; // did modules prior to this one in pixelpipe have changed? -> check via hash value uint64_t hash = dt_dev_hash_plus(self->dev, self->dev->preview_pipe, 0, self->priority - 1); dt_pthread_mutex_lock(&g->lock); g->isflipped = isflipped; // save a copy of preview input buffer for parameter fitting if(g->buf == NULL || (size_t)g->buf_width * g->buf_height < (size_t)width * height) { // if needed to allocate buffer free(g->buf); // a no-op if g->buf is NULL // only get new buffer if no old buffer available or old buffer does not fit in terms of size g->buf = malloc((size_t)width * height * 4 * sizeof(float)); } if(g->buf /* && hash != g->buf_hash */) { // copy data memcpy(g->buf, ivoid, (size_t)width * height * ch * sizeof(float)); g->buf_width = width; g->buf_height = height; g->buf_x_off = x_off; g->buf_y_off = y_off; g->buf_scale = scale; g->buf_hash = hash; } dt_pthread_mutex_unlock(&g->lock); } // if module is set to neutral parameters we just copy input->output and are done if(isneutral(data)) { memcpy(ovoid, ivoid, (size_t)roi_out->width * roi_out->height * ch * sizeof(float)); return; } const struct dt_interpolation *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF); float ihomograph[3][3]; homography((float *)ihomograph, data->rotation, data->lensshift_v, data->lensshift_h, data->shear, data->f_length_kb, data->orthocorr, data->aspect, piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_INVERTED); // clipping offset const float fullwidth = (float)piece->buf_out.width / (data->cr - data->cl); const float fullheight = (float)piece->buf_out.height / (data->cb - data->ct); const float cx = roi_out->scale * fullwidth * data->cl; const float cy = roi_out->scale * fullheight * data->ct; #ifdef _OPENMP //#pragma omp parallel for schedule(static) shared(ihomograph, interpolation) #endif // go over all pixels of output image for(int j = 0; j < roi_out->height; j++) { float *out = ((float *)ovoid) + (size_t)ch * j * roi_out->width; for(int i = 0; i < roi_out->width; i++, out += ch) { float pin[3], pout[3]; /* if (j == roi_out->height - 1) { */ /* printf("HERE\n"); */ /* } */ // convert output pixel coordinates to original image coordinates pout[0] = roi_out->x + i + cx; pout[1] = roi_out->y + j + cy; pout[0] /= roi_out->scale; pout[1] /= roi_out->scale; pout[2] = 1.0f; // apply homograph mat3mulv(pin, (float *)ihomograph, pout); // convert to input pixel coordinates pin[0] /= pin[2]; pin[1] /= pin[2]; pin[0] *= roi_in->scale; pin[1] *= roi_in->scale; /* if (pin[0] < 0 || pin[1] < 0) { */ /* printf("NEGATIVE: %f %f -> %f %f\n", pout[0], pout[1], pin[0], pin[1]); */ /* fflush(stdout); */ /* } */ pin[0] -= roi_in->x; pin[1] -= roi_in->y; // get output values by interpolation from input image dt_interpolation_compute_pixel4c(interpolation, (float *)ivoid, out, pin[0], pin[1], roi_in->width, roi_in->height, ch_width); } } } #ifdef HAVE_OPENCL int process_cl(struct 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_iop_ashift_data_t *d = (dt_iop_ashift_data_t *)piece->data; dt_iop_ashift_global_data_t *gd = (dt_iop_ashift_global_data_t *)self->data; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; const int devid = piece->pipe->devid; const int iwidth = roi_in->width; const int iheight = roi_in->height; const int width = roi_out->width; const int height = roi_out->height; cl_int err = -999; cl_mem dev_homo = NULL; // only for preview pipe: collect input buffer data and do some other evaluations if(self->dev->gui_attached && g && piece->pipe->type == DT_DEV_PIXELPIPE_PREVIEW) { // we want to find out if the final output image is flipped in relation to this iop // so we can adjust the gui labels accordingly const int x_off = roi_in->x; const int y_off = roi_in->y; const float scale = roi_in->scale; // origin of image and opposite corner as reference points float points[4] = { 0.0f, 0.0f, (float)piece->buf_in.width, (float)piece->buf_in.height }; float ivec[2] = { points[2] - points[0], points[3] - points[1] }; float ivecl = sqrt(ivec[0] * ivec[0] + ivec[1] * ivec[1]); // where do they go? dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->priority + 1, 9999999, points, 2); float ovec[2] = { points[2] - points[0], points[3] - points[1] }; float ovecl = sqrt(ovec[0] * ovec[0] + ovec[1] * ovec[1]); // angle between input vector and output vector float alpha = acos(CLAMP((ivec[0] * ovec[0] + ivec[1] * ovec[1]) / (ivecl * ovecl), -1.0f, 1.0f)); // we are interested if |alpha| is in the range of 90° +/- 45° -> we assume the image is flipped int isflipped = fabs(fmod(alpha + M_PI, M_PI) - M_PI / 2.0f) < M_PI / 4.0f ? 1 : 0; // do modules coming before this one in pixelpipe have changed? -> check via hash value uint64_t hash = dt_dev_hash_plus(self->dev, self->dev->preview_pipe, 0, self->priority - 1); dt_pthread_mutex_lock(&g->lock); g->isflipped = isflipped; // save a copy of preview input buffer for parameter fitting if(g->buf == NULL || (size_t)g->buf_width * g->buf_height < (size_t)iwidth * iheight) { // if needed allocate buffer free(g->buf); // a no-op if g->buf is NULL // only get new buffer if no old buffer or old buffer does not fit in terms of size g->buf = malloc((size_t)iwidth * iheight * 4 * sizeof(float)); } if(g->buf /* && hash != g->buf_hash */) { // copy data err = dt_opencl_copy_device_to_host(devid, g->buf, dev_in, iwidth, iheight, 4 * sizeof(float)); g->buf_width = iwidth; g->buf_height = iheight; g->buf_x_off = x_off; g->buf_y_off = y_off; g->buf_scale = scale; g->buf_hash = hash; } dt_pthread_mutex_unlock(&g->lock); if(err != CL_SUCCESS) goto error; } // if module is set to neutral parameters we just copy input->output and are done if(isneutral(d)) { size_t origin[] = { 0, 0, 0 }; size_t region[] = { width, height, 1 }; err = dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, origin, origin, region); if(err != CL_SUCCESS) goto error; return TRUE; } float ihomograph[3][3]; homography((float *)ihomograph, d->rotation, d->lensshift_v, d->lensshift_h, d->shear, d->f_length_kb, d->camera_pitch, d->camera_yaw, d->orthocorr, d->aspect, piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_INVERTED); // clipping offset const float fullwidth = (float)piece->buf_out.width / (d->cr - d->cl); const float fullheight = (float)piece->buf_out.height / (d->cb - d->ct); const float cx = roi_out->scale * fullwidth * d->cl; const float cy = roi_out->scale * fullheight * d->ct; dev_homo = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 9, ihomograph); if(dev_homo == NULL) goto error; const int iroi[2] = { roi_in->x, roi_in->y }; const int oroi[2] = { roi_out->x, roi_out->y }; const float in_scale = roi_in->scale; const float out_scale = roi_out->scale; const float clip[2] = { cx, cy }; size_t sizes[] = { ROUNDUPWD(width), ROUNDUPHT(height), 1 }; const struct dt_interpolation *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF); int ldkernel = -1; switch(interpolation->id) { case DT_INTERPOLATION_BILINEAR: ldkernel = gd->kernel_ashift_bilinear; break; case DT_INTERPOLATION_BICUBIC: ldkernel = gd->kernel_ashift_bicubic; break; case DT_INTERPOLATION_LANCZOS2: ldkernel = gd->kernel_ashift_lanczos2; break; case DT_INTERPOLATION_LANCZOS3: ldkernel = gd->kernel_ashift_lanczos3; break; default: goto error; } dt_opencl_set_kernel_arg(devid, ldkernel, 0, sizeof(cl_mem), (void *)&dev_in); dt_opencl_set_kernel_arg(devid, ldkernel, 1, sizeof(cl_mem), (void *)&dev_out); dt_opencl_set_kernel_arg(devid, ldkernel, 2, sizeof(int), (void *)&width); dt_opencl_set_kernel_arg(devid, ldkernel, 3, sizeof(int), (void *)&height); dt_opencl_set_kernel_arg(devid, ldkernel, 4, sizeof(int), (void *)&iwidth); dt_opencl_set_kernel_arg(devid, ldkernel, 5, sizeof(int), (void *)&iheight); dt_opencl_set_kernel_arg(devid, ldkernel, 6, 2 * sizeof(int), (void *)iroi); dt_opencl_set_kernel_arg(devid, ldkernel, 7, 2 * sizeof(int), (void *)oroi); dt_opencl_set_kernel_arg(devid, ldkernel, 8, sizeof(float), (void *)&in_scale); dt_opencl_set_kernel_arg(devid, ldkernel, 9, sizeof(float), (void *)&out_scale); dt_opencl_set_kernel_arg(devid, ldkernel, 10, 2 * sizeof(float), (void *)clip); dt_opencl_set_kernel_arg(devid, ldkernel, 11, sizeof(cl_mem), (void *)&dev_homo); err = dt_opencl_enqueue_kernel_2d(devid, ldkernel, sizes); if(err != CL_SUCCESS) goto error; dt_opencl_release_mem_object(dev_homo); return TRUE; error: dt_opencl_release_mem_object(dev_homo); dt_print(DT_DEBUG_OPENCL, "[opencl_ashift] couldn't enqueue kernel! %d\n", err); return FALSE; } #endif // gather information about "near"-ness in g->points_idx static void get_near(const float *points, dt_iop_ashift_points_idx_t *points_idx, const int lines_count, float pzx, float pzy, float delta) { const float delta2 = delta * delta; for(int n = 0; n < lines_count; n++) { points_idx[n].near = 0; // skip irrelevant lines if(points_idx[n].type == ASHIFT_LINE_IRRELEVANT) continue; // first check if the mouse pointer is outside the bounding box of the line -> skip this line if(pzx < points_idx[n].bbx - delta && pzx > points_idx[n].bbX + delta && pzy < points_idx[n].bby - delta && pzy > points_idx[n].bbY + delta) continue; // pointer is inside bounding box size_t offset = points_idx[n].offset; const int length = points_idx[n].length; // sanity check (this should not happen) if(length < 2) continue; // check line point by point for(int l = 0; l < length; l++, offset++) { float dx = pzx - points[offset * 2]; float dy = pzy - points[offset * 2 + 1]; if(dx * dx + dy * dy < delta2) { points_idx[n].near = 1; break; } } } } // mark lines which are inside a rectangular area in isbounding mode static void get_bounded_inside(const float *points, dt_iop_ashift_points_idx_t *points_idx, const int points_lines_count, float pzx, float pzy, float pzx2, float pzy2, dt_iop_ashift_bounding_t mode) { // get bounding box coordinates float ax = pzx; float ay = pzy; float bx = pzx2; float by = pzy2; if(pzx > pzx2) { ax = pzx2; bx = pzx; } if(pzy > pzy2) { ay = pzy2; by = pzy; } // we either look for the selected or the deselected lines dt_iop_ashift_linetype_t mask = ASHIFT_LINE_SELECTED; dt_iop_ashift_linetype_t state = (mode == ASHIFT_BOUNDING_DESELECT) ? ASHIFT_LINE_SELECTED : 0; for(int n = 0; n < points_lines_count; n++) { // mark line as "not near" and "not bounded" points_idx[n].near = 0; points_idx[n].bounded = 0; // skip irrelevant lines if(points_idx[n].type == ASHIFT_LINE_IRRELEVANT) continue; // is the line inside the box ? if(points_idx[n].bbx >= ax && points_idx[n].bbx <= bx && points_idx[n].bbX >= ax && points_idx[n].bbX <= bx && points_idx[n].bby >= ay && points_idx[n].bby <= by && points_idx[n].bbY >= ay && points_idx[n].bbY <= by) { points_idx[n].bounded = 1; // only mark "near"-ness of those lines we are interested in points_idx[n].near = ((points_idx[n].type & mask) != state) ? 0 : 1; } } } // generate hash value for lines taking into account only the end point coordinates static uint64_t get_lines_hash(const dt_iop_ashift_line_t *lines, const int lines_count) { uint64_t hash = 5381; for(int n = 0; n < lines_count; n++) { float v[4] = { lines[n].p1[0], lines[n].p1[1], lines[n].p2[0], lines[n].p2[1] }; for(int i = 0; i < 4; i++) hash = ((hash << 5) + hash) ^ ((uint32_t *)v)[i]; } return hash; } // update color information in points_idx if lines have changed in terms of type (but not in terms // of number or position) static int update_colors(struct dt_iop_module_t *self, dt_iop_ashift_points_idx_t *points_idx, int points_lines_count) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; // is the display flipped relative to the original image? const int isflipped = g->isflipped; // go through all lines for(int n = 0; n < points_lines_count; n++) { const dt_iop_ashift_linetype_t type = points_idx[n].type; // set line color according to line type/orientation // note: if the screen display is flipped versus the original image we need // to respect that fact in the color selection if((type & ASHIFT_LINE_MASK) == ASHIFT_LINE_VERTICAL_SELECTED) points_idx[n].color = isflipped ? ASHIFT_LINECOLOR_BLUE : ASHIFT_LINECOLOR_GREEN; else if((type & ASHIFT_LINE_MASK) == ASHIFT_LINE_VERTICAL_NOT_SELECTED) points_idx[n].color = isflipped ? ASHIFT_LINECOLOR_YELLOW : ASHIFT_LINECOLOR_RED; else if((type & ASHIFT_LINE_MASK) == ASHIFT_LINE_HORIZONTAL_SELECTED) points_idx[n].color = isflipped ? ASHIFT_LINECOLOR_GREEN : ASHIFT_LINECOLOR_BLUE; else if((type & ASHIFT_LINE_MASK) == ASHIFT_LINE_HORIZONTAL_NOT_SELECTED) points_idx[n].color = isflipped ? ASHIFT_LINECOLOR_RED : ASHIFT_LINECOLOR_YELLOW; else points_idx[n].color = ASHIFT_LINECOLOR_GREY; } return TRUE; } // get all the points to display lines in the gui static int get_points(struct dt_iop_module_t *self, const dt_iop_ashift_line_t *lines, const int lines_count, const int lines_version, float **points, dt_iop_ashift_points_idx_t **points_idx, int *points_lines_count) { dt_develop_t *dev = self->dev; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; dt_iop_ashift_points_idx_t *my_points_idx = NULL; float *my_points = NULL; // is the display flipped relative to the original image? const int isflipped = g->isflipped; // allocate new index array my_points_idx = (dt_iop_ashift_points_idx_t *)malloc(lines_count * sizeof(dt_iop_ashift_points_idx_t)); if(my_points_idx == NULL) goto error; // account for total number of points size_t total_points = 0; // first step: basic initialization of my_points_idx and counting of total_points for(int n = 0; n < lines_count; n++) { const int length = lines[n].length; total_points += length; my_points_idx[n].length = length; my_points_idx[n].near = 0; my_points_idx[n].bounded = 0; const dt_iop_ashift_linetype_t type = lines[n].type; my_points_idx[n].type = type; // set line color according to line type/orientation // note: if the screen display is flipped versus the original image we need // to respect that fact in the color selection if((type & ASHIFT_LINE_MASK) == ASHIFT_LINE_VERTICAL_SELECTED) my_points_idx[n].color = isflipped ? ASHIFT_LINECOLOR_BLUE : ASHIFT_LINECOLOR_GREEN; else if((type & ASHIFT_LINE_MASK) == ASHIFT_LINE_VERTICAL_NOT_SELECTED) my_points_idx[n].color = isflipped ? ASHIFT_LINECOLOR_YELLOW : ASHIFT_LINECOLOR_RED; else if((type & ASHIFT_LINE_MASK) == ASHIFT_LINE_HORIZONTAL_SELECTED) my_points_idx[n].color = isflipped ? ASHIFT_LINECOLOR_GREEN : ASHIFT_LINECOLOR_BLUE; else if((type & ASHIFT_LINE_MASK) == ASHIFT_LINE_HORIZONTAL_NOT_SELECTED) my_points_idx[n].color = isflipped ? ASHIFT_LINECOLOR_RED : ASHIFT_LINECOLOR_YELLOW; else my_points_idx[n].color = ASHIFT_LINECOLOR_GREY; } // now allocate new points buffer my_points = (float *)malloc((size_t)2 * total_points * sizeof(float)); if(my_points == NULL) goto error; // second step: generate points for each line for(int n = 0, offset = 0; n < lines_count; n++) { my_points_idx[n].offset = offset; float x = lines[n].p1[0]; float y = lines[n].p1[1]; const int length = lines[n].length; const float dx = (lines[n].p2[0] - x) / (float)(length - 1); const float dy = (lines[n].p2[1] - y) / (float)(length - 1); for(int l = 0; l < length && offset < total_points; l++, offset++) { my_points[2 * offset] = x; my_points[2 * offset + 1] = y; x += dx; y += dy; } } // third step: transform all points if(!dt_dev_distort_transform_plus(dev, dev->preview_pipe, self->priority, 9999999, my_points, total_points)) goto error; // fourth step: get bounding box in final coordinates (used later for checking "near"-ness to mouse pointer) for(int n = 0; n < lines_count; n++) { float xmin = FLT_MAX, xmax = FLT_MIN, ymin = FLT_MAX, ymax = FLT_MIN; size_t offset = my_points_idx[n].offset; int length = my_points_idx[n].length; for(int l = 0; l < length; l++) { xmin = fmin(xmin, my_points[2 * offset]); xmax = fmax(xmax, my_points[2 * offset]); ymin = fmin(ymin, my_points[2 * offset + 1]); ymax = fmax(ymax, my_points[2 * offset + 1]); } my_points_idx[n].bbx = xmin; my_points_idx[n].bbX = xmax; my_points_idx[n].bby = ymin; my_points_idx[n].bbY = ymax; } // check if lines_version has changed in-between -> too bad: we can forget about all we did :( if(g->lines_version > lines_version) goto error; *points = my_points; *points_idx = my_points_idx; *points_lines_count = lines_count; return TRUE; error: if(my_points_idx != NULL) free(my_points_idx); if(my_points != NULL) free(my_points); return FALSE; } // does this gui have focus? static int gui_has_focus(struct dt_iop_module_t *self) { return self->dev->gui_module == self; } void gui_post_expose(struct dt_iop_module_t *self, cairo_t *cr, int32_t width, int32_t height, int32_t pointerx, int32_t pointery) { dt_develop_t *dev = self->dev; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; // the usual rescaling stuff const float wd = dev->preview_pipe->backbuf_width; const float ht = dev->preview_pipe->backbuf_height; if(wd < 1.0 || ht < 1.0) return; const float zoom_y = dt_control_get_dev_zoom_y(); const float zoom_x = dt_control_get_dev_zoom_x(); const dt_dev_zoom_t zoom = dt_control_get_dev_zoom(); const int closeup = dt_control_get_dev_closeup(); const float zoom_scale = dt_dev_get_zoom_scale(dev, zoom, 1<buf has been processed if(g->buf && (p->cropmode != ASHIFT_CROP_OFF) && self->enabled) { // roi data of the preview pipe input buffer const float iwd = g->buf_width; const float iht = g->buf_height; const float ixo = g->buf_x_off; const float iyo = g->buf_y_off; // the four corners of the input buffer of this module const float V[4][2] = { { ixo, iyo }, { ixo, iyo + iht }, { ixo + iwd, iyo + iht }, { ixo + iwd, iyo } }; // convert coordinates of corners to coordinates of this module's output if(!dt_dev_distort_transform_plus(self->dev, self->dev->preview_pipe, self->priority, self->priority + 1, (float *)V, 4)) return; // get x/y-offset as well as width and height of output buffer float xmin = FLT_MAX, ymin = FLT_MAX, xmax = FLT_MIN, ymax = FLT_MIN; for(int n = 0; n < 4; n++) { xmin = MIN(xmin, V[n][0]); xmax = MAX(xmax, V[n][0]); ymin = MIN(ymin, V[n][1]); ymax = MAX(ymax, V[n][1]); } const float owd = xmax - xmin; const float oht = ymax - ymin; // the four clipping corners const float C[4][2] = { { xmin + p->cl * owd, ymin + p->ct * oht }, { xmin + p->cl * owd, ymin + p->cb * oht }, { xmin + p->cr * owd, ymin + p->cb * oht }, { xmin + p->cr * owd, ymin + p->ct * oht } }; // convert clipping corners to final output image if(!dt_dev_distort_transform_plus(self->dev, self->dev->preview_pipe, self->priority + 1, 9999999, (float *)C, 4)) return; cairo_save(cr); double dashes = DT_PIXEL_APPLY_DPI(5.0) / zoom_scale; cairo_set_dash(cr, &dashes, 0, 0); cairo_rectangle(cr, 0, 0, width, height); cairo_clip(cr); // mask parts of image outside of clipping area in dark grey cairo_set_source_rgba(cr, .2, .2, .2, .8); cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); cairo_rectangle(cr, 0, 0, width, height); cairo_translate(cr, width / 2.0, height / 2.0); cairo_scale(cr, zoom_scale, zoom_scale); cairo_translate(cr, -.5f * wd - zoom_x * wd, -.5f * ht - zoom_y * ht); cairo_move_to(cr, C[0][0], C[0][1]); cairo_line_to(cr, C[1][0], C[1][1]); cairo_line_to(cr, C[2][0], C[2][1]); cairo_line_to(cr, C[3][0], C[3][1]); cairo_close_path(cr); cairo_fill(cr); // draw white outline around clipping area cairo_set_source_rgb(cr, .7, .7, .7); cairo_move_to(cr, C[0][0], C[0][1]); cairo_line_to(cr, C[1][0], C[1][1]); cairo_line_to(cr, C[2][0], C[2][1]); cairo_line_to(cr, C[3][0], C[3][1]); cairo_close_path(cr); cairo_stroke(cr); // if adjusting crop, draw indicator if (g->adjust_crop && p->cropmode == ASHIFT_CROP_ASPECT) { const double xpos = (C[1][0] + C[2][0]) / 2.0f; const double ypos = (C[0][1] + C[1][1]) / 2.0f; const double size_circle = (C[2][0] - C[1][0]) / 30.0f; const double size_line = (C[2][0] - C[1][0]) / 5.0f; const double size_arrow = (C[2][0] - C[1][0]) / 25.0f; cairo_set_line_width(cr, 2.0 / zoom_scale); cairo_set_source_rgb(cr, .7, .7, .7); cairo_arc (cr, xpos, ypos, size_circle, 0, 2.0 * M_PI); cairo_stroke(cr); cairo_fill(cr); cairo_set_line_width(cr, 2.0 / zoom_scale); cairo_set_source_rgb(cr, .7, .7, .7); // horizontal line cairo_move_to(cr, xpos - size_line, ypos); cairo_line_to(cr, xpos + size_line, ypos); cairo_move_to(cr, xpos - size_line, ypos); cairo_rel_line_to(cr, size_arrow, size_arrow); cairo_move_to(cr, xpos - size_line, ypos); cairo_rel_line_to(cr, size_arrow, -size_arrow); cairo_move_to(cr, xpos + size_line, ypos); cairo_rel_line_to(cr, -size_arrow, size_arrow); cairo_move_to(cr, xpos + size_line, ypos); cairo_rel_line_to(cr, -size_arrow, -size_arrow); // vertical line cairo_move_to(cr, xpos, ypos - size_line); cairo_line_to(cr, xpos, ypos + size_line); cairo_move_to(cr, xpos, ypos - size_line); cairo_rel_line_to(cr, -size_arrow, size_arrow); cairo_move_to(cr, xpos, ypos - size_line); cairo_rel_line_to(cr, size_arrow, size_arrow); cairo_move_to(cr, xpos, ypos + size_line); cairo_rel_line_to(cr, -size_arrow, -size_arrow); cairo_move_to(cr, xpos, ypos + size_line); cairo_rel_line_to(cr, size_arrow, -size_arrow); cairo_stroke(cr); } cairo_restore(cr); } // show guide lines on request if(g->show_guides) { dt_guides_t *guide = (dt_guides_t *)g_list_nth_data(darktable.guides, 0); double dashes = DT_PIXEL_APPLY_DPI(5.0); cairo_save(cr); cairo_rectangle(cr, 0, 0, width, height); cairo_clip(cr); cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.0)); cairo_set_source_rgb(cr, .8, .8, .8); cairo_set_dash(cr, &dashes, 1, 0); guide->draw(cr, 0, 0, width, height, 1.0, guide->user_data); cairo_stroke_preserve(cr); cairo_set_dash(cr, &dashes, 0, 0); cairo_set_source_rgba(cr, 0.3, .3, .3, .8); cairo_stroke(cr); cairo_restore(cr); } // structural data are currently being collected or fit procedure is running? -> skip if(g->fitting) return; // no structural data or visibility switched off? -> stop here if(g->lines == NULL || g->lines_suppressed || !gui_has_focus(self)) return; // get hash value that changes if distortions from here to the end of the pixelpipe changed uint64_t hash = dt_dev_hash_distort(dev); // get hash value that changes if coordinates of lines have changed uint64_t lines_hash = get_lines_hash(g->lines, g->lines_count); // points data are missing or outdated, or distortion has changed? if(g->points == NULL || g->points_idx == NULL || hash != g->grid_hash || (g->lines_version > g->points_version && g->lines_hash != lines_hash)) { // we need to reprocess points free(g->points); g->points = NULL; free(g->points_idx); g->points_idx = NULL; g->points_lines_count = 0; if(!get_points(self, g->lines, g->lines_count, g->lines_version, &g->points, &g->points_idx, &g->points_lines_count)) return; g->points_version = g->lines_version; g->grid_hash = hash; g->lines_hash = lines_hash; } else if(g->lines_hash == lines_hash) { // update line type information in points_idx for(int n = 0; n < g->points_lines_count; n++) g->points_idx[n].type = g->lines[n].type; // coordinates of lines are unchanged -> we only need to update colors if(!update_colors(self, g->points_idx, g->points_lines_count)) return; g->points_version = g->lines_version; } // a final check if(g->points == NULL || g->points_idx == NULL) return; cairo_save(cr); cairo_rectangle(cr, 0, 0, width, height); cairo_clip(cr); cairo_translate(cr, width / 2.0, height / 2.0); cairo_scale(cr, zoom_scale, zoom_scale); cairo_translate(cr, -.5f * wd - zoom_x * wd, -.5f * ht - zoom_y * ht); // this must match the sequence of enum dt_iop_ashift_linecolor_t! const float line_colors[5][4] = { { 0.3f, 0.3f, 0.3f, 0.8f }, // grey (misc. lines) { 0.0f, 1.0f, 0.0f, 0.8f }, // green (selected vertical lines) { 0.8f, 0.0f, 0.0f, 0.8f }, // red (de-selected vertical lines) { 0.0f, 0.0f, 1.0f, 0.8f }, // blue (selected horizontal lines) { 0.8f, 0.8f, 0.0f, 0.8f } }; // yellow (de-selected horizontal lines) cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); // now draw all lines for(int n = 0; n < g->points_lines_count; n++) { // is the near flag set? -> draw line a bit thicker if(g->points_idx[n].near) cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(3.0) / zoom_scale); else cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.5) / zoom_scale); // the color of this line const float *color = line_colors[g->points_idx[n].color]; cairo_set_source_rgba(cr, color[0], color[1], color[2], color[3]); size_t offset = g->points_idx[n].offset; const int length = g->points_idx[n].length; // sanity check (this should not happen) if(length < 2) continue; // set starting point of multi-segment line cairo_move_to(cr, g->points[offset * 2], g->points[offset * 2 + 1]); offset++; // draw individual line segments for(int l = 1; l < length; l++, offset++) { cairo_line_to(cr, g->points[offset * 2], g->points[offset * 2 + 1]); } // finally stroke the line cairo_stroke(cr); } // and we draw the selection box if any if(g->isbounding != ASHIFT_BOUNDING_OFF) { float pzx, pzy; dt_dev_get_pointer_zoom_pos(dev, pointerx, pointery, &pzx, &pzy); pzx += 0.5f; pzy += 0.5f; double dashed[] = { 4.0, 4.0 }; dashed[0] /= zoom_scale; dashed[1] /= zoom_scale; int len = sizeof(dashed) / sizeof(dashed[0]); cairo_rectangle(cr, g->lastx * wd, g->lasty * ht, (pzx - g->lastx) * wd, (pzy - g->lasty) * ht); cairo_set_source_rgba(cr, .3, .3, .3, .8); cairo_set_line_width(cr, 1.0 / zoom_scale); cairo_set_dash(cr, dashed, len, 0); cairo_stroke_preserve(cr); cairo_set_source_rgba(cr, .8, .8, .8, .8); cairo_set_dash(cr, dashed, len, 4); cairo_stroke(cr); } // indicate which area is used for "near"-ness detection when selecting/deselecting lines if(g->near_delta > 0) { float pzx, pzy; dt_dev_get_pointer_zoom_pos(dev, pointerx, pointery, &pzx, &pzy); pzx += 0.5f; pzy += 0.5f; double dashed[] = { 4.0, 4.0 }; dashed[0] /= zoom_scale; dashed[1] /= zoom_scale; int len = sizeof(dashed) / sizeof(dashed[0]); cairo_arc(cr, pzx * wd, pzy * ht, g->near_delta, 0, 2.0 * M_PI); cairo_set_source_rgba(cr, .3, .3, .3, .8); cairo_set_line_width(cr, 1.0 / zoom_scale); cairo_set_dash(cr, dashed, len, 0); cairo_stroke_preserve(cr); cairo_set_source_rgba(cr, .8, .8, .8, .8); cairo_set_dash(cr, dashed, len, 4); cairo_stroke(cr); } cairo_restore(cr); } #endif // if 0 //----------------------------------------------------------------------------- // update the number of selected vertical and horizontal lines static void update_lines_count(const dt_iop_ashift_line_t *lines, const int lines_count, int *vertical_count, int *horizontal_count) { int vlines = 0; int hlines = 0; for(int n = 0; n < lines_count; n++) { if((lines[n].type & ASHIFT_LINE_MASK) == ASHIFT_LINE_VERTICAL_SELECTED) vlines++; else if((lines[n].type & ASHIFT_LINE_MASK) == ASHIFT_LINE_HORIZONTAL_SELECTED) hlines++; } *vertical_count = vlines; *horizontal_count = hlines; } //----------------------------------------------------------------------------- // RT: BEGIN COMMENT #if 0 int mouse_moved(struct dt_iop_module_t *self, double x, double y, double pressure, int which) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; int handled = 0; const float wd = self->dev->preview_pipe->backbuf_width; const float ht = self->dev->preview_pipe->backbuf_height; if(wd < 1.0 || ht < 1.0) return 1; float pzx, pzy; dt_dev_get_pointer_zoom_pos(self->dev, x, y, &pzx, &pzy); pzx += 0.5f; pzy += 0.5f; if (g->adjust_crop) { dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; const float newx = g->crop_cx + pzx - g->lastx; const float newy = g->crop_cy + pzy - g->lasty; crop_adjust(self, p, newx, newy); dt_dev_add_history_item(darktable.develop, self, TRUE); return TRUE; } // if in rectangle selecting mode adjust "near"-ness of lines according to // the rectangular selection if(g->isbounding != ASHIFT_BOUNDING_OFF) { if(wd >= 1.0 && ht >= 1.0) { // mark lines inside the rectangle get_bounded_inside(g->points, g->points_idx, g->points_lines_count, pzx * wd, pzy * ht, g->lastx * wd, g->lasty * ht, g->isbounding); } dt_control_queue_redraw_center(); return FALSE; } // gather information about "near"-ness in g->points_idx get_near(g->points, g->points_idx, g->points_lines_count, pzx * wd, pzy * ht, g->near_delta); // if we are in sweeping mode iterate over lines as we move the pointer and change "selected" state. if(g->isdeselecting || g->isselecting) { for(int n = 0; g->selecting_lines_version == g->lines_version && n < g->points_lines_count; n++) { if(g->points_idx[n].near == 0) continue; if(g->isdeselecting) g->lines[n].type &= ~ASHIFT_LINE_SELECTED; else if(g->isselecting) g->lines[n].type |= ASHIFT_LINE_SELECTED; handled = 1; } } if(handled) { update_lines_count(g->lines, g->lines_count, &g->vertical_count, &g->horizontal_count); g->lines_version++; g->selecting_lines_version++; } dt_control_queue_redraw_center(); // if not in sweeping mode we need to pass the event return (g->isdeselecting || g->isselecting); } int button_pressed(struct dt_iop_module_t *self, double x, double y, double pressure, int which, int type, uint32_t state) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; int handled = 0; float pzx, pzy; dt_dev_get_pointer_zoom_pos(self->dev, x, y, &pzx, &pzy); pzx += 0.5f; pzy += 0.5f; const float wd = self->dev->preview_pipe->backbuf_width; const float ht = self->dev->preview_pipe->backbuf_height; if(wd < 1.0 || ht < 1.0) return 1; // if visibility of lines is switched off or no lines available -> potentially adjust crop area if(g->lines_suppressed || g->lines == NULL) { dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; if (p->cropmode == ASHIFT_CROP_ASPECT) { dt_control_change_cursor(GDK_HAND1); g->adjust_crop = TRUE; g->lastx = pzx; g->lasty = pzy; g->crop_cx = 0.5f * (p->cl + p->cr); g->crop_cy = 0.5f * (p->ct + p->cb); return TRUE; } else return FALSE; } // remember lines version at this stage so we can continuously monitor if the // lines have changed in-between g->selecting_lines_version = g->lines_version; // if shift button is pressed go into bounding mode (selecting or deselecting // in a rectangle area) if((state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) { g->lastx = pzx; g->lasty = pzy; g->isbounding = (which == 3) ? ASHIFT_BOUNDING_DESELECT : ASHIFT_BOUNDING_SELECT; dt_control_change_cursor(GDK_CROSS); return TRUE; } dt_dev_zoom_t zoom = dt_control_get_dev_zoom(); const int closeup = dt_control_get_dev_closeup(); const float min_scale = dt_dev_get_zoom_scale(self->dev, DT_ZOOM_FIT, 1<dev, zoom, 1<points_lines_count > 0); g->near_delta = dt_conf_get_float("plugins/darkroom/ashift/near_delta"); // gather information about "near"-ness in g->points_idx get_near(g->points, g->points_idx, g->points_lines_count, pzx * wd, pzy * ht, g->near_delta); // iterate over all lines close to the pointer and change "selected" state. // left-click selects and right-click deselects the line for(int n = 0; g->selecting_lines_version == g->lines_version && n < g->points_lines_count; n++) { if(g->points_idx[n].near == 0) continue; if(which == 3) g->lines[n].type &= ~ASHIFT_LINE_SELECTED; else g->lines[n].type |= ASHIFT_LINE_SELECTED; handled = 1; } // we switch into sweeping mode either if we anyhow take control // or if cursor was close to a line when button was pressed. in other // cases we hand over the event (for image panning) if((take_control || handled) && which == 3) { dt_control_change_cursor(GDK_PIRATE); g->isdeselecting = 1; } else if(take_control || handled) { dt_control_change_cursor(GDK_PLUS); g->isselecting = 1; } if(handled) { update_lines_count(g->lines, g->lines_count, &g->vertical_count, &g->horizontal_count); g->lines_version++; g->selecting_lines_version++; } return (take_control || handled); } int button_released(struct dt_iop_module_t *self, double x, double y, int which, uint32_t state) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; // stop adjust crop g->adjust_crop = FALSE; dt_control_change_cursor(GDK_LEFT_PTR); // finalize the isbounding mode // if user has released the shift button in-between -> do nothing if(g->isbounding != ASHIFT_BOUNDING_OFF && (state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) { int handled = 0; // we compute the rectangle selection float pzx, pzy; dt_dev_get_pointer_zoom_pos(self->dev, x, y, &pzx, &pzy); pzx += 0.5f; pzy += 0.5f; const float wd = self->dev->preview_pipe->backbuf_width; const float ht = self->dev->preview_pipe->backbuf_height; if(wd >= 1.0 && ht >= 1.0) { // mark lines inside the rectangle get_bounded_inside(g->points, g->points_idx, g->points_lines_count, pzx * wd, pzy * ht, g->lastx * wd, g->lasty * ht, g->isbounding); // select or deselect lines within the rectangle according to isbounding state for(int n = 0; g->selecting_lines_version == g->lines_version && n < g->points_lines_count; n++) { if(g->points_idx[n].bounded == 0) continue; if(g->isbounding == ASHIFT_BOUNDING_DESELECT) g->lines[n].type &= ~ASHIFT_LINE_SELECTED; else g->lines[n].type |= ASHIFT_LINE_SELECTED; handled = 1; } if(handled) { update_lines_count(g->lines, g->lines_count, &g->vertical_count, &g->horizontal_count); g->lines_version++; g->selecting_lines_version++; } dt_control_queue_redraw_center(); } } // end of sweeping/isbounding mode dt_control_change_cursor(GDK_LEFT_PTR); g->isselecting = g->isdeselecting = 0; g->isbounding = ASHIFT_BOUNDING_OFF; g->near_delta = 0; g->lastx = g->lasty = -1.0f; g->crop_cx = g->crop_cy = -1.0f; return 0; } int scrolled(struct dt_iop_module_t *self, double x, double y, int up, uint32_t state) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; // do nothing if visibility of lines is switched off or no lines available if(g->lines_suppressed || g->lines == NULL) return FALSE; if(g->near_delta > 0 && (g->isdeselecting || g->isselecting)) { int handled = 0; float pzx, pzy; dt_dev_get_pointer_zoom_pos(self->dev, x, y, &pzx, &pzy); pzx += 0.5f; pzy += 0.5f; const float wd = self->dev->preview_pipe->backbuf_width; const float ht = self->dev->preview_pipe->backbuf_height; float near_delta = dt_conf_get_float("plugins/darkroom/ashift/near_delta"); const float amount = up ? 0.8f : 1.25f; near_delta = MAX(4.0f, MIN(near_delta * amount, 100.0f)); dt_conf_set_float("plugins/darkroom/ashift/near_delta", near_delta); g->near_delta = near_delta; // gather information about "near"-ness in g->points_idx get_near(g->points, g->points_idx, g->points_lines_count, pzx * wd, pzy * ht, g->near_delta); // iterate over all lines close to the pointer and change "selected" state. for(int n = 0; g->selecting_lines_version == g->lines_version && n < g->points_lines_count; n++) { if(g->points_idx[n].near == 0) continue; if(g->isdeselecting) g->lines[n].type &= ~ASHIFT_LINE_SELECTED; else if(g->isselecting) g->lines[n].type |= ASHIFT_LINE_SELECTED; handled = 1; } if(handled) { update_lines_count(g->lines, g->lines_count, &g->vertical_count, &g->horizontal_count); g->lines_version++; g->selecting_lines_version++; } dt_control_queue_redraw_center(); return TRUE; } return FALSE; } static void rotation_callback(GtkWidget *slider, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; p->rotation = dt_bauhaus_slider_get(slider); #ifdef ASHIFT_DEBUG dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; model_probe(self, p, g->lastfit); #endif do_crop(self, p); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void lensshift_v_callback(GtkWidget *slider, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; p->lensshift_v = dt_bauhaus_slider_get(slider); #ifdef ASHIFT_DEBUG dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; model_probe(self, p, g->lastfit); #endif do_crop(self, p); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void lensshift_h_callback(GtkWidget *slider, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; p->lensshift_h = dt_bauhaus_slider_get(slider); #ifdef ASHIFT_DEBUG dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; model_probe(self, p, g->lastfit); #endif do_crop(self, p); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void shear_callback(GtkWidget *slider, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; p->shear = dt_bauhaus_slider_get(slider); #ifdef ASHIFT_DEBUG dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; model_probe(self, p, g->lastfit); #endif do_crop(self, p); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void guide_lines_callback(GtkWidget *widget, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; g->show_guides = dt_bauhaus_combobox_get(widget); dt_iop_request_focus(self); dt_dev_reprocess_all(self->dev); } static void cropmode_callback(GtkWidget *widget, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; p->cropmode = dt_bauhaus_combobox_get(widget); if(g->lines != NULL && !g->lines_suppressed) { g->lines_suppressed = 1; gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->eye), g->lines_suppressed); } do_crop(self, p); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void mode_callback(GtkWidget *widget, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; p->mode = dt_bauhaus_combobox_get(widget); switch(p->mode) { case ASHIFT_MODE_GENERIC: gtk_widget_hide(g->f_length); gtk_widget_hide(g->crop_factor); gtk_widget_hide(g->orthocorr); gtk_widget_hide(g->aspect); break; case ASHIFT_MODE_SPECIFIC: default: gtk_widget_show(g->f_length); gtk_widget_show(g->crop_factor); gtk_widget_show(g->orthocorr); gtk_widget_show(g->aspect); break; } do_crop(self, p); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void f_length_callback(GtkWidget *slider, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; p->f_length = dt_bauhaus_slider_get(slider); do_crop(self, p); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void crop_factor_callback(GtkWidget *slider, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; p->crop_factor = dt_bauhaus_slider_get(slider); do_crop(self, p); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void orthocorr_callback(GtkWidget *slider, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; p->orthocorr = dt_bauhaus_slider_get(slider); do_crop(self, p); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void aspect_callback(GtkWidget *slider, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(self->dt->gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; p->aspect = dt_bauhaus_slider_get(slider); do_crop(self, p); dt_dev_add_history_item(darktable.develop, self, TRUE); } static int fit_v_button_clicked(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(darktable.gui->reset) return FALSE; if(event->button == 1) { dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; const int control = (event->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK; const int shift = (event->state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK; dt_iop_ashift_fitaxis_t fitaxis = ASHIFT_FIT_NONE; if(control) g->lastfit = fitaxis = ASHIFT_FIT_ROTATION_VERTICAL_LINES; else if(shift) g->lastfit = fitaxis = ASHIFT_FIT_VERTICALLY_NO_ROTATION; else g->lastfit = fitaxis = ASHIFT_FIT_VERTICALLY; dt_iop_request_focus(self); dt_dev_reprocess_all(self->dev); if(self->enabled) { // module is enable -> we process directly if(do_fit(self, p, fitaxis)) { darktable.gui->reset = 1; dt_bauhaus_slider_set_soft(g->rotation, p->rotation); dt_bauhaus_slider_set_soft(g->lensshift_v, p->lensshift_v); dt_bauhaus_slider_set_soft(g->lensshift_h, p->lensshift_h); dt_bauhaus_slider_set_soft(g->shear, p->shear); darktable.gui->reset = 0; } } else { // module is not enabled -> invoke it and queue the job to be processed once // the preview image is ready g->jobcode = ASHIFT_JOBCODE_FIT; g->jobparams = g->lastfit = fitaxis; p->toggle ^= 1; } dt_dev_add_history_item(darktable.develop, self, TRUE); return TRUE; } return FALSE; } static int fit_h_button_clicked(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(darktable.gui->reset) return FALSE; if(event->button == 1) { dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; const int control = (event->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK; const int shift = (event->state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK; dt_iop_ashift_fitaxis_t fitaxis = ASHIFT_FIT_NONE; if(control) g->lastfit = fitaxis = ASHIFT_FIT_ROTATION_HORIZONTAL_LINES; else if(shift) g->lastfit = fitaxis = ASHIFT_FIT_HORIZONTALLY_NO_ROTATION; else g->lastfit = fitaxis = ASHIFT_FIT_HORIZONTALLY; dt_iop_request_focus(self); dt_dev_reprocess_all(self->dev); if(self->enabled) { // module is enable -> we process directly if(do_fit(self, p, fitaxis)) { darktable.gui->reset = 1; dt_bauhaus_slider_set_soft(g->rotation, p->rotation); dt_bauhaus_slider_set_soft(g->lensshift_v, p->lensshift_v); dt_bauhaus_slider_set_soft(g->lensshift_h, p->lensshift_h); dt_bauhaus_slider_set_soft(g->shear, p->shear); darktable.gui->reset = 0; } } else { // module is not enabled -> invoke it and queue the job to be processed once // the preview image is ready g->jobcode = ASHIFT_JOBCODE_FIT; g->jobparams = g->lastfit = fitaxis; p->toggle ^= 1; } dt_dev_add_history_item(darktable.develop, self, TRUE); return TRUE; } return FALSE; } static int fit_both_button_clicked(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(darktable.gui->reset) return FALSE; if(event->button == 1) { dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; const int control = (event->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK; const int shift = (event->state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK; dt_iop_ashift_fitaxis_t fitaxis = ASHIFT_FIT_NONE; if(control && shift) fitaxis = ASHIFT_FIT_BOTH; else if(control) fitaxis = ASHIFT_FIT_ROTATION_BOTH_LINES; else if(shift) fitaxis = ASHIFT_FIT_BOTH_NO_ROTATION; else fitaxis = ASHIFT_FIT_BOTH_SHEAR; dt_iop_request_focus(self); dt_dev_reprocess_all(self->dev); if(self->enabled) { // module is enable -> we process directly if(do_fit(self, p, fitaxis)) { darktable.gui->reset = 1; dt_bauhaus_slider_set_soft(g->rotation, p->rotation); dt_bauhaus_slider_set_soft(g->lensshift_v, p->lensshift_v); dt_bauhaus_slider_set_soft(g->lensshift_h, p->lensshift_h); dt_bauhaus_slider_set_soft(g->shear, p->shear); darktable.gui->reset = 0; } } else { // module is not enabled -> invoke it and queue the job to be processed once // the preview image is ready g->jobcode = ASHIFT_JOBCODE_FIT; g->jobparams = g->lastfit = fitaxis; p->toggle ^= 1; } dt_dev_add_history_item(darktable.develop, self, TRUE); return TRUE; } return FALSE; } static int structure_button_clicked(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(darktable.gui->reset) return FALSE; if(event->button == 1) { dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; const int control = (event->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK; const int shift = (event->state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK; dt_iop_ashift_enhance_t enhance; if(control && shift) enhance = ASHIFT_ENHANCE_EDGES | ASHIFT_ENHANCE_DETAIL; else if(shift) enhance = ASHIFT_ENHANCE_DETAIL; else if(control) enhance = ASHIFT_ENHANCE_EDGES; else enhance = ASHIFT_ENHANCE_NONE; dt_iop_request_focus(self); dt_dev_reprocess_all(self->dev); if(self->enabled) { // module is enabled -> process directly (void)do_get_structure(self, p, enhance); } else { // module is not enabled -> invoke it and queue the job to be processed once // the preview image is ready g->jobcode = ASHIFT_JOBCODE_GET_STRUCTURE; g->jobparams = enhance; p->toggle ^= 1; } dt_dev_add_history_item(darktable.develop, self, TRUE); return TRUE; } return FALSE; } static void clean_button_clicked(GtkButton *button, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; if(darktable.gui->reset) return; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; (void)do_clean_structure(self, p); dt_iop_request_focus(self); dt_control_queue_redraw_center(); } static void eye_button_toggled(GtkToggleButton *togglebutton, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; if(darktable.gui->reset) return; if(g->lines == NULL) { g->lines_suppressed = 0; gtk_toggle_button_set_active(togglebutton, 0); } else { g->lines_suppressed = gtk_toggle_button_get_active(togglebutton); } dt_iop_request_focus(self); dt_control_queue_redraw_center(); } // routine that is called after preview image has been processed. we use it // to perform structure collection or fitting in case those have been triggered while // the module had not yet been enabled static void process_after_preview_callback(gpointer instance, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; dt_iop_ashift_jobcode_t jobcode = g->jobcode; int jobparams = g->jobparams; // purge g->jobcode = ASHIFT_JOBCODE_NONE; g->jobparams = 0; if(darktable.gui->reset) return; switch(jobcode) { case ASHIFT_JOBCODE_GET_STRUCTURE: (void)do_get_structure(self, p, (dt_iop_ashift_enhance_t)jobparams); break; case ASHIFT_JOBCODE_FIT: if(do_fit(self, p, (dt_iop_ashift_fitaxis_t)jobparams)) { darktable.gui->reset = 1; dt_bauhaus_slider_set_soft(g->rotation, p->rotation); dt_bauhaus_slider_set_soft(g->lensshift_v, p->lensshift_v); dt_bauhaus_slider_set_soft(g->lensshift_h, p->lensshift_h); dt_bauhaus_slider_set_soft(g->shear, p->shear); darktable.gui->reset = 0; } dt_dev_add_history_item(darktable.develop, self, TRUE); break; case ASHIFT_JOBCODE_NONE: default: break; } dt_control_queue_redraw_center(); } void commit_params(struct dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)p1; dt_iop_ashift_data_t *d = (dt_iop_ashift_data_t *)piece->data; d->rotation = p->rotation; d->lensshift_v = p->lensshift_v; d->lensshift_h = p->lensshift_h; d->shear = p->shear; d->f_length_kb = (p->mode == ASHIFT_MODE_GENERIC) ? DEFAULT_F_LENGTH : p->f_length * p->crop_factor; d->orthocorr = (p->mode == ASHIFT_MODE_GENERIC) ? 0.0f : p->orthocorr; d->aspect = (p->mode == ASHIFT_MODE_GENERIC) ? 1.0f : p->aspect; if(gui_has_focus(self)) { // if gui has focus we want to see the full uncropped image d->cl = 0.0f; d->cr = 1.0f; d->ct = 0.0f; d->cb = 1.0f; } else { d->cl = p->cl; d->cr = p->cr; d->ct = p->ct; d->cb = p->cb; } } void init_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { dt_iop_ashift_data_t *d = (dt_iop_ashift_data_t *)calloc(1, sizeof(dt_iop_ashift_data_t)); piece->data = (void *)d; self->commit_params(self, self->default_params, pipe, piece); } void cleanup_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { free(piece->data); piece->data = NULL; } void gui_update(struct dt_iop_module_t *self) { dt_iop_module_t *module = (dt_iop_module_t *)self; dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)module->params; dt_bauhaus_slider_set_soft(g->rotation, p->rotation); dt_bauhaus_slider_set_soft(g->lensshift_v, p->lensshift_v); dt_bauhaus_slider_set_soft(g->lensshift_h, p->lensshift_h); dt_bauhaus_slider_set_soft(g->shear, p->shear); dt_bauhaus_slider_set_soft(g->f_length, p->f_length); dt_bauhaus_slider_set_soft(g->crop_factor, p->crop_factor); dt_bauhaus_slider_set(g->orthocorr, p->orthocorr); dt_bauhaus_slider_set(g->aspect, p->aspect); dt_bauhaus_combobox_set(g->mode, p->mode); dt_bauhaus_combobox_set(g->guide_lines, g->show_guides); dt_bauhaus_combobox_set(g->cropmode, p->cropmode); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->eye), 0); switch(p->mode) { case ASHIFT_MODE_GENERIC: gtk_widget_hide(g->f_length); gtk_widget_hide(g->crop_factor); gtk_widget_hide(g->orthocorr); gtk_widget_hide(g->aspect); break; case ASHIFT_MODE_SPECIFIC: default: gtk_widget_show(g->f_length); gtk_widget_show(g->crop_factor); gtk_widget_show(g->orthocorr); gtk_widget_show(g->aspect); break; } } void init(dt_iop_module_t *module) { module->params = calloc(1, sizeof(dt_iop_ashift_params_t)); module->default_params = calloc(1, sizeof(dt_iop_ashift_params_t)); module->default_enabled = 0; module->priority = 214; // module order created by iop_dependencies.py, do not edit! module->params_size = sizeof(dt_iop_ashift_params_t); module->gui_data = NULL; dt_iop_ashift_params_t tmp = (dt_iop_ashift_params_t){ 0.0f, 0.0f, 0.0f, 0.0f, DEFAULT_F_LENGTH, 1.0f, 100.0f, 1.0f, ASHIFT_MODE_GENERIC, 0, ASHIFT_CROP_OFF, 0.0f, 1.0f, 0.0f, 1.0f }; memcpy(module->params, &tmp, sizeof(dt_iop_ashift_params_t)); memcpy(module->default_params, &tmp, sizeof(dt_iop_ashift_params_t)); } void reload_defaults(dt_iop_module_t *module) { // our module is disabled by default module->default_enabled = 0; int isflipped = 0; float f_length = DEFAULT_F_LENGTH; float crop_factor = 1.0f; // try to get information on orientation, focal length and crop factor from image data if(module->dev) { const dt_image_t *img = &module->dev->image_storage; // orientation only needed as a-priori information to correctly label some sliders // before pixelpipe has been set up. later we will get a definite result by // assessing the pixelpipe isflipped = (img->orientation == ORIENTATION_ROTATE_CCW_90_DEG || img->orientation == ORIENTATION_ROTATE_CW_90_DEG) ? 1 : 0; // focal length should be available in exif data if lens is electronically coupled to the camera f_length = isfinite(img->exif_focal_length) && img->exif_focal_length > 0.0f ? img->exif_focal_length : f_length; // crop factor of the camera is often not available and user will need to set it manually in the gui crop_factor = isfinite(img->exif_crop) && img->exif_crop > 0.0f ? img->exif_crop : crop_factor; } // init defaults: dt_iop_ashift_params_t tmp = (dt_iop_ashift_params_t){ 0.0f, 0.0f, 0.0f, 0.0f, f_length, crop_factor, 100.0f, 1.0f, ASHIFT_MODE_GENERIC, 0, ASHIFT_CROP_OFF, 0.0f, 1.0f, 0.0f, 1.0f }; memcpy(module->params, &tmp, sizeof(dt_iop_ashift_params_t)); memcpy(module->default_params, &tmp, sizeof(dt_iop_ashift_params_t)); // reset gui elements if(module->gui_data) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)module->gui_data; char string_v[256]; char string_h[256]; snprintf(string_v, sizeof(string_v), _("lens shift (%s)"), isflipped ? _("horizontal") : _("vertical")); snprintf(string_h, sizeof(string_h), _("lens shift (%s)"), isflipped ? _("vertical") : _("horizontal")); dt_bauhaus_widget_set_label(g->lensshift_v, NULL, string_v); dt_bauhaus_widget_set_label(g->lensshift_h, NULL, string_h); dt_bauhaus_slider_set_default(g->f_length, tmp.f_length); dt_bauhaus_slider_set_default(g->crop_factor, tmp.crop_factor); dt_pthread_mutex_lock(&g->lock); free(g->buf); g->buf = NULL; g->buf_width = 0; g->buf_height = 0; g->buf_x_off = 0; g->buf_y_off = 0; g->buf_scale = 1.0f; g->buf_hash = 0; g->isflipped = -1; g->lastfit = ASHIFT_FIT_NONE; dt_pthread_mutex_unlock(&g->lock); g->fitting = 0; free(g->lines); g->lines = NULL; g->lines_count =0; g->horizontal_count = 0; g->vertical_count = 0; g->grid_hash = 0; g->lines_hash = 0; g->rotation_range = ROTATION_RANGE_SOFT; g->lensshift_v_range = LENSSHIFT_RANGE_SOFT; g->lensshift_h_range = LENSSHIFT_RANGE_SOFT; g->shear_range = SHEAR_RANGE_SOFT; g->lines_suppressed = 0; g->lines_version = 0; g->show_guides = 0; g->isselecting = 0; g->isdeselecting = 0; g->isbounding = ASHIFT_BOUNDING_OFF; g->near_delta = 0; g->selecting_lines_version = 0; free(g->points); g->points = NULL; free(g->points_idx); g->points_idx = NULL; g->points_lines_count = 0; g->points_version = 0; g->jobcode = ASHIFT_JOBCODE_NONE; g->jobparams = 0; g->adjust_crop = FALSE; g->lastx = g->lasty = -1.0f; g->crop_cx = g->crop_cy = 1.0f; } } void init_global(dt_iop_module_so_t *module) { dt_iop_ashift_global_data_t *gd = (dt_iop_ashift_global_data_t *)malloc(sizeof(dt_iop_ashift_global_data_t)); module->data = gd; const int program = 2; // basic.cl, from programs.conf gd->kernel_ashift_bilinear = dt_opencl_create_kernel(program, "ashift_bilinear"); gd->kernel_ashift_bicubic = dt_opencl_create_kernel(program, "ashift_bicubic"); gd->kernel_ashift_lanczos2 = dt_opencl_create_kernel(program, "ashift_lanczos2"); gd->kernel_ashift_lanczos3 = dt_opencl_create_kernel(program, "ashift_lanczos3"); } void cleanup(dt_iop_module_t *module) { free(module->params); module->params = NULL; } void cleanup_global(dt_iop_module_so_t *module) { dt_iop_ashift_global_data_t *gd = (dt_iop_ashift_global_data_t *)module->data; dt_opencl_free_kernel(gd->kernel_ashift_bilinear); dt_opencl_free_kernel(gd->kernel_ashift_bicubic); dt_opencl_free_kernel(gd->kernel_ashift_lanczos2); dt_opencl_free_kernel(gd->kernel_ashift_lanczos3); free(module->data); module->data = NULL; } // adjust labels of lens shift parameters according to flip status of image static gboolean draw(GtkWidget *widget, cairo_t *cr, dt_iop_module_t *self) { dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; if(darktable.gui->reset) return FALSE; dt_pthread_mutex_lock(&g->lock); const int isflipped = g->isflipped; dt_pthread_mutex_unlock(&g->lock); if(isflipped == -1) return FALSE; char string_v[256]; char string_h[256]; snprintf(string_v, sizeof(string_v), _("lens shift (%s)"), isflipped ? _("horizontal") : _("vertical")); snprintf(string_h, sizeof(string_h), _("lens shift (%s)"), isflipped ? _("vertical") : _("horizontal")); darktable.gui->reset = 1; dt_bauhaus_widget_set_label(g->lensshift_v, NULL, string_v); dt_bauhaus_widget_set_label(g->lensshift_h, NULL, string_h); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->eye), g->lines_suppressed); darktable.gui->reset = 0; return FALSE; } void gui_focus(struct dt_iop_module_t *self, gboolean in) { if(self->enabled) dt_dev_reprocess_all(self->dev); } static float log10_callback(GtkWidget *self, float inval, dt_bauhaus_callback_t dir) { float outval; switch(dir) { case DT_BAUHAUS_SET: outval = log10(fmax(inval, 1e-15f)); break; case DT_BAUHAUS_GET: outval = exp(M_LN10 * inval); break; default: outval = inval; } return outval; } static float log2_callback(GtkWidget *self, float inval, dt_bauhaus_callback_t dir) { float outval; switch(dir) { case DT_BAUHAUS_SET: outval = log(fmax(inval, 1e-15f)) / M_LN2; break; case DT_BAUHAUS_GET: outval = exp(M_LN2 * inval); break; default: outval = inval; } return outval; } void gui_init(struct dt_iop_module_t *self) { self->gui_data = malloc(sizeof(dt_iop_ashift_gui_data_t)); dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)self->params; dt_pthread_mutex_init(&g->lock, NULL); dt_pthread_mutex_lock(&g->lock); g->buf = NULL; g->buf_width = 0; g->buf_height = 0; g->buf_x_off = 0; g->buf_y_off = 0; g->buf_scale = 1.0f; g->buf_hash = 0; g->isflipped = -1; g->lastfit = ASHIFT_FIT_NONE; dt_pthread_mutex_unlock(&g->lock); g->fitting = 0; g->lines = NULL; g->lines_count = 0; g->vertical_count = 0; g->horizontal_count = 0; g->lines_version = 0; g->lines_suppressed = 0; g->points = NULL; g->points_idx = NULL; g->points_lines_count = 0; g->points_version = 0; g->grid_hash = 0; g->lines_hash = 0; g->rotation_range = ROTATION_RANGE_SOFT; g->lensshift_v_range = LENSSHIFT_RANGE_SOFT; g->lensshift_h_range = LENSSHIFT_RANGE_SOFT; g->shear_range = SHEAR_RANGE_SOFT; g->show_guides = 0; g->isselecting = 0; g->isdeselecting = 0; g->isbounding = ASHIFT_BOUNDING_OFF; g->near_delta = 0; g->selecting_lines_version = 0; g->jobcode = ASHIFT_JOBCODE_NONE; g->jobparams = 0; g->adjust_crop = FALSE; g->lastx = g->lasty = -1.0f; g->crop_cx = g->crop_cy = 1.0f; self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_BAUHAUS_SPACE); dt_gui_add_help_link(self->widget, dt_get_help_url(self->op)); g->rotation = dt_bauhaus_slider_new_with_range(self, -ROTATION_RANGE, ROTATION_RANGE, 0.01*ROTATION_RANGE, p->rotation, 2); dt_bauhaus_widget_set_label(g->rotation, NULL, _("rotation")); dt_bauhaus_slider_set_format(g->rotation, "%.2f°"); dt_bauhaus_slider_enable_soft_boundaries(g->rotation, -ROTATION_RANGE_SOFT, ROTATION_RANGE_SOFT); gtk_box_pack_start(GTK_BOX(self->widget), g->rotation, TRUE, TRUE, 0); g->lensshift_v = dt_bauhaus_slider_new_with_range(self, -LENSSHIFT_RANGE, LENSSHIFT_RANGE, 0.01*LENSSHIFT_RANGE, p->lensshift_v, 3); dt_bauhaus_widget_set_label(g->lensshift_v, NULL, _("lens shift (vertical)")); dt_bauhaus_slider_enable_soft_boundaries(g->lensshift_v, -LENSSHIFT_RANGE_SOFT, LENSSHIFT_RANGE_SOFT); gtk_box_pack_start(GTK_BOX(self->widget), g->lensshift_v, TRUE, TRUE, 0); g->lensshift_h = dt_bauhaus_slider_new_with_range(self, -LENSSHIFT_RANGE, LENSSHIFT_RANGE, 0.01*LENSSHIFT_RANGE, p->lensshift_h, 3); dt_bauhaus_widget_set_label(g->lensshift_h, NULL, _("lens shift (horizontal)")); dt_bauhaus_slider_enable_soft_boundaries(g->lensshift_h, -LENSSHIFT_RANGE_SOFT, LENSSHIFT_RANGE_SOFT); gtk_box_pack_start(GTK_BOX(self->widget), g->lensshift_h, TRUE, TRUE, 0); g->shear = dt_bauhaus_slider_new_with_range(self, -SHEAR_RANGE, SHEAR_RANGE, 0.01*SHEAR_RANGE, p->shear, 3); dt_bauhaus_widget_set_label(g->shear, NULL, _("shear")); dt_bauhaus_slider_enable_soft_boundaries(g->shear, -SHEAR_RANGE_SOFT, SHEAR_RANGE_SOFT); gtk_box_pack_start(GTK_BOX(self->widget), g->shear, TRUE, TRUE, 0); g->guide_lines = dt_bauhaus_combobox_new(self); dt_bauhaus_widget_set_label(g->guide_lines, NULL, _("guides")); dt_bauhaus_combobox_add(g->guide_lines, _("off")); dt_bauhaus_combobox_add(g->guide_lines, _("on")); gtk_box_pack_start(GTK_BOX(self->widget), g->guide_lines, TRUE, TRUE, 0); g->cropmode = dt_bauhaus_combobox_new(self); dt_bauhaus_widget_set_label(g->cropmode, NULL, _("automatic cropping")); dt_bauhaus_combobox_add(g->cropmode, _("off")); dt_bauhaus_combobox_add(g->cropmode, _("largest area")); dt_bauhaus_combobox_add(g->cropmode, _("original format")); gtk_box_pack_start(GTK_BOX(self->widget), g->cropmode, TRUE, TRUE, 0); g->mode = dt_bauhaus_combobox_new(self); dt_bauhaus_widget_set_label(g->mode, NULL, _("lens model")); dt_bauhaus_combobox_add(g->mode, _("generic")); dt_bauhaus_combobox_add(g->mode, _("specific")); gtk_box_pack_start(GTK_BOX(self->widget), g->mode, TRUE, TRUE, 0); g->f_length = dt_bauhaus_slider_new_with_range(self, 1.0f, 3.0f, 0.01f, 1.0f, 2); dt_bauhaus_widget_set_label(g->f_length, NULL, _("focal length")); dt_bauhaus_slider_set_callback(g->f_length, log10_callback); dt_bauhaus_slider_set_format(g->f_length, "%.0fmm"); dt_bauhaus_slider_set_default(g->f_length, DEFAULT_F_LENGTH); dt_bauhaus_slider_set(g->f_length, DEFAULT_F_LENGTH); dt_bauhaus_slider_enable_soft_boundaries(g->f_length, 1.0f, 2000.0f); gtk_box_pack_start(GTK_BOX(self->widget), g->f_length, TRUE, TRUE, 0); g->crop_factor = dt_bauhaus_slider_new_with_range(self, 1.0f, 2.0f, 0.01f, p->crop_factor, 2); dt_bauhaus_widget_set_label(g->crop_factor, NULL, _("crop factor")); dt_bauhaus_slider_enable_soft_boundaries(g->crop_factor, 0.5f, 10.0f); gtk_box_pack_start(GTK_BOX(self->widget), g->crop_factor, TRUE, TRUE, 0); g->orthocorr = dt_bauhaus_slider_new_with_range(self, 0.0f, 100.0f, 1.0f, p->orthocorr, 2); dt_bauhaus_widget_set_label(g->orthocorr, NULL, _("lens dependence")); dt_bauhaus_slider_set_format(g->orthocorr, "%.0f%%"); #if 0 // this parameter could serve to finetune between generic model (0%) and specific model (100%). // however, users can more easily get the same effect with the aspect adjust parameter so we keep // this one hidden. gtk_box_pack_start(GTK_BOX(self->widget), g->orthocorr, TRUE, TRUE, 0); #endif g->aspect = dt_bauhaus_slider_new_with_range(self, -1.0f, 1.0f, 0.01f, 0.0f, 2); dt_bauhaus_widget_set_label(g->aspect, NULL, _("aspect adjust")); dt_bauhaus_slider_set_callback(g->aspect, log2_callback); dt_bauhaus_slider_set_default(g->aspect, 1.0f); dt_bauhaus_slider_set(g->aspect, 1.0f); gtk_box_pack_start(GTK_BOX(self->widget), g->aspect, TRUE, TRUE, 0); GtkWidget *grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(grid), 2 * DT_BAUHAUS_SPACE); gtk_grid_set_column_spacing(GTK_GRID(grid), DT_PIXEL_APPLY_DPI(10)); GtkWidget *label1 = gtk_label_new(_("automatic fit")); gtk_widget_set_halign(label1, GTK_ALIGN_START); gtk_grid_attach(GTK_GRID(grid), label1, 0, 0, 1, 1); g->fit_v = dtgtk_button_new(dtgtk_cairo_paint_perspective, CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER | 1, NULL); gtk_widget_set_hexpand(GTK_WIDGET(g->fit_v), TRUE); gtk_widget_set_size_request(g->fit_v, -1, DT_PIXEL_APPLY_DPI(24)); gtk_grid_attach_next_to(GTK_GRID(grid), g->fit_v, label1, GTK_POS_RIGHT, 1, 1); g->fit_h = dtgtk_button_new(dtgtk_cairo_paint_perspective, CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER | 2, NULL); gtk_widget_set_hexpand(GTK_WIDGET(g->fit_h), TRUE); gtk_widget_set_size_request(g->fit_h, -1, DT_PIXEL_APPLY_DPI(24)); gtk_grid_attach_next_to(GTK_GRID(grid), g->fit_h, g->fit_v, GTK_POS_RIGHT, 1, 1); g->fit_both = dtgtk_button_new(dtgtk_cairo_paint_perspective, CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER | 3, NULL); gtk_widget_set_hexpand(GTK_WIDGET(g->fit_both), TRUE); gtk_widget_set_size_request(g->fit_both, -1, DT_PIXEL_APPLY_DPI(24)); gtk_grid_attach_next_to(GTK_GRID(grid), g->fit_both, g->fit_h, GTK_POS_RIGHT, 1, 1); GtkWidget *label2 = gtk_label_new(_("get structure")); gtk_widget_set_halign(label2, GTK_ALIGN_START); gtk_grid_attach(GTK_GRID(grid), label2, 0, 1, 1, 1); g->structure = dtgtk_button_new(dtgtk_cairo_paint_structure, CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER, NULL); gtk_widget_set_hexpand(GTK_WIDGET(g->structure), TRUE); gtk_grid_attach_next_to(GTK_GRID(grid), g->structure, label2, GTK_POS_RIGHT, 1, 1); g->clean = dtgtk_button_new(dtgtk_cairo_paint_cancel, CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER, NULL); gtk_widget_set_hexpand(GTK_WIDGET(g->clean), TRUE); gtk_grid_attach_next_to(GTK_GRID(grid), g->clean, g->structure, GTK_POS_RIGHT, 1, 1); g->eye = dtgtk_togglebutton_new(dtgtk_cairo_paint_eye_toggle, CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER, NULL); gtk_widget_set_hexpand(GTK_WIDGET(g->eye), TRUE); gtk_grid_attach_next_to(GTK_GRID(grid), g->eye, g->clean, GTK_POS_RIGHT, 1, 1); gtk_box_pack_start(GTK_BOX(self->widget), grid, TRUE, TRUE, 0); gtk_widget_show_all(g->f_length); gtk_widget_set_no_show_all(g->f_length, TRUE); gtk_widget_show_all(g->crop_factor); gtk_widget_set_no_show_all(g->crop_factor, TRUE); gtk_widget_show_all(g->orthocorr); gtk_widget_set_no_show_all(g->orthocorr, TRUE); gtk_widget_show_all(g->aspect); gtk_widget_set_no_show_all(g->aspect, TRUE); switch(p->mode) { case ASHIFT_MODE_GENERIC: gtk_widget_hide(g->f_length); gtk_widget_hide(g->crop_factor); gtk_widget_hide(g->orthocorr); gtk_widget_hide(g->aspect); break; case ASHIFT_MODE_SPECIFIC: default: gtk_widget_show(g->f_length); gtk_widget_show(g->crop_factor); gtk_widget_show(g->orthocorr); gtk_widget_show(g->aspect); break; } gtk_widget_set_tooltip_text(g->rotation, _("rotate image")); gtk_widget_set_tooltip_text(g->lensshift_v, _("apply lens shift correction in one direction")); gtk_widget_set_tooltip_text(g->lensshift_h, _("apply lens shift correction in one direction")); gtk_widget_set_tooltip_text(g->shear, _("shear the image along one diagonal")); gtk_widget_set_tooltip_text(g->guide_lines, _("display guide lines overlay")); gtk_widget_set_tooltip_text(g->cropmode, _("automatically crop to avoid black edges")); gtk_widget_set_tooltip_text(g->mode, _("lens model of the perspective correction: " "generic or according to the focal length")); gtk_widget_set_tooltip_text(g->f_length, _("focal length of the lens, " "default value set from exif data if available")); gtk_widget_set_tooltip_text(g->crop_factor, _("crop factor of the camera sensor, " "default value set from exif data if available, " "manual setting is often required")); gtk_widget_set_tooltip_text(g->orthocorr, _("the level of lens dependent correction, set to maximum for full lens dependency, " "set to zero for the generic case")); gtk_widget_set_tooltip_text(g->aspect, _("adjust aspect ratio of image by horizontal and vertical scaling")); gtk_widget_set_tooltip_text(g->fit_v, _("automatically correct for vertical perspective distortion\n" "ctrl-click to only fit rotation\n" "shift-click to only fit lens shift")); gtk_widget_set_tooltip_text(g->fit_h, _("automatically correct for horizontal perspective distortion\n" "ctrl-click to only fit rotation\n" "shift-click to only fit lens shift")); gtk_widget_set_tooltip_text(g->fit_both, _("automatically correct for vertical and " "horizontal perspective distortions; fitting rotation," "lens shift in both directions, and shear\n" "ctrl-click to only fit rotation\n" "shift-click to only fit lens shift\n" "ctrl-shift-click to only fit rotation and lens shift")); gtk_widget_set_tooltip_text(g->structure, _("analyse line structure in image\n" "ctrl-click for an additional edge enhancement\n" "shift-click for an additional detail enhancement\n" "ctrl-shift-click for a combination of both methods")); gtk_widget_set_tooltip_text(g->clean, _("remove line structure information")); gtk_widget_set_tooltip_text(g->eye, _("toggle visibility of structure lines")); g_signal_connect(G_OBJECT(g->rotation), "value-changed", G_CALLBACK(rotation_callback), self); g_signal_connect(G_OBJECT(g->lensshift_v), "value-changed", G_CALLBACK(lensshift_v_callback), self); g_signal_connect(G_OBJECT(g->lensshift_h), "value-changed", G_CALLBACK(lensshift_h_callback), self); g_signal_connect(G_OBJECT(g->shear), "value-changed", G_CALLBACK(shear_callback), self); g_signal_connect(G_OBJECT(g->guide_lines), "value-changed", G_CALLBACK(guide_lines_callback), self); g_signal_connect(G_OBJECT(g->cropmode), "value-changed", G_CALLBACK(cropmode_callback), self); g_signal_connect(G_OBJECT(g->mode), "value-changed", G_CALLBACK(mode_callback), self); g_signal_connect(G_OBJECT(g->f_length), "value-changed", G_CALLBACK(f_length_callback), self); g_signal_connect(G_OBJECT(g->crop_factor), "value-changed", G_CALLBACK(crop_factor_callback), self); g_signal_connect(G_OBJECT(g->orthocorr), "value-changed", G_CALLBACK(orthocorr_callback), self); g_signal_connect(G_OBJECT(g->aspect), "value-changed", G_CALLBACK(aspect_callback), self); g_signal_connect(G_OBJECT(g->fit_v), "button-press-event", G_CALLBACK(fit_v_button_clicked), (gpointer)self); g_signal_connect(G_OBJECT(g->fit_h), "button-press-event", G_CALLBACK(fit_h_button_clicked), (gpointer)self); g_signal_connect(G_OBJECT(g->fit_both), "button-press-event", G_CALLBACK(fit_both_button_clicked), (gpointer)self); g_signal_connect(G_OBJECT(g->structure), "button-press-event", G_CALLBACK(structure_button_clicked), (gpointer)self); g_signal_connect(G_OBJECT(g->clean), "clicked", G_CALLBACK(clean_button_clicked), (gpointer)self); g_signal_connect(G_OBJECT(g->eye), "toggled", G_CALLBACK(eye_button_toggled), (gpointer)self); g_signal_connect(G_OBJECT(self->widget), "draw", G_CALLBACK(draw), self); /* add signal handler for preview pipe finish to redraw the overlay */ dt_control_signal_connect(darktable.signals, DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, G_CALLBACK(process_after_preview_callback), self); } void gui_cleanup(struct dt_iop_module_t *self) { dt_control_signal_disconnect(darktable.signals, G_CALLBACK(process_after_preview_callback), self); dt_iop_ashift_gui_data_t *g = (dt_iop_ashift_gui_data_t *)self->gui_data; dt_pthread_mutex_destroy(&g->lock); free(g->lines); free(g->buf); free(g->points); free(g->points_idx); free(self->gui_data); self->gui_data = NULL; } #endif // if 0 //----------------------------------------------------------------------------- // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;