How to get an expanded or contracted contour in OpenCV? - c++

Is it possible to get the expanded or contracted version of a contour?
For example in the below image, I have used cv::findContour() and cv::drawContour on a binary image to get the contours:
I would like to draw another contour which has a customed pixel distance from the original contour, like these:
Except for eroding, which I think it might not be a good idea as it seems hard to control the pixel distance using eroding, I have no idea on how to solve this problem. May I know what should be the correct direction?

Using cv::erode with a small kernel and multiple iterations may be enough for your needs, even if it's not exact.
C++ code:
cv::Mat img = ...;
int iterations = 10;
cv::erode(img, img,
cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3)),
cv::Point(-1,-1),
iterations);
Demo:
# img is the image containing the original black contour
for form in [cv.MORPH_RECT, cv.MORPH_CROSS]:
eroded = cv.erode(img, cv.getStructuringElement(form, (3,3)), iterations=10)
contours, hierarchy = cv.findContours(~eroded, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
vis = cv.cvtColor(img, cv.COLOR_GRAY2BGR)
cv.drawContours(vis, contours, 0, (0,0,255))
cv.drawContours(vis, contours, 1, (255,0,0))
show_image(vis)
10 iterations with cv.MORPH_RECT with a 3x3 kernel:
10 iterations with cv.MORPH_CROSS with a 3x3 kernel:
You can change the offset by adjusting the number of iterations.
A much more accurate approach would be to use cv::distanceTransform to find all pixels that lie approximately 10px away from the contour:
dist = cv.distanceTransform(img, cv.DIST_L2, cv.DIST_MASK_PRECISE)
ring = cv.inRange(dist, 9.5, 10.5) # take all pixels at distance between 9.5px and 10.5px
show_image(ring)
contours, hierarchy = cv.findContours(ring, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
vis = cv.cvtColor(img, cv.COLOR_GRAY2BGR)
cv.drawContours(vis, contours, 0, (0,0,255))
cv.drawContours(vis, contours, 2, (255,0,0))
show_image(vis)
You'll get two contours on each side of the original contour. Use findContours with RETR_EXTERNAL to recover only the outer contour. To also recover the inner contour, use RETR_LIST

I think the solution can be easier, without dilataion and new contours.
For each contour search mass center: cv::moments(contours[i]) -> cv::Point2f mc(mu.m10 / mu.m00), mu.m01 / mu.m00));
For each point point of contour: make shift for mass center -> multiply by coefficient K -> shift backward: pt_new = (k * (pt - mc) + mc);
But coefficient k must be individual for each point. I will calculate it a little later...

Related

Filter OpenCV Contours by inside colors

I have a problem with filtering some contours by colors in it. I want to remove all contours, which has black pixels inside and keep only contours with white pixels (see pictures below).
Code to create a contours list. I've used a RETR_TREE contour retrieval mode with CHAIN_APPROX_SIMPLE points selection to avoid a lot of points inside contours.
cv::cvtColor(src_img, gray_img, cv::COLOR_BGR2GRAY);
cv::threshold(gray_img, bin_img, minRGB, maxRGB, cv::THRESH_BINARY_INV);
std::vector<std::vector<cv::Point> > contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(bin_img, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
Then, using these contours, I've built closed paths and display them on the screen.
An input image:
Current my results:
What I need. Fill only contours, which has white content.
I've tried to scale all contours to 1 pixel inside and check if all the pixels equal to dark, but it doesn't work as I've expected. See the code below.
double scaleX = (double(src_img.cols) - 2) / double(src_img.cols);
double scaleY = (double(src_img.rows) - 2) / double(src_img.rows);
for (int i = 0; i < contours.size(); i++) {
std::vector<cv::Point> contour = contours[i];
cv::Moments M = cv::moments(contour);
int cx = int(M.m10 / M.m00);
int cy = int(M.m01 / M.m00);
std::vector<cv::Point> scaledContour(contour.size());
for (int j = 0; j < contour.size(); j++) {
cv::Point point = contour[j];
point = cv::Point(point.x - cx, point.y - cy);
point = cv::Point(point.x * scaleX, point.y * scaleY);
point = cv::Point(point.x + cx, point.y + cy);
scaledContour[j] = point;
}
contours[i] = scaledContour;
}
I will be very grateful if you help with any ideas or solutions, thank you very much!
Hopefully, one thing is clear that the objects in the image should be white and the background black when finding contours that you have done by using THRESH_BINARY_INV.
So we are essentially trying to find white lines and not black. I am not providing the code as I work in python but I'll list it out how it can be done.
Create a black array of the size of the input image. Let's call it mask.
After finding the contours, draw them on mask with white i.e. 255, while providing thickness=-1. This means we are essentially filling the contour.
Now we need to remove the boundary of the contour so the only portion left is the part inside the contour. This can be achieved by again drawing the contour on mask, this time with black with a thickness of 1.
Perform bitwise_and between the image and mask. Only areas having white inside the contour will be left.
Now you just need to see whether the output is completely black or not. If it is not that means you don't need to fill that contour as it contains something inside it.
EDIT
Ohh I didn't realize that your images would be having 600 contours, yes it will take a lot of time for that and I don't know why I didn't think of using hierarchy before.
You can use RETR_TREE itself and the hierarchy values are [next, previous, first_child, parent]. So we just need to check if the value of first_child=-1, that would mean there are no contours inside and you can fill it.
I've changed the mode to RETR_CCOMP and add a region filtration by a hierarchy[contour index][3] != -1 (means, no parent), and my problem was solved.
Thank you!

Proper way to determine thresholding parameters

I am trying to find triangles (blue contours) and trapezoids (yellow contours) in real time. In general it's okay.
But there is some problems. First it's a false positives. Triangles become trapezoids and vice versa. And I don't know how to how to solve this problem.
Second it's "noise". . I tried to check area of the figure, but the noise can be equal to the area. So it did not help so much. The noise depends on the thresholding parameters. cv::adaptiveThresholddoes not help at all. It's adds even more noise (and it so SLOW) erode and dilate cant fix it in a proper way
And here is my code.
cv::Mat detect(cv::Mat imageRGB)
{
//RGB -> GRAY
cv::Mat imageGray;
cv::cvtColor(imageRGB, imageGray, CV_BGR2GRAY);
//Bluring it
cv::Mat image;
cv::GaussianBlur(imageGray, image, cv::Size(5,5), 2);
//Thresholding
cv::threshold(image, image, 100, 255, CV_THRESH_BINARY_INV);
//SLOW and NOISE
//cv::adaptiveThreshold(image, image, 255.0, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, 21, 0);
//Calculating canny params.
cv::Scalar mu;
cv::Scalar sigma;
cv::meanStdDev(image, mu, sigma);
cv::Mat imageCanny;
cv::Canny(image,
imageCanny,
mu.val[0] + sigma.val[0],
mu.val[0] - sigma.val[0]);
//Detecting conturs.
std::vector<std::vector<cv::Point> > contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(imageCanny, contours, hierarchy,CV_RETR_TREE, CV_CHAIN_APPROX_NONE);
//Hierarchy is not needed here so clear it.
hierarchy.clear();
for (std::size_t i = 0; i < contours.size(); i++)
{
//fitEllipse need at last 5 points.
if (contours.at(i).size() < 5)
{
continue;
}
//Skip small contours.
if (std::fabs(cv::contourArea(contours.at(i))) < 800.0)
{
continue;
}
//Calculating RotatedRect from contours NOT from hull
//because fitEllipse need at last 5 points.
cv::RotatedRect bEllipse = cv::fitEllipse(contours.at(i));
//Finds the convex hull of a point set.
std::vector<cv::Point> hull;
cv::convexHull(contours.at(i), hull, true);
//Approx it, so we'll get 3 point for triangles
//and 4 points for trapez.
cv::approxPolyDP(hull, hull, 15, true);
//Is our contour convex. It's mast be.
if (!cv::isContourConvex(hull))
{
continue;
}
//Triangle
if (hull.size() == 3)
{
cv::drawContours(imageRGB, contours, i, cv::Scalar(255, 0, 0), 2);
cv::circle(imageRGB, bEllipse.center, 3, cv::Scalar(0, 255, 0), 2);
}
//trapez
if (hull.size() == 4)
{
cv::drawContours(imageRGB, contours, i, cv::Scalar(0, 255, 255), 2);
cv::circle(imageRGB, bEllipse.center, 3, cv::Scalar(0, 0, 255), 2);
}
}
return imageRGB;
}
So... In general all problems coused by wrong thresholding parameters, how can I calculete it in a proper way (automatically, of course)? And how can I can (lol, sorry for my english) prevent false positives?
Thesholding - i think that you should try Otsu binarization - here is some theory and a nice picture and here is documentation. This kind of thresholding generally is trying to find 2 most common values in image and use average value of them as a threshold value.
Alternatively consider using HSV color space, it might be easier to distinguish black and white regions from other regions. Another idea is to use inRange function (in RGB or in HSV color space - should work in woth situations) - you need to find 2 ranges (one from black regions and one for white) and search only for those regions (using inRange function) - look at this post.
Another way to accomplish this task might be using some library for blob extraction like this one or blob extractor which is part of OpenCV.
Distinguish triangle from trapezoid - i see 2 basic ways to improve you solution here:
in this line cv::approxPolyDP(hull, hull, 15, true); make third parameter (15 in this situation) not a constant value, but some part of contour area or length. Definitely it should adapt to contour size, it can't be just a canstant value. It's hard to say how to calculate it without some testing - try to start with 1-5% of contour area or length (i would start with length, but this is just my guess) and see whether this value is fine/to big/to small an check other values if needed. Unfortunetely there is no other way, but finding this equation manually shouldn't take very long time.
when you have 4 or 5 points calculate the equations of lines which join consecutive points (point 1 with point 2, point 2 with point 3, etc don't forget to calculate line between first point and last point), than check whether any 2 of those lines are parallel (or at least are close to being parallel - angle between them is close to 0 degress) - if you find any parallel lines than this contour is trapezoid, otherwise it's a triangle.

Matching small grayscale images

I want to test whether two images match. Partial matches also interest me.
The problem is that the images suffer from strong noise. Another problem is that the images might be rotated with an unknown angle. The objects shown in the images will roughly always have the same scale!
The images show area scans from a top-shot perspective. "Lines" are mostly walls and other objects are mostly trees and different kinds of plants.
Another problem was, that the left image was very blurry and the right one's lines were very thin.
To compensate for this difference I used dilation. The resulting images are the ones I uploaded.
Although It can easily be seen that these images match almost perfectly I cannot convince my algorithm of this fact.
My first idea was a feature based matching, but the matches are horrible. It only worked for a rotation angle of -90°, 0° and 90°. Although most descriptors are rotation invariant (in past projects they really were), the rotation invariance seems to fail for this example.
My second idea was to split the images into several smaller segments and to use template matching. So I segmented the images and, again, for the human eye they are pretty easy to match. The goal of this step was to segment the different walls and trees/plants.
The upper row are parts of the left, and the lower are parts of the right image. After the segmentation the segments were dilated again.
As already mentioned: Template matching failed, as did contour based template matching and contour matching.
I think the dilation of the images was very important, because it was nearly impossible for the human eye to match the segments without dilation before the segmentation. Another dilation after the segmentation made this even less difficult.
Your first job should be to fix the orientation. I am not sure what is the best algorithm to do that but here is an approach I would use: fix one of the images and start rotating the other. For each rotation compute a histogram for the color intense on each of the rows/columns. Compute some distance between the resulting vectors(e.g. use cross product). Choose the rotation that results in smallest cross product. It may be good idea to combine this approach with hill climbing.
Once you have the images aligned in approximately the same direction, I believe matching should be easier. As the two images are supposed to be at the same scale, compute something analogous to the geometrical center for both images: compute weighted sum of all pixels - a completely white pixel would have a weight of 1, and a completely black - weight 0, the sum should be a vector of size 2(x and y coordinate). After that divide those values by the dimensions of the image and call this "geometrical center of the image". Overlay the two images in a way that the two centers coincide and then once more compute cross product for the difference between the images. I would say this should be their difference.
You can also try following methods to find rotation and similarity.
Use image moments to get the rotation as shown here.
Once you rotate the image, use cross-correlation to evaluate the similarity.
EDIT
I tried this with OpenCV and C++ for the two sample images. I'm posting the code and results below as it seems to work well at least for the given samples.
Here's the function to calculate the orientation vector using image moments:
Mat orientVec(Mat& im)
{
Moments m = moments(im);
double cov[4] = {m.mu20/m.m00, m.mu11/m.m00, m.mu11/m.m00, m.mu02/m.m00};
Mat covMat(2, 2, CV_64F, cov);
Mat evals, evecs;
eigen(covMat, evals, evecs);
return evecs.row(0);
}
Rotate and match sample images:
Mat im1 = imread(INPUT_FOLDER_PATH + string("WojUi.png"), 0);
Mat im2 = imread(INPUT_FOLDER_PATH + string("XbrsV.png"), 0);
// get the orientation vector
Mat v1 = orientVec(im1);
Mat v2 = orientVec(im2);
double angle = acos(v1.dot(v2))*180/CV_PI;
// rotate im2. try rotating with -angle and +angle. here using -angle
Mat rot = getRotationMatrix2D(Point(im2.cols/2, im2.rows/2), -angle, 1.0);
Mat im2Rot;
warpAffine(im2, im2Rot, rot, Size(im2.rows, im2.cols));
// add a border to rotated image
int borderSize = im1.rows > im2.cols ? im1.rows/2 + 1 : im1.cols/2 + 1;
Mat im2RotBorder;
copyMakeBorder(im2Rot, im2RotBorder, borderSize, borderSize, borderSize, borderSize,
BORDER_CONSTANT, Scalar(0, 0, 0));
// normalized cross-correlation
Mat& image = im2RotBorder;
Mat& templ = im1;
Mat nxcor;
matchTemplate(image, templ, nxcor, CV_TM_CCOEFF_NORMED);
// take the max
double max;
Point maxPt;
minMaxLoc(nxcor, NULL, &max, NULL, &maxPt);
// draw the match
Mat rgb;
cvtColor(image, rgb, CV_GRAY2BGR);
rectangle(rgb, maxPt, Point(maxPt.x+templ.cols-1, maxPt.y+templ.rows-1), Scalar(0, 255, 255), 2);
cout << "max: " << max << endl;
With -angle rotation in code, I get max = 0.758. Below is the rotated image in this case with the matching region.
Otherwise max = 0.293

Automatic perspective correction OpenCV

I am trying to implement Automatic perspective correction in my iOS program and when I use the test image I found on the tutorial everything works as expected. But when I take a picture I get back a weird result.
I am using code found in this tutorial
When I give it an image that looks like this:
I get this as the result:
Here is what dst gives me that might help.
I am using this to call the method which contains the code.
quadSegmentation(Img, bw, dst, quad);
Can anyone tell me when I am getting so many green lines compared to the tutorial? And how I might be able to fix this and properly crop the image to only contain the card?
For perspective transform you need,
source points->Coordinates of quadrangle vertices in the source image.
destination points-> Coordinates of the corresponding quadrangle vertices in the destination image.
Here we will calculate these point by contour process.
Calculate Coordinates of quadrangle vertices in the source image
You will get the your card as contour by just by blurring, thresholding, then find contour, find largest contour etc..
After finding largest contour just calculate approximates a polygonal curve, here you should get 4 Point which represent corners of your card. You can adjust the parameter epsilon to make 4 co-ordinates.
Calculate Coordinates of the corresponding quadrangle vertices in the destination image
This can be easily find out by calculating bounding rectangle for largest contour.
In below image the red rectangle represent source points and green for destination points.
Adjust the co-ordinates order and Apply Perspective transform
Here I manually adjust the co-ordinates order and you can use some sorting algorithm.
Then calculate transformation matrix and apply wrapPrespective
See the final result
Code
Mat src=imread("card.jpg");
Mat thr;
cvtColor(src,thr,CV_BGR2GRAY);
threshold( thr, thr, 70, 255,CV_THRESH_BINARY );
vector< vector <Point> > contours; // Vector for storing contour
vector< Vec4i > hierarchy;
int largest_contour_index=0;
int largest_area=0;
Mat dst(src.rows,src.cols,CV_8UC1,Scalar::all(0)); //create destination image
findContours( thr.clone(), contours, hierarchy,CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE ); // Find the contours in the image
for( int i = 0; i< contours.size(); i++ ){
double a=contourArea( contours[i],false); // Find the area of contour
if(a>largest_area){
largest_area=a;
largest_contour_index=i; //Store the index of largest contour
}
}
drawContours( dst,contours, largest_contour_index, Scalar(255,255,255),CV_FILLED, 8, hierarchy );
vector<vector<Point> > contours_poly(1);
approxPolyDP( Mat(contours[largest_contour_index]), contours_poly[0],5, true );
Rect boundRect=boundingRect(contours[largest_contour_index]);
if(contours_poly[0].size()==4){
std::vector<Point2f> quad_pts;
std::vector<Point2f> squre_pts;
quad_pts.push_back(Point2f(contours_poly[0][0].x,contours_poly[0][0].y));
quad_pts.push_back(Point2f(contours_poly[0][1].x,contours_poly[0][1].y));
quad_pts.push_back(Point2f(contours_poly[0][3].x,contours_poly[0][3].y));
quad_pts.push_back(Point2f(contours_poly[0][2].x,contours_poly[0][2].y));
squre_pts.push_back(Point2f(boundRect.x,boundRect.y));
squre_pts.push_back(Point2f(boundRect.x,boundRect.y+boundRect.height));
squre_pts.push_back(Point2f(boundRect.x+boundRect.width,boundRect.y));
squre_pts.push_back(Point2f(boundRect.x+boundRect.width,boundRect.y+boundRect.height));
Mat transmtx = getPerspectiveTransform(quad_pts,squre_pts);
Mat transformed = Mat::zeros(src.rows, src.cols, CV_8UC3);
warpPerspective(src, transformed, transmtx, src.size());
Point P1=contours_poly[0][0];
Point P2=contours_poly[0][1];
Point P3=contours_poly[0][2];
Point P4=contours_poly[0][3];
line(src,P1,P2, Scalar(0,0,255),1,CV_AA,0);
line(src,P2,P3, Scalar(0,0,255),1,CV_AA,0);
line(src,P3,P4, Scalar(0,0,255),1,CV_AA,0);
line(src,P4,P1, Scalar(0,0,255),1,CV_AA,0);
rectangle(src,boundRect,Scalar(0,255,0),1,8,0);
rectangle(transformed,boundRect,Scalar(0,255,0),1,8,0);
imshow("quadrilateral", transformed);
imshow("thr",thr);
imshow("dst",dst);
imshow("src",src);
imwrite("result1.jpg",dst);
imwrite("result2.jpg",src);
imwrite("result3.jpg",transformed);
waitKey();
}
else
cout<<"Make sure that your are getting 4 corner using approxPolyDP..."<<endl;
teethe This typically happens when you rely on somebody else code to solve your particular problem instead of adopting the code. Look at the processing stages and also the difference between their and your image (it is a good idea by the way to start with their image and make sure the code works):
Get the edge map. - will probably work since your edges are fine
Detect lines with Hough transform. - fail since you have lines not only on the contour but also inside of your card. So expect a lot of false alarm lines
Get the corners by finding intersections between lines. - fail for the above mentioned reason
Check if the approximate polygonal curve has 4 vertices. - fail
Determine top-left, bottom-left, top-right, and bottom-right corner. - fail
Apply the perspective transformation. - fail completely
To fix your problem you have to ensure that only lines on the periphery are extracted. If you always have a dark background you can use this fact to discard the lines with other contrasts/polarities. Alternatively you can extract all the lines and then select the ones that are closest to the image boundary (if your background doesn't have lines).

OpenCV Canny + Watershed

I'm using a canny edge detection and a finding contours function (both OpenCV) to create markers for the watershed transform. Everything works fine but I'm not 100% satisfied with the results. The reason is that some edges are missing and therefore important information is lost. In more detail, I got a bunch of windows (front views), which are rectangles, after the watershed transform I end up with something like this:
but I would rather have nice rectangles, that are complete and not open to one side. While maintaining irregular shapes (bushes in front of the house, cars..) Any ideas how I could solve this problem?I thought about overlaying the whole image with a grid, but I can't make it work.
Thank you very much.
Here is my code:
Mat gray;
cvtColor(im, gray, CV_BGR2GRAY);
// Use Canny instead of threshold to catch squares with gradient shading
Mat bw;
Canny(gray, bw, 0, 100, 5, true);
// Find contours
vector<vector<Point> > contours;
vector<Vec4i> hierarchy;
findContours( bw, contours, hierarchy,
CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE );
// watershed
Mat markers(bw.size(), CV_32S);
markers = Scalar::all(0);
int idx = 0;
int compCount = 0;
for( ; idx >= 0; idx = hierarchy[idx][0], compCount++ ) {
if (fabs(contourArea(contours[compCount])) < min_size )
continue;
drawContours(markers, contours, idx, Scalar::all(compCount+1), 1, 8, hierarchy, INT_MAX);
}
watershed( im, markers );
As requested, here is the original image, the image I would like to get and my output:
And I would like to have a segmentation like this (although over segmentation does not hurt, I just need to make sure, I get all the details):
While I get something like this:
(please ignore the colours, they are not important for this question and are just a result of my overall program). This is only one example, if you want, I can show you more, also please have a look at the etrims dataset, all my pictures are from there.
Two things -
1) As already mentioned, edge detection results in spurious edges being picked up.
2) Using these edges as markers for watershed segmentation results in over-segmentation because every marker produces a segmented region in the output.
Strategy -
(i) Preprocessing: Smooth the image heavily (morphological opening by reconstruction can be used for homogenizing the intensities without significantly affecting edges you are interested in).
(ii) Markers: Instead of using edges as seeds, I'd use the local extrema. Ideally, we want one marker for every region we want segmented.
(iii) Segmentation: Find the gradient magnitude (range filtering is also a good option) of the image from step (i) and use that as the segmentation function.
Using this strategy, I get the following segmentation.
Alternatively, after step (i), you can use Canny edge detection and do some morphological cleanup (to fill contours and remove edges that remain). This is what I get.
These are not exactly the expected segmentation (some objects like the car are not detected), but are a good start.
Edit: The MATLAB code used to generate the images -
% convert to grayscale
img = rgb2gray(origImg);
% create an appropriate structuring element
w_size = 20;
seSquare = strel('square', w_size);
% opening by reconstruction - to smooth dark regions
imgEroded = imerode(img, seSquare);
imgRecon = imreconstruct(imgEroded, img);
% invert and repeat - to smooth bright regions
imgReconComp = imcomplement(imgRecon);
imgEroded2 = imerode(imgReconComp, seSquare);
imgRecon2 = imreconstruct(imgEroded2, imgReconComp);
% get foreground markers
fgm = imregionalmax(imgRecon2);
% get background markers - this step can be skipped
% in which case only fgm would be the marker image
% and the segmentation would be different
distTrans = bwdist(fgm);
wLines= watershed(distTrans);
bgm = wLines == 0;
% get the segmentation function and impose markers
% perform watershed segmentation
seSquare3 = strel('square', 3);
rangeImg = rangefilt(imgRecon2, getnhood(seSquare3));
segFunc = imimposemin(rangeImg, fgm | bgm);
grayLabel = watershed(segFunc);
rgbLabel= label2rgb(grayLabel);
figure, imshow(rgbLabel); title('Output using Watershed')
% alternatively, extract edges from the preprocessed image
% perform morph cleanup
bwEdges = edge(imgRecon2, 'canny');
bwFilled = imfill(bwEdges, 'holes');
bwRegions = imopen(bwFilled, seSquare3);
grayLabel = bwlabel(bwRegions);
rgbLabel = label2rgb(grayLabel, 'jet', 'k');
figure, imshow(rgbLabel); title('Output using Canny')
from the looks of the desired output and the program's output, it seems that the edge detector is finding spurious edges. Canny edge detector contains a low-pass filter, but it might help for you to do a separate Gaussian low-pass filtering step before you actually run the Canny edge detector.
Other than that, it is difficult to achieve the desired result. For e.g., look at the top-most windows in the picture. They have distinct colors --- the frame, the shadow of the frame, and the window. The boundaries of these colors will be detected as edges by the Edge detector.