Zaibi
Zaibi

Reputation: 343

Detect middle vertical line of a double side scanned image

I am trying to detect the long middle separator vertical line in my image so that I can crop the image based on that line into two separate images, trying to use HoughLinesP but how do I make sure it only picks the middle vertical line, I can clearly see that that line is pretty distinguishable.

Here is the file in question

I applied opencv HoughLinesP to it and got the following result

enter image description here

Using the following code:

img = cv2.imread('./image.jpeg', 1)
img_gray = gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)


cannied = cv2.Canny(img_gray, threshold1=50, threshold2=200, apertureSize=3)
lines = cv2.HoughLinesP(cannied, rho=1, theta=np.pi / 180, threshold=80, minLineLength=30, maxLineGap=10)

for line in get_lines(lines):
    leftx, boty, rightx, topy = line
    cv2.line(img, (leftx, boty), (rightx,topy), (255, 255, 0), 2)

cv2.imwrite('./lines.png', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

More resources I applied

PyImageSearch

and several others

Upvotes: 2

Views: 1125

Answers (2)

stateMachine
stateMachine

Reputation: 5815

Here's a possible solution. It involves selecting a sample from a couple of rows on the input image, then reducing the sample to a row image using the cv2.reduce function. Loop through the reduced row and look for the pixel intensity jumps from 0 to 255, since that transitions denotes the areas of the page you are using to guide your crop. Finally, store the locations of the jumps in a list to further crop the pages from the image. These are the steps:

  1. Get a sample ROI from the image
  2. Reduce the sample ROI to a row image of size 1 x width, using the cv2.reduce function in MAX mode to get the maximum intensity values of each column
  3. Loop through the image and look for the intensity transitions from 0 to 255 - this point is where a vertical line is located
  4. Store each "transition location" in a list
  5. Crop the image using the info in "transition locations" list

Let's see the code:

# Imports:
import numpy as np
import cv2

# Set the image path
path = "D://opencvImages//"
fileName = "mx8lW.jpg"

# Read the image in default mode:
inputImage = cv2.imread(path + fileName)

# Convert RGB to grayscale:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)

# Set the sample ROI and crop it:
(imageHeight, imageWidth) = grayscaleImage.shape[:2]
roiX = 0
roiY = int(0.05 * imageHeight)
roiWidth = imageWidth
roiHeight = int(0.05 * imageHeight)

# Crop the image:
imageRoi = grayscaleImage[roiY:roiY+roiHeight, roiX:roiWidth]

The first bit reads the image, converts it from BGR to grayscale and defines the sample ROI. This ROI is used to locate the vertical lines. Since the vertical lines are constant through the image, selecting and cropping just a sample will do just fine instead of processing the whole image. The ROI I defined selects an area just below the image top, because there are some dark portions that could affect the reduction of the ROI to a row.

This image shows the selected ROI, drawn with a green rectangle:

Note that the two (leftmost and rightmost) vertical lines are shown clearly inside the ROI. If we crop that, we will get this sample image:

All the info we need to crop the pages is right there. Let's threshold the image to get a binary mask:

# Thresholding:
_, binaryImage = cv2.threshold(imageRoi, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

This is the binary mask:

Note the jumps from 0 to 255 and what they imply. Each transition denotes the starting and ending points that we can use to crop the two pages. Ok, let's reduce the binary mask to just one row. We will use the MAX mode, where each pixel value in the row is defined as the maximum intensity value corresponding to that image column:

# Reduce the ROI to a 1 x imageWidth row:
reducedImg = cv2.reduce(binaryImage, 0, cv2.REDUCE_MAX)

This is the image produced, which is just a row:

Let's loop through this image (actually just a plain numpy array) and look for those transitions. Additionally, let's prepare a list and store the jump the locations as we find them:

# Store the transition positions here:
linePositions = []

# Find transitions from 0 to 255:
pastPixel = 255
for x in range(reducedImg.shape[1]):
    # Get current pixel:
    currentPixel = reducedImg[0,x]
    # Check for the "jumps":
    if currentPixel == 255 and pastPixel == 0:
        # Store the jump locations in list:
        print("Got Jump at:"+str(x))
        linePositions.append(x)
    # Set current pixel to past pixel:
    pastPixel = currentPixel

Cool, we have the jump locations ready in linePositions. Let's finally crop the image. I've achieved this looping through the locations list and setting the parameters for cropping:

# Crop pages:
for i in range(len(linePositions)):
    # Get top left:
    cropX = linePositions[i]
    
    # Get top left:
    if i != len(linePositions)-1:
        # Get point from the list:
        cropWidth = linePositions[i+1]
    else:
        # Set point from the image's original width:
        cropWidth = reducedImg.shape[1]

    # Crop page:
    cropY = 0
    cropHeight = imageHeight
    currentCrop = inputImage[cropY:cropHeight,cropX:cropWidth]

    # Show current crop:
    cv2.imshow("CurrentCrop", currentCrop)
    cv2.waitKey(0)

Which produces two crops:

Upvotes: 1

Guang
Guang

Reputation: 395

Based on this input image, you want to find the longest vertical line that sits at the middle of the image. Therefore you have several options to try:

  1. Crop the image on the left and right, to keep only the middle 1/3 or 1/4, which is roughly where the actual middle line is.
  2. Adjust the parameter minLineLength in your cv2.HoughLinesP, to eliminate shorter lines.
  3. If there are still more than one lines left, the longest one is probably what you want. To double check, calculate the lines' slops, the most vertical + longest one should be the final answer.

Upvotes: 0

Related Questions