python检测屏幕红色参数异常信息

工作中有pi系统有多个参数需要观察,可以设置参数超上下限后变成红色,但是没有提示声音,应用如下脚本辅助参数异常提醒。
请添加图片描述

使用脚本框选出数字区域,完成检测操作。操作的界面如下:
请添加图片描述
大致的流程如下:
在这里插入图片描述

第一步 先建立屏幕的选区:获取鼠标按下的 xstart, ystart,鼠标拖动后松开的 xend, yend,固定该区域检测红色像素。截图主屏幕会变暗,框选的区域变亮,截图的外边缘有紫色的虚线。截图完成即刻开始下一步开始识别 start_recognition() 。

# 用鼠标获取选区
def select_screen_region():
    load_alarm_sound("alarm.mp3")  # 提前加载报警音,防止终止运行
    text_widget.delete(1.0, tk.END) 
    print('点击开始选区后,使用鼠标左键托选出选区作为监控区域,' + '\n''松开鼠标即刻工作')  # 点击开始选区后 不可以重复点击 ,需要暂停后再次启动

    global xstart, ystart, xend, yend

    def button_1(event):  # 鼠标左键点击-
        global xstart, ystart, rectangle  # xstart, ystart存储了鼠标左键按下时的x,y轴坐标。按下左键创建rectangle
        xstart, ystart = event.x, event.y
        cv.config(highlightthickness=0)  # 无边框
        cv.place(x=event.x, y=event.y)  # 定位到什么位置 place(x =画布在窗口x轴的位置,y=画布在窗口y轴的位置), 以点击的位置作为画布的起点
        rectangle = cv.create_rectangle(0, 0, 0, 0, outline='blue', width=6,
                                        dash=(4, 4))  # 控件位置(x,y)中,任何一个参数小于0,这个控件就会显示不全或不显示

    def b1_Motion(event):  # 鼠标左键移动
        global xstart, ystart
        cv.configure(
            height=event.y - ystart)  # 在灰色的主窗口上拖动鼠标画出一个白色的画布。当前位置与鼠标左键按下时的 Y 轴位置之差。目的是为了使 Canvas 组件的高度等于鼠标的垂直移动距离,从而实现动态调整高度的效果。
        cv.configure(width=event.x - xstart)  # x+y轴鼠标拖拽过程中显示出矩形选框大小变化,
        cv.coords(rectangle, 0, 0, event.x - xstart,
                  event.y - ystart)  # 根据鼠标的位置变换矩形的大小,即矩形的宽与高,而起点不变为0,0  https://blog.csdn.net/m0_37264397/article/details/79179956

    def buttonRelease_1(event):  # 鼠标左键松开
        global xend, yend  # xend, yend存储了鼠标左键松开时的x,y轴坐标
        xend, yend = event.x, event.y
        cv.delete(rectangle)
        cv.place_forget()  # 该方法轻松地隐藏或移除Tkinter窗口中的小部件,如现在的用于绘制框选的画布。 https://deepinout.com/python/python-top-articles/1695446507_tr_place-forget-method-using-tkinter-in-python.html
        root.destroy()  # destroy() 终止mainloop并删除所有小部件。将主窗口销毁
        start_recognition()  # 松开鼠标 开始识别操作,如果截图大小有误,可以点击暂停,重新开始截图

    root = tk.Tk()  # tk.Tk()创建一个Tkinter主窗口实例。
    root.overrideredirect(True)  # 隐藏窗口的标题栏
    root.attributes("-alpha", 0.1)  # 窗口透明度10%
    root.geometry(f"{root.winfo_screenwidth()}x{root.winfo_screenheight()}+0+0")  # 主窗口为全屏幕大小
    root.configure(bg="black")  # 点击开始选区按钮后 主窗口全屏显示浅浅的灰

    cv = tk.Canvas(root)  # 在主窗口中创建一个Canvas组件,Canvas用于圈选

    root.bind("<Button-1>", button_1)  # 鼠标左键点击->显示子窗口
    root.bind("<B1-Motion>", b1_Motion)  # 鼠标左键移动->改变子窗口大小
    root.bind("<ButtonRelease-1>", buttonRelease_1)  # 鼠标左键释放->记录最后光标的位置
    root.mainloop()

第二步 开启识别线程:开始识别之后,“开始选区”按钮变灰色,禁止操作。确保未开启线程的情况下才能启动th_check线程。在Python中使用Tkinter库创建GUI应用时,可以通过设置daemon=True来将主线程的子线程变为守护线程。这样当主线程结束时,所有守护线程也会被自动关闭。th_check加入守护线程,root.destroy() # 退出主线程(包括守护线程),识别操作将立即停止。

def start_recognition():
    btn_suspend.config(state=tk.NORMAL)
    dropdown.config(state=tk.DISABLED)
    global should_exit
    global suspend
    suspend = False
    should_exit = False
    print("开始参数监控")
    if not check_thread_created:  # 检查线程是否已创建
        th_check = threading.Thread(target=check_the_exception)
        th_check.daemon = True
        th_check.start()  # 启动线程

第三步 定期检测框选区域红色像素:使用while not should_exit:来循环检测,除非要退出或暂停,否则一直在定期检查。
在Python的NumPy库中,np.where函数用于返回满足给定条件的元素的索引。img是一个三维数组(通常是一个彩色图像),其形状为(height, width, channels),其中channels代表颜色通道(如RGB)。

代码中的条件是用来过滤图像中的像素,通道0是红色,通道1是绿色,通道2是蓝色,调整阀值大小来检测不同纯度的红色。使用截图获取图像,像素会有失真,纯红色(255,0,0)需要一些阀值调整才能检测到:

img[:, :, 0] >= threshold_r:选择红色通道值大于或等于threshold_r的像素。
img[:, :, 1] <= threshold_g:选择绿色通道值小于或等于threshold_g的像素。
img[:, :, 2] <= threshold_b:选择蓝色通道值小于或等于threshold_b的像素。

np.where函数返回一个元组,其中包含满足条件的元素的索引。具体来说,ind是一个包含三个数组的元组,每个数组对应于图像的一个通道。每个数组的元素是满足相应条件的像素的行和列索引。

python的for遍历图像像素,循环效率很很低,等待时间长。所以能用np.where就用这个了。python pil像素遍历 python遍历图像像素点

def check_the_exception():
    global frequency, should_exit, is_alarm, check_thread_created

    # 设置标志变量,表示线程已创建
    check_thread_created = True

    btn_suspend['text'] = '暂停监控'
    if suspend == True:
        btn_create_region['text'] = '重建选区'
        btn_create_region.config(state=tk.NORMAL)
    else:
        btn_create_region.config(state=tk.DISABLED)

    while not should_exit:
        img = pyautogui.screenshot(region=[xstart + 20, ystart + 20, xend - xstart - 30, yend - ystart - 30])  # 校正一些偏差
        img = np.array(img)
        threshold_r = 250  # 红色通道的阈值范围
        threshold_g = 40  # 绿色通道的阈值范围
        threshold_b = 40  # 蓝色通道的阈值范围
        # 查找接近红色的像素  np.where(condition) 当where内只有一个参数时,参数表示条件,当条件等于True,where以元组的形式返回每个符合condition条件元素的坐标
        ind = np.where((img[:, :, 0] >= threshold_r) &  # 这些条件通过逻辑AND (&) 连接,意味着只有同时满足所有条件的像素才会被选中。
                       (img[:, :, 1] <= threshold_g) &
                       (img[:, :, 2] <= threshold_b))
        combineXY = list(zip(ind[0], ind[1]))  # 将满足红色像素条件的行索引x轴,列索引y轴坐标组合成列表

        if combineXY:  # 如果捕捉到红色像素
            is_alarm = True
            play_alarm()  # 播放报警音
            now = datetime.now()
            ts = now.strftime("%Y-%m-%d %H:%M:%S")
            print(ts + ":出现参数异常")

        # threading.Timer(frequency, reset_status).start()  python通过线程实现定时器timer的方法,可以代替while not, https://pythonjishu.com/ytxvkemcskemjeq/
        time.sleep(frequency)  # 等待15秒后再次检测  ,在这里如果开始检测被暂停之后,未到15秒又继续,将开启两个线程,所以加入check_thread_created

    if should_exit and suspend:
        print('暂停参数监控')
        check_thread_created = False
    else:  # should_exit 和 suspend 都是 False ,或者should_exit = not suspend
        cv2.destroyAllWindows()

所有的代码如下:

import sys
import tkinter as tk  # 创建图形用户界面(GUI)的标准库
import time
import cv2
import pyautogui  # 键鼠库
import pygame  # 播放报警音的库
import threading  # 线程库
import numpy as np
from datetime import datetime  # 格式化的时间


# 初始化pygame库
pygame.init()
pygame.mixer.init()

frequency = 15
suspend = False
should_exit = False
is_alarm = False

def print_to_text(*args):
    global is_alarm
    for arg in args:
        if not is_alarm:
            text_widget.tag_configure('red_text', foreground='blue')  # 正常字体
        if is_alarm:
            text_widget.tag_configure('red_text', foreground='red')  # 报警为红色
        text_widget.insert('end', str(arg), 'red_text')
    text_widget.insert('end', '\n')
    text_widget.yview_scroll(1, 'units')  # 向下滚动一格

# 将print的输出重定向到text_widget
print = print_to_text.__get__(None, print)

# 全局变量,加载MP3文件
def load_alarm_sound(file_path):
    try:
        pygame.mixer.music.load(file_path)
    except pygame.error:
        print(f"无法加载音频文件:{file_path},检查文件名称和后缀是否正确")


# 封装一个播放MP3警报的函数
def play_alarm():
    try:
        # 播放MP3文件
        pygame.mixer.music.play()
    except pygame.error:
        print("播放警报音频失败")

# 用鼠标获取选区
def select_screen_region():
    load_alarm_sound("alarm.mp3")  # 放到这里防止错误提示终止运行
    text_widget.delete(1.0, tk.END)
    print('点击开始选区后,使用鼠标左键托选出选区作为监控区域,' + '\n''松开鼠标即刻工作')  # 点击开始选区后 不可以重复点击 ,需要暂停后再次启动

    global xstart, ystart, xend, yend

    def button_1(event):  # 鼠标左键点击-
        global xstart, ystart, rectangle  # xstart, ystart存储了鼠标左键按下时的x,y轴坐标。按下左键创建rectangle
        xstart, ystart = event.x, event.y
        cv.config(highlightthickness=0)  # 无边框
        cv.place(x=event.x, y=event.y)  # 定位到什么位置 place(x =画布在窗口x轴的位置,y=画布在窗口y轴的位置), 以点击的位置作为画布的起点
        rectangle = cv.create_rectangle(0, 0, 0, 0, outline='blue', width=6,
                                        dash=(4, 4))  # 控件位置(x,y)中,任何一个参数小于0,这个控件就会显示不全或不显示

    def b1_Motion(event):  # 鼠标左键移动
        global xstart, ystart
        cv.configure(
            height=event.y - ystart)  # 在灰色的主窗口上拖动鼠标画出一个白色的画布。当前位置与鼠标左键按下时的 Y 轴位置之差。目的是为了使 Canvas 组件的高度等于鼠标的垂直移动距离,从而实现动态调整高度的效果。
        cv.configure(width=event.x - xstart)  # x+y轴鼠标拖拽过程中显示出矩形选框大小变化,
        cv.coords(rectangle, 0, 0, event.x - xstart,
                  event.y - ystart)  # 根据鼠标的位置变换矩形的大小,即矩形的宽与高,而起点不变为0,0  https://blog.csdn.net/m0_37264397/article/details/79179956

    def buttonRelease_1(event):  # 鼠标左键松开
        global xend, yend  # xend, yend存储了鼠标左键松开时的x,y轴坐标
        xend, yend = event.x, event.y
        cv.delete(rectangle)
        cv.place_forget()  # 该方法轻松地隐藏或移除Tkinter窗口中的小部件,如现在的用于绘制框选的画布。 https://deepinout.com/python/python-top-articles/1695446507_tr_place-forget-method-using-tkinter-in-python.html
        root.destroy()  # destroy() 终止mainloop并删除所有小部件。将主窗口销毁
        start_recognition()  # 松开鼠标 开始识别操作,如果截图大小有误,可以点击暂停,重新开始截图

    root = tk.Tk()  # tk.Tk()创建一个Tkinter主窗口实例。
    root.overrideredirect(True)  # 隐藏窗口的标题栏
    root.attributes("-alpha", 0.1)  # 窗口透明度10%
    root.geometry(f"{root.winfo_screenwidth()}x{root.winfo_screenheight()}+0+0")  # 主窗口为全屏幕大小
    root.configure(bg="black")  # 点击开始选区按钮后 主窗口全屏显示浅浅的灰

    cv = tk.Canvas(root)  # 在主窗口中创建一个Canvas组件,Canvas用于圈选

    root.bind("<Button-1>", button_1)  # 鼠标左键点击->显示子窗口
    root.bind("<B1-Motion>", b1_Motion)  # 鼠标左键移动->改变子窗口大小
    root.bind("<ButtonRelease-1>", buttonRelease_1)  # 鼠标左键释放->记录最后光标的位置
    root.mainloop()


# 全局变量,用于记录线程是否已经创建
check_thread_created = False


def check_the_exception():
    global frequency, should_exit, is_alarm, check_thread_created

    # 设置标志变量,表示线程已创建
    check_thread_created = True

    btn_suspend['text'] = '暂停监控'
    if suspend == True:
        btn_create_region['text'] = '重建选区'
        btn_create_region.config(state=tk.NORMAL)
    else:
        btn_create_region.config(state=tk.DISABLED)

    while not should_exit:
        img = pyautogui.screenshot(region=[xstart + 20, ystart + 20, xend - xstart - 30, yend - ystart - 30])  # 校正一些偏差
        img = np.array(img)
        threshold_r = 250  # 红色通道的阈值范围
        threshold_g = 40  # 绿色通道的阈值范围
        threshold_b = 40  # 蓝色通道的阈值范围
        # 查找接近红色的像素  np.where(condition) 当where内只有一个参数时,参数表示条件,当条件等于True,where以元组的形式返回每个符合condition条件元素的坐标
        ind = np.where((img[:, :, 0] >= threshold_r) &  # 这些条件通过逻辑AND (&) 连接,意味着只有同时满足所有条件的像素才会被选中。
                       (img[:, :, 1] <= threshold_g) &
                       (img[:, :, 2] <= threshold_b))
        combineXY = list(zip(ind[0], ind[1]))  # 将满足红色像素条件的行索引x轴,列索引y轴坐标组合成列表

        if combineXY:  # 如果捕捉到红色像素
            is_alarm = True
            play_alarm()  # 播放报警音
            now = datetime.now()
            ts = now.strftime("%Y-%m-%d %H:%M:%S")
            print(ts + ":出现参数异常")

        # threading.Timer(frequency, reset_status).start()  python通过线程实现定时器timer的方法,可以代替while not, https://pythonjishu.com/ytxvkemcskemjeq/
        time.sleep(frequency)  # 等待15秒后再次检测  ,在这里如果开始检测被暂停之后,未到15秒又继续,将开启两个线程,所以加入check_thread_created

    if should_exit and suspend:
        print('暂停参数监控')
        check_thread_created = False
    else:  # should_exit 和 suspend 都是 False ,或者should_exit = not suspend
        cv2.destroyAllWindows()

def start_recognition():
    btn_suspend.config(state=tk.NORMAL)
    dropdown.config(state=tk.DISABLED)
    global should_exit
    global suspend
    suspend = False
    should_exit = False
    print("开始参数监控")
    if not check_thread_created:  # 检查线程是否已创建
        th_check = threading.Thread(target=check_the_exception)
        th_check.daemon = True
        th_check.start()  # 启动线程

def suspend_recognition():  # 将暂停和继续放入同一个按钮
    global suspend
    global should_exit
    btn_create_region.config(state=tk.NORMAL)
    should_exit = not should_exit
    suspend = not suspend
    if suspend:
        btn_suspend['text'] = '继续监控'
        btn_create_region['text'] = '重建选区'
        btn_create_region.config(state=tk.NORMAL)
    else:
        btn_suspend['text'] = '暂停监控'
        btn_create_region.config(state=tk.DISABLED)
        start_recognition()

def end_program():
    global should_exit
    should_exit = True
    print("结束监控")
    sys.exit(1)  # 在pycharm调试Tkinter程序的时候,关闭右上角的X实际上并未退出进程,长期以往再大的内存也会被耗尽。

# 创建主窗口
root = tk.Tk()
root.title("监控红色参数异常报警")
root.wm_attributes('-topmost', True)
root.protocol("WM_DELETE_WINDOW", end_program)
text_widget = tk.Text(root, width=55, height=10)  # 设置文本框的最小宽度为40个字符,最小高度为10行
root.eval('tk::PlaceWindow . center')  # 界面居中显示
text_widget.pack()  # pack()只在左右居中,没有上下居中   https://blog.51cto.com/u_13303/6359424

# 添加按钮
print('默认频率15秒每次,若要调整需在开始选区前修改,否则会有多个线程开启。 \n 点击 开始选区按钮后,开始截屏。')
btn_create_region = tk.Button(root, text="开始选区", command=select_screen_region)
btn_suspend = tk.Button(root, text="暂停监控", command=suspend_recognition)
btn_end = tk.Button(root, text="结束监控", command=end_program)

# 使用pack布局并设置side参数为LEFT实现横向排列

btn_suspend.config(state=tk.DISABLED)  # 暂停按键停用
btn_create_region.pack(side=tk.LEFT, padx=10, pady=10)
btn_suspend.pack(side=tk.LEFT, padx=10, pady=10)
btn_end.pack(side=tk.LEFT, padx=10, pady=10)

# 创建一个下拉列表变量
root.var = tk.StringVar(root)
# 设置下拉列表的默认值
root.var.set("监控频率默认15秒次")
# 定义下拉列表的选项
options = ["5秒次", "15秒次", "30秒次", "60秒次", "150秒次"]
# 创建下拉列表
dropdown = tk.OptionMenu(root, root.var, *options)
# 将下拉列表放置到主窗口
dropdown.pack(side=tk.LEFT, padx=10, pady=10)

def option_selected(*args):
    global frequency
    text_widget.delete(1.0, tk.END)
    print(f"选项 {root.var.get()} 被选择了!")
    if root.var.get() == "5秒次":
        frequency = 5
    if root.var.get() == "15秒次":
        frequency = 15
    if root.var.get() == "30秒次":
        frequency = 30
    if root.var.get() == "60秒次":
        frequency = 60
    if root.var.get() == "150秒次":
        frequency = 150

root.var.trace("w", option_selected)

# 进入事件循环
root.mainloop()

在该基础上增加了后台监控,首先要取得窗口句柄的标题,使用win32gui.findwindow()找到对应的句柄,在多个窗口被覆盖也能监控到数据异常。但是不能最小化窗口,否则找不到对应的窗口。

hwnd = None # 初始化 hwnd 为 None

# 颜色检测逻辑
def detect_red_pixels(img, threshold_r, threshold_g, threshold_b):
    ind = np.where((img[:, :, 2] >= threshold_r) &  # 注意这里使用的是BGR通道,红色在第三通道
                     (img[:, :, 1] <= threshold_g) &
                     (img[:, :, 0] <= threshold_b))
    return list(zip(ind[0], ind[1]))
# 屏幕截图逻辑

def capture_screen(hwnd, xstart, ystart, xend, yend):
    hWndDC = win32gui.GetWindowDC(hwnd)
    mfcDC = win32ui.CreateDCFromHandle(hWndDC)
    saveDC = mfcDC.CreateCompatibleDC()
    saveBitMap = win32ui.CreateBitmap()
    saveBitMap.CreateCompatibleBitmap(mfcDC, xend - xstart, yend - ystart)
    saveDC.SelectObject(saveBitMap)
    saveDC.BitBlt((0, 0), (xend - xstart, yend - ystart), mfcDC, (xstart, ystart), win32con.SRCCOPY) # 保存图片转cv图像
    signedIntsArray = saveBitMap.GetBitmapBits(True)
    im_opencv = np.frombuffer(signedIntsArray, dtype='uint8')
    im_opencv.shape = (yend - ystart, xend - xstart, 4)
    img = cv2.cvtColor(im_opencv, cv2.COLOR_BGRA2BGR)  # 注意这里使用BGRA到BGR的转换
    win32gui.DeleteObject(saveBitMap.GetHandle())
    saveDC.DeleteDC() #上下文资源释放
    mfcDC.DeleteDC()
    win32gui.ReleaseDC(hwnd, hWndDC)
    return img
    
def check_the_exception():
    global frequency, should_exit, is_alarm, check_thread_created,hwnd

    # 设置标志变量,表示线程已创建
    check_thread_created = True

    btn_suspend['text'] = '暂停监控'
    if suspend == True:
        btn_create_region['text'] = '重建选区'
        btn_create_region.config(state=tk.NORMAL)
    else:
        btn_create_region.config(state=tk.DISABLED)

    while not should_exit:
        # 如果 hwnd 不存在,则重新查找窗口
        if hwnd is None or not win32gui.IsWindow(hwnd):
            hwnd = win32gui.FindWindow(None,'python-2024.docx - WPS Office') #第二个参数是窗口标题名称
        img = capture_screen(hwnd, xstart, ystart, xend, yend)
 
        # cv2.imshow("ii",img)    #用于显示截图
        # cv2.waitKey()

        combineXY =detect_red_pixels(img,240,30,30)

        if combineXY:  # 如果捕捉到红色像素
            is_alarm = True
            play_alarm()  # 播放报警音
            now = datetime.now()
            ts = now.strftime("%Y-%m-%d %H:%M:%S")
            print(ts + ":出现参数异常")

        time.sleep(frequency) 

    if should_exit and suspend:
        print('暂停参数监控')
        check_thread_created = False
    else:  
        cv2.destroyAllWindows()

参考了以下作者的文章:
【kimol君的无聊小发明】—用python写截屏小工具
python 手动选取屏幕一块区域 实时比对是否变化
Python实现指定区域桌面变化监控并报警

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值