简体   繁体   中英

how to find a rectangular contour in an image?

I am currently working on a project of identifying football fields from a satellite image.

It's a top view satellite image of a football field 这是足球场的顶视图卫星图像

I used mediumblur function to clear all the small impurities in this image. later I selected only green part of the image and made a mask using the command cv2.inRange(hsv_img, light_green, dark_green) where light_green and dark_green is my range of green in hsv. after I got my mask. I got this as an output:
蒙版后输出

Since it has some impurities, i used mediumblur function median = cv2.medianBlur(image, 45) the output that I got is:
使用中模糊后

As you can see, I have got many contours and the main rectangular contour in the middle. I need an algorithm which selects such rectangular contours from the image and I have to neglect the rest. what should I do after this?

My approach would be:

  1. find "square" or "rectangle" like shapes from the input image
  2. find the one with max area size.

Assuming the input is the "median" result as you have computed:

在此处输入图片说明

Median Image (Input):

  1. First of all, import necessary libraries and purify the image.

     import cv2 import numpy as np # assuming you have the result image store in median # median = cv2.imread("abc.jpg", 0) image_gray = median image_gray = np.where(image_gray > 30, 255, image_gray) image_gray = np.where(image_gray <= 30, 0, image_gray) image_gray = cv2.adaptiveThreshold(image_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 115, 1) 
  2. Find the contours and then apply filter function based on their shapes.

     _, contours, _ = cv2.findContours(image_gray, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) rect_cnts = [] for cnt in contours: peri = cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, 0.04 * peri, True) (x, y, w, h) = cv2.boundingRect(cnt) ar = w / float(h) if len(approx) == 4: # shape filtering condition rect_cnts.append(cnt) 
  3. Find the one with the maximum area, and draw the result.

     max_area = 0 football_square = None for cnt in rect_cnts: (x, y, w, h) = cv2.boundingRect(cnt) if max_area < w*h: max_area = w*h football_square = cnt # Draw the result image = cv2.cvtColor(image_gray, cv2.COLOR_GRAY2RGB) cv2.drawContours(image, [football_square], -1, (0, 0,255), 5) cv2.imshow("Result Preview", image) cv2.waitKey() 

Result Preview:

在此处输入图片说明

This answer is derived from the great article at PyImageSearch " OpenCV shape detection ". Thanks.

So, a couple notes:

  1. I'm confident there's a better way to do this (there probably is a library, I haven't really checked).
  2. This code only works when looking for closed areas, like rectangles. Stars, donuts and other 'complicated' shapes will not be (completely) found using this code.
  3. This only finds the largest single area, it also returns a list of areas, you can probably write something that checks if a specific area has the right dimensions and is rectangular enough yourself

The program works as following:

First it reads the image and determines per pixel wether it is black or white. Next, it reads where a white region starts and ends, per line. After this, it acclomerates regions where at least one pixel overlap is required and each region has to be on a consecutive line. Watch out, it only connects one region per line, if you have, for instance, a star shape where two parts connect at a lower point this code WILL NOT WORK, and you will have to do some rework (see below for an example of what I exactly mean) . Lastly, it checks what region is the largest and adds a fat red line around it.

在此处输入图片说明

from PIL import Image
from copy import copy


def area(lst):
    '''
    :param lst: a list of tuples where each subsequent tuple indicates a row and the first two values indicate the start and end values of the row 
    :return: the total area of the shape described by these tuples
    '''
    pixels_counted = 0
    for i in lst:
        pixels_counted += i[1] - i[0]
    return pixels_counted


def get_image(location):
    '''
    :param location: where your image is saved
    :return:
        - an Image class
        - a list of lists where everything is either a 1 (white) or 0 (black)
        - a picture class
    '''
    picture = Image.open(location)
    rgb_im = picture.convert('RGB')
    w, y = picture.size
    rgb = [[1 if sum(rgb_im.getpixel((i, j))) < 255 * 1.5 else 0 for i in range(w)] for j in range(y)]
    return picture, rgb, rgb_im


def get_borders(rgb):
    borders = []
    for i in range(len(rgb)):
        border = []
        if 0 in rgb[i]:
            start = rgb[i].index(0)
            for j in range(start, len(rgb[i])):
                if start != -1 and rgb[i][j] == 1:
                    border.append((start, j - 1, i))
                    start = -1
                if start == -1:
                    if rgb[i][j] == 0:
                        start = j
            if start != -1:
                border.append((start, j - 1, i))

        borders.append(copy(border))
    return borders


def get_rectangles(borders):
    '''
    :param borders: a list of lists, for each row it lists where an area starts or ends
    :return: a list of areas

    This function reads from the top to the bottom. it tries to group the largest areas together. This will work
    as long as these areas are relatively simple, however, if they split up (like a donut for instance) this will
    definitely raise issues.
    '''
    rectangles = []
    started = []
    for i in range(len(borders)):
        started_new = []
        started_borders = [z[1] for z in sorted([(z[1] - z[0], z) for z in borders[i]], reverse=True)]
        for region in started_borders:
            existing = False
            left = region[0]
            right = region[1]
            started_areas = [z[1] for z in sorted([(area(z), z) for z in started], reverse=True)]

            # If in the previous row an area existsed in that overlaps with this region, this region is connected to it
            for k in started_areas:
                if right < k[-1][0] or left > k[-1][1]:
                    continue
                started_new.append(k + [region])
                existing = True
                del started[started.index(k)]
            # If there was no part in the previous row that already connects to it, it will be added to the list of
            # shapes as a new area of itself
            if not existing:
                started_new.append([region])
        for k in started:
            rectangles.append(copy(k))
        started = copy(started_new)

    # Add any remaining areas to the list
    for i in started_new:
        rectangles.append(i)
    return rectangles


def get_biggest_rectangle(rectangles):
    areas = []
    for i in rectangles:
        areas.append((area(i), i))

    probable_rectangle = sorted(areas)[-1][1]
    return probable_rectangle


def show_image(rgb, rgb_im, probable_rectangle):
    # I honestly cannot figure out how to change the picture variable, so I just make a new figure
    w, y = len(rgb[0]), len(rgb)
    img = Image.new('RGB', (w, y), "black")
    pixels = img.load()

    for i in range(w):
        for j in range(y):
            pixels[i, j] = rgb_im.getpixel((i, j))  # set the colour accordingly

    for i in probable_rectangle:
        pixels[i[0], i[-1]] = (255, 0, 0)
        pixels[i[1], i[-1]] = (255, 0, 0)
        for y in range(-10, 10):
            for z in range(-10, 10):
                pixels[i[0] + y, i[-1] + z] = (255, 0, 0)
                pixels[i[1] + y, i[-1] + z] = (255, 0, 0)

    img.show()


if __name__ == '__main__':
    picture, rgb, rgb_im = get_image('C:\\Users\\Nathan\\Downloads\\stack.jpg')
    borders = get_borders(rgb)
    rectangles = get_rectangles(borders)
    probable_rectangle = get_biggest_rectangle(rectangles)
    show_image(rgb, rgb_im, probable_rectangle)

Returning:

在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM