当前位置:首页>python>Python抠图换背景工具 v1.0

Python抠图换背景工具 v1.0

  • 2026-10-11 08:40:51
Python抠图换背景工具 v1.0

抠图换背景工具 v1.0

基于 PyQt5 + rembg 的一键抠图换背景桌面工具,支持证件照生成、自定义背景色、多种尺寸、取色器。

作者

项目
内容
作者
杨一凡
公众号
Python学在坚持
微信
ysp2338084

安装与运行

# 安装依赖(注意 rembg 要带 [cpu])pip install PyQt5 "rembg[cpu]" Pillow numpy# 运行python ysp/code/photo_tool.py

首次运行 rembg 会自动下载 AI 抠图模型(约170MB),下载一次后缓存到本地。

界面布局

+------------------+-------------------+-------------------+| 上传图片          |  原图              |  结果              || [进度条]          | +---------------+ | +---------------+ ||                  | |               | | |               | || 背景颜色          | |   原始图片     | | |  抠图+换背景   | || [白][红][蓝]...   | |               | | |               | || [取色器]          | +---------------+ | +---------------+ || [颜色预览块]      |                   |                   ||                  |                   |                   || 输出尺寸          |                   |                   || [一寸 295x413]   |                   |                   || 宽:[295] 高:[413]|                   |                   ||                  |                   |                   || 导出              |                   |                   || [导出PNG]         |                   |                   || [导出JPG]         |                   |                   |+------------------+-------------------+-------------------+

功能说明

自动抠图

  • 使用 rembg(基于 U2-Net AI 模型)自动去除背景
  • 子线程执行,不阻塞界面
  • 支持人像、物品、动物等各类主体

背景颜色

  • 7 种预设色:白色、红色、蓝色、浅蓝、渐变蓝、灰色、透明
  • 圆形色块一键切换
  • 取色器:打开系统调色板选任意颜色
  • 颜色预览块实时显示当前背景色

输出尺寸

  • 9 种预设证件照尺寸 + 自定义
名称
尺寸(px)
用途
一寸
295x413
简历、考试报名
二寸
413x579
护照申请
小一寸
260x378
驾照
小二寸
413x531
普通证件
大一寸
390x567
签证
护照
354x472
护照
身份证
358x441
身份证
签证
600x600
美国签证
社交头像
500x500
微信/微博头像
自定义
任意
手动输入宽高

导出

  • PNG:保留透明背景(选"透明"背景色时)
  • JPG:白底/彩底(自动转RGB)
  • 质量 95%

代码架构

photo_tool.py+-- SIZES                  # 预设尺寸列表+-- BG_COLORS              # 预设背景色列表+-- RemoveBgWorker(QThread) # 抠图子线程|   +-- 调用 rembg.remove()|   +-- 返回 RGBA 图片+-- pil_to_qpixmap()       # PIL Image -> QPixmap+-- compose()              # 前景RGBA + 背景色 + 尺寸 -> 合成图+-- PreviewLabel(QLabel)    # 棋盘格背景预览控件+-- PhotoToolApp(QMainWindow)    +-- _upload()           # 上传图片 -> 启动抠图线程    +-- _on_rembg_done()    # 抠图完成 -> 刷新预览    +-- _pick_color()       # 打开取色器    +-- _refresh()          # 合成预览(背景色+尺寸变化时)    +-- _export()           # 导出PNG/JPG

核心流程

上传图片  |  vRemoveBgWorker [子线程]  | rembg.remove() AI抠图  | 返回 RGBA 图片(背景透明)  v保存前景 fg_rgba  |  vcompose(fg_rgba, bg_color, width, height)  | 1. 创建指定尺寸的背景画布  | 2. 前景缩放(保持比例)  | 3. 居中粘贴到背景上  v预览显示 + 可导出

依赖

PyQt5              # GUIrembg[cpu]         # AI抠图(含onnxruntime)Pillow             # 图片处理numpy              # 数组操作

安装命令:

pip install PyQt5 "rembg[cpu]" Pillow numpy

常见问题

问题
解决
No module named rembg
pip install "rembg[cpu]"
No onnxruntime backend
pip install "rembg[cpu]"
(带[cpu]后缀)
首次抠图很慢
正在下载AI模型(约170MB),只需一次
抠图效果不好
rembg 对人像效果最好,复杂背景可能有残留
导出JPG透明变黑
JPG不支持透明,会自动转为白底
-- coding: utf-8 --"""证件照/抠图换背景工具 v1.0 - PyQt5功能:上传图片 -> 自动抠图 -> 换背景色 -> 自定义尺寸 -> 导出作者:杨少平 | 公众号:Python学在坚持运行:python photo_tool.py依赖:pip install PyQt5 opencv-python Pillow numpy"""import sys, os, ioimport numpy as npimport cv2from PIL import Imagefrom PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QPushButton, QComboBox, QColorDialog, QFileDialog,QMessageBox, QGroupBox, QSpinBox, QFrame, QProgressBar)from PyQt5.QtCore import Qt, pyqtSignal, QThreadfrom PyQt5.QtGui import QFont, QColor, QPixmap, QImage, QPainter常用证件照尺寸(名称, 宽px, 高px)SIZES = [("一寸 (295x413)", 295, 413),("二寸 (413x579)", 413, 579),("小一寸 (260x378)", 260, 378),("小二寸 (413x531)", 413, 531),("大一寸 (390x567)", 390, 567),("护照 (354x472)", 354, 472),("身份证 (358x441)", 358, 441),("签证 (600x600)", 600, 600),("社交头像 (500x500)", 500, 500),("自定义", 0, 0),]常用背景色BG_COLORS = [("白色", "#FFFFFF"),("红色", "#FF0000"),("蓝色", "#438EDB"),("浅蓝", "#86C8F5"),("渐变蓝", "#2B7FD4"),("灰色", "#CCCCCC"),("透明", "transparent"),]class RemoveBgWorker(QThread):"""抠图子线程 - 使用 OpenCV GrabCut(无需额外AI模型)"""finished = pyqtSignal(object)  # PIL Image (RGBA)def __init__(self, img_path):    super().__init__()    self.img_path = img_pathdef run(self):    try:        img = cv2.imread(self.img_path)        if img is None:            self.finished.emit(None)            return        h, w = img.shape[:2]        # GrabCut 需要一个初始矩形框住前景,取中心80%区域        margin_x, margin_y = int(w * 0.05), int(h * 0.02)        rect = (margin_x, margin_y, w - 2 * margin_x, h - 2 * margin_y)        mask = np.zeros((h, w), np.uint8)        bgd_model = np.zeros((1, 65), np.float64)        fgd_model = np.zeros((1, 65), np.float64)        # 迭代5次        cv2.grabCut(img, mask, rect, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_RECT)        # 再用中心区域作为确定前景,迭代3次优化        center_mask = np.zeros((h, w), np.uint8)        cx1, cy1 = int(w * 0.2), int(h * 0.1)        cx2, cy2 = int(w * 0.8), int(h * 0.9)        center_mask[cy1:cy2, cx1:cx2] = 1        mask[center_mask == 1] = np.where(mask[center_mask == 1] == 0, 3, mask[center_mask == 1])        cv2.grabCut(img, mask, None, bgd_model, fgd_model, 3, cv2.GC_INIT_WITH_MASK)        # 生成前景mask        fg_mask = np.where((mask == 1) | (mask == 3), 255, 0).astype(np.uint8)        # 平滑边缘        fg_mask = cv2.GaussianBlur(fg_mask, (5, 5), 0)        # 转RGBA        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)        rgba = np.dstack([img_rgb, fg_mask])        pil_img = Image.fromarray(rgba, "RGBA")        self.finished.emit(pil_img)    except Exception as e:        print(f"抠图失败: {e}")        self.finished.emit(None)def pil_to_qpixmap(pil_img):"""PIL Image -> QPixmap"""if pil_img.mode == "RGBA":data = pil_img.tobytes("raw", "RGBA")qimg = QImage(data, pil_img.width, pil_img.height, QImage.Format_RGBA8888)else:data = pil_img.tobytes("raw", "RGB")qimg = QImage(data, pil_img.width, pil_img.height, QImage.Format_RGB888)return QPixmap.fromImage(qimg)def compose(fg_rgba, bg_color, width, height):"""合成:前景RGBA + 背景色 -> 指定尺寸RGB图"""# 缩放前景到目标尺寸(保持比例居中)fg = fg_rgba.copy()fg.thumbnail((width, height), Image.LANCZOS)fw, fh = fg.sizeif bg_color == "transparent":    bg = Image.new("RGBA", (width, height), (0, 0, 0, 0))else:    r, g, b = int(bg_color[1:3], 16), int(bg_color[3:5], 16), int(bg_color[5:7], 16)    bg = Image.new("RGBA", (width, height), (r, g, b, 255))# 居中粘贴x = (width - fw) // 2y = (height - fh) // 2bg.paste(fg, (x, y), fg)return bgclass PreviewLabel(QLabel):"""带棋盘格背景的预览控件"""def init(self):super().init()self.setAlignment(Qt.AlignCenter)self.setMinimumSize(200, 260)self.setStyleSheet("border:1px solid #ddd;border-radius:4px;")self._pixmap = Nonedef set_image(self, pixmap):    self._pixmap = pixmap    self.update()def paintEvent(self, e):    p = QPainter(self)    # 棋盘格    tile = 10    for y in range(0, self.height(), tile):        for x in range(0, self.width(), tile):            c = QColor(220, 220, 220) if (x // tile + y // tile) % 2 == 0 else QColor(255, 255, 255)            p.fillRect(x, y, tile, tile, c)    if self._pixmap:        scaled = self._pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)        x = (self.width() - scaled.width()) // 2        y = (self.height() - scaled.height()) // 2        p.drawPixmap(x, y, scaled)    else:        p.setPen(QColor(150, 150, 150))        p.setFont(QFont("Microsoft YaHei", 12))        p.drawText(self.rect(), Qt.AlignCenter, "上传图片")    p.end()==================== 主窗口 ====================class PhotoToolApp(QMainWindow):def init(self):super().init()self.setWindowTitle("📷 抠图换背景工具 v1.0 | Python学在坚持")self.setMinimumSize(850, 550)self.resize(950, 620)self.fg_rgba = None  # 抠图后的前景RGBAself.bg_color = "#FFFFFF"self.worker = Noneself._build()def _build(self):    central = QWidget()    self.setCentralWidget(central)    root = QHBoxLayout(central)    root.setContentsMargins(10, 10, 10, 10)    root.setSpacing(10)    # ---- 左侧控制 ----    left = QWidget()    left.setMaximumWidth(260)    ll = QVBoxLayout(left)    ll.setContentsMargins(0, 0, 0, 0)    ll.setSpacing(8)    btn_upload = QPushButton("📂 上传图片")    btn_upload.setStyleSheet("padding:12px;background:#1976d2;color:#fff;border:none;border-radius:6px;font-size:14px;font-weight:bold;")    btn_upload.clicked.connect(self._upload)    ll.addWidget(btn_upload)    self.progress = QProgressBar()    self.progress.setMaximumHeight(6)    self.progress.setTextVisible(False)    self.progress.setRange(0, 0)    self.progress.hide()    ll.addWidget(self.progress)    # 背景色    g1 = QGroupBox("🎨 背景颜色")    g1l = QVBoxLayout()    self.cb_color = QComboBox()    for name, _ in BG_COLORS:        self.cb_color.addItem(name)    self.cb_color.currentIndexChanged.connect(self._on_color_change)    g1l.addWidget(self.cb_color)    # 颜色预览块    color_row = QHBoxLayout()    for name, hex_c in BG_COLORS:        if hex_c == "transparent": continue        b = QPushButton()        b.setFixedSize(28, 28)        b.setStyleSheet(f"background:{hex_c};border:2px solid #ccc;border-radius:14px;")        b.setToolTip(name)        b.clicked.connect(lambda _, c=hex_c: self._set_color(c))        color_row.addWidget(b)    g1l.addLayout(color_row)    btn_picker = QPushButton("🎨 取色器")    btn_picker.setStyleSheet("padding:6px;")    btn_picker.clicked.connect(self._pick_color)    g1l.addWidget(btn_picker)    self.color_preview = QFrame()    self.color_preview.setFixedHeight(30)    self.color_preview.setStyleSheet("background:#FFFFFF;border:1px solid #ccc;border-radius:4px;")    g1l.addWidget(self.color_preview)    g1.setLayout(g1l)    ll.addWidget(g1)    # 尺寸    g2 = QGroupBox("📐 输出尺寸")    g2l = QVBoxLayout()    self.cb_size = QComboBox()    for name, _, _ in SIZES:        self.cb_size.addItem(name)    self.cb_size.currentIndexChanged.connect(self._on_size_change)    g2l.addWidget(self.cb_size)    size_row = QHBoxLayout()    size_row.addWidget(QLabel("宽:"))    self.sp_w = QSpinBox()    self.sp_w.setRange(50, 5000)    self.sp_w.setValue(295)    self.sp_w.valueChanged.connect(self._refresh)    size_row.addWidget(self.sp_w)    size_row.addWidget(QLabel("高:"))    self.sp_h = QSpinBox()    self.sp_h.setRange(50, 5000)    self.sp_h.setValue(413)    self.sp_h.valueChanged.connect(self._refresh)    size_row.addWidget(self.sp_h)    g2l.addLayout(size_row)    g2.setLayout(g2l)    ll.addWidget(g2)    # 导出    g3 = QGroupBox("💾 导出")    g3l = QVBoxLayout()    btn_png = QPushButton("导出 PNG(透明背景可用)")    btn_png.setStyleSheet("padding:8px;background:#388e3c;color:#fff;border:none;border-radius:4px;")    btn_png.clicked.connect(lambda: self._export("png"))    btn_jpg = QPushButton("导出 JPG(白底/彩底)")    btn_jpg.setStyleSheet("padding:8px;background:#1976d2;color:#fff;border:none;border-radius:4px;")    btn_jpg.clicked.connect(lambda: self._export("jpg"))    g3l.addWidget(btn_png)    g3l.addWidget(btn_jpg)    g3.setLayout(g3l)    ll.addWidget(g3)    self.lbl_status = QLabel("就绪")    self.lbl_status.setStyleSheet("color:#888;font-size:11px;")    ll.addWidget(self.lbl_status)    ll.addStretch()    root.addWidget(left)    # ---- 右侧预览:原图 + 结果 ----    right = QWidget()    rl = QVBoxLayout(right)    rl.setContentsMargins(0, 0, 0, 0)    rl.setSpacing(6)    labels = QHBoxLayout()    labels.addWidget(QLabel("📷 原图"))    labels.addWidget(QLabel("✅ 结果"))    rl.addLayout(labels)    preview_row = QHBoxLayout()    self.preview_orig = PreviewLabel()    self.preview_result = PreviewLabel()    preview_row.addWidget(self.preview_orig)    preview_row.addWidget(self.preview_result)    rl.addLayout(preview_row, 1)    root.addWidget(right, 1)    self.statusBar().showMessage("公众号: Python学在坚持 | 微信: ysp2338084 | 作者: 杨少平")# ---- 上传 ----def _upload(self):    path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片 (*.png *.jpg *.jpeg *.bmp *.webp);;所有 (*)")    if not path: return    # 显示原图    orig = Image.open(path)    self.preview_orig.set_image(pil_to_qpixmap(orig.convert("RGB")))    # 开始抠图    self.lbl_status.setText("⏳ 抠图中(GrabCut)...")    self.progress.show()    self.worker = RemoveBgWorker(path)    self.worker.finished.connect(self._on_rembg_done)    self.worker.start()def _on_rembg_done(self, rgba_img):    self.progress.hide()    if rgba_img is None:        self.lbl_status.setText("❌ 抠图失败")        QMessageBox.critical(self, "失败", "抠图失败,请确认图片格式正确")        return    self.fg_rgba = rgba_img    self.lbl_status.setText("✅ 抠图完成(GrabCut)")    self._refresh()# ---- 颜色 ----def _on_color_change(self, idx):    if 0 <= idx < len(BG_COLORS):        self.bg_color = BG_COLORS[idx][1]        if self.bg_color != "transparent":            self.color_preview.setStyleSheet(f"background:{self.bg_color};border:1px solid #ccc;border-radius:4px;")        else:            self.color_preview.setStyleSheet("background:repeating-conic-gradient(#ddd 0% 25%,#fff 0% 50%) 50%/10px 10px;border:1px solid #ccc;border-radius:4px;")        self._refresh()def _set_color(self, hex_c):    self.bg_color = hex_c    self.color_preview.setStyleSheet(f"background:{hex_c};border:1px solid #ccc;border-radius:4px;")    self._refresh()def _pick_color(self):    c = QColorDialog.getColor(QColor(self.bg_color if self.bg_color != "transparent" else "#FFFFFF"), self, "选择背景色")    if c.isValid():        self._set_color(c.name())# ---- 尺寸 ----def _on_size_change(self, idx):    if 0 <= idx < len(SIZES):        _, w, h = SIZES[idx]        if w > 0 and h > 0:            self.sp_w.setValue(w)            self.sp_h.setValue(h)# ---- 刷新预览 ----def _refresh(self):    if self.fg_rgba is None: return    w, h = self.sp_w.value(), self.sp_h.value()    result = compose(self.fg_rgba, self.bg_color, w, h)    self.preview_result.set_image(pil_to_qpixmap(result))# ---- 导出 ----def _export(self, fmt):    if self.fg_rgba is None:        QMessageBox.warning(self, "提示", "请先上传并抠图"); return    w, h = self.sp_w.value(), self.sp_h.value()    result = compose(self.fg_rgba, self.bg_color, w, h)    ext = "PNG (*.png)" if fmt == "png" else "JPEG (*.jpg)"    path, _ = QFileDialog.getSaveFileName(self, "导出图片", f"photo.{fmt}", ext)    if not path: return    if fmt == "jpg":        result = result.convert("RGB")    result.save(path, quality=95)    self.lbl_status.setText(f"✅ 已导出: {os.path.basename(path)}")    QMessageBox.information(self, "成功", f"已保存到:\n{path}\n尺寸: {w}x{h}")==================== 启动 ====================if name == "main":app = QApplication(sys.argv)app.setFont(QFont("Microsoft YaHei", 10))win = PhotoToolApp()win.show()sys.exit(app.exec_())

最新文章

随机文章