/*
This file is part of darktable,
Copyright (C) 2016-2026 darktable developers.
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see .
*/
#include "common/gdk_event_utils.h"
#include "bauhaus/bauhaus.h"
#include "common/bilateral.h"
#include "common/colorspaces_inline_conversions.h"
#include "common/debug.h"
#include "common/imagebuf.h"
#include "common/interpolation.h"
#include "common/math.h"
#include "common/matrices.h"
#include "common/opencl.h"
#include "control/control.h"
#include "develop/develop.h"
#include "develop/imageop.h"
#include "develop/imageop_gui.h"
#include "dtgtk/button.h"
#include "dtgtk/expander.h"
#include "gui/accelerators.h"
#include "gui/draw.h"
#include "gui/gtk.h"
#include "iop/iop_api.h"
#include "gui/guides.h"
#include
#include
#include
#include
#include
#include
// 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 180 // allowed min/max range for rotation parameter with manual adjustment
#define LENSSHIFT_RANGE 1.0 // allowed min/max default range for lensshift parameters
#define LENSSHIFT_RANGE_SOFT 2.0 // 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 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 2 // 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
// maximum number of drawn lines that can be saved in parameters
// any change in this value needs to upgrade parameters version !
#define MAX_SAVED_LINES 50
// 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"
DT_MODULE_INTROSPECTION(5, dt_iop_ashift_params_t)
const char *name()
{
return _("rotate and perspective");
}
const char *aliases()
{
return _("rotation|keystone|distortion|crop|reframe");
}
const char **description(dt_iop_module_t *self)
{
return dt_iop_set_description(self, _("rotate or distort perspective"),
_("corrective or creative"),
_("linear, RGB, scene-referred"),
_("geometric, RGB"),
_("linear, RGB, scene-referred"));
}
int flags()
{
return IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_TILING_FULL_ROI | IOP_FLAGS_ONE_INSTANCE
| IOP_FLAGS_ALLOW_FAST_PIPE
| IOP_FLAGS_GUIDES_SPECIAL_DRAW | IOP_FLAGS_GUIDES_WIDGET;
}
int default_group()
{
return IOP_GROUP_CORRECT | IOP_GROUP_TECHNICAL;
}
int operation_tags()
{
return IOP_TAG_DISTORT;
}
int operation_tags_filter()
{
// switch off cropping and decoration, we want to see the full image.
return IOP_TAG_DECORATION | IOP_TAG_CROPPING;
}
dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
return IOP_CS_RGB;
}
typedef enum dt_iop_ashift_method_t
{
ASHIFT_METHOD_NONE = 0,
ASHIFT_METHOD_AUTO,
ASHIFT_METHOD_QUAD,
ASHIFT_METHOD_LINES
} dt_iop_ashift_method_t;
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
{
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 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
{
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 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, // $DESCRIPTION: "generic"
ASHIFT_MODE_SPECIFIC = 1 // $DESCRIPTION: "specific"
} dt_iop_ashift_mode_t;
typedef enum dt_iop_ashift_crop_t
{
ASHIFT_CROP_OFF = 0, // $DESCRIPTION: "off"
ASHIFT_CROP_LARGEST = 1,// $DESCRIPTION: "largest area"
ASHIFT_CROP_ASPECT = 2 // $DESCRIPTION: "original format"
} 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,
ASHIFT_JOBCODE_GET_STRUCTURE_LINES = 3,
ASHIFT_JOBCODE_GET_STRUCTURE_QUAD = 4,
ASHIFT_JOBCODE_DO_CROP = 5
} dt_iop_ashift_jobcode_t;
typedef struct dt_iop_ashift_params_t
{
float rotation; // $MIN: -ROTATION_RANGE_SOFT $MAX: ROTATION_RANGE_SOFT $DEFAULT: 0.0
float lensshift_v; // $MIN: -LENSSHIFT_RANGE_SOFT $MAX: LENSSHIFT_RANGE_SOFT $DEFAULT: 0.0 $DESCRIPTION: "lens shift (vertical)"
float lensshift_h; // $MIN: -LENSSHIFT_RANGE_SOFT $MAX: LENSSHIFT_RANGE_SOFT $DEFAULT: 0.0 $DESCRIPTION: "lens shift (horizontal)"
float shear; // $MIN: -SHEAR_RANGE_SOFT $MAX: SHEAR_RANGE_SOFT $DEFAULT: 0.0 $DESCRIPTION: "shear"
float f_length; // $MIN: 1.0 $MAX: 2000.0 $DEFAULT: DEFAULT_F_LENGTH $DESCRIPTION: "focal length"
float crop_factor; // $MIN: 0.5 $MAX: 10.0 $DEFAULT: 1.0 $DESCRIPTION: "crop factor"
float orthocorr; // $MIN: 0.0 $MAX: 100.0 $DEFAULT: 100.0 $DESCRIPTION: "lens dependence"
float aspect; // $MIN: 0.5 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "aspect adjust"
dt_iop_ashift_mode_t mode; // $DEFAULT: ASHIFT_MODE_GENERIC $DESCRIPTION: "lens model"
dt_iop_ashift_crop_t cropmode; // $DEFAULT: ASHIFT_CROP_LARGEST $DESCRIPTION: "automatic cropping"
float cl; // $DEFAULT: 0.0 $MIN: 0.0 $MAX: 1.0
float cr; // $DEFAULT: 1.0 $MIN: 0.0 $MAX: 1.0
float ct; // $DEFAULT: 0.0 $MIN: 0.0 $MAX: 1.0
float cb; // $DEFAULT: 1.0 $MIN: 0.0 $MAX: 1.0
float last_drawn_lines[MAX_SAVED_LINES * 4];
int last_drawn_lines_count;
float last_quad_lines[8];
} 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 DT_ALIGNED_PIXEL 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 rotation_range;
float lensshift_v_range;
float lensshift_h_range;
float shear_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 DT_ALIGNED_ARRAY homograph[3][3];
float DT_ALIGNED_ARRAY 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 *cropmode;
GtkWidget *mode;
GtkWidget *specifics;
GtkWidget *f_length;
GtkWidget *crop_factor;
GtkWidget *orthocorr;
GtkWidget *aspect;
GtkWidget *fit_v;
GtkWidget *fit_h;
GtkWidget *fit_both;
GtkWidget *structure_auto;
GtkWidget *structure_quad;
GtkWidget *structure_lines;
gboolean straightening;
float straighten_x;
float straighten_y;
int fitting;
int isflipped;
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;
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;
dt_hash_t lines_hash;
dt_hash_t grid_hash;
dt_hash_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;
gboolean adjust_crop;
float cl; // shadow copy of dt_iop_ashift_data_t.cl
float cr; // shadow copy of dt_iop_ashift_data_t.cr
float ct; // shadow copy of dt_iop_ashift_data_t.ct
float cb; // shadow copy of dt_iop_ashift_data_t.cb
dt_iop_ashift_method_t current_structure_method;
int draw_near_point;
gboolean draw_point_move;
int draw_line_move;
float draw_pointmove_x;
float draw_pointmove_y;
float *draw_points;
dt_gui_collapsible_section_t cs;
} 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;
int legacy_params(dt_iop_module_t *self,
const void *const old_params,
const int old_version,
void **new_params,
int32_t *new_params_size,
int *new_version)
{
typedef struct dt_iop_ashift_params_v5_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;
dt_iop_ashift_crop_t cropmode;
float cl;
float cr;
float ct;
float cb;
float last_drawn_lines[MAX_SAVED_LINES * 4];
int last_drawn_lines_count;
float last_quad_lines[8];
} dt_iop_ashift_params_v5_t;
if(old_version == 1)
{
typedef struct dt_iop_ashift_params_v1_t
{
float rotation;
float lensshift_v;
float lensshift_h;
int toggle;
} dt_iop_ashift_params_v1_t;
const dt_iop_ashift_params_v1_t *o = old_params;
dt_iop_ashift_params_v5_t *n = malloc(sizeof(dt_iop_ashift_params_v5_t));
n->rotation = o->rotation;
n->lensshift_v = o->lensshift_v;
n->lensshift_h = o->lensshift_h;
n->shear = 0.0f;
n->f_length = DEFAULT_F_LENGTH;
n->crop_factor = 1.0f;
n->orthocorr = 100.0f;
n->aspect = 1.0f;
n->mode = ASHIFT_MODE_GENERIC;
n->cropmode = ASHIFT_CROP_OFF;
n->cl = 0.0f;
n->cr = 1.0f;
n->ct = 0.0f;
n->cb = 1.0f;
for(int i = 0; i < MAX_SAVED_LINES * 4; i++) n->last_drawn_lines[i] = 0.0f;
for(int i = 0; i < 8; i++) n->last_quad_lines[i] = 0.0f;
n->last_drawn_lines_count = 0;
*new_params = n;
*new_params_size = sizeof(dt_iop_ashift_params_v5_t);
*new_version = 5;
return 0;
}
if(old_version == 2)
{
typedef struct dt_iop_ashift_params_v2_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_params_v2_t;
const dt_iop_ashift_params_v2_t *o = old_params;
dt_iop_ashift_params_v5_t *n = malloc(sizeof(dt_iop_ashift_params_v5_t));
n->rotation = o->rotation;
n->lensshift_v = o->lensshift_v;
n->lensshift_h = o->lensshift_h;
n->shear = 0.0f;
n->f_length = o->f_length;
n->crop_factor = o->crop_factor;
n->orthocorr = o->orthocorr;
n->aspect = o->aspect;
n->mode = o->mode;
n->cropmode = ASHIFT_CROP_OFF;
n->cl = 0.0f;
n->cr = 1.0f;
n->ct = 0.0f;
n->cb = 1.0f;
for(int i = 0; i < MAX_SAVED_LINES * 4; i++) n->last_drawn_lines[i] = 0.0f;
for(int i = 0; i < 8; i++) n->last_quad_lines[i] = 0.0f;
n->last_drawn_lines_count = 0;
*new_params = n;
*new_params_size = sizeof(dt_iop_ashift_params_v5_t);
*new_version = 5;
return 0;
}
if(old_version == 3)
{
typedef struct dt_iop_ashift_params_v3_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_params_v3_t;
const dt_iop_ashift_params_v3_t *o = old_params;
dt_iop_ashift_params_v5_t *n = malloc(sizeof(dt_iop_ashift_params_v5_t));
n->rotation = o->rotation;
n->lensshift_v = o->lensshift_v;
n->lensshift_h = o->lensshift_h;
n->shear = 0.0f;
n->f_length = o->f_length;
n->crop_factor = o->crop_factor;
n->orthocorr = o->orthocorr;
n->aspect = o->aspect;
n->mode = o->mode;
n->cropmode = o->cropmode;
n->cl = o->cl;
n->cr = o->cr;
n->ct = o->ct;
n->cb = o->cb;
for(int i = 0; i < MAX_SAVED_LINES * 4; i++) n->last_drawn_lines[i] = 0.0f;
for(int i = 0; i < 8; i++) n->last_quad_lines[i] = 0.0f;
n->last_drawn_lines_count = 0;
*new_params = n;
*new_params_size = sizeof(dt_iop_ashift_params_v5_t);
*new_version = 5;
return 0;
}
if(old_version == 4)
{
typedef struct dt_iop_ashift_params_v4_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;
} dt_iop_ashift_params_v4_t;
const dt_iop_ashift_params_v4_t *o = old_params;
dt_iop_ashift_params_v5_t *n = malloc(sizeof(dt_iop_ashift_params_v5_t));
n->rotation = o->rotation;
n->lensshift_v = o->lensshift_v;
n->lensshift_h = o->lensshift_h;
n->shear = o->shear;
n->f_length = o->f_length;
n->crop_factor = o->crop_factor;
n->orthocorr = o->orthocorr;
n->aspect = o->aspect;
n->mode = o->mode;
n->cropmode = o->cropmode;
n->cl = o->cl;
n->cr = o->cr;
n->ct = o->ct;
n->cb = o->cb;
for(int i = 0; i < MAX_SAVED_LINES * 4; i++) n->last_drawn_lines[i] = 0.0f;
for(int i = 0; i < 8; i++) n->last_quad_lines[i] = 0.0f;
n->last_drawn_lines_count = 0;
*new_params = n;
*new_params_size = sizeof(dt_iop_ashift_params_v5_t);
*new_version = 5;
return 0;
}
return 1;
}
// normalized product of two 3x1 vectors
// dst needs to be different from v1 and v2
// same accumulation order and fma as the ashift OpenCL kernels, so both sides round identically
static inline void _mat3mulv_fma(float *dst, const float *const mat, const float *const v)
{
for(int i = 0; i < 3; i++)
{
dst[i] = 0.0f;
for(int j = 0; j < 3; j++) dst[i] = fmaf(mat[3 * i + j], v[j], dst[i]);
}
}
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 = sqrtf(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 = sqrtf(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 = dt_fast_hypotf(v[0], 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 (fabsf(v[0]) < eps && fabsf(v[1]) < eps && fabsf(v[2]) < eps);
}
static inline void _shadow_crop_box(const dt_iop_ashift_params_t *p,
dt_iop_ashift_gui_data_t *g)
{
// copy actual crop box values into shadow variables
g->cl = p->cl;
g->cr = p->cr;
g->ct = p->ct;
g->cb = p->cb;
}
static void _clear_shadow_crop_box(dt_iop_ashift_gui_data_t *g)
{
// reset the crop to the full image
g->cl = 0.0f;
g->cr = 1.0f;
g->ct = 0.0f;
g->cb = 1.0f;
}
static inline void _commit_crop_box(dt_iop_ashift_params_t *p,
const dt_iop_ashift_gui_data_t *g)
{
// copy shadow values for crop box into actual parameters
p->cl = g->cl;
p->cr = g->cr;
p->ct = g->ct;
p->cb = g->cb;
}
static inline void _swap_shadow_crop_box(dt_iop_ashift_params_t *p,
dt_iop_ashift_gui_data_t *g)
{
// exchange shadow values and actual crop values this is needed for
// a temporary commit to be able to properly update the undo history
float tmp;
tmp = p->cl; p->cl = g->cl; g->cl = tmp;
tmp = p->cr; p->cr = g->cr; g->cr = tmp;
tmp = p->ct; p->ct = g->ct; g->ct = tmp;
tmp = p->cb; p->cb = g->cb; g->cb = tmp;
}
// process() and process_cl() publish g->buf and the geometry describing it in
// one critical section; a reader taking the fields one by one can pair a new
// width with an old height and fit a crop to a size that never existed, so
// snapshot them together
typedef struct dt_iop_ashift_bufgeom_t
{
gboolean valid; // g->buf was allocated when the snapshot was taken
int width;
int height;
int x_off;
int y_off;
} dt_iop_ashift_bufgeom_t;
static dt_iop_ashift_bufgeom_t _get_buf_geometry(dt_iop_module_t *self)
{
const dt_iop_ashift_gui_data_t *g = self->gui_data;
dt_iop_ashift_bufgeom_t b;
dt_iop_gui_enter_critical_section(self);
b.valid = g->buf != NULL;
b.width = g->buf_width;
b.height = g->buf_height;
b.x_off = g->buf_x_off;
b.y_off = g->buf_y_off;
dt_iop_gui_leave_critical_section(self);
return b;
}
// isflipped is published by the same two writers, and stays -1 until the
// preview pipe has run once
static int _get_isflipped(dt_iop_module_t *self)
{
const dt_iop_ashift_gui_data_t *g = self->gui_data;
dt_iop_gui_enter_critical_section(self);
const int isflipped = g->isflipped;
dt_iop_gui_leave_critical_section(self);
return isflipped;
}
#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,
const 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 phi = deg2radf(angle);
const float cosi = cosf(phi);
const float sini = sinf(phi);
const float ascale = sqrtf(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 = expf(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(atanf(rad_v), -1.5f, 1.5f);
const float rt_v = sinf(0.5f * alpha_v);
const float r_v = fmaxf(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 = expf(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(atanf(rad_h), -1.5f, 1.5f);
const float rt_h = sinf(0.5f * alpha_h);
const float r_h = fmaxf(0.1f, 2.0f * (vertifac - 1.0f) * rt_h * rt_h + 1.0f);
// three intermediate buffers for matrix calculation ...
float DT_ALIGNED_ARRAY m1[3][3];
float DT_ALIGNED_ARRAY m2[3][3];
float DT_ALIGNED_ARRAY 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, sizeof(float) * 9);
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, sizeof(float) * 9);
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, sizeof(float) * 9);
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, sizeof(float) * 9);
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, sizeof(float) * 9);
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, sizeof(float) * 9);
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, sizeof(float) * 9);
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, sizeof(float) * 9);
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, sizeof(float) * 9);
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);
// 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 = MIN(umin, po[0] / po[2]);
vmin = MIN(vmin, po[1] / po[2]);
}
memset(mwork, 0, sizeof(float) * 9);
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, sizeof(float) * 9);
}
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, sizeof(float) * 9);
mwork[0][0] = 1.0f;
mwork[1][1] = 1.0f;
mwork[2][2] = 1.0f;
memcpy(homograph, mwork, sizeof(float) * 9);
}
}
}
#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 gboolean _isneutral(const dt_iop_ashift_data_t *data)
{
// values lower than this have no visible effect
const float eps = 1.0e-4f;
return(feqf(data->rotation, 0.0f, eps) &&
feqf(data->lensshift_v, 0.0f, eps) &&
feqf(data->lensshift_h, 0.0f, eps) &&
feqf(data->shear, 0, eps) &&
feqf(data->aspect, 1.0f, eps) &&
feqf(data->cl, 0.0f, eps) &&
feqf(data->cr, 1.0f, eps) &&
feqf(data->ct, 0.0f, eps) &&
feqf(data->cb, 1.0f, eps));
}
gboolean distort_transform(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
float *const restrict points,
const size_t points_count)
{
const dt_iop_ashift_data_t *const data = piece->data;
// nothing to be done if parameters are set to neutral values
if(_isneutral(data)) return TRUE;
float DT_ALIGNED_ARRAY 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;
float *const pts = DT_IS_ALIGNED(points);
DT_OMP_FOR(if(points_count > 100))
for(size_t i = 0; i < points_count * 2; i += 2)
{
const float DT_ALIGNED_PIXEL pi[3] = { pts[i], pts[i + 1], 1.0f };
float DT_ALIGNED_PIXEL po[3];
mat3mulv(po, (float *)homograph, pi);
pts[i] = po[0] / po[2] - cx;
pts[i + 1] = po[1] / po[2] - cy;
}
return TRUE;
}
gboolean distort_backtransform(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
float *points,
const size_t points_count)
{
const dt_iop_ashift_data_t *const data = piece->data;
// nothing to be done if parameters are set to neutral values
if(_isneutral(data)) return TRUE;
float DT_ALIGNED_ARRAY 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;
float *const pts = DT_IS_ALIGNED(points);
DT_OMP_FOR(if(points_count > 100))
for(size_t i = 0; i < points_count * 2; i += 2)
{
const float DT_ALIGNED_PIXEL pi[3] = { pts[i] + cx, pts[i + 1] + cy, 1.0f };
float DT_ALIGNED_PIXEL po[3];
mat3mulv(po, (float *)ihomograph, pi);
pts[i] = po[0] / po[2];
pts[i + 1] = po[1] / po[2];
}
return TRUE;
}
void distort_mask(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const float *const in,
float *const out,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out)
{
const dt_iop_ashift_data_t *const data = piece->data;
// if module is set to neutral parameters we just copy input->output and are done
if(_isneutral(data))
{
dt_iop_image_copy_by_size(out, in, roi_out->width, roi_out->height, 1);
return;
}
const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP);
float DT_ALIGNED_ARRAY 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;
DT_OMP_FOR(shared(ihomograph))
// go over all pixels of output image
for(int j = 0; j < roi_out->height; j++)
{
float *const restrict _out = out + (size_t)j * roi_out->width;
for(int i = 0; i < roi_out->width; i++)
{
float DT_ALIGNED_PIXEL pin[3], DT_ALIGNED_PIXEL pout[3];
// 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;
pin[0] -= roi_in->x;
pin[1] -= roi_in->y;
// get output values by interpolation from input image
_out[i] = CLIP(dt_interpolation_compute_sample(interpolation, in,
pin[0], pin[1],
roi_in->width, roi_in->height, 1, roi_in->width));
}
}
}
void modify_roi_out(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
dt_iop_roi_t *roi_out,
const dt_iop_roi_t *roi_in)
{
const dt_iop_ashift_data_t *data = piece->data;
*roi_out = *roi_in;
// nothing more to be done if parameters are set to neutral values
if(_isneutral(data)) return;
float DT_ALIGNED_ARRAY 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 DT_ALIGNED_PIXEL pin[3], DT_ALIGNED_PIXEL 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]);
}
}
const float width = (xM - xm + 1.0f) * (data->cr - data->cl);
const float height = (yM - ym + 1.0f) * (data->cb - data->ct);
roi_out->width = floorf(width);
roi_out->height = floorf(height);
if(roi_out->width < 4 || roi_out->height < 4)
{
dt_print_pipe(DT_DEBUG_PIPE,
"insane data", piece->pipe, self, DT_DEVICE_NONE, roi_in, roi_out);
if(dt_pipe_is_full(piece->pipe))
{
dt_control_log
(_("module '%s' has insane data so it is bypassed for now."
" you should disable it or change parameters\n"),
self->name());
}
roi_out->width = roi_in->width;
roi_out->height = roi_in->height;
piece->enabled = FALSE;
}
}
void modify_roi_in(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const dt_iop_roi_t *const roi_out,
dt_iop_roi_t *roi_in)
{
const dt_iop_ashift_data_t *data = piece->data;
*roi_in = *roi_out;
// nothing more to be done if parameters are set to neutral values
if(_isneutral(data)) return;
float DT_ALIGNED_ARRAY 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 DT_ALIGNED_PIXEL pin[3], DT_ALIGNED_PIXEL 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 dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP);
const float iw1 = interpolation->width;
const float iw2 = 2.0f * iw1;
roi_in->x = xm - iw1;
roi_in->y = ym - iw1;
roi_in->width = xM + iw2 - xm + 1.0f;
roi_in->height = yM + iw2 - ym + 1.0f;
// 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, 4, (int)floorf(orig_w) - roi_in->x);
roi_in->height = CLAMP(roi_in->height, 4, (int)floorf(orig_h) - roi_in->y);
}
// 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 *const in,
double *const out,
const int width,
const int height)
{
const size_t npixels = (size_t)width * height;
DT_OMP_FOR()
for(size_t index = 0; index < npixels; index++)
{
out[index] = (0.3f * in[4*index+0]
+ 0.59f * in[4*index+1]
+ 0.11f * in[4*index+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,
const 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;
DT_OMP_FOR()
// 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;
}
}
DT_OMP_FOR()
// 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[(size_t)j * width + i];
if(j < khwidth)
val = out[(size_t)(khwidth - j) * width + i];
else if(j >= height - khwidth)
val = out[(size_t)(j - khwidth) * width + i];
else if(i < khwidth)
val = out[(size_t)j * width + (khwidth - i)];
else if(i >= width - khwidth)
val = out[(size_t)j * width + (i - khwidth)];
out[(size_t)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 gboolean edge_enhance(const double *in,
double *out,
const int width,
const int height)
{
double *Gx = NULL;
double *Gy = NULL;
Gx = malloc(sizeof(double) * width * height);
if(Gx == NULL) goto error;
Gy = malloc(sizeof(double) * width * height);
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
DT_OMP_FOR()
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;
}
// detail enhancement via bilateral grid (function arguments in and
// out may represent identical buffers)
static gboolean detail_enhance(const float *const in,
float *const out,
const int width,
const int height)
{
const float sigma_r = 5.0f;
const float sigma_s = fminf(width, height) * 0.02f;
const float detail = 10.0f;
const size_t npixels = (size_t)width * height;
gboolean 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
DT_OMP_FOR()
for(size_t index = 0; index < 4*npixels; index += 4)
{
dt_aligned_pixel_t XYZ;
dt_Rec709_to_XYZ_D50(in + index, XYZ); // convert linear sRBG to XYZ
dt_XYZ_to_Lab(XYZ, out + index);
}
// 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
DT_OMP_FOR()
for(size_t index = 0; index < 4*npixels; index += 4)
{
dt_aligned_pixel_t XYZ;
dt_Lab_to_XYZ(out + index, XYZ);
dt_XYZ_to_linearRGB(XYZ, out + index);
}
return success;
}
// apply gamma correction to RGB buffer (function arguments in and out
// may represent identical buffers)
static void gamma_correct(const float *const in,
float *const out,
const int width,
const int height)
{
const size_t npixels = (size_t)width * height;
DT_OMP_FOR()
for(size_t index = 0; index < 4 * npixels; index += 4)
{
for_three_channels(c)
out[index+c] = powf(in[index+c], LSD_GAMMA);
}
}
// do actual line_detection based on LSD algorithm and return results according
// to this module's conventions
static gboolean 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,
const dt_iop_ashift_enhance_t enhance,
const gboolean 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;
// 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 = malloc(sizeof(double) * width * height);
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]'
int 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);
// we count the lines that we really want to use
int lct = 0;
if(lines_count > 0)
{
// aggregate lines data into our own structures
ashift_lines = malloc(sizeof(dt_iop_ashift_line_t) * lines_count);
if(ashift_lines == NULL) goto error;
for(int n = 0; n < lines_count; n++)
{
const float x1 = lsd_lines[n * 7 + 0];
const float y1 = lsd_lines[n * 7 + 1];
const float x2 = lsd_lines[n * 7 + 2];
const 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((fabsf(x1 - x2) < 1 && fmaxf(x1, x2) < 2)
|| (fabsf(x1 - x2) < 1 && fminf(x1, x2) > width - 3)
|| (fabsf(y1 - y2) < 1 && fmaxf(y1, y2) < 2)
|| (fabsf(y1 - y2) < 1 && fminf(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 (= length * width * angle precision)
const float weight = ashift_lines[lct].length
* ashift_lines[lct].width
* lsd_lines[n * 7 + 5];
ashift_lines[lct].weight = weight;
const float angle = rad2degf(atan2f(py2 - py1, px2 - px1));
const gboolean vertical = fabsf(fabsf(angle) - 90.0f) < MAX_TANGENTIAL_DEVIATION;
const gboolean horizontal = fabsf(fabsf(fabsf(angle) - 90.0f) - 90.0f) < MAX_TANGENTIAL_DEVIATION;
const gboolean relevant = ashift_lines[lct].length > MIN_LINE_LENGTH;
// 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;
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 *self,
const dt_iop_ashift_enhance_t enhance)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
float *buffer = NULL;
int width = 0;
int height = 0;
int x_off = 0;
int y_off = 0;
float scale = 0.0f;
dt_iop_gui_enter_critical_section(self);
// 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 = dt_alloc_align_float((size_t)4 * width * height);
if(buffer != NULL)
dt_iop_image_copy_by_size(buffer, g->buf, width, height, 4);
}
dt_iop_gui_leave_critical_section(self);
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, dt_image_is_raw(&self->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 = lines;
dt_free_align(buffer);
return TRUE;
error:
dt_free_align(buffer);
return FALSE;
}
// swap two integer values
static inline void swap(int *a, int *b)
{
const int tmp = *a;
*a = *b;
*b = tmp;
}
// do complete permutations
static gboolean quickperm(int *a, int *p, const int N, int *i)
{
if(*i >= N) return FALSE;
p[*i]--;
const 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++)
{
const 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 = malloc(set_size);
memcpy(best_set, index_set, set_size);
int *best_inout = 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 = powf(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 = malloc(sizeof(int) * (set_count + 1));
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 = 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 DT_ALIGNED_PIXEL 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) ||
(fabsf(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 = fabsf(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 = powf(10.0f, log10(epsilon) - epsilon_step);
else if(ratio > RANSAC_ELIMINATION_RATIO)
epsilon = powf(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 gboolean _remove_outliers(const dt_iop_module_t *self)
{
dt_iop_ashift_gui_data_t *g = self->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;
if(g->lines_count < 1)
return TRUE;
// holds the index set of lines we want to work on
int *lines_set = malloc(sizeof(int) * g->lines_count);
// holds the result of ransac
int *inout_set = malloc(sizeof(int) * g->lines_count);
if(!lines_set || !inout_set)
goto error;
// 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(const double x, const double min, const 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
const 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(const double L, const double min, const double max)
{
const 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)
{
const dt_iop_ashift_fit_params_t *fit = (dt_iop_ashift_fit_params_t *)data;
// just for convenience: get shorter names
const 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;
const float rotation_range = fit->rotation_range;
const float lensshift_v_range = fit->lensshift_v_range;
const float lensshift_h_range = fit->lensshift_h_range;
const float shear_range = fit->shear_range;
int pcount = 0;
// fill in fit parameters from params[]. Attention: order matters!!!
if(dt_isnan(rotation))
{
rotation = ilogit(params[pcount], -rotation_range, rotation_range);
pcount++;
}
if(dt_isnan(lensshift_v))
{
lensshift_v = ilogit(params[pcount], -lensshift_v_range, lensshift_v_range);
pcount++;
}
if(dt_isnan(lensshift_h))
{
lensshift_h = ilogit(params[pcount], -lensshift_h_range, lensshift_h_range);
pcount++;
}
if(dt_isnan(shear))
{
shear = ilogit(params[pcount], -shear_range, shear_range);
pcount++;
}
assert(pcount == fit->params_count);
// the possible reference axes
const float DT_ALIGNED_PIXEL Av[3] = { 1.0f, 0.0f, 0.0f };
const float DT_ALIGNED_PIXEL Ah[3] = { 0.0f, 1.0f, 0.0f };
// generate homograph out of the parameters
float DT_ALIGNED_ARRAY homograph[3][3];
_homography((float *)homograph, rotation, lensshift_v, lensshift_h, shear, 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 DT_ALIGNED_PIXEL P1[3];
float DT_ALIGNED_PIXEL P2[3];
mat3mulv(P1, (float *)homograph, lines[n].p1);
mat3mulv(P2, (float *)homograph, lines[n].p2);
// get line connecting the two points
float DT_ALIGNED_PIXEL 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
const float s = vec3scalar(L, A);
// sum up weighted s^2 for both directions individually
sumsq_v += isvertical ? s * s * lines[n].weight : 0.0;
weight_v += isvertical ? lines[n].weight : 0.0;
count_v += isvertical ? 1 : 0;
sumsq_h += !isvertical ? 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);
#endif
return sum;
}
// setup all data structures for fitting and call NM simplex
static dt_iop_ashift_nmsresult_t nmsfit(dt_iop_module_t *self,
dt_iop_ashift_params_t *p,
const dt_iop_ashift_fitaxis_t dir)
{
const dt_iop_ashift_gui_data_t *g = self->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)
? 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.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.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 ^= _get_isflipped(self) ? 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(fit.rotation, -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_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;
}
// 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
const 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 = dt_isnan(fit.rotation)
? ilogit(params[pcount++], -fit.rotation_range, fit.rotation_range)
: fit.rotation;
fit.lensshift_v = dt_isnan(fit.lensshift_v)
? ilogit(params[pcount++], -fit.lensshift_v_range, fit.lensshift_v_range)
: fit.lensshift_v;
fit.lensshift_h = dt_isnan(fit.lensshift_h)
? ilogit(params[pcount++], -fit.lensshift_h_range, fit.lensshift_h_range)
: fit.lensshift_h;
fit.shear = dt_isnan(fit.shear)
? ilogit(params[pcount++], -fit.shear_range, fit.shear_range)
: fit.shear;
#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);
#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 DT_ALIGNED_ARRAY 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 DT_ALIGNED_PIXEL pi[3], DT_ALIGNED_PIXEL 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 = MIN(xm, po[0]);
ym = MIN(ym, po[1]);
xM = MAX(xM, po[0]);
yM = MAX(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;
return NMS_SUCCESS;
}
#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 *self,
dt_iop_ashift_params_t *p,
const dt_iop_ashift_fitaxis_t dir)
{
dt_iop_ashift_gui_data_t *g = self->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 ^= _get_isflipped(self) ? 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
// function to keep crop fitting parameters within constraints
static void crop_constraint(double *params, const 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)
{
const 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 = dt_isnan(cropfit->x) ? params[0] : cropfit->x;
const float y = dt_isnan(cropfit->y) ? params[1] : cropfit->y;
const float alpha = dt_isnan(cropfit->alpha) ? params[2] : cropfit->alpha;
// the center of the rectangle in input image coordinates
const float DT_ALIGNED_PIXEL Pc[3] = { x * wd, y * ht, 1.0f };
// convert to the output image coordinates and normalize
float DT_ALIGNED_PIXEL 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 DT_ALIGNED_ARRAY Pa[2][3] =
{ { P[0] + 10.0f * cosf(alpha), P[1] + 10.0f * sinf(alpha), 1.0f },
{ P[0] + 10.0f * cosf(alpha), P[1] - 10.0f * sinf(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 = sqrf(P[0] - I[0]) + sqrf(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 * sinf(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;
}
// 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 *self, dt_iop_ashift_params_t *p)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
const dt_iop_ashift_bufgeom_t b = _get_buf_geometry(self);
// if sizes are not ready (module disabled), just ignore this
if(b.width == 0 || b.height == 0) return;
// skip if fitting is still running
if(g->fitting) return;
// reset fit margins if auto-cropping is off
if(p->cropmode == ASHIFT_CROP_OFF)
{
_clear_shadow_crop_box(g);
_commit_crop_box(p, g);
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 DT_ALIGNED_ARRAY cropfit;
cropfit.width = b.width;
cropfit.height = b.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] = atan2f((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 = atan2f((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);
// 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 = dt_isnan(cropfit.x) ? params[0] : cropfit.x;
cropfit.y = dt_isnan(cropfit.y) ? params[1] : cropfit.y;
cropfit.alpha = dt_isnan(cropfit.alpha) ? params[2] : cropfit.alpha;
// the area of the best fitting rectangle
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
const float d = sqrtf(A / (2.0f * sinf(2.0f * cropfit.alpha)));
// the rectangle's center in input image (homogeneous) coordinates
const float Pc[3] = { cropfit.x * wd, cropfit.y * ht, 1.0f };
// convert rectangle center to output image coordinates and normalize
float DT_ALIGNED_PIXEL P[3];
mat3mulv(P, (float *)cropfit.homograph, Pc);
P[0] /= P[2];
P[1] /= P[2];
// calculate clipping margins relative to output image dimensions
g->cl = CLAMP((P[0] - d * cosf(cropfit.alpha)) / owd, 0.0f, 1.0f);
g->cr = CLAMP((P[0] + d * cosf(cropfit.alpha)) / owd, 0.0f, 1.0f);
g->ct = CLAMP((P[1] - d * sinf(cropfit.alpha)) / oht, 0.0f, 1.0f);
g->cb = CLAMP((P[1] + d * sinf(cropfit.alpha)) / oht, 0.0f, 1.0f);
// final sanity check
if(g->cr - g->cl <= 0.0f || g->cb - g->ct <= 0.0f) goto failed;
g->fitting = 0;
dt_print(DT_DEBUG_PIPE | DT_DEBUG_VERBOSE,
"margins after crop fitting: iter=%d x=%.4f y=%.4f angle=%.4f"
" crop area (%.4f %.4f %.4f %.4f) wd=%i ht=%i",
iter, cropfit.x, cropfit.y, cropfit.alpha,
g->cl, g->cr, g->ct, g->cb, cropfit.width, cropfit.height);
dt_control_queue_redraw_center();
return;
failed:
// in case of failure: reset clipping margins, set "automatic cropping" parameter
// to "off" state, and display warning message
_clear_shadow_crop_box(g);
_commit_crop_box(p, g);
p->cropmode = ASHIFT_CROP_OFF;
dt_bauhaus_combobox_set(g->cropmode, p->cropmode);
g->fitting = 0;
dt_control_log(_("automatic cropping failed"));
return;
}
// manually adjust crop area by shifting its center
static void crop_adjust(dt_iop_module_t *self,
const dt_iop_ashift_params_t *const p,
const float newx,
const float newy)
{
dt_iop_ashift_gui_data_t *g = self->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 dt_iop_ashift_bufgeom_t b = _get_buf_geometry(self);
const float wd = b.width;
const float ht = b.height;
const float alpha = atan2f(ht, wd);
float DT_ALIGNED_ARRAY 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 DT_ALIGNED_PIXEL 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 * cosf(alpha), P[1] + 10.0f * sinf(alpha), 1.0f },
{ P[0] + 10.0f * cosf(alpha), P[1] - 10.0f * sinf(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 = sqrf(P[0] - I[0]) + sqrf(P[1] - I[1]);
// the minimum distance over all intersection points
d2min = MIN(d2min, d2);
}
const float d = sqrtf(d2min);
// do not allow crop area to drop below 1% of input image area
const float A = 2.0f * d * d * sinf(2.0f * alpha);
if(A < 0.01f * wd * ht) return;
// calculate clipping margins relative to output image dimensions
g->cl = CLAMP((P[0] - d * cosf(alpha)) / owd, 0.0f, 1.0f);
g->cr = CLAMP((P[0] + d * cosf(alpha)) / owd, 0.0f, 1.0f);
g->ct = CLAMP((P[1] - d * sinf(alpha)) / oht, 0.0f, 1.0f);
g->cb = CLAMP((P[1] + d * sinf(alpha)) / oht, 0.0f, 1.0f);
dt_print(DT_DEBUG_PIPE | DT_DEBUG_VERBOSE,
"margins after crop adjustment: x=%.3f y=%.3f angle=%.3f"
" crop area (%.3f %.3f %.3f %.3f) width=%i height=%i",
0.5f * (g->cl + g->cr), 0.5f * (g->ct + g->cb), alpha,
g->cl, g->cr, g->ct, g->cb, (int)wd, (int)ht);
return;
}
// determine if the line is vertical or horizontal
static void _draw_retrieve_line_type(dt_iop_ashift_line_t *line)
{
dt_iop_ashift_linetype_t linetype = ASHIFT_LINE_VERTICAL_SELECTED;
if(fabsf(line->p1[0] - line->p2[0]) > fabsf(line->p1[1] - line->p2[1]))
linetype = ASHIFT_LINE_HORIZONTAL_SELECTED;
line->type = linetype;
}
// add a basic line. used for drawing perspective method
static void _draw_basic_line(dt_iop_ashift_line_t *line,
const float x1,
const float y1,
const float x2,
const float y2,
const dt_iop_ashift_linetype_t type)
{
// store as homogeneous coordinates
line->p1[0] = x1;
line->p1[1] = y1;
line->p1[2] = 1.0f;
line->p2[0] = x2;
line->p2[1] = y2;
line->p2[2] = 1.0f;
// calculate homogeneous coordinates of connecting line (defined by the two points)
vec3prodn(line->L, line->p1, line->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(line->L, line->L);
// length and width of rectangle (see LSD)
line->length = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
line->width = 1.0f;
line->weight = 1.0f;
// register type of line
line->type = type;
}
static void _gui_update_structure_states(const dt_iop_module_t *self, GtkWidget *widget)
{
const dt_iop_ashift_gui_data_t *g = self->gui_data;
if(widget && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)))
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), FALSE);
else
{
if(widget != g->structure_lines)
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->structure_lines), FALSE);
if(widget != g->structure_quad)
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->structure_quad), FALSE);
if(widget != g->structure_auto)
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->structure_auto), FALSE);
if(widget) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), TRUE);
}
// update fit buttons state
const gboolean enable =
(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->structure_auto))
|| gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->structure_quad))
|| gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->structure_lines)));
gtk_widget_set_sensitive(g->fit_v, enable);
gtk_widget_set_sensitive(g->fit_h, enable);
gtk_widget_set_sensitive(g->fit_both, enable);
}
static void _draw_save_lines_to_params(dt_iop_module_t *self)
{
// to save drawn lines in parameters, we only need extremas positions
// this positions needs to be saved in "original image" reference
dt_iop_ashift_gui_data_t *g = self->gui_data;
dt_iop_ashift_params_t *p = self->params;
if(!g || !p) return;
// save quad lines (we only handle the 2 vertical lines)
if(g->current_structure_method == ASHIFT_METHOD_QUAD && g->lines && g->lines_count >= 4)
{
float pts[8] =
{ g->lines[0].p1[0], g->lines[0].p1[1],
g->lines[0].p2[0], g->lines[0].p2[1],
g->lines[1].p1[0], g->lines[1].p1[1],
g->lines[1].p2[0], g->lines[1].p2[1] };
if(dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe,
self->iop_order,
DT_DEV_TRANSFORM_DIR_BACK_EXCL, pts, 4))
{
for(int i = 0; i < 8; i++)
p->last_quad_lines[i] = pts[i];
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
}
// save drawn lines (we drop the unselected ones)
if(g->current_structure_method == ASHIFT_METHOD_LINES && g->lines)
{
p->last_drawn_lines_count = 0;
for(int i = 0; i < g->lines_count; i++)
{
// we only save selected lines, not removed ones
if(g->lines[i].type == ASHIFT_LINE_HORIZONTAL_SELECTED
|| g->lines[i].type == ASHIFT_LINE_VERTICAL_SELECTED)
{
p->last_drawn_lines[p->last_drawn_lines_count * 4 ] = g->lines[i].p1[0];
p->last_drawn_lines[p->last_drawn_lines_count * 4 + 1] = g->lines[i].p1[1];
p->last_drawn_lines[p->last_drawn_lines_count * 4 + 2] = g->lines[i].p2[0];
p->last_drawn_lines[p->last_drawn_lines_count * 4 + 3] = g->lines[i].p2[1];
p->last_drawn_lines_count++;
if(p->last_drawn_lines_count >= MAX_SAVED_LINES) break;
}
}
if(dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe,
self->iop_order,
DT_DEV_TRANSFORM_DIR_BACK_EXCL,
p->last_drawn_lines,
p->last_drawn_lines_count * 2))
{
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
}
}
static gboolean _draw_retrieve_lines_from_params(dt_iop_module_t *self,
const dt_iop_ashift_method_t method)
{
// parameters contains lines extremas positions in "original image" reference
// so we need to translate them in module input reference
// and to compute length and ... values
dt_iop_ashift_gui_data_t *g = self->gui_data;
dt_iop_ashift_params_t *p = self->params;
if(!g || !p) return FALSE;
const dt_dev_pixelpipe_iop_t *piece =
dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self);
if(method == ASHIFT_METHOD_QUAD
&& p->last_quad_lines[0] > 0.0f && p->last_quad_lines[1] > 0.0f
&& p->last_quad_lines[2] > 0.0f && p->last_quad_lines[3] > 0.0f)
{
float pts[8] = { p->last_quad_lines[0], p->last_quad_lines[1],
p->last_quad_lines[2], p->last_quad_lines[3],
p->last_quad_lines[4], p->last_quad_lines[5],
p->last_quad_lines[6], p->last_quad_lines[7] };
if(dt_dev_distort_transform_plus(self->dev, self->dev->preview_pipe, self->iop_order,
DT_DEV_TRANSFORM_DIR_BACK_EXCL, pts, 4))
{
if(g->lines) free(g->lines);
g->lines = calloc(4, sizeof(dt_iop_ashift_line_t));
// vertical lines
_draw_basic_line
(&g->lines[0],
pts[0], pts[1], pts[2], pts[3],
ASHIFT_LINE_VERTICAL_SELECTED);
_draw_basic_line
(&g->lines[1],
pts[4], pts[5], pts[6], pts[7],
ASHIFT_LINE_VERTICAL_SELECTED);
// horizontal lines
_draw_basic_line
(&g->lines[2],
pts[0],
pts[1], pts[4], pts[5],
ASHIFT_LINE_HORIZONTAL_SELECTED);
_draw_basic_line
(&g->lines[3],
pts[2],
pts[3], pts[6], pts[7],
ASHIFT_LINE_HORIZONTAL_SELECTED);
g->lines_count = 4;
g->vertical_count = 2;
g->horizontal_count = 2;
g->vertical_weight = 2.0;
g->horizontal_weight = 2.0;
g->lines_in_width = piece->iwidth;
g->lines_in_height = piece->iheight;
g->current_structure_method = method;
return TRUE;
}
}
// the stored count indexes the fixed saved-lines array
if(method == ASHIFT_METHOD_LINES && p->last_drawn_lines_count > 0
&& p->last_drawn_lines_count <= MAX_SAVED_LINES)
{
float pts[MAX_SAVED_LINES * 4] = { 0.0f };
for(int i = 0; i < p->last_drawn_lines_count * 4; i++)
pts[i] = p->last_drawn_lines[i];
if(dt_dev_distort_transform_plus(self->dev, self->dev->preview_pipe, self->iop_order,
DT_DEV_TRANSFORM_DIR_BACK_EXCL, pts,
p->last_drawn_lines_count * 2))
{
if(g->lines) free(g->lines);
g->lines = calloc(p->last_drawn_lines_count, sizeof(dt_iop_ashift_line_t));
int vnb = 0; // number of vertical lines
int hnb = 0; // number of horizontal lines
for(int i = 0; i < p->last_drawn_lines_count; i++)
{
// determine if the line is vertical or horizontal
dt_iop_ashift_linetype_t linetype = ASHIFT_LINE_VERTICAL_SELECTED;
if(fabsf(pts[i * 4] - pts[i * 4 + 2]) > fabsf(pts[i * 4 + 1] - pts[i * 4 + 3]))
linetype = ASHIFT_LINE_HORIZONTAL_SELECTED;
_draw_basic_line(&g->lines[i],
pts[i * 4], pts[i * 4 + 1],
pts[i * 4 + 2], pts[i * 4 + 3],
linetype);
if(linetype == ASHIFT_LINE_VERTICAL_SELECTED)
vnb++;
else
hnb++;
}
g->lines_count = p->last_drawn_lines_count;
g->vertical_count = vnb;
g->horizontal_count = hnb;
g->vertical_weight = (float)vnb;
g->horizontal_weight = (float)hnb;
g->lines_in_width = piece->iwidth;
g->lines_in_height = piece->iheight;
g->current_structure_method = method;
return TRUE;
}
}
return FALSE;
}
// helper function to clean structural data
static gboolean _do_clean_structure(dt_iop_module_t *self,
dt_iop_ashift_params_t *p,
const gboolean save_drawn)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
if(g->fitting) return FALSE;
// if needed, we save the actual drawn line
if(save_drawn) _draw_save_lines_to_params(self);
g->fitting = 1;
g->lines_count = 0;
g->vertical_count = 0;
g->horizontal_count = 0;
if(g->lines) free(g->lines);
g->lines = NULL;
g->lines_version++;
g->current_structure_method = ASHIFT_METHOD_NONE;
g->fitting = 0;
return TRUE;
}
// helper function to start analysis for structural data and report about errors
static gboolean _do_get_structure_auto(dt_iop_module_t *self,
dt_iop_ashift_params_t *p,
const dt_iop_ashift_enhance_t enhance)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
if(g->fitting) return FALSE;
g->fitting = 1;
dt_iop_gui_enter_critical_section(self);
const float *b = g->buf;
dt_iop_gui_leave_critical_section(self);
if(b == NULL)
{
dt_control_log(_("data pending - please repeat"));
// force to reprocess the preview, otherwise the buffer is ko
dt_dev_reprocess_preview(self->dev, self->iop_order);
goto error;
}
if(!_get_structure(self, 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(self))
{
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;
}
// initialise the lines structure method
static void _do_get_structure_lines(dt_iop_module_t *self)
{
dt_iop_ashift_params_t *p = self->params;
dt_iop_ashift_gui_data_t *g = self->gui_data;
// we verify that we have a valid buffer
dt_iop_gui_enter_critical_section(self);
const float *b = g->buf;
dt_iop_gui_leave_critical_section(self);
if(b == NULL)
{
dt_control_log(_("data pending - please repeat"));
// force to reprocess the preview, otherwise the buffer is ko
dt_dev_reprocess_preview(self->dev, self->iop_order);
return;
}
_gui_update_structure_states(self, g->structure_lines);
const dt_dev_pixelpipe_iop_t *piece =
dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self);
_do_clean_structure(self, p, TRUE);
// if the button is unselected, we don't go further
if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->structure_lines)))
{
dt_control_queue_redraw_center();
return;
}
g->current_structure_method = ASHIFT_METHOD_LINES;
g->lines_in_width = piece->iwidth;
g->lines_in_height = piece->iheight;
g->lines_x_off = 0;
g->lines_y_off = 0;
// we try to recover eventual saved lines
_draw_retrieve_lines_from_params(self, ASHIFT_METHOD_LINES);
dt_control_queue_redraw_center();
}
// initialise the quad structure method
static void _do_get_structure_quad(dt_iop_module_t *self)
{
dt_iop_ashift_params_t *p = self->params;
dt_iop_ashift_gui_data_t *g = self->gui_data;
// we verify that we have a valid buffer
dt_iop_gui_enter_critical_section(self);
const float *b = g->buf;
dt_iop_gui_leave_critical_section(self);
if(b == NULL)
{
dt_control_log(_("data pending - please repeat"));
// force to reprocess the preview, otherwise the buffer is ko
dt_dev_reprocess_preview(self->dev, self->iop_order);
return;
}
_gui_update_structure_states(self, g->structure_quad);
const dt_dev_pixelpipe_iop_t *piece =
dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self);
_do_clean_structure(self, p, TRUE);
// if the button is unselected, we don't go further
if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->structure_quad)))
{
dt_control_queue_redraw_center();
return;
}
// we try to recover eventual saved lines
if(_draw_retrieve_lines_from_params(self, ASHIFT_METHOD_QUAD))
{
dt_control_queue_redraw_center();
}
else
{
float wd, ht;
dt_dev_get_preview_size(self->dev, &wd, &ht);
float pts[8] =
{ wd * 0.2, ht * 0.2, wd * 0.2, ht * 0.8,
wd * 0.8, ht * 0.2, wd * 0.8, ht * 0.8 };
if(dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe,
self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_INCL, pts, 4))
{
g->current_structure_method = ASHIFT_METHOD_QUAD;
g->lines = calloc(4, sizeof(dt_iop_ashift_line_t));
g->lines_count = 4;
_draw_basic_line(&g->lines[0],
pts[0], pts[1],
pts[2], pts[3],
ASHIFT_LINE_VERTICAL_SELECTED);
_draw_basic_line(&g->lines[1],
pts[4],
pts[5], pts[6], pts[7],
ASHIFT_LINE_VERTICAL_SELECTED);
_draw_basic_line(&g->lines[2],
pts[0], pts[1], pts[4],
pts[5],
ASHIFT_LINE_HORIZONTAL_SELECTED);
_draw_basic_line(&g->lines[3],
pts[2],
pts[3], pts[6], pts[7],
ASHIFT_LINE_HORIZONTAL_SELECTED);
// get real line type (they may be wrong due to image rotation)
for(int i = 0; i < 4; i++) _draw_retrieve_line_type(&g->lines[i]);
g->lines_in_width = piece->iwidth;
g->lines_in_height = piece->iheight;
g->lines_x_off = 0;
g->lines_y_off = 0;
g->vertical_count = 2;
g->horizontal_count = 2;
g->vertical_weight = 2.0;
g->horizontal_weight = 2.0;
g->lines_version++;
dt_control_queue_redraw_center();
}
}
}
// helper function to start parameter fit and report about errors
static void do_fit(dt_iop_module_t *self,
dt_iop_ashift_params_t *p,
const dt_iop_ashift_fitaxis_t dir)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
if(g->fitting) return;
// if no structure available get it
if(g->lines == NULL)
if(!_do_get_structure_auto(self, p, ASHIFT_ENHANCE_NONE)) return;
g->fitting = 1;
const dt_iop_ashift_nmsresult_t res = nmsfit(self, p, dir);
g->fitting = 0;
switch(res)
{
case NMS_NOT_ENOUGH_LINES:
dt_control_log(
_("not enough structure for automatic correction\n"
"minimum %d lines in each relevant direction"),
MINIMUM_FITLINES);
return;
case NMS_DID_NOT_CONVERGE:
case NMS_INSANE:
dt_control_log(_("automatic correction failed, please correct manually"));
return;
case NMS_SUCCESS:
default:
break;
}
// finally apply cropping
do_crop(self, p);
dt_dev_invalidate_all(darktable.develop);
DT_ENTER_GUI_UPDATE();
dt_bauhaus_slider_set(g->rotation, p->rotation);
dt_bauhaus_slider_set(g->lensshift_v, p->lensshift_v);
dt_bauhaus_slider_set(g->lensshift_h, p->lensshift_h);
dt_bauhaus_slider_set(g->shear, p->shear);
DT_LEAVE_GUI_UPDATE();
}
void process(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const void *const ivoid,
void *const ovoid,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out)
{
const dt_iop_ashift_data_t *data = piece->data;
dt_iop_ashift_gui_data_t *g = 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(g && self->dev->gui_attached && dt_pipe_is_preview(piece->pipe))
{
// 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
// origin of image and opposite corner as reference points
dt_boundingbox_t points = { 0.0f,
0.0f,
(float)piece->buf_in.width,
(float)piece->buf_in.height };
const float ivec[2] = { points[2] - points[0], points[3] - points[1] };
const float ivecl = dt_fast_hypotf(ivec[0], ivec[1]);
// where do they go?
dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_EXCL, points, 2);
const float ovec[2] = { points[2] - points[0], points[3] - points[1] };
const float ovecl = dt_fast_hypotf(ovec[0], ovec[1]);
// angle between input vector and output vector
const float alpha = acosf(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
const int isflipped = fabsf(fmodf(alpha + M_PI_F, M_PI_F) - M_PI_2f) < M_PI_4f;
// did modules prior to this one in pixelpipe have changed? -> check via hash value
const dt_hash_t hash = dt_dev_hash_plus(self->dev, self->dev->preview_pipe,
self->iop_order, DT_DEV_TRANSFORM_DIR_BACK_EXCL);
dt_iop_gui_enter_critical_section(self);
g->isflipped = isflipped;
const size_t requested_size = (size_t)roi_in->width * roi_in->height;
// save a copy of preview input buffer for parameter fitting
if(g->buf == NULL
|| (size_t)g->buf_width * g->buf_height < requested_size)
{
// if needed allocate buffer
dt_free_align(g->buf);
// only get new buffer if no old buffer available or old buffer
// does not fit in terms of size
g->buf = dt_alloc_align_float(4 * requested_size);
}
if(g->buf /* && hash != g->buf_hash */)
{
// copy data using actual roi_in dimensions (can exceed buf_in when scale > 1.0)
dt_iop_image_copy_by_size(g->buf, ivoid, roi_in->width, roi_in->height, ch);
g->buf_width = roi_in->width;
g->buf_height = roi_in->height;
g->buf_x_off = roi_in->x;
g->buf_y_off = roi_in->y;
g->buf_scale = roi_in->scale;
g->buf_hash = hash;
}
dt_iop_gui_leave_critical_section(self);
}
// if module is set to neutral parameters we just copy input->output and are done
if(_isneutral(data))
{
dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, ch);
return;
}
const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP);
float DT_ALIGNED_ARRAY 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;
DT_OMP_FOR(shared(ihomograph))
// go over all pixels of output image
for(int j = 0; j < roi_out->height; j++)
{
float *const restrict out = ((float *)ovoid) + (size_t)ch * j * roi_out->width;
for(int i = 0; i < roi_out->width; i++)
{
float pin[3], pout[3];
// 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_fma(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;
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 + ch*i,
pin[0], pin[1], roi_in->width,
roi_in->height, ch_width);
}
}
}
#ifdef HAVE_OPENCL
int process_cl(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
cl_mem dev_in,
cl_mem dev_out,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out)
{
const dt_iop_ashift_data_t *d = piece->data;
const dt_iop_ashift_global_data_t *gd = self->global_data;
dt_iop_ashift_gui_data_t *g = self->gui_data;
const int devid = piece->pipe->devid;
cl_int err = DT_OPENCL_DEFAULT_ERROR;
cl_mem dev_homo = NULL;
// only for preview pipe: collect input buffer data and do some other evaluations
if(self->dev->gui_attached && g && dt_pipe_is_preview(piece->pipe))
{
// 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
// origin of image and opposite corner as reference points
dt_boundingbox_t points = { 0.0f,
0.0f,
(float)piece->buf_in.width,
(float)piece->buf_in.height };
const float ivec[2] = { points[2] - points[0], points[3] - points[1] };
const float ivecl = dt_fast_hypotf(ivec[0], ivec[1]);
// where do they go?
dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_EXCL, points, 2);
const float ovec[2] = { points[2] - points[0], points[3] - points[1] };
const float ovecl = dt_fast_hypotf(ovec[0], ovec[1]);
// angle between input vector and output vector
const float alpha =
acosf(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
const int isflipped =
fabsf(fmodf(alpha + M_PI_F, M_PI_F) - M_PI_2f) < M_PI_4f;
// do modules coming before this one in pixelpipe have changed? -> check via hash value
const dt_hash_t hash = dt_dev_hash_plus(self->dev, self->dev->preview_pipe,
self->iop_order, DT_DEV_TRANSFORM_DIR_BACK_EXCL);
dt_iop_gui_enter_critical_section(self);
g->isflipped = isflipped;
const size_t requested_size = (size_t)roi_in->width * roi_in->height;
// save a copy of preview input buffer for parameter fitting
if(g->buf == NULL || (size_t)g->buf_width * g->buf_height < requested_size)
{
// if needed allocate buffer
dt_free_align(g->buf);
// only get new buffer if no old buffer or old buffer does not fit in terms of size
g->buf = dt_alloc_align_float(4 * requested_size);
}
if(g->buf /* && hash != g->buf_hash */)
{
// copy data using actual roi_in dimensions (can exceed buf_in when scale > 1.0)
err = dt_opencl_copy_image_to_host(devid, g->buf, dev_in,
roi_in->width, roi_in->height, sizeof(float) * 4);
g->buf_width = roi_in->width;
g->buf_height = roi_in->height;
g->buf_x_off = roi_in->x;
g->buf_y_off = roi_in->y;
g->buf_scale = roi_in->scale;
g->buf_hash = hash;
}
dt_iop_gui_leave_critical_section(self);
if(err != CL_SUCCESS) goto error;
}
// if module is set to neutral parameters we just copy input->output and are done
if(_isneutral(d))
{
const size_t region[2] = { roi_out->width, roi_out->height };
err = dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, CLIMG_ORIGIN, CLIMG_ORIGIN, region);
if(err != CL_SUCCESS) goto error;
return CL_SUCCESS;
}
float DT_ALIGNED_ARRAY ihomograph[3][3];
_homography((float *)ihomograph, d->rotation, d->lensshift_v, d->lensshift_h,
d->shear, d->f_length_kb,
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;
err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
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 clip[2] = { cx, cy };
const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP);
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:
err = DT_OPENCL_DEFAULT_ERROR;
goto error;
}
err = dt_opencl_enqueue_kernel_2d_args
(devid, ldkernel, roi_out->width, roi_out->height,
CLARG(dev_in), CLARG(dev_out), CLARG(roi_out->width), CLARG(roi_out->height),
CLARG(roi_in->width), CLARG(roi_in->height), CLARRAY(2, iroi),
CLARRAY(2, oroi),
CLARG(roi_in->scale), CLARG(roi_out->scale), CLARRAY(2, clip), CLARG(dev_homo));
error:
dt_opencl_release_mem_object(dev_homo);
return err;
}
#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,
const float pzx,
const float pzy,
const float delta,
const gboolean multiple)
{
const float delta2 = delta * delta;
if(lines_count < 1 || points_idx == NULL)
return;
for(int n = 0; n < lines_count; n++)
points_idx[n].near = 0;
for(int n = 0; n < lines_count; n++)
{
// 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++)
{
const float dx = pzx - points[offset * 2];
const float dy = pzy - points[offset * 2 + 1];
if(dx * dx + dy * dy < delta2)
{
points_idx[n].near = 1;
break;
}
}
// if we don't want multiple selection, stop here
if(!multiple && points_idx[n].near) 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,
const float pzx,
const float pzy,
const float pzx2,
const float pzy2,
const 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
const dt_iop_ashift_linetype_t mask = ASHIFT_LINE_SELECTED;
const 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 dt_hash_t _get_lines_hash(const dt_iop_ashift_line_t *lines,
const int lines_count)
{
dt_hash_t hash = DT_INITHASH;
for(int n = 0; n < lines_count; n++)
{
const uint32_t v[4] = { (uint32_t)lines[n].p1[0],
(uint32_t)lines[n].p1[1],
(uint32_t)lines[n].p2[0],
(uint32_t)lines[n].p2[1] };
hash = dt_hash(hash, &v, sizeof(v));
}
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 gboolean _update_colors(dt_iop_module_t *self,
dt_iop_ashift_points_idx_t *points_idx,
const int points_lines_count)
{
// is the display flipped relative to the original image?
const gboolean isflipped = _get_isflipped(self);
// 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 gboolean _get_points(dt_iop_module_t *self,
const dt_iop_ashift_line_t *lines,
const int lines_count,
const int lines_version,
float **points,
float **extremas,
dt_iop_ashift_points_idx_t **points_idx,
int *points_lines_count,
const float scale)
{
dt_develop_t *dev = self->dev;
const dt_iop_ashift_gui_data_t *g = self->gui_data;
if(lines_count < 1)
return FALSE;
dt_iop_ashift_points_idx_t *my_points_idx = NULL;
float *my_points = NULL;
float *my_extremas = NULL;
// is the display flipped relative to the original image?
const int isflipped = _get_isflipped(self);
// allocate new index array
my_points_idx = malloc(sizeof(dt_iop_ashift_points_idx_t) * lines_count);
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 = MAX(lines[n].length, 2);
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(sizeof(float) * 2 * total_points);
my_extremas = (float *)malloc(sizeof(float) * 2 * 2 * lines_count);
if(my_points == NULL || my_extremas == NULL) goto error;
// second step: generate points for each line
for(int n = 0, offset = 0; n < lines_count; n++)
{
my_extremas[4 * n] = lines[n].p1[0] / scale;
my_extremas[4 * n + 1] = lines[n].p1[1] / scale;
my_extremas[4 * n + 2] = lines[n].p2[0] / scale;
my_extremas[4 * n + 3] = lines[n].p2[1] / scale;
my_points_idx[n].offset = offset;
float x = lines[n].p1[0] / scale;
float y = lines[n].p1[1] / scale;
const int length = lines[n].length;
const float dx = (lines[n].p2[0] / scale - x) / (float)(length - 1);
const float dy = (lines[n].p2[1] / scale - y) / (float)(length - 1);
// for very small length, we set the second extrema at last point
if(length < 2)
{
my_points[2 * offset] = x;
my_points[2 * offset + 1] = y;
offset++;
my_points[2 * offset] = lines[n].p2[0] / scale;
my_points[2 * offset + 1] = lines[n].p2[1] / scale;
offset++;
}
else
{
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->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_INCL,
my_points, total_points))
goto error;
if(!dt_dev_distort_transform_plus(dev, dev->preview_pipe, self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_INCL,
my_extremas, 2 * lines_count))
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;
const size_t offset = my_points_idx[n].offset;
const int length = my_points_idx[n].length;
for(int l = 0; l < length; l++)
{
xmin = MIN(xmin, my_points[2 * offset]);
xmax = MAX(xmax, my_points[2 * offset]);
ymin = MIN(ymin, my_points[2 * offset + 1]);
ymax = MAX(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;
*extremas = my_extremas;
return TRUE;
error:
if(my_points_idx != NULL) free(my_points_idx);
if(my_points != NULL) free(my_points);
if(my_extremas) free(my_extremas);
return FALSE;
}
static int call_distort_transform(const dt_develop_t *dev,
const dt_dev_pixelpipe_t *pipe,
dt_iop_module_t *self,
float *points,
const size_t points_count)
{
int ret = 0;
dt_dev_pixelpipe_iop_t *piece =
dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self);
if(!piece) return ret;
if(piece->module == self
// && *piece->enabled see note below
&& !(dt_iop_module_is_skipped(dev, piece->module)
&& dt_pipe_is_basic(pipe)))
{
ret = piece->module->distort_transform(piece->module, piece, points, points_count);
}
return ret;
//NOTE: piece->enabled is FALSE for exactly the first mouse_moved
// event following a button_pressed event when ASHIFT_CROP_ASPECT
// is active, which causes the first gui_post_expose call on
// starting to resize the crop box to draw the center image without
// the crop overlay, resulting in an annoying visual glitch.
// Removing the check appears to have no adverse effects and
// eliminates the glitch.
}
static float _calculate_straightening(const dt_iop_module_t *self,
const float pzx, const float pzy,
const float bzx, const float bzy,
const float wd, const float ht,
const float zoom_scale)
{
float dx = (pzx - bzx) * wd, dy = (pzy - bzy) * ht;
if(sqrt(dx * dx + dy * dy) * zoom_scale < DT_PIXEL_APPLY_DPI(25))
return 0.0f;
// adjust the line with possible current angle and flip on this module
float pts[4] = { 0, 0, dx, dy };
dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe,
self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_EXCL, pts, 2);
dx = pts[0] - pts[2];
dy = pts[1] - pts[3];
if(dx < 0)
{
dx = -dx;
dy = -dy;
}
const float angle = atan2f(dy, dx);
if(!(angle >= -M_PI_2f && angle <= M_PI_2f))
return 0.0f;
float close = angle;
if(close > M_PI_4f)
close = M_PI_2f - close;
else if(close < -M_PI_4f)
close = -M_PI_2f - close;
else
close = -close;
float a = rad2degf(close);
if(a < -180.f) a += 360.f;
if(a > 180.f) a -= 360.f;
return a;
}
void gui_post_expose(dt_iop_module_t *self,
cairo_t *cr,
const float wd,
const float ht,
const float pzx,
const float pzy,
const float zoom_scale)
{
dt_develop_t *dev = self->dev;
dt_iop_ashift_gui_data_t *g = self->gui_data;
const dt_iop_ashift_params_t *p = self->params;
const gboolean dimmed = dt_iop_canvas_not_sensitive(dev);
const double lwidth = (dimmed ? 0.5 : 1.0) / zoom_scale;
const double fillc = dimmed ? 0.9 : 0.2;
// we draw the cropping area; we need x_off/y_off/width/height which is only available
// after g->buf has been processed. take the lock only once the cheap tests
// say the outline will actually be drawn
dt_iop_ashift_bufgeom_t b = { .valid = FALSE };
if(self->enabled && dt_iop_has_focus(self))
b = _get_buf_geometry(self);
if(b.valid)
{
// roi data of the preview pipe input buffer
const float iwd = b.width;
const float iht = b.height;
const float ixo = b.x_off;
const float iyo = b.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(!call_distort_transform(self->dev, self->dev->preview_pipe, self, (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 + g->cl * owd, ymin + g->ct * oht },
{ xmin + g->cl * owd, ymin + g->cb * oht },
{ xmin + g->cr * owd, ymin + g->cb * oht },
{ xmin + g->cr * owd, ymin + g->ct * oht } };
// convert clipping corners to final output image
if(!dt_dev_distort_transform_plus(self->dev, self->dev->preview_pipe, self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_EXCL, (float *)C, 4))
return;
cairo_save(cr);
const double dashes = DT_PIXEL_APPLY_DPI(5.0) / zoom_scale;
cairo_set_dash(cr, &dashes, 0, 0);
// mask parts of image outside of clipping area in dark grey
cairo_set_source_rgba(cr, fillc, fillc, fillc, 1.0 - fillc);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
cairo_rectangle(cr, 0, 0, wd, 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
dt_draw_set_color_overlay(cr, TRUE, 1.0);
cairo_set_line_width(cr, 2.0 *lwidth);
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);
// we draw the guides correctly scaled here instead of using the
// darkroom expose callback
const float cx = fminf(C[0][0], fminf(C[1][0], fminf(C[2][0], C[3][0])));
const float cy = fminf(C[0][1], fminf(C[1][1], fminf(C[2][1], C[3][1])));
const float cw = fmaxf(C[0][0], fmaxf(C[1][0], fmaxf(C[2][0], C[3][0]))) - cx;
const float ch = fmaxf(C[0][1], fmaxf(C[1][1], fmaxf(C[2][1], C[3][1]))) - cy;
dt_guides_draw(cr, cx, cy, cw, ch, zoom_scale);
// if adjusting crop, draw indicator
if(g->adjust_crop && p->cropmode == ASHIFT_CROP_ASPECT)
{
const double x1 = C[0][0];
const double x2 = fabs(x1 - C[1][0]) < 0.001f ? C[2][0] : C[1][0];
const double y1 = C[0][1];
const double y2 = fabs(y1 - C[1][1]) < 0.001f ? C[2][1] : C[1][1];
const double xpos = (x1 + x2) / 2.0f;
const double ypos = (y1 + y2) / 2.0f;
const double base_size = fabs(x1 - x2);
const double size_circle = base_size / 30.0f;
const double size_line = base_size / 5.0f;
const double size_arrow = base_size / 25.0f;
cairo_set_line_width(cr, 2.0 * lwidth);
dt_draw_set_color_overlay(cr, TRUE, 1.0);
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 * lwidth);
dt_draw_set_color_overlay(cr, TRUE, 1.0);
// 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);
}
else
dt_guides_draw(cr, 0.0f, 0.0f, wd, ht, zoom_scale);
// we draw the straightening line
if(g->straightening)
{
cairo_save(cr);
cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.0) * lwidth);
dt_draw_set_color_overlay(cr, TRUE, 1.0);
const float bzx = g->straighten_x, bzy = g->straighten_y;
cairo_arc(cr, bzx * wd, bzy * ht,
DT_PIXEL_APPLY_DPI(3) / zoom_scale, 0, 2.0 * M_PI);
cairo_stroke(cr);
cairo_arc(cr, pzx * wd, pzy * ht,
DT_PIXEL_APPLY_DPI(3) / zoom_scale, 0, 2.0 * M_PI);
cairo_stroke(cr);
cairo_move_to(cr, bzx * wd, bzy * ht);
cairo_line_to(cr, pzx * wd, pzy * ht);
cairo_stroke(cr);
const float angle =
_calculate_straightening(self, pzx, pzy, bzx, bzy, wd, ht, zoom_scale);
if(angle != 0.0f)
{
PangoRectangle ink;
PangoLayout *layout;
PangoFontDescription *desc = dt_gui_get_font();
pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD);
pango_font_description_set_absolute_size
(desc,
DT_PIXEL_APPLY_DPI(16) * PANGO_SCALE / zoom_scale);
layout = pango_cairo_create_layout(cr);
pango_layout_set_font_description(layout, desc);
char view_angle[16];
view_angle[0] = '\0';
snprintf(view_angle, sizeof(view_angle), "%.2f°", angle);
pango_layout_set_text(layout, view_angle, -1);
pango_layout_get_pixel_extents(layout, &ink, NULL);
const float text_w = ink.width;
const float text_h = DT_PIXEL_APPLY_DPI(16 + 2) / zoom_scale;
const float margin = DT_PIXEL_APPLY_DPI(6) / zoom_scale;
cairo_set_source_rgba(cr, .5, .5, .5, .9);
const float xp = pzx * wd + DT_PIXEL_APPLY_DPI(20) / zoom_scale;
const float yp = pzy * ht - ink.height;
dt_gui_draw_rounded_rectangle(cr,
text_w + 2 * margin,
text_h + 2 * margin,
xp - margin, yp - margin);
cairo_set_source_rgba(cr, .7, .7, .7, .7);
cairo_move_to(cr, xp, yp);
pango_cairo_show_layout(cr, layout);
pango_font_description_free(desc);
g_object_unref(layout);
}
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 || !dt_iop_has_focus(self)) return;
// get hash value that reflects distortions from here to the end of the pixelpipe
const dt_hash_t hash = dt_dev_hash_distort_plus(dev,
dev->preview_pipe,
self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_INCL);
// get hash value that changes if coordinates of lines have changed
const dt_hash_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;
free(g->draw_points);
g->draw_points = NULL;
g->points_lines_count = 0;
if(!_get_points(self, g->lines, g->lines_count, g->lines_version,
&g->points, &g->draw_points, &g->points_idx,
&g->points_lines_count, 1.0f))
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);
// 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++)
{
// hide removed lines in drawn mode
if((g->current_structure_method == ASHIFT_METHOD_QUAD
|| g->current_structure_method == ASHIFT_METHOD_LINES)
&& g->points_idx[n].type != ASHIFT_LINE_HORIZONTAL_SELECTED
&& g->points_idx[n].type != ASHIFT_LINE_VERTICAL_SELECTED)
continue;
// 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) * lwidth);
else
cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.5) * lwidth);
// 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);
}
// we also draw the corner in case of drawn perspective
if((g->current_structure_method == ASHIFT_METHOD_QUAD
|| g->current_structure_method == ASHIFT_METHOD_LINES)
&& g->draw_points)
{
dt_draw_set_color_overlay(cr, FALSE, 1.0);
const int nb = (g->current_structure_method == ASHIFT_METHOD_LINES)
? g->lines_count * 2
: 4;
for(int i = 0; i < nb; i++)
{
// hide removed lines
if(g->lines[i / 2].type != ASHIFT_LINE_HORIZONTAL_SELECTED
&& g->lines[i / 2].type != ASHIFT_LINE_VERTICAL_SELECTED)
continue;
if(g->draw_near_point == i)
cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(4.0) * lwidth);
else
cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.0) * lwidth);
cairo_arc(cr,
g->draw_points[i * 2],
g->draw_points[i * 2 + 1],
DT_PIXEL_APPLY_DPI(5.0) / zoom_scale, 0,
2.0 * M_PI);
cairo_stroke(cr);
}
}
// and we draw the selection box if any
if(g->isbounding != ASHIFT_BOUNDING_OFF)
{
double dashed[] = { 4.0, 4.0 };
dashed[0] /= zoom_scale;
dashed[1] /= zoom_scale;
const 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)
{
double dashed[] = { 4.0, 4.0 };
dashed[0] /= zoom_scale;
dashed[1] /= zoom_scale;
const 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 * lwidth);
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);
}
// 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 && lines; 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;
}
// determine if we are near a drawn line extrema
static int _draw_near_point(const float x,
const float y,
const float zoom_scale,
const float *points,
const int limit)
{
const float delta = DT_PIXEL_APPLY_DPI(6) / zoom_scale;
for(int i = 0; i < limit; i++)
{
if(x - points[i * 2] < delta
&& x - points[i * 2] > -delta
&& y - points[i * 2 + 1] < delta
&& y - points[i * 2 + 1] > -delta)
return i;
}
return -1;
}
static void _draw_recompute_line_length(dt_iop_ashift_line_t *line)
{
line->length = sqrt((line->p2[0] - line->p1[0]) * (line->p2[0] - line->p1[0])
+ (line->p2[1] - line->p1[1]) * (line->p2[1] - line->p1[1]));
}
int mouse_moved(dt_iop_module_t *self,
const float pzx,
const float pzy,
const double pressure,
const int which,
const float zoom_scale)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
if(g->straightening)
{
dt_control_queue_redraw_center();
return TRUE;
}
if(!self->enabled)
return FALSE;
gboolean handled = FALSE;
float wd, ht;
dt_dev_get_preview_size(self->dev, &wd, &ht);
if(g->adjust_crop)
{
const dt_iop_ashift_params_t *p = self->params;
dt_boundingbox_t pts = { pzx, pzy, 1.0f, 1.0f };
dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_INCL, pts, 2);
const float newx = g->crop_cx + (pts[0] - pts[2]) - g->lastx;
const float newy = g->crop_cy + (pts[1] - pts[3]) - g->lasty;
crop_adjust(self, p, newx, newy);
dt_control_queue_redraw_center();
return TRUE;
}
// if visibility of lines is switched off or no lines available, we
// would normally adjust the crop box but since g->adjust_crop was
// FALSE, we have nothing to do
if(!g->lines) return FALSE;
// if we are moving a drawn line extrema, we do the change here
if(g->draw_point_move)
{
float pts[2] = { pzx * wd, pzy * ht };
if(dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe,
self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_INCL, pts, 1))
{
// first we move the point
if(g->draw_near_point >= 0)
{
const int l = g->draw_near_point / 2;
if(g->draw_near_point % 2 == 0)
{
g->lines[l].p1[0] = pts[0];
g->lines[l].p1[1] = pts[1];
}
else
{
g->lines[l].p2[0] = pts[0];
g->lines[l].p2[1] = pts[1];
}
_draw_recompute_line_length(&g->lines[l]);
}
// for the rectangle method, we need to move the horizontal line too
if(g->current_structure_method == ASHIFT_METHOD_QUAD)
{
if(g->draw_near_point == 0)
{
g->lines[2].p1[0] = pts[0];
g->lines[2].p1[1] = pts[1];
_draw_recompute_line_length(&g->lines[2]);
}
else if(g->draw_near_point == 1)
{
g->lines[3].p1[0] = pts[0];
g->lines[3].p1[1] = pts[1];
_draw_recompute_line_length(&g->lines[3]);
}
else if(g->draw_near_point == 2)
{
g->lines[2].p2[0] = pts[0];
g->lines[2].p2[1] = pts[1];
_draw_recompute_line_length(&g->lines[2]);
}
else if(g->draw_near_point == 3)
{
g->lines[3].p2[0] = pts[0];
g->lines[3].p2[1] = pts[1];
_draw_recompute_line_length(&g->lines[3]);
}
}
g->lines_hash++;
g->lines_version++;
dt_control_queue_redraw_center();
}
return TRUE;
}
// case where we move a drawn line
if(g->draw_line_move >= 0)
{
float pts[2] = { pzx * wd, pzy * ht };
if(dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe,
self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_INCL, pts, 1))
{
const float dx = (pts[0] - g->draw_pointmove_x);
const float dy = (pts[1] - g->draw_pointmove_y);
const int n = g->draw_line_move;
g->draw_pointmove_x = pts[0];
g->draw_pointmove_y = pts[1];
// we move the line extremas
g->lines[n].p1[0] += dx;
g->lines[n].p1[1] += dy;
g->lines[n].p2[0] += dx;
g->lines[n].p2[1] += dy;
// sanity check to be sure the extremas don't go outside the image area
g->lines[n].p1[0] = CLAMPF(g->lines[n].p1[0], 0.0f, g->lines_in_width);
g->lines[n].p1[1] = CLAMPF(g->lines[n].p1[1], 0.0f, g->lines_in_height);
g->lines[n].p2[0] = CLAMPF(g->lines[n].p2[0], 0.0f, g->lines_in_width);
g->lines[n].p2[1] = CLAMPF(g->lines[n].p2[1], 0.0f, g->lines_in_height);
_draw_recompute_line_length(&g->lines[n]);
// for the rectangle method, we need to move the adjacent lines too
if(g->current_structure_method == ASHIFT_METHOD_QUAD)
{
if(n == 0)
{
g->lines[2].p1[0] = g->lines[n].p1[0];
g->lines[2].p1[1] = g->lines[n].p1[1];
g->lines[3].p1[0] = g->lines[n].p2[0];
g->lines[3].p1[1] = g->lines[n].p2[1];
_draw_recompute_line_length(&g->lines[2]);
_draw_recompute_line_length(&g->lines[3]);
}
else if(n == 1)
{
g->lines[2].p2[0] = g->lines[n].p1[0];
g->lines[2].p2[1] = g->lines[n].p1[1];
g->lines[3].p2[0] = g->lines[n].p2[0];
g->lines[3].p2[1] = g->lines[n].p2[1];
_draw_recompute_line_length(&g->lines[2]);
_draw_recompute_line_length(&g->lines[3]);
}
else if(n == 2)
{
g->lines[0].p1[0] = g->lines[n].p1[0];
g->lines[0].p1[1] = g->lines[n].p1[1];
g->lines[1].p1[0] = g->lines[n].p2[0];
g->lines[1].p1[1] = g->lines[n].p2[1];
_draw_recompute_line_length(&g->lines[0]);
_draw_recompute_line_length(&g->lines[1]);
}
else if(n == 3)
{
g->lines[0].p2[0] = g->lines[n].p1[0];
g->lines[0].p2[1] = g->lines[n].p1[1];
g->lines[1].p2[0] = g->lines[n].p2[0];
g->lines[1].p2[1] = g->lines[n].p2[1];
_draw_recompute_line_length(&g->lines[0]);
_draw_recompute_line_length(&g->lines[1]);
}
}
g->lines_hash++;
g->lines_version++;
dt_control_queue_redraw_center();
}
return TRUE;
}
// if we are in draw mode, we check if we are near a corner
if(g->draw_points
&& ((g->current_structure_method == ASHIFT_METHOD_QUAD
&& g->lines_count >= 4)
|| g->current_structure_method == ASHIFT_METHOD_LINES))
{
const int limit = (g->current_structure_method == ASHIFT_METHOD_LINES)
? g->lines_count * 2
: 4;
g->draw_near_point = _draw_near_point(pzx * wd, pzy * ht, zoom_scale, g->draw_points, limit);
}
// 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,
!(g->current_structure_method == ASHIFT_METHOD_LINES
|| g->current_structure_method == ASHIFT_METHOD_QUAD));
// 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;
handled = TRUE;
}
else if(g->isselecting && g->current_structure_method != ASHIFT_METHOD_LINES)
{
g->lines[n].type |= ASHIFT_LINE_SELECTED;
handled = TRUE;
}
}
}
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(dt_iop_module_t *self,
const float pzx,
const float pzy,
const double pressure,
const int which,
const int type,
const uint32_t state,
const float zoom_scale)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
gboolean handled = FALSE;
// avoid unexpected back to lt mode:
if(type == GDK_2BUTTON_PRESS && which == GDK_BUTTON_PRIMARY)
return TRUE;
float wd, ht;
if(!dt_dev_get_preview_size(self->dev, &wd, &ht)) return 1;
// if we start to draw a straightening line
if(!g->lines && which == GDK_BUTTON_SECONDARY)
{
dt_control_change_cursor("crosshair");
g->straightening = TRUE;
g->straighten_x = pzx;
g->straighten_y = pzy;
return TRUE;
}
// if no lines available -> potentially adjust crop area
if(g->current_structure_method != ASHIFT_METHOD_LINES && !g->lines)
{
const dt_iop_ashift_params_t *p = self->params;
if(p->cropmode == ASHIFT_CROP_ASPECT)
{
dt_control_change_cursor("pointer");
g->adjust_crop = TRUE;
dt_boundingbox_t pts = { pzx, pzy, 1.0f, 1.0f };
dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_INCL, pts, 2);
g->lastx = pts[0] - pts[2];
g->lasty = pts[1] - pts[3];
g->crop_cx = 0.5f * (g->cl + g->cr);
g->crop_cy = 0.5f * (g->ct + g->cb);
return TRUE;
}
else
return FALSE;
}
// grab a draw corner
if((g->current_structure_method == ASHIFT_METHOD_QUAD
|| g->current_structure_method == ASHIFT_METHOD_LINES)
&& g->draw_near_point >= 0)
{
g->draw_point_move = TRUE;
g->lastx = pzx;
g->lasty = pzy;
return TRUE;
}
// 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(dt_modifier_is(state, GDK_SHIFT_MASK))
{
g->lastx = pzx;
g->lasty = pzy;
g->isbounding = (which == GDK_BUTTON_SECONDARY) ? ASHIFT_BOUNDING_DESELECT : ASHIFT_BOUNDING_SELECT;
dt_control_change_cursor("crosshair");
return TRUE;
}
// if we are zoomed out (no panning possible) and we have lines to display we take control
const int take_control = (dt_dev_get_zoomed_in() == 1.0f) && (g->points_lines_count > 0);
if(g->current_structure_method == ASHIFT_METHOD_QUAD
|| g->current_structure_method == ASHIFT_METHOD_LINES)
g->near_delta = dt_conf_get_float("plugins/darkroom/ashift/near_delta_draw");
else
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,
!(g->current_structure_method == ASHIFT_METHOD_QUAD
|| g->current_structure_method == ASHIFT_METHOD_LINES));
if((g->current_structure_method == ASHIFT_METHOD_LINES && which == GDK_BUTTON_PRIMARY)
|| g->current_structure_method == ASHIFT_METHOD_QUAD)
{
// we search the selected line and mark it as the moved line
for(int n = 0; n < g->points_lines_count; n++)
{
if(g->points_idx[n].near)
{
float pts[2] = { pzx * wd, pzy * ht };
dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe,
self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_INCL, pts, 1);
g->draw_line_move = n;
g->draw_pointmove_x = pts[0];
g->draw_pointmove_y = pts[1];
return TRUE;
}
}
// for the rectangle draw fitting, we don't go further
if(g->current_structure_method == ASHIFT_METHOD_QUAD) return FALSE;
}
else
{
// 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 == GDK_BUTTON_SECONDARY)
{
if(g->current_structure_method != ASHIFT_METHOD_LINES)
g->lines[n].type &= ~ASHIFT_LINE_SELECTED;
else
{
// we completely remove the line from the list
if(g->lines[n].type == ASHIFT_LINE_HORIZONTAL_SELECTED)
{
g->horizontal_count--;
g->horizontal_weight -= 1.0f;
}
else
{
g->vertical_count--;
g->vertical_weight -= 1.0f;
}
const int count = g->lines_count - 1;
dt_iop_ashift_line_t *lines = calloc(count, sizeof(dt_iop_ashift_line_t));
if(lines)
{
int pos = 0;
for(int i = 0; i < count; i++)
{
if(i != n)
{
lines[pos] = g->lines[i];
pos++;
}
}
if(g->lines) free(g->lines);
g->lines = lines;
g->lines_count = count;
}
}
handled = TRUE;
}
else if(g->current_structure_method != ASHIFT_METHOD_LINES)
{
g->lines[n].type |= ASHIFT_LINE_SELECTED;
handled = TRUE;
}
}
}
if(!handled && g->current_structure_method == ASHIFT_METHOD_LINES && which == GDK_BUTTON_PRIMARY)
{
// start to draw a manual line
g->draw_point_move = TRUE;
g->lastx = pzx;
g->lasty = pzy;
// we instantiate a new line with both extrema at the current position
// and enable the "move point" mode with the second extrema
float pts[2] = { pzx * wd, pzy * ht };
dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->iop_order,
DT_DEV_TRANSFORM_DIR_FORW_INCL, pts, 1);
const int count = g->lines_count + 1;
// if count > MAX_SAVED_LINES we alert that the next lines won't
// be saved in params but they still may be used for the current
// section (that's why we still allow them)
if(count > MAX_SAVED_LINES)
dt_control_log(_("only %d lines can be saved in parameters"), MAX_SAVED_LINES);
dt_iop_ashift_line_t *lines = malloc(sizeof(dt_iop_ashift_line_t) * count);
for(int i = 0; i < g->lines_count; i++)
{
lines[i] = g->lines[i];
}
if(g->lines) free(g->lines);
g->lines = lines;
g->lines_count = count;
_draw_basic_line(&g->lines[count - 1],
pts[0],
pts[1],
pts[0],
pts[1],
ASHIFT_LINE_VERTICAL_SELECTED);
g->vertical_count++;
g->vertical_weight += 1.0f;
g->draw_near_point = g->lines_count * 2 - 1;
return TRUE;
}
// 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 == GDK_BUTTON_SECONDARY)
{
dt_control_change_cursor("not-allowed");
g->isdeselecting = 1;
}
else if(take_control || handled)
{
dt_control_change_cursor("cell");
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(dt_iop_module_t *self,
const float pzx,
const float pzy,
const int which,
const uint32_t state,
const float zoom_scale)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
float wd, ht;
dt_dev_get_preview_size(self->dev, &wd, &ht);
dt_control_change_cursor("default");
// ends eventual line move
if(g->draw_line_move >= 0)
{
g->draw_line_move = -1;
// we save the lines in params
_draw_save_lines_to_params(self);
return TRUE;
}
if(g->straightening)
{
g->straightening = FALSE;
const float bzx = g->straighten_x, bzy = g->straighten_y;
const float angle = _calculate_straightening(self, pzx, pzy, bzx, bzy, wd, ht, zoom_scale);
if(angle == 0.0f) return TRUE;
const float n = dt_bauhaus_slider_get(g->rotation) - angle;
dt_bauhaus_slider_set(g->rotation, n);
dt_toast_log(_("rotation adjusted by %3.2f° to %3.2f°"), - angle, n);
return TRUE;
}
// release a drawn corner
if(g->draw_point_move)
{
// we determine the vertical/horizontal line type (that may have changed)
// we also save the lines in params
// points move are done directly in mouse_move routine
for(int l = 0; l < g->lines_count; l++)
{
const dt_iop_ashift_linetype_t old_linetype = g->lines[l].type;
_draw_retrieve_line_type(&g->lines[l]);
if(g->lines[l].type != old_linetype
&& g->lines[l].type == ASHIFT_LINE_VERTICAL_SELECTED)
{
g->vertical_count++;
g->vertical_weight += 1.0f;
g->horizontal_count--;
g->horizontal_weight -= 1.0f;
}
else if(g->lines[l].type != old_linetype)
{
g->horizontal_count++;
g->horizontal_weight += 1.0f;
g->vertical_count--;
g->vertical_weight -= 1.0f;
}
g->lines_version++;
}
g->draw_point_move = FALSE;
g->draw_near_point = -1;
// we save the lines in params
_draw_save_lines_to_params(self);
dt_control_queue_redraw_center();
return TRUE;
}
if(g->adjust_crop)
{
// stop adjust crop
g->adjust_crop = FALSE;
dt_iop_ashift_params_t *p = self->params;
_swap_shadow_crop_box(p,g); // temporarily update the crop box in p
dt_dev_add_history_item(darktable.develop, self, TRUE);
_swap_shadow_crop_box(p,g); // restore p
}
// finalize the isbounding mode
// if user has released the shift button in-between -> do nothing
if(g->isbounding != ASHIFT_BOUNDING_OFF && dt_modifier_is(state, GDK_SHIFT_MASK))
{
gboolean handled = FALSE;
// we compute the rectangle selection
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;
handled = TRUE;
}
else if(g->current_structure_method != ASHIFT_METHOD_LINES)
{
g->lines[n].type |= ASHIFT_LINE_SELECTED;
handled = TRUE;
}
}
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
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;
// if we have deselected drawn lines, we need to update params
if(g->current_structure_method == ASHIFT_METHOD_LINES && which == GDK_BUTTON_SECONDARY)
{
// we save the lines in params
_draw_save_lines_to_params(self);
}
return 0;
}
int scrolled(dt_iop_module_t *self,
const float pzx,
const float pzy,
const int up,
const uint32_t state)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
// do nothing if visibility of lines is switched off or no lines available
if(!g->lines) return FALSE;
if(g->near_delta > 0 && (g->isdeselecting || g->isselecting))
{
gboolean handled = FALSE;
float wd, ht;
dt_dev_get_preview_size(self->dev, &wd, &ht);
float near_delta = 5.0f;
if(g->current_structure_method == ASHIFT_METHOD_QUAD
|| g->current_structure_method == ASHIFT_METHOD_LINES)
near_delta = dt_conf_get_float("plugins/darkroom/ashift/near_delta_draw");
else
near_delta = dt_conf_get_float("plugins/darkroom/ashift/near_delta");
const float amount = up ? 1.25f : 0.8f;
near_delta = MAX(4.0f, MIN(near_delta * amount, 100.0f));
if(g->current_structure_method == ASHIFT_METHOD_QUAD
|| g->current_structure_method == ASHIFT_METHOD_LINES)
dt_conf_set_float("plugins/darkroom/ashift/near_delta_draw", near_delta);
else
dt_conf_set_float("plugins/darkroom/ashift/near_delta", near_delta);
g->near_delta = near_delta;
// for drawn structure, we stop here
if(g->current_structure_method == ASHIFT_METHOD_QUAD
|| g->current_structure_method == ASHIFT_METHOD_LINES)
return TRUE;
// 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,
TRUE);
// 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;
handled = TRUE;
}
else if(g->isselecting && g->current_structure_method != ASHIFT_METHOD_LINES)
{
g->lines[n].type |= ASHIFT_LINE_SELECTED;
handled = TRUE;
}
handled = TRUE;
}
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;
}
void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
{
dt_iop_ashift_params_t *p = self->params;
dt_iop_ashift_gui_data_t *g = self->gui_data;
#ifdef ASHIFT_DEBUG
model_probe(self, p, g->lastfit);
#endif
if(w != g->cropmode) dt_dev_invalidate_all(self->dev);
const dt_iop_ashift_bufgeom_t b = _get_buf_geometry(self);
if(b.height > 0 && b.width > 0)
{
do_crop(self, p);
_commit_crop_box(p, g);
}
else
{
g->jobcode = ASHIFT_JOBCODE_DO_CROP;
}
if(w == g->mode)
{
gtk_widget_set_visible(g->specifics, p->mode == ASHIFT_MODE_SPECIFIC);
}
}
void gui_reset(dt_iop_module_t *self)
{
dt_iop_ashift_params_t *p = self->params;
/* reset eventual remaining structures */
_do_clean_structure(self, p, FALSE);
_gui_update_structure_states(self, NULL);
// force to reprocess the preview, otherwise the buffer is ko
dt_dev_invalidate_all(self->dev);
dt_dev_pixelpipe_cache_flush(self->dev->preview_pipe);
}
static void cropmode_callback(GtkWidget *widget, dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
dt_iop_ashift_params_t *p = self->params;
dt_iop_ashift_gui_data_t *g = self->gui_data;
dt_conf_set_int("plugins/darkroom/ashift/autocrop_value",
dt_bauhaus_combobox_get(g->cropmode));
_swap_shadow_crop_box(p,g); //temporarily update real crop box
dt_dev_add_history_item(darktable.develop, self, TRUE);
_swap_shadow_crop_box(p,g);
}
static void _event_fit_v_button_clicked(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY)
{
dt_iop_ashift_params_t *p = self->params;
dt_iop_ashift_gui_data_t *g = self->gui_data;
const GdkModifierType state = dt_gui_current_state(gesture);
const int control = dt_modifiers_include(state, GDK_CONTROL_MASK);
const int shift = dt_modifiers_include(state, 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);
if(self->enabled)
{
// module is enable -> we process directly
do_fit(self, p, fitaxis);
}
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;
}
// temporarily update real crop box
_swap_shadow_crop_box(p, g);
//also calls dt_control_queue_redraw_center
dt_dev_add_history_item(darktable.develop, self, TRUE);
_swap_shadow_crop_box(p, g);
}
}
static void _event_fit_h_button_clicked(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY)
{
dt_iop_ashift_params_t *p = self->params;
dt_iop_ashift_gui_data_t *g = self->gui_data;
const GdkModifierType state = dt_gui_current_state(gesture);
const int control = dt_modifiers_include(state, GDK_CONTROL_MASK);
const int shift = dt_modifiers_include(state, 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);
if(self->enabled)
{
// module is enable -> we process directly
do_fit(self, p, fitaxis);
}
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;
}
// temporarily update real crop box
_swap_shadow_crop_box(p, g);
//also calls dt_control_queue_redraw_center
dt_dev_add_history_item(darktable.develop, self, TRUE);
_swap_shadow_crop_box(p, g);
}
}
static void _event_fit_both_button_clicked(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY)
{
dt_iop_ashift_params_t *p = self->params;
dt_iop_ashift_gui_data_t *g = self->gui_data;
const GdkModifierType state = dt_gui_current_state(gesture);
const int control = dt_modifiers_include(state, GDK_CONTROL_MASK);
const int shift = dt_modifiers_include(state, 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);
if(self->enabled)
{
// module is enable -> we process directly
do_fit(self, p, fitaxis);
}
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;
}
// temporarily update real crop box
_swap_shadow_crop_box(p, g);
//also calls dt_control_queue_redraw_center
dt_dev_add_history_item(darktable.develop, self, TRUE);
_swap_shadow_crop_box(p, g);
}
}
static void _event_structure_auto_clicked(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_iop_module_t *self)
{
GtkWidget *widget = dt_gui_get_widget(gesture);
DT_GUARD_GUI_UPDATE();
if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY)
{
dt_iop_ashift_params_t *p = self->params;
dt_iop_ashift_gui_data_t *g = self->gui_data;
_do_clean_structure(self, p, TRUE);
const GdkModifierType state = dt_gui_current_state(gesture);
const int control = dt_modifiers_include(state, GDK_CONTROL_MASK);
const int shift = dt_modifiers_include(state, 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;
// if the button is unselected, we don't go further
if(enhance == ASHIFT_ENHANCE_NONE
&& gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)))
{
_gui_update_structure_states(self, widget);
dt_control_queue_redraw_center();
return;
}
else
{
// force the button to be untoggled, so the update routine can enable it
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), FALSE);
_gui_update_structure_states(self, widget);
}
g->current_structure_method = ASHIFT_METHOD_AUTO;
dt_iop_request_focus(self);
if(self->enabled)
{
// module is enabled -> process directly
(void)_do_get_structure_auto(self, p, enhance);
}
else
{
// module is not enabled -> invoke it and queue the job to be processed once
// the preview image is ready
dt_dev_invalidate_all(self->dev);
g->jobcode = ASHIFT_JOBCODE_GET_STRUCTURE;
g->jobparams = enhance;
}
// also calls dt_control_queue_redraw_center
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
}
// 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 _event_process_after_preview_callback(gpointer instance, dt_iop_module_t *self)
{
dt_iop_ashift_params_t *p = self->params;
dt_iop_ashift_gui_data_t *g = self->gui_data;
const dt_iop_ashift_jobcode_t jobcode = g->jobcode;
const int jobparams = g->jobparams;
// purge
g->jobcode = ASHIFT_JOBCODE_NONE;
g->jobparams = 0;
DT_GUARD_GUI_UPDATE();
switch(jobcode)
{
case ASHIFT_JOBCODE_DO_CROP:
{
// the signal can come from a preview run that started before the
// module was enabled and so never filled g->buf; do_crop() would
// return without cropping, so keep the request for the next run
const dt_iop_ashift_bufgeom_t b = _get_buf_geometry(self);
if(b.width == 0 || b.height == 0)
{
g->jobcode = ASHIFT_JOBCODE_DO_CROP;
break;
}
do_crop(self, p);
_commit_crop_box(p, g);
// save all that
_swap_shadow_crop_box(p, g); // temporarily update real crop box
dt_dev_add_history_item(darktable.develop, self, TRUE);
_swap_shadow_crop_box(p, g);
dt_dev_invalidate_all(darktable.develop);
break;
}
case ASHIFT_JOBCODE_GET_STRUCTURE_QUAD:
_do_get_structure_quad(self);
break;
case ASHIFT_JOBCODE_GET_STRUCTURE_LINES:
_do_get_structure_lines(self);
break;
case ASHIFT_JOBCODE_GET_STRUCTURE:
(void)_do_get_structure_auto(self, p, (dt_iop_ashift_enhance_t)jobparams);
break;
case ASHIFT_JOBCODE_FIT:
do_fit(self, p, (dt_iop_ashift_fitaxis_t)jobparams);
dt_dev_add_history_item(darktable.develop, self, TRUE);
break;
case ASHIFT_JOBCODE_NONE:
default:
break;
}
dt_control_queue_redraw_center();
}
void commit_params(dt_iop_module_t *self,
dt_iop_params_t *p1,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
const dt_iop_ashift_params_t *p = (dt_iop_ashift_params_t *)p1;
dt_iop_ashift_data_t *d = 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(dt_iop_has_focus(self)
|| dt_isnan(p->cl)
|| dt_isnan(p->cr)
|| dt_isnan(p->ct)
|| dt_isnan(p->cb))
{
// 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(dt_iop_module_t *self,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
dt_iop_ashift_data_t *d = calloc(1, sizeof(dt_iop_ashift_data_t));
piece->data = (void *)d;
}
void cleanup_pipe(dt_iop_module_t *self,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
free(piece->data);
piece->data = NULL;
}
void gui_update(dt_iop_module_t *self)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
const dt_iop_ashift_params_t *p = self->params;
gtk_widget_set_visible(g->specifics, p->mode == ASHIFT_MODE_SPECIFIC);
// copy crop box into shadow variables
_shadow_crop_box(p,g);
dt_gui_update_collapsible_section(&g->cs);
}
void reload_defaults(dt_iop_module_t *self)
{
// our module is disabled by default
self->default_enabled = FALSE;
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(self->dev)
{
const dt_image_t *img = &self->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);
// focal length should be available in exif data if lens is
// electronically coupled to the camera
f_length = (img->exif_focal_length > 0.0f && img->exif_focal_length <= 1000000.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 = (img->exif_crop > 0.0f && img->exif_crop <= 1000.0f) ? img->exif_crop : crop_factor;
}
// init defaults:
dt_iop_ashift_params_t *d = self->default_params;
d->f_length = f_length;
d->crop_factor = crop_factor;
d->cropmode = dt_conf_get_int("plugins/darkroom/ashift/autocrop_value");
// reset gui elements
dt_iop_ashift_gui_data_t *g = self->gui_data;
if(g)
{
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, f_length);
dt_bauhaus_slider_set_default(g->crop_factor, crop_factor);
dt_iop_gui_enter_critical_section(self);
dt_free_align(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 = DT_INVALID_HASH;
g->isflipped = -1;
g->lastfit = ASHIFT_FIT_NONE;
dt_iop_gui_leave_critical_section(self);
g->fitting = 0;
free(g->lines);
g->lines = NULL;
g->lines_count =0;
g->horizontal_count = 0;
g->vertical_count = 0;
g->grid_hash = DT_INVALID_HASH;
g->lines_hash = DT_INVALID_HASH;
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_version = 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;
g->current_structure_method = ASHIFT_METHOD_NONE;
g->draw_line_move = -1;
g->draw_near_point = -1;
g->draw_point_move = FALSE;
_gui_update_structure_states(self, NULL);
}
}
void init_global(dt_iop_module_so_t *self)
{
dt_iop_ashift_global_data_t *gd = malloc(sizeof(dt_iop_ashift_global_data_t));
self->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_global(dt_iop_module_so_t *self)
{
const dt_iop_ashift_global_data_t *gd = self->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(self->data);
self->data = NULL;
}
// adjust labels of lens shift parameters according to flip status of image
static gboolean _event_draw(GtkWidget *widget,
cairo_t *cr,
dt_iop_module_t *self)
{
const dt_iop_ashift_gui_data_t *g = self->gui_data;
DT_GUARD_GUI_UPDATE(FALSE);
const int isflipped = _get_isflipped(self);
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"));
DT_ENTER_GUI_UPDATE();
dt_bauhaus_widget_set_label(g->lensshift_v, NULL, string_v);
dt_bauhaus_widget_set_label(g->lensshift_h, NULL, string_h);
DT_LEAVE_GUI_UPDATE();
return FALSE;
}
void gui_focus(dt_iop_module_t *self, const gboolean in)
{
darktable.develop->history_postpone_invalidate = in
&& dt_dev_modulegroups_test_activated(darktable.develop);
if(self->enabled)
{
dt_iop_ashift_params_t *p = self->params;
dt_iop_ashift_gui_data_t *g = self->gui_data;
if(in)
{
_shadow_crop_box(p,g);
dt_control_queue_redraw_center();
}
else
{
_commit_crop_box(p, g);
_gui_update_structure_states(self, NULL);
_do_clean_structure(self, p, TRUE);
}
}
}
static float log2_curve(const float inval, const dt_bauhaus_curve_t dir)
{
float outval;
if(dir == DT_BAUHAUS_SET)
{
outval = log2f(inval * 1.5f + 0.5f) / 2.0f + 0.5f;
}
else
{
outval = (exp2f(inval * 2.0 - 1.0) - 0.5f) / 1.5f;
}
return outval;
}
static void _event_structure_quad_clicked(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_iop_module_t *self)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
DT_GUARD_GUI_UPDATE();
dt_iop_request_focus(self);
if(self->enabled)
{
// module is enabled -> process directly
_do_get_structure_quad(self);
}
else
{
// module is not enabled -> invoke it and queue the job to be
// processed once the preview image is ready
dt_dev_invalidate_all(self->dev);
g->jobcode = ASHIFT_JOBCODE_GET_STRUCTURE_QUAD;
}
// also calls dt_control_queue_redraw_center
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
static void _event_structure_lines_clicked(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_iop_module_t *self)
{
dt_iop_ashift_gui_data_t *g = self->gui_data;
DT_GUARD_GUI_UPDATE();
dt_iop_request_focus(self);
if(self->enabled)
{
// module is enabled -> process directly
_do_get_structure_lines(self);
}
else
{
// module is not enabled -> invoke it and queue the job to be processed once
// the preview image is ready
dt_dev_invalidate_all(self->dev);
g->jobcode = ASHIFT_JOBCODE_GET_STRUCTURE_LINES;
}
// also calls dt_control_queue_redraw_center
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
// the structure buttons implement radio-button behavior by managing all
// toggle states themselves, so keep GTK from flipping the button on release:
// claim the sequence in CAPTURE phase (same pattern as dt_iop_togglebutton_new)
// and the button's own bubble-phase gesture never emits "clicked"
static GtkGestureSingle *_connect_structure_button(GtkWidget *widget,
GCallback pressed,
dt_iop_module_t *self)
{
GtkGestureSingle *gesture = dt_gui_connect_click(widget, pressed, NULL, self);
gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(gesture),
GTK_PHASE_CAPTURE);
g_signal_connect(G_OBJECT(gesture), "begin", G_CALLBACK(dt_gui_gesture_claim), NULL);
return gesture;
}
void gui_init(dt_iop_module_t *self)
{
dt_iop_ashift_gui_data_t *g = IOP_GUI_ALLOC(ashift);
//not actually needed, we're the only one with a pointer to this instance
dt_iop_gui_enter_critical_section(self);
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 = DT_INVALID_HASH;
g->isflipped = -1;
g->lastfit = ASHIFT_FIT_NONE;
dt_iop_gui_leave_critical_section(self);
g->fitting = 0;
g->lines = NULL;
g->lines_count = 0;
g->vertical_count = 0;
g->horizontal_count = 0;
g->lines_version = 0;
g->points = NULL;
g->points_idx = NULL;
g->points_lines_count = 0;
g->points_version = 0;
g->grid_hash = DT_INVALID_HASH;
g->lines_hash = DT_INVALID_HASH;
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->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;
g->draw_near_point = -1;
g->draw_line_move = -1;
g->rotation = dt_bauhaus_slider_from_params(self, N_("rotation"));
dt_bauhaus_slider_set_format(g->rotation, "°");
dt_bauhaus_slider_set_factor(g->rotation, -1.f);
dt_bauhaus_slider_set_soft_range(g->rotation, -ROTATION_RANGE, ROTATION_RANGE);
dt_action_t *ac = dt_action_widget(g->rotation);
dt_shortcut_register(ac, 0, DT_ACTION_EFFECT_UP, GDK_KEY_bracketleft, GDK_MOD1_MASK);
dt_shortcut_register(ac, 0, DT_ACTION_EFFECT_DOWN, GDK_KEY_bracketright, GDK_MOD1_MASK);
dt_shortcut_register(ac, 0, 0, GDK_KEY_r, GDK_MOD1_MASK);
g->cropmode = dt_bauhaus_combobox_from_params(self, "cropmode");
g_signal_connect(G_OBJECT(g->cropmode), "value-changed",
G_CALLBACK(cropmode_callback), self);
GtkWidget *main_box = self->widget;
dt_gui_new_collapsible_section
(&g->cs,
"plugins/darkroom/ashift/expand_values",
_("manual perspective"),
GTK_BOX(main_box),
DT_ACTION(self));
self->widget = GTK_WIDGET(g->cs.container);
g->lensshift_v = dt_bauhaus_slider_from_params(self, "lensshift_v");
dt_bauhaus_slider_set_soft_range(g->lensshift_v, -LENSSHIFT_RANGE, LENSSHIFT_RANGE);
dt_bauhaus_slider_set_digits(g->lensshift_v, 3);
g->lensshift_h = dt_bauhaus_slider_from_params(self, "lensshift_h");
dt_bauhaus_slider_set_soft_range(g->lensshift_h, -LENSSHIFT_RANGE, LENSSHIFT_RANGE);
dt_bauhaus_slider_set_digits(g->lensshift_h, 3);
g->shear = dt_bauhaus_slider_from_params(self, "shear");
dt_bauhaus_slider_set_soft_range(g->shear, -SHEAR_RANGE, SHEAR_RANGE);
g->mode = dt_bauhaus_combobox_from_params(self, "mode");
self->widget = g->specifics = dt_gui_vbox();
g->f_length = dt_bauhaus_slider_from_params(self, "f_length");
dt_bauhaus_slider_set_soft_range(g->f_length, 10.0f, 1000.0f);
dt_bauhaus_slider_set_log_curve(g->f_length);
dt_bauhaus_slider_set_digits(g->f_length, 0);
dt_bauhaus_slider_set_format(g->f_length, " mm");
g->crop_factor = dt_bauhaus_slider_from_params(self, "crop_factor");
dt_bauhaus_slider_set_soft_range(g->crop_factor, 1.0f, 2.0f);
g->orthocorr = dt_bauhaus_slider_from_params(self, "orthocorr");
dt_bauhaus_slider_set_format(g->orthocorr, "%");
// 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_widget_set_no_show_all(g->orthocorr, TRUE);
gtk_widget_set_visible(g->orthocorr, FALSE);
g->aspect = dt_bauhaus_slider_from_params(self, "aspect");
dt_bauhaus_slider_set_curve(g->aspect, log2_curve);
dt_gui_box_add(g->cs.container, g->specifics);
self->widget = main_box;
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "perspective")));
GtkGrid *auto_grid = GTK_GRID(gtk_grid_new());
gtk_grid_set_row_spacing(auto_grid, 2 * DT_BAUHAUS_SPACE);
gtk_grid_set_column_spacing(auto_grid, DT_PIXEL_APPLY_DPI(10));
gtk_grid_attach(auto_grid, dt_ui_label_new(_("structure")), 0, 0, 1, 1);
g->structure_lines = dtgtk_togglebutton_new(dtgtk_cairo_paint_masks_drawn, 0, NULL);
gtk_widget_set_hexpand(GTK_WIDGET(g->structure_lines), TRUE);
gtk_grid_attach(auto_grid, g->structure_lines, 1, 0, 1, 1);
g->structure_quad = dtgtk_togglebutton_new(dtgtk_cairo_paint_draw_structure, 0, NULL);
gtk_widget_set_hexpand(GTK_WIDGET(g->structure_quad), TRUE);
gtk_grid_attach(auto_grid, g->structure_quad, 2, 0, 1, 1);
g->structure_auto = dtgtk_togglebutton_new(dtgtk_cairo_paint_structure, 0, NULL);
gtk_widget_set_hexpand(GTK_WIDGET(g->structure_auto), TRUE);
gtk_grid_attach(auto_grid, g->structure_auto, 3, 0, 1, 1);
gtk_grid_attach(auto_grid, dt_ui_label_new(_("fit")), 0, 1, 1, 1);
g->fit_v = dtgtk_button_new(dtgtk_cairo_paint_perspective, 1, NULL);
gtk_widget_set_hexpand(GTK_WIDGET(g->fit_v), TRUE);
gtk_grid_attach(auto_grid, g->fit_v, 1, 1, 1, 1);
g->fit_h = dtgtk_button_new(dtgtk_cairo_paint_perspective, 2, NULL);
gtk_widget_set_hexpand(GTK_WIDGET(g->fit_h), TRUE);
gtk_grid_attach(auto_grid, g->fit_h, 2, 1, 1, 1);
g->fit_both = dtgtk_button_new(dtgtk_cairo_paint_perspective, 3, NULL);
gtk_widget_set_hexpand(GTK_WIDGET(g->fit_both), TRUE);
gtk_grid_attach(auto_grid, g->fit_both, 3, 1, 1, 1);
gtk_widget_show_all(GTK_WIDGET(auto_grid));
dt_gui_box_add(self->widget, auto_grid);
gtk_widget_set_tooltip_text
(g->rotation,
_("rotate image\nright-click and drag to define a horizontal or vertical"
" line by drawing on the 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->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_auto, _("automatically 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->structure_quad, _("manually define perspective rectangle"));
gtk_widget_set_tooltip_text(g->structure_lines, _("manually draw structure lines"));
g_object_set_data(G_OBJECT(g->fit_v), DT_ACTION_GESTURE_KEY,
dt_gui_connect_click(g->fit_v, _event_fit_v_button_clicked, NULL, self));
g_object_set_data(G_OBJECT(g->fit_h), DT_ACTION_GESTURE_KEY,
dt_gui_connect_click(g->fit_h, _event_fit_h_button_clicked, NULL, self));
g_object_set_data(G_OBJECT(g->fit_both), DT_ACTION_GESTURE_KEY,
dt_gui_connect_click(g->fit_both, _event_fit_both_button_clicked, NULL, self));
g_object_set_data(G_OBJECT(g->structure_quad), DT_ACTION_GESTURE_KEY,
_connect_structure_button(g->structure_quad, G_CALLBACK(_event_structure_quad_clicked), self));
g_object_set_data(G_OBJECT(g->structure_lines), DT_ACTION_GESTURE_KEY,
_connect_structure_button(g->structure_lines, G_CALLBACK(_event_structure_lines_clicked), self));
g_object_set_data(G_OBJECT(g->structure_auto), DT_ACTION_GESTURE_KEY,
_connect_structure_button(g->structure_auto, G_CALLBACK(_event_structure_auto_clicked), self));
g_signal_connect(G_OBJECT(self->widget), "draw", G_CALLBACK(_event_draw), self);
dt_action_define_iop(self, N_("fit"),
N_("vertical"), g->fit_v, &dt_action_def_button);
dt_action_define_iop(self, N_("fit"),
N_("horizontal"), g->fit_h, &dt_action_def_button);
dt_action_define_iop(self, N_("fit"),
N_("both"), g->fit_both, &dt_action_def_button);
dt_action_define_iop(self, N_("structure"),
N_("rectangle"), g->structure_quad, &dt_action_def_toggle);
dt_action_define_iop(self, N_("structure"),
N_("lines"), g->structure_lines, &dt_action_def_toggle);
dt_action_define_iop(self, N_("structure"),
N_("auto"), g->structure_auto, &dt_action_def_toggle);
/* add signal handler for preview pipe finish to redraw the overlay */
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, _event_process_after_preview_callback);
darktable.develop->proxy.rotate = self;
}
void gui_cleanup(dt_iop_module_t *self)
{
if(darktable.develop->proxy.rotate == self)
darktable.develop->proxy.rotate = NULL;
const dt_iop_ashift_gui_data_t *g = self->gui_data;
if(g->lines) free(g->lines);
dt_free_align(g->buf);
if(g->points) free(g->points);
if(g->points_idx) free(g->points_idx);
}
GSList *mouse_actions(dt_iop_module_t *self)
{
GSList *lm = NULL;
lm = dt_mouse_action_create_format
(lm, DT_MOUSE_ACTION_RIGHT_DRAG, 0, _("[%s] define/rotate horizon"), self->name());
lm = dt_mouse_action_create_format
(lm, DT_MOUSE_ACTION_LEFT, 0, _("[%s on segment] select segment"), self->name());
lm = dt_mouse_action_create_format
(lm, DT_MOUSE_ACTION_RIGHT, 0,
_("[%s on segment] unselect segment"), self->name());
lm = dt_mouse_action_create_format
(lm, DT_MOUSE_ACTION_LEFT_DRAG, GDK_SHIFT_MASK,
_("[%s] select all segments from zone"), self->name());
lm = dt_mouse_action_create_format
(lm, DT_MOUSE_ACTION_RIGHT_DRAG, GDK_SHIFT_MASK,
_("[%s] unselect all segments from zone"), self->name());
return lm;
}
// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on