cv2.drawContours() - unfill circles inside characters (Python, OpenCV)

半世苍凉 提交于 2019-12-06 10:47:35

问题


As suggested by @Silencer, I used the code he posted here to draw contours around the numbers in my image. At some point, working with numbers like 0,6,8,9 I saw that their inside contours (the circles) are being filled as well. How can I prevent this ? Is there a min/max area of action to set for cv2.drawContours() so I can exclude the inner area ?

I tried to pass cv2.RETR_EXTERNAL but with this parameter only the whole external area is considered.

The code is this (again, thanks Silencer. Was searching for this for months..):

import numpy as np
import cv2

im = cv2.imread('imgs\\2.png')
imgray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 127, 255, 0)
image, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

#contours.sort(key=lambda x: int(x.split('.')[0]))

for i, cnts in enumerate(contours):
    ## this contour is a 3D numpy array
    cnt = contours[i]
    res = cv2.drawContours(im, [cnt], 0, (255, 0, 0), 1)
    cv2.imwrite("contours.png", res)
    '''
    ## Method 1: crop the region
    x,y,w,h = cv2.boundingRect(cnt)
    croped = res[y:y+h, x:x+w]
    cv2.imwrite("cnts\\croped{}.png".format(i), croped)
    '''
    ## Method 2: draw on blank
    # get the 0-indexed coords
    offset = cnt.min(axis=0)
    cnt = cnt - cnt.min(axis=0)
    max_xy = cnt.max(axis=0) + 1
    w, h = max_xy[0][0], max_xy[0][1]
    # draw on blank
    canvas = np.ones((h, w, 3), np.uint8) * 255
    cv2.drawContours(canvas, [cnt], -1, (0, 0, 0), -1)

    #if h > 15 and w < 60:
    cv2.imwrite("cnts\\canvas{}.png".format(i), canvas)

The main image on which I am working..

Thanks

UPDATE

I implemented Fiver answer below and this is the result:

import cv2
import numpy as np

img = cv2.imread('img.png')
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
img_v = img_hsv[:, :, 2]

ret, thresh = cv2.threshold(~img_v, 127, 255, 0)
image, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

for i, c in enumerate(contours):
    tmp_img = np.zeros(img_v.shape, dtype=np.uint8)
    res = cv2.drawContours(tmp_img, [c], -1, 255, cv2.FILLED)

    tmp_img = np.bitwise_and(tmp_img, ~img_v)

    ret, inverted = cv2.threshold(tmp_img, 127, 255, cv2.THRESH_BINARY_INV)

    cnt = contours[i]

    x, y, w, h = cv2.boundingRect(cnt)
    cropped = inverted[y:y + h, x:x + w]

    cv2.imwrite("roi{}.png".format(i), cropped)

回答1:


To draw the char without filled the closed inner regions:

  1. find the contours on the threshed binary image with hierarchy.

  2. find the outer contours that don't have inner objects (by flag hierarchyi).

  3. for each outer contour:

    3.1 fill it(maybe need check whether needed);

    3.2 then iterate in it's inner children contours, fill then with other color(such as inversed color).

  4. combine with the crop code, crop them.

  5. maybe you need sort them, resplit them, normalize them.
  6. maybe, now you can do ocr with the trained model.

FindContours, refill the inner closed regions.

Combine with this answer Copy shape to blank canvas (OpenCV, Python), do more steps, maybe you can get this or better:


The core code to refill the inner closed regions is as follow:

#!/usr/bin/python3
# 2018.01.14 09:48:15 CST
# 2018.01.15 17:56:32 CST
# 2018.01.15 20:52:42 CST

import numpy as np
import cv2

img = cv2.imread('img02.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

## Threshold 
ret, threshed = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)

## FindContours
cnts, hiers = cv2.findContours(threshed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[-2:]

canvas = np.zeros_like(img)
n = len(cnts)
hiers = hiers[0]

for i in range(n):
    if hiers[i][3] != -1:
        ## If is inside, the continue 
        continue
    ## draw 
    cv2.drawContours(canvas, cnts, i,  (0,255,0), -1, cv2.LINE_AA)

    ## Find all inner contours and draw 
    ch = hiers[i][2]
    while ch!=-1:
        print(" {:02} {}".format(ch, hiers[ch]))
        cv2.drawContours(canvas, cnts, ch, (255,0,255), -1, cv2.LINE_AA)
        ch = hiers[ch][0]

cv2.imwrite("001_res.png", canvas)

Run this code with this image:

You will get:


Of course, this is for two hierarchies. I haven't test for more than two. You who need can do test by yourself.


Update:

Notice in different OpenCVs, the cv2.findContours return different values. To keep code executable, we can just get the last two returned values use: cnts, hiers = cv2.findContours(...)[-2:]

In OpenCV 3.4:

In OpenCV 4.0:





回答2:


Since you already have a mask from your threshold step, you can also use it to bitwise_and against the drawn contour:

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('drawn_chars.png')
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
img_v = img_hsv[:, :, 2]

ret, thresh = cv2.threshold(~img_v, 127, 255, 0)
image, contours, hierarchy = cv2.findContours(
    thresh, 
    cv2.RETR_EXTERNAL, 
    cv2.CHAIN_APPROX_SIMPLE
)

for c in contours:
    tmp_img = np.zeros(img_v.shape, dtype=np.uint8)
    cv2.drawContours(tmp_img, [c], -1, 255, cv2.FILLED)

    tmp_img = np.bitwise_and(tmp_img, ~img_v)

    plt.figure(figsize=(16, 2))
    plt.imshow(tmp_img, cmap='gray')

I've inverted the image so the contours are white and I left out the cropping as you already solved that. Here is the result on one of the "O" characters:




回答3:


Full code...

This will not sort the images.

import numpy as np
import cv2

im = cv2.imread('imgs\\1.png')
imgray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)

## Threshold
ret, threshed = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)

## FindContours
image, cnts, hiers = cv2.findContours(threshed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

canvas = np.zeros_like(im)
n = len(cnts)
hiers = hiers[0]

for i, imgs in enumerate(cnts):

    cnt = cnts[i]
    res = cv2.drawContours(im, [cnt], 0, (0, 0, 0), -1)

    x, y, w, h = cv2.boundingRect(cnt)
    croped = res[y:y + h, x:x + w]

    if h > 10:
        cv2.imwrite("out\\croped{}.png".format(i), croped)
        cv2.imshow('i', croped)
        cv2.waitKey(0)

for i, value in enumerate(cnts):

    ## this contour is a 3D numpy array
    cnt = cnts[i]
    res = cv2.drawContours(im, [cnt], 0, (0, 0, 0), -1)
    # cv2.imwrite("out\\contours{}.png".format(i), res)

    ## Find all inner contours and draw
    ch = hiers[i][2]
    while ch != -1:
        print(" {:02} {}".format(ch, hiers[ch]))
        res1 = cv2.drawContours(im, cnts, ch, (255, 255, 255), -1)
        ch = hiers[ch][0]

        x, y, w, h = cv2.boundingRect(cnt)
        croped = res[y:y + h, x:x + w]

        if h > 10:
            cv2.imwrite("out\\croped{}.png".format(i), croped)

Any correction is accepted.




回答4:


This will do definetively the job...

import cv2
import os
import numpy as np

img = cv2.imread("image.png")

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

retval, thresholded = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)

medianFiltered = cv2.medianBlur(thresholded, 3)

_, contours, hierarchy = cv2.findContours(medianFiltered, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

contour_list = []
for contour in contours:
    area = cv2.contourArea(contour)
    if area > 80:
        contour_list.append(contour)

numbers = cv2.drawContours(img, contour_list, -1, (0, 0, 0), 2)

cv2.imshow('i', numbers)
cv2.waitKey(0)

sorted_ctrs = sorted(contours, key=lambda ctr: cv2.boundingRect(ctr)[0])

for i, cnts in enumerate(contours):

    cnt = contours[i]

    x, y, w, h = cv2.boundingRect(cnt)
    croped = numbers[y:y + h, x:x + w]

    h, w = croped.shape[:2]
    print(h, w)

    if h > 15:
        cv2.imwrite("croped{}.png".format(i), croped)



回答5:


This is conceptually similar to Fivers answer, just that bitwise_and occurs outside the for loop and perhaps is better in terms of performance. Source code is in C++ for those looking for C++ answer for this problem.

int thWin = 3;
int thOffset = 1;
cv::adaptiveThreshold(image, th, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY_INV, thWin, thOffset);

int minMoveCharCtrArea = 140;
std::vector<std::vector<cv::Point> > contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(th.clone(), contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);
cv::Mat filtImg = cv::Mat::zeros(img.rows, img.cols, CV_8UC1 );

for (int i = 0; i< contours.size(); ++i) {
    int ctrArea = cv::contourArea(contours[i]);
    if (ctrArea > minMoveCharCtrArea) {
        cv::drawContours(filtImg, contours, i, 255, -1);
    }
}
cv::bitwise_and(th, filtImg, filtImg);

Remember to clone the image (for python it should be copy) when passing source image argument to findContours, since findContours modifies the original image. I reckon later versions of opencv (perhaps opencv3 +) don't require cloning.



来源:https://stackoverflow.com/questions/48259724/cv2-drawcontours-unfill-circles-inside-characters-python-opencv

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!