找回密码
 立即注册
搜索
查看: 2270|回复: 8

[其他] 什么狗屁我自己来,不如AI来,尝试用OpenCV+OCR+LLM写了个聪明点的脚本机器人

[复制链接]
     
发表于 2024-11-25 17:11 | 显示全部楼层 |阅读模式
本帖最后由 泰坦失足 于 2024-11-25 17:25 编辑

此处抛砖引玉下。
主要思路是:
1:截取当前屏幕为主截图
2:针对没有图标的特殊组件,截图后放入res文件夹。然后opencv从截图中匹配这些小截图,将 XX组件.png和当前屏幕截图匹配上后,去掉当前屏幕截图的匹配部分改为写上png的文字. 再将注释后的图片保存为注释截图。
这一步速度最开始特别慢,稍微优化了下,还是瓶颈之一。opencv是纯cpu运行,而且N个文件要匹配n次(不考虑并行处理),可能训练个模型一次gpu推理会好点、
3:使用百度 paddle ocr识别注释截图,得到dataframe,一个 文字-坐标的词典
之前试过Tesseract,不好用。百度这个虽说一路安完了,但是对繁体游戏(宝可梦TCGP)的识别不好,文字都是残缺的,纯靠LLM自己的推理能力硬匹配上。应该使用在线OCR API服务的。
4 使用LLM分析当前屏幕内容,根据prompt中指示进行操作
还以为有了LLM会好很多,想实现 对战->导出领奖励的复杂操作,最后灰溜溜的只实现是不停对战-投降-谢谢
5 pyautogui点击屏幕

目前的最大问题是运行速度太慢了,进入游戏PTCGP对局后要40秒才能投降完成。慢慢优化吧,目前LLM跑在自己显卡上可能也是缓慢的原因,但只要电费。openai的服务是快,一张图2分钱, 按3秒一张图的处理速度,挂60分钟就要23rmb。
第二大问题就是:要这玩意干啥呢?数了下这玩意一个月的PTCGP的收益是5包,相当于45块日服月卡的1/6=8元(我都干了什么)。像什么火影忍者手游,他们也不满足你肝游戏,经常拿些东西诱惑你花现金的。可能以后能用于3D游戏的挂通行证吧,能想到的是这个负责操作各种菜单处理意外情况,摇杆永远向前,如果前面是堵墙则左转90度继续向前走,然后无限开火蹭助攻。
优点大概是可以动态更新运行脚本,和随时截无法识别的组件图片放入res文件夹教会LLM哪些没有文字的按键是干嘛的。



https://github.com/Gaaskiller/Ga ... in/GaasKillerBot.py

import os
import cv2
import numpy as np
from concurrent.futures import ThreadPoolExecutor
from PIL import Image, ImageDraw, ImageFont
from paddleocr import PaddleOCR
import pandas as pd
from openai import OpenAI
import re
import base64
import pyautogui
import json_repair
from IPython.display import display, Image
from PIL import Image as PILImage

# 保存屏幕截图
def 保存截图(保存路径):
    """
    保存当前屏幕截图到指定路径。
    """
    截图 = pyautogui.screenshot()
    截图.save(保存路径)
    print(f"截图已保存至:{保存路径}")

def 注释图片(输入图片路径, 模板文件夹路径, 输出图片路径, 匹配阈值):
    """
    遍历模板文件夹中的模板图片,在输入图片中查找其最佳匹配区域,并在匹配区域绘制动态文字。
    """
    def 读取图片(文件路径):
        """
        使用 cv2.imdecode 结合 np.fromfile 读取图片,支持中文路径。
        """
        文件数据 = np.fromfile(文件路径, dtype=np.uint8)
        return cv2.imdecode(文件数据, cv2.IMREAD_COLOR)

    def 查找最佳匹配(主图, 模板):
        """
        在主图中查找模板的最佳匹配区域,匹配比例固定为1:1。
        """
        最佳得分 = -1
        最佳位置 = 最佳区域大小 = None

        # 固定比例1:1进行模板匹配
        匹配结果 = cv2.matchTemplate(主图, 模板, cv2.TM_CCOEFF_NORMED)
        _, 最大值, _, 最大值位置 = cv2.minMaxLoc(匹配结果)

        if 最大值 > 最佳得分:
            最佳得分 = 最大值
            最佳位置 = 最大值位置
            最佳区域大小 = 模板.shape[1::-1]

        # 如果找到匹配区域,计算其中心点
        if 最佳位置 and 最佳区域大小:
            中心点 = (int(最佳位置[0] + 最佳区域大小[0] / 2),
                    int(最佳位置[1] + 最佳区域大小[1] / 2))
        else:
            中心点 = None

        return 中心点, 最佳位置, 最佳区域大小, 最佳得分


    def 绘制文字(主图, 文字, 匹配中心点, 匹配区域大小, 字体路径):
        """
        在匹配区域绘制文字,并动态调整字体大小适应区域。
        """
        pil图 = PILImage.fromarray(cv2.cvtColor(主图, cv2.COLOR_BGR2RGB))
        绘制器 = ImageDraw.Draw(pil图)
        最大宽度, 最大高度 = 匹配区域大小

        最小字体大小 = 10
        最大字体大小 = 100
        合适字体大小 = 最小字体大小

        while 最小字体大小 <= 最大字体大小:
            字体大小 = (最小字体大小 + 最大字体大小) // 2
            字体 = ImageFont.truetype(字体路径, 字体大小)
            文本边框 = 绘制器.textbbox((0, 0), 文字, font=字体)
            文本宽度 = 文本边框[2] - 文本边框[0]
            文本高度 = 文本边框[3] - 文本边框[1]

            if 文本宽度 <= 最大宽度 and 文本高度 <= 最大高度:
                合适字体大小 = 字体大小
                最小字体大小 = 字体大小 + 1
            else:
                最大字体大小 = 字体大小 - 1

        字体 = ImageFont.truetype(字体路径, 合适字体大小)
        文本边框 = 绘制器.textbbox((0, 0), 文字, font=字体)
        文本宽度 = 文本边框[2] - 文本边框[0]
        文本高度 = 文本边框[3] - 文本边框[1]
        文本X = 匹配中心点[0] - 文本宽度 // 2
        文本Y = 匹配中心点[1] - 文本高度 // 2
        绘制器.text((文本X, 文本Y), 文字, font=字体, fill=(0, 0, 0))
        主图[:] = cv2.cvtColor(np.array(pil图), cv2.COLOR_RGB2BGR)

    # 读取主图
    主图 = cv2.imread(输入图片路径, cv2.IMREAD_COLOR)
    if 主图 is None:
        raise FileNotFoundError(f"无法加载图片:{输入图片路径}")

    # 如果需要,可以调整缩放因子,或者不缩放
    缩放因子 = 1  # 可以根据需要调整缩放比例,或者注释掉这行代码不进行缩放
    主图 = cv2.resize(主图, None, fx=缩放因子, fy=缩放因子, interpolation=cv2.INTER_AREA)

    模板文件列表 = [f for f in os.listdir(模板文件夹路径) if f.endswith('.png')]

    def 处理单个模板(文件名):
        模板路径 = os.path.join(模板文件夹路径, 文件名)
        模板图片 = 读取图片(模板路径)
        if 模板图片 is None:
            print(f"无法加载模板图片:{模板路径}")
            return None

        # 如果主图进行了缩放,这里也需要对模板进行相同的缩放
        if '缩放因子' in locals():
            模板图片 = cv2.resize(模板图片, None, fx=缩放因子, fy=缩放因子, interpolation=cv2.INTER_AREA)

        中心点, 左上角, 区域大小, 得分 = 查找最佳匹配(主图, 模板图片)

        if 得分 >= 匹配阈值 and 左上角 and 区域大小:
            return (左上角, 区域大小, os.path.splitext(文件名)[0], 中心点)
        else:
            return None

    with ThreadPoolExecutor() as executor:
        处理结果列表 = list(executor.map(处理单个模板, 模板文件列表))

    # 在主线程中应用修改
    for 处理结果 in 处理结果列表:
        if 处理结果 is not None:
            左上角, 区域大小, 文字, 中心点 = 处理结果
            右下角 = (左上角[0] + 区域大小[0], 左上角[1] + 区域大小[1])
            cv2.rectangle(主图, 左上角, 右下角, (255, 255, 255), -1)
            绘制文字(主图, 文字, 中心点, 区域大小, 字体路径="msyh.ttc")

    # 保存处理后的图像
    cv2.imwrite(输出图片路径, 主图)
    print(f"注释后的图片已保存至:{输出图片路径}")

# 注释图片
def 注释图片_旧版(输入图片路径, 模板文件夹路径, 输出图片路径, 匹配阈值):
    """
    遍历模板文件夹中的模板图片,在输入图片中查找其最佳匹配区域,并在匹配区域绘制动态文字。
    """
    def 读取图片(文件路径):
        """
        使用 cv2.imdecode 结合 np.fromfile 读取图片,支持中文路径。
        """
        文件数据 = np.fromfile(文件路径, dtype=np.uint8)
        return cv2.imdecode(文件数据, cv2.IMREAD_COLOR)

    def 匹配比例(主图, 模板, 缩放比例):
        """
        按给定缩放比例对模板进行匹配,返回匹配得分和位置。
        """
        缩放后的模板 = cv2.resize(模板, None, fx=缩放比例, fy=缩放比例, interpolation=cv2.INTER_LINEAR)
        匹配结果 = cv2.matchTemplate(主图, 缩放后的模板, cv2.TM_CCOEFF_NORMED)
        _, 最大值, _, 最大值位置 = cv2.minMaxLoc(匹配结果)
        return 最大值, 最大值位置, 缩放后的模板.shape[1::-1]

    def 查找最佳匹配(主图, 模板, 缩放范围, 缩放步长):
        """
        在主图中查找模板的最佳匹配区域,考虑多种缩放比例。
        """
        最佳得分 = -1
        最佳位置 = 最佳区域大小 = None
        缩放列表 = np.arange(缩放范围[0], 缩放范围[1], 缩放步长)

        with ThreadPoolExecutor() as 线程池:
            匹配结果列表 = 线程池.map(lambda 缩放: 匹配比例(主图, 模板, 缩放), 缩放列表)

        for 匹配值, 匹配位置, 区域大小 in 匹配结果列表:
            if 匹配值 > 最佳得分:
                最佳得分 = 匹配值
                最佳位置 = 匹配位置
                最佳区域大小 = 区域大小

        if 最佳位置 and 最佳区域大小:
            中心点 = (int(最佳位置[0] + 最佳区域大小[0] / 2),
                    int(最佳位置[1] + 最佳区域大小[1] / 2))
        else:
            中心点 = None

        return 中心点, 最佳位置, 最佳区域大小, 最佳得分


    def 绘制文字(主图, 文字, 匹配中心点, 匹配区域大小, 字体路径):
        """
        在匹配区域绘制文字,并动态调整字体大小适应区域。
        """
        pil图 = PILImage.fromarray(cv2.cvtColor(主图, cv2.COLOR_BGR2RGB))  # 显式使用 PILImage
        绘制器 = ImageDraw.Draw(pil图)
        字体大小 = 20
        最大宽度, 最大高度 = 匹配区域大小

        while True:
            字体 = ImageFont.truetype(字体路径, 字体大小)
            文本边框 = 绘制器.textbbox((0, 0), 文字, font=字体)
            文本宽度 = 文本边框[2] - 文本边框[0]
            文本高度 = 文本边框[3] - 文本边框[1]

            if 文本宽度 > 最大宽度 or 文本高度 > 最大高度:
                字体大小 -= 1
                if 字体大小 <= 10:
                    break
            else:
                break

        文本X = 匹配中心点[0] - 文本宽度 // 2
        文本Y = 匹配中心点[1] - 文本高度 // 2
        绘制器.text((文本X, 文本Y), 文字, font=字体, fill=(0, 0, 0))
        主图[:] = cv2.cvtColor(np.array(pil图), cv2.COLOR_RGB2BGR)


    主图 = cv2.imread(输入图片路径, cv2.IMREAD_COLOR)
    if 主图 is None:
        raise FileNotFoundError(f"无法加载图片:{输入图片路径}")

    for 文件名 in os.listdir(模板文件夹路径):
        if 文件名.endswith('.png'):
            模板路径 = os.path.join(模板文件夹路径, 文件名)
            模板图片 = 读取图片(模板路径)
            if 模板图片 is None:
                print(f"无法加载模板图片:{模板路径}")
                continue

            中心点, 左上角, 区域大小, 得分 = 查找最佳匹配(主图, 模板图片, 缩放范围=(0.9, 1.1), 缩放步长=0.02)
            if 得分 >= 匹配阈值 and 左上角 and 区域大小:
                右下角 = (左上角[0] + 区域大小[0], 左上角[1] + 区域大小[1])
                cv2.rectangle(主图, 左上角, 右下角, (255, 255, 255), -1)
                绘制文字(主图, os.path.splitext(文件名)[0], 中心点, 区域大小, 字体路径="msyh.ttc")

    cv2.imwrite(输出图片路径, 主图)
    print(f"注释后的图片已保存至:{输出图片路径}")

# 识别图片
def 识别图片(图片路径, 置信度阈值,语言):
    """
    使用 PaddleOCR 识别图片中的文字,并返回符合置信度要求的结果 DataFrame。
    """
    import logging
    logging.getLogger('ppocr').setLevel(logging.ERROR)  # 设置日志级别为 ERROR

    ocr工具 = PaddleOCR(lang=语言)  # 初始化 OCR 工具
    识别结果 = ocr工具.ocr(图片路径, cls=False)  # 执行 OCR 识别

    def 计算中心点(点列表):
        """
        计算识别框的中心点坐标。
        """
        x坐标 = [点[0] for 点 in 点列表]
        y坐标 = [点[1] for 点 in 点列表]
        return [sum(x坐标) / len(x坐标), sum(y坐标) / len(y坐标)]

    数据 = []
    for 识别项 in 识别结果[0]:
        坐标, (文字, 置信度) = 识别项
        if 置信度 >= 置信度阈值:  # 过滤低置信度结果
            中心点 = 计算中心点(坐标)
            数据.append({"文字": 文字, "位置": 中心点, "置信度": 置信度})

    df = pd.DataFrame(数据)  # 转换为 DataFrame
    return df

def LLM推理下一步动作(图片路径: str, 图片文字元素DF: pd.DataFrame,运动逻辑地址,是否打印逻辑,运行情况记录) -> str:
    """
    使用 LLM 推理下一步应做的动作。
   
    :param 图片路径: 图片文件的路径。
    :param 图片文字元素DF: 包含图片内文字元素的 DataFrame。
    :return: LLM 推理的结果字符串。
    """
    def 构造消息(image_path: str, prompt_text: str):
        """
        将图像编码为 base64 并构造消息。
        """
        # 编码图像为 base64
        with open(image_path, "rb") as image_file:
            encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
        
        # 构造图像和文本的消息
        messages = [
            {
                "role": "user",
                "content": [
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": "data:image/png;base64," + encoded_string
                        }
                    },
                    {
                        "type": "text",
                        "text": prompt_text
                    }
                ]
            }
        ]
        return messages

    # 生成描述文字
    图片内文字描述 = 图片文字元素DF.to_string(index=False)  # 转换 DataFrame 为文本描述
        # 读取运动逻辑文件内容
    with open(运动逻辑地址, "r", encoding="utf-8") as f:
        运动逻辑内容 = f.read().strip()

    # 构造 prompt_text
    prompt_text = """
    任务要求:
    根据
    1:我输入的图片,
    2图中识别出的文字坐标:""" + 图片内文字描述 + """3: 过去的运行情况记录:""" +运行情况记录+"""4: Assistant的运动逻辑:""" + 运动逻辑内容 + """请告诉我,根据我之后将告诉你的\"运动逻辑\",下一步我应该点击哪个文字,对应的坐标是什么?
   
    输出要求:
    1:你需要根据图片中的文字和运动逻辑进行推理,给出下一步的操作和对应的文字,然后根据操作的文字,从“文字坐标”中查阅对应的坐标,返回给我。文字坐标中的key是文字,但是文字可能因为识别的原因有误差或者存在繁体/简体的问题,所以你需要根据你的推理,找到最接近的文字,然后返回对应的坐标。该文字对应的坐标X轴要严格的和“图中识别出的文字和XY坐标”中的X轴(第一个数字)对应,Y轴同理
   
    2: 你返回的输出是严格的JSON格式 ```json {"当前情况": %此处Assistant写下我输入的图片中有什么%,"Assistant思考下一步操作": %此处Assistant写下按照运动逻辑的指示,LLM要做什么%, "下一步要点击的文字": %此处根据LLM的推理写下下一步要点击的文字%,"点击坐标X轴": %此处是文字对应的X轴坐标%, "点击坐标Y轴": %此处是文字对应的Y轴坐标%, "运行情况记录": %记录下你之前打算做什么,现在正在做什么, 每次都要更新,不能一直用我输入的运行情况记录%}
   
    3 如果根据你的推理,现在什么操作都不用做,则在"点击坐标X轴“和”点击坐标Y轴“中填写"无"
   
    ```
    """

    if 是否打印逻辑:
        print("当前逻辑:", 运动逻辑内容)
    # 构造消息
    prompt = 构造消息(图片路径, prompt_text)

    # 配置 OpenAI 客户端
    client = OpenAI(api_key=%这里填入APIKEY%, base_url='https://api.openai.com/v1')
    setmodel = re.search(r"id='(.*?)', created=", str(client.models.list())).group(1) if re.search(r"id='(.*?)', created=", str(client.models.list())) else None

    # 调用 LLM 接口
    chat_completion_from_base64 = client.chat.completions.create(
        messages=prompt,  # 传入外部构造好的消息
        model=setmodel,
        max_tokens=4096,  # 最大 token
        temperature=0.7,  # 温度值
    )

    # 获取 LLM 的响应
    final_response = chat_completion_from_base64.choices[0].message.content

    return final_response


运动逻辑全文:

0:你遇见的
1: 进入打牌界面
打牌是一个最优先的事项,屏幕上显示“未在打牌”的时候,说明现在的界面没有选择打牌,点击“未在打牌”以进入打牌状态
2 打牌界面中打牌:
屏幕上显示“已在打牌”并且有"与他人对战"和“单人对战”按钮时,点击“与他人对战”
3 与他人对战的模式选择
屏幕上显示 活動對戰賽/私人對戰赛/隨機對戰賽 时候 点击活動對戰賽。
4 卡组的选择与开始对战
当你的屏幕上出现一个“对战”按钮时,点击“对战”按钮,然后等待进入“对战中”状态
5 对战中
屏幕上显示“Time limit”时,表示在对战,此时只需要投降即可,依次点击“战斗菜单”,“投降”
6:对战结束
屏幕上显示“VS XXXX”时, 此时不断点击继续,只有遇见“要送出谢谢吗?”时才点击“谢谢”。如果屏幕上只有一个“走人了",,则点击“关闭”。


如果遇上"点击画面开始游戏"时点击"Pocket"
如果碰上 "這場對戰推薦使用X屬性! 使用「自動編制」建立X屬性的牌組, 或去確認看看牌組任務吧" 时点击取消
如果碰上“错误,发生错误时候”点击“再试一次”




回复

使用道具 举报

     
发表于 2024-11-25 17:18 | 显示全部楼层
感觉不如直接用autojs写个脚本
回复

使用道具 举报

     
 楼主| 发表于 2024-11-25 17:32 | 显示全部楼层
Flareon 发表于 2024-11-25 17:18
感觉不如直接用autojs写个脚本

那个没法处理意料外情况吧,而且游戏有暴露出自己的activity可以点击吗?
回复

使用道具 举报

     
发表于 2024-11-26 09:47 | 显示全部楼层
咦,回帖的表情选择UI换了


PTCGP已经够悠闲了其实,每天上线5奖励可以靠好友点赞收藏来拿(不够才去对战一两局),每日任务登录+印卡+开包就够了所以对战也不是必须。

(回头有空试试能不能拿来肝废狗
回复

使用道具 举报

     
 楼主| 发表于 2024-11-26 09:51 | 显示全部楼层
本帖最后由 泰坦失足 于 2024-11-26 09:55 编辑
棍机凹升龙 发表于 2024-11-26 09:47
咦,回帖的表情选择UI换了



我后来在PTCGP的专楼里发现其实5秒内循环点这几个点,就能超快速的投降。算了,就当发明了个通用性的轮子吧,把pyautogui库的调用改成输入手柄信号还能去挂本地运行的游戏/串流的主机游戏


更新:不对 3/4这步会按到卡牌选择界面。如果返回战斗准备界面的瞬间正好在点3/4会进入选卡组界面 之所以还需要加上点取消的6才行。这么一看还是LLM能处理各种意外情况

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

     
发表于 2024-11-26 09:52 来自手机 | 显示全部楼层
卡够的话,我觉得不如直接微调多模态模型

—— 来自 OPPO PCLM10, Android 12上的 S1Next-鹅版 v2.5.4
回复

使用道具 举报

     
 楼主| 发表于 2024-11-26 09:57 | 显示全部楼层
本帖最后由 泰坦失足 于 2024-11-26 10:01 编辑
pf67 发表于 2024-11-26 09:52
卡够的话,我觉得不如直接微调多模态模型

—— 来自 OPPO PCLM10, Android 12上的 S1Next-鹅版 v2.5.4 ...
微调的意义是让模型知道没有文字的抽象图标是干嘛的吧,把图标总结在一个图上,作为第二张图发送过去作为也够了。
主要是很难指示到底要按哪个点。VLM能理解推理出要按哪个地方,却无法实时读取坐标。我试过给图像加上一个20x20的网格,每个网格中间有个数字指示。然后问VLM点击哪个数字。GPT-4o能很好的告诉你点击哪个数字,本地多模态模型就不行了。应该有paper和现成的开源项目,不过自己搭个轮子也没花多少时间。
回复

使用道具 举报

发表于 2024-11-26 11:07 | 显示全部楼层
github 链接失效了
回复

使用道具 举报

     
 楼主| 发表于 2024-11-26 11:15 | 显示全部楼层

https://shorturl.at/oQnsD
也就是主楼里这些东西
回复

使用道具 举报

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

本版积分规则

Archiver|手机版|小黑屋|上海互联网违法和不良信息举报中心|网上有害信息举报专区|962110 反电信诈骗|举报电话 021-62035905|Stage1st ( 沪ICP备13020230号-1|沪公网安备 31010702007642号 )

GMT+8, 2024-12-29 01:34 , Processed in 0.037984 second(s), 5 queries , Gzip On, Redis On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表