为了账号安全,请及时绑定邮箱和手机立即绑定

我用回溯算法破解领英皇后游戏的那些事儿

使用OpenCV自动识别谜题并重绘答案

LinkedIn最近推出了一项游戏功能,鼓励忙碌的专业人士在一天中抽空,做一些既能激发大脑又完全放松的游戏。这些游戏提供了一个短暂的工作间歇,帮助你重置思维,让你回到工作时更加专注。通过这些游戏,LinkedIn希望激发创造力,提高解决问题的能力,并在工作中重新点燃人际关系。

他们自己说

在 LinkedIn 上玩游戏?

没错,就是这样的。

每年,我们都会研究世界上最优秀的工作场所。发现,加深和重新点燃工作关系的最好方法之一就是一起享受乐趣。

因此,我们很高兴推出三款需要思考的益智游戏——指针游戏、女王游戏和攀爬游戏,让您能够轻松实现。

与您的同事竞争,激发对话,打破僵局。游戏能够建立关系,而关系正是我们一切工作的核心。

该功能最初引起了好坏参半的反应,有些人认为它偏离了其核心使命和总体目标,但随后的评论都变得积极起来。最近来自The VergeTechCrunch的文章指出,游戏提供了一个逃离日常生活的短暂机会,并且游戏有助于开发新的神经通路。与其他不断鼓励用户参与的游戏应用程序或网站不同,LinkedIn上的游戏要简单得多,每天只会提醒你一次参与。

对于那些还没有玩过或者不熟悉LinkedIn的‘Queens’游戏的各位,

如何玩

目标是在每一行、每一列和每一个颜色区域中恰好放置一个Q

规则说明:两个 Q 不能挨在一起 —— 无论横向、纵向还是斜向都不能紧挨着

作为一个周末项目,我觉得程序化地解决皇后问题会很有趣。最初的想法是用一个大型语言模型解决它,并让我们知道它是如何选择在特定位置放置QX的,这部分内容将留到另一篇文章再讲。在这篇文章中,我们将探索如何从截图中识别出棋盘网格,将其转换成二维数组,最后使用经典的回溯算法来解决它,由于LinkedIn保证总是有解,这意味着我们的代码每次运行都能找到解。

最终演示版

想法就是:

  • 截取包含谜题的屏幕截图
  • 我们的程序应该自动识别谜题网格
  • 裁剪或清理谜题图片
  • 将谜题转化为二维数组
  • 使用回溯算法解决经典的n皇后问题
  • 添加或修改N皇后问题中的皇后条件
  • 使用解决方案重新生成正确放置皇后位置的图像
进入OpenCV(一个开源计算机视觉库):看看如何检测并处理拼图图像

原始截图,检测出的谜题网格,以及灰度处理后的谜题图

OpenCV(全称为开源计算机视觉库)是一个用于计算机视觉、机器学习和图像处理的库。它可以用来识别图像中的模式、提取特征并对其进行数学运算。第一步是使用OpenCV处理谜题截图,让我们快速回顾一下OpenCV图像处理的基础知识。

安装Python的OpenCV包

pip install opencv-python  # 安装OpenCV库

怎样加载图片

cv.imread读取一个图像文件并将其转换为OpenCV矩阵。如果读取图像失败,可能是因为文件丢失、格式不被OpenCV识别等原因,此时将返回一个空矩阵。可以使用cv.imshow函数来显示OpenCV矩阵对应的图像。

    导入 cv2 作为 cv  
    导入 numpy 作为 np  

    # 读取一幅图像  
    original = cv.imread("<file_name>")
    cv.imshow("原图", original)

如何在同一张图上画线、画圆、画长方形和写字

一旦检测到网格,我们就需要用线条重新绘制它,并在文本中放置 Q。下面来看如何在读取的矩阵上画线、圆、矩形并添加文本的简短代码。


    # 绘制一条线段
    line = cv.line(original, (original.shape[1]//2, original.shape[0]//2), (0,0) , (0,255,0), thickness=2)  
    cv.imshow("line", line)  # 显示图像窗口

    # 绘制更多形状,包括圆、矩形和文本
    circle = cv.circle(line, (line.shape[1]//2, line.shape[0]//2), 50, (0,0,255), thickness=2)  
    rect = cv.rectangle(circle, (10,10), (circle.shape[1]//2, circle.shape[0]//2), (255,0,0), thickness=2)  
    text = cv.putText(rect, "Hi", (rect.shape[1]//2, rect.shape[0]//2), cv.FONT_HERSHEY_SIMPLEX, 1, (255,255,255), thickness=2)  # 在矩形中心添加文本 'Hi'

    cv.imshow("all shapes", text)  # 显示所有绘制的形状

如何找到轮廓

轮廓简单来说就是连接所有颜色和亮度相同的点形成的连续边缘。这些在检测形状和物体轮廓检测时非常有用。我们将通过检测每个单元格来画出我们的谜题网格。

    # 最好将图像转换为灰度,  
    # 并添加一点模糊以更好地检测轮廓,  
    # 因为我们图像是网格,所以不需要模糊处理  

    # 默认情况下,OpenCV 读取图像为 BGR,  
    # 与传统的 RGB 相反  
    gray = cv.cvtColor(original, cv.COLOR_BGR2GRAY)  
    轮廓, _ = cv.findContours(gray, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)

默认情况下,OpenCV以BGR格式读取图像,与传统上使用的RGB不同

裁图

为了从截图中去除不必要的部分并减少噪音,一检测到轮廓就可以开始。

    # 这基本上是从整个图像中选取我们所需的像素部分。
    cropped = original[0:original.shape[1]//2, 0:original.shape[0]//2]  
    cv.imshow("cropped", cropped)
把基础知识结合起来

首先,我们将图像加载到内存,并将其转换为灰度图像。这有助于简化轮廓检测,这是一个通常会遵循的步骤,因为它减少了图像的复杂性。接下来,我们找到轮廓,对其进行排序,并选择最大的一个。通常,第一个轮廓是原始图像的边界框,所以我们使用第二大的轮廓来分离数独网格。然后,我们裁剪图像,只保留网格部分。因为我们再次找到轮廓,因为此时噪音已经减少,可以更好地检测网格。我们确定网格中的单元格数量,并对每个单元格进行迭代,计算平均颜色,并为每个颜色分配一个数字。这给我们提供了数独的二维数组。

    # 读取输入图像并保存原始图像
    original = cv.imread(file_name)
    cv.imwrite("solution/original.png", original)

    # 将图像转换为灰度图像
    gray = cv.cvtColor(original, cv.COLOR_BGR2GRAY)

    # 在灰度图像中查找轮廓并按面积排序
    contours, _ = cv.findContours(gray, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
    contours = sorted(contours, key=cv.contourArea, reverse=True)

    # 提取网格的边界框(使用第二大的轮廓)
    x, y, w, h = cv.boundingRect(contours[1])

    # 从原始图像中裁剪网格区域
    grid = original[y:y+h, x:x+w]
    cv.imwrite("solution/grid.png", grid)

    # 将裁剪后的网格转换为灰度图像
    gray = cv.cvtColor(grid, cv.COLOR_BGR2GRAY)
    cv.imwrite("solution/gray-grid.png", gray)

    # 在裁剪后的灰度网格中查找轮廓
    contours, _ = cv.findContours(gray, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
    contours = sorted(contours, key=cv.contourArea)

    # 确定网格中的总格子数
    total_cells = len(contours) - 2
    grid_size = int(math.sqrt(total_cells))

    # 检查是否检测到完整的正方形网格
    if total_cells != grid_size**2:
        print("无法检测到完整网格!退出")

    # 计算每个格子的尺寸
    cell_width = w // grid_size
    cell_height = h // grid_size

    # 初始化颜色映射和棋盘表示
    colors = []
    board = []
    color_index = 1
    color_map = {}
    reverse_color_map = {}
    padding = 10

    # 遍历网格中的每个格子
    for i in range(grid_size):
        row = []
        for j in range(grid_size):
            # 计算每个格子的坐标
            cell_x = j * cell_width
            cell_y = i * cell_height
            padding = 15
            cell = grid[cell_y+padding:cell_y+cell_height-padding, cell_x+padding:cell_x+cell_width-padding]

            # 获取格子的平均颜色
            avg_color = cell.mean(axis=0).mean(axis=0)
            avg_color = avg_color.astype(int)
            avg_color = tuple(avg_color)

            # 将颜色映射到一个唯一的索引,如果还没有映射
            if avg_color not in color_map:
                color_map[avg_color] = str(color_index)
                reverse_color_map[str(color_index)] = avg_color
                color_index += 1

            # 将颜色索引添加至该行
            row.append(color_map[avg_color])

        # 将行添加至棋盘
        board.append(row)

原始截图画面 vs 识别出的二维数组图

N皇后难题

八皇后问题的维基页面

八皇后难题是指在8×8的国际象棋棋盘上放置八个皇后,使任何两个皇后都无法互相威胁;因此,每个解法都需要确保八个皇后不在同一行、同一列或同一对角线上。共有92种解决方案。该难题最早在19世纪中期被提出。在现代,它通常被用来展示各种计算机编程技术。

这可以通过回溯轻松解决。下面是一些直观理解:

从左边第一列开始,我们在每一行放置一个皇后,检查是否有与其他皇后冲突。如果有冲突,我们回退到前一列,把它放在下一行,然后继续尝试这一列。一旦我们到了最后一列并且能够找到一个有效的解,我们就找到了一个满足所有条件的解决方案。这里用回溯法解决了经典的N皇后(或八皇后)问题。

    def is_safe(board, row, col, n):  
        # 检查这一行的左边是否有皇后
        for i in range(col):  
            if board[row][i] == 'Q':  
                return False  

        # 检查左上方的斜线上是否有皇后
        for i, j in zip(range(row, -1, -1), range(col, -1, -1)):  
            if board[i][j] == 'Q':  
                return False  

        # 检查左下方的斜线上是否有皇后
        for i, j in zip(range(row, n), range(col, -1, -1)):  
            if board[i][j] == 'Q':  
                return False  

        # 如果没有冲突位置的皇后,则可以安全地在这里放置皇后
        return True  

    def solve_nqueens_util(board, col, n):  
        # 如果所有皇后都已放置,返回True
        if col >= n:  
            return True  

        # 尝试在当前列的每一行放置皇后
        for i in range(n):  
            # 检查在(i, col)处放置皇后是否安全
            if is_safe(board, i, col, n):  
                # 在(i, col)处放置皇后
                board[i][col] = 'Q'  

                # 递归地在下一列放置皇后
                if solve_nqueens_util(board, col + 1, n):  
                    return True  

                # 如果在(i, col)处放置皇后不能成功解决问题,则移除皇后(回溯)
                board[i][col] = '.'  

        # 如果当前列无法放置皇后则返回False
        return False  

    def solve_nqueens(n):  
        # 初始化棋盘,用'.'表示空格
        board = [['.' for _ in range(n)] for _ in range(n)]  

        # 从第一列开始解决问题
        if not solve_nqueens_util(board, 0, n):  
            return "没有解存在"  

        # 如果找到了解决方案,则返回带有皇后放置的棋盘
        return board  

    def print_board(board):  
        # 以可读的格式打印棋盘
        for row in board:  
            print(" ".join(row))  

    # 示例用法:解决8x8棋盘上的N皇后问题
    n = 8  
    solution = solve_nqueens(n)  
    if solution != "没有解存在":  
        print_board(solution)  
    else:  
        print(solution)
LinkedIn女性的注意事项

以上述代码为基础,我们将修改我们的 is_safe 方法,使得每个颜色组中最多只能有一个 Q。我们的新 is_safe 方法如下:

    def is_safe(original_board, board, row, col, queens_in_colors, n):  
        # 检查当前行的左边是否有其他皇后  
        for i in range(col):  
            if board[row][i] == 'Q':  
                return False  

        # 检查左上方的对角线是否有皇后  
        if col - 1 >= 0 and row - 1 >= 0:  
            if board[row-1][col-1] == 'Q':  
                return False  

        # 检查左下方的对角线是否有皇后  
        if col - 1 >= 0 and row + 1 < n:  
            if board[row+1][col-1] == 'Q':  
                return False  

        # 检查当前列上方的行是否有皇后  
        for i in range(row):  
            if board[i][col] == 'Q':  
                return False  

        # 检查当前颜色是否已经有其他皇后  
        current_color = original_board[row][col]  
        if queens_in_colors[current_color]:  
            return False  

        # 如果所有检查都通过,可以放置皇后  
        return True
基于解决办法重新生成图像

最后,是时候通过重新生成图像并将皇后放在正确的位置上来可视化解决方案了。这包括绘制_矩形_来表示单元格,_线_来表示边界,以及_文字_来表示皇后。

    # 解决给定棋盘上的 N 皇后问题  
    solved_board = solve_n_queens(board)  

    # 检查检测到的颜色数量是否超过网格大小;如果不匹配则终止  
    if len(color_map) != grid_size:  
        print("检测到的颜色数量超过网格大小!终止")  

    # 初始化一个空的输出图像以重新创建网格  
    output_image = np.ones((h, w, 3), dtype="uint8")  

    # 设置视觉元素的边框和字母大小  
    border_size = 1  
    letter_height = 10  

    # 遍历网格中的每个单元格  
    for i in range(grid_size):  
        for j in range(grid_size):  
            # 计算当前单元格的位置  
            cell_x = j * cell_width  
            cell_y = i * cell_height  

            # 从反向颜色映射中获取当前单元格的颜色  
            color_pick = reverse_color_map.get(board[i][j])  
            color = (int(color_pick[0]), int(color_pick[1]), int(color_pick[2]))  

            # 用相应的颜色绘制当前单元格  
            output_image = cv.rectangle(  
                output_image,  
                (cell_x + border_size, cell_y + border_size),  
                (cell_x + cell_width - border_size, cell_y + cell_height - border_size),  
                color,  
                thickness=-1  
            )  

            # 绘制单元格之间的网格线  
            output_image = cv.line(  
                output_image,  
                (cell_x, cell_y),  
                (cell_x + cell_width, cell_y),  
                (0, 0, 0),  
                thickness=1  
            )  

            # 如果该单元格放置了皇后,则在单元格中心绘制字母 "Q"  
            if solved_board[i][j] == "Q":  
                output_image = cv.putText(  
                    output_image,  
                    "Q",  
                    (cell_x + cell_width // 2 - letter_height, cell_y + cell_height // 2 + letter_height),  
                    cv.FONT_HERSHEY_COMPLEX,  
                    1,  
                    (0, 0, 0),  
                    lineType=cv.LINE_AA,  
                    thickness=2  
                )  

    # 保存带有解的棋盘图像  
    cv.imwrite("solution/solve.png", output_image)

最后的结果

最后说一下

结论

我们探索了使用经典的回溯算法来程序化地解决LinkedIn的八皇后游戏。首先,我们对棋盘进行了预处理,将棋盘图像转换为二维数组。然后,我们实现了N皇后问题的解决方案,并根据我们的需求调整了N皇后问题的约束条件。最后,我们使用OpenCV展示了解决方案的图像。

GitHub上的全部代码链接

如无特别说明,所有图片均为作者提供,

原文发布于https://blog.memsranga.com,时间为2024年9月7日。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消