diff --git a/fourcell/analyze.py b/fourcell/analyze.py deleted file mode 100644 index ab6d260..0000000 --- a/fourcell/analyze.py +++ /dev/null @@ -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) - diff --git a/fourcell/normalize.py b/fourcell/normalize.py new file mode 100644 index 0000000..b4a95b1 --- /dev/null +++ b/fourcell/normalize.py @@ -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 + + # or upper - , 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) \ No newline at end of file