9

I have a sample image like this

enter image description here

I'm looking for a way to black out the noise from the image such that I end up with an image that just has black text on white background so that I may send it to tesseract.

I've tried morphing with

kernel = np.ones((4,4),np.uint8)
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
cv2.imshow("opening", opening)

but it doesn't seem to work.

I've also tried to find contours

img = cv2.cvtColor(rotated, cv2.COLOR_BGR2GRAY)
(cnts, _) = cv2.findContours(img, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:1]
for c in cnts:
    x,y,w,h = cv2.boundingRect(c)
    roi=rotated[y:y+h,x:x+w].copy()
    cv2.imwrite("roi.png", roi)

With the above code, I get the following contours:

enter image description here

which leads to this image when cropped:

enter image description here

which is still not good enough. I want black text on white background, so that I can send it to tesseract OCR and have good success rate.

Is there anything else I can try?

Update

Here is an additional similar image. This one is a bit easier because it has a smooth rectangle in it

enter image description here

Anthony
  • 33,838
  • 42
  • 169
  • 278
  • 2
    to the voter who said question is too-broad. Please let me know how to narrow it down further. I don't think the question is broad.. – Anthony Oct 05 '15 at 05:18
  • You need to do some adaptive thresholding, and open and close operations. http://docs.opencv.org/doc/tutorials/imgproc/opening_closing_hats/opening_closing_hats.html You could also do custom erosion or dilation (which is basicall what open and close operations do). – bad_keypoints Oct 05 '15 at 05:44
  • Is this the input image ? Or result from some kind of thresholding ? – ZdaR Oct 05 '15 at 05:59
  • will it always be some rectangular background? – Micka Oct 05 '15 at 07:11
  • @ZdaR this isn't the original image. This is the image I get after pre-processing the original image to extract the part that I believe has text. I'm doing that by using thresholding, morphing, houghlinesp, deskewing, and resizing. – Anthony Oct 05 '15 at 11:38
  • @Micka Yes, The patten is that it will always be some rectangular background. I have updated the question with an additional example image – Anthony Oct 05 '15 at 11:39

3 Answers3

4

The following works for your given example, although it might need tweaking for a wider range of images.

import numpy as np
import cv2

image_src = cv2.imread("input.png")
gray = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
ret, gray = cv2.threshold(gray, 250,255,0)

image, contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
largest_area = sorted(contours, key=cv2.contourArea)[-1]
mask = np.zeros(image_src.shape, np.uint8)
cv2.drawContours(mask, [largest_area], 0, (255,255,255,255), -1)
dst = cv2.bitwise_and(image_src, mask)
mask = 255 - mask
roi = cv2.add(dst, mask)

roi_gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
ret, gray = cv2.threshold(roi_gray, 250,255,0)
image, contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

max_x = 0
max_y = 0
min_x = image_src.shape[1]
min_y = image_src.shape[0]

for c in contours:
    if 150 < cv2.contourArea(c) < 100000:
        x, y, w, h = cv2.boundingRect(c)
        min_x = min(x, min_x)
        min_y = min(y, min_y)
        max_x = max(x+w, max_x)
        max_y = max(y+h, max_y)

roi = roi[min_y:max_y, min_x:max_x]
cv2.imwrite("roi.png", roi)

Giving you the following type of output images:

enter image description here

And...

enter image description here

The code works by first locating the largest contour area. From this a mask is created which is used to first select only the area inside, i.e. the text. The inverse of the mask is then added to the image to convert the area outside the mask to white.

Lastly contours are found again for this new image. Any contour areas outside a suitable size range are discarded (this is used to ignore any small noise areas), and a bounding rect is found for each. With each of these rectangles, an outer bounding rect is calculated for all of the remaining contours, and a crop is made using these values to give the final image.

Update - To get the remainder of the image, i.e. with the above area removed, the following could be used:

image_src = cv2.imread("input.png")
gray = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
ret, gray = cv2.threshold(gray, 10, 255,0)
image, contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
largest_area = sorted(contours, key=cv2.contourArea)[-1]
mask = np.zeros(image_src.shape, np.uint8)
cv2.drawContours(mask, [largest_area], 0, (255,255,255,255), -1)
image_remainder = cv2.bitwise_and(image_src, 255 - mask)

cv2.imwrite("remainder.png", image_remainder)
Martin Evans
  • 45,791
  • 17
  • 81
  • 97
  • If possible, could you please explain your approach. Thanks for the answer! – Anthony Oct 05 '15 at 11:37
  • thanks. hmm I'm getting an error on line 8 `image, contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) ValueError: need more than 2 values to unpack` – Anthony Oct 05 '15 at 11:51
  • It was coded using the latest OpenCV 3 standard, in theory you can just remove the first parameter. Some of the constants will also probably need changing. – Martin Evans Oct 05 '15 at 11:52
  • Sometimes I have an edge case where the image might be like this http://s11.postimg.org/kkzld020j/image.png . There is a white rectangle part and then some other text. Any ideas/approaches to make this into two images? Upper and bottom. Upper can be processed by your answer and the botton I can send to OCR as-is – Anthony Oct 05 '15 at 12:47
  • I've updated the answer to give a possible starting solution. It uses a different threshold and masks out the original portion. It will probably need further processing though. – Martin Evans Oct 05 '15 at 13:16
  • This answer was what I needed for this question: https://stackoverflow.com/questions/41138000/fit-quadrilateral-tetragon-to-a-blob – Michael Currie Oct 13 '22 at 16:21
1

I get this: Result

Source Code:

if __name__ == '__main__':
  SrcImg = cv2.imread('./Yahi9.png', cv2.CV_LOAD_IMAGE_GRAYSCALE)
  _, BinImg = cv2.threshold(SrcImg, 80, 255, cv2.THRESH_OTSU)

  Contours, Hierarchy = cv2.findContours(image=copy.deepcopy(SrcImg),
                                         mode=cv2.cv.CV_RETR_EXTERNAL,
                                         method=cv2.cv.CV_CHAIN_APPROX_NONE)
  MaxContour, _ = getMaxContour(Contours)
  Canvas = np.ones(SrcImg.shape, np.uint8)
  cv2.drawContours(image=Canvas, contours=[MaxContour], contourIdx=0, color=(255), thickness=-1)
  mask = (Canvas != 255)
  RoiImg = copy.deepcopy(BinImg)
  RoiImg[mask] = 255
  RoiImg = cv2.morphologyEx(src=RoiImg, op=cv2.MORPH_CLOSE, kernel=np.ones((3,3)), iterations=4)
  cv2.imshow('RoiImg', RoiImg)
  cv2.waitKey(0)

Function:

def getMaxContour(contours):
  MaxArea = 0
  Location = 0
  for idx in range(0, len(contours)):
      Area = cv2.contourArea(contours[idx])
      if Area > MaxArea:
          MaxArea = Area
          Location = idx
  MaxContour = np.array(contours[Location])
  return MaxContour, MaxArea

Ehh, it's python code. It only works when the white region is the max contour.

Hao Li
  • 1,593
  • 15
  • 27
0

Basic idea of this answer is to use border around text.

1) Erode horizontally with a very large kernel, say size of 100 px or 8 times size of single expected character, something like that. It should be done row-wise. The extreme ordinate will give y-location of boundaries around text.

2) Process vertically same way to get x-location of boundaries around text. Then use these locations to crop out image you want.

-- One benefit of this method is you will get every sentence/word segmented separately which, I presume, is good for an OCR.

Happy Coding :)

Edited in by Mark Setchell

Here is a demo of 1)

enter image description here

Here is a demo of 2)

enter image description here

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
Pervez Alam
  • 1,246
  • 10
  • 20
  • 1
    I hope you don't mind my addition of an animation - feel free to delete it if you do. – Mark Setchell Oct 05 '15 at 10:45
  • I will try this approach as it might be more applicable to other similar images. I'll update my progress here. I hope opencv has a way to do horizontal and vertical eroding – Anthony Oct 05 '15 at 11:37
  • Yes it has, you need to make desired kernel and need to apply it properly. As bro Mark shown in animation, you can use even bigger kernel than 200.. experiment it and you will find out :) – Pervez Alam Oct 06 '15 at 06:36