📜 ⬆️ ⬇️

Separate scanned photos together (Python 3 + OpenCV3)

Dozens of family photo albums have been stored on the drawers of cabinets and dusty shelves for decades. The condition of some of them has long made us think about the "digitization" of the accumulated material. And in order to speed up the process at least a little, it was decided to scan several photos at a time. However, the prospect of raking the resulting content as a result of this and I did not smile at my hands to split it up into individual frames. As a result, a solution was born ...

Given my acquaintance with the basics of python and interest in computer vision, the practical task turned up came in very handy. At the very beginning, I conducted testing on an image assembled in Pixelmator of three others. Looking ahead, I must say that then I did not consider the possibility of tilting photos on the original image. Each in his own way. Then, after selecting a few photos and making a couple of visits to the MFP, I received two images that checked the performance of the code as I wrote it. In view of the personal content of the albums, I will give an example in other images.

So the program looks from the terminal:
')


Source image


results








Interested? We continue.

I will not consider the installation of OpenCV, numerous instructions can be found in the vast web. So, initially we import the dependencies:

from numpy import int0, zeros_like, deg2rad, sin, cos, dot, array as nparray from math import ceil import cv2 from os import mkdir, chdir from os.path import basename, dirname, isdir, join as path_join from argparse import ArgumentParser 

The main () function looks like this:

 def main(): parser = ArgumentParser(description='   ') parser.add_argument('-n', type=int, dest='number', required=True, help='   ') parser.add_argument('-i', dest='image', required=True, help='   ') args = parser.parse_args() folder = dirname(args.image) image_name = basename(args.image) extension = image_name.split('.')[-1] image_name_without_extension = '.'.join(image_name.split('.')[:-1]) if folder: chdir(folder) if not isdir(image_name_without_extension): mkdir(image_name_without_extension) image = cv2.imread(image_name) contours = get_contours(image, args.number) i = 1 for c in contours: ca = int0(cv2.boxPoints(cv2.minAreaRect(c))) im = image[ca[2][1]:ca[0][1],ca[1][0]:ca[3][0]] im = rotate(ca, im) cv2.imwrite(path_join(image_name_without_extension, '%s.%s'%(i, extension)), im) i += 1 cv2.destroyAllWindows() 

Too hard? Let's take it in order.

 parser = ArgumentParser(description='   ') parser.add_argument('-n', type=int, dest='number', required=True, help='   ') parser.add_argument('-i', dest='image', required=True, help='   ') args = parser.parse_args() 

Create an object of type ArgumentParser with a description argument describing our future application. Add the argument '-n' of integer type, which will be stored as 'number', and also attach the parameter description for the -h / - help command. Similar to the string argument '-i', both arguments are required (required = True). The last line parses the arguments that were passed to the script at startup.

Further:

 folder = dirname(args.image) image_name = basename(args.image) extension = image_name.split('.')[-1] image_name_without_extension = ''.join(image_name.split('.')[:-1]) 

From the parameter args.image we get the path to the directory with the file (if any), the file name, extension and name without extension.

 if folder: chdir(folder) if not isdir(image_name_without_extension): mkdir(image_name_without_extension) 

If the folder with the image is not the current working directory, go to it. On the spot we create a folder with the name of the original image without an extension where we will add the final ones.

 image = cv2.imread(image_name) contours = get_contours(image, args.number) 

Open our image. Find the describing rectangles for each of the args.number of images.
The get_contours () function description is below.

Set the counter. Then a cycle begins in which we loop through the contours of the found images:

 i=1 for c in contours: 

Inside the loop:

We obtain a two-dimensional array of points of the minimum describing rectangle.

 ca = int0(cv2.boxPoints(cv2.minAreaRect(c))) 

Crop images on these points.

 im = image[ca[2][1]:ca[0][1],ca[1][0]:ca[3][0]] 

Rotate the image. The rotate () function is described below.

 im = rotate(ca, im) 

We save the image under the corresponding number in the folder created by the name of the source file without extension.

 cv2.imwrite(path_join(image_name_without_extension, '%s.%s'%(i, extension)), im) 

Ticking counter.

 i += 1 

And so until we go through all the elements of the original image. In general, everything. Now consider the function.

get_contour () looks like this:

 def get_contours(src, num=0): src = cv2.copyMakeBorder(src, 2, 2, 2, 2, cv2.BORDER_CONSTANT, value=(255, 255, 255)) gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) thresh = cv2.threshold(gray, 230, 255, cv2.THRESH_BINARY)[1] contours = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_TC89_L1)[1] if not num: return sorted(contours, key = cv2.contourArea, reverse = True)[1] else: return sorted(contours, key = cv2.contourArea, reverse = True)[1:num+1] 

Inside the function, we slightly expand the image to determine the images of "stuck" to the edges, lead to shades of gray, binarize and find the contours. If the call to this function was made from the rotate () function, the num parameter will remain equal to 0, then we return only one (the second, because the first, with index 0, describes the entire original image) element (contour) of the array sorted by area . If the call was made from the main () function, the num parameter contains args.number and the get_contours () function returns args.number of contours.

The rotate () function. Here I allow myself to do with the comments in the code.

 def rotate(contour, src): #   angle = cv2.minAreaRect(contour)[2] if angle > 45: angle -= 90 if angle < -45: angle += 90 #    w, h = src.shape[1], src.shape[0] #    rotangle = deg2rad(angle) #      nw = abs(sin(rotangle)*h) + abs(cos(rotangle)*w) nh = abs(cos(rotangle)*h) + abs(sin(rotangle)*w) #   rotation_matrix = cv2.getRotationMatrix2D((nw*0.5, nh*0.5), angle, 1.0) rotatiom_move = dot(rotation_matrix, nparray([(nw-w)*0.5, (nh-h)*0.5,0])) rotation_matrix[0,2] += rotatiom_move[0] rotation_matrix[1,2] += rotatiom_move[1] #  src = cv2.warpAffine(src, rotation_matrix, (int(ceil(nw)), int(ceil(nh))), flags=cv2.INTER_LANCZOS4, borderValue=(255,255,255)) #      ca = int0(cv2.boxPoints(cv2.minAreaRect(get_contours(src)))) #     , ""  return src[ca[2][1]+14:ca[0][1]-3,ca[1][0]+3:ca[3][0]-3] 

Thanks for reading. If someone knows how to optimize the algorithm and what kind of shoals I allowed, welcome to the comments. I hope someone this article will be useful.

Complete code
 #!/usr/local/bin/python3 from numpy import int0, zeros_like, deg2rad, sin, cos, dot, array as nparray from math import ceil import cv2 from os import mkdir, chdir from os.path import basename, dirname, isdir, join as path_join from argparse import ArgumentParser def get_contours(src, num=0): #     ""   src = cv2.copyMakeBorder(src, 2, 2, 2, 2, cv2.BORDER_CONSTANT, value=(255, 255, 255)) #    gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) # thresh = cv2.threshold(gray, 230, 255, cv2.THRESH_BINARY)[1] #  contours = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_TC89_L1)[1] if not num: #        return sorted(contours, key = cv2.contourArea, reverse = True)[1] else: # ars.n      main() return sorted(contours, key = cv2.contourArea, reverse = True)[1:num+1] def rotate(contour, src): #   angle = cv2.minAreaRect(contour)[2] if angle > 45: angle -= 90 if angle < -45: angle += 90 #    w, h = src.shape[1], src.shape[0] #    rotangle = deg2rad(angle) #      nw = abs(sin(rotangle)*h) + abs(cos(rotangle)*w) nh = abs(cos(rotangle)*h) + abs(sin(rotangle)*w) #   rotation_matrix = cv2.getRotationMatrix2D((nw*0.5, nh*0.5), angle, 1.0) rotatiom_move = dot(rotation_matrix, nparray([(nw-w)*0.5, (nh-h)*0.5,0])) rotation_matrix[0,2] += rotatiom_move[0] rotation_matrix[1,2] += rotatiom_move[1] #  src = cv2.warpAffine(src, rotation_matrix, (int(ceil(nw)), int(ceil(nh))), flags=cv2.INTER_LANCZOS4, borderValue=(255, 255, 255)) #    ca = int0(cv2.boxPoints(cv2.minAreaRect(get_contours(src)))) #   , ""  return src[ca[2][1]+14:ca[0][1]-3, ca[1][0]+3:ca[3][0]-3] def main(): parser = ArgumentParser(description='   ') parser.add_argument('-n', type=int, dest='number', required=True, help='   ') parser.add_argument('-i', dest='image', required=True, help='   ') args = parser.parse_args() folder = dirname(args.image) image_name = basename(args.image) extension = image_name.split('.')[-1] image_name_without_extension = '.'.join(image_name.split('.')[:-1]) if folder:#    -  cwd chdir(folder)#   if not isdir(image_name_without_extension): mkdir(image_name_without_extension)#        #  image = cv2.imread(image_name) #   contours = get_contours(image, args.number) i = 1# for c in contours: # np.     ca = int0(cv2.boxPoints(cv2.minAreaRect(c))) #      im = image[ca[2][1]:ca[0][1], ca[1][0]:ca[3][0]] #  im = rotate(ca, im) #   cv2.imwrite(path_join(image_name_without_extension, '%s.%s'%(i, extension)), im) #  i += 1 if __name__=='__main__': main() 

Source: https://habr.com/ru/post/281669/


All Articles