Skip to content

Instantly share code, notes, and snippets.

@smeschke
Last active December 26, 2024 17:25
Show Gist options
  • Select an option

  • Save smeschke/aa989df78551a9050a78e0d7a8c50495 to your computer and use it in GitHub Desktop.

Select an option

Save smeschke/aa989df78551a9050a78e0d7a8c50495 to your computer and use it in GitHub Desktop.

Revisions

  1. smeschke revised this gist Jun 26, 2019. No changes.
  2. smeschke revised this gist Jun 26, 2019. 1 changed file with 113 additions and 262 deletions.
    375 changes: 113 additions & 262 deletions align_scan.py
    Original file line number Diff line number Diff line change
    @@ -1,269 +1,120 @@
    import cv2, numpy as np, random, math

    # Find contour edges
    # Find the edge that is torn
    # use the hough line transform
    # create a mask image where the lines and white on a black background
    # check if the point is in a white or black region
    # Rotate the torn edges
    # Measure how much they overlap
    # The rotation with the maximum overlap will be how they should align
    # Align and display the images

    # Finds the shards of paper in an image
    def get_shards(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Gray
    gray = cv2.blur(gray, (2,2))
    canny = cv2.Canny(gray, 30, 150) # Canny
    cv2.imshow('canny', canny)
    # Find contours
    contours, _ = cv2.findContours(canny,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    # Draw contours on canny (this connects the contours)
    cv2.drawContours(canny, contours, -1, 255, 6)
    # Get mask for floodfill
    h, w = canny.shape[:2]
    mask = np.zeros((h+2, w+2), np.uint8)
    # Floodfill from point (0, 0)
    cv2.floodFill(canny, mask, (0,0), 123);
    cv2.imshow('ff', canny)
    cv2.imwrite('/home/stephen/Desktop/with.png', canny)
    # Get an image that is only the gray floodfilled area
    hsv = cv2.cvtColor(canny, cv2.COLOR_GRAY2BGR)
    lower, upper = np.array([122,122,122]), np.array([124,124,124])
    # Threshold the HSV image to get only blue colors
    mask = cv2.inRange(hsv, lower, upper)
    # Bitwise-AND mask and original image
    res = cv2.bitwise_and(hsv,hsv, mask= mask)
    gray = 255 - cv2.cvtColor(res, cv2.COLOR_BGR2GRAY)
    _, thresh = cv2.threshold(gray, 220, 255, cv2.THRESH_BINARY_INV);
    contours, _ = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    res = np.zeros_like(res)
    # Create a list for unconnected contours
    unconnectedContours = []
    for contour in contours:
    area = cv2.contourArea(contour)
    # If the contour is not really small, or really big
    if area > 987 and area < img.shape[0]*img.shape[1]-9000:
    cv2.drawContours(res, [contour], 0, (255,255,255), cv2.FILLED)
    unconnectedContours.append(contour)
    # Return the unconnected contours image and list of contours
    cv2.imshow('res', res)
    print(len(unconnectedContours), largest_contour(contours))
    cv2.waitKey()
    return res, unconnectedContours[0]

    # Distance between two points
    def distance(a,b): return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2)

    # Returns a single contour
    def largest_contour(contours):
    c = max(contours, key=cv2.contourArea)
    return c[0]

    # Draw a contour, but doesn't connect contour[0] with contour[-1]
    def draw_contour(img, contour, color, thick):
    for idx in range(len(contour)-1):
    a, b = contour[idx], contour[idx+1]
    a,b = tuple(a[0]), tuple(b[0])
    if distance(a,b) < 321: cv2.line(img, a, b, color, thick)
    return img

    # Finds the lines in an image
    def lines_mask(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    dst = cv2.Canny(gray, 50, 200, None, 3)
    # Create mask image
    mask = np.zeros_like(gray)
    # Find the lines
    lines = cv2.HoughLines(dst, 1, np.pi / 180, 100, None, 0, 0)
    # Draw the lines
    if lines is not None:
    for i in range(0, len(lines)):
    rho = lines[i][0][0]
    theta = lines[i][0][1]
    a = math.cos(theta)
    b = math.sin(theta)
    x0 = a * rho
    y0 = b * rho
    pt1 = (int(x0 + 1000*(-b)), int(y0 + 1000*(a)))
    pt2 = (int(x0 - 1000*(-b)), int(y0 - 1000*(a)))
    cv2.line(mask, pt1, pt2, 255, 18, cv2.LINE_AA)
    #cv2.imshow('mask', mask)
    return mask

    # Takes an image and a contour and returns the torn edge
    def find_torn_edge(img, cnt, img_lines):
    # Create a temporary iamge
    img_h, img_w = img.shape[0], img.shape[1]
    temp = np.zeros((img_h, img_w), np.uint8)
    temp_human = np.zeros((img_h, img_w), np.uint8)
    cv2.drawContours(temp_human, [cnt], 0, 78, cv2.FILLED)
    torn_edge = []
    for i in range(len(cnt)):
    x,y = cnt[i][0]
    if img_lines[y,x] == 0: torn_edge.append((x,y))
    #cnt1 = np.array(torn_edge1)
    for i in range(len(torn_edge)-1):
    a = torn_edge[i]
    b = torn_edge[i+1]
    cv2.line(temp, a, b, 255, 2)
    cv2.line(temp_human, a, b, 255, 14)
    return torn_edge, temp_human, temp

    # Rotate a contour
    def rotate_contour(contour, edge_mask, wOverlay, hOverlay, rotation):
    hw = 2* max(wOverlay, hOverlay)
    temp = np.zeros((hw,hw), np.uint8)
    #contour = contour + max(wOverlay, hOverlay)
    #temp = draw_contour(temp, contour, 255, 2)
    temp = edge_mask.copy()
    #cv2.imshow('temp', cv2.resize(temp, (456,456)))
    rows,cols = temp.shape
    M = cv2.getRotationMatrix2D((cols/2,rows/2),rotation,1)
    dst = cv2.warpAffine(temp,M,(cols,rows))
    _, thresh = cv2.threshold(dst, 123, 255, cv2.THRESH_BINARY)
    contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    return contours[0]

    # Translate a contour so that is located above another contour
    def align_translate(left, right):
    right_center, _ = cv2.minEnclosingCircle(right)
    left_center, _ = cv2.minEnclosingCircle(left)
    dx, dy = right_center[0]-left_center[0], right_center[1]-left_center[1]
    for i in range(len(right)):
    previous = right[i][0][0]
    right[i][0][0] = previous - dx
    previous = right[i][0][1]
    right[i][0][1] = previous - dy
    return left, right, dx, dy

    # Draws the output image
    def draw_background(best_match, img1, img2, img1_mask, img2_mask, offset):
    print(best_match)
    # Create a background for each of the images
    bgA = np.zeros((2345,2345,3), np.uint8)
    bgB = np.zeros((2345,2345,3), np.uint8)
    # Mask images for writing
    img1 = cv2.bitwise_and(img1, img1, mask = img1_mask)
    img2 = cv2.bitwise_and(img2, img2, mask = img2_mask)
    # Determine A buffer size
    _, rotation, dx, dy = best_match
    rotation, dx, dy = int(rotation), int(dx), int(dy)
    b = int(max(abs(dx), abs(dy))) + 10
    # Draw the first image on the background
    bgA[offset+b:offset+b+img_h, offset+b:offset+b+img_w] = img2
    # Rotate the second image
    M = cv2.getRotationMatrix2D((img_w/2,img_h/2),rotation,1)
    dst = cv2.warpAffine(img1,M,(img_w,img_h))
    # Translate it and paste it on the background
    bgB[offset+b-dy:offset+b-dy+img_h, offset+b-dx:offset+b-dx+img_w] = dst
    # Combine the backgrounds
    bg = bgA + bgB
    # Crop and resize
    x_vals = b-dx,b-dx+img_w, b,b+img_w
    y_vals = b, b+img_h, b-dy, b-dy+img_h
    bg = bg[min(y_vals): max(y_vals), min(x_vals): max(x_vals)]
    bg = cv2.resize(bg, (987,987))
    return bg

    # Read in images
    img1 = cv2.imread('/home/stephen/Desktop/paper3.jpg')
    img2 = cv2.imread('/home/stephen/Desktop/paper4.jpg')
    img_w, img_h = 780,1040
    img1 = cv2.resize(img1, (img_w, img_h))
    img2 = cv2.resize(img2, (img_w, img_h))
    # Get shards of paper
    res1, cnt1 = get_shards(img1)
    res2, cnt2 = get_shards(img2)
    # Find the lines in the image
    img1_lines = lines_mask(res1)
    img2_lines = lines_mask(res2)
    # Find the torn edges
    torn_edge1, temp_human1, edge_mask1 = find_torn_edge(img1, cnt1, img1_lines)
    cv2.imshow('img1', temp_human1)
    cv2.waitKey()
    torn_edge2, temp_human2, edge_mask2 = find_torn_edge(img2, cnt2, img2_lines)
    #cv2.imshow('img1', temp_human2)
    #cv2.waitKey()
    import cv2
    import numpy as np


    src = 255 - cv2.imread('/home/stephen/Desktop/I7Ykpbs.jpg',0)
    scores = []

    h,w = src.shape
    small_dimention = min(h,w)
    src = src[:small_dimention, :small_dimention]

    out = cv2.VideoWriter('/home/stephen/Desktop/rotate.avi',
    cv2.VideoWriter_fourcc('M','J','P','G'),
    15, (320,320))


    def rotate(img, angle):
    rows,cols = img.shape
    M = cv2.getRotationMatrix2D((cols/2,rows/2),angle,1)
    dst = cv2.warpAffine(img,M,(cols,rows))
    return dst

    def sum_rows(img):
    # Create a list to store the row sums
    row_sums = []
    # Iterate through the rows
    for r in range(img.shape[0]-1):
    # Sum the row
    row_sum = sum(sum(img[r:r+1,:]))
    # Add the sum to the list
    row_sums.append(row_sum)
    # Normalize range to (0,255)
    row_sums = (row_sums/max(row_sums)) * 255
    # Return
    return row_sums

    def display_data(roi, row_sums, buffer):
    # Create background to draw transform on
    bg = np.zeros((buffer*2, buffer*2), np.uint8)
    # Iterate through the rows and draw on the background
    for row in range(roi.shape[0]-1):
    row_sum = row_sums[row]
    bg[row:row+1, :] = row_sum
    left_side = int(buffer/3)
    bg[:, left_side:] = roi[:,left_side:]
    cv2.imshow('bg1', bg)
    k = cv2.waitKey(1)
    out.write(cv2.cvtColor(cv2.resize(bg, (320,320)), cv2.COLOR_GRAY2BGR))
    return k

    # Rotate the image around in a circle
    angle = 0
    while angle <= 360:
    # Rotate the source image
    img = rotate(src, angle)
    # Crop the center 1/3rd of the image (roi is filled with text)
    h,w = img.shape
    buffer = min(h, w) - int(min(h,w)/1.5)
    #roi = img.copy()
    roi = img[int(h/2-buffer):int(h/2+buffer), int(w/2-buffer):int(w/2+buffer)]
    # Create background to draw transform on
    bg = np.zeros((buffer*2, buffer*2), np.uint8)
    # Threshold image
    _, roi = cv2.threshold(roi, 140, 255, cv2.THRESH_BINARY)
    # Compute the sums of the rows
    row_sums = sum_rows(roi)
    # High score --> Zebra stripes
    score = np.count_nonzero(row_sums)
    if sum(row_sums) < 100000: scores.append(angle)
    k = display_data(roi, row_sums, buffer)
    if k == 27: break
    # Increment angle and try again
    angle += .5
    cv2.destroyAllWindows()

    # Plot
    # Create images for display purposes
    display = src.copy()
    # Create an image that contains bins.
    bins_image = np.zeros_like(display)
    for angle in scores:
    # Rotate the image and draw a line on it
    display = rotate(display, angle)
    cv2.line(display, (0,int(h/2)), (w,int(h/2)), 255, 1)
    display = rotate(display, -angle)
    # Rotate the bins image
    bins_image = rotate(bins_image, angle)
    # Draw a line on a temporary image
    temp = np.zeros_like(bins_image)
    cv2.line(temp, (0,int(h/2)), (w,int(h/2)), 50, 1)
    # 'Fill' up the bins
    bins_image += temp
    bins_image = rotate(bins_image, -angle)

    # Find the most filled bin
    for col in range(bins_image.shape[0]-1):
    column = bins_image[:, col:col+1]
    if np.amax(column) == np.amax(bins_image): x = col
    for col in range(bins_image.shape[0]-1):
    column = bins_image[:, col:col+1]
    if np.amax(column) == np.amax(bins_image): y = col
    # Draw circles showing the most filled bin
    cv2.circle(display, (x,y), 560, 255, 5)

    # Plot with Matplotlib
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg
    f, axarr = plt.subplots(2,4, sharex=True)
    axarr[0,0].imshow(img1)
    axarr[1,0].imshow(img2)
    outline1 = cv2.drawContours(img1.copy(), [cnt1], 0, (0,255,0), 15)
    outline2 = cv2.drawContours(img2.copy(), [cnt2], 0, (0,255,0), 15)
    img1_mask = np.zeros((img_h, img_w), np.uint8)
    img2_mask = np.zeros((img_h, img_w), np.uint8)
    img1_mask = cv2.drawContours(img1_mask, [cnt1], 0, 255, cv2.FILLED)
    img2_mask = cv2.drawContours(img2_mask, [cnt2], 0, 255, cv2.FILLED)
    axarr[0,1].imshow(outline1)
    axarr[1,1].imshow(outline2)
    axarr[0,2].imshow(img1_lines)
    axarr[1,2].imshow(img2_lines)
    axarr[0,3].imshow(temp_human1)
    axarr[1,3].imshow(temp_human2)
    axarr[0, 0].set_title('Source Images')
    axarr[0, 1].set_title('Paper Edges')
    axarr[0, 2].set_title('Hough Lines')
    axarr[0, 3].set_title('Torn Edge')
    f, axarr = plt.subplots(1,3, sharex=True)
    axarr[0].imshow(src)
    axarr[1].imshow(display)
    axarr[2].imshow(bins_image)
    axarr[0].set_title('Source Image')
    axarr[1].set_title('Output')
    axarr[2].set_title('Bins Image')
    axarr[0].axis('off')
    axarr[1].axis('off')
    axarr[2].axis('off')
    plt.show()

    vid_writer = cv2.VideoWriter('/home/stephen/Desktop/re_encode.avi',cv2.VideoWriter_fourcc('M','J','P','G'),20, (987, 987))

    # Rotate the contour
    left, right = cnt1, cnt2
    match, matches, angle, best_match = 0, [-1], 0, (0,0,0,0)
    graph = np.zeros((987,987,3), np.uint8)

    while angle < 380:
    # Copy the images and create temporary images
    img, overlay = img2.copy(), img1.copy()
    tempA = np.zeros((img.shape[0], img.shape[1]), np.uint8)
    tempB = np.zeros((img.shape[0], img.shape[1]), np.uint8)
    # Rotate the contour
    rotatedContour = rotate_contour(torn_edge1, edge_mask1, max(img.shape), max(img.shape), angle)
    # Clean left contour
    clean_left = rotate_contour(torn_edge2, edge_mask2, max(img.shape), max(img.shape), 0)
    # Translate the contour
    a,b, dx, dy = align_translate(clean_left, rotatedContour)
    # Draw the contour
    tempA = draw_contour(tempA, b, 123, 3)
    tempB = draw_contour(tempB, a, 123, 3)
    tempC = tempA + tempB
    cv2.imwrite('/home/stephen/Desktop/thresh.png', tempC/1.5)
    _, thresh = cv2.threshold(tempC, 220, 255, cv2.THRESH_BINARY_INV);

    thresh = 255 - thresh
    match = sum(sum(thresh))
    matches.append(match)
    # Is this the best match?
    if match >= max(matches): best_match = b, angle, int(dx), int(dy)

    # Make the graph
    p1 = int(angle*2.35), 0
    p2 = int(angle*2.35), int(sum(sum(thresh))/75)
    cv2.line(graph, p1, p2, (0,255,0), 2)

    bg = draw_background((_, angle, dx, dy), img1, img2, img1_mask, img2_mask, 0)
    bg += graph

    img = draw_contour(bg, b, (255,0,255), 2)
    img = draw_contour(bg, a, (255,255,0), 2)
    img = draw_contour(bg, best_match[0], (0,255,255), 4)
    cv2.imshow('bg', bg)

    vid_writer.write(bg)
    k=cv2.waitKey(1)
    if k == 27: break
    angle += 1
    cv2.destroyAllWindows()

    # Show user the best match
    bg = draw_background(best_match, img1, img2, img1_mask, img2_mask, 0)
    cv2.imshow('img', bg)
    cv2.imwrite('/home/stephen/Desktop/paperReconstruction.png', bg)
    cv2.waitKey()
    cv2.destroyAllWindows()

  3. smeschke revised this gist May 24, 2019. No changes.
  4. smeschke revised this gist May 24, 2019. 1 changed file with 68 additions and 57 deletions.
    125 changes: 68 additions & 57 deletions align_scan.py
    Original file line number Diff line number Diff line change
    @@ -13,20 +13,22 @@
    # Finds the shards of paper in an image
    def get_shards(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Gray
    #blurred = cv2.GaussianBlur(gray, (5, 5), 0) # Blur
    gray = cv2.blur(gray, (2,2))
    canny = cv2.Canny(gray, 30, 150) # Canny
    cv2.imshow('canny', canny)
    # Find contours
    contours, _ = cv2.findContours(canny,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    # Draw contours on canny (this connects the contours)
    cv2.drawContours(canny, contours, -1, 255, 2)
    _, thresh = cv2.threshold(canny, 230, 255, cv2.THRESH_BINARY_INV);
    cv2.drawContours(canny, contours, -1, 255, 6)
    # Get mask for floodfill
    h, w = thresh.shape[:2]
    h, w = canny.shape[:2]
    mask = np.zeros((h+2, w+2), np.uint8)
    # Floodfill from point (0, 0)
    cv2.floodFill(thresh, mask, (0,0), 123);
    cv2.floodFill(canny, mask, (0,0), 123);
    cv2.imshow('ff', canny)
    cv2.imwrite('/home/stephen/Desktop/with.png', canny)
    # Get an image that is only the gray floodfilled area
    hsv = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR)
    hsv = cv2.cvtColor(canny, cv2.COLOR_GRAY2BGR)
    lower, upper = np.array([122,122,122]), np.array([124,124,124])
    # Threshold the HSV image to get only blue colors
    mask = cv2.inRange(hsv, lower, upper)
    @@ -44,12 +46,20 @@ def get_shards(img):
    if area > 987 and area < img.shape[0]*img.shape[1]-9000:
    cv2.drawContours(res, [contour], 0, (255,255,255), cv2.FILLED)
    unconnectedContours.append(contour)
    # Return the unconnected contours image and list of contours
    return res, unconnectedContours
    # Return the unconnected contours image and list of contours
    cv2.imshow('res', res)
    print(len(unconnectedContours), largest_contour(contours))
    cv2.waitKey()
    return res, unconnectedContours[0]

    # Distance between two points
    def distance(a,b): return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2)

    # Returns a single contour
    def largest_contour(contours):
    c = max(contours, key=cv2.contourArea)
    return c[0]

    # Draw a contour, but doesn't connect contour[0] with contour[-1]
    def draw_contour(img, contour, color, thick):
    for idx in range(len(contour)-1):
    @@ -79,21 +89,7 @@ def lines_mask(img):
    pt2 = (int(x0 - 1000*(-b)), int(y0 - 1000*(a)))
    cv2.line(mask, pt1, pt2, 255, 18, cv2.LINE_AA)
    #cv2.imshow('mask', mask)
    return mask

    # Read in images
    img1 = cv2.imread('/home/stephen/Desktop/paper6.jpg')
    img2 = cv2.imread('/home/stephen/Desktop/paper5.jpg')
    img_w, img_h = 780,1040
    img1 = cv2.resize(img1, (img_w, img_h))
    img2 = cv2.resize(img2, (img_w, img_h))
    # Find the lines in the image
    img1_lines = lines_mask(img1)
    img2_lines = lines_mask(img2)
    # Get shards of paper
    _, cnt1 = get_shards(img1)
    _, cnt2 = get_shards(img2)
    cnt1, cnt2 = cnt1[0], cnt2[0]
    return mask

    # Takes an image and a contour and returns the torn edge
    def find_torn_edge(img, cnt, img_lines):
    @@ -114,38 +110,6 @@ def find_torn_edge(img, cnt, img_lines):
    cv2.line(temp_human, a, b, 255, 14)
    return torn_edge, temp_human, temp

    # Find the torn edges
    torn_edge1, temp_human1, edge_mask1 = find_torn_edge(img1, cnt1, img1_lines)
    #cv2.imshow('img1', temp_human1)
    #cv2.waitKey()
    torn_edge2, temp_human2, edge_mask2 = find_torn_edge(img2, cnt2, img2_lines)
    #cv2.imshow('img1', temp_human2)
    #cv2.waitKey()

    # Plot
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg
    f, axarr = plt.subplots(2,4, sharex=True)
    axarr[0,0].imshow(img1)
    axarr[1,0].imshow(img2)
    outline1 = cv2.drawContours(img1.copy(), [cnt1], 0, (0,255,0), 15)
    outline2 = cv2.drawContours(img2.copy(), [cnt2], 0, (0,255,0), 15)
    img1_mask = np.zeros((img_h, img_w), np.uint8)
    img2_mask = np.zeros((img_h, img_w), np.uint8)
    img1_mask = cv2.drawContours(img1_mask, [cnt1], 0, 255, cv2.FILLED)
    img2_mask = cv2.drawContours(img2_mask, [cnt2], 0, 255, cv2.FILLED)
    axarr[0,1].imshow(outline1)
    axarr[1,1].imshow(outline2)
    axarr[0,2].imshow(img1_lines)
    axarr[1,2].imshow(img2_lines)
    axarr[0,3].imshow(temp_human1)
    axarr[1,3].imshow(temp_human2)
    axarr[0, 0].set_title('Source Images')
    axarr[0, 1].set_title('Paper Edges')
    axarr[0, 2].set_title('Hough Lines')
    axarr[0, 3].set_title('Torn Edge')
    plt.show()

    # Rotate a contour
    def rotate_contour(contour, edge_mask, wOverlay, hOverlay, rotation):
    hw = 2* max(wOverlay, hOverlay)
    @@ -201,14 +165,59 @@ def draw_background(best_match, img1, img2, img1_mask, img2_mask, offset):
    bg = bg[min(y_vals): max(y_vals), min(x_vals): max(x_vals)]
    bg = cv2.resize(bg, (987,987))
    return bg

    # Read in images
    img1 = cv2.imread('/home/stephen/Desktop/paper3.jpg')
    img2 = cv2.imread('/home/stephen/Desktop/paper4.jpg')
    img_w, img_h = 780,1040
    img1 = cv2.resize(img1, (img_w, img_h))
    img2 = cv2.resize(img2, (img_w, img_h))
    # Get shards of paper
    res1, cnt1 = get_shards(img1)
    res2, cnt2 = get_shards(img2)
    # Find the lines in the image
    img1_lines = lines_mask(res1)
    img2_lines = lines_mask(res2)
    # Find the torn edges
    torn_edge1, temp_human1, edge_mask1 = find_torn_edge(img1, cnt1, img1_lines)
    cv2.imshow('img1', temp_human1)
    cv2.waitKey()
    torn_edge2, temp_human2, edge_mask2 = find_torn_edge(img2, cnt2, img2_lines)
    #cv2.imshow('img1', temp_human2)
    #cv2.waitKey()

    # Plot
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg
    f, axarr = plt.subplots(2,4, sharex=True)
    axarr[0,0].imshow(img1)
    axarr[1,0].imshow(img2)
    outline1 = cv2.drawContours(img1.copy(), [cnt1], 0, (0,255,0), 15)
    outline2 = cv2.drawContours(img2.copy(), [cnt2], 0, (0,255,0), 15)
    img1_mask = np.zeros((img_h, img_w), np.uint8)
    img2_mask = np.zeros((img_h, img_w), np.uint8)
    img1_mask = cv2.drawContours(img1_mask, [cnt1], 0, 255, cv2.FILLED)
    img2_mask = cv2.drawContours(img2_mask, [cnt2], 0, 255, cv2.FILLED)
    axarr[0,1].imshow(outline1)
    axarr[1,1].imshow(outline2)
    axarr[0,2].imshow(img1_lines)
    axarr[1,2].imshow(img2_lines)
    axarr[0,3].imshow(temp_human1)
    axarr[1,3].imshow(temp_human2)
    axarr[0, 0].set_title('Source Images')
    axarr[0, 1].set_title('Paper Edges')
    axarr[0, 2].set_title('Hough Lines')
    axarr[0, 3].set_title('Torn Edge')
    plt.show()

    vid_writer = cv2.VideoWriter('/home/stephen/Desktop/re_encode.avi',cv2.VideoWriter_fourcc('M','J','P','G'),20, (987, 987))

    # Rotate the contour
    left, right = cnt1, cnt2
    match, matches, angle, best_match = 0, [-1], 0, (0,0,0,0)
    graph = np.zeros((987,987,3), np.uint8)

    while angle < 360:
    while angle < 380:
    # Copy the images and create temporary images
    img, overlay = img2.copy(), img1.copy()
    tempA = np.zeros((img.shape[0], img.shape[1]), np.uint8)
    @@ -223,7 +232,9 @@ def draw_background(best_match, img1, img2, img1_mask, img2_mask, offset):
    tempA = draw_contour(tempA, b, 123, 3)
    tempB = draw_contour(tempB, a, 123, 3)
    tempC = tempA + tempB
    cv2.imwrite('/home/stephen/Desktop/thresh.png', tempC/1.5)
    _, thresh = cv2.threshold(tempC, 220, 255, cv2.THRESH_BINARY_INV);

    thresh = 255 - thresh
    match = sum(sum(thresh))
    matches.append(match)
    @@ -235,7 +246,7 @@ def draw_background(best_match, img1, img2, img1_mask, img2_mask, offset):
    p2 = int(angle*2.35), int(sum(sum(thresh))/75)
    cv2.line(graph, p1, p2, (0,255,0), 2)

    bg = draw_background((_, angle, dx, dy), img1, img2, img1_mask, img2_mask, 123)
    bg = draw_background((_, angle, dx, dy), img1, img2, img1_mask, img2_mask, 0)
    bg += graph

    img = draw_contour(bg, b, (255,0,255), 2)
  5. smeschke revised this gist May 7, 2019. 1 changed file with 254 additions and 96 deletions.
    350 changes: 254 additions & 96 deletions align_scan.py
    Original file line number Diff line number Diff line change
    @@ -1,100 +1,258 @@
    import cv2
    import numpy as np

    out = cv2.VideoWriter('/home/stephen/Desktop/smooth_pose.avi',cv2.VideoWriter_fourcc('M','J','P','G'), 60, (640,640))

    src = 255 - cv2.imread('/home/stephen/Desktop/scan.jpg',0)
    scores = []

    def rotate(img, angle):
    rows,cols = img.shape
    M = cv2.getRotationMatrix2D((cols/2,rows/2),angle,1)
    dst = cv2.warpAffine(img,M,(cols,rows))
    return dst

    def sum_rows(img):
    # Create a list to store the row sums
    row_sums = []
    # Iterate through the rows
    for r in range(img.shape[0]-1):
    # Sum the row
    row_sum = sum(sum(img[r:r+1,:]))
    # Add the sum to the list
    row_sums.append(row_sum)
    # Normalize range to (0,255)
    row_sums = (row_sums/max(row_sums)) * 255
    # Return
    return row_sums

    def display_data(roi, row_sums, buffer):
    # Create background to draw transform on
    bg = np.zeros((buffer*2, buffer*2), np.uint8)
    # Iterate through the rows and draw on the background
    for row in range(roi.shape[0]-1):
    row_sum = row_sums[row]
    bg[row:row+1, :] = row_sum
    left_side = int(buffer/3)
    bg[:, left_side:] = roi[:,left_side:]
    cv2.imshow('bg1', bg)
    k = cv2.waitKey(1)
    out.write(cv2.cvtColor(cv2.resize(bg, (640,640)), cv2.COLOR_GRAY2BGR))
    return k

    # Rotate the image around in a circle
    angle = 0
    while angle <= 360:
    # Rotate the source image
    img = rotate(src, angle)
    # Crop the center 1/3rd of the image (roi is filled with text)
    h,w = img.shape
    buffer = min(h, w) - int(min(h,w)/1.5)
    roi = img[int(h/2-buffer):int(h/2+buffer), int(w/2-buffer):int(w/2+buffer)]
    # Create background to draw transform on
    bg = np.zeros((buffer*2, buffer*2), np.uint8)
    # Compute the sums of the rows
    row_sums = sum_rows(roi)
    # High score --> Zebra stripes
    score = np.count_nonzero(row_sums)
    scores.append(score)
    # Image has best rotation
    if score <= min(scores):
    # Save the rotatied image
    print('found optimal rotation')
    best_rotation = img.copy()
    k = display_data(roi, row_sums, buffer)
    import cv2, numpy as np, random, math

    # Find contour edges
    # Find the edge that is torn
    # use the hough line transform
    # create a mask image where the lines and white on a black background
    # check if the point is in a white or black region
    # Rotate the torn edges
    # Measure how much they overlap
    # The rotation with the maximum overlap will be how they should align
    # Align and display the images

    # Finds the shards of paper in an image
    def get_shards(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Gray
    #blurred = cv2.GaussianBlur(gray, (5, 5), 0) # Blur
    canny = cv2.Canny(gray, 30, 150) # Canny
    # Find contours
    contours, _ = cv2.findContours(canny,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    # Draw contours on canny (this connects the contours)
    cv2.drawContours(canny, contours, -1, 255, 2)
    _, thresh = cv2.threshold(canny, 230, 255, cv2.THRESH_BINARY_INV);
    # Get mask for floodfill
    h, w = thresh.shape[:2]
    mask = np.zeros((h+2, w+2), np.uint8)
    # Floodfill from point (0, 0)
    cv2.floodFill(thresh, mask, (0,0), 123);
    # Get an image that is only the gray floodfilled area
    hsv = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR)
    lower, upper = np.array([122,122,122]), np.array([124,124,124])
    # Threshold the HSV image to get only blue colors
    mask = cv2.inRange(hsv, lower, upper)
    # Bitwise-AND mask and original image
    res = cv2.bitwise_and(hsv,hsv, mask= mask)
    gray = 255 - cv2.cvtColor(res, cv2.COLOR_BGR2GRAY)
    _, thresh = cv2.threshold(gray, 220, 255, cv2.THRESH_BINARY_INV);
    contours, _ = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    res = np.zeros_like(res)
    # Create a list for unconnected contours
    unconnectedContours = []
    for contour in contours:
    area = cv2.contourArea(contour)
    # If the contour is not really small, or really big
    if area > 987 and area < img.shape[0]*img.shape[1]-9000:
    cv2.drawContours(res, [contour], 0, (255,255,255), cv2.FILLED)
    unconnectedContours.append(contour)
    # Return the unconnected contours image and list of contours
    return res, unconnectedContours

    # Distance between two points
    def distance(a,b): return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2)

    # Draw a contour, but doesn't connect contour[0] with contour[-1]
    def draw_contour(img, contour, color, thick):
    for idx in range(len(contour)-1):
    a, b = contour[idx], contour[idx+1]
    a,b = tuple(a[0]), tuple(b[0])
    if distance(a,b) < 321: cv2.line(img, a, b, color, thick)
    return img

    # Finds the lines in an image
    def lines_mask(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    dst = cv2.Canny(gray, 50, 200, None, 3)
    # Create mask image
    mask = np.zeros_like(gray)
    # Find the lines
    lines = cv2.HoughLines(dst, 1, np.pi / 180, 100, None, 0, 0)
    # Draw the lines
    if lines is not None:
    for i in range(0, len(lines)):
    rho = lines[i][0][0]
    theta = lines[i][0][1]
    a = math.cos(theta)
    b = math.sin(theta)
    x0 = a * rho
    y0 = b * rho
    pt1 = (int(x0 + 1000*(-b)), int(y0 + 1000*(a)))
    pt2 = (int(x0 - 1000*(-b)), int(y0 - 1000*(a)))
    cv2.line(mask, pt1, pt2, 255, 18, cv2.LINE_AA)
    #cv2.imshow('mask', mask)
    return mask

    # Read in images
    img1 = cv2.imread('/home/stephen/Desktop/paper6.jpg')
    img2 = cv2.imread('/home/stephen/Desktop/paper5.jpg')
    img_w, img_h = 780,1040
    img1 = cv2.resize(img1, (img_w, img_h))
    img2 = cv2.resize(img2, (img_w, img_h))
    # Find the lines in the image
    img1_lines = lines_mask(img1)
    img2_lines = lines_mask(img2)
    # Get shards of paper
    _, cnt1 = get_shards(img1)
    _, cnt2 = get_shards(img2)
    cnt1, cnt2 = cnt1[0], cnt2[0]

    # Takes an image and a contour and returns the torn edge
    def find_torn_edge(img, cnt, img_lines):
    # Create a temporary iamge
    img_h, img_w = img.shape[0], img.shape[1]
    temp = np.zeros((img_h, img_w), np.uint8)
    temp_human = np.zeros((img_h, img_w), np.uint8)
    cv2.drawContours(temp_human, [cnt], 0, 78, cv2.FILLED)
    torn_edge = []
    for i in range(len(cnt)):
    x,y = cnt[i][0]
    if img_lines[y,x] == 0: torn_edge.append((x,y))
    #cnt1 = np.array(torn_edge1)
    for i in range(len(torn_edge)-1):
    a = torn_edge[i]
    b = torn_edge[i+1]
    cv2.line(temp, a, b, 255, 2)
    cv2.line(temp_human, a, b, 255, 14)
    return torn_edge, temp_human, temp

    # Find the torn edges
    torn_edge1, temp_human1, edge_mask1 = find_torn_edge(img1, cnt1, img1_lines)
    #cv2.imshow('img1', temp_human1)
    #cv2.waitKey()
    torn_edge2, temp_human2, edge_mask2 = find_torn_edge(img2, cnt2, img2_lines)
    #cv2.imshow('img1', temp_human2)
    #cv2.waitKey()

    # Plot
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg
    f, axarr = plt.subplots(2,4, sharex=True)
    axarr[0,0].imshow(img1)
    axarr[1,0].imshow(img2)
    outline1 = cv2.drawContours(img1.copy(), [cnt1], 0, (0,255,0), 15)
    outline2 = cv2.drawContours(img2.copy(), [cnt2], 0, (0,255,0), 15)
    img1_mask = np.zeros((img_h, img_w), np.uint8)
    img2_mask = np.zeros((img_h, img_w), np.uint8)
    img1_mask = cv2.drawContours(img1_mask, [cnt1], 0, 255, cv2.FILLED)
    img2_mask = cv2.drawContours(img2_mask, [cnt2], 0, 255, cv2.FILLED)
    axarr[0,1].imshow(outline1)
    axarr[1,1].imshow(outline2)
    axarr[0,2].imshow(img1_lines)
    axarr[1,2].imshow(img2_lines)
    axarr[0,3].imshow(temp_human1)
    axarr[1,3].imshow(temp_human2)
    axarr[0, 0].set_title('Source Images')
    axarr[0, 1].set_title('Paper Edges')
    axarr[0, 2].set_title('Hough Lines')
    axarr[0, 3].set_title('Torn Edge')
    plt.show()

    # Rotate a contour
    def rotate_contour(contour, edge_mask, wOverlay, hOverlay, rotation):
    hw = 2* max(wOverlay, hOverlay)
    temp = np.zeros((hw,hw), np.uint8)
    #contour = contour + max(wOverlay, hOverlay)
    #temp = draw_contour(temp, contour, 255, 2)
    temp = edge_mask.copy()
    #cv2.imshow('temp', cv2.resize(temp, (456,456)))
    rows,cols = temp.shape
    M = cv2.getRotationMatrix2D((cols/2,rows/2),rotation,1)
    dst = cv2.warpAffine(temp,M,(cols,rows))
    _, thresh = cv2.threshold(dst, 123, 255, cv2.THRESH_BINARY)
    contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    return contours[0]

    # Translate a contour so that is located above another contour
    def align_translate(left, right):
    right_center, _ = cv2.minEnclosingCircle(right)
    left_center, _ = cv2.minEnclosingCircle(left)
    dx, dy = right_center[0]-left_center[0], right_center[1]-left_center[1]
    for i in range(len(right)):
    previous = right[i][0][0]
    right[i][0][0] = previous - dx
    previous = right[i][0][1]
    right[i][0][1] = previous - dy
    return left, right, dx, dy

    # Draws the output image
    def draw_background(best_match, img1, img2, img1_mask, img2_mask, offset):
    print(best_match)
    # Create a background for each of the images
    bgA = np.zeros((2345,2345,3), np.uint8)
    bgB = np.zeros((2345,2345,3), np.uint8)
    # Mask images for writing
    img1 = cv2.bitwise_and(img1, img1, mask = img1_mask)
    img2 = cv2.bitwise_and(img2, img2, mask = img2_mask)
    # Determine A buffer size
    _, rotation, dx, dy = best_match
    rotation, dx, dy = int(rotation), int(dx), int(dy)
    b = int(max(abs(dx), abs(dy))) + 10
    # Draw the first image on the background
    bgA[offset+b:offset+b+img_h, offset+b:offset+b+img_w] = img2
    # Rotate the second image
    M = cv2.getRotationMatrix2D((img_w/2,img_h/2),rotation,1)
    dst = cv2.warpAffine(img1,M,(img_w,img_h))
    # Translate it and paste it on the background
    bgB[offset+b-dy:offset+b-dy+img_h, offset+b-dx:offset+b-dx+img_w] = dst
    # Combine the backgrounds
    bg = bgA + bgB
    # Crop and resize
    x_vals = b-dx,b-dx+img_w, b,b+img_w
    y_vals = b, b+img_h, b-dy, b-dy+img_h
    bg = bg[min(y_vals): max(y_vals), min(x_vals): max(x_vals)]
    bg = cv2.resize(bg, (987,987))
    return bg
    vid_writer = cv2.VideoWriter('/home/stephen/Desktop/re_encode.avi',cv2.VideoWriter_fourcc('M','J','P','G'),20, (987, 987))

    # Rotate the contour
    left, right = cnt1, cnt2
    match, matches, angle, best_match = 0, [-1], 0, (0,0,0,0)
    graph = np.zeros((987,987,3), np.uint8)

    while angle < 360:
    # Copy the images and create temporary images
    img, overlay = img2.copy(), img1.copy()
    tempA = np.zeros((img.shape[0], img.shape[1]), np.uint8)
    tempB = np.zeros((img.shape[0], img.shape[1]), np.uint8)
    # Rotate the contour
    rotatedContour = rotate_contour(torn_edge1, edge_mask1, max(img.shape), max(img.shape), angle)
    # Clean left contour
    clean_left = rotate_contour(torn_edge2, edge_mask2, max(img.shape), max(img.shape), 0)
    # Translate the contour
    a,b, dx, dy = align_translate(clean_left, rotatedContour)
    # Draw the contour
    tempA = draw_contour(tempA, b, 123, 3)
    tempB = draw_contour(tempB, a, 123, 3)
    tempC = tempA + tempB
    _, thresh = cv2.threshold(tempC, 220, 255, cv2.THRESH_BINARY_INV);
    thresh = 255 - thresh
    match = sum(sum(thresh))
    matches.append(match)
    # Is this the best match?
    if match >= max(matches): best_match = b, angle, int(dx), int(dy)

    # Make the graph
    p1 = int(angle*2.35), 0
    p2 = int(angle*2.35), int(sum(sum(thresh))/75)
    cv2.line(graph, p1, p2, (0,255,0), 2)

    bg = draw_background((_, angle, dx, dy), img1, img2, img1_mask, img2_mask, 123)
    bg += graph

    img = draw_contour(bg, b, (255,0,255), 2)
    img = draw_contour(bg, a, (255,255,0), 2)
    img = draw_contour(bg, best_match[0], (0,255,255), 4)
    cv2.imshow('bg', bg)

    vid_writer.write(bg)
    k=cv2.waitKey(1)
    if k == 27: break
    # Increment angle and try again
    angle += .5
    angle += 1
    cv2.destroyAllWindows()

    def area_to_top_of_text(img):
    # Create a background to draw on
    bg = np.zeros_like(img)
    # Iterate through the rows
    for position in range(w-1):
    # Find the top value in the column
    column = np.array(img[:,position:position+1])
    top = np.argmax(column)
    # Fill in the area from the top of the page to top of the text
    a = position, 0
    b = position, top
    cv2.line(img, a, b, 123, 1)
    cv2.line(bg, a, b, 255, 1)
    # Show and return
    cv2.imshow('img', img)
    cv2.waitKey(0)
    return img, bg

    # Find the area from the top of page to top of image
    _, bg = area_to_top_of_text(best_rotation.copy())
    right_side_up = sum(sum(bg))
    # Flip image and try again
    best_rotation_flipped = rotate(best_rotation, 180)
    _, bg = area_to_top_of_text(best_rotation_flipped.copy())
    upside_down = sum(sum(bg))
    # Check which area is larger
    if right_side_up < upside_down: aligned_image = best_rotation
    else: aligned_image = best_rotation_flipped
    # Save aligned image
    cv2.imwrite('/home/stephen/Desktop/best_rotation.png', 255-aligned_image)
    # Show user the best match
    bg = draw_background(best_match, img1, img2, img1_mask, img2_mask, 0)
    cv2.imshow('img', bg)
    cv2.imwrite('/home/stephen/Desktop/paperReconstruction.png', bg)
    cv2.waitKey()
    cv2.destroyAllWindows()

  6. smeschke created this gist Apr 18, 2019.
    100 changes: 100 additions & 0 deletions align_scan.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,100 @@
    import cv2
    import numpy as np

    out = cv2.VideoWriter('/home/stephen/Desktop/smooth_pose.avi',cv2.VideoWriter_fourcc('M','J','P','G'), 60, (640,640))

    src = 255 - cv2.imread('/home/stephen/Desktop/scan.jpg',0)
    scores = []

    def rotate(img, angle):
    rows,cols = img.shape
    M = cv2.getRotationMatrix2D((cols/2,rows/2),angle,1)
    dst = cv2.warpAffine(img,M,(cols,rows))
    return dst

    def sum_rows(img):
    # Create a list to store the row sums
    row_sums = []
    # Iterate through the rows
    for r in range(img.shape[0]-1):
    # Sum the row
    row_sum = sum(sum(img[r:r+1,:]))
    # Add the sum to the list
    row_sums.append(row_sum)
    # Normalize range to (0,255)
    row_sums = (row_sums/max(row_sums)) * 255
    # Return
    return row_sums

    def display_data(roi, row_sums, buffer):
    # Create background to draw transform on
    bg = np.zeros((buffer*2, buffer*2), np.uint8)
    # Iterate through the rows and draw on the background
    for row in range(roi.shape[0]-1):
    row_sum = row_sums[row]
    bg[row:row+1, :] = row_sum
    left_side = int(buffer/3)
    bg[:, left_side:] = roi[:,left_side:]
    cv2.imshow('bg1', bg)
    k = cv2.waitKey(1)
    out.write(cv2.cvtColor(cv2.resize(bg, (640,640)), cv2.COLOR_GRAY2BGR))
    return k

    # Rotate the image around in a circle
    angle = 0
    while angle <= 360:
    # Rotate the source image
    img = rotate(src, angle)
    # Crop the center 1/3rd of the image (roi is filled with text)
    h,w = img.shape
    buffer = min(h, w) - int(min(h,w)/1.5)
    roi = img[int(h/2-buffer):int(h/2+buffer), int(w/2-buffer):int(w/2+buffer)]
    # Create background to draw transform on
    bg = np.zeros((buffer*2, buffer*2), np.uint8)
    # Compute the sums of the rows
    row_sums = sum_rows(roi)
    # High score --> Zebra stripes
    score = np.count_nonzero(row_sums)
    scores.append(score)
    # Image has best rotation
    if score <= min(scores):
    # Save the rotatied image
    print('found optimal rotation')
    best_rotation = img.copy()
    k = display_data(roi, row_sums, buffer)
    if k == 27: break
    # Increment angle and try again
    angle += .5
    cv2.destroyAllWindows()

    def area_to_top_of_text(img):
    # Create a background to draw on
    bg = np.zeros_like(img)
    # Iterate through the rows
    for position in range(w-1):
    # Find the top value in the column
    column = np.array(img[:,position:position+1])
    top = np.argmax(column)
    # Fill in the area from the top of the page to top of the text
    a = position, 0
    b = position, top
    cv2.line(img, a, b, 123, 1)
    cv2.line(bg, a, b, 255, 1)
    # Show and return
    cv2.imshow('img', img)
    cv2.waitKey(0)
    return img, bg

    # Find the area from the top of page to top of image
    _, bg = area_to_top_of_text(best_rotation.copy())
    right_side_up = sum(sum(bg))
    # Flip image and try again
    best_rotation_flipped = rotate(best_rotation, 180)
    _, bg = area_to_top_of_text(best_rotation_flipped.copy())
    upside_down = sum(sum(bg))
    # Check which area is larger
    if right_side_up < upside_down: aligned_image = best_rotation
    else: aligned_image = best_rotation_flipped
    # Save aligned image
    cv2.imwrite('/home/stephen/Desktop/best_rotation.png', 255-aligned_image)
    cv2.destroyAllWindows()