高性能仿微信截图区域暗亮色比对

下述代码实现了高性能(10^-5次方级别)的截图区域亮暗度比对,避免了常规通过PhotoImage反复构建半透明蒙版的思路: 

import tkinter as tk
from PIL import Image, ImageTk
import ctypes


class TransparentOverlayApp(tk.Tk):
    alpha = 80  # 透明程度(0 - 255, 越小越透明)
    def __init__(self, image_path: str):
        super().__init__()
        self.image: Image.Image = None
        self.rect_pos: list[int, int, int, int] = [0, 0, 0, 0]
        self.local_image_canvas: tk.Canvas = None     
        self.local_window: int = None       # self.canvas.create_window返回的id
        self.local_image: int = None     # create_image返回的id
        self.title("仿微信截图区域暗亮比对")
        self.load_image(image_path)
        self.canvas: tk.Canvas = self.set_basic_canvas()
        self.config_basic_canvas()

    def load_image(self, image_path) -> None:
        self.image = Image.open(image_path)
        self.orig_imagetk = ImageTk.PhotoImage(self.image)
        self.overlay_imagetk = ImageTk.PhotoImage(self.overlay_image(self.image))

    def set_basic_canvas(self) -> tk.Canvas:
        canvas = tk.Canvas(self, width=self.image.width, height=self.image.height)
        canvas.pack()
        return canvas
    
    def config_basic_canvas(self) -> None:
        self.orig_imageid = self.canvas.create_image(0, 0, anchor="nw", image=self.orig_imagetk)
        self.canvas.bind("<ButtonPress-1>", self.start_overlay)
        self.canvas.bind("<ButtonRelease-1>", self.stop_overlay)

    def overlay_image(self, image: Image.Image) -> Image.Image:
        # 此处调节暗色区域的透明度
        overlay = Image.new(
            "RGBA", (self.image.width, self.image.height), (0, 0, 0, self.__class__.alpha)
        )
        overlayed_image = Image.alpha_composite(image.convert("RGBA"), overlay)
        return overlayed_image

    def start_overlay(self, event) -> None:
        self.canvas.itemconfig(self.orig_imageid, image=self.overlay_imagetk)
        self.local_image_canvas = tk.Canvas(self, highlightthickness=2, highlightbackground="#1AAE1A")
        self.local_image = self.local_image_canvas.create_image(0, 0, anchor="nw", image=self.orig_imagetk)
        self.local_window = self.canvas.create_window(-self.image.width, -self.image.height, anchor="nw", window=self.local_image_canvas)
        self.rect_pos = [event.x, event.y, event.x, event.y]
        self.canvas.bind("<B1-Motion>", self.update_overlay)

    def update_overlay(self, event) -> None:
        self.rect_pos[2] = event.x
        self.rect_pos[3] = event.y
        self.update_canvas()

    def stop_overlay(self, _) -> None:
        self.canvas.unbind("<B1-Motion>")
        self.canvas.itemconfig(self.orig_imageid, image=self.orig_imagetk)
        self.canvas.delete(self.local_window)
        self.local_image_canvas.destroy()
        self.local_window = None
        self.local_image = None

    def update_canvas(self) -> None:
        x1, y1, x2, y2 = self.rect_pos
        # 确保坐标是有效的
        x1, y1, x2, y2 = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)
        self.canvas.itemconfig(self.local_window, width=max(x2 - x1, 1), height=max(y2 - y1, 1))  # 新大小
        self.canvas.coords(self.local_window, x1, y1)
        self.local_image_canvas.coords(self.local_image, -x1, -y1)


if __name__ == "__main__":
    # 自动替换为你的文件路径
    image_path = r"D:\Users\pbl\Desktop\2879x1795.png"
    ctypes.windll.shcore.SetProcessDpiAwareness(1)
    app = TransparentOverlayApp(image_path)
    app.mainloop()

效果图:

效果:拖拽矩形,矩形区域内颜色保持透明,外部颜色保持半透明。性能极强,高分辨率下依然能达到10^-5次方。 

优点:在调整矩形边框时,性能很高,高速调整边框也不会看出矩形内实际是镶嵌了另外一张图片

不足:如果为矩形添加移动属性,移动时会发现矩形内部图像有十分严重的“残影”现象。

进一步改良:尝试使用了多种防抖动技术,如使用Canvas对象的scan_mark和scan_dragto增量移动(避免coords重定位);以及限制更新频率,效果不佳。参考代码如下(由于效果不达意,且不太好写边界处理,矩形框移动到边界处有显著Bug!):

import tkinter as tk
from PIL import Image, ImageTk
import ctypes
 
 
class TransparentOverlayApp(tk.Tk):
    alpha = 80  # 透明程度(0 - 255, 越小越透明)
    def __init__(self, image_path: str):
        super().__init__()
        self.image: Image.Image = None
        self.rect_pos: list[int, int, int, int] = [0, 0, 0, 0]
        self.on_press_pos: tuple[int, int] = [0, 0]
        self.local_image_canvas: tk.Canvas = None     
        self.local_window: int = None       # self.canvas.create_window返回的id
        self.local_image: int = None     # create_image返回的id
        self.after_id: int = None      # 限制移动频率以防抖动
        self.title("仿微信截图区域暗亮比对")
        self.load_image(image_path)
        self.canvas: tk.Canvas = self.set_basic_canvas()
        self.config_basic_canvas()
 
    def load_image(self, image_path) -> None:
        self.image = Image.open(image_path)
        self.orig_imagetk = ImageTk.PhotoImage(self.image)
        self.overlay_imagetk = ImageTk.PhotoImage(self.overlay_image(self.image))
 
    def set_basic_canvas(self) -> tk.Canvas:
        canvas = tk.Canvas(self, width=self.image.width, height=self.image.height)
        canvas.pack()
        return canvas
    
    def config_basic_canvas(self) -> None:
        self.orig_imageid = self.canvas.create_image(0, 0, anchor="nw", image=self.orig_imagetk)
        self.canvas.bind("<ButtonPress-1>", self.start_overlay)
        self.canvas.bind("<ButtonRelease-1>", self.enter_move_mode)
 
    def overlay_image(self, image: Image.Image) -> Image.Image:
        # 此处调节暗色区域的透明度
        overlay = Image.new(
            "RGBA", (self.image.width, self.image.height), (0, 0, 0, self.__class__.alpha)
        )
        overlayed_image = Image.alpha_composite(image.convert("RGBA"), overlay)
        return overlayed_image
 
    def start_overlay(self, event) -> None:
        self.canvas.itemconfig(self.orig_imageid, image=self.overlay_imagetk)
        self.local_image_canvas = tk.Canvas(self, highlightthickness=2, highlightbackground="#1AAE1A")
        self.local_image = self.local_image_canvas.create_image(0, 0, anchor="nw", image=self.orig_imagetk)
        self.local_window = self.canvas.create_window(-self.image.width, -self.image.height, anchor="nw", window=self.local_image_canvas)
        self.rect_pos = [event.x, event.y, event.x, event.y]
        self.canvas.bind("<B1-Motion>", self.update_overlay)

    def enter_move_mode(self, _) -> None:
        def start_move(event):
            self.on_press_pos = (event.x_root, event.y_root)
            self.local_image_canvas.scan_mark(event.x_root, event.y_root)
        self.canvas.unbind("<ButtonPress-1>")
        self.canvas.unbind("<B1-Motion>")
        self.canvas.unbind("<ButtonRelease-1>")
        self.local_image_canvas.bind("<ButtonPress-1>", start_move)
        self.local_image_canvas.bind("<B1-Motion>", self.move_overlay)

    def move_overlay(self, event) -> None:
        offset_x = event.x_root - self.on_press_pos[0]
        offset_y = event.y_root - self.on_press_pos[1]
        x1, y1, x2, y2 = self.vaild_rect_pos()
        if x1 + offset_x >= 0 and x2 + offset_x <= self.canvas.winfo_width():
            x1 += offset_x
            x2 += offset_x
        if  y1 + offset_y >= 0 and y2 + offset_y <= self.canvas.winfo_height():
            y1 += offset_y
            y2 += offset_y
        self.on_press_pos = (event.x_root, event.y_root)
        self.rect_pos = [x1, y1, x2, y2]
        # self.resize_view()
        # self.move_view(event)
        self.delayed_move_view(event)

    def update_overlay(self, event) -> None:
        self.rect_pos[2] = event.x
        self.rect_pos[3] = event.y
        self.resize_view()
 
    def stop_overlay(self, _) -> None:
        self.canvas.unbind("<B1-Motion>")
        self.canvas.itemconfig(self.orig_imageid, image=self.orig_imagetk)
        self.canvas.delete(self.local_window)
        self.local_image_canvas.destroy()
        self.local_window = None
        self.local_image = None
 
    def resize_view(self) -> None:
        x1, y1, x2, y2 = self.vaild_rect_pos()
        self.canvas.itemconfig(self.local_window, width=max(x2 - x1, 1), height=max(y2 - y1, 1))  # 新大小
        self.local_image_canvas.coords(self.local_image, -x1, -y1)
        self.canvas.coords(self.local_window, x1, y1)

    def delayed_move_view(self, event) -> None:
        if self.after_id is not None:
            self.after_cancel(self.after_id)
        self.after_id = self.after(3, lambda: self.move_view(event))

    def move_view(self, event) -> None:
        # 下面的gain = -1表示反方向移动,关键所在!!!
        x1, y1, _, _ = self.vaild_rect_pos()
        self.canvas.coords(self.local_window, x1, y1)
        # 这里应该增加额外的逻辑限制scan_dragto把矩形内的视图移歪
        self.local_image_canvas.config(scrollregion=self.local_image_canvas.bbox("all"))
        self.local_image_canvas.scan_dragto(event.x_root, event.y_root, gain=-1)
        

    def vaild_rect_pos(self) -> bool:
        x1, y1, x2, y2 = self.rect_pos
        x1, y1, x2, y2 = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)
        return x1, y1, x2, y2

 
if __name__ == "__main__":
    image_path = r"2880x1800.png"
    ctypes.windll.shcore.SetProcessDpiAwareness(1)
    app = TransparentOverlayApp(image_path)
    app.mainloop()

我们还注意到,当用户移动选区时,此时选区的大小是不变的,只有位置在变化。那么何不在此时直接创建一个半透明图层,从中挖出这个和选区大小一样的透明区域的ImageTk对象呢?这样我们就能在移动时只创建一次PhotoImage对象,然后一直动态修改它的位置,性能应该不错?

示例代码如下,不过实测代码效果并不是很好:

import tkinter as tk
from PIL import Image, ImageTk, ImageDraw
import ctypes
import time


class TransparentOverlayApp(tk.Tk):
    alpha = 80  # 透明程度(0 - 255, 越小越透明)
    def __init__(self, image_path: str):
        super().__init__()
        self.image: Image.Image = None
        self.rect_pos: list[int, int, int, int] = [0, 0, 0, 0]
        self.move_pos: list[int, int] = [0, 0]
        self.local_image_canvas: tk.Canvas = None     
        self.local_window: int = None       # self.canvas.create_window返回的id
        self.local_image: int = None     # create_image返回的id
        self.move_event: str = None
        self.title("仿微信截图区域暗亮比对")
        self.load_image(image_path)
        self.canvas: tk.Canvas = self.set_basic_canvas()
        self.config_basic_canvas()

    def load_image(self, image_path) -> None:
        self.image = Image.open(image_path)
        self.orig_imagetk = ImageTk.PhotoImage(self.image)

    def set_basic_canvas(self) -> tk.Canvas:
        canvas = tk.Canvas(self, width=self.image.width, height=self.image.height)
        canvas.pack()
        return canvas
    
    def config_basic_canvas(self) -> None:
        self.overlay_imagetk = ImageTk.PhotoImage(self.get_overlay_image())
        self.orig_imageid = self.canvas.create_image(0, 0, anchor="nw", image=self.orig_imagetk)
        self.canvas.bind("<ButtonPress-1>", self.start_overlay)
        self.canvas.bind("<ButtonRelease-1>", self.enter_move_mode)
    
    def get_overlay_image(self, alpha_pos: tuple[int, int, int, int] = None) -> Image.Image:
        if alpha_pos is None:
            return Image.new("RGBA", (self.image.width, self.image.height), (0, 0, 0, self.__class__.alpha))
        else:
            x1, y1, x2, y2 = alpha_pos
            overlay = Image.new(
                "RGBA", 
                (self.image.width * 2 + (x2 - x1), self.image.height * 2 + (y2 - y1)), 
                (0, 0, 0, self.__class__.alpha)
            )
            new_x1 = x1 + (self.image.width - x2)
            new_y1 = y1 + (self.image.height - y2)
            new_alpha_pos = (new_x1, new_y1, new_x1 + x2 - x1, new_y1 + y2 - y1)
            draw = ImageDraw.Draw(overlay)
            draw.rectangle(new_alpha_pos, fill=(0, 0, 0, 0), outline="#1AAE1A", width=2)
            return overlay

    def start_overlay(self, event) -> None:
        self.canvas.create_image(0, 0, anchor="nw", image=self.overlay_imagetk, tags="overlay")
        self.local_image_canvas = tk.Canvas(self, highlightthickness=2, highlightbackground="#1AAE1A")
        self.local_image = self.local_image_canvas.create_image(0, 0, anchor="nw", image=self.orig_imagetk)
        self.local_window = self.canvas.create_window(-self.image.width, -self.image.height, anchor="nw", window=self.local_image_canvas)
        self.rect_pos = [event.x, event.y, event.x, event.y]
        self.canvas.bind("<B1-Motion>", self.update_overlay)
        self.bind("<space>", self.stop_overlay)

    def update_overlay(self, event) -> None:
        self.rect_pos[2] = event.x
        self.rect_pos[3] = event.y
        self.update_canvas()

    def enter_move_mode(self, event) -> None:
        self.canvas.unbind("<ButtonPress-1>")
        self.canvas.unbind("<ButtonRelease-1>")
        self.canvas.unbind("<B1-Motion>")
        self.overlay_imagetk = ImageTk.PhotoImage(self.get_overlay_image(self.rect_pos))
        self.canvas.itemconfig("overlay", image=self.overlay_imagetk)
        self.canvas.coords("overlay", self.rect_pos[2] - self.image.width, self.rect_pos[3] - self.image.height)
        self.canvas.delete(self.local_window)
        self.canvas.bind("<ButtonPress-1>", self.initilize_move_pos)
        self.canvas.bind("<B1-Motion>", self.move_overlay)

    def initilize_move_pos(self, event) -> None:
        self.move_pos = [event.x_root, event.y_root]

    def move_overlay(self, event) -> None:
        def _move_overlay():
            self.canvas.move("overlay", final_dx, final_dy)
            self.move_event = None
        offset_x = event.x_root - self.move_pos[0]
        offset_y = event.y_root - self.move_pos[1]
        final_dx = final_dy = 0
        start_x, start_y, end_x, end_y = self.rect_pos
        if start_x + offset_x >= 0 and end_x + offset_x <= self.winfo_width():
            start_x += offset_x
            end_x += offset_x
            final_dx = offset_x
        if  start_y + offset_y >= 0 and end_y + offset_y <= self.winfo_height():
            start_y += offset_y
            end_y += offset_y
            final_dy = offset_y
        if not self.move_event:
            self.move_event = self.after(0, _move_overlay)
        self.rect_pos = [start_x, start_y, end_x, end_y]
        self.initilize_move_pos(event)

    def stop_overlay(self, _) -> None:
        self.canvas.unbind("<B1-Motion>")
        self.unbind("<space>")
        self.canvas.itemconfig(self.orig_imageid, image=self.orig_imagetk)
        self.canvas.delete(self.local_window)
        self.canvas.delete("overlay")
        self.local_image_canvas.destroy()
        self.local_window = None
        self.local_image = None
        self.config_basic_canvas()

    def update_canvas(self) -> None:
        x1, y1, x2, y2 = self.rect_pos
        # 确保坐标是有效的
        x1, y1, x2, y2 = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)
        self.canvas.itemconfig(self.local_window, width=max(x2 - x1, 1), height=max(y2 - y1, 1))  # 新大小
        self.canvas.coords(self.local_window, x1, y1)
        self.local_image_canvas.coords(self.local_image, -x1, -y1)


if __name__ == "__main__":
    # 自动替换为你的文件路径
    image_path = r"test.png"
    ctypes.windll.shcore.SetProcessDpiAwareness(1)
    app = TransparentOverlayApp(image_path)
    app.mainloop()

旧代码参考

思路:反复使用PhotoImage创建半透明图层,并从中抠出全透明图层。同时尝试通过差分法减少ImageDraw绘制区域以优化性能。

优点:移动矩形框时不会出现“残影”;

不足:性能很差(每次更新需10^-2s),无论是调整还是移动矩形框,都有十分显著的卡顿感。

过程反思:在写代码的过程中未经过单独的性能测试,错误的将优化方向指向了ImageDraw(尝试差分法,发现没什么效果) 。后续通过测试发现性能瓶颈在PhotoImage的构建后,尝试使用PhotoImage的paste方法,但发现该方法与Image对象的paste方法不同,实际上并不能指定粘贴区域(被注释掉的地方)。

import tkinter as tk
from PIL import Image, ImageDraw, ImageTk
import ctypes
from time import time, perf_counter

class TransparentOverlayApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Transparent Overlay Demo")

        # 加载图片
        self.image = Image.open(r"D:\Users\pbl\Desktop\2879x1795.png")  # 替换为您的图片路径
        self.photo = ImageTk.PhotoImage(self.image)

        # 创建 Canvas
        self.canvas = tk.Canvas(root, width=self.image.width, height=self.image.height)
        self.canvas.pack()

        # 在 Canvas 上显示图片
        self.canvas.create_image(0, 0, anchor="nw", image=self.photo)

        # 绑定鼠标事件
        self.canvas.bind("<ButtonPress-1>", self.start_overlay)
        self.canvas.bind("<ButtonRelease-1>", self.stop_overlay)

        # 初始化遮罩
        self.overlay = None
        self.rect = None
        self.rect_id = None
        self.prev_coords = None

    def start_overlay(self, event):
        # 创建半透明遮罩
        self.overlay = Image.new("RGBA", (self.image.width, self.image.height), (0, 0, 0, 128))
        self.draw = ImageDraw.Draw(self.overlay)
        self.rect = [event.x, event.y, event.x, event.y]  # 初始化矩形区域
        self.rect_id = self.canvas.create_rectangle(*self.rect, outline="red", tag="overlay")
        self.update_canvas()
        # 绑定鼠标移动事件
        self.canvas.bind("<B1-Motion>", self.update_overlay)

    def update_overlay(self, event):
        # 更新矩形区域
        self.rect[2] = event.x
        self.rect[3] = event.y
        self.canvas.coords(self.rect_id, *self.rect)
        self.update_canvas()

    def stop_overlay(self, _):
        # 销毁遮罩
        self.overlay = None
        self.prev_coords = None
        self.canvas.delete(self.rect_id)
        self.canvas.delete(self.image_id)
        self.canvas.imagetk = None

    def update_canvas(self):
        current_coords = self.canvas.bbox(self.rect_id)
        if self.prev_coords:
            # 计算交集
            intersection = self.calculate_intersection(self.prev_coords, current_coords)
            # 计算差集
            diff_a_list = self.calculate_difference(self.prev_coords, intersection)
            diff_b_list = self.calculate_difference(current_coords, intersection)
            for diff_a in diff_a_list:
                self.update_difference(diff_a, (0, 0, 0, 128))  # 恢复上一次区域
            for diff_b in diff_b_list:
                self.update_difference(diff_b, (0, 0, 0, 0))  # 更新当前区域
            self.imagetk = ImageTk.PhotoImage(self.overlay)
            self.canvas.itemconfigure(self.image_id, image=self.imagetk)
        else:
            self.draw.rectangle(current_coords, fill=(0, 0, 0, 0))
            self.imagetk = ImageTk.PhotoImage(self.overlay)
            self.image_id = self.canvas.create_image(0, 0, anchor="nw", image=self.imagetk)
        self.prev_coords = current_coords

    def calculate_intersection(self, rect1, rect2):
        # 计算两个矩形的交集
        left = max(rect1[0], rect2[0])
        top = max(rect1[1], rect2[1])
        right = min(rect1[2], rect2[2])
        bottom = min(rect1[3], rect2[3])
        return (left, top, right, bottom)

    def calculate_difference(self, rect1, rect2):
        start = perf_counter()
        intersection = self.calculate_intersection(rect1, rect2)
        if not intersection or intersection[0] >= intersection[2] or intersection[1] >= intersection[3]:
            # 如果没有交集,差集就是整个矩形
            return [rect1]

        diff = []
        if rect1[0] < intersection[0]:
            diff.append((rect1[0], rect1[1], intersection[0], rect1[3]))
        if rect1[2] > intersection[2]:
            diff.append((intersection[2], rect1[1], rect1[2], rect1[3]))
        if rect1[1] < intersection[1]:
            diff.append((rect1[0], rect1[1], rect1[2], intersection[1]))
        if rect1[3] > intersection[3]:
            diff.append((rect1[0], intersection[3], rect1[2], rect1[3]))
        return diff

    def update_difference(self, diff: tuple[int, int, int, int], color: tuple[int, int, int, int]):
        if diff:
            self.draw.rectangle(diff, fill=color)
            # width, height = diff[2] - diff[0], diff[3] - diff[1]
            # self.imagetk.paste(Image.new("RGBA", (width, height), color), diff)



# 主程序
if __name__ == "__main__":
    ctypes.windll.shcore.SetProcessDpiAwareness(1)
    root = tk.Tk()
    app = TransparentOverlayApp(root)
    root.mainloop()

不过,我最后还是找到了一种方法可以有效解决移动方框时内部的移动残影感问题且拥有较好的性能。具体实现详看我的截图小工具的github项目:

Python制作的截图小工具-Lightscreenshot

在源代码中的Widgets.py文件中, 里面的AdjustableRect类中的move_rect方法就实现了这一功能,具体的技术叫做“离屏重绘”,真正意义上让tkinter强制将正确的图像重绘出来,这样就完全没有移动的残影感了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值