%SURFPoints Object for storing SURF interest points
%
%   SURFPoints object describes SURF interest points.
%
%   POINTS = SURFPoints(LOCATION, PARAM1, VAL1, PARAM2, VAL2, ...)
%   constructs a SURFPoints object from an M-by-2 array of [x y] 
%   coordinates, LOCATION, and optional input parameters listed below.
%   Note that each additional parameter can be specified as a scalar or 
%   a vector whose length matches number of coordinates in LOCATION. 
%   The available parameters are:
%
%   'Scale'       Value greater than or equal to 1.6. Specifies scale at 
%                 which the interest points were detected.
%
%                 Default: 1.6
%
%   'Metric'      Value describing strength of detected feature. SURF 
%                 algorithm uses a determinant of approximated Hessian.
%
%                 Default: 0.0
%
%   'SignOfLaplacian' -1, 0, or 1 integer indicating sign of the Laplacian
%                 determined during the detection process.  This value
%                 can be used to accelerate the feature matching process.
%
%                 Default: 0
%
%   'Orientation' Value, in radians, describing orientation of the detected
%                 feature. It is typically set during the descriptor 
%                 extraction process. extractFeatures function modifies
%                 the default value of 0. See extractFeatures for further 
%                 details.
%
%                 Default: 0.0
%
%   Notes:
%   ======
%   - The main purpose of this class is to pass the data between
%     detectSURFFeatures and extractFeatures functions. It can also be used
%     to manipulate and plot the data returned by these functions.
%     Using the class to fill the points interactively is considered an
%     advanced maneuver. It is useful in situations where you might want to
%     mix a non-SURF interest point detector with a SURF descriptor.
%   - 'Orientation' is specified as an angle, in radians, as measured from
%     the X-axis with the origin at 'Location'.  'Orientation' should not 
%     be set manually.  You should rely on the call to extractFeatures for 
%     filling this value. 'Orientation' is mainly useful for visualization
%     purposes.
%   - 'SignOfLaplacian' is a property unique to SURF detector. Blobs with 
%     identical metric values but different signs of Laplacian will differ 
%     by their intensity values: white blob on black background vs. black
%     blob on white background. This value can be used to quickly eliminate
%     blobs that don't match in this sense. For non-SURF detectors, this 
%     value is not relevant although it should be set consistently as to
%     not affect the matching process.  For example, for corner features, 
%     you can simply use the default value of 0.
%   - Note that SURFPoints is always a scalar object which may hold many
%     points. Therefore, NUMEL(surfPoints) always returns 1. This may be 
%     different from LENGTH(surfPoints), which returns the true number
%     of points held by the object.
%
%   SURFPoints methods:
%      selectStrongest  - Select N interest points with strongest metrics
%      plot             - Plot SURF points
%      append           - Append points
%      length           - Return number of stored points
%      isempty          - Return true for empty SURFPoints object
%      size             - Return size of the SURFPoints object
%
%   SURFPoints public properties:
%      Count            - Number of points held by the object
%      Location         - Matrix of [X,Y] point coordinates
%      Scale            - Scale at which the feature was detected
%      Metric           - Strength of each feature
%      SignOfLaplacian  - Sign of the Laplacian
%      Orientation      - Orientation assigned to the feature during 
%                         the descriptor extraction process
%
%   Example 1
%   ---------
%   % Detect SURF features
%   I = imread('cameraman.tif');
%   points = detectSURFFeatures(I);
%   % Display 10 strongest points in an image and on command line
%   strongest = points.selectStrongest(10);
%   imshow(I); hold on;
%   plot(strongest);   % show location and scale
%   strongest.Location % display [x y] coordinates
%
%   Example 2
%   ---------
%   % Detect SURF features
%   I = imread('cameraman.tif');
%   points = detectSURFFeatures(I);
%   % Display the last 5 points
%   imshow(I); hold on;
%   plot(points(end-4:end));
%
%   Example 3
%   ---------
%   % Mix FAST corner detector with SURF descriptors
%   I = imread('cameraman.tif');
%   % Construct corner detector System object
%   hcornerdet = vision.CornerDetector( ... 
%          'Method', 'Local intensity comparison (Rosten & Drummond)');
%
%   % Invoke the object on image I
%   ptsArray = step(hcornerdet, I);
%
%   % Transform the array of [x y] coordinates into SURFPoints object
%   ptsObj = SURFPoints(ptsArray);
%   
%   % Extract SURF descriptors and visualize the first 5 descriptors
%   % including their scale and orientation which was computed during 
%   % the extraction process
%   [features, validPtsObj] = extractFeatures(I, ptsObj);
%   imshow(I); hold on;
%   plot(validPtsObj(1:5),'showOrientation',true);
%
% See also detectSURFFeatures, extractFeatures, matchFeatures

% Copyright 2011 The MathWorks, Inc.
% $Revision: 1.1.6.2.2.1 $  $Date: 2011/06/07 22:46:54 $

classdef SURFPoints
    
   properties (SetAccess='private', GetAccess='public', Dependent = true)
       %Count Number of stored interest points
       Count;
   end
     
   properties (Access='public', Dependent = true)
       %Location Array of [x y] point coordinates
       Location;
       %Scale Array of point scales
       Scale;
       %Metric Value indicating feature's strength
       Metric;
       %SignOfLaplacian Sign of Laplacian
       SignOfLaplacian;
       %Orientation Orientation of the feature
       Orientation;
   end

   % Internal properties that are accessible only indirectly through
   % dependent properties
   properties (Access='private')
       pLocation        = ones(0,2,'single');
       pScale           = ones(0,1,'single');
       pMetric          = ones(0,1,'single');
       pSignOfLaplacian = ones(0,1,'int8'  );
       pOrientation     = ones(0,1,'single');
   end
   
   methods % Accessors for Dependent properties
       %-----------------------------------------------
       function this = set.Location(this, in)
           this.checkForResizing(in);
           checkLocation(in);
           this.pLocation = in;
       end
       function out = get.Location(this)
           out = this.pLocation;
       end
       %------------------------------------------------
       function this = set.Scale(this, in)
           this.checkForResizing(in);
           checkScale(in);
           this.pScale = in;
       end
       function out = get.Scale(this)
           out = this.pScale;
       end
       %------------------------------------------------
       function this = set.Metric(this, in)
           this.checkForResizing(in);          
           checkMetric(in);
           this.pMetric = in;
       end
       function out = get.Metric(this)
           out = this.pMetric;
       end
       %-------------------------------------------------
       function this = set.SignOfLaplacian(this, in)
           this.checkForResizing(in);           
           checkSignOfLaplacian(in);
           this.pSignOfLaplacian = in;
       end
       function out = get.SignOfLaplacian(this)
           out = this.pSignOfLaplacian;
       end
       %-------------------------------------------------
       function this = set.Orientation(this, in)
           this.checkForResizing(in);
           checkOrientation(in);
           this.pOrientation = in;
       end
       function out = get.Orientation(this)
           out = this.pOrientation;
       end
       %-------------------------------------------------
       function out = get.Count(this)
           out = size(this.Location,1);
       end
   end
   
   %-----------------------------------------------------------------------
   methods (Access='public')
       function this = SURFPoints(varargin)
           %SURFPoints constructor
           
           if nargin >= 1
               this = this.append(varargin{:});
           end
       end  
              
       function this = append(this,varargin)
           %append Appends additional SURF points
           %
           %   surfPoints = surfPoints.append(LOCATION) appends additional
           %   point locations. LOCATION is an M-by-2 matrix of [X,Y] 
           %   point coordinates.
           %
           %   surfPoints = surfPoints.append(LOCATION, PARAM1, VAL1,
           %   PARAM2, VAL2, ....) sets additional parameters of SURF 
           %   points. Each parameter can be specified as a scalar or 
           %   a column vector whose length matches number of coordinates 
           %   in LOCATION. The available parameters are:
           %
           %   'Scale'       Value greater than or equal to 1.6. Specifies 
           %                 scales at which the interest points were 
           %                 detected.
           %
           %                 Default: 1.6
           %
           %   'Metric'      Value describing strength of detected feature.
           %                 SURF algorithm uses a determinant of 
           %                 approximated Hessian.
           %
           %                 Default: 0.0
           %
           %   'SignOfLaplacian' -1, 0, or 1 integer indicating sign of the
           %                 Laplacian determined during the detection 
           %                 process.  This value can be used to 
           %                 accelerate the feature matching process.
           %
           %                 Default: 0
           %
           %   'Orientation' Value, in radians, describing orientation of 
           %                 the detected feature. It is typically set 
           %                 during the descriptor extraction process. 
           %                 extractFeatures function modifies the default
           %                 value of 0. See extractFeatures for further
           %                 details.
           %
           %                 Default: 0.0
           %
           %   Example
           %   -------
           %   % Mix FAST corner detector with SURF descriptors
           %   I = imread('cameraman.tif');
           %   % Construct corner detector System object
           %   hcornerdet = vision.CornerDetector( ...
           %       'Method', ...
           %       'Local intensity comparison (Rosten & Drummond)');
           %
           %   % Invoke the object on I
           %   ptsArray = step(hcornerdet, I);
           %
           %   % Transform the array of [x y] coordinates into SURFPoints
           %   ptsObj = SURFPoints; % start with an empty object
           %   ptsObj = ptsObj.append(ptsArray, 'Scale', 2);
           %
           %   % Extract SURF descriptors and visualize the first 5
           %   % descriptors
           %   [features, validPtsObj] = extractFeatures(I, ptsObj);
           %   imshow(I); hold on;
           %   plot(validPtsObj(1:5),'showOrientation',true);
           %
           
           inputs = parseInputs(varargin{:});
           
           indexS = this.Count + 1;
           indexE = indexS + size(inputs.Location,1) - 1;
           
           this.pLocation(indexS:indexE, 1:2)     = inputs.Location;
           this.pScale(indexS:indexE, 1)          = inputs.Scale;
           this.pMetric(indexS:indexE, 1)         = inputs.Metric;
           this.pSignOfLaplacian(indexS:indexE,1) = inputs.SignOfLaplacian;
           this.pOrientation(indexS:indexE, 1)    = inputs.Orientation;
       end
              
       %------------------------------------------------------------------
       % Note:  NUMEL is not overridden because it interferes with the
       %        desired operation of this object.  SURFPoints is a scalar
       %        object which pretends to be a vector. NUMEL is used during
       %        subsref operations and therefore needs to represent true
       %        number of elements for the object, which is always 1.
       %------------------------------------------------------------------
       function out = length(this)
           %length Returns number of points
           out = this.Count;
       end       
       
       %------------------------------------------------------------------
       function out = isempty(this)
           %isempty Returns true if the object is empty
           out = this.Count == 0;
       end
       
       %------------------------------------------------------------------
       function out = size(this)
           %size Returns size of the object
           out = [this.Count 1];
       end

       %-------------------------------------------------------------------
       function varargout = plot(this, varargin)
           %plot Plot SURF points
           %
           %   surfPoints.plot plots SURF points in the current axis.
           %
           %   surfPoints.plot(AXES_HANDLE,...) plots using axes with
           %   the handle AXES_HANDLE instead of the current axes (gca).
           %
           %   surfPoints.plot(AXES_HANDLE, PARAM1, VAL1, PARAM2,
           %   VAL2, ...) controls additional plot parameters:
           %
           %      'showScale'   true or false.  When true, a circle 
           %                    proportional to the scale of the detected
           %                    feature is drawn around the point's
           %                    location
           %
           %                    Default: true
           %
           %      'showOrientation' true or false. When true, a line
           %                    corresponding to the point's orientation 
           %                    is drawn from the point's location to the
           %                    edge of the circle indicating the scale
           %
           %                    Default: false
           %
           %   Notes
           %   -----
           %   - Scale of the feature is represented by a circle of
           %     6*Scale radius, which is equivalent to the size of
           %     circular area used by the SURF algorithm to compute 
           %     orientation of the feature
           %
           %   Example
           %   -------
           %   % Extract SURF features
           %   I = imread('cameraman.tif');
           %   points = detectSURFFeatures(I);
           %   [features, valid_points] = extractFeatures(I, points);
           %   % Visualize 10 strongest SURF features, including their 
           %   % scales and orientation which were determined during the 
           %   % descriptor extraction process.
           %   imshow(I); hold on;
           %   strongestPoints = valid_points.selectStrongest(10);
           %   strongestPoints.plot('showOrientation',true);
              
           error(nargoutchk(0,1,nargout));
           
           [h inputs] = parsePlotInputs(varargin{:});
           
           if ~inputs.showScale && ~inputs.showOrientation
               % invoke basic points plot
               plot(h, this.pLocation(:,1), this.pLocation(:,2),'g+');
           else
               phi = linspace(0,2*pi);
               x = cos(phi);  % will cause horizontal line +90 for vertical
               y = sin(phi);
               
               if inputs.showOrientation % Plot orientation
                   % the two zeros result in a horizontal line which
                   % will be rotated at a later stage
                   unitCircle = [x 0; y 0];
               else
                   unitCircle = [x; y];
               end
               
               wasHeld = ishold;
               
               for k = 1:this.Count
                   scale  = 6*this.pScale(k);
                   pt     = this.pLocation(k,:)';
                   orient = this.pOrientation(k);
                   
                   rotationMat = [cos(orient) -sin(orient);...
                       sin(orient) cos(orient)];
                   
                   surfCircle = scale*rotationMat*unitCircle + ...
                       pt*ones(1,size(unitCircle,2));
                   plot(h, pt(1), pt(2), 'g+'); % + marking center
                   % turn the hold state on so that all points are 
                   % plotted; this is necessary since we are plotting in
                   % a loop, otherwise each plot command will overwite the
                   % previous result
                   if k==1 && ~wasHeld
                       hold('on');
                   end
                   plot(h, surfCircle(1,:),surfCircle(2,:),'g-');
                   
               end
               
               if ~wasHeld
                   hold('off'); % restore original states of hold
               end
           end
          
           if nargout == 1
               varargout{1} = h;
           end
           
       end
       
       %-------------------------------------------------------------------
       function this = selectStrongest(this, N)
       %selectStrongest Return N points with strongest metrics
       %
       %   surfPoints = surfPoints.selectStrongest(N) keeps N points with
       %   strongest metrics.
       %
       %   Example
       %   -------
       %   % create object holding 50 points
       %   points = SURFPoints(ones(50,2), 'Metric', 1:50);
       %   % keep 2 strongest features
       %   points = points.selectStrongest(2)
       
       validateattributes(N, {'numeric'}, {'scalar', 'integer', ...
           'positive'}, mfilename);

       [this.pMetric idx] = sort(this.pMetric,'descend');
       
       this.pLocation        = this.pLocation(idx,:);
       this.pScale           = this.pScale(idx);
       this.pSignOfLaplacian = this.pSignOfLaplacian(idx);
       this.pOrientation     = this.pOrientation(idx);
       
       this = this.remove(N+1:this.Count);
       
       end

       %-------------------------------------------------------------------       
       function ind = end(this,~,~)
           ind = this.Count;
       end
       
       %-------------------------------------------------------------------
       function varargout = subsref(this,s)
           
           error(nargoutchk(0,1,nargout));
           
           switch s(1).type
               case '()'
                   % location needs special handling since the expected
                   % index is for a vector and Location is an Mx2 matrix;
                   % we modify "s" so that second index is 1:2
                   locS = s; % modify index to access Mx2 location matrix
                   locS.subs{2} = 1:2;
                   this.pLocation        = subsref(this.pLocation,locS);

                   % handle the rest of the properties
                   this.pScale           = subsref(this.pScale,s);
                   this.pMetric          = subsref(this.pMetric,s);
                   this.pSignOfLaplacian = subsref(this.pSignOfLaplacian,s);
                   this.pOrientation     = subsref(this.pOrientation,s);

                   % protect against indexing that would affect integrity
                   % of the object
                   assert(size(this.pLocation,2) == 2 && ...
                          size(this.pMetric,2)   == 1 && ...
                          ndims(this.pMetric)    == 2 ,...
                       'vision:SURFPoints:invalidIndexingOperation',...
                       'Invalid indexing operation.');
                   
                   varargout{1} = this;
               case {'{}', '.'}
                    % the only reason for having nargout check is plot()
                    % which may or may not take an output
                   if nargout == 1                       
                       varargout{1} = builtin('subsref',this,s);
                   else
                       builtin('subsref',this,s)
                   end
           end
       end


       %-------------------------------------------------------------------
       function out = subsasgn(this,s,in)
           switch s(1).type
               case '()'
                   assert(isa(in,'SURFPoints') || isempty(in),...
                       'vision:SURFPoints:badAssignmentInput',...
                       ['Right hand side of the assignement must',...
                       ' be [], or it must be a SURFPoints object.']);
                   
                   locS = s; % modify index to access Mx2 location matrix
                   locS.subs{2} = ':';
                   
                   if isempty(in)
                       this.pLocation = subsasgn(this.pLocation,locS,in);
                       this.pScale    = subsasgn(this.pScale,s,in);
                       this.pMetric   = subsasgn(this.pMetric,s,in);
                       this.pSignOfLaplacian = ...
                           subsasgn(this.pSignOfLaplacian,s,in);
                       this.pOrientation = ...
                           subsasgn(this.pOrientation,s,in);
                   else
                       this.pLocation = ...
                           subsasgn(this.pLocation,locS,in.pLocation);
                       this.pScale = ...
                           subsasgn(this.pScale,s,in.pScale);
                       this.pMetric = ...
                           subsasgn(this.pMetric,s,in.pMetric);
                       this.pSignOfLaplacian = ...
                           subsasgn(this.pSignOfLaplacian,s,...
                           in.pSignOfLaplacian);
                       this.pOrientation = ...
                           subsasgn(this.pOrientation,s,in.pOrientation);
                   end
                   
                   out = this;
               case {'{}', '.'}
                   out = builtin('subsasgn',this,s,in);
           end
       end
       
       %-------------------------------------------------------------------
       % All of the methods below have to be managed because they can 
       % create non-scalar arrays of objects and SURFPoints is strictly
       % a scalar object.
       %-------------------------------------------------------------------
       function out = cat(dim,varargin)
           validateattributes(dim, {'numeric'}, {'nonempty','finite',...
               'integer'},mfilename,'Dimension');
           assert(dim == 2,'vision:SURFPoints:badCatDim',...
               'Dimension must be 2.');
           out = vertcat(varargin{:});
       end
       %
       function out = horzcat(varargin) %#ok<STOUT>
           error('vision:SURFPoints:noHorzcatAllowed', ...
               ['Horizontal concatenation of SURFPoints objects',...
               ' is not permitted. Use ''vertcat'' instead.']);
       end
       %
       function out = vertcat(varargin)
           assert(all(cellfun(@(x) isa(x,'SURFPoints'), varargin)),...
               'vision:SURFPoints:badCatTypes',...
               'Inputs for concatenation must be SURFPoints objects.');
           
           out = varargin{1};
           for i=2:nargin
               out.pLocation = [out.pLocation; varargin{i}.pLocation];
               out.pScale    = [out.pScale   ; varargin{i}.pScale];
               out.pMetric   = [out.pMetric  ; varargin{i}.pMetric];
               out.pSignOfLaplacian = [out.pSignOfLaplacian; ...
                   varargin{i}.pSignOfLaplacian];
               out.pOrientation = [out.pOrientation; ...
                   varargin{i}.pOrientation];
           end           
       end    
       
   end
   
   methods (Access='private')
       %------------------------------------------------------------------
       % Prevent resizing of public properties
       function checkForResizing(this, in)
           assert(size(in,1) == this.Count,...
               'vision:SURFPoints:cannotResizePoints',...
               ['Individual SURFPoints properties cannot be resized',...
               ' using indexing. You can resize the entire object,',...
               ' using MATLAB indexing or by using the append method .']);
       end
       
       %------------------------------------------------------------------
       % Removes points; this method is used by selectStrongest
       function this = remove(this, index)
           this.pLocation(index,:)        = [];
           this.pScale(index,:)           = [];
           this.pMetric(index,:)          = [];
           this.pSignOfLaplacian(index,:) = [];
           this.pOrientation(index,:)     = [];
       end
       
   end
   
end

%--------------------------------------------------------------------------
% Plot input parser
%--------------------------------------------------------------------------
function [h inputs] = parsePlotInputs(varargin)

% Parse the PV pairs
parser = inputParser;
parser.CaseSensitive = true;

parser.addOptional('AXIS_HANDLE', [], @checkHandle)

parser.addParamValue('showScale',       true,  @checkFlag);

parser.addParamValue('showOrientation', false, @checkFlag);

% Parse input
parser.parse(varargin{:});

% Assign return values
h = parser.Results.AXIS_HANDLE;

if isempty(h)
    h = gca;
end

inputs.showScale        = parser.Results.showScale;
inputs.showOrientation  = parser.Results.showOrientation;

end

%--------------------------------------------------------------------------
%  Plot checkers
%--------------------------------------------------------------------------
function tf = checkHandle(in)

validateattributes(in, {'double'}, {'scalar'}, mfilename);

% Must be a valid ax
if ~ishghandle(in,'axes')
    error('vision:SURFPoints:badHandle',...
        'Input must be a valid axes handle');
end

tf = true;
end
%--------------------------------------------------------------------------
function tf = checkFlag(in)

validateattributes(in, {'logical'}, {'scalar'}, mfilename);

tf = true;
end

%--------------------------------------------------------------------------
% Main parser for the class
%--------------------------------------------------------------------------
function [inputs unused] = parseInputs(varargin)

% Parse the PV pairs
parser = inputParser;
parser.CaseSensitive = true;

parser.addRequired('Location',@checkLocation)

parser.addParamValue('Scale',          1.6, @checkScale);
parser.addParamValue('Metric',           0, @checkMetric);
parser.addParamValue('SignOfLaplacian',  0, @checkSignOfLaplacian);
parser.addParamValue('Orientation',      0, @checkOrientation);

% Parse input
parser.parse(varargin{:});

% Populate the parameters to pass into OpenCV's icvfastHessianDetector()
inputs.Location =        single(parser.Results.Location);
inputs.Scale    =        single(parser.Results.Scale);
inputs.Metric   =        single(parser.Results.Metric);
inputs.SignOfLaplacian = single(parser.Results.SignOfLaplacian);
inputs.Orientation     = single(parser.Results.Orientation);

numPts = size(inputs.Location,1);

% All parameters must have the same number of elements or be a scalar
validateParamLength(numel(inputs.Scale), 'Scale', numPts);
validateParamLength(numel(inputs.Metric), 'Metric', numPts);
validateParamLength(numel(inputs.SignOfLaplacian), 'SignOfLaplacian',...
        numPts);
validateParamLength(numel(inputs.Orientation), 'Orientation', numPts);

% Pack the input vectors into column vectors. For convenience, the input
% vector can be Mx1 or 1xN
if ~isscalar(inputs.Scale)
    reshape(inputs.Scale, numPts, 1);
end
if ~isscalar(inputs.Metric)
    reshape(inputs.Metric, numPts, 1);
end
if ~isscalar(inputs.SignOfLaplacian)
    reshape(inputs.SignOfLaplacian, numPts, 1);
end
if ~isscalar(inputs.Orientation)
    reshape(inputs.Orientation, numPts, 1);
end

unused = parser.UsingDefaults;

end

%--------------------------------------------------------------------------
function validateParamLength(numelParam, paramName, numPts)

assert(numelParam == 1 || numelParam == numPts,...
    'vision:SURFPoints:invalidParamLength', ['Number of %s elements ',...
    'must be one or it must match number of coordinates in LOCATION'],...
    paramName);

end

%--------------------------------------------------------------------------
function tf = checkLocation(in)
validateattributes(in, {'numeric'}, {'nonnan', ...
    'finite', 'nonsparse', 'real', 'positive', 'size',[NaN,2]}, ...
    mfilename);
tf = true;
end

%--------------------------------------------------------------------------
function tf = checkScale(in)
validateattributes(in, {'numeric'}, {'nonnan', ...
    'finite', 'nonsparse', 'real', '>=', 1.6, 'vector'}, ...
    mfilename);

tf = true;
end

%--------------------------------------------------------------------------
function tf = checkMetric(in)
validateattributes(in, {'numeric'}, {'nonnan', 'finite', 'nonsparse',...
    'real', 'vector'}, mfilename);
tf = true;
end

%--------------------------------------------------------------------------
function tf = checkSignOfLaplacian(in)
validateattributes(in, {'numeric'}, {'integer', 'real', 'vector'}, ...
    mfilename);

assert(all(in >= -1) && all( in <= 1), ...
    'vision:SURFPoints:invalidSignOfLaplacian',...
    'SignOfLaplacian input must contain 1, 0, or -1 values');

tf = true;
end

%--------------------------------------------------------------------------
function tf = checkOrientation(in)
validateattributes(in, {'numeric'}, {'nonnan', 'finite', 'nonsparse',...
    'real', 'vector'}, mfilename);
tf = true;
end

% LocalWords:  Laplacian
% LocalWords:  OpenCV
