2023-05-22 20:32:50 +01:00
import argparse
2023-05-01 21:57:48 +01:00
import sys
import cv2
import numpy as np
import os
def find_photo_boundaries ( image , background_color , tolerance = 30 , min_area = 10000 , min_contour_area = 500 ):
mask = cv2 . inRange ( image , background_color - tolerance , background_color + tolerance )
mask = cv2 . bitwise_not ( mask )
kernel = np . ones (( 5 , 5 ), np . uint8 )
mask = cv2 . dilate ( mask , kernel , iterations = 2 )
contours , _ = cv2 . findContours ( mask , cv2 . RETR_EXTERNAL , cv2 . CHAIN_APPROX_SIMPLE )
photo_boundaries = []
for contour in contours :
x , y , w , h = cv2 . boundingRect ( contour )
area = w * h
contour_area = cv2 . contourArea ( contour )
if area >= min_area and contour_area >= min_contour_area :
photo_boundaries . append (( x , y , w , h ))
return photo_boundaries
def estimate_background_color ( image , sample_points = 5 ):
h , w , _ = image . shape
points = [
( 0 , 0 ),
( w - 1 , 0 ),
( w - 1 , h - 1 ),
( 0 , h - 1 ),
( w // 2 , h // 2 ),
]
colors = []
for x , y in points :
colors . append ( image [ y , x ])
return np . median ( colors , axis = 0 )
2023-05-22 20:32:50 +01:00
def auto_rotate ( image , angle_threshold = 1 ):
2023-05-01 21:57:48 +01:00
gray = cv2 . cvtColor ( image , cv2 . COLOR_BGR2GRAY )
2023-05-22 20:32:50 +01:00
edges = cv2 . Canny ( gray , 50 , 150 , apertureSize = 3 )
lines = cv2 . HoughLines ( edges , 1 , np . pi / 180 , 200 )
2023-05-01 21:57:48 +01:00
2023-05-22 20:32:50 +01:00
if lines is None :
2023-05-01 21:57:48 +01:00
return image
2023-05-22 20:32:50 +01:00
# compute the median angle of the lines
angles = []
for rho , theta in lines [:, 0 ]:
angles . append (( theta * 180 ) / np . pi - 90 )
2023-05-01 21:57:48 +01:00
2023-05-22 20:32:50 +01:00
angle = np . median ( angles )
2023-05-01 21:57:48 +01:00
if abs ( angle ) < angle_threshold :
return image
( h , w ) = image . shape [: 2 ]
center = ( w // 2 , h // 2 )
M = cv2 . getRotationMatrix2D ( center , angle , 1.0 )
return cv2 . warpAffine ( image , M , ( w , h ), flags = cv2 . INTER_CUBIC , borderMode = cv2 . BORDER_REPLICATE )
2023-05-22 20:32:50 +01:00
2023-05-01 21:57:48 +01:00
def crop_borders ( image , border_color , tolerance = 30 ):
mask = cv2 . inRange ( image , border_color - tolerance , border_color + tolerance )
contours , _ = cv2 . findContours ( mask , cv2 . RETR_EXTERNAL , cv2 . CHAIN_APPROX_SIMPLE )
if len ( contours ) == 0 :
return image
largest_contour = max ( contours , key = cv2 . contourArea )
x , y , w , h = cv2 . boundingRect ( largest_contour )
return image [ y : y + h , x : x + w ]
def split_photos ( input_file , output_directory , tolerance = 30 , min_area = 10000 , min_contour_area = 500 , angle_threshold = 10 , border_size = 0 ):
image = cv2 . imread ( input_file )
background_color = estimate_background_color ( image )
# Add a constant border around the image
image = cv2 . copyMakeBorder ( image , border_size , border_size , border_size , border_size , cv2 . BORDER_CONSTANT , value = background_color )
photo_boundaries = find_photo_boundaries ( image , background_color , tolerance )
if not os . path . exists ( output_directory ):
os . makedirs ( output_directory )
# Get the input file's base name without the extension
input_file_basename = os . path . splitext ( os . path . basename ( input_file ))[ 0 ]
for idx , ( x , y , w , h ) in enumerate ( photo_boundaries ):
cropped_image = image [ y : y + h , x : x + w ]
cropped_image = auto_rotate ( cropped_image , angle_threshold )
# Remove the added border
cropped_image = cropped_image [ border_size : - border_size , border_size : - border_size ]
output_path = os . path . join ( output_directory , f " { input_file_basename } _ { idx + 1 } .png" )
cv2 . imwrite ( output_path , cropped_image )
print ( f "Saved { output_path } " )
if __name__ == "__main__" :
2023-05-22 20:32:50 +01:00
parser = argparse . ArgumentParser ( description = "Split photos in an image" )
parser . add_argument ( "input_file" , help = "The input scanned image containing multiple photos." )
parser . add_argument ( "output_directory" , help = "The directory where the result images should be placed." )
parser . add_argument ( "--tolerance" , type = int , default = 30 , help = "Determines the range of color variation around the estimated background color (default: 30)." )
parser . add_argument ( "--min_area" , type = int , default = 10000 , help = "Sets the minimum area threshold for a photo (default: 10000)." )
parser . add_argument ( "--min_contour_area" , type = int , default = 500 , help = "Sets the minimum contour area threshold for a photo (default: 500)." )
parser . add_argument ( "--angle_threshold" , type = int , default = 10 , help = "Sets the minimum absolute angle required for the image to be rotated (default: 10)." )
parser . add_argument ( "--border_size" , type = int , default = 0 , help = "Sets the size of the border added and removed to prevent white borders in the output (default: 0)." )
args = parser . parse_args ()
2023-05-01 21:57:48 +01:00
2023-05-22 20:32:50 +01:00
split_photos ( args . input_file , args . output_directory , tolerance = args . tolerance , min_area = args . min_area , min_contour_area = args . min_contour_area , angle_threshold = args . angle_threshold , border_size = args . border_size )