|
|
@@ -218,6 +218,50 @@ def crop_to_content(image: np.ndarray, border_px: int = DEFAULT_BORDER_PX) -> np
|
|
218
|
218
|
return image[y1_crop:y2_crop, x1_crop:x2_crop]
|
|
219
|
219
|
|
|
220
|
220
|
|
|
|
221
|
+def _order_points(pts: np.ndarray) -> np.ndarray:
|
|
|
222
|
+ rect = np.zeros((4, 2), dtype=np.float32)
|
|
|
223
|
+ s = pts.sum(axis=1)
|
|
|
224
|
+ rect[0] = pts[np.argmin(s)]
|
|
|
225
|
+ rect[2] = pts[np.argmax(s)]
|
|
|
226
|
+ diff = np.diff(pts, axis=1)
|
|
|
227
|
+ rect[1] = pts[np.argmin(diff)]
|
|
|
228
|
+ rect[3] = pts[np.argmax(diff)]
|
|
|
229
|
+ return rect
|
|
|
230
|
+
|
|
|
231
|
+
|
|
|
232
|
+def _apply_perspective(image: np.ndarray, rect: np.ndarray) -> np.ndarray:
|
|
|
233
|
+ (tl, tr, br, bl) = rect
|
|
|
234
|
+ max_w = int(max(np.linalg.norm(br - bl), np.linalg.norm(tr - tl)))
|
|
|
235
|
+ max_h = int(max(np.linalg.norm(tr - br), np.linalg.norm(tl - bl)))
|
|
|
236
|
+ dst = np.array([[0, 0], [max_w - 1, 0], [max_w - 1, max_h - 1], [0, max_h - 1]], dtype=np.float32)
|
|
|
237
|
+ mtx = cv2.getPerspectiveTransform(rect, dst)
|
|
|
238
|
+ return cv2.warpPerspective(image, mtx, (max_w, max_h), flags=cv2.INTER_CUBIC)
|
|
|
239
|
+
|
|
|
240
|
+
|
|
|
241
|
+def unwarp_image(image: np.ndarray) -> np.ndarray:
|
|
|
242
|
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
|
243
|
+ blurred = cv2.GaussianBlur(gray, (5, 5), 0)
|
|
|
244
|
+ edges = cv2.Canny(blurred, 50, 150)
|
|
|
245
|
+ contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
|
246
|
+ if not contours:
|
|
|
247
|
+ return image
|
|
|
248
|
+
|
|
|
249
|
+ h, w = image.shape[:2]
|
|
|
250
|
+ min_area = w * h * 0.3
|
|
|
251
|
+
|
|
|
252
|
+ contours = sorted(contours, key=cv2.contourArea, reverse=True)
|
|
|
253
|
+ for cnt in contours[:10]:
|
|
|
254
|
+ area = cv2.contourArea(cnt)
|
|
|
255
|
+ if area < min_area:
|
|
|
256
|
+ continue
|
|
|
257
|
+ peri = cv2.arcLength(cnt, True)
|
|
|
258
|
+ approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
|
|
|
259
|
+ if len(approx) == 4:
|
|
|
260
|
+ rect = _order_points(approx.reshape(4, 2))
|
|
|
261
|
+ return _apply_perspective(image, rect)
|
|
|
262
|
+ return image
|
|
|
263
|
+
|
|
|
264
|
+
|
|
221
|
265
|
def process_image(
|
|
222
|
266
|
input_path: Path,
|
|
223
|
267
|
rows: int | None = None,
|
|
|
@@ -226,12 +270,16 @@ def process_image(
|
|
226
|
270
|
pre_rotate: int | None = None,
|
|
227
|
271
|
border_px: int = DEFAULT_BORDER_PX,
|
|
228
|
272
|
post_crop: bool = False,
|
|
|
273
|
+ unwarp: bool = False,
|
|
229
|
274
|
) -> list[Path]:
|
|
230
|
275
|
image = load_image(input_path)
|
|
231
|
276
|
|
|
232
|
277
|
if pre_rotate is not None:
|
|
233
|
278
|
image = rotate_image(image, pre_rotate)
|
|
234
|
279
|
|
|
|
280
|
+ if unwarp:
|
|
|
281
|
+ image = unwarp_image(image)
|
|
|
282
|
+
|
|
235
|
283
|
if rows is None or cols is None:
|
|
236
|
284
|
cols, rows = detect_grid(image)
|
|
237
|
285
|
|