动物/猛兽派对自动匹配挂机脚本
import time
import mss
import cv2
import numpy as np
import pygetwindow as gw
from PIL import ImageGrab

getpic = mss.mss()
tolerancex,tolerancey = 20,20
explocation = {
    "room": (1500, 400, 300, 200),
    "quickgame": (1500, 200, 300, 200),
}

def getWindowPic():
    start = time.time()
    window = gw.getWindowsWithTitle('猛兽派对')[0]
    left, top, width, height = window.left, window.top, window.width, window.height
    screenshot =ImageGrab.grab((left, top, left+width, top+height))
    image = np.array(screenshot)
    image = cv2.cvtColor(image, cv2.COLOR_RGBA2BGR)
    cv2.imwrite("./imgs/exp_r.png", image)
    end = time.time()
    print(end - start)



def getAllExp(bounding_rects):
    try:
        sorted_arr = bounding_rects[bounding_rects[:, 1].argsort()]
        return sorted_arr[0]
    except:
        print('没有数字')
        return None

# 加载图像
def getExpimg(model):
    getWindowPic()
    time.sleep(0.3)
    image_path = cv2.imread('./imgs/exp_r.png')
    if model == 'room':
        region_x, region_y, region_w, region_h = explocation["room"]
    else:
        region_x, region_y, region_w, region_h = explocation["quickgame"]
    image = image_path[region_y:region_y+region_h, region_x:region_x+region_w]
    lower_color = np.array([42,180,140])
    upper_color = np.array([45, 200, 150])
    mask = cv2.inRange(image, lower_color, upper_color)
    # 形态学操作
    kernel = np.ones((10, 10), np.uint8)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
    # 查找轮廓
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE,)
    # 处理每个轮廓
    bounding_rects = []
    for contour in contours:
        x, y, w, h = cv2.boundingRect(contour)
        bounding_rects.append([x+region_x,y+region_y,w,h])
    bounding_rects = np.array(bounding_rects)
    print(bounding_rects)
    result = getAllExp(bounding_rects)
    if result is None:
        return
    rectangle_x,rectangle_y,rectangle_w,rectangle_h = result[0]-tolerancex,result[1]-tolerancey,result[2]+tolerancex+10,result[3]+tolerancey+10
    exp_pic = image_path[rectangle_y:rectangle_y+rectangle_h,rectangle_x:rectangle_x+rectangle_w]
    cv2.imwrite('./imgs/exp.png',exp_pic)

PickExpnumber.py
AprDeci/animal-auto
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'cmdoutput.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_Cmdoutput(object):
    def setupUi(self, Cmdoutput):
        Cmdoutput.setObjectName("Cmdoutput")
        Cmdoutput.resize(456, 265)
        Cmdoutput.setStyleSheet("#Cmdoutput{\n"
"    background-color: qlineargradient(spread:pad, x1:0.233831, y1:0.267, x2:0.831025, y2:0.880364, stop:0 rgba(245,247,250,1), stop:1 rgba(195,207,226));\n"
"}")
        self.Cmd = TextEdit(Cmdoutput)
        self.Cmd.setGeometry(QtCore.QRect(28, 74, 401, 195))
        self.Cmd.setStyleSheet("LineEdit, TextEdit, PlainTextEdit {\n"
"    color: black;\n"
"    background-color: rgba(255, 255, 255, 0.7);\n"
"    border: 1px solid rgba(0, 0, 0, 13);\n"
"    border-bottom: 1px solid rgba(0, 0, 0, 100);\n"
"    border-radius: 5px;\n"
"    /* font: 14px \"Segoe UI\", \"Microsoft YaHei\"; */\n"
"    padding: 0px 10px;\n"
"    selection-background-color: #00a7b3;\n"
"}\n"
"\n"
"TextEdit,\n"
"PlainTextEdit {\n"
"    padding: 2px 3px 2px 8px;\n"
"}\n"
"\n"
"LineEdit:hover, TextEdit:hover, PlainTextEdit:hover {\n"
"    background-color: rgba(249, 249, 249, 0.5);\n"
"    border: 1px solid rgba(0, 0, 0, 13);\n"
"    border-bottom: 1px solid rgba(0, 0, 0, 100);\n"
"}\n"
"\n"
"LineEdit:focus {\n"
"    border-bottom: 1px solid rgba(0, 0, 0, 13);\n"
"    background-color: white;\n"
"}\n"
"\n"
"TextEdit:focus,\n"
"PlainTextEdit:focus {\n"
"    border-bottom: 1px solid #009faa;\n"
"    background-color: white;\n"
"}\n"
"\n"
"LineEdit:disabled, TextEdit:disabled,\n"
"PlainTextEdit:disabled {\n"
"    color: rgba(0, 0, 0, 150);\n"
"    background-color: rgba(249, 249, 249, 0.3);\n"
"    border: 1px solid rgba(0, 0, 0, 13);\n"
"    border-bottom: 1px solid rgba(0, 0, 0, 13);\n"
"}\n"
"\n"
"#lineEditButton {\n"
"    background-color: transparent;\n"
"    border-radius: 4px;\n"
"    margin: 0;\n"
"}\n"
"\n"
"#lineEditButton:hover {\n"
"    background-color: rgba(0, 0, 0, 9);\n"
"}\n"
"\n"
"#lineEditButton:pressed {\n"
"    background-color: rgba(0, 0, 0, 6);\n"
"}\n"
"")
        self.Cmd.setReadOnly(False)
        self.Cmd.setMarkdown("")
        self.Cmd.setObjectName("Cmd")
        self.currentNumLabel = StrongBodyLabel(Cmdoutput)
        self.currentNumLabel.setGeometry(QtCore.QRect(317, 45, 113, 19))
        self.currentNumLabel.setObjectName("currentNumLabel")
        self.explabel = StrongBodyLabel(Cmdoutput)
        self.explabel.setGeometry(QtCore.QRect(41, 45, 160, 25))
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.explabel.sizePolicy().hasHeightForWidth())
        self.explabel.setSizePolicy(sizePolicy)
        self.explabel.setMinimumSize(QtCore.QSize(1, 0))
        self.explabel.setObjectName("explabel")

        self.retranslateUi(Cmdoutput)
        QtCore.QMetaObject.connectSlotsByName(Cmdoutput)

    def retranslateUi(self, Cmdoutput):
        _translate = QtCore.QCoreApplication.translate
        Cmdoutput.setWindowTitle(_translate("Cmdoutput", "Form"))
        self.Cmd.setHtml(_translate("Cmdoutput", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Segoe UI\',\'Microsoft YaHei\',\'PingFang SC\',\'SimSun\'; font-size:14px; font-weight:400; font-style:normal;\">\n"
"<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><br /></p></body></html>"))
        self.currentNumLabel.setText(_translate("Cmdoutput", "🎮当前进行第0局"))
        self.explabel.setText(_translate("Cmdoutput", "🕹总计获取经验:"))
from qfluentwidgets import StrongBodyLabel, TextEdit

Ui/cmdoutput.py
AprDeci/animal-auto
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'fluentuidemo1.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtWidgets


class Ui_mainWindow(object):
    def setupUi(self, mainWindow):
        mainWindow.setObjectName("mainWindow")
        mainWindow.resize(456, 245)
        mainWindow.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
        mainWindow.setStyleSheet("#mainWindow{\n"
"    background-color: qlineargradient(spread:pad, x1:0.233831, y1:0.267, x2:0.831025, y2:0.880364, stop:0 rgba(245,247,250,1), stop:1 rgba(195,207,226,1));\n"
"}")
        self.centralwidget = QtWidgets.QWidget(mainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.layoutWidget = QtWidgets.QWidget(self.centralwidget)
        self.layoutWidget.setGeometry(QtCore.QRect(50, 110, 364, 34))
        self.layoutWidget.setObjectName("layoutWidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.layoutWidget)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.roombuttom = PushButton(self.layoutWidget)
        self.roombuttom.setMinimumSize(QtCore.QSize(140, 0))
        self.roombuttom.setObjectName("roombuttom")
        self.horizontalLayout.addWidget(self.roombuttom)
        self.quickgamebutton = PrimaryPushButton(self.layoutWidget)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.quickgamebutton.sizePolicy().hasHeightForWidth())
        self.quickgamebutton.setSizePolicy(sizePolicy)
        self.quickgamebutton.setMinimumSize(QtCore.QSize(140, 0))
        self.quickgamebutton.setObjectName("quickgamebutton")
        self.horizontalLayout.addWidget(self.quickgamebutton)
        self.layoutWidget1 = QtWidgets.QWidget(self.centralwidget)
        self.layoutWidget1.setGeometry(QtCore.QRect(100, 30, 260, 68))
        self.layoutWidget1.setObjectName("layoutWidget1")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget1)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.BodyLabel = BodyLabel(self.layoutWidget1)
        self.BodyLabel.setObjectName("BodyLabel")
        self.horizontalLayout_3.addWidget(self.BodyLabel)
        spacerItem = QtWidgets.QSpacerItem(58, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout_3.addItem(spacerItem)
        self.gameNumberInput = SpinBox(self.layoutWidget1)
        self.gameNumberInput.setObjectName("gameNumberInput")
        self.horizontalLayout_3.addWidget(self.gameNumberInput)
        self.verticalLayout.addLayout(self.horizontalLayout_3)
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.ifNumber = CheckBox(self.layoutWidget1)
        self.ifNumber.setObjectName("ifNumber")
        self.horizontalLayout_2.addWidget(self.ifNumber)
        spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout_2.addItem(spacerItem1)
        self.ifshutdown = CheckBox(self.layoutWidget1)
        self.ifshutdown.setMinimumSize(QtCore.QSize(29, 22))
        self.ifshutdown.setObjectName("ifshutdown")
        self.horizontalLayout_2.addWidget(self.ifshutdown)
        self.verticalLayout.addLayout(self.horizontalLayout_2)
        self.layoutWidget2 = QtWidgets.QWidget(self.centralwidget)
        self.layoutWidget2.setGeometry(QtCore.QRect(40, 160, 361, 33))
        self.layoutWidget2.setObjectName("layoutWidget2")
        self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.layoutWidget2)
        self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")
        self.BlogLink = HyperlinkButton(self.layoutWidget2)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.BlogLink.sizePolicy().hasHeightForWidth())
        self.BlogLink.setSizePolicy(sizePolicy)
        self.BlogLink.setUrl(QtCore.QUrl("http://www.aprdec.top"))
        self.BlogLink.setObjectName("BlogLink")
        self.horizontalLayout_4.addWidget(self.BlogLink)
        self.Githublink = HyperlinkButton(self.layoutWidget2)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.Githublink.sizePolicy().hasHeightForWidth())
        self.Githublink.setSizePolicy(sizePolicy)
        self.Githublink.setObjectName("Githublink")
        self.Githublink.setUrl(QtCore.QUrl("https://github.com/q1263868407/animal-auto"))
        self.horizontalLayout_4.addWidget(self.Githublink)
        mainWindow.setCentralWidget(self.centralwidget)
        self.statusbar = QtWidgets.QStatusBar(mainWindow)
        self.statusbar.setObjectName("statusbar")
        mainWindow.setStatusBar(self.statusbar)
        self.retranslateUi(mainWindow)
        self.quickgamebutton.clicked.connect(mainWindow.beginquick) # type: ignore
        self.roombuttom.clicked.connect(mainWindow.beginroom) # type: ignore
        self.ifshutdown.clicked.connect(lambda:self.ifNumber.setChecked(True))# type: ignore
        QtCore.QMetaObject.connectSlotsByName(mainWindow)

    def retranslateUi(self, mainWindow):
        _translate = QtCore.QCoreApplication.translate
        mainWindow.setWindowTitle(_translate("mainWindow", "MainWindow"))
        self.roombuttom.setText(_translate("mainWindow", "自建房间"))
        self.quickgamebutton.setText(_translate("mainWindow", "快速游戏"))
        self.BodyLabel.setText(_translate("mainWindow", "限定局数"))
        self.ifNumber.setText(_translate("mainWindow", "限定局数"))
        self.ifshutdown.setText(_translate("mainWindow", "挂机后关机"))
        self.BlogLink.setText(_translate("mainWindow", "作者博客"))
        self.Githublink.setText(_translate("mainWindow", "项目地址"))
from qfluentwidgets import BodyLabel, CheckBox, HyperlinkButton, PrimaryPushButton, PushButton, SpinBox

Ui/controWin.py
AprDeci/animal-auto
from enum import Enum

from qfluentwidgets import getIconColor, Theme, FluentIconBase


class myIcon(FluentIconBase, Enum):
    """ Custom icons """

    CMD='cmd'
    GAME='游戏'

    def path(self, theme=Theme.AUTO):
        return f'./imgs/ui/{self.value}.svg'

Ui/myIcon.py
AprDeci/animal-auto
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'setup.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_setup(object):
    def setupUi(self, setup):
        setup.setObjectName("setup")
        setup.resize(444, 450)
        setup.setStyleSheet("#setup{\n"
"    background-color: qlineargradient(spread:pad, x1:0.233831, y1:0.267, x2:0.831025, y2:0.880364, stop:0 rgba(245,247,250,1), stop:1 rgba(195,207,226));\n"
"}")
        self.OcrselectWidget = HeaderCardWidget(setup)
        self.OcrselectWidget.setGeometry(QtCore.QRect(0, 80, 431, 111))
        self.OcrselectWidget.setTitle("")
        self.OcrselectWidget.setObjectName("OcrselectWidget")
        self.freeocrApi = LineEdit(self.OcrselectWidget)
        self.freeocrApi.setGeometry(QtCore.QRect(10, 60, 431, 40))
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.freeocrApi.sizePolicy().hasHeightForWidth())
        self.freeocrApi.setSizePolicy(sizePolicy)
        self.freeocrApi.setMinimumSize(QtCore.QSize(0, 40))
        self.freeocrApi.setObjectName("freeocrApi")
        self.splitter = QtWidgets.QSplitter(self.OcrselectWidget)
        self.splitter.setGeometry(QtCore.QRect(10, 10, 581, 24))
        self.splitter.setOrientation(QtCore.Qt.Horizontal)
        self.splitter.setObjectName("splitter")
        self.LocalOcr = RadioButton(self.splitter)
        self.LocalOcr.setObjectName("LocalOcr")
        self.FreeOcr = RadioButton(self.splitter)
        self.FreeOcr.setObjectName("FreeOcr")
        self.layoutWidget = QtWidgets.QWidget(setup)
        self.layoutWidget.setGeometry(QtCore.QRect(19, 44, 408, 29))
        self.layoutWidget.setObjectName("layoutWidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.layoutWidget)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.SubtitleLabel = SubtitleLabel(self.layoutWidget)
        self.SubtitleLabel.setObjectName("SubtitleLabel")
        self.horizontalLayout.addWidget(self.SubtitleLabel)
        spacerItem = QtWidgets.QSpacerItem(203, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)
        self.HyperlinkLabel = HyperlinkLabel(self.layoutWidget)
        self.HyperlinkLabel.setObjectName("HyperlinkLabel")
        self.HyperlinkLabel.setUrl('https://ocr.space/')
        self.horizontalLayout.addWidget(self.HyperlinkLabel)
        self.OK = PrimaryPushButton(setup)
        self.OK.setGeometry(QtCore.QRect(270, 210, 153, 32))
        self.OK.setObjectName("OK")

        self.retranslateUi(setup)
        QtCore.QMetaObject.connectSlotsByName(setup)

    def retranslateUi(self, setup):
        _translate = QtCore.QCoreApplication.translate
        setup.setWindowTitle(_translate("setup", "Form"))
        self.freeocrApi.setPlaceholderText(_translate("setup", "输入你的FreeOcr Api key"))
        self.LocalOcr.setText(_translate("setup", "本地OCR"))
        self.FreeOcr.setText(_translate("setup", "FREEOCR"))
        self.SubtitleLabel.setText(_translate("setup", "OCR选择"))
        self.HyperlinkLabel.setText(_translate("setup", "FreeOCRAPI申请"))
        self.OK.setText(_translate("setup", "确定"))
from qfluentwidgets import HeaderCardWidget, HyperlinkLabel, LineEdit, PrimaryPushButton, RadioButton, SubtitleLabel

Ui/setup.py
AprDeci/animal-auto
import cv2
import numpy as np


def isthereCircle(filepath):
    img = cv2.imread(filepath, 0)
    size = 200
    x1 = 0
    y1 = 0
    x2 = x1+size
    y2 = y1+size
    cut = img[y1:y2, x1:x2]
    numpy_img = cv2.adaptiveThreshold(cut, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 9, 13)   # 自动阈值二值化
    circles = cv2.HoughCircles(numpy_img, cv2.HOUGH_GRADIENT , 1 , 90 , param1=100 , param2= 30 , minRadius=30 , maxRadius=60)
    if circles is not None:
        print(f'圆形坐标{circles}')
        return True
    else:
        return False


if __name__ == '__main__':
    isthereCircle('./testrecources/1.png')
circle_detection.py
AprDeci/animal-auto
import sys

from PyQt5 import QtCore
from PyQt5.QtCore import pyqtSignal, QThread
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import QWidget, QApplication

import freeocr
from Ui.cmdoutput import Ui_Cmdoutput



class cmdoutput(QWidget, Ui_Cmdoutput):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.Cmd.setReadOnly(True)
        self.CurrentExp = 0


    def append_to_log(self, text):
        cursor = self.Cmd.textCursor()
        cursor.movePosition(QTextCursor.End)
        cursor.insertText(text)
        self.Cmd.setTextCursor(cursor)
    def change_currentNum_Label(self,text):
        self.currentNumLabel.setText(text)
    def change_currentexp(self,text):
        self.CurrentExp += int(text)
        self.explabel.setText(f"🕹总计获取经验: {self.CurrentExp}")
if __name__=="__main__":
    QApplication.setHighDpiScaleFactorRoundingPolicy(QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
    QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
    QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)

    app = QApplication(sys.argv)
    window = cmdoutput()
    window.show()
    app.exit(app.exec_())
cmdoutput_inter.py
AprDeci/animal-auto
import os
import sys
import time
import configparser as ini
import cv2
import mss
import numpy as np
import pyautogui
from PyQt5.QtCore import QThread, pyqtSignal
import doacntion
import freeocr
import paddleOcr
from Ui.controWin import Ui_mainWindow
from PyQt5.QtWidgets import QWidget, QMainWindow
from qfluentwidgets import FluentIcon as FIF

from circle_detection import isthereCircle
from PickExpnumber import getExpimg
import pygetwindow as gw


THRESHOLD=0.02


class WorkerThread(QThread):
    send_signal = pyqtSignal(str)
    QcurrentNum_signal = pyqtSignal(str)
    Qgetexp_signal = pyqtSignal(str)
    def __init__(self, operation_func, *args, **kwargs):
        super().__init__()
        self.operation_func = operation_func
        self.args = args
        self.kwargs = kwargs
        self.ingame = False
        self.to_action = True
        self.last_executed_times = {
            'begin_room.png': 0,
            'begin.png': 0,
            'ready.png': 0,
            'endgame.png': 0,
            'expup2.png': 0,
            'quickgame.png': 0,
            'cemera.png': 0,
            'gamenum':0,
            'longtimeO.png':0,
            'whengame.png':0
        }
        self.action_ready = False
        self.current_gameNum = 0
        conf = ini.ConfigParser()
        conf.read("./imgs/config.ini",encoding='utf-8')
        self.localOcr = conf.get("ocr", "localocr")
        self.FreeOcr = conf.get("ocr", "freeocr")
        self.FreeOcr_api = conf.get("ocr", "freeocrapi")
    def run(self):
        if self.operation_func=="quickgame":
            self.quickgame(*self.args, **self.kwargs)
        elif self.operation_func=="roomgame":
            self.roomgame(*self.args, **self.kwargs)
    def stop(self):
        self.terminate()
        self.finished.emit()

    def get_screenshot(self,region=None):
        if region==None:
            pyautogui.screenshot().save("./imgs/screenshot.png")
        else:
            pyautogui.screenshot(region=region).save("./imgs/exp.png")

    def get_xy(self, img_path):
        img = cv2.imread("./imgs/screenshot.png")
        img_aim = cv2.imread(img_path)
        height, width, channel = img_aim.shape
        result = cv2.matchTemplate(img, img_aim, cv2.TM_SQDIFF_NORMED)
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
        if min_val < THRESHOLD:
            upper_left = min_loc
            # 计算出匹配区域右下角图标（左上角坐标加上模板的长宽即可得到）
            lower_right = (upper_left[0] + width, upper_left[1] + height)
            # 计算坐标的平均值并将其返回
            avg = (int((upper_left[0] + lower_right[0]) / 2), int((upper_left[1] + lower_right[1]) / 2))
        else:  # 左上角坐标
            avg = None
        return avg

    def auto_click(self, coords):
        pyautogui.click(coords[0], coords[1], button='left')
        time.sleep(1)

    def routine_click(self, img_path, name):
        avg = self.get_xy(img_path)
        if avg == None:
            pass
        else:
            print(f"正在点击{name},{avg}")
            self.send_signal.emit(f"正在点击{name},{avg}\n")
            self.auto_click(avg)
            self.to_action = False

    def ifrouutineclick(self, img_path, name):
        avg = self.get_xy(img_path)
        if avg == None:
            return False
        else:
            print(f"正在点击{name},{avg}")
            self.send_signal.emit(f"正在点击{name},{avg}\n")
            time.sleep(0.5)
            pyautogui.click(avg)
            self.to_action = False
            return True

    def ifthereimg(self, img_path, name):
        avg = self.get_xy(img_path)
        if avg == None:
            return False,avg
        else:
            print(f"{name},{avg}")
            self.send_signal.emit(f"{name},{avg}\n")
            return True,avg

    def routinespace(self, img_path, name):
        avg = self.get_xy(img_path)
        if avg == None:
            pass
        else:
            print(f"正在点击{name},{avg}")
            self.send_signal.emit(f"正在点击{name},{avg}\n")
            time.sleep(0.5)
            pyautogui.press('space')
            self.to_action = False

    def ifroutinpress(self, img_path, name, key):
        avg = self.get_xy(img_path)
        if avg == None:
            return False
        else:
            print(f"正在点击{name},{avg}")
            self.send_signal.emit(f"正在点击{name},{avg}\n")
            time.sleep(0.5)
            pyautogui.press(key)
            self.to_action = False
            return True

    def Ocr(self, img_path):
        if self.localOcr=='True':
            return paddleOcr.getexp(img_path)
        else:
            if self.FreeOcr_api==None:
                return freeocr.getexp(img_path)
            else:
                return freeocr.getexp(img_path,self.FreeOcr_api)

    def execute_condition(self, current_time, condition, interval):
        if current_time - self.last_executed_times[condition] >= interval:
            self.last_executed_times[condition] = current_time
            return True
        else:
            return False

    def quickgame(self, limit_number, shutdown, game_number):
        while True:
            current_time = time.time()
            self.get_screenshot()
            time.sleep(0.3)
            if not self.ingame:#游戏状态外
                if self.execute_condition(current_time, 'ready.png', 30):
                    if self.ifroutinpress('./imgs/ready3.png', '准备', 'space'):
                        time.sleep(20)
                        self.action_ready = True
                        self.ingame = True
                if self.execute_condition(current_time, 'endgame.png', 5):
                    avg=self.ifthereimg('./imgs/end.png', '结束游戏')[1]
                    if avg is not None:
                        getExpimg('quickgame')
                        pyautogui.leftClick(avg[0],avg[1])
                        self.action_ready = False
                        if (limit_number):
                            if (self.current_gameNum == game_number):
                                self.exit(shutdown)
                            break
                        getexp=self.Ocr("./imgs/exp.png")
                        if getexp == 'error' or getexp =='Network error':
                            self.send_signal.emit(f"没有读取到数字或网络请求失败了\n")
                            continue
                        else:
                            self.send_signal.emit(f"获取经验: {getexp}\n")
                            self.Qgetexp_signal.emit(getexp)
                if self.execute_condition(current_time, 'begin.png', 5):
                    if self.ifrouutineclick('./imgs/begin.png', '开始游戏'):
                        self.current_gameNum += 1
                        self.QcurrentNum_signal.emit(f"🎮当前进行第{self.current_gameNum}局")
                if self.execute_condition(current_time, 'expup2.png', 50):
                    self.routine_click('./imgs/expup.png', '升级')
                if self.execute_condition(current_time, 'expup2.png', 50):
                    self.routine_click('./imgs/expup2.png', '升级2')
                if self.execute_condition(current_time, 'quickgame.png', 50):
                    self.routine_click('./imgs/quickgame.png', '快速游戏')
                if self.execute_condition(current_time, 'longtimeO.png', 50):
                    self.routine_click('./imgs/longtimeO.png', '长时间不操作')
            else:
                if self.execute_condition(current_time,'whengame.png',50):
                    if isthereCircle('./imgs/screenshot.png'):#检测头像
                        self.action_ready=True
                if self.execute_condition(current_time, 'cemera.png', 5):
                    if self.ifthereimg('./imgs/cemera.png', '暂停行动')[0]:
                        self.action_ready = False
                        self.ingame=False
                if self.action_ready:  # 只在准备后结束前运行
                    if self.to_action:  # 只在没有检测到目标图片时运行
                        doacntion.action(self.current_gameNum)
                self.to_action = True
                time.sleep(0.3)

    def exit(self, shutdown):
        time.sleep(2)
        while True:
            self.get_screenshot()
            time.sleep(0.8)
            self.routine_click('./imgs/return.png', '返回主界面')
            self.routine_click('./imgs/exit.png', '退出游戏')
            time.sleep(3)
            if self.ifrouutineclick('./imgs/exitis.png', '是'):
                if shutdown:
                    os.system("shutdown -s -t  30 ")
                sys.exit()  # 退出程序

    def roomgame(self, shutdown):
        while True:
            self.get_screenshot()
            time.sleep(0.3)
            pyautogui.press('w')
            self.routinespace('./imgs/ready3.png', '准备')
            if self.ifrouutineclick('./imgs/begin_room.png', '开始游戏'):
                self.current_gameNum += 1
                self.QcurrentNum_signal.emit(f"🎮当前进行第{self.current_gameNum}局")
            self.routine_click('./imgs/expup.png', '升级')
            self.routine_click('./imgs/expup2.png', '升级2')
            if self.ifthereimg('./imgs/xiangce.png', '结算')[0]:
                getExpimg('room')
                time.sleep(0.3)
                getexp=self.Ocr("./imgs/exp.png")
                if getexp == 'error':
                    continue
                elif getexp == '0':
                    if(shutdown):
                        os.system("shutdown -s -t  30 ")
                    sys.exit()
                else:
                    self.send_signal.emit(f"获取经验: {getexp}\n")
                    self.Qgetexp_signal.emit(getexp)
                    time.sleep(8)
class controlWin_inter(QMainWindow,Ui_mainWindow):
    print_signal = pyqtSignal(str)
    switch_signal = pyqtSignal()
    currentNum_signal=pyqtSignal(str)
    currentExp_signal=pyqtSignal(str)
    def __init__(self,parent=None):
        super().__init__(parent=parent)
        self.setupUi(self)
        self.setContentsMargins(0, 40, 0, 0)
        self.Githublink.setIcon(FIF.GITHUB)
        self.BlogLink.setIcon(FIF.LINK)
        self.operation_thread=None
        self.ifshutdown.clicked.connect(lambda: self.ifNumber.setChecked(True))




    def getinfo(self):
        self.shutdown = self.ifshutdown.isChecked()
        self.limitNumber = self.ifNumber.isChecked()
        self.gameNumber = self.gameNumberInput.value()

    def start_operation_thread(self, operation_func, *args, **kwargs):
        if not self.operation_thread or not self.operation_thread.isRunning():
            self.operation_thread = WorkerThread(operation_func, *args, **kwargs)
            self.operation_thread.send_signal.connect(self.sendmessage)
            self.operation_thread.QcurrentNum_signal.connect(self.currentNumLabel_change)
            self.operation_thread.Qgetexp_signal.connect(self.currentExpLabel_change)
            self.operation_thread.start()
            match operation_func:
                case "roomgame":
                    self.roombuttom.setText("停止脚本")
                    self.quickgamebutton.setEnabled(False)
                    self.switch_signal.emit()
                case "quickgame":
                    self.quickgamebutton.setText("停止脚本")
                    self.roombuttom.setEnabled(False)
                    self.switch_signal.emit()
        else:
            self.operation_thread.stop()
            match operation_func:
                case "roomgame":
                    self.roombuttom.setText("自建房间")
                    self.quickgamebutton.setEnabled(True)
                case "quickgame":
                    self.quickgamebutton.setText("快速游戏")
                    self.roombuttom.setEnabled(True)


    def on_operation_finished(self):
        pass

    def beginquick(self):
        self.getinfo()
        self.start_operation_thread("quickgame",self.limitNumber, self.shutdown, self.gameNumber)

    def beginroom(self):
        self.getinfo()
        self.start_operation_thread("roomgame",self.shutdown)

    def sendmessage(self,str):
        self.print_signal.emit(str)
        
    def currentNumLabel_change(self,str):
        self.currentNum_signal.emit(str)
    def currentExpLabel_change(self,str):
        self.currentExp_signal.emit(str)




controWin_inter.py
AprDeci/animal-auto
import random

import pyautogui
import time

# 模拟按下和释放多个按键的函数
def press_simulta_keys(keys):
    for key in keys:
        pyautogui.keyDown(key)
    time.sleep(random.randint(1,3))
    for key in keys:
        pyautogui.keyUp(key)

# 定义一些游戏操作函数
keys=[
    ['a','w'],
    ['d','s'],
    ['j','space'],
    ['a','s'],
    ['d','a'],
    ['space','a'],
    ['d','w'],
    ['d','space'],
    ['w','space'],
    ['s','space'],
    ['j','w','a'],
    ['j','s','a'],
    ['j','d','a'],
    ['j','d','w'],
    ['j','space','a'],
    ['j','space','w'],
    ['j','space','s'],
    ['j','space','d'],
    ['w','shift'],
    ['s','shift'],
    ['a','shift'],
    ['d','shift'],
    ['w','a','shift'],
    ['w','d','shift'],
    ['s','a','shift'],
    ['s','d','shift'],
]

def action(game_number):
    for i in range(3,5):
        nextkeys = random.choice(keys)
        print(f"第{game_number}局:", nextkeys)
        press_simulta_keys(nextkeys)

doacntion.py
AprDeci/animal-auto
import requests
def getexp(file_path,key='K87420617888957'):
    url = 'https://api.ocr.space/parse/image'
    file_path = file_path
    file = open(file_path, 'rb')
    files = {'file': file}
    data = {
        "apikey": key,
        "language": "eng",
    }
    try:
        response = requests.post(url, files=files, data=data)
        if response.status_code != 200:
            print('error')
            return 'error'
        result = response.json()['ParsedResults'][0]['ParsedText'].replace(',', '').replace('+', '')
        file.close()  # 关闭文件
        if result == '':
            print('获得经验：' + result)
            return 'error'
        else:
            print('获得经验：' + result)
            return result
    except Exception as e:
        return 'Network error'
if __name__=="__main__":
    getexp('./imgs/exp.png')
freeocr.py
AprDeci/animal-auto
import sys
import requests
from PyQt5 import QtCore
from PyQt5.QtCore import  QUrl,Qt
from PyQt5.QtGui import QDesktopServices, QIcon
from PyQt5.QtWidgets import QApplication
from qfluentwidgets import SplitFluentWindow, setThemeColor, NavigationAvatarWidget, NavigationItemPosition, MessageBox, \
    InfoBar, InfoBarPosition, FluentIcon
from cmdoutput_inter import cmdoutput
from controWin_inter import  controlWin_inter
from Ui.myIcon import myIcon
from setup_inter import setupWin


current_version = 1.83
class mainwindow(SplitFluentWindow):
    def __init__(self):
        super().__init__()
        setThemeColor('#0066CC')
        self.setWindowTitle(f"动物排队挂机脚本{current_version}")
        self.setWindowIcon(QIcon("./imgs/ui/ico.png"))
        self.setMinimumSize(500,260)
        self.resize(500, 290)
        self.setFixedSize(self.width(), self.height())
        self.controwin=controlWin_inter()
        self.addSubInterface(self.controwin, myIcon.GAME, "游戏")
        self.cmdoutput = cmdoutput()
        self.addSubInterface(self.cmdoutput, myIcon.CMD, "统计")
        self.setupWin = setupWin()
        self.navigationInterface.addWidget(
            routeKey='avatar',
            widget=NavigationAvatarWidget('AprDec', './imgs/ui/aprdec.jpg'),
            onClick=self.showMessageBox,
            position=NavigationItemPosition.BOTTOM,
        )
        self.addSubInterface(self.setupWin, FluentIcon.SETTING, "设置",position=NavigationItemPosition.BOTTOM)


        self.controwin.print_signal.connect(self.cmdoutput.append_to_log)
        self.controwin.switch_signal.connect(self.Switchto)
        self.controwin.currentNum_signal.connect(self.cmdoutput.change_currentNum_Label)
        self.controwin.currentExp_signal.connect(self.cmdoutput.change_currentexp)
        self.check_version()

    def showMessageBox(self):
        w = MessageBox(
            '支持作者🥰',
            '学习作品,如果觉得不错,打赏一杯柠檬水吧🍸,或者给本项目点个Star吧✨',
            self
        )
        w.yesButton.setText('支持一波')
        w.cancelButton.setText('下次一定')
        if w.exec():
            QDesktopServices.openUrl(QUrl("https://afdian.net/a/Aprdec"))
    def Switchto(self):
        self.switchTo(self.cmdoutput)

    def check_version(self):
        try:
            latest_version= requests.get('https://api.github.com/repos/AprDeci/animal-auto/releases/latest').json()['tag_name'][1:]
            if current_version < float(latest_version):
                InfoBar.new(
                    icon=FluentIcon.GITHUB,
                    title='',
                    content=f"作者又叕叒更新啦,新版本{latest_version}已经发布啦,快去下载吧",
                    orient=Qt.Horizontal,
                    isClosable=True,  # disable close button
                    position=InfoBarPosition.TOP_LEFT,
                    duration=4000,
                    parent=self
                ).setCustomBackgroundColor("#F5C563", "#000000")
            else:
                InfoBar.success(
                    title='',
                    content=f"最新版本呢!",
                    orient=Qt.Horizontal,
                    isClosable=True,  # disable close button
                    position=InfoBarPosition.TOP_LEFT,
                    duration=2000,
                    parent=self
                )
        except Exception as e:
            InfoBar.new(
                icon=FluentIcon.GITHUB,
                title='',
                content=f"网络请求失败,版本检测不到了o(╥﹏╥)o",
                orient=Qt.Horizontal,
                isClosable=True,  # disable close button
                position=InfoBarPosition.TOP_LEFT,
                duration=4000,
                parent=self
            ).setCustomBackgroundColor("#FF6A6A","#000000")

if __name__=="__main__":
    QApplication.setHighDpiScaleFactorRoundingPolicy(QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
    QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
    QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
    app = QApplication(sys.argv)
    window = mainwindow()
    window.show()
    app.exit(app.exec_())
mainGui.py
AprDeci/animal-auto
# 调用 PaddleOCR-json.exe 的 Python Api
# 项目主页：
# https://github.com/hiroi-sora/PaddleOCR-json

import os
import socket # 套接字
import subprocess  # 进程，管道
from json import loads as jsonLoads, dumps as jsonDumps
from sys import platform as sysPlatform  # popen静默模式
from base64 import b64encode # base64 编码


class PPOCR_pipe:
    """调用OCR（管道模式）"""

    def __init__(self, exePath: str, argument: dict = None):
        """初始化识别器（管道模式）。\n
        `exePath`: 识别器`PaddleOCR_json.exe`的路径。\n
        `argument`: 启动参数，字典`{"键":值}`。参数说明见 https://github.com/hiroi-sora/PaddleOCR-json
        """
        cwd = os.path.abspath(os.path.join(exePath, os.pardir))  # 获取exe父文件夹
        # 处理启动参数
        if not argument == None:
            for key, value in argument.items():
                if isinstance(value, str):  # 字符串类型的值加双引号
                    exePath += f' --{key}="{value}"'
                else:
                    exePath += f" --{key}={value}"
        # 设置子进程启用静默模式，不显示控制台窗口
        startupinfo = None
        if "win32" in str(sysPlatform).lower():
            startupinfo = subprocess.STARTUPINFO()
            startupinfo.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW
            startupinfo.wShowWindow = subprocess.SW_HIDE
        self.ret = subprocess.Popen(  # 打开管道
            exePath, cwd=cwd,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,  # 丢弃stderr的内容
            startupinfo=startupinfo  # 开启静默模式
        )
        # 启动子进程
        while True:
            if not self.ret.poll() == None:  # 子进程已退出，初始化失败
                raise Exception(f"OCR init fail.")
            initStr = self.ret.stdout.readline().decode("utf-8", errors="ignore")
            if "OCR init completed." in initStr:  # 初始化成功
                break

    def runDict(self, writeDict: dict):
        """传入指令字典，发送给引擎进程。\n
        `writeDict`: 指令字典。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        # 检查子进程
        if not self.ret.poll() == None:
            return {"code": 901, "data": f"子进程已崩溃。"}
        # 输入信息
        writeStr = jsonDumps(writeDict, ensure_ascii=True, indent=None)+"\n"
        try:
            self.ret.stdin.write(writeStr.encode("utf-8"))
            self.ret.stdin.flush()
        except Exception as e:
            return {"code": 902, "data": f"向识别器进程传入指令失败，疑似子进程已崩溃。{e}"}
        # 获取返回值
        try:
            getStr = self.ret.stdout.readline().decode("utf-8", errors="ignore")
        except Exception as e:
            return {"code": 903, "data": f"读取识别器进程输出值失败。异常信息：[{e}]"}
        try:
            return jsonLoads(getStr)
        except Exception as e:
            return {"code": 904, "data": f"识别器输出值反序列化JSON失败。异常信息：[{e}]。原始内容：[{getStr}]"}

    def run(self, imgPath: str):
        """对一张本地图片进行文字识别。\n
        `exePath`: 图片路径。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        writeDict = {"image_path": imgPath}
        return self.runDict(writeDict)

    def runClipboard(self):
        """立刻对剪贴板第一位的图片进行文字识别。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        return self.run("clipboard")

    def runBase64(self, imageBase64: str):
        """对一张编码为base64字符串的图片进行文字识别。\n
        `imageBase64`: 图片base64字符串。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        writeDict = {"image_base64": imageBase64}
        return self.runDict(writeDict)

    def runBytes(self, imageBytes):
        """对一张图片的字节流信息进行文字识别。\n
        `imageBytes`: 图片字节流。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        imageBase64 = b64encode(imageBytes).decode('utf-8')
        return self.runBase64(imageBase64)

    def exit(self):
        """关闭引擎子进程"""
        self.ret.kill()  # 关闭子进程

    @staticmethod
    def printResult(res: dict):
        """用于调试，格式化打印识别结果。\n
        `res`: OCR识别结果。"""

        # 识别成功
        if res["code"] == 100:
            index = 1
            for line in res["data"]:
                print(f"{index}-置信度：{round(line['score'], 2)}，文本：{line['text']}")
                index+=1
        elif res["code"] == 100:
            print("图片中未识别出文字。")
        else:
            print(f"图片识别失败。错误码：{res['code']}，错误信息：{res['data']}")

    def __del__(self):
        self.exit()

class PPOCR_socket(PPOCR_pipe):
    """调用OCR（套接字模式）"""

    def __init__(self, exePath: str, argument: dict = None):
        """初始化识别器（套接字模式）。\n
        `exePath`: 识别器`PaddleOCR_json.exe`的路径。\n
        `argument`: 启动参数，字典`{"键":值}`。参数说明见 https://github.com/hiroi-sora/PaddleOCR-json
        """
        # 处理参数
        if not argument:
            argument = {}
        argument["port"] = 0 # 随机端口号
        argument["addr"] = "loopback" # 本地环回地址
        super().__init__(exePath, argument) # 父类构造函数
        # 再获取一行输出，检查是否成功启动服务器
        initStr = self.ret.stdout.readline().decode("utf-8", errors="ignore")
        if not self.ret.poll() == None:  # 子进程已退出，初始化失败
            raise Exception(f"Socket init fail.")
        if "Socket init completed. " in initStr:  # 初始化成功
            splits = initStr.split(":")
            self.ip = splits[0].split("Socket init completed. ")[1]
            self.port = int(splits[1])   # 提取端口号
            self.ret.stdout.close() # 关闭管道重定向，防止缓冲区填满导致堵塞
            print(f"套接字服务器初始化成功。{self.ip}:{self.port}")
            return
        # 异常
        self.exit()
        raise Exception(f"Socket init fail.")

    def runDict(self, writeDict: dict):
        """传入指令字典，发送给引擎进程。\n
        `writeDict`: 指令字典。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        # 检查子进程
        if not self.ret.poll() == None:
            return {"code": 901, "data": f"子进程已崩溃。"}
        # 通信
        writeStr = jsonDumps(writeDict, ensure_ascii=True, indent=None)+"\n"
        try:
            # 创建TCP连接
            clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            clientSocket.connect((self.ip, self.port))
            # 发送数据
            clientSocket.sendall(writeStr.encode())
            # 接收数据
            resData = b''
            while True:
                chunk = clientSocket.recv(1024)
                if not chunk:
                    break
                resData += chunk
            getStr = resData.decode()
        except ConnectionRefusedError:
            return {"code":902, "data": "连接被拒绝"}
        except TimeoutError:
            return {"code":903, "data": "连接超时"}
        except Exception as e:
            return {"code":904, "data": f"网络错误：{e}"}
        finally:
            clientSocket.close()  # 关闭连接
        # 反序列输出信息
        try:
            return jsonLoads(getStr)
        except Exception as e:
            return {"code": 905, "data": f"识别器输出值反序列化JSON失败。异常信息：[{e}]。原始内容：[{getStr}]"}

def GetOcrApi(exePath: str, argument: dict = None, ipcMode: str = "pipe"):
    """获取识别器API对象。\n
    `exePath`: 识别器`PaddleOCR_json.exe`的路径。\n
    `argument`: 启动参数，字典`{"键":值}`。参数说明见 https://github.com/hiroi-sora/PaddleOCR-json\n
    `ipcMode`: 进程通信模式，可选值为套接字模式`socket` 或 管道模式`pipe`。用法上完全一致。
    """
    if ipcMode == "socket":
        return PPOCR_socket(exePath, argument)
    elif ipcMode == "pipe":
        return PPOCR_pipe(exePath, argument)
    else:
        raise Exception(f'ipcMode可选值为 套接字模式"socket" 或 管道模式"pipe" ，不允许{ipcMode}。')
ocr/PPOCR_api.py
AprDeci/animal-auto
# 将 PaddleOCR-json 结果可视化表现
# 项目主页：
# https://github.com/hiroi-sora/PaddleOCR-json
from PIL import Image, ImageDraw, ImageFont
import math


class visualize:
    '''可视化'''

    # ================================ 静态方法 ================================

    @staticmethod
    def createBox(textBlocks, size,
                  fill='#00500040', outline='#11ff22', width=6):
        '''创建包围盒图层，返回PIL Image对象。\n
        :textBlocks: 文本块列表。\n
        :size: 图片尺寸。\n
        以下为可选字段：（颜色为十六进制6位RGB或8位RGBA字符串，如 #112233ff）\n
        :fill: 包围盒填充颜色。\n
        :outline: 包围盒轮廓颜色。\n
        :width: 包围盒轮廓粗细，像素。
        '''
        img = Image.new('RGBA', size, 0)
        draw = ImageDraw.Draw(img)
        for tb in textBlocks:
            box = [tuple(tb['box'][0]), tuple(tb['box'][1]),
                   tuple(tb['box'][2]), tuple(tb['box'][3])]
            draw.polygon(box, fill=fill, outline=outline, width=width)
        return img

    @staticmethod
    def createText(textBlocks, size,
                   ttfPath='C:\Windows\Fonts\msyh.ttc', ttfScale=0.9,
                   fill='#ff0000'):
        '''创建文字图层，返回PIL Image对象。\n
        :textBlocks: 文本块列表。\n
        :size: 图片尺寸。\n
        以下为可选字段：\n
        :ttfPath: 字体文件路径。默认为微软雅黑，若不存在此字体会报错。\n
        :ttfScale: 字体大小整体缩放系数，应在1附近。\n
        :fill: 文字颜色，十六进制6位RGB或8位RGBA字符串，如 #112233ff。\n
        '''
        img = Image.new('RGBA', size, 0)
        draw = ImageDraw.Draw(img)
        ttfDict = {}  # 缓存不同大小的字体对象
        for tb in textBlocks:
            text = tb['text']
            xy = tuple(tb['box'][0])  # 左上角坐标
            xy1 = tb['box'][3]  # 左下角坐标# 行高
            hight = round(
                math.sqrt(((xy[0]-xy1[0])**2)+((xy[1]-xy1[1])**2))*ttfScale)
            if hight not in ttfDict:
                ttfDict[hight] = ImageFont.truetype(
                    ttfPath, hight)  # 创建新大小的字体
            draw.text(xy, text, font=ttfDict[hight], fill=fill)
        return img

    @staticmethod
    def createOrder(textBlocks, size,
                    ttfPath='C:\Windows\Fonts\msyh.ttc', ttfSize=50,
                    fill='#2233ff', bg='#ffffffe0'):
        '''创建序号图层，返回PIL Image对象。\n
        :textBlocks: 文本块列表。\n
        :size: 图片尺寸。\n
        以下为可选字段：\n
        :ttfPath: 字体文件路径。默认为微软雅黑，若不存在此字体会报错。\n
        :ttfSize: 字体大小。\n
        :fill: 文字颜色，十六进制6位RGB或8位RGBA字符串，如 #112233ff。\n
        '''
        img = Image.new('RGBA', size, 0)
        draw = ImageDraw.Draw(img)
        ttf = ImageFont.truetype(ttfPath, ttfSize)  # 字体
        for index, tb in enumerate(textBlocks):
            text = f'{index+1}'
            xy = tuple(tb['box'][0])  # 左上角坐标
            x_, y_, w, h = ttf.getbbox(text)  # 获取宽高。只需要w和h
            w *= 1.1
            h *= 1.1
            draw.rectangle((xy, (xy[0]+w, xy[1]+h)), fill=bg, width=0)  # 背景矩形
            draw.text(xy, text, font=ttf, fill=fill)  # 文字
        return img

    @staticmethod
    def createContrast(img1, img2):
        '''左右拼合两个图片，创建对比图层，返回PIL Image对象。'''
        size = (img1.size[0]+img2.size[0], max(img1.size[1], img2.size[1]))
        img = Image.new('RGBA', size, 0)
        img.paste(img1, (0, 0))
        img.paste(img2, (img1.size[0], 0))
        return img

    @staticmethod
    def composite(img1, img2):
        '''传入两个PIL Image对象（RGBA格式），以img1为底，将img2叠加在其上
        返回生成的图片'''
        return Image.alpha_composite(img1, img2)

    # ================================ 快捷接口 ================================

    def __init__(self, textBlocks, imagePath):
        """创建可视化对象。\n
        :textBlocks: 文本块列表，即OCR返回的data部分\n
        :imagePath: 对应的图片路径。
        """
        self.imgSource = Image.open(imagePath).convert('RGBA')  # 原始图片图层
        self.size = self.imgSource.size
        self.imgBox = self.createBox(textBlocks, self.size)  # 包围盒图层
        self.imgText = self.createText(textBlocks, self.size)  # 文字图层
        self.imgOrder = self.createOrder(textBlocks, self.size)  # 序号图层

    def get(self, isBox=True, isText=False, isOrder=False, isSource=True):
        '''返回合成可视化结果的PIL Image图像。\n
        :isBox: T时返回包围盒图层。\n
        :isText: T时返回文字图层。\n
        :isOrder: T时返回序号图层。\n
        :isSource: T时返回原图。F时返回透明背景的纯可视化结果。\n
        '''
        img = Image.new('RGBA', self.size, 0)
        flags = (isSource, isBox, isText, isOrder)
        for index, im in enumerate([self.imgSource, self.imgBox, self.imgText, self.imgOrder]):
            if im and flags[index]:
                img = visualize.composite(img, im)
        return img

    def show(self, isBox=True, isText=False, isOrder=False, isSource=True):
        '''显示可视化结果图像。\n
        :isBox: T时返回包围盒图层。\n
        :isText: T时返回文字图层。\n
        :isOrder: T时返回序号图层。\n
        :isSource: T时返回原图。F时返回透明背景的纯可视化结果。\n
        '''
        img = self.get(isBox, isText, isOrder, isSource)
        img.show()

    def save(self, path='', isBox=True, isText=False, isOrder=False, isSource=True):
        '''保存可视化结果图像。\n
        :path: 保存路径。\n
        :isBox: T时返回包围盒图层。\n
        :isText: T时返回文字图层。\n
        :isOrder: T时返回序号图层。\n
        :isSource: T时返回原图。F时返回透明背景的纯可视化结果。\n
        '''
        img = self.get(isBox, isText, isOrder, isSource)
        img.save(path)
ocr/PPOCR_visualize.py
AprDeci/animal-auto
# tbpu : text block processing unit
# 文本块处理单元

# OCR返回的结果中，一项包含文字、包围盒、置信度的元素，称为一个“文块” - text block 。
# 文块不一定是完整的一句话或一个段落。反之，一般是零散的文字。
# 一个OCR结果常由多个文块组成。
# 文块处理器就是：将传入的多个文块进行处理，比如合并、排序、删除文块。


# 概念解析：
# 栏：一页纸可能有单栏、双栏、多栏，不同栏之间的文本块不会接壤。不同栏的文块是绝对不能合并的。
# 段：一栏可能有多个段落，可能由行间距、起始空格等来区分。怎么划分段落、怎么合并，不同Tbpu有不同方案。
# 行：一段内可能有多个行。应该尽量合并。
# 块：文本块是OCR结果中的最小单位，一行可能被意外划分为多个块，这是不正常的，必须要合并。


from .merge_line_h import TbpuLineH
from .merge_line_h_m_left import TbpuLineHMultiLeft
from .merge_line_h_m_paragraph import TbpuLineHMultiParagraph
from .merge_line_h_m_fuzzy import TbpuLineHMultiFuzzy
from .merge_line_v_lr import TbpuLineVlr
from .merge_line_v_rl import TbpuLineVrl

# 执行后处理可能破坏原列表textBlocks！


def run_merge_line_h(textBlocks):
    '''横排-单行'''
    tbpu = TbpuLineH()
    tb, debugMsg = tbpu.run(textBlocks, None)
    return tb


def run_merge_line_h_m_left(textBlocks):
    '''横排-合并多行-左对齐'''
    tbpu = TbpuLineHMultiLeft()
    tb, debugMsg = tbpu.run(textBlocks, None)
    return tb


def run_merge_line_h_m_paragraph(textBlocks):
    '''横排-合并多行-自然段'''
    tbpu = TbpuLineHMultiParagraph()
    tb, debugMsg = tbpu.run(textBlocks, None)
    return tb


def run_merge_line_h_m_fuzzy(textBlocks):
    '''横排-合并多行-模糊匹配'''
    tbpu = TbpuLineHMultiFuzzy()
    tb, debugMsg = tbpu.run(textBlocks, None)
    return tb


def run_merge_line_v_lr(textBlocks):
    '''竖排-从左到右-单行'''
    tbpu = TbpuLineVlr()
    tb, debugMsg = tbpu.run(textBlocks, None)
    return tb


def run_merge_line_v_rl(textBlocks):
    '''竖排-从左到右-单行'''
    tbpu = TbpuLineVrl()
    tb, debugMsg = tbpu.run(textBlocks, None)
    return tb

ocr/tbpu/__init__.py
AprDeci/animal-auto
# 文块处理：横排-单行
from .tbpu import Tbpu

from time import time


class TbpuLineH(Tbpu):
    def __init__(self):
        self.tbpuName = '横排-单行'

    def getInitInfo(self):
        return f'文块后处理：[{self.tbpuName}]'

    def run(self, textBlocks, imgInfo):
        '''传入 文块组、图片信息。返回文块组、debug信息字符串。'''
        timeIn = time()
        # 所有文块，按左上角点的x坐标排序
        textBlocks.sort(key=lambda tb: tb['box'][0][0])
        # 遍历每个文块，寻找后续文块中与它接壤、且行高一致的项，合并两个文块
        resList = []
        listlen = len(textBlocks)
        for index in range(listlen):
            tb = textBlocks[index]
            if not tb:
                continue
            box = tb['box']
            bx, by = box[1][0], box[1][1]  # 右上角xy
            bh = box[3][1] - box[0][1]  # 行高
            limitX, limitY = bh, round(bh/2)  # x、y 合并阈值，行高、行高一半
            num = 1  # 合并个数
            # 遍历后续文块
            for i in range(index+1, listlen):
                tb2 = textBlocks[i]
                if not tb2:
                    continue
                box2 = tb2['box']
                b2x, b2y = box2[0][0], box2[0][1]  # 左上角xy
                b2h = box2[3][1] - box2[0][1]  # 行高
                # 文块1的右上角与文块2的左上角接壤，且二者行高一致，则合并
                if abs(b2x-bx) < limitX and abs(b2y-by) < limitY and abs(b2h-bh) < limitY:
                    num += 1
                    # 合并两个文块box
                    yTop = min(box[0][1], box[1][1], box2[0][1], box2[1][1])
                    yBottom = max(box[2][1], box[3][1], box2[2][1], box2[3][1])
                    box[0][1] = box[1][1] = yTop  # y上
                    box[2][1] = box[3][1] = yBottom  # y下
                    box[0][0] = box[3][0] = min(box[0][0], box[3][0])  # x左
                    box[1][0] = box[2][0] = max(box2[1][0], box2[2][0])  # x右
                    # 刷新临时变量
                    bx, by = box[1][0], box[1][1]  # 右上角xy
                    bh = box[3][1] - box[0][1]  # 行高
                    limitX, limitY = bh, round(bh/2)  # x、y 合并阈值，行高、行高一半
                    # 合并内容
                    tb['score'] += tb2['score']  # 合并置信度
                    tb['text'] += tb2['text']  # 合并文本
                    textBlocks[i] = None  # 置为空，标记删除
            if num > 1:
                tb['score'] /= num  # 平均置信度
            resList.append(tb)  # 装填入结果
        # 所有新文块，按左上角点的y坐标从高到低排序
        resList.sort(key=lambda tb: tb['box'][0][1])
        # 返回新文块组和debug字符串
        return resList, f'[{self.tbpuName}] 原{listlen}块，合并后{len(resList)}块，耗时{time()-timeIn}s'

ocr/tbpu/merge_line_h.py
AprDeci/animal-auto
# 文块处理：横排-合并多行-模糊匹配
from .merge_line_h_m_left import TbpuLineHMultiLeft


class TbpuLineHMultiFuzzy(TbpuLineHMultiLeft):
    def __init__(self):
        super().__init__()
        self.tbpuName = '横排-合并多行-模糊匹配'

    def isRuleMerge(self, box1, box2):
        '''合并规则：两个box可以合并时返回T'''
        # 1与2在x上互相包含，y上接壤时OK
        return self.isBoxInX(box1, box2) and abs(box2[0][1]-box1[3][1]) <= self.limitY

    def isRuleNew(self, box1, box2):
        '''新段规则：两个box属于同一栏但不同段落时返回T'''
        # 无需判断
        return False

ocr/tbpu/merge_line_h_m_fuzzy.py
AprDeci/animal-auto
# 文块处理：横排-合并多行-左对齐
from .merge_line_h import TbpuLineH

from time import time


class TbpuLineHMultiLeft(TbpuLineH):
    def __init__(self):
        self.tbpuName = '横排-合并多行-左对齐'
        # x、y方向上合并的允许阈值，为该行的行高乘上比例因子
        self.factorX = 1
        self.factorY = 2
        # x、y方向上合并的允许阈值，像素。由行高与比例因子动态刷新。
        self.limitX = 10
        self.limitY = 10

        self.mergeNum = 1  # 当前合并段数
        self.rowHeight = 10  # 当前行高

    def getInitInfo(self):
        return f'文块后处理：[{self.tbpuName}]'

    @staticmethod
    def isBoxInX(box1, box2):
        '''两个box在x方向上互相包含时返回T'''
        return (box1[0][0] <= box2[0][0] <= box1[1][0])\
            or (box1[0][0] <= box2[1][0] <= box1[1][0])\
            or (box2[0][0] <= box1[0][0] <= box2[1][0])\
            or (box2[0][0] <= box1[1][0] <= box2[1][0])

    def isRuleMerge(self, box1, box2):
        '''合并规则：两个box可以合并时返回T'''
        # 1的左下角与2的左上角接壤时OK
        return abs(box2[0][0]-box1[3][0]) <= self.limitX\
            and abs(box2[0][1]-box1[3][1]) <= self.limitY

    def isRuleNew(self, box1, box2):
        '''新段规则：两个box属于同一栏但不同段落时返回T'''
        # 1与2互相包含时OK
        return self.isBoxInX(box1, box2)

    def run(self, textBlocks, imgInfo):
        '''传入 文块组、图片信息。返回文块组、debug信息字符串。'''
        timeIn = time()
        hList, s = super().run(textBlocks, imgInfo)  # 获取单行合并
        listlen = len(hList)
        resList = []
        # 遍历每个文块，寻找后续文块中x坐标相近、且行高一致的项，合并两个文块
        for index in range(listlen):
            tb = hList[index]
            if not tb:
                continue
            box = tb['box']
            self.rowHeight = box[3][1] - box[0][1]  # 行高
            # 该行的 x、y 合并阈值
            self.limitX = self.factorX * self.rowHeight
            self.limitY = self.factorY * self.rowHeight
            self.mergeNum = 1  # 合并个数
            # 遍历后续文块
            for i in range(index+1, listlen):
                tb2 = hList[i]
                if not tb2:
                    continue
                box2 = tb2['box']
                b2h = box2[3][1] - box2[0][1]  # 行高
                # 当行高一致，即有可能在同一栏内
                if abs(b2h-self.rowHeight) <= self.limitY:
                    # 符合合并规则，则合并
                    if self.isRuleMerge(box, box2):
                        self.mergeNum += 1
                        # 合并两个文块box
                        box[0][0] = box[3][0] = min(  # x左
                            box[0][0], box[3][0], box2[0][0], box2[3][0])
                        box[1][0] = box[2][0] = max(  # x右
                            box[1][0], box[2][0], box2[1][0], box2[2][0])
                        box[0][1] = box[1][1] = min(  # y上
                            box[0][1], box[1][1])
                        box[2][1] = box[3][1] = max(  # y下
                            box2[2][1], box2[3][1])
                        # 合并内容
                        tb['score'] += tb2['score']  # 合并置信度
                        tb['text'] += tb2['text']  # 合并文本
                        hList[i] = None  # 置为空，标记删除
                    # 否则，若符合新段规则，则跳出对1的继续匹配
                    elif self.isRuleNew(box, box2):
                        break
            if self.mergeNum > 1:
                tb['score'] /= self.mergeNum  # 平均置信度
            resList.append(tb)  # 装填入结果
        # 返回新文块组和debug字符串
        return resList, f'[{self.tbpuName}] 原{len(textBlocks)}块，合并后{len(resList)}块，耗时{time()-timeIn}s'

ocr/tbpu/merge_line_h_m_left.py
AprDeci/animal-auto
# 文块处理：横排-合并多行-自然段
from .merge_line_h_m_left import TbpuLineHMultiLeft


class TbpuLineHMultiParagraph(TbpuLineHMultiLeft):
    def __init__(self):
        super().__init__()
        self.tbpuName = '横排-合并多行-自然段'

    def isRuleMerge(self, box1, box2):
        '''合并规则：两个box可以合并时返回T'''
        # y不接壤，说啥也没用
        if abs(box2[0][1]-box1[3][1]) > self.limitY:
            return False
        # 若当前是第一行，则额外允许第二行的x位置前移最多2个全角字符（即行高）的距离
        if self.mergeNum == 1:
            # 第二行x额外允许的范围
            xMax = box1[3][0] + self.limitX  # 上界：第一行x
            xMin = box1[3][0] - self.rowHeight*2 - self.limitX  # 下界：第一行x提前2个行高
            return xMin <= box2[0][0] <= xMax
        # 1的左下角与2的左上角接壤时OK
        return abs(box2[0][0]-box1[3][0]) <= self.limitX

ocr/tbpu/merge_line_h_m_paragraph.py
AprDeci/animal-auto
# 文块后处理：[竖排-从左到右-单行]
from .tbpu import Tbpu

from time import time


class TbpuLineVlr(Tbpu):
    def __init__(self):
        self.tbpuName = '竖排-从左到右-单行'
        self.rl = False  # T为从右到左，F为从左到右

    def getInitInfo(self):
        return f'文块后处理：[{self.tbpuName}]'

    def run(self, textBlocks, imgInfo):
        '''传入 文块组、图片信息。返回文块组、debug信息字符串。'''
        timeIn = time()
        # 所有文块，按左上角点的y坐标排序
        textBlocks.sort(key=lambda tb: tb['box'][0][1])
        # 遍历每个文块，寻找后续文块中与它接壤、且行宽一致的项，合并两个文块
        resList = []
        listlen = len(textBlocks)
        for index in range(listlen):
            tb = textBlocks[index]
            if not tb:
                continue
            box = tb['box']
            bx, by = box[2][0], box[2][1]  # 右下角xy
            bw = box[1][0] - box[0][0]  # 行宽
            limitX, limitY = round(bw/2), bw  # x、y 合并阈值，行宽一半、行宽
            num = 1  # 合并个数
            # 遍历后续文块
            for i in range(index+1, listlen):
                tb2 = textBlocks[i]
                if not tb2:
                    continue
                box2 = tb2['box']
                b2x, b2y = box2[1][0], box2[1][1]  # 右上角xy
                b2w = box2[1][0] - box2[0][0]  # 行宽
                # 文块1的右下角与文块2的右上角接壤，且二者行宽一致，则合并
                if abs(b2x-bx) < limitX and abs(b2y-by) < limitY and abs(b2w-bw) < limitY:
                    num += 1
                    # 合并两个文块box
                    xMin = min(box[0][0], box[3][0], box2[0][0], box2[3][0])
                    xMax = max(box[1][0], box[2][0], box2[1][0], box2[2][0])
                    box[0][0] = box[3][0] = xMin  # x左
                    box[1][0] = box[2][0] = xMax  # x右
                    box[0][1] = box[1][1] = min(box[0][1], box[1][1])  # y上
                    box[2][1] = box[3][1] = max(box2[2][1], box2[3][1])  # y下
                    # 刷新临时变量
                    bx, by = box[2][0], box[2][1]  # 右下角xy
                    bw = box[1][0] - box[0][0]  # 行宽
                    limitX, limitY = round(bw/2), bw  # x、y 合并阈值，行宽一半、行宽
                    # 合并内容
                    tb['score'] += tb2['score']  # 合并置信度
                    tb['text'] += tb2['text']  # 合并文本
                    textBlocks[i] = None  # 置为空，标记删除
            if num > 1:
                tb['score'] /= num  # 平均置信度
            resList.append(tb)  # 装填入结果
        # 所有新文块，按左上角点的x坐标从左到右排序
        resList.sort(key=lambda tb: tb['box'][0][0], reverse=self.rl)
        # 返回新文块组和debug字符串
        return resList, f'[{self.tbpuName}] 原{listlen}块，合并后{len(resList)}块，耗时{time()-timeIn}s'

ocr/tbpu/merge_line_v_lr.py
AprDeci/animal-auto
# 文块处理：合并竖排单行-从右至左
from .merge_line_v_lr import TbpuLineVlr

from time import time


class TbpuLineVrl(TbpuLineVlr):
    def __init__(self):
        self.tbpuName = '竖排-从右到左-单行'
        self.rl = True  # T为从右到左，F为从左到右

    def getInitInfo(self):
        return f'文块后处理：[{self.tbpuName}]'

    def run(self, textBlocks, imgInfo):
        '''传入 文块组、图片信息。返回文块组、debug信息字符串。'''
        timeIn = time()
        resList, s = super().run(textBlocks, imgInfo)
        # 返回新文块组和debug字符串
        return resList, f'[{self.tbpuName}] 原{len(textBlocks)}块，合并后{len(resList)}块，耗时{time()-timeIn}s'

ocr/tbpu/merge_line_v_rl.py
AprDeci/animal-auto
# tbpu : text block processing unit
# 文块处理器的基类。
# OCR返回的结果中，一项包含文字、包围盒、置信度的元素，称为一个“文块” - text block 。
# 文块不一定是完整的一句话或一个段落。反之，一般是零散的文字。
# 一个OCR结果常由多个文块组成。
# 文块处理器就是：将传入的多个文块进行处理，比如合并、排序、删除文块。

class Tbpu:
    def __init__(self):
        self.tbpuName = '文块处理单元-未知'

    def getInitInfo(self):
        '''返回初始化信息字符串'''
        return f'文块后处理：[{self.tbpuName}]'

    def run(self, textBlocks, imgInfo=None):
        '''输入：textBlocks文块 , img图片信息\n
        输出：textBlocks文块 , 处理日志'''
        return textBlocks, f'[{self.tbpuName}]'

ocr/tbpu/tbpu.py
AprDeci/animal-auto
from ocr.PPOCR_api import GetOcrApi
import os.path as path
ocr = GetOcrApi('./PaddleOCR-json_v.1.3.0/PaddleOCR-json.exe')
def getexp(img_path):
    img = path.abspath(img_path)
    try:
        result_pending = ocr.run(img)
    except:
        return  'error'
    result = result_pending['data']
    if type(result) == str:
        print('error')
        return 'error'
    else:
        exp = result[-1]['text'].replace('.', '').replace('+', '').replace(',', '')
        print(f'paddleOcr:{exp}')
        return exp

if __name__ == '__main__':
    getexp('./imgs/exp.png')
paddleOcr.py
AprDeci/animal-auto
import os.path

from PyQt5.QtWidgets import QWidget
import configparser
from Ui.setup import Ui_setup


class setupWin(QWidget, Ui_setup):
    def __init__(self):
        super().__init__()
        self.conf = configparser.ConfigParser()
        self.conf.read(os.path.abspath('./imgs/config.ini'), encoding='utf-8')
        self.setupUi(self)
        self.LocalOcr.setChecked(True)
        self.OK.clicked.connect(self.setConfig)
        self.freeocrApi.setText(self.conf.get('ocr', 'freeocrapi'))


    def setConfig(self):

        if not self.LocalOcr.isChecked():
            self.conf.set("ocr", "localOcr", "False")
        else:
            self.conf.set("ocr", "localOcr", "True")
        if not self.FreeOcr.isChecked():
            self.conf.set("ocr", "FreeOcr", "False")
        else:
            self.conf.set("ocr", "FreeOcr", "True")
        if self.freeocrApi.text():
            self.conf.set("ocr", "freeocrapi", self.freeocrApi.text())
        else:
            self.conf.set("ocr", "freeocrapi", "")
        with open("./imgs/config.ini", "r+", encoding='utf-8') as configfile:
            self.conf.write(configfile)

setup_inter.py
AprDeci/animal-auto