找回密码
 立即注册
首页 业界区 安全 基于二维码的图像正射矫正工具 —— 用于三维建模辅助 ...

基于二维码的图像正射矫正工具 —— 用于三维建模辅助

彼瞄 2026-3-14 17:15:00
拍照辅助三维建模 → 角度不垂直 → 用二维码做标定 → 矫正成正射图 → 用于描边、测量、辅助建模。
基于二维码的图像正射矫正工具——用于三维建模辅助

在使用手机或相机拍摄物体进行三维建模辅助时,经常会遇到拍摄角度无法完全垂直的问题,导致照片存在透视畸变,无法直接用于图像描边、尺寸测量、正射底图等后续建模工作。
为了解决这个问题,我基于开源二维码矫正逻辑,开发了这款二维码辅助图像正射矫正工具,可以快速将倾斜拍摄的图片矫正为无透视畸变的正射图,大幅提升建模前期素材质量。
工具用途与背景


  • 拍摄物体用于三维建模、手绘描边、尺寸标注、平面重建
  • 手机/相机难以做到绝对垂直拍摄,图像存在透视变形
  • 在拍摄场景中放入二维码作为标定参照物
  • 通过二维码定位 + 透视变换,输出标准化正射图像
  • 矫正后的图片可直接用于:

    • 正射底图
    • 轮廓描边
    • 尺寸测量
    • 建模参考

核心功能


  • 二维码标定 + 图像正射矫正
    利用画面中的二维码做坐标标定,自动计算透视变换矩阵,将倾斜照片矫正为垂直视角的正射图。
  • 批量图片处理
    支持多选图片批量矫正,适合建模素材批量预处理。
  • 可视化界面(扁平简约风格)

    • 选择图片
    • 开始处理
    • 清空列表
    • 打开文件位置
      全程图形化操作,无需命令行。

  • 详细信息列表展示

    • 文件名
    • 文件路径
    • 文件大小
    • 处理状态(成功 ✅ / 失败 ❌)

  • 实时进度条 + 多线程防卡顿
    处理图片时界面不会卡死,进度实时可见。
  • 自动保存到原文件夹
    输出文件命名:原文件名_corrected.png,方便整理建模素材。
实现原理


  • 二维码检测
    使用 pyzbar 识别图像中的二维码,获取四个角点坐标。
  • 角点排序
    通过坐标和与差值排序,确定左上、右上、右下、左下四个顶点。
  • 透视变换矫正
    使用 OpenCV 计算投影变换矩阵,将图像矫正为正射投影图,消除拍摄角度带来的畸变。
  • GUI 界面与多线程
    基于 tkinter + ttkbootstrap 构建界面,多线程处理耗时任务,保证流畅操作。
使用流程


  • 拍摄物体时,在场景内放入二维码作为标定参照物
  • 使用本工具选择图片
  • 一键批量矫正为正射图
  • 直接使用矫正后的图片进行:

    • 描线建模
    • 尺寸测量
    • 平面重建
    • 纹理校正

环境依赖
  1. pip install opencv-python numpy pyzbar ttkbootstrap pillow
复制代码
代码来源说明

本项目核心二维码矫正逻辑参考并改进自开源代码:
https://github.com/wywzxxz/qrcoderectification/blob/main/main.ipynb
在原版基础上,我进行了工程化扩展

  • 封装为可批量处理的函数
  • 增加完整 GUI 可视化界面
  • 添加多线程、进度条、文件列表、状态显示
  • 支持一键打开输出文件夹
  • 优化异常处理与工具稳定性
完整代码
  1. import cv2import numpy as npimport osimport threadingimport tkinter as tkfrom tkinter import ttkimport ttkbootstrap as ttkbfrom ttkbootstrap.constants import *from tkinter import filedialog, messageboxfrom pyzbar import pyzbarfrom PIL import Image, ImageTkimport mathimport subprocess  # 新增:跨平台打开文件夹def order_points(pts):    """排序二维码的四个顶点(左上、右上、右下、左下)"""    rect = np.zeros((4, 2), dtype="float32")    s = pts.sum(axis=1)    rect[0] = pts[np.argmin(s)]    rect[2] = pts[np.argmax(s)]    diff = np.diff(pts, axis=1)    rect[1] = pts[np.argmin(diff)]    rect[3] = pts[np.argmax(diff)]    return rectdef get_file_size(file_path):    """获取文件大小,转换为KB/MB单位"""    size = os.path.getsize(file_path)    if size < 1024:        return f"{size} B"    elif size < 1024 * 1024:        return f"{round(size / 1024, 2)} KB"    else:        return f"{round(size / (1024 * 1024), 2)} MB"def correct_qrcode(image_path):    """矫正单张图片中的二维码,返回(是否成功, 错误信息/保存路径)"""    try:        image = cv2.imread(image_path)        if image is None:            return False, "图片读取失败"                decoded_objects = pyzbar.decode(image)        if len(decoded_objects) == 0:            return False, "未识别到二维码"                pts = np.array(decoded_objects[0].polygon, dtype=np.int32)        rect = order_points(pts)        (tl, tr, br, bl) = rect                width = np.sqrt(((tr[0] - tl[0])**2) + ((tr[1] - tl[1])**2))        dst = np.array([            [tl[0], tl[1]],            [tl[0] + width - 1, tl[1]],            [tl[0] + width - 1, tl[1] + width - 1],            [tl[0], tl[1] + width - 1]        ], dtype="float32")                orig_height, orig_wid = image.shape[:2]        M = cv2.getPerspectiveTransform(rect, dst)        warped = cv2.warpPerspective(src=image, M=M, dsize=(orig_wid, orig_height))                # 保存到原文件夹        dir_name = os.path.dirname(image_path)        file_name = os.path.splitext(os.path.basename(image_path))[0]        save_path = os.path.join(dir_name, f"{file_name}_corrected.png")        cv2.imwrite(save_path, warped)                return True, save_path        except Exception as e:        return False, str(e)def open_file_location(file_path):    """跨平台打开文件所在文件夹并定位到文件"""    try:        if not os.path.exists(file_path):            messagebox.showwarning("提示", "文件不存在!")            return                # 不同系统的打开方式        if os.name == 'nt':  # Windows            os.startfile(os.path.dirname(file_path))        elif os.name == 'posix':  # Mac/Linux            if 'darwin' in os.uname().sysname.lower():  # Mac                subprocess.run(['open', '-R', file_path])            else:  # Linux                subprocess.run(['xdg-open', os.path.dirname(file_path)])        # messagebox.showinfo("提示", f"已打开文件所在文件夹:\n{os.path.dirname(file_path)}")    except Exception as e:        messagebox.showerror("错误", f"打开文件夹失败:{str(e)}")class QRCodeCorrectorApp:    def __init__(self, root):        self.root = root        self.root.title("二维码矫正工具")        self.root.geometry("850x600")        self.root.resizable(False, False)                # 存储选中的图片信息:{索引: (文件名, 路径, 大小, 状态, 保存路径)}        self.image_list = []        self.current_index = 0        self.success_save_paths = []  # 存储处理成功的文件保存路径                # 初始化界面        self._setup_ui()        def _setup_ui(self):        """搭建界面布局"""        # 1. 顶部插画+标题区域        top_frame = ttkb.Frame(self.root, bootstyle=LIGHT)        top_frame.pack(fill=X, padx=10, pady=10)                # 简约二维码插画(用PIL绘制)        illustration = self._create_qr_illustration()        ill_label = ttkb.Label(top_frame, image=illustration)        ill_label.image = illustration        ill_label.pack(side=LEFT, padx=20)                # 标题        title_label = ttkb.Label(            top_frame,             text="二维码矫正工具",             font=("微软雅黑", 18, "bold"),            bootstyle=PRIMARY        )        title_label.pack(side=LEFT, padx=20, pady=10)                # 2. 按钮区域        btn_frame = ttkb.Frame(self.root, bootstyle=LIGHT)        btn_frame.pack(fill=X, padx=20, pady=5)                self.select_btn = ttkb.Button(            btn_frame,            text="选择图片",            command=self._select_images,            bootstyle=SUCCESS,            width=15        )        self.select_btn.pack(side=LEFT, padx=5)                self.process_btn = ttkb.Button(            btn_frame,            text="开始处理",            command=self._start_process_thread,            bootstyle=PRIMARY,            width=15        )        self.process_btn.pack(side=LEFT, padx=5)                self.clear_btn = ttkb.Button(            btn_frame,            text="清空列表",            command=self._clear_list,            bootstyle=SECONDARY,            width=15        )        self.clear_btn.pack(side=LEFT, padx=5)                # 新增:打开文件位置按钮(初始禁用)        self.open_folder_btn = ttkb.Button(            btn_frame,            text="打开文件位置",            command=self._open_selected_file_location,            bootstyle=INFO,            width=15,            state=DISABLED  # 初始禁用        )        self.open_folder_btn.pack(side=LEFT, padx=5)                # 3. 进度条        self.progress_var = tk.DoubleVar()        self.progress_bar = ttkb.Progressbar(            self.root,            variable=self.progress_var,            maximum=100,            bootstyle=SUCCESS        )        self.progress_bar.pack(fill=X, padx=20, pady=10)                # 4. 图片信息列表        list_frame = ttkb.Frame(self.root)        list_frame.pack(fill=BOTH, expand=True, padx=20, pady=5)                # 列表表头        columns = ("文件名", "路径", "大小", "处理状态")        self.tree = ttkb.Treeview(            list_frame,            columns=columns,            show="headings",            bootstyle=LIGHT        )                # 设置列宽和表头        self.tree.heading("文件名", text="文件名")        self.tree.heading("路径", text="文件路径")        self.tree.heading("大小", text="文件大小")        self.tree.heading("处理状态", text="处理状态")                self.tree.column("文件名", width=150)        self.tree.column("路径", width=400)        self.tree.column("大小", width=80)        self.tree.column("处理状态", width=80)                # 滚动条        scrollbar = ttkb.Scrollbar(            list_frame,            orient=VERTICAL,            command=self.tree.yview        )        self.tree.configure(yscrollcommand=scrollbar.set)                self.tree.pack(side=LEFT, fill=BOTH, expand=True)        scrollbar.pack(side=RIGHT, fill=Y)        def _create_qr_illustration(self):        """绘制简约二维码插画(扁平风格)"""        # 创建空白画布        img = Image.new("RGB", (80, 80), (255, 255, 255))        draw = ImageDraw.Draw(img)                # 绘制二维码定位角(三个正方形)        # 左上        draw.rectangle((5, 5, 20, 20), fill=(0, 0, 0))        draw.rectangle((8, 8, 17, 17), fill=(255, 255, 255))        # 右上        draw.rectangle((60, 5, 75, 20), fill=(0, 0, 0))        draw.rectangle((63, 8, 72, 17), fill=(255, 255, 255))        # 左下        draw.rectangle((5, 60, 20, 75), fill=(0, 0, 0))        draw.rectangle((8, 63, 17, 72), fill=(255, 255, 255))                # 绘制随机小方块(模拟二维码点阵)        for i in range(30):            x = random.randint(25, 55)            y = random.randint(25, 55)            draw.rectangle((x, y, x+2, y+2), fill=(0, 0, 0))                # 转换为tkinter可用格式        img = img.resize((80, 80), Image.Resampling.LANCZOS)        return ImageTk.PhotoImage(img)        def _select_images(self):        """选择多张图片并添加到列表"""        file_types = [            ("图片文件", "*.jpg *.jpeg *.png *.bmp *.tiff"),            ("所有文件", "*.*")        ]        file_paths = filedialog.askopenfilenames(title="选择需要处理的图片", filetypes=file_types)        if not file_paths:            return                # 清空原有列表(可选,也可追加)        self._clear_list()                # 添加新选中的图片到列表        for path in file_paths:            file_name = os.path.basename(path)            file_size = get_file_size(path)            # 新增保存路径字段,初始为空            self.image_list.append((file_name, path, file_size, "未处理", ""))            # 插入到Treeview            self.tree.insert("", END, values=(file_name, path, file_size, "未处理"))                # 重置进度条        self.progress_var.set(0)        # 禁用打开文件按钮        self.open_folder_btn.config(state=DISABLED)        def _clear_list(self):        """清空图片列表"""        self.image_list = []        self.success_save_paths = []  # 清空成功路径列表        for item in self.tree.get_children():            self.tree.delete(item)        self.progress_var.set(0)        # 禁用打开文件按钮        self.open_folder_btn.config(state=DISABLED)        def _start_process_thread(self):        """启动多线程处理图片,避免界面卡顿"""        if not self.image_list:            messagebox.showwarning("提示", "请先选择需要处理的图片!")            return                # 清空成功路径列表        self.success_save_paths = []                # 禁用按钮,防止重复点击        self.process_btn.config(state=DISABLED)        self.select_btn.config(state=DISABLED)        self.open_folder_btn.config(state=DISABLED)                # 启动子线程处理        process_thread = threading.Thread(target=self._process_images)        process_thread.daemon = True  # 主线程退出时子线程也退出        process_thread.start()        def _process_images(self):        """批量处理图片(在子线程中执行)"""        total = len(self.image_list)        for idx, (file_name, path, size, _, _) in enumerate(self.image_list):            # 处理单张图片            success, msg = correct_qrcode(path)                        # 更新状态(必须在主线程中操作UI)            self.root.after(0, self._update_item_status, idx, success, msg)                        # 更新进度条            progress = (idx + 1) / total * 100            self.root.after(0, self.progress_var.set, progress)                # 处理完成后恢复按钮状态(主线程)        self.root.after(0, self._process_complete)        def _update_item_status(self, idx, success, msg):        """更新列表中图片的处理状态"""        # 更新内存中的状态        if success:            status = "✅ 成功"            self.success_save_paths.append(msg)  # 保存成功的文件路径            self.image_list[idx] = (self.image_list[idx][0], self.image_list[idx][1], self.image_list[idx][2], status, msg)        else:            status = f"❌ 失败:{msg}"            self.image_list[idx] = (self.image_list[idx][0], self.image_list[idx][1], self.image_list[idx][2], status, "")                # 更新Treeview显示        item = self.tree.get_children()[idx]        self.tree.item(item, values=(            self.image_list[idx][0],            self.image_list[idx][1],            self.image_list[idx][2],            self.image_list[idx][3]        ))        def _process_complete(self):        """处理完成后的回调"""        self.process_btn.config(state=NORMAL)        self.select_btn.config(state=NORMAL)        # 如果有成功处理的文件,启用打开文件按钮        if self.success_save_paths:            self.open_folder_btn.config(state=NORMAL)        # messagebox.showinfo("完成", "所有图片处理完毕!")        def _open_selected_file_location(self):        """打开文件位置的回调函数"""        if not self.success_save_paths:            messagebox.showwarning("提示", "暂无处理成功的文件!")            return        # 打开第一个成功处理的文件所在文件夹        open_file_location(self.success_save_paths[0])# 补充缺失的导入(PIL的ImageDraw和random)from PIL import ImageDrawimport randomif __name__ == "__main__":    # 使用ttkbootstrap的扁平主题    root = ttkb.Window(themename="flatly")    app = QRCodeCorrectorApp(root)    root.mainloop()
复制代码
运行效果

1.png

2.png


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册