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:
Matthew McWilliams 2022-11-10 18:06:47 -05:00
parent 602b6dc26c
commit ca6d7d7d0b
2 changed files with 386 additions and 212 deletions

View File

@ -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)

386
fourcell/normalize.py Normal file
View File

@ -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)