前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >CodeBuddy-打造一款科研论文绘图利器PlotBuddy

CodeBuddy-打造一款科研论文绘图利器PlotBuddy

原创
作者头像
Vaeeeee
修改于 2025-05-21 16:30:27
修改于 2025-05-21 16:30:27
28700
代码可运行
举报
运行总次数:0
代码可运行

我正在参加CodeBuddy「首席试玩官」内容创作大赛,本文所使用的 CodeBuddy 免费下载链接:腾讯云代码助手 CodeBuddy - AI 时代的智能编程伙伴


前言

在科研写作过程中,“画图”往往是一件令人头疼却又不得不精益求精的工作。无论是模型框架、流程图、对比图,还是实验结果可视化。其中,经常会出现一些局部细节放大展示的图像,也就是“画中画”的效果,如下所示:

为了快速实现这一效果,我选择基于CodeBuddy平台打造了专注于科研绘图的轻量工具——PlotBuddy。它不仅支持高质量的“画中画”图像生成,还提供可视化参数控制、样式定制、多区域联动放大等功能,帮助科研人员更高效地完成论文插图工作,让科研绘图也能“所见即所得”。

之前也写过实现该功能的一个代码,不过非常粗糙,只是满足基本使用而已。正好最近腾讯自研了一款AI编程提效工具——CodeBuddy,是国内首个支持 MCP 的代码助手,还为开发者提供开发智能体 Craft、智能代码补全、单元测试、代码诊断等多项高效功能。于是打算借助CodeBuddy的力量来打造一款绘图利器——PlotBuddy,满足科研工作者需求,又具备良好可拓展性的论文绘图利器。同时,也来测试一下CodeBuddy这款工具的实际效果。👀


  • 先看原始代码(写得比较混乱🤦‍♂️):
代码语言:python
代码运行次数:0
运行
AI代码解释
复制
import cv2
import numpy as np

def save_magnify(path,a,b): # a:矩形框左上角坐标  ; b:矩形框右下角坐标
    imagee = cv2.imread(path)
    lw = 2
    # 截出放大的部分
    img_magnify = imagee[a[1]-lw:b[1]+lw,a[0]-lw:b[0]+lw,:]
    
    # 计算缩放比例
    new_W = imagee.shape[1] - 2*lw
    ratio = float(new_W) / img_magnify.shape[1]

    # 根据缩放比例计算缩放后的尺寸
    new_height = int(img_magnify.shape[0] * ratio)
    new_size = (new_W,new_height)
    # 进行缩放
    resized_image = cv2.resize(img_magnify, new_size)

    # 添加边框
    border_size = lw 
    border_color = (0, 0, 255)  # 红色
    resized_image = cv2.copyMakeBorder(resized_image, border_size, border_size, border_size, border_size, cv2.BORDER_CONSTANT, value=border_color)

    # 画矩形框
    cv2.rectangle(imagee, a, b, (0, 0, 255), lw)

    # 拼接
    patch = np.vstack((imagee, resized_image))
    cv2.imshow(path.split('\\')[-1],patch)
    cv2.waitKey(0)
    # cv2.imwrite("E:\png_jpg"+  '/' + path.split('\\')[-1],patch)



# 全局变量
drawing = False  # 是否正在绘制矩形框
start_x, start_y = -1, -1  # 矩形框左上角坐标
end_x, end_y = -1, -1  # 矩形框右下角坐标


def draw_rectangle(event, x, y, flags, param):
    global drawing, start_x, start_y, end_x, end_y

    if event == cv2.EVENT_LBUTTONDOWN:        # 左键击下 
        drawing = True
        start_x, start_y = x, y

    elif event == cv2.EVENT_LBUTTONUP:        # 左键弹起
        drawing = False
        end_x, end_y = x, y

    if drawing:
        # 绘制矩形框
        image_copy = image.copy()
        cv2.rectangle(image_copy, (start_x, start_y), (x, y), (0, 0, 255), 2)
        cv2.imshow("Image", image_copy)

    if not drawing and start_x != -1 and start_y != -1:
        # 显示矩形框的左上角和右下角坐标
        print(f"矩形框左上角坐标:({start_x}, {start_y})")
        print(f"矩形框右下角坐标:({end_x}, {end_y})")
        for i in range(1):
            save_magnify(image_path,a = (start_x,start_y),b = (end_x,end_y))
            save_magnify(image_path_2,a = (start_x,start_y),b = (end_x,end_y))
            break
        start_x = -1
        

if __name__ == '__main__':
    # 输入图像路径
    image_path = r'F:\Picture\a1.jpg'
    image_path_2 = r'F:\Picture\a2.jpg'

    # 读取图像
    global imagee 
    image = cv2.imread(image_path)

    # 创建窗口并显示图像
    cv2.namedWindow("Image")
    cv2.imshow("Image", image)

    # 设置鼠标回调函数
    cv2.setMouseCallback("Image", draw_rectangle)

    # 循环等待按键事件
    while True:

        key = cv2.waitKey(1) & 0xFF

        # 按下 'q' 键 或者 退出窗口时  退出程序
        if key == ord('w') or cv2.getWindowProperty("Image", cv2.WND_PROP_VISIBLE) < 1:
            break

    cv2.destroyAllWindows()
  • 先让CodeBuddy帮我优化一下整个代码结构和逻辑,CodeBuddy的回答如下:

重构后的代码如下:

代码语言:python
代码运行次数:0
运行
AI代码解释
复制
import cv2
import numpy as np
import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox

class ImageMagnifier:
    def __init__(self):
        # 初始化变量
        self.image_path = None
        self.image = None
        self.drawing = False
        self.preview_mode = False
        self.start_point = (-1, -1)
        self.end_point = (-1, -1)
        self.preview_window_name = "Preview"
        self.main_window_name = "Image"
        self.lw = 2  # 线宽

    def select_image(self):
        """打开文件选择对话框选择图片"""
        root = tk.Tk()
        root.withdraw()  # 隐藏主窗口
        file_path = filedialog.askopenfilename(
            title="选择图片",
            filetypes=[
                ("Image files", "*.jpg *.jpeg *.png *.bmp"),
                ("All files", "*.*")
            ]
        )
        if file_path:
            self.image_path = file_path
            self.image = cv2.imread(self.image_path)
            if self.image is None:
                messagebox.showerror("错误", "无法加载所选图片")
                return False
            return True
        return False

    def create_magnified_image(self, start_point, end_point):
        """创建放大的图像"""
        # 确保坐标在有效范围内
        x1, y1 = max(self.lw, start_point[0]), max(self.lw, start_point[1])
        x2, y2 = min(self.image.shape[1]-self.lw, end_point[0]), min(self.image.shape[0]-self.lw, end_point[1])
        
        # 截取选中区域
        img_magnify = self.image[y1-self.lw:y2+self.lw, x1-self.lw:x2+self.lw].copy()
        
        # 计算缩放比例
        new_W = self.image.shape[1] - 2*self.lw
        ratio = float(new_W) / img_magnify.shape[1]
        new_height = int(img_magnify.shape[0] * ratio)
        
        # 缩放图像
        resized_image = cv2.resize(img_magnify, (new_W, new_height))
        
        # 添加边框
        resized_image = cv2.copyMakeBorder(
            resized_image, 
            self.lw, self.lw, self.lw, self.lw, 
            cv2.BORDER_CONSTANT, 
            value=(0, 0, 255)
        )
        
        # 在原图上画矩形
        image_with_rect = self.image.copy()
        cv2.rectangle(image_with_rect, (x1, y1), (x2, y2), (0, 0, 255), self.lw)
        
        # 拼接图像
        return np.vstack((image_with_rect, resized_image))

    def mouse_callback(self, event, x, y, flags, param):
        """鼠标事件回调函数"""
        if event == cv2.EVENT_LBUTTONDOWN:  # 左键按下
            self.drawing = True
            self.start_point = (x, y)
            self.preview_mode = False
            
        elif event == cv2.EVENT_MOUSEMOVE and self.drawing:  # 鼠标移动
            # 显示实时框选效果
            temp_img = self.image.copy()
            cv2.rectangle(temp_img, self.start_point, (x, y), (0, 0, 255), self.lw)
            cv2.imshow(self.main_window_name, temp_img)
            
        elif event == cv2.EVENT_LBUTTONUP:  # 左键释放
            self.drawing = False
            self.end_point = (x, y)
            self.preview_mode = True
            
            # 显示预览
            result = self.create_magnified_image(self.start_point, self.end_point)
            cv2.imshow(self.preview_window_name, result)
            cv2.moveWindow(self.preview_window_name, 100, 100)  # 调整预览窗口位置

    def run(self):
        """主运行函数"""
        if not self.select_image():
            return

        cv2.namedWindow(self.main_window_name)
        cv2.imshow(self.main_window_name, self.image)
        cv2.setMouseCallback(self.main_window_name, self.mouse_callback)

        while True:
            key = cv2.waitKey(1) & 0xFF
            
            if key == ord('q'):  # 按q退出
                break
            elif key == ord('c'):  # 按c取消当前选择
                if self.preview_mode:
                    cv2.destroyWindow(self.preview_window_name)
                    cv2.imshow(self.main_window_name, self.image)
                    self.preview_mode = False
                    self.start_point = (-1, -1)
                    self.end_point = (-1, -1)
            elif key == ord('s'):  # 按s保存当前结果
                if self.preview_mode:
                    result = self.create_magnified_image(self.start_point, self.end_point)
                    save_path = self.image_path.rsplit('.', 1)[0] + '_magnified.' + self.image_path.rsplit('.', 1)[1]
                    cv2.imwrite(save_path, result)
                    messagebox.showinfo("成功", f"图片已保存至:\n{save_path}")
            
            # 检查窗口是否被关闭
            if cv2.getWindowProperty(self.main_window_name, cv2.WND_PROP_VISIBLE) < 1:
                break

        cv2.destroyAllWindows()

if __name__ == '__main__':
    magnifier = ImageMagnifier()
    magnifier.run()
  • 问题分析、结构优化、功能展示、代码生成。。。一气呵成,体验感拉满了,确实好用!🐱‍🏍所以我要做的就是提需求、提需求、还是™的提需求就可以了。

需求🖍

  • 观察前面给出的一些论文插图以及结合目前的代码,发现主要有以下这些需求:

功能点

说明

放大区域位置可选

放大视图不局限在下方,比如左上、右下等,支持自定义

多个放大区域

支持多处细节放大展示,互不遮挡,且区域颜色要对应

区域颜色匹配

放大框与放大视图边框颜色一致,便于区分

距离/间隔优化

多个放大区域需合理排布,避免重叠

样式可定制

放大框颜色、线宽等支持自定义

多图联动放大

对一张图的操作能同步到其它图(对比图中很常见)

  • 与其手动修改或逐段生成代码,不如直接将上述需求交给 CodeBuddy 的 Craft 功能来处理。相比于传统的 Chat功能 ,Craft 支持对完整代码文件进行批量生成、结构化修改与智能优化,能够一键完成整个代码文件的创建,大幅提升开发效率。
  • 在修改代码时,CodeBuddy 还会同时提供两个版本的差异对比,清晰标注出每处内容变更的位置,极大地方便了查阅与确认。如果修改后的文件运行无误,可以直接点击修改,一键更新最新的代码,这个功能还是非常好用的。👍 图图图图图
  • 所以直接将上面需求扔给CodeBuddy:
  • 给出的回答依旧很全面:
  • 不过可能是需求一次性提太多,导致实际上很多功能没有实现,最终效果如下,直接默认在下方显示,并且多个区域叠加在一起了。
  • 后面我分步提了需求,逐步进行优化,总之就是在它的基础上完善一些小细节。尽管CodeBuddy十分强大,但是强大的背后往往少不了高手的引导🤷‍♀️ 经过多轮的提需求和细节优化,最终的代码如下:

代码

代码语言:python
代码运行次数:0
运行
AI代码解释
复制
import sys
import cv2
import numpy as np
from PIL import Image
import tkinter as tk
from tkinter import filedialog, messagebox, colorchooser, ttk, simpledialog
import os

class ImageMagnifier:
    def __init__(self):
        # 初始化变量
        self.image_path          = None
        self.image               = None
        self.drawing             = False
        self.preview_mode        = False
        self.start_point         = (-1, -1)
        self.end_point           = (-1, -1)
        self.preview_window_name = "Preview"
        self.main_window_name    = "Image"
        self.preview             = None
        
        # 可自定义参数
        self.lw             = 2  # 线宽
        self.region_count   = 1  # 放大区域个数
        self.current_region = 0  # 当前区域索引
        self.border_color   = [(0, 0, 255)]  # 存储边框的颜色
        self.boxes          = []  # 保存放大区域的坐标
        self.area           = 'Down' # 默认放大区域显示在图像下方 D U L R
        self.gap            = 0 # 区域间距

    def show_settings_dialog(self):
        """显示参数设置对话框"""
        # 创建对话框窗口
        settings_window = tk.Tk()
        settings_window.title("参数设置")
        settings_window.geometry("400x400")  # 调整高度以适应可能增加的颜色设置
        settings_window.resizable(False, False)
        
        # 居中显示
        settings_window.update_idletasks()
        width = settings_window.winfo_width()
        height = settings_window.winfo_height()
        x = (settings_window.winfo_screenwidth() // 2) - (width // 2)
        y = (settings_window.winfo_screenheight() // 2) - (height // 2)
        settings_window.geometry(f'{width}x{height}+{x}+{y}')
        
        # 创建主框架
        main_frame = ttk.Frame(settings_window, padding="20")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 区域个数设置 - 使用加减按钮
        ttk.Label(main_frame, text="放大区域个数:").grid(row=0, column=0, sticky=tk.W, pady=10)
        
        region_frame = ttk.Frame(main_frame)
        region_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=10)
        
        region_count_var = tk.IntVar(value=self.region_count)
        
        def decrease_region_count():
            current = region_count_var.get()
            if current > 1:
                region_count_var.set(current - 1)
                # 强制更新显示
                region_count_entry.configure(state='normal')
                region_count_entry.delete(0, tk.END)
                region_count_entry.insert(0, region_count_var.get())
                region_count_entry.configure(state='readonly')
        
        def increase_region_count():
            current = region_count_var.get()
            if current < 4:
                region_count_var.set(current + 1)
                # 强制更新显示
                region_count_entry.configure(state='normal')
                region_count_entry.delete(0, tk.END)
                region_count_entry.insert(0, region_count_var.get())
                region_count_entry.configure(state='readonly')
        
        ttk.Button(region_frame, text="-", width=5, command=decrease_region_count).pack(side=tk.LEFT)
        region_count_entry = ttk.Entry(region_frame, textvariable=region_count_var, width=5, justify=tk.CENTER)
        region_count_entry.pack(side=tk.LEFT, padx=5)
        region_count_entry.configure(state='readonly')
        ttk.Button(region_frame, text="+", width=5, command=increase_region_count).pack(side=tk.LEFT)
        
        # 只需要放大一个区域时,不需要设置放大区域间隔
        def on_region_count_change(*args):
            if region_count_var.get() == 1:
                gap_entry.configure(state='disabled')
            else:
                gap_entry.configure(state='normal')
        region_count_var.trace_add("write", on_region_count_change)

        
        # 线宽设置 - 使用加减按钮
        ttk.Label(main_frame, text="线宽设置:").grid(row=1, column=0, sticky=tk.W, pady=10)
        
        line_width_frame = ttk.Frame(main_frame)
        line_width_frame.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=10)
        
        line_width_var = tk.IntVar(value=self.lw)

        
        # 区域位置选择 - 使用下拉框
        ttk.Label(main_frame, text="放大区域位置:").grid(row=2, column=0, sticky=tk.W, pady=10)

        position_options = ["Down", "Up", "Left", "Right"]
        position_var = tk.StringVar(value=position_options[0])
        position_combobox = ttk.Combobox(main_frame, textvariable=position_var, values=position_options, state="readonly", width=5)
        position_combobox.grid(row=2, column=1, sticky=tk.W, pady=10)
        position_combobox.current(0)
        ## 实时获取每次下拉框中选择的值
        def get_area(event):
            self.area = position_combobox.get()
        position_combobox.bind("<<ComboboxSelected>>", get_area)
        
        # 放大区域间隔设置
        ttk.Label(main_frame, text="放大区域间隔:").grid(row=2, column=2, sticky=tk.W, padx=5)
        gap_var = tk.IntVar(value=self.region_gap if hasattr(self, 'region_gap') else 10)  # 设置初始值,可设为实例变量

        def validate_integer_input(new_value):
            if new_value == "":
                return True  # 允许清空
            try:
                value = int(new_value)
                if self.area == 'Up' or 'Down':
                    return 0 <= value <= self.image.shape[1] // 2  # 最大不要超过图像W/2
                else:
                    return 0 <= value <= self.image.shape[0] // 2  # 最大不要超过图像H/2
            except ValueError:
                return False
        ## 实时获取设置的 放大区域间隔
        def get_gap(event):
            self.gap = gap_entry.get()

        vcmd = (settings_window.register(validate_integer_input), '%P')

        gap_entry = ttk.Entry(main_frame, textvariable=gap_var, width=5, validate='key', validatecommand=vcmd)
        gap_entry.grid(row=2, column=3, sticky=tk.W)
        gap_entry.bind("<KeyRelease>", get_gap)

        
        def decrease_line_width():
            current = line_width_var.get()
            if current > 1:
                line_width_var.set(current - 1)
                # 强制更新显示
                line_width_entry.configure(state='normal')
                line_width_entry.delete(0, tk.END)
                line_width_entry.insert(0, line_width_var.get())
                line_width_entry.configure(state='readonly')
        
        def increase_line_width():
            current = line_width_var.get()
            if current < 10:
                line_width_var.set(current + 1)
                # 强制更新显示
                line_width_entry.configure(state='normal')
                line_width_entry.delete(0, tk.END)
                line_width_entry.insert(0, line_width_var.get())
                line_width_entry.configure(state='readonly')
        
        ttk.Button(line_width_frame, text="-", width=5, command=decrease_line_width).pack(side=tk.LEFT)
        line_width_entry = ttk.Entry(line_width_frame, textvariable=line_width_var, width=5, justify=tk.CENTER)
        line_width_entry.pack(side=tk.LEFT, padx=5)
        line_width_entry.configure(state='readonly')
        ttk.Button(line_width_frame, text="+", width=5, command=increase_line_width).pack(side=tk.LEFT)
        
        # 颜色设置框架 - 动态更新
        color_frame = ttk.LabelFrame(main_frame, text="边框颜色设置")
        color_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
        
        # 颜色预览和选择组件列表
        color_previews = []
        color_buttons = []
        
        # 初始化颜色设置UI
        def update_color_ui():
            # 清空现有组件
            for widget in color_frame.winfo_children():
                widget.destroy()
            
            # 获取当前区域数量
            count = region_count_var.get()
            
            # 确保颜色列表长度足够
            while len(self.border_color) < count:
                self.border_color.append((0, 0, 255))  # 默认添加蓝色
            if len(self.border_color) > count:
                self.border_color = self.border_color[:count]
            
            # 创建对应数量的颜色设置UI
            color_previews.clear()
            color_buttons.clear()
            
            for i in range(count):
                # 颜色标签
                ttk.Label(color_frame, text=f"区域 {i+1} 颜色:").grid(row=i, column=0, sticky=tk.W, pady=5)
                
                # 当前颜色预览
                preview = tk.Canvas(color_frame, width=30, height=20, bg='white')
                preview.grid(row=i, column=1, sticky=tk.W, pady=5)
                color_previews.append(preview)
                
                # BGR转RGB显示
                bgr_color = self.border_color[i]
                rgb_color = f'#{bgr_color[2]:02x}{bgr_color[1]:02x}{bgr_color[0]:02x}'
                preview.create_rectangle(2, 2, 28, 18, fill=rgb_color, outline="")
                
                # 颜色选择按钮
                def choose_color(index=i):
                    # BGR转RGB
                    bgr_color = self.border_color[index]
                    rgb_color = (bgr_color[2], bgr_color[1], bgr_color[0])
                    
                    color = colorchooser.askcolor(
                        color=f'#{rgb_color[0]:02x}{rgb_color[1]:02x}{rgb_color[2]:02x}',
                        title=f"选择区域 {index+1} 边框颜色"
                    )
                    
                    if color[0] is not None:
                        r, g, b = [int(x) for x in color[0]]
                        # RGB转BGR存储
                        self.border_color[index] = (b, g, r)
                        color_previews[index].create_rectangle(2, 2, 28, 18, fill=color[1], outline="")
                
                button = ttk.Button(color_frame, text="选择颜色", command=choose_color)
                button.grid(row=i, column=2, sticky=tk.W, pady=5)
                color_buttons.append(button)
        
        # 初始更新颜色UI
        update_color_ui()
        
        # 区域数量变化时更新颜色UI
        region_count_var.trace_add("write", lambda *args: update_color_ui())
        
        # 确定和取消按钮
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=4, column=0, columnspan=3, pady=20)
        
        def on_ok():
            # 更新类成员变量
            self.region_count = region_count_var.get()
            self.lw = line_width_var.get()
            self.current_region = 0  # 重置当前区域索引
            settings_window.destroy()
            settings_window.quit()
        
        ttk.Button(button_frame, text="确定", command=on_ok).pack(side=tk.LEFT, padx=10)
        ttk.Button(button_frame, text="取消", command=settings_window.destroy).pack(side=tk.LEFT, padx=10)
        
        # 显示对话框并等待
        settings_window.mainloop()

    def show_save_dialog(self, final_img):
        """显示结果保存对话框"""
            
        def save_image():
            # 打开文件夹选择对话框
            folder_selected = filedialog.askdirectory(title="选择保存图像的文件夹")
            if folder_selected:
                save_path = os.path.join(folder_selected, "final_result.png")
                cv2.imwrite(save_path, final_img)
                messagebox.showinfo("保存成功", f"图像已保存到:{save_path}")
            else:
                messagebox.showwarning("未保存", "未选择文件夹,图像未保存。")
            window.quit()
            window.destroy()
            cv2.destroyAllWindows()
            sys.exit()


        def batch_process():
            messagebox.showinfo(
                "注意事项!!!",
                "请保证要处理的图像都在同一文件夹下,且该文件夹下所有图片大小一致,图片名称不同!"
            )
            folder_path = filedialog.askdirectory(title="选择批量处理图片所在的文件夹")
            
            # 在folder_path 同级新建一个结果文件夹
            new_folder_path = os.path.join(os.path.dirname(folder_path), 'Result')
            os.makedirs(new_folder_path, exist_ok=True)
            cv2.imwrite(os.path.join(new_folder_path, 'current_image_result.png'), final_img)
            
            if folder_path:
                image_names = os.listdir(folder_path)

                messagebox.showinfo("提示", f"共找到 {len(image_names)} 张图片,即将进行批量处理。")
                print("需要处理的图像:", image_names)

                print('正在处理......')
                for img_name in image_names:
                    img_path = os.path.join(folder_path, img_name)

                    self.preview = cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), cv2.IMREAD_COLOR)
                    self.image = self.preview.copy()
                    for i in range(len(self.boxes)):
                        cv2.rectangle(self.preview, self.boxes[i][0], self.boxes[i][1],
                        self.border_color[i], self.lw)
                    
                    res = self.image_stitchingh() 
                    cv2.imshow(f'{img_name}', res)
                    cv2.imwrite(os.path.join(new_folder_path, f'{img_name.split(".")[0]}-result.{img_name.split(".")[1]}'), res)
                message = tk.Tk()
                message.withdraw()
                messagebox.showinfo("提示", f"批量处理完成。结果已保存到:\n{new_folder_path}")
                message.quit()
                message.destroy()
                cv2.destroyAllWindows()
                sys.exit()

            window.destroy()

        def exit_program():
            messagebox.showinfo("退出", "程序已退出。")
            window.quit()
            window.destroy()
            cv2.destroyAllWindows()
            sys.exit()

        # 创建窗口
        window = tk.Tk()
        window.title("请选择要执行的操作")
        window.geometry("300x180")
        window.resizable(False, False)

        # 创建按钮
        btn1 = tk.Button(window, text="1. 直接保存(英文路径)", width=25, height=2, command=save_image)
        btn1.pack(pady=10)

        btn2 = tk.Button(window, text="2. 保存结果并批量处理其他图像", width=25, height=2, command=batch_process)
        btn2.pack(pady=10)

        btn3 = tk.Button(window, text="3. 不保存,直接退出", width=25, height=2, command=exit_program)
        btn3.pack(pady=10)

        # 启动事件循环
        window.mainloop()     
    def select_image(self):
        """打开文件选择对话框选择图片"""
        root = tk.Tk()
        root.withdraw()  # 隐藏主窗口
        file_path = filedialog.askopenfilename(
            title="选择图片",
            filetypes=[
                ("Image files", "*.jpg *.jpeg *.png *.bmp"),
                ("All files", "*.*")
            ]
        )
        if file_path:
            self.image_path = file_path
            self.image = cv2.imread(self.image_path)
            if self.image is None:
                messagebox.showerror("错误", "无法加载所选图片")
                return False
            return True
        return False

    def create_magnified_image(self, start_point, end_point, region_index=0):
        """创建放大的图像(支持多区域颜色)"""
        current_color = self.border_color[region_index] if self.border_color else (0, 0, 255)  # 默认红色

        # 坐标有效性检查
        x1, y1 = max(self.lw, start_point[0]), max(self.lw, start_point[1])
        x2, y2 = min(self.image.shape[1] - self.lw, end_point[0]), min(self.image.shape[0] - self.lw, end_point[1])
        
        # 处理无效区域(防止框选区域过小或反向)
        if x1 >= x2 or y1 >= y2:
            return None
        
        # 截取选中区域
        img_magnify = self.image[y1 - self.lw:y2 + self.lw, x1 - self.lw:x2 + self.lw].copy()
        
        # 处理空图像(例如框选到边界外)
        if img_magnify.size == 0:
            return None
        
        # 计算缩放比例(保持宽高比)
        original_width, original_height = img_magnify.shape[1], img_magnify.shape[0]
        new_width = self.image.shape[1] - 2 * self.lw  # 目标宽度(扣除边框)
        ratio = new_width / original_width if original_width != 0 else 1.0
        new_height = int(original_height * ratio)
        
        # 缩放图像(使用双线性插值保持平滑)
        resized_image = cv2.resize(img_magnify, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
        
        # 添加边框(使用当前区域颜色)
        resized_image = cv2.copyMakeBorder(
            resized_image,
            top=self.lw, bottom=self.lw, left=self.lw, right=self.lw,
            borderType=cv2.BORDER_CONSTANT,
            value=current_color
        )
        
        # 在原图上绘制矩形(使用当前区域颜色)
        image_with_rect = self.image.copy()
        cv2.rectangle(
            image_with_rect, (x1, y1), (x2, y2),
            color=current_color, thickness=self.lw, lineType=cv2.LINE_AA
        )
        
        return  resized_image


    def mouse_callback(self, event, x, y, flags, param):
        """鼠标事件回调函数(支持多区域绘制)"""
        # 确保颜色列表长度足够

        while len(self.border_color) < self.region_count:
            self.border_color.append((255, 0, 0))  # 默认添加蓝色
        
        if event == cv2.EVENT_LBUTTONDOWN:
            # 左键按下,开始新区域绘制
            if self.current_region >= self.region_count:
                return  # 已达到最大区域数
                
            self.drawing = True
            self.start_point = (x, y)
            self.preview_mode = False
            
        elif event == cv2.EVENT_MOUSEMOVE and self.drawing:
            # 鼠标移动,实时显示当前区域框选
            temp_img = self.image.copy()

            # 绘制已完成的区域
            for i in range(len(self.boxes)):
                cv2.rectangle(temp_img, self.boxes[i][0], self.boxes[i][1], 
                            self.border_color[i], self.lw) 

            # 绘制当前正在框选的区域
            if self.current_region < len(self.border_color):
                cv2.rectangle(temp_img, self.start_point, (x, y), 
                            self.border_color[self.current_region], self.lw)

            cv2.imshow(self.main_window_name, temp_img)
            self.preview = temp_img
            
        elif event == cv2.EVENT_LBUTTONUP:
            # 左键释放,完成当前区域
            if not self.drawing:
                return
                
            self.drawing = False
            self.end_point = (x, y)
            
            # 保存当前区域坐标
            self.boxes.append([self.start_point, self.end_point])

            # 更新预览- 每框选一个区域就显示其放大的图像
            self.current_region += 1
            if self.current_region <= self.region_count:
                self.update_preview()

            # 如果还有区域需要绘制,重置状态
            if self.current_region < self.region_count:
                self.drawing = False
                self.preview_mode = False
            else:
                self.preview_mode = True

    def image_stitchingh(self):
        """拼接图像"""

        magnified_All = []   # 保存所有待放大的区域 
        
        for box in self.boxes:
            x1, y1 = box[0][0], box[0][1]
            x2, y2 = box[1][0], box[1][1]
            img  = self.image[y1 - self.lw:y2 + self.lw, x1 - self.lw:x2 + self.lw].copy()
            magnified_All.append(img)

        if self.area == 'Down' or self.area == 'Up':
            ## 若放大区域个数为1
            if len(magnified_All) == 1:
                W = self.image.shape[1] - 2 * self.lw
                H = (magnified_All[0].shape[0] * W / magnified_All[0].shape[1])
                H = int(H)
                magnified_img = cv2.resize(magnified_All[0], (W, H), interpolation=cv2.INTER_LINEAR)
                magnified_img = cv2.copyMakeBorder(
                    magnified_img,
                    top=self.lw, bottom=self.lw, left=self.lw, right=self.lw,
                    borderType=cv2.BORDER_CONSTANT,
                    value=self.border_color[0]
                )

                
            else:    
                # 统一放大区域的高度
                H  = (self.image.shape[1] - 2 * self.lw * self.region_count - int(self.gap) * (self.region_count - 1)) / (sum(img.shape[1] / img.shape[0] for img in magnified_All) )
                H   = int(H)

                # 缩放图像(使用双线性插值保持平滑)
                for i in range(len(magnified_All)):
                    new_width  = int(magnified_All[i].shape[1] * H / magnified_All[i].shape[0])
                    magnified_All[i] = cv2.resize(magnified_All[i], (new_width, H), interpolation=cv2.INTER_LINEAR)

                
                    # 添加边框(使用当前区域颜色)
                    magnified_All[i] = cv2.copyMakeBorder(
                        magnified_All[i],
                        top=self.lw, bottom=self.lw, left=self.lw, right=self.lw,
                        borderType=cv2.BORDER_CONSTANT,
                        value=self.border_color[i]
                    )
            
                num_images = len(magnified_All)
                total_img_width = sum(img.shape[1] for img in magnified_All)
                
                # 计算基础间隔宽度和多余的像素
                num_gaps = num_images - 1 if num_images > 1 else 1
                total_gap_width = self.image.shape[1] - total_img_width
                base_gap = total_gap_width // num_gaps
                extra_gap = total_gap_width % num_gaps  # 用于分配到前几个间隔中

                # 初始化输出图像
                magnified_img = np.full((H + 2 * self.lw, self.image.shape[1], 3), 255, dtype=magnified_All[0].dtype)

                x = 0
                for i in range(num_images):
                    img = magnified_All[i]
                    w = img.shape[1]
                    magnified_img[:, x:x + w, :] = img
                    x += w
                    if i < num_images - 1:
                        x += base_gap + (1 if i < extra_gap else 0)
            
            if self.area == 'Down':
                result  = np.vstack([self.preview, magnified_img])
            else:
                result  = np.vstack([magnified_img, self.preview])
            return result

        else :
            ## 若放大区域个数为1
            if len(magnified_All) == 1:
                H = self.image.shape[0] - 2 * self.lw
                W = (magnified_All[0].shape[1] * H / magnified_All[0].shape[0])
                W = int(W)
                magnified_img = cv2.resize(magnified_All[0], (W, H), interpolation=cv2.INTER_LINEAR)
                magnified_img = cv2.copyMakeBorder(
                    magnified_img,
                    top=self.lw, bottom=self.lw, left=self.lw, right=self.lw,
                    borderType=cv2.BORDER_CONSTANT,
                    value=self.border_color[0]
                )

                
            else:    
                # 统一放大区域的宽度
                W  = (self.image.shape[0] - 2 * self.lw * self.region_count - int(self.gap) * (self.region_count - 1)) / (sum(img.shape[0] / img.shape[1] for img in magnified_All))
                W   = int(W)

                # 缩放图像(使用双线性插值保持平滑)
                for i in range(len(magnified_All)):
                    new_height  = int(magnified_All[i].shape[0] * W / magnified_All[i].shape[1])
                    magnified_All[i] = cv2.resize(magnified_All[i], (W, new_height), interpolation=cv2.INTER_LINEAR)

                
                    # 添加边框(使用当前区域颜色)
                    magnified_All[i] = cv2.copyMakeBorder(
                        magnified_All[i],
                        top=self.lw, bottom=self.lw, left=self.lw, right=self.lw,
                        borderType=cv2.BORDER_CONSTANT,
                        value=self.border_color[i]
                    )
            
                num_images = len(magnified_All)
                total_img_height = sum(img.shape[0] for img in magnified_All)
                
                # 计算基础间隔宽度和多余的像素
                num_gaps = num_images - 1 if num_images > 1 else 1
                total_gap_width = self.image.shape[0] - total_img_height
                base_gap = total_gap_width // num_gaps
                extra_gap = total_gap_width % num_gaps  # 用于分配到前几个间隔中

                # 初始化输出图像
                magnified_img = np.full((self.image.shape[0], W + 2 * self.lw, 3), 255, dtype=magnified_All[0].dtype)

                x = 0
                for i in range(num_images):
                    img = magnified_All[i]
                    h = img.shape[0]
                    magnified_img[x:x + h, :, :] = img
                    x += h
                    if i < num_images - 1:
                        x += base_gap + (1 if i < extra_gap else 0)
            
            if self.area == 'Right':
                result  = np.hstack([self.preview, magnified_img])
            else:
                result  = np.hstack([magnified_img, self.preview])
            return result
        
    def update_preview(self):
        """更新预览窗口,显示所有已绘制区域的放大效果"""
            
        # 为每个区域创建放大图
        magnified = self.create_magnified_image(self.boxes[-1][0], self.boxes[-1][1], self.current_region - 1)
        cv2.imshow(self.preview_window_name + f"{self.current_region}", magnified)
        
        # 区域框选完成则显示最终拼接图像
        if self.current_region == self.region_count:
            final_img = self.image_stitchingh()
            cv2.imshow('final', final_img)

            # 结果保存设置
            self.show_save_dialog(final_img)

    def run(self):
        """主运行函数"""
        # 先选择图片,再显示参数设置对话框
        if not self.select_image():
            return
            
        # 显示参数设置对话框
        self.show_settings_dialog()

        cv2.namedWindow(self.main_window_name)
        cv2.imshow(self.main_window_name, self.image)
        cv2.setMouseCallback(self.main_window_name, self.mouse_callback)

        while True:
            key = cv2.waitKey(1) & 0xFF
            
            if key == ord('q'):  # 按q退出
                break
            elif key == ord('c'):  # 按c取消当前选择
                if self.preview_mode:
                    cv2.destroyWindow(self.preview_window_name)
                    cv2.imshow(self.main_window_name, self.image)
                    self.preview_mode = False
                    self.start_point = (-1, -1)
                    self.end_point = (-1, -1)
                    if self.current_region > 0:
                        self.current_region -= 1
            elif key == ord('s'):  # 按s保存当前结果
                if self.preview_mode:
                    result = self.create_magnified_image(self.start_point, self.end_point)
                    save_path = self.image_path.rsplit('.', 1)[0] + '_magnified.' + self.image_path.rsplit('.', 1)[1]
                    cv2.imwrite(save_path, result)
                    messagebox.showinfo("成功", f"图片已保存至:\n{save_path}")
            elif key == ord('p'):  # 按p重新打开参数设置对话框
                self.show_settings_dialog()
            
            # 检查窗口是否被关闭
            if cv2.getWindowProperty(self.main_window_name, cv2.WND_PROP_VISIBLE) < 1:
                break

        cv2.destroyAllWindows()

if __name__ == '__main__':
    magnifier = ImageMagnifier()
    magnifier.run()    

效果💥

  • 支持上面需求中的所有功能,效果展示:

总结

  • CodeBuddy 的功能确实强大,至少在代码生成、智能修改方面可以大大提高开发效率。内嵌Vscode+自动识别上下文,效率拉满。👍
  • 由CodeBuddy和我共同开发的绘图利器PlotBuddy,希望能够为有相关需求的伙伴带来便利。⭐
  • 最后再让CodeBuddy帮我写一份README,收工。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 需求🖍
  • 代码
  • 效果💥
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档