rawTherapee/rtengine/ashift_dt.c
2021-04-27 09:59:52 +02:00

5136 lines
174 KiB
C

/* -*- C++ -*-
*
* This file is part of RawTherapee.
*
* Copyright (c) 2019 Alberto Griggio <alberto.griggio@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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 <http://www.gnu.org/licenses/>.
*/
// 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<float> 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<float> camera_center_yaw_pitch =
rtengine::homogeneous::rotationMatrix<float>(pitch, rtengine::homogeneous::Axis::X) *
rtengine::homogeneous::rotationMatrix<float>(yaw, rtengine::homogeneous::Axis::Y) *
center;
const rtengine::homogeneous::Matrix<float> matrix =
// Perspective correction.
rtengine::homogeneous::projectionMatrix<float>(camera_center_yaw_pitch[2], rtengine::homogeneous::Axis::Z) *
rtengine::homogeneous::rotationMatrix<float>(yaw, rtengine::homogeneous::Axis::Y) *
rtengine::homogeneous::rotationMatrix<float>(pitch, rtengine::homogeneous::Axis::X) *
// Rotation.
rtengine::homogeneous::rotationMatrix<float>(rot, rtengine::homogeneous::Axis::Z) *
// Lens/sensor shift and move to z == camera_focal_length.
rtengine::homogeneous::translationMatrix<float>((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<<closeup, 1);
// we draw the cropping area; we need x_off/y_off/width/height which is only available
// after g->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<<closeup, 0);
const float cur_scale = dt_dev_get_zoom_scale(self->dev, zoom, 1<<closeup, 0);
// if we are zoomed out (no panning possible) and we have lines to display we take control
const int take_control = (cur_scale == min_scale) && (g->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;