Compare commits

...

4 Commits

5 changed files with 233 additions and 127 deletions

1
fourcell/.gitignore vendored
View File

@ -1 +1,2 @@
env
__pycache__

View File

@ -8,9 +8,27 @@ Two scripts:
## Analysis Steps
1. Locate positions of 6 hole punches
2. Orient to square position
1. Locate positions of 6 hole punches - normalize.py
2. Orient to square position - normalize.py
3. Find all 4 fiducials
4. Calculate their position relative to the hole punches
5. Create a template
6. Use the template-filling script to recreate calibration page as a proof to be checked on a lightbox
## Page Hole Punch Order
```
______________
| |
| 1 3 |
| |
| |
| |
| 3 4 |
| |
| |
| |
| 5 6 |
| |
---------------
```

62
fourcell/calibrate.py Normal file
View File

@ -0,0 +1,62 @@
import sys
import cv2
import numpy as np
import math
from os.path import exists, basename
from common import image_resize, display, normalize_angle
#clockwise from top left
order = [ 1, 3, 4, 6, 5, 2 ]
def read_text (textPath) :
holePunches = {}
with open(textPath) as t:
for line in t:
i = int(line[0])
parts = line.split(' : ')
vals = parts[1].split(',')
holePunches[i] = {
'x' : int(vals[0]),
'y' : int(vals[1])
}
return holePunches
#
# CALIBRATE
#
if len(sys.argv) < 2:
print('Please provide path of normalized scan to calibrate to')
exit(1)
if len(sys.argv) < 3:
print('Please provide path to output svg template')
exit(2)
normalImage = sys.argv[-2]
if not exists(normalImage) :
print('Normalized scan does not exist, please provide one that does')
exit(2)
normalText = normalImage + '.txt'
if not exists(normalText) :
print('Corresponding normalized scan text does not exist, please generate one')
exit(3)
outputTmpl = sys.argv[-1]
print(f'Calibrating to scan {basename(normalImage)}')
holePunches = read_text(normalText)
original = cv2.imread(normalImage)
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)
display(img)

122
fourcell/common.py Normal file
View File

@ -0,0 +1,122 @@
import cv2
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 cv2.getWindowProperty('img', cv2.WND_PROP_VISIBLE) > 0:
key = cv2.waitKey(0)
if key == 27:
cv2.destroyAllWindows()
break
exit(0)
# taken from
# https://gist.github.com/phn/1111712/35e8883de01916f64f7f97da9434622000ac0390
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

View File

@ -2,34 +2,12 @@ import sys
import cv2
import numpy as np
import math
from json import dumps
from os.path import exists
from common import image_resize, display, normalize_angle
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 cv2.getWindowProperty('img', cv2.WND_PROP_VISIBLE) > 0:
key = cv2.waitKey(0)
if key == 27:
cv2.destroyAllWindows()
break
exit(0)
#clockwise from top left
order = [ 1, 3, 4, 6, 5, 2 ]
def get_center (contour) :
M = cv2.moments(contour)
@ -61,98 +39,6 @@ def is_close (point, points) :
return True
return False
# taken from
# https://gist.github.com/phn/1111712/35e8883de01916f64f7f97da9434622000ac0390
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)
@ -249,6 +135,15 @@ def find_hole_punches (img) :
return holePunches
def simplify_hole_punches (holePunches) :
simple = {}
for hp in holePunches :
simple[hp['order']] = {
'x' : hp['x'],
'y' : hp['y']
}
return simple
def correct_rotation (img, original, holePunches) :
horizLines = [
(3, 1),
@ -395,6 +290,10 @@ def normalize_image(blank, rotated, offset, tl) :
return blank
#
# NORMALIZE
#
if len(sys.argv) < 2:
print('Please provide path of scan to normalize')
exit(1)
@ -404,6 +303,11 @@ if len(sys.argv) < 3:
exit(2)
scanImage = sys.argv[-2]
if not exists(scanImage) :
print('Scan provided does not exist')
exit(5)
normalImage = sys.argv[-1]
pageDim = (11, 8.5)
pageRatio = pageDim[1] / pageDim[0]
@ -455,9 +359,8 @@ print(f'Writing normalized image to {normalImage}')
cv2.imwrite(normalImage, normal)
evaluation = find_hole_punches(normal)
jsonOut = simplify_hole_punches(evaluation)
with open(f'{normalImage}.txt', 'w') as evalFile :
for hp in evaluation:
evalFile.write(f'{hp["order"] + 1} : {hp["x"]},{hp["y"]}\n')
#display(normal)
with open(f'{normalImage}.json', 'w') as output:
output.write(dumps(jsonOut, sort_keys = True, indent = 4))
print(f'Wrote hole punch definition file to {normalImage}.json')