I have black and white image, which has some object of interest in the middle of the image and noise in the upper an lower parts of the image
I want to crop the image to include only the middle word without the noise. I've implemented a very naive algorithm that traverses the image from the middle and going up/down and searching for the first line that is totally black.
for y in range(int(height/2), -1, -1):
# Get the pixel color
r, g, b = im.getpixel((int(width/2), y))
# Check if the pixel is black
if r == 0 and g == 0 and b == 0:
# If it is, set the top crop coordinate
top = y
break
# Iterate through each pixel in the center column of the image
for y in range(int(height/2), height):
# Get the pixel color
r, g, b = im.getpixel((int(width/2), y))
# Check if the pixel is black
if r == 0 and g == 0 and b == 0:
# If it is, set the bottom crop coordinate
bottom = y
break
# Crop the image
im = im.crop((left, top, right, bottom))
im.show()
It does the job, but I'm wondering if there is a more elegant way to do it with existing functions from opencv or skimage
will appreciate if someone could help me with this
CodePudding user response:
One simple way to do that in Python/OpenCV is to apply a morphology close to your image to close up the spacing between the numerals and then find the largest contour and crop to the bounding box of the contour.
Image:
import cv2
import numpy as np
# read the input as grayscale
img = cv2.imread('numerals_2023.png', 0)
# do morphology
kernel = cv2.getStructuringElement(cv2.MORPH_RECT , (51,3))
morph = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
# get largest contour and its bounding box
contours = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
big_contour = max(contours, key=cv2.contourArea)
x,y,w,h = cv2.boundingRect(big_contour)
# crop the image at the bounding box
crop = img[y:y h, x:x w]
# crop the image at the bounding box with padding
pad = 20
crop_padded = img[y-pad:y h pad, x-pad:x w pad]
# save the results
cv2.imwrite('numerals_2023_crop.png', crop)
cv2.imwrite('numerals_2023_crop_padded.png', crop_padded)
# show the results
cv2.imshow('morph', morph)
cv2.imshow('crop', crop)
cv2.imshow('crop_padded', crop_padded)
cv2.waitKey()
Crop:
Crop Padded:
CodePudding user response:
I did something like this several years ago, but I used ImageMagick.
The "line" algorithm I also tried, but it had the problem that found false positives if the image had two subjects separated by background - in that case, one of the two would be rejected as "noise".
The final idea was to convolve the image with a series of elliptical filters (a quick search tells me that OpenCV can do that), each removing some brightess, so that the boundary region became completely black, while the very center retained its full brightness.
This was run on a downsampled image to both accelerate things and reject high-frequency noise ("dust").
At that point, I found the largest ellipse that contained some above-threshold pixels, and the circumscribed rectangle was used for the crop.