diff --git a/+cv/ShapeTransformer.m b/+cv/ShapeTransformer.m new file mode 100644 index 000000000..b2865685a --- /dev/null +++ b/+cv/ShapeTransformer.m @@ -0,0 +1,239 @@ +classdef ShapeTransformer < handle + %SHAPETRANSFORMER Base class for shape transformation algorithms + % + % See also: cv.ShapeTransformer.ShapeTransformer, + % cv.ShapeContextDistanceExtractor, tpaps + % + + properties (SetAccess = private) + id % Object ID + end + + properties (Dependent, Hidden) + % The regularization parameter for relaxing the exact interpolation + % requirements of the TPS algorithm. + % Beta value of the regularization parameter, default 0 + % (only applicable for ThinPlateSplineShapeTransformer) + RegularizationParameter + % default true + % (only applicable for AffineTransformer) + FullAffine + end + + %% Constructor/destructor + methods + function this = ShapeTransformer(transformerType, varargin) + %SHAPETRANSFORMER Constructor + % + % obj = cv.ShapeTransformer(transformerType) + % obj = cv.ShapeTransformer(transformerType, 'OptionName',optionValue, ...) + % + % ## Input + % * __transformerType__ an algorithm that defines the aligning + % transformation. One of: + % * __ThinPlateSplineShapeTransformer__ Definition of the + % transformation occupied in the paper [Bookstein89]. + % * __AffineTransformer__ Wrapper class for the OpenCV + % Affine Transformation algorithm. + % See cv.estimateRigidTransform, cv.warpAffine, and + % cv.transform + % + % ## Options + % The following are options for the various algorithms: + % + % ### `ThinPlateSplineShapeTransformer` + % * __RegularizationParameter__ The regularization parameter for + % relaxing the exact interpolation requirements of the TPS + % algorithm. default 0 + % + % ### `AffineTransformer` + % * __FullAffine__ see cv.estimateRigidTransform, default true + % + % ## References + % [Bookstein89]: + % > "Principal Warps: Thin-Plate Splines and Decomposition of + % > Deformations", by F.L. Bookstein (PAMI 1989) + % + % See also: cv.ShapeTransformer.estimateTransformation + % + this.id = ShapeTransformer_(0, 'new', transformerType, varargin{:}); + end + + function delete(this) + %DELETE Destructor + % + % obj.delete() + % + % See also: cv.ShapeTransformer + % + if isempty(this.id), return; end + ShapeTransformer_(this.id, 'delete'); + end + end + + %% ShapeTransformer + methods + function estimateTransformation(this, transformingShape, targetShape, matches) + %ESTIMATETRANSFORMATION Estimate the transformation parameters of the current transformer algorithm, based on point matches + % + % obj.estimateTransformation(transformingShape, targetShape, matches) + % + % ## Input + % * __transformingShape__ Contour defining first shape. A numeric + % Nx2/Nx1x2/1xNx2 array or a cell-array of 2D points + % `{[x,y], ...}` + % * __targetShape__ Contour defining second shape (target). Same + % format as `transformingShape`. + % * __matches__ Standard vector of matches between points. + % + % See also: cv.ShapeTransformer.applyTransformation, + % cv.ShapeTransformer.warpImage + % + ShapeTransformer_(this.id, 'estimateTransformation', transformingShape, targetShape, matches); + end + + function [cost, output] = applyTransformation(this, input) + %APPLYTRANSFORMATION Apply a transformation, given a pre-estimated transformation parameters + % + % [cost, output] = obj.applyTransformation(input) + % + % ## Input + % * __input__ Contour (set of points) to apply the transformation. + % + % ## Output + % * __cost__ Transformation cost. + % * __output__ Output contour. + % + % See also: cv.ShapeTransformer.estimateTransformation + % + [cost, output] = ShapeTransformer_(this.id, 'applyTransformation', input); + end + + function output = warpImage(this, transformingImage, varargin) + %WARPIMAGE Apply a transformation, given a pre-estimated transformation parameters, to an Image + % + % output = obj.warpImage(transformingImage) + % output = obj.warpImage(..., 'OptionName',optionValue, ...) + % + % ## Input + % * __transformingImage__ Input image. + % + % ## Output + % * __output__ Output image. + % + % ## Options + % * __Interpolation__ Image interpolation method. default 'Linear' + % * __BorderType__ border style. default 'Constant' + % * __BorderValue__ border value. default 0 + % + % See cv.remap or cv.warpAffine for options description. + % + % See also: cv.ShapeTransformer.estimateTransformation + % + output = ShapeTransformer_(this.id, 'warpImage', transformingImage, varargin{:}); + end + end + + %% Algorithm + methods + function clear(this) + %CLEAR Clears the algorithm state + % + % obj.clear() + % + % See also: cv.ShapeTransformer.empty, cv.ShapeTransformer.load + % + ShapeTransformer_(this.id, 'clear'); + end + + function b = empty(this) + %EMPTY Returns true if the algorithm is empty + % + % b = obj.empty() + % + % ## Output + % * __b__ Returns true if the detector object is empty (e.g in the + % very beginning or after unsuccessful read). + % + % See also: cv.ShapeTransformer.clear, cv.ShapeTransformer.load + % + b = ShapeTransformer_(this.id, 'empty'); + end + + function save(this, filename) + %SAVE Saves the algorithm parameters to a file + % + % obj.save(filename) + % + % ## Input + % * __filename__ Name of the file to save to. + % + % This method stores the algorithm parameters in the specified + % XML or YAML file. + % + % See also: cv.ShapeTransformer.load + % + ShapeTransformer_(this.id, 'save', filename); + end + + function load(this, fname_or_str, varargin) + %LOAD Loads algorithm from a file or a string + % + % obj.load(fname) + % obj.load(str, 'FromString',true) + % obj.load(..., 'OptionName',optionValue, ...) + % + % ## Input + % * __fname__ Name of the file to read. + % * __str__ String containing the serialized model you want to + % load. + % + % ## Options + % * __ObjName__ The optional name of the node to read (if empty, + % the first top-level node will be used). default empty + % * __FromString__ Logical flag to indicate whether the input is + % a filename or a string containing the serialized model. + % default false + % + % This method reads algorithm parameters from the specified XML or + % YAML file (either from disk or serialized string). The previous + % algorithm state is discarded. + % + % See also: cv.ShapeTransformer.save + % + ShapeTransformer_(this.id, 'load', fname_or_str, varargin{:}); + end + + function name = getDefaultName(this) + %GETDEFAULTNAME Returns the algorithm string identifier + % + % name = obj.getDefaultName() + % + % ## Output + % * __name__ This string is used as top level XML/YML node tag + % when the object is saved to a file or string. + % + % See also: cv.ShapeTransformer.save, cv.ShapeTransformer.load + % + name = ShapeTransformer_(this.id, 'getDefaultName'); + end + end + + %% Getters/Setters + methods + function value = get.RegularizationParameter(this) + value = ShapeTransformer_(this.id, 'get', 'RegularizationParameter'); + end + function set.RegularizationParameter(this, value) + ShapeTransformer_(this.id, 'set', 'RegularizationParameter', value); + end + + function value = get.FullAffine(this) + value = ShapeTransformer_(this.id, 'get', 'FullAffine'); + end + function set.FullAffine(this, value) + ShapeTransformer_(this.id, 'set', 'FullAffine', value); + end + end + +end diff --git a/src/+cv/private/ShapeTransformer_.cpp b/src/+cv/private/ShapeTransformer_.cpp new file mode 100644 index 000000000..d042f607b --- /dev/null +++ b/src/+cv/private/ShapeTransformer_.cpp @@ -0,0 +1,216 @@ +/** + * @file ShapeTransformer_.cpp + * @brief mex interface for cv::ShapeTransformer + * @ingroup shape + * @author Amro + * @date 2017 + */ +#include "mexopencv.hpp" +#include "mexopencv_shape.hpp" +using namespace std; +using namespace cv; + +namespace { +// Persistent objects +/// Last object id to allocate +int last_id = 0; +/// Object container +map > obj_; +} + +/** + * Main entry called from Matlab + * @param nlhs number of left-hand-side arguments + * @param plhs pointers to mxArrays in the left-hand-side + * @param nrhs number of right-hand-side arguments + * @param prhs pointers to mxArrays in the right-hand-side + */ +void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) +{ + // Check the number of arguments + nargchk(nrhs>=2 && nlhs<=2); + + // Argument vector + vector rhs(prhs, prhs+nrhs); + int id = rhs[0].toInt(); + string method(rhs[1].toString()); + + // Constructor is called. Create a new object from argument + if (method == "new") { + nargchk(nrhs>=3 && nlhs<=1); + obj_[++last_id] = create_ShapeTransformer( + rhs[2].toString(), rhs.begin() + 3, rhs.end()); + plhs[0] = MxArray(last_id); + mexLock(); + return; + } + + // Big operation switch + Ptr obj = obj_[id]; + if (obj.empty()) + mexErrMsgIdAndTxt("mexopencv:error", "Object not found id=%d", id); + if (method == "delete") { + nargchk(nrhs==2 && nlhs==0); + obj_.erase(id); + mexUnlock(); + } + else if (method == "clear") { + nargchk(nrhs==2 && nlhs==0); + obj->clear(); + } + else if (method == "load") { + nargchk(nrhs>=3 && (nrhs%2)==1 && nlhs==0); + string objname; + bool loadFromString = false; + for (int i=3; i(rhs[2].toString(), objname) : + Algorithm::load(rhs[2].toString(), objname)); + */ + ///* + // HACK: ShapeTransformer is abstract, use polymorphic obj->read + FileStorage fs(rhs[2].toString(), FileStorage::READ + + (loadFromString ? FileStorage::MEMORY : 0)); + if (!fs.isOpened()) + mexErrMsgIdAndTxt("mexopencv:error", "Failed to open file"); + FileNode fn(objname.empty() ? fs.getFirstTopLevelNode() : fs[objname]); + if (fn.empty()) + mexErrMsgIdAndTxt("mexopencv:error", "Failed to get node"); + obj->read(fn); + //*/ + } + else if (method == "save") { + nargchk(nrhs==3 && nlhs==0); + obj->save(rhs[2].toString()); + } + else if (method == "empty") { + nargchk(nrhs==2 && nlhs<=1); + plhs[0] = MxArray(obj->empty()); + } + else if (method == "getDefaultName") { + nargchk(nrhs==2 && nlhs<=1); + plhs[0] = MxArray(obj->getDefaultName()); + } + else if (method == "estimateTransformation") { + nargchk(nrhs==5 && nlhs==0); + vector matches(rhs[4].toVector()); + if (rhs[2].isNumeric() && rhs[3].isNumeric()) { + // contours expected as 1xNx2 + Mat transformingShape(rhs[2].toMat(CV_32F).reshape(2,1)), + targetShape(rhs[3].toMat(CV_32F).reshape(2,1)); + obj->estimateTransformation(transformingShape, targetShape, matches); + } + else if (rhs[2].isCell() && rhs[3].isCell()) { + vector transformingShape(rhs[2].toVector()), + targetShape(rhs[3].toVector()); + obj->estimateTransformation(transformingShape, targetShape, matches); + } + else + mexErrMsgIdAndTxt("mexopencv:error", "Invalid contour argument"); + } + else if (method == "applyTransformation") { + nargchk(nrhs==3 && nlhs<=2); + float transformCost = 0; + if (rhs[2].isNumeric()) { + // Nx2/1xNx2/Nx1x2 -> 1xNx2 + Mat input(rhs[2].toMat(CV_32F).reshape(2,1)), + output; + transformCost = obj->applyTransformation(input, + (nlhs>1 ? output : noArray())); + if (nlhs > 1) + // 1xNx2 -> Nx2 + plhs[1] = MxArray(output.reshape(1, output.cols)); + } + else if (rhs[2].isCell() && rhs[3].isCell()) { + vector input(rhs[2].toVector()), + output; + transformCost = obj->applyTransformation(input, + (nlhs>1 ? output : noArray())); + if (nlhs > 1) + plhs[1] = MxArray(output); + } + plhs[0] = MxArray(transformCost); + } + else if (method == "warpImage") { + nargchk(nrhs>=3 && (nrhs%2)==1 && nlhs<=1); + int flags = cv::INTER_LINEAR; + int borderMode = cv::BORDER_CONSTANT; + Scalar borderValue; + for (int i=3; iwarpImage(transformingImage, output, + flags, borderMode, borderValue); + plhs[0] = MxArray(output); + } + else if (method == "get") { + nargchk(nrhs==3 && nlhs<=1); + string prop(rhs[2].toString()); + if (prop == "RegularizationParameter") { + Ptr p = + obj.dynamicCast(); + if (p.empty()) + mexErrMsgIdAndTxt("mexopencv:error", + "Only supported for ThinPlateSplineShapeTransformer"); + plhs[0] = MxArray(p->getRegularizationParameter()); + } + else if (prop == "FullAffine") { + Ptr p = obj.dynamicCast(); + if (p.empty()) + mexErrMsgIdAndTxt("mexopencv:error", + "Only supported for AffineTransformer"); + plhs[0] = MxArray(p->getFullAffine()); + } + else + mexErrMsgIdAndTxt("mexopencv:error", + "Unrecognized property %s", prop.c_str()); + } + else if (method == "set") { + nargchk(nrhs==4 && nlhs==0); + string prop(rhs[2].toString()); + if (prop == "RegularizationParameter") { + Ptr p = + obj.dynamicCast(); + if (p.empty()) + mexErrMsgIdAndTxt("mexopencv:error", + "Only supported for ThinPlateSplineShapeTransformer"); + p->setRegularizationParameter(rhs[3].toDouble()); + } + else if (prop == "FullAffine") { + Ptr p = obj.dynamicCast(); + if (p.empty()) + mexErrMsgIdAndTxt("mexopencv:error", + "Only supported for AffineTransformer"); + p->setFullAffine(rhs[3].toBool()); + } + else + mexErrMsgIdAndTxt("mexopencv:error", + "Unrecognized property %s", prop.c_str()); + } + else + mexErrMsgIdAndTxt("mexopencv:error", + "Unrecognized operation %s", method.c_str()); +} diff --git a/test/unit_tests/TestShapeTransformer.m b/test/unit_tests/TestShapeTransformer.m new file mode 100644 index 000000000..cc1cd7fc1 --- /dev/null +++ b/test/unit_tests/TestShapeTransformer.m @@ -0,0 +1,43 @@ +classdef TestShapeTransformer + %TestShapeTransformer + + methods (Static) + function test_1 + img1 = cv.imread(fullfile(mexopencv.root(),'test','basketball1.png'), ... + 'Grayscale',true, 'ReduceScale',2); + img2 = cv.imread(fullfile(mexopencv.root(),'test','basketball2.png'), ... + 'Grayscale',true, 'ReduceScale',2); + + detector = cv.AKAZE(); + [keypoints1, descriptors1] = detector.detectAndCompute(img1); + [keypoints2, descriptors2] = detector.detectAndCompute(img2); + matcher = cv.DescriptorMatcher('BFMatcher', ... + 'NormType',detector.defaultNorm()); + matches = matcher.match(descriptors1, descriptors2); + pts1 = cat(1, keypoints1.pt); + pts2 = cat(1, keypoints2.pt); + + for i=1:2 + if i==1 + tps = cv.ShapeTransformer('AffineTransformer', ... + 'FullAffine',true); + assert(isequal(tps.FullAffine, true)); + else + tps = cv.ShapeTransformer('ThinPlateSplineShapeTransformer', ... + 'RegularizationParameter',25000); + assert(isequal(tps.RegularizationParameter, 25000)); + end + + tps.estimateTransformation(pts1, pts2, matches); + + [cost, pts3] = tps.applyTransformation(pts2); + validateattributes(cost, {'numeric'}, {'scalar'}); + validateattributes(pts3, {'numeric'}, {'size',size(pts2)}); + + img3 = tps.warpImage(img2); + validateattributes(img3, {class(img2)}, {'size',size(img2)}); + end + end + end + +end