Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to split image of table at vertical lines into three images?

Tags:

opencv

I want to split an image of a table at the vertical lines into three images as shown below. Is it possible? The width of each column is variable. And the sad thing is that the left vertical line is drawn down from the header as you can see.

  • Input image (input.png)

enter image description here

  • Output image (output1.png)

enter image description here

  • Output image (output2.png)

enter image description here

  • Output image (output3.png)

enter image description here


Update 1

And the sad thing is that the left vertical line is drawn down from the header as you can see.

It means I guess the following image B is easier to split. But my case is A.

enter image description here


Update 2

I am trying to do the way @HansHirse gave me. My expectation is sub_image_1.png, sub_image_2.png and sub_image_3.png are stored in the out folder. But no luck so far. I'm looking into it.

https://github.com/zono/ocr/blob/16fd0ec9a2c7d2e26279ec53947fe7fbab9f526d/src/opencv.py

$ git clone https://github.com/zono/ocr.git
$ cd ocr
$ git checkout 16fd0ec9a2c7d2e26279ec53947fe7fbab9f526d
$ docker-compose up -d
$ docker exec -it ocr /bin/bash
$ python3 opencv.py
like image 585
zono Avatar asked Feb 11 '20 07:02

zono


Video Answer


2 Answers

Since your table is perfectly aligned, you can inverse binary threshold your image, and count (white) pixels along the y-axis to detect the vertical lines:

Count white pixels along y-axis

You'll need to clean the peaks, since you might get plateaus for the thicker lines.

That'd be my idea in Python OpenCV:

import cv2
import numpy as np
from skimage import io              # Only needed for web reading images

# Web read image via scikit-image; convert to OpenCV's BGR color ordering
img = cv2.cvtColor(io.imread('https://i.stack.imgur.com/BTqBs.png'), cv2.COLOR_RGB2BGR)

# Inverse binary threshold grayscale version of image
img_thr = cv2.threshold(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), 128, 255, cv2.THRESH_BINARY_INV)[1]

# Count pixels along the y-axis, find peaks
thr_y = 200
y_sum = np.count_nonzero(img_thr, axis=0)
peaks = np.where(y_sum > thr_y)[0]

# Clean peaks
thr_x = 50
temp = np.diff(peaks).squeeze()
idx = np.where(temp > thr_x)[0]
peaks = np.concatenate(([0], peaks[idx+1]), axis=0) + 1

# Save sub-images
for i in np.arange(peaks.shape[0] - 1):
    cv2.imwrite('sub_image_' + str(i) + '.png', img[:, peaks[i]:peaks[i+1]])

I get the following three images:

Sub image 1

Sub image 2

Sub image 3

As you can see, you might want to modify the selection by +/- 1 pixel, if an actual line is only 1 pixel wide.

Hope that helps!

----------------------------------------
System information
----------------------------------------
Platform:    Windows-10-10.0.16299-SP0
Python:      3.8.1
NumPy:       1.18.1
OpenCV:      4.2.0
----------------------------------------
like image 127
HansHirse Avatar answered Oct 05 '22 02:10

HansHirse


OpenCV has a line detection function:

You can filter the lines that are returned by passing min_theta and max_theta. For vertical lines you can specify maybe : 88 and 92 respectively for margin.

This is a edited sample taken from openCV documentation:

import sys
import math
import cv2 as cv
import numpy as np
def main(argv):

    default_file = 'img.png'
    filename = argv[0] if len(argv) > 0 else default_file
    # Loads an image
    src = cv.imread(cv.samples.findFile(filename), cv.IMREAD_GRAYSCALE)

    #some preparation of the photo
    dst = cv.Canny(src, 50, 200, None, 3)

    # Copy edges to the images that will display the results in BGR
    cdst = cv.cvtColor(dst, cv.COLOR_GRAY2BGR)
    cdstP = np.copy(cdst)

    lines = cv.HoughLines(dst, 1, np.pi / 180, 150, None, 88, 92) #min and max theta

You can get the x, y coordinate of the line and draw them by using the following code.

    if lines is not None:
        for i in range(0, len(lines)):
            rho = lines[i][0][0]
            theta = lines[i][0][2]
            a = math.cos(theta)
            b = math.sin(theta)
            x0 = a * rho
            y0 = b * rho
            pt1 = (int(x0 + 1000*(-b)), int(y0 + 1000*(a)))
            pt2 = (int(x0 - 1000*(-b)), int(y0 - 1000*(a)))
            cv.line(cdst, pt1, pt2, (0,0,255), 3, cv.LINE_AA)

Alternatively you can also use HoughLinesP as this allows you to specify a minimum length, which will help your filtering. Also the lines are returned as x,y pairs for each end making it easier to work with.

    linesP = cv.HoughLinesP(dst, 1, np.pi / 180, 50, None, 50, 10)

    if linesP is not None:
        for i in range(0, len(linesP)):
            l = linesP[i][0]
            cv.line(cdstP, (l[0], l[2]), (l[2], l[3]), (0,0,255), 3, cv.LINE_AA)

    cv.imshow("Source", src)
    cv.imshow("Detected Lines (in red) - Standard Hough Line Transform", cdst)
    cv.imshow("Detected Lines (in red) - Probabilistic Line Transform", cdstP)

    cv.waitKey()
    return 0

Documentation

To crop your image you can take the x coordinates of the lines you detected and use numpy slicing.

for i in range(0, len(linesP) - 1):
            l = linesP[i][0]
            xcoords = l[0], linesP[i+1][0][0]
            slice = img[:xcoords[0],xcoords[1]]
            cv.imshow('slice', slice)
            cv.waitKey(0)
like image 28
Josh Sharkey Avatar answered Oct 05 '22 02:10

Josh Sharkey