4

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 ?

example

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..

src

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)
Jeru Luke
  • 20,118
  • 13
  • 80
  • 87
lucians
  • 2,239
  • 5
  • 36
  • 64
  • drawcontours last -1 is thickness, when negative it fills the inside, when positive it just draws the line. At least this happens in the last drawcontours... not in the first one – api55 Jan 15 '18 at 09:06
  • I used that, but with 1 it show me only the external contour. So I will have something [like this](https://imgur.com/a/UJ0y8), which is not the main goal.. – lucians Jan 15 '18 at 09:09
  • oh I see, true, maybe you can draw twice, 1 like what you have and the second time the inner ones (look at the hierarchy) and draw them filled in white – api55 Jan 15 '18 at 09:15
  • Used different hierarchy but nothing seems to change at output level. – lucians Jan 15 '18 at 09:52
  • @Link To be honest, I tried use hierarchy, but failed. I just don't know how to use it... And an alternative is crop the region, then find small contours again, then draw the smaller in inversed color.. – Kinght 金 Jan 15 '18 at 09:53
  • @Silencer I'll try now the alternative you think...If I am thinking right, it's just matter of minutes.. – lucians Jan 15 '18 at 09:55
  • I am trying to use cv2.contourArea(cnt) to find the area of each contour and then exclude the ones below a certain value. By doing this, if he finds a circle in number 6 it will make all the image white, not only that specified contour. – lucians Jan 15 '18 at 10:11
  • I know where I was wrong. https://i.stack.imgur.com/tSh4C.png – Kinght 金 Jan 15 '18 at 10:13
  • @Silencer please could you share the solution / explain what you did ? For me, what I am trying actually doesn't work.. – lucians Jan 15 '18 at 11:15

5 Answers5

6

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.

enter image description here

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

enter image description here


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:

enter image description here


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:

enter image description here

In OpenCV 4.0:

enter image description here


Kinght 金
  • 17,681
  • 4
  • 60
  • 74
  • Still trying to solve this puzzle, I am begin to think I am missing something at this point.. LOL – lucians Jan 16 '18 at 23:06
  • BIG improvement: I achieved to save all the numbers which have contours inside (0,6,8,9) right. Still didn't find a way to save the "normal" chars... – lucians Jan 17 '18 at 00:07
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:

enter image description here

Fiver
  • 9,909
  • 9
  • 43
  • 63
  • Thanks. I'll try as soon as possible. – lucians Oct 17 '18 at 13:53
  • What does `~img_v` mean ? – lucians Oct 23 '18 at 12:23
  • The `~` is the complement operator, it flips the bits, and in doing so inverts the grayscale image...255 become 0, 254 becomes 1, etc. It comes in quite handy at times. It isn't strictly necessary as you could just threshold from 0 to 127. – Fiver Oct 23 '18 at 12:58
1

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.

lucians
  • 2,239
  • 5
  • 36
  • 64
  • In fact, here there is a problem. The second part is cropping instead of contouring. Which is wrong, for now.. – lucians Jan 17 '18 at 00:46
  • @Silencer What I am doing wrong ? By putting the code which copy the contours into canvas instead of the cropping one, it still get me the inner contour blacked... – lucians Jan 17 '18 at 10:27
1

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)
lucians
  • 2,239
  • 5
  • 36
  • 64
1

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.

Knight Forked
  • 1,529
  • 12
  • 14