From 5f9ecca302c52125af73b9699a1bdfeb5ec1434a Mon Sep 17 00:00:00 2001 From: erik Date: Sun, 11 Oct 2026 22:55:29 +0000 Subject: [PATCH] Make only a scan's unscanned area transparent, not every white pixel The page view made every pure-white pixel transparent. The white point puts the paper's 97th percentile at 245, so the brightest paper clips to 255 and showed as dark speckles; opened images with a white background turned dark entirely; and the live preview (unscanned area already dark) only got the speckles. Now only fresh scans are masked, and only the white regions connected to the border, which is where the stitcher's uncovered area is after cropping. Co-Authored-By: Claude Opus 5.5 --- myscan/app.py | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) 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)