Related
I have this input image (feel free to download it and try your solution, please):
I need to find points A and B that are closest to the left down and right upper corner. And than I would like to cut of the image. See desired output:
So far I have this function, but it does not find points A, B correctly:
def CheckForLess(list1, val):
return(all(x < val for x in list1))
def find_corner_pixels(img):
# Get image dimensions
height, width = img.shape[:2]
# Find the first non-black pixel closest to the left-down and right-up corners
nonempty = []
for i in range(height):
for j in range(width):
# Check if the current pixel is non-black
if not CheckForLess(img[i, j], 10):
nonempty.append([i, 1080 - j])
return min(nonempty) , max(nonempty)
Can you help me please?
EDIT:
Solution by Achille works on one picture, but if I change input image to this:
It gives wrong output:
I noticed that your image has an alpha mask that already segment the foreground. This imply using the flag cv.IMREAD_UNCHANGED when reading the image with openCV (cv.imread(filename, cv.IMREAD_UNCHANGED)).
If this is the case you can have a try to the following:
import sys
from typing import Tuple
import cv2 as cv
import numpy as np
class DetectROI:
def __init__(self,
alpha_threshold: int = 125,
display: bool = False,
gaussian_sigma: float = 1.,
gaussian_window: Tuple[int, int] = (3, 3),
relative_corner: float = 0.25,
relative_line_length: float = 0.25,
relative_max_line_gap: float = 0.02,
working_size: Tuple[int, int] = (256, 256)):
self.alpha_threshold = alpha_threshold
self.display = display
self.working_size = working_size
self.gaussian_sigma = gaussian_sigma
self.gaussian_window = gaussian_window
self.relative_line_length = relative_line_length
self.relative_max_line_gap = relative_max_line_gap
self.relative_corner = relative_corner
self._origin: Tuple[int, int] = (0, 0)
self._src_shape: Tuple[int, int] = (0, 0)
def __call__(self, src):
# get cropped contour
cnt_img = self.get_cropped_contour(src)
left_lines, right_lines = self.detect_lines(cnt_img)
x, y, w, h = self.get_bounding_rectangle(left_lines + right_lines)
# top_left = (x, y)
top_right = (x + w, y)
# bottom_right = (x + w, y + h)
bottom_left = (x, y + h)
if self.display:
src = cv.rectangle(src, bottom_left, top_right, (0, 0, 255, 255), 3)
cv.namedWindow("Source", cv.WINDOW_KEEPRATIO)
cv.imshow("Source", src)
cv.waitKey()
return bottom_left, top_right
def get_cropped_contour(self, src):
self._src_shape = tuple(src.shape[:2])
msk = np.uint8((src[:, :, 3] > self.alpha_threshold) * 255)
msk = cv.resize(msk, self.working_size)
cnt, _ = cv.findContours(msk, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
cnt_img = cv.drawContours(np.zeros_like(msk), cnt, 0, (255,))
cnt = cnt[0]
x, y, w, h = cv.boundingRect(np.array(cnt))
top_left = (x, y)
# top_right = (x + w, y)
bottom_right = (x + w, y + h)
# bottom_left = (x, y + h)
self._origin = top_left
cnt_img = cnt_img[self._origin[1]:bottom_right[1], self._origin[0]:bottom_right[0]]
if self.display:
cv.namedWindow("Contours", cv.WINDOW_KEEPRATIO)
cv.imshow("Contours", cnt_img)
return cnt_img
def detect_lines(self, img):
img = cv.GaussianBlur(img, self.gaussian_window, self.gaussian_sigma)
lines = cv.HoughLinesP(img, 1, np.pi / 180, 50, 50,
int(self.relative_line_length*img.shape[0]),
int(self.relative_max_line_gap*img.shape[0]))
if self.display:
lines_img = np.repeat(img[:, :, None], 3, axis=2)
if lines is not None:
for i in range(0, len(lines)):
l = lines[i][0]
cv.line(lines_img, (l[0], l[1]), (l[2], l[3]), (255, 0, 0), 2, cv.LINE_AA)
# keep lines close to bottom left and bottom right images
corner = self.relative_corner
left_lines = []
right_lines = []
if lines is not None:
# left side
for i in range(0, len(lines)):
l = lines[i][0]
if (l[1] > (1 - corner) * img.shape[1] and l[0] < corner * img.shape[0]) \
or (l[3] > (1 - corner) * img.shape[1] and l[2] < corner * img.shape[0]):
left_lines.append(l)
elif (l[1] > (1 - corner) * img.shape[1] and l[0] > (1 - corner) * img.shape[0]) \
or (l[3] > (1 - corner) * img.shape[1] and l[2] > (1 - corner) * img.shape[0]):
right_lines.append(l)
if self.display:
if lines is not None:
for l in left_lines + right_lines:
cv.line(lines_img, (l[0], l[1]), (l[2], l[3]), (0, 0, 255), 2, cv.LINE_AA)
cv.namedWindow("Contours", cv.WINDOW_KEEPRATIO)
cv.imshow("Contours", lines_img)
return left_lines, right_lines
def get_bounding_rectangle(self, lines):
cnt = sum(([(l[0], l[1]), (l[2], l[3])] for l in lines), [])
x, y, w, h = cv.boundingRect(np.array(cnt))
x += self._origin[0]
y += self._origin[1]
y = np.int32(np.round(y * self._src_shape[0] / self.working_size[0]))
h = np.int32(np.round(h * self._src_shape[0] / self.working_size[0]))
x = np.int32(np.round(x * self._src_shape[1] / self.working_size[1]))
w = np.int32(np.round(w * self._src_shape[1] / self.working_size[1]))
return x, y, w, h
def main(argv):
default_file = r'book.png'
filename = argv[0] if len(argv) > 0 else default_file
src = cv.imread(filename, cv.IMREAD_UNCHANGED)
detector = DetectROI(display=True)
return detector(src)
if __name__ == "__main__":
print("bottom_left: {}, top_right: {}".format(*main(sys.argv[1:])))
The underlying idea is the following:
threshold the alpha mask to get the foreground
compute the contour of the alpha mask
detect the lines (assuming the right and left border to be rather strait)
keep the lines that start from the bottom left and the bottom right of the image (drawn in red)
Here is the obtained result
I hope this is robust enough
I'm a bit rusted, haven't practiced opencv2 for a long time but this is what I came up with:
import numpy as np
import cv2
img = cv2.imread("book.png")
timg = img.copy()
cv2.imshow("img", img)
# Get a mask to get only the colour you need (cover of the book)
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower = np.array([10, 150, 150])
upper = np.array([35, 255, 255])
mask = cv2.inRange(hsv_img, lower, upper)
masked = cv2.bitwise_and(hsv_img, hsv_img, mask=mask)
img[mask == 0] = 255
cv2.imshow("mask", img)
# Find contours of the masked image
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# For some reason, first contour was the entire screen so only take the second rectangle
contours = sorted(contours, key=cv2.contourArea, reverse=True)[1:2]
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
# get the corners of the rectangle
top_left = (x, y)
top_right = (x + w, y)
bottom_right = (x + w, y + h)
bottom_left = (x, y + h)
height, width = img.shape[:2]
pt1 = (0, top_left[1])
pt2 = (width, top_left[1])
pt3 = (0, bottom_left[1])
pt4 = (width, bottom_left[1])
cv2.line(timg, pt1, pt2, [10, 150, 150],1 )
cv2.line(timg, pt3, pt4, [10, 150, 150], 1)
cv2.imshow("Bounding Rectangles", timg)
cv2.waitKey(0)
hope this helps (Note that you could retrieve only the book by getting the content of the contours
Then, cropping is really easy
# Select the area to crop
cropped = img[y1:y2, x1:x2]
I am new to OpenCV and I am not even sure how to tackle this problem. I have this image of 500x500 pixel with red dots and white lines in it.
Considering each red dot as center and could I draw a fixed bounding box of 25X25 size around the red dot? I need to identify every red dot in the image.
Note: condition is that I need to find a bounding box of fixed size (25x25) and the red dot must be in the center of the bounding box.
Any help would be appreciated. Thank you in advance.
Another solution, using numpy slicing to get the red channel, where to create a mask of the red dots and cv2.findContours to get the bounding rectangles of the dots. We can use this info to draw the new 25 x 25 rectangles:
# Imports
import cv2
import numpy as np
# Read image
imagePath = "C://opencvImages//"
inputImage = cv2.imread(imagePath + "oHk9s.png")
# Deep copy for results:
inputImageCopy = inputImage.copy()
# Slice the Red channel from the image:
r = inputImage[:, :, 2]
# Convert type to unsigned integer (8 bit):
r = np.where(r == 237, 255, 0).astype("uint8")
# Extract blobs (the red dots are all the white pixels in this mask):
contours, _ = cv2.findContours(r, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Store bounding rectangles here:
boundingRectangles = []
# Loop through the blobs and draw a 25 x 25 green rectangle around them:
for c in contours:
# Get dot bounding box:
x, y, w, h = cv2.boundingRect(c)
# Set new bounding box dimensions:
boxWidth = 25
boxHeight = 25
# Center rectangle around blob:
boxX = int(x + 0.5 * (w - boxWidth))
boxY = int(y + 0.5 * (h - boxHeight))
# Store data:
boundingRectangles.append((boxX, boxY, boxWidth, boxHeight))
# Draw and show new bounding rectangles
color = (0, 255, 0)
cv2.rectangle(inputImageCopy, (boxX, boxY), (boxX + boxWidth, boxY + boxHeight), color, 2)
cv2.imshow("Boxes", inputImageCopy)
cv2.waitKey(0)
Additionally, I've stored the top left coordinate, width and height of the rectangles in the boundingRectangles list. This is the output:
Here is how you can use an HSV mask to mask out everything in your image except for the red pixels:
import cv2
import numpy as np
def draw_box(img, cnt):
x, y, w, h = cv2.boundingRect(cnt)
half_w = w // 2
half_h = h // 2
x1 = x + half_h - 12
x2 = x + half_h + 13
y1 = y + half_w - 12
y2 = y + half_w + 13
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0))
img = cv2.imread("red_dots.png")
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
ranges = np.array([[100, 0, 0], [179, 255, 255]])
mask = cv2.inRange(img_hsv, *ranges)
img_masked = cv2.bitwise_and(img, img, mask=mask)
img_gray = cv2.cvtColor(img_masked, cv2.COLOR_BGR2GRAY)
contours, _ = cv2.findContours(img_gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
for cnt in contours:
draw_box(img, cnt)
cv2.imshow("Image", img)
cv2.waitKey(0)
Output:
Notice at this part of the draw_box() function:
x1 = x + half_h - 12
x2 = x + half_h + 13
y1 = y + half_w - 12
y2 = y + half_w + 13
Ideally, instead of - 12 and + 13, it should be - 12.5 and + 12.5, but there cannot be half pixels in OpenCV, or an error would be thrown.
![enter image description here][1]
Able to identify and get blue colour but culdnt identify the red colour range unable to fix colour range for red/purple stripes.I have used contour and created range for red,green,blue colour .That stripes colour range is not correct I tried settig maximum range for red/purple
import numpy as np
import cv2
img = cv2.imread(r'/home/pavithra/Downloads/pic.jpeg')
hsvFrame = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# Set range for red color and
red_lower = np.array([136, 86, 86], np.uint8)
red_upper = np.array([239, 12, 50], np.uint8)
red_mask = cv2.inRange(hsvFrame, red_lower, red_upper)
green_lower = np.array([25, 52, 72], np.uint8)
green_upper = np.array([102, 255, 255], np.uint8)
green_mask = cv2.inRange(hsvFrame, green_lower, green_upper)
blue_lower = np.array([94, 80, 2], np.uint8)
blue_upper = np.array([120, 255, 255], np.uint8)
blue_mask = cv2.inRange(hsvFrame, blue_lower, blue_upper)
kernal = np.ones((5, 5), "uint8")
red_mask = cv2.dilate(red_mask, kernal)
res_red = cv2.bitwise_and(img, img,
mask = red_mask)
# For green color
green_mask = cv2.dilate(green_mask, kernal)
res_green = cv2.bitwise_and(img, img,
mask = green_mask)
# For blue color
blue_mask = cv2.dilate(blue_mask, kernal)
res_blue = cv2.bitwise_and(img, img,
mask = blue_mask)
# Creating contour to track red color
contours, hierarchy = cv2.findContours(red_mask,
cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)
for pic, contour in enumerate(contours):
area = cv2.contourArea(contour)
if(area > 300):
x, y, w, h = cv2.boundingRect(contour)
img = cv2.rectangle(img, (x, y),
(x + w, y + h),
(0, 255, 0), 2)
cv2.putText(img, "Red", (x, y),
cv2.FONT_HERSHEY_SIMPLEX, 1.0,
(0, 255, 0),2)
contours, hierarchy = cv2.findContours(green_mask,
cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)
for pic, contour in enumerate(contours):
area = cv2.contourArea(contour)
if(area > 300):
x, y, w, h = cv2.boundingRect(contour)
img = cv2.rectangle(img, (x, y),
(x + w, y + h),
(0, 255, 0), 2)
cv2.putText(img, "Green", (x, y),
cv2.FONT_HERSHEY_SIMPLEX,
1.0, (0, 255, 0),2)
contours, hierarchy = cv2.findContours(blue_mask,
cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)
for pic, contour in enumerate(contours):
area = cv2.contourArea(contour)
if(area > 300):
x, y, w, h = cv2.boundingRect(contour)
img = cv2.rectangle(img, (x, y),
(x + w, y + h),
(255, 0, 0), 2)
cv2.putText(img, "Blue", (x, y),
cv2.FONT_HERSHEY_SIMPLEX,
1.0, (255, 0, 0),2)
resize = cv2.resize(img, (800, 480))
cv2.imshow("All clg", resize)
cv2.waitKey(0)
I split the image into half just to avoid unnecessary noise reductions. Then rotated the image to make the text horizontal. Binarized the image and obtained the three big blue-colored covid19 labels. Sorted them according to their y-coordinate (top to bottom). Performed some operations to obtain the region of interest (ROI, the two stripes which I named labels). Binarized each label to obtain the two contours of the colored stripes. Adjusted the gamma of the labels so that the colors become a little darker. Obtained the red-component of each stripe. Sorted them according to their x-coordinate (left to right). Assigned the stripe with the highest red value the red color, the other as purple.
Code:
def adjust_gamma(image, gamma=1.0):
# build a lookup table mapping the pixel values [0, 255] to
# their adjusted gamma values
invGamma = 1.0 / gamma
table = np.array([((i / 255.0) ** invGamma) * 255
for i in np.arange(0, 256)]).astype("uint8")
# apply gamma correction using the lookup table
return cv2.LUT(image, table)
def rotate(image: np.ndarray,angle, background_color):
old_width, old_height = image.shape[:2]
angle_radian = math.radians(angle)
width = abs(np.sin(angle_radian) * old_height) + abs(np.cos(angle_radian) * old_width)
height = abs(np.sin(angle_radian) * old_width) + abs(np.cos(angle_radian) * old_height)
image_center = tuple(np.array(image.shape[1::-1]) / 2)
rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0)
rot_mat[1, 2] += (width - old_width) / 2
rot_mat[0, 2] += (height - old_height) / 2
return cv2.warpAffine(image, rot_mat, (int(round(height)), int(round(width))), borderValue=background_color)
img = cv2.imread("covid.jpg")
rows,cols = img.shape[:2]
img = img[:,:cols//2]
rot = rotate(img,90,(255,255,255))
gray = cv2.cvtColor(rot,cv2.COLOR_BGR2GRAY)
otsu = cv2.threshold(gray,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]
opening = cv2.morphologyEx(otsu,cv2.MORPH_OPEN,np.ones((3,3),np.uint8),iterations=3)
dilate = cv2.dilate(opening,np.ones((1,5),np.uint8),iterations=5)
dilate2 = cv2.dilate(dilate,np.ones((3,3),np.uint8),iterations=3)
contours,_ = cv2.findContours(dilate2,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
lables = [] # the three covid19
y_coords = [] # to sort the lables from top to bottom (the one with single color comes 1st)
for cnt in contours:
x,y,w,h = cv2.boundingRect(cnt)
x = x + 10
w = w - 20
y_coords.append(y)
lables.append(rot[y+h//3:y+h-h//3,x+w//2:x+int(w*0.70)])
y_coords,lables = zip(*sorted(zip(y_coords,lables))) # sorting the lables from top to bottom
y_coords = list(y_coords)
lables = list(lables)
otsus = []
grays = []
for image in lables:
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
grays.append(gray)
mean = np.mean(gray)
median = np.median(gray)
otsus.append(cv2.threshold(gray,median-10,255,cv2.THRESH_BINARY)[1])
s = [] # just a list to store all the red and purple stripes
for i in range(len(otsus)):
otsus[i] = ~otsus[i]
rows,cols = otsus[i].shape[:2]
M = np.float32([[1,0,5],[0,1,5]])
lables[i] = cv2.warpAffine(lables[i],M,(cols+5,rows+10)) # giving some padding
# adjusting the gamma to make the colors more dark
lables[i] = adjust_gamma(lables[i],gamma=0.40)
otsus[i] = cv2.warpAffine(otsus[i],M,(cols+5,rows+10)) # same padding
strips,_ = cv2.findContours(otsus[i],cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
x_coords = [] # to sort the red and purple stripes from left to right
color = ["red","red"] # setting inital colors both to red
red_components = []
for j in range(len(strips)):
x,y,w,h = cv2.boundingRect(strips[j])
x_coords.append(x)
# strip = lables[i][y:y+h,x:x+w,:]
# strip = lables[i][y+h//4:y+h-h//4,x+w//4:x+w-w//4,:]
strip = lables[i][y:y+h,x+w//5:x+w-w//5,:]
s.append(strip)
try:
strip_color_b = int(np.mean(strip[0]))
strip_color_g = int(np.mean(strip[1]))
strip_color_r = int(np.mean(strip[2]))
# strip_color = (strip_color_b,strip_color_g,strip_color_r)
red = strip_color_r
red_components.append(red)
except IndexError:
print("Lable number ",i,"has only a single color.")
x_coords,red_components = zip(*sorted(zip(x_coords,red_components))) # simultaneously sorting the red and purple stripes from left to right
try:
if red_components[0] < red_components[1]:
color[0] = "purple"
else:
color[1] = "purple"
# print("red components = ",red_components)
print("Lable = ",i,"strip = ",0,"color = ",color[0])
print("Lable = ",i,"strip = ",1,"color = ",color[1]) # LEFT to RIGHT
except IndexError:
print("Lable number ",i,"is excluded. Continuing with further labels.")
print("TOTAL NUMBER OF LABLES = ",len(lables))
# reading the text
custom_oem_psm_config = r'--oem 3 --psm 3'
print(pytesseract.image_to_string(otsu,config=custom_oem_psm_config))
cv2.imshow("Lable 0",lables[0])
cv2.imshow("Lable 1",lables[1])
cv2.imshow("Lable 2",lables[2])
cv2.imshow("rotated",rot)
cv2.waitKey(0)
OUTPUT:
Lable number 0 has only a single color.
Lable number 0 is excluded. Continuing with further labels.
Lable = 1 strip = 0 color = purple
Lable = 1 strip = 1 color = red
Lable = 2 strip = 0 color = purple
Lable = 2 strip = 1 color = red
TOTAL NUMBER OF LABLES = 3
COVID-19Aq
COVID-19Ag
Please correct me if I am wrong.
I tried doing this but it takes too long to run. The problem is that the black text in the image is made by a lot of different shades of gray and the color. I also want to remove shades of gray that range between 230 to 255.
How can I do this better?
OLD_PATH = r'C:\Users\avivb\Desktop\Untitled.png'
NEW_PATH = r'C:\Users\avivb\Desktop\test.png'
R_OLD, G_OLD, B_OLD = (range(230,255), range(230,255), range(230,255))
R_NEW, G_NEW, B_NEW = (255, 255, 255)
from PIL import Image
im = Image.open(OLD_PATH)
pixels = im.load()
width, height = im.size
for x in range(width):
for y in range(height):
r, g, b = pixels[x, y]
for i in R_OLD:
for j in G_OLD:
for k in B_OLD:
if (r, g, b) == (i, j, k):
pixels[x, y] = (R_NEW, G_NEW, B_NEW)
im.save(NEW_PATH)
If you are looking for performance, I would avoid as many for statements as I can, as they are slower in python than in other low-level languages (like C or C++).
This is my approach using openCV, should be very fast:
import cv2 as cv
# Set range of color values
lower = np.array([230, 230, 230])
upper = np.array([255, 255, 255])
# Threshold the image to get only selected colors
mask = cv.inRange(img, lower, upper)
# Set the new value to the masked image
img[mask.astype(bool)] = 255
Please note that there are no explicit for in this code!
Hope it helps!
There is no need to iterate over the old ranges like that - use an if on each channel and your speed will greatly improve.
threshold = 230
replacement_pixel = (255, 255, 255)
from PIL import Image
im = Image.open(OLD_PATH)
pixels = im.load()
width, height = im.size
for x in range(width):
for y in range(height):
r, g, b = pixels[x, y]
if r >= threshold and g >= threshold and b >= treshold:
pixels[x, y] = replacement_pixel
im.save(NEW_PATH)
If the initial image is like this(above) then I can successfully introduce space between the 2 lines and get this image(below)
using the code below:
import os
import cv2
def space_between_lines_and_skewness_correction(file_path):
img = cv2.imread(os.path.expanduser(file_path))
grey = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
th, threshed = cv2.threshold(grey, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
pts = cv2.findNonZero(threshed)
ret = cv2.minAreaRect(pts)
(cx, cy), (w, h), ang = ret
if w < h:
w, h = h, w
ang += 90
M = cv2.getRotationMatrix2D((cx, cy), ang, 1.0)
rotated = cv2.warpAffine(threshed, M, (img.shape[1], img.shape[0]))
hist = cv2.reduce(rotated, 1, cv2.REDUCE_AVG).reshape(-1)
th = 2
H, W = img.shape[:2]
delimeter = [y for y in range(H - 1) if hist[y] <= th < hist[y + 1]]
arr = []
y_prev = 0
y_curr = 0
for y in delimeter:
y_prev = y_curr
y_curr = y
arr.append(rotated[y_prev:y_curr, 0:W])
arr.append(rotated[y_curr:H, 0:W])
space_arr = np.zeros((10, W))
final_img = np.zeros((1, W))
for im in arr:
v = np.concatenate((space_arr, im), axis=0)
final_img = np.concatenate((final_img, v), axis=0)
return final_img
The above code will remove skewness and introduce space.
But for few cases, the above code doesn't work.
These are cases like:
The output for the image is
How to handle cases such as this?
Note:
I tried to resize to a bigger size and do pixel by pixel iteration and building a custom algorithm for this case, but it is taking a huge amount of time to solve and sometimes giving memory error.
Please Note: The input of the above code is actually the inverse image(white background) of the image provided here
Maybe this helps:
def detect_letters(img):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# just to remove noise
thresh_val, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
num_labels, _, stats, centroids = cv2.connectedComponentsWithStats(thresh)
for i in range(num_labels):
leftmost_x = stats[i, cv2.CC_STAT_LEFT]
topmost_y = stats[i, cv2.CC_STAT_TOP]
width = stats[i, cv2.CC_STAT_WIDTH]
height = stats[i, cv2.CC_STAT_HEIGHT]
# enclose all detected components in a blue rectangle
cv2.rectangle(img, (leftmost_x, topmost_y), (leftmost_x + width, topmost_y + height), (255, 0, 0), 2)
cv2.imshow("window", img)
cv2.waitKey(0) & 0xFF
Input:
Output:
The main intent of the above solution is just to get an enclosing rectangle around every letter.
Now all you need to do is shift all those letters above or below or wherever you want to.
For example, see how the football was shifted in the following link : https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_core/py_basic_ops/py_basic_ops.html
As you know the topmost and bottom-most y coordinate for every letter now, you can see how much far away they currently are and if they are very close just shift the letter as in the above link.
The letters on the same line will have very little difference in their vertex coordinates or centroids. You can have a tolerence range to spot out all those letters.
If any issues, feel free to ask.