自动连连看

让你感受连连看不一样的爽玩法

程序说明: 本自动游戏程序仅用作学习使用,请维护游戏的公平性,禁止作弊使用

依赖库

  • numpy
  • pyautogui
  • pillow(py3)
  • pywin32

脚本

文件名说明
核心文件:
game_auto.py自动游戏运行脚本
graph_path.py路径识别算法
image_contrast.py图像对比
image_get.py图像获取分割等
辅助脚本:
pyautogui_test.py模块测试 + 坐标获取 + RGB采集
picture_split.py图像分割

算法

感知哈希算法

  1. 将图像大小归一化(例如8*8)

    去除大小,纵横比差异

  2. 灰度化处理

    • 原理

    • 实际应用 参数50用于四舍五入,防止浮点数计算精度问题

    • 实际应用2 移位方式避免除法导致的性能低问题

    • 不常用 精度比较低,图像转化为灰度效果不是太好

    • PhotoShop 运行速度偏慢,但是效果好。(^.^) 谁让人家是大佬呢!

  3. 计算平均灰度值avg (为后续平均Hash做准备)

  4. 信息指纹 (计算方式有多种)

    • 遍历8*8=64个像素点,分别和平均灰度值avg进行对比
    • 平均哈希 大于等于avg 则为1,否则记为0
    • 并按照一定顺序排列称64bit指纹编码
  5. 比较两幅图的指纹编码,计算其相似度

    汉明距离 Hamming distance 《信息论》

    • 两个等长字符串之间的汉明距离为两个字符串之间对应位置不同字符的个数
    • 若汉明距离<=5,则认为相似
    • 若汉明距离>10,则认为不同

直方图方法

思想 基于简单的向量相似度来对图像相似度进行度量

  1. 分别计算两幅图的直方图
  2. 归一化
  3. 按照某种距离度量标准进行相似度测量
  • 优点 方便归一化,计算量适中。较适合描述难以自动分割的图像
  • 缺点 仅反映的是灰度的概率分布,没有图像的空间位置。容易误判。

    从信息论角度考虑,直方图转换,信息量丢失较大。因此单一通过直方图匹配不够精确

问题及解决方案

  • Qllk 相同物体不同颜色的区分

    采用灰度直方图,分别计算每个通道的相似度,随后3通道求平均值。

源码

自动游戏运行脚本

"""
    # 游戏参数相关说明:
    MATRIX_EXTEND  # 矩阵扩展 1.开启 0.关闭

    该自动连连看脚本
    默认是针对不随着进度变化的界面操作的(省时间)
    若需要针对动态游戏,则将@1处相关代码去注释即可
"""
from image_get import *
from graph_path import connect_find
import pyautogui
import time


"""
# 1. 练练看v4.1 需要开启
# 2. QQ游戏 - 连连看角色版 不需要开启
"""
MATRIX_EXTEND = 0   # 矩阵扩展 1.开启 0.关闭


def auto_game_init(windows_title):
    # 获取窗口左上角坐标
    win_start = get_windows(windows_title)
    # 获取游戏区坐标
    game_area = get_game_area(*win_start)
    return game_area


def get_game_matrix(game_area):
    # 获取游戏区域图像
    game_area_image = grab_game_area(*game_area)
    # 将一张图像切割为多个项目图
    images = game_area_image_to_item_images(game_area_image)
    # 将多个项目图像转换为矩阵
    matrix = images_to_matrix(images)
    return matrix


def execute_one_step(one_step, game_area,
                     item_width, item_height):
    from_row, from_col, to_row, to_col = one_step
    game_area_left, game_area_top, right, bottom = game_area

    from_x = game_area_left + (from_col + 0.5) * item_width
    from_y = game_area_top + (from_row + 0.5) * item_height

    to_x = game_area_left + (to_col + 0.5) * item_width
    to_y = game_area_top + (to_row + 0.5) * item_height

    pyautogui.moveTo(from_x, from_y)
    pyautogui.click()

    pyautogui.moveTo(to_x, to_y)
    pyautogui.click()


if __name__ == '__main__':
    game_area = auto_game_init('QQ游戏 - 连连看角色版')
    while True:
        matrix = get_game_matrix(game_area)

        # 扩展外围
        if MATRIX_EXTEND == 1:
            col_max = item_count[0]
            row_max = item_count[1]
            matrix_ex = numpy.zeros((row_max + 2, col_max + 2), int)
            matrix_ex[1:(row_max + 1), 1:(col_max + 1)] = matrix
        else:
            matrix_ex = matrix

        while True:
            # 寻找可以连通的图标
            # 返回起点坐标和目标坐标

            print('------------------------------------------')
            print(matrix_ex, end='\n\n')
            one_step_ex = connect_find(matrix_ex)
            if not one_step_ex:
                # error_exit('----- 当前已没有可选择的路径 -----')
                print('----- 当前已没有可选择的路径 -----')
                time.sleep(0.25)
                break

            # 执行实际操作
            if MATRIX_EXTEND == 1:
                one_step = tuple(map(lambda x: x - 1, one_step_ex))
            else:
                one_step = one_step_ex

            # 执行该步骤
            execute_one_step(
                one_step, game_area, *item_size)

            # 执行成功后,更新matrix
            from_row, from_col, to_row, to_col = one_step_ex
            matrix_ex[from_row][from_col] = 0
            matrix_ex[to_row][to_col] = 0

            print('({},{}) -> ({},{})'.format(from_row, from_col,
                                              to_row, to_col))
            if numpy.all(matrix_ex == 0):
                error_exit('----- 通关完成 -----')
                exit(0)

            # @1 执行成功一步之后,延时一会儿
            # 重新截图,方便处理动态变化的图像
            # time.sleep(0.025)
            # break

路径识别算法

import numpy

def connect_zero_corner(matrix, pos_src, pos_des):
    # 起点行,起点列,目标行,目标列
    s_r, s_c = pos_src
    d_r, d_c = pos_des
    # print(s_r, s_c)
    # print(d_r, d_c)

    # 横向判断
    if s_r == d_r:
        col_min = min(s_c, d_c)
        col_max = max(s_c, d_c)

        if numpy.all(matrix[s_r, col_min+1:col_max] == 0):
            return True
    # 纵向判断
    if s_c == d_c:
        row_min = min(s_r, d_r)
        row_max = max(s_r, d_r)

        if numpy.all(matrix[row_min+1:row_max, s_c] == 0):
            return True

    return False


def connect_one_corner(matrix, pos_src, pos_des):
    # 起点行,起点列,目标行,目标列
    s_r, s_c = pos_src
    d_r, d_c = pos_des
    # print(s_r, s_c)
    # print(d_r, d_c)

    if s_r == d_r or s_c == d_c:
        return False

    if matrix[s_r][d_c] == 0:
        col_min = min(s_c, d_c)
        col_max = max(s_c, d_c)
        row_min = min(s_r, d_r)
        row_max = max(s_r, d_r)

        if numpy.all(matrix[s_r, col_min+1:col_max] == 0) and\
                numpy.all(matrix[row_min+1:row_max, d_c] == 0):
            return True
    if matrix[d_r][s_c] == 0:
        col_min = min(s_c, d_c)
        col_max = max(s_c, d_c)
        row_min = min(s_r, d_r)
        row_max = max(s_r, d_r)

        if numpy.all(matrix[d_r, col_min+1:col_max] == 0) and\
                numpy.all(matrix[row_min+1:row_max, s_c] == 0):
            return True

    return False


def connect_two_corner(matrix, pos_src, pos_des):
    """
        将两个拐角的问题,切换为源坐标横纵方向上所有
        空点的任意元素 与 目标点之间的一拐角问题
    """
    row_max = len(matrix)
    col_max = len(matrix[0])

    s_r, s_c = pos_src
    d_r, d_c = pos_des

    # 向上
    if s_r != 0:
        for r in range(s_r - 1, -1, -1):
            if matrix[r][s_c] == 0:
                pos = connect_one_corner(matrix, (r, s_c), pos_des)
                if pos is not False:
                    return True
            else:
                break
    # 向下
    if s_r != row_max:
        for r in range(s_r + 1, row_max, 1):
            if matrix[r][s_c] == 0:
                pos = connect_one_corner(matrix, (r, s_c), pos_des)
                if pos is not False:
                    return True
            else:
                break
    # 向左
    if s_c != 0:
        for c in range(s_c - 1, -1, -1):
            if matrix[s_r][c] == 0:
                pos = connect_one_corner(matrix, (s_r, c), pos_des)
                if pos is not False:
                    return True
            else:
                break
    # 向右
    if s_c != col_max:
        for c in range(s_c + 1, col_max, 1):
            if matrix[s_r][c] == 0:
                pos = connect_one_corner(matrix, (s_r, c), pos_des)
                if pos is not False:
                    return True
            else:
                break

    return False


def connect_find(matrix):
    row_max = len(matrix)
    col_max = len(matrix[0])

    max_id = numpy.max(matrix)
    for id in range(1, max_id + 1):
        # 获取相当元素位置信息
        pos = numpy.where(matrix == id)
        position = map(lambda x, y: (x, y), pos[0], pos[1])
        position = list(position)

        # 循环判断两坐标点是否连通
        pos_count = len(position)
        for i in range(pos_count):
            for j in range(i + 1, pos_count):
                pos_src = position[i]
                pos_des = position[j]
                # print('{} -> {}'.format(pos_src, pos_des))

                # 直线连通的判断
                ret = connect_zero_corner(matrix, pos_src, pos_des)
                if ret:
                    return pos_src[0], pos_src[1], pos_des[0], pos_des[1]

                # 有一个拐角连通的判断
                ret = connect_one_corner(matrix, pos_src, pos_des)
                if ret:
                    return pos_src[0], pos_src[1], pos_des[0], pos_des[1]

                # 有两个拐角连通的判断
                ret = connect_two_corner(matrix, pos_src, pos_des)
                if ret:
                    return pos_src[0], pos_src[1], pos_des[0], pos_des[1]

    return False


if __name__ == '__main__':
    col_max = 12
    row_max = 7

    graph = numpy.array([[1, 2, 3, 2, 4, 5, 6, 7, 8, 7, 1, 9],
                         [10, 8, 11, 12, 13, 6, 4, 4, 14, 5, 14, 15],
                         [3, 16, 16, 3, 13, 17, 17, 18, 9, 5, 19, 17],
                         [15, 9, 12, 3, 7, 6, 1, 7, 12, 16, 8, 13],
                         [20, 11, 14, 11, 12, 21, 15, 20, 13, 10, 19, 16],
                         [9, 20, 10, 18, 21, 18, 10, 5, 11, 1, 15, 4],
                         [21, 2, 2, 19, 6, 19, 18, 17, 14, 8, 20, 21]])

    graph_ex = numpy.zeros((row_max + 2, col_max + 2), int)
    graph_ex[1:(row_max + 1), 1:(col_max + 1)] = graph
    print(graph_ex)
    ret = connect_find(graph_ex)
    print(ret)

图像对比

"""
    image contrast
    执行该脚本前,请先执行picture_splist.py,以便于生成供该脚本使用的源文件
"""
import cv2
import numpy as np
import matplotlib.pyplot as plt


def normalization(image, size=(64, 64)):
    """
        归一化
    """
    return cv2.resize(image, size)


def grayscale(image):
    """
        灰度化处理
    """
    return cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)


def get_hash(image):
    """
        获取信息指纹
    """
    # print(image.shape)
    average = np.mean(image)
    hash = []
    for i in range(image.shape[0]):
        for j in range(image.shape[1]):
            if image[i][j] > average:
                hash.append(1)
            else:
                hash.append(0)
    return hash


def hammming_distance(hash1, hash2):
    length = len(hash1)
    if length != len(hash2):
        raise Exception("汉明距离:信息指纹长度不相等")

    count = 0
    for i in range(length):
        if hash1[i] != hash2[i]:
            count += 1
    return count


def classify_aHash(image1, image2):
    """
        平均哈希
        计算平均值,误差很大
    """
    image1 = normalization(image1, size=(32, 32))
    image2 = normalization(image2, size=(32, 32))
    gray1 = grayscale(image1)
    gray2 = grayscale(image2)
    # Test
    # cv2.imshow('gray_1', gray1)
    # cv2.imshow('gray_2', gray2)

    hash1 = get_hash(gray1)
    hash2 = get_hash(gray2)
    return hammming_distance(hash1, hash2)


def classify_pHash(image1, image2):
    """
        感知哈希
        精确,但计算速度过慢
    """
    image1 = normalization(image1, size=(32, 32))
    image2 = normalization(image2, size=(32, 32))
    gray1 = grayscale(image1)
    gray2 = grayscale(image2)

    # 将灰度图转为浮点型,再进行dct变换
    dct1 = cv2.dct(np.float32(gray1))
    dct2 = cv2.dct(np.float32(gray2))
    # 缩小DCT,保留左上角8*8的像素
    dct1_roi = dct1[0:8, 0:8]
    dct2_roi = dct2[0:8, 0:8]

    hash1 = get_hash(dct1_roi)
    hash2 = get_hash(dct2_roi)
    return hammming_distance(hash1, hash2)


def classify_gray_hist(image1, image2, size=(256, 256)):
    """
        灰度直方图
        1. 灰度计算直方图,使用第一个通道
    """
    image1 = cv2.resize(image1, size)
    image2 = cv2.resize(image2, size)
    hist1 = cv2.calcHist([image1], [0], None, [256], [0.0, 255.0])
    hist2 = cv2.calcHist([image2], [0], None, [256], [0.0, 255.0])
    # 可以比较下直方图
    plt.plot(range(256), hist1, 'r')
    plt.plot(range(256), hist2, 'b')
    plt.show()
    # 计算直方图的重合度
    degree = 0
    for i in range(len(hist1)):
        if hist1[i] != hist2[i]:
            degree = degree + \
                (1 - abs(hist1[i]-hist2[i])/max(hist1[i], hist2[i]))
        else:
            degree = degree + 1
    degree = degree/len(hist1)
    return degree


def calculate(image1, image2):
    """
        计算单通道的直方图的相似值
    """
    hist1 = cv2.calcHist([image1], [0], None, [256], [0.0, 255.0])
    hist2 = cv2.calcHist([image2], [0], None, [256], [0.0, 255.0])
    # 计算直方图的重合度
    degree = 0
    for i in range(len(hist1)):
        if hist1[i] != hist2[i]:
            degree = degree + \
                (1 - abs(hist1[i]-hist2[i])/max(hist1[i], hist2[i]))
        else:
            degree = degree + 1
    degree = degree/len(hist1)
    return degree


def classify_hist_with_split(image1, image2, size=(256, 256)):
    """
        通过得到每个通道的直方图来计算相似度
        将图像resize后,分离为三个通道,再计算每个通道的相似值
    """
    image1 = cv2.resize(image1, size)
    image2 = cv2.resize(image2, size)
    sub_image1 = cv2.split(image1)
    sub_image2 = cv2.split(image2)

    sub_data = 0
    for im1, im2 in zip(sub_image1, sub_image2):
        sub_data += calculate(im1, im2)
    sub_data = sub_data/3
    return sub_data


def image_contrast(image1, image2, hashmode='H'):
    if hashmode == 'A':
        hamm_dis = classify_aHash(image1, image2)
        # print('classify_aHash: ', hamm_dis)
        if hamm_dis > 10:
            return False
        return True
    elif hashmode == 'P':
        hamm_dis = classify_pHash(image1, image2)
        # print('classify_pHash: ', hamm_dis)
        if hamm_dis > 3:
            return False
        return True
    elif hashmode == 'H':
        degree = classify_hist_with_split(image1, image2, (32, 32))
        # print('hist_degree: ', degree)
        if degree > 0.95:
            return True
    elif hashmode == 'ht':
        # gray hist test
        classify_gray_hist(image1, image2)
    return False


def contrast_test():
    picture1 = './img/9.png'
    picture2 = './img/23.png'
    picture3 = './img/58.png'

    # 直接获取灰度图
    # img1 = cv2.imread(picture1, 0)
    img1 = cv2.imread(picture1)
    img2 = cv2.imread(picture2)
    img3 = cv2.imread(picture3)

    # Test
    # cv2.imshow('image_1', img1)
    # cv2.imshow('image_2', img2)
    # cv2.imshow('image_3', img3)

    result = image_contrast(img1, img2)
    print(result)
    result = image_contrast(img1, img3)
    print(result)

    # 延时毫秒数,0表示无穷大(相当于暂停)
    # 给imshow提供延时
    cv2.waitKey(0)


if __name__ == "__main__":
    contrast_test()

图像获取分割等

"""
    # 调试参数
    DEBUG_SWITCH  # 1.开启 0.关闭

    # 游戏参数相关说明:
    item_count 项目数
    item_size 每项尺寸
    area_start 游戏区左上角相对于窗口左上角的坐标
"""
import win32api
import win32gui
import win32con

import PIL.ImageGrab

import numpy
import os

from image_contrast import image_contrast

# 调试开关
DEBUG_SWITCH = 0    # 1.开启 0.关闭


"""
# ---------- 连连看 v4.1 初级 ----------
# 项目数:每行,每列
item_count = (12, 7)
# 每项尺寸:宽,高
item_size = (40, 50)
# 游戏区左上角相对于窗口左上角的坐标:left,top
area_start = (164, 162)

# ---------- 连连看 v4.1 特高 ----------
# 项目数:每行,每列
item_count = (18, 10)
# 每项尺寸:宽,高
item_size = (40, 50)
# 游戏区左上角相对于窗口左上角的坐标:left,top
area_start = (41, 64)
"""

# ---------- QQ游戏 - 连连看角色版 ----------
# 项目数:每行,每列
item_count = (19, 11)
# 每项尺寸:宽,高
item_size = (31, 35)
# 游戏区左上角相对于窗口左上角的坐标:left,top
area_start = (14, 181)


def error_exit(des):
    print(des)
    exit(-1)


def get_screen_size():
    screen_width = win32api.GetSystemMetrics(0)
    screen_height = win32api.GetSystemMetrics(1)
    print('屏幕分辨率:\n\twidth,height = {0},{1}'.format(
        screen_width, screen_height))
    return (screen_width, screen_height)


def get_windows(window_title):
    # 获取窗口
    hwnd = win32gui.FindWindow(win32con.NULL, window_title)
    if hwnd == 0:
        error_exit('"%s" not found' % window_title)

    # 前置 + 激活
    win32gui.SetForegroundWindow(hwnd)
    win32gui.SetActiveWindow(hwnd)

    # 获取屏幕分辨率
    screen_width, screen_height = get_screen_size()

    window_left, window_top, window_right, window_bottom =\
        win32gui.GetWindowRect(hwnd)
    print('游戏窗口坐标:\n\tleft,top = {0},{1}\n\tright,bottom = \
{2},{3}'.format(window_left, window_top,
                window_right, window_bottom))

    if min(window_left, window_top) < 0\
            or window_right > screen_width\
            or window_bottom > screen_height:
        error_exit('window is at wrong position')

    """
    window_width = window_right - window_left
    window_height = window_bottom - window_top
    print('----- 程序窗口尺寸 -----')
    print('窗口尺寸:\nwidth,height = {0},{1}'.format(
        window_width, window_height))
    """
    # 获取左上角坐标
    return (window_left, window_top)


def get_game_area(win_left, win_top):
    game_area_left = win_left + area_start[0]
    game_area_top = win_top + area_start[1]
    game_area_right = game_area_left + item_count[0] * item_size[0]
    game_area_bottom = game_area_top + item_count[1] * item_size[1]
    print('游戏区坐标:\n\tleft,top = {0},{1}\n\tright,bottom = \
{2},{3}'.format(game_area_left,
                game_area_top,
                game_area_right,
                game_area_bottom))
    return (game_area_left, game_area_top, game_area_right, game_area_bottom)


def grab_game_area(left, top, right, bottom):
    game_area_image = PIL.ImageGrab.grab((left, top, right, bottom))
    if DEBUG_SWITCH == 1:
        game_area_image.save('llk_.png', 'PNG')
    # game_area_image.show()
    return game_area_image


def get_item_image(image, left, top, right, bottom):
    if DEBUG_SWITCH == 1:
        # 调试阶段,可以恢复为全部,方便调试
        item_image = image.crop((left, top, right, bottom))
    else:
        # 去掉边缘,防止因为边界花边造成对判断的影响
        item_image = image.crop((left + 2, top + 2, right - 2, bottom - 2))
    # item_image.show()
    return item_image


def game_area_image_to_item_images(gram_area_image):
    row_max = item_count[1]
    col_max = item_count[0]

    item_width = item_size[0]
    item_height = item_size[1]

    # 记录每个单独的图像
    item_images = {}
    for row in range(row_max):
        item_images[row] = {}
        for col in range(col_max):
            item_left = col * item_width
            item_top = row * item_height
            item_right = item_left + item_width
            item_bottom = item_top + item_height

            item_image = get_item_image(gram_area_image,
                                        item_left, item_top,
                                        item_right, item_bottom)

            if DEBUG_SWITCH == 1:
                if not os.path.exists('./img'):
                    os.mkdir('./img')
                item_image.save(
                    './img/{}.png'.format(row*col_max + col + 1), 'PNG')

            item_images[row][col] = item_image

    return item_images


def is_empty_item(image):
    im = image
    # 针对 "练练看 v4.1"
    # 我手动去掉了背景,要不然这些影响,我目前还处理不了
    center = im.resize(size=(int(im.width / 4.0), int(im.height / 4.0)
                             ),
                       box=(int(im.width / 4.0), int(im.height / 4.0),
                            int(im.width / 4.0 * 3.0),
                            int(im.height / 4.0 * 3.0)
                            )
                       )

    for color in center.getdata():
        # 因为当前背景被我配置为了纯黑色
        # if color != (0, 0, 0):
        if color != (48, 76, 112):
            return False
    return True


def same_item(image_a, image_b):
    numpy_array_a = numpy.array(image_a)
    numpy_array_b = numpy.array(image_b)
    return image_contrast(numpy_array_a, numpy_array_b)


def images_to_matrix(images):
    row_max = item_count[1]
    col_max = item_count[0]

    item_width = item_size[0]
    item_height = item_size[1]

    # 记录独立的图像
    image_record = []
    item_matirx = numpy.zeros((row_max, col_max), int)
    for row in range(row_max):
        for col in range(col_max):
            # 当前图像
            cur_image = images[row][col]

            same_flag = False
            if not is_empty_item(images[row][col]):
                for index in range(len(image_record)):
                    if same_item(image_record[index], cur_image):
                        id = index + 1
                        item_matirx[row][col] = id
                        same_flag = True
                        break

                # 若没有找到相似
                if not same_flag:
                    image_record.append(cur_image)
                    id = len(image_record)
                    item_matirx[row][col] = id

    # print(item_matirx)
    return item_matirx


if __name__ == '__main__':
    # 获取窗口左上角坐标
    win_start = get_windows('连连看 v4.1')
    # 获取游戏区坐标
    game_area = get_game_area(*win_start)
    # 获取游戏区域图像
    game_area_image = grab_game_area(*game_area)
    # 将一张图像切割为多个项目图
    images = game_area_image_to_item_images(game_area_image)
    # 将多个项目图像转换为矩阵
    matrix = images_to_matrix(images)