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:
- Get a sample ROI from the image
- 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
- Loop through the image and look for the intensity transitions from
0 to 255 - this point is where a vertical line is located
- Store each "transition location" in a
list
- 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: