6

I'm working on a project in which I use OpenCV to detect shapes and their colors.

There are 5 colors (red, green, yellow, blue and white) and 4 shapes (rectangle, star, circle and heart). I've been able to reliably discern the colors and I can detect the shapes when the image used is a drawn image like this using this code. Note, the image is for demonstration only, the range values in my code are not for these colors.

import cv2
import numpy as np
class Shape():

    def __init__(self, color, shape, x, y, approx):
        self.color = color
        self.shape = shape
        self.x = x
        self.y = y
        self.approx = approx
def closing(mask):
kernel = np.ones((7,7),np.uint8) 
closing = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
return closing

def opening(mask):
    kernel = np.ones((6,6),np.uint8)
    opening = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
    return opening

#Define Red
lower_red = np.array([0, 90, 60], dtype=np.uint8)
upper_red = np.array([10, 255, 255], dtype=np.uint8)
red = [lower_red, upper_red, 'red']

#Define Green
lower_green = np.array([60, 55, 0], dtype=np.uint8)
upper_green = np.array([100, 255, 120], dtype=np.uint8)
green = [lower_green, upper_green, 'green']

#Define Blue
lower_blue = np.array([90, 20, 60], dtype=np.uint8)
upper_blue = np.array([130, 255, 180], dtype=np.uint8)
blue = [lower_blue, upper_blue, 'blue']

#Define Yellow
lower_yellow = np.array([5, 110, 200], dtype=np.uint8)
upper_yellow = np.array([50, 255, 255], dtype=np.uint8)
yellow = [lower_yellow, upper_yellow, 'yellow']

#Define White
lower_white = np.array([0, 90, 60], dtype=np.uint8)
upper_white = np.array([10, 255, 255], dtype=np.uint8)
white = [lower_white, upper_white ,'white']

colors = [red, green, blue, yellow, white]

def detect_shapes(image_location):
    #Open image
    img = cv2.imread(image_location)

    #Convert to hsv
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    #Shape list
    shapes = []

    #Lay over masks and detect shapes
    for color in colors:
        mask = cv2.inRange(hsv, color[0], color[1])
        mask = closing(mask)
        mask = opening(mask)
        contours, h = cv2.findContours(mask, 1, cv2.CHAIN_APPROX_SIMPLE)
        contours.sort(key = len)
        for contour in contours[-3:]:
            #Amount of edges
            approx = cv2.approxPolyDP(contour, 0.01*cv2.arcLength(contour, True), True)
            #Center locations
            M = cv2.moments(contour)
            if M['m00'] == 0.0:
                continue
            centroid_x = int(M['m10']/M['m00'])
            centroid_y = int(M['m01']/M['m00'])

            if len(approx) == 4:
                shape_name = 'rectangle'
            elif len(approx) == 10:
                shape_name = 'star'
            elif len(approx) >= 11:
                shape_name = 'oval'
            else:
                shape_name ='undefined'

            shape = Shape(color[2], shape_name, centroid_x, centroid_y, len(approx))
            shapes.append(shape)

    return shapes

This is largely based off the answers on this question.

However, when I try to detect the shapes on an actual photo, I cannot reliably use this. The amount of edges I get varies wildly. This is an example of a photo I need to recognize the shapes on. I'm guessing this happens because of small imperfections on the edges of the shapes, but I can't figure out how I would approximate those edges with straight lines, or how I would reliably recognize circles. What would I need to change in the code to do this? Intensive googling hasn't given me an answer yet but that might be because I'm not using the correct terminology in the searches...

Also, if this question is not formatted correctly, let me know!

Community
  • 1
  • 1
Habba
  • 475
  • 2
  • 5
  • 15
  • It may work better if a larger number than `0.01` is used when computing `approx`. You would likely then have problems with the circle components, however, which may be better treated separately. – Geoff Feb 25 '14 at 19:49
  • @Habba Give a try on [Convex Hull](http://docs.opencv.org/doc/tutorials/imgproc/shapedescriptors/hull/hull.html) – Haris Feb 26 '14 at 04:54
  • @Haris That might work for circles and rectangles, but would stars be recognized? Or should I look for pentagons then? – Habba Feb 26 '14 at 11:13
  • @Habba If you don’t have complex shapes you can proceed with approxPolyDP() where you will have 15 more Point set for star shapes, see the [result](http://i.imgur.com/FLAubnf.jpg?1) I got, here green is detected shapes. – Haris Feb 26 '14 at 12:14
  • @Haris Wow that looks really good. Could you explain what you did? Convex hull first and then approxPolyDP? Or Canny? I've read about that but couldn't figure out if it would be useful for this problem. – Habba Feb 26 '14 at 12:26
  • @Habba The steps are blur->gray->canny->findcontour->approxPolyDP etc..I have done it in C++. If you need I will paste it below. – Haris Feb 26 '14 at 12:29
  • @Haris That would be really nice, the conversion to Python isn't too hard usually! So you haven't used the convex hull at all? – Habba Feb 26 '14 at 12:33
  • @ Habba No, you can try it as another option... – Haris Feb 26 '14 at 12:34
  • @Haris Oh yeah of course... Could you paste your C++ code? – Habba Feb 26 '14 at 12:40

1 Answers1

9

Here is the code I proceed with your image, the code will do

  1. Blur the source
  2. Canny Edge detection.
  3. Find contour.
  4. approxPolyDP for the contour.
  5. Check total size of approxPolyDP points.

Code:

   Mat src=imread("src.jpg",1);
   Mat thr,gray;
   blur(src,src,Size(3,3));
   cvtColor(src,gray,CV_BGR2GRAY);
   Canny(gray,thr,50, 190, 3, false );
   vector<vector<Point> > contours;
   vector<Vec4i> hierarchy;
   findContours( thr.clone(),contours,hierarchy,CV_RETR_EXTERNAL,CV_CHAIN_APPROX_SIMPLE,Point(0,0));

   vector<vector<Point> > contours_poly(contours.size());
   vector<Rect> boundRect( contours.size() );
   vector<Point2f>center( contours.size() );
   vector<float>radius( contours.size() );
   vector<vector<Point> >hull( contours.size() );
   for( int i = 0; i < contours.size(); i++ )
    {
    approxPolyDP( Mat(contours[i]), contours_poly[i], 10, true );
    boundRect[i] = boundingRect( Mat(contours_poly[i]) );
    minEnclosingCircle( (Mat)contours_poly[i], center[i], radius[i] );
    convexHull( Mat(contours[i]), hull[i], false );

    if( contours_poly[i].size()>15) // Check for corner
       drawContours( src, contours_poly, i, Scalar(0,255,0), 2, 8, vector<Vec4i>(), 0, Point() ); // True object
    else
       drawContours( src, contours_poly, i, Scalar(0,0,255), 2, 8, vector<Vec4i>(), 0, Point() ); // false object
      //drawContours( src, hull, i, Scalar(0,0,255), 2, 8, vector<Vec4i>(), 0, Point() );
      // rectangle( src, boundRect[i].tl(), boundRect[i].br(), Scalar(0,255,0), 2, 8, 0 );
       //circle( src, center[i], (int)radius[i], Scalar(0,0,255), 2, 8, 0 );
    }
   imshow("src",src);
   imshow("Canny",thr);
   waitKey();

enter image description here enter image description here

Haris
  • 13,645
  • 12
  • 90
  • 121
  • Thanks a lot! This will definitely help me understand how to do these things. – Habba Feb 26 '14 at 12:56
  • Could you explain a bit more the fifth step? Thanks! – Btc Sources May 13 '15 at 11:33
  • 1
    That is you have to check how many points are there inside `contours_poly` which is a vector of type cv::Point. contours_poly contains corner points for each contour. So that you can confirm whether object is star or not. – Haris May 13 '15 at 15:55
  • This is c++ right , is the python code around ? Just curious I don't write c, I can convert it to python anyway thanks this is great ! – naeluh Mar 09 '17 at 04:19