乐于分享
好东西不私藏

Python 文档漂白工具实战V2.0自调整版

Python 文档漂白工具实战V2.0自调整版

📄 拍照文档秒变打印级清晰 — Python 文档漂白工具实战

一行代码去掉拍照文档的灰色底色,打印效果堪比原版扫描件。


你有没有遇到过这种情况:急需打印一份文件,手边没有扫描仪,只能用手机拍一张照片。结果打印出来一看——整张纸灰蒙蒙的,文字和背景混在一起,根本看不清。

或者老师发了一份试卷的照片到群里,你想打印给孩子做,结果打出来黑乎乎一片,墨都浪费了,效果还不如手抄。

再比如你在图书馆拍了几页书,想打印出来慢慢看,但拍照时光线不均匀,有的地方亮有的地方暗,直接打印简直是灾难。

这些问题的本质都是一样的:拍照文档有灰色背景底色,需要”漂白”成纯白底+黑字,才能打印清晰。

今天这篇文章,我用 Python + OpenCV 写了一个「文档漂白打印工具」,PyQt5 桌面版,上传一张拍照文档,调几个参数,点一下渲染,背景瞬间变白,文字清晰锐利,直接打印或保存。

核心原理就一个:自适应阈值二值化。听起来高大上,其实就是让电脑自动判断每个像素是”文字”还是”背景”,文字保留黑色,背景全部变白。

工具特点:3 种漂白方法可选、块大小/阈值/亮度/对比度全部可调、调完参数点「渲染预览」实时看效果、支持保存和打印预览。依赖只有 PyQt5 + opencv-python + Pillow + numpy,pip 装完就能跑。

接下来我带你拆解核心代码,看看这个工具是怎么实现的。


🔧 第一部分:漂白核心算法 — 自适应阈值二值化

这段代码做了什么

这是整个工具的灵魂函数。接收一张图片路径和各种参数,输出漂白后的黑白图。支持三种方法:自适应阈值(推荐)、大津法、简单阈值。

核心代码

defbleach_image(img_path, method="adaptive", block_size=15, c_val=10,                 brightness=0, contrast=0, denoise=False):# 用 np.fromfile + imdecode 支持中文路径    data = np.fromfile(img_path, dtype=np.uint8)    img = cv2.imdecode(data, cv2.IMREAD_COLOR)if img isNone:returnNone# 转灰度    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 亮度/对比度调整if brightness != 0or contrast != 0:        alpha = 1.0 + contrast / 100.0# 对比度系数        beta = brightness                # 亮度偏移        gray = cv2.convertScaleAbs(gray, alpha=alpha, beta=beta)# 降噪(可选,较慢)if denoise:        gray = cv2.fastNlMeansDenoising(gray, h=10)# 二值化:核心步骤if method == "adaptive":        bs = block_size if block_size % 2 == 1else block_size + 1        result = cv2.adaptiveThreshold(            gray, 255,            cv2.ADAPTIVE_THRESH_GAUSSIAN_C,  # 高斯加权均值            cv2.THRESH_BINARY,                # 二值化            bs,                                # 块大小            c_val                              # 阈值常数        )elif method == "otsu":        _, result = cv2.threshold(gray, 0255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)else:        _, result = cv2.threshold(gray, 127255, cv2.THRESH_BINARY)return result

逐行解析

步骤
代码
说明
读取图片
np.fromfile

 + cv2.imdecode
支持中文路径(cv2.imread 不支持)
转灰度
cv2.cvtColor(BGR2GRAY)
彩色转单通道灰度
亮度对比度
convertScaleAbs(alpha, beta)
alpha 控制对比度,beta 控制亮度
自适应阈值
adaptiveThreshold
每个像素根据周围 block_size 区域的均值决定黑白
大津法
THRESH_OTSU
自动计算全局最优阈值

效果

  • 输入:灰色背景 + 黑色文字的拍照文档
  • 输出:纯白背景 + 黑色文字的二值图,打印效果清晰

🔧 第二部分:渲染预览机制 — 调参后点按钮才处理

这段代码做了什么

滑块调参时只更新数值显示,不触发图像处理。用户调好所有参数后,点击「🎨 渲染预览」按钮才执行漂白,避免每拖一下滑块就重新处理导致卡顿。

核心代码

# 滑块只更新数值,不触发处理self.sl_block.valueChanged.connect(lambda v: self.lbl_block.setText(str(v if v % 2 == 1else v + 1)))self.sl_c.valueChanged.connect(lambda v: self.lbl_c.setText(str(v)))# 渲染按钮:点击才执行漂白处理btn_render = QPushButton("🎨 渲染预览")btn_render.setStyleSheet("padding:10px;background:#ff9800;color:#fff;""border:none;border-radius:4px;font-size:13px;font-weight:bold;")btn_render.clicked.connect(self._process)

设计思路

方案
体验
性能
滑块实时触发
拖动即见效果
大图卡顿严重
按钮手动触发
调好参数再渲染
流畅不卡

选择按钮触发方案,用户可以一次性调好块大小、阈值、亮度、对比度,然后一键渲染,体验更好。


🔧 第三部分:打印预览与输出

这段代码做了什么

使用 PyQt5 的 QPrintPreviewDialog 实现打印预览,将漂白后的图片按页面大小自适应缩放,居中输出。

核心代码

def_print(self):if self.result_img isNone:        QMessageBox.warning(self, "提示""请先上传并处理")return    pix = cv2_to_qpixmap(self.result_img)    dlg = QPrintPreviewDialog()    dlg.paintRequested.connect(lambda printer: self._do_print(printer, pix))    dlg.exec_()def_do_print(self, printer, pix):    p = QPainter(printer)    rect = p.viewport()# 按页面大小等比缩放    scaled = pix.scaled(rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)# 水平居中    p.drawPixmap((rect.width() - scaled.width()) // 20, scaled)    p.end()

逐行解析

  1. QPrintPreviewDialog — Qt 内置打印预览对话框,带缩放、翻页、打印机选择
  2. paintRequested 信号 — 预览窗口需要绘制内容时触发
  3. pix.scaled(KeepAspectRatio) — 保持比例缩放到页面大小
  4. drawPixmap 居中绘制 — 水平居中,顶部对齐

📦 完整代码

完整可运行代码见文件 doc_bleach.py(已脱敏),核心结构:

doc_bleach.py+-- bleach_image()           # 漂白核心(自适应阈值/大津法/简单阈值)+-- cv2_to_qpixmap()         # OpenCV图 -> Qt显示+-- ImageLabel               # 自适应缩放图片控件+-- BleachTab                # Tab1: 漂白处理(上传+参数+渲染+保存)+-- PrintTab                 # Tab2: 打印设置(纸张/边距/预览)+-- AboutTab                 # Tab3: 关于信息+-- DocBleachApp             # 主窗口

运行方式

pip install PyQt5 opencv-python Pillow numpypython doc_bleach.py

📚 知识点总结

知识点
说明
应用场景
自适应阈值
每个像素根据局部区域均值决定黑白
光照不均匀的拍照文档漂白
大津法(Otsu)
自动计算全局最优二值化阈值
光照均匀的扫描件处理
np.fromfile + imdecode
绕过 cv2.imread 不支持中文路径的问题
任何需要读取中文路径图片的场景
convertScaleAbs
alpha*像素+beta 调整亮度对比度
图像增强预处理
QPrintPreviewDialog
Qt 内置打印预览
桌面应用打印功能
QThread 子线程
耗时操作不阻塞GUI
任何需要后台处理的桌面应用
GaussianBlur
高斯模糊平滑边缘
去噪、边缘平滑

🚀 拓展场景

  1. 批量漂白:添加文件夹批量导入,一次性处理几十张拍照文档
  2. OCR 预处理:漂白后的图片作为 OCR 文字识别的输入,识别率大幅提升
  3. 试卷去答案:结合颜色过滤,去掉红色/蓝色笔迹,只保留黑色印刷体
  4. 老照片修复:调整亮度对比度 + 降噪,修复泛黄老照片
  5. 电子书制作:拍照书页漂白后合并为 PDF,制作电子书

🧪 测试步骤

  1. 安装依赖:pip install PyQt5 opencv-python Pillow numpy
  2. 启动程序:python doc_bleach.py
  3. 点击「📂 上传图片/文档」,选择一张手机拍的文档照片
  4. 确认左侧显示原图(灰色背景)
  5. 保持默认参数(自适应阈值,块大小15,C值10),点击「🎨 渲染预览」
  6. 确认右侧显示漂白效果(白色背景,文字清晰)
  7. 拖动「块大小」滑块到 25,点击「渲染预览」,观察效果变化(更平滑)
  8. 拖动「阈值C」到 5,渲染,观察文字变细
  9. 切换方法为「大津法」,渲染,对比效果
  10. 点击「💾 保存图片」,保存为 PNG
  11. 点击「🖨 打印」,确认打印预览中图片清晰、居中
  12. 异常测试:上传非图片文件,确认提示”抠图失败”而非崩溃

关于作者

👤 作者:杨一凡 📱 公众号:Python学在坚持💬 微信:ysp2338084

关注公众号,获取更多 Python 实战教程和源码!


如果觉得有帮助,请点赞、在看、转发三连,你的支持是我持续创作的动力!

-- coding: utf-8 --"""文档漂白打印工具 v1.0 - PyQt5 + OpenCV功能:上传拍照文档/图片 -> 去除背景底色 -> 漂白处理 -> 打印预览 -> 导出作者:杨少平 | 公众号:Python学在坚持运行:python doc_bleach.py依赖:pip install PyQt5 opencv-python Pillow numpy"""import sys, osimport cv2import numpy as npfrom PIL import Imagefrom PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QPushButton, QSlider, QComboBox, QTabWidget, QGroupBox,QFileDialog, QMessageBox, QFrame, QCheckBox, QSpinBox, QSplitter)from PyQt5.QtCore import Qtfrom PyQt5.QtGui import QFont, QColor, QPixmap, QImage, QPainterfrom PyQt5.QtPrintSupport import QPrinter, QPrintDialog, QPrintPreviewDialogAPP_VERSION = "v1.0"==================== 漂白引擎 ====================def bleach_image(img_path, method="adaptive", block_size=15, c_val=10, brightness=0, contrast=0, denoise=False):"""漂白处理核心method: adaptive(自适应阈值) / otsu(大津法) / simple(简单阈值)block_size: 自适应阈值的块大小(奇数)c_val: 自适应阈值的常数Cbrightness: 亮度调整 -100~100contrast: 对比度调整 -100~100"""# 用 np.fromfile + imdecode 支持中文路径data = np.fromfile(img_path, dtype=np.uint8)img = cv2.imdecode(data, cv2.IMREAD_COLOR)if img is None:return None# 转灰度gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 亮度/对比度调整if brightness != 0 or contrast != 0:alpha = 1.0 + contrast / 100.0beta = brightnessgray = cv2.convertScaleAbs(gray, alpha=alpha, beta=beta)# 降噪if denoise:gray = cv2.fastNlMeansDenoising(gray, h=10)# 二值化if method == "adaptive":bs = block_size if block_size % 2 == 1 else block_size + 1bs = max(3, bs)result = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, bs, c_val)elif method == "otsu":_, result = cv2.threshold(gray, 0255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)else:_, result = cv2.threshold(gray, 127255, cv2.THRESH_BINARY)return resultdef cv2_to_qpixmap(cv_img):"""OpenCV灰度/BGR图 -> QPixmap"""if len(cv_img.shape) == 2:h, w = cv_img.shapeqi = QImage(cv_img.data, w, h, w, QImage.Format_Grayscale8)else:h, w, ch = cv_img.shapergb = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)qi = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888)return QPixmap.fromImage(qi)class ImageLabel(QLabel):"""带自适应缩放的图片显示"""def init(self, text=""):super().init()self.setAlignment(Qt.AlignCenter)self.setMinimumSize(200260)self.setStyleSheet("background:#f0f0f0;border:1px solid #ddd;border-radius:4px;")self._pix = Noneself._text = textdef set_pixmap(self, pix):    self._pix = pix    self.update()def paintEvent(self, e):    p = QPainter(self)    p.fillRect(self.rect(), QColor(245245245))    if self._pix:        s = self._pix.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)        p.drawPixmap((self.width() - s.width()) // 2, (self.height() - s.height()) // 2, s)    else:        p.setPen(QColor(160160160))        p.setFont(QFont("Microsoft YaHei"12))        p.drawText(self.rect(), Qt.AlignCenter, self._text or "上传图片")    p.end()==================== Tab1: 漂白处理 ====================class BleachTab(QWidget):def init(self):super().init()self.img_path = ""self.result_img = None  # cv2 灰度图self._build()def _build(self):    root = QHBoxLayout(self)    root.setContentsMargins(8888)    root.setSpacing(8)    # 左侧控制    left = QWidget()    left.setMaximumWidth(250)    ll = QVBoxLayout(left)    ll.setContentsMargins(0000)    ll.setSpacing(6)    btn_up = QPushButton("📂 上传图片/文档")    btn_up.setStyleSheet("padding:10px;background:#1976d2;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:bold;")    btn_up.clicked.connect(self._upload)    ll.addWidget(btn_up)    # 方法    g1 = QGroupBox("漂白方法")    g1l = QVBoxLayout()    self.cb_method = QComboBox()    self.cb_method.addItems(["自适应阈值(推荐)""大津法(Otsu)""简单阈值"])    self.cb_method.currentIndexChanged.connect(self._process)    g1l.addWidget(self.cb_method)    g1.setLayout(g1l)    ll.addWidget(g1)    # 参数    g2 = QGroupBox("参数调节")    g2l = QVBoxLayout()    g2l.addWidget(QLabel("块大小(奇数,越大越平滑):"))    self.sl_block = QSlider(Qt.Horizontal)    self.sl_block.setRange(351)    self.sl_block.setValue(15)    self.sl_block.setSingleStep(2)    self.lbl_block = QLabel("15")    row1 = QHBoxLayout()    row1.addWidget(self.sl_block, 1)    row1.addWidget(self.lbl_block)    self.sl_block.valueChanged.connect(lambda v: self.lbl_block.setText(str(v if v % 2 == 1 else v + 1)))    g2l.addLayout(row1)    g2l.addWidget(QLabel("阈值常数C(越大文字越粗):"))    self.sl_c = QSlider(Qt.Horizontal)    self.sl_c.setRange(130)    self.sl_c.setValue(10)    self.lbl_c = QLabel("10")    row2 = QHBoxLayout()    row2.addWidget(self.sl_c, 1)    row2.addWidget(self.lbl_c)    self.sl_c.valueChanged.connect(lambda v: self.lbl_c.setText(str(v)))    g2l.addLayout(row2)    g2l.addWidget(QLabel("亮度:"))    self.sl_bright = QSlider(Qt.Horizontal)    self.sl_bright.setRange(-100100)    self.sl_bright.setValue(0)    self.lbl_bright = QLabel("0")    row3 = QHBoxLayout()    row3.addWidget(self.sl_bright, 1)    row3.addWidget(self.lbl_bright)    self.sl_bright.valueChanged.connect(lambda v: self.lbl_bright.setText(str(v)))    g2l.addLayout(row3)    g2l.addWidget(QLabel("对比度:"))    self.sl_contrast = QSlider(Qt.Horizontal)    self.sl_contrast.setRange(-100100)    self.sl_contrast.setValue(0)    self.lbl_contrast = QLabel("0")    row4 = QHBoxLayout()    row4.addWidget(self.sl_contrast, 1)    row4.addWidget(self.lbl_contrast)    self.sl_contrast.valueChanged.connect(lambda v: self.lbl_contrast.setText(str(v)))    g2l.addLayout(row4)    self.chk_denoise = QCheckBox("降噪(较慢)")    g2l.addWidget(self.chk_denoise)    btn_render = QPushButton("🎨 渲染预览")    btn_render.setStyleSheet("padding:10px;background:#ff9800;color:#fff;border:none;border-radius:4px;font-size:13px;font-weight:bold;")    btn_render.clicked.connect(self._process)    g2l.addWidget(btn_render)    btn_reset = QPushButton("🔄 重置参数")    btn_reset.clicked.connect(self._reset)    g2l.addWidget(btn_reset)    g2.setLayout(g2l)    ll.addWidget(g2)    # 导出    g3 = QGroupBox("操作")    g3l = QVBoxLayout()    btn_save = QPushButton("💾 保存图片")    btn_save.setStyleSheet("padding:8px;background:#388e3c;color:#fff;border:none;border-radius:4px;")    btn_save.clicked.connect(self._save)    g3l.addWidget(btn_save)    btn_print = QPushButton("🖨 打印")    btn_print.setStyleSheet("padding:8px;background:#d32f2f;color:#fff;border:none;border-radius:4px;")    btn_print.clicked.connect(self._print)    g3l.addWidget(btn_print)    g3.setLayout(g3l)    ll.addWidget(g3)    self.lbl_st = QLabel("就绪")    self.lbl_st.setStyleSheet("color:#888;font-size:11px;")    ll.addWidget(self.lbl_st)    ll.addStretch()    root.addWidget(left)    # 右侧预览    right = QWidget()    rl = QVBoxLayout(right)    rl.setContentsMargins(0000)    rl.setSpacing(4)    labels = QHBoxLayout()    labels.addWidget(QLabel("📷 原图"))    labels.addWidget(QLabel("✅ 漂白效果"))    rl.addLayout(labels)    imgs = QHBoxLayout()    self.pv_orig = ImageLabel("上传图片/文档照片")    self.pv_result = ImageLabel("漂白结果")    self.pv_result.setStyleSheet("background:#fff;border:1px solid #ddd;border-radius:4px;")    imgs.addWidget(self.pv_orig)    imgs.addWidget(self.pv_result)    rl.addLayout(imgs, 1)    root.addWidget(right, 1)def _upload(self):    p, _ = QFileDialog.getOpenFileName(self"选择图片""""图片 (*.png *.jpg *.jpeg *.bmp *.tiff *.webp);;所有 (*)")    if not p: return    self.img_path = p    data = np.fromfile(p, dtype=np.uint8)    img = cv2.imdecode(data, cv2.IMREAD_COLOR)    if img is not None:        self.pv_orig.set_pixmap(cv2_to_qpixmap(img))    self.lbl_st.setText("✅ 已加载,调整参数后点击「渲染预览」")def _get_method(self):    idx = self.cb_method.currentIndex()    return ["adaptive""otsu""simple"][idx]def _process(self):    if not self.img_path: return    bs = self.sl_block.value()    if bs % 2 == 0: bs += 1    result = bleach_image(        self.img_path,        method=self._get_method(),        block_size=bs,        c_val=self.sl_c.value(),        brightness=self.sl_bright.value(),        contrast=self.sl_contrast.value(),        denoise=self.chk_denoise.isChecked(),    )    if result is not None:        self.result_img = result        self.pv_result.set_pixmap(cv2_to_qpixmap(result))        self.lbl_st.setText(f"✅ 已处理 | {result.shape[1]}x{result.shape[0]}")def _reset(self):    self.sl_block.setValue(15)    self.sl_c.setValue(10)    self.sl_bright.setValue(0)    self.sl_contrast.setValue(0)    self.chk_denoise.setChecked(False)    self.cb_method.setCurrentIndex(0)def _save(self):    if self.result_img is None:        QMessageBox.warning(self"提示""请先上传并处理"); return    p, _ = QFileDialog.getSaveFileName(self"保存""bleached.png""PNG (*.png);;JPG (*.jpg)")    if p:        cv2.imwrite(p, self.result_img)        self.lbl_st.setText(f"✅ 已保存: {os.path.basename(p)}")        QMessageBox.information(self"成功"f"已保存到:\n{p}")def _print(self):    if self.result_img is None:        QMessageBox.warning(self"提示""请先上传并处理"); return    pix = cv2_to_qpixmap(self.result_img)    dlg = QPrintPreviewDialog()    dlg.paintRequested.connect(lambda printer: self._do_print(printer, pix))    dlg.exec_()def _do_print(self, printer, pix):    p = QPainter(printer)    rect = p.viewport()    scaled = pix.scaled(rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)    p.drawPixmap((rect.width() - scaled.width()) // 20, scaled)    p.end()==================== Tab2: 打印设置 ====================class PrintTab(QWidget):def init(self, bleach_tab):super().init()self.bleach_tab = bleach_tabself._build()def _build(self):    lay = QVBoxLayout(self)    lay.setContentsMargins(16161616)    lay.setSpacing(12)    lay.addWidget(QLabel("🖨 打印设置与预览"))    g1 = QGroupBox("纸张设置")    g1l = QHBoxLayout()    g1l.addWidget(QLabel("纸张:"))    self.cb_paper = QComboBox()    self.cb_paper.addItems(["A4 (210x297mm)""A5 (148x210mm)""B5 (176x250mm)""Letter (216x279mm)""自定义"])    g1l.addWidget(self.cb_paper, 1)    g1l.addWidget(QLabel("方向:"))    self.cb_orient = QComboBox()    self.cb_orient.addItems(["纵向""横向"])    g1l.addWidget(self.cb_orient)    g1.setLayout(g1l)    lay.addWidget(g1)    g2 = QGroupBox("边距 (mm)")    g2l = QHBoxLayout()    for name in ["上""下""左""右"]:        g2l.addWidget(QLabel(name + ":"))        sp = QSpinBox()        sp.setRange(050)        sp.setValue(10)        g2l.addWidget(sp)    g2.setLayout(g2l)    lay.addWidget(g2)    g3 = QGroupBox("缩放")    g3l = QHBoxLayout()    self.cb_scale = QComboBox()    self.cb_scale.addItems(["适合页面""100%""75%""50%""150%""200%"])    g3l.addWidget(QLabel("缩放:"))    g3l.addWidget(self.cb_scale, 1)    g3.setLayout(g3l)    lay.addWidget(g3)    # 预览    lay.addWidget(QLabel("📄 打印预览"))    self.preview = ImageLabel("处理后的图片将在这里预览")    self.preview.setMinimumHeight(300)    self.preview.setStyleSheet("background:#fff;border:2px solid #e0e0e0;border-radius:4px;")    lay.addWidget(self.preview, 1)    btn_row = QHBoxLayout()    btn_refresh = QPushButton("🔄 刷新预览")    btn_refresh.setStyleSheet("padding:8px 16px;background:#1976d2;color:#fff;border:none;border-radius:4px;")    btn_refresh.clicked.connect(self._refresh_preview)    btn_print = QPushButton("🖨 打印")    btn_print.setStyleSheet("padding:8px 16px;background:#d32f2f;color:#fff;border:none;border-radius:4px;")    btn_print.clicked.connect(self._print)    btn_row.addWidget(btn_refresh)    btn_row.addWidget(btn_print)    btn_row.addStretch()    lay.addLayout(btn_row)def _refresh_preview(self):    if self.bleach_tab.result_img is not None:        self.preview.set_pixmap(cv2_to_qpixmap(self.bleach_tab.result_img))    else:        QMessageBox.information(self"提示""请先在「漂白处理」页上传并处理图片")def _print(self):    self.bleach_tab._print()==================== Tab3: 关于 ====================class AboutTab(QWidget):def init(self):super().init()lay = QVBoxLayout(self)lay.setContentsMargins(30403030)lay.setAlignment(Qt.AlignTop)    t = QLabel(f"📄 文档漂白打印工具 {APP_VERSION}")    t.setStyleSheet("font-size:22px;font-weight:bold;color:#1976d2;")    lay.addWidget(t)    lay.addSpacing(16)    line = QFrame()    line.setFrameShape(QFrame.HLine)    line.setStyleSheet("color:#ddd;")    lay.addWidget(line)    lay.addSpacing(12)    for label, value in [        ("👤 作者""杨少平"),        ("📱 公众号""Python学在坚持"),        ("💬 微信""ysp2338084"),        ("📦 版本", APP_VERSION),        ("🔧 技术栈""PyQt5 + OpenCV"),        ("📝 功能""拍照文档去底色/漂白/打印优化"),    ]:        row = QHBoxLayout()        lbl = QLabel(label)        lbl.setStyleSheet("font-size:14px;color:#666;min-width:80px;")        val = QLabel(value)        val.setStyleSheet("font-size:14px;font-weight:bold;color:#333;")        val.setTextInteractionFlags(Qt.TextSelectableByMouse)        row.addWidget(lbl)        row.addWidget(val)        row.addStretch()        lay.addLayout(row)        lay.addSpacing(4)    lay.addSpacing(20)    line2 = QFrame()    line2.setFrameShape(QFrame.HLine)    line2.setStyleSheet("color:#ddd;")    lay.addWidget(line2)    lay.addSpacing(10)    desc = QLabel(        "使用场景:\n"        "• 手机拍照的文档/试卷/书页,背景灰暗\n"        "• 扫描件有底色,打印出来黑乎乎\n"        "• 需要去除背景色,只保留黑色文字\n"        "• 打印前预览,确保效果清晰\n\n"        "原理:OpenCV 自适应阈值二值化,\n"        "将灰色背景转为纯白,文字保持黑色。"    )    desc.setStyleSheet("color:#888;font-size:12px;")    desc.setWordWrap(True)    lay.addWidget(desc)    lay.addStretch()==================== 主窗口 ====================class DocBleachApp(QMainWindow):def init(self):super().init()self.setWindowTitle(f"📄 文档漂白打印工具 {APP_VERSION} | Python学在坚持")self.setMinimumSize(900600)self.resize(1050680)    tabs = QTabWidget()    tabs.setStyleSheet("""        QTabBar::tab{padding:10px 24px;font-size:13px;}        QTabBar::tab:selected{background:#1976d2;color:#fff;border-radius:4px 4px 0 0;}    """)    self.bleach_tab = BleachTab()    tabs.addTab(self.bleach_tab, "🖼 漂白处理")    tabs.addTab(PrintTab(self.bleach_tab), "🖨 打印设置")    tabs.addTab(AboutTab(), "ℹ️ 关于")    self.setCentralWidget(tabs)    self.statusBar().showMessage("公众号: Python学在坚持 | 微信: ysp2338084 | 作者: 杨少平")if name == "main":app = QApplication(sys.argv)app.setFont(QFont("Microsoft YaHei"10))win = DocBleachApp()win.show()sys.exit(app.exec_())
-- coding: utf-8 --"""文档漂白打印工具 v1.0 - PyQt5 + OpenCV功能:上传拍照文档/图片 -> 去除背景底色 -> 漂白处理 -> 打印预览 -> 导出作者:杨少平 | 公众号:Python学在坚持运行:python doc_bleach.py依赖:pip install PyQt5 opencv-python Pillow numpy"""import sys, osimport cv2import numpy as npfrom PIL import Imagefrom PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QPushButton, QSlider, QComboBox, QTabWidget, QGroupBox,QFileDialog, QMessageBox, QFrame, QCheckBox, QSpinBox, QSplitter)from PyQt5.QtCore import Qtfrom PyQt5.QtGui import QFont, QColor, QPixmap, QImage, QPainterfrom PyQt5.QtPrintSupport import QPrinter, QPrintDialog, QPrintPreviewDialogAPP_VERSION = "v1.0"==================== 漂白引擎 ====================def bleach_image(img_path, method="adaptive", block_size=15, c_val=10, brightness=0, contrast=0, denoise=False):"""漂白处理核心method: adaptive(自适应阈值) / otsu(大津法) / simple(简单阈值)block_size: 自适应阈值的块大小(奇数)c_val: 自适应阈值的常数Cbrightness: 亮度调整 -100~100contrast: 对比度调整 -100~100"""# 用 np.fromfile + imdecode 支持中文路径data = np.fromfile(img_path, dtype=np.uint8)img = cv2.imdecode(data, cv2.IMREAD_COLOR)if img is None:return None# 转灰度gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 亮度/对比度调整if brightness != 0 or contrast != 0:alpha = 1.0 + contrast / 100.0beta = brightnessgray = cv2.convertScaleAbs(gray, alpha=alpha, beta=beta)# 降噪if denoise:gray = cv2.fastNlMeansDenoising(gray, h=10)# 二值化if method == "adaptive":bs = block_size if block_size % 2 == 1 else block_size + 1bs = max(3, bs)result = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, bs, c_val)elif method == "otsu":_, result = cv2.threshold(gray, 0255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)else:_, result = cv2.threshold(gray, 127255, cv2.THRESH_BINARY)return resultdef cv2_to_qpixmap(cv_img):"""OpenCV灰度/BGR图 -> QPixmap"""if len(cv_img.shape) == 2:h, w = cv_img.shapeqi = QImage(cv_img.data, w, h, w, QImage.Format_Grayscale8)else:h, w, ch = cv_img.shapergb = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)qi = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888)return QPixmap.fromImage(qi)class ImageLabel(QLabel):"""带自适应缩放的图片显示"""def init(self, text=""):super().init()self.setAlignment(Qt.AlignCenter)self.setMinimumSize(200260)self.setStyleSheet("background:#f0f0f0;border:1px solid #ddd;border-radius:4px;")self._pix = Noneself._text = textdef set_pixmap(self, pix):    self._pix = pix    self.update()def paintEvent(self, e):    p = QPainter(self)    p.fillRect(self.rect(), QColor(245245245))    if self._pix:        s = self._pix.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)        p.drawPixmap((self.width() - s.width()) // 2, (self.height() - s.height()) // 2, s)    else:        p.setPen(QColor(160160160))        p.setFont(QFont("Microsoft YaHei"12))        p.drawText(self.rect(), Qt.AlignCenter, self._text or "上传图片")    p.end()==================== Tab1: 漂白处理 ====================class BleachTab(QWidget):def init(self):super().init()self.img_path = ""self.result_img = None  # cv2 灰度图self._build()def _build(self):    root = QHBoxLayout(self)    root.setContentsMargins(8888)    root.setSpacing(8)    # 左侧控制    left = QWidget()    left.setMaximumWidth(250)    ll = QVBoxLayout(left)    ll.setContentsMargins(0000)    ll.setSpacing(6)    btn_up = QPushButton("📂 上传图片/文档")    btn_up.setStyleSheet("padding:10px;background:#1976d2;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:bold;")    btn_up.clicked.connect(self._upload)    ll.addWidget(btn_up)    # 方法    g1 = QGroupBox("漂白方法")    g1l = QVBoxLayout()    self.cb_method = QComboBox()    self.cb_method.addItems(["自适应阈值(推荐)""大津法(Otsu)""简单阈值"])    self.cb_method.currentIndexChanged.connect(self._process)    g1l.addWidget(self.cb_method)    g1.setLayout(g1l)    ll.addWidget(g1)    # 参数    g2 = QGroupBox("参数调节")    g2l = QVBoxLayout()    g2l.addWidget(QLabel("块大小(奇数,越大越平滑):"))    self.sl_block = QSlider(Qt.Horizontal)    self.sl_block.setRange(351)    self.sl_block.setValue(15)    self.sl_block.setSingleStep(2)    self.lbl_block = QLabel("15")    row1 = QHBoxLayout()    row1.addWidget(self.sl_block, 1)    row1.addWidget(self.lbl_block)    self.sl_block.valueChanged.connect(lambda v: self.lbl_block.setText(str(v if v % 2 == 1 else v + 1)))    g2l.addLayout(row1)    g2l.addWidget(QLabel("阈值常数C(越大文字越粗):"))    self.sl_c = QSlider(Qt.Horizontal)    self.sl_c.setRange(130)    self.sl_c.setValue(10)    self.lbl_c = QLabel("10")    row2 = QHBoxLayout()    row2.addWidget(self.sl_c, 1)    row2.addWidget(self.lbl_c)    self.sl_c.valueChanged.connect(lambda v: self.lbl_c.setText(str(v)))    g2l.addLayout(row2)    g2l.addWidget(QLabel("亮度:"))    self.sl_bright = QSlider(Qt.Horizontal)    self.sl_bright.setRange(-100100)    self.sl_bright.setValue(0)    self.lbl_bright = QLabel("0")    row3 = QHBoxLayout()    row3.addWidget(self.sl_bright, 1)    row3.addWidget(self.lbl_bright)    self.sl_bright.valueChanged.connect(lambda v: self.lbl_bright.setText(str(v)))    g2l.addLayout(row3)    g2l.addWidget(QLabel("对比度:"))    self.sl_contrast = QSlider(Qt.Horizontal)    self.sl_contrast.setRange(-100100)    self.sl_contrast.setValue(0)    self.lbl_contrast = QLabel("0")    row4 = QHBoxLayout()    row4.addWidget(self.sl_contrast, 1)    row4.addWidget(self.lbl_contrast)    self.sl_contrast.valueChanged.connect(lambda v: self.lbl_contrast.setText(str(v)))    g2l.addLayout(row4)    self.chk_denoise = QCheckBox("降噪(较慢)")    g2l.addWidget(self.chk_denoise)    btn_render = QPushButton("🎨 渲染预览")    btn_render.setStyleSheet("padding:10px;background:#ff9800;color:#fff;border:none;border-radius:4px;font-size:13px;font-weight:bold;")    btn_render.clicked.connect(self._process)    g2l.addWidget(btn_render)    btn_reset = QPushButton("🔄 重置参数")    btn_reset.clicked.connect(self._reset)    g2l.addWidget(btn_reset)    g2.setLayout(g2l)    ll.addWidget(g2)    # 导出    g3 = QGroupBox("操作")    g3l = QVBoxLayout()    btn_save = QPushButton("💾 保存图片")    btn_save.setStyleSheet("padding:8px;background:#388e3c;color:#fff;border:none;border-radius:4px;")    btn_save.clicked.connect(self._save)    g3l.addWidget(btn_save)    btn_print = QPushButton("🖨 打印")    btn_print.setStyleSheet("padding:8px;background:#d32f2f;color:#fff;border:none;border-radius:4px;")    btn_print.clicked.connect(self._print)    g3l.addWidget(btn_print)    g3.setLayout(g3l)    ll.addWidget(g3)    self.lbl_st = QLabel("就绪")    self.lbl_st.setStyleSheet("color:#888;font-size:11px;")    ll.addWidget(self.lbl_st)    ll.addStretch()    root.addWidget(left)    # 右侧预览    right = QWidget()    rl = QVBoxLayout(right)    rl.setContentsMargins(0000)    rl.setSpacing(4)    labels = QHBoxLayout()    labels.addWidget(QLabel("📷 原图"))    labels.addWidget(QLabel("✅ 漂白效果"))    rl.addLayout(labels)    imgs = QHBoxLayout()    self.pv_orig = ImageLabel("上传图片/文档照片")    self.pv_result = ImageLabel("漂白结果")    self.pv_result.setStyleSheet("background:#fff;border:1px solid #ddd;border-radius:4px;")    imgs.addWidget(self.pv_orig)    imgs.addWidget(self.pv_result)    rl.addLayout(imgs, 1)    root.addWidget(right, 1)def _upload(self):    p, _ = QFileDialog.getOpenFileName(self"选择图片""""图片 (*.png *.jpg *.jpeg *.bmp *.tiff *.webp);;所有 (*)")    if not p: return    self.img_path = p    data = np.fromfile(p, dtype=np.uint8)    img = cv2.imdecode(data, cv2.IMREAD_COLOR)    if img is not None:        self.pv_orig.set_pixmap(cv2_to_qpixmap(img))    self.lbl_st.setText("✅ 已加载,调整参数后点击「渲染预览」")def _get_method(self):    idx = self.cb_method.currentIndex()    return ["adaptive""otsu""simple"][idx]def _process(self):    if not self.img_path: return    bs = self.sl_block.value()    if bs % 2 == 0: bs += 1    result = bleach_image(        self.img_path,        method=self._get_method(),        block_size=bs,        c_val=self.sl_c.value(),        brightness=self.sl_bright.value(),        contrast=self.sl_contrast.value(),        denoise=self.chk_denoise.isChecked(),    )    if result is not None:        self.result_img = result        self.pv_result.set_pixmap(cv2_to_qpixmap(result))        self.lbl_st.setText(f"✅ 已处理 | {result.shape[1]}x{result.shape[0]}")def _reset(self):    self.sl_block.setValue(15)    self.sl_c.setValue(10)    self.sl_bright.setValue(0)    self.sl_contrast.setValue(0)    self.chk_denoise.setChecked(False)    self.cb_method.setCurrentIndex(0)def _save(self):    if self.result_img is None:        QMessageBox.warning(self"提示""请先上传并处理"); return    p, _ = QFileDialog.getSaveFileName(self"保存""bleached.png""PNG (*.png);;JPG (*.jpg)")    if p:        cv2.imwrite(p, self.result_img)        self.lbl_st.setText(f"✅ 已保存: {os.path.basename(p)}")        QMessageBox.information(self"成功"f"已保存到:\n{p}")def _print(self):    if self.result_img is None:        QMessageBox.warning(self"提示""请先上传并处理"); return    pix = cv2_to_qpixmap(self.result_img)    dlg = QPrintPreviewDialog()    dlg.paintRequested.connect(lambda printer: self._do_print(printer, pix))    dlg.exec_()def _do_print(self, printer, pix):    p = QPainter(printer)    rect = p.viewport()    scaled = pix.scaled(rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)    p.drawPixmap((rect.width() - scaled.width()) // 20, scaled)    p.end()==================== Tab2: 打印设置 ====================class PrintTab(QWidget):def init(self, bleach_tab):super().init()self.bleach_tab = bleach_tabself._build()def _build(self):    lay = QVBoxLayout(self)    lay.setContentsMargins(16161616)    lay.setSpacing(12)    lay.addWidget(QLabel("🖨 打印设置与预览"))    g1 = QGroupBox("纸张设置")    g1l = QHBoxLayout()    g1l.addWidget(QLabel("纸张:"))    self.cb_paper = QComboBox()    self.cb_paper.addItems(["A4 (210x297mm)""A5 (148x210mm)""B5 (176x250mm)""Letter (216x279mm)""自定义"])    g1l.addWidget(self.cb_paper, 1)    g1l.addWidget(QLabel("方向:"))    self.cb_orient = QComboBox()    self.cb_orient.addItems(["纵向""横向"])    g1l.addWidget(self.cb_orient)    g1.setLayout(g1l)    lay.addWidget(g1)    g2 = QGroupBox("边距 (mm)")    g2l = QHBoxLayout()    for name in ["上""下""左""右"]:        g2l.addWidget(QLabel(name + ":"))        sp = QSpinBox()        sp.setRange(050)        sp.setValue(10)        g2l.addWidget(sp)    g2.setLayout(g2l)    lay.addWidget(g2)    g3 = QGroupBox("缩放")    g3l = QHBoxLayout()    self.cb_scale = QComboBox()    self.cb_scale.addItems(["适合页面""100%""75%""50%""150%""200%"])    g3l.addWidget(QLabel("缩放:"))    g3l.addWidget(self.cb_scale, 1)    g3.setLayout(g3l)    lay.addWidget(g3)    # 预览    lay.addWidget(QLabel("📄 打印预览"))    self.preview = ImageLabel("处理后的图片将在这里预览")    self.preview.setMinimumHeight(300)    self.preview.setStyleSheet("background:#fff;border:2px solid #e0e0e0;border-radius:4px;")    lay.addWidget(self.preview, 1)    btn_row = QHBoxLayout()    btn_refresh = QPushButton("🔄 刷新预览")    btn_refresh.setStyleSheet("padding:8px 16px;background:#1976d2;color:#fff;border:none;border-radius:4px;")    btn_refresh.clicked.connect(self._refresh_preview)    btn_print = QPushButton("🖨 打印")    btn_print.setStyleSheet("padding:8px 16px;background:#d32f2f;color:#fff;border:none;border-radius:4px;")    btn_print.clicked.connect(self._print)    btn_row.addWidget(btn_refresh)    btn_row.addWidget(btn_print)    btn_row.addStretch()    lay.addLayout(btn_row)def _refresh_preview(self):    if self.bleach_tab.result_img is not None:        self.preview.set_pixmap(cv2_to_qpixmap(self.bleach_tab.result_img))    else:        QMessageBox.information(self"提示""请先在「漂白处理」页上传并处理图片")def _print(self):    self.bleach_tab._print()==================== Tab3: 关于 ====================class AboutTab(QWidget):def init(self):super().init()lay = QVBoxLayout(self)lay.setContentsMargins(30403030)lay.setAlignment(Qt.AlignTop)    t = QLabel(f"📄 文档漂白打印工具 {APP_VERSION}")    t.setStyleSheet("font-size:22px;font-weight:bold;color:#1976d2;")    lay.addWidget(t)    lay.addSpacing(16)    line = QFrame()    line.setFrameShape(QFrame.HLine)    line.setStyleSheet("color:#ddd;")    lay.addWidget(line)    lay.addSpacing(12)    for label, value in [        ("👤 作者""杨少平"),        ("📱 公众号""Python学在坚持"),        ("💬 微信""ysp2338084"),        ("📦 版本", APP_VERSION),        ("🔧 技术栈""PyQt5 + OpenCV"),        ("📝 功能""拍照文档去底色/漂白/打印优化"),    ]:        row = QHBoxLayout()        lbl = QLabel(label)        lbl.setStyleSheet("font-size:14px;color:#666;min-width:80px;")        val = QLabel(value)        val.setStyleSheet("font-size:14px;font-weight:bold;color:#333;")        val.setTextInteractionFlags(Qt.TextSelectableByMouse)        row.addWidget(lbl)        row.addWidget(val)        row.addStretch()        lay.addLayout(row)        lay.addSpacing(4)    lay.addSpacing(20)    line2 = QFrame()    line2.setFrameShape(QFrame.HLine)    line2.setStyleSheet("color:#ddd;")    lay.addWidget(line2)    lay.addSpacing(10)    desc = QLabel(        "使用场景:\n"        "• 手机拍照的文档/试卷/书页,背景灰暗\n"        "• 扫描件有底色,打印出来黑乎乎\n"        "• 需要去除背景色,只保留黑色文字\n"        "• 打印前预览,确保效果清晰\n\n"        "原理:OpenCV 自适应阈值二值化,\n"        "将灰色背景转为纯白,文字保持黑色。"    )    desc.setStyleSheet("color:#888;font-size:12px;")    desc.setWordWrap(True)    lay.addWidget(desc)    lay.addStretch()==================== 主窗口 ====================class DocBleachApp(QMainWindow):def init(self):super().init()self.setWindowTitle(f"📄 文档漂白打印工具 {APP_VERSION} | Python学在坚持")self.setMinimumSize(900600)self.resize(1050680)    tabs = QTabWidget()    tabs.setStyleSheet("""        QTabBar::tab{padding:10px 24px;font-size:13px;}        QTabBar::tab:selected{background:#1976d2;color:#fff;border-radius:4px 4px 0 0;}    """)    self.bleach_tab = BleachTab()    tabs.addTab(self.bleach_tab, "🖼 漂白处理")    tabs.addTab(PrintTab(self.bleach_tab), "🖨 打印设置")    tabs.addTab(AboutTab(), "ℹ️ 关于")    self.setCentralWidget(tabs)    self.statusBar().showMessage("公众号: Python学在坚持 | 微信: ysp2338084 | 作者: 杨少平")if name == "main":app = QApplication(sys.argv)app.setFont(QFont("Microsoft YaHei"10))win = DocBleachApp()win.show()sys.exit(app.exec_())