app.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import streamlit as st
  2. import pathlib
  3. from streamlit_drawable_canvas import st_canvas
  4. import cv2
  5. import numpy as np
  6. import io
  7. import base64
  8. from PIL import Image
  9. # We create a downloads directory within the streamlit static asset directory
  10. # and we write output files to it
  11. STREAMLIT_STATIC_PATH = pathlib.Path(st.__path__[0]) / 'static'
  12. DOWNLOADS_PATH = (STREAMLIT_STATIC_PATH / "downloads")
  13. if not DOWNLOADS_PATH.is_dir():
  14. DOWNLOADS_PATH.mkdir()
  15. def order_points(pts):
  16. '''Rearrange coordinates to order:
  17. top-left, top-right, bottom-right, bottom-left'''
  18. rect = np.zeros((4, 2), dtype='float32')
  19. pts = np.array(pts)
  20. s = pts.sum(axis=1)
  21. # Top-left point will have the smallest sum.
  22. rect[0] = pts[np.argmin(s)]
  23. # Bottom-right point will have the largest sum.
  24. rect[2] = pts[np.argmax(s)]
  25. diff = np.diff(pts, axis=1)
  26. # Top-right point will have the smallest difference.
  27. rect[1] = pts[np.argmin(diff)]
  28. # Bottom-left will have the largest difference.
  29. rect[3] = pts[np.argmax(diff)]
  30. # return the ordered coordinates
  31. return rect.astype('int').tolist()
  32. def find_dest(pts):
  33. (tl, tr, br, bl) = pts
  34. # Finding the maximum width.
  35. widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
  36. widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
  37. maxWidth = max(int(widthA), int(widthB))
  38. # Finding the maximum height.
  39. heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
  40. heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
  41. maxHeight = max(int(heightA), int(heightB))
  42. # Final destination co-ordinates.
  43. destination_corners = [[0, 0], [maxWidth, 0], [maxWidth, maxHeight], [0, maxHeight]]
  44. return order_points(destination_corners)
  45. def scan(img):
  46. # Resize image to workable size
  47. dim_limit = 1080
  48. max_dim = max(img.shape)
  49. if max_dim > dim_limit:
  50. resize_scale = dim_limit / max_dim
  51. img = cv2.resize(img, None, fx=resize_scale, fy=resize_scale)
  52. # Create a copy of resized original image for later use
  53. orig_img = img.copy()
  54. # Repeated Closing operation to remove text from the document.
  55. kernel = np.ones((5, 5), np.uint8)
  56. img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel, iterations=3)
  57. # GrabCut
  58. mask = np.zeros(img.shape[:2], np.uint8)
  59. bgdModel = np.zeros((1, 65), np.float64)
  60. fgdModel = np.zeros((1, 65), np.float64)
  61. rect = (20, 20, img.shape[1] - 20, img.shape[0] - 20)
  62. cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
  63. mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
  64. img = img * mask2[:, :, np.newaxis]
  65. gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
  66. gray = cv2.GaussianBlur(gray, (11, 11), 0)
  67. # Edge Detection.
  68. canny = cv2.Canny(gray, 0, 200)
  69. canny = cv2.dilate(canny, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)))
  70. # Finding contours for the detected edges.
  71. contours, hierarchy = cv2.findContours(canny, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
  72. # Keeping only the largest detected contour.
  73. page = sorted(contours, key=cv2.contourArea, reverse=True)[:5]
  74. # Detecting Edges through Contour approximation.
  75. # Loop over the contours.
  76. if len(page) == 0:
  77. return orig_img
  78. for c in page:
  79. # Approximate the contour.
  80. epsilon = 0.02 * cv2.arcLength(c, True)
  81. corners = cv2.approxPolyDP(c, epsilon, True)
  82. # If our approximated contour has four points.
  83. if len(corners) == 4:
  84. break
  85. # Sorting the corners and converting them to desired shape.
  86. corners = sorted(np.concatenate(corners).tolist())
  87. # For 4 corner points being detected.
  88. corners = order_points(corners)
  89. destination_corners = find_dest(corners)
  90. # Getting the homography.
  91. M = cv2.getPerspectiveTransform(np.float32(corners), np.float32(destination_corners))
  92. # Perspective transform using homography.
  93. final = cv2.warpPerspective(orig_img, M, (destination_corners[2][0], destination_corners[2][1]),
  94. flags=cv2.INTER_LINEAR)
  95. return final
  96. # Generating a link to download a particular image file.
  97. def get_image_download_link(img, filename, text):
  98. buffered = io.BytesIO()
  99. img.save(buffered, format='JPEG')
  100. img_str = base64.b64encode(buffered.getvalue()).decode()
  101. href = f'<a href="data:file/txt;base64,{img_str}" download="{filename}">{text}</a>'
  102. return href
  103. # Set title.
  104. st.sidebar.title('Document Scanner')
  105. # Specify canvas parameters in application
  106. uploaded_file = st.sidebar.file_uploader("Upload Image of Document:", type=["png", "jpg"])
  107. image = None
  108. final = None
  109. col1, col2 = st.columns(2)
  110. if uploaded_file is not None:
  111. # Convert the file to an opencv image.
  112. file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)
  113. image = cv2.imdecode(file_bytes, 1)
  114. manual = st.sidebar.checkbox('Adjust Manually', False)
  115. h, w = image.shape[:2]
  116. h_, w_ = int(h * 400 / w), 400
  117. if manual:
  118. st.subheader('Select the 4 corners')
  119. st.markdown('### Double-Click to reset last point, Right-Click to select')
  120. # Create a canvas component
  121. canvas_result = st_canvas(
  122. fill_color="rgba(255, 165, 0, 0.3)", # Fixed fill color with some opacity
  123. stroke_width=3,
  124. background_image=Image.open(uploaded_file).resize((h_, w_)),
  125. update_streamlit=True,
  126. height=h_,
  127. width=w_,
  128. drawing_mode='polygon',
  129. key="canvas",
  130. )
  131. st.sidebar.caption('Happy with the manual selection?')
  132. if st.sidebar.button('Get Scanned'):
  133. # Do something interesting with the image data and paths
  134. points = order_points([i[1:3] for i in canvas_result.json_data['objects'][0]['path'][:4]])
  135. points = np.multiply(points, w / 400)
  136. dest = find_dest(points)
  137. # Getting the homography.
  138. M = cv2.getPerspectiveTransform(np.float32(points), np.float32(dest))
  139. # Perspective transform using homography.
  140. final = cv2.warpPerspective(image, M, (dest[2][0], dest[2][1]), flags=cv2.INTER_LINEAR)
  141. st.image(final, channels='BGR', use_column_width=True)
  142. else:
  143. with col1:
  144. st.title('Input')
  145. st.image(image, channels='BGR', use_column_width=True)
  146. with col2:
  147. st.title('Scanned')
  148. final = scan(image)
  149. st.image(final, channels='BGR', use_column_width=True)
  150. if final is not None:
  151. # Display link.
  152. result = Image.fromarray(final[:, :, ::-1])
  153. st.sidebar.markdown(get_image_download_link(result, 'output.png', 'Download ' + 'Output'),
  154. unsafe_allow_html=True)