Specified the "analyze" script as the "normalize" script. Can be used for multiple processes.
At the point where the straightened image needs to be centered onto an idealized blank sheet.
This commit is contained in:
parent
602b6dc26c
commit
ca6d7d7d0b
|
@ -1,212 +0,0 @@
|
|||
import sys
|
||||
import cv2
|
||||
import numpy as np
|
||||
import math
|
||||
|
||||
def image_resize(image, width = None, height = None, inter = cv2.INTER_AREA):
|
||||
dim = None
|
||||
(h, w) = image.shape[:2]
|
||||
|
||||
if width is None and height is None:
|
||||
return image
|
||||
|
||||
if width is None:
|
||||
r = height / float(h)
|
||||
dim = (int(w * r), height)
|
||||
else:
|
||||
r = width / float(w)
|
||||
dim = (width, int(h * r))
|
||||
resized = cv2.resize(image, dim, interpolation = inter)
|
||||
|
||||
return resized
|
||||
|
||||
def display (image) :
|
||||
resized = image_resize(image, 800, 800)
|
||||
cv2.imshow('img', resized)
|
||||
|
||||
while True:
|
||||
key = cv2.waitKey(0)
|
||||
if key == 27:
|
||||
cv2.destroyAllWindows()
|
||||
break
|
||||
exit(0)
|
||||
|
||||
def get_center (contour) :
|
||||
M = cv2.moments(contour)
|
||||
cX = int(M["m10"] / M["m00"])
|
||||
cY = int(M["m01"] / M["m00"])
|
||||
return cX, cY
|
||||
|
||||
def draw_line (image, hps, a, b) :
|
||||
print(f'{a} -> {b}')
|
||||
lA = (hps[a-1]['x'], hps[a-1]['y'])
|
||||
lB = (hps[b-1]['x'], hps[b-1]['y'])
|
||||
cv2.line(image, lA, lB, [0, 255, 0], 10)
|
||||
return (lA, lB)
|
||||
|
||||
def horiz_angle (line) :
|
||||
deltaY = line[1][1] - line[0][1] #P2_y - P1_y
|
||||
deltaX = line[1][0] - line[0][0] #P2_x - P1_x
|
||||
|
||||
angleInDegrees = math.degrees(math.atan2(deltaY, deltaX))
|
||||
print(angleInDegrees)
|
||||
return angleInDegrees
|
||||
|
||||
def verts_angle (line) :
|
||||
deltaY = line[1][1] - line[0][1] #P2_y - P1_y
|
||||
deltaX = line[1][0] - line[0][0] #P2_x - P1_x
|
||||
|
||||
angleInDegrees = - (math.atan2(deltaX, deltaY) * 180 / math.pi)
|
||||
print(angleInDegrees)
|
||||
return angleInDegrees
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print('Please provide path to image for analysis')
|
||||
exit(1)
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print('Please provide path to template file to create')
|
||||
exit(2)
|
||||
|
||||
scanImage = sys.argv[-2]
|
||||
templateFile = sys.argv[-1]
|
||||
|
||||
print(f'Analyzing {scanImage} and creating {templateFile}')
|
||||
|
||||
orig = cv2.imread(scanImage)
|
||||
img = orig.copy()
|
||||
|
||||
height, width = img.shape[:2]
|
||||
orientation = height > width
|
||||
pageDim = (11, 8.5)
|
||||
if not orientation :
|
||||
pageDim = (8.5, 11)
|
||||
pageRatio = pageDim[1] / pageDim[0]
|
||||
|
||||
left=-1
|
||||
right=-1
|
||||
top=-1
|
||||
bottom=-1
|
||||
|
||||
if orientation :
|
||||
left = width * 0.2
|
||||
right = width * 0.8
|
||||
else :
|
||||
top = height * 0.2
|
||||
bottom = height * 0.8
|
||||
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
blur = cv2.medianBlur(gray, 31)
|
||||
ret, thresh = cv2.threshold(blur, 200, 255, cv2.THRESH_BINARY)
|
||||
canny = cv2.Canny(thresh, 75, 200)
|
||||
contours, hierarchy = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
|
||||
|
||||
contourList = []
|
||||
areaList = []
|
||||
for contour in contours:
|
||||
approx = cv2.approxPolyDP(contour, 0.03 * cv2.arcLength(contour, True), True)
|
||||
if cv2.isContourConvex(approx) :
|
||||
cX, cY = get_center(contour)
|
||||
if (orientation and ( cX < left or cX > right) ) or ( not orientation and ( cY < top or cY > bottom)) :
|
||||
area = cv2.contourArea(contour)
|
||||
areaList.append(area)
|
||||
contourList.append(contour)
|
||||
|
||||
maxArea=0
|
||||
maxIndex=0
|
||||
|
||||
#reduce to lambda
|
||||
for i in range(len(areaList)) :
|
||||
area = areaList[i]
|
||||
if area > maxArea:
|
||||
maxArea = area
|
||||
maxIndex = i
|
||||
|
||||
count = 0
|
||||
holePunches = []
|
||||
centersStr = []
|
||||
areaRange = 0
|
||||
topLeft = None
|
||||
minDist = 1000000
|
||||
|
||||
# pretty good
|
||||
# add position constraint
|
||||
|
||||
while count < 6 :
|
||||
areaRange+=1
|
||||
for i in range(len(areaList)) :
|
||||
area = areaList[i]
|
||||
if area == maxArea or area * ((100 + areaRange) / 100) > maxArea :
|
||||
cX, cY = get_center(contourList[i])
|
||||
strC = f'{cX},{cY}'
|
||||
if strC in centersStr :
|
||||
continue
|
||||
centersStr.append(strC)
|
||||
print(f'{cX},{cY}')
|
||||
#cv2.circle(img, (cX, cY), 40, (255, 0, 0), -1)
|
||||
hp = {
|
||||
'x' : cX,
|
||||
'y' : cY,
|
||||
'contour' : contourList[i],
|
||||
'dist' : math.dist((cX, cY), (0, 0)),
|
||||
'order': -1
|
||||
}
|
||||
if hp['dist'] < minDist :
|
||||
minDist = hp['dist']
|
||||
topLeft = hp
|
||||
holePunches.append(hp)
|
||||
count+=1
|
||||
|
||||
for hp in holePunches :
|
||||
hp['dist'] = math.dist( (topLeft['x'], topLeft['y']), (hp['x'], hp['y']) )
|
||||
|
||||
holePunches = sorted(holePunches, key = lambda hp: hp['dist'])
|
||||
|
||||
i = 0
|
||||
for hp in holePunches :
|
||||
hp['order'] = i
|
||||
cv2.putText(img, str(i + 1), (hp['x'], hp['y']), cv2.FONT_HERSHEY_SIMPLEX, 20, (0, 0, 255), 5, cv2.LINE_AA, False)
|
||||
i+=1
|
||||
|
||||
verts = []
|
||||
horiz = []
|
||||
|
||||
|
||||
#across top
|
||||
horiz.append(horiz_angle(draw_line(img, holePunches, 1, 3)))
|
||||
#across bottom
|
||||
horiz.append(horiz_angle(draw_line(img, holePunches, 4, 6)))
|
||||
#middle
|
||||
horiz.append(horiz_angle(draw_line(img, holePunches, 2, 5)))
|
||||
|
||||
#top left
|
||||
verts.append(verts_angle(draw_line(img, holePunches, 1, 2)))
|
||||
#long left
|
||||
verts.append(verts_angle(draw_line(img, holePunches, 1, 4)))
|
||||
|
||||
#top right
|
||||
verts.append(verts_angle(draw_line(img, holePunches, 3, 5)))
|
||||
#long right
|
||||
verts.append(verts_angle(draw_line(img, holePunches, 3, 6)))
|
||||
|
||||
#bottom left
|
||||
verts.append(verts_angle(draw_line(img, holePunches, 2, 4)))
|
||||
#bottom right
|
||||
verts.append(verts_angle(draw_line(img, holePunches, 5, 6)))
|
||||
|
||||
|
||||
#for v in verts :
|
||||
#print(v)
|
||||
|
||||
#for h in horiz :
|
||||
# print(h)
|
||||
# horiz_angle(h)
|
||||
|
||||
print(f'Found hole punches within {areaRange}% of largest')
|
||||
#print(holePunches)
|
||||
|
||||
#cv2.drawContours(img, list(map(lambda hp : hp['contour'], holePunches)), -1, (0, 255, 0), 20)
|
||||
#cv2.circle(img, (topLeft['x'], topLeft['y']), 50, (0, 0, 255), -1)
|
||||
|
||||
display(img)
|
||||
|
|
@ -0,0 +1,386 @@
|
|||
import sys
|
||||
import cv2
|
||||
import numpy as np
|
||||
import math
|
||||
|
||||
def image_resize(image, width = None, height = None, inter = cv2.INTER_AREA):
|
||||
dim = None
|
||||
(h, w) = image.shape[:2]
|
||||
|
||||
if width is None and height is None:
|
||||
return image
|
||||
|
||||
if width is None:
|
||||
r = height / float(h)
|
||||
dim = (int(w * r), height)
|
||||
else:
|
||||
r = width / float(w)
|
||||
dim = (width, int(h * r))
|
||||
resized = cv2.resize(image, dim, interpolation = inter)
|
||||
|
||||
return resized
|
||||
|
||||
def display (image) :
|
||||
resized = image_resize(image, 800, 800)
|
||||
cv2.imshow('img', resized)
|
||||
|
||||
while True:
|
||||
key = cv2.waitKey(0)
|
||||
if key == 27:
|
||||
cv2.destroyAllWindows()
|
||||
break
|
||||
exit(0)
|
||||
|
||||
def get_center (contour) :
|
||||
M = cv2.moments(contour)
|
||||
cX = int(M["m10"] / M["m00"])
|
||||
cY = int(M["m01"] / M["m00"])
|
||||
return cX, cY
|
||||
|
||||
def draw_line (image, hps, a, b) :
|
||||
print(f'{a} -> {b}')
|
||||
lA = (hps[a-1]['x'], hps[a-1]['y'])
|
||||
lB = (hps[b-1]['x'], hps[b-1]['y'])
|
||||
cv2.line(image, lA, lB, [0, 255, 0], 10)
|
||||
return (lA, lB)
|
||||
|
||||
def horiz_angle (line, rotate = 0) :
|
||||
deltaY = line[1][1] - line[0][1] #P2_y - P1_y
|
||||
deltaX = line[1][0] - line[0][0] #P2_x - P1_x
|
||||
|
||||
angleInDegrees = normalize_angle(math.degrees(math.atan2(deltaY, deltaX) + rotate))
|
||||
return angleInDegrees
|
||||
|
||||
def verts_angle (line) :
|
||||
angleInDegrees = normalize_angle(horiz_angle(line, math.pi/2))
|
||||
return angleInDegrees
|
||||
|
||||
def is_close (point, points) :
|
||||
for pt in points :
|
||||
if math.dist(point, pt) < 100 :
|
||||
return True
|
||||
return False
|
||||
|
||||
def normalize_angle (num, lower=0.0, upper=360.0, b=False):
|
||||
"""Normalize number to range [lower, upper) or [lower, upper].
|
||||
Parameters
|
||||
----------
|
||||
num : float
|
||||
The number to be normalized.
|
||||
lower : float
|
||||
Lower limit of range. Default is 0.0.
|
||||
upper : float
|
||||
Upper limit of range. Default is 360.0.
|
||||
b : bool
|
||||
Type of normalization. See notes.
|
||||
Returns
|
||||
-------
|
||||
n : float
|
||||
A number in the range [lower, upper) or [lower, upper].
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If lower >= upper.
|
||||
Notes
|
||||
-----
|
||||
If the keyword `b == False`, the default, then the normalization
|
||||
is done in the following way. Consider the numbers to be arranged
|
||||
in a circle, with the lower and upper marks sitting on top of each
|
||||
other. Moving past one limit, takes the number into the beginning
|
||||
of the other end. For example, if range is [0 - 360), then 361
|
||||
becomes 1. Negative numbers move from higher to lower
|
||||
numbers. So, -1 normalized to [0 - 360) becomes 359.
|
||||
If the keyword `b == True` then the given number is considered to
|
||||
"bounce" between the two limits. So, -91 normalized to [-90, 90],
|
||||
becomes -89, instead of 89. In this case the range is [lower,
|
||||
upper]. This code is based on the function `fmt_delta` of `TPM`.
|
||||
Range must be symmetric about 0 or lower == 0.
|
||||
Examples
|
||||
--------
|
||||
>>> normalize(-270,-180,180)
|
||||
90
|
||||
>>> import math
|
||||
>>> math.degrees(normalize(-2*math.pi,-math.pi,math.pi))
|
||||
0.0
|
||||
>>> normalize(181,-180,180)
|
||||
-179
|
||||
>>> normalize(-180,0,360)
|
||||
180
|
||||
>>> normalize(36,0,24)
|
||||
12
|
||||
>>> normalize(368.5,-180,180)
|
||||
8.5
|
||||
>>> normalize(-100, -90, 90, b=True)
|
||||
-80.0
|
||||
>>> normalize(100, -90, 90, b=True)
|
||||
80.0
|
||||
>>> normalize(181, -90, 90, b=True)
|
||||
-1.0
|
||||
>>> normalize(270, -90, 90, b=True)
|
||||
-90.0
|
||||
"""
|
||||
# abs(num + upper) and abs(num - lower) are needed, instead of
|
||||
# abs(num), since the lower and upper limits need not be 0. We need
|
||||
# to add half size of the range, so that the final result is lower +
|
||||
# <value> or upper - <value>, respectively.
|
||||
res = num
|
||||
if not b:
|
||||
if lower >= upper:
|
||||
raise ValueError("Invalid lower and upper limits: (%s, %s)" %
|
||||
(lower, upper))
|
||||
|
||||
res = num
|
||||
if num > upper or num == lower:
|
||||
num = lower + abs(num + upper) % (abs(lower) + abs(upper))
|
||||
if num < lower or num == upper:
|
||||
num = upper - abs(num - lower) % (abs(lower) + abs(upper))
|
||||
|
||||
res = lower if res == upper else num
|
||||
else:
|
||||
total_length = abs(lower) + abs(upper)
|
||||
if num < -total_length:
|
||||
num += math.ceil(num / (-2 * total_length)) * 2 * total_length
|
||||
if num > total_length:
|
||||
num -= math.floor(num / (2 * total_length)) * 2 * total_length
|
||||
if num > upper:
|
||||
num = total_length - num
|
||||
if num < lower:
|
||||
num = -total_length - num
|
||||
|
||||
res = num * 1.0 # Make all numbers float, to be consistent
|
||||
|
||||
return res
|
||||
|
||||
def mean (lst):
|
||||
return sum(lst) / len(lst)
|
||||
|
||||
def find_hole_punches (img) :
|
||||
left=-1
|
||||
right=-1
|
||||
top=-1
|
||||
bottom=-1
|
||||
|
||||
if orientation :
|
||||
left = width * 0.2
|
||||
right = width * 0.8
|
||||
else :
|
||||
top = height * 0.2
|
||||
bottom = height * 0.8
|
||||
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
blur = cv2.medianBlur(gray, 31)
|
||||
ret, thresh = cv2.threshold(blur, 200, 255, cv2.THRESH_BINARY)
|
||||
canny = cv2.Canny(thresh, 75, 200)
|
||||
contours, hierarchy = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
|
||||
|
||||
contourList = []
|
||||
areaList = []
|
||||
for contour in contours:
|
||||
approx = cv2.approxPolyDP(contour, 0.03 * cv2.arcLength(contour, True), True)
|
||||
if cv2.isContourConvex(approx) :
|
||||
cX, cY = get_center(contour)
|
||||
if (orientation and ( cX < left or cX > right) ) or ( not orientation and ( cY < top or cY > bottom)) :
|
||||
area = cv2.contourArea(contour)
|
||||
areaList.append(area)
|
||||
contourList.append(contour)
|
||||
|
||||
maxArea=0
|
||||
maxIndex=0
|
||||
|
||||
#reduce to lambda
|
||||
for i in range(len(areaList)) :
|
||||
area = areaList[i]
|
||||
if area > maxArea:
|
||||
maxArea = area
|
||||
maxIndex = i
|
||||
|
||||
count = 0
|
||||
holePunches = []
|
||||
centers = []
|
||||
areaRange = 0
|
||||
topLeft = None
|
||||
minDist = 1000000
|
||||
|
||||
# pretty good
|
||||
# add position constraint
|
||||
|
||||
while count < 6 :
|
||||
areaRange+=1
|
||||
for i in range(len(areaList)) :
|
||||
area = areaList[i]
|
||||
if area == maxArea or area * ((100 + areaRange) / 100) > maxArea :
|
||||
cX, cY = get_center(contourList[i])
|
||||
if is_close((cX, cY), centers) :
|
||||
continue
|
||||
centers.append((cX, cY))
|
||||
print(f'{cX},{cY}')
|
||||
hp = {
|
||||
'x' : cX,
|
||||
'y' : cY,
|
||||
'contour' : contourList[i],
|
||||
'dist' : math.dist((cX, cY), (0, 0)),
|
||||
'order': -1
|
||||
}
|
||||
if hp['dist'] < minDist :
|
||||
minDist = hp['dist']
|
||||
topLeft = hp
|
||||
holePunches.append(hp)
|
||||
count+=1
|
||||
|
||||
for hp in holePunches :
|
||||
hp['dist'] = math.dist( (topLeft['x'], topLeft['y']), (hp['x'], hp['y']) )
|
||||
|
||||
print(f'Hole punches: {len(holePunches)}')
|
||||
print(f'Found hole punches within {areaRange}% of largest')
|
||||
|
||||
if len(holePunches) != 6:
|
||||
print(f'Wrong number of hole punches, exiting...')
|
||||
exit(4)
|
||||
|
||||
holePunches = sorted(holePunches, key = lambda hp: hp['dist'])
|
||||
|
||||
i = 0
|
||||
for hp in holePunches :
|
||||
hp['order'] = i
|
||||
#cv2.putText(img, str(i + 1), (hp['x'], hp['y']), cv2.FONT_HERSHEY_SIMPLEX, 20, (0, 0, 255), 5, cv2.LINE_AA, False)
|
||||
i+=1
|
||||
|
||||
return holePunches
|
||||
|
||||
def correct_rotation (img, original, holePunches) :
|
||||
horizLines = [
|
||||
(3, 1),
|
||||
(6, 4),
|
||||
(5, 2)
|
||||
]
|
||||
vertsLines = [
|
||||
(1, 2),
|
||||
(1, 4), #double long left
|
||||
(1, 4), #
|
||||
(3, 5),
|
||||
(3, 6), #double long right
|
||||
(3, 6), #
|
||||
(2, 4),
|
||||
(5, 6)
|
||||
]
|
||||
|
||||
rotations = []
|
||||
|
||||
for h in horizLines :
|
||||
line = draw_line(img, holePunches, h[0], h[1])
|
||||
angle = horiz_angle(line)
|
||||
print(angle)
|
||||
rotations.append(angle)
|
||||
|
||||
for v in vertsLines :
|
||||
line = draw_line(img, holePunches, v[0], v[1])
|
||||
angle = verts_angle(line)
|
||||
print(angle)
|
||||
rotations.append(angle)
|
||||
|
||||
correctionRotation = mean(rotations) - 180
|
||||
print(f'Mean rotation: {correctionRotation}')
|
||||
|
||||
(cX, cY) = (width // 2, height // 2)
|
||||
M = cv2.getRotationMatrix2D((cX, cY), correctionRotation, 1.0)
|
||||
|
||||
#create rotation of original
|
||||
return cv2.warpAffine(original, M, (width, height))
|
||||
|
||||
def create_blank (w, h, rgb_color = (255, 255, 255)) :
|
||||
blank = np.zeros([h, w, 3], dtype=np.uint8)
|
||||
color = tuple(reversed(rgb_color))
|
||||
blank[:] = color
|
||||
return blank
|
||||
|
||||
def place (bottom, top, x, y) :
|
||||
return bottom.paste(top, (x, y))
|
||||
|
||||
def get_mean_rect (holePunches) :
|
||||
left = 0
|
||||
right = 0
|
||||
top = 0
|
||||
bottom = 0
|
||||
for hp in holePunches :
|
||||
if hp['order'] == 0 :
|
||||
left += float(hp['x'])
|
||||
top += float(hp['y'])
|
||||
elif hp['order'] == 2 :
|
||||
right += float(hp['y'])
|
||||
top += float(hp['y'])
|
||||
elif hp['order'] == 3 :
|
||||
left += float(hp['x'])
|
||||
bottom += float(hp['y'])
|
||||
elif hp['order'] == 5 :
|
||||
right += float(hp['x'])
|
||||
bottom += float(hp['y'])
|
||||
x = round((right / 2.0) - (left / 2.0))
|
||||
y = round((bottom / 2.0) - (top / 2.0))
|
||||
return (x, y)
|
||||
|
||||
|
||||
def center_within (larger, smaller) :
|
||||
w1 = larger[0]
|
||||
h1 = larger[1]
|
||||
w2 = smaller[0]
|
||||
h2 = smaller[1]
|
||||
x = ((w1 - w2) / 2)
|
||||
y = ((h1 - h2) / 2)
|
||||
return (x, y)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print('Please provide path of scan to normalize')
|
||||
exit(1)
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print('Please provide path to output file')
|
||||
exit(2)
|
||||
|
||||
scanImage = sys.argv[-2]
|
||||
normalImage = sys.argv[-1]
|
||||
pageDim = (11, 8.5)
|
||||
pageRatio = pageDim[1] / pageDim[0]
|
||||
|
||||
print(f'Normalizing {scanImage} as {normalImage}')
|
||||
|
||||
original = cv2.imread(scanImage)
|
||||
img = original.copy()
|
||||
|
||||
height, width = img.shape[:2]
|
||||
orientation = height > width
|
||||
if not orientation :
|
||||
print(f'Scan is not in portrait mode, exiting...')
|
||||
exit(3)
|
||||
|
||||
normalHeight = round(float(width) / pageRatio)
|
||||
|
||||
holePunches = find_hole_punches(img)
|
||||
rotated = correct_rotation(img, original, holePunches)
|
||||
|
||||
holePunches = find_hole_punches(rotated)
|
||||
blank = create_blank(width, normalHeight)
|
||||
|
||||
tl = None
|
||||
for hp in holePunches :
|
||||
if tl is None :
|
||||
tl = hp
|
||||
print(f'{hp["order"] + 1} {hp["x"]},{hp["y"]}')
|
||||
|
||||
print(f'Normal dimensions: {width},{normalHeight}')
|
||||
|
||||
# the mean rectangle is the average width and height
|
||||
# determined by the four corner hole punches
|
||||
meanRect = get_mean_rect(holePunches)
|
||||
print(f'Mean rectangle: {meanRect[0]},{meanRect[1]}')
|
||||
|
||||
# offset is the position within the new normal image
|
||||
# the top left hole punch should be centered to
|
||||
offset = center_within((width, normalHeight), meanRect)
|
||||
print(f'Offset: {offset[0]},{offset[1]}')
|
||||
|
||||
|
||||
|
||||
|
||||
#normal = place(blank, rotated, 0, 0)
|
||||
|
||||
#display(normal)
|
Loading…
Reference in New Issue