From b2dd122907a6abe24d2fae46e453f23b3f5e390f Mon Sep 17 00:00:00 2001 From: Mohamed Aly Date: Thu, 13 Nov 2014 13:47:34 +0300 Subject: [PATCH] Initial port to GitHub --- README.md | 22 +- README.md~ | 4 + README.txt | 152 + matlab/Stats.m | 28 + matlab/ccvCheckMergeSplines.m | 51 + matlab/ccvEvalBezSpline.m | 100 + matlab/ccvGetLaneDetectionStats.m | 179 + matlab/ccvLabel.m | 444 ++ matlab/ccvReadLaneDetectionResultsFile.m | 61 + src/CameraInfo.conf | 21 + src/CameraInfoOpt.c | 878 +++ src/CameraInfoOpt.ggo | 27 + src/CameraInfoOpt.h | 100 + src/CameraInfoOpt.o | Bin 0 -> 25888 bytes src/InversePerspectiveMapping.cc | 556 ++ src/InversePerspectiveMapping.hh | 195 + src/InversePerspectiveMapping.o | Bin 0 -> 42760 bytes src/LaneDetector.cc | 7273 +++++++++++++++++++++ src/LaneDetector.cc~ | 7274 ++++++++++++++++++++++ src/LaneDetector.hh | 1393 +++++ src/LaneDetector.o | Bin 0 -> 777480 bytes src/LaneDetector32 | Bin 0 -> 468737 bytes src/LaneDetector64 | Bin 0 -> 558776 bytes src/LaneDetectorOpt.c | 5278 ++++++++++++++++ src/LaneDetectorOpt.ggo | 269 + src/LaneDetectorOpt.h | 500 ++ src/LaneDetectorOpt.o | Bin 0 -> 187592 bytes src/Lanes-mode2.conf | 175 + src/Lanes.conf | 175 + src/Makefile | 68 + src/Stoplines.conf | 174 + src/cmdline.c | 998 +++ src/cmdline.ggo | 68 + src/cmdline.h | 122 + src/cmdline.o | Bin 0 -> 30408 bytes src/main.cc | 277 + src/main.hh | 54 + src/main.o | Bin 0 -> 84816 bytes src/mcv.cc | 182 + src/mcv.hh | 85 + src/mcv.o | Bin 0 -> 9776 bytes src/ranker.h | 202 + src/run.sh | 31 + 43 files changed, 27413 insertions(+), 3 deletions(-) create mode 100644 README.md~ create mode 100755 README.txt create mode 100644 matlab/Stats.m create mode 100644 matlab/ccvCheckMergeSplines.m create mode 100644 matlab/ccvEvalBezSpline.m create mode 100644 matlab/ccvGetLaneDetectionStats.m create mode 100644 matlab/ccvLabel.m create mode 100644 matlab/ccvReadLaneDetectionResultsFile.m create mode 100644 src/CameraInfo.conf create mode 100644 src/CameraInfoOpt.c create mode 100644 src/CameraInfoOpt.ggo create mode 100644 src/CameraInfoOpt.h create mode 100644 src/CameraInfoOpt.o create mode 100755 src/InversePerspectiveMapping.cc create mode 100755 src/InversePerspectiveMapping.hh create mode 100644 src/InversePerspectiveMapping.o create mode 100755 src/LaneDetector.cc create mode 100755 src/LaneDetector.cc~ create mode 100755 src/LaneDetector.hh create mode 100644 src/LaneDetector.o create mode 100755 src/LaneDetector32 create mode 100755 src/LaneDetector64 create mode 100644 src/LaneDetectorOpt.c create mode 100644 src/LaneDetectorOpt.ggo create mode 100644 src/LaneDetectorOpt.h create mode 100644 src/LaneDetectorOpt.o create mode 100644 src/Lanes-mode2.conf create mode 100644 src/Lanes.conf create mode 100755 src/Makefile create mode 100644 src/Stoplines.conf create mode 100644 src/cmdline.c create mode 100755 src/cmdline.ggo create mode 100644 src/cmdline.h create mode 100644 src/cmdline.o create mode 100755 src/main.cc create mode 100644 src/main.hh create mode 100644 src/main.o create mode 100644 src/mcv.cc create mode 100755 src/mcv.hh create mode 100644 src/mcv.o create mode 100644 src/ranker.h create mode 100755 src/run.sh diff --git a/README.md b/README.md index df1b4b1..0246a8c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,20 @@ -caltech-lane-detection -====================== - Caltech Lane Detection Software +=============================== + +This package contains C/C++ and Matlab source code that implements the work in [1]. It implements a real time lane detection system for single images by fitting robust Bezier splines. It can detect all lanes in the street, or the two lane markers of the current lane. To quickly see it in action, download the software below and also the Caltech Lanes Dataset . + +The detection runs in real time, about 40-50 Hz and detects all lanes in the street. It was compiled and tested on Ubuntu Lucid Lynx 32-bit machine and Red Hat Enterprise Linux 5.5 64-bit machine. + +It also includes several functionalities that were missing from OpenCV at the time, including: + +Routines for obtaining the Inverse Perspective Mapping (IPM) of an image i.e. getting a bird's eye view of the road. +Routines for conversion to/from image pixel coordinates and coordinates on the road plane (using the ground plane assumption). +Robust & RANSAC line fitting. +Robust & RANSAC Bezier spline fitting. +Bezier spline rasterization and plotting. +Bresenham's line raterization. + +Various utility functions for checking intersections of lines with lines and bounding boxes, checking points inside a rectangle, ... etc. +An implementation of a general Hough transform routine for lines. + +[1] Mohamed Aly, Real time Detection of Lane Markers in Urban Streets, IEEE Intelligent Vehicles Symposium, Eindhoven, The Netherlands, June 2008. [pdf] diff --git a/README.md~ b/README.md~ new file mode 100644 index 0000000..df1b4b1 --- /dev/null +++ b/README.md~ @@ -0,0 +1,4 @@ +caltech-lane-detection +====================== + +Caltech Lane Detection Software diff --git a/README.txt b/README.txt new file mode 100755 index 0000000..aebba5c --- /dev/null +++ b/README.txt @@ -0,0 +1,152 @@ +# Author: Mohamed Aly +# Date: 10/7/2010 + +============================================================================ + REAL TIME LANE DETECTOR SOFTWARE +============================================================================ + +This package contains source code and dataset that implements the work in the +paper [1]. + +========= +Contents +========= +src/: contains the C/C++ source files + +|_ CameraInfo.conf: contains the camera calibration ifo + +|_ CameraInfoOpt.*: contain gengetopt files for parsing the camera info files + +|_ cmdline.*: contains gengetopt files for parsing command lines + +|_ InversePerspectiveMapping.*: code for obtainig the IPM of an image + +|_ LaneDetector.*: code for the bulk of the algorithm, including Hough + Transforms, Spline fitting, Post processing, ... + +|_ Lanes.conf: the typical configuration file for lane detection + +|_ main.*: code for the main binary + +|_ Makefile: the Make file + +|_ mcv.*: contain utility functions + +|_ ranker.h: code for obtaining the median of a vector + +|_ run.sh: Shell script for running the detector on the four clips in + Caltech Lanes Dataset + +matlab/: contains the Matlab source files + +|_ ccvCheckMergeSplines.m: checks if two splines are matching + +|_ ccvEvalBezSpline.m: returns points on a spline given its control points + +|_ ccvGetLaneDetectionStats.m: computes stats from detections and ground truth + +|_ ccvLabel.m: handles the ground truth labels + +|_ ccvReadLaneDetectionResultsFile.m: reads a detection file output from the + binary file LaneDetector32/64 + +|_ Stats.m: computes stats for the detections on the Caltech Lanes Dataset and + its ground truth labels + +============== +Prerequisites +============== +1. OpenCV 2.0 or higher http://sourceforge.net/projects/opencvlibrary/ +3. (Optional) Gengetopt http://www.gnu.org/software/gengetopt/ + +=========== +Compiling +=========== +Unzip the archive somewhere, let's say ~/lane-detector: + +unzip lane-detector.zip -d ~/lane-detector +cd ~/lane-detector/src +make release + +This will generate LaneDetector32 or LaneDetector64 depending on your system. + +====================== +Caltech Lanes Dataset +====================== +To view the lane detector in action, you can download the Caltech Lanes Dataset +available at http://www.vision.caltech.edu/malaa/datasets/caltech-lanes + +=========== +Running +=========== +To run the detector on the Caltech Lanes dataset, which might be in +~/caltech-lanes/ + +cd ~/lane-detector/ +ln -s ~/caltech-lanes/ clips +cd ~/lane-detector/src/ +bash run.sh + +This will create the results files inside +~/caltech-lanes/*/list.txt_results.txt + +To view the statistics of the results, open Matlab and run the file: + +cd ~/lane-detector/matlab/ +matlab& +>>Stats + +====================== +Command line options +====================== +LinePerceptor 1.0 + +Detects lanes in street images. + +Usage: LinePerceptor [OPTIONS]... [FILES]... + + -h, --help Print help and exit + -V, --version Print version and exit + +Basic options: + --lanes-conf=STRING Configuration file for lane detection + (default=`Lanes.conf') + --stoplines-conf=STRING Configuration file for stopline detection + (default=`StopLines.conf') + --no-stoplines Don't detect stop lines (default=on) + --no-lanes Don't detect lanes (default=off) + --camera-conf=STRING Configuration file for the camera paramters + (default=`CameraInfo.conf') + --list-file=STRING Text file containing a list of images one per + line + --list-path=STRING Path where the image files are located, this is + just appended at the front of each line in + --list-file (default=`') + --image-file=STRING The path to an image + +Debugging options: + --wait=INT Number of milliseconds to show the detected + lanes. Put 0 for infinite i.e. waits for + keypress. (default=`0') + --show Show the detected lines (default=off) + --step Step through each image (needs a keypress) or + fall through (waits for --wait msecs) + (default=off) + --show-lane-numbers Show the lane numbers on the output image + (default=off) + --output-suffix=STRING Suffix of images and results + (default=`_results') + --save-images Export all images with detected lanes to the by + appending --output-suffix + '.png' to each + input image (default=off) + --save-lanes Export all detected lanes to a text file by + appending --output-suffix + '.txt' to + --list-file (default=off) + --debug Show debugging information and images + (default=off) + +=========== +References +=========== +[1] Mohamed Aly, Real time Detection of Lane Markers in Urban Streets, + IEEE Intelligent Vehicles Symposium, Eindhoven, The Netherlands, June 2008. diff --git a/matlab/Stats.m b/matlab/Stats.m new file mode 100644 index 0000000..aca404b --- /dev/null +++ b/matlab/Stats.m @@ -0,0 +1,28 @@ +function Stats +% STATS computes stats for the results of LaneDetector +% +% Inputs: +% ------- +% +% Outputs: +% -------- +% + +% The detection files +detectionFiles = { + '../clips/cordova1/list.txt_results.txt' + '../clips/cordova2/list.txt_results.txt' + '../clips/washington1/list.txt_results.txt' + '../clips/washington1/list.txt_results.txt' + }; + +% The ground truth labels +truthFiles = { + '../clips/cordova1/labels.ccvl' + '../clips/cordova2/labels.ccvl' + '../clips/washington1/labels.ccvl' + '../clips/washington1/labels.ccvl' + }; + +% Get statistics +ccvGetLaneDetectionStats(detectionFiles, truthFiles); diff --git a/matlab/ccvCheckMergeSplines.m b/matlab/ccvCheckMergeSplines.m new file mode 100644 index 0000000..fbec2d1 --- /dev/null +++ b/matlab/ccvCheckMergeSplines.m @@ -0,0 +1,51 @@ +function merge = ccvCheckMergeSplines(spline1, spline2, meanDistThreshold, ... + medianDistThreshold) +% CCVCHECKMERGESPLINES checks if two splines are to be merged (matched) or +% not. It does this by computing the distance from every point in each +% spline to every other point in the other spline and checking that the mean +% or median distances are below the given thresholds. +% +% Inputs: +% ------- +% spline2: Nx2 matrix of spline control points +% spline2: Nx2 matrix of spline control points +% meanDistThreshold: threshold for the mean distance between points on the +% two splines +% medianDistThreshold: threshold for median distance between points on the +% two splines +% +% Outputs: +% -------- +% merge: 1 if to merge, 0 otherwise +% + +%get points on both +p1 = ccvEvalBezSpline(spline1, .01); +p2 = ccvEvalBezSpline(spline2, .01); + +%now for every point in spline1, compute nearest in spline2, and get that +%distance +dist1 = zeros(1, size(p1,1)); +for i=1:size(p1, 1) + %get diff + d = repmat(p1(i,:), size(p2, 1), 1) - p2; + %get distance + d = sqrt(sum(d.^2, 2)); + %get min + dist1(i) = min(d); +end; +dist2 = zeros(1, size(p2,1)); +for i=1:size(p2, 1) + %get diff + d = repmat(p2(i,:), size(p1, 1), 1) - p1; + %get distance + d = sqrt(sum(d.^2, 2)); + %get min + dist2(i) = min(d); +end; + +%compute mean and median +meanDist = min(mean(dist1), mean(dist2)); +medianDist = min(median(dist1), median(dist2)); + +merge = (meanDist <= meanDistThreshold) || (medianDist <= medianDistThreshold); diff --git a/matlab/ccvEvalBezSpline.m b/matlab/ccvEvalBezSpline.m new file mode 100644 index 0000000..44c5155 --- /dev/null +++ b/matlab/ccvEvalBezSpline.m @@ -0,0 +1,100 @@ +function [outPoints, tangent] = ccvEvalBezSpline(spline, h) +% ccvEvalBezSpline evaluates a Bezier spline with the specified degree + +% INPUTS +% ------ +% spline - input 3 or 4 points in a matrix 3x2 or 4x2 [xs, ys] +% h - [0.05] the interval to use for evaluation +% +% OUTPUTS +% ------- +% outPoints - output points nx2 [xs; ys] +% tangent - the tangent at the two end-points [t0; t1] +% +% EXAMPLE +% ------- +% [p, t] = ccvEvalBezSpline(sp, 0.1); +% +% See also ccvDrawBezSpline + +if nargin<2, h = 0.05; end + +%get the degree +degree = size(spline, 1) - 1; + +%compute number of return points +n = floor(1/h)+1; + +%degree +switch degree + %Quadratic Bezier curve + case 2 + M = [1, -2, 1; ... + -2, 2, 0; ... + 1, 0, 0]; + + %compute constants [a, b, c] + abcd = M * spline; + a = abcd(1,:); b = abcd(2,:); c = abcd(3,:); + + %compute at time 0 + P = c; + dP = b * h + a * h^2; + ddP = 2*a*h^2; + + %loop + outPoints = zeros(n, size(spline,2)); + outPoints(1,:) = P; + for i=2:n + %calculate new point + P = P + dP; + %update steps + dP = dP + ddP; + %put back + outPoints(i,:) = P; + end; + + %tangents: t0 = b + t0 = b; + %t1 = 2a+b + t1 = 2*a+b; + + %Cubic Bezier curve + case 3 + M = [-1, 3, -3, 1; ... + 3, -6, 3, 0; ... + -3, 3, 0, 0; ... + 1, 0, 0, 0]; + + %compute constants [a, b, c, d] + abcd = M * spline; + a = abcd(1,:); b = abcd(2,:); c = abcd(3,:); d = abcd(4,:); + + %compute at time 0 + P = d; + dP = c*h + b * h^2 + a * h^3; + ddP = 2*b*h^2 + 6*a*h^3; + dddP = 6*a*h^3; + + %loop + outPoints = zeros(n, size(spline,2)); + outPoints(1,:) = P; + for i=2:n + %calculate new point + P = P + dP; + %update steps + dP = dP + ddP; + ddP = ddP + dddP; + %put back + outPoints(i,:) = P; + end; + + %tangents: t0 = c + t0 = c; + %t1 = 3a+2*b+c + t1 = 3*a+2*b+c; +end; + +%put tangents together +tangent = [t0; t1]; + diff --git a/matlab/ccvGetLaneDetectionStats.m b/matlab/ccvGetLaneDetectionStats.m new file mode 100644 index 0000000..ace1353 --- /dev/null +++ b/matlab/ccvGetLaneDetectionStats.m @@ -0,0 +1,179 @@ +function ccvGetLaneDetectionStats(detectionFiles, truthFiles) +% CCVGETLANEDETECTIONSTATS computes stats for the results compared to the +% ground truth +% +% INPUTS +% ------ +% detectionFiles - a cell array of the detection files +% truthFiles - a cell array of the corresponding ground truth files +% +% OUTPUTS +% ------- +% +% See also ccvLabel +% + +% Thresholds for merging i.e. matching splines +meanDistThreshold = 15; +medianDistThreshold = 20; + +% Initialize +allResults = []; +allDetectionTotal = 0; +allTruthTotal = 0; +allNumFrames = 0; +allTp = 0; +allFp = 0; + +disp('------------------------------------------------------------------'); + +for d=1:length(detectionFiles) + %get detection and truth file + detectionFile = detectionFiles{d}; + truthFile = truthFiles{d}; + + %load the ground truth + truths = ccvLabel('read', truthFile); + + %load the detections file + detections = ccvReadLaneDetectionResultsFile(detectionFile); + + %results for this file + results = []; + detectionTotal = 0; + truthTotal = 0; + numFrames = 0; + + %progress index + prog = 0; + progress= '-\|/'; + fprintf(1, '\n-'); + + %loop on results and compare splines + for i=1:length(detections) + %get frame + detectionFrame = detections(i); + detectionSplines = detectionFrame.splines; + + %display progress + if mod(length(results), 10)==0 + fprintf(1, '\b%s', progress(prog+1)); + prog = mod(prog+1, length(progress)); + end; + + %get truth splines for that frame + truthFrame = ccvLabel('getFrame', truths, i); + if isempty(truthFrame), continue; end; + numFrames = numFrames + 1; + truthSplines = GetTruthSplines(truthFrame.labels); + + %update totals + detectionTotal = detectionTotal + length(detectionSplines); + truthTotal = truthTotal + length(truthSplines); + + %loop on these splines and compare to ground truth to get the closest + frameDetections = []; + truthDetections = zeros(1, length(truthSplines)); + for j=1:length(detectionSplines) + %flag + detection = 0; + %loop on truth and get which one + k = 1; + while detection==0 && k<=length(truthSplines) + if ccvCheckMergeSplines(detectionSplines{j}, ... + truthSplines{k}, meanDistThreshold, ... + medianDistThreshold); + %not false pos + detection = 1; + truthDetections(k) = 1; + end; + %inc + k = k+1; + end; %while + + %check result + result.score = detectionFrame.scores(j); + result.detection = detection; + results = [results, result]; + frameDetections = [frameDetections, detection]; + end; %for + + %get number of missed splines + frameNumMissed = length(truthSplines) - length(find(frameDetections==1)); + frameNumFalse = length(find(frameDetections==0)); + end; % for i + + %print out some stats + tp = length(find([results.detection]==1)); + fp = length(find([results.detection]==0)); + % numFrames = length(detections); + + fprintf(1,'\n\n\n'); + disp(sprintf('Detection File %d: %s', i, detectionFile)); + disp(sprintf('Number of frames = %d', numFrames)); + disp(' '); + disp(sprintf('Total detections = %d', detectionTotal)); + disp(sprintf('Total truth = %d', truthTotal)); + disp(' '); + disp(sprintf('Number of correct detections = %d', tp)); + disp(sprintf('Number of false detections = %d', fp)); + disp(' '); + disp(sprintf('Percentage of correct detections = %f', tp/truthTotal)); + disp(sprintf('Percentage of false detections = %f', fp/truthTotal)); + disp(' '); + disp(sprintf('False detections/frame= %f', fp/numFrames)); + + %put in total stats + allResults = [allResults, results]; + allDetectionTotal = allDetectionTotal + detectionTotal; + allTruthTotal = allTruthTotal + truthTotal; + allNumFrames = allNumFrames + numFrames; + allTp = allTp + tp; + allFp = allFp + fp; + + dResults{d} = results; + dDetectionTotal(d) = detectionTotal; + dTruthTotal(d) = truthTotal; + dNumFrames(d) = numFrames; + dTp(d) = tp; + dFp(d) = fp; +end; %for + + +fprintf(1,'\n\n\n'); +disp('Overall results'); +disp(sprintf('Number of frames = %d', allNumFrames)); +disp(' '); +disp(sprintf('Total detections = %d', allDetectionTotal)); +disp(sprintf('Total truth = %d', allTruthTotal)); +disp(' '); +disp(sprintf('Number of correct detections = %d', allTp)); +disp(sprintf('Number of false detections = %d', allFp)); +disp(' '); +disp(sprintf('Percentage of correct detections = %f', allTp/allTruthTotal)); +disp(sprintf('Percentage of false detections = %f', allFp/allTruthTotal)); +disp(' '); +disp(sprintf('False detections/frame= %f', allFp/allNumFrames)); + + +fprintf(1,'\n\n\n-----'); +disp('Summary results'); +for d=1:length(dDetectionTotal) + disp(' '); + disp(sprintf('Detection %s', detectionFile)); + disp(sprintf('Total = %d', dTruthTotal(d))); + disp(sprintf('Total detections = %d', dDetectionTotal(d))); + disp(sprintf('correct detections = %.2f', 100*dTp(d)/dTruthTotal(d))); + disp(sprintf('false detections = %.2f', 100*dFp(d)/dTruthTotal(d))); + disp(sprintf('false detections / frame = %.3f', dFp(d)/dNumFrames(d))); +end; + +% --------------------------------------------------------------------------- +function splines = GetTruthSplines(labels) +% returns splines in the labels as a cell array of splines +splines = {}; +for i=1:length(labels) + if strcmp(labels(i).type, 'spline') + splines{end+1} = labels(i).points; + end; +end; diff --git a/matlab/ccvLabel.m b/matlab/ccvLabel.m new file mode 100644 index 0000000..85a60f0 --- /dev/null +++ b/matlab/ccvLabel.m @@ -0,0 +1,444 @@ +function varargout = ccvLabel(f, varargin) +% CCVLABEL performs different tasks on the label structure, like creating +% new structure, adding frames, labels, ...etc. +% +% INPUTS +% ------ +% f - the input function to perform +% varargin - the rest of the inputs (potentially zero) +% +% OUTPUTS +% ------- +% varargout - the outputs from the selected operation +% +% See also ccvLabeler +% +% AUTHOR - Mohamed Aly +% DATE - May 26, 2009 +% + +%check if we have a valid input function +if isempty(f) || ~exist(f, 'file'), error('Please enter a valid function'); end; + +%call the function +varargout = cell(1, nargout); +[varargout{:}] = feval(f, varargin{:}); +end + +function ld = create() +% NEW creates a new empty structure +% +% INPUTS +% ------ +% +% OUTPUTS +% ------- +% ld - the output empty label data +% + +ld.version = 0; +ld.source = 'image'; +ld.frames = struct('frame', {}, ... + 'labels', struct('points',{}, 'type',{}, 'subtype',{}, 'obj',{})); +ld.objects = struct('id',{}); +end + +function ld = read(fname) +% READ loads label data from a file +% +% INPUTS +% ------ +% fname - the input file name +% +% OUTPUTS +% ------- +% ld - the output empty label data +% + +%load the file +ld = []; +try + load(fname, '-mat'); +catch + return; +end; + +%check version +if ~exist('ld', 'var') || ~ld.version<0 + error('invalid input file'); +end; + +%check objects +if ~isfield(ld,'objects'), ld.objects = []; end; + + +end + +function write(fname, ld) %#ok +% WRITE saves label data to a file +% +% INPUTS +% ------ +% fname - the input file name +% ld - the input label data +% +% OUTPUTS +% ------- +% + +%load the file +save(fname, 'ld', '-mat'); + +end + +function [obj] = createObj(objId) +% CREATEOBJ creates a new object and returns it +% +% INPUTS +% ------ +% objId - the object id of the new object +% +% OUTPUTS +% ------- +% obj - the new obj +% + +obj = struct('id', objId); + +end + +function [ld, objId] = addObj(ld) +% ADDOBJ adds a new object and returns the object id +% +% INPUTS +% ------ +% ld - the input label data +% +% OUTPUTS +% ------- +% ld - the output label data +% objId - the id of the new object added +% + +%get id of new object +objId = max([ld.objects.id]) + 1; +if isempty(objId), objId = 1; end; +%add it +ld.objects = [ld.objects createObj(objId)]; + +end + +function ld = removeObj(ld, objId) +% REMOVEOBJ deletes an object and clears objects of every label with that +% object id +% +% INPUTS +% ------ +% ld - the input label data +% objId - the id of the object to remove +% +% OUTPUTS +% ------- +% ld - the output label data +% + +%get index of object +objInd = find([ld.objects.id] == objId); +%make sure it's valid +if ~isempty(objInd) + %clear it + ld.objects(objInd) = []; + %update all labels with that object id, loop all frames and check + for f=1:length(ld.frames) + %reset labels with that object label + for l=1:length(ld.frames(f).labels) + if ld.frames(f).labels(l).obj == objId, + ld.frames(f).labels(l).obj = []; + end; + end; +% lbls = find([ld.frames(f).labels.obj] == objId); +% for l=lbls, ld.frames(f).labels(l).obj = []; end; + end; +end; %if + +end + +function [objIds] = getObjIds(ld) +% GETOBJIDS returns the object ids present +% +% INPUTS +% ------ +% ld - the input label data +% +% OUTPUTS +% ------- +% objIds - the list of object ids +% + +%get ids of objects +objIds = [ld.objects.id]; + +end + +function nframes = nFrames(ld) +% NFRAMES returns the number of frames +% +% INPUTS +% ------ +% ld - the input label data +% +% OUTPUTS +% ------- +% nframes - the number of frames +% + +%get the frame +nframes = length(ld.frames); +end + +function frame = getFrame(ld, frameIdx) +% GETFRAME returns the required frame +% +% INPUTS +% ------ +% ld - the input label data +% frameIdx - the frame index +% +% OUTPUTS +% ------- +% frame - the returned frame, which is a structure with fields +% .frame - the index or file name of the frame +% .labels - the array of labels in this frame +% + +%get the frame +frame = ld.frames(frameIdx); +end + +function frm = createFrame(frame, labels) +% CREATEFRAME creates a new frame +% +% INPUTS +% ------ +% frame - the frame id or file name +% labels - the frame labels +% +% OUTPUTS +% ------- +% frm - the output new frame +% +if nargin<1, frame = []; end; +if nargin<2, labels = createLabel(); end; + +%create the new frame +frm = struct('frame',frame, 'labels',labels); + +end + +function [ld, frameIdx] = addFrame(ld, frame, labels) +% ADDFRAME adds a frame into the data structure +% +% INPUTS +% ------ +% ld - the input label data +% frame - the frame id or file name +% labels - the frame labels +% +% OUTPUTS +% ------- +% ld - the update ld structure +% frameIdx - the index of the new frame +% +if nargin<2, frame = []; end; +if nargin<3, labels = createLabel(); end; + +%get the frame index +frameIdx = length(ld.frames) + 1; + +%put the new frame +ld.frames(frameIdx) = createFrame(frame, labels); + +end + +function ld = removeFrame(ld, frameIdx) +% REMOVEFRAME removes the frame +% +% INPUTS +% ------ +% ld - the input label data +% frameIdx - the frame index +% +% OUTPUTS +% ------- +% ld - the update ld structure +% + +%remove the frame +ld.frames(frameIdx) = []; +end + +function label = createLabel(points, type, subtype, objId) +% CREATELABEL creates a new label +% +% INPUTS +% ------ +% points - the points for that label +% type - the type of label +% subtype - the subtype of the label +% objId - the objId of the label +% +% OUTPUTS +% ------- +% ld - the output updated label data +% +if nargin<1, points = {}; end; +if nargin<2, type = []; end; +if nargin<3, subtype = []; end; +if nargin<4, objId = []; end; + +%create a new label +label = struct('points',points, 'type',type, ... + 'subtype',subtype, 'obj',objId); + +end + +function nl = nLabels(ld, frameIdx) +% NLABELS gets the number of labels in the required frame +% +% INPUTS +% ------ +% ld - the input label data +% frameIdx - the frame index +% +% OUTPUTS +% ------- +% nl - the number of labels +% + +nl = length(ld.frames(frameIdx).labels); + +end + + +function [ld, lblIdx] = addLabel(ld, frameIdx, points, type, subtype, objId) +% ADDLABEL adds a new label +% +% INPUTS +% ------ +% ld - the input label data +% frameIdx - the frame index +% points - the points for that label or the label structure if given +% type - the type of label +% subtype - the subtype of the label +% objId - the objId of the label +% +% OUTPUTS +% ------- +% ld - the output updated label data +% lblIdx - the new label index +% +if nargin<3, points = []; end; +if nargin<4, type = []; end; +if nargin<5, subtype = []; end; +if nargin<6, objId = []; end; + +%get the new label index +lblIdx = nLabels(ld, frameIdx) + 1; + +%create the new label if not a struct +if isstruct(points), label = points; +else label = createLabel(points, type, subtype, objId); +end; + +%add the label to the required frame +ld.frames(frameIdx).labels(lblIdx) = label; + +end + +function ld = updateLabel(ld, frameIdx, lblIdx, points, type, subtype, objId) +% UPDATELABEL updates an existing label +% +% INPUTS +% ------ +% ld - the input label data +% frameIdx - the frame id +% lblIdx - the index of the label to change +% points - the points for that label (don't change if nan). It can also +% be a structure, in which case it is a label structure, +% so just replace it +% type - the type of label (don't change if nan) +% subtype - the subtype of the label (don't change if nan) +% objId - the objId of the label (don't change if nan) +% +% OUTPUTS +% ------- +% ld - the output updated label data +% + +%check if just to replace it +if nargin>=4 && isstruct(points) + label = points; +%we are passaed in independent components of the labels +else + %get the label + label = ld.frames(frameIdx).labels(lblIdx); + + %update the label + if nargin>=7 && ~any(isnan(objId)), label.obj = objId; end; + if nargin>=6 && ~any(isnan(subtype)), label.subtype = subtype; end; + if nargin>=5 && ~any(isnan(type)), label.type = type ; end; + if nargin>=4 && ~any(any(isnan(points))), label.points = points; end; +end; + +%put it back +ld.frames(frameIdx).labels(lblIdx) = label; +end + +function label = getLabel(ld, frameIdx, lblIdx) +% GETLABEL retuns the required label +% +% INPUTS +% ------ +% ld - the input label data +% frameIdx - the frame index +% lblIdx - the index of the label to return. If empty or absent, then +% return the labels in this frame +% +% OUTPUTS +% ------- +% label - the returned label(s), which is a structure with fields +% .points - the label points +% .type - the label type +% .subtype - the label subtype +% .obj - the label object id +% + +%get the label +if nargin<3 || isempty(lblIdx) + lblIdx = 1:length(ld.frames(frameIdx).labels); +end; +%return +label = ld.frames(frameIdx).labels(lblIdx); + +end + +function ld = removeLabel(ld, frameIdx, lblIdx) +% REMOVELABEL removes a label +% +% INPUTS +% ------ +% ld - the input label data +% frameIdx - the frame id +% lblIdx - the index of the label to remove +% +% OUTPUTS +% ------- +% ld - the output updated label data +% + +%remove the label +ld.frames(frameIdx).labels(lblIdx) = []; + +end + + diff --git a/matlab/ccvReadLaneDetectionResultsFile.m b/matlab/ccvReadLaneDetectionResultsFile.m new file mode 100644 index 0000000..8f09cf4 --- /dev/null +++ b/matlab/ccvReadLaneDetectionResultsFile.m @@ -0,0 +1,61 @@ +function [frames] = ccvReadLaneDetectionResultsFile(filename) +% CCVREADLANEDETECTIONRESULTSFILE reads a results file from the binary +% LaneDetector saved with --save-lanes flag +% +% INPUTS +% ------ +% filename - the input results file +% +% OUTPUTS +% ------- +% frames - the output structure array, one per frame (image) with fields +% .id - the frame id +% .splines - the splines cell array +% .numSplines - the number of splines +% .scores - the scores of the splines +% +% See also +% + +%open file +file = fopen(filename, 'r'); + +%loop on file and read data +frames = []; +while 1 + %read frame + d = fscanf(file, 'frame#%08d has %d splines\n', 2); + %if no frames, then exit + if isempty(d), break; end; + + %get id and number of splines + id = d(1); + numSplines = d(2); + + %now loop for this amount + frame = []; + splines = {}; + scores = []; + for i=1:numSplines + %get header + d = fscanf(file, '\tspline#%d has %d points and score %f\n'); + if isempty(d), continue; end; + scores = [scores, d(3)]; + + %get spline points + d = fscanf(file, '\t\t%f, %f\n'); + if isempty(d), continue, end; + spline = reshape(d, 2, [])'; + + %put spline + splines = [splines, spline]; + end; + + %put frame + frame.id = id; + frame.splines = splines; + frame.numSplines = length(splines); + frame.scores = scores; + frames = [frames, frame]; + +end; diff --git a/src/CameraInfo.conf b/src/CameraInfo.conf new file mode 100644 index 0000000..cf91a1e --- /dev/null +++ b/src/CameraInfo.conf @@ -0,0 +1,21 @@ + +# focal length +focalLengthX 309.4362 +focalLengthY 344.2161 + +# optical center +opticalCenterX 317.9034 +opticalCenterY 256.5352 + +# height of the camera in mm +cameraHeight 2179.8 # 393.7 + 1786.1 + +# pitch of the camera +pitch 14.0 + +# yaw of the camera +yaw 0.0 + +# imag width and height +imageWidth 640 +imageHeight 480 diff --git a/src/CameraInfoOpt.c b/src/CameraInfoOpt.c new file mode 100644 index 0000000..909e8e9 --- /dev/null +++ b/src/CameraInfoOpt.c @@ -0,0 +1,878 @@ +/* + File autogenerated by gengetopt version 2.18 + generated with the following command: + gengetopt -i cameraInfoOpt.ggo -F cameraInfoOpt --func-name=cameraInfoParser --arg-struct-name=CameraInfoParserInfo --conf-parser + + The developers of gengetopt consider the fixed text that goes in all + gengetopt output files to be in the public domain: + we make no copyright claims on it. +*/ + +/* If we use autoconf. */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include "getopt.h" + +#include "CameraInfoOpt.h" + +const char *CameraInfoParserInfo_purpose = ""; + +const char *CameraInfoParserInfo_usage = "Usage: inversePerspectiveMapping [OPTIONS]..."; + +const char *CameraInfoParserInfo_help[] = { + " -h, --help Print help and exit", + " -V, --version Print version and exit", + " --focalLengthX=DOUBLE Focal lenght in horizontal direction in pixels", + " --focalLengthY=DOUBLE Focal lenght in vertical direction in pixels", + " --opticalCenterX=DOUBLE X-coordinate of optical center in pixels", + " --opticalCenterY=DOUBLE Y-coordinate of optical center in pixels", + " --cameraHeight=DOUBLE Height of camera above ground in mm", + " --pitch=DOUBLE pitch of camera in degrees (+ve downwards)", + " --yaw=DOUBLE yaw of camera in degrees (+ve clockwise)", + " --imageWidth=DOUBLE width of image in pixels", + " --imageHeight=DOUBLE height of image in pixels", + 0 +}; + +static +void clear_given (struct CameraInfoParserInfo *args_info); +static +void clear_args (struct CameraInfoParserInfo *args_info); + +static int +cameraInfoParser_internal (int argc, char * const *argv, struct CameraInfoParserInfo *args_info, int override, int initialize, int check_required, const char *additional_error); + +static int +cameraInfoParser_required2 (struct CameraInfoParserInfo *args_info, const char *prog_name, const char *additional_error); +struct line_list +{ + char * string_arg; + struct line_list * next; +}; + +static struct line_list *cmd_line_list = 0; +static struct line_list *cmd_line_list_tmp = 0; + +static void +free_cmd_list(void) +{ + /* free the list of a previous call */ + if (cmd_line_list) + { + while (cmd_line_list) { + cmd_line_list_tmp = cmd_line_list; + cmd_line_list = cmd_line_list->next; + free (cmd_line_list_tmp->string_arg); + free (cmd_line_list_tmp); + } + } +} + + +static char * +gengetopt_strdup (const char *s); + +static +void clear_given (struct CameraInfoParserInfo *args_info) +{ + args_info->help_given = 0 ; + args_info->version_given = 0 ; + args_info->focalLengthX_given = 0 ; + args_info->focalLengthY_given = 0 ; + args_info->opticalCenterX_given = 0 ; + args_info->opticalCenterY_given = 0 ; + args_info->cameraHeight_given = 0 ; + args_info->pitch_given = 0 ; + args_info->yaw_given = 0 ; + args_info->imageWidth_given = 0 ; + args_info->imageHeight_given = 0 ; +} + +static +void clear_args (struct CameraInfoParserInfo *args_info) +{ + args_info->focalLengthX_orig = NULL; + args_info->focalLengthY_orig = NULL; + args_info->opticalCenterX_orig = NULL; + args_info->opticalCenterY_orig = NULL; + args_info->cameraHeight_orig = NULL; + args_info->pitch_orig = NULL; + args_info->yaw_orig = NULL; + args_info->imageWidth_orig = NULL; + args_info->imageHeight_orig = NULL; + +} + +static +void init_args_info(struct CameraInfoParserInfo *args_info) +{ + args_info->help_help = CameraInfoParserInfo_help[0] ; + args_info->version_help = CameraInfoParserInfo_help[1] ; + args_info->focalLengthX_help = CameraInfoParserInfo_help[2] ; + args_info->focalLengthY_help = CameraInfoParserInfo_help[3] ; + args_info->opticalCenterX_help = CameraInfoParserInfo_help[4] ; + args_info->opticalCenterY_help = CameraInfoParserInfo_help[5] ; + args_info->cameraHeight_help = CameraInfoParserInfo_help[6] ; + args_info->pitch_help = CameraInfoParserInfo_help[7] ; + args_info->yaw_help = CameraInfoParserInfo_help[8] ; + args_info->imageWidth_help = CameraInfoParserInfo_help[9] ; + args_info->imageHeight_help = CameraInfoParserInfo_help[10] ; + +} + +void +cameraInfoParser_print_version (void) +{ + printf ("%s %s\n", CAMERAINFOPARSER_PACKAGE, CAMERAINFOPARSER_VERSION); +} + +void +cameraInfoParser_print_help (void) +{ + int i = 0; + cameraInfoParser_print_version (); + + if (strlen(CameraInfoParserInfo_purpose) > 0) + printf("\n%s\n", CameraInfoParserInfo_purpose); + + printf("\n%s\n\n", CameraInfoParserInfo_usage); + while (CameraInfoParserInfo_help[i]) + printf("%s\n", CameraInfoParserInfo_help[i++]); +} + +void +cameraInfoParser_init (struct CameraInfoParserInfo *args_info) +{ + clear_given (args_info); + clear_args (args_info); + init_args_info (args_info); +} + +static void +cameraInfoParser_release (struct CameraInfoParserInfo *args_info) +{ + + if (args_info->focalLengthX_orig) + { + free (args_info->focalLengthX_orig); /* free previous argument */ + args_info->focalLengthX_orig = 0; + } + if (args_info->focalLengthY_orig) + { + free (args_info->focalLengthY_orig); /* free previous argument */ + args_info->focalLengthY_orig = 0; + } + if (args_info->opticalCenterX_orig) + { + free (args_info->opticalCenterX_orig); /* free previous argument */ + args_info->opticalCenterX_orig = 0; + } + if (args_info->opticalCenterY_orig) + { + free (args_info->opticalCenterY_orig); /* free previous argument */ + args_info->opticalCenterY_orig = 0; + } + if (args_info->cameraHeight_orig) + { + free (args_info->cameraHeight_orig); /* free previous argument */ + args_info->cameraHeight_orig = 0; + } + if (args_info->pitch_orig) + { + free (args_info->pitch_orig); /* free previous argument */ + args_info->pitch_orig = 0; + } + if (args_info->yaw_orig) + { + free (args_info->yaw_orig); /* free previous argument */ + args_info->yaw_orig = 0; + } + if (args_info->imageWidth_orig) + { + free (args_info->imageWidth_orig); /* free previous argument */ + args_info->imageWidth_orig = 0; + } + if (args_info->imageHeight_orig) + { + free (args_info->imageHeight_orig); /* free previous argument */ + args_info->imageHeight_orig = 0; + } + + clear_given (args_info); +} + +int +cameraInfoParser_file_save(const char *filename, struct CameraInfoParserInfo *args_info) +{ + FILE *outfile; + int i = 0; + + outfile = fopen(filename, "w"); + + if (!outfile) + { + fprintf (stderr, "%s: cannot open file for writing: %s\n", CAMERAINFOPARSER_PACKAGE, filename); + return EXIT_FAILURE; + } + + if (args_info->help_given) { + fprintf(outfile, "%s\n", "help"); + } + if (args_info->version_given) { + fprintf(outfile, "%s\n", "version"); + } + if (args_info->focalLengthX_given) { + if (args_info->focalLengthX_orig) { + fprintf(outfile, "%s=\"%s\"\n", "focalLengthX", args_info->focalLengthX_orig); + } else { + fprintf(outfile, "%s\n", "focalLengthX"); + } + } + if (args_info->focalLengthY_given) { + if (args_info->focalLengthY_orig) { + fprintf(outfile, "%s=\"%s\"\n", "focalLengthY", args_info->focalLengthY_orig); + } else { + fprintf(outfile, "%s\n", "focalLengthY"); + } + } + if (args_info->opticalCenterX_given) { + if (args_info->opticalCenterX_orig) { + fprintf(outfile, "%s=\"%s\"\n", "opticalCenterX", args_info->opticalCenterX_orig); + } else { + fprintf(outfile, "%s\n", "opticalCenterX"); + } + } + if (args_info->opticalCenterY_given) { + if (args_info->opticalCenterY_orig) { + fprintf(outfile, "%s=\"%s\"\n", "opticalCenterY", args_info->opticalCenterY_orig); + } else { + fprintf(outfile, "%s\n", "opticalCenterY"); + } + } + if (args_info->cameraHeight_given) { + if (args_info->cameraHeight_orig) { + fprintf(outfile, "%s=\"%s\"\n", "cameraHeight", args_info->cameraHeight_orig); + } else { + fprintf(outfile, "%s\n", "cameraHeight"); + } + } + if (args_info->pitch_given) { + if (args_info->pitch_orig) { + fprintf(outfile, "%s=\"%s\"\n", "pitch", args_info->pitch_orig); + } else { + fprintf(outfile, "%s\n", "pitch"); + } + } + if (args_info->yaw_given) { + if (args_info->yaw_orig) { + fprintf(outfile, "%s=\"%s\"\n", "yaw", args_info->yaw_orig); + } else { + fprintf(outfile, "%s\n", "yaw"); + } + } + if (args_info->imageWidth_given) { + if (args_info->imageWidth_orig) { + fprintf(outfile, "%s=\"%s\"\n", "imageWidth", args_info->imageWidth_orig); + } else { + fprintf(outfile, "%s\n", "imageWidth"); + } + } + if (args_info->imageHeight_given) { + if (args_info->imageHeight_orig) { + fprintf(outfile, "%s=\"%s\"\n", "imageHeight", args_info->imageHeight_orig); + } else { + fprintf(outfile, "%s\n", "imageHeight"); + } + } + + fclose (outfile); + + i = EXIT_SUCCESS; + return i; +} + +void +cameraInfoParser_free (struct CameraInfoParserInfo *args_info) +{ + cameraInfoParser_release (args_info); +} + + +/* gengetopt_strdup() */ +/* strdup.c replacement of strdup, which is not standard */ +char * +gengetopt_strdup (const char *s) +{ + char *result = NULL; + if (!s) + return result; + + result = (char*)malloc(strlen(s) + 1); + if (result == (char*)0) + return (char*)0; + strcpy(result, s); + return result; +} + +int +cameraInfoParser (int argc, char * const *argv, struct CameraInfoParserInfo *args_info) +{ + return cameraInfoParser2 (argc, argv, args_info, 0, 1, 1); +} + +int +cameraInfoParser2 (int argc, char * const *argv, struct CameraInfoParserInfo *args_info, int override, int initialize, int check_required) +{ + int result; + + result = cameraInfoParser_internal (argc, argv, args_info, override, initialize, check_required, NULL); + + if (result == EXIT_FAILURE) + { + cameraInfoParser_free (args_info); + exit (EXIT_FAILURE); + } + + return result; +} + +int +cameraInfoParser_required (struct CameraInfoParserInfo *args_info, const char *prog_name) +{ + int result = EXIT_SUCCESS; + + if (cameraInfoParser_required2(args_info, prog_name, NULL) > 0) + result = EXIT_FAILURE; + + if (result == EXIT_FAILURE) + { + cameraInfoParser_free (args_info); + exit (EXIT_FAILURE); + } + + return result; +} + +int +cameraInfoParser_required2 (struct CameraInfoParserInfo *args_info, const char *prog_name, const char *additional_error) +{ + int error = 0; + + /* checks for required options */ + if (! args_info->focalLengthX_given) + { + fprintf (stderr, "%s: '--focalLengthX' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->focalLengthY_given) + { + fprintf (stderr, "%s: '--focalLengthY' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->opticalCenterX_given) + { + fprintf (stderr, "%s: '--opticalCenterX' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->opticalCenterY_given) + { + fprintf (stderr, "%s: '--opticalCenterY' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->cameraHeight_given) + { + fprintf (stderr, "%s: '--cameraHeight' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->pitch_given) + { + fprintf (stderr, "%s: '--pitch' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->yaw_given) + { + fprintf (stderr, "%s: '--yaw' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->imageWidth_given) + { + fprintf (stderr, "%s: '--imageWidth' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->imageHeight_given) + { + fprintf (stderr, "%s: '--imageHeight' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + + /* checks for dependences among options */ + + return error; +} + +int +cameraInfoParser_internal (int argc, char * const *argv, struct CameraInfoParserInfo *args_info, int override, int initialize, int check_required, const char *additional_error) +{ + int c; /* Character of the parsed option. */ + + int error = 0; + struct CameraInfoParserInfo local_args_info; + + if (initialize) + cameraInfoParser_init (args_info); + + cameraInfoParser_init (&local_args_info); + + optarg = 0; + optind = 0; + opterr = 1; + optopt = '?'; + + while (1) + { + int option_index = 0; + char *stop_char; + + static struct option long_options[] = { + { "help", 0, NULL, 'h' }, + { "version", 0, NULL, 'V' }, + { "focalLengthX", 1, NULL, 0 }, + { "focalLengthY", 1, NULL, 0 }, + { "opticalCenterX", 1, NULL, 0 }, + { "opticalCenterY", 1, NULL, 0 }, + { "cameraHeight", 1, NULL, 0 }, + { "pitch", 1, NULL, 0 }, + { "yaw", 1, NULL, 0 }, + { "imageWidth", 1, NULL, 0 }, + { "imageHeight", 1, NULL, 0 }, + { NULL, 0, NULL, 0 } + }; + + stop_char = 0; + c = getopt_long (argc, argv, "hV", long_options, &option_index); + + if (c == -1) break; /* Exit from `while (1)' loop. */ + + switch (c) + { + case 'h': /* Print help and exit. */ + cameraInfoParser_print_help (); + cameraInfoParser_free (&local_args_info); + exit (EXIT_SUCCESS); + + case 'V': /* Print version and exit. */ + cameraInfoParser_print_version (); + cameraInfoParser_free (&local_args_info); + exit (EXIT_SUCCESS); + + + case 0: /* Long option with no short option */ + /* Focal lenght in horizontal direction in pixels. */ + if (strcmp (long_options[option_index].name, "focalLengthX") == 0) + { + if (local_args_info.focalLengthX_given) + { + fprintf (stderr, "%s: `--focalLengthX' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->focalLengthX_given && ! override) + continue; + local_args_info.focalLengthX_given = 1; + args_info->focalLengthX_given = 1; + args_info->focalLengthX_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->focalLengthX_orig) + free (args_info->focalLengthX_orig); /* free previous string */ + args_info->focalLengthX_orig = gengetopt_strdup (optarg); + } + /* Focal lenght in vertical direction in pixels. */ + else if (strcmp (long_options[option_index].name, "focalLengthY") == 0) + { + if (local_args_info.focalLengthY_given) + { + fprintf (stderr, "%s: `--focalLengthY' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->focalLengthY_given && ! override) + continue; + local_args_info.focalLengthY_given = 1; + args_info->focalLengthY_given = 1; + args_info->focalLengthY_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->focalLengthY_orig) + free (args_info->focalLengthY_orig); /* free previous string */ + args_info->focalLengthY_orig = gengetopt_strdup (optarg); + } + /* X-coordinate of optical center in pixels. */ + else if (strcmp (long_options[option_index].name, "opticalCenterX") == 0) + { + if (local_args_info.opticalCenterX_given) + { + fprintf (stderr, "%s: `--opticalCenterX' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->opticalCenterX_given && ! override) + continue; + local_args_info.opticalCenterX_given = 1; + args_info->opticalCenterX_given = 1; + args_info->opticalCenterX_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->opticalCenterX_orig) + free (args_info->opticalCenterX_orig); /* free previous string */ + args_info->opticalCenterX_orig = gengetopt_strdup (optarg); + } + /* Y-coordinate of optical center in pixels. */ + else if (strcmp (long_options[option_index].name, "opticalCenterY") == 0) + { + if (local_args_info.opticalCenterY_given) + { + fprintf (stderr, "%s: `--opticalCenterY' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->opticalCenterY_given && ! override) + continue; + local_args_info.opticalCenterY_given = 1; + args_info->opticalCenterY_given = 1; + args_info->opticalCenterY_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->opticalCenterY_orig) + free (args_info->opticalCenterY_orig); /* free previous string */ + args_info->opticalCenterY_orig = gengetopt_strdup (optarg); + } + /* Height of camera above ground in mm. */ + else if (strcmp (long_options[option_index].name, "cameraHeight") == 0) + { + if (local_args_info.cameraHeight_given) + { + fprintf (stderr, "%s: `--cameraHeight' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->cameraHeight_given && ! override) + continue; + local_args_info.cameraHeight_given = 1; + args_info->cameraHeight_given = 1; + args_info->cameraHeight_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->cameraHeight_orig) + free (args_info->cameraHeight_orig); /* free previous string */ + args_info->cameraHeight_orig = gengetopt_strdup (optarg); + } + /* pitch of camera in degrees (+ve downwards). */ + else if (strcmp (long_options[option_index].name, "pitch") == 0) + { + if (local_args_info.pitch_given) + { + fprintf (stderr, "%s: `--pitch' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->pitch_given && ! override) + continue; + local_args_info.pitch_given = 1; + args_info->pitch_given = 1; + args_info->pitch_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->pitch_orig) + free (args_info->pitch_orig); /* free previous string */ + args_info->pitch_orig = gengetopt_strdup (optarg); + } + /* yaw of camera in degrees (+ve clockwise). */ + else if (strcmp (long_options[option_index].name, "yaw") == 0) + { + if (local_args_info.yaw_given) + { + fprintf (stderr, "%s: `--yaw' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->yaw_given && ! override) + continue; + local_args_info.yaw_given = 1; + args_info->yaw_given = 1; + args_info->yaw_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->yaw_orig) + free (args_info->yaw_orig); /* free previous string */ + args_info->yaw_orig = gengetopt_strdup (optarg); + } + /* width of image in pixels. */ + else if (strcmp (long_options[option_index].name, "imageWidth") == 0) + { + if (local_args_info.imageWidth_given) + { + fprintf (stderr, "%s: `--imageWidth' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->imageWidth_given && ! override) + continue; + local_args_info.imageWidth_given = 1; + args_info->imageWidth_given = 1; + args_info->imageWidth_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->imageWidth_orig) + free (args_info->imageWidth_orig); /* free previous string */ + args_info->imageWidth_orig = gengetopt_strdup (optarg); + } + /* height of image in pixels. */ + else if (strcmp (long_options[option_index].name, "imageHeight") == 0) + { + if (local_args_info.imageHeight_given) + { + fprintf (stderr, "%s: `--imageHeight' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->imageHeight_given && ! override) + continue; + local_args_info.imageHeight_given = 1; + args_info->imageHeight_given = 1; + args_info->imageHeight_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->imageHeight_orig) + free (args_info->imageHeight_orig); /* free previous string */ + args_info->imageHeight_orig = gengetopt_strdup (optarg); + } + + break; + case '?': /* Invalid option. */ + /* `getopt_long' already printed an error message. */ + goto failure; + + default: /* bug: option not considered. */ + fprintf (stderr, "%s: option unknown: %c%s\n", CAMERAINFOPARSER_PACKAGE, c, (additional_error ? additional_error : "")); + abort (); + } /* switch */ + } /* while */ + + + + if (check_required) + { + error += cameraInfoParser_required2 (args_info, argv[0], additional_error); + } + + cameraInfoParser_release (&local_args_info); + + if ( error ) + return (EXIT_FAILURE); + + return 0; + +failure: + + cameraInfoParser_release (&local_args_info); + return (EXIT_FAILURE); +} + +#ifndef CONFIG_FILE_LINE_SIZE +#define CONFIG_FILE_LINE_SIZE 2048 +#endif +#define ADDITIONAL_ERROR " in configuration file " + +#define CONFIG_FILE_LINE_BUFFER_SIZE (CONFIG_FILE_LINE_SIZE+3) +/* 3 is for "--" and "=" */ + +char my_argv[CONFIG_FILE_LINE_BUFFER_SIZE+1]; + +int +cameraInfoParser_configfile (char * const filename, struct CameraInfoParserInfo *args_info, int override, int initialize, int check_required) +{ + FILE* file; + char linebuf[CONFIG_FILE_LINE_SIZE]; + int line_num = 0; + int i, result, equal; + char *fopt, *farg; + char *str_index; + size_t len, next_token; + char delimiter; + int my_argc = 0; + char **my_argv_arg; + char *additional_error; + + /* store the program name */ + cmd_line_list_tmp = (struct line_list *) malloc (sizeof (struct line_list)); + cmd_line_list_tmp->next = cmd_line_list; + cmd_line_list = cmd_line_list_tmp; + cmd_line_list->string_arg = gengetopt_strdup (CAMERAINFOPARSER_PACKAGE); + + if ((file = fopen(filename, "r")) == NULL) + { + fprintf (stderr, "%s: Error opening configuration file '%s'\n", + CAMERAINFOPARSER_PACKAGE, filename); + result = EXIT_FAILURE; + goto conf_failure; + } + + while ((fgets(linebuf, CONFIG_FILE_LINE_SIZE, file)) != NULL) + { + ++line_num; + my_argv[0] = '\0'; + len = strlen(linebuf); + if (len > (CONFIG_FILE_LINE_BUFFER_SIZE-1)) + { + fprintf (stderr, "%s:%s:%d: Line too long in configuration file\n", + CAMERAINFOPARSER_PACKAGE, filename, line_num); + result = EXIT_FAILURE; + goto conf_failure; + } + + /* find first non-whitespace character in the line */ + next_token = strspn ( linebuf, " \t\r\n"); + str_index = linebuf + next_token; + + if ( str_index[0] == '\0' || str_index[0] == '#') + continue; /* empty line or comment line is skipped */ + + fopt = str_index; + + /* truncate fopt at the end of the first non-valid character */ + next_token = strcspn (fopt, " \t\r\n="); + + if (fopt[next_token] == '\0') /* the line is over */ + { + farg = NULL; + equal = 0; + goto noarg; + } + + /* remember if equal sign is present */ + equal = (fopt[next_token] == '='); + fopt[next_token++] = '\0'; + + /* advance pointers to the next token after the end of fopt */ + next_token += strspn (fopt + next_token, " \t\r\n"); + /* check for the presence of equal sign, and if so, skip it */ + if ( !equal ) + if ((equal = (fopt[next_token] == '='))) + { + next_token++; + next_token += strspn (fopt + next_token, " \t\r\n"); + } + str_index += next_token; + + /* find argument */ + farg = str_index; + if ( farg[0] == '\"' || farg[0] == '\'' ) + { /* quoted argument */ + str_index = strchr (++farg, str_index[0] ); /* skip opening quote */ + if (! str_index) + { + fprintf + (stderr, + "%s:%s:%d: unterminated string in configuration file\n", + CAMERAINFOPARSER_PACKAGE, filename, line_num); + result = EXIT_FAILURE; + goto conf_failure; + } + } + else + { /* read up the remaining part up to a delimiter */ + next_token = strcspn (farg, " \t\r\n#\'\""); + str_index += next_token; + } + + /* truncate farg at the delimiter and store it for further check */ + delimiter = *str_index, *str_index++ = '\0'; + + /* everything but comment is illegal at the end of line */ + if (delimiter != '\0' && delimiter != '#') + { + str_index += strspn(str_index, " \t\r\n"); + if (*str_index != '\0' && *str_index != '#') + { + fprintf + (stderr, + "%s:%s:%d: malformed string in configuration file\n", + CAMERAINFOPARSER_PACKAGE, filename, line_num); + result = EXIT_FAILURE; + goto conf_failure; + } + } + + noarg: + ++my_argc; + len = strlen(fopt); + + strcat (my_argv, len > 1 ? "--" : "-"); + strcat (my_argv, fopt); + if (len > 1 && ((farg &&*farg) || equal)) + strcat (my_argv, "="); + if (farg && *farg) + strcat (my_argv, farg); + + cmd_line_list_tmp = (struct line_list *) malloc (sizeof (struct line_list)); + cmd_line_list_tmp->next = cmd_line_list; + cmd_line_list = cmd_line_list_tmp; + cmd_line_list->string_arg = gengetopt_strdup(my_argv); + } /* while */ + + ++my_argc; /* for program name */ + my_argv_arg = (char **) malloc((my_argc+1) * sizeof(char *)); + cmd_line_list_tmp = cmd_line_list; + for (i = my_argc - 1; i >= 0; --i) { + my_argv_arg[i] = cmd_line_list_tmp->string_arg; + cmd_line_list_tmp = cmd_line_list_tmp->next; + } + my_argv_arg[my_argc] = 0; + + additional_error = (char *)malloc(strlen(filename) + strlen(ADDITIONAL_ERROR) + 1); + strcpy (additional_error, ADDITIONAL_ERROR); + strcat (additional_error, filename); + result = + cameraInfoParser_internal (my_argc, my_argv_arg, args_info, override, initialize, check_required, additional_error); + + free (additional_error); + free (my_argv_arg); + +conf_failure: + if (file) + fclose(file); + + free_cmd_list(); + if (result == EXIT_FAILURE) + { + cameraInfoParser_free (args_info); + exit (EXIT_FAILURE); + } + + return result; +} diff --git a/src/CameraInfoOpt.ggo b/src/CameraInfoOpt.ggo new file mode 100644 index 0000000..2327d7c --- /dev/null +++ b/src/CameraInfoOpt.ggo @@ -0,0 +1,27 @@ +#Parameters for camera info +#command line: gengetopt -i cameraInfoOpt.ggo -F cameraInfoOpt \ +# --func-name=cameraInfoParser --arg-struct-name=CameraInfoParserInfo \ +# --conf-parser + +package "inversePerspectiveMapping" +version "0.1" + +option "focalLengthX" - + "Focal lenght in horizontal direction in pixels" double required +option "focalLengthY" - + "Focal lenght in vertical direction in pixels" double required +option "opticalCenterX" - + "X-coordinate of optical center in pixels" double required +option "opticalCenterY" - + "Y-coordinate of optical center in pixels" double required +option "cameraHeight" - + "Height of camera above ground in mm" double required +option "pitch" - + "pitch of camera in degrees (+ve downwards)" double required +option "yaw" - + "yaw of camera in degrees (+ve clockwise)" double required +option "imageWidth" - + "width of image in pixels" double required +option "imageHeight" - + "height of image in pixels" double required + diff --git a/src/CameraInfoOpt.h b/src/CameraInfoOpt.h new file mode 100644 index 0000000..54ddc8e --- /dev/null +++ b/src/CameraInfoOpt.h @@ -0,0 +1,100 @@ +/* cameraInfoOpt.h */ + +/* File autogenerated by gengetopt version 2.18 */ + +#ifndef CAMERAINFOOPT_H +#define CAMERAINFOOPT_H + +/* If we use autoconf. */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#ifndef CAMERAINFOPARSER_PACKAGE +#define CAMERAINFOPARSER_PACKAGE "inversePerspectiveMapping" +#endif + +#ifndef CAMERAINFOPARSER_VERSION +#define CAMERAINFOPARSER_VERSION "0.1" +#endif + +struct CameraInfoParserInfo +{ + const char *help_help; /* Print help and exit help description. */ + const char *version_help; /* Print version and exit help description. */ + double focalLengthX_arg; /* Focal lenght in horizontal direction in pixels. */ + char * focalLengthX_orig; /* Focal lenght in horizontal direction in pixels original value given at command line. */ + const char *focalLengthX_help; /* Focal lenght in horizontal direction in pixels help description. */ + double focalLengthY_arg; /* Focal lenght in vertical direction in pixels. */ + char * focalLengthY_orig; /* Focal lenght in vertical direction in pixels original value given at command line. */ + const char *focalLengthY_help; /* Focal lenght in vertical direction in pixels help description. */ + double opticalCenterX_arg; /* X-coordinate of optical center in pixels. */ + char * opticalCenterX_orig; /* X-coordinate of optical center in pixels original value given at command line. */ + const char *opticalCenterX_help; /* X-coordinate of optical center in pixels help description. */ + double opticalCenterY_arg; /* Y-coordinate of optical center in pixels. */ + char * opticalCenterY_orig; /* Y-coordinate of optical center in pixels original value given at command line. */ + const char *opticalCenterY_help; /* Y-coordinate of optical center in pixels help description. */ + double cameraHeight_arg; /* Height of camera above ground in mm. */ + char * cameraHeight_orig; /* Height of camera above ground in mm original value given at command line. */ + const char *cameraHeight_help; /* Height of camera above ground in mm help description. */ + double pitch_arg; /* pitch of camera in degrees (+ve downwards). */ + char * pitch_orig; /* pitch of camera in degrees (+ve downwards) original value given at command line. */ + const char *pitch_help; /* pitch of camera in degrees (+ve downwards) help description. */ + double yaw_arg; /* yaw of camera in degrees (+ve clockwise). */ + char * yaw_orig; /* yaw of camera in degrees (+ve clockwise) original value given at command line. */ + const char *yaw_help; /* yaw of camera in degrees (+ve clockwise) help description. */ + double imageWidth_arg; /* width of image in pixels. */ + char * imageWidth_orig; /* width of image in pixels original value given at command line. */ + const char *imageWidth_help; /* width of image in pixels help description. */ + double imageHeight_arg; /* height of image in pixels. */ + char * imageHeight_orig; /* height of image in pixels original value given at command line. */ + const char *imageHeight_help; /* height of image in pixels help description. */ + + int help_given ; /* Whether help was given. */ + int version_given ; /* Whether version was given. */ + int focalLengthX_given ; /* Whether focalLengthX was given. */ + int focalLengthY_given ; /* Whether focalLengthY was given. */ + int opticalCenterX_given ; /* Whether opticalCenterX was given. */ + int opticalCenterY_given ; /* Whether opticalCenterY was given. */ + int cameraHeight_given ; /* Whether cameraHeight was given. */ + int pitch_given ; /* Whether pitch was given. */ + int yaw_given ; /* Whether yaw was given. */ + int imageWidth_given ; /* Whether imageWidth was given. */ + int imageHeight_given ; /* Whether imageHeight was given. */ + +} ; + +extern const char *CameraInfoParserInfo_purpose; +extern const char *CameraInfoParserInfo_usage; +extern const char *CameraInfoParserInfo_help[]; + +int cameraInfoParser (int argc, char * const *argv, + struct CameraInfoParserInfo *args_info); +int cameraInfoParser2 (int argc, char * const *argv, + struct CameraInfoParserInfo *args_info, + int override, int initialize, int check_required); +int cameraInfoParser_file_save(const char *filename, + struct CameraInfoParserInfo *args_info); + +void cameraInfoParser_print_help(void); +void cameraInfoParser_print_version(void); + +void cameraInfoParser_init (struct CameraInfoParserInfo *args_info); +void cameraInfoParser_free (struct CameraInfoParserInfo *args_info); + +int cameraInfoParser_configfile (char * const filename, + struct CameraInfoParserInfo *args_info, + int override, int initialize, int check_required); + +int cameraInfoParser_required (struct CameraInfoParserInfo *args_info, + const char *prog_name); + + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* CAMERAINFOOPT_H */ diff --git a/src/CameraInfoOpt.o b/src/CameraInfoOpt.o new file mode 100644 index 0000000000000000000000000000000000000000..5273b5b556b3ee499270c2adcd3f5df311d7eee5 GIT binary patch literal 25888 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 literal 0 HcmV?d00001 diff --git a/src/InversePerspectiveMapping.cc b/src/InversePerspectiveMapping.cc new file mode 100755 index 0000000..6748160 --- /dev/null +++ b/src/InversePerspectiveMapping.cc @@ -0,0 +1,556 @@ +/*** + * \file InversePerspectiveMapping.cc + * \author Mohamed Aly + * \date 11/29/2006 + */ + +#include "InversePerspectiveMapping.hh" + +#include "CameraInfoOpt.h" + +#include +#include +#include +#include + +using namespace std; +#include +#include + +namespace LaneDetector +{ + +#define VP_PORTION 0.05 + +/* + We are assuming the world coordinate frame center is at the camera, + the ground plane is at height -h, the X-axis is going right, + the Y-axis is going forward, the Z-axis is going up. The + camera is looking forward with optical axis in direction of + Y-axis, with possible pitch angle (above or below the Y-axis) + and yaw angle (left or right). + The camera coordinates have the same center as the world, but the Xc-axis goes right, + the Yc-axis goes down, and the Zc-axis (optical cxis) goes forward. The + uv-plane of the image is such that u is horizontal going right, v is + vertical going down. + The image coordinates uv are such that the pixels are at half coordinates + i.e. first pixel is (.5,.5) ...etc where the top-left point is (0,0) i.e. + the tip of the first pixel is (0,0) +*/ + +/** + * This function returns the Inverse Perspective Mapping + * of the input image, assuming a flat ground plane, and + * given the camera parameters. + * + * \param inImage the input image + * \param outImage the output image in IPM + * \param ipmInfo the returned IPM info for the transformation + * \param focalLength focal length (in x and y direction) + * \param cameraInfo the camera parameters + * \param outPoints indices of points outside the image + */ +void mcvGetIPM(const CvMat* inImage, CvMat* outImage, + IPMInfo *ipmInfo, const CameraInfo *cameraInfo, + list *outPoints) +{ + //check input images types + //CvMat inMat, outMat; + //cvGetMat(inImage, &inMat); + //cvGetMat(outImage, &outMat); + //cout << CV_MAT_TYPE(inImage->type) << " " << CV_MAT_TYPE(FLOAT_MAT_TYPE) << " " << CV_MAT_TYPE(INT_MAT_TYPE)<<"\n"; + if (!(CV_ARE_TYPES_EQ(inImage, outImage) && + (CV_MAT_TYPE(inImage->type)==CV_MAT_TYPE(FLOAT_MAT_TYPE) || + (CV_MAT_TYPE(inImage->type)==CV_MAT_TYPE(INT_MAT_TYPE))))) + { + cerr << "Unsupported image types in mcvGetIPM"; + exit(1); + } + + //get size of input image + FLOAT u, v; + v = inImage->height; + u = inImage->width; + + //get the vanishing point + FLOAT_POINT2D vp; + vp = mcvGetVanishingPoint(cameraInfo); + vp.y = MAX(0, vp.y); + //vp.y = 30; + + //get extent of the image in the xfyf plane + FLOAT_MAT_ELEM_TYPE eps = ipmInfo->vpPortion * v;//VP_PORTION*v; + ipmInfo->ipmLeft = MAX(0, ipmInfo->ipmLeft); + ipmInfo->ipmRight = MIN(u-1, ipmInfo->ipmRight); + ipmInfo->ipmTop = MAX(vp.y+eps, ipmInfo->ipmTop); + ipmInfo->ipmBottom = MIN(v-1, ipmInfo->ipmBottom); + FLOAT_MAT_ELEM_TYPE uvLimitsp[] = {vp.x, + ipmInfo->ipmRight, ipmInfo->ipmLeft, vp.x, + ipmInfo->ipmTop, ipmInfo->ipmTop, ipmInfo->ipmTop, ipmInfo->ipmBottom}; + //{vp.x, u, 0, vp.x, + //vp.y+eps, vp.y+eps, vp.y+eps, v}; + CvMat uvLimits = cvMat(2, 4, FLOAT_MAT_TYPE, uvLimitsp); + + //get these points on the ground plane + CvMat * xyLimitsp = cvCreateMat(2, 4, FLOAT_MAT_TYPE); + CvMat xyLimits = *xyLimitsp; + mcvTransformImage2Ground(&uvLimits, &xyLimits,cameraInfo); + //SHOW_MAT(xyLimitsp, "xyLImits"); + + //get extent on the ground plane + CvMat row1, row2; + cvGetRow(&xyLimits, &row1, 0); + cvGetRow(&xyLimits, &row2, 1); + double xfMax, xfMin, yfMax, yfMin; + cvMinMaxLoc(&row1, (double*)&xfMin, (double*)&xfMax, 0, 0, 0); + cvMinMaxLoc(&row2, (double*)&yfMin, (double*)&yfMax, 0, 0, 0); + + INT outRow = outImage->height; + INT outCol = outImage->width; + + FLOAT_MAT_ELEM_TYPE stepRow = (yfMax-yfMin)/outRow; + FLOAT_MAT_ELEM_TYPE stepCol = (xfMax-xfMin)/outCol; + + //construct the grid to sample + CvMat *xyGrid = cvCreateMat(2, outRow*outCol, FLOAT_MAT_TYPE); + INT i, j; + FLOAT_MAT_ELEM_TYPE x, y; + //fill it with x-y values on the ground plane in world frame + for (i=0, y=yfMax-.5*stepRow; iu-1 || vi<0 || vi>v-1) \*/ \ + if (uiipmLeft || ui>ipmInfo->ipmRight || \ + viipmTop || vi>ipmInfo->ipmBottom) \ + { \ + CV_MAT_ELEM(*outImage, type, i, j) = (type)mean; \ + } \ + /*not out of bounds, then get nearest neighbor*/ \ + else \ + { \ + /*Bilinear interpolation*/ \ + if (ipmInfo->ipmInterpolation == 0) \ + { \ + int x1 = int(ui), x2 = int(ui+1); \ + int y1 = int(vi), y2 = int(vi+1); \ + float x = ui - x1, y = vi - y1; \ + float val = CV_MAT_ELEM(*inImage, type, y1, x1) * (1-x) * (1-y) + \ + CV_MAT_ELEM(*inImage, type, y1, x2) * x * (1-y) + \ + CV_MAT_ELEM(*inImage, type, y2, x1) * (1-x) * y + \ + CV_MAT_ELEM(*inImage, type, y2, x2) * x * y; \ + CV_MAT_ELEM(*outImage, type, i, j) = (type)val; \ + } \ + /*nearest-neighbor interpolation*/ \ + else \ + CV_MAT_ELEM(*outImage, type, i, j) = \ + CV_MAT_ELEM(*inImage, type, int(vi+.5), int(ui+.5)); \ + } \ + if (outPoints && \ + (uiipmLeft+10 || ui>ipmInfo->ipmRight-10 || \ + viipmTop || vi>ipmInfo->ipmBottom-2) )\ + outPoints->push_back(cvPoint(j, i)); \ + } + if (CV_MAT_TYPE(inImage->type)==FLOAT_MAT_TYPE) + { + MCV_GET_IPM(FLOAT_MAT_ELEM_TYPE) + } + else + { + MCV_GET_IPM(INT_MAT_ELEM_TYPE) + } + //return the ipm info + ipmInfo->xLimits[0] = CV_MAT_ELEM(*xyGrid, FLOAT_MAT_ELEM_TYPE, 0, 0); + ipmInfo->xLimits[1] = + CV_MAT_ELEM(*xyGrid, FLOAT_MAT_ELEM_TYPE, 0, (outRow-1)*outCol+outCol-1); + ipmInfo->yLimits[1] = CV_MAT_ELEM(*xyGrid, FLOAT_MAT_ELEM_TYPE, 1, 0); + ipmInfo->yLimits[0] = + CV_MAT_ELEM(*xyGrid, FLOAT_MAT_ELEM_TYPE, 1, (outRow-1)*outCol+outCol-1); + ipmInfo->xScale = 1/stepCol; + ipmInfo->yScale = 1/stepRow; + ipmInfo->width = outCol; + ipmInfo->height = outRow; + + //clean + cvReleaseMat(&xyLimitsp); + cvReleaseMat(&xyGrid); + cvReleaseMat(&uvGrid); +} + + +/** + * Transforms points from the image frame (uv-coordinates) + * into the real world frame on the ground plane (z=-height) + * + * \param inPoints input points in the image frame + * \param outPoints output points in the world frame on the ground + * (z=-height) + * \param cemaraInfo the input camera parameters + * + */ +void mcvTransformImage2Ground(const CvMat *inPoints, + CvMat *outPoints, const CameraInfo *cameraInfo) +{ + + //add two rows to the input points + CvMat *inPoints4 = cvCreateMat(inPoints->rows+2, inPoints->cols, + cvGetElemType(inPoints)); + + //copy inPoints to first two rows + CvMat inPoints2, inPoints3, inPointsr4, inPointsr3; + cvGetRows(inPoints4, &inPoints2, 0, 2); + cvGetRows(inPoints4, &inPoints3, 0, 3); + cvGetRow(inPoints4, &inPointsr3, 2); + cvGetRow(inPoints4, &inPointsr4, 3); + cvSet(&inPointsr3, cvRealScalar(1)); + cvCopy(inPoints, &inPoints2); + //create the transformation matrix + float c1 = cos(cameraInfo->pitch); + float s1 = sin(cameraInfo->pitch); + float c2 = cos(cameraInfo->yaw); + float s2 = sin(cameraInfo->yaw); + float matp[] = { + -cameraInfo->cameraHeight*c2/cameraInfo->focalLength.x, + cameraInfo->cameraHeight*s1*s2/cameraInfo->focalLength.y, + (cameraInfo->cameraHeight*c2*cameraInfo->opticalCenter.x/ + cameraInfo->focalLength.x)- + (cameraInfo->cameraHeight *s1*s2* cameraInfo->opticalCenter.y/ + cameraInfo->focalLength.y) - cameraInfo->cameraHeight *c1*s2, + + cameraInfo->cameraHeight *s2 /cameraInfo->focalLength.x, + cameraInfo->cameraHeight *s1*c2 /cameraInfo->focalLength.y, + (-cameraInfo->cameraHeight *s2* cameraInfo->opticalCenter.x + /cameraInfo->focalLength.x)-(cameraInfo->cameraHeight *s1*c2* + cameraInfo->opticalCenter.y /cameraInfo->focalLength.y) - + cameraInfo->cameraHeight *c1*c2, + + 0, + cameraInfo->cameraHeight *c1 /cameraInfo->focalLength.y, + (-cameraInfo->cameraHeight *c1* cameraInfo->opticalCenter.y / + cameraInfo->focalLength.y) + cameraInfo->cameraHeight *s1, + + 0, + -c1 /cameraInfo->focalLength.y, + (c1* cameraInfo->opticalCenter.y /cameraInfo->focalLength.y) - s1, + }; + CvMat mat = cvMat(4, 3, CV_32FC1, matp); + //multiply + cvMatMul(&mat, &inPoints3, inPoints4); + //divide by last row of inPoints4 + for (int i=0; icols; i++) + { + float div = CV_MAT_ELEM(inPointsr4, float, 0, i); + CV_MAT_ELEM(*inPoints4, float, 0, i) = + CV_MAT_ELEM(*inPoints4, float, 0, i) / div ; + CV_MAT_ELEM(*inPoints4, float, 1, i) = + CV_MAT_ELEM(*inPoints4, float, 1, i) / div; + } + //put back the result into outPoints + cvCopy(&inPoints2, outPoints); + //clear + cvReleaseMat(&inPoints4); +} + + +/** + * Transforms points from the ground plane (z=-h) in the world frame + * into points on the image in image frame (uv-coordinates) + * + * \param inPoints 2xN array of input points on the ground in world coordinates + * \param outPoints 2xN output points in on the image in image coordinates + * \param cameraInfo the camera parameters + * + */ +void mcvTransformGround2Image(const CvMat *inPoints, + CvMat *outPoints, const CameraInfo *cameraInfo) +{ + //add two rows to the input points + CvMat *inPoints3 = cvCreateMat(inPoints->rows+1, inPoints->cols, + cvGetElemType(inPoints)); + + //copy inPoints to first two rows + CvMat inPoints2, inPointsr3; + cvGetRows(inPoints3, &inPoints2, 0, 2); + cvGetRow(inPoints3, &inPointsr3, 2); + cvSet(&inPointsr3, cvRealScalar(-cameraInfo->cameraHeight)); + cvCopy(inPoints, &inPoints2); + //create the transformation matrix + float c1 = cos(cameraInfo->pitch); + float s1 = sin(cameraInfo->pitch); + float c2 = cos(cameraInfo->yaw); + float s2 = sin(cameraInfo->yaw); + float matp[] = { + cameraInfo->focalLength.x * c2 + c1*s2* cameraInfo->opticalCenter.x, + -cameraInfo->focalLength.x * s2 + c1*c2* cameraInfo->opticalCenter.x, + - s1 * cameraInfo->opticalCenter.x, + + s2 * (-cameraInfo->focalLength.y * s1 + c1* cameraInfo->opticalCenter.y), + c2 * (-cameraInfo->focalLength.y * s1 + c1* cameraInfo->opticalCenter.y), + -cameraInfo->focalLength.y * c1 - s1* cameraInfo->opticalCenter.y, + + c1*s2, + c1*c2, + -s1 + }; + CvMat mat = cvMat(3, 3, CV_32FC1, matp); + //multiply + cvMatMul(&mat, inPoints3, inPoints3); + //divide by last row of inPoints4 + for (int i=0; icols; i++) + { + float div = CV_MAT_ELEM(inPointsr3, float, 0, i); + CV_MAT_ELEM(*inPoints3, float, 0, i) = + CV_MAT_ELEM(*inPoints3, float, 0, i) / div ; + CV_MAT_ELEM(*inPoints3, float, 1, i) = + CV_MAT_ELEM(*inPoints3, float, 1, i) / div; + } + //put back the result into outPoints + cvCopy(&inPoints2, outPoints); + //clear + cvReleaseMat(&inPoints3); +} + + +/** + * Computes the vanishing point in the image plane uv. It is + * the point of intersection of the image plane with the line + * in the XY-plane in the world coordinates that makes an + * angle yaw clockwise (form Y-axis) with Y-axis + * + * \param cameraInfo the input camera parameter + * + * \return the computed vanishing point in image frame + * + */ +FLOAT_POINT2D mcvGetVanishingPoint(const CameraInfo *cameraInfo) +{ + //get the vp in world coordinates + FLOAT_MAT_ELEM_TYPE vpp[] = {sin(cameraInfo->yaw)/cos(cameraInfo->pitch), + cos(cameraInfo->yaw)/cos(cameraInfo->pitch), 0}; + CvMat vp = cvMat(3, 1, FLOAT_MAT_TYPE, vpp); + + //transform from world to camera coordinates + // + //rotation matrix for yaw + FLOAT_MAT_ELEM_TYPE tyawp[] = {cos(cameraInfo->yaw), -sin(cameraInfo->yaw), 0, + sin(cameraInfo->yaw), cos(cameraInfo->yaw), 0, + 0, 0, 1}; + CvMat tyaw = cvMat(3, 3, FLOAT_MAT_TYPE, tyawp); + //rotation matrix for pitch + FLOAT_MAT_ELEM_TYPE tpitchp[] = {1, 0, 0, + 0, -sin(cameraInfo->pitch), -cos(cameraInfo->pitch), + 0, cos(cameraInfo->pitch), -sin(cameraInfo->pitch)}; + CvMat transform = cvMat(3, 3, FLOAT_MAT_TYPE, tpitchp); + //combined transform + cvMatMul(&transform, &tyaw, &transform); + + // + //transformation from (xc, yc) in camra coordinates + // to (u,v) in image frame + // + //matrix to shift optical center and focal length + FLOAT_MAT_ELEM_TYPE t1p[] = { + cameraInfo->focalLength.x, 0, + cameraInfo->opticalCenter.x, + 0, cameraInfo->focalLength.y, + cameraInfo->opticalCenter.y, + 0, 0, 1}; + CvMat t1 = cvMat(3, 3, FLOAT_MAT_TYPE, t1p); + //combine transform + cvMatMul(&t1, &transform, &transform); + //transform + cvMatMul(&transform, &vp, &vp); + + // + //clean and return + // + FLOAT_POINT2D ret; + ret.x = cvGetReal1D(&vp, 0); + ret.y = cvGetReal1D(&vp, 1); + return ret; +} + + +/** + * Converts a point from IPM pixel coordinates into world coordinates + * + * \param point in/out point + * \param ipmInfo the ipm info from mcvGetIPM + * + */ +void mcvPointImIPM2World(FLOAT_POINT2D *point, const IPMInfo *ipmInfo) +{ + //x-direction + point->x /= ipmInfo->xScale; + point->x += ipmInfo->xLimits[0]; + //y-direction + point->y /= ipmInfo->yScale; + point->y = ipmInfo->yLimits[1] - point->y; +} + + +/** + * Converts from IPM pixel coordinates into world coordinates + * + * \param inMat input matrix 2xN + * \param outMat output matrix 2xN + * \param ipmInfo the ipm info from mcvGetIPM + * + */ +void mcvTransformImIPM2Ground(const CvMat *inMat, CvMat* outMat, const IPMInfo *ipmInfo) +{ + CvMat *mat; + mat = outMat; + if(inMat != mat) + { + cvCopy(inMat, mat); + } + + //work on the x-direction i.e. first row + CvMat row; + cvGetRow(mat, &row, 0); + cvConvertScale(&row, &row, 1./ipmInfo->xScale, ipmInfo->xLimits[0]); + + //work on y-direction + cvGetRow(mat, &row, 1); + cvConvertScale(&row, &row, -1./ipmInfo->yScale, ipmInfo->yLimits[1]); +} + +/** + * Converts from IPM pixel coordinates into Image coordinates + * + * \param inMat input matrix 2xN + * \param outMat output matrix 2xN + * \param ipmInfo the ipm info from mcvGetIPM + * \param cameraInfo the camera info + * + */ +void mcvTransformImIPM2Im(const CvMat *inMat, CvMat* outMat, const IPMInfo *ipmInfo, + const CameraInfo *cameraInfo) +{ + //convert to world coordinates + mcvTransformImIPM2Ground(inMat, outMat, ipmInfo); + + //convert to image coordinates + mcvTransformGround2Image(outMat, outMat, cameraInfo); + +} + + +/** + * Initializes the cameraInfo structure with data read from the conf file + * + * \param fileName the input camera conf file name + * \param cameraInfo the returned camera parametrs struct + * + */ +void mcvInitCameraInfo (char * const fileName, CameraInfo *cameraInfo) +{ + //parsed camera data + CameraInfoParserInfo camInfo; + //read the data + assert(cameraInfoParser_configfile(fileName, &camInfo, 0, 1, 1)==0); + //init the strucure + cameraInfo->focalLength.x = camInfo.focalLengthX_arg; + cameraInfo->focalLength.y = camInfo.focalLengthY_arg; + cameraInfo->opticalCenter.x = camInfo.opticalCenterX_arg; + cameraInfo->opticalCenter.y = camInfo.opticalCenterY_arg; + cameraInfo->cameraHeight = camInfo.cameraHeight_arg; + cameraInfo->pitch = camInfo.pitch_arg * CV_PI/180; + cameraInfo->yaw = camInfo.yaw_arg * CV_PI/180; + cameraInfo->imageWidth = camInfo.imageWidth_arg; + cameraInfo->imageHeight = camInfo.imageHeight_arg; +} + + +/** + * Scales the cameraInfo according to the input image size + * + * \param cameraInfo the input/return structure + * \param size the input image size + * + */ + void mcvScaleCameraInfo (CameraInfo *cameraInfo, CvSize size) + { + //compute the scale factor + double scaleX = size.width/cameraInfo->imageWidth; + double scaleY = size.height/cameraInfo->imageHeight; + //scale + cameraInfo->imageWidth = size.width; + cameraInfo->imageHeight = size.height; + cameraInfo->focalLength.x *= scaleX; + cameraInfo->focalLength.y *= scaleY; + cameraInfo->opticalCenter.x *= scaleX; + cameraInfo->opticalCenter.y *= scaleY; + } + + +/** + * Gets the extent of the image on the ground plane given the camera parameters + * + * \param cameraInfo the input camera info + * \param ipmInfo the IPM info containing the extent on ground plane: + * xLimits & yLimits only are changed + * + */ +void mcvGetIPMExtent(const CameraInfo *cameraInfo, IPMInfo *ipmInfo ) +{ + //get size of input image + FLOAT u, v; + v = cameraInfo->imageHeight; + u = cameraInfo->imageWidth; + + //get the vanishing point + FLOAT_POINT2D vp; + vp = mcvGetVanishingPoint(cameraInfo); + vp.y = MAX(0, vp.y); + + //get extent of the image in the xfyf plane + FLOAT_MAT_ELEM_TYPE eps = VP_PORTION*v; + FLOAT_MAT_ELEM_TYPE uvLimitsp[] = {vp.x, u, 0, vp.x, + vp.y+eps, vp.y+eps, vp.y+eps, v}; + CvMat uvLimits = cvMat(2, 4, FLOAT_MAT_TYPE, uvLimitsp); + + //get these points on the ground plane + CvMat * xyLimitsp = cvCreateMat(2, 4, FLOAT_MAT_TYPE); + CvMat xyLimits = *xyLimitsp; + mcvTransformImage2Ground(&uvLimits, &xyLimits,cameraInfo); + //SHOW_MAT(xyLimitsp, "xyLImits"); + + //get extent on the ground plane + CvMat row1, row2; + cvGetRow(&xyLimits, &row1, 0); + cvGetRow(&xyLimits, &row2, 1); + double xfMax, xfMin, yfMax, yfMin; + cvMinMaxLoc(&row1, (double*)&xfMin, (double*)&xfMax, 0, 0, 0); + cvMinMaxLoc(&row2, (double*)&yfMin, (double*)&yfMax, 0, 0, 0); + + //return + ipmInfo->xLimits[0] = xfMin; + ipmInfo->xLimits[1] = xfMax; + ipmInfo->yLimits[1] = yfMax; + ipmInfo->yLimits[0] = yfMin; + +} + +} // namespace LaneDetector diff --git a/src/InversePerspectiveMapping.hh b/src/InversePerspectiveMapping.hh new file mode 100755 index 0000000..283d628 --- /dev/null +++ b/src/InversePerspectiveMapping.hh @@ -0,0 +1,195 @@ +/*** + * \file InversePerspectiveMapping.hh + * \author Mohamed Aly + * \date 11/29/2006 + */ + +#ifndef INVERSEPERSPECTIVEMAPPING_HH_ +#define INVERSEPERSPECTIVEMAPPING_HH_ + + +#include "cv.h" +#include "mcv.hh" +#include + +//conf file for cameraInfo +#include "CameraInfoOpt.h" + +using namespace std; + +namespace LaneDetector +{ + +/** + * Structure to hold the info about IPM transformation + */ +typedef struct IPMInfo +{ + ///min and max x-value on ground in world coordinates + FLOAT xLimits[2]; + ///min and max y-value on ground in world coordinates + FLOAT yLimits[2]; + ///conversion between mm in world coordinate on the ground + ///in x-direction and pixel in image + FLOAT xScale; + ///conversion between mm in world coordinate on the ground + ///in y-direction and pixel in image + FLOAT yScale; + ///width + int width; + ///height + int height; + ///portion of image height to add to y-coordinate of + ///vanishing point + float vpPortion; + ///Left point in original image of region to make IPM for + float ipmLeft; + ///Right point in original image of region to make IPM for + float ipmRight; + ///Top point in original image of region to make IPM for + float ipmTop; + ///Bottom point in original image of region to make IPM for + float ipmBottom; + ///interpolation to use for IPM (0: bilinear, 1:nearest neighbor) + int ipmInterpolation; +}IPMInfo; + +///Camera Calibration info +typedef struct CameraInfo +{ + ///focal length in x and y + FLOAT_POINT2D focalLength; + ///optical center coordinates in image frame (origin is (0,0) at top left) + FLOAT_POINT2D opticalCenter; + ///height of camera above ground + FLOAT cameraHeight; + ///pitch angle in radians (+ve downwards) + FLOAT pitch; + ///yaw angle in radians (+ve clockwise) + FLOAT yaw; + ///width of images + FLOAT imageWidth; + ///height of images + FLOAT imageHeight; +}CameraInfo; + +//functions definitions +/** + * This function returns the Inverse Perspective Mapping + * of the input image, assuming a flat ground plane, and + * given the camera parameters. + * + * \param inImage the input image + * \param outImage the output image in IPM + * \param ipmInfo the returned IPM info for the transformation + * \param focalLength focal length (in x and y direction) + * \param cameraInfo the camera parameters + */ +void mcvGetIPM(const CvMat* inImage, CvMat* outImage, + IPMInfo *ipmInfo, const CameraInfo *cameraInfo, + list* outPoints=NULL); + + +/** + * Transforms points from the image frame (uv-coordinates) + * into the real world frame on the ground plane (z=-height) + * + * \param inPoints input points in the image frame (2xN matrix) + * \param outPoints output points in the world frame on the ground + * (z=-height) (2xN matrix with xw, yw and implicit z=-height) + * \param cemaraInfo the input camera parameters + * + */ +void mcvTransformImage2Ground(const CvMat *inPoints, + CvMat *outPoints, const CameraInfo *cameraInfo); + + +/** + * Transforms points from the ground plane (z=-h) in the world frame + * into points on the image in image frame (uv-coordinates) + * + * \param inPoints 2xN array of input points on the ground in world coordinates + * \param outPoints 2xN output points in on the image in image coordinates + * \param cameraInfo the camera parameters + * + */ +void mcvTransformGround2Image(const CvMat *inPoints, + CvMat *outPoints, const CameraInfo *cameraInfo); + +/** + * Computes the vanishing point in the image plane uv. It is + * the point of intersection of the image plane with the line + * in the XY-plane in the world coordinates that makes an + * angle yaw clockwise (form Y-axis) with Y-axis + * + * \param cameraInfo the input camera parameter + * + * \return the computed vanishing point in image frame + * + */ +FLOAT_POINT2D mcvGetVanishingPoint(const CameraInfo *cameraInfo); + +/** + * Converts a point from IPM pixel coordinates into world coordinates + * + * \param point in/out point + * \param ipmInfo the ipm info from mcvGetIPM + * + */ +void mcvPointImIPM2World(FLOAT_POINT2D *point, const IPMInfo *ipmInfo); + +/** + * Initializes the cameraInfo structure with data read from the conf file + * + * \param fileName the input camera conf file name + * \param cameraInfo the returned camera parametrs struct + * + */ +void mcvInitCameraInfo (char *const fileName, CameraInfo *cameraInfo); + +/** + * Scales the cameraInfo according to the input image size + * + * \param cameraInfo the input/return structure + * \param size the input image size + * + */ + void mcvScaleCameraInfo (CameraInfo *cameraInfo, CvSize size); + +/** + * Gets the extent of the image on the ground plane given the camera parameters + * + * \param cameraInfo the input camera info + * \param ipmInfo the IPM info containing the extent on ground plane: + * xLimits & yLimits only are changed + * + */ +void mcvGetIPMExtent(const CameraInfo *cameraInfo, IPMInfo *ipmInfo); + +/** + * Converts from IPM pixel coordinates into world coordinates + * + * \param inMat input matrix 2xN + * \param outMat output matrix 2xN + * \param ipmInfo the ipm info from mcvGetIPM + * + */ +void mcvTransformImIPM2Ground(const CvMat *inMat, CvMat* outMat, + const IPMInfo *ipmInfo); + +/** + * Converts from IPM pixel coordinates into Image coordinates + * + * \param inMat input matrix 2xN + * \param outMat output matrix 2xN + * \param ipmInfo the ipm info from mcvGetIPM + * \param cameraInfo the camera info + * + */ +void mcvTransformImIPM2Im(const CvMat *inMat, CvMat* outMat, + const IPMInfo *ipmInfo, + const CameraInfo *cameraInfo); + +} // namespace LaneDetector + +#endif /*INVERSEPERSPECTIVEMAPPING_HH_*/ diff --git a/src/InversePerspectiveMapping.o b/src/InversePerspectiveMapping.o new file mode 100644 index 0000000000000000000000000000000000000000..c3c970b320714c2878ab59e28458f9f07b7af603 GIT binary patch literal 42760 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 z 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 + * \date Thu 26 Jul, 2007 + * + */ + +#include "LaneDetector.hh" + +#include "mcv.hh" +#include "InversePerspectiveMapping.hh" +#include "LaneDetectorOpt.h" +#include "ranker.h" + +#include +#include +#include +#include +#include + +using namespace std; + +#include +#include + +namespace LaneDetector +{ + // used for debugging + int DEBUG_LINES = 0; + +/** + * This function filters the input image looking for horizontal + * or vertical lines with specific width or height. + * + * \param inImage the input image + * \param outImage the output image in IPM + * \param wx width of kernel window in x direction = 2*wx+1 + * (default 2) + * \param wy width of kernel window in y direction = 2*wy+1 + * (default 2) + * \param sigmax std deviation of kernel in x (default 1) + * \param sigmay std deviation of kernel in y (default 1) + * \param lineType type of the line + * LINE_HORIZONTAL (default) + * LINE_VERTICAL + */ + void mcvFilterLines(const CvMat *inImage, CvMat *outImage, + unsigned char wx, unsigned char wy, FLOAT sigmax, + FLOAT sigmay, LineType lineType) +{ + //define the two kernels + //this is for 7-pixels wide +// FLOAT_MAT_ELEM_TYPE derivp[] = {-2.328306e-10, -6.984919e-09, -1.008157e-07, -9.313226e-07, -6.178394e-06, -3.129616e-05, -1.255888e-04, -4.085824e-04, -1.092623e-03, -2.416329e-03, -4.408169e-03, -6.530620e-03, -7.510213e-03, -5.777087e-03, -5.777087e-04, 6.932504e-03, 1.372058e-02, 1.646470e-02, 1.372058e-02, 6.932504e-03, -5.777087e-04, -5.777087e-03, -7.510213e-03, -6.530620e-03, -4.408169e-03, -2.416329e-03, -1.092623e-03, -4.085824e-04, -1.255888e-04, -3.129616e-05, -6.178394e-06, -9.313226e-07, -1.008157e-07, -6.984919e-09, -2.328306e-10}; +// int derivLen = 35; +// FLOAT_MAT_ELEM_TYPE smoothp[] = {2.384186e-07, 5.245209e-06, 5.507469e-05, 3.671646e-04, 1.744032e-03, 6.278515e-03, 1.778913e-02, 4.066086e-02, 7.623911e-02, 1.185942e-01, 1.541724e-01, 1.681881e-01, 1.541724e-01, 1.185942e-01, 7.623911e-02, 4.066086e-02, 1.778913e-02, 6.278515e-03, 1.744032e-03, 3.671646e-04, 5.507469e-05, 5.245209e-06, 2.384186e-07}; +// int smoothLen = 23; + CvMat fx; + CvMat fy; + //create the convoultion kernel + switch (lineType) + { + case LINE_HORIZONTAL: + { + //this is for 5-pixels wide + FLOAT_MAT_ELEM_TYPE derivp[] = {-2.384186e-07, -4.768372e-06, -4.482269e-05, -2.622604e-04, -1.064777e-03, -3.157616e-03, -6.976128e-03, -1.136112e-02, -1.270652e-02, -6.776810e-03, 6.776810e-03, 2.156258e-02, 2.803135e-02, 2.156258e-02, 6.776810e-03, -6.776810e-03, -1.270652e-02, -1.136112e-02, -6.976128e-03, -3.157616e-03, -1.064777e-03, -2.622604e-04, -4.482269e-05, -4.768372e-06, -2.384186e-07}; + int derivLen = 25; + FLOAT_MAT_ELEM_TYPE smoothp[] = {2.384186e-07, 5.245209e-06, 5.507469e-05, 3.671646e-04, 1.744032e-03, 6.278515e-03, 1.778913e-02, 4.066086e-02, 7.623911e-02, 1.185942e-01, 1.541724e-01, 1.681881e-01, 1.541724e-01, 1.185942e-01, 7.623911e-02, 4.066086e-02, 1.778913e-02, 6.278515e-03, 1.744032e-03, 3.671646e-04, 5.507469e-05, 5.245209e-06, 2.384186e-07}; + int smoothLen = 23; + //horizontal is smoothing and vertical is derivative + fx = cvMat(1, smoothLen, FLOAT_MAT_TYPE, smoothp); + fy = cvMat(derivLen, 1, FLOAT_MAT_TYPE, derivp); + } + break; + + case LINE_VERTICAL: + { + //this is for 5-pixels wide + FLOAT_MAT_ELEM_TYPE derivp[] = //{1.000000e-11, 8.800000e-10, 3.531000e-08, 8.536000e-07, 1.383415e-05, 1.581862e-04, 1.306992e-03, 7.852691e-03, 3.402475e-02, 1.038205e-01, 2.137474e-01, 2.781496e-01, 2.137474e-01, 1.038205e-01, 3.402475e-02, 7.852691e-03, 1.306992e-03, 1.581862e-04, 1.383415e-05, 8.536000e-07, 3.531000e-08, 8.800000e-10, 1.000000e-11}; + //{1.000000e-06, 4.800000e-05, 9.660000e-04, 1.048000e-02, 6.529500e-02, 2.278080e-01, 3.908040e-01, 2.278080e-01, 6.529500e-02, 1.048000e-02, 9.660000e-04, 4.800000e-05, 1.000000e-06}; + {1.000000e-16, 1.280000e-14, 7.696000e-13, 2.886400e-11, 7.562360e-10, 1.468714e-08, 2.189405e-07, 2.558828e-06, 2.374101e-05, 1.759328e-04, 1.042202e-03, 4.915650e-03, 1.829620e-02, 5.297748e-02, 1.169560e-01, 1.918578e-01, 2.275044e-01, 1.918578e-01, 1.169560e-01, 5.297748e-02, 1.829620e-02, 4.915650e-03, 1.042202e-03, 1.759328e-04, 2.374101e-05, 2.558828e-06, 2.189405e-07, 1.468714e-08, 7.562360e-10, 2.886400e-11, 7.696000e-13, 1.280000e-14, 1.000000e-16}; + int derivLen = 33; //23; 13; 33; + FLOAT_MAT_ELEM_TYPE smoothp[] = {-1.000000e-03, -2.200000e-02, -1.480000e-01, -1.940000e-01, 7.300000e-01, -1.940000e-01, -1.480000e-01, -2.200000e-02, -1.000000e-03}; + //{-1.000000e-07, -5.400000e-06, -1.240000e-04, -1.561000e-03, -1.149400e-02, -4.787020e-02, -9.073680e-02, 2.144300e-02, 2.606970e-01, 2.144300e-02, -9.073680e-02, -4.787020e-02, -1.149400e-02, -1.561000e-03, -1.240000e-04, -5.400000e-06, -1.000000e-07}; + int smoothLen = 9; //9; 17; + //horizontal is derivative and vertical is smoothing + fy = cvMat(1, smoothLen, FLOAT_MAT_TYPE, smoothp); + fx = cvMat(derivLen, 1, FLOAT_MAT_TYPE, derivp); + } + break; + } + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //SHOW_MAT(kernel, "Kernel:"); + }//#endif + +#warning "still check subtracting mean from image" + //subtract mean + CvScalar mean = cvAvg(inImage); + cvSubS(inImage, mean, outImage); + + + //do the filtering + cvFilter2D(outImage, outImage, &fx); //inImage outImage + cvFilter2D(outImage, outImage, &fy); + + +// CvMat *deriv = cvCreateMat +// //define x +// CvMat *x = cvCreateMat(2*wx+1, 1, FLOAT_MAT_TYPE); +// //define y +// CvMat *y = cvCreateMat(2*wy+1, 1, FLOAT_MAT_TYPE); + +// //create the convoultion kernel +// switch (lineType) +// { +// case FILTER_LINE_HORIZONTAL: +// //guassian in x direction +// mcvGetGaussianKernel(x, wx, sigmax); +// //derivative of guassian in y direction +// mcvGet2DerivativeGaussianKernel(y, wy, sigmay); +// break; + +// case FILTER_LINE_VERTICAL: +// //guassian in y direction +// mcvGetGaussianKernel(y, wy, sigmay); +// //derivative of guassian in x direction +// mcvGet2DerivativeGaussianKernel(x, wx, sigmax); +// break; +// } + +// //combine the 2D kernel +// CvMat *kernel = cvCreateMat(2*wy+1, 2*wx+1, FLOAT_MAT_TYPE); +// cvGEMM(y, x, 1, 0, 1, kernel, CV_GEMM_B_T); + +// //subtract the mean +// CvScalar mean = cvAvg(kernel); +// cvSubS(kernel, mean, kernel); + +// #ifdef DEBUG_GET_STOP_LINES +// //SHOW_MAT(kernel, "Kernel:"); +// #endif + +// //do the filtering +// cvFilter2D(inImage, outImage, kernel); + +// //clean +// cvReleaseMat(&x); +// cvReleaseMat(&y); +// cvReleaseMat(&kernel); +} + +/** + * This function gets a 1-D gaussian filter with specified + * std deviation and range + * + * \param kernel input mat to hold the kernel (2*w+1x1) + * column vector (already allocated) + * \param w width of kernel is 2*w+1 + * \param sigma std deviation + */ +void mcvGetGaussianKernel(CvMat *kernel, unsigned char w, FLOAT sigma) +{ + //get variance + sigma *= sigma; + + //get the kernel + for (double i=-w; i<=w; i++) + CV_MAT_ELEM(*kernel, FLOAT_MAT_ELEM_TYPE, int(i+w), 0) = + (FLOAT_MAT_ELEM_TYPE) exp(-(.5/sigma)*(i*i)); +} + +/** + * This function gets a 1-D second derivative gaussian filter + * with specified std deviation and range + * + * \param kernel input mat to hold the kernel (2*w+1x1) + * column vector (already allocated) + * \param w width of kernel is 2*w+1 + * \param sigma std deviation + */ +void mcvGet2DerivativeGaussianKernel(CvMat *kernel, + unsigned char w, FLOAT sigma) +{ + //get variance + sigma *= sigma; + + //get the kernel + for (double i=-w; i<=w; i++) + CV_MAT_ELEM(*kernel, FLOAT_MAT_ELEM_TYPE, int(i+w), 0) = + (FLOAT_MAT_ELEM_TYPE) + (exp(-.5*i*i)/sigma - (i*i)*exp(-(.5/sigma)*i*i)/(sigma*sigma)); +} + + +/** This function groups the input filtered image into + * horizontal or vertical lines. + * + * \param inImage input image + * \param lines returned detected lines (vector of points) + * \param lineScores scores of the detected lines (vector of floats) + * \param lineType type of lines to detect + * LINE_HORIZONTAL (default) or LINE_VERTICAL + * \param linePixelWidth width (or height) of lines to detect + * \param localMaxima whether to detect local maxima or just get + * the maximum + * \param detectionThreshold threshold for detection + * \param smoothScores whether to smooth scores detected or not + */ +void mcvGetHVLines(const CvMat *inImage, vector *lines, + vector *lineScores, LineType lineType, + FLOAT linePixelWidth, bool binarize, bool localMaxima, + FLOAT detectionThreshold, bool smoothScores) +{ + CvMat * image = cvCloneMat(inImage); + //binarize input image if to binarize + if (binarize) + { + //mcvBinarizeImage(image); + image = cvCreateMat(inImage->rows, inImage->cols, INT_MAT_TYPE); + cvThreshold(inImage, image, 0, 1, CV_THRESH_BINARY); //0.05 + } + + //get sum of lines through horizontal or vertical + //sumLines is a column vector + CvMat sumLines, *sumLinesp; + int maxLineLoc = 0; + switch (lineType) + { + case LINE_HORIZONTAL: + sumLinesp = cvCreateMat(image->height, 1, FLOAT_MAT_TYPE); + cvReduce(image, sumLinesp, 1, CV_REDUCE_SUM); //_AVG + cvReshape(sumLinesp, &sumLines, 0, 0); + //max location for a detected line + maxLineLoc = image->height-1; + break; + case LINE_VERTICAL: + sumLinesp = cvCreateMat(1, image->width, FLOAT_MAT_TYPE); + cvReduce(image, sumLinesp, 0, CV_REDUCE_SUM); //_AVG + cvReshape(sumLinesp, &sumLines, 0, image->width); + //max location for a detected line + maxLineLoc = image->width-1; + break; + } + //SHOW_MAT(&sumLines, "sumLines:"); + + //smooth it + + float smoothp[] = { + 0.000003726653172, 0.000040065297393, 0.000335462627903, 0.002187491118183, + 0.011108996538242, 0.043936933623407, 0.135335283236613, 0.324652467358350, + 0.606530659712633, 0.882496902584595, 1.000000000000000, 0.882496902584595, + 0.606530659712633, 0.324652467358350, 0.135335283236613, 0.043936933623407, + 0.011108996538242, 0.002187491118183, 0.000335462627903, 0.000040065297393, + 0.000003726653172}; +// {0.000004,0.000010,0.000025,0.000063,0.000148,0.000335,0.000732,0.001534,0.003089,0.005976,0.011109,0.019841,0.034047,0.056135,0.088922,0.135335,0.197899,0.278037,0.375311,0.486752,0.606531,0.726149,0.835270,0.923116,0.980199,1.000000,0.980199,0.923116,0.835270,0.726149,0.606531,0.486752,0.375311,0.278037,0.197899,0.135335,0.088922,0.056135,0.034047,0.019841,0.011109,0.005976,0.003089,0.001534,0.000732,0.000335,0.000148,0.000063,0.000025,0.000010,0.000004}; + int smoothWidth = 21; //21; 51; + CvMat smooth = cvMat(1, smoothWidth, CV_32FC1, smoothp); + if (smoothScores) + cvFilter2D(&sumLines, &sumLines, &smooth); +// SHOW_MAT(&sumLines, "sumLines:"); + + + + //get the max and its location + vector sumLinesMaxLoc; + vector sumLinesMax; + int maxLoc; double max; + //TODO: put the ignore in conf + #define MAX_IGNORE 0 //(int(smoothWidth/2.)+1) + #define LOCAL_MAX_IGNORE (int(MAX_IGNORE/4)) + mcvGetVectorMax(&sumLines, &max, &maxLoc, MAX_IGNORE); + + //put the local maxima stuff here + if (localMaxima) + { + //loop to get local maxima + for(int i=1+LOCAL_MAX_IGNORE; i CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, i-1, 0)) + && (val > CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, i+1, 0)) + // && (i != maxLoc) + && (val >= detectionThreshold) ) + { + //iterators for the two vectors + vector::iterator j; + vector::iterator k; + //loop till we find the place to put it in descendingly + for(j=sumLinesMax.begin(), k=sumLinesMaxLoc.begin(); + j != sumLinesMax.end() && val<= *j; j++,k++); + //add its index + sumLinesMax.insert(j, val); + sumLinesMaxLoc.insert(k, i); + } + } + } + + //check if didnt find local maxima + if(sumLinesMax.size()==0 && max>detectionThreshold) + { + //put maximum + sumLinesMaxLoc.push_back(maxLoc); + sumLinesMax.push_back(max); + } + +// //sort it descendingly +// sort(sumLinesMax.begin(), sumLinesMax.end(), greater()); +// //sort the indices +// for (int i=0; i<(int)sumLinesMax.size(); i++) +// for (int j=i; j<(int)sumLinesMax.size(); j++) +// if(sumLinesMax[i] == CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, +// sumLinesMaxLoc[j], 0)) +// { +// int k = sumLinesMaxLoc[j]; +// sumLinesMaxLoc[j] = sumLinesMaxLoc[i]; +// sumLinesMaxLoc[i] = k; +// } +// //sort(sumLinesMaxLoc.begin(), sumLinesMaxLoc.end(), greater()); + + //plot the line scores and the local maxima + //if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES +// gnuplot_ctrl *h = mcvPlotMat1D(NULL, &sumLines, "Line Scores"); +// CvMat *y = mcvVector2Mat(sumLinesMax); +// CvMat *x = mcvVector2Mat(sumLinesMaxLoc); +// mcvPlotMat2D(h, x, y); +// //gnuplot_plot_xy(h, (double*)&sumLinesMaxLoc,(double*)&sumLinesMax, sumLinesMax.size(),""); +// cin.get(); +// gnuplot_close(h); +// cvReleaseMat(&x); +// cvReleaseMat(&y); +//}//#endif + //process the found maxima + for (int i=0; i<(int)sumLinesMax.size(); i++) + { + //get subpixel accuracy + double maxLocAcc = mcvGetLocalMaxSubPixel( + CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, MAX(sumLinesMaxLoc[i]-1,0), 0), + CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, sumLinesMaxLoc[i], 0), + CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, + MIN(sumLinesMaxLoc[i]+1,maxLineLoc), 0) ); + maxLocAcc += sumLinesMaxLoc[i]; + maxLocAcc = MIN(MAX(0, maxLocAcc), maxLineLoc); + + + //TODO: get line extent + + //put the extracted line + Line line; + switch (lineType) + { + case LINE_HORIZONTAL: + line.startPoint.x = 0.5; + line.startPoint.y = (FLOAT)maxLocAcc + .5;//sumLinesMaxLoc[i]+.5; + line.endPoint.x = inImage->width-.5; + line.endPoint.y = line.startPoint.y; + break; + case LINE_VERTICAL: + line.startPoint.x = (FLOAT)maxLocAcc + .5;//sumLinesMaxLoc[i]+.5; + line.startPoint.y = .5; + line.endPoint.x = line.startPoint.x; + line.endPoint.y = inImage->height-.5; + break; + } + (*lines).push_back(line); + if (lineScores) + (*lineScores).push_back(sumLinesMax[i]); + }//for + + if(DEBUG_LINES) + {//#ifdef DEBUG_GET_STOP_LINES + CvMat *im, *im2 = cvCloneMat(image); + if (binarize) + cvConvertScale(im2, im2, 255, 0); + + if (binarize) + im = cvCreateMat(image->rows, image->cols, CV_8UC3); + else + im = cvCreateMat(image->rows, image->cols, CV_32FC3); + mcvScaleMat(im2, im2); + cvCvtColor(im2, im, CV_GRAY2RGB); + for (unsigned int i=0; isize(); i++) + { + Line line = (*lines)[i]; + mcvIntersectLineWithBB(&line, cvSize(image->cols, image->rows), &line); + if (binarize) + mcvDrawLine(im, line, CV_RGB(255,0,0), 1); + else + mcvDrawLine(im, line, CV_RGB(1,0,0), 1); + } + + char str[256]; + switch (lineType) + { + case LINE_HORIZONTAL: + sprintf(str, "%s", "Horizontal Lines"); + break; + case LINE_VERTICAL: + sprintf(str, "%s", "Vertical Lines"); + break; + } + SHOW_IMAGE(im, str, 10); + cvReleaseMat(&im); + cvReleaseMat(&im2); + } + + //clean + cvReleaseMat(&sumLinesp); + //cvReleaseMat(&smooth); + sumLinesMax.clear(); + sumLinesMaxLoc.clear(); + cvReleaseMat(&image); +} + + +/** This function detects lines in images using Hough transform + * + * \param inImage input image + * \param lines vector of lines to hold the results + * \param lineScores scores of the detected lines (vector of floats) + * \param rMin minimum r use for finding the lines (default 0) + * \param rMax maximum r to find (default max(size(im))) + * \param rStep step to use for binning (default is 2) + * \param thetaMin minimum angle theta to look for (default 0) all in radians + * \param thetaMax maximum angle theta to look for (default 2*pi) + * \param thetaStep step to use for binning theta (default 5) + * \param binarize if to binarize the input image or use the raw values so that + * non-zero values are not treated as equal + * \param localMaxima whether to detect local maxima or just get + * the maximum + * \param detectionThreshold threshold for detection + * \param smoothScores whether to smooth scores detected or not + * \param group whether to group nearby detections (1) or not (0 default) + * \param groupThreshold the minimum distance used for grouping (default 10) + */ + +void mcvGetHoughTransformLines(const CvMat *inImage, vector *lines, + vector *lineScores, + FLOAT rMin, FLOAT rMax, FLOAT rStep, + FLOAT thetaMin, FLOAT thetaMax, + FLOAT thetaStep, bool binarize, bool localMaxima, + FLOAT detectionThreshold, bool smoothScores, + bool group, FLOAT groupThreshold) +{ + CvMat *image; + + //binarize input image if to binarize + if (!binarize) + { + image = cvCloneMat(inImage); assert(image!=0); + // mcvBinarizeImage(image); + } + //binarize input image + else + { + image = cvCreateMat(inImage->rows, inImage->cols, INT_MAT_TYPE); + cvThreshold(inImage, image, 0, 1, CV_THRESH_BINARY); //0.05 + //get max of image + //double maxval, minval; + //cvMinMaxLoc(inImage, &minval, &maxval); + //cout << "Max = " << maxval << "& Min=" << minval << "\n"; + //CvScalar mean = cvAvg(inImage); + //cout << "Mean=" << mean.val[0] << "\n"; + } + + if(DEBUG_LINES) + {//#ifdef DEBUG_GET_STOP_LINES + SHOW_IMAGE(image, "Hough thresholded image", 10); + }//#endif + + //define the accumulator array: rows correspond to r and columns to theta + int rBins = int((rMax-rMin)/rStep); + int thetaBins = int((thetaMax-thetaMin)/thetaStep); + CvMat *houghSpace = cvCreateMat(rBins, thetaBins, CV_MAT_TYPE(image->type)); //FLOAT_MAT_TYPE); + assert(houghSpace!=0); + //init to zero + cvSet(houghSpace, cvRealScalar(0)); + + //init values of r and theta + FLOAT *rs = new FLOAT[rBins]; + FLOAT *thetas = new FLOAT[thetaBins]; + FLOAT r, theta; + int ri, thetai; + for (r=rMin+rStep/2, ri=0 ; riwidth; i++) + for (int j=0; jheight; j++) + if ( cvGetReal2D(image, j, i) ) + { + CV_MAT_ELEM(*nzPoints, int, idx, 0) = i; + CV_MAT_ELEM(*nzPoints, int, idx, 1) = j; + idx++; + } + + //calculate r values for all theta and all points + //CvMat *rPoints = cvCreateMat(image->width*image->height, thetaBins, CV_32SC1);//FLOAT_MAT_TYPE) + //CvMat *rPoints = cvCreateMat(nzCount, thetaBins, CV_32SC1);//FLOAT_MAT_TYPE); + //cvSet(rPoints, cvRealScalar(-1)); + //loop on x + //float x=0.5, y=0.5; + int i, k; //j + for (i=0; i=0 && rwidth; i++) //x=0; x++ +// //loop on y +// for (j=0; jheight; j++) //y=0 y++ +// //loop on theta +// for (k=0; krBins-1) +// if (lineConf->binarize && CV_MAT_ELEM(*image, INT_MAT_ELEM_TYPE, j, i) !=0) +// inside = true; +// else if (!lineConf->binarize && CV_MAT_ELEM(*image, FLOAT_MAT_ELEM_TYPE, +// j, i) !=0) +// inside = true; +// else +// inside = false; +// if (inside) +// { +// theta = thetas[k]; +// float rval = i * cos(theta) + j * sin(theta); //x y +// CV_MAT_ELEM(*rPoints, int, +// i*image->height + j, k) = +// (int)( ( rval - lineConf->rMin) / lineConf->rStep); +// } + +// } + +// SHOW_MAT(rPoints, "rPoints"); +// cin.get(); + + //now we should accumulate the values into the approprate bins in the houghSpace +// for (ri=0; riwidth; i++) +// for (j=0; jheight; j++) +// { +// //check if this cell belongs to that bin or not +// if (CV_MAT_ELEM(*rPoints, int, +// i*image->height + j , thetai)==ri) +// { +// if(lineConf->binarize) +// CV_MAT_ELEM(*houghSpace, INT_MAT_ELEM_TYPE, ri, thetai)++; +// //CV_MAT_ELEM(*image, INT_MAT_ELEM_TYPE, j, i); +// else +// CV_MAT_ELEM(*houghSpace, FLOAT_MAT_ELEM_TYPE, ri, thetai)+= +// CV_MAT_ELEM(*image, FLOAT_MAT_ELEM_TYPE, j, i); +// } +// } + + + //smooth hough transform + if (smoothScores) + cvSmooth(houghSpace, houghSpace, CV_GAUSSIAN, 3); + + //get local maxima + vector maxLineScores; + vector maxLineLocs; + if (localMaxima) + { + //get local maxima in the hough space + mcvGetMatLocalMax(houghSpace, maxLineScores, maxLineLocs, detectionThreshold); + } + else + { + //get the maxima above the threshold + mcvGetMatMax(houghSpace, maxLineScores, maxLineLocs, detectionThreshold); + } + + //get the maximum value + double maxLineScore; + CvPoint maxLineLoc; + cvMinMaxLoc(houghSpace, 0, &maxLineScore, 0, &maxLineLoc); + if (maxLineScores.size()==0 && maxLineScore>=detectionThreshold) + { + maxLineScores.push_back(maxLineScore); + maxLineLocs.push_back(maxLineLoc); + } + + + if(DEBUG_LINES) + {//#ifdef DEBUG_GET_STOP_LINES + // cout << "Local maxima = " << maxLineScores.size() << "\n"; + + { + CvMat *im, *im2 = cvCloneMat(image); + if (binarize) + cvConvertScale(im2, im2, 255, 0); + + if (binarize) + im = cvCreateMat(image->rows, image->cols, CV_8UC3);//cvCloneMat(image); + else + im = cvCreateMat(image->rows, image->cols, CV_32FC3); + cvCvtColor(im2, im, CV_GRAY2RGB); + for (int i=0; i<(int)maxLineScores.size(); i++) + { + Line line; + assert(maxLineLocs[i].x>=0 && maxLineLocs[i].x=0 && maxLineLocs[i].ycols, image->rows), &line); + if (binarize) + mcvDrawLine(im, line, CV_RGB(255,0,0), 1); + else + mcvDrawLine(im, line, CV_RGB(1,0,0), 1); + } + SHOW_IMAGE(im, "Hough before grouping", 10); + cvReleaseMat(&im); + cvReleaseMat(&im2); + + // //debug + // cout << "Maxima detected:\n"; + // for(int ii=0; ii<(int)maxLineScores.size(); ii++) + // cout << " " << maxLineScores[ii]; + // cout << "\n"; + } + }//#endif + + //group detected maxima + if (group && maxLineScores.size()>1) + { + //flag for stopping + bool stop = false; + while (!stop) + { + //minimum distance so far + float minDist = groupThreshold+5, dist = 0.; + vector::iterator iloc, jloc, + minIloc=maxLineLocs.begin(), minJloc=minIloc+1; + vector::iterator iscore, jscore, minIscore, minJscore; + //compute pairwise distance between detected maxima + for (iloc=maxLineLocs.begin(), iscore=maxLineScores.begin(); + iloc!=maxLineLocs.end(); iloc++, iscore++) + for (jscore=iscore+1, jloc=iloc+1; jscore!=maxLineScores.end(); + jloc++, jscore++) + { + //add pi if neg + float t1 = thetas[iloc->x]<0 ? thetas[iloc->x] : thetas[iloc->x]+CV_PI; + float t2 = thetas[jloc->x]<0 ? thetas[jloc->x] : thetas[jloc->x]+CV_PI; + //get distance + dist = fabs(rs[iloc->y]-rs[jloc->y]) + + 0.1 * fabs(t1 - t2);//fabs(thetas[iloc->x]-thetas[jloc->x]); + //check if minimum + if (dist= groupThreshold) + stop = true; + else + { + // //debug + // cout << "Before grouping:\n"; + // for(int ii=0; ii<(int)maxLineScores.size(); ii++) + // cout << " " << maxLineScores[ii]; + // cout << "\n"; + + //combine the two minimum ones with weighted average of + //their scores + double x = (minIloc->x * *minIscore + minJloc->x * *minJscore) / + (*minIscore + *minJscore); + double y = (minIloc->y * *minIscore + minJloc->y * *minJscore) / + (*minIscore + *minJscore); + //put into the first + minIloc->x = (int)x;// ((minJloc->x + minJloc->x)/2.0); // (int) x; + minIloc->y = (int)y;// ((minJloc->y + minIloc->y)/2.0); // (int) y; + *minIscore = (*minJscore + *minIscore)/2;///2; + //delete second one + maxLineLocs.erase(minJloc); + maxLineScores.erase(minJscore); + + // //debug + // cout << "Before sorting:\n"; + // for(int ii=0; ii<(int)maxLineScores.size(); ii++) + // cout << " " << maxLineScores[ii]; + // cout << "\n"; + + //check if to put somewhere else depending on the changed score + for (iscore=maxLineScores.begin(), iloc=maxLineLocs.begin(); + iscore!=maxLineScores.end() && *minIscore <= *iscore; + iscore++, iloc++); + //swap the original location if different + if (iscore!=minIscore ) + { + //insert in new position + maxLineScores.insert(iscore, *minIscore); + maxLineLocs.insert(iloc, *minIloc); + //delte old + maxLineScores.erase(minIscore); + maxLineLocs.erase(minIloc); + // //if at end, then back up one position + // if(iscore == maxLineScores.end()) + // { + // iscore--; + // iloc--; + // } + // CvPoint tloc; + // double tscore; + // //swap + // tloc = *minIloc; + // tscore = *minIscore; + + // *minIloc = *iloc; + // *minIscore = *iscore; + + // *iloc = tloc; + // *iscore = tscore; + } + // cout << "after sorting:\n"; + // for(int ii=0; ii<(int)maxLineScores.size(); ii++) + // cout << " " << maxLineScores[ii]; + // cout << "\n"; + } + } + } + + if(DEBUG_LINES) //#ifdef DEBUG_GET_STOP_LINES + { + CvMat *im, *im2 = cvCloneMat(image); + if (binarize) + cvConvertScale(im2, im2, 255, 0); + if (binarize) + im = cvCreateMat(image->rows, image->cols, CV_8UC3);//cvCloneMat(image); + else + im = cvCreateMat(image->rows, image->cols, CV_32FC3); + cvCvtColor(im2, im, CV_GRAY2RGB); + for (int i=0; i<(int)maxLineScores.size(); i++) + { + Line line; + assert(maxLineLocs[i].x>=0 && maxLineLocs[i].x=0 && maxLineLocs[i].ycols, image->rows), &line); + if (binarize) + mcvDrawLine(im, line, CV_RGB(255,0,0), 1); + else + mcvDrawLine(im, line, CV_RGB(1,0,0), 1); + } + SHOW_IMAGE(im, "Hough after grouping", 10); + cvReleaseMat(&im); + cvReleaseMat(&im2); + + //put local maxima in image + CvMat *houghSpaceClr; + if(binarize) + houghSpaceClr = cvCreateMat(houghSpace->height, houghSpace->width, + CV_8UC3); + else + houghSpaceClr = cvCreateMat(houghSpace->height, houghSpace->width, + CV_32FC3); + mcvScaleMat(houghSpace, houghSpace); + cvCvtColor(houghSpace, houghSpaceClr, CV_GRAY2RGB); + for (int i=0; i<(int)maxLineLocs.size(); i++) + cvCircle(houghSpaceClr, cvPoint(maxLineLocs[i].x, maxLineLocs[i].y), + 1, CV_RGB(1, 0, 0), -1); + // if(lineConf->binarize) + // CV_MAT_ELEM(*houghSpace, unsigned char, maxLineLocs[i].y, + // maxLineLocs[i].x) = 255; + // else + // CV_MAT_ELEM(*houghSpace, float, maxLineLocs[i].y, maxLineLocs[i].x) = 1.f; + //show the hough space + SHOW_IMAGE(houghSpaceClr, "Hough Space", 10); + cvReleaseMat(&houghSpaceClr); + //SHOW_MAT(houghSpace, "Hough Space:"); + }//#endif + + //process detected maxima and return detected line(s) + for(int i=0; i=detectionThreshold) + { + //get sub-pixel accuracy + // + //get the two end points from the r-theta + Line line; + assert(maxLineLocs[i].x>=0 && maxLineLocs[i].x=0 && maxLineLocs[i].ycols, image->rows), &line); + //get line extent + //put the extracted line + lines->push_back(line); + if (lineScores) + (*lineScores).push_back(maxLineScores[i]); + + } + //not above threshold + else + { + //exit out of the for loop, as the scores are sorted descendingly + break; + } + } + + //clean up + cvReleaseMat(&image); + cvReleaseMat(&houghSpace); + //cvReleaseMat(&rPoints); + delete [] rs; + delete [] thetas; + maxLineScores.clear(); + maxLineLocs.clear(); +} + + + +/** This function binarizes the input image i.e. nonzero elements + * becomen 1 and others are 0. + * + * \param inImage input & output image + */ +void mcvBinarizeImage(CvMat *inImage) +{ + + if (CV_MAT_TYPE(inImage->type)==FLOAT_MAT_TYPE) + { + for (int i=0; iheight; i++) + for (int j=0; jwidth; j++) + if (CV_MAT_ELEM(*inImage, FLOAT_MAT_ELEM_TYPE, i, j) != 0.f) + CV_MAT_ELEM(*inImage, FLOAT_MAT_ELEM_TYPE, i, j)=1; + } + else if (CV_MAT_TYPE(inImage->type)==INT_MAT_TYPE) + { + for (int i=0; iheight; i++) + for (int j=0; jwidth; j++) + if (CV_MAT_ELEM(*inImage, INT_MAT_ELEM_TYPE, i, j) != 0) + CV_MAT_ELEM(*inImage, INT_MAT_ELEM_TYPE, i, j)=1; + } + else + { + cerr << "Unsupported type in mcvBinarizeImage\n"; + exit(1); + } +} + + +/** This function gets the maximum value in a vector (row or column) + * and its location + * + * \param inVector the input vector + * \param max the output max value + * \param maxLoc the location (index) of the first max + * + */ +#define MCV_VECTOR_MAX(type) \ + /*row vector*/ \ + if (inVector->height==1) \ + { \ + /*initial value*/ \ + tmax = (double) CV_MAT_ELEM(*inVector, type, 0, inVector->width-1); \ + tmaxLoc = inVector->width-1; \ + /*loop*/ \ + for (int i=inVector->width-1-ignore; i>=0+ignore; i--) \ + { \ + if (tmaxheight-1, 0); \ + tmaxLoc = inVector->height-1; \ + /*loop*/ \ + for (int i=inVector->height-1-ignore; i>=0+ignore; i--) \ + { \ + if (tmaxtype)==FLOAT_MAT_TYPE) + { + MCV_VECTOR_MAX(FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inVector->type)==INT_MAT_TYPE) + { + MCV_VECTOR_MAX(INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetVectorMax\n"; + exit(1); + } + + //return + if (max) + *max = tmax; + if (maxLoc) + *maxLoc = tmaxLoc; +} + + + +/** This function gets the local maxima in a matrix and their positions + * and its location + * + * \param inMat input matrix + * \param localMaxima the output vector of local maxima + * \param localMaximaLoc the vector of locations of the local maxima, + * where each location is cvPoint(x=col, y=row) zero-based + * + */ + +void mcvGetMatLocalMax(const CvMat *inMat, vector &localMaxima, + vector &localMaximaLoc, double threshold) +{ + + double val; + +#define MCV_MAT_LOCAL_MAX(type) \ + /*loop on the matrix and get points that are larger than their*/ \ + /*neighboring 8 pixels*/ \ + for(int i=1; irows-1; i++) \ + for (int j=1; jcols-1; j++) \ + { \ + /*get the current value*/ \ + val = CV_MAT_ELEM(*inMat, type, i, j); \ + /*check if it's larger than all its neighbors*/ \ + if( val > CV_MAT_ELEM(*inMat, type, i-1, j-1) && \ + val > CV_MAT_ELEM(*inMat, type, i-1, j) && \ + val > CV_MAT_ELEM(*inMat, type, i-1, j+1) && \ + val > CV_MAT_ELEM(*inMat, type, i, j-1) && \ + val > CV_MAT_ELEM(*inMat, type, i, j+1) && \ + val > CV_MAT_ELEM(*inMat, type, i+1, j-1) && \ + val > CV_MAT_ELEM(*inMat, type, i+1, j) && \ + val > CV_MAT_ELEM(*inMat, type, i+1, j+1) && \ + val >= threshold) \ + { \ + /*found a local maxima, put it in the return vector*/ \ + /*in decending order*/ \ + /*iterators for the two vectors*/ \ + vector::iterator k; \ + vector::iterator l; \ + /*loop till we find the place to put it in descendingly*/ \ + for(k=localMaxima.begin(), l=localMaximaLoc.begin(); \ + k != localMaxima.end() && val<= *k; k++,l++); \ + /*add its index*/ \ + localMaxima.insert(k, val); \ + localMaximaLoc.insert(l, cvPoint(j, i)); \ + } \ + } + + //check type + if (CV_MAT_TYPE(inMat->type)==FLOAT_MAT_TYPE) + { + MCV_MAT_LOCAL_MAX(FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==INT_MAT_TYPE) + { + MCV_MAT_LOCAL_MAX(INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetMatLocalMax\n"; + exit(1); + } +} + +/** This function gets the locations and values of all points + * above a certain threshold + * + * \param inMat input matrix + * \param maxima the output vector of maxima + * \param maximaLoc the vector of locations of the maxima, + * where each location is cvPoint(x=col, y=row) zero-based + * + */ + +void mcvGetMatMax(const CvMat *inMat, vector &maxima, + vector &maximaLoc, double threshold) +{ + + double val; + +#define MCV_MAT_MAX(type) \ + /*loop on the matrix and get points that are larger than their*/ \ + /*neighboring 8 pixels*/ \ + for(int i=1; irows-1; i++) \ + for (int j=1; jcols-1; j++) \ + { \ + /*get the current value*/ \ + val = CV_MAT_ELEM(*inMat, type, i, j); \ + /*check if it's larger than threshold*/ \ + if (val >= threshold) \ + { \ + /*found a maxima, put it in the return vector*/ \ + /*in decending order*/ \ + /*iterators for the two vectors*/ \ + vector::iterator k; \ + vector::iterator l; \ + /*loop till we find the place to put it in descendingly*/ \ + for(k=maxima.begin(), l=maximaLoc.begin(); \ + k != maxima.end() && val<= *k; k++,l++); \ + /*add its index*/ \ + maxima.insert(k, val); \ + maximaLoc.insert(l, cvPoint(j, i)); \ + } \ + } + + //check type + if (CV_MAT_TYPE(inMat->type)==FLOAT_MAT_TYPE) + { + MCV_MAT_MAX(FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==INT_MAT_TYPE) + { + MCV_MAT_MAX(INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetMatMax\n"; + exit(1); + } +} + + +/** This function gets the local maxima in a vector and their positions + * + * \param inVec input vector + * \param localMaxima the output vector of local maxima + * \param localMaximaLoc the vector of locations of the local maxima, + * + */ +#define MCV_VECTOR_LOCAL_MAX(type) \ + /*loop on the vector and get points that are larger than their*/ \ + /*neighboring points*/ \ + if(inVec->height == 1) \ + { \ + for(int i=1; iwidth-1; i++) \ + { \ + /*get the current value*/ \ + val = CV_MAT_ELEM(*inVec, type, 0, i); \ + /*check if it's larger than all its neighbors*/ \ + if( val > CV_MAT_ELEM(*inVec, type, 0, i-1) && \ + val > CV_MAT_ELEM(*inVec, type, 0, i+1) ) \ + { \ + /*found a local maxima, put it in the return vector*/ \ + /*in decending order*/ \ + /*iterators for the two vectors*/ \ + vector::iterator k; \ + vector::iterator l; \ + /*loop till we find the place to put it in descendingly*/ \ + for(k=localMaxima.begin(), l=localMaximaLoc.begin(); \ + k != localMaxima.end() && val<= *k; k++,l++); \ + /*add its index*/ \ + localMaxima.insert(k, val); \ + localMaximaLoc.insert(l, i); \ + } \ + } \ + } \ + else \ + { \ + for(int i=1; iheight-1; i++) \ + { \ + /*get the current value*/ \ + val = CV_MAT_ELEM(*inVec, type, i, 0); \ + /*check if it's larger than all its neighbors*/ \ + if( val > CV_MAT_ELEM(*inVec, type, i-1, 0) && \ + val > CV_MAT_ELEM(*inVec, type, i+1, 0) ) \ + { \ + /*found a local maxima, put it in the return vector*/ \ + /*in decending order*/ \ + /*iterators for the two vectors*/ \ + vector::iterator k; \ + vector::iterator l; \ + /*loop till we find the place to put it in descendingly*/ \ + for(k=localMaxima.begin(), l=localMaximaLoc.begin(); \ + k != localMaxima.end() && val<= *k; k++,l++); \ + /*add its index*/ \ + localMaxima.insert(k, val); \ + localMaximaLoc.insert(l, i); \ + } \ + } \ + } + +void mcvGetVectorLocalMax(const CvMat *inVec, vector &localMaxima, + vector &localMaximaLoc) +{ + + double val; + + //check type + if (CV_MAT_TYPE(inVec->type)==FLOAT_MAT_TYPE) + { + MCV_VECTOR_LOCAL_MAX(FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inVec->type)==INT_MAT_TYPE) + { + MCV_VECTOR_LOCAL_MAX(INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetVectorLocalMax\n"; + exit(1); + } +} + + +/** This function gets the qtile-th quantile of the input matrix + * + * \param mat input matrix + * \param qtile required input quantile probability + * \return the returned value + * + */ +FLOAT mcvGetQuantile(const CvMat *mat, FLOAT qtile) +{ + //make it a row vector + CvMat rowMat; + cvReshape(mat, &rowMat, 0, 1); + + //get the quantile + FLOAT qval; + qval = quantile((FLOAT*) rowMat.data.ptr, rowMat.width, qtile); + + return qval; +} + + +/** This function thresholds the image below a certain value to the threshold + * so: outMat(i,j) = inMat(i,j) if inMat(i,j)>=threshold + * = threshold otherwise + * + * \param inMat input matrix + * \param outMat output matrix + * \param threshold threshold value + * + */ +void mcvThresholdLower(const CvMat *inMat, CvMat *outMat, FLOAT threshold) +{ + +#define MCV_THRESHOLD_LOWER(type) \ + for (int i=0; iheight; i++) \ + for (int j=0; jwidth; j++) \ + if ( CV_MAT_ELEM(*inMat, type, i, j)type)==FLOAT_MAT_TYPE) + { + MCV_THRESHOLD_LOWER(FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==INT_MAT_TYPE) + { + MCV_THRESHOLD_LOWER(INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetVectorMax\n"; + exit(1); + } +} + +/** This function detects stop lines in the input image using IPM + * transformation and the input camera parameters. The returned lines + * are in a vector of Line objects, having start and end point in + * input image frame. + * + * \param image the input image + * \param stopLines a vector of returned stop lines in input image coordinates + * \param linescores a vector of line scores returned + * \param cameraInfo the camera parameters + * \param stopLineConf parameters for stop line detection + * + * + */ +void mcvGetStopLines(const CvMat *inImage, vector *stopLines, + vector *lineScores, const CameraInfo *cameraInfo, + LaneDetectorConf *stopLineConf) + +{ + //input size + CvSize inSize = cvSize(inImage->width, inImage->height); + + //TODO: smooth image + CvMat *image = cvCloneMat(inImage); + //cvSmooth(image, image, CV_GAUSSIAN, 5, 5, 1, 1); + + IPMInfo ipmInfo; + +// //get the IPM size such that we have height of the stop line +// //is 3 pixels +// double ipmWidth, ipmHeight; +// mcvGetIPMExtent(cameraInfo, &ipmInfo); +// ipmHeight = 3*(ipmInfo.yLimits[1]-ipmInfo.yLimits[0]) / (stopLineConf->lineHeight/3.); +// ipmWidth = ipmHeight * 4/3; +// //put into the conf +// stopLineConf->ipmWidth = int(ipmWidth); +// stopLineConf->ipmHeight = int(ipmHeight); + +// if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES +// cout << "IPM width:" << stopLineConf->ipmWidth << " IPM height:" +// << stopLineConf->ipmHeight << "\n"; +// }//#endif + + + //Get IPM + CvSize ipmSize = cvSize((int)stopLineConf->ipmWidth, + (int)stopLineConf->ipmHeight); + CvMat * ipm; + ipm = cvCreateMat(ipmSize.height, ipmSize.width, inImage->type); + //mcvGetIPM(inImage, ipm, &ipmInfo, cameraInfo); + ipmInfo.vpPortion = stopLineConf->ipmVpPortion; + ipmInfo.ipmLeft = stopLineConf->ipmLeft; + ipmInfo.ipmRight = stopLineConf->ipmRight; + ipmInfo.ipmTop = stopLineConf->ipmTop; + ipmInfo.ipmBottom = stopLineConf->ipmBottom; + ipmInfo.ipmInterpolation = stopLineConf->ipmInterpolation; + list outPixels; + list::iterator outPixelsi; + mcvGetIPM(image, ipm, &ipmInfo, cameraInfo, &outPixels); + + //smooth the IPM + //cvSmooth(ipm, ipm, CV_GAUSSIAN, 5, 5, 1, 1); + + //debugging + CvMat *dbIpmImage; + if(DEBUG_LINES) {// #ifdef DEBUG_GET_STOP_LINES + dbIpmImage = cvCreateMat(ipm->height, ipm->width, ipm->type); + cvCopy(ipm, dbIpmImage); + }//#endif + + + //compute stop line width: 2000 mm + FLOAT stopLinePixelWidth = stopLineConf->lineWidth * ipmInfo.xScale; + //stop line pixel height: 12 inches = 12*25.4 mm + FLOAT stopLinePixelHeight = stopLineConf->lineHeight * ipmInfo.yScale; + //kernel dimensions + //unsigned char wx = 2; + //unsigned char wy = 2; + FLOAT sigmax = stopLinePixelWidth; + FLOAT sigmay = stopLinePixelHeight; + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //cout << "Line width:" << stopLinePixelWidth << "Line height:" + // << stopLinePixelHeight << "\n"; + }//#endif + + //filter the IPM image + mcvFilterLines(ipm, ipm, stopLineConf->kernelWidth, + stopLineConf->kernelHeight, sigmax, sigmay, + LINE_HORIZONTAL); + + //zero out points outside the image in IPM view + for(outPixelsi=outPixels.begin(); outPixelsi!=outPixels.end(); outPixelsi++) + CV_MAT_ELEM(*ipm, float, (*outPixelsi).y, (*outPixelsi).x) = 0; + outPixels.clear(); + + //zero out negative values + mcvThresholdLower(ipm, ipm, 0); + + //compute quantile: .985 + FLOAT qtileThreshold = mcvGetQuantile(ipm, stopLineConf->lowerQuantile); + mcvThresholdLower(ipm, ipm, qtileThreshold); + + //debugging + CvMat *dbIpmImageThresholded; + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + dbIpmImageThresholded = cvCreateMat(ipm->height, ipm->width, ipm->type); + cvCopy(ipm, dbIpmImageThresholded); + }//#endif + + + + //group stop lines + switch(stopLineConf->groupingType) + { + //use HV grouping + case GROUPING_TYPE_HV_LINES: + //vector ipmStopLines; + //vector lineScores; + mcvGetHVLines(ipm, stopLines, lineScores, LINE_HORIZONTAL, + stopLinePixelHeight, stopLineConf->binarize, + stopLineConf->localMaxima, stopLineConf->detectionThreshold, + stopLineConf->smoothScores); + break; + + //use Hough Transform grouping + case GROUPING_TYPE_HOUGH_LINES: + //FLOAT rMin = 0.05*ipm->height, rMax = 0.4*ipm->height, rStep = 1; + //FLOAT thetaMin = 88*CV_PI/180, thetaMax = 92*CV_PI/180, thetaStep = 1*CV_PI/180; + //bool group = false; FLOAT groupThreshold = 1; + mcvGetHoughTransformLines(ipm, stopLines, lineScores, + stopLineConf->rMin, stopLineConf->rMax, + stopLineConf->rStep, stopLineConf->thetaMin, + stopLineConf->thetaMax, stopLineConf->thetaStep, + stopLineConf->binarize, stopLineConf->localMaxima, + stopLineConf->detectionThreshold, + stopLineConf->smoothScores, stopLineConf->group, + stopLineConf->groupThreshold); + break; + } + + //get RANSAC lines + if (stopLineConf->ransac) + { + mcvGetRansacLines(ipm, *stopLines, *lineScores, stopLineConf, LINE_HORIZONTAL); + } + + if(stopLineConf->getEndPoints) + { + //get line extent in IPM image + for(int i=0; i<(int)stopLines->size(); i++) + mcvGetLineExtent(ipm, (*stopLines)[i], (*stopLines)[i]); + } + + vector dbIpmStopLines; + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + dbIpmStopLines = *stopLines; +// //print out lineScores +// cout << "LineScores:"; +// //for (int i=0; i<(int)lineScores->size(); i++) +// for (int i=0; i<1 && lineScores->size()>0; i++) +// cout << (*lineScores)[i] << " "; +// cout << "\n"; + }//#endif + + //check if returned anything + if (stopLines->size()!=0) + { + //convert the line into world frame + for (unsigned int i=0; isize(); i++) + { + Line *line; + line = &(*stopLines)[i]; + + mcvPointImIPM2World(&(line->startPoint), &ipmInfo); + mcvPointImIPM2World(&(line->endPoint), &ipmInfo); + } + //convert them from world frame into camera frame + // + //put a dummy line at the beginning till we check that cvDiv bug + Line dummy = {{1.,1.},{2.,2.}}; + stopLines->insert(stopLines->begin(), dummy); + //convert to mat and get in image coordinates + CvMat *mat = cvCreateMat(2, 2*stopLines->size(), FLOAT_MAT_TYPE); + mcvLines2Mat(stopLines, mat); + stopLines->clear(); + mcvTransformGround2Image(mat, mat, cameraInfo); + //get back to vector + mcvMat2Lines(mat, stopLines); + //remove the dummy line at the beginning + stopLines->erase(stopLines->begin()); + //clear + cvReleaseMat(&mat); + + //clip the lines found and get their extent + for (unsigned int i=0; isize(); i++) + { + //clip + mcvIntersectLineWithBB(&(*stopLines)[i], inSize, &(*stopLines)[i]); + + //get the line extent + //mcvGetLineExtent(inImage, (*stopLines)[i], (*stopLines)[i]); + } + + + } + + //debugging + if(DEBUG_LINES) + {//#ifdef DEBUG_GET_STOP_LINES + //show the IPM image + SHOW_IMAGE(dbIpmImage, "IPM image", 10); + //thresholded ipm + SHOW_IMAGE(dbIpmImageThresholded, "Stoplines thresholded IPM image", 10); + //draw lines in IPM image + //for (int i=0; i<(int)dbIpmStopLines.size(); i++) + for (int i=0; i<1 && dbIpmStopLines.size()>0; i++) + { + mcvDrawLine(dbIpmImage, dbIpmStopLines[i], CV_RGB(0,0,0), 3); + } + SHOW_IMAGE(dbIpmImage, "Stoplines IPM with lines", 10); + //draw lines on original image + //CvMat *image = cvCreateMat(inImage->height, inImage->width, CV_32FC3); + //cvCvtColor(inImage, image, CV_GRAY2RGB); + //CvMat *image = cvCloneMat(inImage); + //for (int i=0; i<(int)stopLines->size(); i++) + for (int i=0; i<1 && stopLines->size()>0; i++) + { + //SHOW_POINT((*stopLines)[i].startPoint, "start"); + //SHOW_POINT((*stopLines)[i].endPoint, "end"); + mcvDrawLine(image, (*stopLines)[i], CV_RGB(255,0,0), 3); + } + SHOW_IMAGE(image, "Detected Stoplines", 10); + //cvReleaseMat(&image); + cvReleaseMat(&dbIpmImage); + cvReleaseMat(&dbIpmImageThresholded); + dbIpmStopLines.clear(); + }//#endif //DEBUG_GET_STOP_LINES + + //clear + cvReleaseMat(&ipm); + cvReleaseMat(&image); + //ipmStopLines.clear(); +} + +/** This function detects lanes in the input image using IPM + * transformation and the input camera parameters. The returned lines + * are in a vector of Line objects, having start and end point in + * input image frame. + * + * \param image the input image + * \param lanes a vector of returned stop lines in input image coordinates + * \param linescores a vector of line scores returned + * \param cameraInfo the camera parameters + * \param stopLineConf parameters for stop line detection + * \param state returns the current state and inputs the previous state to + * initialize the current detection (NULL to ignore) + * + * + */ +void mcvGetLanes(const CvMat *inImage, const CvMat* clrImage, + vector *lanes, vector *lineScores, + vector *splines, vector *splineScores, + CameraInfo *cameraInfo, LaneDetectorConf *stopLineConf, + LineState* state) +{ + //input size + CvSize inSize = cvSize(inImage->width, inImage->height); + + //TODO: smooth image + CvMat *image = cvCloneMat(inImage); + //cvSmooth(image, image, CV_GAUSSIAN, 5, 5, 1, 1); + + //SHOW_IMAGE(image, "Input image", 10); + + IPMInfo ipmInfo; + + //state: create a new structure, and put pointer to it if it's null + LineState newState; + if(!state) state = &newState; + +// //get the IPM size such that we have height of the stop line +// //is 3 pixels +// double ipmWidth, ipmHeight; +// mcvGetIPMExtent(cameraInfo, &ipmInfo); +// ipmHeight = 3*(ipmInfo.yLimits[1]-ipmInfo.yLimits[0]) / (stopLineConf->lineHeight/3.); +// ipmWidth = ipmHeight * 4/3; +// //put into the conf +// stopLineConf->ipmWidth = int(ipmWidth); +// stopLineConf->ipmHeight = int(ipmHeight); + +// #ifdef DEBUG_GET_STOP_LINES +// cout << "IPM width:" << stopLineConf->ipmWidth << " IPM height:" +// << stopLineConf->ipmHeight << "\n"; +// #endif + + + //Get IPM + CvSize ipmSize = cvSize((int)stopLineConf->ipmWidth, + (int)stopLineConf->ipmHeight); + CvMat * ipm; + ipm = cvCreateMat(ipmSize.height, ipmSize.width, inImage->type); + //mcvGetIPM(inImage, ipm, &ipmInfo, cameraInfo); + ipmInfo.vpPortion = stopLineConf->ipmVpPortion; + ipmInfo.ipmLeft = stopLineConf->ipmLeft; + ipmInfo.ipmRight = stopLineConf->ipmRight; + ipmInfo.ipmTop = stopLineConf->ipmTop; + ipmInfo.ipmBottom = stopLineConf->ipmBottom; + ipmInfo.ipmInterpolation = stopLineConf->ipmInterpolation; + list outPixels; + list::iterator outPixelsi; + mcvGetIPM(image, ipm, &ipmInfo, cameraInfo, &outPixels); + + //smooth the IPM image with 5x5 gaussian filter +#warning "Check: Smoothing IPM image" + //cvSmooth(ipm, ipm, CV_GAUSSIAN, 3); + // SHOW_MAT(ipm, "ipm"); + + // //subtract mean + // CvScalar mean = cvAvg(ipm); + // cvSubS(ipm, mean, ipm); + + //keep copy + CvMat* rawipm = cvCloneMat(ipm); + + //smooth the IPM + //cvSmooth(ipm, ipm, CV_GAUSSIAN, 5, 5, 1, 1); + + //debugging + CvMat *dbIpmImage; + if(DEBUG_LINES) + {//#ifdef DEBUG_GET_STOP_LINES + dbIpmImage = cvCreateMat(ipm->height, ipm->width, ipm->type); + cvCopy(ipm, dbIpmImage); + //show the IPM image + SHOW_IMAGE(dbIpmImage, "IPM image", 10); + }//#endif + + //compute stop line width: 2000 mm + FLOAT stopLinePixelWidth = stopLineConf->lineWidth * + ipmInfo.xScale; + //stop line pixel height: 12 inches = 12*25.4 mm + FLOAT stopLinePixelHeight = stopLineConf->lineHeight * + ipmInfo.yScale; + //kernel dimensions + //unsigned char wx = 2; + //unsigned char wy = 2; + FLOAT sigmax = stopLinePixelWidth; + FLOAT sigmay = stopLinePixelHeight; + +// //filter in the horizontal direction +// CvMat * ipmt = cvCreateMat(ipm->width, ipm->height, ipm->type); +// cvTranspose(ipm, ipmt); +// mcvFilterLines(ipmt, ipmt, stopLineConf->kernelWidth, +// stopLineConf->kernelHeight, sigmax, sigmay, +// LINE_VERTICAL); +// //retranspose +// CvMat *ipm2 = cvCreateMat(ipm->height, ipm->width, ipm->type); +// cvTranspose(ipmt, ipm2); +// cvReleaseMat(&ipmt); + + //filter the IPM image + mcvFilterLines(ipm, ipm, stopLineConf->kernelWidth, + stopLineConf->kernelHeight, sigmax, sigmay, + LINE_VERTICAL); +// mcvFilterLines(ipm, ipm, stopLineConf->kernelWidth, +// stopLineConf->kernelHeight, sigmax, sigmay, +// LINE_VERTICAL); + + //zero out points outside the image in IPM view + for(outPixelsi=outPixels.begin(); outPixelsi!=outPixels.end(); outPixelsi++) + { + CV_MAT_ELEM(*ipm, float, (*outPixelsi).y, (*outPixelsi).x) = 0; + // CV_MAT_ELEM(*ipm2, float, (*outPixelsi).y, (*outPixelsi).x) = 0; + } + outPixels.clear(); + +#warning "Check this clearing of IPM image for 2 lanes" + if (stopLineConf->ipmWindowClear) + { + //check to blank out other periferi of the image + //blank from 60->100 (width 40) + CvRect mask = cvRect(stopLineConf->ipmWindowLeft, 0, + stopLineConf->ipmWindowRight - + stopLineConf->ipmWindowLeft + 1, + ipm->height); + mcvSetMat(ipm, mask, 0); + } + + //show filtered image + if (DEBUG_LINES) { + SHOW_IMAGE(ipm, "Lane unthresholded filtered", 10); + } + + //take the negative to get double yellow lines + //cvScale(ipm, ipm, -1); + + CvMat *fipm = cvCloneMat(ipm); + + //zero out negative values +// SHOW_MAT(fipm, "fipm"); +#warning "clean negative parts in filtered image" + mcvThresholdLower(ipm, ipm, 0); +// mcvThresholdLower(ipm2, ipm2, 0); + +// //add the two images +// cvAdd(ipm, ipm2, ipm); + +// //clear the horizontal filtered image +// cvReleaseMat(&ipm2); + + //fipm was here + //make copy of filteed ipm image + + vector dbIpmStopLines; + vector dbIpmSplines; + + //int numStrips = 2; + int stripHeight = ipm->height / stopLineConf->numStrips; + for (int i=0; inumStrips; i++) //lines + { + //get the mask + CvRect mask; + mask = cvRect(0, i*stripHeight, ipm->width, + stripHeight); + // SHOW_RECT(mask, "Mask"); + + //get the subimage to work on + CvMat *subimage = cvCloneMat(ipm); + //clear all but the mask + mcvSetMat(subimage, mask, 0); + + //compute quantile: .985 + FLOAT qtileThreshold = mcvGetQuantile(subimage, stopLineConf->lowerQuantile); + mcvThresholdLower(subimage, subimage, qtileThreshold); + // FLOAT qtileThreshold = mcvGetQuantile(ipm, stopLineConf->lowerQuantile); + // mcvThresholdLower(ipm, ipm, qtileThreshold); + + // qtileThreshold = mcvGetQuantile(ipm2, stopLineConf->lowerQuantile); + // mcvThresholdLower(ipm2, ipm2, qtileThreshold); + + //and fipm was here last + // //make copy of filtered ipm image + // CvMat *fipm = cvCloneMat(ipm); + vector subimageLines; + vector subimageSplines; + vector subimageLineScores, subimageSplineScores; + + //check to blank out other periferi of the image +// mask = cvRect(40, 0, 80, subimage->height); +// mcvSetMat(subimage, mask, 0); + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + CvMat *dbIpmImageThresholded; + dbIpmImageThresholded = cvCreateMat(ipm->height, ipm->width, ipm->type); + cvCopy(subimage, dbIpmImageThresholded); //ipm + char str[256]; + sprintf(str, "Lanes #%d thresholded IPM", i); + //thresholded ipm + SHOW_IMAGE(dbIpmImageThresholded, str, 10); + cvReleaseMat(&dbIpmImageThresholded); + } + + //get the lines/splines + mcvGetLines(subimage, LINE_VERTICAL, subimageLines, subimageLineScores, + subimageSplines, subimageSplineScores, stopLineConf, + state); +// mcvGetLines(ipm, LINE_VERTICAL, *lanes, *lineScores, +// *splines, *splineScores, stopLineConf, +// state); + //put back + for (unsigned int k=0; kpush_back(subimageLines[k]); + lineScores->push_back(subimageLineScores[k]); + } + for (unsigned int k=0; kpush_back(subimageSplines[k]); + splineScores->push_back(subimageSplineScores[k]); + } + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + for (unsigned int k=0; ksize(); k++) + dbIpmStopLines.push_back((*lanes)[k]); + for (unsigned int k=0; ksize(); k++) + dbIpmSplines.push_back((*splines)[k]); + + CvMat *dbIpmImageThresholded; + dbIpmImageThresholded = cvCreateMat(ipm->height, ipm->width, ipm->type); + cvCopy(subimage, dbIpmImageThresholded); //ipm + char str[256]; + sprintf(str, "Lanes #%d thresholded IPM", i); + //thresholded ipm + SHOW_IMAGE(dbIpmImageThresholded, str, 10); + cvReleaseMat(&dbIpmImageThresholded); + + //dbIpmStopLines = *lanes; + //dbIpmSplines = *splines; + // //print out lineScores + // cout << "LineScores:"; + // //for (int i=0; i<(int)lineScores->size(); i++) + // for (int i=0; i<(int)lineScores->size(); i++) + // cout << (*lineScores)[i] << " "; + // cout << "\n"; + }//#endif + + //release + cvReleaseMat(&subimage); + } + + //postprocess lines/splines //rawipm + mcvPostprocessLines(image, clrImage, fipm, ipm, *lanes, *lineScores, + *splines, *splineScores, + stopLineConf, state, ipmInfo, *cameraInfo); //rawipm + + if (DEBUG_LINES) { + dbIpmSplines = state->ipmSplines; + } + + //debugging + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + char str[256]; + //draw lines in IPM image + //for (int i=0; i<(int)dbIpmStopLines.size(); i++) + if (stopLineConf->ransacLine || !stopLineConf->ransacSpline) + for (int i=0; i<(int)dbIpmStopLines.size(); i++) + mcvDrawLine(dbIpmImage, dbIpmStopLines[i], CV_RGB(0,0,0), 1); + if (stopLineConf->ransacSpline) + for (int i=0; i<(int)dbIpmSplines.size(); i++) + mcvDrawSpline(dbIpmImage, dbIpmSplines[i], CV_RGB(0,0,0), 1); + + SHOW_IMAGE(dbIpmImage, "Lanes IPM with lines", 10); + //draw lines on original image + CvMat *imageClr = cvCreateMat(inImage->height, inImage->width, CV_32FC3); + cvCvtColor(image, imageClr, CV_GRAY2RGB); + //CvMat *image = cvCloneMat(inImage); + //for (int i=0; i<(int)stopLines->size(); i++) + if (stopLineConf->ransacLine || !stopLineConf->ransacSpline) + for (int i=0; i<(int)lanes->size(); i++) + mcvDrawLine(imageClr, (*lanes)[i], CV_RGB(255,0,0), 1); + if (stopLineConf->ransacSpline) + for (int i=0; i<(int)splines->size(); i++) + { + mcvDrawSpline(imageClr, (*splines)[i], CV_RGB(255,0,0), 1); + sprintf(str, "%.2f", (*splineScores)[i]); + mcvDrawText(imageClr, str, + cvPointFrom32f((*splines)[i].points[(*splines)[i].degree]), + .5, CV_RGB(0, 0, 255)); + } + + SHOW_IMAGE(imageClr, "Detected lanes", 0); + //cvReleaseMat(&image); + cvReleaseMat(&dbIpmImage); + //cvReleaseMat(&dbIpmImageThresholded); + cvReleaseMat(&imageClr); + dbIpmStopLines.clear(); + dbIpmSplines.clear(); + }//#endif //DEBUG_GET_STOP_LINES + + //clear + cvReleaseMat(&ipm); + cvReleaseMat(&image); + cvReleaseMat(&fipm); + cvReleaseMat(&rawipm); + //ipmStopLines.clear(); +} + + +/** This function postprocesses the detected lines/splines to better localize + * and extend them + * + * \param image the input image + * \param clrImage the inpout color image + * \param rawipm the raw ipm image + * \param fipm the filtered ipm iamge + * \param lines a vector of lines + * \param lineScores the line scores + * \param splines a vector of returned splines + * \param splineScores the spline scores + * \param lineConf the conf structure + * \param state the state for RANSAC splines + * \param ipmInfo the ipmInfo structure + * \param cameraInfo the camera info structure + * + */ +void mcvPostprocessLines(const CvMat* image, const CvMat* clrImage, + const CvMat* rawipm, const CvMat* fipm, + vector &lines, vector &lineScores, + vector &splines, vector &splineScores, + LaneDetectorConf *lineConf, LineState *state, + IPMInfo &ipmInfo, CameraInfo &cameraInfo) +{ + CvSize inSize = cvSize(image->width-1, image->height-1); + + //vector of splines to keep + vector keepSplines; + vector keepSplineScores; + +// //get line extent +// if(lineConf->getEndPoints) +// { +// //get line extent in IPM image +// for(int i=0; i<(int)lines.size(); i++) +// mcvGetLineExtent(rawipm, lines[i], lines[i]); +// } + + //if return straight lines + if (lineConf->ransacLine || !lineConf->ransacSpline) + { + mcvLinesImIPM2Im(lines, ipmInfo, cameraInfo, inSize); + } + //return spline + if (lineConf->ransacSpline) + { + //localize splines + for(int i=0; i<(int)splines.size(); i++) + { + //get spline status + int splineStatus = lineConf->checkIPMSplines ? + mcvCheckSpline(splines[i], + lineConf->checkIPMSplinesCurvenessThreshold, + lineConf->checkIPMSplinesLengthThreshold, + lineConf->checkIPMSplinesThetaDiffThreshold, + lineConf->checkIPMSplinesThetaThreshold) + : 0; + + //check it + if (!(((splineStatus & CurvedSpline) && (splineStatus & CurvedSplineTheta)) + || splineStatus & HorizontalSpline)) + { + //better localize points + CvMat *points = mcvEvalBezierSpline(splines[i], .1); //.05 + //mcvLocalizePoints(ipm, points, points); //inImage + //extend spline + CvMat* p = mcvExtendPoints(rawipm, points, + lineConf->extendIPMAngleThreshold, + lineConf->extendIPMMeanDirAngleThreshold, + lineConf->extendIPMLinePixelsTangent, + lineConf->extendIPMLinePixelsNormal, + lineConf->extendIPMContThreshold, + lineConf->extendIPMDeviationThreshold, + cvRect(0, lineConf->extendIPMRectTop, + rawipm->width-1, + lineConf->extendIPMRectBottom-lineConf->extendIPMRectTop), + false); + //refit spline + Spline spline = mcvFitBezierSpline(p, lineConf->ransacSplineDegree); + + //save +#warning "Check this later: extension in IPM. Check threshold value" +// splines[i] = spline; + + //calculate the score from fipm or ipm (thresholded) + //float lengthRatio = 0.5; //.8 + //float angleRatio = 0.8; //.4 + //vector jitter = mcvGetJitterVector(lineConf->splineScoreJitter);//2); + + float score = mcvGetSplineScore(fipm, splines[i], + lineConf->splineScoreStep, //.1 + lineConf->splineScoreJitter, //jitter, + lineConf->splineScoreLengthRatio, + lineConf->splineScoreAngleRatio); + //jitter.clear(); + splineScores[i] = score; + + //check score + if (splineScores[i] >= lineConf->finalSplineScoreThreshold) + { + keepSplines.push_back(spline); + keepSplineScores.push_back(splineScores[i]); + } + //clear + cvReleaseMat(&points); + cvReleaseMat(&p); + } //if + } //for + + //put back + splines.clear(); + splineScores.clear(); + splines = keepSplines; + splineScores = keepSplineScores; + keepSplines.clear(); + keepSplineScores.clear(); + +// if (DEBUG_LINES) { +// dbIpmSplines = *splines; +// } + + //save state + //save IPM splines to use in next frame + state->ipmSplines.clear(); + state->ipmSplines = splines; + + //convert to image coordinates + mcvSplinesImIPM2Im(splines, ipmInfo, cameraInfo, inSize); + + + // fprintf(stderr, "start of splines------------------------\n"); + // for (unsigned int i=0; icheckSplines ? + mcvCheckSpline(splines[i], + lineConf->checkSplinesCurvenessThreshold, + lineConf->checkSplinesLengthThreshold, + lineConf->checkSplinesThetaDiffThreshold, + lineConf->checkSplinesThetaThreshold) + : 0; + //check if short, then put the corresponding line instead + if (splineStatus & (ShortSpline|CurvedSpline)) + { + for (unsigned int j=0; jransacSplineDegree); + //check if that merges with the current one + if (mcvCheckMergeSplines(splines[i], sp, .4, 50, .4, 50, 50)) + { + //put the spline + splines[i] = sp; + splineStatus = 0; + break; + } + } + //splines[i] = mcvLineXY2Spline(lines[i], lineConf->ransacSplineDegree); + } + if (!(splineStatus & (CurvedSpline|CurvedSplineTheta))) + { + //better localize points + CvMat *points = mcvEvalBezierSpline(splines[i], .05); + // CvMat *points = mcvGetBezierSplinePixels((*splines)[i], .05, + // cvSize(inImage->width-1, + // inImage->height-1), + // true); + // CvMat *p = cvCreateMat(points->height, points->width, CV_32FC1); + // cvConvert(points, p); + mcvLocalizePoints(image, points, points, lineConf->localizeNumLinePixels, + lineConf->localizeAngleThreshold); //inImage + + //get color + CvMat* clrPoints = points; + + //extend spline + CvMat* p = mcvExtendPoints(image, points, + lineConf->extendAngleThreshold, + lineConf->extendMeanDirAngleThreshold, + lineConf->extendLinePixelsTangent, + lineConf->extendLinePixelsNormal, + lineConf->extendContThreshold, + lineConf->extendDeviationThreshold, + cvRect(0, lineConf->extendRectTop, + image->width, + lineConf->extendRectBottom-lineConf->extendRectTop)); + + //refit + Spline spline = mcvFitBezierSpline(p, lineConf->ransacSplineDegree); + //splines[i] = spline; + clrPoints = p; + + //check the extended one + if (lineConf->checkSplines && + (mcvCheckSpline(spline, + lineConf->checkSplinesCurvenessThreshold, + lineConf->checkSplinesLengthThreshold, + lineConf->checkSplinesThetaDiffThreshold, + lineConf->checkSplinesThetaThreshold) + & CurvedSpline)) + { + //rfit using points before extending + spline = mcvFitBezierSpline(points, lineConf->ransacSplineDegree); + clrPoints = points; + + //check again + if (mcvCheckSpline(spline, + lineConf->checkSplinesCurvenessThreshold, + lineConf->checkSplinesLengthThreshold, + lineConf->checkSplinesThetaDiffThreshold, + lineConf->checkSplinesThetaThreshold) + & CurvedSpline) + //use spline before localization + spline = splines[i]; + } + + //get color +// fprintf(stderr, "Color for spline %d\n", i); + LineColor clr = lineConf->checkColor ? + mcvGetPointsColor(clrImage, clrPoints, + lineConf->checkColorWindow, + lineConf->checkColorNumYellowMin, + lineConf->checkColorRGMin, + lineConf->checkColorRGMax, + lineConf->checkColorGBMin, + lineConf->checkColorRBMin, + lineConf->checkColorRBF, + lineConf->checkColorRBFThreshold) + : LINE_COLOR_WHITE; + + //clear + cvReleaseMat(&points); + cvReleaseMat(&p); + + //put it + spline.color = clr; + keepSplines.push_back(spline); + keepSplineScores.push_back(splineScores[i]); + } //if + } //for + + //put them back + splines.clear(); + splineScores.clear(); + splines = keepSplines; + splineScores = keepSplineScores; + keepSplines.clear(); + keepSplineScores.clear(); + +// fprintf(stderr, "start of splines------------------------\n"); +// for (unsigned int i=0; i &lines, vector &lineScores, + vector &splines, vector &splineScores, + LaneDetectorConf *lineConf, LineState *state) +{ + + //initial grouping of lines + switch(lineConf->groupingType) + { + //use HV grouping + case GROUPING_TYPE_HV_LINES: + //vector ipmStopLines; + //vector lineScores; + mcvGetHVLines(image, &lines, &lineScores, lineType, + 6, //stopLinePixelHeight, + lineConf->binarize, lineConf->localMaxima, + lineConf->detectionThreshold, + lineConf->smoothScores); + break; + + //use Hough Transform grouping + case GROUPING_TYPE_HOUGH_LINES: + //FLOAT rMin = 0.05*ipm->height, rMax = 0.4*ipm->height, rStep = 1; + //FLOAT thetaMin = 88*CV_PI/180, thetaMax = 92*CV_PI/180, thetaStep = 1*CV_PI/180; + // bool group = true; FLOAT groupThreshold = 15; + mcvGetHoughTransformLines(image, &lines, &lineScores, + lineConf->rMin, lineConf->rMax, + lineConf->rStep, lineConf->thetaMin, + lineConf->thetaMax, lineConf->thetaStep, + lineConf->binarize, lineConf->localMaxima, + lineConf->detectionThreshold, + lineConf->smoothScores, + lineConf->group, lineConf->groupThreshold); + + break; + } + + //get only two lines if in this mode +// if (lineConf->group) +// mcvGroupLines(lines, lineScores, +// lineConf->groupThreshold, +// cvSize(image->width, image->height)); + if (lineConf->checkLaneWidth) + mcvCheckLaneWidth(lines, lineScores, + lineConf->checkLaneWidthMean, + lineConf->checkLaneWidthStd); //70&20 65&10 25&10 + + //check if to do RANSAC + if (lineConf->ransac) + { + //do RANSAC lines? + if (lineConf->ransacLine) + mcvGetRansacLines(image, lines, lineScores, lineConf, lineType); + + //do RANSAC splines? + if (lineConf->ransacSpline) + mcvGetRansacSplines(image, lines, lineScores, + lineConf, lineType, splines, splineScores, state); + } + + //get bounding boxes around returned splines to pass to the next + //frame + state->ipmBoxes.clear(); + mcvGetSplinesBoundingBoxes(splines, lineType, + cvSize(image->width, image->height), + state->ipmBoxes); +} + +/** This function makes some checks on splines and decides + * whether to keep them or not + * + * \param spline the input spline + * \param curvenessThreshold minimum curveness score it should have + * \param lengthThreshold mimimum threshold it should have + * \param thetaDiffThreshold max theta diff it should have + * \param thetaThreshold max theta it should have to be considered horizontal + * + * \return code that determines what to do with the spline + * + */ +int mcvCheckSpline(const Spline &spline, float curvenessThreshold, + float lengthThreshold, float thetaDiffThreshold, + float thetaThreshold) +{ + + //get the spline features + float theta, r, meanTheta, meanR, length, curveness; + mcvGetSplineFeatures(spline, 0, &theta, &r, &length, + &meanTheta, &meanR, &curveness); + float thetaDiff = fabs(meanTheta - theta); +// thetaDiff = thetaDiff>CV_PI ? thetaDiff-CV_PI : thetaDiff; + +// float curvenessThreshold = .92; //.85; +// float lengthThreshold = 30; +// float thetaDiffThreshold = .1; +// float thetaThreshold = 70. * CV_PI/180; + + + char check = 0; + if (curveness thetaDiffThreshold) + check |= CurvedSplineTheta; + if (lengththetaThreshold) + check |= HorizontalSpline; + + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + fprintf(stderr, "thetaDiffThreshold=%f\n", thetaDiffThreshold); + fprintf(stderr, "%s: curveness=%f, length=%f, thetaDiff=%f, meanTheta=%f, theta=%f\n", + check & (ShortSpline|CurvedSpline|HorizontalSpline) + ? "YES" : "NO ", curveness, length, thetaDiff, meanTheta, theta); + fprintf(stderr, "\t%s\t%s\t%s\t%s\n", + check&CurvedSpline? "curved" : "not curved", + check&CurvedSplineTheta? "curved theta" : "not curved theta", + check&ShortSpline? "short" : "not short", + check&HorizontalSpline? "horiz" : "not horiz"); + + CvMat* im = cvCreateMat(480, 640, CV_8UC3); + cvSet(im, cvRealScalar(0.)); + //draw splines + mcvDrawSpline(im, spline, CV_RGB(255, 0, 0), 1); + SHOW_IMAGE(im, "Check Splines", 10); + //clear + cvReleaseMat(&im); + }//#endif + + return check; +} + +/** This function makes some checks on points and decides + * whether to keep them or not + * + * \param points the array of points to check + * + * \return code that determines what to do with the points + * + */ +int mcvCheckPoints(const CvMat* points) +{ + + //get the spline features + float theta, r, meanTheta, meanR, curveness; //length + mcvGetPointsFeatures(points, 0, &theta, &r, 0, + &meanTheta, &meanR, &curveness); + float thetaDiff = fabs(meanTheta - theta); + + float curvenessThreshold = .8; + float thetaDiffThreshold = .3; + + int check = 0; + if (curveness thetaDiffThreshold) + check |= CurvedSplineTheta; + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + fprintf(stderr, "%s: curveness=%f, thetaDiff=%f, meanTheta=%f\n", + check & (ShortSpline|CurvedSpline|HorizontalSpline) + ? "YES" : "NO ", curveness, thetaDiff, meanTheta); + + CvMat* im = cvCreateMat(480, 640, CV_8UC3); + cvSet(im, cvRealScalar(0.)); + //draw splines + for (int i=0; iheight-1; i++) + { + Line line; + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i, 0), + CV_MAT_ELEM(*points, float, i, 1)); + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i+1, 0), + CV_MAT_ELEM(*points, float, i+1, 1)); + mcvDrawLine(im, line, CV_RGB(255, 0, 0), 1); + } + SHOW_IMAGE(im, "Check Points", 0); + //clear + cvReleaseMat(&im); + }//#endif + + return check; +} + + +/** This function converts an array of lines to a matrix (already allocated) + * + * \param lines input vector of lines + * \param size number of lines to convert + * \return the converted matrix, it has 2x2*size where size is the + * number of lines, first row is x values (start.x, end.x) and second + * row is y-values + * + * + */ +void mcvLines2Mat(const vector *lines, CvMat *mat) +{ + //allocate the matrix + //*mat = cvCreateMat(2, size*2, FLOAT_MAT_TYPE); + + //loop and put values + int j; + for (int i=0; i<(int)lines->size(); i++) + { + j = 2*i; + CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 0, j) = (*lines)[i].startPoint.x; + CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 1, j) = (*lines)[i].startPoint.y; + CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 0, j+1) = (*lines)[i].endPoint.x; + CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 1, j+1) = (*lines)[i].endPoint.y; + } +} + + +/** This function converts matrix into n array of lines + * + * \param mat input matrix , it has 2x2*size where size is the + * number of lines, first row is x values (start.x, end.x) and second + * row is y-values + * \param lines the rerurned vector of lines + * + * + */ +void mcvMat2Lines(const CvMat *mat, vector *lines) +{ + + Line line; + //loop and put values + for (int i=0; iwidth/2); i++) + { + int j = 2*i; + //get the line + line.startPoint.x = CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 0, j); + line.startPoint.y = CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 1, j); + line.endPoint.x = CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 0, j+1); + line.endPoint.y = CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 1, j+1); + //push it + lines->push_back(line); + } +} + + + +/** This function intersects the input line with the given bounding box + * + * \param inLine the input line + * \param bbox the bounding box + * \param outLine the output line + * + */ +void mcvIntersectLineWithBB(const Line *inLine, const CvSize bbox, + Line *outLine) +{ + //put output + outLine->startPoint.x = inLine->startPoint.x; + outLine->startPoint.y = inLine->startPoint.y; + outLine->endPoint.x = inLine->endPoint.x; + outLine->endPoint.y = inLine->endPoint.y; + + //check which points are inside + bool startInside, endInside; + startInside = mcvIsPointInside(inLine->startPoint, bbox); + endInside = mcvIsPointInside(inLine->endPoint, bbox); + + //now check + if (!(startInside && endInside)) + { + //difference + FLOAT deltax, deltay; + deltax = inLine->endPoint.x - inLine->startPoint.x; + deltay = inLine->endPoint.y - inLine->startPoint.y; + //hold parameters + FLOAT t[4]={2,2,2,2}; + FLOAT xup, xdown, yleft, yright; + + //intersect with top and bottom borders: y=0 and y=bbox.height-1 + if (deltay==0) //horizontal line + { + xup = xdown = bbox.width+2; + } + else + { + t[0] = -inLine->startPoint.y/deltay; + xup = inLine->startPoint.x + t[0]*deltax; + t[1] = (bbox.height-inLine->startPoint.y)/deltay; + xdown = inLine->startPoint.x + t[1]*deltax; + } + + //intersect with left and right borders: x=0 and x=bbox.widht-1 + if (deltax==0) //horizontal line + { + yleft = yright = bbox.height+2; + } + else + { + t[2] = -inLine->startPoint.x/deltax; + yleft = inLine->startPoint.y + t[2]*deltay; + t[3] = (bbox.width-inLine->startPoint.x)/deltax; + yright = inLine->startPoint.y + t[3]*deltay; + } + + //points of intersection + FLOAT_POINT2D pts[4] = {{xup, 0},{xdown,bbox.height}, + {0, yleft},{bbox.width, yright}}; + + //now decide which stays and which goes + int i; + if (!startInside) + { + bool cont=true; + for (i=0; i<4 && cont; i++) + { + if (t[i]>=0 && t[i]<=1 && mcvIsPointInside(pts[i],bbox) && + !(pts[i].x == outLine->endPoint.x && + pts[i].y == outLine->endPoint.y) ) + { + outLine->startPoint.x = pts[i].x; + outLine->startPoint.y = pts[i].y; + t[i] = 2; + cont = false; + } + } + //check if not replaced + if(cont) + { + //loop again removing restriction on endpoint this time + for (i=0; i<4 && cont; i++) + { + if (t[i]>=0 && t[i]<=1 && mcvIsPointInside(pts[i],bbox)) + { + outLine->startPoint.x = pts[i].x; + outLine->startPoint.y = pts[i].y; + t[i] = 2; + cont = false; + } + } + } + } + if (!endInside) + { + bool cont=true; + for (i=0; i<4 && cont; i++) + { + if (t[i]>=0 && t[i]<=1 && mcvIsPointInside(pts[i],bbox) && + !(pts[i].x == outLine->startPoint.x && + pts[i].y == outLine->startPoint.y) ) + { + outLine->endPoint.x = pts[i].x; + outLine->endPoint.y = pts[i].y; + t[i] = 2; + cont = false; + } + } + //check if not replaced + if(cont) + { + //loop again removing restriction on endpoint this time + for (i=0; i<4 && cont; i++) + { + if (t[i]>=0 && t[i]<=1 && mcvIsPointInside(pts[i],bbox)) + { + outLine->endPoint.x = pts[i].x; + outLine->endPoint.y = pts[i].y; + t[i] = 2; + cont = false; + } + } + } + } + } +} + + +/** This function intersects the input line (given in r and theta) with + * the given bounding box where the line is represented by: + * x cos(theta) + y sin(theta) = r + * + * \param r the r value for the input line + * \param theta the theta value for the input line + * \param bbox the bounding box + * \param outLine the output line + * + */ +void mcvIntersectLineRThetaWithBB(FLOAT r, FLOAT theta, const CvSize bbox, + Line *outLine) +{ + //hold parameters + double xup, xdown, yleft, yright; + + //intersect with top and bottom borders: y=0 and y=bbox.height-1 + if (cos(theta)==0) //horizontal line + { + xup = xdown = bbox.width+2; + } + else + { + xup = r / cos(theta); + xdown = (r-bbox.height*sin(theta))/cos(theta); + } + + //intersect with left and right borders: x=0 and x=bbox.widht-1 + if (sin(theta)==0) //horizontal line + { + yleft = yright = bbox.height+2; + } + else + { + yleft = r/sin(theta); + yright = (r-bbox.width*cos(theta))/sin(theta); + } + + //points of intersection + FLOAT_POINT2D pts[4] = {{xup, 0},{xdown,bbox.height}, + {0, yleft},{bbox.width, yright}}; + + //get the starting point + int i; + for (i=0; i<4; i++) + { + //if point inside, then put it + if(mcvIsPointInside(pts[i], bbox)) + { + outLine->startPoint.x = pts[i].x; + outLine->startPoint.y = pts[i].y; + //get out of for loop + break; + } + } + //get the ending point + for (i++; i<4; i++) + { + //if point inside, then put it + if(mcvIsPointInside(pts[i], bbox)) + { + outLine->endPoint.x = pts[i].x; + outLine->endPoint.y = pts[i].y; + //get out of for loop + break; + } + } +} + + +/** This function checks if the given point is inside the bounding box + * specified + * + * \param inLine the input line + * \param bbox the bounding box + * \param outLine the output line + * + */ +bool mcvIsPointInside(FLOAT_POINT2D point, CvSize bbox) +{ + return (point.x>=0 && point.x<=bbox.width + && point.y>=0 && point.y<=bbox.height) ? true : false; +} + + +/** This function intersects the input line (given in r and theta) with + * the given rectangle where the line is represented by: + * x cos(theta) + y sin(theta) = r + * + * \param r the r value for the input line + * \param theta the theta value for the input line + * \param rect the input rectangle (given two opposite points in the rectangle, + * upperleft->startPoint and bottomright->endPoint where x->right and y->down) + * \param outLine the output line + * + */ +void mcvIntersectLineRThetaWithRect(FLOAT r, FLOAT theta, const Line &rect, + Line &outLine) +{ + //hold parameters + double xup, xdown, yleft, yright; + + //intersect with top and bottom borders: y=rect->startPoint.y and y=rect->endPoint.y + if (cos(theta)==0) //horizontal line + { + xup = xdown = rect.endPoint.x+2; + } + else + { + xup = (r-rect.startPoint.y*sin(theta)) / cos(theta); + xdown = (r-rect.endPoint.y*sin(theta)) / cos(theta); + } + + //intersect with left and right borders: x=rect->startPoint.x and x=rect->endPoint.x + if (sin(theta)==0) //horizontal line + { + yleft = yright = rect.endPoint.y+2; + } + else + { + yleft = (r-rect.startPoint.x*cos(theta)) / sin(theta); + yright = (r-rect.endPoint.x*cos(theta)) / sin(theta); + } + + //points of intersection + FLOAT_POINT2D pts[4] = {{xup, rect.startPoint.y},{xdown,rect.endPoint.y}, + {rect.startPoint.x, yleft},{rect.endPoint.x, yright}}; + + //get the starting point + int i; + for (i=0; i<4; i++) + { + //if point inside, then put it + if(mcvIsPointInside(pts[i], rect)) + { + outLine.startPoint.x = pts[i].x; + outLine.startPoint.y = pts[i].y; + //get out of for loop + break; + } + } + //get the ending point + for (i++; i<4; i++) + { + //if point inside, then put it + if(mcvIsPointInside(pts[i], rect)) + { + outLine.endPoint.x = pts[i].x; + outLine.endPoint.y = pts[i].y; + //get out of for loop + break; + } + } +} + + +/** This function checks if the given point is inside the rectangle specified + * + * \param inLine the input line + * \param rect the specified rectangle + * + */ +bool mcvIsPointInside(FLOAT_POINT2D &point, const Line &rect) +{ + return (point.x>=rect.startPoint.x && point.x<=rect.endPoint.x + && point.y>=rect.startPoint.y && point.y<=rect.endPoint.y) ? true : false; +} + +/** This function checks if the given point is inside the rectangle specified + * + * \param inLine the input line + * \param rect the specified rectangle + * + */ +bool mcvIsPointInside(FLOAT_POINT2D &point, const CvRect &rect) +{ + return (point.x>=rect.x && point.x<=(rect.x+rect.width) + && point.y>=rect.y && point.y<=(rect.y+rect.height)) ? true : false; +} + +/** This function converts an INT mat into a FLOAT mat (already allocated) + * + * \param inMat input INT matrix + * \param outMat output FLOAT matrix + * + */ +void mcvMatInt2Float(const CvMat *inMat, CvMat *outMat) +{ + for (int i=0; iheight; i++) + for (int j=0; jwidth; j++) + CV_MAT_ELEM(*outMat, FLOAT_MAT_ELEM_TYPE, i, j) = + (FLOAT_MAT_ELEM_TYPE) CV_MAT_ELEM(*inMat, INT_MAT_ELEM_TYPE, + i, j)/255; +} + + +/** This function draws a line onto the passed image + * + * \param image the input iamge + * \param line input line + * \param line color + * \param width line width + * + */ +void mcvDrawLine(CvMat *image, Line line, CvScalar color, int width) +{ + cvLine(image, cvPoint((int)line.startPoint.x,(int)line.startPoint.y), + cvPoint((int)line.endPoint.x,(int)line.endPoint.y), + color, width); +} + +/** This initializes the LaneDetectorinfo structure + * + * \param fileName the input file name + * \param stopLineConf the structure to fill + * + * + */ + void mcvInitLaneDetectorConf(char * const fileName, + LaneDetectorConf *stopLineConf) +{ + //parsed camera data + LaneDetectorParserInfo stopLineParserInfo; + //read the data + assert(LaneDetectorParser_configfile(fileName, &stopLineParserInfo, 0, 1, 1)==0); + //init the strucure + stopLineConf->ipmWidth = stopLineParserInfo.ipmWidth_arg; + stopLineConf->ipmHeight = stopLineParserInfo.ipmHeight_arg; + stopLineConf->ipmLeft = stopLineParserInfo.ipmLeft_arg; + stopLineConf->ipmRight = stopLineParserInfo.ipmRight_arg; + stopLineConf->ipmBottom = stopLineParserInfo.ipmBottom_arg; + stopLineConf->ipmTop = stopLineParserInfo.ipmTop_arg; + stopLineConf->ipmInterpolation = stopLineParserInfo.ipmInterpolation_arg; + + stopLineConf->lineWidth = stopLineParserInfo.lineWidth_arg; + stopLineConf->lineHeight = stopLineParserInfo.lineHeight_arg; + stopLineConf->kernelWidth = stopLineParserInfo.kernelWidth_arg; + stopLineConf->kernelHeight = stopLineParserInfo.kernelHeight_arg; + stopLineConf->lowerQuantile = + stopLineParserInfo.lowerQuantile_arg; + stopLineConf->localMaxima = + stopLineParserInfo.localMaxima_arg; + stopLineConf->groupingType = stopLineParserInfo.groupingType_arg; + stopLineConf->binarize = stopLineParserInfo.binarize_arg; + stopLineConf->detectionThreshold = + stopLineParserInfo.detectionThreshold_arg; + stopLineConf->smoothScores = + stopLineParserInfo.smoothScores_arg; + stopLineConf->rMin = stopLineParserInfo.rMin_arg; + stopLineConf->rMax = stopLineParserInfo.rMax_arg; + stopLineConf->rStep = stopLineParserInfo.rStep_arg; + stopLineConf->thetaMin = stopLineParserInfo.thetaMin_arg * CV_PI/180; + stopLineConf->thetaMax = stopLineParserInfo.thetaMax_arg * CV_PI/180; + stopLineConf->thetaStep = stopLineParserInfo.thetaStep_arg * CV_PI/180; + stopLineConf->ipmVpPortion = stopLineParserInfo.ipmVpPortion_arg; + stopLineConf->getEndPoints = stopLineParserInfo.getEndPoints_arg; + stopLineConf->group = stopLineParserInfo.group_arg; + stopLineConf->groupThreshold = stopLineParserInfo.groupThreshold_arg; + stopLineConf->ransac = stopLineParserInfo.ransac_arg; + + stopLineConf->ransacLineNumSamples = stopLineParserInfo.ransacLineNumSamples_arg; + stopLineConf->ransacLineNumIterations = stopLineParserInfo.ransacLineNumIterations_arg; + stopLineConf->ransacLineNumGoodFit = stopLineParserInfo.ransacLineNumGoodFit_arg; + stopLineConf->ransacLineThreshold = stopLineParserInfo.ransacLineThreshold_arg; + stopLineConf->ransacLineScoreThreshold = stopLineParserInfo.ransacLineScoreThreshold_arg; + stopLineConf->ransacLineBinarize = stopLineParserInfo.ransacLineBinarize_arg; + stopLineConf->ransacLineWindow = stopLineParserInfo.ransacLineWindow_arg; + + stopLineConf->ransacSplineNumSamples = stopLineParserInfo.ransacSplineNumSamples_arg; + stopLineConf->ransacSplineNumIterations = stopLineParserInfo.ransacSplineNumIterations_arg; + stopLineConf->ransacSplineNumGoodFit = stopLineParserInfo.ransacSplineNumGoodFit_arg; + stopLineConf->ransacSplineThreshold = stopLineParserInfo.ransacSplineThreshold_arg; + stopLineConf->ransacSplineScoreThreshold = stopLineParserInfo.ransacSplineScoreThreshold_arg; + stopLineConf->ransacSplineBinarize = stopLineParserInfo.ransacSplineBinarize_arg; + stopLineConf->ransacSplineWindow = stopLineParserInfo.ransacSplineWindow_arg; + + stopLineConf->ransacSplineDegree = stopLineParserInfo.ransacSplineDegree_arg; + + stopLineConf->ransacSpline = stopLineParserInfo.ransacSpline_arg; + stopLineConf->ransacLine = stopLineParserInfo.ransacLine_arg; + stopLineConf->ransacSplineStep = stopLineParserInfo.ransacSplineStep_arg; + + stopLineConf->overlapThreshold = stopLineParserInfo.overlapThreshold_arg; + + stopLineConf->localizeAngleThreshold = stopLineParserInfo.localizeAngleThreshold_arg; + stopLineConf->localizeNumLinePixels = stopLineParserInfo.localizeNumLinePixels_arg; + + stopLineConf->extendAngleThreshold = stopLineParserInfo.extendAngleThreshold_arg; + stopLineConf->extendMeanDirAngleThreshold = stopLineParserInfo.extendMeanDirAngleThreshold_arg; + stopLineConf->extendLinePixelsTangent = stopLineParserInfo.extendLinePixelsTangent_arg; + stopLineConf->extendLinePixelsNormal = stopLineParserInfo.extendLinePixelsNormal_arg; + stopLineConf->extendContThreshold = stopLineParserInfo.extendContThreshold_arg; + stopLineConf->extendDeviationThreshold = stopLineParserInfo.extendDeviationThreshold_arg; + stopLineConf->extendRectTop = stopLineParserInfo.extendRectTop_arg; + stopLineConf->extendRectBottom = stopLineParserInfo.extendRectBottom_arg; + + stopLineConf->extendIPMAngleThreshold = stopLineParserInfo.extendIPMAngleThreshold_arg; + stopLineConf->extendIPMMeanDirAngleThreshold = stopLineParserInfo.extendIPMMeanDirAngleThreshold_arg; + stopLineConf->extendIPMLinePixelsTangent = stopLineParserInfo.extendIPMLinePixelsTangent_arg; + stopLineConf->extendIPMLinePixelsNormal = stopLineParserInfo.extendIPMLinePixelsNormal_arg; + stopLineConf->extendIPMContThreshold = stopLineParserInfo.extendIPMContThreshold_arg; + stopLineConf->extendIPMDeviationThreshold = stopLineParserInfo.extendIPMDeviationThreshold_arg; + stopLineConf->extendIPMRectTop = stopLineParserInfo.extendIPMRectTop_arg; + stopLineConf->extendIPMRectBottom = stopLineParserInfo.extendIPMRectBottom_arg; + + stopLineConf->splineScoreJitter = stopLineParserInfo.splineScoreJitter_arg; + stopLineConf->splineScoreLengthRatio = stopLineParserInfo.splineScoreLengthRatio_arg; + stopLineConf->splineScoreAngleRatio = stopLineParserInfo.splineScoreAngleRatio_arg; + stopLineConf->splineScoreStep = stopLineParserInfo.splineScoreStep_arg; + + stopLineConf->splineTrackingNumAbsentFrames = stopLineParserInfo.splineTrackingNumAbsentFrames_arg; + stopLineConf->splineTrackingNumSeenFrames = stopLineParserInfo.splineTrackingNumSeenFrames_arg; + + stopLineConf->mergeSplineThetaThreshold = stopLineParserInfo.mergeSplineThetaThreshold_arg; + stopLineConf->mergeSplineRThreshold = stopLineParserInfo.mergeSplineRThreshold_arg; + stopLineConf->mergeSplineMeanThetaThreshold = stopLineParserInfo.mergeSplineMeanThetaThreshold_arg; + stopLineConf->mergeSplineMeanRThreshold = stopLineParserInfo.mergeSplineMeanRThreshold_arg; + stopLineConf->mergeSplineCentroidThreshold = stopLineParserInfo.mergeSplineCentroidThreshold_arg; + + stopLineConf->lineTrackingNumAbsentFrames = stopLineParserInfo.lineTrackingNumAbsentFrames_arg; + stopLineConf->lineTrackingNumSeenFrames = stopLineParserInfo.lineTrackingNumSeenFrames_arg; + + stopLineConf->mergeLineThetaThreshold = stopLineParserInfo.mergeLineThetaThreshold_arg; + stopLineConf->mergeLineRThreshold = stopLineParserInfo.mergeLineRThreshold_arg; + + stopLineConf->numStrips = stopLineParserInfo.numStrips_arg; + + + stopLineConf->checkSplines = stopLineParserInfo.checkSplines_arg; + stopLineConf->checkSplinesCurvenessThreshold = stopLineParserInfo.checkSplinesCurvenessThreshold_arg; + stopLineConf->checkSplinesLengthThreshold = stopLineParserInfo.checkSplinesLengthThreshold_arg; + stopLineConf->checkSplinesThetaDiffThreshold = stopLineParserInfo.checkSplinesThetaDiffThreshold_arg; + stopLineConf->checkSplinesThetaThreshold = stopLineParserInfo.checkSplinesThetaThreshold_arg; + + stopLineConf->checkIPMSplines = stopLineParserInfo.checkIPMSplines_arg; + stopLineConf->checkIPMSplinesCurvenessThreshold = stopLineParserInfo.checkIPMSplinesCurvenessThreshold_arg; + stopLineConf->checkIPMSplinesLengthThreshold = stopLineParserInfo.checkIPMSplinesLengthThreshold_arg; + stopLineConf->checkIPMSplinesThetaDiffThreshold = stopLineParserInfo.checkIPMSplinesThetaDiffThreshold_arg; + stopLineConf->checkIPMSplinesThetaThreshold = stopLineParserInfo.checkIPMSplinesThetaThreshold_arg; + + stopLineConf->finalSplineScoreThreshold = stopLineParserInfo.finalSplineScoreThreshold_arg; + + stopLineConf->useGroundPlane = stopLineParserInfo.useGroundPlane_arg; + + stopLineConf->checkColor = stopLineParserInfo.checkColor_arg; + stopLineConf->checkColorNumBins = stopLineParserInfo.checkColorNumBins_arg; + stopLineConf->checkColorWindow = stopLineParserInfo.checkColorWindow_arg; + stopLineConf->checkColorNumYellowMin = stopLineParserInfo.checkColorNumYellowMin_arg; + stopLineConf->checkColorRGMin = stopLineParserInfo.checkColorRGMin_arg; + stopLineConf->checkColorRGMax = stopLineParserInfo.checkColorRGMax_arg; + stopLineConf->checkColorGBMin = stopLineParserInfo.checkColorGBMin_arg; + stopLineConf->checkColorRBMin = stopLineParserInfo.checkColorRBMin_arg; + stopLineConf->checkColorRBFThreshold = stopLineParserInfo.checkColorRBFThreshold_arg; + stopLineConf->checkColorRBF = stopLineParserInfo.checkColorRBF_arg; + + stopLineConf->ipmWindowClear = stopLineParserInfo.ipmWindowClear_arg;; + stopLineConf->ipmWindowLeft = stopLineParserInfo.ipmWindowLeft_arg;; + stopLineConf->ipmWindowRight = stopLineParserInfo.ipmWindowRight_arg;; + + stopLineConf->checkLaneWidth = stopLineParserInfo.checkLaneWidth_arg;; + stopLineConf->checkLaneWidthMean = stopLineParserInfo.checkLaneWidthMean_arg;; + stopLineConf->checkLaneWidthStd = stopLineParserInfo.checkLaneWidthStd_arg;; +} + +void SHOW_LINE(const Line line, char str[]) +{ + cerr << str; + cerr << "(" << line.startPoint.x << "," << line.startPoint.y << ")"; + cerr << "->"; + cerr << "(" << line.endPoint.x << "," << line.endPoint.y << ")"; + cerr << "\n"; +} + +void SHOW_SPLINE(const Spline spline, char str[]) +{ + cerr << str; + cerr << "(" << spline.degree << ")"; + for (int i=0; i &x, vector &y) +CvMat * mcvGetLinePixels(const Line &line) +{ + //get two end points + CvPoint start; + start.x = int(line.startPoint.x); start.y = int(line.startPoint.y); + CvPoint end; + end.x = int(line.endPoint.x); end.y = int(line.endPoint.y); + + //get deltas + int deltay = end.y - start.y; + int deltax = end.x - start.x; + + //check if slope is steep, then reflect the line along y=x i.e. swap x and y + bool steep = false; + if (abs(deltay) > abs(deltax)) + { + steep = true; + //swap x and y + int t; + t = start.x; + start.x = start.y; + start.y = t; + t = end.x; + end.x = end.y; + end.y = t; + } + + + //check to make sure we are going right + bool swap = false; + if(start.x>end.x) + { + //swap the two points + CvPoint t = start; + start = end; + end = t; + //we swapped + swap = true; + } + + //get deltas again + deltay = end.y - start.y; + deltax = end.x - start.x; + + //error + int error = 0; + + //delta error + int deltaerror = abs(deltay); + + //ystep + int ystep = -1; + if (deltay>=0) + { + ystep = 1; + } + + //create the return matrix + CvMat *pixels = cvCreateMat(end.x-start.x+1, 2, CV_32SC1); + + //loop + int i, j; + j = start.y; + //list x, y; + //index for array + int k, kupdate; + if (!swap) + { + k = 0; + kupdate = 1; + } + else + { + k = pixels->height-1; + kupdate = -1; + } + + for (i=start.x; i<=end.x; i++, k+=kupdate) + { + //put the new point + if(steep) + { + CV_MAT_ELEM(*pixels, int, k, 0) = j; + CV_MAT_ELEM(*pixels, int, k, 1) = i; + // x.push_back(j); + // y.push_back(i); + } + else + { + CV_MAT_ELEM(*pixels, int, k, 0) = i; + CV_MAT_ELEM(*pixels, int, k, 1) = j; + // x.push_back(i); + // y.push_back(j); + } + + //adjust error + error += deltaerror; + //check + if(2*error>=deltax) + { + j = j + ystep; + error -= deltax; + } + } + + //return + return pixels; +} + +/** This functions implements Bresenham's algorithm for getting pixels of the + * line given its two endpoints + + * + * \param im the input image + * \param inLine the input line + * \param outLine the output line + * + */ +void mcvGetLineExtent(const CvMat *im, const Line &inLine, Line &outLine) +{ + //first clip the input line to the image coordinates + Line line = inLine; + mcvIntersectLineWithBB(&inLine, cvSize(im->width-1, im->height-1), &line); + + //then get the pixel values of the line in the image + CvMat *pixels; //vector x, y; + pixels = mcvGetLinePixels(line); //, x, y); + + //check which way to shift the line to get multiple readings + bool changey = false; + if (fabs(line.startPoint.x-line.endPoint.x) > + fabs(line.startPoint.y-line.endPoint.y)) + { + //change the y-coordiantes + changey = true; + } + char changes[] = {0, -1, 1};//, -2, 2}; + int numChanges = 3; + + //loop on the changes and get possible extents + vector startLocs; + vector endLocs; + int endLoc; + int startLoc; + CvMat *pix = cvCreateMat(1, im->width, FLOAT_MAT_TYPE); + CvMat *rstep = cvCreateMat(pix->height, pix->width, FLOAT_MAT_TYPE); + CvMat *fstep = cvCreateMat(pix->height, pix->width, FLOAT_MAT_TYPE); + for (int c=0; cheight; i++) + { + CV_MAT_ELEM(*pix, FLOAT_MAT_ELEM_TYPE, 0, i) = + CV_MAT_ELEM(*im, FLOAT_MAT_ELEM_TYPE, + changey ? + min(max(CV_MAT_ELEM(*pixels, int, i, 1)+ + changes[c],0),im->height-1) : + CV_MAT_ELEM(*pixels, int, i, 1), + changey ? CV_MAT_ELEM(*pixels, int, i, 0) : + min(max(CV_MAT_ELEM(*pixels, int, i, 0)+ + changes[c],0),im->width-1)); + // changey ? min(max(y[i]+changes[c],0),im->height-1) : y[i], + // changey ? x[i] : min(max(x[i]+changes[c],0),im->width-1)); + } + //remove the mean + CvScalar mean = cvAvg(pix); + cvSubS(pix, mean, pix); + + //now convolve with rising step to get start point + FLOAT_MAT_ELEM_TYPE stepp[] = {-0.3000, -0.2, -0.1, 0, 0, 0.1, 0.2, 0.3, 0.4}; + // {-0.6, -0.4, -0.2, 0.2, 0.4, 0.6}; + int stepsize = 9; + //{-0.2, -0.4, -0.2, 0, 0, 0.2, 0.4, 0.2}; //{-.75, -.5, .5, .75}; + CvMat step = cvMat(1, stepsize, FLOAT_MAT_TYPE, stepp); + // SHOW_MAT(&step,"step"); + //smooth + // FLOAT_MAT_ELEM_TYPE smoothp[] = {.25, .5, .25}; + //CvMat smooth = cvMat(1, 3, FLOAT_MAT_TYPE, smoothp); + //cvFilter2D(&step, &step, &smooth); + //SHOW_MAT(&step,"smoothed step"); + //convolve + cvFilter2D(pix, rstep, &step); + //get local max + // vector localMax; + // vector localMaxLoc; + // mcvGetVectorLocalMax(rstep, localMax, localMaxLoc); + // int startLoc = localMaxLoc[0]; + double max; + mcvGetVectorMax(rstep, &max, &startLoc, 0); + //check if zero + if(max==0) + startLoc = startLocs[c-1]; + + //convolve with falling step to get end point + //cvFlip(&step, NULL, 1); + //convolve + //cvFilter2D(pix, fstep, &step); + //get local max + // localMax.clear(); + // localMaxLoc.clear(); + // mcvGetVectorLocalMax(fstep, localMax, localMaxLoc); + // int endLoc = localMaxLoc[0]; + //take the negative + cvConvertScale(rstep, fstep, -1); + mcvGetVectorMax(fstep, &max, &endLoc, 0); + //check if zero + if(max==0) + endLoc = endLocs[c-1]; + if(endLoc<=startLoc) + endLoc = im->width-1; + + //put into vectors + startLocs.push_back(startLoc); + endLocs.push_back(endLoc); + } + + //get median + startLoc = quantile(startLocs, 0); + endLoc = quantile(endLocs, 1); + // for (int i=0; i<(int)startLocs.size(); i++) cout << startLocs[i] << " "; + //cout << "\n"; + //for (int i=0; i<(int)endLocs.size(); i++) cout << endLocs[i] << " "; + + //get the end-point + outLine.startPoint.x = CV_MAT_ELEM(*pixels, int, startLoc, 0); + outLine.startPoint.y = CV_MAT_ELEM(*pixels, int, startLoc, 1); + outLine.endPoint.x = CV_MAT_ELEM(*pixels, int, endLoc, 0); + outLine.endPoint.y = CV_MAT_ELEM(*pixels, int, endLoc, 1); + // outLine.startPoint.x = x[startLoc]; outLine.startPoint.y = y[startLoc]; + // outLine.endPoint.x = x[endLoc]; outLine.endPoint.y = y[endLoc]; + + //clear + cvReleaseMat(&pix); + cvReleaseMat(&rstep); + cvReleaseMat(&fstep); + cvReleaseMat(&pixels); + // localMax.clear(); + // localMaxLoc.clear(); + startLocs.clear(); + endLocs.clear(); +} + + + +/** This functions converts a line defined by its two end-points into its + * r and theta (origin is at top-left corner with x right and y down and + * theta measured positive clockwise(with y pointing down) -pi < theta < pi ) + * + * \param line input line + * \param r the returned r (normal distance to the line from the origin) + * \param outLine the output line + * + */ +void mcvLineXY2RTheta(const Line &line, float &r, float &theta) +{ + //check if vertical line x1==x2 + if(line.startPoint.x == line.endPoint.x) + { + //r is the x + r = fabs(line.startPoint.x); + //theta is 0 or pi + theta = line.startPoint.x>=0 ? 0. : CV_PI; + } + //check if horizontal i.e. y1==y2 + else if(line.startPoint.y == line.endPoint.y) + { + //r is the y + r = fabs(line.startPoint.y); + //theta is pi/2 or -pi/2 + theta = (float) line.startPoint.y>=0 ? CV_PI/2 : -CV_PI/2; + } + //general line + else + { + //tan(theta) = (x2-x1)/(y1-y2) + theta = atan2(line.endPoint.x-line.startPoint.x, + line.startPoint.y-line.endPoint.y); + //r = x*cos(theta)+y*sin(theta) + float r1 = line.startPoint.x * cos(theta) + line.startPoint.y * sin(theta); + r = line.endPoint.x * cos(theta) + line.endPoint.y * sin(theta); + //adjust to add pi if necessary + if(r1<0 || r<0) + { + //add pi + theta += CV_PI; + if(theta>CV_PI) + theta -= 2*CV_PI; + //take abs + r = fabs(r); + } + } +} + +/** This functions fits a line using the orthogonal distance to the line + by minimizing the sum of squares of this distance. + + * + * \param points the input points to fit the line to which is + * 2xN matrix with x values on first row and y values on second + * \param lineRTheta the return line [r, theta] where the line is + * x*cos(theta)+y*sin(theta)=r + * \param lineAbc the return line in [a, b, c] where the line is + * a*x+b*y+c=0 + * + */ +void mcvFitRobustLine(const CvMat *points, float *lineRTheta, + float *lineAbc) +{ + // check number of points + if (points->cols < 2) + { + return; + } + + //clone the points + CvMat *cpoints = cvCloneMat(points); + //get mean of the points and subtract from the original points + float meanX=0, meanY=0; + CvScalar mean; + CvMat row1, row2; + //get first row, compute avg and store + cvGetRow(cpoints, &row1, 0); + mean = cvAvg(&row1); + meanX = (float) mean.val[0]; + cvSubS(&row1, mean, &row1); + //same for second row + cvGetRow(cpoints, &row2, 1); + mean = cvAvg(&row2); + meanY = (float) mean.val[0]; + cvSubS(&row2, mean, &row2); + + //compute the SVD for the centered points array + CvMat *W = cvCreateMat(2, 1, CV_32FC1); + CvMat *V = cvCreateMat(2, 2, CV_32FC1); + // CvMat *V = cvCreateMat(2, 2, CV_32fC1); + CvMat *cpointst = cvCreateMat(cpoints->cols, cpoints->rows, CV_32FC1); + + cvTranspose(cpoints, cpointst); + cvSVD(cpointst, W, 0, V, CV_SVD_V_T); + cvTranspose(V, V); + cvReleaseMat(&cpointst); + + //get the [a,b] which is the second column corresponding to + //smaller singular value + float a, b, c; + a = CV_MAT_ELEM(*V, float, 0, 1); + b = CV_MAT_ELEM(*V, float, 1, 1); + + //c = -meanX*a-meanY*b + c = -(meanX * a + meanY * b); + + //compute r and theta + //theta = atan(b/a) + //r = meanX cos(theta) + meanY sin(theta) + float r, theta; + theta = atan2(b, a); + r = meanX * cos(theta) + meanY * sin(theta); + //correct + if (r<0) + { + //correct r + r = -r; + //correct theta + theta += CV_PI; + if (theta>CV_PI) + theta -= 2*CV_PI; + } + //return + if (lineRTheta) + { + lineRTheta[0] = r; + lineRTheta[1] = theta; + } + if (lineAbc) + { + lineAbc[0] = a; + lineAbc[1] = b; + lineAbc[2] = c; + } + //clear + cvReleaseMat(&cpoints); + cvReleaseMat(&W); + cvReleaseMat(&V); +} + + + +/** This functions implements RANSAC algorithm for line fitting + * given an image + * + * + * \param image input image + * \param numSamples number of samples to take every iteration + * \param numIterations number of iterations to run + * \param threshold threshold to use to assess a point as a good fit to a line + * \param numGoodFit number of points close enough to say there's a good fit + * \param getEndPoints whether to get the end points of the line from the data, + * just intersect with the image boundaries + * \param lineType the type of line to look for (affects getEndPoints) + * \param lineXY the fitted line + * \param lineRTheta the fitted line [r; theta] + * \param lineScore the score of the line detected + * + */ +void mcvFitRansacLine(const CvMat *image, int numSamples, int numIterations, + float threshold, float scoreThreshold, int numGoodFit, + bool getEndPoints, LineType lineType, + Line *lineXY, float *lineRTheta, float *lineScore) +{ + + //get the points with non-zero pixels + CvMat *points; + points = mcvGetNonZeroPoints(image,true); + if (!points) + return; + //check numSamples + if (numSamples>points->cols) + numSamples = points->cols; + //subtract half + cvAddS(points, cvRealScalar(0.5), points); + + //normalize pixels values to get weights of each non-zero point + //get third row of points containing the pixel values + CvMat w; + cvGetRow(points, &w, 2); + //normalize it + CvMat *weights = cvCloneMat(&w); + cvNormalize(weights, weights, 1, 0, CV_L1); + //get cumulative sum + mcvCumSum(weights, weights); + + //random number generator + CvRNG rng = cvRNG(0xffffffff); + //matrix to hold random sample + CvMat *randInd = cvCreateMat(numSamples, 1, CV_32SC1); + CvMat *samplePoints = cvCreateMat(2, numSamples, CV_32FC1); + //flag for points currently included in the set + CvMat *pointIn = cvCreateMat(1, points->cols, CV_8SC1); + //returned lines + float curLineRTheta[2], curLineAbc[3]; + float bestLineRTheta[2]={-1.f,0.f}, bestLineAbc[3]; + float bestScore=0, bestDist=1e5; + float dist, score; + Line curEndPointLine={{-1.,-1.},{-1.,-1.}}, + bestEndPointLine={{-1.,-1.},{-1.,-1.}}; + //variabels for getting endpoints + //int mini, maxi; + float minc=1e5f, maxc=-1e5f, mind, maxd; + float x, y, c=0.; + CvPoint2D32f minp={-1., -1.}, maxp={-1., -1.}; + //outer loop + for (int i=0; icols)); + mcvSampleWeighted(weights, numSamples, randInd, &rng); + + for (int j=0; jmaxc) + { + maxc = c; + maxp = cvPoint2D32f(x, y); + } + if (ccols; j++) + { + // //if not already inside + // if (!CV_MAT_ELEM(*pointIn, char, 0, j)) + // { + //compute distance to line + dist = fabs(CV_MAT_ELEM(*points, float, 0, j) * curLineAbc[0] + + CV_MAT_ELEM(*points, float, 1, j) * curLineAbc[1] + curLineAbc[2]); + //check distance + if (dist<=threshold) + { + //add this point + CV_MAT_ELEM(*pointIn, char, 0, j) = 1; + //update score + score += cvGetReal2D(image, (int)(CV_MAT_ELEM(*points, float, 1, j)-.5), + (int)(CV_MAT_ELEM(*points, float, 0, j)-.5)); + } + // } + } + + //check the number of close points and whether to consider this a good fit + int numClose = cvCountNonZero(pointIn); + //cout << "numClose=" << numClose << "\n"; + if (numClose >= numGoodFit) + { + //get the points included to fit this line + CvMat *fitPoints = cvCreateMat(2, numClose, CV_32FC1); + int k=0; + //loop on points and copy points included + for (int j=0; jcols; j++) + if(CV_MAT_ELEM(*pointIn, char, 0, j)) + { + CV_MAT_ELEM(*fitPoints, float, 0, k) = + CV_MAT_ELEM(*points, float, 0, j); + CV_MAT_ELEM(*fitPoints, float, 1, k) = + CV_MAT_ELEM(*points, float, 1, j); + k++; + + } + + //fit the line + mcvFitRobustLine(fitPoints, curLineRTheta, curLineAbc); + + //compute distances to new line + dist = 0.; + for (int j=0; jcols; j++) + { + //compute distance to line + x = CV_MAT_ELEM(*fitPoints, float, 0, j); + y = CV_MAT_ELEM(*fitPoints, float, 1, j); + float d = fabs( x * curLineAbc[0] + + y * curLineAbc[1] + + curLineAbc[2]) + * cvGetReal2D(image, (int)(y-.5), (int)(x-.5)); + dist += d; + + // //check min and max coordinates to get extent + // if (getEndPoints) + // { + // //get the coordinate to work on + // if (lineType == LINE_HORIZONTAL) + // c = x; + // else if (lineType == LINE_VERTICAL) + // c = y; + // //compare + // if (c>maxc) + // { + // maxc = c; + // maxd = d; + // maxp = cvPoint2D32f(x, y); + // } + // if (c=scoreThreshold && score>bestScore)//dist bestScore) + { + //update max + bestScore = score; //numClose; + bestDist = dist; + //copy + bestLineRTheta[0] = curLineRTheta[0]; + bestLineRTheta[1] = curLineRTheta[1]; + bestLineAbc[0] = curLineAbc[0]; + bestLineAbc[1] = curLineAbc[1]; + bestLineAbc[2] = curLineAbc[2]; + bestEndPointLine = curEndPointLine; + } + } // if numClose + + //debug + if (DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + char str[256]; + //convert image to rgb + CvMat* im = cvCloneMat(image); + mcvScaleMat(image, im); + CvMat *imageClr = cvCreateMat(image->rows, image->cols, CV_32FC3); + cvCvtColor(im, imageClr, CV_GRAY2RGB); + + Line line; + //draw current line if there + if (curLineRTheta[0]>0) + { + mcvIntersectLineRThetaWithBB(curLineRTheta[0], curLineRTheta[1], + cvSize(image->cols, image->rows), &line); + mcvDrawLine(imageClr, line, CV_RGB(1,0,0), 1); + if (getEndPoints) + mcvDrawLine(imageClr, curEndPointLine, CV_RGB(0,1,0), 1); + } + + //draw best line + if (bestLineRTheta[0]>0) + { + mcvIntersectLineRThetaWithBB(bestLineRTheta[0], bestLineRTheta[1], + cvSize(image->cols, image->rows), &line); + mcvDrawLine(imageClr, line, CV_RGB(0,0,1), 1); + if (getEndPoints) + mcvDrawLine(imageClr, bestEndPointLine, CV_RGB(1,1,0), 1); + } + sprintf(str, "scor=%.2f, best=%.2f", score, bestScore); + mcvDrawText(imageClr, str, cvPoint(30, 30), .25, CV_RGB(255,255,255)); + + SHOW_IMAGE(imageClr, "Fit Ransac Line", 10); + + //clear + cvReleaseMat(&im); + cvReleaseMat(&imageClr); + }//#endif + } // for i + + //return + if (lineRTheta) + { + lineRTheta[0] = bestLineRTheta[0]; + lineRTheta[1] = bestLineRTheta[1]; + } + if (lineXY) + { + if (getEndPoints) + *lineXY = bestEndPointLine; + else + mcvIntersectLineRThetaWithBB(lineRTheta[0], lineRTheta[1], + cvSize(image->cols-1, image->rows-1), + lineXY); + } + if (lineScore) + *lineScore = bestScore; + + //clear + cvReleaseMat(&points); + cvReleaseMat(&samplePoints); + cvReleaseMat(&randInd); + cvReleaseMat(&pointIn); +} + + + + +/** This function gets the indices of the non-zero values in a matrix + * + * \param inMat the input matrix + * \param outMat the output matrix, with 2xN containing the x and y in + * each column and the pixels value [xs; ys; pixel values] + * \param floatMat whether to return floating points or integers for + * the outMat + */ +CvMat* mcvGetNonZeroPoints(const CvMat *inMat, bool floatMat) +{ + + +#define MCV_GET_NZ_POINTS(inMatType, outMatType) \ + /*loop and allocate the points*/ \ + for (int i=0; irows; i++) \ + for (int j=0; jcols; j++) \ + if (CV_MAT_ELEM(*inMat, inMatType, i, j)) \ + { \ + CV_MAT_ELEM(*outMat, outMatType, 0, k) = j; \ + CV_MAT_ELEM(*outMat, outMatType, 1, k) = i; \ + CV_MAT_ELEM(*outMat, outMatType, 2, k) = \ + (outMatType) CV_MAT_ELEM(*inMat, inMatType, i, j); \ + k++; \ + } \ + + int k=0; + + //get number of non-zero points + int numnz = cvCountNonZero(inMat); + + //allocate the point array and get the points + CvMat* outMat; + if (numnz) + { + if (floatMat) + outMat = cvCreateMat(3, numnz, CV_32FC1); + else + outMat = cvCreateMat(3, numnz, CV_32SC1); + } + else + return NULL; + + //check type + if (CV_MAT_TYPE(inMat->type)==FLOAT_MAT_TYPE && + CV_MAT_TYPE(outMat->type)==FLOAT_MAT_TYPE) + { + MCV_GET_NZ_POINTS(FLOAT_MAT_ELEM_TYPE, FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==FLOAT_MAT_TYPE && + CV_MAT_TYPE(outMat->type)==INT_MAT_TYPE) + { + MCV_GET_NZ_POINTS(FLOAT_MAT_ELEM_TYPE, INT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==INT_MAT_TYPE && + CV_MAT_TYPE(outMat->type)==FLOAT_MAT_TYPE) + { + MCV_GET_NZ_POINTS(INT_MAT_ELEM_TYPE, FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==INT_MAT_TYPE && + CV_MAT_TYPE(outMat->type)==INT_MAT_TYPE) + { + MCV_GET_NZ_POINTS(INT_MAT_ELEM_TYPE, INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetMatLocalMax\n"; + exit(1); + } + + //return + return outMat; +} + + +/** This function groups nearby lines + * + * \param lines vector of lines + * \param lineScores scores of input lines + * \param groupThreshold the threshold used for grouping + * \param bbox the bounding box to intersect with + */ +void mcvGroupLines(vector &lines, vector &lineScores, + float groupThreshold, CvSize bbox) +{ + + //convert the lines into r-theta parameters + int numInLines = lines.size(); + vector rs(numInLines); + vector thetas(numInLines); + for (int i=0; i::iterator ir, jr, itheta, jtheta, minIr, minJr, minItheta, minJtheta, + iscore, jscore, minIscore, minJscore; + //compute pairwise distance between detected maxima + for (ir=rs.begin(), itheta=thetas.begin(), iscore=lineScores.begin(); + ir!=rs.end(); ir++, itheta++, iscore++) + for (jr=ir+1, jtheta=itheta+1, jscore=iscore+1; + jr!=rs.end(); jr++, jtheta++, jscore++) + { + //add pi if neg + float t1 = *itheta<0 ? *itheta : *itheta+CV_PI; + float t2 = *jtheta<0 ? *jtheta : *jtheta+CV_PI; + //get distance + dist = 1 * fabs(*ir - *jr) + 1 * fabs(t1 - t2);//fabs(*itheta - *jtheta); + //check if minimum + if (dist= groupThreshold) + stop = true; + else + { + //put into the first + *minIr = (*minIr + *minJr)/2; + *minItheta = (*minItheta + *minJtheta)/2; + *minIscore = (*minIscore + *minJscore)/2; + //delete second one + rs.erase(minJr); + thetas.erase(minJtheta); + lineScores.erase(minJscore); + } + }//while + + //put back the lines + lines.clear(); + //lines.resize(rs.size()); + vector newScores=lineScores; + lineScores.clear(); + for (int i=0; i<(int)rs.size(); i++) + { + //get the line + Line line; + mcvIntersectLineRThetaWithBB(rs[i], thetas[i], bbox, &line); + //put in place descendingly + vector::iterator iscore; + vector::iterator iline; + for (iscore=lineScores.begin(), iline=lines.begin(); + iscore!=lineScores.end() && newScores[i]<=*iscore; iscore++, iline++); + lineScores.insert(iscore, newScores[i]); + lines.insert(iline, line); + } + //clear + newScores.clear(); +} + +/** This function groups nearby splines + * + * \param splines vector of splines + * \param lineScores scores of input lines + */ +void mcvGroupSplines(vector &splines, vector &scores) + +{ + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + CvMat* im = cvCreateMat(240, 320, CV_8UC3); + cvSet(im, cvRealScalar(0.)); + //draw splines + for (unsigned int i=0; i::iterator spi, spj; + vector::iterator si, sj; + for (spi=splines.begin(), si=scores.begin(); + spi!=splines.end(); spi++, si++) + for (spj=spi+1, sj=si+1; spj!=splines.end(); spj++, sj++) + //if to merge them + if (mcvCheckMergeSplines(*spi, *spj, .1, 5, .2, 10, 15)) + { + stop = false; + //keep straighter one + float ci, cj; + mcvGetSplineFeatures(*spi, 0, 0, 0, 0, 0, 0, &ci); + mcvGetSplineFeatures(*spj, 0, 0, 0, 0, 0, 0, &cj); + //put j in i if less curved + if (cj>ci) + { + //put spline j into i + *spi = *spj; + *si = *sj; + } + //remove j + splines.erase(spj); + scores.erase(sj); + + //break + break; + } + }//while + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + CvMat* im = cvCreateMat(240, 320, CV_8UC3); + cvSet(im, cvRealScalar(0.)); + //draw splines + for (unsigned int i=0; i &boxes, LineType type, + float groupThreshold) +{ + bool cont = true; + + //Todo: check if to intersect with bounding box or not + + //save boxes + //vector tboxes = boxes; + + //loop to get the largest overlap (according to type) and check + //the overlap ratio + float overlap, maxOverlap; + while(cont) + { + maxOverlap = overlap = -1e5; + //loop on lines and get max overlap + vector::iterator i, j, maxI, maxJ; + for(i = boxes.begin(); i != boxes.end(); i++) + { + for(j = i+1; j != boxes.end(); j++) + { + switch(type) + { + case LINE_VERTICAL: + //get one with smallest x, and compute the x2 - x1 / width of smallest + //i.e. (x12 - x21) / (x22 - x21) + overlap = i->x < j->x ? + (i->x + i->width - j->x) / (float)j->width : + (j->x + j->width - i->x) / (float)i->width; + + break; + + case LINE_HORIZONTAL: + //get one with smallest y, and compute the y2 - y1 / height of smallest + //i.e. (y12 - y21) / (y22 - y21) + overlap = i->y < j->y ? + (i->y + i->height - j->y) / (float)j->height : + (j->y + j->height - i->y) / (float)i->height; + + break; + + } //switch + + //get maximum + if(overlap > maxOverlap) + { + maxI = i; + maxJ = j; + maxOverlap = overlap; + } + } //for j + } // for i + // //debug + // if(DEBUG_LINES) { + // cout << "maxOverlap=" << maxOverlap << endl; + // cout << "Before grouping\n"; + // for(unsigned int k=0; k= groupThreshold) + { + //combine the two boxes + *maxI = cvRect(min((*maxI).x, (*maxJ).x), + min((*maxI).y, (*maxJ).y), + max((*maxI).width, (*maxJ).width), + max((*maxI).height, (*maxJ).height)); + //delete the second one + boxes.erase(maxJ); + } + else + //stop + cont = false; + + // //debug + // if(DEBUG_LINES) { + // cout << "After grouping\n"; + // for(unsigned int k=0; k &lines, + vector &lineScores, LaneDetectorConf *lineConf, + LineType lineType) +{ + //check if to binarize image + CvMat *image = cvCloneMat(im); + if (lineConf->ransacLineBinarize) + mcvBinarizeImage(image); + + int width = image->width-1; + int height = image->height-1; + //try grouping the lines into regions + //float groupThreshold = 15; + mcvGroupLines(lines, lineScores, lineConf->groupThreshold, + cvSize(width, height)); + + //group bounding boxes of lines + float overlapThreshold = lineConf->overlapThreshold; //0.5; //.8; + vector boxes; + mcvGetLinesBoundingBoxes(lines, lineType, cvSize(width, height), + boxes); + mcvGroupBoundingBoxes(boxes, lineType, overlapThreshold); + // mcvGroupLinesBoundingBoxes(lines, lineType, overlapThreshold, + // cvSize(width, height), boxes); + + // //check if there're no lines, then check the whole image + // if (boxes.size()<1) + // boxes.push_back(cvRect(0, 0, width-1, height-1)); + + int window = lineConf->ransacLineWindow; //15; + vector newLines; + vector newScores; + for (int i=0; i<(int)boxes.size(); i++) //lines + { + // fprintf(stderr, "i=%d\n", i); + //Line line = lines[i]; + CvRect mask, box; + //get box + box = boxes[i]; + switch (lineType) + { + case LINE_HORIZONTAL: + { + //get extent + //int ystart = (int)fmax(fmin(line.startPoint.y, line.endPoint.y)-window, 0); + //int yend = (int)fmin(fmax(line.startPoint.y, line.endPoint.y)+window, height-1); + int ystart = (int)fmax(box.y - window, 0); + int yend = (int)fmin(box.y + box.height + window, height-1); + //get the mask + mask = cvRect(0, ystart, width, yend-ystart+1); + } + break; + + case LINE_VERTICAL: + { + //get extent of window to search in + //int xstart = (int)fmax(fmin(line.startPoint.x, line.endPoint.x)-window, 0); + //int xend = (int)fmin(fmax(line.startPoint.x, line.endPoint.x)+window, width-1); + int xstart = (int)fmax(box.x - window, 0); + int xend = (int)fmin(box.x + box.width + window, width-1); + //get the mask + mask = cvRect(xstart, 0, xend-xstart+1, height); + } + break; + } + //get the subimage to work on + CvMat *subimage = cvCloneMat(image); + //clear all but the mask + mcvSetMat(subimage, mask, 0); + + //get the RANSAC line in this part + //int numSamples = 5, numIterations = 10, numGoodFit = 15; + //float threshold = 0.5; + float lineRTheta[2]={-1,0}; + float lineScore; + Line line; + mcvFitRansacLine(subimage, lineConf->ransacLineNumSamples, + lineConf->ransacLineNumIterations, + lineConf->ransacLineThreshold, + lineConf->ransacLineScoreThreshold, + lineConf->ransacLineNumGoodFit, + lineConf->getEndPoints, lineType, + &line, lineRTheta, &lineScore); + + //store the line if found and make sure it's not + //near horizontal or vertical (depending on type) + #warning "check this screening in ransacLines" + if (lineRTheta[0]>=0) + { + bool put =true; + switch(lineType) + { + case LINE_HORIZONTAL: + //make sure it's not vertical + if (fabs(lineRTheta[1]) < 30*CV_PI/180) + put = false; + break; + + case LINE_VERTICAL: + //make sure it's not horizontal + if((fabs(lineRTheta[1]) > 20*CV_PI/180)) + put = false; + break; + } + if (put) + { + newLines.push_back(line); + newScores.push_back(lineScore); + } + } // if + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //get string + char str[256]; + switch (lineType) + { + case LINE_HORIZONTAL: + sprintf(str, "Subimage Line H #%d", i); + break; + case LINE_VERTICAL: + sprintf(str, "Subimage Line V #%d", i); + break; + } + //convert image to rgb + mcvScaleMat(subimage, subimage); + CvMat *subimageClr = cvCreateMat(subimage->rows, subimage->cols, + CV_32FC3); + cvCvtColor(subimage, subimageClr, CV_GRAY2RGB); + //draw rectangle + // mcvDrawRectangle(subimageClr, box, + // CV_RGB(255, 255, 0), 1); + mcvDrawRectangle(subimageClr, mask, CV_RGB(255, 255, 255), 1); + + //draw line + if (lineRTheta[0]>0) + mcvDrawLine(subimageClr, line, CV_RGB(1,0,0), 1); + SHOW_IMAGE(subimageClr, str, 10); + //clear + cvReleaseMat(&subimageClr); + }//#endif + + //clear + cvReleaseMat(&subimage); + } // for i + + //group lines + vector oldLines; + if (DEBUG_LINES) + oldLines = lines; + lines.clear(); + lineScores.clear(); + #warning "not grouping at end of getRansacLines" + //mcvGroupLines(newLines, newScores, lineConf->groupThreshold, cvSize(width, height)); + lines = newLines; + lineScores = newScores; + + //draw splines + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //get string + char title[256]; //str[256], + switch (lineType) + { + case LINE_HORIZONTAL: + sprintf(title, "Lines H"); + break; + case LINE_VERTICAL: + sprintf(title, "Lines V"); + break; + } + //convert image to rgb + CvMat* im2 = cvCloneMat(im); + mcvScaleMat(im2, im2); + CvMat *imClr = cvCreateMat(im->rows, im->cols, CV_32FC3); + cvCvtColor(im2, imClr, CV_GRAY2RGB); + CvMat* imClr2 = cvCloneMat(imClr); + cvReleaseMat(&im2); + + //draw spline + for (unsigned int j=0; j::iterator li; +// vector::iterator si; +// for (int i=0; i<(int)newLines.size(); i++) +// { + // //get its position + // for (li=lines.begin(), si=lineScores.begin(); + // si!=lineScores.end() && newScores[i]<=*si; + // si++, li++); + // lines.insert(li, newLines[i]); + // lineScores.insert(si, newScores[i]); + // } + + //clean + boxes.clear(); + newLines.clear(); + newScores.clear(); + cvReleaseMat(&image); +} + +/** This function sets the matrix to a value except for the mask window passed in + * + * \param inMat input matrix + * \param mask the rectangle defining the mask: (xleft, ytop, width, height) + * \param val the value to put + */ +void mcvSetMat(CvMat *inMat, CvRect mask, double val) +{ + + //get x-end points of region to work on, and work on the whole image height + //(int)fmax(fmin(line.startPoint.x, line.endPoint.x)-xwindow, 0); + int xstart = mask.x, xend = mask.x + mask.width-1; + //xend = (int)fmin(fmax(line.startPoint.x, line.endPoint.x), width-1); + int ystart = mask.y, yend = mask.y + mask.height-1; + + //set other two windows to zero + CvMat maskMat; + CvRect rect; + //part to the left of required region + rect = cvRect(0, 0, xstart-1, inMat->height); + if (rect.xwidth && rect.yheight && + rect.x>=0 && rect.y>=0 && rect.width>0 && rect.height>0) + { + cvGetSubRect(inMat, &maskMat, rect); + cvSet(&maskMat, cvRealScalar(val)); + } + //part to the right of required region + rect = cvRect(xend+1, 0, inMat->width-xend-1, inMat->height); + if (rect.xwidth && rect.yheight && + rect.x>=0 && rect.y>=0 && rect.width>0 && rect.height>0) + { + cvGetSubRect(inMat, &maskMat, rect); + cvSet(&maskMat, cvRealScalar(val)); + } + + //part to the top + rect = cvRect(xstart, 0, mask.width, ystart-1); + if (rect.xwidth && rect.yheight && + rect.x>=0 && rect.y>=0 && rect.width>0 && rect.height>0) + { + cvGetSubRect(inMat, &maskMat, rect); + cvSet(&maskMat, cvRealScalar(val)); + } + + //part to the bottom + rect = cvRect(xstart, yend+1, mask.width, inMat->height-yend-1); + if (rect.xwidth && rect.yheight && + rect.x>=0 && rect.y>=0 && rect.width>0 && rect.height>0) + { + cvGetSubRect(inMat, &maskMat, rect); + cvSet(&maskMat, cvRealScalar(val)); + } +} + + +/** This function sorts a set of points + * + * \param inPOints Nx2 matrix of points [x,y] + * \param outPOints Nx2 matrix of points [x,y] + * \param dim the dimension to sort on (0: x, 1:y) + * \param dir direction of sorting (0: ascending, 1:descending) + */ +void mcvSortPoints(const CvMat *inPoints, CvMat *outPoints, + int dim, int dir) +{ + //make a copy of the input + CvMat *pts = cvCloneMat(inPoints); + + //clear the output + //cvSetZero(outPoints); + + //make the list of sorted indices + list sorted; + list::iterator sortedi; + int i, j; + + //loop on elements and adjust its index + for (i=0; iheight; i++) + { + //if ascending + if (dir==0) + for (sortedi = sorted.begin(); + sortedi != sorted.end() && + (CV_MAT_ELEM(*pts, float, i, dim) >= + CV_MAT_ELEM(*outPoints, float, *sortedi, dim)); + sortedi++); + //descending + else + for (sortedi = sorted.begin(); + sortedi != sorted.end() && + (CV_MAT_ELEM(*pts, float, i, dim) <= + CV_MAT_ELEM(*outPoints, float, *sortedi, dim)); + sortedi++); + + //found the position, so put it into sorted + sorted.insert(sortedi, i); + } + + //sorted the array, so put back + for (i=0, sortedi=sorted.begin(); sortedi != sorted.end(); sortedi++, i++) + for(j=0; jwidth; j++) + CV_MAT_ELEM(*outPoints, float, i, j) = CV_MAT_ELEM(*pts, float, + *sortedi, j); + + //clear + cvReleaseMat(&pts); + sorted.clear(); +} + +/** This function fits a Bezier spline to the passed input points + * + * \param points the input points + * \param degree the required spline degree + * \return spline the returned spline + */ +Spline mcvFitBezierSpline(CvMat *points, int degree) +{ + + //set the degree + Spline spline; + spline.degree = degree; + + //get number of points + int n = points->height; + //float step = 1./(n-1); + + //sort the pointa + mcvSortPoints(points, points, 1, 0); + // SHOW_MAT(points, "Points after sorting:"); + + //get first point and distance between points + CvPoint2D32f p0 = cvPoint2D32f(CV_MAT_ELEM(*points, float, 0, 0), + CV_MAT_ELEM(*points, float, 0, 1)); + + float diff = 0.f; + float *us = new float[points->height]; + us[0] = 0; + for (int i=1; iheight; ++i) + { + float dx = CV_MAT_ELEM(*points, float, i, 0) - + CV_MAT_ELEM(*points, float, i-1, 0); + float dy = CV_MAT_ELEM(*points, float, i, 1) - + CV_MAT_ELEM(*points, float, i-1, 1); + us[i] = cvSqrt(dx*dx + dy*dy) + us[i-1]; + // diff += us[i];; + } + diff = us[points->height-1]; + + //float y0 = CV_MAT_ELEM(*points, float, 0, 1); + //float ydiff = CV_MAT_ELEM(*points, float, points->height-1, 1) - y0; + + //M matrices: M2 for quadratic (degree 2) and M3 for cubic + float M2[] = {1, -2, 1, + -2, 2, 0, + 1, 0, 0}; + float M3[] = {-1, 3, -3, 1, + 3, -6, 3, 0, + -3, 3, 0, 0, + 1, 0, 0, 0}; + + //M matrix for Bezier + CvMat M; + + //Basis matrix + CvMat *B; + + //u value for points to create the basis matrix + float u = 0.f; + + //switch on the degree + switch(degree) + { + //Quadratic spline + case 2: + //M matrix + M = cvMat(3, 3, CV_32FC1, M2); + + //create the basis matrix + B = cvCreateMat(n, 3, CV_32FC1); + for (int i=0; iheight; i++) //u+=step + { + //get u as ratio of y-coordinate + // u = i / ((float)n-1); + + // u = (CV_MAT_ELEM(*points, float, i, 1) - y0) / ydiff; + + // float dx = CV_MAT_ELEM(*points, float, i, 0) - p0.x; + // float dy = CV_MAT_ELEM(*points, float, i, 1) - p0.y; + // u = cvSqrt(dx*dx + dy*dy) / diff; + u = us[i] / diff; + + CV_MAT_ELEM(*B, float, i, 2) = 1; //1 + CV_MAT_ELEM(*B, float, i, 1) = u; //u + CV_MAT_ELEM(*B, float, i, 0) = u*u; //u^2 + } + break; + + //Cubic spline + case 3: + //M matrix + M = cvMat(4, 4, CV_32FC1, M3); + + //create the basis matrix + B = cvCreateMat(n, 4, CV_32FC1); + for (int i=0; iheight; i++) //, u+=step) + { + //get u as ratio of y-coordinate + // u = i / ((float)n-1); + + // u = (CV_MAT_ELEM(*points, float, i, 1) - y0) / ydiff; + + // float dx = CV_MAT_ELEM(*points, float, i, 0) - p0.x; + // float dy = CV_MAT_ELEM(*points, float, i, 1) - p0.y; + // u = cvSqrt(dx*dx + dy*dy) / diff; + u = us[i] / diff; + + CV_MAT_ELEM(*B, float, i, 3) = 1; //1 + CV_MAT_ELEM(*B, float, i, 2) = u; //u + CV_MAT_ELEM(*B, float, i, 1) = u*u; //u^2 + CV_MAT_ELEM(*B, float, i, 0) = u*u*u; //u^2 + } + break; + } // switch degree + + //multiply B by M + cvMatMul(B, &M, B); + + + //return the required control points by LS + CvMat *sp = cvCreateMat(degree+1, 2, CV_32FC1); + cvSolve(B, points, sp, CV_SVD); + + // SHOW_MAT(sp, "Spline points:"); + + //put back into spline + memcpy((float *)spline.points, sp->data.fl, sizeof(float)*(spline.degree+1)*2); + // if(spline.points[0].x<0) + // SHOW_MAT(points, "INput Points"); + + //clear + cvReleaseMat(&B); + cvReleaseMat(&sp); + delete [] us; + + //return + return spline; +} + + + +/** This function evaluates Bezier spline with given resolution + * + * \param spline input spline + * \param h the input resolution + * \param tangents compute tangents at the two endpoints [t0; t1] + * \return computed points in an array Nx2 [x,y] + */ +CvMat* mcvEvalBezierSpline(const Spline &spline, float h, CvMat *tangents) +{ + //compute number of points to return + int n = (int)(1./h)+1; + + //allocate the points + CvMat *points = cvCreateMat(n, 2, CV_32FC1); + + //M matrices + CvMat M; + float M2[] = {1, -2, 1, + -2, 2, 0, + 1, 0, 0}; + float M3[] = {-1, 3, -3, 1, + 3, -6, 3, 0, + -3, 3, 0, 0, + 1, 0, 0, 0}; + + //spline points + CvMat *sp = cvCreateMat(spline.degree+1, 2, CV_32FC1); + memcpy(sp->data.fl, (float *)spline.points, + sizeof(float)*(spline.degree+1)*2); + + //abcd + CvMat *abcd; + + float P[2], dP[2], ddP[2], dddP[2]; + float h2 = h*h, h3 = h2*h; + + //switch the degree + switch(spline.degree) + { + //Quadratic + case 2: + //get M matrix + M = cvMat(3, 3, CV_32FC1, M2); + + //get abcd where a=row 0, b=row 1, ... + abcd = cvCreateMat(3, 2, CV_32FC1); + cvMatMul(&M, sp, abcd); + + //P = c + P[0] = CV_MAT_ELEM(*abcd, float, 2, 0); + P[1] = CV_MAT_ELEM(*abcd, float, 2, 1); + + //dP = b*h+a*h^2 + dP[0] = CV_MAT_ELEM(*abcd, float, 1, 0)*h + + CV_MAT_ELEM(*abcd, float, 0, 0)*h2; + dP[1] = CV_MAT_ELEM(*abcd, float, 1, 1)*h + + CV_MAT_ELEM(*abcd, float, 0, 1)*h2; + + //ddP = 2*a*h^2 + ddP[0] = 2 * CV_MAT_ELEM(*abcd, float, 0, 0)*h2; + ddP[1] = 2 * CV_MAT_ELEM(*abcd, float, 0, 1)*h2; + + //loop and put points + for (int i=0; iheight, 1, CV_8SC1); + //cvSet(, CvScalar value, const CvArr* mask=NULL); + list inpoints; + list::iterator inpointsi; + int lastin = -1, numin = 0; + for (int i=0; iheight; i++) + { + //round + CV_MAT_ELEM(*points, float, i, 0) = cvRound(CV_MAT_ELEM(*points, float, i, 0)); + CV_MAT_ELEM(*points, float, i, 1) = cvRound(CV_MAT_ELEM(*points, float, i, 1)); + + //check boundaries + if(CV_MAT_ELEM(*points, float, i, 0) >= 0 && + CV_MAT_ELEM(*points, float, i, 0) < box.width && + CV_MAT_ELEM(*points, float, i, 1) >= 0 && + CV_MAT_ELEM(*points, float, i, 1) < box.height) + { + //it's inside, so check if the same as last one + if(lastin<0 || + (lastin>=0 && + !(CV_MAT_ELEM(*points, float, lastin, 1)== + CV_MAT_ELEM(*points, float, i, 1) && + CV_MAT_ELEM(*points, float, lastin, 0)== + CV_MAT_ELEM(*points, float, i, 0) )) ) + { + //put inside + //CV_MAT_ELEM(*inpoints, char, i, 0) = 1; + inpoints.push_back(i); + lastin = i; + numin++; + } + } + } + + //check if to extend the spline with lines + CvMat *pixelst0, *pixelst1; + if (extendSpline) + { + //get first point inside + int p0 = inpoints.front(); + //extend from the starting point by going backwards along the tangent + //line from that point to the start of spline + Line line; + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, p0, 0) - 10 * + CV_MAT_ELEM(*tangents, float, 0, 0), + CV_MAT_ELEM(*points, float, p0, 1) - 10 * + CV_MAT_ELEM(*tangents, float, 0, 1)); + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, p0, 0), + CV_MAT_ELEM(*points, float, p0, 1)); + //intersect the line with the bounding box + mcvIntersectLineWithBB(&line, cvSize(box.width-1, box.height-1), &line); + //get line pixels + pixelst0 = mcvGetLinePixels(line); + numin += pixelst0->height; + + //get last point inside + int p1 = inpoints.back(); + //extend from end of spline along tangent + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, p1, 0) + 10 * + CV_MAT_ELEM(*tangents, float, 1, 0), + CV_MAT_ELEM(*points, float, p1, 1) + 10 * + CV_MAT_ELEM(*tangents, float, 1, 1)); + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, p1, 0), + CV_MAT_ELEM(*points, float, p1, 1)); + //intersect the line with the bounding box + mcvIntersectLineWithBB(&line, cvSize(box.width-1, box.height-1), &line); + //get line pixels + pixelst1 = mcvGetLinePixels(line); + numin += pixelst1->height; + } + + //put the results in another matrix + CvMat *rpoints; + if (numin>0) + rpoints = cvCreateMat(numin, 2, CV_32SC1); + else + { + return NULL; + } + + + //first put extended line segment if available + if(extendSpline) + { + //copy + memcpy(cvPtr2D(rpoints, 0, 0), pixelst0->data.fl, + sizeof(float)*2*pixelst0->height); + } + + //put spline pixels + int ri = extendSpline ? pixelst0->height : 0; + for (inpointsi=inpoints.begin(); + inpointsi!=inpoints.end(); ri++, inpointsi++) + { + CV_MAT_ELEM(*rpoints, int, ri, 0) = (int)CV_MAT_ELEM(*points, + float, *inpointsi, 0); + CV_MAT_ELEM(*rpoints, int, ri, 1) = (int)CV_MAT_ELEM(*points, + float, *inpointsi, 1); + } + + //put second extended piece of spline + if(extendSpline) + { + //copy + memcpy(cvPtr2D(rpoints, ri, 0), pixelst1->data.fl, + sizeof(float)*2*pixelst1->height); + //clear + cvReleaseMat(&pixelst0); + cvReleaseMat(&pixelst1); + } + + + //release + // cvReleaseMat(&inpoints); + cvReleaseMat(&points); + cvReleaseMat(&tangents); + inpoints.clear(); + + //return + return rpoints; +} + + +/** This function performs a RANSAC validation step on the detected lines to + * get splines + * + * \param image the input image + * \param lines vector of input lines to refine + * \param lineSCores the line scores input + * \param groupThreshold the threshold used for grouping + * \param bbox the bounding box to intersect with + * \param lineType the line type to work on (horizontal or vertical) + * \param prevSplines the previous splines to use in initializing the detection + */ +void mcvGetRansacSplines(const CvMat *im, vector &lines, + vector &lineScores, LaneDetectorConf *lineConf, + LineType lineType, vector &splines, + vector &splineScores, LineState* state) +{ + //check if to binarize image + CvMat *image = cvCloneMat(im); + if (lineConf->ransacSplineBinarize) + mcvBinarizeImage(image); // ((topmost-intro . 147431)) + + int width = image->width; + int height = image->height; + //try grouping the lines into regions + //float groupThreshold = 15; + #warning "no line grouping in getRansacSplines" + vector tlines = lines; + vector tlineScores = lineScores; + mcvGroupLines(tlines, tlineScores, lineConf->groupThreshold, + cvSize(width-1, height-1)); + + //put the liens into the prevSplines to initialize it + for (unsigned int i=0; state->ipmSplines.size() && + iransacSplineDegree); + state->ipmSplines.push_back(spline); + } + + //group bounding boxes of lines + float overlapThreshold = lineConf->overlapThreshold; //0.5; //.8; + vector boxes; + CvSize size = cvSize(width, height); + mcvGetLinesBoundingBoxes(tlines, lineType, size, boxes); + mcvGroupBoundingBoxes(boxes, lineType, overlapThreshold); + // mcvGroupLinesBoundingBoxes(tlines, lineType, overlapThreshold, + // cvSize(width, height), boxes); + tlines.clear(); + tlineScores.clear(); + + //add bounding boxes from previous frame + #warning "Turned off adding boxes from previous frame" + // boxes.insert(boxes.end(), state->ipmBoxes.begin(), + // state->ipmBoxes.end()); + + // //check if there're no lines, then check the whole image + // if (boxes.size()<1) + // boxes.push_back(cvRect(0, 0, width-1, height-1)); + + int window = lineConf->ransacSplineWindow; //15; + vector newSplines; + vector newSplineScores; + for (int i=0; i<(int)boxes.size(); i++) //lines + { + //Line line = lines[i]; + + CvRect mask, box; + + //get box + box = boxes[i]; + + switch (lineType) + { + case LINE_HORIZONTAL: + { + //get extent + //int ystart = (int)fmax(fmin(line.startPoint.y, line.endPoint.y)-window, 0); + //int yend = (int)fmin(fmax(line.startPoint.y, line.endPoint.y)+window, height-1); + int ystart = (int)fmax(box.y - window, 0); + int yend = (int)fmin(box.y + box.height + window, height-1); + //get the mask + mask = cvRect(0, ystart, width, yend-ystart+1); + } + break; + + case LINE_VERTICAL: + { + //get extent of window to search in + //int xstart = (int)fmax(fmin(line.startPoint.x, line.endPoint.x)-window, 0); + //int xend = (int)fmin(fmax(line.startPoint.x, line.endPoint.x)+window, width-1); + int xstart = (int)fmax(box.x - window, 0); + int xend = (int)fmin(box.x + box.width + window, width-1); + //get the mask + mask = cvRect(xstart, 0, xend-xstart+1, height); + } + break; + } + //get the subimage to work on + CvMat *subimage = cvCloneMat(image); + //clear all but the mask + mcvSetMat(subimage, mask, 0); + + //get the RANSAC spline in this part + //int numSamples = 5, numIterations = 10, numGoodFit = 15; + //float threshold = 0.5; + Spline spline; + float splineScore; + //resolution to use in pixelizing the spline + float h = lineConf->ransacSplineStep; // .1; //1. / max(image->width, image->height); + mcvFitRansacSpline(subimage, lineConf->ransacSplineNumSamples, + lineConf->ransacSplineNumIterations, + lineConf->ransacSplineThreshold, + lineConf->ransacSplineScoreThreshold, + lineConf->ransacSplineNumGoodFit, + lineConf->ransacSplineDegree, h, + &spline, &splineScore, + lineConf->splineScoreJitter, + lineConf->splineScoreLengthRatio, + lineConf->splineScoreAngleRatio, + lineConf->splineScoreStep, + &state->ipmSplines); + + //store the line if found + if (spline.degree > 0) + { + newSplines.push_back(spline); + newSplineScores.push_back(splineScore); + } + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //get string + char str[256], title[256];; + switch (lineType) + { + case LINE_HORIZONTAL: + sprintf(title, "Subimage Spline H #%d", i); + break; + case LINE_VERTICAL: + sprintf(title, "Subimage Spline V #%d", i); + break; + } + //convert image to rgb + mcvScaleMat(subimage, subimage); + CvMat *subimageClr = cvCreateMat(subimage->rows, subimage->cols, + CV_32FC3); + cvCvtColor(subimage, subimageClr, CV_GRAY2RGB); + + //draw rectangle + //mcvDrawRectangle(subimageClr, box, + // CV_RGB(255, 255, 0), 1); + mcvDrawRectangle(subimageClr, mask, CV_RGB(255, 255, 255), 1); + + //put text + sprintf(str, "score=%.2f", splineScore); + // mcvDrawText(subimageClr, str, cvPoint(30, 30), + // .25f, CV_RGB(1,1,1)); + + //draw spline + if (spline.degree > 0) + mcvDrawSpline(subimageClr, spline, CV_RGB(1,0,0), 1); + SHOW_IMAGE(subimageClr, title, 10); + //clear + cvReleaseMat(&subimageClr); + }//#endif + + //clear + cvReleaseMat(&subimage); + }//for + + + //put splines back in descending order of scores + splines.clear(); + splineScores.clear(); + vector::iterator li; + vector::iterator si; + for (int i=0; i<(int)newSplines.size(); i++) + { + //get its position + for (li=splines.begin(), si=splineScores.begin(); + si!=splineScores.end() && newSplineScores[i]<=*si; + si++, li++); + splines.insert(li, newSplines[i]); + splineScores.insert(si, newSplineScores[i]); + } + + //group the splines + mcvGroupSplines(splines, splineScores); + + //draw splines + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //get string + char title[256]; //str[256], + switch (lineType) + { + case LINE_HORIZONTAL: + sprintf(title, "Splines H"); + break; + case LINE_VERTICAL: + sprintf(title, "Splines V"); + break; + } + //convert image to rgb + CvMat* im2 = cvCloneMat(im); + mcvScaleMat(im2, im2); + CvMat *imClr = cvCreateMat(im->rows, im->cols, CV_32FC3); + cvCvtColor(im2, imClr, CV_GRAY2RGB); + cvReleaseMat(&im2); + + //draw spline + for (unsigned int j=0; j *prevSplines) +{ + //get the points with non-zero pixels + CvMat *points = mcvGetNonZeroPoints(image, true); + if (points==0 || points->cols < numSamples) + { + if (spline) spline->degree = -1; + cvReleaseMat(&points); + return; + } + // fprintf(stderr, "num points=%d", points->cols); + //subtract half + #warning "check adding half to points" + CvMat p; + cvGetRows(points, &p, 0, 2); + cvAddS(&p, cvRealScalar(0.5), &p); + + //normalize pixels values to get weights of each non-zero point + //get third row of points containing the pixel values + CvMat w; + cvGetRow(points, &w, 2); + //normalize it + CvMat *weights = cvCloneMat(&w); + cvNormalize(weights, weights, 1, 0, CV_L1); + //get cumulative sum + mcvCumSum(weights, weights); + + //random number generator + CvRNG rng = cvRNG(0xffffffff); + //matrix to hold random sample + CvMat *randInd = cvCreateMat(numSamples, 1, CV_32SC1); + CvMat *samplePoints = cvCreateMat(numSamples, 2, CV_32FC1); + //flag for points currently included in the set + CvMat *pointIn = cvCreateMat(1, points->cols, CV_8SC1); + //returned splines + Spline curSpline, bestSpline; + bestSpline.degree = 0;//initialize + float bestScore=0; //, bestDist=1e5; + + //iterator for previous splines + vector::iterator prevSpline; + bool randSpline = prevSplines==NULL || prevSplines->size()==0; + if (!randSpline) prevSpline = prevSplines->begin(); + + //fprintf(stderr, "spline degree=%d\n", prevSpline->degree); + + //outer loop + for (int i=0; iend(); + } // if + //get random spline + else + { + //set flag to zero + cvSetZero(pointIn); + //get random sample from the points + //cvRandArr(&rng, randInd, CV_RAND_UNI, cvRealScalar(0), cvRealScalar(points->cols)); + mcvSampleWeighted(weights, numSamples, randInd, &rng); + // SHOW_MAT(randInd, "randInd"); + for (int j=0; jrows; j++) //numSamples + { + //flag it as included + int p = CV_MAT_ELEM(*randInd, int, j, 0); + CV_MAT_ELEM(*pointIn, char, 0, p) = 1; + //put point + CV_MAT_ELEM(*samplePoints, float, j, 0) = + CV_MAT_ELEM(*points, float, 0, p); + CV_MAT_ELEM(*samplePoints, float, j, 1) = + CV_MAT_ELEM(*points, float, 1, p); + } + + //fit the spline + curSpline = mcvFitBezierSpline(samplePoints, splineDegree); + // SHOW_MAT(samplePoints, "Sampled points"); + } // else + + + //get score + //float lengthRatio = 0.5; //.8 + //float angleRatio = 0.8; //.4 + //vectorjitter = mcvGetJitterVector(splineScoreJitter); //2); + float score = mcvGetSplineScore(image, curSpline, splineScoreStep,//.05,//h, + splineScoreJitter, //jitter, + splineScoreLengthRatio, + splineScoreAngleRatio); + + //jitter.clear(); + + //check if better than best score so far + //printf("Score=%.2f & scoreThreshold=%.2f\n", score, scoreThreshold); + if (score>bestScore && score >= scoreThreshold) + { + //put it + bestScore = score; + bestSpline = curSpline; + } + + //show image for debugging + if(0) { //DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //get string + char str[256]; + sprintf(str, "Spline Fit: score=%f, best=%f", score, bestScore); + //fprintf(stderr, str); + // SHOW_SPLINE(curSpline, "curSpline:"); + + + //convert image to rgb + CvMat *imageClr = cvCreateMat(image->rows, image->cols, + CV_32FC3); + CvMat *im = cvCloneMat(image); + mcvScaleMat(image, im); + cvCvtColor(im, imageClr, CV_GRAY2RGB); + //draw spline + //previous splines + for (unsigned int k=0; prevSplines && ksize(); ++k) + mcvDrawSpline(imageClr, (*prevSplines)[k], CV_RGB(0,1,0), 1); + if(curSpline.degree>0) + mcvDrawSpline(imageClr, curSpline, CV_RGB(1,0,0), 1); + if(bestSpline.degree>0) + mcvDrawSpline(imageClr, bestSpline, CV_RGB(0,0,1), 1); + + //put text + CvFont font; + cvInitFont(&font, CV_FONT_HERSHEY_TRIPLEX, .25f, .25f); + sprintf(str, "score=%.2f bestScre=%.2f", score, bestScore); + cvPutText(imageClr, str, cvPoint(30, 30), &font, CV_RGB(1,1,1)); + + sprintf(str, "Spline Fit"); + SHOW_IMAGE(imageClr, str, 10); + //clear + cvReleaseMat(&imageClr); + cvReleaseMat(&im); + }//#endif + } //for + + //return + if (spline) + *spline = bestSpline; + if (splineScore) + *splineScore = bestScore; + + + //clear + cvReleaseMat(&points); + cvReleaseMat(&samplePoints); + cvReleaseMat(&randInd); + cvReleaseMat(&pointIn); + cvReleaseMat(&weights); +} + +/** This function draws a spline onto the passed image + * + * \param image the input iamge + * \param spline input spline + * \param spline color + * + */ +void mcvDrawSpline(CvMat *image, Spline spline, CvScalar color, int width) +{ + //get spline pixels + CvMat *pixels = mcvGetBezierSplinePixels(spline, .05, + cvSize(image->width, image->height), + false); + //if no pixels + if (!pixels) + return; + + //draw pixels in image with that color + for (int i=0; iheight-1; i++) + // cvSet2D(image, + // (int)cvGetReal2D(pixels, i, 1), + // (int)cvGetReal2D(pixels, i, 0), + // color); + cvLine(image, cvPoint((int)cvGetReal2D(pixels, i, 0), + (int)cvGetReal2D(pixels, i, 1)), + cvPoint((int)cvGetReal2D(pixels, i+1, 0), + (int)cvGetReal2D(pixels, i+1, 1)), + color, width); + + //put the control points with circles + for (int i=0; i &lines, IPMInfo &ipmInfo, + CameraInfo &cameraInfo, CvSize imSize) +{ + //check if returned anything + if (lines.size()!=0) + { + //convert the line into world frame + for (unsigned int i=0; istartPoint), &ipmInfo); + mcvPointImIPM2World(&(line->endPoint), &ipmInfo); + } + + //convert them from world frame into camera frame + // + //put a dummy line at the beginning till we check that cvDiv bug + Line dummy = {{1.,1.},{2.,2.}}; + lines.insert(lines.begin(), dummy); + //convert to mat and get in image coordinates + CvMat *mat = cvCreateMat(2, 2*lines.size(), FLOAT_MAT_TYPE); + mcvLines2Mat(&lines, mat); + lines.clear(); + mcvTransformGround2Image(mat, mat, &cameraInfo); + //get back to vector + mcvMat2Lines(mat, &lines); + //remove the dummy line at the beginning + lines.erase(lines.begin()); + //clear + cvReleaseMat(&mat); + + //clip the lines found and get their extent + for (unsigned int i=0; i &splines, IPMInfo &ipmInfo, + CameraInfo &cameraInfo, CvSize imSize) +{ + //loop on splines and convert + for (int i=0; i<(int)splines.size(); i++) + { + //get points for this spline in IPM image + CvMat *points = mcvEvalBezierSpline(splines[i], .1); + + //transform these points to image coordinates + CvMat *points2 = cvCreateMat(2, points->height, CV_32FC1); + cvTranspose(points, points2); + //mcvPointImIPM2World(CvMat *mat, const IPMInfo *ipmInfo); + //mcvTransformGround2Image(points2, points2, &cameraInfo); + mcvTransformImIPM2Im(points2, points2, &ipmInfo, &cameraInfo); + cvTranspose(points2, points); + cvReleaseMat(&points2); + + //refit the points into a spline in output image + splines[i] = mcvFitBezierSpline(points, splines[i].degree); + } +} + + +/** This function samples uniformly with weights + * + * \param cumSum cumulative sum for normalized weights for the differnet + * samples (last is 1) + * \param numSamples the number of samples + * \param randInd a 1XnumSamples of int containing the indices + * \param rng a pointer to a random number generator + * + */ +void mcvSampleWeighted(const CvMat *cumSum, int numSamples, CvMat *randInd, + CvRNG *rng) +{ +// //get cumulative sum of the weights +// //OPTIMIZE:should pass it later instead of recomputing it +// CvMat *cumSum = cvCloneMat(weights); +// for (int i=1; icols; i++) +// CV_MAT_ELEM(*cumSum, float, 0, i) += CV_MAT_ELEM(*cumSum, float, 0, i-1); + + //check if numSamples is equal or more + int i=0; + if (numSamples >= cumSum->cols) + { + for (; icols && r>CV_MAT_ELEM(*cumSum, float, 0, j); j++); + + //make sure this index wasnt chosen before + bool put = true; + for (int k=0; krows == 1) \ + for (int i=1; icols; i++) \ + CV_MAT_ELEM(*outMat, type, 0, i) += \ + CV_MAT_ELEM(*outMat, type, 0, i-1); \ + /*column vector*/ \ + else \ + for (int i=1; irows; i++) \ + CV_MAT_ELEM(*outMat, type, i, 0) += \ + CV_MAT_ELEM(*outMat, type, i-1, 0); + + //copy to output if not equal + if(inMat != outMat) + cvCopy(inMat, outMat); + + //check type + if (CV_MAT_TYPE(inMat->type)==CV_32FC1) + { + MCV_CUM_SUM(float) + } + else if (CV_MAT_TYPE(inMat->type)==CV_32SC1) + { + MCV_CUM_SUM(int) + } + else + { + cerr << "Unsupported type in mcvCumSum\n"; + exit(1); + } +} + + +/** This functions gives better localization of points along lines + * + * \param im the input image + * \param inPoints the input points Nx2 matrix of points + * \param outPoints the output points Nx2 matrix of points + * \param numLinePixels Number of pixels to go in normal direction for + * localization + * \param angleThreshold Angle threshold used for localization + * (cosine, 1: most restrictive, 0: most liberal) + * + */ +void mcvLocalizePoints(const CvMat *im, const CvMat *inPoints, + CvMat *outPoints, int numLinePixels, + float angleThreshold) +{ + //size of inPoints must be at least 3 + if(inPoints->height<3) + { + cvCopy(inPoints, outPoints); + return; + } + + //number of pixels in line around each point + //int numLinePixels = 20; + //tangent and normal + CvPoint2D32f tangent, normal;// peakTangent; + + //threshold for accepting new point (if not changing orientation too much) + //float angleThreshold = .7;//.96; + CvMat *imageClr; + char str[256]; + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //get string + sprintf(str, "Localize Points"); + + //convert image to rgb + imageClr = cvCreateMat(im->rows, im->cols, CV_32FC3); + cvCvtColor(im, imageClr, CV_GRAY2RGB); + }//#endif + + + //loop on the points + for (int i=0; iheight; i++) + { + + //get tangent to current point + if (i==0) + { + //first point, then tangent is vector to next point + tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 1, 0) - + CV_MAT_ELEM(*inPoints, float, 0, 0), + CV_MAT_ELEM(*inPoints, float, 1, 1) - + CV_MAT_ELEM(*inPoints, float, 0, 1)); + } + else if (i==1) + tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 1, 0) - + CV_MAT_ELEM(*outPoints, float, 0, 0), + CV_MAT_ELEM(*inPoints, float, 1, 1) - + CV_MAT_ELEM(*outPoints, float, 0, 1)); + + else //if (i==inPoints->height-1) + { + //last pointm then vector from previous two point + tangent = cvPoint2D32f(CV_MAT_ELEM(*outPoints, float, i-1, 0) - + CV_MAT_ELEM(*outPoints, float, i-2, 0), + CV_MAT_ELEM(*outPoints, float, i-1, 1) - + CV_MAT_ELEM(*outPoints, float, i-2, 1)); + // tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, i, 0) - + // CV_MAT_ELEM(*outPoints, float, i-1, 0), + // CV_MAT_ELEM(*inPoints, float, i, 1) - + // CV_MAT_ELEM(*outPoints, float, i-1, 1)); + } +// else +// { +// //general point, then take next - previous +// tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, i, 0) - //i+1 +// CV_MAT_ELEM(*outPoints, float, i-1, 0), +// CV_MAT_ELEM(*inPoints, float, i, 1) - //i+1 +// CV_MAT_ELEM(*outPoints, float, i-1, 1)); +// } + + //get normal + float ss = cvInvSqrt(tangent.x * tangent.x + tangent.y * tangent.y); + tangent.x *= ss; tangent.y *= ss; + normal.x = tangent.y; normal.y = -tangent.x; + + //get points in normal direction + Line line; + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, i, 0) + + numLinePixels * normal.x, + CV_MAT_ELEM(*inPoints, float, i, 1) + + numLinePixels * normal.y); + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, i, 0) - + numLinePixels * normal.x, + CV_MAT_ELEM(*inPoints, float, i, 1) - + numLinePixels * normal.y); + + + CvPoint2D32f prevPoint = {0., 0.}; + if (i>0) + prevPoint = cvPoint2D32f(CV_MAT_ELEM(*outPoints, float, i-1, 0), + CV_MAT_ELEM(*outPoints, float, i-1, 1)); + + //get line peak i.e. point in middle of bright line on dark background + CvPoint2D32f peak; + // float val = mcvGetLinePeak(im, line, peak); + //get line peak + vector peaks; + vector peakVals; + float val = mcvGetLinePeak(im, line, peaks, peakVals); + + //choose the best peak + // int index = mcvChooseBestPeak(peaks, peakVals, peak, val, + // 0, tangent, + // prevPoint, angleThreshold); + peak = peaks.front(); + val = peakVals.front(); + //clear + peaks.clear(); + peakVals.clear(); + + //check new peak + if (mcvIsPointInside(line.startPoint, cvSize(im->width, im->height)) && + mcvIsPointInside(line.endPoint, cvSize(im->width, im->height)) && + (//!i || + (i>0 && + mcvIsValidPeak(peak, tangent, prevPoint, + angleThreshold))) ) + { + //put new peak + CV_MAT_ELEM(*outPoints, float, i, 0) = peak.x; + CV_MAT_ELEM(*outPoints, float, i, 1) = peak.y; + } + else + { + //keep original point + CV_MAT_ELEM(*outPoints, float, i, 0) = CV_MAT_ELEM(*inPoints, float, i, 0); + CV_MAT_ELEM(*outPoints, float, i, 1) = CV_MAT_ELEM(*inPoints, float, i, 1); + } + + //debugging + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + fprintf(stderr, "Localize val=%.3f\n", val); + + //draw original point, localized point, and line endpoints + cvLine(imageClr, cvPointFrom32f(line.startPoint), + cvPointFrom32f(line.endPoint), CV_RGB(0, 0, 1)); + //output points + cvCircle(imageClr, cvPoint((int)CV_MAT_ELEM(*outPoints, float, i, 0), + (int)CV_MAT_ELEM(*outPoints, float, i, 1)), + 1, CV_RGB(0, 1, 0), -1); + //input points + cvCircle(imageClr, cvPoint((int)(line.startPoint.x+line.endPoint.x)/2, + (int)(line.startPoint.y+line.endPoint.y)/2), + 1, CV_RGB(1, 0, 0), -1); + //show image + SHOW_IMAGE(imageClr, str, 10); + }//#endif + } // for i + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + SHOW_IMAGE(imageClr, str, 10); + //clear + cvReleaseMat(&imageClr); + }//#endif +} + + +/** This functions checks the peak point if much change in orientation + * + * \param peak the input peak point + * \param tangent the tangent line along which the peak was found normal to + * (normalized) + * \param prevPoint the previous point along the tangent + * \param angleThreshold the angle threshold to consider for valid peaks + * \return true if useful peak, zero otherwise + * + */ +bool mcvIsValidPeak(const CvPoint2D32f &peak, const CvPoint2D32f &tangent, + const CvPoint2D32f &prevPoint, float angleThreshold) +{ + //compute the tangent line for the peak + CvPoint2D32f peakTangent; + peakTangent.x = peak.x - prevPoint.x; + peakTangent.y = peak.y - prevPoint.y; + + //normalize new tangent + float ss = cvInvSqrt(peakTangent.x * peakTangent.x + peakTangent.y * + peakTangent.y); + peakTangent.x *= ss; peakTangent.y *= ss; + + //check angle between newTangent and tangent, and refuse peak if too far + float angle = fabs(peakTangent.x*tangent.x + peakTangent.y*tangent.y); + if (DEBUG_LINES) + fprintf(stderr, "angle=%f\n", angle); + //return + return (angle >= angleThreshold) ? true : false; + +} + + +/** This functions chooses the best peak that minimizes deviation + * from the tangent direction given + * + * \param peaks the peaks found + * \param peakVals the values for the peaks + * \param peak the returned peak + * \param peakVal the peak value for chosen peak, -1 if nothing + * \param contThreshold the threshold to get peak above + * \param tangent the tangent line along which the peak was found normal to + * (normalized) + * \param prevPoint the previous point along the tangent + * \param angleThreshold the angle threshold to consider for valid peaks + * \return index of peak chosen, -1 if nothing + * + */ +int mcvChooseBestPeak(const vector &peaks, + const vector &peakVals, + CvPoint2D32f &peak, float &peakVal, + float contThreshold, const CvPoint2D32f &tangent, + const CvPoint2D32f &prevPoint, float angleThreshold) +{ + int index=-1; + float maxAngle=0; + peakVal = -1; + + //loop and check + for (unsigned int i=0; i=contThreshold && angle>=angleThreshold && + angle>maxAngle) + { + //mark it as chosen + maxAngle = angle; + index = i; + } + } // for i + + //return + if (index>=0) + { + peak = peaks[index]; + peakVal = peakVals[index]; + } + + if (DEBUG_LINES) + fprintf(stderr, "Chosen peak is: (%f, %f)\n", peak.x, peak.y); + + return index; +} + + +/** This functions extends the given set of points in both directions to + * extend curves and lines in the image + * + * \param im the input image + * \param inPoints the input points Nx2 matrix of points + * \param angleThreshold angle threshold used for extending + * \param meanDirAngleThreshold angle threshold from mean direction + * \param linePixelsTangent number of pixels to go in tangent direction + * \param linePixelsNormal number of pixels to go in normal direction + * \param contThreshold number of pixels to go in tangent direction + * \param deviationThreshold Stop extending when number of deviating points + * exceeds this threshold + * \param bbox a bounding box not to get points outside + * \param smoothPeak whether to smooth for calculating peaks or not + * + */ +CvMat* mcvExtendPoints(const CvMat *im, const CvMat *inPoints, + float angleThreshold, float meanDirAngleThreshold, + int linePixelsTangent, int linePixelsNormal, + float contThreshold, int deviationThreshold, + CvRect bbox, bool smoothPeaks) +{ + //size of inPoints must be at least 3 + if(inPoints->height<4) + { + return cvCloneMat(inPoints); + } + + + char str[256]; + CvMat *imageClr; + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //get string + sprintf(str, "Extend Points"); + + //convert image to rgb + imageClr = cvCreateMat(im->rows, im->cols, CV_32FC3); + CvMat *im2 = cvCloneMat(im); + mcvScaleMat(im, im2); + cvCvtColor(im2, imageClr, CV_GRAY2RGB); + cvReleaseMat(&im2); + + //show original points + for(int i=0; iheight; i++) + //input points + cvCircle(imageClr, cvPoint((int)(CV_MAT_ELEM(*inPoints, float, i, 0)), + (int)(CV_MAT_ELEM(*inPoints, float, i, 1))), + 1, CV_RGB(0, 1, 1), -1); + //show image + SHOW_IMAGE(imageClr, str, 10); + }//#endif + + //tangent and normal + CvPoint2D32f tangent, curPoint, peak, nextPoint, meanDir; + //prevTangent, pprevTangent, + + //number of pixels away to look for points + //int linePixelsTangent = 5, linePixelsNormal = 20; + bool cont = true; + + //threshold for stopping + //float contThreshold = 0.1; //.1 for gaussian top //0.01; + + //angle threshold (max orientation change allowed) + //float angleThreshold = .7;//.5 //.8;//.5 //.866; + //float meanDirAngleThreshold = .7; + + //threshold to stop when deviating from normal orientation + //int deviationThreshold = 2; + + //first go in one direction: from first point backward + vector backPoints; + int numBack = 0; + int deviationCount = 0; + vector peaks; + vector peakVals; + //get mean direction of points + meanDir = mcvGetPointsMeanVector(inPoints, false); + while(cont) + { + int outSize = (int)backPoints.size(); + //get tangent from previous point in input points if no output points yet + if(outSize==0) + { + curPoint = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 0, 0), + CV_MAT_ELEM(*inPoints, float, 0, 1)); + tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 0, 0) - + CV_MAT_ELEM(*inPoints, float, 1, 0), + CV_MAT_ELEM(*inPoints, float, 0, 1) - + CV_MAT_ELEM(*inPoints, float, 1, 1)); + // prevTangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 1, 0) - + // CV_MAT_ELEM(*inPoints, float, 2, 0), + // CV_MAT_ELEM(*inPoints, float, 1, 1) - + // CV_MAT_ELEM(*inPoints, float, 2, 1)); + // prevTangent = mcvNormalizeVector(prevTangent); + + // pprevTangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 2, 0) - + // CV_MAT_ELEM(*inPoints, float, 3, 0), + // CV_MAT_ELEM(*inPoints, float, 2, 1) - + // CV_MAT_ELEM(*inPoints, float, 3, 1)); + // pprevTangent = mcvNormalizeVector(pprevTangent); + } + //only one out point till now + else + { + // pprevTangent = prevTangent; + // prevTangent = tangent; + curPoint = backPoints[outSize-1]; + if (outSize==1) + { + tangent = cvPoint2D32f(backPoints[outSize-1].x - + CV_MAT_ELEM(*inPoints, float, 0, 0), + backPoints[outSize-1].y - + CV_MAT_ELEM(*inPoints, float, 0, 1)); + } + //more than one + else + { + tangent = cvPoint2D32f(backPoints[outSize-1].x - + backPoints[outSize-2].x, + backPoints[outSize-1].y - + backPoints[outSize-2].y); + } + } + + //get the line normal to tangent (tangent is normalized in function) + Line line; + line = mcvGetExtendedNormalLine(curPoint, tangent, linePixelsTangent, + linePixelsNormal, nextPoint); + + //check if still inside + //if (mcvIsPointInside(nextPoint, cvSize(im->width-1, im->height-1))) + if (mcvIsPointInside(nextPoint, bbox)) + { + //clip line + mcvIntersectLineWithBB(&line, cvSize(im->width-1, im->height-1), &line); + + //get line peak + float val = mcvGetLinePeak(im, line, peaks, peakVals, + true, smoothPeaks); + + //choose the best peak + //int index = + mcvChooseBestPeak(peaks, peakVals, peak, val, contThreshold, tangent, + curPoint, 0); //angleThreshold); + //clear + peaks.clear(); + peakVals.clear(); + + //check the peak + // !mcvIsValidPeak(peak, prevTangent, curPoint, angleThreshold) || + // !mcvIsValidPeak(peak, pprevTangent, curPoint, angleThreshold) || + if (!mcvIsValidPeak(peak, tangent, curPoint, angleThreshold) || + !mcvIsValidPeak(peak, meanDir, curPoint, meanDirAngleThreshold)) + { + peak = nextPoint; + deviationCount++; + } + else + deviationCount = 0; + + if (DEBUG_LINES){ + fprintf(stderr, "Extension back #%d val=%.3f\n", outSize, val); + fprintf(stderr, "Deviation Count=%d\n", deviationCount); + } + + //check value + //check value + if(valdeviationThreshold) + { + cont = false; + // for(int k=0; kdeviationThreshold) + // { + // cont = false; + // backPoints.erase(backPoints.end() - deviationThreshold, + // backPoints.end()); + // //numBack -= deviationThreshold; + // } + else + { + //push back + backPoints.push_back(peak); + //numBack++; + } + } // if mcvIsPointInside + //line got outside, so stop + else + cont = false; + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //draw original point, localized point, and line endpoints + cvLine(imageClr, cvPointFrom32f(line.startPoint), + cvPointFrom32f(line.endPoint), + CV_RGB(0, 0, 1)); + //output points + cvCircle(imageClr, cvPointFrom32f(peak), 1, CV_RGB(0, 1, 0), -1); + //input points + cvCircle(imageClr, cvPointFrom32f(nextPoint), 1, CV_RGB(1, 0, 0), -1); + //show image + SHOW_IMAGE(imageClr, str, 10); + }//#endif + } // while cont + + //do the same for the opposite direction + cont = true; + vector frontPoints; + int numFront = 0; + deviationCount = 0; + //get mean direction in forward direction + meanDir = mcvGetPointsMeanVector(inPoints, true); + while(cont) + { + int outSize = (int)frontPoints.size(); + //get tangent from previous point in input points if no output points yet + if(outSize==0) + { + curPoint = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 0), + CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 1)); + tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 0) - + CV_MAT_ELEM(*inPoints, float, + inPoints->height-2, 0), + CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 1) - + CV_MAT_ELEM(*inPoints, float, + inPoints->height-2, 1)); + + // prevTangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, inPoints->height-2, 0) - + // CV_MAT_ELEM(*inPoints, float, inPoints->height-3, 0), + // CV_MAT_ELEM(*inPoints, float, inPoints->height-2, 1) - + // CV_MAT_ELEM(*inPoints, float, inPoints->height-3, 1)); + // prevTangent = mcvNormalizeVector(prevTangent); + + // pprevTangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, inPoints->height-3, 0) - + // CV_MAT_ELEM(*inPoints, float, inPoints->height-4, 0), + // CV_MAT_ELEM(*inPoints, float, inPoints->height-3, 1) - + // CV_MAT_ELEM(*inPoints, float, inPoints->height-4, 1)); + // pprevTangent = mcvNormalizeVector(pprevTangent); + } + //only one out point till now + else + { + // pprevTangent = prevTangent; + // prevTangent = tangent; + curPoint = frontPoints[outSize-1]; + if (outSize==1) + { + tangent = cvPoint2D32f(frontPoints[outSize-1].x - + CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 0), + frontPoints[outSize-1].y - + CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 1)); + } + //more than one + else + { + tangent = cvPoint2D32f(frontPoints[outSize-1].x - + frontPoints[outSize-2].x, + frontPoints[outSize-1].y - + frontPoints[outSize-2].y); + } + } + + Line line; + line = mcvGetExtendedNormalLine(curPoint, tangent, linePixelsTangent, + linePixelsNormal, nextPoint); + + //check if still inside + // if (mcvIsPointInside(nextPoint, cvSize(im->width-1, im->height-1))) + if (mcvIsPointInside(nextPoint, bbox)) + { + //clip line + mcvIntersectLineWithBB(&line, cvSize(im->width-1, im->height-1), &line); + + //get line peak + // float val = mcvGetLinePeak(im, line, peak); + float val = mcvGetLinePeak(im, line, peaks, peakVals, true, smoothPeaks); + + //choose the best peak + //int index = + mcvChooseBestPeak(peaks, peakVals, peak, val, contThreshold, tangent, + curPoint, 0); //angleThreshold); + + //clear + peaks.clear(); + peakVals.clear(); + + //check the peak +// !mcvIsValidPeak(peak, prevTangent, curPoint, angleThreshold) || +// !mcvIsValidPeak(peak, pprevTangent, curPoint, angleThreshold) || + if(!mcvIsValidPeak(peak, tangent, curPoint, angleThreshold) || + !mcvIsValidPeak(peak, meanDir, curPoint, meanDirAngleThreshold)) + { + //put normal point + peak = nextPoint; + //increment deviation count + deviationCount++; + } + else + deviationCount = 0; + + if (DEBUG_LINES){ + fprintf(stderr, "Extension front #%d val=%.3f\n", outSize, val); + fprintf(stderr, "Deviation Count=%d\n", deviationCount); + } + + //check value + if(valdeviationThreshold) + { + cont = false; + // for(int k=0; kdeviationThreshold) + // { + // cont = false; + // frontPoints.erase(frontPoints.end() - deviationThreshold, + // frontPoints.end()); + // } + else + { + //push back + frontPoints.push_back(peak); + //numFront++; + } + } + //line got outside, so stop + else + cont = false; + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //draw original point, localized point, and line endpoints + cvLine(imageClr, cvPointFrom32f(line.startPoint), + cvPointFrom32f(line.endPoint), CV_RGB(0, 0, 1)); + //output points + cvCircle(imageClr, cvPointFrom32f(peak), 1, CV_RGB(0, 1, 0), -1); + //input points + cvCircle(imageClr, cvPointFrom32f(nextPoint), 1, CV_RGB(1, 0, 0), -1); + //show image + SHOW_IMAGE(imageClr, str, 10); + }//#endif + } + + numFront = frontPoints.size(); + numBack = backPoints.size(); + //now that we have extended the points in both directions, we need to put them + //back into the return matrix + CvMat *extendedPoints = cvCreateMat(inPoints->height + numBack + numFront, + 2, CV_32FC1); + //first put back points in reverse order + vector::iterator pointi; + int i = 0; + for (i=0, pointi=backPoints.end()-1; idata.fl, + sizeof(float)*2*inPoints->height); + + //then put the front points in normal order + for (i = numBack+inPoints->height, pointi=frontPoints.begin(); + iheight; pointi++, i++) + { + CV_MAT_ELEM(*extendedPoints, float, i, 0) = (*pointi).x; + CV_MAT_ELEM(*extendedPoints, float, i, 1) = (*pointi).y; + } + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + SHOW_IMAGE(imageClr, str, 10); + //clear + cvReleaseMat(&imageClr); + }//#endif + + //clear + backPoints.clear(); + frontPoints.clear(); + //return + return extendedPoints; +} + + + +/** This functions extends a point along the tangent and gets the normal line + * at the new point + * + * \param curPoint the current point to extend + * \param tangent the tangent at this point (not necessarily normalized) + * \param linePixelsTangent the number of pixels to go in tangent direction + * \param linePixelsNormal the number of pixels to go in normal direction + * \param nextPoint the next point on the extended line + * \return the normal line at new point + */ +Line mcvGetExtendedNormalLine(CvPoint2D32f &curPoint, CvPoint2D32f &tangent, + int linePixelsTangent, int linePixelsNormal, + CvPoint2D32f &nextPoint) +{ + //normalize tangent + float ssq = cvInvSqrt(tangent.x*tangent.x + tangent.y*tangent.y); + tangent.x *= ssq; + tangent.y *= ssq; + + //get next point along the way + nextPoint.x = curPoint.x + linePixelsTangent * tangent.x; + nextPoint.y = curPoint.y + linePixelsTangent * tangent.y; + + //get normal direction + CvPoint2D32f normal = cvPoint2D32f(-tangent.y, tangent.x); + + //get two points along the normal line + Line line; + line.startPoint = cvPoint2D32f(nextPoint.x + linePixelsNormal*normal.x, + nextPoint.y + linePixelsNormal*normal.y); + line.endPoint = cvPoint2D32f(nextPoint.x - linePixelsNormal*normal.x, + nextPoint.y - linePixelsNormal*normal.y); + + //return + return line; +} + + + +/** This functions gets the point on the input line that matches the + * peak in the input image, where the peak is the middle of a bright + * line on dark background in the image + * + * \param im the input image + * \param line input line + * \param peaks a vector of peak outputs + * \param peakVals the values for each of these peaks + * \param positivePeak whether we are looking for positive or + * negative peak(default true) + * \param smoothPeaks whether to smooth pixels for calculating peaks + * or not + * + */ +float mcvGetLinePeak(const CvMat *im, const Line &line, + vector &peaks, + vector &peakVals, bool positivePeak, + bool smoothPeaks) +{ + //create step to convolve with + FLOAT_MAT_ELEM_TYPE stepp[] = //{-1, 0, 1};//{-1, -1, -1, 1, 1, 1}; + // {-0.3000, -0.2, -0.1, 0, 0, 0.1, 0.2, 0.3, 0.4}; + // {-0.6, -0.4, -0.2, 0.2, 0.4, 0.6}; + //latest-> {-0.2, -0.4, -0.2, 0, 0, 0.2, 0.4, 0.2}; //{-.75, -.5, .5, .75}; + { 0.000003726653172, 0.000040065297393, 0.000335462627903, + 0.002187491118183, 0.011108996538242, 0.043936933623407, + 0.135335283236613, 0.324652467358350, 0.606530659712633, + 0.882496902584595, 1.000000000000000, 0.882496902584595, + 0.606530659712633, 0.324652467358350, 0.135335283236613, + 0.043936933623407, 0.011108996538242, 0.002187491118183, + 0.000335462627903, 0.000040065297393, 0.000003726653172}; + int stepsize = 21; + CvMat step = cvMat(1, stepsize, CV_32FC1, stepp); + + //take negative to work for opposite polarity + if (!positivePeak) + cvScale(&step, &step, -1); + // //get the gaussian kernel to convolve with + // int width = 5; + // float step = .5; + // CvMat *step = cvCreateMat(1, (int)(2*width/step+1), CV_32FC1); + // int j; float i; + // for (i=-w, j=0; i<=w; i+=step, ++j) + // CV_MAT_ELEM(*step, FLOAT_MAT_ELEM_TYPE, 0, j) = + // (float) exp(-(.5*i*i)); + + + //then get the pixel coordinates of the line in the image + CvMat *pixels; + pixels = mcvGetLinePixels(line); + //get pixel values + CvMat *pix = cvCreateMat(1, pixels->height, CV_32FC1); + for(int j=0; jheight; j++) + { + CV_MAT_ELEM(*pix, float, 0, j) = + cvGetReal2D(im, + MIN(MAX(CV_MAT_ELEM(*pixels, int, j, 1),0),im->height-1), + MIN(MAX(CV_MAT_ELEM(*pixels, int, j, 0),0),im->width-1)); + } + //clear + cvReleaseMat(&pixels); + + //remove the mean + CvScalar mean = cvAvg(pix); + cvSubS(pix, mean, pix); + + //convolve with step + CvMat *pixStep = cvCreateMat(pix->height, pix->width, CV_32FC1); + if (smoothPeaks) + cvFilter2D(pix, pixStep, &step); + else + cvCopy(pix, pixStep); + // SHOW_MAT(pixStep, "pixStep"); + // SHOW_MAT(pix, "pixels"); + + //get local maxima + double topVal; + float top; + vector maxima; + vector maximaLoc; + CvPoint2D32f peak; + //get top point + mcvGetVectorLocalMax(pixStep, maxima, maximaLoc); + if(maximaLoc.size()>0) + { + //get max + topVal = maxima.front(); + //loop and get peaks + for (unsigned int i=0; iwidth-1)); + top = (float)mcvGetLocalMaxSubPixel(val1, maxima[i], val3); + top += maximaLoc[i]; + //fprintf(stderr, "val1=%f, val2=%f, val3=%f\n", val1, maxima[i], val3); + //fprintf(stderr, "top=%d, subpixel=%f\n", maximaLoc[i], top); + top /= pix->width; + //get loc +// top = maximaLoc[i]/(float)(pix->width); + //get peak + peak.x = line.startPoint.x*(1-top) + top * line.endPoint.x; + peak.y = line.startPoint.y*(1-top) + top * line.endPoint.y; + //push back + peaks.push_back(peak); + peakVals.push_back(maxima[i]); + } + } // if + else + { + top = (pix->width-2)/2./(pix->width); + topVal = -1; + //push back + peak.x = line.startPoint.x*(1-top) + top * line.endPoint.x; + peak.y = line.startPoint.y*(1-top) + top * line.endPoint.y; + //push back + peaks.push_back(peak); + peakVals.push_back(topVal); + + } + maxima.clear(); + maximaLoc.clear(); + +// //get new point +// top /= (pix->width); +// peak.x = line.startPoint.x*(1-top) + top * line.endPoint.x; +// peak.y = line.startPoint.y*(1-top) + top * line.endPoint.y; + + //clear + cvReleaseMat(&pix); + cvReleaseMat(&pixStep); + + //return mean of rising and falling val + return topVal;//MIN(risingVal, fallingVal);//no minus //(risingVal+fallingVal)/2; +} + +/** This functions normalizes the given vector + * + * \param vector the input vector to normalize + */ +CvPoint2D32f mcvNormalizeVector(const CvPoint2D32f &v) +{ + //return vector + CvPoint2D32f ret = v; + + //normalize vector + float ssq = cvInvSqrt(ret.x*ret.x + ret.y*ret.y); + ret.x *= ssq; + ret.y *= ssq; + + //return + return ret; +} + + +/** This functions normalizes the given vector + * + * \param vector the input vector to normalize + */ +CvPoint2D32f mcvNormalizeVector(const CvPoint &v) +{ + //return vector + return mcvNormalizeVector(cvPointTo32f(v)); + +} + +/** This functions normalizes the given vector + * + * \param x the x component + * \param y the y component + */ +CvPoint2D32f mcvNormalizeVector(float x, float y) +{ + //return vector + return mcvNormalizeVector(cvPoint2D32f(x, y)); +} + + +/** This functions adds two vectors and returns the result + * + * \param v1 the first vector + * \param v2 the second vector + * \return the sum + */ +CvPoint2D32f mcvAddVector(CvPoint2D32f v1, CvPoint2D32f v2) +{ + //get sum + CvPoint2D32f sum = cvPoint2D32f(v1.x + v2.x, v1.y + v2.y); + //return vector + return sum; +} + + +/** This functions multiplies a vector by a scalar + * + * \param v the vector + * \param s the scalar + * \return the sum + */ +CvPoint2D32f mcvMultiplyVector(CvPoint2D32f v, float s) +{ + //get sum + CvPoint2D32f prod; + prod.x = v.x * s; + prod.y = v.y * s; + //return vector + return prod; +} + +/** This functions computes the score of the given spline from the + * input image + * + * \param image the input image + * \param spline the input spline + * \param h spline resolution + * \param jitterVal the amounts to count scores around the spline in x & y + * directions + * \param lengthRatio the ratio to add to score from the spline length + * \param angleRatio the ratio to add to score from spline curvature measure + * + * \return the score + */ +float mcvGetSplineScore(const CvMat* image, Spline& spline, float h, + int jitterVal, float lengthRatio, float angleRatio) +{ + + //check that all control points for spline are inside the image + CvSize size = cvSize(image->width-1, image->height-1); + // SHOW_SPLINE(spline, "spline"); + for (int i=0; i<=spline.degree; i++) + if (!mcvIsPointInside(spline.points[i], size)) + return -100.f; + + //get the pixels that belong to the spline + CvMat *pixels = mcvGetBezierSplinePixels(spline, h, size, false); + if(!pixels) + return -100.f; + + //get jitter vector + vectorjitter = mcvGetJitterVector(jitterVal); //2); + + //compute its score by summing up pixel values belonging to it + //int jitter[] = {0, 1, -1, 2, -2}, jitterLength = 5; + //SHOW_MAT(pixels, "pixels"); + float score = 0.f; + for (unsigned int j=0; jheight; i++) + { + //jitter in x + // int k = MIN(MAX(CV_MAT_ELEM(*pixels, int, i, 0)+ + // jitter[j], 0), image->width-1); + // fprintf(stderr, "col=%d\n & row=%d", k, CV_MAT_ELEM(*pixels, int, i, 1)); + score += cvGetReal2D(image, CV_MAT_ELEM(*pixels, int, i, 1), + MIN(MAX(CV_MAT_ELEM(*pixels, int, i, 0) + + jitter[j], 0), image->width-1)); + // //jitter the y + // score += cvGetReal2D(image, + // MIN(MAX(CV_MAT_ELEM(*pixels, int, i, 1)+ + // jitter[j], 0), image->height-1), + // CV_MAT_ELEM(*pixels, int, i, 0)); + } // for i + + //length: min 0 and max of 1 (normalized according to max of width and height + //of image) + //float length = ((float)pixels->height) / MAX(image->width, image->height); + float length = 0.f; + // for (int i=0; iheight-1; i++) + // { + // //get the vector between every two consecutive points + // CvPoint2D32f v = + // mcvSubtractVector(cvPoint2D32f(CV_MAT_ELEM(*pixels, int, i+1, 0), + // CV_MAT_ELEM(*pixels, int, i+1, 1)), + // cvPoint2D32f(CV_MAT_ELEM(*pixels, int, i, 0), + // CV_MAT_ELEM(*pixels, int, i, 1))); + // //add to length + // length += cvSqrt(v.x * v.x + v.y * v.y); + // } + //get length between first and last control point + CvPoint2D32f v = mcvSubtractVector(spline.points[0], spline.points[spline.degree]); + length = cvSqrt(v.x * v.x + v.y * v.y); + //normalize + length /= image->height; //MAX(image->width, image->height); + + //add measure of spline straightness: angle between vectors from points 1&2 and + //points 2&3: clsoer to 1 the better (straight) + //add 1 to value to make it range from 0->2 (2 better) + float angle = 0; + for (int i=0; i1 (with 1 best) + angle += 1; + angle /= 2; + + //add ratio of spline length + //score = .8*score + .4*pixels->height; //.8 & .3 + + // printf("angle = %f\n", angle); + //score = 0.6*score + 0.4*(angle*score); //.6 .4 + + //make 0 best and -1 worse + angle -= 1; + length -= 1; + + // printf("angle=%.2f, length=%.2f, score=%.2f", angle, length, score); + //add angle and length ratios + // angle = score*angle; //(1-angleRatio)*score + angleRatio*angle; //angle*score + // length = lengthRatio*length*score;//(1-lengthRatio)*score + lengthRatio*length*score; + // score = angle + length; + // score = score + angleRatio*angle*score + lengthRatio*length*score; + + if (DEBUG_LINES) + fprintf(stderr, "raw score=%.2f, angle=%.2f, length=%.2f, final=%.2f\n", + score, angle, length, score * + (1 + (angleRatio*angle + lengthRatio*length)/2)); + score = score * (1 + (angleRatio*angle + lengthRatio*length)/2); + // printf(" final score=%.2f\n", score); + + //clear pixels + cvReleaseMat(&pixels); + jitter.clear(); + + //return + return score; +} + + + +/** This functions returns a vector of jitter from the input maxJitter value + * This is used for computing spline scores for example, to get scores + * around the rasterization of the spline + * + * \param maxJitter the max value to look around + * + * \return the required vector of jitter values + */ +vector mcvGetJitterVector(int maxJitter) +{ + vector jitter(2*maxJitter+1); + + //fill in + jitter.push_back(0); + for(int i=1; i<=maxJitter; ++i) + { + jitter.push_back(i); + jitter.push_back(-i); + } + + //return + return jitter; +} + + +/** This functions gets the average direction of the set of points + * by computing the mean vector between points + * + * \param points the input points [Nx2] matrix + * \param forward go forward or backward in computation (default true) + * \return the mean direction + * + */ +CvPoint2D32f mcvGetPointsMeanVector(const CvMat *points, bool forward) +{ + CvPoint2D32f mean, v; + + //init + mean = cvPoint2D32f(0,0); + + //go forward direction + for (int i=1; iheight; ++i) + { + //get the vector joining the two points + v = cvPoint2D32f(CV_MAT_ELEM(*points, float, i, 0) - + CV_MAT_ELEM(*points, float, i-1, 0), + CV_MAT_ELEM(*points, float, i, 1) - + CV_MAT_ELEM(*points, float, i-1, 1)); + //normalize + v = mcvNormalizeVector(v); + //get mean + mean.x = (mean.x * (i-1) + v.x) / i; + mean.y = (mean.y * (i-1) + v.y) / i; + //renormlaize + mean = mcvNormalizeVector(mean); + } + + //check if to return forward or backward + if (!forward) + mean = cvPoint2D32f(-mean.x, -mean.y); + + return mean; +} + + +/** This functions checks if to merge two splines or not + * + * \param sp1 the first spline + * \param sp2 the second spline + * \param thetaThreshold Angle threshold for merging splines (radians) + * \param rThreshold R threshold (distance from origin) for merginn splines + * \param MeanhetaThreshold Mean angle threshold for merging splines (radians) + * \param MeanRThreshold Mean r threshold (distance from origin) for merginn + * splines + * \param centroidThreshold Distance threshold between spline cetroids for + * merging + * + * \return true if to merge, false otherwise + * + */ +bool mcvCheckMergeSplines(const Spline& sp1, const Spline& sp2, + float thetaThreshold, float rThreshold, + float meanThetaThreshold, float meanRThreshold, + float centroidThreshold) +{ + //get spline stats + CvPoint2D32f centroid1, centroid2; + float theta1, theta2, length1, length2, r1, r2; + float meanTheta1, meanTheta2, meanR1, meanR2; + mcvGetSplineFeatures(sp1, ¢roid1, &theta1, &r1, + &length1, &meanTheta1, &meanR1); + mcvGetSplineFeatures(sp2, ¢roid2, &theta2, &r2, + &length2, &meanTheta2, &meanR2); + + //threshold for difference in orientation + //float thetaThreshold = 30*CV_PI/180.; + //threshold for difference in centroid (squared) + //float centroidThreshold = 50; + //threshold for meanR + //float rThreshold = 15; + float meanThetaDist = fabs(meanTheta1 - meanTheta2);//fabs(theta1-theta2); + float meanRDist = fabs(meanR1 - meanR2); + float thetaDist = fabs(theta1 - theta2);//fabs(theta1-theta2); + float rDist = fabs(r1 - r2); + float centroidDist = fabs(mcvGetVectorNorm(mcvSubtractVector(centroid1, centroid2))); + + //correct theta diff + // thetaDist = thetaDist>CV_PI ? thetaDist-CV_PI : thetaDist; + // meanThetaDist = meanThetaDist>CV_PI ? meanThetaDist-CV_PI : + // meanThetaDist; + + bool meanThetaOk = meanThetaDist <= meanThetaThreshold; + bool meanROk = meanRDist <= meanRThreshold; + bool thetaOk = thetaDist <= thetaThreshold; + bool rOk = rDist <= rThreshold; + bool centroidOk = centroidDist <= centroidThreshold; + + bool centroidNotOk = centroidDist >= 200; + bool rNotOk = rDist >= 100; + bool thetaNotOk = thetaDist >= .8; + + bool merge = false; + //((thetaOk || meanThetaOk) && centroidOk) || + if ((thetaOk || meanThetaOk) && (rOk || meanROk || centroidOk) && + !rNotOk && !centroidNotOk && !thetaNotOk) + merge = true; + + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //show splines + // SHOW_SPLINE(sp1, "S1"); + // SHOW_SPLINE(sp2, "S2"); + fprintf(stderr, "%s: thetaDist=%.2f, meanThetaDist=%.2f, " + "rDist=%.2f, meanRDist=%.2f, centroidDist=%.2f\n", + merge? "Merged " : "Not merged", + thetaDist, meanThetaDist, rDist, meanRDist, centroidDist); + + fprintf(stderr, "\ttheta1=%.2f, theta2=%.2f\n", theta1, theta2); + + CvMat* im = cvCreateMat(480, 640, CV_8UC3); + cvSet(im, cvRealScalar(0.)); + //draw splines + mcvDrawSpline(im, sp1, CV_RGB(255, 0, 0), 1); + mcvDrawSpline(im, sp2, CV_RGB(0, 255, 0), 1); + SHOW_IMAGE(im, "Check Merge Splines", 10); + //clear + cvReleaseMat(&im); + + }//#endif + + //return + return merge; +} + +/** This functions computes some features for a set of points + * + * \param points the input points + * \param centroid the computed centroid of the points + * \param theta the major orientation of the points (angle of line joining + * first and last points, angle as in Hough Transform lines) + * \param r distance from origin for line from first to last point + * \param length the length of the line from first to last point + * \param meanTheta the average orientation of the points (by computing + * mean theta for line segments form the points) + * \param meanR the average distance from the origin of the points (the + * same computations as for meanTheta) + * \param curveness computes the angle between vectors of points, + * which gives an indication of the curveness of the spline + * -1-->1 with 1 best and -1 worst + * + */ +void mcvGetPointsFeatures(const CvMat* points, CvPoint2D32f* centroid, + float* theta, float* r, float* length, + float* meanTheta, float* meanR, float* curveness) +{ + + //get start and end point + CvPoint2D32f start = cvPoint2D32f(CV_MAT_ELEM(*points, float, 0, 0), + CV_MAT_ELEM(*points, float, 0, 1)); + CvPoint2D32f end = cvPoint2D32f(CV_MAT_ELEM(*points, float, + points->height-1, 0), + CV_MAT_ELEM(*points, float, + points->height-1, 1)); + //compute centroid + if (centroid) + { + //get sum of control points + *centroid = cvPoint2D32f(0, 0); + for (int i=0; i<=points->height; ++i) + *centroid = mcvAddVector(*centroid, + cvPoint2D32f(CV_MAT_ELEM(*points, float, i, 0), + CV_MAT_ELEM(*points, float, i, 1))); + //take mean + *centroid = cvPoint2D32f(centroid->x / (points->height), + centroid->y / (points->height)); + } + + //compute theta + if (theta && r) + { + //get line from first and last control points + Line line; + line.startPoint = start; + line.endPoint = end; + //get theta + //float r; + mcvLineXY2RTheta(line, *r, *theta); + //add pi if negative + if (*theta<0) + *theta += CV_PI; + } + + //mean theta + if (meanTheta && meanR) + { + *meanTheta = 0; + *meanR = 0; + + //loop and get theta + for (int i=0; iheight-1; i++) + { + //get the line + Line line; + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i, 0), + CV_MAT_ELEM(*points, float, i, 1)); + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i+1, 0), + CV_MAT_ELEM(*points, float, i+1, 1)); + //get theta and r + float r, t; + mcvLineXY2RTheta(line, r, t); + //add pi if neg + if (t<0) t += CV_PI; + //add + *meanTheta += t; + *meanR += r; + } + + //normalize + *meanTheta /= points->height - 1; + *meanR /= points->height - 1; + } + + //compute length of spline: length of vector between first and last point + if (length) + { + //get the vector + CvPoint2D32f v = mcvSubtractVector(start, end); + + //compute length + *length = cvSqrt(v.x * v.x + v.y * v.y); + } + + //compute curveness + if (curveness) + { + *curveness = 0; + if (points->height>2) + { + //initialize + CvPoint2D32f p0; + CvPoint2D32f p1 = start; + CvPoint2D32f p2 = cvPoint2D32f(CV_MAT_ELEM(*points, float, 1, 0), + CV_MAT_ELEM(*points, float, 1, 1)); + + for (int i=0; iheight-2; i++) + { + //go next + p0 = p1; + p1 = p2; + p2 = cvPoint2D32f(CV_MAT_ELEM(*points, float, i+2, 0), + CV_MAT_ELEM(*points, float, i+2, 1)); + //get first vector + CvPoint2D32f t1 = mcvNormalizeVector(mcvSubtractVector(p1, p0)); + + //get second vector + CvPoint2D32f t2 = mcvNormalizeVector (mcvSubtractVector(p2, p1)); + //get angle + *curveness += t1.x*t2.x + t1.y*t2.y; + } + //get mean + *curveness /= points->height-2; + } + } +} + + +/** This functions computes some features for the spline + * + * \param spline the input spline + * \param centroid the computed centroid of spline (mean of control points) + * \param theta the major orientation of the spline (angle of line joining + * first and last control points, angle as in Hough Transform lines) + * \param r distance from origin for line from first to last control point + * \param length the length of the line from first to last control point + * \param meanTheta the average orientation of the spline (by computing + * mean theta for line segments form the spline) + * \param meanR the average distance from the origin of the spline (the + * same computations as for meanTheta) + * \param curveness computes the angle between vectors of control points, + * which gives an indication of the curveness of the spline + * -1-->1 with 1 best and -1 worst + * + */ +void mcvGetSplineFeatures(const Spline& spline, CvPoint2D32f* centroid, + float* theta, float* r, float* length, + float* meanTheta, float* meanR, float* curveness) +{ + //compute centroid + if (centroid) + { + //get sum of control points + *centroid = cvPoint2D32f(0, 0); + for (int i=0; i<=spline.degree; ++i) + *centroid = mcvAddVector(*centroid, spline.points[i]); + //take mean + *centroid = cvPoint2D32f(centroid->x / (spline.degree+1), + centroid->y / (spline.degree+1)); + } + + //compute theta + if (theta && r) + { + //get line from first and last control points + Line line; + line.startPoint = spline.points[0]; + line.endPoint = spline.points[spline.degree]; + //get theta + //float r; + mcvLineXY2RTheta(line, *r, *theta); + //add pi if negative + //if (*theta<0) *theta += CV_PI; + + //compute theta as angle to the horizontal x-axis + *theta = mcvGetLineAngle(line); + } + + //mean theta + if (meanTheta && meanR) + { + *meanTheta = 0; + *meanR = 0; + //get points on the spline + CvMat* points = mcvEvalBezierSpline(spline, .1); + //loop and get theta + for (int i=0; iheight-1; i++) + { + //get the line + Line line; + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i, 0), + CV_MAT_ELEM(*points, float, i, 1)); + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i+1, 0), + CV_MAT_ELEM(*points, float, i+1, 1)); + //get theta and r + float r, t; + mcvLineXY2RTheta(line, r, t); + //add pi if neg + #warning "add pi to theta calculations for spline feature" + //if (t<0) t += CV_PI; + //add + t = mcvGetLineAngle(line); + *meanTheta += t; + *meanR += r; + } + + //normalize + *meanTheta /= points->height - 1; + *meanR /= points->height - 1; + + //clear + cvReleaseMat(&points); + } + + //compute length of spline: length of vector between first and last point + if (length) + { + //get the vector + CvPoint2D32f v = cvPoint2D32f(spline.points[0].x - + spline.points[spline.degree].x, + spline.points[0].y - + spline.points[spline.degree].y); + //compute length + *length = cvSqrt(v.x * v.x + v.y * v.y); + } + + //compute curveness + if (curveness) + { + *curveness = 0; + for (int i=0; irows; ++i) + { + //clear histogram + cvSet(hist, cvRealScalar(0)); + + //get the window indices + int xmin = MAX(cvRound(CV_MAT_ELEM(*points, float, i, 0)-window), 0); + int xmax = MIN(cvRound(CV_MAT_ELEM(*points, float, i, 0)+window), + im->cols); + int ymin = MAX(cvRound(CV_MAT_ELEM(*points, float, i, 1)-window), 0); + int ymax = MIN(cvRound(CV_MAT_ELEM(*points, float, i, 1)+window), + im->rows); + + //get mean for every channel + float r=0.f, g=0.f, b=0.f, rr, gg, bb; + int bin; + for (int x=xmin; x<=xmax; x++) + for (int y=ymin; y<=ymax; y++) + { + //get colors + rr = (im->data.ptr + im->step*y)[x*3]; + gg = (im->data.ptr + im->step*y)[x*3+1]; + bb = (im->data.ptr + im->step*y)[x*3+2]; + //add to totals + r += rr; + g += gg; + b += bb; + + if (rbf) + { + //compute histogram + bin = MIN((int)(rr / binWidth), numBins); + hist->data.fl[bin] ++; + bin = MIN((int)(gg / binWidth), numBins); + hist->data.fl[bin + numBins] ++; + bin = MIN((int)(bb / binWidth), numBins); + hist->data.fl[bin + 2*numBins] ++; + } + } + + //normalize + int num = (xmax-xmin+1) * (ymax-ymin+1); + r /= num; + g /= num; + b /= num; + + //now compute differences + float rg = r - g; + float gb = g - b; + float rb = r - b; + + //add differences to histogram + if (rbf) + { + hist->data.fl[hist->width-2] = fabs(rg); + hist->data.fl[hist->width-1] = fabs(gb); + hist->data.fl[hist->width] = fabs(rb); + + //compute output of RBF model + // + //add rest of terms + for (int j=0; jdata.fl[k] - + rbfCentroids[j*histLen + k]; + d += t*t; + } + + //compute product with weight + f += rbfWeights[j+1] * exp(-.5 * d /rbfSigma); + } + } + + //classify + bool yellow; + if (rbf) + yellow = f > rbfThreshold; + else + yellow = rg>rgMin && rggbMin && rb>rbMin; + if (yellow) + numYellow++; + + if (DEBUG_LINES) + fprintf(stderr, "%s: f=%f, rg=%.2f, gb=%.2f, rb=%.2f\n", + yellow? "YES" : "NO ", f, rg, gb, rb); + // fprintf(stderr, "Point: %d is %s\n", i, yellow ? "YELLOW" : "WHITE"); + // fprintf(stderr, "r=%f, g=%f, b=%f\n", r, g, b); + // fprintf(stderr, "rg=%f, gb=%f, rb=%f\n\n", rg, gb, rb); + } + + //classify line + LineColor clr = LINE_COLOR_WHITE; + if (numYellow > numYellowMin*points->rows) + clr = LINE_COLOR_YELLOW; + + //release + cvReleaseMat(&hist); + + return clr; +} + + +/** \brief This function extracts bounding boxes from splines + * + * \param splines vector of splines + * \param type the type of lines (LINE_HORIZONTAL or LINE_VERTICAL) + * \param size the size of image containing the lines + * \param boxes a vector of output bounding boxes + */ +void mcvGetSplinesBoundingBoxes(const vector &splines, LineType type, + CvSize size, vector &boxes) +{ + //copy lines to boxes + int start, end; + //clear + boxes.clear(); + switch(type) + { + case LINE_VERTICAL: + for(unsigned int i=0; i &lines, LineType type, + CvSize size, vector &boxes) +{ + //copy lines to boxes + int start, end; + //clear + boxes.clear(); + switch(type) + { + case LINE_VERTICAL: + for(unsigned int i=0; i &lines, vector &scores, + float wMu, float wSigma) +{ + //check if we have only 1, then exit + if (lines.size() <2) + return; + + //get distance between the lines assuming they are vertical + int numInLines = lines.size(); + vector rs; + for (int i=0; i::iterator ir, jr; + int i, j, maxi, maxj; + double score = 0., maxScore = -5.; + wSigma *= wSigma; + for (i=0, ir=rs.begin(); ir!=rs.end(); ir++, i++) + for (j=i+1, jr=ir+1; jr!=rs.end(); jr++, j++) + { + //get that score + score = fabs(*ir - *jr) - wMu; + score = exp(-.5 * score * score / wSigma); + + //check max + if (score >= maxScore) + { + maxScore = score; + maxi = i; + maxj = j; + + fprintf(stderr, "diff=%.5f score=%.10f i=%d j=%d\n", + *ir - *jr, score, maxi, maxj); + } + } // for j + + //now return the max and check threshold + vector newLines; + vector newScores; + + if (maxScore<.4) //.25 + { + maxi = scores[maxi] > scores[maxj] ? maxi : maxj; + newLines.push_back(lines[maxi]); + newScores.push_back(scores[maxi]); + } + //add both + else + { + newLines.push_back(lines[maxi]); + newLines.push_back(lines[maxj]); + newScores.push_back(scores[maxi]); + newScores.push_back(scores[maxj]); + } + lines = newLines; + scores = newScores; + + //clear + newLines.clear(); + newScores.clear(); + rs.clear(); +} + + +void dummy() +{ + +} + +} // namespace LaneDetector + diff --git a/src/LaneDetector.cc~ b/src/LaneDetector.cc~ new file mode 100755 index 0000000..fe08cf3 --- /dev/null +++ b/src/LaneDetector.cc~ @@ -0,0 +1,7274 @@ +/** + * \file LaneDetector.cc + * \author Mohamed Aly + * \date Thu 26 Jul, 2007 + * + */ + +#include "LaneDetector.hh" + +#include "mcv.hh" +#include "InversePerspectiveMapping.hh" +#include "LaneDetectorOpt.h" +#include "ranker.h" + +#include +#include +#include +#include +#include + +using namespace std; + +#include +#include + +namespace LaneDetector +{ + // used for debugging + int DEBUG_LINES = 0; + +/** + * This function filters the input image looking for horizontal + * or vertical lines with specific width or height. + * + * \param inImage the input image + * \param outImage the output image in IPM + * \param wx width of kernel window in x direction = 2*wx+1 + * (default 2) + * \param wy width of kernel window in y direction = 2*wy+1 + * (default 2) + * \param sigmax std deviation of kernel in x (default 1) + * \param sigmay std deviation of kernel in y (default 1) + * \param lineType type of the line + * LINE_HORIZONTAL (default) + * LINE_VERTICAL + */ + void mcvFilterLines(const CvMat *inImage, CvMat *outImage, + unsigned char wx, unsigned char wy, FLOAT sigmax, + FLOAT sigmay, LineType lineType) +{ + //define the two kernels + //this is for 7-pixels wide +// FLOAT_MAT_ELEM_TYPE derivp[] = {-2.328306e-10, -6.984919e-09, -1.008157e-07, -9.313226e-07, -6.178394e-06, -3.129616e-05, -1.255888e-04, -4.085824e-04, -1.092623e-03, -2.416329e-03, -4.408169e-03, -6.530620e-03, -7.510213e-03, -5.777087e-03, -5.777087e-04, 6.932504e-03, 1.372058e-02, 1.646470e-02, 1.372058e-02, 6.932504e-03, -5.777087e-04, -5.777087e-03, -7.510213e-03, -6.530620e-03, -4.408169e-03, -2.416329e-03, -1.092623e-03, -4.085824e-04, -1.255888e-04, -3.129616e-05, -6.178394e-06, -9.313226e-07, -1.008157e-07, -6.984919e-09, -2.328306e-10}; +// int derivLen = 35; +// FLOAT_MAT_ELEM_TYPE smoothp[] = {2.384186e-07, 5.245209e-06, 5.507469e-05, 3.671646e-04, 1.744032e-03, 6.278515e-03, 1.778913e-02, 4.066086e-02, 7.623911e-02, 1.185942e-01, 1.541724e-01, 1.681881e-01, 1.541724e-01, 1.185942e-01, 7.623911e-02, 4.066086e-02, 1.778913e-02, 6.278515e-03, 1.744032e-03, 3.671646e-04, 5.507469e-05, 5.245209e-06, 2.384186e-07}; +// int smoothLen = 23; + CvMat fx; + CvMat fy; + //create the convoultion kernel + switch (lineType) + { + case LINE_HORIZONTAL: + { + //this is for 5-pixels wide + FLOAT_MAT_ELEM_TYPE derivp[] = {-2.384186e-07, -4.768372e-06, -4.482269e-05, -2.622604e-04, -1.064777e-03, -3.157616e-03, -6.976128e-03, -1.136112e-02, -1.270652e-02, -6.776810e-03, 6.776810e-03, 2.156258e-02, 2.803135e-02, 2.156258e-02, 6.776810e-03, -6.776810e-03, -1.270652e-02, -1.136112e-02, -6.976128e-03, -3.157616e-03, -1.064777e-03, -2.622604e-04, -4.482269e-05, -4.768372e-06, -2.384186e-07}; + int derivLen = 25; + FLOAT_MAT_ELEM_TYPE smoothp[] = {2.384186e-07, 5.245209e-06, 5.507469e-05, 3.671646e-04, 1.744032e-03, 6.278515e-03, 1.778913e-02, 4.066086e-02, 7.623911e-02, 1.185942e-01, 1.541724e-01, 1.681881e-01, 1.541724e-01, 1.185942e-01, 7.623911e-02, 4.066086e-02, 1.778913e-02, 6.278515e-03, 1.744032e-03, 3.671646e-04, 5.507469e-05, 5.245209e-06, 2.384186e-07}; + int smoothLen = 23; + //horizontal is smoothing and vertical is derivative + fx = cvMat(1, smoothLen, FLOAT_MAT_TYPE, smoothp); + fy = cvMat(derivLen, 1, FLOAT_MAT_TYPE, derivp); + } + break; + + case LINE_VERTICAL: + { + //this is for 5-pixels wide + FLOAT_MAT_ELEM_TYPE derivp[] = //{1.000000e-11, 8.800000e-10, 3.531000e-08, 8.536000e-07, 1.383415e-05, 1.581862e-04, 1.306992e-03, 7.852691e-03, 3.402475e-02, 1.038205e-01, 2.137474e-01, 2.781496e-01, 2.137474e-01, 1.038205e-01, 3.402475e-02, 7.852691e-03, 1.306992e-03, 1.581862e-04, 1.383415e-05, 8.536000e-07, 3.531000e-08, 8.800000e-10, 1.000000e-11}; + //{1.000000e-06, 4.800000e-05, 9.660000e-04, 1.048000e-02, 6.529500e-02, 2.278080e-01, 3.908040e-01, 2.278080e-01, 6.529500e-02, 1.048000e-02, 9.660000e-04, 4.800000e-05, 1.000000e-06}; + {1.000000e-16, 1.280000e-14, 7.696000e-13, 2.886400e-11, 7.562360e-10, 1.468714e-08, 2.189405e-07, 2.558828e-06, 2.374101e-05, 1.759328e-04, 1.042202e-03, 4.915650e-03, 1.829620e-02, 5.297748e-02, 1.169560e-01, 1.918578e-01, 2.275044e-01, 1.918578e-01, 1.169560e-01, 5.297748e-02, 1.829620e-02, 4.915650e-03, 1.042202e-03, 1.759328e-04, 2.374101e-05, 2.558828e-06, 2.189405e-07, 1.468714e-08, 7.562360e-10, 2.886400e-11, 7.696000e-13, 1.280000e-14, 1.000000e-16}; + int derivLen = 33; //23; 13; 33; + FLOAT_MAT_ELEM_TYPE smoothp[] = {-1.000000e-03, -2.200000e-02, -1.480000e-01, -1.940000e-01, 7.300000e-01, -1.940000e-01, -1.480000e-01, -2.200000e-02, -1.000000e-03}; + //{-1.000000e-07, -5.400000e-06, -1.240000e-04, -1.561000e-03, -1.149400e-02, -4.787020e-02, -9.073680e-02, 2.144300e-02, 2.606970e-01, 2.144300e-02, -9.073680e-02, -4.787020e-02, -1.149400e-02, -1.561000e-03, -1.240000e-04, -5.400000e-06, -1.000000e-07}; + int smoothLen = 9; //9; 17; + //horizontal is derivative and vertical is smoothing + fy = cvMat(1, smoothLen, FLOAT_MAT_TYPE, smoothp); + fx = cvMat(derivLen, 1, FLOAT_MAT_TYPE, derivp); + } + break; + } + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //SHOW_MAT(kernel, "Kernel:"); + }//#endif + +#warning "still check subtracting mean from image" + //subtract mean + CvScalar mean = cvAvg(inImage); + cvSubS(inImage, mean, outImage); + + + //do the filtering + cvFilter2D(outImage, outImage, &fx); //inImage outImage + cvFilter2D(outImage, outImage, &fy); + + +// CvMat *deriv = cvCreateMat +// //define x +// CvMat *x = cvCreateMat(2*wx+1, 1, FLOAT_MAT_TYPE); +// //define y +// CvMat *y = cvCreateMat(2*wy+1, 1, FLOAT_MAT_TYPE); + +// //create the convoultion kernel +// switch (lineType) +// { +// case FILTER_LINE_HORIZONTAL: +// //guassian in x direction +// mcvGetGaussianKernel(x, wx, sigmax); +// //derivative of guassian in y direction +// mcvGet2DerivativeGaussianKernel(y, wy, sigmay); +// break; + +// case FILTER_LINE_VERTICAL: +// //guassian in y direction +// mcvGetGaussianKernel(y, wy, sigmay); +// //derivative of guassian in x direction +// mcvGet2DerivativeGaussianKernel(x, wx, sigmax); +// break; +// } + +// //combine the 2D kernel +// CvMat *kernel = cvCreateMat(2*wy+1, 2*wx+1, FLOAT_MAT_TYPE); +// cvGEMM(y, x, 1, 0, 1, kernel, CV_GEMM_B_T); + +// //subtract the mean +// CvScalar mean = cvAvg(kernel); +// cvSubS(kernel, mean, kernel); + +// #ifdef DEBUG_GET_STOP_LINES +// //SHOW_MAT(kernel, "Kernel:"); +// #endif + +// //do the filtering +// cvFilter2D(inImage, outImage, kernel); + +// //clean +// cvReleaseMat(&x); +// cvReleaseMat(&y); +// cvReleaseMat(&kernel); +} + +/** + * This function gets a 1-D gaussian filter with specified + * std deviation and range + * + * \param kernel input mat to hold the kernel (2*w+1x1) + * column vector (already allocated) + * \param w width of kernel is 2*w+1 + * \param sigma std deviation + */ +void mcvGetGaussianKernel(CvMat *kernel, unsigned char w, FLOAT sigma) +{ + //get variance + sigma *= sigma; + + //get the kernel + for (double i=-w; i<=w; i++) + CV_MAT_ELEM(*kernel, FLOAT_MAT_ELEM_TYPE, int(i+w), 0) = + (FLOAT_MAT_ELEM_TYPE) exp(-(.5/sigma)*(i*i)); +} + +/** + * This function gets a 1-D second derivative gaussian filter + * with specified std deviation and range + * + * \param kernel input mat to hold the kernel (2*w+1x1) + * column vector (already allocated) + * \param w width of kernel is 2*w+1 + * \param sigma std deviation + */ +void mcvGet2DerivativeGaussianKernel(CvMat *kernel, + unsigned char w, FLOAT sigma) +{ + //get variance + sigma *= sigma; + + //get the kernel + for (double i=-w; i<=w; i++) + CV_MAT_ELEM(*kernel, FLOAT_MAT_ELEM_TYPE, int(i+w), 0) = + (FLOAT_MAT_ELEM_TYPE) + (exp(-.5*i*i)/sigma - (i*i)*exp(-(.5/sigma)*i*i)/(sigma*sigma)); +} + + +/** This function groups the input filtered image into + * horizontal or vertical lines. + * + * \param inImage input image + * \param lines returned detected lines (vector of points) + * \param lineScores scores of the detected lines (vector of floats) + * \param lineType type of lines to detect + * LINE_HORIZONTAL (default) or LINE_VERTICAL + * \param linePixelWidth width (or height) of lines to detect + * \param localMaxima whether to detect local maxima or just get + * the maximum + * \param detectionThreshold threshold for detection + * \param smoothScores whether to smooth scores detected or not + */ +void mcvGetHVLines(const CvMat *inImage, vector *lines, + vector *lineScores, LineType lineType, + FLOAT linePixelWidth, bool binarize, bool localMaxima, + FLOAT detectionThreshold, bool smoothScores) +{ + CvMat * image = cvCloneMat(inImage); + //binarize input image if to binarize + if (binarize) + { + //mcvBinarizeImage(image); + image = cvCreateMat(inImage->rows, inImage->cols, INT_MAT_TYPE); + cvThreshold(inImage, image, 0, 1, CV_THRESH_BINARY); //0.05 + } + + //get sum of lines through horizontal or vertical + //sumLines is a column vector + CvMat sumLines, *sumLinesp; + int maxLineLoc = 0; + switch (lineType) + { + case LINE_HORIZONTAL: + sumLinesp = cvCreateMat(image->height, 1, FLOAT_MAT_TYPE); + cvReduce(image, sumLinesp, 1, CV_REDUCE_SUM); //_AVG + cvReshape(sumLinesp, &sumLines, 0, 0); + //max location for a detected line + maxLineLoc = image->height-1; + break; + case LINE_VERTICAL: + sumLinesp = cvCreateMat(1, image->width, FLOAT_MAT_TYPE); + cvReduce(image, sumLinesp, 0, CV_REDUCE_SUM); //_AVG + cvReshape(sumLinesp, &sumLines, 0, image->width); + //max location for a detected line + maxLineLoc = image->width-1; + break; + } + //SHOW_MAT(&sumLines, "sumLines:"); + + //smooth it + + float smoothp[] = { + 0.000003726653172, 0.000040065297393, 0.000335462627903, 0.002187491118183, + 0.011108996538242, 0.043936933623407, 0.135335283236613, 0.324652467358350, + 0.606530659712633, 0.882496902584595, 1.000000000000000, 0.882496902584595, + 0.606530659712633, 0.324652467358350, 0.135335283236613, 0.043936933623407, + 0.011108996538242, 0.002187491118183, 0.000335462627903, 0.000040065297393, + 0.000003726653172}; +// {0.000004,0.000010,0.000025,0.000063,0.000148,0.000335,0.000732,0.001534,0.003089,0.005976,0.011109,0.019841,0.034047,0.056135,0.088922,0.135335,0.197899,0.278037,0.375311,0.486752,0.606531,0.726149,0.835270,0.923116,0.980199,1.000000,0.980199,0.923116,0.835270,0.726149,0.606531,0.486752,0.375311,0.278037,0.197899,0.135335,0.088922,0.056135,0.034047,0.019841,0.011109,0.005976,0.003089,0.001534,0.000732,0.000335,0.000148,0.000063,0.000025,0.000010,0.000004}; + int smoothWidth = 21; //21; 51; + CvMat smooth = cvMat(1, smoothWidth, CV_32FC1, smoothp); + if (smoothScores) + cvFilter2D(&sumLines, &sumLines, &smooth); +// SHOW_MAT(&sumLines, "sumLines:"); + + + + //get the max and its location + vector sumLinesMaxLoc; + vector sumLinesMax; + int maxLoc; double max; + //TODO: put the ignore in conf + #define MAX_IGNORE 0 //(int(smoothWidth/2.)+1) + #define LOCAL_MAX_IGNORE (int(MAX_IGNORE/4)) + mcvGetVectorMax(&sumLines, &max, &maxLoc, MAX_IGNORE); + + //put the local maxima stuff here + if (localMaxima) + { + //loop to get local maxima + for(int i=1+LOCAL_MAX_IGNORE; i CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, i-1, 0)) + && (val > CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, i+1, 0)) + // && (i != maxLoc) + && (val >= detectionThreshold) ) + { + //iterators for the two vectors + vector::iterator j; + vector::iterator k; + //loop till we find the place to put it in descendingly + for(j=sumLinesMax.begin(), k=sumLinesMaxLoc.begin(); + j != sumLinesMax.end() && val<= *j; j++,k++); + //add its index + sumLinesMax.insert(j, val); + sumLinesMaxLoc.insert(k, i); + } + } + } + + //check if didnt find local maxima + if(sumLinesMax.size()==0 && max>detectionThreshold) + { + //put maximum + sumLinesMaxLoc.push_back(maxLoc); + sumLinesMax.push_back(max); + } + +// //sort it descendingly +// sort(sumLinesMax.begin(), sumLinesMax.end(), greater()); +// //sort the indices +// for (int i=0; i<(int)sumLinesMax.size(); i++) +// for (int j=i; j<(int)sumLinesMax.size(); j++) +// if(sumLinesMax[i] == CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, +// sumLinesMaxLoc[j], 0)) +// { +// int k = sumLinesMaxLoc[j]; +// sumLinesMaxLoc[j] = sumLinesMaxLoc[i]; +// sumLinesMaxLoc[i] = k; +// } +// //sort(sumLinesMaxLoc.begin(), sumLinesMaxLoc.end(), greater()); + + //plot the line scores and the local maxima + //if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES +// gnuplot_ctrl *h = mcvPlotMat1D(NULL, &sumLines, "Line Scores"); +// CvMat *y = mcvVector2Mat(sumLinesMax); +// CvMat *x = mcvVector2Mat(sumLinesMaxLoc); +// mcvPlotMat2D(h, x, y); +// //gnuplot_plot_xy(h, (double*)&sumLinesMaxLoc,(double*)&sumLinesMax, sumLinesMax.size(),""); +// cin.get(); +// gnuplot_close(h); +// cvReleaseMat(&x); +// cvReleaseMat(&y); +//}//#endif + //process the found maxima + for (int i=0; i<(int)sumLinesMax.size(); i++) + { + //get subpixel accuracy + double maxLocAcc = mcvGetLocalMaxSubPixel( + CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, MAX(sumLinesMaxLoc[i]-1,0), 0), + CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, sumLinesMaxLoc[i], 0), + CV_MAT_ELEM(sumLines, FLOAT_MAT_ELEM_TYPE, + MIN(sumLinesMaxLoc[i]+1,maxLineLoc), 0) ); + maxLocAcc += sumLinesMaxLoc[i]; + maxLocAcc = MIN(MAX(0, maxLocAcc), maxLineLoc); + + + //TODO: get line extent + + //put the extracted line + Line line; + switch (lineType) + { + case LINE_HORIZONTAL: + line.startPoint.x = 0.5; + line.startPoint.y = (FLOAT)maxLocAcc + .5;//sumLinesMaxLoc[i]+.5; + line.endPoint.x = inImage->width-.5; + line.endPoint.y = line.startPoint.y; + break; + case LINE_VERTICAL: + line.startPoint.x = (FLOAT)maxLocAcc + .5;//sumLinesMaxLoc[i]+.5; + line.startPoint.y = .5; + line.endPoint.x = line.startPoint.x; + line.endPoint.y = inImage->height-.5; + break; + } + (*lines).push_back(line); + if (lineScores) + (*lineScores).push_back(sumLinesMax[i]); + }//for + + if(DEBUG_LINES) + {//#ifdef DEBUG_GET_STOP_LINES + CvMat *im, *im2 = cvCloneMat(image); + if (binarize) + cvConvertScale(im2, im2, 255, 0); + + if (binarize) + im = cvCreateMat(image->rows, image->cols, CV_8UC3); + else + im = cvCreateMat(image->rows, image->cols, CV_32FC3); + mcvScaleMat(im2, im2); + cvCvtColor(im2, im, CV_GRAY2RGB); + for (unsigned int i=0; isize(); i++) + { + Line line = (*lines)[i]; + mcvIntersectLineWithBB(&line, cvSize(image->cols, image->rows), &line); + if (binarize) + mcvDrawLine(im, line, CV_RGB(255,0,0), 1); + else + mcvDrawLine(im, line, CV_RGB(1,0,0), 1); + } + + char str[256]; + switch (lineType) + { + case LINE_HORIZONTAL: + sprintf(str, "%s", "Horizontal Lines"); + break; + case LINE_VERTICAL: + sprintf(str, "%s", "Vertical Lines"); + break; + } + SHOW_IMAGE(im, str, 10); + cvReleaseMat(&im); + cvReleaseMat(&im2); + } + + //clean + cvReleaseMat(&sumLinesp); + //cvReleaseMat(&smooth); + sumLinesMax.clear(); + sumLinesMaxLoc.clear(); + cvReleaseMat(&image); +} + + +/** This function detects lines in images using Hough transform + * + * \param inImage input image + * \param lines vector of lines to hold the results + * \param lineScores scores of the detected lines (vector of floats) + * \param rMin minimum r use for finding the lines (default 0) + * \param rMax maximum r to find (default max(size(im))) + * \param rStep step to use for binning (default is 2) + * \param thetaMin minimum angle theta to look for (default 0) all in radians + * \param thetaMax maximum angle theta to look for (default 2*pi) + * \param thetaStep step to use for binning theta (default 5) + * \param binarize if to binarize the input image or use the raw values so that + * non-zero values are not treated as equal + * \param localMaxima whether to detect local maxima or just get + * the maximum + * \param detectionThreshold threshold for detection + * \param smoothScores whether to smooth scores detected or not + * \param group whether to group nearby detections (1) or not (0 default) + * \param groupThreshold the minimum distance used for grouping (default 10) + */ + +void mcvGetHoughTransformLines(const CvMat *inImage, vector *lines, + vector *lineScores, + FLOAT rMin, FLOAT rMax, FLOAT rStep, + FLOAT thetaMin, FLOAT thetaMax, + FLOAT thetaStep, bool binarize, bool localMaxima, + FLOAT detectionThreshold, bool smoothScores, + bool group, FLOAT groupThreshold) +{ + CvMat *image; + + //binarize input image if to binarize + if (!binarize) + { + image = cvCloneMat(inImage); assert(image!=0); + // mcvBinarizeImage(image); + } + //binarize input image + else + { + image = cvCreateMat(inImage->rows, inImage->cols, INT_MAT_TYPE); + cvThreshold(inImage, image, 0, 1, CV_THRESH_BINARY); //0.05 + //get max of image + //double maxval, minval; + //cvMinMaxLoc(inImage, &minval, &maxval); + //cout << "Max = " << maxval << "& Min=" << minval << "\n"; + //CvScalar mean = cvAvg(inImage); + //cout << "Mean=" << mean.val[0] << "\n"; + } + + if(DEBUG_LINES) + {//#ifdef DEBUG_GET_STOP_LINES + SHOW_IMAGE(image, "Hough thresholded image", 10); + }//#endif + + //define the accumulator array: rows correspond to r and columns to theta + int rBins = int((rMax-rMin)/rStep); + int thetaBins = int((thetaMax-thetaMin)/thetaStep); + CvMat *houghSpace = cvCreateMat(rBins, thetaBins, CV_MAT_TYPE(image->type)); //FLOAT_MAT_TYPE); + assert(houghSpace!=0); + //init to zero + cvSet(houghSpace, cvRealScalar(0)); + + //init values of r and theta + FLOAT *rs = new FLOAT[rBins]; + FLOAT *thetas = new FLOAT[thetaBins]; + FLOAT r, theta; + int ri, thetai; + for (r=rMin+rStep/2, ri=0 ; riwidth; i++) + for (int j=0; jheight; j++) + if ( cvGetReal2D(image, j, i) ) + { + CV_MAT_ELEM(*nzPoints, int, idx, 0) = i; + CV_MAT_ELEM(*nzPoints, int, idx, 1) = j; + idx++; + } + + //calculate r values for all theta and all points + //CvMat *rPoints = cvCreateMat(image->width*image->height, thetaBins, CV_32SC1);//FLOAT_MAT_TYPE) + //CvMat *rPoints = cvCreateMat(nzCount, thetaBins, CV_32SC1);//FLOAT_MAT_TYPE); + //cvSet(rPoints, cvRealScalar(-1)); + //loop on x + //float x=0.5, y=0.5; + int i, k; //j + for (i=0; i=0 && rwidth; i++) //x=0; x++ +// //loop on y +// for (j=0; jheight; j++) //y=0 y++ +// //loop on theta +// for (k=0; krBins-1) +// if (lineConf->binarize && CV_MAT_ELEM(*image, INT_MAT_ELEM_TYPE, j, i) !=0) +// inside = true; +// else if (!lineConf->binarize && CV_MAT_ELEM(*image, FLOAT_MAT_ELEM_TYPE, +// j, i) !=0) +// inside = true; +// else +// inside = false; +// if (inside) +// { +// theta = thetas[k]; +// float rval = i * cos(theta) + j * sin(theta); //x y +// CV_MAT_ELEM(*rPoints, int, +// i*image->height + j, k) = +// (int)( ( rval - lineConf->rMin) / lineConf->rStep); +// } + +// } + +// SHOW_MAT(rPoints, "rPoints"); +// cin.get(); + + //now we should accumulate the values into the approprate bins in the houghSpace +// for (ri=0; riwidth; i++) +// for (j=0; jheight; j++) +// { +// //check if this cell belongs to that bin or not +// if (CV_MAT_ELEM(*rPoints, int, +// i*image->height + j , thetai)==ri) +// { +// if(lineConf->binarize) +// CV_MAT_ELEM(*houghSpace, INT_MAT_ELEM_TYPE, ri, thetai)++; +// //CV_MAT_ELEM(*image, INT_MAT_ELEM_TYPE, j, i); +// else +// CV_MAT_ELEM(*houghSpace, FLOAT_MAT_ELEM_TYPE, ri, thetai)+= +// CV_MAT_ELEM(*image, FLOAT_MAT_ELEM_TYPE, j, i); +// } +// } + + + //smooth hough transform + if (smoothScores) + cvSmooth(houghSpace, houghSpace, CV_GAUSSIAN, 3); + + //get local maxima + vector maxLineScores; + vector maxLineLocs; + if (localMaxima) + { + //get local maxima in the hough space + mcvGetMatLocalMax(houghSpace, maxLineScores, maxLineLocs, detectionThreshold); + } + else + { + //get the maxima above the threshold + mcvGetMatMax(houghSpace, maxLineScores, maxLineLocs, detectionThreshold); + } + + //get the maximum value + double maxLineScore; + CvPoint maxLineLoc; + cvMinMaxLoc(houghSpace, 0, &maxLineScore, 0, &maxLineLoc); + if (maxLineScores.size()==0 && maxLineScore>=detectionThreshold) + { + maxLineScores.push_back(maxLineScore); + maxLineLocs.push_back(maxLineLoc); + } + + + if(DEBUG_LINES) + {//#ifdef DEBUG_GET_STOP_LINES + // cout << "Local maxima = " << maxLineScores.size() << "\n"; + + { + CvMat *im, *im2 = cvCloneMat(image); + if (binarize) + cvConvertScale(im2, im2, 255, 0); + + if (binarize) + im = cvCreateMat(image->rows, image->cols, CV_8UC3);//cvCloneMat(image); + else + im = cvCreateMat(image->rows, image->cols, CV_32FC3); + cvCvtColor(im2, im, CV_GRAY2RGB); + for (int i=0; i<(int)maxLineScores.size(); i++) + { + Line line; + assert(maxLineLocs[i].x>=0 && maxLineLocs[i].x=0 && maxLineLocs[i].ycols, image->rows), &line); + if (binarize) + mcvDrawLine(im, line, CV_RGB(255,0,0), 1); + else + mcvDrawLine(im, line, CV_RGB(1,0,0), 1); + } + SHOW_IMAGE(im, "Hough before grouping", 10); + cvReleaseMat(&im); + cvReleaseMat(&im2); + + // //debug + // cout << "Maxima detected:\n"; + // for(int ii=0; ii<(int)maxLineScores.size(); ii++) + // cout << " " << maxLineScores[ii]; + // cout << "\n"; + } + }//#endif + + //group detected maxima + if (group && maxLineScores.size()>1) + { + //flag for stopping + bool stop = false; + while (!stop) + { + //minimum distance so far + float minDist = groupThreshold+5, dist = 0.; + vector::iterator iloc, jloc, + minIloc=maxLineLocs.begin(), minJloc=minIloc+1; + vector::iterator iscore, jscore, minIscore, minJscore; + //compute pairwise distance between detected maxima + for (iloc=maxLineLocs.begin(), iscore=maxLineScores.begin(); + iloc!=maxLineLocs.end(); iloc++, iscore++) + for (jscore=iscore+1, jloc=iloc+1; jscore!=maxLineScores.end(); + jloc++, jscore++) + { + //add pi if neg + float t1 = thetas[iloc->x]<0 ? thetas[iloc->x] : thetas[iloc->x]+CV_PI; + float t2 = thetas[jloc->x]<0 ? thetas[jloc->x] : thetas[jloc->x]+CV_PI; + //get distance + dist = fabs(rs[iloc->y]-rs[jloc->y]) + + 0.1 * fabs(t1 - t2);//fabs(thetas[iloc->x]-thetas[jloc->x]); + //check if minimum + if (dist= groupThreshold) + stop = true; + else + { + // //debug + // cout << "Before grouping:\n"; + // for(int ii=0; ii<(int)maxLineScores.size(); ii++) + // cout << " " << maxLineScores[ii]; + // cout << "\n"; + + //combine the two minimum ones with weighted average of + //their scores + double x = (minIloc->x * *minIscore + minJloc->x * *minJscore) / + (*minIscore + *minJscore); + double y = (minIloc->y * *minIscore + minJloc->y * *minJscore) / + (*minIscore + *minJscore); + //put into the first + minIloc->x = (int)x;// ((minJloc->x + minJloc->x)/2.0); // (int) x; + minIloc->y = (int)y;// ((minJloc->y + minIloc->y)/2.0); // (int) y; + *minIscore = (*minJscore + *minIscore)/2;///2; + //delete second one + maxLineLocs.erase(minJloc); + maxLineScores.erase(minJscore); + + // //debug + // cout << "Before sorting:\n"; + // for(int ii=0; ii<(int)maxLineScores.size(); ii++) + // cout << " " << maxLineScores[ii]; + // cout << "\n"; + + //check if to put somewhere else depending on the changed score + for (iscore=maxLineScores.begin(), iloc=maxLineLocs.begin(); + iscore!=maxLineScores.end() && *minIscore <= *iscore; + iscore++, iloc++); + //swap the original location if different + if (iscore!=minIscore ) + { + //insert in new position + maxLineScores.insert(iscore, *minIscore); + maxLineLocs.insert(iloc, *minIloc); + //delte old + maxLineScores.erase(minIscore); + maxLineLocs.erase(minIloc); + // //if at end, then back up one position + // if(iscore == maxLineScores.end()) + // { + // iscore--; + // iloc--; + // } + // CvPoint tloc; + // double tscore; + // //swap + // tloc = *minIloc; + // tscore = *minIscore; + + // *minIloc = *iloc; + // *minIscore = *iscore; + + // *iloc = tloc; + // *iscore = tscore; + } + // cout << "after sorting:\n"; + // for(int ii=0; ii<(int)maxLineScores.size(); ii++) + // cout << " " << maxLineScores[ii]; + // cout << "\n"; + } + } + } + + if(DEBUG_LINES) //#ifdef DEBUG_GET_STOP_LINES + { + CvMat *im, *im2 = cvCloneMat(image); + if (binarize) + cvConvertScale(im2, im2, 255, 0); + if (binarize) + im = cvCreateMat(image->rows, image->cols, CV_8UC3);//cvCloneMat(image); + else + im = cvCreateMat(image->rows, image->cols, CV_32FC3); + cvCvtColor(im2, im, CV_GRAY2RGB); + for (int i=0; i<(int)maxLineScores.size(); i++) + { + Line line; + assert(maxLineLocs[i].x>=0 && maxLineLocs[i].x=0 && maxLineLocs[i].ycols, image->rows), &line); + if (binarize) + mcvDrawLine(im, line, CV_RGB(255,0,0), 1); + else + mcvDrawLine(im, line, CV_RGB(1,0,0), 1); + } + SHOW_IMAGE(im, "Hough after grouping", 10); + cvReleaseMat(&im); + cvReleaseMat(&im2); + + //put local maxima in image + CvMat *houghSpaceClr; + if(binarize) + houghSpaceClr = cvCreateMat(houghSpace->height, houghSpace->width, + CV_8UC3); + else + houghSpaceClr = cvCreateMat(houghSpace->height, houghSpace->width, + CV_32FC3); + mcvScaleMat(houghSpace, houghSpace); + cvCvtColor(houghSpace, houghSpaceClr, CV_GRAY2RGB); + for (int i=0; i<(int)maxLineLocs.size(); i++) + cvCircle(houghSpaceClr, cvPoint(maxLineLocs[i].x, maxLineLocs[i].y), + 1, CV_RGB(1, 0, 0), -1); + // if(lineConf->binarize) + // CV_MAT_ELEM(*houghSpace, unsigned char, maxLineLocs[i].y, + // maxLineLocs[i].x) = 255; + // else + // CV_MAT_ELEM(*houghSpace, float, maxLineLocs[i].y, maxLineLocs[i].x) = 1.f; + //show the hough space + SHOW_IMAGE(houghSpaceClr, "Hough Space", 10); + cvReleaseMat(&houghSpaceClr); + //SHOW_MAT(houghSpace, "Hough Space:"); + }//#endif + + //process detected maxima and return detected line(s) + for(int i=0; i=detectionThreshold) + { + //get sub-pixel accuracy + // + //get the two end points from the r-theta + Line line; + assert(maxLineLocs[i].x>=0 && maxLineLocs[i].x=0 && maxLineLocs[i].ycols, image->rows), &line); + //get line extent + //put the extracted line + lines->push_back(line); + if (lineScores) + (*lineScores).push_back(maxLineScores[i]); + + } + //not above threshold + else + { + //exit out of the for loop, as the scores are sorted descendingly + break; + } + } + + //clean up + cvReleaseMat(&image); + cvReleaseMat(&houghSpace); + //cvReleaseMat(&rPoints); + delete [] rs; + delete [] thetas; + maxLineScores.clear(); + maxLineLocs.clear(); +} + + + +/** This function binarizes the input image i.e. nonzero elements + * becomen 1 and others are 0. + * + * \param inImage input & output image + */ +void mcvBinarizeImage(CvMat *inImage) +{ + + if (CV_MAT_TYPE(inImage->type)==FLOAT_MAT_TYPE) + { + for (int i=0; iheight; i++) + for (int j=0; jwidth; j++) + if (CV_MAT_ELEM(*inImage, FLOAT_MAT_ELEM_TYPE, i, j) != 0.f) + CV_MAT_ELEM(*inImage, FLOAT_MAT_ELEM_TYPE, i, j)=1; + } + else if (CV_MAT_TYPE(inImage->type)==INT_MAT_TYPE) + { + for (int i=0; iheight; i++) + for (int j=0; jwidth; j++) + if (CV_MAT_ELEM(*inImage, INT_MAT_ELEM_TYPE, i, j) != 0) + CV_MAT_ELEM(*inImage, INT_MAT_ELEM_TYPE, i, j)=1; + } + else + { + cerr << "Unsupported type in mcvBinarizeImage\n"; + exit(1); + } +} + + +/** This function gets the maximum value in a vector (row or column) + * and its location + * + * \param inVector the input vector + * \param max the output max value + * \param maxLoc the location (index) of the first max + * + */ +#define MCV_VECTOR_MAX(type) \ + /*row vector*/ \ + if (inVector->height==1) \ + { \ + /*initial value*/ \ + tmax = (double) CV_MAT_ELEM(*inVector, type, 0, inVector->width-1); \ + tmaxLoc = inVector->width-1; \ + /*loop*/ \ + for (int i=inVector->width-1-ignore; i>=0+ignore; i--) \ + { \ + if (tmaxheight-1, 0); \ + tmaxLoc = inVector->height-1; \ + /*loop*/ \ + for (int i=inVector->height-1-ignore; i>=0+ignore; i--) \ + { \ + if (tmaxtype)==FLOAT_MAT_TYPE) + { + MCV_VECTOR_MAX(FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inVector->type)==INT_MAT_TYPE) + { + MCV_VECTOR_MAX(INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetVectorMax\n"; + exit(1); + } + + //return + if (max) + *max = tmax; + if (maxLoc) + *maxLoc = tmaxLoc; +} + + + +/** This function gets the local maxima in a matrix and their positions + * and its location + * + * \param inMat input matrix + * \param localMaxima the output vector of local maxima + * \param localMaximaLoc the vector of locations of the local maxima, + * where each location is cvPoint(x=col, y=row) zero-based + * + */ + +void mcvGetMatLocalMax(const CvMat *inMat, vector &localMaxima, + vector &localMaximaLoc, double threshold) +{ + + double val; + +#define MCV_MAT_LOCAL_MAX(type) \ + /*loop on the matrix and get points that are larger than their*/ \ + /*neighboring 8 pixels*/ \ + for(int i=1; irows-1; i++) \ + for (int j=1; jcols-1; j++) \ + { \ + /*get the current value*/ \ + val = CV_MAT_ELEM(*inMat, type, i, j); \ + /*check if it's larger than all its neighbors*/ \ + if( val > CV_MAT_ELEM(*inMat, type, i-1, j-1) && \ + val > CV_MAT_ELEM(*inMat, type, i-1, j) && \ + val > CV_MAT_ELEM(*inMat, type, i-1, j+1) && \ + val > CV_MAT_ELEM(*inMat, type, i, j-1) && \ + val > CV_MAT_ELEM(*inMat, type, i, j+1) && \ + val > CV_MAT_ELEM(*inMat, type, i+1, j-1) && \ + val > CV_MAT_ELEM(*inMat, type, i+1, j) && \ + val > CV_MAT_ELEM(*inMat, type, i+1, j+1) && \ + val >= threshold) \ + { \ + /*found a local maxima, put it in the return vector*/ \ + /*in decending order*/ \ + /*iterators for the two vectors*/ \ + vector::iterator k; \ + vector::iterator l; \ + /*loop till we find the place to put it in descendingly*/ \ + for(k=localMaxima.begin(), l=localMaximaLoc.begin(); \ + k != localMaxima.end() && val<= *k; k++,l++); \ + /*add its index*/ \ + localMaxima.insert(k, val); \ + localMaximaLoc.insert(l, cvPoint(j, i)); \ + } \ + } + + //check type + if (CV_MAT_TYPE(inMat->type)==FLOAT_MAT_TYPE) + { + MCV_MAT_LOCAL_MAX(FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==INT_MAT_TYPE) + { + MCV_MAT_LOCAL_MAX(INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetMatLocalMax\n"; + exit(1); + } +} + +/** This function gets the locations and values of all points + * above a certain threshold + * + * \param inMat input matrix + * \param maxima the output vector of maxima + * \param maximaLoc the vector of locations of the maxima, + * where each location is cvPoint(x=col, y=row) zero-based + * + */ + +void mcvGetMatMax(const CvMat *inMat, vector &maxima, + vector &maximaLoc, double threshold) +{ + + double val; + +#define MCV_MAT_MAX(type) \ + /*loop on the matrix and get points that are larger than their*/ \ + /*neighboring 8 pixels*/ \ + for(int i=1; irows-1; i++) \ + for (int j=1; jcols-1; j++) \ + { \ + /*get the current value*/ \ + val = CV_MAT_ELEM(*inMat, type, i, j); \ + /*check if it's larger than threshold*/ \ + if (val >= threshold) \ + { \ + /*found a maxima, put it in the return vector*/ \ + /*in decending order*/ \ + /*iterators for the two vectors*/ \ + vector::iterator k; \ + vector::iterator l; \ + /*loop till we find the place to put it in descendingly*/ \ + for(k=maxima.begin(), l=maximaLoc.begin(); \ + k != maxima.end() && val<= *k; k++,l++); \ + /*add its index*/ \ + maxima.insert(k, val); \ + maximaLoc.insert(l, cvPoint(j, i)); \ + } \ + } + + //check type + if (CV_MAT_TYPE(inMat->type)==FLOAT_MAT_TYPE) + { + MCV_MAT_MAX(FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==INT_MAT_TYPE) + { + MCV_MAT_MAX(INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetMatMax\n"; + exit(1); + } +} + + +/** This function gets the local maxima in a vector and their positions + * + * \param inVec input vector + * \param localMaxima the output vector of local maxima + * \param localMaximaLoc the vector of locations of the local maxima, + * + */ +#define MCV_VECTOR_LOCAL_MAX(type) \ + /*loop on the vector and get points that are larger than their*/ \ + /*neighboring points*/ \ + if(inVec->height == 1) \ + { \ + for(int i=1; iwidth-1; i++) \ + { \ + /*get the current value*/ \ + val = CV_MAT_ELEM(*inVec, type, 0, i); \ + /*check if it's larger than all its neighbors*/ \ + if( val > CV_MAT_ELEM(*inVec, type, 0, i-1) && \ + val > CV_MAT_ELEM(*inVec, type, 0, i+1) ) \ + { \ + /*found a local maxima, put it in the return vector*/ \ + /*in decending order*/ \ + /*iterators for the two vectors*/ \ + vector::iterator k; \ + vector::iterator l; \ + /*loop till we find the place to put it in descendingly*/ \ + for(k=localMaxima.begin(), l=localMaximaLoc.begin(); \ + k != localMaxima.end() && val<= *k; k++,l++); \ + /*add its index*/ \ + localMaxima.insert(k, val); \ + localMaximaLoc.insert(l, i); \ + } \ + } \ + } \ + else \ + { \ + for(int i=1; iheight-1; i++) \ + { \ + /*get the current value*/ \ + val = CV_MAT_ELEM(*inVec, type, i, 0); \ + /*check if it's larger than all its neighbors*/ \ + if( val > CV_MAT_ELEM(*inVec, type, i-1, 0) && \ + val > CV_MAT_ELEM(*inVec, type, i+1, 0) ) \ + { \ + /*found a local maxima, put it in the return vector*/ \ + /*in decending order*/ \ + /*iterators for the two vectors*/ \ + vector::iterator k; \ + vector::iterator l; \ + /*loop till we find the place to put it in descendingly*/ \ + for(k=localMaxima.begin(), l=localMaximaLoc.begin(); \ + k != localMaxima.end() && val<= *k; k++,l++); \ + /*add its index*/ \ + localMaxima.insert(k, val); \ + localMaximaLoc.insert(l, i); \ + } \ + } \ + } + +void mcvGetVectorLocalMax(const CvMat *inVec, vector &localMaxima, + vector &localMaximaLoc) +{ + + double val; + + //check type + if (CV_MAT_TYPE(inVec->type)==FLOAT_MAT_TYPE) + { + MCV_VECTOR_LOCAL_MAX(FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inVec->type)==INT_MAT_TYPE) + { + MCV_VECTOR_LOCAL_MAX(INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetVectorLocalMax\n"; + exit(1); + } +} + + +/** This function gets the qtile-th quantile of the input matrix + * + * \param mat input matrix + * \param qtile required input quantile probability + * \return the returned value + * + */ +FLOAT mcvGetQuantile(const CvMat *mat, FLOAT qtile) +{ + //make it a row vector + CvMat rowMat; + cvReshape(mat, &rowMat, 0, 1); + + //get the quantile + FLOAT qval; + qval = quantile((FLOAT*) rowMat.data.ptr, rowMat.width, qtile); + + return qval; +} + + +/** This function thresholds the image below a certain value to the threshold + * so: outMat(i,j) = inMat(i,j) if inMat(i,j)>=threshold + * = threshold otherwise + * + * \param inMat input matrix + * \param outMat output matrix + * \param threshold threshold value + * + */ +void mcvThresholdLower(const CvMat *inMat, CvMat *outMat, FLOAT threshold) +{ + +#define MCV_THRESHOLD_LOWER(type) \ + for (int i=0; iheight; i++) \ + for (int j=0; jwidth; j++) \ + if ( CV_MAT_ELEM(*inMat, type, i, j)type)==FLOAT_MAT_TYPE) + { + MCV_THRESHOLD_LOWER(FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==INT_MAT_TYPE) + { + MCV_THRESHOLD_LOWER(INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetVectorMax\n"; + exit(1); + } +} + +/** This function detects stop lines in the input image using IPM + * transformation and the input camera parameters. The returned lines + * are in a vector of Line objects, having start and end point in + * input image frame. + * + * \param image the input image + * \param stopLines a vector of returned stop lines in input image coordinates + * \param linescores a vector of line scores returned + * \param cameraInfo the camera parameters + * \param stopLineConf parameters for stop line detection + * + * + */ +void mcvGetStopLines(const CvMat *inImage, vector *stopLines, + vector *lineScores, const CameraInfo *cameraInfo, + LaneDetectorConf *stopLineConf) + +{ + //input size + CvSize inSize = cvSize(inImage->width, inImage->height); + + //TODO: smooth image + CvMat *image = cvCloneMat(inImage); + //cvSmooth(image, image, CV_GAUSSIAN, 5, 5, 1, 1); + + IPMInfo ipmInfo; + +// //get the IPM size such that we have height of the stop line +// //is 3 pixels +// double ipmWidth, ipmHeight; +// mcvGetIPMExtent(cameraInfo, &ipmInfo); +// ipmHeight = 3*(ipmInfo.yLimits[1]-ipmInfo.yLimits[0]) / (stopLineConf->lineHeight/3.); +// ipmWidth = ipmHeight * 4/3; +// //put into the conf +// stopLineConf->ipmWidth = int(ipmWidth); +// stopLineConf->ipmHeight = int(ipmHeight); + +// if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES +// cout << "IPM width:" << stopLineConf->ipmWidth << " IPM height:" +// << stopLineConf->ipmHeight << "\n"; +// }//#endif + + + //Get IPM + CvSize ipmSize = cvSize((int)stopLineConf->ipmWidth, + (int)stopLineConf->ipmHeight); + CvMat * ipm; + ipm = cvCreateMat(ipmSize.height, ipmSize.width, inImage->type); + //mcvGetIPM(inImage, ipm, &ipmInfo, cameraInfo); + ipmInfo.vpPortion = stopLineConf->ipmVpPortion; + ipmInfo.ipmLeft = stopLineConf->ipmLeft; + ipmInfo.ipmRight = stopLineConf->ipmRight; + ipmInfo.ipmTop = stopLineConf->ipmTop; + ipmInfo.ipmBottom = stopLineConf->ipmBottom; + ipmInfo.ipmInterpolation = stopLineConf->ipmInterpolation; + list outPixels; + list::iterator outPixelsi; + mcvGetIPM(image, ipm, &ipmInfo, cameraInfo, &outPixels); + + //smooth the IPM + //cvSmooth(ipm, ipm, CV_GAUSSIAN, 5, 5, 1, 1); + + //debugging + CvMat *dbIpmImage; + if(DEBUG_LINES) {// #ifdef DEBUG_GET_STOP_LINES + dbIpmImage = cvCreateMat(ipm->height, ipm->width, ipm->type); + cvCopy(ipm, dbIpmImage); + }//#endif + + + //compute stop line width: 2000 mm + FLOAT stopLinePixelWidth = stopLineConf->lineWidth * ipmInfo.xScale; + //stop line pixel height: 12 inches = 12*25.4 mm + FLOAT stopLinePixelHeight = stopLineConf->lineHeight * ipmInfo.yScale; + //kernel dimensions + //unsigned char wx = 2; + //unsigned char wy = 2; + FLOAT sigmax = stopLinePixelWidth; + FLOAT sigmay = stopLinePixelHeight; + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //cout << "Line width:" << stopLinePixelWidth << "Line height:" + // << stopLinePixelHeight << "\n"; + }//#endif + + //filter the IPM image + mcvFilterLines(ipm, ipm, stopLineConf->kernelWidth, + stopLineConf->kernelHeight, sigmax, sigmay, + LINE_HORIZONTAL); + + //zero out points outside the image in IPM view + for(outPixelsi=outPixels.begin(); outPixelsi!=outPixels.end(); outPixelsi++) + CV_MAT_ELEM(*ipm, float, (*outPixelsi).y, (*outPixelsi).x) = 0; + outPixels.clear(); + + //zero out negative values + mcvThresholdLower(ipm, ipm, 0); + + //compute quantile: .985 + FLOAT qtileThreshold = mcvGetQuantile(ipm, stopLineConf->lowerQuantile); + mcvThresholdLower(ipm, ipm, qtileThreshold); + + //debugging + CvMat *dbIpmImageThresholded; + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + dbIpmImageThresholded = cvCreateMat(ipm->height, ipm->width, ipm->type); + cvCopy(ipm, dbIpmImageThresholded); + }//#endif + + + + //group stop lines + switch(stopLineConf->groupingType) + { + //use HV grouping + case GROUPING_TYPE_HV_LINES: + //vector ipmStopLines; + //vector lineScores; + mcvGetHVLines(ipm, stopLines, lineScores, LINE_HORIZONTAL, + stopLinePixelHeight, stopLineConf->binarize, + stopLineConf->localMaxima, stopLineConf->detectionThreshold, + stopLineConf->smoothScores); + break; + + //use Hough Transform grouping + case GROUPING_TYPE_HOUGH_LINES: + //FLOAT rMin = 0.05*ipm->height, rMax = 0.4*ipm->height, rStep = 1; + //FLOAT thetaMin = 88*CV_PI/180, thetaMax = 92*CV_PI/180, thetaStep = 1*CV_PI/180; + //bool group = false; FLOAT groupThreshold = 1; + mcvGetHoughTransformLines(ipm, stopLines, lineScores, + stopLineConf->rMin, stopLineConf->rMax, + stopLineConf->rStep, stopLineConf->thetaMin, + stopLineConf->thetaMax, stopLineConf->thetaStep, + stopLineConf->binarize, stopLineConf->localMaxima, + stopLineConf->detectionThreshold, + stopLineConf->smoothScores, stopLineConf->group, + stopLineConf->groupThreshold); + break; + } + + //get RANSAC lines + if (stopLineConf->ransac) + { + mcvGetRansacLines(ipm, *stopLines, *lineScores, stopLineConf, LINE_HORIZONTAL); + } + + if(stopLineConf->getEndPoints) + { + //get line extent in IPM image + for(int i=0; i<(int)stopLines->size(); i++) + mcvGetLineExtent(ipm, (*stopLines)[i], (*stopLines)[i]); + } + + vector dbIpmStopLines; + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + dbIpmStopLines = *stopLines; +// //print out lineScores +// cout << "LineScores:"; +// //for (int i=0; i<(int)lineScores->size(); i++) +// for (int i=0; i<1 && lineScores->size()>0; i++) +// cout << (*lineScores)[i] << " "; +// cout << "\n"; + }//#endif + + //check if returned anything + if (stopLines->size()!=0) + { + //convert the line into world frame + for (unsigned int i=0; isize(); i++) + { + Line *line; + line = &(*stopLines)[i]; + + mcvPointImIPM2World(&(line->startPoint), &ipmInfo); + mcvPointImIPM2World(&(line->endPoint), &ipmInfo); + } + //convert them from world frame into camera frame + // + //put a dummy line at the beginning till we check that cvDiv bug + Line dummy = {{1.,1.},{2.,2.}}; + stopLines->insert(stopLines->begin(), dummy); + //convert to mat and get in image coordinates + CvMat *mat = cvCreateMat(2, 2*stopLines->size(), FLOAT_MAT_TYPE); + mcvLines2Mat(stopLines, mat); + stopLines->clear(); + mcvTransformGround2Image(mat, mat, cameraInfo); + //get back to vector + mcvMat2Lines(mat, stopLines); + //remove the dummy line at the beginning + stopLines->erase(stopLines->begin()); + //clear + cvReleaseMat(&mat); + + //clip the lines found and get their extent + for (unsigned int i=0; isize(); i++) + { + //clip + mcvIntersectLineWithBB(&(*stopLines)[i], inSize, &(*stopLines)[i]); + + //get the line extent + //mcvGetLineExtent(inImage, (*stopLines)[i], (*stopLines)[i]); + } + + + } + + //debugging + if(DEBUG_LINES) + {//#ifdef DEBUG_GET_STOP_LINES + //show the IPM image + SHOW_IMAGE(dbIpmImage, "IPM image", 10); + //thresholded ipm + SHOW_IMAGE(dbIpmImageThresholded, "Stoplines thresholded IPM image", 10); + //draw lines in IPM image + //for (int i=0; i<(int)dbIpmStopLines.size(); i++) + for (int i=0; i<1 && dbIpmStopLines.size()>0; i++) + { + mcvDrawLine(dbIpmImage, dbIpmStopLines[i], CV_RGB(0,0,0), 3); + } + SHOW_IMAGE(dbIpmImage, "Stoplines IPM with lines", 10); + //draw lines on original image + //CvMat *image = cvCreateMat(inImage->height, inImage->width, CV_32FC3); + //cvCvtColor(inImage, image, CV_GRAY2RGB); + //CvMat *image = cvCloneMat(inImage); + //for (int i=0; i<(int)stopLines->size(); i++) + for (int i=0; i<1 && stopLines->size()>0; i++) + { + //SHOW_POINT((*stopLines)[i].startPoint, "start"); + //SHOW_POINT((*stopLines)[i].endPoint, "end"); + mcvDrawLine(image, (*stopLines)[i], CV_RGB(255,0,0), 3); + } + SHOW_IMAGE(image, "Detected Stoplines", 10); + //cvReleaseMat(&image); + cvReleaseMat(&dbIpmImage); + cvReleaseMat(&dbIpmImageThresholded); + dbIpmStopLines.clear(); + }//#endif //DEBUG_GET_STOP_LINES + + //clear + cvReleaseMat(&ipm); + cvReleaseMat(&image); + //ipmStopLines.clear(); +} + +/** This function detects lanes in the input image using IPM + * transformation and the input camera parameters. The returned lines + * are in a vector of Line objects, having start and end point in + * input image frame. + * + * \param image the input image + * \param lanes a vector of returned stop lines in input image coordinates + * \param linescores a vector of line scores returned + * \param cameraInfo the camera parameters + * \param stopLineConf parameters for stop line detection + * \param state returns the current state and inputs the previous state to + * initialize the current detection (NULL to ignore) + * + * + */ +void mcvGetLanes(const CvMat *inImage, const CvMat* clrImage, + vector *lanes, vector *lineScores, + vector *splines, vector *splineScores, + CameraInfo *cameraInfo, LaneDetectorConf *stopLineConf, + LineState* state) +{ + //input size + CvSize inSize = cvSize(inImage->width, inImage->height); + + //TODO: smooth image + CvMat *image = cvCloneMat(inImage); + //cvSmooth(image, image, CV_GAUSSIAN, 5, 5, 1, 1); + + //SHOW_IMAGE(image, "Input image", 10); + + IPMInfo ipmInfo; + + //state: create a new structure, and put pointer to it if it's null + LineState newState; + if(!state) state = &newState; + +// //get the IPM size such that we have height of the stop line +// //is 3 pixels +// double ipmWidth, ipmHeight; +// mcvGetIPMExtent(cameraInfo, &ipmInfo); +// ipmHeight = 3*(ipmInfo.yLimits[1]-ipmInfo.yLimits[0]) / (stopLineConf->lineHeight/3.); +// ipmWidth = ipmHeight * 4/3; +// //put into the conf +// stopLineConf->ipmWidth = int(ipmWidth); +// stopLineConf->ipmHeight = int(ipmHeight); + +// #ifdef DEBUG_GET_STOP_LINES +// cout << "IPM width:" << stopLineConf->ipmWidth << " IPM height:" +// << stopLineConf->ipmHeight << "\n"; +// #endif + + + //Get IPM + CvSize ipmSize = cvSize((int)stopLineConf->ipmWidth, + (int)stopLineConf->ipmHeight); + CvMat * ipm; + ipm = cvCreateMat(ipmSize.height, ipmSize.width, inImage->type); + //mcvGetIPM(inImage, ipm, &ipmInfo, cameraInfo); + ipmInfo.vpPortion = stopLineConf->ipmVpPortion; + ipmInfo.ipmLeft = stopLineConf->ipmLeft; + ipmInfo.ipmRight = stopLineConf->ipmRight; + ipmInfo.ipmTop = stopLineConf->ipmTop; + ipmInfo.ipmBottom = stopLineConf->ipmBottom; + ipmInfo.ipmInterpolation = stopLineConf->ipmInterpolation; + list outPixels; + list::iterator outPixelsi; + mcvGetIPM(image, ipm, &ipmInfo, cameraInfo, &outPixels); + + //smooth the IPM image with 5x5 gaussian filter +#warning "Check: Smoothing IPM image" + //cvSmooth(ipm, ipm, CV_GAUSSIAN, 3); + // SHOW_MAT(ipm, "ipm"); + + // //subtract mean + // CvScalar mean = cvAvg(ipm); + // cvSubS(ipm, mean, ipm); + + //keep copy + CvMat* rawipm = cvCloneMat(ipm); + + //smooth the IPM + //cvSmooth(ipm, ipm, CV_GAUSSIAN, 5, 5, 1, 1); + + //debugging + CvMat *dbIpmImage; + if(DEBUG_LINES) + {//#ifdef DEBUG_GET_STOP_LINES + dbIpmImage = cvCreateMat(ipm->height, ipm->width, ipm->type); + cvCopy(ipm, dbIpmImage); + //show the IPM image + SHOW_IMAGE(dbIpmImage, "IPM image", 10); + }//#endif + + //compute stop line width: 2000 mm + FLOAT stopLinePixelWidth = stopLineConf->lineWidth * + ipmInfo.xScale; + //stop line pixel height: 12 inches = 12*25.4 mm + FLOAT stopLinePixelHeight = stopLineConf->lineHeight * + ipmInfo.yScale; + //kernel dimensions + //unsigned char wx = 2; + //unsigned char wy = 2; + FLOAT sigmax = stopLinePixelWidth; + FLOAT sigmay = stopLinePixelHeight; + +// //filter in the horizontal direction +// CvMat * ipmt = cvCreateMat(ipm->width, ipm->height, ipm->type); +// cvTranspose(ipm, ipmt); +// mcvFilterLines(ipmt, ipmt, stopLineConf->kernelWidth, +// stopLineConf->kernelHeight, sigmax, sigmay, +// LINE_VERTICAL); +// //retranspose +// CvMat *ipm2 = cvCreateMat(ipm->height, ipm->width, ipm->type); +// cvTranspose(ipmt, ipm2); +// cvReleaseMat(&ipmt); + + //filter the IPM image + mcvFilterLines(ipm, ipm, stopLineConf->kernelWidth, + stopLineConf->kernelHeight, sigmax, sigmay, + LINE_VERTICAL); +// mcvFilterLines(ipm, ipm, stopLineConf->kernelWidth, +// stopLineConf->kernelHeight, sigmax, sigmay, +// LINE_VERTICAL); + + //zero out points outside the image in IPM view + for(outPixelsi=outPixels.begin(); outPixelsi!=outPixels.end(); outPixelsi++) + { + CV_MAT_ELEM(*ipm, float, (*outPixelsi).y, (*outPixelsi).x) = 0; + // CV_MAT_ELEM(*ipm2, float, (*outPixelsi).y, (*outPixelsi).x) = 0; + } + outPixels.clear(); + +#warning "Check this clearing of IPM image for 2 lanes" + if (stopLineConf->ipmWindowClear) + { + //check to blank out other periferi of the image + //blank from 60->100 (width 40) + CvRect mask = cvRect(stopLineConf->ipmWindowLeft, 0, + stopLineConf->ipmWindowRight - + stopLineConf->ipmWindowLeft + 1, + ipm->height); + mcvSetMat(ipm, mask, 0); + } + + //show filtered image + if (DEBUG_LINES) { + SHOW_IMAGE(ipm, "Lane unthresholded filtered", 10); + } + + //take the negative to get double yellow lines + //cvScale(ipm, ipm, -1); + + CvMat *fipm = cvCloneMat(ipm); + + //zero out negative values +// SHOW_MAT(fipm, "fipm"); +#warning "clean negative parts in filtered image" + mcvThresholdLower(ipm, ipm, 0); +// mcvThresholdLower(ipm2, ipm2, 0); + +// //add the two images +// cvAdd(ipm, ipm2, ipm); + +// //clear the horizontal filtered image +// cvReleaseMat(&ipm2); + + //fipm was here + //make copy of filteed ipm image + + vector dbIpmStopLines; + vector dbIpmSplines; + + //int numStrips = 2; + int stripHeight = ipm->height / stopLineConf->numStrips; + for (int i=0; inumStrips; i++) //lines + { + //get the mask + CvRect mask; + mask = cvRect(0, i*stripHeight, ipm->width, + stripHeight); + // SHOW_RECT(mask, "Mask"); + + //get the subimage to work on + CvMat *subimage = cvCloneMat(ipm); + //clear all but the mask + mcvSetMat(subimage, mask, 0); + + //compute quantile: .985 + FLOAT qtileThreshold = mcvGetQuantile(subimage, stopLineConf->lowerQuantile); + mcvThresholdLower(subimage, subimage, qtileThreshold); + // FLOAT qtileThreshold = mcvGetQuantile(ipm, stopLineConf->lowerQuantile); + // mcvThresholdLower(ipm, ipm, qtileThreshold); + + // qtileThreshold = mcvGetQuantile(ipm2, stopLineConf->lowerQuantile); + // mcvThresholdLower(ipm2, ipm2, qtileThreshold); + + //and fipm was here last + // //make copy of filtered ipm image + // CvMat *fipm = cvCloneMat(ipm); + vector subimageLines; + vector subimageSplines; + vector subimageLineScores, subimageSplineScores; + + //check to blank out other periferi of the image +// mask = cvRect(40, 0, 80, subimage->height); +// mcvSetMat(subimage, mask, 0); + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + CvMat *dbIpmImageThresholded; + dbIpmImageThresholded = cvCreateMat(ipm->height, ipm->width, ipm->type); + cvCopy(subimage, dbIpmImageThresholded); //ipm + char str[256]; + sprintf(str, "Lanes #%d thresholded IPM", i); + //thresholded ipm + SHOW_IMAGE(dbIpmImageThresholded, str, 10); + cvReleaseMat(&dbIpmImageThresholded); + } + + //get the lines/splines + mcvGetLines(subimage, LINE_VERTICAL, subimageLines, subimageLineScores, + subimageSplines, subimageSplineScores, stopLineConf, + state); +// mcvGetLines(ipm, LINE_VERTICAL, *lanes, *lineScores, +// *splines, *splineScores, stopLineConf, +// state); + //put back + for (unsigned int k=0; kpush_back(subimageLines[k]); + lineScores->push_back(subimageLineScores[k]); + } + for (unsigned int k=0; kpush_back(subimageSplines[k]); + splineScores->push_back(subimageSplineScores[k]); + } + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + for (unsigned int k=0; ksize(); k++) + dbIpmStopLines.push_back((*lanes)[k]); + for (unsigned int k=0; ksize(); k++) + dbIpmSplines.push_back((*splines)[k]); + + CvMat *dbIpmImageThresholded; + dbIpmImageThresholded = cvCreateMat(ipm->height, ipm->width, ipm->type); + cvCopy(subimage, dbIpmImageThresholded); //ipm + char str[256]; + sprintf(str, "Lanes #%d thresholded IPM", i); + //thresholded ipm + SHOW_IMAGE(dbIpmImageThresholded, str, 10); + cvReleaseMat(&dbIpmImageThresholded); + + //dbIpmStopLines = *lanes; + //dbIpmSplines = *splines; + // //print out lineScores + // cout << "LineScores:"; + // //for (int i=0; i<(int)lineScores->size(); i++) + // for (int i=0; i<(int)lineScores->size(); i++) + // cout << (*lineScores)[i] << " "; + // cout << "\n"; + }//#endif + + //release + cvReleaseMat(&subimage); + } + + //postprocess lines/splines //rawipm + mcvPostprocessLines(image, clrImage, fipm, ipm, *lanes, *lineScores, + *splines, *splineScores, + stopLineConf, state, ipmInfo, *cameraInfo); //rawipm + + if (DEBUG_LINES) { + dbIpmSplines = state->ipmSplines; + } + + //debugging + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + char str[256]; + //draw lines in IPM image + //for (int i=0; i<(int)dbIpmStopLines.size(); i++) + if (stopLineConf->ransacLine || !stopLineConf->ransacSpline) + for (int i=0; i<(int)dbIpmStopLines.size(); i++) + mcvDrawLine(dbIpmImage, dbIpmStopLines[i], CV_RGB(0,0,0), 1); + if (stopLineConf->ransacSpline) + for (int i=0; i<(int)dbIpmSplines.size(); i++) + mcvDrawSpline(dbIpmImage, dbIpmSplines[i], CV_RGB(0,0,0), 1); + + SHOW_IMAGE(dbIpmImage, "Lanes IPM with lines", 10); + //draw lines on original image + CvMat *imageClr = cvCreateMat(inImage->height, inImage->width, CV_32FC3); + cvCvtColor(image, imageClr, CV_GRAY2RGB); + //CvMat *image = cvCloneMat(inImage); + //for (int i=0; i<(int)stopLines->size(); i++) + if (stopLineConf->ransacLine || !stopLineConf->ransacSpline) + for (int i=0; i<(int)lanes->size(); i++) + mcvDrawLine(imageClr, (*lanes)[i], CV_RGB(255,0,0), 1); + if (stopLineConf->ransacSpline) + for (int i=0; i<(int)splines->size(); i++) + { + mcvDrawSpline(imageClr, (*splines)[i], CV_RGB(255,0,0), 1); + sprintf(str, "%.2f", (*splineScores)[i]); + mcvDrawText(imageClr, str, + cvPointFrom32f((*splines)[i].points[(*splines)[i].degree]), + .5, CV_RGB(0, 0, 255)); + } + + SHOW_IMAGE(imageClr, "Detected lanes", 0); + //cvReleaseMat(&image); + cvReleaseMat(&dbIpmImage); + //cvReleaseMat(&dbIpmImageThresholded); + cvReleaseMat(&imageClr); + dbIpmStopLines.clear(); + dbIpmSplines.clear(); + }//#endif //DEBUG_GET_STOP_LINES + + //clear + cvReleaseMat(&ipm); + cvReleaseMat(&image); + cvReleaseMat(&fipm); + cvReleaseMat(&rawipm); + //ipmStopLines.clear(); +} + + +/** This function postprocesses the detected lines/splines to better localize + * and extend them + * + * \param image the input image + * \param clrImage the inpout color image + * \param rawipm the raw ipm image + * \param fipm the filtered ipm iamge + * \param lines a vector of lines + * \param lineScores the line scores + * \param splines a vector of returned splines + * \param splineScores the spline scores + * \param lineConf the conf structure + * \param state the state for RANSAC splines + * \param ipmInfo the ipmInfo structure + * \param cameraInfo the camera info structure + * + */ +void mcvPostprocessLines(const CvMat* image, const CvMat* clrImage, + const CvMat* rawipm, const CvMat* fipm, + vector &lines, vector &lineScores, + vector &splines, vector &splineScores, + LaneDetectorConf *lineConf, LineState *state, + IPMInfo &ipmInfo, CameraInfo &cameraInfo) +{ + CvSize inSize = cvSize(image->width-1, image->height-1); + + //vector of splines to keep + vector keepSplines; + vector keepSplineScores; + +// //get line extent +// if(lineConf->getEndPoints) +// { +// //get line extent in IPM image +// for(int i=0; i<(int)lines.size(); i++) +// mcvGetLineExtent(rawipm, lines[i], lines[i]); +// } + + //if return straight lines + if (lineConf->ransacLine || !lineConf->ransacSpline) + { + mcvLinesImIPM2Im(lines, ipmInfo, cameraInfo, inSize); + } + //return spline + if (lineConf->ransacSpline) + { + //localize splines + for(int i=0; i<(int)splines.size(); i++) + { + //get spline status + int splineStatus = lineConf->checkIPMSplines ? + mcvCheckSpline(splines[i], + lineConf->checkIPMSplinesCurvenessThreshold, + lineConf->checkIPMSplinesLengthThreshold, + lineConf->checkIPMSplinesThetaDiffThreshold, + lineConf->checkIPMSplinesThetaThreshold) + : 0; + + //check it + if (!(((splineStatus & CurvedSpline) && (splineStatus & CurvedSplineTheta)) + || splineStatus & HorizontalSpline)) + { + //better localize points + CvMat *points = mcvEvalBezierSpline(splines[i], .1); //.05 + //mcvLocalizePoints(ipm, points, points); //inImage + //extend spline + CvMat* p = mcvExtendPoints(rawipm, points, + lineConf->extendIPMAngleThreshold, + lineConf->extendIPMMeanDirAngleThreshold, + lineConf->extendIPMLinePixelsTangent, + lineConf->extendIPMLinePixelsNormal, + lineConf->extendIPMContThreshold, + lineConf->extendIPMDeviationThreshold, + cvRect(0, lineConf->extendIPMRectTop, + rawipm->width-1, + lineConf->extendIPMRectBottom-lineConf->extendIPMRectTop), + false); + //refit spline + Spline spline = mcvFitBezierSpline(p, lineConf->ransacSplineDegree); + + //save +#warning "Check this later: extension in IPM. Check threshold value" +// splines[i] = spline; + + //calculate the score from fipm or ipm (thresholded) + //float lengthRatio = 0.5; //.8 + //float angleRatio = 0.8; //.4 + //vector jitter = mcvGetJitterVector(lineConf->splineScoreJitter);//2); + + float score = mcvGetSplineScore(fipm, splines[i], + lineConf->splineScoreStep, //.1 + lineConf->splineScoreJitter, //jitter, + lineConf->splineScoreLengthRatio, + lineConf->splineScoreAngleRatio); + //jitter.clear(); + splineScores[i] = score; + + //check score + if (splineScores[i] >= lineConf->finalSplineScoreThreshold) + { + keepSplines.push_back(spline); + keepSplineScores.push_back(splineScores[i]); + } + //clear + cvReleaseMat(&points); + cvReleaseMat(&p); + } //if + } //for + + //put back + splines.clear(); + splineScores.clear(); + splines = keepSplines; + splineScores = keepSplineScores; + keepSplines.clear(); + keepSplineScores.clear(); + +// if (DEBUG_LINES) { +// dbIpmSplines = *splines; +// } + + //save state + //save IPM splines to use in next frame + state->ipmSplines.clear(); + state->ipmSplines = splines; + + //convert to image coordinates + mcvSplinesImIPM2Im(splines, ipmInfo, cameraInfo, inSize); + + + // fprintf(stderr, "start of splines------------------------\n"); + // for (unsigned int i=0; icheckSplines ? + mcvCheckSpline(splines[i], + lineConf->checkSplinesCurvenessThreshold, + lineConf->checkSplinesLengthThreshold, + lineConf->checkSplinesThetaDiffThreshold, + lineConf->checkSplinesThetaThreshold) + : 0; + //check if short, then put the corresponding line instead + if (splineStatus & (ShortSpline|CurvedSpline)) + { + for (unsigned int j=0; jransacSplineDegree); + //check if that merges with the current one + if (mcvCheckMergeSplines(splines[i], sp, .4, 50, .4, 50, 50)) + { + //put the spline + splines[i] = sp; + splineStatus = 0; + break; + } + } + //splines[i] = mcvLineXY2Spline(lines[i], lineConf->ransacSplineDegree); + } + if (!(splineStatus & (CurvedSpline|CurvedSplineTheta))) + { + //better localize points + CvMat *points = mcvEvalBezierSpline(splines[i], .05); + // CvMat *points = mcvGetBezierSplinePixels((*splines)[i], .05, + // cvSize(inImage->width-1, + // inImage->height-1), + // true); + // CvMat *p = cvCreateMat(points->height, points->width, CV_32FC1); + // cvConvert(points, p); + mcvLocalizePoints(image, points, points, lineConf->localizeNumLinePixels, + lineConf->localizeAngleThreshold); //inImage + + //get color + CvMat* clrPoints = points; + + //extend spline + CvMat* p = mcvExtendPoints(image, points, + lineConf->extendAngleThreshold, + lineConf->extendMeanDirAngleThreshold, + lineConf->extendLinePixelsTangent, + lineConf->extendLinePixelsNormal, + lineConf->extendContThreshold, + lineConf->extendDeviationThreshold, + cvRect(0, lineConf->extendRectTop, + image->width, + lineConf->extendRectBottom-lineConf->extendRectTop)); + + //refit + Spline spline = mcvFitBezierSpline(p, lineConf->ransacSplineDegree); + //splines[i] = spline; + clrPoints = p; + + //check the extended one + if (lineConf->checkSplines && + (mcvCheckSpline(spline, + lineConf->checkSplinesCurvenessThreshold, + lineConf->checkSplinesLengthThreshold, + lineConf->checkSplinesThetaDiffThreshold, + lineConf->checkSplinesThetaThreshold) + & CurvedSpline)) + { + //rfit using points before extending + spline = mcvFitBezierSpline(points, lineConf->ransacSplineDegree); + clrPoints = points; + + //check again + if (mcvCheckSpline(spline, + lineConf->checkSplinesCurvenessThreshold, + lineConf->checkSplinesLengthThreshold, + lineConf->checkSplinesThetaDiffThreshold, + lineConf->checkSplinesThetaThreshold) + & CurvedSpline) + //use spline before localization + spline = splines[i]; + } + + //get color +// fprintf(stderr, "Color for spline %d\n", i); + LineColor clr = lineConf->checkColor ? + mcvGetPointsColor(clrImage, clrPoints, + lineConf->checkColorWindow, + lineConf->checkColorNumYellowMin, + lineConf->checkColorRGMin, + lineConf->checkColorRGMax, + lineConf->checkColorGBMin, + lineConf->checkColorRBMin, + lineConf->checkColorRBF, + lineConf->checkColorRBFThreshold) + : LINE_COLOR_WHITE; + + //clear + cvReleaseMat(&points); + cvReleaseMat(&p); + + //put it + spline.color = clr; + keepSplines.push_back(spline); + keepSplineScores.push_back(splineScores[i]); + } //if + } //for + + //put them back + splines.clear(); + splineScores.clear(); + splines = keepSplines; + splineScores = keepSplineScores; + keepSplines.clear(); + keepSplineScores.clear(); + +// fprintf(stderr, "start of splines------------------------\n"); +// for (unsigned int i=0; i &lines, vector &lineScores, + vector &splines, vector &splineScores, + LaneDetectorConf *lineConf, LineState *state) +{ + + //initial grouping of lines + switch(lineConf->groupingType) + { + //use HV grouping + case GROUPING_TYPE_HV_LINES: + //vector ipmStopLines; + //vector lineScores; + mcvGetHVLines(image, &lines, &lineScores, lineType, + 6, //stopLinePixelHeight, + lineConf->binarize, lineConf->localMaxima, + lineConf->detectionThreshold, + lineConf->smoothScores); + break; + + //use Hough Transform grouping + case GROUPING_TYPE_HOUGH_LINES: + //FLOAT rMin = 0.05*ipm->height, rMax = 0.4*ipm->height, rStep = 1; + //FLOAT thetaMin = 88*CV_PI/180, thetaMax = 92*CV_PI/180, thetaStep = 1*CV_PI/180; + // bool group = true; FLOAT groupThreshold = 15; + mcvGetHoughTransformLines(image, &lines, &lineScores, + lineConf->rMin, lineConf->rMax, + lineConf->rStep, lineConf->thetaMin, + lineConf->thetaMax, lineConf->thetaStep, + lineConf->binarize, lineConf->localMaxima, + lineConf->detectionThreshold, + lineConf->smoothScores, + lineConf->group, lineConf->groupThreshold); + + break; + } + + //get only two lines if in this mode +// if (lineConf->group) +// mcvGroupLines(lines, lineScores, +// lineConf->groupThreshold, +// cvSize(image->width, image->height)); + if (lineConf->checkLaneWidth) + mcvCheckLaneWidth(lines, lineScores, + lineConf->checkLaneWidthMean, + lineConf->checkLaneWidthStd); //70&20 65&10 25&10 + + //check if to do RANSAC + if (lineConf->ransac) + { + //do RANSAC lines? + if (lineConf->ransacLine) + mcvGetRansacLines(image, lines, lineScores, lineConf, lineType); + + //do RANSAC splines? + if (lineConf->ransacSpline) + mcvGetRansacSplines(image, lines, lineScores, + lineConf, lineType, splines, splineScores, state); + } + + //get bounding boxes around returned splines to pass to the next + //frame + state->ipmBoxes.clear(); + mcvGetSplinesBoundingBoxes(splines, lineType, + cvSize(image->width, image->height), + state->ipmBoxes); +} + +/** This function makes some checks on splines and decides + * whether to keep them or not + * + * \param spline the input spline + * \param curvenessThreshold minimum curveness score it should have + * \param lengthThreshold mimimum threshold it should have + * \param thetaDiffThreshold max theta diff it should have + * \param thetaThreshold max theta it should have to be considered horizontal + * + * \return code that determines what to do with the spline + * + */ +int mcvCheckSpline(const Spline &spline, float curvenessThreshold, + float lengthThreshold, float thetaDiffThreshold, + float thetaThreshold) +{ + + //get the spline features + float theta, r, meanTheta, meanR, length, curveness; + mcvGetSplineFeatures(spline, 0, &theta, &r, &length, + &meanTheta, &meanR, &curveness); + float thetaDiff = fabs(meanTheta - theta); +// thetaDiff = thetaDiff>CV_PI ? thetaDiff-CV_PI : thetaDiff; + +// float curvenessThreshold = .92; //.85; +// float lengthThreshold = 30; +// float thetaDiffThreshold = .1; +// float thetaThreshold = 70. * CV_PI/180; + + + char check = 0; + if (curveness thetaDiffThreshold) + check |= CurvedSplineTheta; + if (lengththetaThreshold) + check |= HorizontalSpline; + + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + fprintf(stderr, "thetaDiffThreshold=%f\n", thetaDiffThreshold); + fprintf(stderr, "%s: curveness=%f, length=%f, thetaDiff=%f, meanTheta=%f, theta=%f\n", + check & (ShortSpline|CurvedSpline|HorizontalSpline) + ? "YES" : "NO ", curveness, length, thetaDiff, meanTheta, theta); + fprintf(stderr, "\t%s\t%s\t%s\t%s\n", + check&CurvedSpline? "curved" : "not curved", + check&CurvedSplineTheta? "curved theta" : "not curved theta", + check&ShortSpline? "short" : "not short", + check&HorizontalSpline? "horiz" : "not horiz"); + + CvMat* im = cvCreateMat(480, 640, CV_8UC3); + cvSet(im, cvRealScalar(0.)); + //draw splines + mcvDrawSpline(im, spline, CV_RGB(255, 0, 0), 1); + SHOW_IMAGE(im, "Check Splines", 10); + //clear + cvReleaseMat(&im); + }//#endif + + return check; +} + +/** This function makes some checks on points and decides + * whether to keep them or not + * + * \param points the array of points to check + * + * \return code that determines what to do with the points + * + */ +int mcvCheckPoints(const CvMat* points) +{ + + //get the spline features + float theta, r, meanTheta, meanR, curveness; //length + mcvGetPointsFeatures(points, 0, &theta, &r, 0, + &meanTheta, &meanR, &curveness); + float thetaDiff = fabs(meanTheta - theta); + + float curvenessThreshold = .8; + float thetaDiffThreshold = .3; + + int check = 0; + if (curveness thetaDiffThreshold) + check |= CurvedSplineTheta; + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + fprintf(stderr, "%s: curveness=%f, thetaDiff=%f, meanTheta=%f\n", + check & (ShortSpline|CurvedSpline|HorizontalSpline) + ? "YES" : "NO ", curveness, thetaDiff, meanTheta); + + CvMat* im = cvCreateMat(480, 640, CV_8UC3); + cvSet(im, cvRealScalar(0.)); + //draw splines + for (int i=0; iheight-1; i++) + { + Line line; + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i, 0), + CV_MAT_ELEM(*points, float, i, 1)); + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i+1, 0), + CV_MAT_ELEM(*points, float, i+1, 1)); + mcvDrawLine(im, line, CV_RGB(255, 0, 0), 1); + } + SHOW_IMAGE(im, "Check Points", 0); + //clear + cvReleaseMat(&im); + }//#endif + + return check; +} + + +/** This function converts an array of lines to a matrix (already allocated) + * + * \param lines input vector of lines + * \param size number of lines to convert + * \return the converted matrix, it has 2x2*size where size is the + * number of lines, first row is x values (start.x, end.x) and second + * row is y-values + * + * + */ +void mcvLines2Mat(const vector *lines, CvMat *mat) +{ + //allocate the matrix + //*mat = cvCreateMat(2, size*2, FLOAT_MAT_TYPE); + + //loop and put values + int j; + for (int i=0; i<(int)lines->size(); i++) + { + j = 2*i; + CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 0, j) = (*lines)[i].startPoint.x; + CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 1, j) = (*lines)[i].startPoint.y; + CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 0, j+1) = (*lines)[i].endPoint.x; + CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 1, j+1) = (*lines)[i].endPoint.y; + } +} + + +/** This function converts matrix into n array of lines + * + * \param mat input matrix , it has 2x2*size where size is the + * number of lines, first row is x values (start.x, end.x) and second + * row is y-values + * \param lines the rerurned vector of lines + * + * + */ +void mcvMat2Lines(const CvMat *mat, vector *lines) +{ + + Line line; + //loop and put values + for (int i=0; iwidth/2); i++) + { + int j = 2*i; + //get the line + line.startPoint.x = CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 0, j); + line.startPoint.y = CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 1, j); + line.endPoint.x = CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 0, j+1); + line.endPoint.y = CV_MAT_ELEM(*mat, FLOAT_MAT_ELEM_TYPE, 1, j+1); + //push it + lines->push_back(line); + } +} + + + +/** This function intersects the input line with the given bounding box + * + * \param inLine the input line + * \param bbox the bounding box + * \param outLine the output line + * + */ +void mcvIntersectLineWithBB(const Line *inLine, const CvSize bbox, + Line *outLine) +{ + //put output + outLine->startPoint.x = inLine->startPoint.x; + outLine->startPoint.y = inLine->startPoint.y; + outLine->endPoint.x = inLine->endPoint.x; + outLine->endPoint.y = inLine->endPoint.y; + + //check which points are inside + bool startInside, endInside; + startInside = mcvIsPointInside(inLine->startPoint, bbox); + endInside = mcvIsPointInside(inLine->endPoint, bbox); + + //now check + if (!(startInside && endInside)) + { + //difference + FLOAT deltax, deltay; + deltax = inLine->endPoint.x - inLine->startPoint.x; + deltay = inLine->endPoint.y - inLine->startPoint.y; + //hold parameters + FLOAT t[4]={2,2,2,2}; + FLOAT xup, xdown, yleft, yright; + + //intersect with top and bottom borders: y=0 and y=bbox.height-1 + if (deltay==0) //horizontal line + { + xup = xdown = bbox.width+2; + } + else + { + t[0] = -inLine->startPoint.y/deltay; + xup = inLine->startPoint.x + t[0]*deltax; + t[1] = (bbox.height-inLine->startPoint.y)/deltay; + xdown = inLine->startPoint.x + t[1]*deltax; + } + + //intersect with left and right borders: x=0 and x=bbox.widht-1 + if (deltax==0) //horizontal line + { + yleft = yright = bbox.height+2; + } + else + { + t[2] = -inLine->startPoint.x/deltax; + yleft = inLine->startPoint.y + t[2]*deltay; + t[3] = (bbox.width-inLine->startPoint.x)/deltax; + yright = inLine->startPoint.y + t[3]*deltay; + } + + //points of intersection + FLOAT_POINT2D pts[4] = {{xup, 0},{xdown,bbox.height}, + {0, yleft},{bbox.width, yright}}; + + //now decide which stays and which goes + int i; + if (!startInside) + { + bool cont=true; + for (i=0; i<4 && cont; i++) + { + if (t[i]>=0 && t[i]<=1 && mcvIsPointInside(pts[i],bbox) && + !(pts[i].x == outLine->endPoint.x && + pts[i].y == outLine->endPoint.y) ) + { + outLine->startPoint.x = pts[i].x; + outLine->startPoint.y = pts[i].y; + t[i] = 2; + cont = false; + } + } + //check if not replaced + if(cont) + { + //loop again removing restriction on endpoint this time + for (i=0; i<4 && cont; i++) + { + if (t[i]>=0 && t[i]<=1 && mcvIsPointInside(pts[i],bbox)) + { + outLine->startPoint.x = pts[i].x; + outLine->startPoint.y = pts[i].y; + t[i] = 2; + cont = false; + } + } + } + } + if (!endInside) + { + bool cont=true; + for (i=0; i<4 && cont; i++) + { + if (t[i]>=0 && t[i]<=1 && mcvIsPointInside(pts[i],bbox) && + !(pts[i].x == outLine->startPoint.x && + pts[i].y == outLine->startPoint.y) ) + { + outLine->endPoint.x = pts[i].x; + outLine->endPoint.y = pts[i].y; + t[i] = 2; + cont = false; + } + } + //check if not replaced + if(cont) + { + //loop again removing restriction on endpoint this time + for (i=0; i<4 && cont; i++) + { + if (t[i]>=0 && t[i]<=1 && mcvIsPointInside(pts[i],bbox)) + { + outLine->endPoint.x = pts[i].x; + outLine->endPoint.y = pts[i].y; + t[i] = 2; + cont = false; + } + } + } + } + } +} + + +/** This function intersects the input line (given in r and theta) with + * the given bounding box where the line is represented by: + * x cos(theta) + y sin(theta) = r + * + * \param r the r value for the input line + * \param theta the theta value for the input line + * \param bbox the bounding box + * \param outLine the output line + * + */ +void mcvIntersectLineRThetaWithBB(FLOAT r, FLOAT theta, const CvSize bbox, + Line *outLine) +{ + //hold parameters + double xup, xdown, yleft, yright; + + //intersect with top and bottom borders: y=0 and y=bbox.height-1 + if (cos(theta)==0) //horizontal line + { + xup = xdown = bbox.width+2; + } + else + { + xup = r / cos(theta); + xdown = (r-bbox.height*sin(theta))/cos(theta); + } + + //intersect with left and right borders: x=0 and x=bbox.widht-1 + if (sin(theta)==0) //horizontal line + { + yleft = yright = bbox.height+2; + } + else + { + yleft = r/sin(theta); + yright = (r-bbox.width*cos(theta))/sin(theta); + } + + //points of intersection + FLOAT_POINT2D pts[4] = {{xup, 0},{xdown,bbox.height}, + {0, yleft},{bbox.width, yright}}; + + //get the starting point + int i; + for (i=0; i<4; i++) + { + //if point inside, then put it + if(mcvIsPointInside(pts[i], bbox)) + { + outLine->startPoint.x = pts[i].x; + outLine->startPoint.y = pts[i].y; + //get out of for loop + break; + } + } + //get the ending point + for (i++; i<4; i++) + { + //if point inside, then put it + if(mcvIsPointInside(pts[i], bbox)) + { + outLine->endPoint.x = pts[i].x; + outLine->endPoint.y = pts[i].y; + //get out of for loop + break; + } + } +} + + +/** This function checks if the given point is inside the bounding box + * specified + * + * \param inLine the input line + * \param bbox the bounding box + * \param outLine the output line + * + */ +bool mcvIsPointInside(FLOAT_POINT2D point, CvSize bbox) +{ + return (point.x>=0 && point.x<=bbox.width + && point.y>=0 && point.y<=bbox.height) ? true : false; +} + + +/** This function intersects the input line (given in r and theta) with + * the given rectangle where the line is represented by: + * x cos(theta) + y sin(theta) = r + * + * \param r the r value for the input line + * \param theta the theta value for the input line + * \param rect the input rectangle (given two opposite points in the rectangle, + * upperleft->startPoint and bottomright->endPoint where x->right and y->down) + * \param outLine the output line + * + */ +void mcvIntersectLineRThetaWithRect(FLOAT r, FLOAT theta, const Line &rect, + Line &outLine) +{ + //hold parameters + double xup, xdown, yleft, yright; + + //intersect with top and bottom borders: y=rect->startPoint.y and y=rect->endPoint.y + if (cos(theta)==0) //horizontal line + { + xup = xdown = rect.endPoint.x+2; + } + else + { + xup = (r-rect.startPoint.y*sin(theta)) / cos(theta); + xdown = (r-rect.endPoint.y*sin(theta)) / cos(theta); + } + + //intersect with left and right borders: x=rect->startPoint.x and x=rect->endPoint.x + if (sin(theta)==0) //horizontal line + { + yleft = yright = rect.endPoint.y+2; + } + else + { + yleft = (r-rect.startPoint.x*cos(theta)) / sin(theta); + yright = (r-rect.endPoint.x*cos(theta)) / sin(theta); + } + + //points of intersection + FLOAT_POINT2D pts[4] = {{xup, rect.startPoint.y},{xdown,rect.endPoint.y}, + {rect.startPoint.x, yleft},{rect.endPoint.x, yright}}; + + //get the starting point + int i; + for (i=0; i<4; i++) + { + //if point inside, then put it + if(mcvIsPointInside(pts[i], rect)) + { + outLine.startPoint.x = pts[i].x; + outLine.startPoint.y = pts[i].y; + //get out of for loop + break; + } + } + //get the ending point + for (i++; i<4; i++) + { + //if point inside, then put it + if(mcvIsPointInside(pts[i], rect)) + { + outLine.endPoint.x = pts[i].x; + outLine.endPoint.y = pts[i].y; + //get out of for loop + break; + } + } +} + + +/** This function checks if the given point is inside the rectangle specified + * + * \param inLine the input line + * \param rect the specified rectangle + * + */ +bool mcvIsPointInside(FLOAT_POINT2D &point, const Line &rect) +{ + return (point.x>=rect.startPoint.x && point.x<=rect.endPoint.x + && point.y>=rect.startPoint.y && point.y<=rect.endPoint.y) ? true : false; +} + +/** This function checks if the given point is inside the rectangle specified + * + * \param inLine the input line + * \param rect the specified rectangle + * + */ +bool mcvIsPointInside(FLOAT_POINT2D &point, const CvRect &rect) +{ + return (point.x>=rect.x && point.x<=(rect.x+rect.width) + && point.y>=rect.y && point.y<=(rect.y+rect.height)) ? true : false; +} + +/** This function converts an INT mat into a FLOAT mat (already allocated) + * + * \param inMat input INT matrix + * \param outMat output FLOAT matrix + * + */ +void mcvMatInt2Float(const CvMat *inMat, CvMat *outMat) +{ + for (int i=0; iheight; i++) + for (int j=0; jwidth; j++) + CV_MAT_ELEM(*outMat, FLOAT_MAT_ELEM_TYPE, i, j) = + (FLOAT_MAT_ELEM_TYPE) CV_MAT_ELEM(*inMat, INT_MAT_ELEM_TYPE, + i, j)/255; +} + + +/** This function draws a line onto the passed image + * + * \param image the input iamge + * \param line input line + * \param line color + * \param width line width + * + */ +void mcvDrawLine(CvMat *image, Line line, CvScalar color, int width) +{ + cvLine(image, cvPoint((int)line.startPoint.x,(int)line.startPoint.y), + cvPoint((int)line.endPoint.x,(int)line.endPoint.y), + color, width); +} + +/** This initializes the LaneDetectorinfo structure + * + * \param fileName the input file name + * \param stopLineConf the structure to fill + * + * + */ + void mcvInitLaneDetectorConf(char * const fileName, + LaneDetectorConf *stopLineConf) +{ + //parsed camera data + LaneDetectorParserInfo stopLineParserInfo; + //read the data + assert(LaneDetectorParser_configfile(fileName, &stopLineParserInfo, 0, 1, 1)==0); + //init the strucure + stopLineConf->ipmWidth = stopLineParserInfo.ipmWidth_arg; + stopLineConf->ipmHeight = stopLineParserInfo.ipmHeight_arg; + stopLineConf->ipmLeft = stopLineParserInfo.ipmLeft_arg; + stopLineConf->ipmRight = stopLineParserInfo.ipmRight_arg; + stopLineConf->ipmBottom = stopLineParserInfo.ipmBottom_arg; + stopLineConf->ipmTop = stopLineParserInfo.ipmTop_arg; + stopLineConf->ipmInterpolation = stopLineParserInfo.ipmInterpolation_arg; + + stopLineConf->lineWidth = stopLineParserInfo.lineWidth_arg; + stopLineConf->lineHeight = stopLineParserInfo.lineHeight_arg; + stopLineConf->kernelWidth = stopLineParserInfo.kernelWidth_arg; + stopLineConf->kernelHeight = stopLineParserInfo.kernelHeight_arg; + stopLineConf->lowerQuantile = + stopLineParserInfo.lowerQuantile_arg; + stopLineConf->localMaxima = + stopLineParserInfo.localMaxima_arg; + stopLineConf->groupingType = stopLineParserInfo.groupingType_arg; + stopLineConf->binarize = stopLineParserInfo.binarize_arg; + stopLineConf->detectionThreshold = + stopLineParserInfo.detectionThreshold_arg; + stopLineConf->smoothScores = + stopLineParserInfo.smoothScores_arg; + stopLineConf->rMin = stopLineParserInfo.rMin_arg; + stopLineConf->rMax = stopLineParserInfo.rMax_arg; + stopLineConf->rStep = stopLineParserInfo.rStep_arg; + stopLineConf->thetaMin = stopLineParserInfo.thetaMin_arg * CV_PI/180; + stopLineConf->thetaMax = stopLineParserInfo.thetaMax_arg * CV_PI/180; + stopLineConf->thetaStep = stopLineParserInfo.thetaStep_arg * CV_PI/180; + stopLineConf->ipmVpPortion = stopLineParserInfo.ipmVpPortion_arg; + stopLineConf->getEndPoints = stopLineParserInfo.getEndPoints_arg; + stopLineConf->group = stopLineParserInfo.group_arg; + stopLineConf->groupThreshold = stopLineParserInfo.groupThreshold_arg; + stopLineConf->ransac = stopLineParserInfo.ransac_arg; + + stopLineConf->ransacLineNumSamples = stopLineParserInfo.ransacLineNumSamples_arg; + stopLineConf->ransacLineNumIterations = stopLineParserInfo.ransacLineNumIterations_arg; + stopLineConf->ransacLineNumGoodFit = stopLineParserInfo.ransacLineNumGoodFit_arg; + stopLineConf->ransacLineThreshold = stopLineParserInfo.ransacLineThreshold_arg; + stopLineConf->ransacLineScoreThreshold = stopLineParserInfo.ransacLineScoreThreshold_arg; + stopLineConf->ransacLineBinarize = stopLineParserInfo.ransacLineBinarize_arg; + stopLineConf->ransacLineWindow = stopLineParserInfo.ransacLineWindow_arg; + + stopLineConf->ransacSplineNumSamples = stopLineParserInfo.ransacSplineNumSamples_arg; + stopLineConf->ransacSplineNumIterations = stopLineParserInfo.ransacSplineNumIterations_arg; + stopLineConf->ransacSplineNumGoodFit = stopLineParserInfo.ransacSplineNumGoodFit_arg; + stopLineConf->ransacSplineThreshold = stopLineParserInfo.ransacSplineThreshold_arg; + stopLineConf->ransacSplineScoreThreshold = stopLineParserInfo.ransacSplineScoreThreshold_arg; + stopLineConf->ransacSplineBinarize = stopLineParserInfo.ransacSplineBinarize_arg; + stopLineConf->ransacSplineWindow = stopLineParserInfo.ransacSplineWindow_arg; + + stopLineConf->ransacSplineDegree = stopLineParserInfo.ransacSplineDegree_arg; + + stopLineConf->ransacSpline = stopLineParserInfo.ransacSpline_arg; + stopLineConf->ransacLine = stopLineParserInfo.ransacLine_arg; + stopLineConf->ransacSplineStep = stopLineParserInfo.ransacSplineStep_arg; + + stopLineConf->overlapThreshold = stopLineParserInfo.overlapThreshold_arg; + + stopLineConf->localizeAngleThreshold = stopLineParserInfo.localizeAngleThreshold_arg; + stopLineConf->localizeNumLinePixels = stopLineParserInfo.localizeNumLinePixels_arg; + + stopLineConf->extendAngleThreshold = stopLineParserInfo.extendAngleThreshold_arg; + stopLineConf->extendMeanDirAngleThreshold = stopLineParserInfo.extendMeanDirAngleThreshold_arg; + stopLineConf->extendLinePixelsTangent = stopLineParserInfo.extendLinePixelsTangent_arg; + stopLineConf->extendLinePixelsNormal = stopLineParserInfo.extendLinePixelsNormal_arg; + stopLineConf->extendContThreshold = stopLineParserInfo.extendContThreshold_arg; + stopLineConf->extendDeviationThreshold = stopLineParserInfo.extendDeviationThreshold_arg; + stopLineConf->extendRectTop = stopLineParserInfo.extendRectTop_arg; + stopLineConf->extendRectBottom = stopLineParserInfo.extendRectBottom_arg; + + stopLineConf->extendIPMAngleThreshold = stopLineParserInfo.extendIPMAngleThreshold_arg; + stopLineConf->extendIPMMeanDirAngleThreshold = stopLineParserInfo.extendIPMMeanDirAngleThreshold_arg; + stopLineConf->extendIPMLinePixelsTangent = stopLineParserInfo.extendIPMLinePixelsTangent_arg; + stopLineConf->extendIPMLinePixelsNormal = stopLineParserInfo.extendIPMLinePixelsNormal_arg; + stopLineConf->extendIPMContThreshold = stopLineParserInfo.extendIPMContThreshold_arg; + stopLineConf->extendIPMDeviationThreshold = stopLineParserInfo.extendIPMDeviationThreshold_arg; + stopLineConf->extendIPMRectTop = stopLineParserInfo.extendIPMRectTop_arg; + stopLineConf->extendIPMRectBottom = stopLineParserInfo.extendIPMRectBottom_arg; + + stopLineConf->splineScoreJitter = stopLineParserInfo.splineScoreJitter_arg; + stopLineConf->splineScoreLengthRatio = stopLineParserInfo.splineScoreLengthRatio_arg; + stopLineConf->splineScoreAngleRatio = stopLineParserInfo.splineScoreAngleRatio_arg; + stopLineConf->splineScoreStep = stopLineParserInfo.splineScoreStep_arg; + + stopLineConf->splineTrackingNumAbsentFrames = stopLineParserInfo.splineTrackingNumAbsentFrames_arg; + stopLineConf->splineTrackingNumSeenFrames = stopLineParserInfo.splineTrackingNumSeenFrames_arg; + + stopLineConf->mergeSplineThetaThreshold = stopLineParserInfo.mergeSplineThetaThreshold_arg; + stopLineConf->mergeSplineRThreshold = stopLineParserInfo.mergeSplineRThreshold_arg; + stopLineConf->mergeSplineMeanThetaThreshold = stopLineParserInfo.mergeSplineMeanThetaThreshold_arg; + stopLineConf->mergeSplineMeanRThreshold = stopLineParserInfo.mergeSplineMeanRThreshold_arg; + stopLineConf->mergeSplineCentroidThreshold = stopLineParserInfo.mergeSplineCentroidThreshold_arg; + + stopLineConf->lineTrackingNumAbsentFrames = stopLineParserInfo.lineTrackingNumAbsentFrames_arg; + stopLineConf->lineTrackingNumSeenFrames = stopLineParserInfo.lineTrackingNumSeenFrames_arg; + + stopLineConf->mergeLineThetaThreshold = stopLineParserInfo.mergeLineThetaThreshold_arg; + stopLineConf->mergeLineRThreshold = stopLineParserInfo.mergeLineRThreshold_arg; + + stopLineConf->numStrips = stopLineParserInfo.numStrips_arg; + + + stopLineConf->checkSplines = stopLineParserInfo.checkSplines_arg; + stopLineConf->checkSplinesCurvenessThreshold = stopLineParserInfo.checkSplinesCurvenessThreshold_arg; + stopLineConf->checkSplinesLengthThreshold = stopLineParserInfo.checkSplinesLengthThreshold_arg; + stopLineConf->checkSplinesThetaDiffThreshold = stopLineParserInfo.checkSplinesThetaDiffThreshold_arg; + stopLineConf->checkSplinesThetaThreshold = stopLineParserInfo.checkSplinesThetaThreshold_arg; + + stopLineConf->checkIPMSplines = stopLineParserInfo.checkIPMSplines_arg; + stopLineConf->checkIPMSplinesCurvenessThreshold = stopLineParserInfo.checkIPMSplinesCurvenessThreshold_arg; + stopLineConf->checkIPMSplinesLengthThreshold = stopLineParserInfo.checkIPMSplinesLengthThreshold_arg; + stopLineConf->checkIPMSplinesThetaDiffThreshold = stopLineParserInfo.checkIPMSplinesThetaDiffThreshold_arg; + stopLineConf->checkIPMSplinesThetaThreshold = stopLineParserInfo.checkIPMSplinesThetaThreshold_arg; + + stopLineConf->finalSplineScoreThreshold = stopLineParserInfo.finalSplineScoreThreshold_arg; + + stopLineConf->useGroundPlane = stopLineParserInfo.useGroundPlane_arg; + + stopLineConf->checkColor = stopLineParserInfo.checkColor_arg; + stopLineConf->checkColorNumBins = stopLineParserInfo.checkColorNumBins_arg; + stopLineConf->checkColorWindow = stopLineParserInfo.checkColorWindow_arg; + stopLineConf->checkColorNumYellowMin = stopLineParserInfo.checkColorNumYellowMin_arg; + stopLineConf->checkColorRGMin = stopLineParserInfo.checkColorRGMin_arg; + stopLineConf->checkColorRGMax = stopLineParserInfo.checkColorRGMax_arg; + stopLineConf->checkColorGBMin = stopLineParserInfo.checkColorGBMin_arg; + stopLineConf->checkColorRBMin = stopLineParserInfo.checkColorRBMin_arg; + stopLineConf->checkColorRBFThreshold = stopLineParserInfo.checkColorRBFThreshold_arg; + stopLineConf->checkColorRBF = stopLineParserInfo.checkColorRBF_arg; + + stopLineConf->ipmWindowClear = stopLineParserInfo.ipmWindowClear_arg;; + stopLineConf->ipmWindowLeft = stopLineParserInfo.ipmWindowLeft_arg;; + stopLineConf->ipmWindowRight = stopLineParserInfo.ipmWindowRight_arg;; + + stopLineConf->checkLaneWidth = stopLineParserInfo.checkLaneWidth_arg;; + stopLineConf->checkLaneWidthMean = stopLineParserInfo.checkLaneWidthMean_arg;; + stopLineConf->checkLaneWidthStd = stopLineParserInfo.checkLaneWidthStd_arg;; +} + +void SHOW_LINE(const Line line, char str[]) +{ + cerr << str; + cerr << "(" << line.startPoint.x << "," << line.startPoint.y << ")"; + cerr << "->"; + cerr << "(" << line.endPoint.x << "," << line.endPoint.y << ")"; + cerr << "\n"; +} + +void SHOW_SPLINE(const Spline spline, char str[]) +{ + cerr << str; + cerr << "(" << spline.degree << ")"; + for (int i=0; i &x, vector &y) +CvMat * mcvGetLinePixels(const Line &line) +{ + //get two end points + CvPoint start; + start.x = int(line.startPoint.x); start.y = int(line.startPoint.y); + CvPoint end; + end.x = int(line.endPoint.x); end.y = int(line.endPoint.y); + + //get deltas + int deltay = end.y - start.y; + int deltax = end.x - start.x; + + //check if slope is steep, then reflect the line along y=x i.e. swap x and y + bool steep = false; + if (abs(deltay) > abs(deltax)) + { + steep = true; + //swap x and y + int t; + t = start.x; + start.x = start.y; + start.y = t; + t = end.x; + end.x = end.y; + end.y = t; + } + + + //check to make sure we are going right + bool swap = false; + if(start.x>end.x) + { + //swap the two points + CvPoint t = start; + start = end; + end = t; + //we swapped + swap = true; + } + + //get deltas again + deltay = end.y - start.y; + deltax = end.x - start.x; + + //error + int error = 0; + + //delta error + int deltaerror = abs(deltay); + + //ystep + int ystep = -1; + if (deltay>=0) + { + ystep = 1; + } + + //create the return matrix + CvMat *pixels = cvCreateMat(end.x-start.x+1, 2, CV_32SC1); + + //loop + int i, j; + j = start.y; + //list x, y; + //index for array + int k, kupdate; + if (!swap) + { + k = 0; + kupdate = 1; + } + else + { + k = pixels->height-1; + kupdate = -1; + } + + for (i=start.x; i<=end.x; i++, k+=kupdate) + { + //put the new point + if(steep) + { + CV_MAT_ELEM(*pixels, int, k, 0) = j; + CV_MAT_ELEM(*pixels, int, k, 1) = i; + // x.push_back(j); + // y.push_back(i); + } + else + { + CV_MAT_ELEM(*pixels, int, k, 0) = i; + CV_MAT_ELEM(*pixels, int, k, 1) = j; + // x.push_back(i); + // y.push_back(j); + } + + //adjust error + error += deltaerror; + //check + if(2*error>=deltax) + { + j = j + ystep; + error -= deltax; + } + } + + //return + return pixels; +} + +/** This functions implements Bresenham's algorithm for getting pixels of the + * line given its two endpoints + + * + * \param im the input image + * \param inLine the input line + * \param outLine the output line + * + */ +void mcvGetLineExtent(const CvMat *im, const Line &inLine, Line &outLine) +{ + //first clip the input line to the image coordinates + Line line = inLine; + mcvIntersectLineWithBB(&inLine, cvSize(im->width-1, im->height-1), &line); + + //then get the pixel values of the line in the image + CvMat *pixels; //vector x, y; + pixels = mcvGetLinePixels(line); //, x, y); + + //check which way to shift the line to get multiple readings + bool changey = false; + if (fabs(line.startPoint.x-line.endPoint.x) > + fabs(line.startPoint.y-line.endPoint.y)) + { + //change the y-coordiantes + changey = true; + } + char changes[] = {0, -1, 1};//, -2, 2}; + int numChanges = 3; + + //loop on the changes and get possible extents + vector startLocs; + vector endLocs; + int endLoc; + int startLoc; + CvMat *pix = cvCreateMat(1, im->width, FLOAT_MAT_TYPE); + CvMat *rstep = cvCreateMat(pix->height, pix->width, FLOAT_MAT_TYPE); + CvMat *fstep = cvCreateMat(pix->height, pix->width, FLOAT_MAT_TYPE); + for (int c=0; cheight; i++) + { + CV_MAT_ELEM(*pix, FLOAT_MAT_ELEM_TYPE, 0, i) = + CV_MAT_ELEM(*im, FLOAT_MAT_ELEM_TYPE, + changey ? + min(max(CV_MAT_ELEM(*pixels, int, i, 1)+ + changes[c],0),im->height-1) : + CV_MAT_ELEM(*pixels, int, i, 1), + changey ? CV_MAT_ELEM(*pixels, int, i, 0) : + min(max(CV_MAT_ELEM(*pixels, int, i, 0)+ + changes[c],0),im->width-1)); + // changey ? min(max(y[i]+changes[c],0),im->height-1) : y[i], + // changey ? x[i] : min(max(x[i]+changes[c],0),im->width-1)); + } + //remove the mean + CvScalar mean = cvAvg(pix); + cvSubS(pix, mean, pix); + + //now convolve with rising step to get start point + FLOAT_MAT_ELEM_TYPE stepp[] = {-0.3000, -0.2, -0.1, 0, 0, 0.1, 0.2, 0.3, 0.4}; + // {-0.6, -0.4, -0.2, 0.2, 0.4, 0.6}; + int stepsize = 9; + //{-0.2, -0.4, -0.2, 0, 0, 0.2, 0.4, 0.2}; //{-.75, -.5, .5, .75}; + CvMat step = cvMat(1, stepsize, FLOAT_MAT_TYPE, stepp); + // SHOW_MAT(&step,"step"); + //smooth + // FLOAT_MAT_ELEM_TYPE smoothp[] = {.25, .5, .25}; + //CvMat smooth = cvMat(1, 3, FLOAT_MAT_TYPE, smoothp); + //cvFilter2D(&step, &step, &smooth); + //SHOW_MAT(&step,"smoothed step"); + //convolve + cvFilter2D(pix, rstep, &step); + //get local max + // vector localMax; + // vector localMaxLoc; + // mcvGetVectorLocalMax(rstep, localMax, localMaxLoc); + // int startLoc = localMaxLoc[0]; + double max; + mcvGetVectorMax(rstep, &max, &startLoc, 0); + //check if zero + if(max==0) + startLoc = startLocs[c-1]; + + //convolve with falling step to get end point + //cvFlip(&step, NULL, 1); + //convolve + //cvFilter2D(pix, fstep, &step); + //get local max + // localMax.clear(); + // localMaxLoc.clear(); + // mcvGetVectorLocalMax(fstep, localMax, localMaxLoc); + // int endLoc = localMaxLoc[0]; + //take the negative + cvConvertScale(rstep, fstep, -1); + mcvGetVectorMax(fstep, &max, &endLoc, 0); + //check if zero + if(max==0) + endLoc = endLocs[c-1]; + if(endLoc<=startLoc) + endLoc = im->width-1; + + //put into vectors + startLocs.push_back(startLoc); + endLocs.push_back(endLoc); + } + + //get median + startLoc = quantile(startLocs, 0); + endLoc = quantile(endLocs, 1); + // for (int i=0; i<(int)startLocs.size(); i++) cout << startLocs[i] << " "; + //cout << "\n"; + //for (int i=0; i<(int)endLocs.size(); i++) cout << endLocs[i] << " "; + + //get the end-point + outLine.startPoint.x = CV_MAT_ELEM(*pixels, int, startLoc, 0); + outLine.startPoint.y = CV_MAT_ELEM(*pixels, int, startLoc, 1); + outLine.endPoint.x = CV_MAT_ELEM(*pixels, int, endLoc, 0); + outLine.endPoint.y = CV_MAT_ELEM(*pixels, int, endLoc, 1); + // outLine.startPoint.x = x[startLoc]; outLine.startPoint.y = y[startLoc]; + // outLine.endPoint.x = x[endLoc]; outLine.endPoint.y = y[endLoc]; + + //clear + cvReleaseMat(&pix); + cvReleaseMat(&rstep); + cvReleaseMat(&fstep); + cvReleaseMat(&pixels); + // localMax.clear(); + // localMaxLoc.clear(); + startLocs.clear(); + endLocs.clear(); +} + + + +/** This functions converts a line defined by its two end-points into its + * r and theta (origin is at top-left corner with x right and y down and + * theta measured positive clockwise(with y pointing down) -pi < theta < pi ) + * + * \param line input line + * \param r the returned r (normal distance to the line from the origin) + * \param outLine the output line + * + */ +void mcvLineXY2RTheta(const Line &line, float &r, float &theta) +{ + //check if vertical line x1==x2 + if(line.startPoint.x == line.endPoint.x) + { + //r is the x + r = fabs(line.startPoint.x); + //theta is 0 or pi + theta = line.startPoint.x>=0 ? 0. : CV_PI; + } + //check if horizontal i.e. y1==y2 + else if(line.startPoint.y == line.endPoint.y) + { + //r is the y + r = fabs(line.startPoint.y); + //theta is pi/2 or -pi/2 + theta = (float) line.startPoint.y>=0 ? CV_PI/2 : -CV_PI/2; + } + //general line + else + { + //tan(theta) = (x2-x1)/(y1-y2) + theta = atan2(line.endPoint.x-line.startPoint.x, + line.startPoint.y-line.endPoint.y); + //r = x*cos(theta)+y*sin(theta) + float r1 = line.startPoint.x * cos(theta) + line.startPoint.y * sin(theta); + r = line.endPoint.x * cos(theta) + line.endPoint.y * sin(theta); + //adjust to add pi if necessary + if(r1<0 || r<0) + { + //add pi + theta += CV_PI; + if(theta>CV_PI) + theta -= 2*CV_PI; + //take abs + r = fabs(r); + } + } +} + +/** This functions fits a line using the orthogonal distance to the line + by minimizing the sum of squares of this distance. + + * + * \param points the input points to fit the line to which is + * 2xN matrix with x values on first row and y values on second + * \param lineRTheta the return line [r, theta] where the line is + * x*cos(theta)+y*sin(theta)=r + * \param lineAbc the return line in [a, b, c] where the line is + * a*x+b*y+c=0 + * + */ +void mcvFitRobustLine(const CvMat *points, float *lineRTheta, + float *lineAbc) +{ + // check number of points + if (points->cols < 2) + { + return; + } + + //clone the points + CvMat *cpoints = cvCloneMat(points); + //get mean of the points and subtract from the original points + float meanX=0, meanY=0; + CvScalar mean; + CvMat row1, row2; + //get first row, compute avg and store + cvGetRow(cpoints, &row1, 0); + mean = cvAvg(&row1); + meanX = (float) mean.val[0]; + cvSubS(&row1, mean, &row1); + //same for second row + cvGetRow(cpoints, &row2, 1); + mean = cvAvg(&row2); + meanY = (float) mean.val[0]; + cvSubS(&row2, mean, &row2); + + //compute the SVD for the centered points array + CvMat *W = cvCreateMat(2, 1, CV_32FC1); + CvMat *V = cvCreateMat(2, 2, CV_32FC1); + // CvMat *V = cvCreateMat(2, 2, CV_32fC1); + CvMat *cpointst = cvCreateMat(cpoints->cols, cpoints->rows, CV_32FC1); + // now + printf("%d & %d\n", cpoints->cols, cpoints->rows); + cvTranspose(cpoints, cpointst); + cvSVD(cpointst, W, 0, V, CV_SVD_V_T); + cvTranspose(V, V); + cvReleaseMat(&cpointst); + + //get the [a,b] which is the second column corresponding to + //smaller singular value + float a, b, c; + a = CV_MAT_ELEM(*V, float, 0, 1); + b = CV_MAT_ELEM(*V, float, 1, 1); + + //c = -meanX*a-meanY*b + c = -(meanX * a + meanY * b); + + //compute r and theta + //theta = atan(b/a) + //r = meanX cos(theta) + meanY sin(theta) + float r, theta; + theta = atan2(b, a); + r = meanX * cos(theta) + meanY * sin(theta); + //correct + if (r<0) + { + //correct r + r = -r; + //correct theta + theta += CV_PI; + if (theta>CV_PI) + theta -= 2*CV_PI; + } + //return + if (lineRTheta) + { + lineRTheta[0] = r; + lineRTheta[1] = theta; + } + if (lineAbc) + { + lineAbc[0] = a; + lineAbc[1] = b; + lineAbc[2] = c; + } + //clear + cvReleaseMat(&cpoints); + cvReleaseMat(&W); + cvReleaseMat(&V); +} + + + +/** This functions implements RANSAC algorithm for line fitting + * given an image + * + * + * \param image input image + * \param numSamples number of samples to take every iteration + * \param numIterations number of iterations to run + * \param threshold threshold to use to assess a point as a good fit to a line + * \param numGoodFit number of points close enough to say there's a good fit + * \param getEndPoints whether to get the end points of the line from the data, + * just intersect with the image boundaries + * \param lineType the type of line to look for (affects getEndPoints) + * \param lineXY the fitted line + * \param lineRTheta the fitted line [r; theta] + * \param lineScore the score of the line detected + * + */ +void mcvFitRansacLine(const CvMat *image, int numSamples, int numIterations, + float threshold, float scoreThreshold, int numGoodFit, + bool getEndPoints, LineType lineType, + Line *lineXY, float *lineRTheta, float *lineScore) +{ + + //get the points with non-zero pixels + CvMat *points; + points = mcvGetNonZeroPoints(image,true); + if (!points) + return; + //check numSamples + if (numSamples>points->cols) + numSamples = points->cols; + //subtract half + cvAddS(points, cvRealScalar(0.5), points); + + //normalize pixels values to get weights of each non-zero point + //get third row of points containing the pixel values + CvMat w; + cvGetRow(points, &w, 2); + //normalize it + CvMat *weights = cvCloneMat(&w); + cvNormalize(weights, weights, 1, 0, CV_L1); + //get cumulative sum + mcvCumSum(weights, weights); + + //random number generator + CvRNG rng = cvRNG(0xffffffff); + //matrix to hold random sample + CvMat *randInd = cvCreateMat(numSamples, 1, CV_32SC1); + CvMat *samplePoints = cvCreateMat(2, numSamples, CV_32FC1); + //flag for points currently included in the set + CvMat *pointIn = cvCreateMat(1, points->cols, CV_8SC1); + //returned lines + float curLineRTheta[2], curLineAbc[3]; + float bestLineRTheta[2]={-1.f,0.f}, bestLineAbc[3]; + float bestScore=0, bestDist=1e5; + float dist, score; + Line curEndPointLine={{-1.,-1.},{-1.,-1.}}, + bestEndPointLine={{-1.,-1.},{-1.,-1.}}; + //variabels for getting endpoints + //int mini, maxi; + float minc=1e5f, maxc=-1e5f, mind, maxd; + float x, y, c=0.; + CvPoint2D32f minp={-1., -1.}, maxp={-1., -1.}; + //outer loop + for (int i=0; icols)); + mcvSampleWeighted(weights, numSamples, randInd, &rng); + + for (int j=0; jmaxc) + { + maxc = c; + maxp = cvPoint2D32f(x, y); + } + if (ccols; j++) + { + // //if not already inside + // if (!CV_MAT_ELEM(*pointIn, char, 0, j)) + // { + //compute distance to line + dist = fabs(CV_MAT_ELEM(*points, float, 0, j) * curLineAbc[0] + + CV_MAT_ELEM(*points, float, 1, j) * curLineAbc[1] + curLineAbc[2]); + //check distance + if (dist<=threshold) + { + //add this point + CV_MAT_ELEM(*pointIn, char, 0, j) = 1; + //update score + score += cvGetReal2D(image, (int)(CV_MAT_ELEM(*points, float, 1, j)-.5), + (int)(CV_MAT_ELEM(*points, float, 0, j)-.5)); + } + // } + } + + //check the number of close points and whether to consider this a good fit + int numClose = cvCountNonZero(pointIn); + //cout << "numClose=" << numClose << "\n"; + if (numClose >= numGoodFit) + { + //get the points included to fit this line + CvMat *fitPoints = cvCreateMat(2, numClose, CV_32FC1); + int k=0; + //loop on points and copy points included + for (int j=0; jcols; j++) + if(CV_MAT_ELEM(*pointIn, char, 0, j)) + { + CV_MAT_ELEM(*fitPoints, float, 0, k) = + CV_MAT_ELEM(*points, float, 0, j); + CV_MAT_ELEM(*fitPoints, float, 1, k) = + CV_MAT_ELEM(*points, float, 1, j); + k++; + + } + + //fit the line + mcvFitRobustLine(fitPoints, curLineRTheta, curLineAbc); + + //compute distances to new line + dist = 0.; + for (int j=0; jcols; j++) + { + //compute distance to line + x = CV_MAT_ELEM(*fitPoints, float, 0, j); + y = CV_MAT_ELEM(*fitPoints, float, 1, j); + float d = fabs( x * curLineAbc[0] + + y * curLineAbc[1] + + curLineAbc[2]) + * cvGetReal2D(image, (int)(y-.5), (int)(x-.5)); + dist += d; + + // //check min and max coordinates to get extent + // if (getEndPoints) + // { + // //get the coordinate to work on + // if (lineType == LINE_HORIZONTAL) + // c = x; + // else if (lineType == LINE_VERTICAL) + // c = y; + // //compare + // if (c>maxc) + // { + // maxc = c; + // maxd = d; + // maxp = cvPoint2D32f(x, y); + // } + // if (c=scoreThreshold && score>bestScore)//dist bestScore) + { + //update max + bestScore = score; //numClose; + bestDist = dist; + //copy + bestLineRTheta[0] = curLineRTheta[0]; + bestLineRTheta[1] = curLineRTheta[1]; + bestLineAbc[0] = curLineAbc[0]; + bestLineAbc[1] = curLineAbc[1]; + bestLineAbc[2] = curLineAbc[2]; + bestEndPointLine = curEndPointLine; + } + } // if numClose + + //debug + if (DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + char str[256]; + //convert image to rgb + CvMat* im = cvCloneMat(image); + mcvScaleMat(image, im); + CvMat *imageClr = cvCreateMat(image->rows, image->cols, CV_32FC3); + cvCvtColor(im, imageClr, CV_GRAY2RGB); + + Line line; + //draw current line if there + if (curLineRTheta[0]>0) + { + mcvIntersectLineRThetaWithBB(curLineRTheta[0], curLineRTheta[1], + cvSize(image->cols, image->rows), &line); + mcvDrawLine(imageClr, line, CV_RGB(1,0,0), 1); + if (getEndPoints) + mcvDrawLine(imageClr, curEndPointLine, CV_RGB(0,1,0), 1); + } + + //draw best line + if (bestLineRTheta[0]>0) + { + mcvIntersectLineRThetaWithBB(bestLineRTheta[0], bestLineRTheta[1], + cvSize(image->cols, image->rows), &line); + mcvDrawLine(imageClr, line, CV_RGB(0,0,1), 1); + if (getEndPoints) + mcvDrawLine(imageClr, bestEndPointLine, CV_RGB(1,1,0), 1); + } + sprintf(str, "scor=%.2f, best=%.2f", score, bestScore); + mcvDrawText(imageClr, str, cvPoint(30, 30), .25, CV_RGB(255,255,255)); + + SHOW_IMAGE(imageClr, "Fit Ransac Line", 10); + + //clear + cvReleaseMat(&im); + cvReleaseMat(&imageClr); + }//#endif + } // for i + + //return + if (lineRTheta) + { + lineRTheta[0] = bestLineRTheta[0]; + lineRTheta[1] = bestLineRTheta[1]; + } + if (lineXY) + { + if (getEndPoints) + *lineXY = bestEndPointLine; + else + mcvIntersectLineRThetaWithBB(lineRTheta[0], lineRTheta[1], + cvSize(image->cols-1, image->rows-1), + lineXY); + } + if (lineScore) + *lineScore = bestScore; + + //clear + cvReleaseMat(&points); + cvReleaseMat(&samplePoints); + cvReleaseMat(&randInd); + cvReleaseMat(&pointIn); +} + + + + +/** This function gets the indices of the non-zero values in a matrix + * + * \param inMat the input matrix + * \param outMat the output matrix, with 2xN containing the x and y in + * each column and the pixels value [xs; ys; pixel values] + * \param floatMat whether to return floating points or integers for + * the outMat + */ +CvMat* mcvGetNonZeroPoints(const CvMat *inMat, bool floatMat) +{ + + +#define MCV_GET_NZ_POINTS(inMatType, outMatType) \ + /*loop and allocate the points*/ \ + for (int i=0; irows; i++) \ + for (int j=0; jcols; j++) \ + if (CV_MAT_ELEM(*inMat, inMatType, i, j)) \ + { \ + CV_MAT_ELEM(*outMat, outMatType, 0, k) = j; \ + CV_MAT_ELEM(*outMat, outMatType, 1, k) = i; \ + CV_MAT_ELEM(*outMat, outMatType, 2, k) = \ + (outMatType) CV_MAT_ELEM(*inMat, inMatType, i, j); \ + k++; \ + } \ + + int k=0; + + //get number of non-zero points + int numnz = cvCountNonZero(inMat); + + //allocate the point array and get the points + CvMat* outMat; + if (numnz) + { + if (floatMat) + outMat = cvCreateMat(3, numnz, CV_32FC1); + else + outMat = cvCreateMat(3, numnz, CV_32SC1); + } + else + return NULL; + + //check type + if (CV_MAT_TYPE(inMat->type)==FLOAT_MAT_TYPE && + CV_MAT_TYPE(outMat->type)==FLOAT_MAT_TYPE) + { + MCV_GET_NZ_POINTS(FLOAT_MAT_ELEM_TYPE, FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==FLOAT_MAT_TYPE && + CV_MAT_TYPE(outMat->type)==INT_MAT_TYPE) + { + MCV_GET_NZ_POINTS(FLOAT_MAT_ELEM_TYPE, INT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==INT_MAT_TYPE && + CV_MAT_TYPE(outMat->type)==FLOAT_MAT_TYPE) + { + MCV_GET_NZ_POINTS(INT_MAT_ELEM_TYPE, FLOAT_MAT_ELEM_TYPE) + } + else if (CV_MAT_TYPE(inMat->type)==INT_MAT_TYPE && + CV_MAT_TYPE(outMat->type)==INT_MAT_TYPE) + { + MCV_GET_NZ_POINTS(INT_MAT_ELEM_TYPE, INT_MAT_ELEM_TYPE) + } + else + { + cerr << "Unsupported type in mcvGetMatLocalMax\n"; + exit(1); + } + + //return + return outMat; +} + + +/** This function groups nearby lines + * + * \param lines vector of lines + * \param lineScores scores of input lines + * \param groupThreshold the threshold used for grouping + * \param bbox the bounding box to intersect with + */ +void mcvGroupLines(vector &lines, vector &lineScores, + float groupThreshold, CvSize bbox) +{ + + //convert the lines into r-theta parameters + int numInLines = lines.size(); + vector rs(numInLines); + vector thetas(numInLines); + for (int i=0; i::iterator ir, jr, itheta, jtheta, minIr, minJr, minItheta, minJtheta, + iscore, jscore, minIscore, minJscore; + //compute pairwise distance between detected maxima + for (ir=rs.begin(), itheta=thetas.begin(), iscore=lineScores.begin(); + ir!=rs.end(); ir++, itheta++, iscore++) + for (jr=ir+1, jtheta=itheta+1, jscore=iscore+1; + jr!=rs.end(); jr++, jtheta++, jscore++) + { + //add pi if neg + float t1 = *itheta<0 ? *itheta : *itheta+CV_PI; + float t2 = *jtheta<0 ? *jtheta : *jtheta+CV_PI; + //get distance + dist = 1 * fabs(*ir - *jr) + 1 * fabs(t1 - t2);//fabs(*itheta - *jtheta); + //check if minimum + if (dist= groupThreshold) + stop = true; + else + { + //put into the first + *minIr = (*minIr + *minJr)/2; + *minItheta = (*minItheta + *minJtheta)/2; + *minIscore = (*minIscore + *minJscore)/2; + //delete second one + rs.erase(minJr); + thetas.erase(minJtheta); + lineScores.erase(minJscore); + } + }//while + + //put back the lines + lines.clear(); + //lines.resize(rs.size()); + vector newScores=lineScores; + lineScores.clear(); + for (int i=0; i<(int)rs.size(); i++) + { + //get the line + Line line; + mcvIntersectLineRThetaWithBB(rs[i], thetas[i], bbox, &line); + //put in place descendingly + vector::iterator iscore; + vector::iterator iline; + for (iscore=lineScores.begin(), iline=lines.begin(); + iscore!=lineScores.end() && newScores[i]<=*iscore; iscore++, iline++); + lineScores.insert(iscore, newScores[i]); + lines.insert(iline, line); + } + //clear + newScores.clear(); +} + +/** This function groups nearby splines + * + * \param splines vector of splines + * \param lineScores scores of input lines + */ +void mcvGroupSplines(vector &splines, vector &scores) + +{ + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + CvMat* im = cvCreateMat(240, 320, CV_8UC3); + cvSet(im, cvRealScalar(0.)); + //draw splines + for (unsigned int i=0; i::iterator spi, spj; + vector::iterator si, sj; + for (spi=splines.begin(), si=scores.begin(); + spi!=splines.end(); spi++, si++) + for (spj=spi+1, sj=si+1; spj!=splines.end(); spj++, sj++) + //if to merge them + if (mcvCheckMergeSplines(*spi, *spj, .1, 5, .2, 10, 15)) + { + stop = false; + //keep straighter one + float ci, cj; + mcvGetSplineFeatures(*spi, 0, 0, 0, 0, 0, 0, &ci); + mcvGetSplineFeatures(*spj, 0, 0, 0, 0, 0, 0, &cj); + //put j in i if less curved + if (cj>ci) + { + //put spline j into i + *spi = *spj; + *si = *sj; + } + //remove j + splines.erase(spj); + scores.erase(sj); + + //break + break; + } + }//while + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + CvMat* im = cvCreateMat(240, 320, CV_8UC3); + cvSet(im, cvRealScalar(0.)); + //draw splines + for (unsigned int i=0; i &boxes, LineType type, + float groupThreshold) +{ + bool cont = true; + + //Todo: check if to intersect with bounding box or not + + //save boxes + //vector tboxes = boxes; + + //loop to get the largest overlap (according to type) and check + //the overlap ratio + float overlap, maxOverlap; + while(cont) + { + maxOverlap = overlap = -1e5; + //loop on lines and get max overlap + vector::iterator i, j, maxI, maxJ; + for(i = boxes.begin(); i != boxes.end(); i++) + { + for(j = i+1; j != boxes.end(); j++) + { + switch(type) + { + case LINE_VERTICAL: + //get one with smallest x, and compute the x2 - x1 / width of smallest + //i.e. (x12 - x21) / (x22 - x21) + overlap = i->x < j->x ? + (i->x + i->width - j->x) / (float)j->width : + (j->x + j->width - i->x) / (float)i->width; + + break; + + case LINE_HORIZONTAL: + //get one with smallest y, and compute the y2 - y1 / height of smallest + //i.e. (y12 - y21) / (y22 - y21) + overlap = i->y < j->y ? + (i->y + i->height - j->y) / (float)j->height : + (j->y + j->height - i->y) / (float)i->height; + + break; + + } //switch + + //get maximum + if(overlap > maxOverlap) + { + maxI = i; + maxJ = j; + maxOverlap = overlap; + } + } //for j + } // for i + // //debug + // if(DEBUG_LINES) { + // cout << "maxOverlap=" << maxOverlap << endl; + // cout << "Before grouping\n"; + // for(unsigned int k=0; k= groupThreshold) + { + //combine the two boxes + *maxI = cvRect(min((*maxI).x, (*maxJ).x), + min((*maxI).y, (*maxJ).y), + max((*maxI).width, (*maxJ).width), + max((*maxI).height, (*maxJ).height)); + //delete the second one + boxes.erase(maxJ); + } + else + //stop + cont = false; + + // //debug + // if(DEBUG_LINES) { + // cout << "After grouping\n"; + // for(unsigned int k=0; k &lines, + vector &lineScores, LaneDetectorConf *lineConf, + LineType lineType) +{ + //check if to binarize image + CvMat *image = cvCloneMat(im); + if (lineConf->ransacLineBinarize) + mcvBinarizeImage(image); + + int width = image->width-1; + int height = image->height-1; + //try grouping the lines into regions + //float groupThreshold = 15; + mcvGroupLines(lines, lineScores, lineConf->groupThreshold, + cvSize(width, height)); + + //group bounding boxes of lines + float overlapThreshold = lineConf->overlapThreshold; //0.5; //.8; + vector boxes; + mcvGetLinesBoundingBoxes(lines, lineType, cvSize(width, height), + boxes); + mcvGroupBoundingBoxes(boxes, lineType, overlapThreshold); + // mcvGroupLinesBoundingBoxes(lines, lineType, overlapThreshold, + // cvSize(width, height), boxes); + + // //check if there're no lines, then check the whole image + // if (boxes.size()<1) + // boxes.push_back(cvRect(0, 0, width-1, height-1)); + + int window = lineConf->ransacLineWindow; //15; + vector newLines; + vector newScores; + for (int i=0; i<(int)boxes.size(); i++) //lines + { + // fprintf(stderr, "i=%d\n", i); + //Line line = lines[i]; + CvRect mask, box; + //get box + box = boxes[i]; + switch (lineType) + { + case LINE_HORIZONTAL: + { + //get extent + //int ystart = (int)fmax(fmin(line.startPoint.y, line.endPoint.y)-window, 0); + //int yend = (int)fmin(fmax(line.startPoint.y, line.endPoint.y)+window, height-1); + int ystart = (int)fmax(box.y - window, 0); + int yend = (int)fmin(box.y + box.height + window, height-1); + //get the mask + mask = cvRect(0, ystart, width, yend-ystart+1); + } + break; + + case LINE_VERTICAL: + { + //get extent of window to search in + //int xstart = (int)fmax(fmin(line.startPoint.x, line.endPoint.x)-window, 0); + //int xend = (int)fmin(fmax(line.startPoint.x, line.endPoint.x)+window, width-1); + int xstart = (int)fmax(box.x - window, 0); + int xend = (int)fmin(box.x + box.width + window, width-1); + //get the mask + mask = cvRect(xstart, 0, xend-xstart+1, height); + } + break; + } + //get the subimage to work on + CvMat *subimage = cvCloneMat(image); + //clear all but the mask + mcvSetMat(subimage, mask, 0); + + //get the RANSAC line in this part + //int numSamples = 5, numIterations = 10, numGoodFit = 15; + //float threshold = 0.5; + float lineRTheta[2]={-1,0}; + float lineScore; + Line line; + mcvFitRansacLine(subimage, lineConf->ransacLineNumSamples, + lineConf->ransacLineNumIterations, + lineConf->ransacLineThreshold, + lineConf->ransacLineScoreThreshold, + lineConf->ransacLineNumGoodFit, + lineConf->getEndPoints, lineType, + &line, lineRTheta, &lineScore); + + //store the line if found and make sure it's not + //near horizontal or vertical (depending on type) + #warning "check this screening in ransacLines" + if (lineRTheta[0]>=0) + { + bool put =true; + switch(lineType) + { + case LINE_HORIZONTAL: + //make sure it's not vertical + if (fabs(lineRTheta[1]) < 30*CV_PI/180) + put = false; + break; + + case LINE_VERTICAL: + //make sure it's not horizontal + if((fabs(lineRTheta[1]) > 20*CV_PI/180)) + put = false; + break; + } + if (put) + { + newLines.push_back(line); + newScores.push_back(lineScore); + } + } // if + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //get string + char str[256]; + switch (lineType) + { + case LINE_HORIZONTAL: + sprintf(str, "Subimage Line H #%d", i); + break; + case LINE_VERTICAL: + sprintf(str, "Subimage Line V #%d", i); + break; + } + //convert image to rgb + mcvScaleMat(subimage, subimage); + CvMat *subimageClr = cvCreateMat(subimage->rows, subimage->cols, + CV_32FC3); + cvCvtColor(subimage, subimageClr, CV_GRAY2RGB); + //draw rectangle + // mcvDrawRectangle(subimageClr, box, + // CV_RGB(255, 255, 0), 1); + mcvDrawRectangle(subimageClr, mask, CV_RGB(255, 255, 255), 1); + + //draw line + if (lineRTheta[0]>0) + mcvDrawLine(subimageClr, line, CV_RGB(1,0,0), 1); + SHOW_IMAGE(subimageClr, str, 10); + //clear + cvReleaseMat(&subimageClr); + }//#endif + + //clear + cvReleaseMat(&subimage); + } // for i + + //group lines + vector oldLines; + if (DEBUG_LINES) + oldLines = lines; + lines.clear(); + lineScores.clear(); + #warning "not grouping at end of getRansacLines" + //mcvGroupLines(newLines, newScores, lineConf->groupThreshold, cvSize(width, height)); + lines = newLines; + lineScores = newScores; + + //draw splines + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //get string + char title[256]; //str[256], + switch (lineType) + { + case LINE_HORIZONTAL: + sprintf(title, "Lines H"); + break; + case LINE_VERTICAL: + sprintf(title, "Lines V"); + break; + } + //convert image to rgb + CvMat* im2 = cvCloneMat(im); + mcvScaleMat(im2, im2); + CvMat *imClr = cvCreateMat(im->rows, im->cols, CV_32FC3); + cvCvtColor(im2, imClr, CV_GRAY2RGB); + CvMat* imClr2 = cvCloneMat(imClr); + cvReleaseMat(&im2); + + //draw spline + for (unsigned int j=0; j::iterator li; +// vector::iterator si; +// for (int i=0; i<(int)newLines.size(); i++) +// { + // //get its position + // for (li=lines.begin(), si=lineScores.begin(); + // si!=lineScores.end() && newScores[i]<=*si; + // si++, li++); + // lines.insert(li, newLines[i]); + // lineScores.insert(si, newScores[i]); + // } + + //clean + boxes.clear(); + newLines.clear(); + newScores.clear(); + cvReleaseMat(&image); +} + +/** This function sets the matrix to a value except for the mask window passed in + * + * \param inMat input matrix + * \param mask the rectangle defining the mask: (xleft, ytop, width, height) + * \param val the value to put + */ +void mcvSetMat(CvMat *inMat, CvRect mask, double val) +{ + + //get x-end points of region to work on, and work on the whole image height + //(int)fmax(fmin(line.startPoint.x, line.endPoint.x)-xwindow, 0); + int xstart = mask.x, xend = mask.x + mask.width-1; + //xend = (int)fmin(fmax(line.startPoint.x, line.endPoint.x), width-1); + int ystart = mask.y, yend = mask.y + mask.height-1; + + //set other two windows to zero + CvMat maskMat; + CvRect rect; + //part to the left of required region + rect = cvRect(0, 0, xstart-1, inMat->height); + if (rect.xwidth && rect.yheight && + rect.x>=0 && rect.y>=0 && rect.width>0 && rect.height>0) + { + cvGetSubRect(inMat, &maskMat, rect); + cvSet(&maskMat, cvRealScalar(val)); + } + //part to the right of required region + rect = cvRect(xend+1, 0, inMat->width-xend-1, inMat->height); + if (rect.xwidth && rect.yheight && + rect.x>=0 && rect.y>=0 && rect.width>0 && rect.height>0) + { + cvGetSubRect(inMat, &maskMat, rect); + cvSet(&maskMat, cvRealScalar(val)); + } + + //part to the top + rect = cvRect(xstart, 0, mask.width, ystart-1); + if (rect.xwidth && rect.yheight && + rect.x>=0 && rect.y>=0 && rect.width>0 && rect.height>0) + { + cvGetSubRect(inMat, &maskMat, rect); + cvSet(&maskMat, cvRealScalar(val)); + } + + //part to the bottom + rect = cvRect(xstart, yend+1, mask.width, inMat->height-yend-1); + if (rect.xwidth && rect.yheight && + rect.x>=0 && rect.y>=0 && rect.width>0 && rect.height>0) + { + cvGetSubRect(inMat, &maskMat, rect); + cvSet(&maskMat, cvRealScalar(val)); + } +} + + +/** This function sorts a set of points + * + * \param inPOints Nx2 matrix of points [x,y] + * \param outPOints Nx2 matrix of points [x,y] + * \param dim the dimension to sort on (0: x, 1:y) + * \param dir direction of sorting (0: ascending, 1:descending) + */ +void mcvSortPoints(const CvMat *inPoints, CvMat *outPoints, + int dim, int dir) +{ + //make a copy of the input + CvMat *pts = cvCloneMat(inPoints); + + //clear the output + //cvSetZero(outPoints); + + //make the list of sorted indices + list sorted; + list::iterator sortedi; + int i, j; + + //loop on elements and adjust its index + for (i=0; iheight; i++) + { + //if ascending + if (dir==0) + for (sortedi = sorted.begin(); + sortedi != sorted.end() && + (CV_MAT_ELEM(*pts, float, i, dim) >= + CV_MAT_ELEM(*outPoints, float, *sortedi, dim)); + sortedi++); + //descending + else + for (sortedi = sorted.begin(); + sortedi != sorted.end() && + (CV_MAT_ELEM(*pts, float, i, dim) <= + CV_MAT_ELEM(*outPoints, float, *sortedi, dim)); + sortedi++); + + //found the position, so put it into sorted + sorted.insert(sortedi, i); + } + + //sorted the array, so put back + for (i=0, sortedi=sorted.begin(); sortedi != sorted.end(); sortedi++, i++) + for(j=0; jwidth; j++) + CV_MAT_ELEM(*outPoints, float, i, j) = CV_MAT_ELEM(*pts, float, + *sortedi, j); + + //clear + cvReleaseMat(&pts); + sorted.clear(); +} + +/** This function fits a Bezier spline to the passed input points + * + * \param points the input points + * \param degree the required spline degree + * \return spline the returned spline + */ +Spline mcvFitBezierSpline(CvMat *points, int degree) +{ + + //set the degree + Spline spline; + spline.degree = degree; + + //get number of points + int n = points->height; + //float step = 1./(n-1); + + //sort the pointa + mcvSortPoints(points, points, 1, 0); + // SHOW_MAT(points, "Points after sorting:"); + + //get first point and distance between points + CvPoint2D32f p0 = cvPoint2D32f(CV_MAT_ELEM(*points, float, 0, 0), + CV_MAT_ELEM(*points, float, 0, 1)); + + float diff = 0.f; + float *us = new float[points->height]; + us[0] = 0; + for (int i=1; iheight; ++i) + { + float dx = CV_MAT_ELEM(*points, float, i, 0) - + CV_MAT_ELEM(*points, float, i-1, 0); + float dy = CV_MAT_ELEM(*points, float, i, 1) - + CV_MAT_ELEM(*points, float, i-1, 1); + us[i] = cvSqrt(dx*dx + dy*dy) + us[i-1]; + // diff += us[i];; + } + diff = us[points->height-1]; + + //float y0 = CV_MAT_ELEM(*points, float, 0, 1); + //float ydiff = CV_MAT_ELEM(*points, float, points->height-1, 1) - y0; + + //M matrices: M2 for quadratic (degree 2) and M3 for cubic + float M2[] = {1, -2, 1, + -2, 2, 0, + 1, 0, 0}; + float M3[] = {-1, 3, -3, 1, + 3, -6, 3, 0, + -3, 3, 0, 0, + 1, 0, 0, 0}; + + //M matrix for Bezier + CvMat M; + + //Basis matrix + CvMat *B; + + //u value for points to create the basis matrix + float u = 0.f; + + //switch on the degree + switch(degree) + { + //Quadratic spline + case 2: + //M matrix + M = cvMat(3, 3, CV_32FC1, M2); + + //create the basis matrix + B = cvCreateMat(n, 3, CV_32FC1); + for (int i=0; iheight; i++) //u+=step + { + //get u as ratio of y-coordinate + // u = i / ((float)n-1); + + // u = (CV_MAT_ELEM(*points, float, i, 1) - y0) / ydiff; + + // float dx = CV_MAT_ELEM(*points, float, i, 0) - p0.x; + // float dy = CV_MAT_ELEM(*points, float, i, 1) - p0.y; + // u = cvSqrt(dx*dx + dy*dy) / diff; + u = us[i] / diff; + + CV_MAT_ELEM(*B, float, i, 2) = 1; //1 + CV_MAT_ELEM(*B, float, i, 1) = u; //u + CV_MAT_ELEM(*B, float, i, 0) = u*u; //u^2 + } + break; + + //Cubic spline + case 3: + //M matrix + M = cvMat(4, 4, CV_32FC1, M3); + + //create the basis matrix + B = cvCreateMat(n, 4, CV_32FC1); + for (int i=0; iheight; i++) //, u+=step) + { + //get u as ratio of y-coordinate + // u = i / ((float)n-1); + + // u = (CV_MAT_ELEM(*points, float, i, 1) - y0) / ydiff; + + // float dx = CV_MAT_ELEM(*points, float, i, 0) - p0.x; + // float dy = CV_MAT_ELEM(*points, float, i, 1) - p0.y; + // u = cvSqrt(dx*dx + dy*dy) / diff; + u = us[i] / diff; + + CV_MAT_ELEM(*B, float, i, 3) = 1; //1 + CV_MAT_ELEM(*B, float, i, 2) = u; //u + CV_MAT_ELEM(*B, float, i, 1) = u*u; //u^2 + CV_MAT_ELEM(*B, float, i, 0) = u*u*u; //u^2 + } + break; + } // switch degree + + //multiply B by M + cvMatMul(B, &M, B); + + + //return the required control points by LS + CvMat *sp = cvCreateMat(degree+1, 2, CV_32FC1); + cvSolve(B, points, sp, CV_SVD); + + // SHOW_MAT(sp, "Spline points:"); + + //put back into spline + memcpy((float *)spline.points, sp->data.fl, sizeof(float)*(spline.degree+1)*2); + // if(spline.points[0].x<0) + // SHOW_MAT(points, "INput Points"); + + //clear + cvReleaseMat(&B); + cvReleaseMat(&sp); + delete [] us; + + //return + return spline; +} + + + +/** This function evaluates Bezier spline with given resolution + * + * \param spline input spline + * \param h the input resolution + * \param tangents compute tangents at the two endpoints [t0; t1] + * \return computed points in an array Nx2 [x,y] + */ +CvMat* mcvEvalBezierSpline(const Spline &spline, float h, CvMat *tangents) +{ + //compute number of points to return + int n = (int)(1./h)+1; + + //allocate the points + CvMat *points = cvCreateMat(n, 2, CV_32FC1); + + //M matrices + CvMat M; + float M2[] = {1, -2, 1, + -2, 2, 0, + 1, 0, 0}; + float M3[] = {-1, 3, -3, 1, + 3, -6, 3, 0, + -3, 3, 0, 0, + 1, 0, 0, 0}; + + //spline points + CvMat *sp = cvCreateMat(spline.degree+1, 2, CV_32FC1); + memcpy(sp->data.fl, (float *)spline.points, + sizeof(float)*(spline.degree+1)*2); + + //abcd + CvMat *abcd; + + float P[2], dP[2], ddP[2], dddP[2]; + float h2 = h*h, h3 = h2*h; + + //switch the degree + switch(spline.degree) + { + //Quadratic + case 2: + //get M matrix + M = cvMat(3, 3, CV_32FC1, M2); + + //get abcd where a=row 0, b=row 1, ... + abcd = cvCreateMat(3, 2, CV_32FC1); + cvMatMul(&M, sp, abcd); + + //P = c + P[0] = CV_MAT_ELEM(*abcd, float, 2, 0); + P[1] = CV_MAT_ELEM(*abcd, float, 2, 1); + + //dP = b*h+a*h^2 + dP[0] = CV_MAT_ELEM(*abcd, float, 1, 0)*h + + CV_MAT_ELEM(*abcd, float, 0, 0)*h2; + dP[1] = CV_MAT_ELEM(*abcd, float, 1, 1)*h + + CV_MAT_ELEM(*abcd, float, 0, 1)*h2; + + //ddP = 2*a*h^2 + ddP[0] = 2 * CV_MAT_ELEM(*abcd, float, 0, 0)*h2; + ddP[1] = 2 * CV_MAT_ELEM(*abcd, float, 0, 1)*h2; + + //loop and put points + for (int i=0; iheight, 1, CV_8SC1); + //cvSet(, CvScalar value, const CvArr* mask=NULL); + list inpoints; + list::iterator inpointsi; + int lastin = -1, numin = 0; + for (int i=0; iheight; i++) + { + //round + CV_MAT_ELEM(*points, float, i, 0) = cvRound(CV_MAT_ELEM(*points, float, i, 0)); + CV_MAT_ELEM(*points, float, i, 1) = cvRound(CV_MAT_ELEM(*points, float, i, 1)); + + //check boundaries + if(CV_MAT_ELEM(*points, float, i, 0) >= 0 && + CV_MAT_ELEM(*points, float, i, 0) < box.width && + CV_MAT_ELEM(*points, float, i, 1) >= 0 && + CV_MAT_ELEM(*points, float, i, 1) < box.height) + { + //it's inside, so check if the same as last one + if(lastin<0 || + (lastin>=0 && + !(CV_MAT_ELEM(*points, float, lastin, 1)== + CV_MAT_ELEM(*points, float, i, 1) && + CV_MAT_ELEM(*points, float, lastin, 0)== + CV_MAT_ELEM(*points, float, i, 0) )) ) + { + //put inside + //CV_MAT_ELEM(*inpoints, char, i, 0) = 1; + inpoints.push_back(i); + lastin = i; + numin++; + } + } + } + + //check if to extend the spline with lines + CvMat *pixelst0, *pixelst1; + if (extendSpline) + { + //get first point inside + int p0 = inpoints.front(); + //extend from the starting point by going backwards along the tangent + //line from that point to the start of spline + Line line; + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, p0, 0) - 10 * + CV_MAT_ELEM(*tangents, float, 0, 0), + CV_MAT_ELEM(*points, float, p0, 1) - 10 * + CV_MAT_ELEM(*tangents, float, 0, 1)); + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, p0, 0), + CV_MAT_ELEM(*points, float, p0, 1)); + //intersect the line with the bounding box + mcvIntersectLineWithBB(&line, cvSize(box.width-1, box.height-1), &line); + //get line pixels + pixelst0 = mcvGetLinePixels(line); + numin += pixelst0->height; + + //get last point inside + int p1 = inpoints.back(); + //extend from end of spline along tangent + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, p1, 0) + 10 * + CV_MAT_ELEM(*tangents, float, 1, 0), + CV_MAT_ELEM(*points, float, p1, 1) + 10 * + CV_MAT_ELEM(*tangents, float, 1, 1)); + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, p1, 0), + CV_MAT_ELEM(*points, float, p1, 1)); + //intersect the line with the bounding box + mcvIntersectLineWithBB(&line, cvSize(box.width-1, box.height-1), &line); + //get line pixels + pixelst1 = mcvGetLinePixels(line); + numin += pixelst1->height; + } + + //put the results in another matrix + CvMat *rpoints; + if (numin>0) + rpoints = cvCreateMat(numin, 2, CV_32SC1); + else + { + return NULL; + } + + + //first put extended line segment if available + if(extendSpline) + { + //copy + memcpy(cvPtr2D(rpoints, 0, 0), pixelst0->data.fl, + sizeof(float)*2*pixelst0->height); + } + + //put spline pixels + int ri = extendSpline ? pixelst0->height : 0; + for (inpointsi=inpoints.begin(); + inpointsi!=inpoints.end(); ri++, inpointsi++) + { + CV_MAT_ELEM(*rpoints, int, ri, 0) = (int)CV_MAT_ELEM(*points, + float, *inpointsi, 0); + CV_MAT_ELEM(*rpoints, int, ri, 1) = (int)CV_MAT_ELEM(*points, + float, *inpointsi, 1); + } + + //put second extended piece of spline + if(extendSpline) + { + //copy + memcpy(cvPtr2D(rpoints, ri, 0), pixelst1->data.fl, + sizeof(float)*2*pixelst1->height); + //clear + cvReleaseMat(&pixelst0); + cvReleaseMat(&pixelst1); + } + + + //release + // cvReleaseMat(&inpoints); + cvReleaseMat(&points); + cvReleaseMat(&tangents); + inpoints.clear(); + + //return + return rpoints; +} + + +/** This function performs a RANSAC validation step on the detected lines to + * get splines + * + * \param image the input image + * \param lines vector of input lines to refine + * \param lineSCores the line scores input + * \param groupThreshold the threshold used for grouping + * \param bbox the bounding box to intersect with + * \param lineType the line type to work on (horizontal or vertical) + * \param prevSplines the previous splines to use in initializing the detection + */ +void mcvGetRansacSplines(const CvMat *im, vector &lines, + vector &lineScores, LaneDetectorConf *lineConf, + LineType lineType, vector &splines, + vector &splineScores, LineState* state) +{ + //check if to binarize image + CvMat *image = cvCloneMat(im); + if (lineConf->ransacSplineBinarize) + mcvBinarizeImage(image); // ((topmost-intro . 147431)) + + int width = image->width; + int height = image->height; + //try grouping the lines into regions + //float groupThreshold = 15; + #warning "no line grouping in getRansacSplines" + vector tlines = lines; + vector tlineScores = lineScores; + mcvGroupLines(tlines, tlineScores, lineConf->groupThreshold, + cvSize(width-1, height-1)); + + //put the liens into the prevSplines to initialize it + for (unsigned int i=0; state->ipmSplines.size() && + iransacSplineDegree); + state->ipmSplines.push_back(spline); + } + + //group bounding boxes of lines + float overlapThreshold = lineConf->overlapThreshold; //0.5; //.8; + vector boxes; + CvSize size = cvSize(width, height); + mcvGetLinesBoundingBoxes(tlines, lineType, size, boxes); + mcvGroupBoundingBoxes(boxes, lineType, overlapThreshold); + // mcvGroupLinesBoundingBoxes(tlines, lineType, overlapThreshold, + // cvSize(width, height), boxes); + tlines.clear(); + tlineScores.clear(); + + //add bounding boxes from previous frame + #warning "Turned off adding boxes from previous frame" + // boxes.insert(boxes.end(), state->ipmBoxes.begin(), + // state->ipmBoxes.end()); + + // //check if there're no lines, then check the whole image + // if (boxes.size()<1) + // boxes.push_back(cvRect(0, 0, width-1, height-1)); + + int window = lineConf->ransacSplineWindow; //15; + vector newSplines; + vector newSplineScores; + for (int i=0; i<(int)boxes.size(); i++) //lines + { + //Line line = lines[i]; + + CvRect mask, box; + + //get box + box = boxes[i]; + + switch (lineType) + { + case LINE_HORIZONTAL: + { + //get extent + //int ystart = (int)fmax(fmin(line.startPoint.y, line.endPoint.y)-window, 0); + //int yend = (int)fmin(fmax(line.startPoint.y, line.endPoint.y)+window, height-1); + int ystart = (int)fmax(box.y - window, 0); + int yend = (int)fmin(box.y + box.height + window, height-1); + //get the mask + mask = cvRect(0, ystart, width, yend-ystart+1); + } + break; + + case LINE_VERTICAL: + { + //get extent of window to search in + //int xstart = (int)fmax(fmin(line.startPoint.x, line.endPoint.x)-window, 0); + //int xend = (int)fmin(fmax(line.startPoint.x, line.endPoint.x)+window, width-1); + int xstart = (int)fmax(box.x - window, 0); + int xend = (int)fmin(box.x + box.width + window, width-1); + //get the mask + mask = cvRect(xstart, 0, xend-xstart+1, height); + } + break; + } + //get the subimage to work on + CvMat *subimage = cvCloneMat(image); + //clear all but the mask + mcvSetMat(subimage, mask, 0); + + //get the RANSAC spline in this part + //int numSamples = 5, numIterations = 10, numGoodFit = 15; + //float threshold = 0.5; + Spline spline; + float splineScore; + //resolution to use in pixelizing the spline + float h = lineConf->ransacSplineStep; // .1; //1. / max(image->width, image->height); + mcvFitRansacSpline(subimage, lineConf->ransacSplineNumSamples, + lineConf->ransacSplineNumIterations, + lineConf->ransacSplineThreshold, + lineConf->ransacSplineScoreThreshold, + lineConf->ransacSplineNumGoodFit, + lineConf->ransacSplineDegree, h, + &spline, &splineScore, + lineConf->splineScoreJitter, + lineConf->splineScoreLengthRatio, + lineConf->splineScoreAngleRatio, + lineConf->splineScoreStep, + &state->ipmSplines); + + //store the line if found + if (spline.degree > 0) + { + newSplines.push_back(spline); + newSplineScores.push_back(splineScore); + } + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //get string + char str[256], title[256];; + switch (lineType) + { + case LINE_HORIZONTAL: + sprintf(title, "Subimage Spline H #%d", i); + break; + case LINE_VERTICAL: + sprintf(title, "Subimage Spline V #%d", i); + break; + } + //convert image to rgb + mcvScaleMat(subimage, subimage); + CvMat *subimageClr = cvCreateMat(subimage->rows, subimage->cols, + CV_32FC3); + cvCvtColor(subimage, subimageClr, CV_GRAY2RGB); + + //draw rectangle + //mcvDrawRectangle(subimageClr, box, + // CV_RGB(255, 255, 0), 1); + mcvDrawRectangle(subimageClr, mask, CV_RGB(255, 255, 255), 1); + + //put text + sprintf(str, "score=%.2f", splineScore); + // mcvDrawText(subimageClr, str, cvPoint(30, 30), + // .25f, CV_RGB(1,1,1)); + + //draw spline + if (spline.degree > 0) + mcvDrawSpline(subimageClr, spline, CV_RGB(1,0,0), 1); + SHOW_IMAGE(subimageClr, title, 10); + //clear + cvReleaseMat(&subimageClr); + }//#endif + + //clear + cvReleaseMat(&subimage); + }//for + + + //put splines back in descending order of scores + splines.clear(); + splineScores.clear(); + vector::iterator li; + vector::iterator si; + for (int i=0; i<(int)newSplines.size(); i++) + { + //get its position + for (li=splines.begin(), si=splineScores.begin(); + si!=splineScores.end() && newSplineScores[i]<=*si; + si++, li++); + splines.insert(li, newSplines[i]); + splineScores.insert(si, newSplineScores[i]); + } + + //group the splines + mcvGroupSplines(splines, splineScores); + + //draw splines + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //get string + char title[256]; //str[256], + switch (lineType) + { + case LINE_HORIZONTAL: + sprintf(title, "Splines H"); + break; + case LINE_VERTICAL: + sprintf(title, "Splines V"); + break; + } + //convert image to rgb + CvMat* im2 = cvCloneMat(im); + mcvScaleMat(im2, im2); + CvMat *imClr = cvCreateMat(im->rows, im->cols, CV_32FC3); + cvCvtColor(im2, imClr, CV_GRAY2RGB); + cvReleaseMat(&im2); + + //draw spline + for (unsigned int j=0; j *prevSplines) +{ + //get the points with non-zero pixels + CvMat *points = mcvGetNonZeroPoints(image, true); + if (points==0 || points->cols < numSamples) + { + if (spline) spline->degree = -1; + cvReleaseMat(&points); + return; + } + // fprintf(stderr, "num points=%d", points->cols); + //subtract half + #warning "check adding half to points" + CvMat p; + cvGetRows(points, &p, 0, 2); + cvAddS(&p, cvRealScalar(0.5), &p); + + //normalize pixels values to get weights of each non-zero point + //get third row of points containing the pixel values + CvMat w; + cvGetRow(points, &w, 2); + //normalize it + CvMat *weights = cvCloneMat(&w); + cvNormalize(weights, weights, 1, 0, CV_L1); + //get cumulative sum + mcvCumSum(weights, weights); + + //random number generator + CvRNG rng = cvRNG(0xffffffff); + //matrix to hold random sample + CvMat *randInd = cvCreateMat(numSamples, 1, CV_32SC1); + CvMat *samplePoints = cvCreateMat(numSamples, 2, CV_32FC1); + //flag for points currently included in the set + CvMat *pointIn = cvCreateMat(1, points->cols, CV_8SC1); + //returned splines + Spline curSpline, bestSpline; + bestSpline.degree = 0;//initialize + float bestScore=0; //, bestDist=1e5; + + //iterator for previous splines + vector::iterator prevSpline; + bool randSpline = prevSplines==NULL || prevSplines->size()==0; + if (!randSpline) prevSpline = prevSplines->begin(); + + //fprintf(stderr, "spline degree=%d\n", prevSpline->degree); + + //outer loop + for (int i=0; iend(); + } // if + //get random spline + else + { + //set flag to zero + cvSetZero(pointIn); + //get random sample from the points + //cvRandArr(&rng, randInd, CV_RAND_UNI, cvRealScalar(0), cvRealScalar(points->cols)); + mcvSampleWeighted(weights, numSamples, randInd, &rng); + // SHOW_MAT(randInd, "randInd"); + for (int j=0; jrows; j++) //numSamples + { + //flag it as included + int p = CV_MAT_ELEM(*randInd, int, j, 0); + CV_MAT_ELEM(*pointIn, char, 0, p) = 1; + //put point + CV_MAT_ELEM(*samplePoints, float, j, 0) = + CV_MAT_ELEM(*points, float, 0, p); + CV_MAT_ELEM(*samplePoints, float, j, 1) = + CV_MAT_ELEM(*points, float, 1, p); + } + + //fit the spline + curSpline = mcvFitBezierSpline(samplePoints, splineDegree); + // SHOW_MAT(samplePoints, "Sampled points"); + } // else + + + //get score + //float lengthRatio = 0.5; //.8 + //float angleRatio = 0.8; //.4 + //vectorjitter = mcvGetJitterVector(splineScoreJitter); //2); + float score = mcvGetSplineScore(image, curSpline, splineScoreStep,//.05,//h, + splineScoreJitter, //jitter, + splineScoreLengthRatio, + splineScoreAngleRatio); + + //jitter.clear(); + + //check if better than best score so far + //printf("Score=%.2f & scoreThreshold=%.2f\n", score, scoreThreshold); + if (score>bestScore && score >= scoreThreshold) + { + //put it + bestScore = score; + bestSpline = curSpline; + } + + //show image for debugging + if(0) { //DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //get string + char str[256]; + sprintf(str, "Spline Fit: score=%f, best=%f", score, bestScore); + //fprintf(stderr, str); + // SHOW_SPLINE(curSpline, "curSpline:"); + + + //convert image to rgb + CvMat *imageClr = cvCreateMat(image->rows, image->cols, + CV_32FC3); + CvMat *im = cvCloneMat(image); + mcvScaleMat(image, im); + cvCvtColor(im, imageClr, CV_GRAY2RGB); + //draw spline + //previous splines + for (unsigned int k=0; prevSplines && ksize(); ++k) + mcvDrawSpline(imageClr, (*prevSplines)[k], CV_RGB(0,1,0), 1); + if(curSpline.degree>0) + mcvDrawSpline(imageClr, curSpline, CV_RGB(1,0,0), 1); + if(bestSpline.degree>0) + mcvDrawSpline(imageClr, bestSpline, CV_RGB(0,0,1), 1); + + //put text + CvFont font; + cvInitFont(&font, CV_FONT_HERSHEY_TRIPLEX, .25f, .25f); + sprintf(str, "score=%.2f bestScre=%.2f", score, bestScore); + cvPutText(imageClr, str, cvPoint(30, 30), &font, CV_RGB(1,1,1)); + + sprintf(str, "Spline Fit"); + SHOW_IMAGE(imageClr, str, 10); + //clear + cvReleaseMat(&imageClr); + cvReleaseMat(&im); + }//#endif + } //for + + //return + if (spline) + *spline = bestSpline; + if (splineScore) + *splineScore = bestScore; + + + //clear + cvReleaseMat(&points); + cvReleaseMat(&samplePoints); + cvReleaseMat(&randInd); + cvReleaseMat(&pointIn); + cvReleaseMat(&weights); +} + +/** This function draws a spline onto the passed image + * + * \param image the input iamge + * \param spline input spline + * \param spline color + * + */ +void mcvDrawSpline(CvMat *image, Spline spline, CvScalar color, int width) +{ + //get spline pixels + CvMat *pixels = mcvGetBezierSplinePixels(spline, .05, + cvSize(image->width, image->height), + false); + //if no pixels + if (!pixels) + return; + + //draw pixels in image with that color + for (int i=0; iheight-1; i++) + // cvSet2D(image, + // (int)cvGetReal2D(pixels, i, 1), + // (int)cvGetReal2D(pixels, i, 0), + // color); + cvLine(image, cvPoint((int)cvGetReal2D(pixels, i, 0), + (int)cvGetReal2D(pixels, i, 1)), + cvPoint((int)cvGetReal2D(pixels, i+1, 0), + (int)cvGetReal2D(pixels, i+1, 1)), + color, width); + + //put the control points with circles + for (int i=0; i &lines, IPMInfo &ipmInfo, + CameraInfo &cameraInfo, CvSize imSize) +{ + //check if returned anything + if (lines.size()!=0) + { + //convert the line into world frame + for (unsigned int i=0; istartPoint), &ipmInfo); + mcvPointImIPM2World(&(line->endPoint), &ipmInfo); + } + + //convert them from world frame into camera frame + // + //put a dummy line at the beginning till we check that cvDiv bug + Line dummy = {{1.,1.},{2.,2.}}; + lines.insert(lines.begin(), dummy); + //convert to mat and get in image coordinates + CvMat *mat = cvCreateMat(2, 2*lines.size(), FLOAT_MAT_TYPE); + mcvLines2Mat(&lines, mat); + lines.clear(); + mcvTransformGround2Image(mat, mat, &cameraInfo); + //get back to vector + mcvMat2Lines(mat, &lines); + //remove the dummy line at the beginning + lines.erase(lines.begin()); + //clear + cvReleaseMat(&mat); + + //clip the lines found and get their extent + for (unsigned int i=0; i &splines, IPMInfo &ipmInfo, + CameraInfo &cameraInfo, CvSize imSize) +{ + //loop on splines and convert + for (int i=0; i<(int)splines.size(); i++) + { + //get points for this spline in IPM image + CvMat *points = mcvEvalBezierSpline(splines[i], .1); + + //transform these points to image coordinates + CvMat *points2 = cvCreateMat(2, points->height, CV_32FC1); + cvTranspose(points, points2); + //mcvPointImIPM2World(CvMat *mat, const IPMInfo *ipmInfo); + //mcvTransformGround2Image(points2, points2, &cameraInfo); + mcvTransformImIPM2Im(points2, points2, &ipmInfo, &cameraInfo); + cvTranspose(points2, points); + cvReleaseMat(&points2); + + //refit the points into a spline in output image + splines[i] = mcvFitBezierSpline(points, splines[i].degree); + } +} + + +/** This function samples uniformly with weights + * + * \param cumSum cumulative sum for normalized weights for the differnet + * samples (last is 1) + * \param numSamples the number of samples + * \param randInd a 1XnumSamples of int containing the indices + * \param rng a pointer to a random number generator + * + */ +void mcvSampleWeighted(const CvMat *cumSum, int numSamples, CvMat *randInd, + CvRNG *rng) +{ +// //get cumulative sum of the weights +// //OPTIMIZE:should pass it later instead of recomputing it +// CvMat *cumSum = cvCloneMat(weights); +// for (int i=1; icols; i++) +// CV_MAT_ELEM(*cumSum, float, 0, i) += CV_MAT_ELEM(*cumSum, float, 0, i-1); + + //check if numSamples is equal or more + int i=0; + if (numSamples >= cumSum->cols) + { + for (; icols && r>CV_MAT_ELEM(*cumSum, float, 0, j); j++); + + //make sure this index wasnt chosen before + bool put = true; + for (int k=0; krows == 1) \ + for (int i=1; icols; i++) \ + CV_MAT_ELEM(*outMat, type, 0, i) += \ + CV_MAT_ELEM(*outMat, type, 0, i-1); \ + /*column vector*/ \ + else \ + for (int i=1; irows; i++) \ + CV_MAT_ELEM(*outMat, type, i, 0) += \ + CV_MAT_ELEM(*outMat, type, i-1, 0); + + //copy to output if not equal + if(inMat != outMat) + cvCopy(inMat, outMat); + + //check type + if (CV_MAT_TYPE(inMat->type)==CV_32FC1) + { + MCV_CUM_SUM(float) + } + else if (CV_MAT_TYPE(inMat->type)==CV_32SC1) + { + MCV_CUM_SUM(int) + } + else + { + cerr << "Unsupported type in mcvCumSum\n"; + exit(1); + } +} + + +/** This functions gives better localization of points along lines + * + * \param im the input image + * \param inPoints the input points Nx2 matrix of points + * \param outPoints the output points Nx2 matrix of points + * \param numLinePixels Number of pixels to go in normal direction for + * localization + * \param angleThreshold Angle threshold used for localization + * (cosine, 1: most restrictive, 0: most liberal) + * + */ +void mcvLocalizePoints(const CvMat *im, const CvMat *inPoints, + CvMat *outPoints, int numLinePixels, + float angleThreshold) +{ + //size of inPoints must be at least 3 + if(inPoints->height<3) + { + cvCopy(inPoints, outPoints); + return; + } + + //number of pixels in line around each point + //int numLinePixels = 20; + //tangent and normal + CvPoint2D32f tangent, normal;// peakTangent; + + //threshold for accepting new point (if not changing orientation too much) + //float angleThreshold = .7;//.96; + CvMat *imageClr; + char str[256]; + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //get string + sprintf(str, "Localize Points"); + + //convert image to rgb + imageClr = cvCreateMat(im->rows, im->cols, CV_32FC3); + cvCvtColor(im, imageClr, CV_GRAY2RGB); + }//#endif + + + //loop on the points + for (int i=0; iheight; i++) + { + + //get tangent to current point + if (i==0) + { + //first point, then tangent is vector to next point + tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 1, 0) - + CV_MAT_ELEM(*inPoints, float, 0, 0), + CV_MAT_ELEM(*inPoints, float, 1, 1) - + CV_MAT_ELEM(*inPoints, float, 0, 1)); + } + else if (i==1) + tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 1, 0) - + CV_MAT_ELEM(*outPoints, float, 0, 0), + CV_MAT_ELEM(*inPoints, float, 1, 1) - + CV_MAT_ELEM(*outPoints, float, 0, 1)); + + else //if (i==inPoints->height-1) + { + //last pointm then vector from previous two point + tangent = cvPoint2D32f(CV_MAT_ELEM(*outPoints, float, i-1, 0) - + CV_MAT_ELEM(*outPoints, float, i-2, 0), + CV_MAT_ELEM(*outPoints, float, i-1, 1) - + CV_MAT_ELEM(*outPoints, float, i-2, 1)); + // tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, i, 0) - + // CV_MAT_ELEM(*outPoints, float, i-1, 0), + // CV_MAT_ELEM(*inPoints, float, i, 1) - + // CV_MAT_ELEM(*outPoints, float, i-1, 1)); + } +// else +// { +// //general point, then take next - previous +// tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, i, 0) - //i+1 +// CV_MAT_ELEM(*outPoints, float, i-1, 0), +// CV_MAT_ELEM(*inPoints, float, i, 1) - //i+1 +// CV_MAT_ELEM(*outPoints, float, i-1, 1)); +// } + + //get normal + float ss = cvInvSqrt(tangent.x * tangent.x + tangent.y * tangent.y); + tangent.x *= ss; tangent.y *= ss; + normal.x = tangent.y; normal.y = -tangent.x; + + //get points in normal direction + Line line; + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, i, 0) + + numLinePixels * normal.x, + CV_MAT_ELEM(*inPoints, float, i, 1) + + numLinePixels * normal.y); + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, i, 0) - + numLinePixels * normal.x, + CV_MAT_ELEM(*inPoints, float, i, 1) - + numLinePixels * normal.y); + + + CvPoint2D32f prevPoint = {0., 0.}; + if (i>0) + prevPoint = cvPoint2D32f(CV_MAT_ELEM(*outPoints, float, i-1, 0), + CV_MAT_ELEM(*outPoints, float, i-1, 1)); + + //get line peak i.e. point in middle of bright line on dark background + CvPoint2D32f peak; + // float val = mcvGetLinePeak(im, line, peak); + //get line peak + vector peaks; + vector peakVals; + float val = mcvGetLinePeak(im, line, peaks, peakVals); + + //choose the best peak + // int index = mcvChooseBestPeak(peaks, peakVals, peak, val, + // 0, tangent, + // prevPoint, angleThreshold); + peak = peaks.front(); + val = peakVals.front(); + //clear + peaks.clear(); + peakVals.clear(); + + //check new peak + if (mcvIsPointInside(line.startPoint, cvSize(im->width, im->height)) && + mcvIsPointInside(line.endPoint, cvSize(im->width, im->height)) && + (//!i || + (i>0 && + mcvIsValidPeak(peak, tangent, prevPoint, + angleThreshold))) ) + { + //put new peak + CV_MAT_ELEM(*outPoints, float, i, 0) = peak.x; + CV_MAT_ELEM(*outPoints, float, i, 1) = peak.y; + } + else + { + //keep original point + CV_MAT_ELEM(*outPoints, float, i, 0) = CV_MAT_ELEM(*inPoints, float, i, 0); + CV_MAT_ELEM(*outPoints, float, i, 1) = CV_MAT_ELEM(*inPoints, float, i, 1); + } + + //debugging + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + fprintf(stderr, "Localize val=%.3f\n", val); + + //draw original point, localized point, and line endpoints + cvLine(imageClr, cvPointFrom32f(line.startPoint), + cvPointFrom32f(line.endPoint), CV_RGB(0, 0, 1)); + //output points + cvCircle(imageClr, cvPoint((int)CV_MAT_ELEM(*outPoints, float, i, 0), + (int)CV_MAT_ELEM(*outPoints, float, i, 1)), + 1, CV_RGB(0, 1, 0), -1); + //input points + cvCircle(imageClr, cvPoint((int)(line.startPoint.x+line.endPoint.x)/2, + (int)(line.startPoint.y+line.endPoint.y)/2), + 1, CV_RGB(1, 0, 0), -1); + //show image + SHOW_IMAGE(imageClr, str, 10); + }//#endif + } // for i + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + SHOW_IMAGE(imageClr, str, 10); + //clear + cvReleaseMat(&imageClr); + }//#endif +} + + +/** This functions checks the peak point if much change in orientation + * + * \param peak the input peak point + * \param tangent the tangent line along which the peak was found normal to + * (normalized) + * \param prevPoint the previous point along the tangent + * \param angleThreshold the angle threshold to consider for valid peaks + * \return true if useful peak, zero otherwise + * + */ +bool mcvIsValidPeak(const CvPoint2D32f &peak, const CvPoint2D32f &tangent, + const CvPoint2D32f &prevPoint, float angleThreshold) +{ + //compute the tangent line for the peak + CvPoint2D32f peakTangent; + peakTangent.x = peak.x - prevPoint.x; + peakTangent.y = peak.y - prevPoint.y; + + //normalize new tangent + float ss = cvInvSqrt(peakTangent.x * peakTangent.x + peakTangent.y * + peakTangent.y); + peakTangent.x *= ss; peakTangent.y *= ss; + + //check angle between newTangent and tangent, and refuse peak if too far + float angle = fabs(peakTangent.x*tangent.x + peakTangent.y*tangent.y); + if (DEBUG_LINES) + fprintf(stderr, "angle=%f\n", angle); + //return + return (angle >= angleThreshold) ? true : false; + +} + + +/** This functions chooses the best peak that minimizes deviation + * from the tangent direction given + * + * \param peaks the peaks found + * \param peakVals the values for the peaks + * \param peak the returned peak + * \param peakVal the peak value for chosen peak, -1 if nothing + * \param contThreshold the threshold to get peak above + * \param tangent the tangent line along which the peak was found normal to + * (normalized) + * \param prevPoint the previous point along the tangent + * \param angleThreshold the angle threshold to consider for valid peaks + * \return index of peak chosen, -1 if nothing + * + */ +int mcvChooseBestPeak(const vector &peaks, + const vector &peakVals, + CvPoint2D32f &peak, float &peakVal, + float contThreshold, const CvPoint2D32f &tangent, + const CvPoint2D32f &prevPoint, float angleThreshold) +{ + int index=-1; + float maxAngle=0; + peakVal = -1; + + //loop and check + for (unsigned int i=0; i=contThreshold && angle>=angleThreshold && + angle>maxAngle) + { + //mark it as chosen + maxAngle = angle; + index = i; + } + } // for i + + //return + if (index>=0) + { + peak = peaks[index]; + peakVal = peakVals[index]; + } + + if (DEBUG_LINES) + fprintf(stderr, "Chosen peak is: (%f, %f)\n", peak.x, peak.y); + + return index; +} + + +/** This functions extends the given set of points in both directions to + * extend curves and lines in the image + * + * \param im the input image + * \param inPoints the input points Nx2 matrix of points + * \param angleThreshold angle threshold used for extending + * \param meanDirAngleThreshold angle threshold from mean direction + * \param linePixelsTangent number of pixels to go in tangent direction + * \param linePixelsNormal number of pixels to go in normal direction + * \param contThreshold number of pixels to go in tangent direction + * \param deviationThreshold Stop extending when number of deviating points + * exceeds this threshold + * \param bbox a bounding box not to get points outside + * \param smoothPeak whether to smooth for calculating peaks or not + * + */ +CvMat* mcvExtendPoints(const CvMat *im, const CvMat *inPoints, + float angleThreshold, float meanDirAngleThreshold, + int linePixelsTangent, int linePixelsNormal, + float contThreshold, int deviationThreshold, + CvRect bbox, bool smoothPeaks) +{ + //size of inPoints must be at least 3 + if(inPoints->height<4) + { + return cvCloneMat(inPoints); + } + + + char str[256]; + CvMat *imageClr; + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //get string + sprintf(str, "Extend Points"); + + //convert image to rgb + imageClr = cvCreateMat(im->rows, im->cols, CV_32FC3); + CvMat *im2 = cvCloneMat(im); + mcvScaleMat(im, im2); + cvCvtColor(im2, imageClr, CV_GRAY2RGB); + cvReleaseMat(&im2); + + //show original points + for(int i=0; iheight; i++) + //input points + cvCircle(imageClr, cvPoint((int)(CV_MAT_ELEM(*inPoints, float, i, 0)), + (int)(CV_MAT_ELEM(*inPoints, float, i, 1))), + 1, CV_RGB(0, 1, 1), -1); + //show image + SHOW_IMAGE(imageClr, str, 10); + }//#endif + + //tangent and normal + CvPoint2D32f tangent, curPoint, peak, nextPoint, meanDir; + //prevTangent, pprevTangent, + + //number of pixels away to look for points + //int linePixelsTangent = 5, linePixelsNormal = 20; + bool cont = true; + + //threshold for stopping + //float contThreshold = 0.1; //.1 for gaussian top //0.01; + + //angle threshold (max orientation change allowed) + //float angleThreshold = .7;//.5 //.8;//.5 //.866; + //float meanDirAngleThreshold = .7; + + //threshold to stop when deviating from normal orientation + //int deviationThreshold = 2; + + //first go in one direction: from first point backward + vector backPoints; + int numBack = 0; + int deviationCount = 0; + vector peaks; + vector peakVals; + //get mean direction of points + meanDir = mcvGetPointsMeanVector(inPoints, false); + while(cont) + { + int outSize = (int)backPoints.size(); + //get tangent from previous point in input points if no output points yet + if(outSize==0) + { + curPoint = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 0, 0), + CV_MAT_ELEM(*inPoints, float, 0, 1)); + tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 0, 0) - + CV_MAT_ELEM(*inPoints, float, 1, 0), + CV_MAT_ELEM(*inPoints, float, 0, 1) - + CV_MAT_ELEM(*inPoints, float, 1, 1)); + // prevTangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 1, 0) - + // CV_MAT_ELEM(*inPoints, float, 2, 0), + // CV_MAT_ELEM(*inPoints, float, 1, 1) - + // CV_MAT_ELEM(*inPoints, float, 2, 1)); + // prevTangent = mcvNormalizeVector(prevTangent); + + // pprevTangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, 2, 0) - + // CV_MAT_ELEM(*inPoints, float, 3, 0), + // CV_MAT_ELEM(*inPoints, float, 2, 1) - + // CV_MAT_ELEM(*inPoints, float, 3, 1)); + // pprevTangent = mcvNormalizeVector(pprevTangent); + } + //only one out point till now + else + { + // pprevTangent = prevTangent; + // prevTangent = tangent; + curPoint = backPoints[outSize-1]; + if (outSize==1) + { + tangent = cvPoint2D32f(backPoints[outSize-1].x - + CV_MAT_ELEM(*inPoints, float, 0, 0), + backPoints[outSize-1].y - + CV_MAT_ELEM(*inPoints, float, 0, 1)); + } + //more than one + else + { + tangent = cvPoint2D32f(backPoints[outSize-1].x - + backPoints[outSize-2].x, + backPoints[outSize-1].y - + backPoints[outSize-2].y); + } + } + + //get the line normal to tangent (tangent is normalized in function) + Line line; + line = mcvGetExtendedNormalLine(curPoint, tangent, linePixelsTangent, + linePixelsNormal, nextPoint); + + //check if still inside + //if (mcvIsPointInside(nextPoint, cvSize(im->width-1, im->height-1))) + if (mcvIsPointInside(nextPoint, bbox)) + { + //clip line + mcvIntersectLineWithBB(&line, cvSize(im->width-1, im->height-1), &line); + + //get line peak + float val = mcvGetLinePeak(im, line, peaks, peakVals, + true, smoothPeaks); + + //choose the best peak + //int index = + mcvChooseBestPeak(peaks, peakVals, peak, val, contThreshold, tangent, + curPoint, 0); //angleThreshold); + //clear + peaks.clear(); + peakVals.clear(); + + //check the peak + // !mcvIsValidPeak(peak, prevTangent, curPoint, angleThreshold) || + // !mcvIsValidPeak(peak, pprevTangent, curPoint, angleThreshold) || + if (!mcvIsValidPeak(peak, tangent, curPoint, angleThreshold) || + !mcvIsValidPeak(peak, meanDir, curPoint, meanDirAngleThreshold)) + { + peak = nextPoint; + deviationCount++; + } + else + deviationCount = 0; + + if (DEBUG_LINES){ + fprintf(stderr, "Extension back #%d val=%.3f\n", outSize, val); + fprintf(stderr, "Deviation Count=%d\n", deviationCount); + } + + //check value + //check value + if(valdeviationThreshold) + { + cont = false; + // for(int k=0; kdeviationThreshold) + // { + // cont = false; + // backPoints.erase(backPoints.end() - deviationThreshold, + // backPoints.end()); + // //numBack -= deviationThreshold; + // } + else + { + //push back + backPoints.push_back(peak); + //numBack++; + } + } // if mcvIsPointInside + //line got outside, so stop + else + cont = false; + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //draw original point, localized point, and line endpoints + cvLine(imageClr, cvPointFrom32f(line.startPoint), + cvPointFrom32f(line.endPoint), + CV_RGB(0, 0, 1)); + //output points + cvCircle(imageClr, cvPointFrom32f(peak), 1, CV_RGB(0, 1, 0), -1); + //input points + cvCircle(imageClr, cvPointFrom32f(nextPoint), 1, CV_RGB(1, 0, 0), -1); + //show image + SHOW_IMAGE(imageClr, str, 10); + }//#endif + } // while cont + + //do the same for the opposite direction + cont = true; + vector frontPoints; + int numFront = 0; + deviationCount = 0; + //get mean direction in forward direction + meanDir = mcvGetPointsMeanVector(inPoints, true); + while(cont) + { + int outSize = (int)frontPoints.size(); + //get tangent from previous point in input points if no output points yet + if(outSize==0) + { + curPoint = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 0), + CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 1)); + tangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 0) - + CV_MAT_ELEM(*inPoints, float, + inPoints->height-2, 0), + CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 1) - + CV_MAT_ELEM(*inPoints, float, + inPoints->height-2, 1)); + + // prevTangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, inPoints->height-2, 0) - + // CV_MAT_ELEM(*inPoints, float, inPoints->height-3, 0), + // CV_MAT_ELEM(*inPoints, float, inPoints->height-2, 1) - + // CV_MAT_ELEM(*inPoints, float, inPoints->height-3, 1)); + // prevTangent = mcvNormalizeVector(prevTangent); + + // pprevTangent = cvPoint2D32f(CV_MAT_ELEM(*inPoints, float, inPoints->height-3, 0) - + // CV_MAT_ELEM(*inPoints, float, inPoints->height-4, 0), + // CV_MAT_ELEM(*inPoints, float, inPoints->height-3, 1) - + // CV_MAT_ELEM(*inPoints, float, inPoints->height-4, 1)); + // pprevTangent = mcvNormalizeVector(pprevTangent); + } + //only one out point till now + else + { + // pprevTangent = prevTangent; + // prevTangent = tangent; + curPoint = frontPoints[outSize-1]; + if (outSize==1) + { + tangent = cvPoint2D32f(frontPoints[outSize-1].x - + CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 0), + frontPoints[outSize-1].y - + CV_MAT_ELEM(*inPoints, float, + inPoints->height-1, 1)); + } + //more than one + else + { + tangent = cvPoint2D32f(frontPoints[outSize-1].x - + frontPoints[outSize-2].x, + frontPoints[outSize-1].y - + frontPoints[outSize-2].y); + } + } + + Line line; + line = mcvGetExtendedNormalLine(curPoint, tangent, linePixelsTangent, + linePixelsNormal, nextPoint); + + //check if still inside + // if (mcvIsPointInside(nextPoint, cvSize(im->width-1, im->height-1))) + if (mcvIsPointInside(nextPoint, bbox)) + { + //clip line + mcvIntersectLineWithBB(&line, cvSize(im->width-1, im->height-1), &line); + + //get line peak + // float val = mcvGetLinePeak(im, line, peak); + float val = mcvGetLinePeak(im, line, peaks, peakVals, true, smoothPeaks); + + //choose the best peak + //int index = + mcvChooseBestPeak(peaks, peakVals, peak, val, contThreshold, tangent, + curPoint, 0); //angleThreshold); + + //clear + peaks.clear(); + peakVals.clear(); + + //check the peak +// !mcvIsValidPeak(peak, prevTangent, curPoint, angleThreshold) || +// !mcvIsValidPeak(peak, pprevTangent, curPoint, angleThreshold) || + if(!mcvIsValidPeak(peak, tangent, curPoint, angleThreshold) || + !mcvIsValidPeak(peak, meanDir, curPoint, meanDirAngleThreshold)) + { + //put normal point + peak = nextPoint; + //increment deviation count + deviationCount++; + } + else + deviationCount = 0; + + if (DEBUG_LINES){ + fprintf(stderr, "Extension front #%d val=%.3f\n", outSize, val); + fprintf(stderr, "Deviation Count=%d\n", deviationCount); + } + + //check value + if(valdeviationThreshold) + { + cont = false; + // for(int k=0; kdeviationThreshold) + // { + // cont = false; + // frontPoints.erase(frontPoints.end() - deviationThreshold, + // frontPoints.end()); + // } + else + { + //push back + frontPoints.push_back(peak); + //numFront++; + } + } + //line got outside, so stop + else + cont = false; + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + //draw original point, localized point, and line endpoints + cvLine(imageClr, cvPointFrom32f(line.startPoint), + cvPointFrom32f(line.endPoint), CV_RGB(0, 0, 1)); + //output points + cvCircle(imageClr, cvPointFrom32f(peak), 1, CV_RGB(0, 1, 0), -1); + //input points + cvCircle(imageClr, cvPointFrom32f(nextPoint), 1, CV_RGB(1, 0, 0), -1); + //show image + SHOW_IMAGE(imageClr, str, 10); + }//#endif + } + + numFront = frontPoints.size(); + numBack = backPoints.size(); + //now that we have extended the points in both directions, we need to put them + //back into the return matrix + CvMat *extendedPoints = cvCreateMat(inPoints->height + numBack + numFront, + 2, CV_32FC1); + //first put back points in reverse order + vector::iterator pointi; + int i = 0; + for (i=0, pointi=backPoints.end()-1; idata.fl, + sizeof(float)*2*inPoints->height); + + //then put the front points in normal order + for (i = numBack+inPoints->height, pointi=frontPoints.begin(); + iheight; pointi++, i++) + { + CV_MAT_ELEM(*extendedPoints, float, i, 0) = (*pointi).x; + CV_MAT_ELEM(*extendedPoints, float, i, 1) = (*pointi).y; + } + + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + SHOW_IMAGE(imageClr, str, 10); + //clear + cvReleaseMat(&imageClr); + }//#endif + + //clear + backPoints.clear(); + frontPoints.clear(); + //return + return extendedPoints; +} + + + +/** This functions extends a point along the tangent and gets the normal line + * at the new point + * + * \param curPoint the current point to extend + * \param tangent the tangent at this point (not necessarily normalized) + * \param linePixelsTangent the number of pixels to go in tangent direction + * \param linePixelsNormal the number of pixels to go in normal direction + * \param nextPoint the next point on the extended line + * \return the normal line at new point + */ +Line mcvGetExtendedNormalLine(CvPoint2D32f &curPoint, CvPoint2D32f &tangent, + int linePixelsTangent, int linePixelsNormal, + CvPoint2D32f &nextPoint) +{ + //normalize tangent + float ssq = cvInvSqrt(tangent.x*tangent.x + tangent.y*tangent.y); + tangent.x *= ssq; + tangent.y *= ssq; + + //get next point along the way + nextPoint.x = curPoint.x + linePixelsTangent * tangent.x; + nextPoint.y = curPoint.y + linePixelsTangent * tangent.y; + + //get normal direction + CvPoint2D32f normal = cvPoint2D32f(-tangent.y, tangent.x); + + //get two points along the normal line + Line line; + line.startPoint = cvPoint2D32f(nextPoint.x + linePixelsNormal*normal.x, + nextPoint.y + linePixelsNormal*normal.y); + line.endPoint = cvPoint2D32f(nextPoint.x - linePixelsNormal*normal.x, + nextPoint.y - linePixelsNormal*normal.y); + + //return + return line; +} + + + +/** This functions gets the point on the input line that matches the + * peak in the input image, where the peak is the middle of a bright + * line on dark background in the image + * + * \param im the input image + * \param line input line + * \param peaks a vector of peak outputs + * \param peakVals the values for each of these peaks + * \param positivePeak whether we are looking for positive or + * negative peak(default true) + * \param smoothPeaks whether to smooth pixels for calculating peaks + * or not + * + */ +float mcvGetLinePeak(const CvMat *im, const Line &line, + vector &peaks, + vector &peakVals, bool positivePeak, + bool smoothPeaks) +{ + //create step to convolve with + FLOAT_MAT_ELEM_TYPE stepp[] = //{-1, 0, 1};//{-1, -1, -1, 1, 1, 1}; + // {-0.3000, -0.2, -0.1, 0, 0, 0.1, 0.2, 0.3, 0.4}; + // {-0.6, -0.4, -0.2, 0.2, 0.4, 0.6}; + //latest-> {-0.2, -0.4, -0.2, 0, 0, 0.2, 0.4, 0.2}; //{-.75, -.5, .5, .75}; + { 0.000003726653172, 0.000040065297393, 0.000335462627903, + 0.002187491118183, 0.011108996538242, 0.043936933623407, + 0.135335283236613, 0.324652467358350, 0.606530659712633, + 0.882496902584595, 1.000000000000000, 0.882496902584595, + 0.606530659712633, 0.324652467358350, 0.135335283236613, + 0.043936933623407, 0.011108996538242, 0.002187491118183, + 0.000335462627903, 0.000040065297393, 0.000003726653172}; + int stepsize = 21; + CvMat step = cvMat(1, stepsize, CV_32FC1, stepp); + + //take negative to work for opposite polarity + if (!positivePeak) + cvScale(&step, &step, -1); + // //get the gaussian kernel to convolve with + // int width = 5; + // float step = .5; + // CvMat *step = cvCreateMat(1, (int)(2*width/step+1), CV_32FC1); + // int j; float i; + // for (i=-w, j=0; i<=w; i+=step, ++j) + // CV_MAT_ELEM(*step, FLOAT_MAT_ELEM_TYPE, 0, j) = + // (float) exp(-(.5*i*i)); + + + //then get the pixel coordinates of the line in the image + CvMat *pixels; + pixels = mcvGetLinePixels(line); + //get pixel values + CvMat *pix = cvCreateMat(1, pixels->height, CV_32FC1); + for(int j=0; jheight; j++) + { + CV_MAT_ELEM(*pix, float, 0, j) = + cvGetReal2D(im, + MIN(MAX(CV_MAT_ELEM(*pixels, int, j, 1),0),im->height-1), + MIN(MAX(CV_MAT_ELEM(*pixels, int, j, 0),0),im->width-1)); + } + //clear + cvReleaseMat(&pixels); + + //remove the mean + CvScalar mean = cvAvg(pix); + cvSubS(pix, mean, pix); + + //convolve with step + CvMat *pixStep = cvCreateMat(pix->height, pix->width, CV_32FC1); + if (smoothPeaks) + cvFilter2D(pix, pixStep, &step); + else + cvCopy(pix, pixStep); + // SHOW_MAT(pixStep, "pixStep"); + // SHOW_MAT(pix, "pixels"); + + //get local maxima + double topVal; + float top; + vector maxima; + vector maximaLoc; + CvPoint2D32f peak; + //get top point + mcvGetVectorLocalMax(pixStep, maxima, maximaLoc); + if(maximaLoc.size()>0) + { + //get max + topVal = maxima.front(); + //loop and get peaks + for (unsigned int i=0; iwidth-1)); + top = (float)mcvGetLocalMaxSubPixel(val1, maxima[i], val3); + top += maximaLoc[i]; + //fprintf(stderr, "val1=%f, val2=%f, val3=%f\n", val1, maxima[i], val3); + //fprintf(stderr, "top=%d, subpixel=%f\n", maximaLoc[i], top); + top /= pix->width; + //get loc +// top = maximaLoc[i]/(float)(pix->width); + //get peak + peak.x = line.startPoint.x*(1-top) + top * line.endPoint.x; + peak.y = line.startPoint.y*(1-top) + top * line.endPoint.y; + //push back + peaks.push_back(peak); + peakVals.push_back(maxima[i]); + } + } // if + else + { + top = (pix->width-2)/2./(pix->width); + topVal = -1; + //push back + peak.x = line.startPoint.x*(1-top) + top * line.endPoint.x; + peak.y = line.startPoint.y*(1-top) + top * line.endPoint.y; + //push back + peaks.push_back(peak); + peakVals.push_back(topVal); + + } + maxima.clear(); + maximaLoc.clear(); + +// //get new point +// top /= (pix->width); +// peak.x = line.startPoint.x*(1-top) + top * line.endPoint.x; +// peak.y = line.startPoint.y*(1-top) + top * line.endPoint.y; + + //clear + cvReleaseMat(&pix); + cvReleaseMat(&pixStep); + + //return mean of rising and falling val + return topVal;//MIN(risingVal, fallingVal);//no minus //(risingVal+fallingVal)/2; +} + +/** This functions normalizes the given vector + * + * \param vector the input vector to normalize + */ +CvPoint2D32f mcvNormalizeVector(const CvPoint2D32f &v) +{ + //return vector + CvPoint2D32f ret = v; + + //normalize vector + float ssq = cvInvSqrt(ret.x*ret.x + ret.y*ret.y); + ret.x *= ssq; + ret.y *= ssq; + + //return + return ret; +} + + +/** This functions normalizes the given vector + * + * \param vector the input vector to normalize + */ +CvPoint2D32f mcvNormalizeVector(const CvPoint &v) +{ + //return vector + return mcvNormalizeVector(cvPointTo32f(v)); + +} + +/** This functions normalizes the given vector + * + * \param x the x component + * \param y the y component + */ +CvPoint2D32f mcvNormalizeVector(float x, float y) +{ + //return vector + return mcvNormalizeVector(cvPoint2D32f(x, y)); +} + + +/** This functions adds two vectors and returns the result + * + * \param v1 the first vector + * \param v2 the second vector + * \return the sum + */ +CvPoint2D32f mcvAddVector(CvPoint2D32f v1, CvPoint2D32f v2) +{ + //get sum + CvPoint2D32f sum = cvPoint2D32f(v1.x + v2.x, v1.y + v2.y); + //return vector + return sum; +} + + +/** This functions multiplies a vector by a scalar + * + * \param v the vector + * \param s the scalar + * \return the sum + */ +CvPoint2D32f mcvMultiplyVector(CvPoint2D32f v, float s) +{ + //get sum + CvPoint2D32f prod; + prod.x = v.x * s; + prod.y = v.y * s; + //return vector + return prod; +} + +/** This functions computes the score of the given spline from the + * input image + * + * \param image the input image + * \param spline the input spline + * \param h spline resolution + * \param jitterVal the amounts to count scores around the spline in x & y + * directions + * \param lengthRatio the ratio to add to score from the spline length + * \param angleRatio the ratio to add to score from spline curvature measure + * + * \return the score + */ +float mcvGetSplineScore(const CvMat* image, Spline& spline, float h, + int jitterVal, float lengthRatio, float angleRatio) +{ + + //check that all control points for spline are inside the image + CvSize size = cvSize(image->width-1, image->height-1); + // SHOW_SPLINE(spline, "spline"); + for (int i=0; i<=spline.degree; i++) + if (!mcvIsPointInside(spline.points[i], size)) + return -100.f; + + //get the pixels that belong to the spline + CvMat *pixels = mcvGetBezierSplinePixels(spline, h, size, false); + if(!pixels) + return -100.f; + + //get jitter vector + vectorjitter = mcvGetJitterVector(jitterVal); //2); + + //compute its score by summing up pixel values belonging to it + //int jitter[] = {0, 1, -1, 2, -2}, jitterLength = 5; + //SHOW_MAT(pixels, "pixels"); + float score = 0.f; + for (unsigned int j=0; jheight; i++) + { + //jitter in x + // int k = MIN(MAX(CV_MAT_ELEM(*pixels, int, i, 0)+ + // jitter[j], 0), image->width-1); + // fprintf(stderr, "col=%d\n & row=%d", k, CV_MAT_ELEM(*pixels, int, i, 1)); + score += cvGetReal2D(image, CV_MAT_ELEM(*pixels, int, i, 1), + MIN(MAX(CV_MAT_ELEM(*pixels, int, i, 0) + + jitter[j], 0), image->width-1)); + // //jitter the y + // score += cvGetReal2D(image, + // MIN(MAX(CV_MAT_ELEM(*pixels, int, i, 1)+ + // jitter[j], 0), image->height-1), + // CV_MAT_ELEM(*pixels, int, i, 0)); + } // for i + + //length: min 0 and max of 1 (normalized according to max of width and height + //of image) + //float length = ((float)pixels->height) / MAX(image->width, image->height); + float length = 0.f; + // for (int i=0; iheight-1; i++) + // { + // //get the vector between every two consecutive points + // CvPoint2D32f v = + // mcvSubtractVector(cvPoint2D32f(CV_MAT_ELEM(*pixels, int, i+1, 0), + // CV_MAT_ELEM(*pixels, int, i+1, 1)), + // cvPoint2D32f(CV_MAT_ELEM(*pixels, int, i, 0), + // CV_MAT_ELEM(*pixels, int, i, 1))); + // //add to length + // length += cvSqrt(v.x * v.x + v.y * v.y); + // } + //get length between first and last control point + CvPoint2D32f v = mcvSubtractVector(spline.points[0], spline.points[spline.degree]); + length = cvSqrt(v.x * v.x + v.y * v.y); + //normalize + length /= image->height; //MAX(image->width, image->height); + + //add measure of spline straightness: angle between vectors from points 1&2 and + //points 2&3: clsoer to 1 the better (straight) + //add 1 to value to make it range from 0->2 (2 better) + float angle = 0; + for (int i=0; i1 (with 1 best) + angle += 1; + angle /= 2; + + //add ratio of spline length + //score = .8*score + .4*pixels->height; //.8 & .3 + + // printf("angle = %f\n", angle); + //score = 0.6*score + 0.4*(angle*score); //.6 .4 + + //make 0 best and -1 worse + angle -= 1; + length -= 1; + + // printf("angle=%.2f, length=%.2f, score=%.2f", angle, length, score); + //add angle and length ratios + // angle = score*angle; //(1-angleRatio)*score + angleRatio*angle; //angle*score + // length = lengthRatio*length*score;//(1-lengthRatio)*score + lengthRatio*length*score; + // score = angle + length; + // score = score + angleRatio*angle*score + lengthRatio*length*score; + + if (DEBUG_LINES) + fprintf(stderr, "raw score=%.2f, angle=%.2f, length=%.2f, final=%.2f\n", + score, angle, length, score * + (1 + (angleRatio*angle + lengthRatio*length)/2)); + score = score * (1 + (angleRatio*angle + lengthRatio*length)/2); + // printf(" final score=%.2f\n", score); + + //clear pixels + cvReleaseMat(&pixels); + jitter.clear(); + + //return + return score; +} + + + +/** This functions returns a vector of jitter from the input maxJitter value + * This is used for computing spline scores for example, to get scores + * around the rasterization of the spline + * + * \param maxJitter the max value to look around + * + * \return the required vector of jitter values + */ +vector mcvGetJitterVector(int maxJitter) +{ + vector jitter(2*maxJitter+1); + + //fill in + jitter.push_back(0); + for(int i=1; i<=maxJitter; ++i) + { + jitter.push_back(i); + jitter.push_back(-i); + } + + //return + return jitter; +} + + +/** This functions gets the average direction of the set of points + * by computing the mean vector between points + * + * \param points the input points [Nx2] matrix + * \param forward go forward or backward in computation (default true) + * \return the mean direction + * + */ +CvPoint2D32f mcvGetPointsMeanVector(const CvMat *points, bool forward) +{ + CvPoint2D32f mean, v; + + //init + mean = cvPoint2D32f(0,0); + + //go forward direction + for (int i=1; iheight; ++i) + { + //get the vector joining the two points + v = cvPoint2D32f(CV_MAT_ELEM(*points, float, i, 0) - + CV_MAT_ELEM(*points, float, i-1, 0), + CV_MAT_ELEM(*points, float, i, 1) - + CV_MAT_ELEM(*points, float, i-1, 1)); + //normalize + v = mcvNormalizeVector(v); + //get mean + mean.x = (mean.x * (i-1) + v.x) / i; + mean.y = (mean.y * (i-1) + v.y) / i; + //renormlaize + mean = mcvNormalizeVector(mean); + } + + //check if to return forward or backward + if (!forward) + mean = cvPoint2D32f(-mean.x, -mean.y); + + return mean; +} + + +/** This functions checks if to merge two splines or not + * + * \param sp1 the first spline + * \param sp2 the second spline + * \param thetaThreshold Angle threshold for merging splines (radians) + * \param rThreshold R threshold (distance from origin) for merginn splines + * \param MeanhetaThreshold Mean angle threshold for merging splines (radians) + * \param MeanRThreshold Mean r threshold (distance from origin) for merginn + * splines + * \param centroidThreshold Distance threshold between spline cetroids for + * merging + * + * \return true if to merge, false otherwise + * + */ +bool mcvCheckMergeSplines(const Spline& sp1, const Spline& sp2, + float thetaThreshold, float rThreshold, + float meanThetaThreshold, float meanRThreshold, + float centroidThreshold) +{ + //get spline stats + CvPoint2D32f centroid1, centroid2; + float theta1, theta2, length1, length2, r1, r2; + float meanTheta1, meanTheta2, meanR1, meanR2; + mcvGetSplineFeatures(sp1, ¢roid1, &theta1, &r1, + &length1, &meanTheta1, &meanR1); + mcvGetSplineFeatures(sp2, ¢roid2, &theta2, &r2, + &length2, &meanTheta2, &meanR2); + + //threshold for difference in orientation + //float thetaThreshold = 30*CV_PI/180.; + //threshold for difference in centroid (squared) + //float centroidThreshold = 50; + //threshold for meanR + //float rThreshold = 15; + float meanThetaDist = fabs(meanTheta1 - meanTheta2);//fabs(theta1-theta2); + float meanRDist = fabs(meanR1 - meanR2); + float thetaDist = fabs(theta1 - theta2);//fabs(theta1-theta2); + float rDist = fabs(r1 - r2); + float centroidDist = fabs(mcvGetVectorNorm(mcvSubtractVector(centroid1, centroid2))); + + //correct theta diff + // thetaDist = thetaDist>CV_PI ? thetaDist-CV_PI : thetaDist; + // meanThetaDist = meanThetaDist>CV_PI ? meanThetaDist-CV_PI : + // meanThetaDist; + + bool meanThetaOk = meanThetaDist <= meanThetaThreshold; + bool meanROk = meanRDist <= meanRThreshold; + bool thetaOk = thetaDist <= thetaThreshold; + bool rOk = rDist <= rThreshold; + bool centroidOk = centroidDist <= centroidThreshold; + + bool centroidNotOk = centroidDist >= 200; + bool rNotOk = rDist >= 100; + bool thetaNotOk = thetaDist >= .8; + + bool merge = false; + //((thetaOk || meanThetaOk) && centroidOk) || + if ((thetaOk || meanThetaOk) && (rOk || meanROk || centroidOk) && + !rNotOk && !centroidNotOk && !thetaNotOk) + merge = true; + + + //debug + if(DEBUG_LINES) {//#ifdef DEBUG_GET_STOP_LINES + + //show splines + // SHOW_SPLINE(sp1, "S1"); + // SHOW_SPLINE(sp2, "S2"); + fprintf(stderr, "%s: thetaDist=%.2f, meanThetaDist=%.2f, " + "rDist=%.2f, meanRDist=%.2f, centroidDist=%.2f\n", + merge? "Merged " : "Not merged", + thetaDist, meanThetaDist, rDist, meanRDist, centroidDist); + + fprintf(stderr, "\ttheta1=%.2f, theta2=%.2f\n", theta1, theta2); + + CvMat* im = cvCreateMat(480, 640, CV_8UC3); + cvSet(im, cvRealScalar(0.)); + //draw splines + mcvDrawSpline(im, sp1, CV_RGB(255, 0, 0), 1); + mcvDrawSpline(im, sp2, CV_RGB(0, 255, 0), 1); + SHOW_IMAGE(im, "Check Merge Splines", 10); + //clear + cvReleaseMat(&im); + + }//#endif + + //return + return merge; +} + +/** This functions computes some features for a set of points + * + * \param points the input points + * \param centroid the computed centroid of the points + * \param theta the major orientation of the points (angle of line joining + * first and last points, angle as in Hough Transform lines) + * \param r distance from origin for line from first to last point + * \param length the length of the line from first to last point + * \param meanTheta the average orientation of the points (by computing + * mean theta for line segments form the points) + * \param meanR the average distance from the origin of the points (the + * same computations as for meanTheta) + * \param curveness computes the angle between vectors of points, + * which gives an indication of the curveness of the spline + * -1-->1 with 1 best and -1 worst + * + */ +void mcvGetPointsFeatures(const CvMat* points, CvPoint2D32f* centroid, + float* theta, float* r, float* length, + float* meanTheta, float* meanR, float* curveness) +{ + + //get start and end point + CvPoint2D32f start = cvPoint2D32f(CV_MAT_ELEM(*points, float, 0, 0), + CV_MAT_ELEM(*points, float, 0, 1)); + CvPoint2D32f end = cvPoint2D32f(CV_MAT_ELEM(*points, float, + points->height-1, 0), + CV_MAT_ELEM(*points, float, + points->height-1, 1)); + //compute centroid + if (centroid) + { + //get sum of control points + *centroid = cvPoint2D32f(0, 0); + for (int i=0; i<=points->height; ++i) + *centroid = mcvAddVector(*centroid, + cvPoint2D32f(CV_MAT_ELEM(*points, float, i, 0), + CV_MAT_ELEM(*points, float, i, 1))); + //take mean + *centroid = cvPoint2D32f(centroid->x / (points->height), + centroid->y / (points->height)); + } + + //compute theta + if (theta && r) + { + //get line from first and last control points + Line line; + line.startPoint = start; + line.endPoint = end; + //get theta + //float r; + mcvLineXY2RTheta(line, *r, *theta); + //add pi if negative + if (*theta<0) + *theta += CV_PI; + } + + //mean theta + if (meanTheta && meanR) + { + *meanTheta = 0; + *meanR = 0; + + //loop and get theta + for (int i=0; iheight-1; i++) + { + //get the line + Line line; + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i, 0), + CV_MAT_ELEM(*points, float, i, 1)); + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i+1, 0), + CV_MAT_ELEM(*points, float, i+1, 1)); + //get theta and r + float r, t; + mcvLineXY2RTheta(line, r, t); + //add pi if neg + if (t<0) t += CV_PI; + //add + *meanTheta += t; + *meanR += r; + } + + //normalize + *meanTheta /= points->height - 1; + *meanR /= points->height - 1; + } + + //compute length of spline: length of vector between first and last point + if (length) + { + //get the vector + CvPoint2D32f v = mcvSubtractVector(start, end); + + //compute length + *length = cvSqrt(v.x * v.x + v.y * v.y); + } + + //compute curveness + if (curveness) + { + *curveness = 0; + if (points->height>2) + { + //initialize + CvPoint2D32f p0; + CvPoint2D32f p1 = start; + CvPoint2D32f p2 = cvPoint2D32f(CV_MAT_ELEM(*points, float, 1, 0), + CV_MAT_ELEM(*points, float, 1, 1)); + + for (int i=0; iheight-2; i++) + { + //go next + p0 = p1; + p1 = p2; + p2 = cvPoint2D32f(CV_MAT_ELEM(*points, float, i+2, 0), + CV_MAT_ELEM(*points, float, i+2, 1)); + //get first vector + CvPoint2D32f t1 = mcvNormalizeVector(mcvSubtractVector(p1, p0)); + + //get second vector + CvPoint2D32f t2 = mcvNormalizeVector (mcvSubtractVector(p2, p1)); + //get angle + *curveness += t1.x*t2.x + t1.y*t2.y; + } + //get mean + *curveness /= points->height-2; + } + } +} + + +/** This functions computes some features for the spline + * + * \param spline the input spline + * \param centroid the computed centroid of spline (mean of control points) + * \param theta the major orientation of the spline (angle of line joining + * first and last control points, angle as in Hough Transform lines) + * \param r distance from origin for line from first to last control point + * \param length the length of the line from first to last control point + * \param meanTheta the average orientation of the spline (by computing + * mean theta for line segments form the spline) + * \param meanR the average distance from the origin of the spline (the + * same computations as for meanTheta) + * \param curveness computes the angle between vectors of control points, + * which gives an indication of the curveness of the spline + * -1-->1 with 1 best and -1 worst + * + */ +void mcvGetSplineFeatures(const Spline& spline, CvPoint2D32f* centroid, + float* theta, float* r, float* length, + float* meanTheta, float* meanR, float* curveness) +{ + //compute centroid + if (centroid) + { + //get sum of control points + *centroid = cvPoint2D32f(0, 0); + for (int i=0; i<=spline.degree; ++i) + *centroid = mcvAddVector(*centroid, spline.points[i]); + //take mean + *centroid = cvPoint2D32f(centroid->x / (spline.degree+1), + centroid->y / (spline.degree+1)); + } + + //compute theta + if (theta && r) + { + //get line from first and last control points + Line line; + line.startPoint = spline.points[0]; + line.endPoint = spline.points[spline.degree]; + //get theta + //float r; + mcvLineXY2RTheta(line, *r, *theta); + //add pi if negative + //if (*theta<0) *theta += CV_PI; + + //compute theta as angle to the horizontal x-axis + *theta = mcvGetLineAngle(line); + } + + //mean theta + if (meanTheta && meanR) + { + *meanTheta = 0; + *meanR = 0; + //get points on the spline + CvMat* points = mcvEvalBezierSpline(spline, .1); + //loop and get theta + for (int i=0; iheight-1; i++) + { + //get the line + Line line; + line.startPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i, 0), + CV_MAT_ELEM(*points, float, i, 1)); + line.endPoint = cvPoint2D32f(CV_MAT_ELEM(*points, float, i+1, 0), + CV_MAT_ELEM(*points, float, i+1, 1)); + //get theta and r + float r, t; + mcvLineXY2RTheta(line, r, t); + //add pi if neg + #warning "add pi to theta calculations for spline feature" + //if (t<0) t += CV_PI; + //add + t = mcvGetLineAngle(line); + *meanTheta += t; + *meanR += r; + } + + //normalize + *meanTheta /= points->height - 1; + *meanR /= points->height - 1; + + //clear + cvReleaseMat(&points); + } + + //compute length of spline: length of vector between first and last point + if (length) + { + //get the vector + CvPoint2D32f v = cvPoint2D32f(spline.points[0].x - + spline.points[spline.degree].x, + spline.points[0].y - + spline.points[spline.degree].y); + //compute length + *length = cvSqrt(v.x * v.x + v.y * v.y); + } + + //compute curveness + if (curveness) + { + *curveness = 0; + for (int i=0; irows; ++i) + { + //clear histogram + cvSet(hist, cvRealScalar(0)); + + //get the window indices + int xmin = MAX(cvRound(CV_MAT_ELEM(*points, float, i, 0)-window), 0); + int xmax = MIN(cvRound(CV_MAT_ELEM(*points, float, i, 0)+window), + im->cols); + int ymin = MAX(cvRound(CV_MAT_ELEM(*points, float, i, 1)-window), 0); + int ymax = MIN(cvRound(CV_MAT_ELEM(*points, float, i, 1)+window), + im->rows); + + //get mean for every channel + float r=0.f, g=0.f, b=0.f, rr, gg, bb; + int bin; + for (int x=xmin; x<=xmax; x++) + for (int y=ymin; y<=ymax; y++) + { + //get colors + rr = (im->data.ptr + im->step*y)[x*3]; + gg = (im->data.ptr + im->step*y)[x*3+1]; + bb = (im->data.ptr + im->step*y)[x*3+2]; + //add to totals + r += rr; + g += gg; + b += bb; + + if (rbf) + { + //compute histogram + bin = MIN((int)(rr / binWidth), numBins); + hist->data.fl[bin] ++; + bin = MIN((int)(gg / binWidth), numBins); + hist->data.fl[bin + numBins] ++; + bin = MIN((int)(bb / binWidth), numBins); + hist->data.fl[bin + 2*numBins] ++; + } + } + + //normalize + int num = (xmax-xmin+1) * (ymax-ymin+1); + r /= num; + g /= num; + b /= num; + + //now compute differences + float rg = r - g; + float gb = g - b; + float rb = r - b; + + //add differences to histogram + if (rbf) + { + hist->data.fl[hist->width-2] = fabs(rg); + hist->data.fl[hist->width-1] = fabs(gb); + hist->data.fl[hist->width] = fabs(rb); + + //compute output of RBF model + // + //add rest of terms + for (int j=0; jdata.fl[k] - + rbfCentroids[j*histLen + k]; + d += t*t; + } + + //compute product with weight + f += rbfWeights[j+1] * exp(-.5 * d /rbfSigma); + } + } + + //classify + bool yellow; + if (rbf) + yellow = f > rbfThreshold; + else + yellow = rg>rgMin && rggbMin && rb>rbMin; + if (yellow) + numYellow++; + + if (DEBUG_LINES) + fprintf(stderr, "%s: f=%f, rg=%.2f, gb=%.2f, rb=%.2f\n", + yellow? "YES" : "NO ", f, rg, gb, rb); + // fprintf(stderr, "Point: %d is %s\n", i, yellow ? "YELLOW" : "WHITE"); + // fprintf(stderr, "r=%f, g=%f, b=%f\n", r, g, b); + // fprintf(stderr, "rg=%f, gb=%f, rb=%f\n\n", rg, gb, rb); + } + + //classify line + LineColor clr = LINE_COLOR_WHITE; + if (numYellow > numYellowMin*points->rows) + clr = LINE_COLOR_YELLOW; + + //release + cvReleaseMat(&hist); + + return clr; +} + + +/** \brief This function extracts bounding boxes from splines + * + * \param splines vector of splines + * \param type the type of lines (LINE_HORIZONTAL or LINE_VERTICAL) + * \param size the size of image containing the lines + * \param boxes a vector of output bounding boxes + */ +void mcvGetSplinesBoundingBoxes(const vector &splines, LineType type, + CvSize size, vector &boxes) +{ + //copy lines to boxes + int start, end; + //clear + boxes.clear(); + switch(type) + { + case LINE_VERTICAL: + for(unsigned int i=0; i &lines, LineType type, + CvSize size, vector &boxes) +{ + //copy lines to boxes + int start, end; + //clear + boxes.clear(); + switch(type) + { + case LINE_VERTICAL: + for(unsigned int i=0; i &lines, vector &scores, + float wMu, float wSigma) +{ + //check if we have only 1, then exit + if (lines.size() <2) + return; + + //get distance between the lines assuming they are vertical + int numInLines = lines.size(); + vector rs; + for (int i=0; i::iterator ir, jr; + int i, j, maxi, maxj; + double score = 0., maxScore = -5.; + wSigma *= wSigma; + for (i=0, ir=rs.begin(); ir!=rs.end(); ir++, i++) + for (j=i+1, jr=ir+1; jr!=rs.end(); jr++, j++) + { + //get that score + score = fabs(*ir - *jr) - wMu; + score = exp(-.5 * score * score / wSigma); + + //check max + if (score >= maxScore) + { + maxScore = score; + maxi = i; + maxj = j; + + fprintf(stderr, "diff=%.5f score=%.10f i=%d j=%d\n", + *ir - *jr, score, maxi, maxj); + } + } // for j + + //now return the max and check threshold + vector newLines; + vector newScores; + + if (maxScore<.4) //.25 + { + maxi = scores[maxi] > scores[maxj] ? maxi : maxj; + newLines.push_back(lines[maxi]); + newScores.push_back(scores[maxi]); + } + //add both + else + { + newLines.push_back(lines[maxi]); + newLines.push_back(lines[maxj]); + newScores.push_back(scores[maxi]); + newScores.push_back(scores[maxj]); + } + lines = newLines; + scores = newScores; + + //clear + newLines.clear(); + newScores.clear(); + rs.clear(); +} + + +void dummy() +{ + +} + +} // namespace LaneDetector + diff --git a/src/LaneDetector.hh b/src/LaneDetector.hh new file mode 100755 index 0000000..e8e5883 --- /dev/null +++ b/src/LaneDetector.hh @@ -0,0 +1,1393 @@ +/** + * \file LaneDetector.hh + * \author Mohamed Aly + * \date Thu 26 Jul, 2007 + * + */ + +#ifndef LANEDETECTOR_HH_ +#define LANEDETECTOR_HH_ + +#include "mcv.hh" +#include "InversePerspectiveMapping.hh" + +namespace LaneDetector +{ + +//Debug global variable +extern int DEBUG_LINES; + +///Line type +typedef enum LineType_ { + LINE_HORIZONTAL = 0, + LINE_VERTICAL = 1 +} LineType; + +/// Line color +typedef enum LineColor_ { + LINE_COLOR_NONE, + LINE_COLOR_YELLOW, + LINE_COLOR_WHITE +} LineColor; + +/// Line structure with start and end points +typedef struct Line +{ + ///start point + FLOAT_POINT2D startPoint; + ///end point + FLOAT_POINT2D endPoint; + ///color of line + LineColor color; + ///score of line + float score; +} Line; + +/// Spline structure +typedef struct Spline +{ + ///degree of spline + int degree; + ///points in spline + CvPoint2D32f points[4]; + ///color of spline + LineColor color; + ///score of spline + float score; +} Spline; + +///Structure to hold state used for initializing the next detection process +///from a previous one +typedef struct LineState_ +{ + ///Splines detected in IPM image + vector ipmSplines; + + ///bounding boxes to work on the splines from the previous frames + vector ipmBoxes; +} LineState; + +typedef enum CheckSplineStatus_ +{ + ShortSpline = 0x1, + //spline is curved i.e. form control points + CurvedSpline = 0x2, + //spline is curved i.e. overall (thetaDiff) + CurvedSplineTheta = 0x4, + HorizontalSpline = 0x8 +} CheckSplineStatus; + +#define GROUPING_TYPE_HV_LINES 0 +#define GROUPING_TYPE_HOUGH_LINES 1 + +///Structure to hold lane detector settings +typedef struct LaneDetectorConf +{ + ///width of IPM image to use + FLOAT ipmWidth; + ///height of IPM image + FLOAT ipmHeight; + ///Left point in original image of region to make IPM for + int ipmLeft; + ///Right point in original image of region to make IPM for + int ipmRight; + ///Top point in original image of region to make IPM for + int ipmTop; + ///Bottom point in original image of region to make IPM for + int ipmBottom; + ///The method to use for IPM interpolation + int ipmInterpolation; + + ///width of line we are detecting + FLOAT lineWidth; + ///height of line we are detecting + FLOAT lineHeight; + ///kernel size to use for filtering + unsigned char kernelWidth; + unsigned char kernelHeight; + ///lower quantile to use for thresholding the filtered image + FLOAT lowerQuantile; + ///whether to return local maxima or just the maximum + bool localMaxima; + ///the type of grouping to use: 0 for HV lines and 1 for Hough Transform + unsigned char groupingType; + ///whether to binarize the thresholded image or use the + ///raw filtered image + bool binarize; + //unsigned char topClip; + ///threshold for line scores to declare as line + FLOAT detectionThreshold; + ///whtehter to smooth the line scores detected or not + bool smoothScores; + ///rMin, rMax and rStep for Hough Transform (pixels) + float rMin, rMax, rStep; + ///thetaMin, thetaMax, thetaStep for Hough Transform (radians) + float thetaMin, thetaMax, thetaStep; + ///portion of image height to add to y-coordinate of vanishing + ///point when computing the IPM image + float ipmVpPortion; + ///get end points or not + bool getEndPoints; + ///group nearby lines + bool group; + ///threshold for grouping nearby lines + float groupThreshold; + ///use RANSAC or not + bool ransac; + ///RANSAC Line parameters + int ransacLineNumSamples; + int ransacLineNumIterations; + int ransacLineNumGoodFit; + float ransacLineThreshold; + float ransacLineScoreThreshold; + bool ransacLineBinarize; + ///half width to use for ransac window + int ransacLineWindow; + ///RANSAC Spline parameters + int ransacSplineNumSamples; + int ransacSplineNumIterations; + int ransacSplineNumGoodFit; + float ransacSplineThreshold; + float ransacSplineScoreThreshold; + bool ransacSplineBinarize; + int ransacSplineWindow; + ///degree of spline to use + int ransacSplineDegree; + ///use a spline or straight line + bool ransacSpline; + bool ransacLine; + + ///step used to pixelize spline in ransac + float ransacSplineStep; + + ///Overlap threshold to use for grouping of bounding boxes + float overlapThreshold; + + ///Angle threshold used for localization (cosine, 1: most restrictive, + /// 0: most liberal) + float localizeAngleThreshold; + ///Number of pixels to go in normal direction for localization + int localizeNumLinePixels; + + ///Angle threshold used for extending (cosine, 1: most restrictive, + /// 0: most liberal) + float extendAngleThreshold; + ///Angle threshold from mean direction used for extending (cosine, 1: + /// most restrictive, 0: most liberal) + float extendMeanDirAngleThreshold; + ///Number of pixels to go in tangent direction for extending + int extendLinePixelsTangent; + ///Number of pixels to go in normal direction for extending + int extendLinePixelsNormal; + ///Trehsold used for stopping the extending process (higher -> + /// less extending) + float extendContThreshold; + ///Stop extending when number of deviating points exceeds this threshold + int extendDeviationThreshold; + ///Top point for extension bounding box + int extendRectTop; + /// Bottom point for extension bounding box + int extendRectBottom; + + ///Angle threshold used for extending (cosine, 1: most restrictive, + /// 0: most liberal) + float extendIPMAngleThreshold; + ///Angle threshold from mean direction used for extending (cosine, + /// 1: most restrictive, 0: most liberal) + float extendIPMMeanDirAngleThreshold; + ///Number of pixels to go in tangent direction for extending + int extendIPMLinePixelsTangent; + ///Number of pixels to go in normal direction for extending + int extendIPMLinePixelsNormal; + ///Trehsold used for stopping the extending process (higher -> + /// less extending) + float extendIPMContThreshold; + ///Stop extending when number of deviating points exceeds this threshold + int extendIPMDeviationThreshold; + ///Top point for extension bounding box + int extendIPMRectTop; + /// Bottom point for extension bounding box + int extendIPMRectBottom; + + + ///Number of pixels to go around the spline to compute score + int splineScoreJitter; + ///Ratio of spline length to use + float splineScoreLengthRatio; + ///Ratio of spline angle to use + float splineScoreAngleRatio; + ///Step to use for spline score computation + float splineScoreStep; + + ///number of frames the track is allowed to be absent before deleting it + int splineTrackingNumAbsentFrames; + ///number of frames before considering the track good + int splineTrackingNumSeenFrames; + + ///Angle threshold for merging splines (radians) + float mergeSplineThetaThreshold; + ///R threshold (distance from origin) for merginn splines + float mergeSplineRThreshold; + ///Mean Angle threshold for merging splines (radians) + float mergeSplineMeanThetaThreshold; + ///Mean R threshold (distance from origin) for merginn splines + float mergeSplineMeanRThreshold; + ///Distance threshold between spline cetroids for merging + float mergeSplineCentroidThreshold; + + ///number of frames the track is allowed to be absent before deleting it + int lineTrackingNumAbsentFrames; + ///number of frames before considering the track good + int lineTrackingNumSeenFrames; + + ///Angle threshold for merging lines (radians) + float mergeLineThetaThreshold; + ///R threshold (distance from origin) for merging lines + float mergeLineRThreshold; + + ///Number of horizontal strips to divide the image to + int numStrips; + + ///Whtethet to check splines or not + bool checkSplines; + ///Curveness Threshold for checking splines + float checkSplinesCurvenessThreshold; + ///Length Threshold for checking splines + float checkSplinesLengthThreshold; + ///ThetaDiff Threshold for checking splines + float checkSplinesThetaDiffThreshold; + ///ThetaThreshold Threshold for checking splines + float checkSplinesThetaThreshold; + + ///Whtethet to check IPM splines or not + bool checkIPMSplines; + ///Curveness Threshold for checking splines + float checkIPMSplinesCurvenessThreshold; + ///Length Threshold for checking splines + float checkIPMSplinesLengthThreshold; + ///ThetaDiff Threshold for checking splines + float checkIPMSplinesThetaDiffThreshold; + ///ThetaThreshold Threshold for checking splines + float checkIPMSplinesThetaThreshold; + + ///Final Threshold for declaring a valid spline + float finalSplineScoreThreshold; + + ///Use ground plane when sending to map or not + bool useGroundPlane; + + ///Whether to check colors or not + bool checkColor; + ///Size of window to use + int checkColorWindow; + ///Number of bins to use for histogram + int checkColorNumBins; + ///Min ratio of yellow points + float checkColorNumYellowMin; + ///Min RG diff + float checkColorRGMin; + ///Max RG diff + float checkColorRGMax; + ///Min GB diff + float checkColorGBMin; + ///Min RB diff + float checkColorRBMin; + ///RBF Threshold + float checkColorRBFThreshold; + ///Whether to use RBF or not + bool checkColorRBF; + + ///Whether to clear part of the IPM image + bool ipmWindowClear; + ///Left corrdinate of window to keep in IPM + int ipmWindowLeft; + ///Left corrdinate of window to keep in IPM + int ipmWindowRight; + + ///Whether to check lane width or not + bool checkLaneWidth; + ///Mean of lane width to look for + float checkLaneWidthMean; + ///Std deviation of lane width to look for + float checkLaneWidthStd; +} LaneDetectorConf; + +//function definitions +/** + * This function gets a 1-D gaussian filter with specified + * std deviation and range + * + * \param kernel input mat to hold the kernel (2*w+1x1) + * column vector (already allocated) + * \param w width of kernel is 2*w+1 + * \param sigma std deviation + */ +void mcvGetGaussianKernel(CvMat *kernel, unsigned char w, FLOAT sigma); + + +/** + * This function gets a 1-D second derivative gaussian filter + * with specified std deviation and range + * + * \param kernel input mat to hold the kernel (2*w+1x1) + * column vector (already allocated) + * \param w width of kernel is 2*w+1 + * \param sigma std deviation + */ +void mcvGet2DerivativeGaussianKernel(CvMat *kernel, unsigned char w, + FLOAT sigma); + + +/** + * This function filters the input image looking for horizontal + * or vertical lines with specific width or height. + * + * \param inImage the input image + * \param outImage the output image in IPM + * \param wx width of kernel window in x direction = 2*wx+1 + * (default 2) + * \param wy width of kernel window in y direction = 2*wy+1 + * (default 2) + * \param sigmax std deviation of kernel in x (default 1) + * \param sigmay std deviation of kernel in y (default 1) + * \param lineType type of the line + * LINE_HORIZONTAL (default) + * LINE_VERTICAL + */ + +#define FILTER_LINE_HORIZONTAL 0 +#define FILTER_LINE_VERTICAL 1 +void mcvFilterLines(const CvMat *inImage, CvMat *outImage, unsigned char wx=2, + unsigned char wy=2, FLOAT sigmax=1, FLOAT sigmay=1, + LineType lineType=LINE_HORIZONTAL); + + +/** This function groups the input filtered image into + * horizontal or vertical lines. + * + * \param inImage input image + * \param lines returned detected lines (vector of points) + * \param lineScores scores of the detected lines (vector of floats) + * \param lineType type of lines to detect + * LINE_HORIZONTAL (default) or LINE_VERTICAL + * \param linePixelWidth width (or height) of lines to detect + * \param localMaxima whether to detect local maxima or just get + * the maximum + * \param detectionThreshold threshold for detection + */ +#define HV_LINES_HORIZONTAL 0 +#define HV_LINES_VERTICAL 1 +void mcvGetHVLines(const CvMat *inImage, vector *lines, + vector *lineScores, LineType lineType=LINE_HORIZONTAL, + FLOAT linePixelWidth=1., bool binarize=false, + bool localMaxima=false, FLOAT detectionThreshold=1., + bool smoothScores=true); + +/** This function binarizes the input image i.e. nonzero elements + * become 1 and others are 0. + * + * \param inImage input & output image + */ +void mcvBinarizeImage(CvMat *inImage); + +/** This function gets the maximum value in a vector (row or column) + * and its location + * + * \param inVector the input vector + * \param max the output max value + * \param maxLoc the location (index) of the first max + * \param ignore don't the first and last ignore elements + * + */ +void mcvGetVectorMax(const CvMat *inVector, double *max, int *maxLoc, + int ignore=0); + +/** This function gets the qtile-th quantile of the input matrix + * + * \param mat input matrix + * \param qtile required input quantile probability + * \return the returned value + * + */ +FLOAT mcvGetQuantile(const CvMat *mat, FLOAT qtile); + +/** This function thresholds the image below a certain value to the threshold + * so: outMat(i,j) = inMat(i,j) if inMat(i,j)>=threshold + * = threshold otherwise + * + * \param inMat input matrix + * \param outMat output matrix + * \param threshold threshold value + * + */ +void mcvThresholdLower(const CvMat *inMat, CvMat *outMat, FLOAT threshold); + +/** This function detects stop lines in the input image using IPM + * transformation and the input camera parameters. The returned lines + * are in a vector of Line objects, having start and end point in + * input image frame. + * + * \param image the input image + * \param stopLines a vector of returned stop lines in input image coordinates + * \param linescores a vector of line scores returned + * \param cameraInfo the camera parameters + * \param stopLineConf parameters for stop line detection + * + * + */ +void mcvGetStopLines(const CvMat *inImage, vector *stopLines, + vector *lineScores, const CameraInfo *cameraInfo, + LaneDetectorConf *stopLineConf); + +/** This function converts an array of lines to a matrix (already allocated) + * + * \param lines input vector of lines + * \param size number of lines to convert + * \return the converted matrix, it has 2x2*size where size is the + * number of lines, first row is x values (start.x, end.x) and second + * row is y-values + * + * + */ +void mcvLines2Mat(const vector *lines, CvMat *mat); + +/** This function converts matrix into n array of lines + * + * \param mat input matrix , it has 2x2*size where size is the + * number of lines, first row is x values (start.x, end.x) and second + * row is y-values + * \param lines the rerurned vector of lines + * + * + */ +void mcvMat2Lines(const CvMat *mat, vector *lines); + +/** This function intersects the input line with the given bounding box + * + * \param inLine the input line + * \param bbox the bounding box + * \param outLine the output line + * + */ +void mcvIntersectLineWithBB(const Line *inLine, const CvSize bbox, + Line *outLine); + +/** This function checks if the given point is inside the bounding box + * specified + * + * \param inLine the input line + * \param bbox the bounding box + * \param outLine the output line + * + */ +bool mcvIsPointInside(FLOAT_POINT2D point, CvSize bbox); + +/** This function converts an INT mat into a FLOAT mat (already allocated) + * + * \param inMat input INT matrix + * \param outMat output FLOAT matrix + * + */ +void mcvMatInt2Float(const CvMat *inMat, CvMat *outMat); + +/** This function draws a line onto the passed image + * + * \param image the input iamge + * \param line input line + * \param line color + * \param width line width + * + */ +void mcvDrawLine(CvMat *image, Line line, CvScalar color=CV_RGB(0,0,0), + int width=1); + +/** This function draws a rectangle onto the passed image + * + * \param image the input image + * \param rect the input rectangle + * \param color the rectangle color + * \param width the rectangle width + * + */ +void mcvDrawRectangle (CvMat *image, CvRect rect, + CvScalar color=CV_RGB(255,0,0), int width=1); + +/** This initializes the LaneDetectorinfo structure + * + * \param fileName the input file name + * \param stopLineConf the structure to fill + * + * + */ +void mcvInitLaneDetectorConf(char * const fileName, + LaneDetectorConf *laneDetectorConf); + +void SHOW_LINE(const Line line, char str[]="Line:"); +void SHOW_SPLINE(const Spline spline, char str[]="Spline"); + +/** This fits a parabola to the entered data to get + * the location of local maximum with sub-pixel accuracy + * + * \param val1 first value + * \param val2 second value + * \param val3 third value + * + * \return the computed location of the local maximum + */ +double mcvGetLocalMaxSubPixel(double val1, double val2, double val3); + + +/** This function detects lines in images using Hough transform + * + * \param inImage input image + * \param lines vector of lines to hold the results + * \param lineScores scores of the detected lines (vector of floats) + * \param rMin minimum r use for finding the lines (default 0) + * \param rMax maximum r to find (default max(size(im))) + * \param rStep step to use for binning (default is 2) + * \param thetaMin minimum angle theta to look for (default 0) all in radians + * \param thetaMax maximum angle theta to look for (default 2*pi) + * \param thetaStep step to use for binning theta (default 5) + * \param binarize if to binarize the input image or use the raw values so that + * non-zero values are not treated as equal + * \param localMaxima whether to detect local maxima or just get + * the maximum + * \param detectionThreshold threshold for detection + * \param smoothScores whether to smooth scores detected or not + * \param group whether to group nearby detections (1) or not (0 default) + * \param groupThreshold the minimum distance used for grouping (default 10) + */ + +void mcvGetHoughTransformLines(const CvMat *inImage, vector *lines, + vector *lineScores, + FLOAT rMin, FLOAT rMax, FLOAT rStep, + FLOAT thetaMin, FLOAT thetaMax, + FLOAT thetaStep, bool binarize, bool localMaxima, + FLOAT detectionThreshold, bool smoothScores, + bool group, FLOAT groupTthreshold); + +/** This function intersects the input line (given in r and theta) with + * the given bounding box where the line is represented by: + * x cos(theta) + y sin(theta) = r + * + * \param r the r value for the input line + * \param theta the theta value for the input line + * \param bbox the bounding box + * \param outLine the output line + * + */ +void mcvIntersectLineRThetaWithBB(FLOAT r, FLOAT theta, const CvSize bbox, + Line *outLine); + +/** This function gets the local maxima in a matrix and their positions + * and its location + * + * \param inMat input matrix + * \param localMaxima the output vector of local maxima + * \param localMaximaLoc the vector of locations of the local maxima, + * where each location is cvPoint(x=col, y=row) zero-based + * \param threshold threshold to return local maxima above + * + */ +void mcvGetMatLocalMax(const CvMat *inMat, vector &localMaxima, + vector &localMaximaLoc, double threshold=0.0); + +/** This function gets the locations and values of all points + * above a certain threshold + * + * \param inMat input matrix + * \param maxima the output vector of maxima + * \param maximaLoc the vector of locations of the maxima, + * where each location is cvPoint(x=col, y=row) zero-based + * \param threshold the threshold to get all points above + * + */ +void mcvGetMatMax(const CvMat *inMat, vector &maxima, + vector &maximaLoc, double threshold); + +/** This function gets the local maxima in a vector and their positions + * + * \param inVec input vector + * \param localMaxima the output vector of local maxima + * \param localMaximaLoc the vector of locations of the local maxima, + * + */ +void mcvGetVectorLocalMax(const CvMat *inVec, vector &localMaxima, + vector &localMaximaLoc); + +/** This functions implements Bresenham's algorithm for getting pixels of the + * line given its two endpoints + + * + * \param line the input line + * + */ +//void mcvGetLinePixels(const Line &line, vector &x, vector &y) +CvMat * mcvGetLinePixels(const Line &line); + +/** This functions implements Bresenham's algorithm for getting pixels of the + * line given its two endpoints + + * + * \param line the input line + * \param x a vector of x locations for the line pixels (0-based) + * \param y a vector of y locations for the line pixels (0-based) + * + */ +//void mcvGetLinePixels(const Line &line, vector &x, vector &y); + +/** This functions implements Bresenham's algorithm for getting pixels of the + * line given its two endpoints + * + * + * \param im the input image + * \param inLine the input line + * \param outLine the output line + * + */ +void mcvGetLineExtent(const CvMat *im, const Line &inLine, Line &outLine); + +/** This functions converts a line defined by its two end-points into its + * r and theta (origin is at top-left corner with x right and y down and theta + * measured positive clockwise -pistartPoint and bottomright->endPoint where x->right and y->down) + * \param outLine the output line + * + */ +void mcvIntersectLineRThetaWithRect(FLOAT r, FLOAT theta, const Line &rect, + Line &outLine); + + +/** This function detects lanes in the input image using IPM + * transformation and the input camera parameters. The returned lines + * are in a vector of Line objects, having start and end point in + * input image frame. + * + * \param image the input image + * \param lanes a vector of returned stop lines in input image coordinates + * \param linescores a vector of line scores returned + * \param cameraInfo the camera parameters + * \param stopLineConf parameters for stop line detection + * \param state returns the current state and inputs the previous state to + * initialize the current detection (NULL to ignore) + * + * + */ +void mcvGetLanes(const CvMat *inImage, const CvMat* clrImage, + vector *lanes, vector *lineScores, + vector *splines, vector *splineScores, + CameraInfo *cameraInfo, LaneDetectorConf *stopLineConf, + LineState* state = NULL); + + +/** This function extracts lines from the passed infiltered and thresholded + * image + * + * \param image the input thresholded filtered image + * \param lineType the line type to look for (LINE_VERTICAL or LINE_HORIZONTAL) + * \param lines a vector of lines + * \param lineScores the line scores + * \param splines a vector of returned splines + * \param splineScores the spline scores + * \param lineConf the conf structure + * \param state the state for RANSAC splines + * + */ +void mcvGetLines(const CvMat* image, LineType lineType, vector &lines, + vector &lineScores, vector &splines, + vector &splineScores, LaneDetectorConf *lineConf, + LineState *state); + +/** This function postprocesses the detected lines/splines to better localize + * and extend them + * + * \param image the input image + * \param clrImage the inpout color image + * \param rawipm the raw ipm image + * \param fipm the filtered ipm iamge + * \param lines a vector of lines + * \param lineScores the line scores + * \param splines a vector of returned splines + * \param splineScores the spline scores + * \param lineConf the conf structure + * \param state the state for RANSAC splines + * \param ipmInfo the ipmInfo structure + * \param cameraInfo the camera info structure + * + */ +void mcvPostprocessLines(const CvMat* image, const CvMat* clrImage, + const CvMat* rawipm, const CvMat* fipm, + vector &lines, vector &lineScores, + vector &splines, vector &splineScores, + LaneDetectorConf *lineConf, LineState *state, + IPMInfo &ipmInfo, CameraInfo &cameraInfo); + +/** This function gets the indices of the non-zero values in a matrix + + * \param inMat the input matrix + * \param outMat the output matrix, with 2xN containing the x and y in + * each column + * \param floatMat whether to return floating points or integers for + * the outMat + */ +CvMat* mcvGetNonZeroPoints(const CvMat *inMat, bool floatMat); + +/** This functions implements RANSAC algorithm for line fitting + given an image + + * + * \param image input image + * \param numSamples number of samples to take every iteration + * \param numIterations number of iterations to run + * \param threshold threshold to use to assess a point as a good fit to a line + * \param numGoodFit number of points close enough to say there's a good fit + * \param getEndPoints whether to get the end points of the line from the data, + * just intersect with the image boundaries + * \param lineType the type of line to look for (affects getEndPoints) + * \param lineXY the fitted line + * \param lineRTheta the fitted line [r; theta] + * \param lineScore the score of the line detected + * + */ +void mcvFitRansacLine(const CvMat *image, int numSamples, int numIterations, + float threshold, float scoreThreshold, int numGoodFit, + bool getEndPoints, LineType lineType, + Line *lineXY, float *lineRTheta, float *lineScore); + +/** This functions fits a line using the orthogonal distance to the line + by minimizing the sum of squares of this distance. + + * + * \param points the input points to fit the line to which is + * 2xN matrix with x values on first row and y values on second + * \param lineRTheta the return line [r, theta] where the line is + * x*cos(theta)+y*sin(theta)=r + * \param lineAbc the return line in [a, b, c] where the line is + * a*x+b*y+c=0 + * + */ +void mcvFitRobustLine(const CvMat *points, float *lineRTheta, float *lineAbc); + +/** This function groups nearby lines + * + * \param inLines vector of lines + * \param outLines vector of grouped lines + * \param groupThreshold the threshold used for grouping + * \param bbox the bounding box to intersect with + */ +void mcvGroupLines(vector &lines, vector &lineScores, + float groupThreshold, CvSize bbox); + + +/** This function performs a RANSAC validation step on the detected lines + * + * \param im the input image + * \param lines vector of input lines + * \param lineScores the scores of input lines + * \param lineConf the parameters controlling its operation + * \param lineType the type of line to work on (LINE_HORIZONTAL or + * LINE_VERTICAL) + */ +void mcvGetRansacLines(const CvMat *im, vector &lines, + vector &lineScores, LaneDetectorConf *lineConf, + LineType lineType); + + +/** This function sets the matrix to a value except for the mask window passed + * in + * + * \param inMat input matrix + * \param mask the rectangle defining the mask: (xleft, ytop, width, height) + * \param val the value to put + */ +void mcvSetMat(CvMat *inMat, CvRect mask, double val); + +/** This function converts splines from IPM image coordinates back to image + * coordinates + * + * \param splines the input splines + * \param ipmInfo the IPM info + * \param cameraInfo the camera info + * \param imSize the output image size (for clipping) + * + */ +void mcvSplinesImIPM2Im(vector &splines, IPMInfo &ipmInfo, + CameraInfo &cameraInfo, CvSize imSize); + +/** This function converts lines from IPM image coordinates back to image + * coordinates + * + * \param lines the input lines + * \param ipmInfo the IPM info + * \param cameraInfo the camera info + * \param imSize the output image size (for clipping) + * + */ +void mcvLinesImIPM2Im(vector &lines, IPMInfo &ipmInfo, + CameraInfo &cameraInfo, CvSize imSize); + +/** This function draws a spline onto the passed image + * + * \param image the input iamge + * \param spline input spline + * \param spline color + * + */ +void mcvDrawSpline(CvMat *image, Spline spline, CvScalar color, int width); + + +/** This functions implements RANSAC algorithm for spline fitting + given an image + + * + * \param image input image + * \param numSamples number of samples to take every iteration + * \param numIterations number of iterations to run + * \param threshold threshold to use to assess a point as a good fit to a line + * \param numGoodFit number of points close enough to say there's a good fit + * \param splineDegree the spline degree to fit + * \param h the resolution to use for splines + * \param spline the fitted line + * \param splineScore the score of the line detected + * \param splineScoreJitter Number of pixels to go around the spline to compute + * score + * \param splineScoreLengthRatio Ratio of spline length to use + * \param splineScoreAngleRatio Ratio of spline angle to use + * \param splineScoreStep Step to use for spline score computation + * \param prevSplines the splines from the previous frame, to use as initial + * seeds + * pass NULL to ignore this input + * + */ +void mcvFitRansacSpline(const CvMat *image, int numSamples, int numIterations, + float threshold, float scoreThreshold, int numGoodFit, + int splineDegree, float h, Spline *spline, + float *splineScore, int splineScoreJitter, + float splineScoreLengthRatio, + float splineScoreAngleRatio, float splineScoreStep, + vector *prevSplines = NULL); + +/** This function performs a RANSAC validation step on the detected lines to + * get splines + * + * \param image the input image + * \param lines vector of input lines to refine + * \param lineSCores the line scores input + * \param groupThreshold the threshold used for grouping + * \param bbox the bounding box to intersect with + * \param lineType the line type to work on (horizontal or vertical) + * \param prevSplines the previous splines to use in initializing the detection + */ +void mcvGetRansacSplines(const CvMat *im, vector &lines, + vector &lineScores, LaneDetectorConf *lineConf, + LineType lineType, vector &splines, + vector &splineScores, LineState* state); + +/** This function returns pixel coordinates for the Bezier + * spline with the given resolution. + * + * \param spline input spline + * \param h the input resolution + * \param box the bounding box + * \param extendSpline whether to extend spline with straight lines or not + * (default false) + * \return computed points in an array Nx2 [x,y], returns NULL if empty output + */ +CvMat* mcvGetBezierSplinePixels(Spline &spline, float h, CvSize box, + bool extendSpline=false); + + +/** This function evaluates Bezier spline with given resolution + * + * \param spline input spline + * \param h the input resolution + * \param tangents the tangents at the two end-points of the spline [t0; t1] + * \return computed points in an array Nx2 [x,y] + */ +CvMat* mcvEvalBezierSpline(const Spline &spline, float h, CvMat *tangents=NULL); + +/** This function fits a Bezier spline to the passed input points + * + * \param points the input points + * \param degree the required spline degree + * \return spline the returned spline + */ +Spline mcvFitBezierSpline(CvMat *points, int degree); + +/** This function fits a Bezier spline to the passed input points + * + * \param inPOints Nx2 matrix of points [x,y] + * \param outPOints Nx2 matrix of points [x,y] + * \param dim the dimension to sort on (0: x, 1:y) + * \param dir direction of sorting (0: ascending, 1:descending) + */ +void mcvSortPoints(const CvMat *inPoints, CvMat *outPoints, + int dim, int dir); + +/** This function samples uniformly with weights + * + * \param cumSum cumulative sum for normalized weights for the differnet + * samples (last is 1) + * \param numSamples the number of samples + * \param randInd a 1XnumSamples of int containing the indices + * \param rng a pointer to a random number generator + * + */ +void mcvSampleWeighted(const CvMat *cumSum, int numSamples, + CvMat *randInd, CvRNG *rng); + + +/** This function computes the cumulative sum for a vector + * + * \param inMat input matrix + * \param outMat output matrix + * + */ +void mcvCumSum(const CvMat *inMat, CvMat *outMat); + +/** This functions gives better localization of points along lines + * + * \param im the input image + * \param inPoints the input points Nx2 matrix of points + * \param outPoints the output points Nx2 matrix of points + * \param numLinePixels Number of pixels to go in normal direction for + * localization + * \param angleThreshold Angle threshold used for localization + * (cosine, 1: most restrictive, 0: most liberal) + * + */ +void mcvLocalizePoints(const CvMat *im, const CvMat *inPoints, + CvMat *outPoints, int numLinePixels, + float angleThreshold); + +/** This functions gets the point on the input line that matches the + * peak in the input image, where the peak is the middle of a bright + * line on dark background in the image + * + * \param im the input image + * \param line input line + * \param peaks a vector of peak outputs + * \param peakVals the values for each of these peaks + * \param positivePeak whether we are looking for positive or + * negative peak(default true) + * \param smoothPeaks whether to smooth pixels for calculating peaks + * or not + * + */ +float mcvGetLinePeak(const CvMat *im, const Line &line, + vector &peaks, vector &peakVals, + bool positivePeak=true, bool smoothPeaks=true); + +/** This functions chooses the best peak that minimizes deviation + * from the tangent direction given + * + * \param peaks the peaks found + * \param peakVals the values for the peaks + * \param peak the returned peak + * \param peakVal the peak value for chosen peak, -1 if nothing + * \param contThreshold the threshold to get peak above + * \param tangent the tangent line along which the peak was found normal + * to (normalized) + * \param prevPoint the previous point along the tangent + * \param angleThreshold the angle threshold to consider for valid peaks + * \return index of peak chosen, -1 if nothing + * + */ +int mcvChooseBestPeak(const vector &peaks, + const vector &peakVals, CvPoint2D32f &peak, + float &peakVal, float contThreshold, + const CvPoint2D32f &tangent, + const CvPoint2D32f &prevPoint, float angleThreshold); + +/** This functions extends the given set of points in both directions to + * extend curves and lines in the image + * + * \param im the input image + * \param inPoints the input points Nx2 matrix of points + * \param angleThreshold angle threshold used for extending + * \param meanDirAngleThreshold angle threshold from mean direction + * \param linePixelsTangent number of pixels to go in tangent direction + * \param linePixelsNormal number of pixels to go in normal direction + * \param contThreshold number of pixels to go in tangent direction + * \param deviationThreshold Stop extending when number of deviating points + * exceeds this threshold + * \param bbox a bounding box not to get points outside + * \param smoothPeak whether to smooth for calculating peaks or not + * + */ +CvMat* mcvExtendPoints(const CvMat *im, const CvMat *inPoints, + float angleThreshold, float meanDirAngleThreshold, + int linePixelsTangent, int linePixelsNormal, + float contThreshold, int deviationThreshold, + CvRect bbox, bool smoothPeaks=true); + +/** This functions extends a point along the tangent and gets the normal line + * at the new point + * + * \param curPoint the current point to extend + * \param tangent the tangent at this point (not necessarily normalized) + * \param linePixelsTangent the number of pixels to go in tangent direction + * \param linePixelsNormal the number of pixels to go in normal direction + * \return the normal line at new point + */ +Line mcvGetExtendedNormalLine(CvPoint2D32f &curPoint, CvPoint2D32f &tangent, + int linePixelsTangent, int linePixelsNormal, + CvPoint2D32f &nextPoint); + +/** This functions checks the peak point if much change in orientation + * + * \param peak the input peak point + * \param tangent the tangent line along which the peak was found normal to + * (normalized) + * \param prevPoint the previous point along the tangent + * \param angleThreshold the angle threshold to consider for valid peaks + * \return true if useful peak, zero otherwise + * + */ +bool mcvIsValidPeak(const CvPoint2D32f &peak, const CvPoint2D32f &tangent, + const CvPoint2D32f &prevPoint, float angleThreshold); + + + +/** \brief This function groups together bounding boxes + * + * \param size the size of image containing the lines + * \param boxes a vector of output grouped bounding boxes * \param type the + * type of lines (LINE_HORIZONTAL or LINE_VERTICAL) + * \param groupThreshold the threshold used for grouping (ratio of overlap) + */ +void mcvGroupBoundingBoxes(vector &boxes, LineType type, + float groupThreshold); + + +/** This functions normalizes the given vector + * + * \param vector the input vector to normalize + */ +CvPoint2D32f mcvNormalizeVector(const CvPoint2D32f &v); + +/** This functions normalizes the given vector + * + * \param vector the input vector to normalize + */ +CvPoint2D32f mcvNormalizeVector(const CvPoint &v); + + +/** This functions normalizes the given vector + * + * \param x the x component + * \param y the y component + */ +CvPoint2D32f mcvNormalizeVector(float x, float y); + + +/** This functions computes the score of the given spline from the + * input image + * + * \param image the input image + * \param spline the input spline + * \param h spline resolution + * \param jitterVal the amounts to count scores around the spline in x & y + * directions + * \param lengthRatio the ratio to add to score from the spline length + * \param angleRatio the ratio to add to score from spline curvature measure + * + * \return the score + */ +float mcvGetSplineScore(const CvMat* image, Spline& spline, float h, + int jitterVal, float lengthRatio, + float angleRatio); + +/** This functions returns a vector of jitter from the input maxJitter value + * This is used for computing spline scores for example, to get scores + * around the rasterization of the spline + * + * \param maxJitter the max value to look around + * + * \return the required vector of jitter values + */ +vector mcvGetJitterVector(int maxJitter); + +/** This functions adds two vectors and returns the result + * + * \param v1 the first vector + * \param v2 the second vector + * \return the sum + */ +CvPoint2D32f mcvAddVector(CvPoint2D32f v1, CvPoint2D32f v2); + +/** This functions gets the average direction of the set of points + * by computing the mean vector between points + * + * \param points the input points [Nx2] matrix + * \param forward go forward or backward in computation (default true) + * \return the mean direction + * + */ +CvPoint2D32f mcvGetPointsMeanVector(const CvMat *points, bool forward = true); + + +/** This functions checks if to merge two splines or not + * + * \param sp1 the first spline + * \param sp2 the second spline + * \param thetaThreshold Angle threshold for merging splines (radians) + * \param rThreshold R threshold (distance from origin) for merginn splines + * \param MeanhetaThreshold Mean angle threshold for merging splines (radians) + * \param MeanRThreshold Mean r threshold (distance from origin) for merginn + * splines + * \param centroidThreshold Distance threshold between spline cetroids for + * merging + * + * \return true if to merge, false otherwise + * + */ +bool mcvCheckMergeSplines(const Spline& sp1, const Spline& sp2, + float thetaThreshold, float rThreshold, + float MeanThetaThreshold, float MeanRThreshold, + float centroidThreshold); + + +/** This functions computes some features for the spline + * + * \param spline the input spline + * \param centroid the computed centroid of spline (mean of control points) + * \param theta the major orientation of the spline (angle of line joining + * first and last control points, angle as in Hough Transform lines) + * \param r distance from origin for line from first to last control point + * \param length the length of the line from first to last control point + * \param meanTheta the average orientation of the spline (by computing + * mean theta for line segments form the spline) + * \param meanR the average distance from the origin of the spline (the + * same computations as for meanTheta) + * \param curveness computes the angle between vectors of control points, + * which gives an indication of the curveness of the spline + * -1-->1 with 1 best and -1 worst + * + */ +void mcvGetSplineFeatures(const Spline& spline, CvPoint2D32f* centroid=0, + float* theta=0, float* r=0, float* length=0, + float* meanTheta=0, float* meanR=0, + float* curveness=0); + +/** This functions computes some features for a set of points + * + * \param points the input points + * \param centroid the computed centroid of the points + * \param theta the major orientation of the points (angle of line joining + * first and last points, angle as in Hough Transform lines) + * \param r distance from origin for line from first to last point + * \param length the length of the line from first to last point + * \param meanTheta the average orientation of the points (by computing + * mean theta for line segments form the points) + * \param meanR the average distance from the origin of the points (the + * same computations as for meanTheta) + * \param curveness computes the angle between vectors of points, + * which gives an indication of the curveness of the spline + * -1-->1 with 1 best and -1 worst + * + */ +void mcvGetPointsFeatures(const CvMat* points, + CvPoint2D32f* centroid=0, + float* theta=0, float* r=0, + float* length=0, float* meanTheta=0, + float* meanR=0, float* curveness=0); + + +/** This functions computes difference between two vectors + * + * \param v1 first vector + * \param v2 second vector + * \return difference vector v1 - v2 + * + */ +CvPoint2D32f mcvSubtractVector(const CvPoint2D32f& v1, const CvPoint2D32f& v2); + +/** This functions computes the vector norm + * + * \param v input vectpr + * \return norm of the vector + * + */ +float mcvGetVectorNorm(const CvPoint2D32f& v); + + +/** This functions checks if to merge two splines or not + * + * \param line1 the first line + * \param line2 the second line + * \param thetaThreshold Angle threshold for merging splines (radians) + * \param rThreshold R threshold (distance from origin) for merginn splines + * + * \return true if to merge, false otherwise + * + */ +bool mcvCheckMergeLines(const Line& line1, const Line& line2, + float thetaThreshold, float rThreshold); + + +/** This functions converts a line to a spline + * + * \param line the line + * \param degree the spline degree + * + * \return the returned spline + * + */ +Spline mcvLineXY2Spline(const Line& line, int degree); + +/** This function checks if the given point is inside the rectangle specified + * + * \param inLine the input line + * \param rect the specified rectangle + * + */ +bool mcvIsPointInside(FLOAT_POINT2D &point, const CvRect &rect); + + +/** This function draws a spline onto the passed image + * + * \param image the input iamge + * \param str the string to put + * \param point the point where to put the text + * \param size the font size + * \param color the font color + * + */ +void mcvDrawText(CvMat *image, char* str, CvPoint point, + float size=.25f, CvScalar color=CV_RGB(1,1,1)); + +/** This function makes some checks on splines and decides + * whether to keep them or not + * + * \param spline the input spline + * \param curvenessThreshold minimum curveness score it should have + * \param lengthThreshold mimimum threshold it should have + * \param thetaDiffThreshold max theta diff it should have + * \param thetaThreshold max theta it should have to be considered horizontal + * + * \return code that determines what to do with the spline + * + */ +int mcvCheckSpline(const Spline &spline, float curvenessThreshold, + float lengthThreshold, float thetaDiffThreshold, + float thetaThreshold); + + +/** This function makes some checks on points and decides + * whether to keep them or not + * + * \param points the array of points to check + * + * \return code that determines what to do with the points + * + */ +int mcvCheckPoints(const CvMat* points); + +/** This functions gets the angle of the line with the horizontal + * + * \param line the line + * + * \return the required angle (radians) + * + */ +float mcvGetLineAngle(const Line& line); + +/** This function groups nearby splines + * + * \param splines vector of splines + * \param lineScores scores of input lines + */ +void mcvGroupSplines(vector &splines, vector &scores); + +/** This functions multiplies a vector by a scalar + * + * \param v the vector + * \param s the scalar + * \return the sum + */ +CvPoint2D32f mcvMultiplyVector(CvPoint2D32f v, float s); + + +/** This functions classifies the passed points according to their + * color to be either white, yellow, or neither + * + * \param im the input color image + * \param points the array of points + * \param window the window to use + * \param numYellowMin min percentage of yellow points + * \param rgMin + * \param rgMax + * \param gbMin + * \param rbMin + * + * \return the line color + * + */ +LineColor mcvGetPointsColor(const CvMat* im, const CvMat* points, + int window, float numYellowMin, + float rgMin, float rgMax, + float gbMin, float rbMin, + bool rbf, float rbfThreshold); + +/** \brief This function extracts bounding boxes from splines + * + * \param splines vector of splines + * \param type the type of lines (LINE_HORIZONTAL or LINE_VERTICAL) + * \param size the size of image containing the lines + * \param boxes a vector of output bounding boxes + */ +void mcvGetSplinesBoundingBoxes(const vector &splines, LineType type, + CvSize size, vector &boxes); + + +/** \brief This function extracts bounding boxes from lines + * + * \param lines vector of lines + * \param type the type of lines (LINE_HORIZONTAL or LINE_VERTICAL) + * \param size the size of image containing the lines + * \param boxes a vector of output bounding boxes + */ +void mcvGetLinesBoundingBoxes(const vector &lines, LineType type, + CvSize size, vector &boxes); + + +/** \brief This function takes a bunch of lines, and check which + * 2 lines can make a lane + * + * \param lines vector of lines + * \param scores vector of line scores + * \param wMu expected lane width + * \param wSigma std deviation of lane width + */ +void mcvCheckLaneWidth(vector &lines, vector &scores, + float wMu, float wSigma); + +} // namespace LaneDetector + +#endif /*LANEDETECTOR_HH_*/ diff --git a/src/LaneDetector.o b/src/LaneDetector.o new file mode 100644 index 0000000000000000000000000000000000000000..54f88a853851330ee651f8d4bf5ea11b311009aa GIT binary patch literal 777480 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ⅈ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 literal 0 HcmV?d00001 diff --git a/src/LaneDetector32 b/src/LaneDetector32 new file mode 100755 index 0000000000000000000000000000000000000000..6dcc5035668269803b4133afbbae1bfee17c93c3 GIT binary patch literal 468737 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�^80$!Ke9 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 literal 0 HcmV?d00001 diff --git a/src/LaneDetector64 b/src/LaneDetector64 new file mode 100755 index 0000000000000000000000000000000000000000..ad6021d34c61222cfe35301e78b0eb8e6a664a2f GIT binary patch literal 558776 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 kdtT3@#14M||5vyv|049xegA +#include +#include + +#include "getopt.h" + +#include "LaneDetectorOpt.h" + +const char *LaneDetectorParserInfo_purpose = ""; + +const char *LaneDetectorParserInfo_usage = "Usage: LaneDetector [OPTIONS]..."; + +const char *LaneDetectorParserInfo_help[] = { + " -h, --help Print help and exit", + " -V, --version Print version and exit", + " --ipmWidth=INT width of IPM image to use", + " --ipmHeight=INT height of IPM image to use", + " --ipmTop=INT Top point in original image of region to make \n IPM for", + " --ipmLeft=INT Left point in original image of region to make \n IPM for", + " --ipmRight=INT Right point in original image region to make \n IPM for", + " --ipmBottom=INT Bottom point in original image region to make \n IPM for", + " --ipmInterpolation=INT The method to use for IPM interpolation", + " --lineWidth=DOUBLE width of line to detect in mm (in the world)", + " --lineHeight=DOUBLE height of line to detect in mm (in the world)", + " --kernelWidth=INT widht of kernel to use for filtering", + " --kernelHeight=INT Height of kernel to use for filtering", + " --lowerQuantile=DOUBLE lower quantile to use for thresholding the \n filtered image", + " --localMaxima=INT whether to return local maxima or just the \n maximum", + " --groupingType=INT type of grouping to use (default 0: HV lines)", + " --binarize=DOUBLE whether to binarize the thresholded image or \n use the raw values", + " --detectionThreshold=DOUBLE\n threshold for line scores to declare as line", + " --smoothScores=INT whether to smooth scores of lines detected or \n not", + " --rMin=DOUBLE rMin for Hough transform (in pixels)", + " --rMax=DOUBLE rMax for Hough transform (in pixels)", + " --rStep=DOUBLE rStep for Hough transform (in pixels)", + " --thetaMin=DOUBLE thetaMin for Hough transform (in degrees)", + " --thetaMax=DOUBLE thetaMax for Hough transform (in degrees)", + " --thetaStep=DOUBLE thetaStep for Hough transform (in degrees)", + " --ipmVpPortion=DOUBLE Portion of IPM image height to add to \n y-coordinate of VP", + " --getEndPoints=INT Get the endpoints of the line", + " --group=INT group nearby lines or not (default 1: group)", + " --groupThreshold=DOUBLE Threshold for grouping nearby lines (default \n 10)", + " --ransac=INT use RANSAC (1) or not (0)", + " --ransacLineNumSamples=INT\n Number of samples to use for RANSAC", + " --ransacLineNumIterations=INT\n Number of iterations to use for RANSAC", + " --ransacLineNumGoodFit=INT\n Number of close points to consider a good line \n fit", + " --ransacLineThreshold=DOUBLE\n Threshold to consider a point close", + " --ransacLineScoreThreshold=DOUBLE\n Threshold for detected line scores", + " --ransacLineBinarize=INT Whether to binarize image for RANSAC or not", + " --ransacLineWindow=INT Half width to use for ransac window", + " --ransacSplineNumSamples=INT\n Number of samples to use for RANSAC", + " --ransacSplineNumIterations=INT\n Number of iterations to use for RANSAC", + " --ransacSplineNumGoodFit=INT\n Number of close points to consider a good line \n fit", + " --ransacSplineThreshold=DOUBLE\n Threshold to consider a point close", + " --ransacSplineScoreThreshold=DOUBLE\n Threshold for detected line scores", + " --ransacSplineBinarize=INT\n Whether to binarize image for RANSAC or not", + " --ransacSplineWindow=INT Half width to use for ransac window", + " --ransacSplineDegree=INT Degree of spline to use", + " --ransacSpline=INT Whether to use splines", + " --ransacLine=INT Whether to use lines", + " --ransacSplineStep=FLOAT Step to use when pixelzing spline in ransac", + " --overlapThreshold=FLOAT Overlap threshold to use for grouping of \n bounding boxes", + " --localizeAngleThreshold=FLOAT\n Angle threshold used for localization (cosine, \n 1: most restrictive, 0: most liberal)", + " --localizeNumLinePixels=INT\n Number of pixels to go in normal direction for \n localization", + " --extendAngleThreshold=FLOAT\n Angle threshold used for extending (cosine, 1: \n most restrictive, 0: most liberal)", + " --extendMeanDirAngleThreshold=FLOAT\n Angle threshold from mean direction used for \n extending (cosine, 1: most restrictive, 0: \n most liberal)", + " --extendLinePixelsTangent=INT\n Number of pixels to go in tangent direction for \n extending", + " --extendLinePixelsNormal=INT\n Number of pixels to go in tangent direction for \n extending", + " --extendContThreshold=FLOAT\n Threhsold used for stopping the extending \n process (higher -> less extending)", + " --extendDeviationThreshold=INT\n Stop extending when number of deviating points \n exceeds this threshold", + " --extendRectTop=INT Top point for extension bounding box", + " --extendRectBottom=INT Bottom point for extension bounding box", + " --extendIPMAngleThreshold=FLOAT\n Angle threshold used for extending (cosine, 1: \n most restrictive, 0: most liberal)", + " --extendIPMMeanDirAngleThreshold=FLOAT\n Angle threshold from mean direction used for \n extending (cosine, 1: most restrictive, 0: \n most liberal)", + " --extendIPMLinePixelsTangent=INT\n Number of pixels to go in tangent direction for \n extending", + " --extendIPMLinePixelsNormal=INT\n Number of pixels to go in tangent direction for \n extending", + " --extendIPMContThreshold=FLOAT\n Threhsold used for stopping the extending \n process (higher -> less extending)", + " --extendIPMDeviationThreshold=INT\n Stop extending when number of deviating points \n exceeds this threshold", + " --extendIPMRectTop=INT Top point for extension bounding box", + " --extendIPMRectBottom=INT Bottom point for extension bounding box", + " --splineScoreJitter=INT Number of pixels to go around the spline to \n compute score", + " --splineScoreLengthRatio=FLOAT\n Ratio of spline length to use", + " --splineScoreAngleRatio=FLOAT\n Ratio of spline angle to use", + " --splineScoreStep=FLOAT Step to use for spline score computation", + " --splineTrackingNumAbsentFrames=INT\n number of frames the track is allowed to be \n absent before deleting it", + " --splineTrackingNumSeenFrames=INT\n number of frames before considering the track \n good", + " --mergeSplineThetaThreshold=FLOAT\n Angle threshold for merging splines (radians)", + " --mergeSplineRThreshold=FLOAT\n R threshold (distance from origin) for merginn \n splines", + " --mergeSplineMeanThetaThreshold=FLOAT\n Mean Angle threshold for merging splines \n (radians)", + " --mergeSplineMeanRThreshold=FLOAT\n Mean R threshold (distance from origin) for \n merginn splines", + " --mergeSplineCentroidThreshold=FLOAT\n Distance threshold between spline cetroids for \n merging", + " --lineTrackingNumAbsentFrames=INT\n number of frames the track is allowed to be \n absent before deleting it", + " --lineTrackingNumSeenFrames=INT\n number of frames before considering the track \n good", + " --mergeLineThetaThreshold=FLOAT\n Angle threshold for merging lines (radians)", + " --mergeLineRThreshold=FLOAT\n R threshold (distance from origin) for merging \n lines", + " --numStrips=INT Number of horizontal strips to divide the image \n to", + " --checkSplines=INT Whtethet to check splines or not", + " --checkSplinesCurvenessThreshold=FLOAT\n Curveness Threshold for checking splines", + " --checkSplinesLengthThreshold=FLOAT\n Length Threshold for checking splines", + " --checkSplinesThetaDiffThreshold=FLOAT\n ThetaDiff Threshold for checking splines", + " --checkSplinesThetaThreshold=FLOAT\n ThetaThreshold Threshold for checking splines", + " --checkIPMSplines=INT Whtethet to check IPM splines or not", + " --checkIPMSplinesCurvenessThreshold=FLOAT\n Curveness Threshold for checking splines", + " --checkIPMSplinesLengthThreshold=FLOAT\n Length Threshold for checking splines", + " --checkIPMSplinesThetaDiffThreshold=FLOAT\n ThetaDiff Threshold for checking splines", + " --checkIPMSplinesThetaThreshold=FLOAT\n ThetaThreshold Threshold for checking splines", + " --finalSplineScoreThreshold=FLOAT\n Final Threshold for declaring a valid spline", + " --useGroundPlane=INT Use groudn plane or not when sending to map", + " --checkColor=INT Whether to check colors or not", + " --checkColorWindow=INT Size of window to use", + " --checkColorNumBins=INT Number of bins to use", + " --checkColorNumYellowMin=FLOAT\n Min ratio of yellow points", + " --checkColorRGMin=FLOAT Min RG diff", + " --checkColorRGMax=FLOAT Max RG diff", + " --checkColorGBMin=FLOAT Min GB diff", + " --checkColorRBMin=FLOAT Min RB diff", + " --checkColorRBFThreshold=FLOAT\n RBF Threshold", + " --checkColorRBF=INT Whether to use RBF or not", + " --ipmWindowClear=INT Whether to clear part of the IPM image", + " --ipmWindowLeft=INT Left corrdinate of window to keep in IPM", + " --ipmWindowRight=INT Left corrdinate of window to keep in IPM", + " --checkLaneWidth=INT Whether to check lane width or not", + " --checkLaneWidthMean=FLOAT\n Mean of lane width to look for", + " --checkLaneWidthStd=FLOAT Std deviation of lane width to look for", + 0 +}; + +static +void clear_given (struct LaneDetectorParserInfo *args_info); +static +void clear_args (struct LaneDetectorParserInfo *args_info); + +static int +LaneDetectorParser_internal (int argc, char * const *argv, struct LaneDetectorParserInfo *args_info, int override, int initialize, int check_required, const char *additional_error); + +static int +LaneDetectorParser_required2 (struct LaneDetectorParserInfo *args_info, const char *prog_name, const char *additional_error); +struct line_list +{ + char * string_arg; + struct line_list * next; +}; + +static struct line_list *cmd_line_list = 0; +static struct line_list *cmd_line_list_tmp = 0; + +static void +free_cmd_list(void) +{ + /* free the list of a previous call */ + if (cmd_line_list) + { + while (cmd_line_list) { + cmd_line_list_tmp = cmd_line_list; + cmd_line_list = cmd_line_list->next; + free (cmd_line_list_tmp->string_arg); + free (cmd_line_list_tmp); + } + } +} + + +static char * +gengetopt_strdup (const char *s); + +static +void clear_given (struct LaneDetectorParserInfo *args_info) +{ + args_info->help_given = 0 ; + args_info->version_given = 0 ; + args_info->ipmWidth_given = 0 ; + args_info->ipmHeight_given = 0 ; + args_info->ipmTop_given = 0 ; + args_info->ipmLeft_given = 0 ; + args_info->ipmRight_given = 0 ; + args_info->ipmBottom_given = 0 ; + args_info->ipmInterpolation_given = 0 ; + args_info->lineWidth_given = 0 ; + args_info->lineHeight_given = 0 ; + args_info->kernelWidth_given = 0 ; + args_info->kernelHeight_given = 0 ; + args_info->lowerQuantile_given = 0 ; + args_info->localMaxima_given = 0 ; + args_info->groupingType_given = 0 ; + args_info->binarize_given = 0 ; + args_info->detectionThreshold_given = 0 ; + args_info->smoothScores_given = 0 ; + args_info->rMin_given = 0 ; + args_info->rMax_given = 0 ; + args_info->rStep_given = 0 ; + args_info->thetaMin_given = 0 ; + args_info->thetaMax_given = 0 ; + args_info->thetaStep_given = 0 ; + args_info->ipmVpPortion_given = 0 ; + args_info->getEndPoints_given = 0 ; + args_info->group_given = 0 ; + args_info->groupThreshold_given = 0 ; + args_info->ransac_given = 0 ; + args_info->ransacLineNumSamples_given = 0 ; + args_info->ransacLineNumIterations_given = 0 ; + args_info->ransacLineNumGoodFit_given = 0 ; + args_info->ransacLineThreshold_given = 0 ; + args_info->ransacLineScoreThreshold_given = 0 ; + args_info->ransacLineBinarize_given = 0 ; + args_info->ransacLineWindow_given = 0 ; + args_info->ransacSplineNumSamples_given = 0 ; + args_info->ransacSplineNumIterations_given = 0 ; + args_info->ransacSplineNumGoodFit_given = 0 ; + args_info->ransacSplineThreshold_given = 0 ; + args_info->ransacSplineScoreThreshold_given = 0 ; + args_info->ransacSplineBinarize_given = 0 ; + args_info->ransacSplineWindow_given = 0 ; + args_info->ransacSplineDegree_given = 0 ; + args_info->ransacSpline_given = 0 ; + args_info->ransacLine_given = 0 ; + args_info->ransacSplineStep_given = 0 ; + args_info->overlapThreshold_given = 0 ; + args_info->localizeAngleThreshold_given = 0 ; + args_info->localizeNumLinePixels_given = 0 ; + args_info->extendAngleThreshold_given = 0 ; + args_info->extendMeanDirAngleThreshold_given = 0 ; + args_info->extendLinePixelsTangent_given = 0 ; + args_info->extendLinePixelsNormal_given = 0 ; + args_info->extendContThreshold_given = 0 ; + args_info->extendDeviationThreshold_given = 0 ; + args_info->extendRectTop_given = 0 ; + args_info->extendRectBottom_given = 0 ; + args_info->extendIPMAngleThreshold_given = 0 ; + args_info->extendIPMMeanDirAngleThreshold_given = 0 ; + args_info->extendIPMLinePixelsTangent_given = 0 ; + args_info->extendIPMLinePixelsNormal_given = 0 ; + args_info->extendIPMContThreshold_given = 0 ; + args_info->extendIPMDeviationThreshold_given = 0 ; + args_info->extendIPMRectTop_given = 0 ; + args_info->extendIPMRectBottom_given = 0 ; + args_info->splineScoreJitter_given = 0 ; + args_info->splineScoreLengthRatio_given = 0 ; + args_info->splineScoreAngleRatio_given = 0 ; + args_info->splineScoreStep_given = 0 ; + args_info->splineTrackingNumAbsentFrames_given = 0 ; + args_info->splineTrackingNumSeenFrames_given = 0 ; + args_info->mergeSplineThetaThreshold_given = 0 ; + args_info->mergeSplineRThreshold_given = 0 ; + args_info->mergeSplineMeanThetaThreshold_given = 0 ; + args_info->mergeSplineMeanRThreshold_given = 0 ; + args_info->mergeSplineCentroidThreshold_given = 0 ; + args_info->lineTrackingNumAbsentFrames_given = 0 ; + args_info->lineTrackingNumSeenFrames_given = 0 ; + args_info->mergeLineThetaThreshold_given = 0 ; + args_info->mergeLineRThreshold_given = 0 ; + args_info->numStrips_given = 0 ; + args_info->checkSplines_given = 0 ; + args_info->checkSplinesCurvenessThreshold_given = 0 ; + args_info->checkSplinesLengthThreshold_given = 0 ; + args_info->checkSplinesThetaDiffThreshold_given = 0 ; + args_info->checkSplinesThetaThreshold_given = 0 ; + args_info->checkIPMSplines_given = 0 ; + args_info->checkIPMSplinesCurvenessThreshold_given = 0 ; + args_info->checkIPMSplinesLengthThreshold_given = 0 ; + args_info->checkIPMSplinesThetaDiffThreshold_given = 0 ; + args_info->checkIPMSplinesThetaThreshold_given = 0 ; + args_info->finalSplineScoreThreshold_given = 0 ; + args_info->useGroundPlane_given = 0 ; + args_info->checkColor_given = 0 ; + args_info->checkColorWindow_given = 0 ; + args_info->checkColorNumBins_given = 0 ; + args_info->checkColorNumYellowMin_given = 0 ; + args_info->checkColorRGMin_given = 0 ; + args_info->checkColorRGMax_given = 0 ; + args_info->checkColorGBMin_given = 0 ; + args_info->checkColorRBMin_given = 0 ; + args_info->checkColorRBFThreshold_given = 0 ; + args_info->checkColorRBF_given = 0 ; + args_info->ipmWindowClear_given = 0 ; + args_info->ipmWindowLeft_given = 0 ; + args_info->ipmWindowRight_given = 0 ; + args_info->checkLaneWidth_given = 0 ; + args_info->checkLaneWidthMean_given = 0 ; + args_info->checkLaneWidthStd_given = 0 ; +} + +static +void clear_args (struct LaneDetectorParserInfo *args_info) +{ + args_info->ipmWidth_orig = NULL; + args_info->ipmHeight_orig = NULL; + args_info->ipmTop_orig = NULL; + args_info->ipmLeft_orig = NULL; + args_info->ipmRight_orig = NULL; + args_info->ipmBottom_orig = NULL; + args_info->ipmInterpolation_orig = NULL; + args_info->lineWidth_orig = NULL; + args_info->lineHeight_orig = NULL; + args_info->kernelWidth_orig = NULL; + args_info->kernelHeight_orig = NULL; + args_info->lowerQuantile_orig = NULL; + args_info->localMaxima_orig = NULL; + args_info->groupingType_orig = NULL; + args_info->binarize_orig = NULL; + args_info->detectionThreshold_orig = NULL; + args_info->smoothScores_orig = NULL; + args_info->rMin_orig = NULL; + args_info->rMax_orig = NULL; + args_info->rStep_orig = NULL; + args_info->thetaMin_orig = NULL; + args_info->thetaMax_orig = NULL; + args_info->thetaStep_orig = NULL; + args_info->ipmVpPortion_orig = NULL; + args_info->getEndPoints_orig = NULL; + args_info->group_orig = NULL; + args_info->groupThreshold_orig = NULL; + args_info->ransac_orig = NULL; + args_info->ransacLineNumSamples_orig = NULL; + args_info->ransacLineNumIterations_orig = NULL; + args_info->ransacLineNumGoodFit_orig = NULL; + args_info->ransacLineThreshold_orig = NULL; + args_info->ransacLineScoreThreshold_orig = NULL; + args_info->ransacLineBinarize_orig = NULL; + args_info->ransacLineWindow_orig = NULL; + args_info->ransacSplineNumSamples_orig = NULL; + args_info->ransacSplineNumIterations_orig = NULL; + args_info->ransacSplineNumGoodFit_orig = NULL; + args_info->ransacSplineThreshold_orig = NULL; + args_info->ransacSplineScoreThreshold_orig = NULL; + args_info->ransacSplineBinarize_orig = NULL; + args_info->ransacSplineWindow_orig = NULL; + args_info->ransacSplineDegree_orig = NULL; + args_info->ransacSpline_orig = NULL; + args_info->ransacLine_orig = NULL; + args_info->ransacSplineStep_orig = NULL; + args_info->overlapThreshold_orig = NULL; + args_info->localizeAngleThreshold_orig = NULL; + args_info->localizeNumLinePixels_orig = NULL; + args_info->extendAngleThreshold_orig = NULL; + args_info->extendMeanDirAngleThreshold_orig = NULL; + args_info->extendLinePixelsTangent_orig = NULL; + args_info->extendLinePixelsNormal_orig = NULL; + args_info->extendContThreshold_orig = NULL; + args_info->extendDeviationThreshold_orig = NULL; + args_info->extendRectTop_orig = NULL; + args_info->extendRectBottom_orig = NULL; + args_info->extendIPMAngleThreshold_orig = NULL; + args_info->extendIPMMeanDirAngleThreshold_orig = NULL; + args_info->extendIPMLinePixelsTangent_orig = NULL; + args_info->extendIPMLinePixelsNormal_orig = NULL; + args_info->extendIPMContThreshold_orig = NULL; + args_info->extendIPMDeviationThreshold_orig = NULL; + args_info->extendIPMRectTop_orig = NULL; + args_info->extendIPMRectBottom_orig = NULL; + args_info->splineScoreJitter_orig = NULL; + args_info->splineScoreLengthRatio_orig = NULL; + args_info->splineScoreAngleRatio_orig = NULL; + args_info->splineScoreStep_orig = NULL; + args_info->splineTrackingNumAbsentFrames_orig = NULL; + args_info->splineTrackingNumSeenFrames_orig = NULL; + args_info->mergeSplineThetaThreshold_orig = NULL; + args_info->mergeSplineRThreshold_orig = NULL; + args_info->mergeSplineMeanThetaThreshold_orig = NULL; + args_info->mergeSplineMeanRThreshold_orig = NULL; + args_info->mergeSplineCentroidThreshold_orig = NULL; + args_info->lineTrackingNumAbsentFrames_orig = NULL; + args_info->lineTrackingNumSeenFrames_orig = NULL; + args_info->mergeLineThetaThreshold_orig = NULL; + args_info->mergeLineRThreshold_orig = NULL; + args_info->numStrips_orig = NULL; + args_info->checkSplines_orig = NULL; + args_info->checkSplinesCurvenessThreshold_orig = NULL; + args_info->checkSplinesLengthThreshold_orig = NULL; + args_info->checkSplinesThetaDiffThreshold_orig = NULL; + args_info->checkSplinesThetaThreshold_orig = NULL; + args_info->checkIPMSplines_orig = NULL; + args_info->checkIPMSplinesCurvenessThreshold_orig = NULL; + args_info->checkIPMSplinesLengthThreshold_orig = NULL; + args_info->checkIPMSplinesThetaDiffThreshold_orig = NULL; + args_info->checkIPMSplinesThetaThreshold_orig = NULL; + args_info->finalSplineScoreThreshold_orig = NULL; + args_info->useGroundPlane_orig = NULL; + args_info->checkColor_orig = NULL; + args_info->checkColorWindow_orig = NULL; + args_info->checkColorNumBins_orig = NULL; + args_info->checkColorNumYellowMin_orig = NULL; + args_info->checkColorRGMin_orig = NULL; + args_info->checkColorRGMax_orig = NULL; + args_info->checkColorGBMin_orig = NULL; + args_info->checkColorRBMin_orig = NULL; + args_info->checkColorRBFThreshold_orig = NULL; + args_info->checkColorRBF_orig = NULL; + args_info->ipmWindowClear_orig = NULL; + args_info->ipmWindowLeft_orig = NULL; + args_info->ipmWindowRight_orig = NULL; + args_info->checkLaneWidth_orig = NULL; + args_info->checkLaneWidthMean_orig = NULL; + args_info->checkLaneWidthStd_orig = NULL; + +} + +static +void init_args_info(struct LaneDetectorParserInfo *args_info) +{ + args_info->help_help = LaneDetectorParserInfo_help[0] ; + args_info->version_help = LaneDetectorParserInfo_help[1] ; + args_info->ipmWidth_help = LaneDetectorParserInfo_help[2] ; + args_info->ipmHeight_help = LaneDetectorParserInfo_help[3] ; + args_info->ipmTop_help = LaneDetectorParserInfo_help[4] ; + args_info->ipmLeft_help = LaneDetectorParserInfo_help[5] ; + args_info->ipmRight_help = LaneDetectorParserInfo_help[6] ; + args_info->ipmBottom_help = LaneDetectorParserInfo_help[7] ; + args_info->ipmInterpolation_help = LaneDetectorParserInfo_help[8] ; + args_info->lineWidth_help = LaneDetectorParserInfo_help[9] ; + args_info->lineHeight_help = LaneDetectorParserInfo_help[10] ; + args_info->kernelWidth_help = LaneDetectorParserInfo_help[11] ; + args_info->kernelHeight_help = LaneDetectorParserInfo_help[12] ; + args_info->lowerQuantile_help = LaneDetectorParserInfo_help[13] ; + args_info->localMaxima_help = LaneDetectorParserInfo_help[14] ; + args_info->groupingType_help = LaneDetectorParserInfo_help[15] ; + args_info->binarize_help = LaneDetectorParserInfo_help[16] ; + args_info->detectionThreshold_help = LaneDetectorParserInfo_help[17] ; + args_info->smoothScores_help = LaneDetectorParserInfo_help[18] ; + args_info->rMin_help = LaneDetectorParserInfo_help[19] ; + args_info->rMax_help = LaneDetectorParserInfo_help[20] ; + args_info->rStep_help = LaneDetectorParserInfo_help[21] ; + args_info->thetaMin_help = LaneDetectorParserInfo_help[22] ; + args_info->thetaMax_help = LaneDetectorParserInfo_help[23] ; + args_info->thetaStep_help = LaneDetectorParserInfo_help[24] ; + args_info->ipmVpPortion_help = LaneDetectorParserInfo_help[25] ; + args_info->getEndPoints_help = LaneDetectorParserInfo_help[26] ; + args_info->group_help = LaneDetectorParserInfo_help[27] ; + args_info->groupThreshold_help = LaneDetectorParserInfo_help[28] ; + args_info->ransac_help = LaneDetectorParserInfo_help[29] ; + args_info->ransacLineNumSamples_help = LaneDetectorParserInfo_help[30] ; + args_info->ransacLineNumIterations_help = LaneDetectorParserInfo_help[31] ; + args_info->ransacLineNumGoodFit_help = LaneDetectorParserInfo_help[32] ; + args_info->ransacLineThreshold_help = LaneDetectorParserInfo_help[33] ; + args_info->ransacLineScoreThreshold_help = LaneDetectorParserInfo_help[34] ; + args_info->ransacLineBinarize_help = LaneDetectorParserInfo_help[35] ; + args_info->ransacLineWindow_help = LaneDetectorParserInfo_help[36] ; + args_info->ransacSplineNumSamples_help = LaneDetectorParserInfo_help[37] ; + args_info->ransacSplineNumIterations_help = LaneDetectorParserInfo_help[38] ; + args_info->ransacSplineNumGoodFit_help = LaneDetectorParserInfo_help[39] ; + args_info->ransacSplineThreshold_help = LaneDetectorParserInfo_help[40] ; + args_info->ransacSplineScoreThreshold_help = LaneDetectorParserInfo_help[41] ; + args_info->ransacSplineBinarize_help = LaneDetectorParserInfo_help[42] ; + args_info->ransacSplineWindow_help = LaneDetectorParserInfo_help[43] ; + args_info->ransacSplineDegree_help = LaneDetectorParserInfo_help[44] ; + args_info->ransacSpline_help = LaneDetectorParserInfo_help[45] ; + args_info->ransacLine_help = LaneDetectorParserInfo_help[46] ; + args_info->ransacSplineStep_help = LaneDetectorParserInfo_help[47] ; + args_info->overlapThreshold_help = LaneDetectorParserInfo_help[48] ; + args_info->localizeAngleThreshold_help = LaneDetectorParserInfo_help[49] ; + args_info->localizeNumLinePixels_help = LaneDetectorParserInfo_help[50] ; + args_info->extendAngleThreshold_help = LaneDetectorParserInfo_help[51] ; + args_info->extendMeanDirAngleThreshold_help = LaneDetectorParserInfo_help[52] ; + args_info->extendLinePixelsTangent_help = LaneDetectorParserInfo_help[53] ; + args_info->extendLinePixelsNormal_help = LaneDetectorParserInfo_help[54] ; + args_info->extendContThreshold_help = LaneDetectorParserInfo_help[55] ; + args_info->extendDeviationThreshold_help = LaneDetectorParserInfo_help[56] ; + args_info->extendRectTop_help = LaneDetectorParserInfo_help[57] ; + args_info->extendRectBottom_help = LaneDetectorParserInfo_help[58] ; + args_info->extendIPMAngleThreshold_help = LaneDetectorParserInfo_help[59] ; + args_info->extendIPMMeanDirAngleThreshold_help = LaneDetectorParserInfo_help[60] ; + args_info->extendIPMLinePixelsTangent_help = LaneDetectorParserInfo_help[61] ; + args_info->extendIPMLinePixelsNormal_help = LaneDetectorParserInfo_help[62] ; + args_info->extendIPMContThreshold_help = LaneDetectorParserInfo_help[63] ; + args_info->extendIPMDeviationThreshold_help = LaneDetectorParserInfo_help[64] ; + args_info->extendIPMRectTop_help = LaneDetectorParserInfo_help[65] ; + args_info->extendIPMRectBottom_help = LaneDetectorParserInfo_help[66] ; + args_info->splineScoreJitter_help = LaneDetectorParserInfo_help[67] ; + args_info->splineScoreLengthRatio_help = LaneDetectorParserInfo_help[68] ; + args_info->splineScoreAngleRatio_help = LaneDetectorParserInfo_help[69] ; + args_info->splineScoreStep_help = LaneDetectorParserInfo_help[70] ; + args_info->splineTrackingNumAbsentFrames_help = LaneDetectorParserInfo_help[71] ; + args_info->splineTrackingNumSeenFrames_help = LaneDetectorParserInfo_help[72] ; + args_info->mergeSplineThetaThreshold_help = LaneDetectorParserInfo_help[73] ; + args_info->mergeSplineRThreshold_help = LaneDetectorParserInfo_help[74] ; + args_info->mergeSplineMeanThetaThreshold_help = LaneDetectorParserInfo_help[75] ; + args_info->mergeSplineMeanRThreshold_help = LaneDetectorParserInfo_help[76] ; + args_info->mergeSplineCentroidThreshold_help = LaneDetectorParserInfo_help[77] ; + args_info->lineTrackingNumAbsentFrames_help = LaneDetectorParserInfo_help[78] ; + args_info->lineTrackingNumSeenFrames_help = LaneDetectorParserInfo_help[79] ; + args_info->mergeLineThetaThreshold_help = LaneDetectorParserInfo_help[80] ; + args_info->mergeLineRThreshold_help = LaneDetectorParserInfo_help[81] ; + args_info->numStrips_help = LaneDetectorParserInfo_help[82] ; + args_info->checkSplines_help = LaneDetectorParserInfo_help[83] ; + args_info->checkSplinesCurvenessThreshold_help = LaneDetectorParserInfo_help[84] ; + args_info->checkSplinesLengthThreshold_help = LaneDetectorParserInfo_help[85] ; + args_info->checkSplinesThetaDiffThreshold_help = LaneDetectorParserInfo_help[86] ; + args_info->checkSplinesThetaThreshold_help = LaneDetectorParserInfo_help[87] ; + args_info->checkIPMSplines_help = LaneDetectorParserInfo_help[88] ; + args_info->checkIPMSplinesCurvenessThreshold_help = LaneDetectorParserInfo_help[89] ; + args_info->checkIPMSplinesLengthThreshold_help = LaneDetectorParserInfo_help[90] ; + args_info->checkIPMSplinesThetaDiffThreshold_help = LaneDetectorParserInfo_help[91] ; + args_info->checkIPMSplinesThetaThreshold_help = LaneDetectorParserInfo_help[92] ; + args_info->finalSplineScoreThreshold_help = LaneDetectorParserInfo_help[93] ; + args_info->useGroundPlane_help = LaneDetectorParserInfo_help[94] ; + args_info->checkColor_help = LaneDetectorParserInfo_help[95] ; + args_info->checkColorWindow_help = LaneDetectorParserInfo_help[96] ; + args_info->checkColorNumBins_help = LaneDetectorParserInfo_help[97] ; + args_info->checkColorNumYellowMin_help = LaneDetectorParserInfo_help[98] ; + args_info->checkColorRGMin_help = LaneDetectorParserInfo_help[99] ; + args_info->checkColorRGMax_help = LaneDetectorParserInfo_help[100] ; + args_info->checkColorGBMin_help = LaneDetectorParserInfo_help[101] ; + args_info->checkColorRBMin_help = LaneDetectorParserInfo_help[102] ; + args_info->checkColorRBFThreshold_help = LaneDetectorParserInfo_help[103] ; + args_info->checkColorRBF_help = LaneDetectorParserInfo_help[104] ; + args_info->ipmWindowClear_help = LaneDetectorParserInfo_help[105] ; + args_info->ipmWindowLeft_help = LaneDetectorParserInfo_help[106] ; + args_info->ipmWindowRight_help = LaneDetectorParserInfo_help[107] ; + args_info->checkLaneWidth_help = LaneDetectorParserInfo_help[108] ; + args_info->checkLaneWidthMean_help = LaneDetectorParserInfo_help[109] ; + args_info->checkLaneWidthStd_help = LaneDetectorParserInfo_help[110] ; + +} + +void +LaneDetectorParser_print_version (void) +{ + printf ("%s %s\n", LANEDETECTORPARSER_PACKAGE, LANEDETECTORPARSER_VERSION); +} + +void +LaneDetectorParser_print_help (void) +{ + int i = 0; + LaneDetectorParser_print_version (); + + if (strlen(LaneDetectorParserInfo_purpose) > 0) + printf("\n%s\n", LaneDetectorParserInfo_purpose); + + printf("\n%s\n\n", LaneDetectorParserInfo_usage); + while (LaneDetectorParserInfo_help[i]) + printf("%s\n", LaneDetectorParserInfo_help[i++]); +} + +void +LaneDetectorParser_init (struct LaneDetectorParserInfo *args_info) +{ + clear_given (args_info); + clear_args (args_info); + init_args_info (args_info); +} + +static void +LaneDetectorParser_release (struct LaneDetectorParserInfo *args_info) +{ + + if (args_info->ipmWidth_orig) + { + free (args_info->ipmWidth_orig); /* free previous argument */ + args_info->ipmWidth_orig = 0; + } + if (args_info->ipmHeight_orig) + { + free (args_info->ipmHeight_orig); /* free previous argument */ + args_info->ipmHeight_orig = 0; + } + if (args_info->ipmTop_orig) + { + free (args_info->ipmTop_orig); /* free previous argument */ + args_info->ipmTop_orig = 0; + } + if (args_info->ipmLeft_orig) + { + free (args_info->ipmLeft_orig); /* free previous argument */ + args_info->ipmLeft_orig = 0; + } + if (args_info->ipmRight_orig) + { + free (args_info->ipmRight_orig); /* free previous argument */ + args_info->ipmRight_orig = 0; + } + if (args_info->ipmBottom_orig) + { + free (args_info->ipmBottom_orig); /* free previous argument */ + args_info->ipmBottom_orig = 0; + } + if (args_info->ipmInterpolation_orig) + { + free (args_info->ipmInterpolation_orig); /* free previous argument */ + args_info->ipmInterpolation_orig = 0; + } + if (args_info->lineWidth_orig) + { + free (args_info->lineWidth_orig); /* free previous argument */ + args_info->lineWidth_orig = 0; + } + if (args_info->lineHeight_orig) + { + free (args_info->lineHeight_orig); /* free previous argument */ + args_info->lineHeight_orig = 0; + } + if (args_info->kernelWidth_orig) + { + free (args_info->kernelWidth_orig); /* free previous argument */ + args_info->kernelWidth_orig = 0; + } + if (args_info->kernelHeight_orig) + { + free (args_info->kernelHeight_orig); /* free previous argument */ + args_info->kernelHeight_orig = 0; + } + if (args_info->lowerQuantile_orig) + { + free (args_info->lowerQuantile_orig); /* free previous argument */ + args_info->lowerQuantile_orig = 0; + } + if (args_info->localMaxima_orig) + { + free (args_info->localMaxima_orig); /* free previous argument */ + args_info->localMaxima_orig = 0; + } + if (args_info->groupingType_orig) + { + free (args_info->groupingType_orig); /* free previous argument */ + args_info->groupingType_orig = 0; + } + if (args_info->binarize_orig) + { + free (args_info->binarize_orig); /* free previous argument */ + args_info->binarize_orig = 0; + } + if (args_info->detectionThreshold_orig) + { + free (args_info->detectionThreshold_orig); /* free previous argument */ + args_info->detectionThreshold_orig = 0; + } + if (args_info->smoothScores_orig) + { + free (args_info->smoothScores_orig); /* free previous argument */ + args_info->smoothScores_orig = 0; + } + if (args_info->rMin_orig) + { + free (args_info->rMin_orig); /* free previous argument */ + args_info->rMin_orig = 0; + } + if (args_info->rMax_orig) + { + free (args_info->rMax_orig); /* free previous argument */ + args_info->rMax_orig = 0; + } + if (args_info->rStep_orig) + { + free (args_info->rStep_orig); /* free previous argument */ + args_info->rStep_orig = 0; + } + if (args_info->thetaMin_orig) + { + free (args_info->thetaMin_orig); /* free previous argument */ + args_info->thetaMin_orig = 0; + } + if (args_info->thetaMax_orig) + { + free (args_info->thetaMax_orig); /* free previous argument */ + args_info->thetaMax_orig = 0; + } + if (args_info->thetaStep_orig) + { + free (args_info->thetaStep_orig); /* free previous argument */ + args_info->thetaStep_orig = 0; + } + if (args_info->ipmVpPortion_orig) + { + free (args_info->ipmVpPortion_orig); /* free previous argument */ + args_info->ipmVpPortion_orig = 0; + } + if (args_info->getEndPoints_orig) + { + free (args_info->getEndPoints_orig); /* free previous argument */ + args_info->getEndPoints_orig = 0; + } + if (args_info->group_orig) + { + free (args_info->group_orig); /* free previous argument */ + args_info->group_orig = 0; + } + if (args_info->groupThreshold_orig) + { + free (args_info->groupThreshold_orig); /* free previous argument */ + args_info->groupThreshold_orig = 0; + } + if (args_info->ransac_orig) + { + free (args_info->ransac_orig); /* free previous argument */ + args_info->ransac_orig = 0; + } + if (args_info->ransacLineNumSamples_orig) + { + free (args_info->ransacLineNumSamples_orig); /* free previous argument */ + args_info->ransacLineNumSamples_orig = 0; + } + if (args_info->ransacLineNumIterations_orig) + { + free (args_info->ransacLineNumIterations_orig); /* free previous argument */ + args_info->ransacLineNumIterations_orig = 0; + } + if (args_info->ransacLineNumGoodFit_orig) + { + free (args_info->ransacLineNumGoodFit_orig); /* free previous argument */ + args_info->ransacLineNumGoodFit_orig = 0; + } + if (args_info->ransacLineThreshold_orig) + { + free (args_info->ransacLineThreshold_orig); /* free previous argument */ + args_info->ransacLineThreshold_orig = 0; + } + if (args_info->ransacLineScoreThreshold_orig) + { + free (args_info->ransacLineScoreThreshold_orig); /* free previous argument */ + args_info->ransacLineScoreThreshold_orig = 0; + } + if (args_info->ransacLineBinarize_orig) + { + free (args_info->ransacLineBinarize_orig); /* free previous argument */ + args_info->ransacLineBinarize_orig = 0; + } + if (args_info->ransacLineWindow_orig) + { + free (args_info->ransacLineWindow_orig); /* free previous argument */ + args_info->ransacLineWindow_orig = 0; + } + if (args_info->ransacSplineNumSamples_orig) + { + free (args_info->ransacSplineNumSamples_orig); /* free previous argument */ + args_info->ransacSplineNumSamples_orig = 0; + } + if (args_info->ransacSplineNumIterations_orig) + { + free (args_info->ransacSplineNumIterations_orig); /* free previous argument */ + args_info->ransacSplineNumIterations_orig = 0; + } + if (args_info->ransacSplineNumGoodFit_orig) + { + free (args_info->ransacSplineNumGoodFit_orig); /* free previous argument */ + args_info->ransacSplineNumGoodFit_orig = 0; + } + if (args_info->ransacSplineThreshold_orig) + { + free (args_info->ransacSplineThreshold_orig); /* free previous argument */ + args_info->ransacSplineThreshold_orig = 0; + } + if (args_info->ransacSplineScoreThreshold_orig) + { + free (args_info->ransacSplineScoreThreshold_orig); /* free previous argument */ + args_info->ransacSplineScoreThreshold_orig = 0; + } + if (args_info->ransacSplineBinarize_orig) + { + free (args_info->ransacSplineBinarize_orig); /* free previous argument */ + args_info->ransacSplineBinarize_orig = 0; + } + if (args_info->ransacSplineWindow_orig) + { + free (args_info->ransacSplineWindow_orig); /* free previous argument */ + args_info->ransacSplineWindow_orig = 0; + } + if (args_info->ransacSplineDegree_orig) + { + free (args_info->ransacSplineDegree_orig); /* free previous argument */ + args_info->ransacSplineDegree_orig = 0; + } + if (args_info->ransacSpline_orig) + { + free (args_info->ransacSpline_orig); /* free previous argument */ + args_info->ransacSpline_orig = 0; + } + if (args_info->ransacLine_orig) + { + free (args_info->ransacLine_orig); /* free previous argument */ + args_info->ransacLine_orig = 0; + } + if (args_info->ransacSplineStep_orig) + { + free (args_info->ransacSplineStep_orig); /* free previous argument */ + args_info->ransacSplineStep_orig = 0; + } + if (args_info->overlapThreshold_orig) + { + free (args_info->overlapThreshold_orig); /* free previous argument */ + args_info->overlapThreshold_orig = 0; + } + if (args_info->localizeAngleThreshold_orig) + { + free (args_info->localizeAngleThreshold_orig); /* free previous argument */ + args_info->localizeAngleThreshold_orig = 0; + } + if (args_info->localizeNumLinePixels_orig) + { + free (args_info->localizeNumLinePixels_orig); /* free previous argument */ + args_info->localizeNumLinePixels_orig = 0; + } + if (args_info->extendAngleThreshold_orig) + { + free (args_info->extendAngleThreshold_orig); /* free previous argument */ + args_info->extendAngleThreshold_orig = 0; + } + if (args_info->extendMeanDirAngleThreshold_orig) + { + free (args_info->extendMeanDirAngleThreshold_orig); /* free previous argument */ + args_info->extendMeanDirAngleThreshold_orig = 0; + } + if (args_info->extendLinePixelsTangent_orig) + { + free (args_info->extendLinePixelsTangent_orig); /* free previous argument */ + args_info->extendLinePixelsTangent_orig = 0; + } + if (args_info->extendLinePixelsNormal_orig) + { + free (args_info->extendLinePixelsNormal_orig); /* free previous argument */ + args_info->extendLinePixelsNormal_orig = 0; + } + if (args_info->extendContThreshold_orig) + { + free (args_info->extendContThreshold_orig); /* free previous argument */ + args_info->extendContThreshold_orig = 0; + } + if (args_info->extendDeviationThreshold_orig) + { + free (args_info->extendDeviationThreshold_orig); /* free previous argument */ + args_info->extendDeviationThreshold_orig = 0; + } + if (args_info->extendRectTop_orig) + { + free (args_info->extendRectTop_orig); /* free previous argument */ + args_info->extendRectTop_orig = 0; + } + if (args_info->extendRectBottom_orig) + { + free (args_info->extendRectBottom_orig); /* free previous argument */ + args_info->extendRectBottom_orig = 0; + } + if (args_info->extendIPMAngleThreshold_orig) + { + free (args_info->extendIPMAngleThreshold_orig); /* free previous argument */ + args_info->extendIPMAngleThreshold_orig = 0; + } + if (args_info->extendIPMMeanDirAngleThreshold_orig) + { + free (args_info->extendIPMMeanDirAngleThreshold_orig); /* free previous argument */ + args_info->extendIPMMeanDirAngleThreshold_orig = 0; + } + if (args_info->extendIPMLinePixelsTangent_orig) + { + free (args_info->extendIPMLinePixelsTangent_orig); /* free previous argument */ + args_info->extendIPMLinePixelsTangent_orig = 0; + } + if (args_info->extendIPMLinePixelsNormal_orig) + { + free (args_info->extendIPMLinePixelsNormal_orig); /* free previous argument */ + args_info->extendIPMLinePixelsNormal_orig = 0; + } + if (args_info->extendIPMContThreshold_orig) + { + free (args_info->extendIPMContThreshold_orig); /* free previous argument */ + args_info->extendIPMContThreshold_orig = 0; + } + if (args_info->extendIPMDeviationThreshold_orig) + { + free (args_info->extendIPMDeviationThreshold_orig); /* free previous argument */ + args_info->extendIPMDeviationThreshold_orig = 0; + } + if (args_info->extendIPMRectTop_orig) + { + free (args_info->extendIPMRectTop_orig); /* free previous argument */ + args_info->extendIPMRectTop_orig = 0; + } + if (args_info->extendIPMRectBottom_orig) + { + free (args_info->extendIPMRectBottom_orig); /* free previous argument */ + args_info->extendIPMRectBottom_orig = 0; + } + if (args_info->splineScoreJitter_orig) + { + free (args_info->splineScoreJitter_orig); /* free previous argument */ + args_info->splineScoreJitter_orig = 0; + } + if (args_info->splineScoreLengthRatio_orig) + { + free (args_info->splineScoreLengthRatio_orig); /* free previous argument */ + args_info->splineScoreLengthRatio_orig = 0; + } + if (args_info->splineScoreAngleRatio_orig) + { + free (args_info->splineScoreAngleRatio_orig); /* free previous argument */ + args_info->splineScoreAngleRatio_orig = 0; + } + if (args_info->splineScoreStep_orig) + { + free (args_info->splineScoreStep_orig); /* free previous argument */ + args_info->splineScoreStep_orig = 0; + } + if (args_info->splineTrackingNumAbsentFrames_orig) + { + free (args_info->splineTrackingNumAbsentFrames_orig); /* free previous argument */ + args_info->splineTrackingNumAbsentFrames_orig = 0; + } + if (args_info->splineTrackingNumSeenFrames_orig) + { + free (args_info->splineTrackingNumSeenFrames_orig); /* free previous argument */ + args_info->splineTrackingNumSeenFrames_orig = 0; + } + if (args_info->mergeSplineThetaThreshold_orig) + { + free (args_info->mergeSplineThetaThreshold_orig); /* free previous argument */ + args_info->mergeSplineThetaThreshold_orig = 0; + } + if (args_info->mergeSplineRThreshold_orig) + { + free (args_info->mergeSplineRThreshold_orig); /* free previous argument */ + args_info->mergeSplineRThreshold_orig = 0; + } + if (args_info->mergeSplineMeanThetaThreshold_orig) + { + free (args_info->mergeSplineMeanThetaThreshold_orig); /* free previous argument */ + args_info->mergeSplineMeanThetaThreshold_orig = 0; + } + if (args_info->mergeSplineMeanRThreshold_orig) + { + free (args_info->mergeSplineMeanRThreshold_orig); /* free previous argument */ + args_info->mergeSplineMeanRThreshold_orig = 0; + } + if (args_info->mergeSplineCentroidThreshold_orig) + { + free (args_info->mergeSplineCentroidThreshold_orig); /* free previous argument */ + args_info->mergeSplineCentroidThreshold_orig = 0; + } + if (args_info->lineTrackingNumAbsentFrames_orig) + { + free (args_info->lineTrackingNumAbsentFrames_orig); /* free previous argument */ + args_info->lineTrackingNumAbsentFrames_orig = 0; + } + if (args_info->lineTrackingNumSeenFrames_orig) + { + free (args_info->lineTrackingNumSeenFrames_orig); /* free previous argument */ + args_info->lineTrackingNumSeenFrames_orig = 0; + } + if (args_info->mergeLineThetaThreshold_orig) + { + free (args_info->mergeLineThetaThreshold_orig); /* free previous argument */ + args_info->mergeLineThetaThreshold_orig = 0; + } + if (args_info->mergeLineRThreshold_orig) + { + free (args_info->mergeLineRThreshold_orig); /* free previous argument */ + args_info->mergeLineRThreshold_orig = 0; + } + if (args_info->numStrips_orig) + { + free (args_info->numStrips_orig); /* free previous argument */ + args_info->numStrips_orig = 0; + } + if (args_info->checkSplines_orig) + { + free (args_info->checkSplines_orig); /* free previous argument */ + args_info->checkSplines_orig = 0; + } + if (args_info->checkSplinesCurvenessThreshold_orig) + { + free (args_info->checkSplinesCurvenessThreshold_orig); /* free previous argument */ + args_info->checkSplinesCurvenessThreshold_orig = 0; + } + if (args_info->checkSplinesLengthThreshold_orig) + { + free (args_info->checkSplinesLengthThreshold_orig); /* free previous argument */ + args_info->checkSplinesLengthThreshold_orig = 0; + } + if (args_info->checkSplinesThetaDiffThreshold_orig) + { + free (args_info->checkSplinesThetaDiffThreshold_orig); /* free previous argument */ + args_info->checkSplinesThetaDiffThreshold_orig = 0; + } + if (args_info->checkSplinesThetaThreshold_orig) + { + free (args_info->checkSplinesThetaThreshold_orig); /* free previous argument */ + args_info->checkSplinesThetaThreshold_orig = 0; + } + if (args_info->checkIPMSplines_orig) + { + free (args_info->checkIPMSplines_orig); /* free previous argument */ + args_info->checkIPMSplines_orig = 0; + } + if (args_info->checkIPMSplinesCurvenessThreshold_orig) + { + free (args_info->checkIPMSplinesCurvenessThreshold_orig); /* free previous argument */ + args_info->checkIPMSplinesCurvenessThreshold_orig = 0; + } + if (args_info->checkIPMSplinesLengthThreshold_orig) + { + free (args_info->checkIPMSplinesLengthThreshold_orig); /* free previous argument */ + args_info->checkIPMSplinesLengthThreshold_orig = 0; + } + if (args_info->checkIPMSplinesThetaDiffThreshold_orig) + { + free (args_info->checkIPMSplinesThetaDiffThreshold_orig); /* free previous argument */ + args_info->checkIPMSplinesThetaDiffThreshold_orig = 0; + } + if (args_info->checkIPMSplinesThetaThreshold_orig) + { + free (args_info->checkIPMSplinesThetaThreshold_orig); /* free previous argument */ + args_info->checkIPMSplinesThetaThreshold_orig = 0; + } + if (args_info->finalSplineScoreThreshold_orig) + { + free (args_info->finalSplineScoreThreshold_orig); /* free previous argument */ + args_info->finalSplineScoreThreshold_orig = 0; + } + if (args_info->useGroundPlane_orig) + { + free (args_info->useGroundPlane_orig); /* free previous argument */ + args_info->useGroundPlane_orig = 0; + } + if (args_info->checkColor_orig) + { + free (args_info->checkColor_orig); /* free previous argument */ + args_info->checkColor_orig = 0; + } + if (args_info->checkColorWindow_orig) + { + free (args_info->checkColorWindow_orig); /* free previous argument */ + args_info->checkColorWindow_orig = 0; + } + if (args_info->checkColorNumBins_orig) + { + free (args_info->checkColorNumBins_orig); /* free previous argument */ + args_info->checkColorNumBins_orig = 0; + } + if (args_info->checkColorNumYellowMin_orig) + { + free (args_info->checkColorNumYellowMin_orig); /* free previous argument */ + args_info->checkColorNumYellowMin_orig = 0; + } + if (args_info->checkColorRGMin_orig) + { + free (args_info->checkColorRGMin_orig); /* free previous argument */ + args_info->checkColorRGMin_orig = 0; + } + if (args_info->checkColorRGMax_orig) + { + free (args_info->checkColorRGMax_orig); /* free previous argument */ + args_info->checkColorRGMax_orig = 0; + } + if (args_info->checkColorGBMin_orig) + { + free (args_info->checkColorGBMin_orig); /* free previous argument */ + args_info->checkColorGBMin_orig = 0; + } + if (args_info->checkColorRBMin_orig) + { + free (args_info->checkColorRBMin_orig); /* free previous argument */ + args_info->checkColorRBMin_orig = 0; + } + if (args_info->checkColorRBFThreshold_orig) + { + free (args_info->checkColorRBFThreshold_orig); /* free previous argument */ + args_info->checkColorRBFThreshold_orig = 0; + } + if (args_info->checkColorRBF_orig) + { + free (args_info->checkColorRBF_orig); /* free previous argument */ + args_info->checkColorRBF_orig = 0; + } + if (args_info->ipmWindowClear_orig) + { + free (args_info->ipmWindowClear_orig); /* free previous argument */ + args_info->ipmWindowClear_orig = 0; + } + if (args_info->ipmWindowLeft_orig) + { + free (args_info->ipmWindowLeft_orig); /* free previous argument */ + args_info->ipmWindowLeft_orig = 0; + } + if (args_info->ipmWindowRight_orig) + { + free (args_info->ipmWindowRight_orig); /* free previous argument */ + args_info->ipmWindowRight_orig = 0; + } + if (args_info->checkLaneWidth_orig) + { + free (args_info->checkLaneWidth_orig); /* free previous argument */ + args_info->checkLaneWidth_orig = 0; + } + if (args_info->checkLaneWidthMean_orig) + { + free (args_info->checkLaneWidthMean_orig); /* free previous argument */ + args_info->checkLaneWidthMean_orig = 0; + } + if (args_info->checkLaneWidthStd_orig) + { + free (args_info->checkLaneWidthStd_orig); /* free previous argument */ + args_info->checkLaneWidthStd_orig = 0; + } + + clear_given (args_info); +} + +int +LaneDetectorParser_file_save(const char *filename, struct LaneDetectorParserInfo *args_info) +{ + FILE *outfile; + int i = 0; + + outfile = fopen(filename, "w"); + + if (!outfile) + { + fprintf (stderr, "%s: cannot open file for writing: %s\n", LANEDETECTORPARSER_PACKAGE, filename); + return EXIT_FAILURE; + } + + if (args_info->help_given) { + fprintf(outfile, "%s\n", "help"); + } + if (args_info->version_given) { + fprintf(outfile, "%s\n", "version"); + } + if (args_info->ipmWidth_given) { + if (args_info->ipmWidth_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmWidth", args_info->ipmWidth_orig); + } else { + fprintf(outfile, "%s\n", "ipmWidth"); + } + } + if (args_info->ipmHeight_given) { + if (args_info->ipmHeight_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmHeight", args_info->ipmHeight_orig); + } else { + fprintf(outfile, "%s\n", "ipmHeight"); + } + } + if (args_info->ipmTop_given) { + if (args_info->ipmTop_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmTop", args_info->ipmTop_orig); + } else { + fprintf(outfile, "%s\n", "ipmTop"); + } + } + if (args_info->ipmLeft_given) { + if (args_info->ipmLeft_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmLeft", args_info->ipmLeft_orig); + } else { + fprintf(outfile, "%s\n", "ipmLeft"); + } + } + if (args_info->ipmRight_given) { + if (args_info->ipmRight_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmRight", args_info->ipmRight_orig); + } else { + fprintf(outfile, "%s\n", "ipmRight"); + } + } + if (args_info->ipmBottom_given) { + if (args_info->ipmBottom_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmBottom", args_info->ipmBottom_orig); + } else { + fprintf(outfile, "%s\n", "ipmBottom"); + } + } + if (args_info->ipmInterpolation_given) { + if (args_info->ipmInterpolation_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmInterpolation", args_info->ipmInterpolation_orig); + } else { + fprintf(outfile, "%s\n", "ipmInterpolation"); + } + } + if (args_info->lineWidth_given) { + if (args_info->lineWidth_orig) { + fprintf(outfile, "%s=\"%s\"\n", "lineWidth", args_info->lineWidth_orig); + } else { + fprintf(outfile, "%s\n", "lineWidth"); + } + } + if (args_info->lineHeight_given) { + if (args_info->lineHeight_orig) { + fprintf(outfile, "%s=\"%s\"\n", "lineHeight", args_info->lineHeight_orig); + } else { + fprintf(outfile, "%s\n", "lineHeight"); + } + } + if (args_info->kernelWidth_given) { + if (args_info->kernelWidth_orig) { + fprintf(outfile, "%s=\"%s\"\n", "kernelWidth", args_info->kernelWidth_orig); + } else { + fprintf(outfile, "%s\n", "kernelWidth"); + } + } + if (args_info->kernelHeight_given) { + if (args_info->kernelHeight_orig) { + fprintf(outfile, "%s=\"%s\"\n", "kernelHeight", args_info->kernelHeight_orig); + } else { + fprintf(outfile, "%s\n", "kernelHeight"); + } + } + if (args_info->lowerQuantile_given) { + if (args_info->lowerQuantile_orig) { + fprintf(outfile, "%s=\"%s\"\n", "lowerQuantile", args_info->lowerQuantile_orig); + } else { + fprintf(outfile, "%s\n", "lowerQuantile"); + } + } + if (args_info->localMaxima_given) { + if (args_info->localMaxima_orig) { + fprintf(outfile, "%s=\"%s\"\n", "localMaxima", args_info->localMaxima_orig); + } else { + fprintf(outfile, "%s\n", "localMaxima"); + } + } + if (args_info->groupingType_given) { + if (args_info->groupingType_orig) { + fprintf(outfile, "%s=\"%s\"\n", "groupingType", args_info->groupingType_orig); + } else { + fprintf(outfile, "%s\n", "groupingType"); + } + } + if (args_info->binarize_given) { + if (args_info->binarize_orig) { + fprintf(outfile, "%s=\"%s\"\n", "binarize", args_info->binarize_orig); + } else { + fprintf(outfile, "%s\n", "binarize"); + } + } + if (args_info->detectionThreshold_given) { + if (args_info->detectionThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "detectionThreshold", args_info->detectionThreshold_orig); + } else { + fprintf(outfile, "%s\n", "detectionThreshold"); + } + } + if (args_info->smoothScores_given) { + if (args_info->smoothScores_orig) { + fprintf(outfile, "%s=\"%s\"\n", "smoothScores", args_info->smoothScores_orig); + } else { + fprintf(outfile, "%s\n", "smoothScores"); + } + } + if (args_info->rMin_given) { + if (args_info->rMin_orig) { + fprintf(outfile, "%s=\"%s\"\n", "rMin", args_info->rMin_orig); + } else { + fprintf(outfile, "%s\n", "rMin"); + } + } + if (args_info->rMax_given) { + if (args_info->rMax_orig) { + fprintf(outfile, "%s=\"%s\"\n", "rMax", args_info->rMax_orig); + } else { + fprintf(outfile, "%s\n", "rMax"); + } + } + if (args_info->rStep_given) { + if (args_info->rStep_orig) { + fprintf(outfile, "%s=\"%s\"\n", "rStep", args_info->rStep_orig); + } else { + fprintf(outfile, "%s\n", "rStep"); + } + } + if (args_info->thetaMin_given) { + if (args_info->thetaMin_orig) { + fprintf(outfile, "%s=\"%s\"\n", "thetaMin", args_info->thetaMin_orig); + } else { + fprintf(outfile, "%s\n", "thetaMin"); + } + } + if (args_info->thetaMax_given) { + if (args_info->thetaMax_orig) { + fprintf(outfile, "%s=\"%s\"\n", "thetaMax", args_info->thetaMax_orig); + } else { + fprintf(outfile, "%s\n", "thetaMax"); + } + } + if (args_info->thetaStep_given) { + if (args_info->thetaStep_orig) { + fprintf(outfile, "%s=\"%s\"\n", "thetaStep", args_info->thetaStep_orig); + } else { + fprintf(outfile, "%s\n", "thetaStep"); + } + } + if (args_info->ipmVpPortion_given) { + if (args_info->ipmVpPortion_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmVpPortion", args_info->ipmVpPortion_orig); + } else { + fprintf(outfile, "%s\n", "ipmVpPortion"); + } + } + if (args_info->getEndPoints_given) { + if (args_info->getEndPoints_orig) { + fprintf(outfile, "%s=\"%s\"\n", "getEndPoints", args_info->getEndPoints_orig); + } else { + fprintf(outfile, "%s\n", "getEndPoints"); + } + } + if (args_info->group_given) { + if (args_info->group_orig) { + fprintf(outfile, "%s=\"%s\"\n", "group", args_info->group_orig); + } else { + fprintf(outfile, "%s\n", "group"); + } + } + if (args_info->groupThreshold_given) { + if (args_info->groupThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "groupThreshold", args_info->groupThreshold_orig); + } else { + fprintf(outfile, "%s\n", "groupThreshold"); + } + } + if (args_info->ransac_given) { + if (args_info->ransac_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransac", args_info->ransac_orig); + } else { + fprintf(outfile, "%s\n", "ransac"); + } + } + if (args_info->ransacLineNumSamples_given) { + if (args_info->ransacLineNumSamples_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacLineNumSamples", args_info->ransacLineNumSamples_orig); + } else { + fprintf(outfile, "%s\n", "ransacLineNumSamples"); + } + } + if (args_info->ransacLineNumIterations_given) { + if (args_info->ransacLineNumIterations_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacLineNumIterations", args_info->ransacLineNumIterations_orig); + } else { + fprintf(outfile, "%s\n", "ransacLineNumIterations"); + } + } + if (args_info->ransacLineNumGoodFit_given) { + if (args_info->ransacLineNumGoodFit_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacLineNumGoodFit", args_info->ransacLineNumGoodFit_orig); + } else { + fprintf(outfile, "%s\n", "ransacLineNumGoodFit"); + } + } + if (args_info->ransacLineThreshold_given) { + if (args_info->ransacLineThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacLineThreshold", args_info->ransacLineThreshold_orig); + } else { + fprintf(outfile, "%s\n", "ransacLineThreshold"); + } + } + if (args_info->ransacLineScoreThreshold_given) { + if (args_info->ransacLineScoreThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacLineScoreThreshold", args_info->ransacLineScoreThreshold_orig); + } else { + fprintf(outfile, "%s\n", "ransacLineScoreThreshold"); + } + } + if (args_info->ransacLineBinarize_given) { + if (args_info->ransacLineBinarize_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacLineBinarize", args_info->ransacLineBinarize_orig); + } else { + fprintf(outfile, "%s\n", "ransacLineBinarize"); + } + } + if (args_info->ransacLineWindow_given) { + if (args_info->ransacLineWindow_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacLineWindow", args_info->ransacLineWindow_orig); + } else { + fprintf(outfile, "%s\n", "ransacLineWindow"); + } + } + if (args_info->ransacSplineNumSamples_given) { + if (args_info->ransacSplineNumSamples_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacSplineNumSamples", args_info->ransacSplineNumSamples_orig); + } else { + fprintf(outfile, "%s\n", "ransacSplineNumSamples"); + } + } + if (args_info->ransacSplineNumIterations_given) { + if (args_info->ransacSplineNumIterations_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacSplineNumIterations", args_info->ransacSplineNumIterations_orig); + } else { + fprintf(outfile, "%s\n", "ransacSplineNumIterations"); + } + } + if (args_info->ransacSplineNumGoodFit_given) { + if (args_info->ransacSplineNumGoodFit_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacSplineNumGoodFit", args_info->ransacSplineNumGoodFit_orig); + } else { + fprintf(outfile, "%s\n", "ransacSplineNumGoodFit"); + } + } + if (args_info->ransacSplineThreshold_given) { + if (args_info->ransacSplineThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacSplineThreshold", args_info->ransacSplineThreshold_orig); + } else { + fprintf(outfile, "%s\n", "ransacSplineThreshold"); + } + } + if (args_info->ransacSplineScoreThreshold_given) { + if (args_info->ransacSplineScoreThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacSplineScoreThreshold", args_info->ransacSplineScoreThreshold_orig); + } else { + fprintf(outfile, "%s\n", "ransacSplineScoreThreshold"); + } + } + if (args_info->ransacSplineBinarize_given) { + if (args_info->ransacSplineBinarize_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacSplineBinarize", args_info->ransacSplineBinarize_orig); + } else { + fprintf(outfile, "%s\n", "ransacSplineBinarize"); + } + } + if (args_info->ransacSplineWindow_given) { + if (args_info->ransacSplineWindow_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacSplineWindow", args_info->ransacSplineWindow_orig); + } else { + fprintf(outfile, "%s\n", "ransacSplineWindow"); + } + } + if (args_info->ransacSplineDegree_given) { + if (args_info->ransacSplineDegree_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacSplineDegree", args_info->ransacSplineDegree_orig); + } else { + fprintf(outfile, "%s\n", "ransacSplineDegree"); + } + } + if (args_info->ransacSpline_given) { + if (args_info->ransacSpline_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacSpline", args_info->ransacSpline_orig); + } else { + fprintf(outfile, "%s\n", "ransacSpline"); + } + } + if (args_info->ransacLine_given) { + if (args_info->ransacLine_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacLine", args_info->ransacLine_orig); + } else { + fprintf(outfile, "%s\n", "ransacLine"); + } + } + if (args_info->ransacSplineStep_given) { + if (args_info->ransacSplineStep_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ransacSplineStep", args_info->ransacSplineStep_orig); + } else { + fprintf(outfile, "%s\n", "ransacSplineStep"); + } + } + if (args_info->overlapThreshold_given) { + if (args_info->overlapThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "overlapThreshold", args_info->overlapThreshold_orig); + } else { + fprintf(outfile, "%s\n", "overlapThreshold"); + } + } + if (args_info->localizeAngleThreshold_given) { + if (args_info->localizeAngleThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "localizeAngleThreshold", args_info->localizeAngleThreshold_orig); + } else { + fprintf(outfile, "%s\n", "localizeAngleThreshold"); + } + } + if (args_info->localizeNumLinePixels_given) { + if (args_info->localizeNumLinePixels_orig) { + fprintf(outfile, "%s=\"%s\"\n", "localizeNumLinePixels", args_info->localizeNumLinePixels_orig); + } else { + fprintf(outfile, "%s\n", "localizeNumLinePixels"); + } + } + if (args_info->extendAngleThreshold_given) { + if (args_info->extendAngleThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendAngleThreshold", args_info->extendAngleThreshold_orig); + } else { + fprintf(outfile, "%s\n", "extendAngleThreshold"); + } + } + if (args_info->extendMeanDirAngleThreshold_given) { + if (args_info->extendMeanDirAngleThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendMeanDirAngleThreshold", args_info->extendMeanDirAngleThreshold_orig); + } else { + fprintf(outfile, "%s\n", "extendMeanDirAngleThreshold"); + } + } + if (args_info->extendLinePixelsTangent_given) { + if (args_info->extendLinePixelsTangent_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendLinePixelsTangent", args_info->extendLinePixelsTangent_orig); + } else { + fprintf(outfile, "%s\n", "extendLinePixelsTangent"); + } + } + if (args_info->extendLinePixelsNormal_given) { + if (args_info->extendLinePixelsNormal_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendLinePixelsNormal", args_info->extendLinePixelsNormal_orig); + } else { + fprintf(outfile, "%s\n", "extendLinePixelsNormal"); + } + } + if (args_info->extendContThreshold_given) { + if (args_info->extendContThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendContThreshold", args_info->extendContThreshold_orig); + } else { + fprintf(outfile, "%s\n", "extendContThreshold"); + } + } + if (args_info->extendDeviationThreshold_given) { + if (args_info->extendDeviationThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendDeviationThreshold", args_info->extendDeviationThreshold_orig); + } else { + fprintf(outfile, "%s\n", "extendDeviationThreshold"); + } + } + if (args_info->extendRectTop_given) { + if (args_info->extendRectTop_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendRectTop", args_info->extendRectTop_orig); + } else { + fprintf(outfile, "%s\n", "extendRectTop"); + } + } + if (args_info->extendRectBottom_given) { + if (args_info->extendRectBottom_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendRectBottom", args_info->extendRectBottom_orig); + } else { + fprintf(outfile, "%s\n", "extendRectBottom"); + } + } + if (args_info->extendIPMAngleThreshold_given) { + if (args_info->extendIPMAngleThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendIPMAngleThreshold", args_info->extendIPMAngleThreshold_orig); + } else { + fprintf(outfile, "%s\n", "extendIPMAngleThreshold"); + } + } + if (args_info->extendIPMMeanDirAngleThreshold_given) { + if (args_info->extendIPMMeanDirAngleThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendIPMMeanDirAngleThreshold", args_info->extendIPMMeanDirAngleThreshold_orig); + } else { + fprintf(outfile, "%s\n", "extendIPMMeanDirAngleThreshold"); + } + } + if (args_info->extendIPMLinePixelsTangent_given) { + if (args_info->extendIPMLinePixelsTangent_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendIPMLinePixelsTangent", args_info->extendIPMLinePixelsTangent_orig); + } else { + fprintf(outfile, "%s\n", "extendIPMLinePixelsTangent"); + } + } + if (args_info->extendIPMLinePixelsNormal_given) { + if (args_info->extendIPMLinePixelsNormal_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendIPMLinePixelsNormal", args_info->extendIPMLinePixelsNormal_orig); + } else { + fprintf(outfile, "%s\n", "extendIPMLinePixelsNormal"); + } + } + if (args_info->extendIPMContThreshold_given) { + if (args_info->extendIPMContThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendIPMContThreshold", args_info->extendIPMContThreshold_orig); + } else { + fprintf(outfile, "%s\n", "extendIPMContThreshold"); + } + } + if (args_info->extendIPMDeviationThreshold_given) { + if (args_info->extendIPMDeviationThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendIPMDeviationThreshold", args_info->extendIPMDeviationThreshold_orig); + } else { + fprintf(outfile, "%s\n", "extendIPMDeviationThreshold"); + } + } + if (args_info->extendIPMRectTop_given) { + if (args_info->extendIPMRectTop_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendIPMRectTop", args_info->extendIPMRectTop_orig); + } else { + fprintf(outfile, "%s\n", "extendIPMRectTop"); + } + } + if (args_info->extendIPMRectBottom_given) { + if (args_info->extendIPMRectBottom_orig) { + fprintf(outfile, "%s=\"%s\"\n", "extendIPMRectBottom", args_info->extendIPMRectBottom_orig); + } else { + fprintf(outfile, "%s\n", "extendIPMRectBottom"); + } + } + if (args_info->splineScoreJitter_given) { + if (args_info->splineScoreJitter_orig) { + fprintf(outfile, "%s=\"%s\"\n", "splineScoreJitter", args_info->splineScoreJitter_orig); + } else { + fprintf(outfile, "%s\n", "splineScoreJitter"); + } + } + if (args_info->splineScoreLengthRatio_given) { + if (args_info->splineScoreLengthRatio_orig) { + fprintf(outfile, "%s=\"%s\"\n", "splineScoreLengthRatio", args_info->splineScoreLengthRatio_orig); + } else { + fprintf(outfile, "%s\n", "splineScoreLengthRatio"); + } + } + if (args_info->splineScoreAngleRatio_given) { + if (args_info->splineScoreAngleRatio_orig) { + fprintf(outfile, "%s=\"%s\"\n", "splineScoreAngleRatio", args_info->splineScoreAngleRatio_orig); + } else { + fprintf(outfile, "%s\n", "splineScoreAngleRatio"); + } + } + if (args_info->splineScoreStep_given) { + if (args_info->splineScoreStep_orig) { + fprintf(outfile, "%s=\"%s\"\n", "splineScoreStep", args_info->splineScoreStep_orig); + } else { + fprintf(outfile, "%s\n", "splineScoreStep"); + } + } + if (args_info->splineTrackingNumAbsentFrames_given) { + if (args_info->splineTrackingNumAbsentFrames_orig) { + fprintf(outfile, "%s=\"%s\"\n", "splineTrackingNumAbsentFrames", args_info->splineTrackingNumAbsentFrames_orig); + } else { + fprintf(outfile, "%s\n", "splineTrackingNumAbsentFrames"); + } + } + if (args_info->splineTrackingNumSeenFrames_given) { + if (args_info->splineTrackingNumSeenFrames_orig) { + fprintf(outfile, "%s=\"%s\"\n", "splineTrackingNumSeenFrames", args_info->splineTrackingNumSeenFrames_orig); + } else { + fprintf(outfile, "%s\n", "splineTrackingNumSeenFrames"); + } + } + if (args_info->mergeSplineThetaThreshold_given) { + if (args_info->mergeSplineThetaThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "mergeSplineThetaThreshold", args_info->mergeSplineThetaThreshold_orig); + } else { + fprintf(outfile, "%s\n", "mergeSplineThetaThreshold"); + } + } + if (args_info->mergeSplineRThreshold_given) { + if (args_info->mergeSplineRThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "mergeSplineRThreshold", args_info->mergeSplineRThreshold_orig); + } else { + fprintf(outfile, "%s\n", "mergeSplineRThreshold"); + } + } + if (args_info->mergeSplineMeanThetaThreshold_given) { + if (args_info->mergeSplineMeanThetaThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "mergeSplineMeanThetaThreshold", args_info->mergeSplineMeanThetaThreshold_orig); + } else { + fprintf(outfile, "%s\n", "mergeSplineMeanThetaThreshold"); + } + } + if (args_info->mergeSplineMeanRThreshold_given) { + if (args_info->mergeSplineMeanRThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "mergeSplineMeanRThreshold", args_info->mergeSplineMeanRThreshold_orig); + } else { + fprintf(outfile, "%s\n", "mergeSplineMeanRThreshold"); + } + } + if (args_info->mergeSplineCentroidThreshold_given) { + if (args_info->mergeSplineCentroidThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "mergeSplineCentroidThreshold", args_info->mergeSplineCentroidThreshold_orig); + } else { + fprintf(outfile, "%s\n", "mergeSplineCentroidThreshold"); + } + } + if (args_info->lineTrackingNumAbsentFrames_given) { + if (args_info->lineTrackingNumAbsentFrames_orig) { + fprintf(outfile, "%s=\"%s\"\n", "lineTrackingNumAbsentFrames", args_info->lineTrackingNumAbsentFrames_orig); + } else { + fprintf(outfile, "%s\n", "lineTrackingNumAbsentFrames"); + } + } + if (args_info->lineTrackingNumSeenFrames_given) { + if (args_info->lineTrackingNumSeenFrames_orig) { + fprintf(outfile, "%s=\"%s\"\n", "lineTrackingNumSeenFrames", args_info->lineTrackingNumSeenFrames_orig); + } else { + fprintf(outfile, "%s\n", "lineTrackingNumSeenFrames"); + } + } + if (args_info->mergeLineThetaThreshold_given) { + if (args_info->mergeLineThetaThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "mergeLineThetaThreshold", args_info->mergeLineThetaThreshold_orig); + } else { + fprintf(outfile, "%s\n", "mergeLineThetaThreshold"); + } + } + if (args_info->mergeLineRThreshold_given) { + if (args_info->mergeLineRThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "mergeLineRThreshold", args_info->mergeLineRThreshold_orig); + } else { + fprintf(outfile, "%s\n", "mergeLineRThreshold"); + } + } + if (args_info->numStrips_given) { + if (args_info->numStrips_orig) { + fprintf(outfile, "%s=\"%s\"\n", "numStrips", args_info->numStrips_orig); + } else { + fprintf(outfile, "%s\n", "numStrips"); + } + } + if (args_info->checkSplines_given) { + if (args_info->checkSplines_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkSplines", args_info->checkSplines_orig); + } else { + fprintf(outfile, "%s\n", "checkSplines"); + } + } + if (args_info->checkSplinesCurvenessThreshold_given) { + if (args_info->checkSplinesCurvenessThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkSplinesCurvenessThreshold", args_info->checkSplinesCurvenessThreshold_orig); + } else { + fprintf(outfile, "%s\n", "checkSplinesCurvenessThreshold"); + } + } + if (args_info->checkSplinesLengthThreshold_given) { + if (args_info->checkSplinesLengthThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkSplinesLengthThreshold", args_info->checkSplinesLengthThreshold_orig); + } else { + fprintf(outfile, "%s\n", "checkSplinesLengthThreshold"); + } + } + if (args_info->checkSplinesThetaDiffThreshold_given) { + if (args_info->checkSplinesThetaDiffThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkSplinesThetaDiffThreshold", args_info->checkSplinesThetaDiffThreshold_orig); + } else { + fprintf(outfile, "%s\n", "checkSplinesThetaDiffThreshold"); + } + } + if (args_info->checkSplinesThetaThreshold_given) { + if (args_info->checkSplinesThetaThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkSplinesThetaThreshold", args_info->checkSplinesThetaThreshold_orig); + } else { + fprintf(outfile, "%s\n", "checkSplinesThetaThreshold"); + } + } + if (args_info->checkIPMSplines_given) { + if (args_info->checkIPMSplines_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkIPMSplines", args_info->checkIPMSplines_orig); + } else { + fprintf(outfile, "%s\n", "checkIPMSplines"); + } + } + if (args_info->checkIPMSplinesCurvenessThreshold_given) { + if (args_info->checkIPMSplinesCurvenessThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkIPMSplinesCurvenessThreshold", args_info->checkIPMSplinesCurvenessThreshold_orig); + } else { + fprintf(outfile, "%s\n", "checkIPMSplinesCurvenessThreshold"); + } + } + if (args_info->checkIPMSplinesLengthThreshold_given) { + if (args_info->checkIPMSplinesLengthThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkIPMSplinesLengthThreshold", args_info->checkIPMSplinesLengthThreshold_orig); + } else { + fprintf(outfile, "%s\n", "checkIPMSplinesLengthThreshold"); + } + } + if (args_info->checkIPMSplinesThetaDiffThreshold_given) { + if (args_info->checkIPMSplinesThetaDiffThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkIPMSplinesThetaDiffThreshold", args_info->checkIPMSplinesThetaDiffThreshold_orig); + } else { + fprintf(outfile, "%s\n", "checkIPMSplinesThetaDiffThreshold"); + } + } + if (args_info->checkIPMSplinesThetaThreshold_given) { + if (args_info->checkIPMSplinesThetaThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkIPMSplinesThetaThreshold", args_info->checkIPMSplinesThetaThreshold_orig); + } else { + fprintf(outfile, "%s\n", "checkIPMSplinesThetaThreshold"); + } + } + if (args_info->finalSplineScoreThreshold_given) { + if (args_info->finalSplineScoreThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "finalSplineScoreThreshold", args_info->finalSplineScoreThreshold_orig); + } else { + fprintf(outfile, "%s\n", "finalSplineScoreThreshold"); + } + } + if (args_info->useGroundPlane_given) { + if (args_info->useGroundPlane_orig) { + fprintf(outfile, "%s=\"%s\"\n", "useGroundPlane", args_info->useGroundPlane_orig); + } else { + fprintf(outfile, "%s\n", "useGroundPlane"); + } + } + if (args_info->checkColor_given) { + if (args_info->checkColor_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkColor", args_info->checkColor_orig); + } else { + fprintf(outfile, "%s\n", "checkColor"); + } + } + if (args_info->checkColorWindow_given) { + if (args_info->checkColorWindow_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkColorWindow", args_info->checkColorWindow_orig); + } else { + fprintf(outfile, "%s\n", "checkColorWindow"); + } + } + if (args_info->checkColorNumBins_given) { + if (args_info->checkColorNumBins_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkColorNumBins", args_info->checkColorNumBins_orig); + } else { + fprintf(outfile, "%s\n", "checkColorNumBins"); + } + } + if (args_info->checkColorNumYellowMin_given) { + if (args_info->checkColorNumYellowMin_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkColorNumYellowMin", args_info->checkColorNumYellowMin_orig); + } else { + fprintf(outfile, "%s\n", "checkColorNumYellowMin"); + } + } + if (args_info->checkColorRGMin_given) { + if (args_info->checkColorRGMin_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkColorRGMin", args_info->checkColorRGMin_orig); + } else { + fprintf(outfile, "%s\n", "checkColorRGMin"); + } + } + if (args_info->checkColorRGMax_given) { + if (args_info->checkColorRGMax_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkColorRGMax", args_info->checkColorRGMax_orig); + } else { + fprintf(outfile, "%s\n", "checkColorRGMax"); + } + } + if (args_info->checkColorGBMin_given) { + if (args_info->checkColorGBMin_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkColorGBMin", args_info->checkColorGBMin_orig); + } else { + fprintf(outfile, "%s\n", "checkColorGBMin"); + } + } + if (args_info->checkColorRBMin_given) { + if (args_info->checkColorRBMin_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkColorRBMin", args_info->checkColorRBMin_orig); + } else { + fprintf(outfile, "%s\n", "checkColorRBMin"); + } + } + if (args_info->checkColorRBFThreshold_given) { + if (args_info->checkColorRBFThreshold_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkColorRBFThreshold", args_info->checkColorRBFThreshold_orig); + } else { + fprintf(outfile, "%s\n", "checkColorRBFThreshold"); + } + } + if (args_info->checkColorRBF_given) { + if (args_info->checkColorRBF_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkColorRBF", args_info->checkColorRBF_orig); + } else { + fprintf(outfile, "%s\n", "checkColorRBF"); + } + } + if (args_info->ipmWindowClear_given) { + if (args_info->ipmWindowClear_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmWindowClear", args_info->ipmWindowClear_orig); + } else { + fprintf(outfile, "%s\n", "ipmWindowClear"); + } + } + if (args_info->ipmWindowLeft_given) { + if (args_info->ipmWindowLeft_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmWindowLeft", args_info->ipmWindowLeft_orig); + } else { + fprintf(outfile, "%s\n", "ipmWindowLeft"); + } + } + if (args_info->ipmWindowRight_given) { + if (args_info->ipmWindowRight_orig) { + fprintf(outfile, "%s=\"%s\"\n", "ipmWindowRight", args_info->ipmWindowRight_orig); + } else { + fprintf(outfile, "%s\n", "ipmWindowRight"); + } + } + if (args_info->checkLaneWidth_given) { + if (args_info->checkLaneWidth_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkLaneWidth", args_info->checkLaneWidth_orig); + } else { + fprintf(outfile, "%s\n", "checkLaneWidth"); + } + } + if (args_info->checkLaneWidthMean_given) { + if (args_info->checkLaneWidthMean_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkLaneWidthMean", args_info->checkLaneWidthMean_orig); + } else { + fprintf(outfile, "%s\n", "checkLaneWidthMean"); + } + } + if (args_info->checkLaneWidthStd_given) { + if (args_info->checkLaneWidthStd_orig) { + fprintf(outfile, "%s=\"%s\"\n", "checkLaneWidthStd", args_info->checkLaneWidthStd_orig); + } else { + fprintf(outfile, "%s\n", "checkLaneWidthStd"); + } + } + + fclose (outfile); + + i = EXIT_SUCCESS; + return i; +} + +void +LaneDetectorParser_free (struct LaneDetectorParserInfo *args_info) +{ + LaneDetectorParser_release (args_info); +} + + +/* gengetopt_strdup() */ +/* strdup.c replacement of strdup, which is not standard */ +char * +gengetopt_strdup (const char *s) +{ + char *result = NULL; + if (!s) + return result; + + result = (char*)malloc(strlen(s) + 1); + if (result == (char*)0) + return (char*)0; + strcpy(result, s); + return result; +} + +int +LaneDetectorParser (int argc, char * const *argv, struct LaneDetectorParserInfo *args_info) +{ + return LaneDetectorParser2 (argc, argv, args_info, 0, 1, 1); +} + +int +LaneDetectorParser2 (int argc, char * const *argv, struct LaneDetectorParserInfo *args_info, int override, int initialize, int check_required) +{ + int result; + + result = LaneDetectorParser_internal (argc, argv, args_info, override, initialize, check_required, NULL); + + if (result == EXIT_FAILURE) + { + LaneDetectorParser_free (args_info); + exit (EXIT_FAILURE); + } + + return result; +} + +int +LaneDetectorParser_required (struct LaneDetectorParserInfo *args_info, const char *prog_name) +{ + int result = EXIT_SUCCESS; + + if (LaneDetectorParser_required2(args_info, prog_name, NULL) > 0) + result = EXIT_FAILURE; + + if (result == EXIT_FAILURE) + { + LaneDetectorParser_free (args_info); + exit (EXIT_FAILURE); + } + + return result; +} + +int +LaneDetectorParser_required2 (struct LaneDetectorParserInfo *args_info, const char *prog_name, const char *additional_error) +{ + int error = 0; + + /* checks for required options */ + if (! args_info->ipmWidth_given) + { + fprintf (stderr, "%s: '--ipmWidth' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ipmHeight_given) + { + fprintf (stderr, "%s: '--ipmHeight' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ipmTop_given) + { + fprintf (stderr, "%s: '--ipmTop' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ipmLeft_given) + { + fprintf (stderr, "%s: '--ipmLeft' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ipmRight_given) + { + fprintf (stderr, "%s: '--ipmRight' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ipmBottom_given) + { + fprintf (stderr, "%s: '--ipmBottom' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ipmInterpolation_given) + { + fprintf (stderr, "%s: '--ipmInterpolation' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->lineWidth_given) + { + fprintf (stderr, "%s: '--lineWidth' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->lineHeight_given) + { + fprintf (stderr, "%s: '--lineHeight' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->kernelWidth_given) + { + fprintf (stderr, "%s: '--kernelWidth' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->kernelHeight_given) + { + fprintf (stderr, "%s: '--kernelHeight' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->lowerQuantile_given) + { + fprintf (stderr, "%s: '--lowerQuantile' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->localMaxima_given) + { + fprintf (stderr, "%s: '--localMaxima' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->groupingType_given) + { + fprintf (stderr, "%s: '--groupingType' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->binarize_given) + { + fprintf (stderr, "%s: '--binarize' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->detectionThreshold_given) + { + fprintf (stderr, "%s: '--detectionThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->smoothScores_given) + { + fprintf (stderr, "%s: '--smoothScores' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->rMin_given) + { + fprintf (stderr, "%s: '--rMin' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->rMax_given) + { + fprintf (stderr, "%s: '--rMax' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->rStep_given) + { + fprintf (stderr, "%s: '--rStep' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->thetaMin_given) + { + fprintf (stderr, "%s: '--thetaMin' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->thetaMax_given) + { + fprintf (stderr, "%s: '--thetaMax' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->thetaStep_given) + { + fprintf (stderr, "%s: '--thetaStep' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ipmVpPortion_given) + { + fprintf (stderr, "%s: '--ipmVpPortion' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->getEndPoints_given) + { + fprintf (stderr, "%s: '--getEndPoints' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->group_given) + { + fprintf (stderr, "%s: '--group' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->groupThreshold_given) + { + fprintf (stderr, "%s: '--groupThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransac_given) + { + fprintf (stderr, "%s: '--ransac' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacLineNumSamples_given) + { + fprintf (stderr, "%s: '--ransacLineNumSamples' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacLineNumIterations_given) + { + fprintf (stderr, "%s: '--ransacLineNumIterations' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacLineNumGoodFit_given) + { + fprintf (stderr, "%s: '--ransacLineNumGoodFit' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacLineThreshold_given) + { + fprintf (stderr, "%s: '--ransacLineThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacLineScoreThreshold_given) + { + fprintf (stderr, "%s: '--ransacLineScoreThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacLineBinarize_given) + { + fprintf (stderr, "%s: '--ransacLineBinarize' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacLineWindow_given) + { + fprintf (stderr, "%s: '--ransacLineWindow' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacSplineNumSamples_given) + { + fprintf (stderr, "%s: '--ransacSplineNumSamples' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacSplineNumIterations_given) + { + fprintf (stderr, "%s: '--ransacSplineNumIterations' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacSplineNumGoodFit_given) + { + fprintf (stderr, "%s: '--ransacSplineNumGoodFit' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacSplineThreshold_given) + { + fprintf (stderr, "%s: '--ransacSplineThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacSplineScoreThreshold_given) + { + fprintf (stderr, "%s: '--ransacSplineScoreThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacSplineBinarize_given) + { + fprintf (stderr, "%s: '--ransacSplineBinarize' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacSplineWindow_given) + { + fprintf (stderr, "%s: '--ransacSplineWindow' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacSplineDegree_given) + { + fprintf (stderr, "%s: '--ransacSplineDegree' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacSpline_given) + { + fprintf (stderr, "%s: '--ransacSpline' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacLine_given) + { + fprintf (stderr, "%s: '--ransacLine' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ransacSplineStep_given) + { + fprintf (stderr, "%s: '--ransacSplineStep' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->overlapThreshold_given) + { + fprintf (stderr, "%s: '--overlapThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->localizeAngleThreshold_given) + { + fprintf (stderr, "%s: '--localizeAngleThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->localizeNumLinePixels_given) + { + fprintf (stderr, "%s: '--localizeNumLinePixels' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendAngleThreshold_given) + { + fprintf (stderr, "%s: '--extendAngleThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendMeanDirAngleThreshold_given) + { + fprintf (stderr, "%s: '--extendMeanDirAngleThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendLinePixelsTangent_given) + { + fprintf (stderr, "%s: '--extendLinePixelsTangent' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendLinePixelsNormal_given) + { + fprintf (stderr, "%s: '--extendLinePixelsNormal' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendContThreshold_given) + { + fprintf (stderr, "%s: '--extendContThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendDeviationThreshold_given) + { + fprintf (stderr, "%s: '--extendDeviationThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendRectTop_given) + { + fprintf (stderr, "%s: '--extendRectTop' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendRectBottom_given) + { + fprintf (stderr, "%s: '--extendRectBottom' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendIPMAngleThreshold_given) + { + fprintf (stderr, "%s: '--extendIPMAngleThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendIPMMeanDirAngleThreshold_given) + { + fprintf (stderr, "%s: '--extendIPMMeanDirAngleThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendIPMLinePixelsTangent_given) + { + fprintf (stderr, "%s: '--extendIPMLinePixelsTangent' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendIPMLinePixelsNormal_given) + { + fprintf (stderr, "%s: '--extendIPMLinePixelsNormal' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendIPMContThreshold_given) + { + fprintf (stderr, "%s: '--extendIPMContThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendIPMDeviationThreshold_given) + { + fprintf (stderr, "%s: '--extendIPMDeviationThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendIPMRectTop_given) + { + fprintf (stderr, "%s: '--extendIPMRectTop' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->extendIPMRectBottom_given) + { + fprintf (stderr, "%s: '--extendIPMRectBottom' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->splineScoreJitter_given) + { + fprintf (stderr, "%s: '--splineScoreJitter' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->splineScoreLengthRatio_given) + { + fprintf (stderr, "%s: '--splineScoreLengthRatio' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->splineScoreAngleRatio_given) + { + fprintf (stderr, "%s: '--splineScoreAngleRatio' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->splineScoreStep_given) + { + fprintf (stderr, "%s: '--splineScoreStep' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->splineTrackingNumAbsentFrames_given) + { + fprintf (stderr, "%s: '--splineTrackingNumAbsentFrames' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->splineTrackingNumSeenFrames_given) + { + fprintf (stderr, "%s: '--splineTrackingNumSeenFrames' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->mergeSplineThetaThreshold_given) + { + fprintf (stderr, "%s: '--mergeSplineThetaThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->mergeSplineRThreshold_given) + { + fprintf (stderr, "%s: '--mergeSplineRThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->mergeSplineMeanThetaThreshold_given) + { + fprintf (stderr, "%s: '--mergeSplineMeanThetaThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->mergeSplineMeanRThreshold_given) + { + fprintf (stderr, "%s: '--mergeSplineMeanRThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->mergeSplineCentroidThreshold_given) + { + fprintf (stderr, "%s: '--mergeSplineCentroidThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->lineTrackingNumAbsentFrames_given) + { + fprintf (stderr, "%s: '--lineTrackingNumAbsentFrames' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->lineTrackingNumSeenFrames_given) + { + fprintf (stderr, "%s: '--lineTrackingNumSeenFrames' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->mergeLineThetaThreshold_given) + { + fprintf (stderr, "%s: '--mergeLineThetaThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->mergeLineRThreshold_given) + { + fprintf (stderr, "%s: '--mergeLineRThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->numStrips_given) + { + fprintf (stderr, "%s: '--numStrips' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkSplines_given) + { + fprintf (stderr, "%s: '--checkSplines' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkSplinesCurvenessThreshold_given) + { + fprintf (stderr, "%s: '--checkSplinesCurvenessThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkSplinesLengthThreshold_given) + { + fprintf (stderr, "%s: '--checkSplinesLengthThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkSplinesThetaDiffThreshold_given) + { + fprintf (stderr, "%s: '--checkSplinesThetaDiffThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkSplinesThetaThreshold_given) + { + fprintf (stderr, "%s: '--checkSplinesThetaThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkIPMSplines_given) + { + fprintf (stderr, "%s: '--checkIPMSplines' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkIPMSplinesCurvenessThreshold_given) + { + fprintf (stderr, "%s: '--checkIPMSplinesCurvenessThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkIPMSplinesLengthThreshold_given) + { + fprintf (stderr, "%s: '--checkIPMSplinesLengthThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkIPMSplinesThetaDiffThreshold_given) + { + fprintf (stderr, "%s: '--checkIPMSplinesThetaDiffThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkIPMSplinesThetaThreshold_given) + { + fprintf (stderr, "%s: '--checkIPMSplinesThetaThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->finalSplineScoreThreshold_given) + { + fprintf (stderr, "%s: '--finalSplineScoreThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->useGroundPlane_given) + { + fprintf (stderr, "%s: '--useGroundPlane' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkColor_given) + { + fprintf (stderr, "%s: '--checkColor' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkColorWindow_given) + { + fprintf (stderr, "%s: '--checkColorWindow' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkColorNumBins_given) + { + fprintf (stderr, "%s: '--checkColorNumBins' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkColorNumYellowMin_given) + { + fprintf (stderr, "%s: '--checkColorNumYellowMin' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkColorRGMin_given) + { + fprintf (stderr, "%s: '--checkColorRGMin' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkColorRGMax_given) + { + fprintf (stderr, "%s: '--checkColorRGMax' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkColorGBMin_given) + { + fprintf (stderr, "%s: '--checkColorGBMin' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkColorRBMin_given) + { + fprintf (stderr, "%s: '--checkColorRBMin' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkColorRBFThreshold_given) + { + fprintf (stderr, "%s: '--checkColorRBFThreshold' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkColorRBF_given) + { + fprintf (stderr, "%s: '--checkColorRBF' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ipmWindowClear_given) + { + fprintf (stderr, "%s: '--ipmWindowClear' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ipmWindowLeft_given) + { + fprintf (stderr, "%s: '--ipmWindowLeft' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->ipmWindowRight_given) + { + fprintf (stderr, "%s: '--ipmWindowRight' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkLaneWidth_given) + { + fprintf (stderr, "%s: '--checkLaneWidth' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkLaneWidthMean_given) + { + fprintf (stderr, "%s: '--checkLaneWidthMean' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + if (! args_info->checkLaneWidthStd_given) + { + fprintf (stderr, "%s: '--checkLaneWidthStd' option required%s\n", prog_name, (additional_error ? additional_error : "")); + error = 1; + } + + + /* checks for dependences among options */ + + return error; +} + +int +LaneDetectorParser_internal (int argc, char * const *argv, struct LaneDetectorParserInfo *args_info, int override, int initialize, int check_required, const char *additional_error) +{ + int c; /* Character of the parsed option. */ + + int error = 0; + struct LaneDetectorParserInfo local_args_info; + + if (initialize) + LaneDetectorParser_init (args_info); + + LaneDetectorParser_init (&local_args_info); + + optarg = 0; + optind = 0; + opterr = 1; + optopt = '?'; + + while (1) + { + int option_index = 0; + char *stop_char; + + static struct option long_options[] = { + { "help", 0, NULL, 'h' }, + { "version", 0, NULL, 'V' }, + { "ipmWidth", 1, NULL, 0 }, + { "ipmHeight", 1, NULL, 0 }, + { "ipmTop", 1, NULL, 0 }, + { "ipmLeft", 1, NULL, 0 }, + { "ipmRight", 1, NULL, 0 }, + { "ipmBottom", 1, NULL, 0 }, + { "ipmInterpolation", 1, NULL, 0 }, + { "lineWidth", 1, NULL, 0 }, + { "lineHeight", 1, NULL, 0 }, + { "kernelWidth", 1, NULL, 0 }, + { "kernelHeight", 1, NULL, 0 }, + { "lowerQuantile", 1, NULL, 0 }, + { "localMaxima", 1, NULL, 0 }, + { "groupingType", 1, NULL, 0 }, + { "binarize", 1, NULL, 0 }, + { "detectionThreshold", 1, NULL, 0 }, + { "smoothScores", 1, NULL, 0 }, + { "rMin", 1, NULL, 0 }, + { "rMax", 1, NULL, 0 }, + { "rStep", 1, NULL, 0 }, + { "thetaMin", 1, NULL, 0 }, + { "thetaMax", 1, NULL, 0 }, + { "thetaStep", 1, NULL, 0 }, + { "ipmVpPortion", 1, NULL, 0 }, + { "getEndPoints", 1, NULL, 0 }, + { "group", 1, NULL, 0 }, + { "groupThreshold", 1, NULL, 0 }, + { "ransac", 1, NULL, 0 }, + { "ransacLineNumSamples", 1, NULL, 0 }, + { "ransacLineNumIterations", 1, NULL, 0 }, + { "ransacLineNumGoodFit", 1, NULL, 0 }, + { "ransacLineThreshold", 1, NULL, 0 }, + { "ransacLineScoreThreshold", 1, NULL, 0 }, + { "ransacLineBinarize", 1, NULL, 0 }, + { "ransacLineWindow", 1, NULL, 0 }, + { "ransacSplineNumSamples", 1, NULL, 0 }, + { "ransacSplineNumIterations", 1, NULL, 0 }, + { "ransacSplineNumGoodFit", 1, NULL, 0 }, + { "ransacSplineThreshold", 1, NULL, 0 }, + { "ransacSplineScoreThreshold", 1, NULL, 0 }, + { "ransacSplineBinarize", 1, NULL, 0 }, + { "ransacSplineWindow", 1, NULL, 0 }, + { "ransacSplineDegree", 1, NULL, 0 }, + { "ransacSpline", 1, NULL, 0 }, + { "ransacLine", 1, NULL, 0 }, + { "ransacSplineStep", 1, NULL, 0 }, + { "overlapThreshold", 1, NULL, 0 }, + { "localizeAngleThreshold", 1, NULL, 0 }, + { "localizeNumLinePixels", 1, NULL, 0 }, + { "extendAngleThreshold", 1, NULL, 0 }, + { "extendMeanDirAngleThreshold", 1, NULL, 0 }, + { "extendLinePixelsTangent", 1, NULL, 0 }, + { "extendLinePixelsNormal", 1, NULL, 0 }, + { "extendContThreshold", 1, NULL, 0 }, + { "extendDeviationThreshold", 1, NULL, 0 }, + { "extendRectTop", 1, NULL, 0 }, + { "extendRectBottom", 1, NULL, 0 }, + { "extendIPMAngleThreshold", 1, NULL, 0 }, + { "extendIPMMeanDirAngleThreshold", 1, NULL, 0 }, + { "extendIPMLinePixelsTangent", 1, NULL, 0 }, + { "extendIPMLinePixelsNormal", 1, NULL, 0 }, + { "extendIPMContThreshold", 1, NULL, 0 }, + { "extendIPMDeviationThreshold", 1, NULL, 0 }, + { "extendIPMRectTop", 1, NULL, 0 }, + { "extendIPMRectBottom", 1, NULL, 0 }, + { "splineScoreJitter", 1, NULL, 0 }, + { "splineScoreLengthRatio", 1, NULL, 0 }, + { "splineScoreAngleRatio", 1, NULL, 0 }, + { "splineScoreStep", 1, NULL, 0 }, + { "splineTrackingNumAbsentFrames", 1, NULL, 0 }, + { "splineTrackingNumSeenFrames", 1, NULL, 0 }, + { "mergeSplineThetaThreshold", 1, NULL, 0 }, + { "mergeSplineRThreshold", 1, NULL, 0 }, + { "mergeSplineMeanThetaThreshold", 1, NULL, 0 }, + { "mergeSplineMeanRThreshold", 1, NULL, 0 }, + { "mergeSplineCentroidThreshold", 1, NULL, 0 }, + { "lineTrackingNumAbsentFrames", 1, NULL, 0 }, + { "lineTrackingNumSeenFrames", 1, NULL, 0 }, + { "mergeLineThetaThreshold", 1, NULL, 0 }, + { "mergeLineRThreshold", 1, NULL, 0 }, + { "numStrips", 1, NULL, 0 }, + { "checkSplines", 1, NULL, 0 }, + { "checkSplinesCurvenessThreshold", 1, NULL, 0 }, + { "checkSplinesLengthThreshold", 1, NULL, 0 }, + { "checkSplinesThetaDiffThreshold", 1, NULL, 0 }, + { "checkSplinesThetaThreshold", 1, NULL, 0 }, + { "checkIPMSplines", 1, NULL, 0 }, + { "checkIPMSplinesCurvenessThreshold", 1, NULL, 0 }, + { "checkIPMSplinesLengthThreshold", 1, NULL, 0 }, + { "checkIPMSplinesThetaDiffThreshold", 1, NULL, 0 }, + { "checkIPMSplinesThetaThreshold", 1, NULL, 0 }, + { "finalSplineScoreThreshold", 1, NULL, 0 }, + { "useGroundPlane", 1, NULL, 0 }, + { "checkColor", 1, NULL, 0 }, + { "checkColorWindow", 1, NULL, 0 }, + { "checkColorNumBins", 1, NULL, 0 }, + { "checkColorNumYellowMin", 1, NULL, 0 }, + { "checkColorRGMin", 1, NULL, 0 }, + { "checkColorRGMax", 1, NULL, 0 }, + { "checkColorGBMin", 1, NULL, 0 }, + { "checkColorRBMin", 1, NULL, 0 }, + { "checkColorRBFThreshold", 1, NULL, 0 }, + { "checkColorRBF", 1, NULL, 0 }, + { "ipmWindowClear", 1, NULL, 0 }, + { "ipmWindowLeft", 1, NULL, 0 }, + { "ipmWindowRight", 1, NULL, 0 }, + { "checkLaneWidth", 1, NULL, 0 }, + { "checkLaneWidthMean", 1, NULL, 0 }, + { "checkLaneWidthStd", 1, NULL, 0 }, + { NULL, 0, NULL, 0 } + }; + + stop_char = 0; + c = getopt_long (argc, argv, "hV", long_options, &option_index); + + if (c == -1) break; /* Exit from `while (1)' loop. */ + + switch (c) + { + case 'h': /* Print help and exit. */ + LaneDetectorParser_print_help (); + LaneDetectorParser_free (&local_args_info); + exit (EXIT_SUCCESS); + + case 'V': /* Print version and exit. */ + LaneDetectorParser_print_version (); + LaneDetectorParser_free (&local_args_info); + exit (EXIT_SUCCESS); + + + case 0: /* Long option with no short option */ + /* width of IPM image to use. */ + if (strcmp (long_options[option_index].name, "ipmWidth") == 0) + { + if (local_args_info.ipmWidth_given) + { + fprintf (stderr, "%s: `--ipmWidth' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmWidth_given && ! override) + continue; + local_args_info.ipmWidth_given = 1; + args_info->ipmWidth_given = 1; + args_info->ipmWidth_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmWidth_orig) + free (args_info->ipmWidth_orig); /* free previous string */ + args_info->ipmWidth_orig = gengetopt_strdup (optarg); + } + /* height of IPM image to use. */ + else if (strcmp (long_options[option_index].name, "ipmHeight") == 0) + { + if (local_args_info.ipmHeight_given) + { + fprintf (stderr, "%s: `--ipmHeight' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmHeight_given && ! override) + continue; + local_args_info.ipmHeight_given = 1; + args_info->ipmHeight_given = 1; + args_info->ipmHeight_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmHeight_orig) + free (args_info->ipmHeight_orig); /* free previous string */ + args_info->ipmHeight_orig = gengetopt_strdup (optarg); + } + /* Top point in original image of region to make IPM for. */ + else if (strcmp (long_options[option_index].name, "ipmTop") == 0) + { + if (local_args_info.ipmTop_given) + { + fprintf (stderr, "%s: `--ipmTop' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmTop_given && ! override) + continue; + local_args_info.ipmTop_given = 1; + args_info->ipmTop_given = 1; + args_info->ipmTop_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmTop_orig) + free (args_info->ipmTop_orig); /* free previous string */ + args_info->ipmTop_orig = gengetopt_strdup (optarg); + } + /* Left point in original image of region to make IPM for. */ + else if (strcmp (long_options[option_index].name, "ipmLeft") == 0) + { + if (local_args_info.ipmLeft_given) + { + fprintf (stderr, "%s: `--ipmLeft' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmLeft_given && ! override) + continue; + local_args_info.ipmLeft_given = 1; + args_info->ipmLeft_given = 1; + args_info->ipmLeft_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmLeft_orig) + free (args_info->ipmLeft_orig); /* free previous string */ + args_info->ipmLeft_orig = gengetopt_strdup (optarg); + } + /* Right point in original image region to make IPM for. */ + else if (strcmp (long_options[option_index].name, "ipmRight") == 0) + { + if (local_args_info.ipmRight_given) + { + fprintf (stderr, "%s: `--ipmRight' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmRight_given && ! override) + continue; + local_args_info.ipmRight_given = 1; + args_info->ipmRight_given = 1; + args_info->ipmRight_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmRight_orig) + free (args_info->ipmRight_orig); /* free previous string */ + args_info->ipmRight_orig = gengetopt_strdup (optarg); + } + /* Bottom point in original image region to make IPM for. */ + else if (strcmp (long_options[option_index].name, "ipmBottom") == 0) + { + if (local_args_info.ipmBottom_given) + { + fprintf (stderr, "%s: `--ipmBottom' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmBottom_given && ! override) + continue; + local_args_info.ipmBottom_given = 1; + args_info->ipmBottom_given = 1; + args_info->ipmBottom_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmBottom_orig) + free (args_info->ipmBottom_orig); /* free previous string */ + args_info->ipmBottom_orig = gengetopt_strdup (optarg); + } + /* The method to use for IPM interpolation. */ + else if (strcmp (long_options[option_index].name, "ipmInterpolation") == 0) + { + if (local_args_info.ipmInterpolation_given) + { + fprintf (stderr, "%s: `--ipmInterpolation' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmInterpolation_given && ! override) + continue; + local_args_info.ipmInterpolation_given = 1; + args_info->ipmInterpolation_given = 1; + args_info->ipmInterpolation_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmInterpolation_orig) + free (args_info->ipmInterpolation_orig); /* free previous string */ + args_info->ipmInterpolation_orig = gengetopt_strdup (optarg); + } + /* width of line to detect in mm (in the world). */ + else if (strcmp (long_options[option_index].name, "lineWidth") == 0) + { + if (local_args_info.lineWidth_given) + { + fprintf (stderr, "%s: `--lineWidth' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->lineWidth_given && ! override) + continue; + local_args_info.lineWidth_given = 1; + args_info->lineWidth_given = 1; + args_info->lineWidth_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->lineWidth_orig) + free (args_info->lineWidth_orig); /* free previous string */ + args_info->lineWidth_orig = gengetopt_strdup (optarg); + } + /* height of line to detect in mm (in the world). */ + else if (strcmp (long_options[option_index].name, "lineHeight") == 0) + { + if (local_args_info.lineHeight_given) + { + fprintf (stderr, "%s: `--lineHeight' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->lineHeight_given && ! override) + continue; + local_args_info.lineHeight_given = 1; + args_info->lineHeight_given = 1; + args_info->lineHeight_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->lineHeight_orig) + free (args_info->lineHeight_orig); /* free previous string */ + args_info->lineHeight_orig = gengetopt_strdup (optarg); + } + /* widht of kernel to use for filtering. */ + else if (strcmp (long_options[option_index].name, "kernelWidth") == 0) + { + if (local_args_info.kernelWidth_given) + { + fprintf (stderr, "%s: `--kernelWidth' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->kernelWidth_given && ! override) + continue; + local_args_info.kernelWidth_given = 1; + args_info->kernelWidth_given = 1; + args_info->kernelWidth_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->kernelWidth_orig) + free (args_info->kernelWidth_orig); /* free previous string */ + args_info->kernelWidth_orig = gengetopt_strdup (optarg); + } + /* Height of kernel to use for filtering. */ + else if (strcmp (long_options[option_index].name, "kernelHeight") == 0) + { + if (local_args_info.kernelHeight_given) + { + fprintf (stderr, "%s: `--kernelHeight' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->kernelHeight_given && ! override) + continue; + local_args_info.kernelHeight_given = 1; + args_info->kernelHeight_given = 1; + args_info->kernelHeight_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->kernelHeight_orig) + free (args_info->kernelHeight_orig); /* free previous string */ + args_info->kernelHeight_orig = gengetopt_strdup (optarg); + } + /* lower quantile to use for thresholding the filtered image. */ + else if (strcmp (long_options[option_index].name, "lowerQuantile") == 0) + { + if (local_args_info.lowerQuantile_given) + { + fprintf (stderr, "%s: `--lowerQuantile' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->lowerQuantile_given && ! override) + continue; + local_args_info.lowerQuantile_given = 1; + args_info->lowerQuantile_given = 1; + args_info->lowerQuantile_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->lowerQuantile_orig) + free (args_info->lowerQuantile_orig); /* free previous string */ + args_info->lowerQuantile_orig = gengetopt_strdup (optarg); + } + /* whether to return local maxima or just the maximum. */ + else if (strcmp (long_options[option_index].name, "localMaxima") == 0) + { + if (local_args_info.localMaxima_given) + { + fprintf (stderr, "%s: `--localMaxima' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->localMaxima_given && ! override) + continue; + local_args_info.localMaxima_given = 1; + args_info->localMaxima_given = 1; + args_info->localMaxima_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->localMaxima_orig) + free (args_info->localMaxima_orig); /* free previous string */ + args_info->localMaxima_orig = gengetopt_strdup (optarg); + } + /* type of grouping to use (default 0: HV lines). */ + else if (strcmp (long_options[option_index].name, "groupingType") == 0) + { + if (local_args_info.groupingType_given) + { + fprintf (stderr, "%s: `--groupingType' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->groupingType_given && ! override) + continue; + local_args_info.groupingType_given = 1; + args_info->groupingType_given = 1; + args_info->groupingType_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->groupingType_orig) + free (args_info->groupingType_orig); /* free previous string */ + args_info->groupingType_orig = gengetopt_strdup (optarg); + } + /* whether to binarize the thresholded image or use the raw values. */ + else if (strcmp (long_options[option_index].name, "binarize") == 0) + { + if (local_args_info.binarize_given) + { + fprintf (stderr, "%s: `--binarize' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->binarize_given && ! override) + continue; + local_args_info.binarize_given = 1; + args_info->binarize_given = 1; + args_info->binarize_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->binarize_orig) + free (args_info->binarize_orig); /* free previous string */ + args_info->binarize_orig = gengetopt_strdup (optarg); + } + /* threshold for line scores to declare as line. */ + else if (strcmp (long_options[option_index].name, "detectionThreshold") == 0) + { + if (local_args_info.detectionThreshold_given) + { + fprintf (stderr, "%s: `--detectionThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->detectionThreshold_given && ! override) + continue; + local_args_info.detectionThreshold_given = 1; + args_info->detectionThreshold_given = 1; + args_info->detectionThreshold_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->detectionThreshold_orig) + free (args_info->detectionThreshold_orig); /* free previous string */ + args_info->detectionThreshold_orig = gengetopt_strdup (optarg); + } + /* whether to smooth scores of lines detected or not. */ + else if (strcmp (long_options[option_index].name, "smoothScores") == 0) + { + if (local_args_info.smoothScores_given) + { + fprintf (stderr, "%s: `--smoothScores' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->smoothScores_given && ! override) + continue; + local_args_info.smoothScores_given = 1; + args_info->smoothScores_given = 1; + args_info->smoothScores_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->smoothScores_orig) + free (args_info->smoothScores_orig); /* free previous string */ + args_info->smoothScores_orig = gengetopt_strdup (optarg); + } + /* rMin for Hough transform (in pixels). */ + else if (strcmp (long_options[option_index].name, "rMin") == 0) + { + if (local_args_info.rMin_given) + { + fprintf (stderr, "%s: `--rMin' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->rMin_given && ! override) + continue; + local_args_info.rMin_given = 1; + args_info->rMin_given = 1; + args_info->rMin_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->rMin_orig) + free (args_info->rMin_orig); /* free previous string */ + args_info->rMin_orig = gengetopt_strdup (optarg); + } + /* rMax for Hough transform (in pixels). */ + else if (strcmp (long_options[option_index].name, "rMax") == 0) + { + if (local_args_info.rMax_given) + { + fprintf (stderr, "%s: `--rMax' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->rMax_given && ! override) + continue; + local_args_info.rMax_given = 1; + args_info->rMax_given = 1; + args_info->rMax_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->rMax_orig) + free (args_info->rMax_orig); /* free previous string */ + args_info->rMax_orig = gengetopt_strdup (optarg); + } + /* rStep for Hough transform (in pixels). */ + else if (strcmp (long_options[option_index].name, "rStep") == 0) + { + if (local_args_info.rStep_given) + { + fprintf (stderr, "%s: `--rStep' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->rStep_given && ! override) + continue; + local_args_info.rStep_given = 1; + args_info->rStep_given = 1; + args_info->rStep_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->rStep_orig) + free (args_info->rStep_orig); /* free previous string */ + args_info->rStep_orig = gengetopt_strdup (optarg); + } + /* thetaMin for Hough transform (in degrees). */ + else if (strcmp (long_options[option_index].name, "thetaMin") == 0) + { + if (local_args_info.thetaMin_given) + { + fprintf (stderr, "%s: `--thetaMin' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->thetaMin_given && ! override) + continue; + local_args_info.thetaMin_given = 1; + args_info->thetaMin_given = 1; + args_info->thetaMin_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->thetaMin_orig) + free (args_info->thetaMin_orig); /* free previous string */ + args_info->thetaMin_orig = gengetopt_strdup (optarg); + } + /* thetaMax for Hough transform (in degrees). */ + else if (strcmp (long_options[option_index].name, "thetaMax") == 0) + { + if (local_args_info.thetaMax_given) + { + fprintf (stderr, "%s: `--thetaMax' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->thetaMax_given && ! override) + continue; + local_args_info.thetaMax_given = 1; + args_info->thetaMax_given = 1; + args_info->thetaMax_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->thetaMax_orig) + free (args_info->thetaMax_orig); /* free previous string */ + args_info->thetaMax_orig = gengetopt_strdup (optarg); + } + /* thetaStep for Hough transform (in degrees). */ + else if (strcmp (long_options[option_index].name, "thetaStep") == 0) + { + if (local_args_info.thetaStep_given) + { + fprintf (stderr, "%s: `--thetaStep' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->thetaStep_given && ! override) + continue; + local_args_info.thetaStep_given = 1; + args_info->thetaStep_given = 1; + args_info->thetaStep_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->thetaStep_orig) + free (args_info->thetaStep_orig); /* free previous string */ + args_info->thetaStep_orig = gengetopt_strdup (optarg); + } + /* Portion of IPM image height to add to y-coordinate of VP. */ + else if (strcmp (long_options[option_index].name, "ipmVpPortion") == 0) + { + if (local_args_info.ipmVpPortion_given) + { + fprintf (stderr, "%s: `--ipmVpPortion' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmVpPortion_given && ! override) + continue; + local_args_info.ipmVpPortion_given = 1; + args_info->ipmVpPortion_given = 1; + args_info->ipmVpPortion_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmVpPortion_orig) + free (args_info->ipmVpPortion_orig); /* free previous string */ + args_info->ipmVpPortion_orig = gengetopt_strdup (optarg); + } + /* Get the endpoints of the line. */ + else if (strcmp (long_options[option_index].name, "getEndPoints") == 0) + { + if (local_args_info.getEndPoints_given) + { + fprintf (stderr, "%s: `--getEndPoints' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->getEndPoints_given && ! override) + continue; + local_args_info.getEndPoints_given = 1; + args_info->getEndPoints_given = 1; + args_info->getEndPoints_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->getEndPoints_orig) + free (args_info->getEndPoints_orig); /* free previous string */ + args_info->getEndPoints_orig = gengetopt_strdup (optarg); + } + /* group nearby lines or not (default 1: group). */ + else if (strcmp (long_options[option_index].name, "group") == 0) + { + if (local_args_info.group_given) + { + fprintf (stderr, "%s: `--group' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->group_given && ! override) + continue; + local_args_info.group_given = 1; + args_info->group_given = 1; + args_info->group_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->group_orig) + free (args_info->group_orig); /* free previous string */ + args_info->group_orig = gengetopt_strdup (optarg); + } + /* Threshold for grouping nearby lines (default 10). */ + else if (strcmp (long_options[option_index].name, "groupThreshold") == 0) + { + if (local_args_info.groupThreshold_given) + { + fprintf (stderr, "%s: `--groupThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->groupThreshold_given && ! override) + continue; + local_args_info.groupThreshold_given = 1; + args_info->groupThreshold_given = 1; + args_info->groupThreshold_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->groupThreshold_orig) + free (args_info->groupThreshold_orig); /* free previous string */ + args_info->groupThreshold_orig = gengetopt_strdup (optarg); + } + /* use RANSAC (1) or not (0). */ + else if (strcmp (long_options[option_index].name, "ransac") == 0) + { + if (local_args_info.ransac_given) + { + fprintf (stderr, "%s: `--ransac' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransac_given && ! override) + continue; + local_args_info.ransac_given = 1; + args_info->ransac_given = 1; + args_info->ransac_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransac_orig) + free (args_info->ransac_orig); /* free previous string */ + args_info->ransac_orig = gengetopt_strdup (optarg); + } + /* Number of samples to use for RANSAC. */ + else if (strcmp (long_options[option_index].name, "ransacLineNumSamples") == 0) + { + if (local_args_info.ransacLineNumSamples_given) + { + fprintf (stderr, "%s: `--ransacLineNumSamples' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacLineNumSamples_given && ! override) + continue; + local_args_info.ransacLineNumSamples_given = 1; + args_info->ransacLineNumSamples_given = 1; + args_info->ransacLineNumSamples_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacLineNumSamples_orig) + free (args_info->ransacLineNumSamples_orig); /* free previous string */ + args_info->ransacLineNumSamples_orig = gengetopt_strdup (optarg); + } + /* Number of iterations to use for RANSAC. */ + else if (strcmp (long_options[option_index].name, "ransacLineNumIterations") == 0) + { + if (local_args_info.ransacLineNumIterations_given) + { + fprintf (stderr, "%s: `--ransacLineNumIterations' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacLineNumIterations_given && ! override) + continue; + local_args_info.ransacLineNumIterations_given = 1; + args_info->ransacLineNumIterations_given = 1; + args_info->ransacLineNumIterations_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacLineNumIterations_orig) + free (args_info->ransacLineNumIterations_orig); /* free previous string */ + args_info->ransacLineNumIterations_orig = gengetopt_strdup (optarg); + } + /* Number of close points to consider a good line fit. */ + else if (strcmp (long_options[option_index].name, "ransacLineNumGoodFit") == 0) + { + if (local_args_info.ransacLineNumGoodFit_given) + { + fprintf (stderr, "%s: `--ransacLineNumGoodFit' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacLineNumGoodFit_given && ! override) + continue; + local_args_info.ransacLineNumGoodFit_given = 1; + args_info->ransacLineNumGoodFit_given = 1; + args_info->ransacLineNumGoodFit_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacLineNumGoodFit_orig) + free (args_info->ransacLineNumGoodFit_orig); /* free previous string */ + args_info->ransacLineNumGoodFit_orig = gengetopt_strdup (optarg); + } + /* Threshold to consider a point close. */ + else if (strcmp (long_options[option_index].name, "ransacLineThreshold") == 0) + { + if (local_args_info.ransacLineThreshold_given) + { + fprintf (stderr, "%s: `--ransacLineThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacLineThreshold_given && ! override) + continue; + local_args_info.ransacLineThreshold_given = 1; + args_info->ransacLineThreshold_given = 1; + args_info->ransacLineThreshold_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacLineThreshold_orig) + free (args_info->ransacLineThreshold_orig); /* free previous string */ + args_info->ransacLineThreshold_orig = gengetopt_strdup (optarg); + } + /* Threshold for detected line scores. */ + else if (strcmp (long_options[option_index].name, "ransacLineScoreThreshold") == 0) + { + if (local_args_info.ransacLineScoreThreshold_given) + { + fprintf (stderr, "%s: `--ransacLineScoreThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacLineScoreThreshold_given && ! override) + continue; + local_args_info.ransacLineScoreThreshold_given = 1; + args_info->ransacLineScoreThreshold_given = 1; + args_info->ransacLineScoreThreshold_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacLineScoreThreshold_orig) + free (args_info->ransacLineScoreThreshold_orig); /* free previous string */ + args_info->ransacLineScoreThreshold_orig = gengetopt_strdup (optarg); + } + /* Whether to binarize image for RANSAC or not. */ + else if (strcmp (long_options[option_index].name, "ransacLineBinarize") == 0) + { + if (local_args_info.ransacLineBinarize_given) + { + fprintf (stderr, "%s: `--ransacLineBinarize' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacLineBinarize_given && ! override) + continue; + local_args_info.ransacLineBinarize_given = 1; + args_info->ransacLineBinarize_given = 1; + args_info->ransacLineBinarize_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacLineBinarize_orig) + free (args_info->ransacLineBinarize_orig); /* free previous string */ + args_info->ransacLineBinarize_orig = gengetopt_strdup (optarg); + } + /* Half width to use for ransac window. */ + else if (strcmp (long_options[option_index].name, "ransacLineWindow") == 0) + { + if (local_args_info.ransacLineWindow_given) + { + fprintf (stderr, "%s: `--ransacLineWindow' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacLineWindow_given && ! override) + continue; + local_args_info.ransacLineWindow_given = 1; + args_info->ransacLineWindow_given = 1; + args_info->ransacLineWindow_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacLineWindow_orig) + free (args_info->ransacLineWindow_orig); /* free previous string */ + args_info->ransacLineWindow_orig = gengetopt_strdup (optarg); + } + /* Number of samples to use for RANSAC. */ + else if (strcmp (long_options[option_index].name, "ransacSplineNumSamples") == 0) + { + if (local_args_info.ransacSplineNumSamples_given) + { + fprintf (stderr, "%s: `--ransacSplineNumSamples' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacSplineNumSamples_given && ! override) + continue; + local_args_info.ransacSplineNumSamples_given = 1; + args_info->ransacSplineNumSamples_given = 1; + args_info->ransacSplineNumSamples_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacSplineNumSamples_orig) + free (args_info->ransacSplineNumSamples_orig); /* free previous string */ + args_info->ransacSplineNumSamples_orig = gengetopt_strdup (optarg); + } + /* Number of iterations to use for RANSAC. */ + else if (strcmp (long_options[option_index].name, "ransacSplineNumIterations") == 0) + { + if (local_args_info.ransacSplineNumIterations_given) + { + fprintf (stderr, "%s: `--ransacSplineNumIterations' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacSplineNumIterations_given && ! override) + continue; + local_args_info.ransacSplineNumIterations_given = 1; + args_info->ransacSplineNumIterations_given = 1; + args_info->ransacSplineNumIterations_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacSplineNumIterations_orig) + free (args_info->ransacSplineNumIterations_orig); /* free previous string */ + args_info->ransacSplineNumIterations_orig = gengetopt_strdup (optarg); + } + /* Number of close points to consider a good line fit. */ + else if (strcmp (long_options[option_index].name, "ransacSplineNumGoodFit") == 0) + { + if (local_args_info.ransacSplineNumGoodFit_given) + { + fprintf (stderr, "%s: `--ransacSplineNumGoodFit' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacSplineNumGoodFit_given && ! override) + continue; + local_args_info.ransacSplineNumGoodFit_given = 1; + args_info->ransacSplineNumGoodFit_given = 1; + args_info->ransacSplineNumGoodFit_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacSplineNumGoodFit_orig) + free (args_info->ransacSplineNumGoodFit_orig); /* free previous string */ + args_info->ransacSplineNumGoodFit_orig = gengetopt_strdup (optarg); + } + /* Threshold to consider a point close. */ + else if (strcmp (long_options[option_index].name, "ransacSplineThreshold") == 0) + { + if (local_args_info.ransacSplineThreshold_given) + { + fprintf (stderr, "%s: `--ransacSplineThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacSplineThreshold_given && ! override) + continue; + local_args_info.ransacSplineThreshold_given = 1; + args_info->ransacSplineThreshold_given = 1; + args_info->ransacSplineThreshold_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacSplineThreshold_orig) + free (args_info->ransacSplineThreshold_orig); /* free previous string */ + args_info->ransacSplineThreshold_orig = gengetopt_strdup (optarg); + } + /* Threshold for detected line scores. */ + else if (strcmp (long_options[option_index].name, "ransacSplineScoreThreshold") == 0) + { + if (local_args_info.ransacSplineScoreThreshold_given) + { + fprintf (stderr, "%s: `--ransacSplineScoreThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacSplineScoreThreshold_given && ! override) + continue; + local_args_info.ransacSplineScoreThreshold_given = 1; + args_info->ransacSplineScoreThreshold_given = 1; + args_info->ransacSplineScoreThreshold_arg = strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacSplineScoreThreshold_orig) + free (args_info->ransacSplineScoreThreshold_orig); /* free previous string */ + args_info->ransacSplineScoreThreshold_orig = gengetopt_strdup (optarg); + } + /* Whether to binarize image for RANSAC or not. */ + else if (strcmp (long_options[option_index].name, "ransacSplineBinarize") == 0) + { + if (local_args_info.ransacSplineBinarize_given) + { + fprintf (stderr, "%s: `--ransacSplineBinarize' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacSplineBinarize_given && ! override) + continue; + local_args_info.ransacSplineBinarize_given = 1; + args_info->ransacSplineBinarize_given = 1; + args_info->ransacSplineBinarize_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacSplineBinarize_orig) + free (args_info->ransacSplineBinarize_orig); /* free previous string */ + args_info->ransacSplineBinarize_orig = gengetopt_strdup (optarg); + } + /* Half width to use for ransac window. */ + else if (strcmp (long_options[option_index].name, "ransacSplineWindow") == 0) + { + if (local_args_info.ransacSplineWindow_given) + { + fprintf (stderr, "%s: `--ransacSplineWindow' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacSplineWindow_given && ! override) + continue; + local_args_info.ransacSplineWindow_given = 1; + args_info->ransacSplineWindow_given = 1; + args_info->ransacSplineWindow_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacSplineWindow_orig) + free (args_info->ransacSplineWindow_orig); /* free previous string */ + args_info->ransacSplineWindow_orig = gengetopt_strdup (optarg); + } + /* Degree of spline to use. */ + else if (strcmp (long_options[option_index].name, "ransacSplineDegree") == 0) + { + if (local_args_info.ransacSplineDegree_given) + { + fprintf (stderr, "%s: `--ransacSplineDegree' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacSplineDegree_given && ! override) + continue; + local_args_info.ransacSplineDegree_given = 1; + args_info->ransacSplineDegree_given = 1; + args_info->ransacSplineDegree_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacSplineDegree_orig) + free (args_info->ransacSplineDegree_orig); /* free previous string */ + args_info->ransacSplineDegree_orig = gengetopt_strdup (optarg); + } + /* Whether to use splines. */ + else if (strcmp (long_options[option_index].name, "ransacSpline") == 0) + { + if (local_args_info.ransacSpline_given) + { + fprintf (stderr, "%s: `--ransacSpline' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacSpline_given && ! override) + continue; + local_args_info.ransacSpline_given = 1; + args_info->ransacSpline_given = 1; + args_info->ransacSpline_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacSpline_orig) + free (args_info->ransacSpline_orig); /* free previous string */ + args_info->ransacSpline_orig = gengetopt_strdup (optarg); + } + /* Whether to use lines. */ + else if (strcmp (long_options[option_index].name, "ransacLine") == 0) + { + if (local_args_info.ransacLine_given) + { + fprintf (stderr, "%s: `--ransacLine' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacLine_given && ! override) + continue; + local_args_info.ransacLine_given = 1; + args_info->ransacLine_given = 1; + args_info->ransacLine_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacLine_orig) + free (args_info->ransacLine_orig); /* free previous string */ + args_info->ransacLine_orig = gengetopt_strdup (optarg); + } + /* Step to use when pixelzing spline in ransac. */ + else if (strcmp (long_options[option_index].name, "ransacSplineStep") == 0) + { + if (local_args_info.ransacSplineStep_given) + { + fprintf (stderr, "%s: `--ransacSplineStep' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ransacSplineStep_given && ! override) + continue; + local_args_info.ransacSplineStep_given = 1; + args_info->ransacSplineStep_given = 1; + args_info->ransacSplineStep_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ransacSplineStep_orig) + free (args_info->ransacSplineStep_orig); /* free previous string */ + args_info->ransacSplineStep_orig = gengetopt_strdup (optarg); + } + /* Overlap threshold to use for grouping of bounding boxes. */ + else if (strcmp (long_options[option_index].name, "overlapThreshold") == 0) + { + if (local_args_info.overlapThreshold_given) + { + fprintf (stderr, "%s: `--overlapThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->overlapThreshold_given && ! override) + continue; + local_args_info.overlapThreshold_given = 1; + args_info->overlapThreshold_given = 1; + args_info->overlapThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->overlapThreshold_orig) + free (args_info->overlapThreshold_orig); /* free previous string */ + args_info->overlapThreshold_orig = gengetopt_strdup (optarg); + } + /* Angle threshold used for localization (cosine, 1: most restrictive, 0: most liberal). */ + else if (strcmp (long_options[option_index].name, "localizeAngleThreshold") == 0) + { + if (local_args_info.localizeAngleThreshold_given) + { + fprintf (stderr, "%s: `--localizeAngleThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->localizeAngleThreshold_given && ! override) + continue; + local_args_info.localizeAngleThreshold_given = 1; + args_info->localizeAngleThreshold_given = 1; + args_info->localizeAngleThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->localizeAngleThreshold_orig) + free (args_info->localizeAngleThreshold_orig); /* free previous string */ + args_info->localizeAngleThreshold_orig = gengetopt_strdup (optarg); + } + /* Number of pixels to go in normal direction for localization. */ + else if (strcmp (long_options[option_index].name, "localizeNumLinePixels") == 0) + { + if (local_args_info.localizeNumLinePixels_given) + { + fprintf (stderr, "%s: `--localizeNumLinePixels' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->localizeNumLinePixels_given && ! override) + continue; + local_args_info.localizeNumLinePixels_given = 1; + args_info->localizeNumLinePixels_given = 1; + args_info->localizeNumLinePixels_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->localizeNumLinePixels_orig) + free (args_info->localizeNumLinePixels_orig); /* free previous string */ + args_info->localizeNumLinePixels_orig = gengetopt_strdup (optarg); + } + /* Angle threshold used for extending (cosine, 1: most restrictive, 0: most liberal). */ + else if (strcmp (long_options[option_index].name, "extendAngleThreshold") == 0) + { + if (local_args_info.extendAngleThreshold_given) + { + fprintf (stderr, "%s: `--extendAngleThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendAngleThreshold_given && ! override) + continue; + local_args_info.extendAngleThreshold_given = 1; + args_info->extendAngleThreshold_given = 1; + args_info->extendAngleThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendAngleThreshold_orig) + free (args_info->extendAngleThreshold_orig); /* free previous string */ + args_info->extendAngleThreshold_orig = gengetopt_strdup (optarg); + } + /* Angle threshold from mean direction used for extending (cosine, 1: most restrictive, 0: most liberal). */ + else if (strcmp (long_options[option_index].name, "extendMeanDirAngleThreshold") == 0) + { + if (local_args_info.extendMeanDirAngleThreshold_given) + { + fprintf (stderr, "%s: `--extendMeanDirAngleThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendMeanDirAngleThreshold_given && ! override) + continue; + local_args_info.extendMeanDirAngleThreshold_given = 1; + args_info->extendMeanDirAngleThreshold_given = 1; + args_info->extendMeanDirAngleThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendMeanDirAngleThreshold_orig) + free (args_info->extendMeanDirAngleThreshold_orig); /* free previous string */ + args_info->extendMeanDirAngleThreshold_orig = gengetopt_strdup (optarg); + } + /* Number of pixels to go in tangent direction for extending. */ + else if (strcmp (long_options[option_index].name, "extendLinePixelsTangent") == 0) + { + if (local_args_info.extendLinePixelsTangent_given) + { + fprintf (stderr, "%s: `--extendLinePixelsTangent' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendLinePixelsTangent_given && ! override) + continue; + local_args_info.extendLinePixelsTangent_given = 1; + args_info->extendLinePixelsTangent_given = 1; + args_info->extendLinePixelsTangent_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendLinePixelsTangent_orig) + free (args_info->extendLinePixelsTangent_orig); /* free previous string */ + args_info->extendLinePixelsTangent_orig = gengetopt_strdup (optarg); + } + /* Number of pixels to go in tangent direction for extending. */ + else if (strcmp (long_options[option_index].name, "extendLinePixelsNormal") == 0) + { + if (local_args_info.extendLinePixelsNormal_given) + { + fprintf (stderr, "%s: `--extendLinePixelsNormal' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendLinePixelsNormal_given && ! override) + continue; + local_args_info.extendLinePixelsNormal_given = 1; + args_info->extendLinePixelsNormal_given = 1; + args_info->extendLinePixelsNormal_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendLinePixelsNormal_orig) + free (args_info->extendLinePixelsNormal_orig); /* free previous string */ + args_info->extendLinePixelsNormal_orig = gengetopt_strdup (optarg); + } + /* Threhsold used for stopping the extending process (higher -> less extending). */ + else if (strcmp (long_options[option_index].name, "extendContThreshold") == 0) + { + if (local_args_info.extendContThreshold_given) + { + fprintf (stderr, "%s: `--extendContThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendContThreshold_given && ! override) + continue; + local_args_info.extendContThreshold_given = 1; + args_info->extendContThreshold_given = 1; + args_info->extendContThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendContThreshold_orig) + free (args_info->extendContThreshold_orig); /* free previous string */ + args_info->extendContThreshold_orig = gengetopt_strdup (optarg); + } + /* Stop extending when number of deviating points exceeds this threshold. */ + else if (strcmp (long_options[option_index].name, "extendDeviationThreshold") == 0) + { + if (local_args_info.extendDeviationThreshold_given) + { + fprintf (stderr, "%s: `--extendDeviationThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendDeviationThreshold_given && ! override) + continue; + local_args_info.extendDeviationThreshold_given = 1; + args_info->extendDeviationThreshold_given = 1; + args_info->extendDeviationThreshold_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendDeviationThreshold_orig) + free (args_info->extendDeviationThreshold_orig); /* free previous string */ + args_info->extendDeviationThreshold_orig = gengetopt_strdup (optarg); + } + /* Top point for extension bounding box. */ + else if (strcmp (long_options[option_index].name, "extendRectTop") == 0) + { + if (local_args_info.extendRectTop_given) + { + fprintf (stderr, "%s: `--extendRectTop' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendRectTop_given && ! override) + continue; + local_args_info.extendRectTop_given = 1; + args_info->extendRectTop_given = 1; + args_info->extendRectTop_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendRectTop_orig) + free (args_info->extendRectTop_orig); /* free previous string */ + args_info->extendRectTop_orig = gengetopt_strdup (optarg); + } + /* Bottom point for extension bounding box. */ + else if (strcmp (long_options[option_index].name, "extendRectBottom") == 0) + { + if (local_args_info.extendRectBottom_given) + { + fprintf (stderr, "%s: `--extendRectBottom' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendRectBottom_given && ! override) + continue; + local_args_info.extendRectBottom_given = 1; + args_info->extendRectBottom_given = 1; + args_info->extendRectBottom_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendRectBottom_orig) + free (args_info->extendRectBottom_orig); /* free previous string */ + args_info->extendRectBottom_orig = gengetopt_strdup (optarg); + } + /* Angle threshold used for extending (cosine, 1: most restrictive, 0: most liberal). */ + else if (strcmp (long_options[option_index].name, "extendIPMAngleThreshold") == 0) + { + if (local_args_info.extendIPMAngleThreshold_given) + { + fprintf (stderr, "%s: `--extendIPMAngleThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendIPMAngleThreshold_given && ! override) + continue; + local_args_info.extendIPMAngleThreshold_given = 1; + args_info->extendIPMAngleThreshold_given = 1; + args_info->extendIPMAngleThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendIPMAngleThreshold_orig) + free (args_info->extendIPMAngleThreshold_orig); /* free previous string */ + args_info->extendIPMAngleThreshold_orig = gengetopt_strdup (optarg); + } + /* Angle threshold from mean direction used for extending (cosine, 1: most restrictive, 0: most liberal). */ + else if (strcmp (long_options[option_index].name, "extendIPMMeanDirAngleThreshold") == 0) + { + if (local_args_info.extendIPMMeanDirAngleThreshold_given) + { + fprintf (stderr, "%s: `--extendIPMMeanDirAngleThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendIPMMeanDirAngleThreshold_given && ! override) + continue; + local_args_info.extendIPMMeanDirAngleThreshold_given = 1; + args_info->extendIPMMeanDirAngleThreshold_given = 1; + args_info->extendIPMMeanDirAngleThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendIPMMeanDirAngleThreshold_orig) + free (args_info->extendIPMMeanDirAngleThreshold_orig); /* free previous string */ + args_info->extendIPMMeanDirAngleThreshold_orig = gengetopt_strdup (optarg); + } + /* Number of pixels to go in tangent direction for extending. */ + else if (strcmp (long_options[option_index].name, "extendIPMLinePixelsTangent") == 0) + { + if (local_args_info.extendIPMLinePixelsTangent_given) + { + fprintf (stderr, "%s: `--extendIPMLinePixelsTangent' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendIPMLinePixelsTangent_given && ! override) + continue; + local_args_info.extendIPMLinePixelsTangent_given = 1; + args_info->extendIPMLinePixelsTangent_given = 1; + args_info->extendIPMLinePixelsTangent_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendIPMLinePixelsTangent_orig) + free (args_info->extendIPMLinePixelsTangent_orig); /* free previous string */ + args_info->extendIPMLinePixelsTangent_orig = gengetopt_strdup (optarg); + } + /* Number of pixels to go in tangent direction for extending. */ + else if (strcmp (long_options[option_index].name, "extendIPMLinePixelsNormal") == 0) + { + if (local_args_info.extendIPMLinePixelsNormal_given) + { + fprintf (stderr, "%s: `--extendIPMLinePixelsNormal' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendIPMLinePixelsNormal_given && ! override) + continue; + local_args_info.extendIPMLinePixelsNormal_given = 1; + args_info->extendIPMLinePixelsNormal_given = 1; + args_info->extendIPMLinePixelsNormal_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendIPMLinePixelsNormal_orig) + free (args_info->extendIPMLinePixelsNormal_orig); /* free previous string */ + args_info->extendIPMLinePixelsNormal_orig = gengetopt_strdup (optarg); + } + /* Threhsold used for stopping the extending process (higher -> less extending). */ + else if (strcmp (long_options[option_index].name, "extendIPMContThreshold") == 0) + { + if (local_args_info.extendIPMContThreshold_given) + { + fprintf (stderr, "%s: `--extendIPMContThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendIPMContThreshold_given && ! override) + continue; + local_args_info.extendIPMContThreshold_given = 1; + args_info->extendIPMContThreshold_given = 1; + args_info->extendIPMContThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendIPMContThreshold_orig) + free (args_info->extendIPMContThreshold_orig); /* free previous string */ + args_info->extendIPMContThreshold_orig = gengetopt_strdup (optarg); + } + /* Stop extending when number of deviating points exceeds this threshold. */ + else if (strcmp (long_options[option_index].name, "extendIPMDeviationThreshold") == 0) + { + if (local_args_info.extendIPMDeviationThreshold_given) + { + fprintf (stderr, "%s: `--extendIPMDeviationThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendIPMDeviationThreshold_given && ! override) + continue; + local_args_info.extendIPMDeviationThreshold_given = 1; + args_info->extendIPMDeviationThreshold_given = 1; + args_info->extendIPMDeviationThreshold_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendIPMDeviationThreshold_orig) + free (args_info->extendIPMDeviationThreshold_orig); /* free previous string */ + args_info->extendIPMDeviationThreshold_orig = gengetopt_strdup (optarg); + } + /* Top point for extension bounding box. */ + else if (strcmp (long_options[option_index].name, "extendIPMRectTop") == 0) + { + if (local_args_info.extendIPMRectTop_given) + { + fprintf (stderr, "%s: `--extendIPMRectTop' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendIPMRectTop_given && ! override) + continue; + local_args_info.extendIPMRectTop_given = 1; + args_info->extendIPMRectTop_given = 1; + args_info->extendIPMRectTop_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendIPMRectTop_orig) + free (args_info->extendIPMRectTop_orig); /* free previous string */ + args_info->extendIPMRectTop_orig = gengetopt_strdup (optarg); + } + /* Bottom point for extension bounding box. */ + else if (strcmp (long_options[option_index].name, "extendIPMRectBottom") == 0) + { + if (local_args_info.extendIPMRectBottom_given) + { + fprintf (stderr, "%s: `--extendIPMRectBottom' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->extendIPMRectBottom_given && ! override) + continue; + local_args_info.extendIPMRectBottom_given = 1; + args_info->extendIPMRectBottom_given = 1; + args_info->extendIPMRectBottom_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->extendIPMRectBottom_orig) + free (args_info->extendIPMRectBottom_orig); /* free previous string */ + args_info->extendIPMRectBottom_orig = gengetopt_strdup (optarg); + } + /* Number of pixels to go around the spline to compute score. */ + else if (strcmp (long_options[option_index].name, "splineScoreJitter") == 0) + { + if (local_args_info.splineScoreJitter_given) + { + fprintf (stderr, "%s: `--splineScoreJitter' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->splineScoreJitter_given && ! override) + continue; + local_args_info.splineScoreJitter_given = 1; + args_info->splineScoreJitter_given = 1; + args_info->splineScoreJitter_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->splineScoreJitter_orig) + free (args_info->splineScoreJitter_orig); /* free previous string */ + args_info->splineScoreJitter_orig = gengetopt_strdup (optarg); + } + /* Ratio of spline length to use. */ + else if (strcmp (long_options[option_index].name, "splineScoreLengthRatio") == 0) + { + if (local_args_info.splineScoreLengthRatio_given) + { + fprintf (stderr, "%s: `--splineScoreLengthRatio' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->splineScoreLengthRatio_given && ! override) + continue; + local_args_info.splineScoreLengthRatio_given = 1; + args_info->splineScoreLengthRatio_given = 1; + args_info->splineScoreLengthRatio_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->splineScoreLengthRatio_orig) + free (args_info->splineScoreLengthRatio_orig); /* free previous string */ + args_info->splineScoreLengthRatio_orig = gengetopt_strdup (optarg); + } + /* Ratio of spline angle to use. */ + else if (strcmp (long_options[option_index].name, "splineScoreAngleRatio") == 0) + { + if (local_args_info.splineScoreAngleRatio_given) + { + fprintf (stderr, "%s: `--splineScoreAngleRatio' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->splineScoreAngleRatio_given && ! override) + continue; + local_args_info.splineScoreAngleRatio_given = 1; + args_info->splineScoreAngleRatio_given = 1; + args_info->splineScoreAngleRatio_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->splineScoreAngleRatio_orig) + free (args_info->splineScoreAngleRatio_orig); /* free previous string */ + args_info->splineScoreAngleRatio_orig = gengetopt_strdup (optarg); + } + /* Step to use for spline score computation. */ + else if (strcmp (long_options[option_index].name, "splineScoreStep") == 0) + { + if (local_args_info.splineScoreStep_given) + { + fprintf (stderr, "%s: `--splineScoreStep' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->splineScoreStep_given && ! override) + continue; + local_args_info.splineScoreStep_given = 1; + args_info->splineScoreStep_given = 1; + args_info->splineScoreStep_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->splineScoreStep_orig) + free (args_info->splineScoreStep_orig); /* free previous string */ + args_info->splineScoreStep_orig = gengetopt_strdup (optarg); + } + /* number of frames the track is allowed to be absent before deleting it. */ + else if (strcmp (long_options[option_index].name, "splineTrackingNumAbsentFrames") == 0) + { + if (local_args_info.splineTrackingNumAbsentFrames_given) + { + fprintf (stderr, "%s: `--splineTrackingNumAbsentFrames' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->splineTrackingNumAbsentFrames_given && ! override) + continue; + local_args_info.splineTrackingNumAbsentFrames_given = 1; + args_info->splineTrackingNumAbsentFrames_given = 1; + args_info->splineTrackingNumAbsentFrames_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->splineTrackingNumAbsentFrames_orig) + free (args_info->splineTrackingNumAbsentFrames_orig); /* free previous string */ + args_info->splineTrackingNumAbsentFrames_orig = gengetopt_strdup (optarg); + } + /* number of frames before considering the track good. */ + else if (strcmp (long_options[option_index].name, "splineTrackingNumSeenFrames") == 0) + { + if (local_args_info.splineTrackingNumSeenFrames_given) + { + fprintf (stderr, "%s: `--splineTrackingNumSeenFrames' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->splineTrackingNumSeenFrames_given && ! override) + continue; + local_args_info.splineTrackingNumSeenFrames_given = 1; + args_info->splineTrackingNumSeenFrames_given = 1; + args_info->splineTrackingNumSeenFrames_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->splineTrackingNumSeenFrames_orig) + free (args_info->splineTrackingNumSeenFrames_orig); /* free previous string */ + args_info->splineTrackingNumSeenFrames_orig = gengetopt_strdup (optarg); + } + /* Angle threshold for merging splines (radians). */ + else if (strcmp (long_options[option_index].name, "mergeSplineThetaThreshold") == 0) + { + if (local_args_info.mergeSplineThetaThreshold_given) + { + fprintf (stderr, "%s: `--mergeSplineThetaThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->mergeSplineThetaThreshold_given && ! override) + continue; + local_args_info.mergeSplineThetaThreshold_given = 1; + args_info->mergeSplineThetaThreshold_given = 1; + args_info->mergeSplineThetaThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->mergeSplineThetaThreshold_orig) + free (args_info->mergeSplineThetaThreshold_orig); /* free previous string */ + args_info->mergeSplineThetaThreshold_orig = gengetopt_strdup (optarg); + } + /* R threshold (distance from origin) for merginn splines. */ + else if (strcmp (long_options[option_index].name, "mergeSplineRThreshold") == 0) + { + if (local_args_info.mergeSplineRThreshold_given) + { + fprintf (stderr, "%s: `--mergeSplineRThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->mergeSplineRThreshold_given && ! override) + continue; + local_args_info.mergeSplineRThreshold_given = 1; + args_info->mergeSplineRThreshold_given = 1; + args_info->mergeSplineRThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->mergeSplineRThreshold_orig) + free (args_info->mergeSplineRThreshold_orig); /* free previous string */ + args_info->mergeSplineRThreshold_orig = gengetopt_strdup (optarg); + } + /* Mean Angle threshold for merging splines (radians). */ + else if (strcmp (long_options[option_index].name, "mergeSplineMeanThetaThreshold") == 0) + { + if (local_args_info.mergeSplineMeanThetaThreshold_given) + { + fprintf (stderr, "%s: `--mergeSplineMeanThetaThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->mergeSplineMeanThetaThreshold_given && ! override) + continue; + local_args_info.mergeSplineMeanThetaThreshold_given = 1; + args_info->mergeSplineMeanThetaThreshold_given = 1; + args_info->mergeSplineMeanThetaThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->mergeSplineMeanThetaThreshold_orig) + free (args_info->mergeSplineMeanThetaThreshold_orig); /* free previous string */ + args_info->mergeSplineMeanThetaThreshold_orig = gengetopt_strdup (optarg); + } + /* Mean R threshold (distance from origin) for merginn splines. */ + else if (strcmp (long_options[option_index].name, "mergeSplineMeanRThreshold") == 0) + { + if (local_args_info.mergeSplineMeanRThreshold_given) + { + fprintf (stderr, "%s: `--mergeSplineMeanRThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->mergeSplineMeanRThreshold_given && ! override) + continue; + local_args_info.mergeSplineMeanRThreshold_given = 1; + args_info->mergeSplineMeanRThreshold_given = 1; + args_info->mergeSplineMeanRThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->mergeSplineMeanRThreshold_orig) + free (args_info->mergeSplineMeanRThreshold_orig); /* free previous string */ + args_info->mergeSplineMeanRThreshold_orig = gengetopt_strdup (optarg); + } + /* Distance threshold between spline cetroids for merging. */ + else if (strcmp (long_options[option_index].name, "mergeSplineCentroidThreshold") == 0) + { + if (local_args_info.mergeSplineCentroidThreshold_given) + { + fprintf (stderr, "%s: `--mergeSplineCentroidThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->mergeSplineCentroidThreshold_given && ! override) + continue; + local_args_info.mergeSplineCentroidThreshold_given = 1; + args_info->mergeSplineCentroidThreshold_given = 1; + args_info->mergeSplineCentroidThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->mergeSplineCentroidThreshold_orig) + free (args_info->mergeSplineCentroidThreshold_orig); /* free previous string */ + args_info->mergeSplineCentroidThreshold_orig = gengetopt_strdup (optarg); + } + /* number of frames the track is allowed to be absent before deleting it. */ + else if (strcmp (long_options[option_index].name, "lineTrackingNumAbsentFrames") == 0) + { + if (local_args_info.lineTrackingNumAbsentFrames_given) + { + fprintf (stderr, "%s: `--lineTrackingNumAbsentFrames' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->lineTrackingNumAbsentFrames_given && ! override) + continue; + local_args_info.lineTrackingNumAbsentFrames_given = 1; + args_info->lineTrackingNumAbsentFrames_given = 1; + args_info->lineTrackingNumAbsentFrames_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->lineTrackingNumAbsentFrames_orig) + free (args_info->lineTrackingNumAbsentFrames_orig); /* free previous string */ + args_info->lineTrackingNumAbsentFrames_orig = gengetopt_strdup (optarg); + } + /* number of frames before considering the track good. */ + else if (strcmp (long_options[option_index].name, "lineTrackingNumSeenFrames") == 0) + { + if (local_args_info.lineTrackingNumSeenFrames_given) + { + fprintf (stderr, "%s: `--lineTrackingNumSeenFrames' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->lineTrackingNumSeenFrames_given && ! override) + continue; + local_args_info.lineTrackingNumSeenFrames_given = 1; + args_info->lineTrackingNumSeenFrames_given = 1; + args_info->lineTrackingNumSeenFrames_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->lineTrackingNumSeenFrames_orig) + free (args_info->lineTrackingNumSeenFrames_orig); /* free previous string */ + args_info->lineTrackingNumSeenFrames_orig = gengetopt_strdup (optarg); + } + /* Angle threshold for merging lines (radians). */ + else if (strcmp (long_options[option_index].name, "mergeLineThetaThreshold") == 0) + { + if (local_args_info.mergeLineThetaThreshold_given) + { + fprintf (stderr, "%s: `--mergeLineThetaThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->mergeLineThetaThreshold_given && ! override) + continue; + local_args_info.mergeLineThetaThreshold_given = 1; + args_info->mergeLineThetaThreshold_given = 1; + args_info->mergeLineThetaThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->mergeLineThetaThreshold_orig) + free (args_info->mergeLineThetaThreshold_orig); /* free previous string */ + args_info->mergeLineThetaThreshold_orig = gengetopt_strdup (optarg); + } + /* R threshold (distance from origin) for merging lines. */ + else if (strcmp (long_options[option_index].name, "mergeLineRThreshold") == 0) + { + if (local_args_info.mergeLineRThreshold_given) + { + fprintf (stderr, "%s: `--mergeLineRThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->mergeLineRThreshold_given && ! override) + continue; + local_args_info.mergeLineRThreshold_given = 1; + args_info->mergeLineRThreshold_given = 1; + args_info->mergeLineRThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->mergeLineRThreshold_orig) + free (args_info->mergeLineRThreshold_orig); /* free previous string */ + args_info->mergeLineRThreshold_orig = gengetopt_strdup (optarg); + } + /* Number of horizontal strips to divide the image to. */ + else if (strcmp (long_options[option_index].name, "numStrips") == 0) + { + if (local_args_info.numStrips_given) + { + fprintf (stderr, "%s: `--numStrips' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->numStrips_given && ! override) + continue; + local_args_info.numStrips_given = 1; + args_info->numStrips_given = 1; + args_info->numStrips_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->numStrips_orig) + free (args_info->numStrips_orig); /* free previous string */ + args_info->numStrips_orig = gengetopt_strdup (optarg); + } + /* Whtethet to check splines or not. */ + else if (strcmp (long_options[option_index].name, "checkSplines") == 0) + { + if (local_args_info.checkSplines_given) + { + fprintf (stderr, "%s: `--checkSplines' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkSplines_given && ! override) + continue; + local_args_info.checkSplines_given = 1; + args_info->checkSplines_given = 1; + args_info->checkSplines_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkSplines_orig) + free (args_info->checkSplines_orig); /* free previous string */ + args_info->checkSplines_orig = gengetopt_strdup (optarg); + } + /* Curveness Threshold for checking splines. */ + else if (strcmp (long_options[option_index].name, "checkSplinesCurvenessThreshold") == 0) + { + if (local_args_info.checkSplinesCurvenessThreshold_given) + { + fprintf (stderr, "%s: `--checkSplinesCurvenessThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkSplinesCurvenessThreshold_given && ! override) + continue; + local_args_info.checkSplinesCurvenessThreshold_given = 1; + args_info->checkSplinesCurvenessThreshold_given = 1; + args_info->checkSplinesCurvenessThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkSplinesCurvenessThreshold_orig) + free (args_info->checkSplinesCurvenessThreshold_orig); /* free previous string */ + args_info->checkSplinesCurvenessThreshold_orig = gengetopt_strdup (optarg); + } + /* Length Threshold for checking splines. */ + else if (strcmp (long_options[option_index].name, "checkSplinesLengthThreshold") == 0) + { + if (local_args_info.checkSplinesLengthThreshold_given) + { + fprintf (stderr, "%s: `--checkSplinesLengthThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkSplinesLengthThreshold_given && ! override) + continue; + local_args_info.checkSplinesLengthThreshold_given = 1; + args_info->checkSplinesLengthThreshold_given = 1; + args_info->checkSplinesLengthThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkSplinesLengthThreshold_orig) + free (args_info->checkSplinesLengthThreshold_orig); /* free previous string */ + args_info->checkSplinesLengthThreshold_orig = gengetopt_strdup (optarg); + } + /* ThetaDiff Threshold for checking splines. */ + else if (strcmp (long_options[option_index].name, "checkSplinesThetaDiffThreshold") == 0) + { + if (local_args_info.checkSplinesThetaDiffThreshold_given) + { + fprintf (stderr, "%s: `--checkSplinesThetaDiffThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkSplinesThetaDiffThreshold_given && ! override) + continue; + local_args_info.checkSplinesThetaDiffThreshold_given = 1; + args_info->checkSplinesThetaDiffThreshold_given = 1; + args_info->checkSplinesThetaDiffThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkSplinesThetaDiffThreshold_orig) + free (args_info->checkSplinesThetaDiffThreshold_orig); /* free previous string */ + args_info->checkSplinesThetaDiffThreshold_orig = gengetopt_strdup (optarg); + } + /* ThetaThreshold Threshold for checking splines. */ + else if (strcmp (long_options[option_index].name, "checkSplinesThetaThreshold") == 0) + { + if (local_args_info.checkSplinesThetaThreshold_given) + { + fprintf (stderr, "%s: `--checkSplinesThetaThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkSplinesThetaThreshold_given && ! override) + continue; + local_args_info.checkSplinesThetaThreshold_given = 1; + args_info->checkSplinesThetaThreshold_given = 1; + args_info->checkSplinesThetaThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkSplinesThetaThreshold_orig) + free (args_info->checkSplinesThetaThreshold_orig); /* free previous string */ + args_info->checkSplinesThetaThreshold_orig = gengetopt_strdup (optarg); + } + /* Whtethet to check IPM splines or not. */ + else if (strcmp (long_options[option_index].name, "checkIPMSplines") == 0) + { + if (local_args_info.checkIPMSplines_given) + { + fprintf (stderr, "%s: `--checkIPMSplines' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkIPMSplines_given && ! override) + continue; + local_args_info.checkIPMSplines_given = 1; + args_info->checkIPMSplines_given = 1; + args_info->checkIPMSplines_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkIPMSplines_orig) + free (args_info->checkIPMSplines_orig); /* free previous string */ + args_info->checkIPMSplines_orig = gengetopt_strdup (optarg); + } + /* Curveness Threshold for checking splines. */ + else if (strcmp (long_options[option_index].name, "checkIPMSplinesCurvenessThreshold") == 0) + { + if (local_args_info.checkIPMSplinesCurvenessThreshold_given) + { + fprintf (stderr, "%s: `--checkIPMSplinesCurvenessThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkIPMSplinesCurvenessThreshold_given && ! override) + continue; + local_args_info.checkIPMSplinesCurvenessThreshold_given = 1; + args_info->checkIPMSplinesCurvenessThreshold_given = 1; + args_info->checkIPMSplinesCurvenessThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkIPMSplinesCurvenessThreshold_orig) + free (args_info->checkIPMSplinesCurvenessThreshold_orig); /* free previous string */ + args_info->checkIPMSplinesCurvenessThreshold_orig = gengetopt_strdup (optarg); + } + /* Length Threshold for checking splines. */ + else if (strcmp (long_options[option_index].name, "checkIPMSplinesLengthThreshold") == 0) + { + if (local_args_info.checkIPMSplinesLengthThreshold_given) + { + fprintf (stderr, "%s: `--checkIPMSplinesLengthThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkIPMSplinesLengthThreshold_given && ! override) + continue; + local_args_info.checkIPMSplinesLengthThreshold_given = 1; + args_info->checkIPMSplinesLengthThreshold_given = 1; + args_info->checkIPMSplinesLengthThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkIPMSplinesLengthThreshold_orig) + free (args_info->checkIPMSplinesLengthThreshold_orig); /* free previous string */ + args_info->checkIPMSplinesLengthThreshold_orig = gengetopt_strdup (optarg); + } + /* ThetaDiff Threshold for checking splines. */ + else if (strcmp (long_options[option_index].name, "checkIPMSplinesThetaDiffThreshold") == 0) + { + if (local_args_info.checkIPMSplinesThetaDiffThreshold_given) + { + fprintf (stderr, "%s: `--checkIPMSplinesThetaDiffThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkIPMSplinesThetaDiffThreshold_given && ! override) + continue; + local_args_info.checkIPMSplinesThetaDiffThreshold_given = 1; + args_info->checkIPMSplinesThetaDiffThreshold_given = 1; + args_info->checkIPMSplinesThetaDiffThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkIPMSplinesThetaDiffThreshold_orig) + free (args_info->checkIPMSplinesThetaDiffThreshold_orig); /* free previous string */ + args_info->checkIPMSplinesThetaDiffThreshold_orig = gengetopt_strdup (optarg); + } + /* ThetaThreshold Threshold for checking splines. */ + else if (strcmp (long_options[option_index].name, "checkIPMSplinesThetaThreshold") == 0) + { + if (local_args_info.checkIPMSplinesThetaThreshold_given) + { + fprintf (stderr, "%s: `--checkIPMSplinesThetaThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkIPMSplinesThetaThreshold_given && ! override) + continue; + local_args_info.checkIPMSplinesThetaThreshold_given = 1; + args_info->checkIPMSplinesThetaThreshold_given = 1; + args_info->checkIPMSplinesThetaThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkIPMSplinesThetaThreshold_orig) + free (args_info->checkIPMSplinesThetaThreshold_orig); /* free previous string */ + args_info->checkIPMSplinesThetaThreshold_orig = gengetopt_strdup (optarg); + } + /* Final Threshold for declaring a valid spline. */ + else if (strcmp (long_options[option_index].name, "finalSplineScoreThreshold") == 0) + { + if (local_args_info.finalSplineScoreThreshold_given) + { + fprintf (stderr, "%s: `--finalSplineScoreThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->finalSplineScoreThreshold_given && ! override) + continue; + local_args_info.finalSplineScoreThreshold_given = 1; + args_info->finalSplineScoreThreshold_given = 1; + args_info->finalSplineScoreThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->finalSplineScoreThreshold_orig) + free (args_info->finalSplineScoreThreshold_orig); /* free previous string */ + args_info->finalSplineScoreThreshold_orig = gengetopt_strdup (optarg); + } + /* Use groudn plane or not when sending to map. */ + else if (strcmp (long_options[option_index].name, "useGroundPlane") == 0) + { + if (local_args_info.useGroundPlane_given) + { + fprintf (stderr, "%s: `--useGroundPlane' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->useGroundPlane_given && ! override) + continue; + local_args_info.useGroundPlane_given = 1; + args_info->useGroundPlane_given = 1; + args_info->useGroundPlane_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->useGroundPlane_orig) + free (args_info->useGroundPlane_orig); /* free previous string */ + args_info->useGroundPlane_orig = gengetopt_strdup (optarg); + } + /* Whether to check colors or not. */ + else if (strcmp (long_options[option_index].name, "checkColor") == 0) + { + if (local_args_info.checkColor_given) + { + fprintf (stderr, "%s: `--checkColor' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkColor_given && ! override) + continue; + local_args_info.checkColor_given = 1; + args_info->checkColor_given = 1; + args_info->checkColor_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkColor_orig) + free (args_info->checkColor_orig); /* free previous string */ + args_info->checkColor_orig = gengetopt_strdup (optarg); + } + /* Size of window to use. */ + else if (strcmp (long_options[option_index].name, "checkColorWindow") == 0) + { + if (local_args_info.checkColorWindow_given) + { + fprintf (stderr, "%s: `--checkColorWindow' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkColorWindow_given && ! override) + continue; + local_args_info.checkColorWindow_given = 1; + args_info->checkColorWindow_given = 1; + args_info->checkColorWindow_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkColorWindow_orig) + free (args_info->checkColorWindow_orig); /* free previous string */ + args_info->checkColorWindow_orig = gengetopt_strdup (optarg); + } + /* Number of bins to use. */ + else if (strcmp (long_options[option_index].name, "checkColorNumBins") == 0) + { + if (local_args_info.checkColorNumBins_given) + { + fprintf (stderr, "%s: `--checkColorNumBins' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkColorNumBins_given && ! override) + continue; + local_args_info.checkColorNumBins_given = 1; + args_info->checkColorNumBins_given = 1; + args_info->checkColorNumBins_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkColorNumBins_orig) + free (args_info->checkColorNumBins_orig); /* free previous string */ + args_info->checkColorNumBins_orig = gengetopt_strdup (optarg); + } + /* Min ratio of yellow points. */ + else if (strcmp (long_options[option_index].name, "checkColorNumYellowMin") == 0) + { + if (local_args_info.checkColorNumYellowMin_given) + { + fprintf (stderr, "%s: `--checkColorNumYellowMin' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkColorNumYellowMin_given && ! override) + continue; + local_args_info.checkColorNumYellowMin_given = 1; + args_info->checkColorNumYellowMin_given = 1; + args_info->checkColorNumYellowMin_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkColorNumYellowMin_orig) + free (args_info->checkColorNumYellowMin_orig); /* free previous string */ + args_info->checkColorNumYellowMin_orig = gengetopt_strdup (optarg); + } + /* Min RG diff. */ + else if (strcmp (long_options[option_index].name, "checkColorRGMin") == 0) + { + if (local_args_info.checkColorRGMin_given) + { + fprintf (stderr, "%s: `--checkColorRGMin' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkColorRGMin_given && ! override) + continue; + local_args_info.checkColorRGMin_given = 1; + args_info->checkColorRGMin_given = 1; + args_info->checkColorRGMin_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkColorRGMin_orig) + free (args_info->checkColorRGMin_orig); /* free previous string */ + args_info->checkColorRGMin_orig = gengetopt_strdup (optarg); + } + /* Max RG diff. */ + else if (strcmp (long_options[option_index].name, "checkColorRGMax") == 0) + { + if (local_args_info.checkColorRGMax_given) + { + fprintf (stderr, "%s: `--checkColorRGMax' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkColorRGMax_given && ! override) + continue; + local_args_info.checkColorRGMax_given = 1; + args_info->checkColorRGMax_given = 1; + args_info->checkColorRGMax_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkColorRGMax_orig) + free (args_info->checkColorRGMax_orig); /* free previous string */ + args_info->checkColorRGMax_orig = gengetopt_strdup (optarg); + } + /* Min GB diff. */ + else if (strcmp (long_options[option_index].name, "checkColorGBMin") == 0) + { + if (local_args_info.checkColorGBMin_given) + { + fprintf (stderr, "%s: `--checkColorGBMin' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkColorGBMin_given && ! override) + continue; + local_args_info.checkColorGBMin_given = 1; + args_info->checkColorGBMin_given = 1; + args_info->checkColorGBMin_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkColorGBMin_orig) + free (args_info->checkColorGBMin_orig); /* free previous string */ + args_info->checkColorGBMin_orig = gengetopt_strdup (optarg); + } + /* Min RB diff. */ + else if (strcmp (long_options[option_index].name, "checkColorRBMin") == 0) + { + if (local_args_info.checkColorRBMin_given) + { + fprintf (stderr, "%s: `--checkColorRBMin' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkColorRBMin_given && ! override) + continue; + local_args_info.checkColorRBMin_given = 1; + args_info->checkColorRBMin_given = 1; + args_info->checkColorRBMin_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkColorRBMin_orig) + free (args_info->checkColorRBMin_orig); /* free previous string */ + args_info->checkColorRBMin_orig = gengetopt_strdup (optarg); + } + /* RBF Threshold. */ + else if (strcmp (long_options[option_index].name, "checkColorRBFThreshold") == 0) + { + if (local_args_info.checkColorRBFThreshold_given) + { + fprintf (stderr, "%s: `--checkColorRBFThreshold' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkColorRBFThreshold_given && ! override) + continue; + local_args_info.checkColorRBFThreshold_given = 1; + args_info->checkColorRBFThreshold_given = 1; + args_info->checkColorRBFThreshold_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkColorRBFThreshold_orig) + free (args_info->checkColorRBFThreshold_orig); /* free previous string */ + args_info->checkColorRBFThreshold_orig = gengetopt_strdup (optarg); + } + /* Whether to use RBF or not. */ + else if (strcmp (long_options[option_index].name, "checkColorRBF") == 0) + { + if (local_args_info.checkColorRBF_given) + { + fprintf (stderr, "%s: `--checkColorRBF' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkColorRBF_given && ! override) + continue; + local_args_info.checkColorRBF_given = 1; + args_info->checkColorRBF_given = 1; + args_info->checkColorRBF_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkColorRBF_orig) + free (args_info->checkColorRBF_orig); /* free previous string */ + args_info->checkColorRBF_orig = gengetopt_strdup (optarg); + } + /* Whether to clear part of the IPM image. */ + else if (strcmp (long_options[option_index].name, "ipmWindowClear") == 0) + { + if (local_args_info.ipmWindowClear_given) + { + fprintf (stderr, "%s: `--ipmWindowClear' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmWindowClear_given && ! override) + continue; + local_args_info.ipmWindowClear_given = 1; + args_info->ipmWindowClear_given = 1; + args_info->ipmWindowClear_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmWindowClear_orig) + free (args_info->ipmWindowClear_orig); /* free previous string */ + args_info->ipmWindowClear_orig = gengetopt_strdup (optarg); + } + /* Left corrdinate of window to keep in IPM. */ + else if (strcmp (long_options[option_index].name, "ipmWindowLeft") == 0) + { + if (local_args_info.ipmWindowLeft_given) + { + fprintf (stderr, "%s: `--ipmWindowLeft' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmWindowLeft_given && ! override) + continue; + local_args_info.ipmWindowLeft_given = 1; + args_info->ipmWindowLeft_given = 1; + args_info->ipmWindowLeft_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmWindowLeft_orig) + free (args_info->ipmWindowLeft_orig); /* free previous string */ + args_info->ipmWindowLeft_orig = gengetopt_strdup (optarg); + } + /* Left corrdinate of window to keep in IPM. */ + else if (strcmp (long_options[option_index].name, "ipmWindowRight") == 0) + { + if (local_args_info.ipmWindowRight_given) + { + fprintf (stderr, "%s: `--ipmWindowRight' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->ipmWindowRight_given && ! override) + continue; + local_args_info.ipmWindowRight_given = 1; + args_info->ipmWindowRight_given = 1; + args_info->ipmWindowRight_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->ipmWindowRight_orig) + free (args_info->ipmWindowRight_orig); /* free previous string */ + args_info->ipmWindowRight_orig = gengetopt_strdup (optarg); + } + /* Whether to check lane width or not. */ + else if (strcmp (long_options[option_index].name, "checkLaneWidth") == 0) + { + if (local_args_info.checkLaneWidth_given) + { + fprintf (stderr, "%s: `--checkLaneWidth' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkLaneWidth_given && ! override) + continue; + local_args_info.checkLaneWidth_given = 1; + args_info->checkLaneWidth_given = 1; + args_info->checkLaneWidth_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkLaneWidth_orig) + free (args_info->checkLaneWidth_orig); /* free previous string */ + args_info->checkLaneWidth_orig = gengetopt_strdup (optarg); + } + /* Mean of lane width to look for. */ + else if (strcmp (long_options[option_index].name, "checkLaneWidthMean") == 0) + { + if (local_args_info.checkLaneWidthMean_given) + { + fprintf (stderr, "%s: `--checkLaneWidthMean' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkLaneWidthMean_given && ! override) + continue; + local_args_info.checkLaneWidthMean_given = 1; + args_info->checkLaneWidthMean_given = 1; + args_info->checkLaneWidthMean_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkLaneWidthMean_orig) + free (args_info->checkLaneWidthMean_orig); /* free previous string */ + args_info->checkLaneWidthMean_orig = gengetopt_strdup (optarg); + } + /* Std deviation of lane width to look for. */ + else if (strcmp (long_options[option_index].name, "checkLaneWidthStd") == 0) + { + if (local_args_info.checkLaneWidthStd_given) + { + fprintf (stderr, "%s: `--checkLaneWidthStd' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->checkLaneWidthStd_given && ! override) + continue; + local_args_info.checkLaneWidthStd_given = 1; + args_info->checkLaneWidthStd_given = 1; + args_info->checkLaneWidthStd_arg = (float)strtod (optarg, &stop_char); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->checkLaneWidthStd_orig) + free (args_info->checkLaneWidthStd_orig); /* free previous string */ + args_info->checkLaneWidthStd_orig = gengetopt_strdup (optarg); + } + + break; + case '?': /* Invalid option. */ + /* `getopt_long' already printed an error message. */ + goto failure; + + default: /* bug: option not considered. */ + fprintf (stderr, "%s: option unknown: %c%s\n", LANEDETECTORPARSER_PACKAGE, c, (additional_error ? additional_error : "")); + abort (); + } /* switch */ + } /* while */ + + + + if (check_required) + { + error += LaneDetectorParser_required2 (args_info, argv[0], additional_error); + } + + LaneDetectorParser_release (&local_args_info); + + if ( error ) + return (EXIT_FAILURE); + + return 0; + +failure: + + LaneDetectorParser_release (&local_args_info); + return (EXIT_FAILURE); +} + +#ifndef CONFIG_FILE_LINE_SIZE +#define CONFIG_FILE_LINE_SIZE 2048 +#endif +#define ADDITIONAL_ERROR " in configuration file " + +#define CONFIG_FILE_LINE_BUFFER_SIZE (CONFIG_FILE_LINE_SIZE+3) +/* 3 is for "--" and "=" */ + +char my_argv[CONFIG_FILE_LINE_BUFFER_SIZE+1]; + +int +LaneDetectorParser_configfile (char * const filename, struct LaneDetectorParserInfo *args_info, int override, int initialize, int check_required) +{ + FILE* file; + char linebuf[CONFIG_FILE_LINE_SIZE]; + int line_num = 0; + int i, result, equal; + char *fopt, *farg; + char *str_index; + size_t len, next_token; + char delimiter; + int my_argc = 0; + char **my_argv_arg; + char *additional_error; + + /* store the program name */ + cmd_line_list_tmp = (struct line_list *) malloc (sizeof (struct line_list)); + cmd_line_list_tmp->next = cmd_line_list; + cmd_line_list = cmd_line_list_tmp; + cmd_line_list->string_arg = gengetopt_strdup (LANEDETECTORPARSER_PACKAGE); + + if ((file = fopen(filename, "r")) == NULL) + { + fprintf (stderr, "%s: Error opening configuration file '%s'\n", + LANEDETECTORPARSER_PACKAGE, filename); + result = EXIT_FAILURE; + goto conf_failure; + } + + while ((fgets(linebuf, CONFIG_FILE_LINE_SIZE, file)) != NULL) + { + ++line_num; + my_argv[0] = '\0'; + len = strlen(linebuf); + if (len > (CONFIG_FILE_LINE_BUFFER_SIZE-1)) + { + fprintf (stderr, "%s:%s:%d: Line too long in configuration file\n", + LANEDETECTORPARSER_PACKAGE, filename, line_num); + result = EXIT_FAILURE; + goto conf_failure; + } + + /* find first non-whitespace character in the line */ + next_token = strspn ( linebuf, " \t\r\n"); + str_index = linebuf + next_token; + + if ( str_index[0] == '\0' || str_index[0] == '#') + continue; /* empty line or comment line is skipped */ + + fopt = str_index; + + /* truncate fopt at the end of the first non-valid character */ + next_token = strcspn (fopt, " \t\r\n="); + + if (fopt[next_token] == '\0') /* the line is over */ + { + farg = NULL; + equal = 0; + goto noarg; + } + + /* remember if equal sign is present */ + equal = (fopt[next_token] == '='); + fopt[next_token++] = '\0'; + + /* advance pointers to the next token after the end of fopt */ + next_token += strspn (fopt + next_token, " \t\r\n"); + /* check for the presence of equal sign, and if so, skip it */ + if ( !equal ) + if ((equal = (fopt[next_token] == '='))) + { + next_token++; + next_token += strspn (fopt + next_token, " \t\r\n"); + } + str_index += next_token; + + /* find argument */ + farg = str_index; + if ( farg[0] == '\"' || farg[0] == '\'' ) + { /* quoted argument */ + str_index = strchr (++farg, str_index[0] ); /* skip opening quote */ + if (! str_index) + { + fprintf + (stderr, + "%s:%s:%d: unterminated string in configuration file\n", + LANEDETECTORPARSER_PACKAGE, filename, line_num); + result = EXIT_FAILURE; + goto conf_failure; + } + } + else + { /* read up the remaining part up to a delimiter */ + next_token = strcspn (farg, " \t\r\n#\'\""); + str_index += next_token; + } + + /* truncate farg at the delimiter and store it for further check */ + delimiter = *str_index, *str_index++ = '\0'; + + /* everything but comment is illegal at the end of line */ + if (delimiter != '\0' && delimiter != '#') + { + str_index += strspn(str_index, " \t\r\n"); + if (*str_index != '\0' && *str_index != '#') + { + fprintf + (stderr, + "%s:%s:%d: malformed string in configuration file\n", + LANEDETECTORPARSER_PACKAGE, filename, line_num); + result = EXIT_FAILURE; + goto conf_failure; + } + } + + noarg: + ++my_argc; + len = strlen(fopt); + + strcat (my_argv, len > 1 ? "--" : "-"); + strcat (my_argv, fopt); + if (len > 1 && ((farg &&*farg) || equal)) + strcat (my_argv, "="); + if (farg && *farg) + strcat (my_argv, farg); + + cmd_line_list_tmp = (struct line_list *) malloc (sizeof (struct line_list)); + cmd_line_list_tmp->next = cmd_line_list; + cmd_line_list = cmd_line_list_tmp; + cmd_line_list->string_arg = gengetopt_strdup(my_argv); + } /* while */ + + ++my_argc; /* for program name */ + my_argv_arg = (char **) malloc((my_argc+1) * sizeof(char *)); + cmd_line_list_tmp = cmd_line_list; + for (i = my_argc - 1; i >= 0; --i) { + my_argv_arg[i] = cmd_line_list_tmp->string_arg; + cmd_line_list_tmp = cmd_line_list_tmp->next; + } + my_argv_arg[my_argc] = 0; + + additional_error = (char *)malloc(strlen(filename) + strlen(ADDITIONAL_ERROR) + 1); + strcpy (additional_error, ADDITIONAL_ERROR); + strcat (additional_error, filename); + result = + LaneDetectorParser_internal (my_argc, my_argv_arg, args_info, override, initialize, check_required, additional_error); + + free (additional_error); + free (my_argv_arg); + +conf_failure: + if (file) + fclose(file); + + free_cmd_list(); + if (result == EXIT_FAILURE) + { + LaneDetectorParser_free (args_info); + exit (EXIT_FAILURE); + } + + return result; +} diff --git a/src/LaneDetectorOpt.ggo b/src/LaneDetectorOpt.ggo new file mode 100644 index 0000000..b3311ba --- /dev/null +++ b/src/LaneDetectorOpt.ggo @@ -0,0 +1,269 @@ +#Parameters for Lane Detector +#command line: gengetopt -i LaneDetectorOpt.ggo -F LaneDetectorOpt \ +# --func-name=LaneDetectorParser --arg-struct-name=LaneDetectorParserInfo \ +# --conf-parser + +package "LaneDetector" +version "0.1" + +option "ipmWidth" - + "width of IPM image to use" int required +option "ipmHeight" - + "height of IPM image to use" int required +option "ipmTop" - + "Top point in original image of region to make IPM for" int required +option "ipmLeft" - + "Left point in original image of region to make IPM for" int required +option "ipmRight" - + "Right point in original image region to make IPM for" int required +option "ipmBottom" - + "Bottom point in original image region to make IPM for" int required + +option "ipmInterpolation" - + "The method to use for IPM interpolation" int required + +option "lineWidth" - + "width of line to detect in mm (in the world)" double required +option "lineHeight" - + "height of line to detect in mm (in the world)" double required +option "kernelWidth" - + "widht of kernel to use for filtering" int required +option "kernelHeight" - + "Height of kernel to use for filtering" int required +option "lowerQuantile" - + "lower quantile to use for thresholding the filtered image" + double required +option "localMaxima" - + "whether to return local maxima or just the maximum" + int required +option "groupingType" - + "type of grouping to use (default 0: HV lines)" int required +option "binarize" - + "whether to binarize the thresholded image or use the raw values" + double required +option "detectionThreshold" - + "threshold for line scores to declare as line" double required +option "smoothScores" - + "whether to smooth scores of lines detected or not" int required + +option "rMin" - + "rMin for Hough transform (in pixels)" double required +option "rMax" - + "rMax for Hough transform (in pixels)" double required +option "rStep" - + "rStep for Hough transform (in pixels)" double required +option "thetaMin" - + "thetaMin for Hough transform (in degrees)" double required +option "thetaMax" - + "thetaMax for Hough transform (in degrees)" double required +option "thetaStep" - + "thetaStep for Hough transform (in degrees)" double required + +option "ipmVpPortion" - + "Portion of IPM image height to add to y-coordinate of VP" double required + +option "getEndPoints" - + "Get the endpoints of the line" int required + +option "group" - + "group nearby lines or not (default 1: group)" int required + +option "groupThreshold" - + "Threshold for grouping nearby lines (default 10)" double required + +#RANSAC options +option "ransac" - + "use RANSAC (1) or not (0)" int required + +option "ransacLineNumSamples" - + "Number of samples to use for RANSAC" int required +option "ransacLineNumIterations" - + "Number of iterations to use for RANSAC" int required +option "ransacLineNumGoodFit" - + "Number of close points to consider a good line fit" int required +option "ransacLineThreshold" - + "Threshold to consider a point close" double required +option "ransacLineScoreThreshold" - + "Threshold for detected line scores" double required +option "ransacLineBinarize" - + "Whether to binarize image for RANSAC or not" int required +option "ransacLineWindow" - + "Half width to use for ransac window" int required + +option "ransacSplineNumSamples" - + "Number of samples to use for RANSAC" int required +option "ransacSplineNumIterations" - + "Number of iterations to use for RANSAC" int required +option "ransacSplineNumGoodFit" - + "Number of close points to consider a good line fit" int required +option "ransacSplineThreshold" - + "Threshold to consider a point close" double required +option "ransacSplineScoreThreshold" - + "Threshold for detected line scores" double required +option "ransacSplineBinarize" - + "Whether to binarize image for RANSAC or not" int required +option "ransacSplineWindow" - + "Half width to use for ransac window" int required + +option "ransacSplineDegree" - + "Degree of spline to use" int required +option "ransacSpline" - + "Whether to use splines" int required +option "ransacLine" - + "Whether to use lines" int required + +option "ransacSplineStep" - + "Step to use when pixelzing spline in ransac" float required + + +option "overlapThreshold" - + "Overlap threshold to use for grouping of bounding boxes" float required + + +option "localizeAngleThreshold" - + "Angle threshold used for localization (cosine, 1: most restrictive, 0: most liberal)" float required +option "localizeNumLinePixels" - + "Number of pixels to go in normal direction for localization" int required + + +option "extendAngleThreshold" - + "Angle threshold used for extending (cosine, 1: most restrictive, 0: most liberal)" float required +option "extendMeanDirAngleThreshold" - + "Angle threshold from mean direction used for extending (cosine, 1: most restrictive, 0: most liberal)" float required +option "extendLinePixelsTangent" - + "Number of pixels to go in tangent direction for extending" int required +option "extendLinePixelsNormal" - + "Number of pixels to go in tangent direction for extending" int required +option "extendContThreshold" - + "Threhsold used for stopping the extending process (higher -> less extending)" float required +option "extendDeviationThreshold" - + "Stop extending when number of deviating points exceeds this threshold" int required +option "extendRectTop" - + "Top point for extension bounding box" int required +option "extendRectBottom" - + "Bottom point for extension bounding box" int required + +option "extendIPMAngleThreshold" - + "Angle threshold used for extending (cosine, 1: most restrictive, 0: most liberal)" float required +option "extendIPMMeanDirAngleThreshold" - + "Angle threshold from mean direction used for extending (cosine, 1: most restrictive, 0: most liberal)" float required +option "extendIPMLinePixelsTangent" - + "Number of pixels to go in tangent direction for extending" int required +option "extendIPMLinePixelsNormal" - + "Number of pixels to go in tangent direction for extending" int required +option "extendIPMContThreshold" - + "Threhsold used for stopping the extending process (higher -> less extending)" float required +option "extendIPMDeviationThreshold" - + "Stop extending when number of deviating points exceeds this threshold" int required +option "extendIPMRectTop" - + "Top point for extension bounding box" int required +option "extendIPMRectBottom" - + "Bottom point for extension bounding box" int required + + +option "splineScoreJitter" - + "Number of pixels to go around the spline to compute score" int required +option "splineScoreLengthRatio" - + "Ratio of spline length to use" float required +option "splineScoreAngleRatio" - + "Ratio of spline angle to use" float required +option "splineScoreStep" - + "Step to use for spline score computation" float required + + +option "splineTrackingNumAbsentFrames" - + "number of frames the track is allowed to be absent before deleting it" int required +option "splineTrackingNumSeenFrames" - + "number of frames before considering the track good" int required + +option "mergeSplineThetaThreshold" - + "Angle threshold for merging splines (radians)" float required +option "mergeSplineRThreshold" - + "R threshold (distance from origin) for merginn splines" float required +option "mergeSplineMeanThetaThreshold" - + "Mean Angle threshold for merging splines (radians)" float required +option "mergeSplineMeanRThreshold" - + "Mean R threshold (distance from origin) for merginn splines" float required +option "mergeSplineCentroidThreshold" - + "Distance threshold between spline cetroids for merging" float required + +option "lineTrackingNumAbsentFrames" - + "number of frames the track is allowed to be absent before deleting it" int required +option "lineTrackingNumSeenFrames" - + "number of frames before considering the track good" int required + +option "mergeLineThetaThreshold" - + "Angle threshold for merging lines (radians)" float required +option "mergeLineRThreshold" - + "R threshold (distance from origin) for merging lines" float required + +option "numStrips" - + "Number of horizontal strips to divide the image to" int required + +option "checkSplines" - + "Whtethet to check splines or not" int required +option "checkSplinesCurvenessThreshold" - + "Curveness Threshold for checking splines" float required +option "checkSplinesLengthThreshold" - + "Length Threshold for checking splines" float required +option "checkSplinesThetaDiffThreshold" - + "ThetaDiff Threshold for checking splines" float required +option "checkSplinesThetaThreshold" - + "ThetaThreshold Threshold for checking splines" float required + +option "checkIPMSplines" - + "Whtethet to check IPM splines or not" int required +option "checkIPMSplinesCurvenessThreshold" - + "Curveness Threshold for checking splines" float required +option "checkIPMSplinesLengthThreshold" - + "Length Threshold for checking splines" float required +option "checkIPMSplinesThetaDiffThreshold" - + "ThetaDiff Threshold for checking splines" float required +option "checkIPMSplinesThetaThreshold" - + "ThetaThreshold Threshold for checking splines" float required + +option "finalSplineScoreThreshold" - + "Final Threshold for declaring a valid spline" float required + +option "useGroundPlane" - + "Use groudn plane or not when sending to map" int required + +option "checkColor" - + "Whether to check colors or not" int required +option "checkColorWindow" - + "Size of window to use" int required +option "checkColorNumBins" - + "Number of bins to use" int required +option "checkColorNumYellowMin" - + "Min ratio of yellow points" float required +option "checkColorRGMin" - + "Min RG diff" float required +option "checkColorRGMax" - + "Max RG diff" float required +option "checkColorGBMin" - + "Min GB diff" float required +option "checkColorRBMin" - + "Min RB diff" float required +option "checkColorRBFThreshold" - + "RBF Threshold" float required +option "checkColorRBF" - + "Whether to use RBF or not" int required + +option "ipmWindowClear" - + "Whether to clear part of the IPM image" int required +option "ipmWindowLeft" - + "Left corrdinate of window to keep in IPM" int required +option "ipmWindowRight" - + "Left corrdinate of window to keep in IPM" int required + +option "checkLaneWidth" - + "Whether to check lane width or not" int required +option "checkLaneWidthMean" - + "Mean of lane width to look for" float required +option "checkLaneWidthStd" - + "Std deviation of lane width to look for" float required + + + + + diff --git a/src/LaneDetectorOpt.h b/src/LaneDetectorOpt.h new file mode 100644 index 0000000..0915c6a --- /dev/null +++ b/src/LaneDetectorOpt.h @@ -0,0 +1,500 @@ +/* LaneDetectorOpt.h */ + +/* File autogenerated by gengetopt version 2.18 */ + +#ifndef LANEDETECTOROPT_H +#define LANEDETECTOROPT_H + +/* If we use autoconf. */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#ifndef LANEDETECTORPARSER_PACKAGE +#define LANEDETECTORPARSER_PACKAGE "LaneDetector" +#endif + +#ifndef LANEDETECTORPARSER_VERSION +#define LANEDETECTORPARSER_VERSION "0.1" +#endif + +struct LaneDetectorParserInfo +{ + const char *help_help; /* Print help and exit help description. */ + const char *version_help; /* Print version and exit help description. */ + int ipmWidth_arg; /* width of IPM image to use. */ + char * ipmWidth_orig; /* width of IPM image to use original value given at command line. */ + const char *ipmWidth_help; /* width of IPM image to use help description. */ + int ipmHeight_arg; /* height of IPM image to use. */ + char * ipmHeight_orig; /* height of IPM image to use original value given at command line. */ + const char *ipmHeight_help; /* height of IPM image to use help description. */ + int ipmTop_arg; /* Top point in original image of region to make IPM for. */ + char * ipmTop_orig; /* Top point in original image of region to make IPM for original value given at command line. */ + const char *ipmTop_help; /* Top point in original image of region to make IPM for help description. */ + int ipmLeft_arg; /* Left point in original image of region to make IPM for. */ + char * ipmLeft_orig; /* Left point in original image of region to make IPM for original value given at command line. */ + const char *ipmLeft_help; /* Left point in original image of region to make IPM for help description. */ + int ipmRight_arg; /* Right point in original image region to make IPM for. */ + char * ipmRight_orig; /* Right point in original image region to make IPM for original value given at command line. */ + const char *ipmRight_help; /* Right point in original image region to make IPM for help description. */ + int ipmBottom_arg; /* Bottom point in original image region to make IPM for. */ + char * ipmBottom_orig; /* Bottom point in original image region to make IPM for original value given at command line. */ + const char *ipmBottom_help; /* Bottom point in original image region to make IPM for help description. */ + int ipmInterpolation_arg; /* The method to use for IPM interpolation. */ + char * ipmInterpolation_orig; /* The method to use for IPM interpolation original value given at command line. */ + const char *ipmInterpolation_help; /* The method to use for IPM interpolation help description. */ + double lineWidth_arg; /* width of line to detect in mm (in the world). */ + char * lineWidth_orig; /* width of line to detect in mm (in the world) original value given at command line. */ + const char *lineWidth_help; /* width of line to detect in mm (in the world) help description. */ + double lineHeight_arg; /* height of line to detect in mm (in the world). */ + char * lineHeight_orig; /* height of line to detect in mm (in the world) original value given at command line. */ + const char *lineHeight_help; /* height of line to detect in mm (in the world) help description. */ + int kernelWidth_arg; /* widht of kernel to use for filtering. */ + char * kernelWidth_orig; /* widht of kernel to use for filtering original value given at command line. */ + const char *kernelWidth_help; /* widht of kernel to use for filtering help description. */ + int kernelHeight_arg; /* Height of kernel to use for filtering. */ + char * kernelHeight_orig; /* Height of kernel to use for filtering original value given at command line. */ + const char *kernelHeight_help; /* Height of kernel to use for filtering help description. */ + double lowerQuantile_arg; /* lower quantile to use for thresholding the filtered image. */ + char * lowerQuantile_orig; /* lower quantile to use for thresholding the filtered image original value given at command line. */ + const char *lowerQuantile_help; /* lower quantile to use for thresholding the filtered image help description. */ + int localMaxima_arg; /* whether to return local maxima or just the maximum. */ + char * localMaxima_orig; /* whether to return local maxima or just the maximum original value given at command line. */ + const char *localMaxima_help; /* whether to return local maxima or just the maximum help description. */ + int groupingType_arg; /* type of grouping to use (default 0: HV lines). */ + char * groupingType_orig; /* type of grouping to use (default 0: HV lines) original value given at command line. */ + const char *groupingType_help; /* type of grouping to use (default 0: HV lines) help description. */ + double binarize_arg; /* whether to binarize the thresholded image or use the raw values. */ + char * binarize_orig; /* whether to binarize the thresholded image or use the raw values original value given at command line. */ + const char *binarize_help; /* whether to binarize the thresholded image or use the raw values help description. */ + double detectionThreshold_arg; /* threshold for line scores to declare as line. */ + char * detectionThreshold_orig; /* threshold for line scores to declare as line original value given at command line. */ + const char *detectionThreshold_help; /* threshold for line scores to declare as line help description. */ + int smoothScores_arg; /* whether to smooth scores of lines detected or not. */ + char * smoothScores_orig; /* whether to smooth scores of lines detected or not original value given at command line. */ + const char *smoothScores_help; /* whether to smooth scores of lines detected or not help description. */ + double rMin_arg; /* rMin for Hough transform (in pixels). */ + char * rMin_orig; /* rMin for Hough transform (in pixels) original value given at command line. */ + const char *rMin_help; /* rMin for Hough transform (in pixels) help description. */ + double rMax_arg; /* rMax for Hough transform (in pixels). */ + char * rMax_orig; /* rMax for Hough transform (in pixels) original value given at command line. */ + const char *rMax_help; /* rMax for Hough transform (in pixels) help description. */ + double rStep_arg; /* rStep for Hough transform (in pixels). */ + char * rStep_orig; /* rStep for Hough transform (in pixels) original value given at command line. */ + const char *rStep_help; /* rStep for Hough transform (in pixels) help description. */ + double thetaMin_arg; /* thetaMin for Hough transform (in degrees). */ + char * thetaMin_orig; /* thetaMin for Hough transform (in degrees) original value given at command line. */ + const char *thetaMin_help; /* thetaMin for Hough transform (in degrees) help description. */ + double thetaMax_arg; /* thetaMax for Hough transform (in degrees). */ + char * thetaMax_orig; /* thetaMax for Hough transform (in degrees) original value given at command line. */ + const char *thetaMax_help; /* thetaMax for Hough transform (in degrees) help description. */ + double thetaStep_arg; /* thetaStep for Hough transform (in degrees). */ + char * thetaStep_orig; /* thetaStep for Hough transform (in degrees) original value given at command line. */ + const char *thetaStep_help; /* thetaStep for Hough transform (in degrees) help description. */ + double ipmVpPortion_arg; /* Portion of IPM image height to add to y-coordinate of VP. */ + char * ipmVpPortion_orig; /* Portion of IPM image height to add to y-coordinate of VP original value given at command line. */ + const char *ipmVpPortion_help; /* Portion of IPM image height to add to y-coordinate of VP help description. */ + int getEndPoints_arg; /* Get the endpoints of the line. */ + char * getEndPoints_orig; /* Get the endpoints of the line original value given at command line. */ + const char *getEndPoints_help; /* Get the endpoints of the line help description. */ + int group_arg; /* group nearby lines or not (default 1: group). */ + char * group_orig; /* group nearby lines or not (default 1: group) original value given at command line. */ + const char *group_help; /* group nearby lines or not (default 1: group) help description. */ + double groupThreshold_arg; /* Threshold for grouping nearby lines (default 10). */ + char * groupThreshold_orig; /* Threshold for grouping nearby lines (default 10) original value given at command line. */ + const char *groupThreshold_help; /* Threshold for grouping nearby lines (default 10) help description. */ + int ransac_arg; /* use RANSAC (1) or not (0). */ + char * ransac_orig; /* use RANSAC (1) or not (0) original value given at command line. */ + const char *ransac_help; /* use RANSAC (1) or not (0) help description. */ + int ransacLineNumSamples_arg; /* Number of samples to use for RANSAC. */ + char * ransacLineNumSamples_orig; /* Number of samples to use for RANSAC original value given at command line. */ + const char *ransacLineNumSamples_help; /* Number of samples to use for RANSAC help description. */ + int ransacLineNumIterations_arg; /* Number of iterations to use for RANSAC. */ + char * ransacLineNumIterations_orig; /* Number of iterations to use for RANSAC original value given at command line. */ + const char *ransacLineNumIterations_help; /* Number of iterations to use for RANSAC help description. */ + int ransacLineNumGoodFit_arg; /* Number of close points to consider a good line fit. */ + char * ransacLineNumGoodFit_orig; /* Number of close points to consider a good line fit original value given at command line. */ + const char *ransacLineNumGoodFit_help; /* Number of close points to consider a good line fit help description. */ + double ransacLineThreshold_arg; /* Threshold to consider a point close. */ + char * ransacLineThreshold_orig; /* Threshold to consider a point close original value given at command line. */ + const char *ransacLineThreshold_help; /* Threshold to consider a point close help description. */ + double ransacLineScoreThreshold_arg; /* Threshold for detected line scores. */ + char * ransacLineScoreThreshold_orig; /* Threshold for detected line scores original value given at command line. */ + const char *ransacLineScoreThreshold_help; /* Threshold for detected line scores help description. */ + int ransacLineBinarize_arg; /* Whether to binarize image for RANSAC or not. */ + char * ransacLineBinarize_orig; /* Whether to binarize image for RANSAC or not original value given at command line. */ + const char *ransacLineBinarize_help; /* Whether to binarize image for RANSAC or not help description. */ + int ransacLineWindow_arg; /* Half width to use for ransac window. */ + char * ransacLineWindow_orig; /* Half width to use for ransac window original value given at command line. */ + const char *ransacLineWindow_help; /* Half width to use for ransac window help description. */ + int ransacSplineNumSamples_arg; /* Number of samples to use for RANSAC. */ + char * ransacSplineNumSamples_orig; /* Number of samples to use for RANSAC original value given at command line. */ + const char *ransacSplineNumSamples_help; /* Number of samples to use for RANSAC help description. */ + int ransacSplineNumIterations_arg; /* Number of iterations to use for RANSAC. */ + char * ransacSplineNumIterations_orig; /* Number of iterations to use for RANSAC original value given at command line. */ + const char *ransacSplineNumIterations_help; /* Number of iterations to use for RANSAC help description. */ + int ransacSplineNumGoodFit_arg; /* Number of close points to consider a good line fit. */ + char * ransacSplineNumGoodFit_orig; /* Number of close points to consider a good line fit original value given at command line. */ + const char *ransacSplineNumGoodFit_help; /* Number of close points to consider a good line fit help description. */ + double ransacSplineThreshold_arg; /* Threshold to consider a point close. */ + char * ransacSplineThreshold_orig; /* Threshold to consider a point close original value given at command line. */ + const char *ransacSplineThreshold_help; /* Threshold to consider a point close help description. */ + double ransacSplineScoreThreshold_arg; /* Threshold for detected line scores. */ + char * ransacSplineScoreThreshold_orig; /* Threshold for detected line scores original value given at command line. */ + const char *ransacSplineScoreThreshold_help; /* Threshold for detected line scores help description. */ + int ransacSplineBinarize_arg; /* Whether to binarize image for RANSAC or not. */ + char * ransacSplineBinarize_orig; /* Whether to binarize image for RANSAC or not original value given at command line. */ + const char *ransacSplineBinarize_help; /* Whether to binarize image for RANSAC or not help description. */ + int ransacSplineWindow_arg; /* Half width to use for ransac window. */ + char * ransacSplineWindow_orig; /* Half width to use for ransac window original value given at command line. */ + const char *ransacSplineWindow_help; /* Half width to use for ransac window help description. */ + int ransacSplineDegree_arg; /* Degree of spline to use. */ + char * ransacSplineDegree_orig; /* Degree of spline to use original value given at command line. */ + const char *ransacSplineDegree_help; /* Degree of spline to use help description. */ + int ransacSpline_arg; /* Whether to use splines. */ + char * ransacSpline_orig; /* Whether to use splines original value given at command line. */ + const char *ransacSpline_help; /* Whether to use splines help description. */ + int ransacLine_arg; /* Whether to use lines. */ + char * ransacLine_orig; /* Whether to use lines original value given at command line. */ + const char *ransacLine_help; /* Whether to use lines help description. */ + float ransacSplineStep_arg; /* Step to use when pixelzing spline in ransac. */ + char * ransacSplineStep_orig; /* Step to use when pixelzing spline in ransac original value given at command line. */ + const char *ransacSplineStep_help; /* Step to use when pixelzing spline in ransac help description. */ + float overlapThreshold_arg; /* Overlap threshold to use for grouping of bounding boxes. */ + char * overlapThreshold_orig; /* Overlap threshold to use for grouping of bounding boxes original value given at command line. */ + const char *overlapThreshold_help; /* Overlap threshold to use for grouping of bounding boxes help description. */ + float localizeAngleThreshold_arg; /* Angle threshold used for localization (cosine, 1: most restrictive, 0: most liberal). */ + char * localizeAngleThreshold_orig; /* Angle threshold used for localization (cosine, 1: most restrictive, 0: most liberal) original value given at command line. */ + const char *localizeAngleThreshold_help; /* Angle threshold used for localization (cosine, 1: most restrictive, 0: most liberal) help description. */ + int localizeNumLinePixels_arg; /* Number of pixels to go in normal direction for localization. */ + char * localizeNumLinePixels_orig; /* Number of pixels to go in normal direction for localization original value given at command line. */ + const char *localizeNumLinePixels_help; /* Number of pixels to go in normal direction for localization help description. */ + float extendAngleThreshold_arg; /* Angle threshold used for extending (cosine, 1: most restrictive, 0: most liberal). */ + char * extendAngleThreshold_orig; /* Angle threshold used for extending (cosine, 1: most restrictive, 0: most liberal) original value given at command line. */ + const char *extendAngleThreshold_help; /* Angle threshold used for extending (cosine, 1: most restrictive, 0: most liberal) help description. */ + float extendMeanDirAngleThreshold_arg; /* Angle threshold from mean direction used for extending (cosine, 1: most restrictive, 0: most liberal). */ + char * extendMeanDirAngleThreshold_orig; /* Angle threshold from mean direction used for extending (cosine, 1: most restrictive, 0: most liberal) original value given at command line. */ + const char *extendMeanDirAngleThreshold_help; /* Angle threshold from mean direction used for extending (cosine, 1: most restrictive, 0: most liberal) help description. */ + int extendLinePixelsTangent_arg; /* Number of pixels to go in tangent direction for extending. */ + char * extendLinePixelsTangent_orig; /* Number of pixels to go in tangent direction for extending original value given at command line. */ + const char *extendLinePixelsTangent_help; /* Number of pixels to go in tangent direction for extending help description. */ + int extendLinePixelsNormal_arg; /* Number of pixels to go in tangent direction for extending. */ + char * extendLinePixelsNormal_orig; /* Number of pixels to go in tangent direction for extending original value given at command line. */ + const char *extendLinePixelsNormal_help; /* Number of pixels to go in tangent direction for extending help description. */ + float extendContThreshold_arg; /* Threhsold used for stopping the extending process (higher -> less extending). */ + char * extendContThreshold_orig; /* Threhsold used for stopping the extending process (higher -> less extending) original value given at command line. */ + const char *extendContThreshold_help; /* Threhsold used for stopping the extending process (higher -> less extending) help description. */ + int extendDeviationThreshold_arg; /* Stop extending when number of deviating points exceeds this threshold. */ + char * extendDeviationThreshold_orig; /* Stop extending when number of deviating points exceeds this threshold original value given at command line. */ + const char *extendDeviationThreshold_help; /* Stop extending when number of deviating points exceeds this threshold help description. */ + int extendRectTop_arg; /* Top point for extension bounding box. */ + char * extendRectTop_orig; /* Top point for extension bounding box original value given at command line. */ + const char *extendRectTop_help; /* Top point for extension bounding box help description. */ + int extendRectBottom_arg; /* Bottom point for extension bounding box. */ + char * extendRectBottom_orig; /* Bottom point for extension bounding box original value given at command line. */ + const char *extendRectBottom_help; /* Bottom point for extension bounding box help description. */ + float extendIPMAngleThreshold_arg; /* Angle threshold used for extending (cosine, 1: most restrictive, 0: most liberal). */ + char * extendIPMAngleThreshold_orig; /* Angle threshold used for extending (cosine, 1: most restrictive, 0: most liberal) original value given at command line. */ + const char *extendIPMAngleThreshold_help; /* Angle threshold used for extending (cosine, 1: most restrictive, 0: most liberal) help description. */ + float extendIPMMeanDirAngleThreshold_arg; /* Angle threshold from mean direction used for extending (cosine, 1: most restrictive, 0: most liberal). */ + char * extendIPMMeanDirAngleThreshold_orig; /* Angle threshold from mean direction used for extending (cosine, 1: most restrictive, 0: most liberal) original value given at command line. */ + const char *extendIPMMeanDirAngleThreshold_help; /* Angle threshold from mean direction used for extending (cosine, 1: most restrictive, 0: most liberal) help description. */ + int extendIPMLinePixelsTangent_arg; /* Number of pixels to go in tangent direction for extending. */ + char * extendIPMLinePixelsTangent_orig; /* Number of pixels to go in tangent direction for extending original value given at command line. */ + const char *extendIPMLinePixelsTangent_help; /* Number of pixels to go in tangent direction for extending help description. */ + int extendIPMLinePixelsNormal_arg; /* Number of pixels to go in tangent direction for extending. */ + char * extendIPMLinePixelsNormal_orig; /* Number of pixels to go in tangent direction for extending original value given at command line. */ + const char *extendIPMLinePixelsNormal_help; /* Number of pixels to go in tangent direction for extending help description. */ + float extendIPMContThreshold_arg; /* Threhsold used for stopping the extending process (higher -> less extending). */ + char * extendIPMContThreshold_orig; /* Threhsold used for stopping the extending process (higher -> less extending) original value given at command line. */ + const char *extendIPMContThreshold_help; /* Threhsold used for stopping the extending process (higher -> less extending) help description. */ + int extendIPMDeviationThreshold_arg; /* Stop extending when number of deviating points exceeds this threshold. */ + char * extendIPMDeviationThreshold_orig; /* Stop extending when number of deviating points exceeds this threshold original value given at command line. */ + const char *extendIPMDeviationThreshold_help; /* Stop extending when number of deviating points exceeds this threshold help description. */ + int extendIPMRectTop_arg; /* Top point for extension bounding box. */ + char * extendIPMRectTop_orig; /* Top point for extension bounding box original value given at command line. */ + const char *extendIPMRectTop_help; /* Top point for extension bounding box help description. */ + int extendIPMRectBottom_arg; /* Bottom point for extension bounding box. */ + char * extendIPMRectBottom_orig; /* Bottom point for extension bounding box original value given at command line. */ + const char *extendIPMRectBottom_help; /* Bottom point for extension bounding box help description. */ + int splineScoreJitter_arg; /* Number of pixels to go around the spline to compute score. */ + char * splineScoreJitter_orig; /* Number of pixels to go around the spline to compute score original value given at command line. */ + const char *splineScoreJitter_help; /* Number of pixels to go around the spline to compute score help description. */ + float splineScoreLengthRatio_arg; /* Ratio of spline length to use. */ + char * splineScoreLengthRatio_orig; /* Ratio of spline length to use original value given at command line. */ + const char *splineScoreLengthRatio_help; /* Ratio of spline length to use help description. */ + float splineScoreAngleRatio_arg; /* Ratio of spline angle to use. */ + char * splineScoreAngleRatio_orig; /* Ratio of spline angle to use original value given at command line. */ + const char *splineScoreAngleRatio_help; /* Ratio of spline angle to use help description. */ + float splineScoreStep_arg; /* Step to use for spline score computation. */ + char * splineScoreStep_orig; /* Step to use for spline score computation original value given at command line. */ + const char *splineScoreStep_help; /* Step to use for spline score computation help description. */ + int splineTrackingNumAbsentFrames_arg; /* number of frames the track is allowed to be absent before deleting it. */ + char * splineTrackingNumAbsentFrames_orig; /* number of frames the track is allowed to be absent before deleting it original value given at command line. */ + const char *splineTrackingNumAbsentFrames_help; /* number of frames the track is allowed to be absent before deleting it help description. */ + int splineTrackingNumSeenFrames_arg; /* number of frames before considering the track good. */ + char * splineTrackingNumSeenFrames_orig; /* number of frames before considering the track good original value given at command line. */ + const char *splineTrackingNumSeenFrames_help; /* number of frames before considering the track good help description. */ + float mergeSplineThetaThreshold_arg; /* Angle threshold for merging splines (radians). */ + char * mergeSplineThetaThreshold_orig; /* Angle threshold for merging splines (radians) original value given at command line. */ + const char *mergeSplineThetaThreshold_help; /* Angle threshold for merging splines (radians) help description. */ + float mergeSplineRThreshold_arg; /* R threshold (distance from origin) for merginn splines. */ + char * mergeSplineRThreshold_orig; /* R threshold (distance from origin) for merginn splines original value given at command line. */ + const char *mergeSplineRThreshold_help; /* R threshold (distance from origin) for merginn splines help description. */ + float mergeSplineMeanThetaThreshold_arg; /* Mean Angle threshold for merging splines (radians). */ + char * mergeSplineMeanThetaThreshold_orig; /* Mean Angle threshold for merging splines (radians) original value given at command line. */ + const char *mergeSplineMeanThetaThreshold_help; /* Mean Angle threshold for merging splines (radians) help description. */ + float mergeSplineMeanRThreshold_arg; /* Mean R threshold (distance from origin) for merginn splines. */ + char * mergeSplineMeanRThreshold_orig; /* Mean R threshold (distance from origin) for merginn splines original value given at command line. */ + const char *mergeSplineMeanRThreshold_help; /* Mean R threshold (distance from origin) for merginn splines help description. */ + float mergeSplineCentroidThreshold_arg; /* Distance threshold between spline cetroids for merging. */ + char * mergeSplineCentroidThreshold_orig; /* Distance threshold between spline cetroids for merging original value given at command line. */ + const char *mergeSplineCentroidThreshold_help; /* Distance threshold between spline cetroids for merging help description. */ + int lineTrackingNumAbsentFrames_arg; /* number of frames the track is allowed to be absent before deleting it. */ + char * lineTrackingNumAbsentFrames_orig; /* number of frames the track is allowed to be absent before deleting it original value given at command line. */ + const char *lineTrackingNumAbsentFrames_help; /* number of frames the track is allowed to be absent before deleting it help description. */ + int lineTrackingNumSeenFrames_arg; /* number of frames before considering the track good. */ + char * lineTrackingNumSeenFrames_orig; /* number of frames before considering the track good original value given at command line. */ + const char *lineTrackingNumSeenFrames_help; /* number of frames before considering the track good help description. */ + float mergeLineThetaThreshold_arg; /* Angle threshold for merging lines (radians). */ + char * mergeLineThetaThreshold_orig; /* Angle threshold for merging lines (radians) original value given at command line. */ + const char *mergeLineThetaThreshold_help; /* Angle threshold for merging lines (radians) help description. */ + float mergeLineRThreshold_arg; /* R threshold (distance from origin) for merging lines. */ + char * mergeLineRThreshold_orig; /* R threshold (distance from origin) for merging lines original value given at command line. */ + const char *mergeLineRThreshold_help; /* R threshold (distance from origin) for merging lines help description. */ + int numStrips_arg; /* Number of horizontal strips to divide the image to. */ + char * numStrips_orig; /* Number of horizontal strips to divide the image to original value given at command line. */ + const char *numStrips_help; /* Number of horizontal strips to divide the image to help description. */ + int checkSplines_arg; /* Whtethet to check splines or not. */ + char * checkSplines_orig; /* Whtethet to check splines or not original value given at command line. */ + const char *checkSplines_help; /* Whtethet to check splines or not help description. */ + float checkSplinesCurvenessThreshold_arg; /* Curveness Threshold for checking splines. */ + char * checkSplinesCurvenessThreshold_orig; /* Curveness Threshold for checking splines original value given at command line. */ + const char *checkSplinesCurvenessThreshold_help; /* Curveness Threshold for checking splines help description. */ + float checkSplinesLengthThreshold_arg; /* Length Threshold for checking splines. */ + char * checkSplinesLengthThreshold_orig; /* Length Threshold for checking splines original value given at command line. */ + const char *checkSplinesLengthThreshold_help; /* Length Threshold for checking splines help description. */ + float checkSplinesThetaDiffThreshold_arg; /* ThetaDiff Threshold for checking splines. */ + char * checkSplinesThetaDiffThreshold_orig; /* ThetaDiff Threshold for checking splines original value given at command line. */ + const char *checkSplinesThetaDiffThreshold_help; /* ThetaDiff Threshold for checking splines help description. */ + float checkSplinesThetaThreshold_arg; /* ThetaThreshold Threshold for checking splines. */ + char * checkSplinesThetaThreshold_orig; /* ThetaThreshold Threshold for checking splines original value given at command line. */ + const char *checkSplinesThetaThreshold_help; /* ThetaThreshold Threshold for checking splines help description. */ + int checkIPMSplines_arg; /* Whtethet to check IPM splines or not. */ + char * checkIPMSplines_orig; /* Whtethet to check IPM splines or not original value given at command line. */ + const char *checkIPMSplines_help; /* Whtethet to check IPM splines or not help description. */ + float checkIPMSplinesCurvenessThreshold_arg; /* Curveness Threshold for checking splines. */ + char * checkIPMSplinesCurvenessThreshold_orig; /* Curveness Threshold for checking splines original value given at command line. */ + const char *checkIPMSplinesCurvenessThreshold_help; /* Curveness Threshold for checking splines help description. */ + float checkIPMSplinesLengthThreshold_arg; /* Length Threshold for checking splines. */ + char * checkIPMSplinesLengthThreshold_orig; /* Length Threshold for checking splines original value given at command line. */ + const char *checkIPMSplinesLengthThreshold_help; /* Length Threshold for checking splines help description. */ + float checkIPMSplinesThetaDiffThreshold_arg; /* ThetaDiff Threshold for checking splines. */ + char * checkIPMSplinesThetaDiffThreshold_orig; /* ThetaDiff Threshold for checking splines original value given at command line. */ + const char *checkIPMSplinesThetaDiffThreshold_help; /* ThetaDiff Threshold for checking splines help description. */ + float checkIPMSplinesThetaThreshold_arg; /* ThetaThreshold Threshold for checking splines. */ + char * checkIPMSplinesThetaThreshold_orig; /* ThetaThreshold Threshold for checking splines original value given at command line. */ + const char *checkIPMSplinesThetaThreshold_help; /* ThetaThreshold Threshold for checking splines help description. */ + float finalSplineScoreThreshold_arg; /* Final Threshold for declaring a valid spline. */ + char * finalSplineScoreThreshold_orig; /* Final Threshold for declaring a valid spline original value given at command line. */ + const char *finalSplineScoreThreshold_help; /* Final Threshold for declaring a valid spline help description. */ + int useGroundPlane_arg; /* Use groudn plane or not when sending to map. */ + char * useGroundPlane_orig; /* Use groudn plane or not when sending to map original value given at command line. */ + const char *useGroundPlane_help; /* Use groudn plane or not when sending to map help description. */ + int checkColor_arg; /* Whether to check colors or not. */ + char * checkColor_orig; /* Whether to check colors or not original value given at command line. */ + const char *checkColor_help; /* Whether to check colors or not help description. */ + int checkColorWindow_arg; /* Size of window to use. */ + char * checkColorWindow_orig; /* Size of window to use original value given at command line. */ + const char *checkColorWindow_help; /* Size of window to use help description. */ + int checkColorNumBins_arg; /* Number of bins to use. */ + char * checkColorNumBins_orig; /* Number of bins to use original value given at command line. */ + const char *checkColorNumBins_help; /* Number of bins to use help description. */ + float checkColorNumYellowMin_arg; /* Min ratio of yellow points. */ + char * checkColorNumYellowMin_orig; /* Min ratio of yellow points original value given at command line. */ + const char *checkColorNumYellowMin_help; /* Min ratio of yellow points help description. */ + float checkColorRGMin_arg; /* Min RG diff. */ + char * checkColorRGMin_orig; /* Min RG diff original value given at command line. */ + const char *checkColorRGMin_help; /* Min RG diff help description. */ + float checkColorRGMax_arg; /* Max RG diff. */ + char * checkColorRGMax_orig; /* Max RG diff original value given at command line. */ + const char *checkColorRGMax_help; /* Max RG diff help description. */ + float checkColorGBMin_arg; /* Min GB diff. */ + char * checkColorGBMin_orig; /* Min GB diff original value given at command line. */ + const char *checkColorGBMin_help; /* Min GB diff help description. */ + float checkColorRBMin_arg; /* Min RB diff. */ + char * checkColorRBMin_orig; /* Min RB diff original value given at command line. */ + const char *checkColorRBMin_help; /* Min RB diff help description. */ + float checkColorRBFThreshold_arg; /* RBF Threshold. */ + char * checkColorRBFThreshold_orig; /* RBF Threshold original value given at command line. */ + const char *checkColorRBFThreshold_help; /* RBF Threshold help description. */ + int checkColorRBF_arg; /* Whether to use RBF or not. */ + char * checkColorRBF_orig; /* Whether to use RBF or not original value given at command line. */ + const char *checkColorRBF_help; /* Whether to use RBF or not help description. */ + int ipmWindowClear_arg; /* Whether to clear part of the IPM image. */ + char * ipmWindowClear_orig; /* Whether to clear part of the IPM image original value given at command line. */ + const char *ipmWindowClear_help; /* Whether to clear part of the IPM image help description. */ + int ipmWindowLeft_arg; /* Left corrdinate of window to keep in IPM. */ + char * ipmWindowLeft_orig; /* Left corrdinate of window to keep in IPM original value given at command line. */ + const char *ipmWindowLeft_help; /* Left corrdinate of window to keep in IPM help description. */ + int ipmWindowRight_arg; /* Left corrdinate of window to keep in IPM. */ + char * ipmWindowRight_orig; /* Left corrdinate of window to keep in IPM original value given at command line. */ + const char *ipmWindowRight_help; /* Left corrdinate of window to keep in IPM help description. */ + int checkLaneWidth_arg; /* Whether to check lane width or not. */ + char * checkLaneWidth_orig; /* Whether to check lane width or not original value given at command line. */ + const char *checkLaneWidth_help; /* Whether to check lane width or not help description. */ + float checkLaneWidthMean_arg; /* Mean of lane width to look for. */ + char * checkLaneWidthMean_orig; /* Mean of lane width to look for original value given at command line. */ + const char *checkLaneWidthMean_help; /* Mean of lane width to look for help description. */ + float checkLaneWidthStd_arg; /* Std deviation of lane width to look for. */ + char * checkLaneWidthStd_orig; /* Std deviation of lane width to look for original value given at command line. */ + const char *checkLaneWidthStd_help; /* Std deviation of lane width to look for help description. */ + + int help_given ; /* Whether help was given. */ + int version_given ; /* Whether version was given. */ + int ipmWidth_given ; /* Whether ipmWidth was given. */ + int ipmHeight_given ; /* Whether ipmHeight was given. */ + int ipmTop_given ; /* Whether ipmTop was given. */ + int ipmLeft_given ; /* Whether ipmLeft was given. */ + int ipmRight_given ; /* Whether ipmRight was given. */ + int ipmBottom_given ; /* Whether ipmBottom was given. */ + int ipmInterpolation_given ; /* Whether ipmInterpolation was given. */ + int lineWidth_given ; /* Whether lineWidth was given. */ + int lineHeight_given ; /* Whether lineHeight was given. */ + int kernelWidth_given ; /* Whether kernelWidth was given. */ + int kernelHeight_given ; /* Whether kernelHeight was given. */ + int lowerQuantile_given ; /* Whether lowerQuantile was given. */ + int localMaxima_given ; /* Whether localMaxima was given. */ + int groupingType_given ; /* Whether groupingType was given. */ + int binarize_given ; /* Whether binarize was given. */ + int detectionThreshold_given ; /* Whether detectionThreshold was given. */ + int smoothScores_given ; /* Whether smoothScores was given. */ + int rMin_given ; /* Whether rMin was given. */ + int rMax_given ; /* Whether rMax was given. */ + int rStep_given ; /* Whether rStep was given. */ + int thetaMin_given ; /* Whether thetaMin was given. */ + int thetaMax_given ; /* Whether thetaMax was given. */ + int thetaStep_given ; /* Whether thetaStep was given. */ + int ipmVpPortion_given ; /* Whether ipmVpPortion was given. */ + int getEndPoints_given ; /* Whether getEndPoints was given. */ + int group_given ; /* Whether group was given. */ + int groupThreshold_given ; /* Whether groupThreshold was given. */ + int ransac_given ; /* Whether ransac was given. */ + int ransacLineNumSamples_given ; /* Whether ransacLineNumSamples was given. */ + int ransacLineNumIterations_given ; /* Whether ransacLineNumIterations was given. */ + int ransacLineNumGoodFit_given ; /* Whether ransacLineNumGoodFit was given. */ + int ransacLineThreshold_given ; /* Whether ransacLineThreshold was given. */ + int ransacLineScoreThreshold_given ; /* Whether ransacLineScoreThreshold was given. */ + int ransacLineBinarize_given ; /* Whether ransacLineBinarize was given. */ + int ransacLineWindow_given ; /* Whether ransacLineWindow was given. */ + int ransacSplineNumSamples_given ; /* Whether ransacSplineNumSamples was given. */ + int ransacSplineNumIterations_given ; /* Whether ransacSplineNumIterations was given. */ + int ransacSplineNumGoodFit_given ; /* Whether ransacSplineNumGoodFit was given. */ + int ransacSplineThreshold_given ; /* Whether ransacSplineThreshold was given. */ + int ransacSplineScoreThreshold_given ; /* Whether ransacSplineScoreThreshold was given. */ + int ransacSplineBinarize_given ; /* Whether ransacSplineBinarize was given. */ + int ransacSplineWindow_given ; /* Whether ransacSplineWindow was given. */ + int ransacSplineDegree_given ; /* Whether ransacSplineDegree was given. */ + int ransacSpline_given ; /* Whether ransacSpline was given. */ + int ransacLine_given ; /* Whether ransacLine was given. */ + int ransacSplineStep_given ; /* Whether ransacSplineStep was given. */ + int overlapThreshold_given ; /* Whether overlapThreshold was given. */ + int localizeAngleThreshold_given ; /* Whether localizeAngleThreshold was given. */ + int localizeNumLinePixels_given ; /* Whether localizeNumLinePixels was given. */ + int extendAngleThreshold_given ; /* Whether extendAngleThreshold was given. */ + int extendMeanDirAngleThreshold_given ; /* Whether extendMeanDirAngleThreshold was given. */ + int extendLinePixelsTangent_given ; /* Whether extendLinePixelsTangent was given. */ + int extendLinePixelsNormal_given ; /* Whether extendLinePixelsNormal was given. */ + int extendContThreshold_given ; /* Whether extendContThreshold was given. */ + int extendDeviationThreshold_given ; /* Whether extendDeviationThreshold was given. */ + int extendRectTop_given ; /* Whether extendRectTop was given. */ + int extendRectBottom_given ; /* Whether extendRectBottom was given. */ + int extendIPMAngleThreshold_given ; /* Whether extendIPMAngleThreshold was given. */ + int extendIPMMeanDirAngleThreshold_given ; /* Whether extendIPMMeanDirAngleThreshold was given. */ + int extendIPMLinePixelsTangent_given ; /* Whether extendIPMLinePixelsTangent was given. */ + int extendIPMLinePixelsNormal_given ; /* Whether extendIPMLinePixelsNormal was given. */ + int extendIPMContThreshold_given ; /* Whether extendIPMContThreshold was given. */ + int extendIPMDeviationThreshold_given ; /* Whether extendIPMDeviationThreshold was given. */ + int extendIPMRectTop_given ; /* Whether extendIPMRectTop was given. */ + int extendIPMRectBottom_given ; /* Whether extendIPMRectBottom was given. */ + int splineScoreJitter_given ; /* Whether splineScoreJitter was given. */ + int splineScoreLengthRatio_given ; /* Whether splineScoreLengthRatio was given. */ + int splineScoreAngleRatio_given ; /* Whether splineScoreAngleRatio was given. */ + int splineScoreStep_given ; /* Whether splineScoreStep was given. */ + int splineTrackingNumAbsentFrames_given ; /* Whether splineTrackingNumAbsentFrames was given. */ + int splineTrackingNumSeenFrames_given ; /* Whether splineTrackingNumSeenFrames was given. */ + int mergeSplineThetaThreshold_given ; /* Whether mergeSplineThetaThreshold was given. */ + int mergeSplineRThreshold_given ; /* Whether mergeSplineRThreshold was given. */ + int mergeSplineMeanThetaThreshold_given ; /* Whether mergeSplineMeanThetaThreshold was given. */ + int mergeSplineMeanRThreshold_given ; /* Whether mergeSplineMeanRThreshold was given. */ + int mergeSplineCentroidThreshold_given ; /* Whether mergeSplineCentroidThreshold was given. */ + int lineTrackingNumAbsentFrames_given ; /* Whether lineTrackingNumAbsentFrames was given. */ + int lineTrackingNumSeenFrames_given ; /* Whether lineTrackingNumSeenFrames was given. */ + int mergeLineThetaThreshold_given ; /* Whether mergeLineThetaThreshold was given. */ + int mergeLineRThreshold_given ; /* Whether mergeLineRThreshold was given. */ + int numStrips_given ; /* Whether numStrips was given. */ + int checkSplines_given ; /* Whether checkSplines was given. */ + int checkSplinesCurvenessThreshold_given ; /* Whether checkSplinesCurvenessThreshold was given. */ + int checkSplinesLengthThreshold_given ; /* Whether checkSplinesLengthThreshold was given. */ + int checkSplinesThetaDiffThreshold_given ; /* Whether checkSplinesThetaDiffThreshold was given. */ + int checkSplinesThetaThreshold_given ; /* Whether checkSplinesThetaThreshold was given. */ + int checkIPMSplines_given ; /* Whether checkIPMSplines was given. */ + int checkIPMSplinesCurvenessThreshold_given ; /* Whether checkIPMSplinesCurvenessThreshold was given. */ + int checkIPMSplinesLengthThreshold_given ; /* Whether checkIPMSplinesLengthThreshold was given. */ + int checkIPMSplinesThetaDiffThreshold_given ; /* Whether checkIPMSplinesThetaDiffThreshold was given. */ + int checkIPMSplinesThetaThreshold_given ; /* Whether checkIPMSplinesThetaThreshold was given. */ + int finalSplineScoreThreshold_given ; /* Whether finalSplineScoreThreshold was given. */ + int useGroundPlane_given ; /* Whether useGroundPlane was given. */ + int checkColor_given ; /* Whether checkColor was given. */ + int checkColorWindow_given ; /* Whether checkColorWindow was given. */ + int checkColorNumBins_given ; /* Whether checkColorNumBins was given. */ + int checkColorNumYellowMin_given ; /* Whether checkColorNumYellowMin was given. */ + int checkColorRGMin_given ; /* Whether checkColorRGMin was given. */ + int checkColorRGMax_given ; /* Whether checkColorRGMax was given. */ + int checkColorGBMin_given ; /* Whether checkColorGBMin was given. */ + int checkColorRBMin_given ; /* Whether checkColorRBMin was given. */ + int checkColorRBFThreshold_given ; /* Whether checkColorRBFThreshold was given. */ + int checkColorRBF_given ; /* Whether checkColorRBF was given. */ + int ipmWindowClear_given ; /* Whether ipmWindowClear was given. */ + int ipmWindowLeft_given ; /* Whether ipmWindowLeft was given. */ + int ipmWindowRight_given ; /* Whether ipmWindowRight was given. */ + int checkLaneWidth_given ; /* Whether checkLaneWidth was given. */ + int checkLaneWidthMean_given ; /* Whether checkLaneWidthMean was given. */ + int checkLaneWidthStd_given ; /* Whether checkLaneWidthStd was given. */ + +} ; + +extern const char *LaneDetectorParserInfo_purpose; +extern const char *LaneDetectorParserInfo_usage; +extern const char *LaneDetectorParserInfo_help[]; + +int LaneDetectorParser (int argc, char * const *argv, + struct LaneDetectorParserInfo *args_info); +int LaneDetectorParser2 (int argc, char * const *argv, + struct LaneDetectorParserInfo *args_info, + int override, int initialize, int check_required); +int LaneDetectorParser_file_save(const char *filename, + struct LaneDetectorParserInfo *args_info); + +void LaneDetectorParser_print_help(void); +void LaneDetectorParser_print_version(void); + +void LaneDetectorParser_init (struct LaneDetectorParserInfo *args_info); +void LaneDetectorParser_free (struct LaneDetectorParserInfo *args_info); + +int LaneDetectorParser_configfile (char * const filename, + struct LaneDetectorParserInfo *args_info, + int override, int initialize, int check_required); + +int LaneDetectorParser_required (struct LaneDetectorParserInfo *args_info, + const char *prog_name); + + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* LANEDETECTOROPT_H */ diff --git a/src/LaneDetectorOpt.o b/src/LaneDetectorOpt.o new file mode 100644 index 0000000000000000000000000000000000000000..3e33ea376376a8e182e8784f277e27107fbff458 GIT binary patch literal 187592 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 literal 0 HcmV?d00001 diff --git a/src/Lanes-mode2.conf b/src/Lanes-mode2.conf new file mode 100644 index 0000000..21bd5a2 --- /dev/null +++ b/src/Lanes-mode2.conf @@ -0,0 +1,175 @@ +#settings for stop line perceptor + +#128 +ipmWidth = 160 #160#320#160 +#96 +ipmHeight = 120 #120#240#120 + +ipmLeft = 140 #100 #80 #90 #115 #140 #50 #85 #100 #85 +ipmRight = 500 #410 #420 #410 #400 #460 #500 #530 #500 #590 #550 +ipmTop = 220 #220 #220 #200 #50 +ipmBottom = 360 #360 #350 #380 + +#0 bilinear, 1: NN +ipmInterpolation = 0 + +ipmVpPortion = 0 #.09 #0.06 #.05 #.125 #.2 #.15 #.075#0.1 #.05 + +lineWidth = 2000 +lineHeight = 304.8 + +kernelWidth = 2 +kernelHeight = 2 + +#changed to detect center broken lines .98 +lowerQuantile = .985 #.98 #.975 #.98 #.985 #.99 #.985#.99#.98#.99#.98#0.975 #0.98 #0.985 + +localMaxima = 1 + +#grouping type: 0 for HV lines, and 1 for Hough lines +groupingType = 0#1#0#1#0 + +#center broken lines: 1 +binarize = 0 #1#0#1 #0 + +#cetner broken lines 4 +detectionThreshold = 6 #6 #5 #4 #2 #2 #4 #5 #6 #5 #7 #10 #.5 #4 #5 #2#0.4#0.6#.8#30#1.8#30#3.5#2 #.15 #.4#.15#.0008#0#.008#2 #0.004#0.005#0.0015#0.012#0.025 + +smoothScores = 1 #0#0#1 + +##Hough Transform settings +rMin = 10#0 +rMax = 150#300#150#120 +rStep = 2#2#3#1 +thetaMin = -45#-30#-20#-40#80#85 +thetaMax = 45#30#20#40#100#95 +thetaStep = 2#3#2#1 + + +getEndPoints = 0 + +group = 1 +groupThreshold = 15 #25 #25 #10 #15 #20#25#20#15 + +#RANSAC options +ransac = 1 + +ransacLineNumSamples = 4 +ransacLineNumIterations = 25 #40 #25 #40 #50 +ransacLineNumGoodFit = 10 #15#10 +ransacLineThreshold = .2 +ransacLineScoreThreshold = 0 #4 #6#4#2.5#5#1#1.5#1.5#3#1.5#3 #20 without smoothing and with binarizing + #15 with smoothing and with binarizing: lines + #1.5 without binarizing or smoothing: lines + #splines: 3 wihtout binarizing or smoothing + #splines: 5 with spline length in score +ransacLineBinarize = 0 +ransacLineWindow = 8 #15 #15 #8 #15 + +ransacSplineNumSamples = 4 +ransacSplineNumIterations = 75 #60 #50 +ransacSplineNumGoodFit = 10 #15#10 +ransacSplineThreshold = .2 +ransacSplineScoreThreshold = 0 #4 #6#4#2.5#5#1#1.5#1.5#3#1.5#3 #20 without smoothing and with binarizing + #15 with smoothing and with binarizing: lines + #1.5 without binarizing or smoothing: lines + #splines: 3 wihtout binarizing or smoothing + #splines: 5 with spline length in score +ransacSplineBinarize = 0 +ransacSplineWindow = 6 #10 #15 #10 #15 #8 #15 + +ransacSpline = 1 +ransacSplineDegree = 3#2#3 +ransacLine = 1 #1 + +ransacSplineStep = .1 + +#spline scores +splineScoreJitter = 2 #2 #2 +splineScoreLengthRatio = 1.5 #1.5 #1.2 #1 #.4 #0.6 #.5 +splineScoreAngleRatio = 1 #1.2 #1.7 #1.5 #1 #.9 #0.8 #.8 +splineScoreStep = .01 #.02 + +#grouping of bounding boxes +overlapThreshold = .8 #.3 #0.5 + +#localization of points +localizeAngleThreshold = .9 #.7#.7 +localizeNumLinePixels = 20 + + +#extension of points +extendAngleThreshold = .86 #.7 +extendMeanDirAngleThreshold = .95 #.86 #.86 #.7 +extendLinePixelsTangent = 10 #5 +extendLinePixelsNormal = 20 +extendContThreshold = .35 #.3 #.25 #.25 #.2 #.1 +extendDeviationThreshold = 2 #1 #2 +extendRectTop = 200 +extendRectBottom = 380 + +extendIPMAngleThreshold = .95 #.9 +extendIPMMeanDirAngleThreshold = .86 #.86 #.7 +extendIPMLinePixelsTangent = 5 #10 #5 +extendIPMLinePixelsNormal = 10 +extendIPMContThreshold = .05 #0# .05 #.1 #.35 #.3 #.25 #.25 #.2 #.1 +extendIPMDeviationThreshold = 2 #1 #2 +extendIPMRectTop = 0 +extendIPMRectBottom = 118 + + +#tracking +splineTrackingNumAbsentFrames = 3 #3 +splineTrackingNumSeenFrames = 2 #5 + +#spline merging +mergeSplineThetaThreshold = .3 #52 #30 deg +mergeSplineRThreshold = 15 +mergeSplineMeanThetaThreshold = .2#52 #30 deg +mergeSplineMeanRThreshold = 20#15 +mergeSplineCentroidThreshold = 80 #50 + +#line tracking +lineTrackingNumAbsentFrames = 2 #3 +lineTrackingNumSeenFrames = 2 #5 + +#spline merging +mergeLineThetaThreshold = .3#52 #30 deg +mergeLineRThreshold = 15 + +numStrips = 1 + +checkSplines = 1 +checkSplinesCurvenessThreshold = .85 #.80 #.9 #.93 +checkSplinesLengthThreshold = 40 #30 +checkSplinesThetaDiffThreshold = .1 +checkSplinesThetaThreshold = 1.22 #70 deg + +checkIPMSplines = 1 +checkIPMSplinesCurvenessThreshold = .8 #.85 +checkIPMSplinesLengthThreshold = 50 #30 +checkIPMSplinesThetaDiffThreshold = .12 #.1 +checkIPMSplinesThetaThreshold = 1.22 #1.4 #1.4->80deg 1.22->70deg + +finalSplineScoreThreshold = 0 + +useGroundPlane = 0 + +checkColor = 0 +checkColorWindow = 3 +checkColorNumBins = 16 +checkColorNumYellowMin = .3 +checkColorRGMin = 1 +checkColorRGMax = 40 +checkColorGBMin = 10 #15 +checkColorRBMin = 25 +checkColorRBFThreshold = -.1 +checkColorRBF = 1 + +ipmWindowClear = 1 +ipmWindowLeft = 60 +ipmWindowRight = 100 + +checkLaneWidth = 1 +checkLaneWidthMean = 30 #25 +checkLaneWidthStd = 10 #10 \ No newline at end of file diff --git a/src/Lanes.conf b/src/Lanes.conf new file mode 100644 index 0000000..8e5910b --- /dev/null +++ b/src/Lanes.conf @@ -0,0 +1,175 @@ +#settings for stop line perceptor + +#128 +ipmWidth = 160 #160#320#160 +#96 +ipmHeight = 120 #120#240#120 + +ipmLeft = 100 #80 #90 #115 #140 #50 #85 #100 #85 +ipmRight = 460 #500 #530 #500 #590 #550 +ipmTop = 220 #220 #200 #50 +ipmBottom = 350 #360 #350 #380 + +#0 bilinear, 1: NN +ipmInterpolation = 0 + +ipmVpPortion = 0 #.09 #0.06 #.05 #.125 #.2 #.15 #.075#0.1 #.05 + +lineWidth = 2000 +lineHeight = 304.8 + +kernelWidth = 2 +kernelHeight = 2 + +#changed to detect center broken lines .98 +lowerQuantile = .975 #.975 #.98 #.985 #.99 #.985#.99#.98#.99#.98#0.975 #0.98 #0.985 + +localMaxima = 1 + +#grouping type: 0 for HV lines, and 1 for Hough lines +groupingType = 0#1#0#1#0 + +#center broken lines: 1 +binarize = 0 #1#0#1 #0 + +#cetner broken lines 4 +detectionThreshold = 4 #2 #2 #4 #5 #6 #5 #7 #10 #.5 #4 #5 #2#0.4#0.6#.8#30#1.8#30#3.5#2 #.15 #.4#.15#.0008#0#.008#2 #0.004#0.005#0.0015#0.012#0.025 + +smoothScores = 1 #0#0#1 + +##Hough Transform settings +rMin = 10#0 +rMax = 150#300#150#120 +rStep = 2#2#3#1 +thetaMin = -45#-30#-20#-40#80#85 +thetaMax = 45#30#20#40#100#95 +thetaStep = 2#3#2#1 + + +getEndPoints = 0 + +group = 1 +groupThreshold = 10 #15 #20#25#20#15 + +#RANSAC options +ransac = 1 + +ransacLineNumSamples = 4 +ransacLineNumIterations = 40 #25 #40 #50 +ransacLineNumGoodFit = 10 #15#10 +ransacLineThreshold = .2 +ransacLineScoreThreshold = 0 #4 #6#4#2.5#5#1#1.5#1.5#3#1.5#3 #20 without smoothing and with binarizing + #15 with smoothing and with binarizing: lines + #1.5 without binarizing or smoothing: lines + #splines: 3 wihtout binarizing or smoothing + #splines: 5 with spline length in score +ransacLineBinarize = 0 +ransacLineWindow = 15 #8 #15 + +ransacSplineNumSamples = 4 +ransacSplineNumIterations = 40 #50 +ransacSplineNumGoodFit = 10 #15#10 +ransacSplineThreshold = .2 +ransacSplineScoreThreshold = 0 #4 #6#4#2.5#5#1#1.5#1.5#3#1.5#3 #20 without smoothing and with binarizing + #15 with smoothing and with binarizing: lines + #1.5 without binarizing or smoothing: lines + #splines: 3 wihtout binarizing or smoothing + #splines: 5 with spline length in score +ransacSplineBinarize = 0 +ransacSplineWindow = 10 #15 #8 #15 + +ransacSpline = 1 +ransacSplineDegree = 3#2#3 +ransacLine = 1 #1 + +ransacSplineStep = .1 + +#spline scores +splineScoreJitter = 2 #2 #2 +splineScoreLengthRatio = 1.5 #1.5 #1.2 #1 #.4 #0.6 #.5 +splineScoreAngleRatio = 1.2 #1.2 #1.7 #1.5 #1 #.9 #0.8 #.8 +splineScoreStep = .01 #.02 + +#grouping of bounding boxes +overlapThreshold = .3 #0.5 + +#localization of points +localizeAngleThreshold = .9 #.7#.7 +localizeNumLinePixels = 20 + + +#extension of points +extendAngleThreshold = .86 #.7 +extendMeanDirAngleThreshold = .95 #.86 #.86 #.7 +extendLinePixelsTangent = 10 #5 +extendLinePixelsNormal = 20 +extendContThreshold = .35 #.3 #.25 #.25 #.2 #.1 +extendDeviationThreshold = 2 #1 #2 +extendRectTop = 200 +extendRectBottom = 380 + +extendIPMAngleThreshold = .95 #.9 +extendIPMMeanDirAngleThreshold = .86 #.86 #.7 +extendIPMLinePixelsTangent = 5 #10 #5 +extendIPMLinePixelsNormal = 10 +extendIPMContThreshold = .05 #0# .05 #.1 #.35 #.3 #.25 #.25 #.2 #.1 +extendIPMDeviationThreshold = 2 #1 #2 +extendIPMRectTop = 0 +extendIPMRectBottom = 118 + + +#tracking +splineTrackingNumAbsentFrames = 3 +splineTrackingNumSeenFrames = 5 + +#spline merging +mergeSplineThetaThreshold = .3 #52 #30 deg +mergeSplineRThreshold = 15 +mergeSplineMeanThetaThreshold = .2#52 #30 deg +mergeSplineMeanRThreshold = 20#15 +mergeSplineCentroidThreshold = 80 #50 + +#line tracking +lineTrackingNumAbsentFrames = 2 #3 +lineTrackingNumSeenFrames = 3 #5 + +#spline merging +mergeLineThetaThreshold = .3#52 #30 deg +mergeLineRThreshold = 15 + +numStrips = 1 + +checkSplines = 1 +checkSplinesCurvenessThreshold = .80 #.9 #.93 +checkSplinesLengthThreshold = 30 +checkSplinesThetaDiffThreshold = .1 +checkSplinesThetaThreshold = 1.22 #70 deg + +checkIPMSplines = 1 +checkIPMSplinesCurvenessThreshold = .8 #.85 +checkIPMSplinesLengthThreshold = 30 +checkIPMSplinesThetaDiffThreshold = .1 +checkIPMSplinesThetaThreshold = 1.22 #1.4 #1.4->80deg 1.22->70deg + +finalSplineScoreThreshold = 0 + +useGroundPlane = 0 + +checkColor = 0 +checkColorWindow = 3 +checkColorNumBins = 16 +checkColorNumYellowMin = .3 +checkColorRGMin = 1 +checkColorRGMax = 40 +checkColorGBMin = 10 #15 +checkColorRBMin = 25 +checkColorRBFThreshold = -.1 +checkColorRBF = 1 + +ipmWindowClear = 0 +ipmWindowLeft = 50 #60 +ipmWindowRight = 110 #100 + +checkLaneWidth = 0 +checkLaneWidthMean = 25 +checkLaneWidthStd = 5 #10 diff --git a/src/Makefile b/src/Makefile new file mode 100755 index 0000000..35f930b --- /dev/null +++ b/src/Makefile @@ -0,0 +1,68 @@ +# +# Make file for LaneDetector +# + +# Author: Mohamed Aly +# Date: 10/7/2010 + +OCVFLAGS = `pkg-config --cflags opencv` +OCVLIBS = `pkg-config --libs opencv` -lstdc++ + +CPP = g++ + +# type of system? +LBITS = $(shell getconf LONG_BIT) +ifeq ($(LBITS),64) + # do 64 bit stuff here, like set some CFLAGS + SFX = 64 +else + SFX = 32 +endif + +# Add stop-line detection +SRCS += CameraInfoOpt.c LaneDetectorOpt.c cmdline.c LaneDetector.cc \ + InversePerspectiveMapping.cc mcv.cc main.cc +OBJECTS += CameraInfoOpt.o LaneDetectorOpt.o cmdline.o \ + LaneDetector.o InversePerspectiveMapping.o mcv.o \ + main.o +CFLAGS += $(OCVFLAGS) +LIBS += $(OCVLIBS) +BINARY = LaneDetector$(SFX) + +all: release + +release: $(OBJECTS) + $(CPP) $^ $(LDFLAGS) $(LIBS) $(CFLAGS) -O3 -o $(BINARY) + +debug: $(OBJECTS) + $(CPP) $^ $(LDFLAGS) $(LIBS) $(CFLAGS) -g -O0 -o $(BINARY) + +# Generate getopts header +LaneDetectorOpt.h: LaneDetectorOpt.ggo + gengetopt -i LaneDetectorOpt.ggo --conf-parser -F LaneDetectorOpt \ + --func-name=LaneDetectorParser --arg-struct-name=LaneDetectorParserInfo + +#get opts for cameraInfo and stopLinePerceptor +cameraInfoOpt.h: cameraInfoOpt.ggo + gengetopt -i cameraInfoOpt.ggo -F cameraInfoOpt \ + --func-name=cameraInfoParser \ + --arg-struct-name=CameraInfoParserInfo \ + --conf-parser + +cmdline.h: cmdline.ggo + gengetopt -i cmdline.ggo -u --conf-parser + +clean: + rm -f *.a $(OBJECTS) $(BINARY) + +.cc.o: + g++ $< $(CFLAGS) $(LIBS) $(LDFLAGS) -c -o $@ + +# headers and sources +.hh.cc: +.h.c: + +# generating gengetopt headers +cmdline.o: cmdline.h +CameraInfoOpt.o: CameraInfoOpt.h +LaneDetectorOpt.o: LaneDetectorOpt.h diff --git a/src/Stoplines.conf b/src/Stoplines.conf new file mode 100644 index 0000000..617f5b1 --- /dev/null +++ b/src/Stoplines.conf @@ -0,0 +1,174 @@ +#settings for stop line perceptor + +#128 +ipmWidth = 160 +#96 +ipmHeight = 120 +ipmLeft = 85 +ipmRight = 550 +ipmTop = 50 +ipmBottom = 380 #350 #300 for latest St-lukes data + +#0 bilinear, 1: NN +ipmInterpolation = 0 + +ipmVpPortion = .2#.075#0.1 #.05 + + +lineWidth = 2000 +lineHeight = 304.8 + +kernelWidth = 2 +kernelHeight = 2 + +lowerQuantile = .975#.98#0.975 #0.98 #0.985 + +localMaxima = 1 + +#grouping type: 0 for HV lines, and 1 for Hough lines +groupingType = 1#0#0#1#0 + +binarize = 0 #0 + +#0.0015 for outdoor scenes +#in the shop, it deosn't make sense, as there's no flat road ahead :) +# threshold: (~0.004) ~.15 for HV lines +# (~1) ~.4 for Hough +#3-11: ~2 for Hough, +#4-13: ~.8 +detectionThreshold = 1#.5#3.5#2 #.15 #.4#.15#.0008#0#.008#2 #0.004#0.005#0.0015#0.012#0.025 + +smoothScores = 0#1 + +##Hough Transform settings +rMin = 0#0 +rMax = 120#120 +rStep = 3#3#1 +thetaMin = 80#80#85 +thetaMax = 100#100#95 +thetaStep = 1#2#1 + + +getEndPoints = 0 + +group = 1 +groupThreshold = 15 + +#RANSAC options +ransac = 1 + +ransacLineNumSamples = 5 +ransacLineNumIterations = 10 +ransacLineNumGoodFit = 10 +ransacLineThreshold = .4#.2 +ransacLineScoreThreshold = .4#.25#.3 + #.25 for LF Stluke 7-10 +ransacLineBinarize = 0 +ransacLineWindow = 15 + +ransacSplineNumSamples = 5 +ransacSplineNumIterations = 10 +ransacSplineNumGoodFit = 10 +ransacSplineThreshold = .4#.2 +ransacSplineScoreThreshold = .4#.25#.3 + #.25 for LF Stluke 7-10 +ransacSplineBinarize = 0 +ransacSplineWindow = 15 + +ransacSpline = 0 +ransacSplineDegree = 2 +ransacLine = 1 + +ransacSplineStep = .1 + +#spline scores +splineScoreJitter = 2 +splineScoreLengthRatio = .5 +splineScoreAngleRatio = .8 +splineScoreStep = .1 + +#grouping of bounding boxes +overlapThreshold = 0.5 + +#localization of points +localizeAngleThreshold = .7 +localizeNumLinePixels = 20 + + +#extension of points +extendAngleThreshold = .7 +extendMeanDirAngleThreshold = .7 +extendLinePixelsTangent = 5 +extendLinePixelsNormal = 20 +extendContThreshold = .1 +extendDeviationThreshold = 2 +extendRectTop = 200 +extendRectBottom = 380 + +extendIPMAngleThreshold = .9 +extendIPMMeanDirAngleThreshold = .86 #.86 #.7 +extendIPMLinePixelsTangent = 5 #10 #5 +extendIPMLinePixelsNormal = 10 +extendIPMContThreshold = .1 #.35 #.3 #.25 #.25 #.2 #.1 +extendIPMDeviationThreshold = 2 #1 #2 +extendIPMRectTop = 0 +extendIPMRectBottom = 380 + + +#tracking +splineTrackingNumAbsentFrames = 3 +splineTrackingNumSeenFrames = 5 + +#spline merging +mergeSplineThetaThreshold = .7#523 #30 deg +mergeSplineRThreshold = 30#15 +mergeSplineMeanThetaThreshold = .7#523 #30 deg +mergeSplineMeanRThreshold = 30#15 +mergeSplineCentroidThreshold = 100 #50 + + +#line tracking +lineTrackingNumAbsentFrames = 3 +lineTrackingNumSeenFrames = 5 + +#spline merging +mergeLineThetaThreshold = .2#.3#52 #30 deg +mergeLineRThreshold = 30#15 + +numStrips = 1 + +checkSplines = 0 +checkSplinesCurvenessThreshold = .93 +checkSplinesLengthThreshold = 30 +checkSplinesThetaDiffThreshold = .1 +checkSplinesThetaThreshold = 1.22 #70 deg + +checkIPMSplines = 0 +checkIPMSplinesCurvenessThreshold = .85 +checkIPMSplinesLengthThreshold = 30 +checkIPMSplinesThetaDiffThreshold = .1 +checkIPMSplinesThetaThreshold = 1.4 #1.4->80deg 1.22->70deg + + +finalSplineScoreThreshold = 0 + +useGroundPlane = 1 + +checkColor = 0 +checkColorWindow = 3 +checkColorNumBins = 16 +checkColorNumYellowMin = .5 +checkColorRGMin = 1 +checkColorRGMax = 40 +checkColorGBMin = 15 +checkColorRBMin = 25 +checkColorRBFThreshold = -.05 +checkColorRBF = 1 + +ipmWindowClear = 0 +ipmWindowLeft = 50 #60 +ipmWindowRight = 110 #100 + +checkLaneWidth = 0 +checkLaneWidthMean = 25 +checkLaneWidthStd = 5 #10 \ No newline at end of file diff --git a/src/cmdline.c b/src/cmdline.c new file mode 100644 index 0000000..781e0c9 --- /dev/null +++ b/src/cmdline.c @@ -0,0 +1,998 @@ +/* + File autogenerated by gengetopt version 2.18 + generated with the following command: + gengetopt -i cmdline.ggo -u --conf-parser + + The developers of gengetopt consider the fixed text that goes in all + gengetopt output files to be in the public domain: + we make no copyright claims on it. +*/ + +/* If we use autoconf. */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include "getopt.h" + +#include "cmdline.h" + +const char *gengetopt_args_info_purpose = "Detects lanes in street images."; + +const char *gengetopt_args_info_usage = "Usage: LinePerceptor [OPTIONS]... [FILES]..."; + +const char *gengetopt_args_info_help[] = { + " -h, --help Print help and exit", + " -V, --version Print version and exit", + "\nBasic options:", + " --lanes-conf=STRING Configuration file for lane detection \n (default=`Lanes.conf')", + " --stoplines-conf=STRING Configuration file for stopline detection \n (default=`StopLines.conf')", + " --no-stoplines Don't detect stop lines (default=on)", + " --no-lanes Don't detect lanes (default=off)", + " --camera-conf=STRING Configuration file for the camera paramters \n (default=`CameraInfo.conf')", + " --list-file=STRING Text file containing a list of images one per \n line", + " --list-path=STRING Path where the image files are located, this is \n just appended at the front of each line in \n --list-file (default=`')", + " --image-file=STRING The path to an image", + "\nDebugging options:", + " --wait=INT Number of milliseconds to show the detected \n lanes. Put 0 for infinite i.e. waits for \n keypress. (default=`0')", + " --show Show the detected lines (default=off)", + " --step Step through each image (needs a keypress) or \n fall through (waits for --wait msecs) \n (default=off)", + " --show-lane-numbers Show the lane numbers on the output image \n (default=off)", + " --output-suffix=STRING Suffix of images and results \n (default=`_results')", + " --save-images Export all images with detected lanes to the by \n appending --output-suffix + '.png' to each \n input image (default=off)", + " --save-lanes Export all detected lanes to a text file by \n appending --output-suffix + '.txt' to \n --list-file (default=off)", + " --debug Show debugging information and images \n (default=off)", + 0 +}; + +static +void clear_given (struct gengetopt_args_info *args_info); +static +void clear_args (struct gengetopt_args_info *args_info); + +static int +cmdline_parser_internal (int argc, char * const *argv, struct gengetopt_args_info *args_info, int override, int initialize, int check_required, const char *additional_error); + +struct line_list +{ + char * string_arg; + struct line_list * next; +}; + +static struct line_list *cmd_line_list = 0; +static struct line_list *cmd_line_list_tmp = 0; + +static void +free_cmd_list(void) +{ + /* free the list of a previous call */ + if (cmd_line_list) + { + while (cmd_line_list) { + cmd_line_list_tmp = cmd_line_list; + cmd_line_list = cmd_line_list->next; + free (cmd_line_list_tmp->string_arg); + free (cmd_line_list_tmp); + } + } +} + + +static char * +gengetopt_strdup (const char *s); + +static +void clear_given (struct gengetopt_args_info *args_info) +{ + args_info->help_given = 0 ; + args_info->version_given = 0 ; + args_info->lanes_conf_given = 0 ; + args_info->stoplines_conf_given = 0 ; + args_info->no_stoplines_given = 0 ; + args_info->no_lanes_given = 0 ; + args_info->camera_conf_given = 0 ; + args_info->list_file_given = 0 ; + args_info->list_path_given = 0 ; + args_info->image_file_given = 0 ; + args_info->wait_given = 0 ; + args_info->show_given = 0 ; + args_info->step_given = 0 ; + args_info->show_lane_numbers_given = 0 ; + args_info->output_suffix_given = 0 ; + args_info->save_images_given = 0 ; + args_info->save_lanes_given = 0 ; + args_info->debug_given = 0 ; +} + +static +void clear_args (struct gengetopt_args_info *args_info) +{ + args_info->lanes_conf_arg = gengetopt_strdup ("Lanes.conf"); + args_info->lanes_conf_orig = NULL; + args_info->stoplines_conf_arg = gengetopt_strdup ("StopLines.conf"); + args_info->stoplines_conf_orig = NULL; + args_info->no_stoplines_flag = 1; + args_info->no_lanes_flag = 0; + args_info->camera_conf_arg = gengetopt_strdup ("CameraInfo.conf"); + args_info->camera_conf_orig = NULL; + args_info->list_file_arg = NULL; + args_info->list_file_orig = NULL; + args_info->list_path_arg = gengetopt_strdup (""); + args_info->list_path_orig = NULL; + args_info->image_file_arg = NULL; + args_info->image_file_orig = NULL; + args_info->wait_arg = 0; + args_info->wait_orig = NULL; + args_info->show_flag = 0; + args_info->step_flag = 0; + args_info->show_lane_numbers_flag = 0; + args_info->output_suffix_arg = gengetopt_strdup ("_results"); + args_info->output_suffix_orig = NULL; + args_info->save_images_flag = 0; + args_info->save_lanes_flag = 0; + args_info->debug_flag = 0; + +} + +static +void init_args_info(struct gengetopt_args_info *args_info) +{ + args_info->help_help = gengetopt_args_info_help[0] ; + args_info->version_help = gengetopt_args_info_help[1] ; + args_info->lanes_conf_help = gengetopt_args_info_help[3] ; + args_info->stoplines_conf_help = gengetopt_args_info_help[4] ; + args_info->no_stoplines_help = gengetopt_args_info_help[5] ; + args_info->no_lanes_help = gengetopt_args_info_help[6] ; + args_info->camera_conf_help = gengetopt_args_info_help[7] ; + args_info->list_file_help = gengetopt_args_info_help[8] ; + args_info->list_path_help = gengetopt_args_info_help[9] ; + args_info->image_file_help = gengetopt_args_info_help[10] ; + args_info->wait_help = gengetopt_args_info_help[12] ; + args_info->show_help = gengetopt_args_info_help[13] ; + args_info->step_help = gengetopt_args_info_help[14] ; + args_info->show_lane_numbers_help = gengetopt_args_info_help[15] ; + args_info->output_suffix_help = gengetopt_args_info_help[16] ; + args_info->save_images_help = gengetopt_args_info_help[17] ; + args_info->save_lanes_help = gengetopt_args_info_help[18] ; + args_info->debug_help = gengetopt_args_info_help[19] ; + +} + +void +cmdline_parser_print_version (void) +{ + printf ("%s %s\n", CMDLINE_PARSER_PACKAGE, CMDLINE_PARSER_VERSION); +} + +void +cmdline_parser_print_help (void) +{ + int i = 0; + cmdline_parser_print_version (); + + if (strlen(gengetopt_args_info_purpose) > 0) + printf("\n%s\n", gengetopt_args_info_purpose); + + printf("\n%s\n\n", gengetopt_args_info_usage); + while (gengetopt_args_info_help[i]) + printf("%s\n", gengetopt_args_info_help[i++]); +} + +void +cmdline_parser_init (struct gengetopt_args_info *args_info) +{ + clear_given (args_info); + clear_args (args_info); + init_args_info (args_info); + + args_info->inputs = NULL; + args_info->inputs_num = 0; +} + +static void +cmdline_parser_release (struct gengetopt_args_info *args_info) +{ + + unsigned int i; + if (args_info->lanes_conf_arg) + { + free (args_info->lanes_conf_arg); /* free previous argument */ + args_info->lanes_conf_arg = 0; + } + if (args_info->lanes_conf_orig) + { + free (args_info->lanes_conf_orig); /* free previous argument */ + args_info->lanes_conf_orig = 0; + } + if (args_info->stoplines_conf_arg) + { + free (args_info->stoplines_conf_arg); /* free previous argument */ + args_info->stoplines_conf_arg = 0; + } + if (args_info->stoplines_conf_orig) + { + free (args_info->stoplines_conf_orig); /* free previous argument */ + args_info->stoplines_conf_orig = 0; + } + if (args_info->camera_conf_arg) + { + free (args_info->camera_conf_arg); /* free previous argument */ + args_info->camera_conf_arg = 0; + } + if (args_info->camera_conf_orig) + { + free (args_info->camera_conf_orig); /* free previous argument */ + args_info->camera_conf_orig = 0; + } + if (args_info->list_file_arg) + { + free (args_info->list_file_arg); /* free previous argument */ + args_info->list_file_arg = 0; + } + if (args_info->list_file_orig) + { + free (args_info->list_file_orig); /* free previous argument */ + args_info->list_file_orig = 0; + } + if (args_info->list_path_arg) + { + free (args_info->list_path_arg); /* free previous argument */ + args_info->list_path_arg = 0; + } + if (args_info->list_path_orig) + { + free (args_info->list_path_orig); /* free previous argument */ + args_info->list_path_orig = 0; + } + if (args_info->image_file_arg) + { + free (args_info->image_file_arg); /* free previous argument */ + args_info->image_file_arg = 0; + } + if (args_info->image_file_orig) + { + free (args_info->image_file_orig); /* free previous argument */ + args_info->image_file_orig = 0; + } + if (args_info->wait_orig) + { + free (args_info->wait_orig); /* free previous argument */ + args_info->wait_orig = 0; + } + if (args_info->output_suffix_arg) + { + free (args_info->output_suffix_arg); /* free previous argument */ + args_info->output_suffix_arg = 0; + } + if (args_info->output_suffix_orig) + { + free (args_info->output_suffix_orig); /* free previous argument */ + args_info->output_suffix_orig = 0; + } + + for (i = 0; i < args_info->inputs_num; ++i) + free (args_info->inputs [i]); + + if (args_info->inputs_num) + free (args_info->inputs); + + clear_given (args_info); +} + +int +cmdline_parser_file_save(const char *filename, struct gengetopt_args_info *args_info) +{ + FILE *outfile; + int i = 0; + + outfile = fopen(filename, "w"); + + if (!outfile) + { + fprintf (stderr, "%s: cannot open file for writing: %s\n", CMDLINE_PARSER_PACKAGE, filename); + return EXIT_FAILURE; + } + + if (args_info->help_given) { + fprintf(outfile, "%s\n", "help"); + } + if (args_info->version_given) { + fprintf(outfile, "%s\n", "version"); + } + if (args_info->lanes_conf_given) { + if (args_info->lanes_conf_orig) { + fprintf(outfile, "%s=\"%s\"\n", "lanes-conf", args_info->lanes_conf_orig); + } else { + fprintf(outfile, "%s\n", "lanes-conf"); + } + } + if (args_info->stoplines_conf_given) { + if (args_info->stoplines_conf_orig) { + fprintf(outfile, "%s=\"%s\"\n", "stoplines-conf", args_info->stoplines_conf_orig); + } else { + fprintf(outfile, "%s\n", "stoplines-conf"); + } + } + if (args_info->no_stoplines_given) { + fprintf(outfile, "%s\n", "no-stoplines"); + } + if (args_info->no_lanes_given) { + fprintf(outfile, "%s\n", "no-lanes"); + } + if (args_info->camera_conf_given) { + if (args_info->camera_conf_orig) { + fprintf(outfile, "%s=\"%s\"\n", "camera-conf", args_info->camera_conf_orig); + } else { + fprintf(outfile, "%s\n", "camera-conf"); + } + } + if (args_info->list_file_given) { + if (args_info->list_file_orig) { + fprintf(outfile, "%s=\"%s\"\n", "list-file", args_info->list_file_orig); + } else { + fprintf(outfile, "%s\n", "list-file"); + } + } + if (args_info->list_path_given) { + if (args_info->list_path_orig) { + fprintf(outfile, "%s=\"%s\"\n", "list-path", args_info->list_path_orig); + } else { + fprintf(outfile, "%s\n", "list-path"); + } + } + if (args_info->image_file_given) { + if (args_info->image_file_orig) { + fprintf(outfile, "%s=\"%s\"\n", "image-file", args_info->image_file_orig); + } else { + fprintf(outfile, "%s\n", "image-file"); + } + } + if (args_info->wait_given) { + if (args_info->wait_orig) { + fprintf(outfile, "%s=\"%s\"\n", "wait", args_info->wait_orig); + } else { + fprintf(outfile, "%s\n", "wait"); + } + } + if (args_info->show_given) { + fprintf(outfile, "%s\n", "show"); + } + if (args_info->step_given) { + fprintf(outfile, "%s\n", "step"); + } + if (args_info->show_lane_numbers_given) { + fprintf(outfile, "%s\n", "show-lane-numbers"); + } + if (args_info->output_suffix_given) { + if (args_info->output_suffix_orig) { + fprintf(outfile, "%s=\"%s\"\n", "output-suffix", args_info->output_suffix_orig); + } else { + fprintf(outfile, "%s\n", "output-suffix"); + } + } + if (args_info->save_images_given) { + fprintf(outfile, "%s\n", "save-images"); + } + if (args_info->save_lanes_given) { + fprintf(outfile, "%s\n", "save-lanes"); + } + if (args_info->debug_given) { + fprintf(outfile, "%s\n", "debug"); + } + + fclose (outfile); + + i = EXIT_SUCCESS; + return i; +} + +void +cmdline_parser_free (struct gengetopt_args_info *args_info) +{ + cmdline_parser_release (args_info); +} + + +/* gengetopt_strdup() */ +/* strdup.c replacement of strdup, which is not standard */ +char * +gengetopt_strdup (const char *s) +{ + char *result = NULL; + if (!s) + return result; + + result = (char*)malloc(strlen(s) + 1); + if (result == (char*)0) + return (char*)0; + strcpy(result, s); + return result; +} + +int +cmdline_parser (int argc, char * const *argv, struct gengetopt_args_info *args_info) +{ + return cmdline_parser2 (argc, argv, args_info, 0, 1, 1); +} + +int +cmdline_parser2 (int argc, char * const *argv, struct gengetopt_args_info *args_info, int override, int initialize, int check_required) +{ + int result; + + result = cmdline_parser_internal (argc, argv, args_info, override, initialize, check_required, NULL); + + if (result == EXIT_FAILURE) + { + cmdline_parser_free (args_info); + exit (EXIT_FAILURE); + } + + return result; +} + +int +cmdline_parser_required (struct gengetopt_args_info *args_info, const char *prog_name) +{ + return EXIT_SUCCESS; +} + +int +cmdline_parser_internal (int argc, char * const *argv, struct gengetopt_args_info *args_info, int override, int initialize, int check_required, const char *additional_error) +{ + int c; /* Character of the parsed option. */ + + int error = 0; + struct gengetopt_args_info local_args_info; + + if (initialize) + cmdline_parser_init (args_info); + + cmdline_parser_init (&local_args_info); + + optarg = 0; + optind = 0; + opterr = 1; + optopt = '?'; + + while (1) + { + int option_index = 0; + char *stop_char; + + static struct option long_options[] = { + { "help", 0, NULL, 'h' }, + { "version", 0, NULL, 'V' }, + { "lanes-conf", 1, NULL, 0 }, + { "stoplines-conf", 1, NULL, 0 }, + { "no-stoplines", 0, NULL, 0 }, + { "no-lanes", 0, NULL, 0 }, + { "camera-conf", 1, NULL, 0 }, + { "list-file", 1, NULL, 0 }, + { "list-path", 1, NULL, 0 }, + { "image-file", 1, NULL, 0 }, + { "wait", 1, NULL, 0 }, + { "show", 0, NULL, 0 }, + { "step", 0, NULL, 0 }, + { "show-lane-numbers", 0, NULL, 0 }, + { "output-suffix", 1, NULL, 0 }, + { "save-images", 0, NULL, 0 }, + { "save-lanes", 0, NULL, 0 }, + { "debug", 0, NULL, 0 }, + { NULL, 0, NULL, 0 } + }; + + stop_char = 0; + c = getopt_long (argc, argv, "hV", long_options, &option_index); + + if (c == -1) break; /* Exit from `while (1)' loop. */ + + switch (c) + { + case 'h': /* Print help and exit. */ + cmdline_parser_print_help (); + cmdline_parser_free (&local_args_info); + exit (EXIT_SUCCESS); + + case 'V': /* Print version and exit. */ + cmdline_parser_print_version (); + cmdline_parser_free (&local_args_info); + exit (EXIT_SUCCESS); + + + case 0: /* Long option with no short option */ + /* Configuration file for lane detection. */ + if (strcmp (long_options[option_index].name, "lanes-conf") == 0) + { + if (local_args_info.lanes_conf_given) + { + fprintf (stderr, "%s: `--lanes-conf' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->lanes_conf_given && ! override) + continue; + local_args_info.lanes_conf_given = 1; + args_info->lanes_conf_given = 1; + if (args_info->lanes_conf_arg) + free (args_info->lanes_conf_arg); /* free previous string */ + args_info->lanes_conf_arg = gengetopt_strdup (optarg); + if (args_info->lanes_conf_orig) + free (args_info->lanes_conf_orig); /* free previous string */ + args_info->lanes_conf_orig = gengetopt_strdup (optarg); + } + /* Configuration file for stopline detection. */ + else if (strcmp (long_options[option_index].name, "stoplines-conf") == 0) + { + if (local_args_info.stoplines_conf_given) + { + fprintf (stderr, "%s: `--stoplines-conf' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->stoplines_conf_given && ! override) + continue; + local_args_info.stoplines_conf_given = 1; + args_info->stoplines_conf_given = 1; + if (args_info->stoplines_conf_arg) + free (args_info->stoplines_conf_arg); /* free previous string */ + args_info->stoplines_conf_arg = gengetopt_strdup (optarg); + if (args_info->stoplines_conf_orig) + free (args_info->stoplines_conf_orig); /* free previous string */ + args_info->stoplines_conf_orig = gengetopt_strdup (optarg); + } + /* Don't detect stop lines. */ + else if (strcmp (long_options[option_index].name, "no-stoplines") == 0) + { + if (local_args_info.no_stoplines_given) + { + fprintf (stderr, "%s: `--no-stoplines' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->no_stoplines_given && ! override) + continue; + local_args_info.no_stoplines_given = 1; + args_info->no_stoplines_given = 1; + args_info->no_stoplines_flag = !(args_info->no_stoplines_flag); + } + /* Don't detect lanes. */ + else if (strcmp (long_options[option_index].name, "no-lanes") == 0) + { + if (local_args_info.no_lanes_given) + { + fprintf (stderr, "%s: `--no-lanes' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->no_lanes_given && ! override) + continue; + local_args_info.no_lanes_given = 1; + args_info->no_lanes_given = 1; + args_info->no_lanes_flag = !(args_info->no_lanes_flag); + } + /* Configuration file for the camera paramters. */ + else if (strcmp (long_options[option_index].name, "camera-conf") == 0) + { + if (local_args_info.camera_conf_given) + { + fprintf (stderr, "%s: `--camera-conf' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->camera_conf_given && ! override) + continue; + local_args_info.camera_conf_given = 1; + args_info->camera_conf_given = 1; + if (args_info->camera_conf_arg) + free (args_info->camera_conf_arg); /* free previous string */ + args_info->camera_conf_arg = gengetopt_strdup (optarg); + if (args_info->camera_conf_orig) + free (args_info->camera_conf_orig); /* free previous string */ + args_info->camera_conf_orig = gengetopt_strdup (optarg); + } + /* Text file containing a list of images one per line. */ + else if (strcmp (long_options[option_index].name, "list-file") == 0) + { + if (local_args_info.list_file_given) + { + fprintf (stderr, "%s: `--list-file' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->list_file_given && ! override) + continue; + local_args_info.list_file_given = 1; + args_info->list_file_given = 1; + if (args_info->list_file_arg) + free (args_info->list_file_arg); /* free previous string */ + args_info->list_file_arg = gengetopt_strdup (optarg); + if (args_info->list_file_orig) + free (args_info->list_file_orig); /* free previous string */ + args_info->list_file_orig = gengetopt_strdup (optarg); + } + /* Path where the image files are located, this is just appended at the front of each line in --list-file. */ + else if (strcmp (long_options[option_index].name, "list-path") == 0) + { + if (local_args_info.list_path_given) + { + fprintf (stderr, "%s: `--list-path' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->list_path_given && ! override) + continue; + local_args_info.list_path_given = 1; + args_info->list_path_given = 1; + if (args_info->list_path_arg) + free (args_info->list_path_arg); /* free previous string */ + args_info->list_path_arg = gengetopt_strdup (optarg); + if (args_info->list_path_orig) + free (args_info->list_path_orig); /* free previous string */ + args_info->list_path_orig = gengetopt_strdup (optarg); + } + /* The path to an image. */ + else if (strcmp (long_options[option_index].name, "image-file") == 0) + { + if (local_args_info.image_file_given) + { + fprintf (stderr, "%s: `--image-file' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->image_file_given && ! override) + continue; + local_args_info.image_file_given = 1; + args_info->image_file_given = 1; + if (args_info->image_file_arg) + free (args_info->image_file_arg); /* free previous string */ + args_info->image_file_arg = gengetopt_strdup (optarg); + if (args_info->image_file_orig) + free (args_info->image_file_orig); /* free previous string */ + args_info->image_file_orig = gengetopt_strdup (optarg); + } + /* Number of milliseconds to show the detected lanes. Put 0 for infinite i.e. waits for keypress.. */ + else if (strcmp (long_options[option_index].name, "wait") == 0) + { + if (local_args_info.wait_given) + { + fprintf (stderr, "%s: `--wait' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->wait_given && ! override) + continue; + local_args_info.wait_given = 1; + args_info->wait_given = 1; + args_info->wait_arg = strtol (optarg, &stop_char, 0); + if (!(stop_char && *stop_char == '\0')) { + fprintf(stderr, "%s: invalid numeric value: %s\n", argv[0], optarg); + goto failure; + } + if (args_info->wait_orig) + free (args_info->wait_orig); /* free previous string */ + args_info->wait_orig = gengetopt_strdup (optarg); + } + /* Show the detected lines. */ + else if (strcmp (long_options[option_index].name, "show") == 0) + { + if (local_args_info.show_given) + { + fprintf (stderr, "%s: `--show' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->show_given && ! override) + continue; + local_args_info.show_given = 1; + args_info->show_given = 1; + args_info->show_flag = !(args_info->show_flag); + } + /* Step through each image (needs a keypress) or fall through (waits for --wait msecs). */ + else if (strcmp (long_options[option_index].name, "step") == 0) + { + if (local_args_info.step_given) + { + fprintf (stderr, "%s: `--step' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->step_given && ! override) + continue; + local_args_info.step_given = 1; + args_info->step_given = 1; + args_info->step_flag = !(args_info->step_flag); + } + /* Show the lane numbers on the output image. */ + else if (strcmp (long_options[option_index].name, "show-lane-numbers") == 0) + { + if (local_args_info.show_lane_numbers_given) + { + fprintf (stderr, "%s: `--show-lane-numbers' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->show_lane_numbers_given && ! override) + continue; + local_args_info.show_lane_numbers_given = 1; + args_info->show_lane_numbers_given = 1; + args_info->show_lane_numbers_flag = !(args_info->show_lane_numbers_flag); + } + /* Suffix of images and results. */ + else if (strcmp (long_options[option_index].name, "output-suffix") == 0) + { + if (local_args_info.output_suffix_given) + { + fprintf (stderr, "%s: `--output-suffix' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->output_suffix_given && ! override) + continue; + local_args_info.output_suffix_given = 1; + args_info->output_suffix_given = 1; + if (args_info->output_suffix_arg) + free (args_info->output_suffix_arg); /* free previous string */ + args_info->output_suffix_arg = gengetopt_strdup (optarg); + if (args_info->output_suffix_orig) + free (args_info->output_suffix_orig); /* free previous string */ + args_info->output_suffix_orig = gengetopt_strdup (optarg); + } + /* Export all images with detected lanes to the by appending --output-suffix + '.png' to each input image. */ + else if (strcmp (long_options[option_index].name, "save-images") == 0) + { + if (local_args_info.save_images_given) + { + fprintf (stderr, "%s: `--save-images' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->save_images_given && ! override) + continue; + local_args_info.save_images_given = 1; + args_info->save_images_given = 1; + args_info->save_images_flag = !(args_info->save_images_flag); + } + /* Export all detected lanes to a text file by appending --output-suffix + '.txt' to --list-file. */ + else if (strcmp (long_options[option_index].name, "save-lanes") == 0) + { + if (local_args_info.save_lanes_given) + { + fprintf (stderr, "%s: `--save-lanes' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->save_lanes_given && ! override) + continue; + local_args_info.save_lanes_given = 1; + args_info->save_lanes_given = 1; + args_info->save_lanes_flag = !(args_info->save_lanes_flag); + } + /* Show debugging information and images. */ + else if (strcmp (long_options[option_index].name, "debug") == 0) + { + if (local_args_info.debug_given) + { + fprintf (stderr, "%s: `--debug' option given more than once%s\n", argv[0], (additional_error ? additional_error : "")); + goto failure; + } + if (args_info->debug_given && ! override) + continue; + local_args_info.debug_given = 1; + args_info->debug_given = 1; + args_info->debug_flag = !(args_info->debug_flag); + } + + break; + case '?': /* Invalid option. */ + /* `getopt_long' already printed an error message. */ + goto failure; + + default: /* bug: option not considered. */ + fprintf (stderr, "%s: option unknown: %c%s\n", CMDLINE_PARSER_PACKAGE, c, (additional_error ? additional_error : "")); + abort (); + } /* switch */ + } /* while */ + + + + + cmdline_parser_release (&local_args_info); + + if ( error ) + return (EXIT_FAILURE); + + if (optind < argc) + { + int i = 0 ; + int found_prog_name = 0; + /* whether program name, i.e., argv[0], is in the remaining args + (this may happen with some implementations of getopt, + but surely not with the one included by gengetopt) */ + + i = optind; + while (i < argc) + if (argv[i++] == argv[0]) { + found_prog_name = 1; + break; + } + i = 0; + + args_info->inputs_num = argc - optind - found_prog_name; + args_info->inputs = + (char **)(malloc ((args_info->inputs_num)*sizeof(char *))) ; + while (optind < argc) + if (argv[optind++] != argv[0]) + args_info->inputs[ i++ ] = gengetopt_strdup (argv[optind-1]) ; + } + + return 0; + +failure: + + cmdline_parser_release (&local_args_info); + return (EXIT_FAILURE); +} + +#ifndef CONFIG_FILE_LINE_SIZE +#define CONFIG_FILE_LINE_SIZE 2048 +#endif +#define ADDITIONAL_ERROR " in configuration file " + +#define CONFIG_FILE_LINE_BUFFER_SIZE (CONFIG_FILE_LINE_SIZE+3) +/* 3 is for "--" and "=" */ + +char my_argv[CONFIG_FILE_LINE_BUFFER_SIZE+1]; + +int +cmdline_parser_configfile (char * const filename, struct gengetopt_args_info *args_info, int override, int initialize, int check_required) +{ + FILE* file; + char linebuf[CONFIG_FILE_LINE_SIZE]; + int line_num = 0; + int i, result, equal; + char *fopt, *farg; + char *str_index; + size_t len, next_token; + char delimiter; + int my_argc = 0; + char **my_argv_arg; + char *additional_error; + + /* store the program name */ + cmd_line_list_tmp = (struct line_list *) malloc (sizeof (struct line_list)); + cmd_line_list_tmp->next = cmd_line_list; + cmd_line_list = cmd_line_list_tmp; + cmd_line_list->string_arg = gengetopt_strdup (CMDLINE_PARSER_PACKAGE); + + if ((file = fopen(filename, "r")) == NULL) + { + fprintf (stderr, "%s: Error opening configuration file '%s'\n", + CMDLINE_PARSER_PACKAGE, filename); + result = EXIT_FAILURE; + goto conf_failure; + } + + while ((fgets(linebuf, CONFIG_FILE_LINE_SIZE, file)) != NULL) + { + ++line_num; + my_argv[0] = '\0'; + len = strlen(linebuf); + if (len > (CONFIG_FILE_LINE_BUFFER_SIZE-1)) + { + fprintf (stderr, "%s:%s:%d: Line too long in configuration file\n", + CMDLINE_PARSER_PACKAGE, filename, line_num); + result = EXIT_FAILURE; + goto conf_failure; + } + + /* find first non-whitespace character in the line */ + next_token = strspn ( linebuf, " \t\r\n"); + str_index = linebuf + next_token; + + if ( str_index[0] == '\0' || str_index[0] == '#') + continue; /* empty line or comment line is skipped */ + + fopt = str_index; + + /* truncate fopt at the end of the first non-valid character */ + next_token = strcspn (fopt, " \t\r\n="); + + if (fopt[next_token] == '\0') /* the line is over */ + { + farg = NULL; + equal = 0; + goto noarg; + } + + /* remember if equal sign is present */ + equal = (fopt[next_token] == '='); + fopt[next_token++] = '\0'; + + /* advance pointers to the next token after the end of fopt */ + next_token += strspn (fopt + next_token, " \t\r\n"); + /* check for the presence of equal sign, and if so, skip it */ + if ( !equal ) + if ((equal = (fopt[next_token] == '='))) + { + next_token++; + next_token += strspn (fopt + next_token, " \t\r\n"); + } + str_index += next_token; + + /* find argument */ + farg = str_index; + if ( farg[0] == '\"' || farg[0] == '\'' ) + { /* quoted argument */ + str_index = strchr (++farg, str_index[0] ); /* skip opening quote */ + if (! str_index) + { + fprintf + (stderr, + "%s:%s:%d: unterminated string in configuration file\n", + CMDLINE_PARSER_PACKAGE, filename, line_num); + result = EXIT_FAILURE; + goto conf_failure; + } + } + else + { /* read up the remaining part up to a delimiter */ + next_token = strcspn (farg, " \t\r\n#\'\""); + str_index += next_token; + } + + /* truncate farg at the delimiter and store it for further check */ + delimiter = *str_index, *str_index++ = '\0'; + + /* everything but comment is illegal at the end of line */ + if (delimiter != '\0' && delimiter != '#') + { + str_index += strspn(str_index, " \t\r\n"); + if (*str_index != '\0' && *str_index != '#') + { + fprintf + (stderr, + "%s:%s:%d: malformed string in configuration file\n", + CMDLINE_PARSER_PACKAGE, filename, line_num); + result = EXIT_FAILURE; + goto conf_failure; + } + } + + noarg: + ++my_argc; + len = strlen(fopt); + + strcat (my_argv, len > 1 ? "--" : "-"); + strcat (my_argv, fopt); + if (len > 1 && ((farg &&*farg) || equal)) + strcat (my_argv, "="); + if (farg && *farg) + strcat (my_argv, farg); + + cmd_line_list_tmp = (struct line_list *) malloc (sizeof (struct line_list)); + cmd_line_list_tmp->next = cmd_line_list; + cmd_line_list = cmd_line_list_tmp; + cmd_line_list->string_arg = gengetopt_strdup(my_argv); + } /* while */ + + ++my_argc; /* for program name */ + my_argv_arg = (char **) malloc((my_argc+1) * sizeof(char *)); + cmd_line_list_tmp = cmd_line_list; + for (i = my_argc - 1; i >= 0; --i) { + my_argv_arg[i] = cmd_line_list_tmp->string_arg; + cmd_line_list_tmp = cmd_line_list_tmp->next; + } + my_argv_arg[my_argc] = 0; + + additional_error = (char *)malloc(strlen(filename) + strlen(ADDITIONAL_ERROR) + 1); + strcpy (additional_error, ADDITIONAL_ERROR); + strcat (additional_error, filename); + result = + cmdline_parser_internal (my_argc, my_argv_arg, args_info, override, initialize, check_required, additional_error); + + free (additional_error); + free (my_argv_arg); + +conf_failure: + if (file) + fclose(file); + + free_cmd_list(); + if (result == EXIT_FAILURE) + { + cmdline_parser_free (args_info); + exit (EXIT_FAILURE); + } + + return result; +} diff --git a/src/cmdline.ggo b/src/cmdline.ggo new file mode 100755 index 0000000..1025e1d --- /dev/null +++ b/src/cmdline.ggo @@ -0,0 +1,68 @@ + +# Command-line options (use gengetopt to generate C the source code) + +package "LinePerceptor" +purpose "Detects lanes in street images." +version "1.0" + +#------------------------------------------------------------------------- +section "Basic options" +#------------------------------------------------------------------------- + +option "lanes-conf" - + "Configuration file for lane detection" string default ="Lanes.conf" + optional + +option "stoplines-conf" - + "Configuration file for stopline detection" string default="StopLines.conf" + optional + +option "no-stoplines" - + "Don't detect stop lines" flag on + +option "no-lanes" - + "Don't detect lanes" flag off + +option "camera-conf" - + "Configuration file for the camera paramters" string + default="CameraInfo.conf" optional + +option "list-file" - + "Text file containing a list of images one per line" string optional + +option "list-path" - + "Path where the image files are located, this is just appended at the front of each line in --list-file" + default="" string optional + +option "image-file" - + "The path to an image" string optional + + +#------------------------------------------------------------------------- +section "Debugging options" +#------------------------------------------------------------------------- + +option "wait" - "Number of milliseconds to show the detected lanes. Put 0 for infinite i.e. waits for keypress." int + default="0" optional + +option "show" - + "Show the detected lines" flag off + +option "step" - + "Step through each image (needs a keypress) or fall through (waits for --wait msecs)" flag off + +option "show-lane-numbers" - + "Show the lane numbers on the output image" flag off + +option "output-suffix" - + "Suffix of images and results" string default="_results" optional + +option "save-images" - + "Export all images with detected lanes to the by appending --output-suffix + '.png' to each input image" + flag off + +option "save-lanes" - + "Export all detected lanes to a text file by appending --output-suffix + '.txt' to --list-file" flag off + +option "debug" - + "Show debugging information and images" flag off diff --git a/src/cmdline.h b/src/cmdline.h new file mode 100644 index 0000000..9a9ad76 --- /dev/null +++ b/src/cmdline.h @@ -0,0 +1,122 @@ +/* cmdline.h */ + +/* File autogenerated by gengetopt version 2.18 */ + +#ifndef CMDLINE_H +#define CMDLINE_H + +/* If we use autoconf. */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#ifndef CMDLINE_PARSER_PACKAGE +#define CMDLINE_PARSER_PACKAGE "LinePerceptor" +#endif + +#ifndef CMDLINE_PARSER_VERSION +#define CMDLINE_PARSER_VERSION "1.0" +#endif + +struct gengetopt_args_info +{ + const char *help_help; /* Print help and exit help description. */ + const char *version_help; /* Print version and exit help description. */ + char * lanes_conf_arg; /* Configuration file for lane detection (default='Lanes.conf'). */ + char * lanes_conf_orig; /* Configuration file for lane detection original value given at command line. */ + const char *lanes_conf_help; /* Configuration file for lane detection help description. */ + char * stoplines_conf_arg; /* Configuration file for stopline detection (default='StopLines.conf'). */ + char * stoplines_conf_orig; /* Configuration file for stopline detection original value given at command line. */ + const char *stoplines_conf_help; /* Configuration file for stopline detection help description. */ + int no_stoplines_flag; /* Don't detect stop lines (default=on). */ + const char *no_stoplines_help; /* Don't detect stop lines help description. */ + int no_lanes_flag; /* Don't detect lanes (default=off). */ + const char *no_lanes_help; /* Don't detect lanes help description. */ + char * camera_conf_arg; /* Configuration file for the camera paramters (default='CameraInfo.conf'). */ + char * camera_conf_orig; /* Configuration file for the camera paramters original value given at command line. */ + const char *camera_conf_help; /* Configuration file for the camera paramters help description. */ + char * list_file_arg; /* Text file containing a list of images one per line. */ + char * list_file_orig; /* Text file containing a list of images one per line original value given at command line. */ + const char *list_file_help; /* Text file containing a list of images one per line help description. */ + char * list_path_arg; /* Path where the image files are located, this is just appended at the front of each line in --list-file (default=''). */ + char * list_path_orig; /* Path where the image files are located, this is just appended at the front of each line in --list-file original value given at command line. */ + const char *list_path_help; /* Path where the image files are located, this is just appended at the front of each line in --list-file help description. */ + char * image_file_arg; /* The path to an image. */ + char * image_file_orig; /* The path to an image original value given at command line. */ + const char *image_file_help; /* The path to an image help description. */ + int wait_arg; /* Number of milliseconds to show the detected lanes. Put 0 for infinite i.e. waits for keypress. (default='0'). */ + char * wait_orig; /* Number of milliseconds to show the detected lanes. Put 0 for infinite i.e. waits for keypress. original value given at command line. */ + const char *wait_help; /* Number of milliseconds to show the detected lanes. Put 0 for infinite i.e. waits for keypress. help description. */ + int show_flag; /* Show the detected lines (default=off). */ + const char *show_help; /* Show the detected lines help description. */ + int step_flag; /* Step through each image (needs a keypress) or fall through (waits for --wait msecs) (default=off). */ + const char *step_help; /* Step through each image (needs a keypress) or fall through (waits for --wait msecs) help description. */ + int show_lane_numbers_flag; /* Show the lane numbers on the output image (default=off). */ + const char *show_lane_numbers_help; /* Show the lane numbers on the output image help description. */ + char * output_suffix_arg; /* Suffix of images and results (default='_results'). */ + char * output_suffix_orig; /* Suffix of images and results original value given at command line. */ + const char *output_suffix_help; /* Suffix of images and results help description. */ + int save_images_flag; /* Export all images with detected lanes to the by appending --output-suffix + '.png' to each input image (default=off). */ + const char *save_images_help; /* Export all images with detected lanes to the by appending --output-suffix + '.png' to each input image help description. */ + int save_lanes_flag; /* Export all detected lanes to a text file by appending --output-suffix + '.txt' to --list-file (default=off). */ + const char *save_lanes_help; /* Export all detected lanes to a text file by appending --output-suffix + '.txt' to --list-file help description. */ + int debug_flag; /* Show debugging information and images (default=off). */ + const char *debug_help; /* Show debugging information and images help description. */ + + int help_given ; /* Whether help was given. */ + int version_given ; /* Whether version was given. */ + int lanes_conf_given ; /* Whether lanes-conf was given. */ + int stoplines_conf_given ; /* Whether stoplines-conf was given. */ + int no_stoplines_given ; /* Whether no-stoplines was given. */ + int no_lanes_given ; /* Whether no-lanes was given. */ + int camera_conf_given ; /* Whether camera-conf was given. */ + int list_file_given ; /* Whether list-file was given. */ + int list_path_given ; /* Whether list-path was given. */ + int image_file_given ; /* Whether image-file was given. */ + int wait_given ; /* Whether wait was given. */ + int show_given ; /* Whether show was given. */ + int step_given ; /* Whether step was given. */ + int show_lane_numbers_given ; /* Whether show-lane-numbers was given. */ + int output_suffix_given ; /* Whether output-suffix was given. */ + int save_images_given ; /* Whether save-images was given. */ + int save_lanes_given ; /* Whether save-lanes was given. */ + int debug_given ; /* Whether debug was given. */ + + char **inputs ; /* unamed options */ + unsigned inputs_num ; /* unamed options number */ +} ; + +extern const char *gengetopt_args_info_purpose; +extern const char *gengetopt_args_info_usage; +extern const char *gengetopt_args_info_help[]; + +int cmdline_parser (int argc, char * const *argv, + struct gengetopt_args_info *args_info); +int cmdline_parser2 (int argc, char * const *argv, + struct gengetopt_args_info *args_info, + int override, int initialize, int check_required); +int cmdline_parser_file_save(const char *filename, + struct gengetopt_args_info *args_info); + +void cmdline_parser_print_help(void); +void cmdline_parser_print_version(void); + +void cmdline_parser_init (struct gengetopt_args_info *args_info); +void cmdline_parser_free (struct gengetopt_args_info *args_info); + +int cmdline_parser_configfile (char * const filename, + struct gengetopt_args_info *args_info, + int override, int initialize, int check_required); + +int cmdline_parser_required (struct gengetopt_args_info *args_info, + const char *prog_name); + + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* CMDLINE_H */ diff --git a/src/cmdline.o b/src/cmdline.o new file mode 100644 index 0000000000000000000000000000000000000000..a4576f9721f3e964c12e4bccff5aa4321b7f8146 GIT binary patch literal 30408 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≫aeI1tA*de_$i~!jl}s*#-}qb?fWBc$4U!- zmhmbJf0^;QO8*qlE$x1_!r!m(RSL(sTKH=N@b!#~oxkOJlZvnFy;kNa z>jLne0Q}Ja{LKJ-IxpF6dsYYFpAWzv3c%kCz-RFK$gX!KLFa*Kr@W&Js&2lo)7|2~U^N|2OS6O`N zub;GV(esZ1dj8qs-^cdsuyBd zN8ltU^qUQ|8?fyL5;y=fFYAjszEDX@IOyMrHyUeq9DSVp3cPV_j!k4miW7ug+!TZMb3diT@C2p6Sl%6WUCC<+&9M7%=Kdf*apVt(wyz`9o@&6w{!dzb*%z->e7!zgZ}AUv+>(l~*GoTB zxZW@PLgCo%V$bRD-}hibe>MLsg=_v&#_c$)U|iy`fbIOW(xc;Wi^Z2Xd{Oar9KLMv zr|>v_x8mzKz+omO+plp7*M8M1T*qOl!Zkn1xE+Vhj7uD3UAI-~(Q){9i!X85r}#P! zPg;Cg2Ry6zIt~l@K-;!6#JJcg>&B}Y$GB-blNMj>%qYILv&Z6Ze=pnFp!nL(M-{I7$x{l~{p7g9HGeE03ZVU( ze=*~B92PJxagcTFVx>p7*LsUDakyFWbsV}a{!xxYkK*e%ysmKV*Pj%w{kjNAknHDi zzQQ%Xm2o=`H!&`8koEH>rANnMr^S~zJfQeG4i8y;S?4{W_&N@iIH?Tb*98jKe%+*S zR4Lzy9-e6qfAp3=PlpY<2b8s;MN%#_n3m8YgbR6bdd|78NQhXhU ztqRxne?{RJKpoQVKUTQrzpQX=&nt|J{Rg;R{-F5Uo~iue)qd`#D_qyRl5soE*D)^j z%6_U*>Cth%-Qr7}zoPisuWwlVe)j8r#n*mK!$mD5+s^YD7dvIYb`j$k2W{uI7GLaK zulU-|xW%8s>-)`$ukC!3aof(nSbAhXSjI0bY&$C%7r(^L`HHXYTx9XXZ09EwU)$No zxNYYFOONam4=O!6p6^(Ev9p4Hw{OM`ZRcdh#jkB_=UIxc?YvdtdffP&!gYT>pm5Fq ziNZDiIOBF4#-Y)W#7^1gPGa0{uS+ey#9^u8R8jJ1Z3*Z-xI=#%(+AvGmA3{eGoK+xfi37du~7 zd~N4{S^S!bU=*Zx6d!NJ&bW)E#c|l=5=`@*)}Liu;wItMCsA}C#vv^pkDapKc$qy(G{_$SK->8U5txgV$b~+zMJiNSn1LB zykPNv&HVpR{7aNQbMa6NlGOX3%)f$hjORz-PwfAR#TWfADZbWUGXu33^b3C;2jocHZhdbItoC|viqcPu??*{^pkT+R*0;-MQP^b7m5q>~uOILw7V zX_vDC_#X=3uL$5L7{_@^*ZcJV{x<{ke@o%m<;BiD3LgzP{$bh|fIng3qUTu)7d?E(0Y6t4T*iwf6v zo`;J(NOu3(qHw)Fydwa=N8v8$6Mr99I8K#Kx!}K3xSk(@La7Ws$M8K~D)?S- zm-Dq2UdQ>cg>U1$l#8B67++)Y4|2Z2!s}2FQj593H%5(}hZyg)a2Ep(smokEehcGi z3m;@$>=pf;<%Za6@x$D)c3Ajk#&=qHFXOu`yoH0k+roD!e=@o@PC@-2Q^gc}|0c%lSjv!ezg|)52wc-DlylpFM2h zvcHp`1c*JdpK`fB3NHH>7ktEo>R2V^Yw5FqM>yYL;qtqRw1vy>Cw5x6{GOoC!sT}Z zhb>%wA3zNPWn!oN4#P!<6fVCTsI_qUUVnpy%kMwZ7B1h@-)Z6UJ^ellm+#{rws84f z2!7vQ+bQ2$H|XF0>4GR$$&az8sFv@J^0+MpY@TH4T`LYTwH{(_b}Tj5PG0xm@fOPR zZ2r;V~XJ-|FkS3dp z_bKuM;F=lz(tRJxM1CEw{}97u%YPIwl&`@;mBuYJnHdVhTZzA1G0bwWsdHFfV|Z_K zuV>7*zlP;qtH8GZ(;z=o{14oOwK-0jE5!KY5HIr5{-*+F#~vW3?h5>LoMv + * \date Wed Oct 6, 2010 + * + */ + +#include "main.hh" + +#include "cmdline.h" +#include "LaneDetector.hh" + +#include +#include +#include +#include +#include +#include + +#include +#include + +// Useful message macro +#define MSG(fmt, ...) \ + (fprintf(stdout, "%s:%d msg " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__) ? 0 : 0) + +// Useful error macro +#define ERROR(fmt, ...) \ + (fprintf(stderr, "%s:%d error " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__) ? -1 : -1) + + +namespace LaneDetector +{ + +/** + * This function reads lines from the input file into a vector of strings + * + * \param filename the input file name + * \param lines the output vector of lines + */ +bool ReadLines(const char* filename, vector *lines) +{ + // make sure it's not NULL + if (!lines) + return false; + // resize + lines->clear(); + + ifstream file; + file.open(filename, ifstream::in); + char buf[5000]; + // read lines and process + while (file.getline(buf, 5000)) + { + string str(buf); + lines->push_back(str); + } + // close + file.close(); + return true; +} + + +/** + * This function processes an input image and detects lanes/stoplines + * based on the passed in command line arguments + * + * \param filename the input file name + * \param cameraInfo the camera calibration info + * \param lanesConf the lane detection settings + * \param stoplinesConf the stop line detection settings + * \param options the command line arguments + * \param outputFile the output file stream to write output lanes to + * \param index the image index (used for saving output files) + * \param elapsedTime if NOT NULL, it is accumulated with clock ticks for + * the detection operation + */ +void ProcessImage(const char* filename, CameraInfo& cameraInfo, + LaneDetectorConf& lanesConf, LaneDetectorConf& stoplinesConf, + gengetopt_args_info& options, ofstream* outputFile, + int index, clock_t *elapsedTime) +{ + // load the image + CvMat *raw_mat, *mat; + mcvLoadImage(filename, &raw_mat, &mat); + + // detect lanes + vector lineScores, splineScores; + vector lanes; + vector splines; + clock_t startTime = clock(); + mcvGetLanes(mat, raw_mat, &lanes, &lineScores, &splines, &splineScores, + &cameraInfo, &lanesConf, NULL); + clock_t endTime = clock(); + MSG("Found %d lanes in %f msec", splines.size(), + static_cast(endTime - startTime) / CLOCKS_PER_SEC * 1000.); + // update elapsed time + if (elapsedTime) + (*elapsedTime) += endTime - startTime; + + // save results? + if (options.save_lanes_flag && outputFile && outputFile->is_open()) + { + (*outputFile) << "frame#" << setw(8) << setfill('0') << index << + " has " << splines.size() << " splines" << endl; + for (int i=0; i(elapsed) / CLOCKS_PER_SEC; + MSG("Total time %f secs for 1 image = %f Hz", elapsedTime, + 1. / elapsedTime); + } + + // process a list of images + if (options.list_file_given) + { + // get the path if exists + string path = ""; + if (options.list_path_given) + path = options.list_path_arg; + + // read file + vector files; + ReadLines(options.list_file_arg, &files); + int numImages = files.size(); + if (numImages<1) + ERROR("File %s is empty", options.list_file_arg); + else + { + // save results? + ofstream outputFile; + stringstream ss; + if (options.save_lanes_flag) + { + ss << options.list_file_arg << options.output_suffix_arg << ".txt"; + outputFile.open(ss.str().c_str(), ios_base::out); + } + + // elapsed time + clock_t elapsed = 0; + // loop + for (int i=0; i(elapsed) / CLOCKS_PER_SEC; + MSG("Total time %f secs for %d images = %f Hz", + elapsedTime, numImages, numImages / elapsedTime); + + // close results file (if open) + if (options.save_lanes_flag) + { + outputFile.close(); + MSG("Results written to %s", ss.str().c_str()); + } + } + } + + return 0; +} + +} // namespace LaneDetector + +using LaneDetector::Process; + +// main entry point +int main(int argc, char** argv) +{ + return Process(argc, argv); +} diff --git a/src/main.hh b/src/main.hh new file mode 100644 index 0000000..3417fbd --- /dev/null +++ b/src/main.hh @@ -0,0 +1,54 @@ +/** + * \file main.hh + * \author Mohamed Aly + * \date Wed Oct 6, 2010 + * + */ + +#ifndef LANE_DETECTOR_HH +#define LANE_DETECTOR_HH + +#include "mcv.hh" +#include "InversePerspectiveMapping.hh" +#include "LaneDetector.hh" +#include "cmdline.h" + +#include +#include + +using namespace std; + +namespace LaneDetector +{ + +/** + * This function processes an input image and detects lanes/stoplines + * based on the passed in command line arguments + * + * \param filename the input file name + * \param cameraInfo the camera calibration info + * \param lanesConf the lane detection settings + * \param stoplinesConf the stop line detection settings + * \param options the command line arguments + * \param outputFile the output file stream to write output lanes to + * \param index the image index (used for saving output files) + * \param elapsedTime if NOT NULL, it is accumulated with clock ticks for + * the detection operation + */ +void ProcessImage(const char* filename, CameraInfo& cameraInfo, + LaneDetectorConf& lanesConf, LaneDetectorConf& stoplinesConf, + gengetopt_args_info& options, ofstream* outputFile = NULL, + int index = 0, clock_t *elapsedTime = NULL); + +/** + * This function reads lines from the input file into a vector of strings + * + * \param filename the input file name + * \param lines the output vector of lines + */ +bool ReadLines(const char* filename, vector *lines); + +} // namespace LaneDetector + +#endif //define LANE_DETECTOR_HH + diff --git a/src/main.o b/src/main.o new file mode 100644 index 0000000000000000000000000000000000000000..6751f6c762b6019bc01a0a784c3d43b918fe0522 GIT binary patch literal 84816 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 literal 0 HcmV?d00001 diff --git a/src/mcv.cc b/src/mcv.cc new file mode 100644 index 0000000..e2901d3 --- /dev/null +++ b/src/mcv.cc @@ -0,0 +1,182 @@ +/*** + * \file mcv.cc + * \author Mohamed Aly + * \date 11/29/2006 + */ + +#include "mcv.hh" + +#include +#include +#include + +using namespace std; + +#include +#include + +namespace LaneDetector +{ + +//some helper functions for debugging + +//print the matrix passed to it +void SHOW_MAT(const CvMat *pmat, char str[]) +{ + cerr << str << "\n"; + for(int i=0; irows; i++) + { + for(int j=0; jcols; j++) + cerr << cvGetReal2D(pmat, i, j) << " "; + cerr << "\n"; + } +} + +void SHOT_MAT_TYPE(const CvMat *pmat) +{ + cout << CV_MAT_TYPE(pmat->type) << "\n"; +} + +void SHOW_IMAGE(const CvMat *pmat, const char str[], int wait) +{ + //cout << "channels:" << CV_MAT_CN(pmat->type) << "\n"; + //scale it + //CvMat *mat = cvCreateMat(pmat->height, pmat->width, pmat->type); + //cvCopy(pmat, mat); + CvMat *mat = cvCloneMat(pmat);//->rows, pmat->cols, INT_MAT_TYPE);//cvCloneMat(pmat); + assert(mat); + //convert to int type + //cvConvert(pmat, mat); + if(CV_MAT_CN(mat->type) == 1)//FLOAT_MAT_TYPE) + mcvScaleMat(mat, mat); + //show it + //cout << "in\n"; + cvNamedWindow(str, CV_WINDOW_AUTOSIZE); //0 1 + cvShowImage(str, mat); + cvWaitKey(wait); + //cvDestroyWindow(str); + //clear + cvReleaseMat(&mat); + //cout << "out\n"; +} + +void SHOW_IMAGE(const IplImage *pmat, char str[]) +{ + //cout << "channels:" << CV_MAT_CN(pmat->type) << "\n"; + //scale it + //CvMat *mat = cvCreateMat(pmat->height, pmat->width, pmat->type); + //cvCopy(pmat, mat); + //CvMat *mat = cvCloneMat(pmat); + //assert(mat); + // mcvScaleMat(mat, mat); + //show it + //cout << "in\n"; + cvNamedWindow(str, 1); + cvShowImage(str, pmat); + cvWaitKey(0); + //cvDestroyWindow(str); + //clear + //cvReleaseMat(&mat); + //cout << "out\n"; +} + +void SHOW_POINT(const FLOAT_POINT2D pt, char str[]) +{ + cerr << str << "(" << pt.x << "," << pt.y << ")\n"; + cerr.flush(); +} + + +void SHOW_RECT(const CvRect rect, char str[]) +{ + cerr << str << "(x=" << rect.x << ", y=" << rect.y + << ", width=" << rect.width << ", height=" + << rect.height << ")" << endl; + cerr.flush(); +} + +/** + * This function reads in an image from file and returns the original color + * image and the first (red) channel scaled to [0 .. 1] with float type. + * images are allocated inside the function, so you will need to deallocate + * them + * + * \param filename the input file name + * \param clrImage the raw input image + * \param channelImage the first channel + */ +void mcvLoadImage(const char *filename, CvMat **clrImage, CvMat** channelImage) +{ + // load the image + IplImage* im; + im = cvLoadImage(filename, CV_LOAD_IMAGE_COLOR); + // convert to mat and get first channel + CvMat temp; + cvGetMat(im, &temp); + *clrImage = cvCloneMat(&temp); + // convert to single channel + CvMat *schannel_mat; + CvMat* tchannelImage = cvCreateMat(im->height, im->width, INT_MAT_TYPE); + cvSplit(*clrImage, tchannelImage, NULL, NULL, NULL); + // convert to float + *channelImage = cvCreateMat(im->height, im->width, FLOAT_MAT_TYPE); + cvConvertScale(tchannelImage, *channelImage, 1./255); + // destroy + cvReleaseMat(&tchannelImage); + cvReleaseImage(&im); +} + +/** + * This function scales the input image to have values 0->1 + * + * \param inImage the input image + * \param outImage hte output iamge + */ +void mcvScaleMat(const CvMat *inMat, CvMat *outMat) +{ + //convert inMat type to outMat type + cvConvert(inMat, outMat); + //if (CV_MAT_DEPTH(inMat->type) + //get the min and subtract it + double min; + cvMinMaxLoc(inMat, &min, 0, 0, 0, 0); + cvSubS(inMat, cvRealScalar(min), outMat); + + //get max and divide by it + double max; + cvMinMaxLoc(outMat, 0, &max, 0, 0, 0); + if(CV_MAT_TYPE(outMat->type) == FLOAT_MAT_TYPE) + cvConvertScale(outMat, outMat, 1/max); + else if(CV_MAT_TYPE(outMat->type) == INT_MAT_TYPE) + cvConvertScale(outMat, outMat, 255/max); +} + +/** + * This function creates a double matrix from an input vector + * + * \param vec the input vector + * \param mat the output matrix (column vector) + * + */ +template +CvMat* mcvVector2Mat(const vector &vec) +{ + CvMat *mat = 0; + + if (vec.size()>0) + { + //create the matrix + mat = cvCreateMat(vec.size(), 1, CV_64FC1); + //loop and get values + for (int i=0; i<(int)vec.size(); i++) + CV_MAT_ELEM(*mat, double, i, 0) =(double) vec[i]; + } + + //return + return mat; +} + +// template CvMat* mcvVector2Mat(const vector &vec); +// template CvMat* mcvVector2Mat(const vector &vec); + +} // namespace LaneDetector diff --git a/src/mcv.hh b/src/mcv.hh new file mode 100755 index 0000000..ae5e077 --- /dev/null +++ b/src/mcv.hh @@ -0,0 +1,85 @@ +/*** + * \file mcv.hh + * \author Mohamed Aly + * \date 11/29/2006 + */ + +#ifndef MCV_HH_ +#define MCV_HH_ + +#include +#include + +#include + +using namespace std; + +namespace LaneDetector +{ + +//constant definitions +#define FLOAT_MAT_TYPE CV_32FC1 +#define FLOAT_MAT_ELEM_TYPE float + +#define INT_MAT_TYPE CV_8UC1 +#define INT_MAT_ELEM_TYPE unsigned char + +#define FLOAT_IMAGE_TYPE IPL_DEPTH_32F +#define FLOAT_IMAGE_ELEM_TYPE float + +#define INT_IMAGE_TYPE IPL_DEPTH_8U +#define INT_IMAGE_ELEM_TYPE unsigned char + +#define FLOAT_POINT2D CvPoint2D32f +#define FLOAT_POINT2D_F cvPoint2D632f + +#define FLOAT float +#define INT int +#define SHORT_INT unsigned char + +//some helper functions for debugging +void SHOW_MAT(const CvMat *pmat, char str[]="Matrix"); + +void SHOT_MAT_TYPE(const CvMat *pmat); + +void SHOW_IMAGE(const CvMat *pmat, const char str[]="Window", int wait=0); +void SHOW_IMAGE(const IplImage *pmat, char str[]="Window"); + +void SHOW_POINT(const FLOAT_POINT2D pt, char str[]="Point:"); + +void SHOW_RECT(const CvRect rect, char str[]="Rect:"); + +/** + * This function reads in an image from file and returns the original color + * image and the first (red) channel scaled to [0 .. 1] with float type. The + * images are allocated inside the function, so you will need to deallocate + * them + * + * \param filename the input file name + * \param clrImage the raw input image + * \param channelImage the first channel + */ +void mcvLoadImage(const char *filename, CvMat **clrImage, CvMat** channelImage); + + +/** + * This function scales the input image to have values 0->1 + * + * \param inImage the input image + * \param outImage hte output iamge + */ +void mcvScaleMat(const CvMat *inImage, CvMat *outMat); + +/** + * This function creates a double matrix from an input vector + * + * \param vec the input vector + * \param mat the output matrix (column vector) + * + */ +template +CvMat* mcvVector2Mat(const vector &vec); + +} // namespace LaneDetector + +#endif /*MCV_HH_*/ diff --git a/src/mcv.o b/src/mcv.o new file mode 100644 index 0000000000000000000000000000000000000000..bf6a0c5add0f3afea84afa7d3aba9893c652ee54 GIT binary patch literal 9776 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 MO?f*8N62mezd*QZEdT%j literal 0 HcmV?d00001 diff --git a/src/ranker.h b/src/ranker.h new file mode 100644 index 0000000..674a61e --- /dev/null +++ b/src/ranker.h @@ -0,0 +1,202 @@ +#ifndef RANKER_H +#define RANKER_H + +#include +#include +#include + +using std::vector; +using std::string; + +#ifndef uint +typedef unsigned int uint; +#endif + +template +class lt { public: static int compare(T a, T b) { return(a < b); } }; +template +class gt { public: static int compare(T a, T b) { return(a > b); } }; + +template +class ranker +{ + private: + const T* p; + uint sz; + + public: + ranker(const vector& v) : p(&v[0]), sz(v.size()) { } + ranker(const T* tp, uint s) : p(tp), sz(s) { } + + int operator()(uint i1, uint i2) const { return(C::compare(p[i1],p[i2])); } + + template + void get_orders(vector& w) const { + w.resize(sz); + w.front() = 0; + for (typename vector::iterator i = w.begin(); i != w.end() - 1; ++i) + *(i + 1) = *i + 1; + std::sort(w.begin(), w.end(), *this); + } + + template + void get_partial_orders(vector& w, uint num) const { + if (num > sz) num = sz; + w.resize(sz); + w.front() = 0; + for (typename vector::iterator i = w.begin(); i != w.end() - 1; ++i) + *(i + 1) = *i + 1; + std::partial_sort(w.begin(), w.begin() + num, w.end(), *this); + w.resize(num); + } + + template + void get_ranks(vector& w, const string& method) const { + w.resize(sz); + vector tmp(w.size()); + get_orders(tmp); + if (method == "average") { + for (uint c = 0, reps; c < w.size(); c += reps) { reps = 1; + while (c + reps < w.size() && p[tmp[c]] == p[tmp[c + reps]]) ++reps; + for (uint k = 0; k < reps; ++k) + w[tmp[c + k]] = S(2 * c + reps - 1) / 2 + 1; + } + } else if (method == "min") { + for (uint c = 0, reps; c < w.size(); c += reps) { reps = 1; + while (c + reps < w.size() && p[tmp[c]] == p[tmp[c + reps]]) ++reps; + for (uint k = 0; k < reps; ++k) w[tmp[c + k]] = c + 1; + } + } else if (method == "max") { + for (uint c = 0, reps; c < w.size(); c += reps) { reps = 1; + while (c + reps < w.size() && p[tmp[c]] == p[tmp[c + reps]]) ++reps; + for (uint k = 0; k < reps; ++k) w[tmp[c + k]] = c + reps; + } + } else // default + for (uint c = 0; c < w.size(); ++c) w[tmp[c]] = c + 1; + } + + template + void get_partial_ranks(vector& w, const string& method, size_t num) const { + if (num > sz) num = sz; + vector tmp(sz); + get_partial_orders(tmp, num); + w.resize(sz); + fill(w.begin(), w.end(), 0); + if (method == "average") { + for (uint c = 0, reps; c < num; c += reps) { reps = 1; + while (c + reps < num && p[tmp[c]] == p[tmp[c + reps]]) ++reps; + for (uint k = 0; k < reps; ++k) + w[tmp[c + k]] = S(2 * c + reps - 1) / 2 + 1; + } + } else if (method == "min") { + for (uint c = 0, reps; c < num; c += reps) { reps = 1; + while (c + reps < num && p[tmp[c]] == p[tmp[c + reps]]) ++reps; + for (uint k = 0; k < reps; ++k) w[tmp[c + k]] = c + 1; + } + } else if (method == "max") { + for (uint c = 0, reps; c < num; c += reps) { reps = 1; + while (c + reps < num && p[tmp[c]] == p[tmp[c + reps]]) ++reps; + for (uint k = 0; k < reps; ++k) w[tmp[c + k]] = c + reps; + } + } else // default + for (uint c = 0; c < num; ++c) w[tmp[c]] = c + 1; + } + +}; + +template +inline void rank(const vector& v, vector& w, + const string& method = "average") + { ranker > r(v); r.get_ranks(w, method); } + +template +inline void rank(const T* d, uint size, vector& w, + const string& method = "average") + { ranker > r(d, size); r.get_ranks(w, method); } + +template +inline void partial_rank(const vector& v, vector& w, uint num, + const string& method = "average") + { ranker > r(v); r.get_partial_ranks(w, method, num); } + +template +inline void partial_rank(const T* d, uint size, vector& w, uint num, + const string& method = "average") + { ranker > r(d, size); r.get_partial_ranks(w, method, num); } + +template +inline void order(const vector& v, vector& w) + { ranker > r(v); r.get_orders(w); } + +template +inline void order(const T* d, uint size, vector& w) + { ranker > r(d, size); r.get_orders(w); } + +template +inline void partial_order(const vector& v, vector& w, uint num) + { ranker > r(v); r.get_partial_orders(w, num); } + +template +inline void partial_order(const T* d, uint size, vector& w, uint num) + { ranker > r(d, size); r.get_partial_orders(w, num); } + +template +inline void rankhigh(const vector& v, vector& w, + const string& method = "average") + { ranker > r(v); r.get_ranks(w, method); } + +template +inline void rankhigh(const T* d, uint size, vector& w, + const string& method = "average") + { ranker > r(d, size); r.get_ranks(w, method); } + +template +inline void partial_rankhigh(const vector& v, vector& w, uint num, + const string& method = "average") + { ranker > r(v); r.get_partial_ranks(w, method, num); } + +template +inline void partial_rankhigh(const T* d, uint size, vector& w, uint num, + const string& method = "average") + { ranker > r(d, size); r.get_partial_ranks(w, method, num); } + +template +inline void orderhigh(const vector& v, vector& w) + { ranker > r(v); r.get_orders(w); } + +template +inline void orderhigh(const T* d, uint size, vector& w) + { ranker > r(d, size); r.get_orders(w); } + +template +inline void partial_orderhigh(const vector& v, vector& w, uint num) + { ranker > r(v); r.get_partial_orders(w, num); } + +template +inline void partial_orderhigh(const T* d, uint size, vector& w, uint num) + { ranker > r(d, size); r.get_partial_orders(w, num); } + +template +inline T quantile(const T* d, const uint size, const double q) +{ + if (size == 0) return T(0); + if (size == 1) return d[0]; + if (q <= 0) return *std::min_element(d, d + size); + if (q >= 1) return *std::max_element(d, d + size); + + double pos = (size - 1) * q; + uint ind = uint(pos); + double delta = pos - ind; + vector w(size); std::copy(d, d + size, w.begin()); + std::nth_element(w.begin(), w.begin() + ind, w.end()); + T i1 = *(w.begin() + ind); + T i2 = *std::min_element(w.begin() + ind + 1, w.end()); + return (T) (i1 * (1.0 - delta) + i2 * delta); +} + +template +inline T quantile(const vector& v, const double q) + { return quantile(&v[0], v.size(), q); } + +#endif + diff --git a/src/run.sh b/src/run.sh new file mode 100755 index 0000000..715f3c0 --- /dev/null +++ b/src/run.sh @@ -0,0 +1,31 @@ +# This file runs the lane detection binary on the four clips available +# in the dataset + +# Author: Mohamed Aly +# Date: 10/7/2010 + +#clips to run +path="../clips" +clips="cordova1 cordova2 washington1 washington2" + +#get options +options=" --show --save-lanes --wait=50 --lanes-conf=Lanes.conf \ + --camera-conf=CameraInfo.conf " + +# suffix +binary="./LaneDetector$(getconf LONG_BIT)" + +#run +for clip in $clips; do + echo "Running for $clip..." + echo "------------------------------------------------------------------" + echo + + # command + command="$binary $options --list-file=$path/$clip/list.txt \ + --list-path=$path/$clip/ --output-suffix=_results " + echo $command + + # run + $command +done