diff --git a/myscan/app.py b/myscan/app.py index fe4f1b5..4af5574 100644 --- a/myscan/app.py +++ b/myscan/app.py @@ -30,22 +30,30 @@ from .ocr import Ocr, layout, paragraph_joins, reflow, text_direction IMAGE_FILTER = "Images (*.png *.jpg *.jpeg *.tif *.tiff *.bmp *.webp)" -def to_qimage(img, page=False): - """BGR or grayscale uint8 array -> QImage (a copy). With `page`, exact-white pixels - (the uncovered corners of a stitched page) become transparent, so when the view - rotates the page to level it those corners show the window background through.""" +def to_qimage(img, transparent=None): + """BGR or grayscale uint8 array -> QImage (a copy); pixels where the bool mask + `transparent` is set get alpha 0.""" img = np.ascontiguousarray(img) h, w = img.shape[:2] - if not page: + if transparent is None: fmt = QImage.Format_Grayscale8 if img.ndim == 2 else QImage.Format_BGR888 return QImage(img.data, w, h, img.strides[0], fmt).copy() bgra = cv2.cvtColor(img, cv2.COLOR_GRAY2BGRA if img.ndim == 2 else cv2.COLOR_BGR2BGRA) - uncovered = (img == 255).all(axis=-1) if img.ndim == 3 else (img == 255) - bgra[uncovered, 3] = 0 - bgra = np.ascontiguousarray(bgra) + bgra[transparent, 3] = 0 return QImage(bgra.data, w, h, bgra.strides[0], QImage.Format_ARGB32).copy() +def unscanned(img): + """Mask of a stitched page's unscanned area: the pure-white regions touching the + border (the stitcher fills uncovered pixels with 255 and crops to the scanned area). + Paper that clips to 255 inside the page is not connected to the border and stays.""" + white = ((img == 255).all(axis=-1) if img.ndim == 3 else img == 255).astype(np.uint8) + _, labels = cv2.connectedComponents(white, connectivity=4) + edge = np.concatenate([labels[0], labels[-1], labels[:, 0], labels[:, -1]]) + edge = np.unique(edge[edge > 0]) + return np.isin(labels, edge) if len(edge) else None + + def imread(path): """cv2.imread that also takes non-ASCII paths on Windows.""" return cv2.imdecode(np.fromfile(path, np.uint8), cv2.IMREAD_COLOR) @@ -98,7 +106,7 @@ class Bridge(QObject): self.start_event, self.stop_event = threading.Event(), threading.Event() def publish(self, name, bgr): - (self.frame if name == "frame" else self.page).emit(to_qimage(bgr, page=(name == "page"))) + (self.frame if name == "frame" else self.page).emit(to_qimage(bgr)) def set_status(self, **kw): self.state = {**self.state, **kw} @@ -567,6 +575,7 @@ class MainWindow(QMainWindow): self.opts = opts self.ocr = Ocr() self.image, self.path, self.lines, self.text = None, None, [], "" + self.blank = None # unscanned-area mask of the shown page (set_page) self.page_title = "Page" self.ocr_thread, self.scan_thread, self.scanning = None, None, False self.page_id = 0 # bumped for every new page, so a slow OCR of an old one is not applied @@ -720,11 +729,11 @@ class MainWindow(QMainWindow): def on_result(self, path): self.scanning = False - self.load(path) + self.load(path, scanned=True) # ---- images ---- - def load(self, path): + def load(self, path, scanned=False): try: img = imread(path) except OSError: @@ -733,15 +742,18 @@ class MainWindow(QMainWindow): log_error(f"cannot read {path}") QMessageBox.warning(self, "myScan", f"cannot read {path}") return - self.set_page(img, path) + self.set_page(img, path, scanned) self.run_ocr() - def set_page(self, img, path=None): + def set_page(self, img, path=None, scanned=False): + """`scanned`: a fresh stitched page, whose unscanned corners are shown transparent + (opened images are shown as they are: their white may be paper).""" self.image, self.path, self.lines, self.text = img, path, [], "" + self.blank = unscanned(img) if scanned else None self.page_id += 1 self.view.set_lines([]) self.view.angle = 0.0 - self.view.set_image(to_qimage(img, page=True)) + self.view.set_image(to_qimage(img, self.blank)) name = os.path.basename(path) if path else "page" self.page_title = f"Page — {name} ({img.shape[1]}×{img.shape[0]})" self.page_head.setText(self.page_title) @@ -808,7 +820,9 @@ class MainWindow(QMainWindow): return if flipped: # the boxes are on the page turned the right way up self.image = np.ascontiguousarray(self.image[::-1, ::-1]) - self.view.set_image(to_qimage(self.image, page=True)) + if self.blank is not None: + self.blank = self.blank[::-1, ::-1] + self.view.set_image(to_qimage(self.image, self.blank)) self.lines = lines self.text, spans = layout(lines) self.view.set_lines(lines, self.text, spans)