结对编程作业

1 博客相关

个人博客链接:https://www.cnblogs.com/AYuaner/p/13843297.html
队友博客链接:https://www.cnblogs.com/yydswlt/p/13838392.html
个人github链接:https://github.com/AYuaner/Group-programming-job
队友github链接:https://github.com/WLT422/Group-programming-job.git
具体分工:

2 原型设计

(1).本次结对编程的设计说明:

本次结对编程需要做出类似数字华容道小游戏的原型,那就先上图

所要实现的功能有以下部分:点击图形的时候能够移动(且不会超出边界)、完成拼图时候跳出的结算判定界面、觉得没有思路的时候可以推翻重来的“重新开始”按钮、左下角计步器。

(2).说明使用的原型开发工具

本次原型开发使用的是Axure Software Solution公司开发的 Axure RP 9 (申请的Student License)

(3).描述结对过程,提供非摆拍讨论图

结对过程:平常一起上课的两个人,就一起做作业了

(4).遇到的问题、以及解决方法:

问题:原型设计这个东西,在之前就从来没听说过,也就是这次作业才接触到,所以相关知识一片空白
解决方法:在B站上看了一些Axure RP的原型开发教学,然后跟着做了一些练习之后才有一定的感觉
收获:收获到了原型开发的一点点皮毛知识(UI相关的审美还是不太行)

3 AI设计

流程图:

(1).网络接口的实现

本次网络接口的实现的是使用了python的request模块,很方便的实现了对网络接口与的请求与接收。

(2).代码组织与内部实现设计

本次代码一共设计了两个类,Board类用来记录此时矩阵的状态以及保存其移动消耗,IDAstart类为核心算法用来还原及强制交换等操作
设计了九个函数,包括但不限于图片读取与识别,图片分割与匹配等。

图片分割采用先填充在分割的方法,利用Image库的size函数获得图片边长并取最长填充成正方形的之后切割成九块并保存。

def fill_image(image):# 将图片填充为正方形
    width, height = image.size
    # 选取长和宽中较大值作为新图片的边长
    new_image_length = width if width > height else height
    # 生成新图片[白底]
    new_image = Image.new(image.mode, (new_image_length, new_image_length), color='white')
    # 将之前的图粘贴在新图上,居中
    if width > height:  # 原图宽大于高,则填充图片的竖直维度
        new_image.paste(image, (0, int((new_image_length - height) / 2)))  # (x,y)二元组表示粘贴上图相对下图的起始位置
    else:
        new_image.paste(image, (int((new_image_length - width) / 2), 0))
    return new_image    #获得填充后的图片


def cut_image(image, n):# 切图(n * n)
    width, height = image.size
    item_width = int(width / n) #每块切图边长为宽的n分之一
    box_list = [] #定义一个空列表用来之后保存分割的图片
    for i in range(0, n):
        for j in range(0, n):
            box = (j * item_width, i * item_width, (j + 1) * item_width, (i + 1) * item_width)
            box_list.append(box)
    #裁切图片。区域由一个4元组定义,表示为坐标是 (left, upper, right, lower)
    image_list = [image.crop(box) for box in box_list]
    return image_list #返回一个切割后的图片列表


def save_images(image_list, content):# 保存
    index = 0
    for image in image_list:
        image.save(content + '/' + str(index) + '.jpg', 'JPEG') #将切图保存在本地方便之后匹配
        index += 1

图片匹配采用的方法将图片转为narray矩阵进行比较,九张图片中有大于七张图片矩阵相同便证明匹配完成

def img_match(img_base64):
    img = base64.b64decode(img_base64)  # 将从接口获取的base64编码转字符串
    img = BytesIO(img)  # 字符串转字节流
    pic = Image.open(img)  # 以图片形式打开img
    # 将读取的测试图片保存到本地,同目录下的test文件夹中,并命名为orig.jpg
    pic.save('d:/jiedui/test/orig.jpg', 'JPEG')
    img_list = cut_image(pic, 3)#将图片分割成九块
    save_images(img_list, 'd:/jiedui/test/ori')
    # 将原图切分为3*3片,存入img_list列表,并将切片保存到同目录ori文件夹中
    img_arr = []  # 定义一个存放乱序切片的numpy矩阵的列表
    for root, dirs, files in os.walk("d:/jiedui/test/ori"):  # 遍历存放乱序切片的test文件夹
        for file in files:  # 处理该文件夹里的所有文件
            p = Image.open(os.path.join(root, file))  # 合成绝对路径,并打开图像
            p = np.asarray(p)  # 图像转矩阵
            img_arr.append(p)  # 将得到的矩阵存入列表
    first_list = [-1, -1, -1, -1, -1, -1, -1, -1, -1]  # 存放乱序图片的状态,-1代表白块,0~8代表该切片是处于原图中的哪一位置
    dir_path = "d:/jiedui/base"#base文件夹为本地文件夹,存放着从图片库所有图片的切割分片,并按图片名并分好类
    # 遍历同目录中文件夹中的所有文件夹
    for root, dirs, files in os.walk(dir_path):
        for dir in dirs:
            # k代表状态列表下标,cnt记录当前已匹配上的切片数
            k =  0
            cnt = 0
            # tmp_list列表存放目标状态,由于不同原图之间可能存在完全一样的切片,会影响tmp_list的最终结果
            # 因此每次与新的一张原图比较前,将tmp_list初始化为全-1
            tmp_list = [-1, -1, -1, -1, -1, -1, -1, -1, -1]
            # 从img_arr列表(即乱序切片的numpy矩阵列表)中,逐个与原图库中的切片比较
            for i in img_arr:
                # index用于指示乱序的切片在原图的哪一位置
                index = 0
                # 遍历存放原图切片的文件夹中的所有文件(即,原图切片)
                for root, dirs, files in os.walk(os.path.join(dir_path, dir)):
                    for j in files:
                        # 用os.path.join()拼接出文件的绝对路径,然后打开该文件(图片)
                        j = Image.open(os.path.join(root, j))
                        j = np.asarray(j)  # 将原图切片转换为numpy矩阵
                        if (i == j).all():  # 判断两个矩阵是否完全相同
                            first_list[k] = index
                            tmp_list[index] = index
                            cnt += 1
                            break
                        index += 1
                    k += 1
            # 若已有8个切片匹配上则说明匹配到了原图
            if cnt > 7:
                print("该图原图是:", dir)  # 打印原图名称
                break 
        if cnt < 8:
            print("ERROR:无匹配图片,请重新确认")
    # 遍历初始状态列表,获得白块的初始位置
    for i in range(len(first_list)):
        if first_list[i] < 0:
            blank = i
            break
    # 返回初始状态(列表)、空白块位置、目标状态(列表)
    return  first_list, blank, tmp_list

本次图片还原的核心算法IDA-star算法(刚好人工智能课在八数码问题的解法,本次题目也可大概看成八数码的还原问题),所谓IDA算法就是迭代加深的A-star算法,而迭代加深,首先,它是深度优先搜索,其次它与普通深度优先搜索不同的是,每次深搜都会有搜索的最大深度限制,如果没有找到解,那么就增大深度,再进行深搜,如此循环直到找到解为止,这样可以找到最浅层的解。A算法是启发式的搜索算法,其关键在于启发函数的定义:

       f(n)=g(n)+h(n);   
       这个式子中:  
                   f(n)表示从初始状态到目标状态的估测代价。
                   g(n)表示从初始状态到当前状态的代价(已经确定)。
                   h(n)表示从当前状态到目标状态的估测代价(预测)。
                   其中:h(n)的好坏直接影响评估函数的好坏。
              一个好的f(n)总能明确指引算法前进的方向,可以迅速的到达目标状态。
       f*(n)=g*(n)+h*(n); 
       我们假设的从初始状态到目标状态的实际最小代价。
       这个式子中:
                  f(n)表示从初始状态到目标状态的实际代价。
                  g*(n)表示从初始状态到当前状态的代价(已经确定)g*(n)和g(n)是相等的。
                  h*(n)表示从当前状态到目标状态的实际代价。
      若h(n)<=h*(n),则总能找到最优解。当h(n)<h*(n)的时候,不可能找到一条从初始状态到达目标状态的路径。在搜索过程中使得h(n)逐渐接近h*(n),最终找到最优路径。  

关于该算法的具体资料如下:
初识A*算法
IDA*算法
利用A* 和IDA* 解决八数码问题

关键代码:

class Board:
    def __init__(oneself, ori_list, pos, step=0, preboard=None, prepath=""):
        oneself.ori_list = ori_list
        oneself.pos = pos
        oneself.step = step
        oneself.cost = oneself.cal_cost()
        oneself.preboard = preboard
        oneself.prepath = prepath
    #计算移动代价
    def cal_cost(oneself):
        count = 0
        sheet = [[0, 0], [0, 1], [0, 2],
                 [1, 0], [1, 1], [1, 2],
                 [2, 0], [2, 1], [2, 2]]#每个位置的坐标
        for i in range(9):
            if oneself.ori_list[i] < 0:
                continue
            count += abs(sheet[i][0] - sheet[oneself.ori_list[i]][0]) + abs(sheet[i][1] - sheet[oneself.ori_list[i]][1]) #启发函数
        # cost = count + oneself.step
        # return cost
        return count + oneself.step


class IDAstar:
    # 当白块在9个位置时可以移动的方向,-1代表无法移动
    # w上, d右, s下, a左
    d = [[-1, 1, 3, -1],  # 0
         [-1, 2, 4, 0],  # 1
         [-1, -1, 5, 1],  # 2
         [0, 4, 6, -1],  # 3
         [1, 5, 7, 3],  # 4
         [2, -1, 8, 4],  # 5
         [3, 7, -1, -1],  # 6
         [4, 8, -1, 6],  # 7
         [5, -1, -1, 7]]  # 8
    # 将移动方向的序列转化为'w', 'd', 's', 'a',上,右,下,左
    index_to_direct = ['w', 'd', 's', 'a']
    swap_record = {}    # 用于记录强制交换阶段的交换方案
    # forced_mark = True  # 标记最初的强制交换是否可解,若可解则不能自由交换
    no_swap_exe = True  # 标记是否执行了强制交换
    find_sol = False  # 标记是否解决8 puzzle问题

    def __init__(oneself, start, pos, target, step_num, swap_scheme):
        # 初始状态、白块初始位置、目标状态、第几步进行强制交换、强制交换的最初方案
        # step_num为数字、swap_scheme为两个元素的列表
        IDAstar.start = start
        IDAstar.pos = pos
        IDAstar.target = target
        IDAstar.init = Board(start, pos)
        IDAstar.maxdep = 0   # 搜索的最大深度
        IDAstar.path = ""
        IDAstar.step_num = step_num
        IDAstar.swap_scheme = swap_scheme
        # 判断目标状态的逆序对数是奇数还是偶数,当前状态必须与目标状态同奇同偶才可解
        IDAstar.solvable = Judge_even(target)
        swap_record = {}    # 用于记录强制交换阶段的交换方案
        # print("IDAstar.solvable: ", IDAstar.solvable)

    def dfs(oneself, now, lastd, n):
        if now.ori_list == oneself.target: 
            oneself.find_sol = True
            return True

        # swap_mark = False  # 强制交换的标记,若本次搜索用到了强制交换,则为True
        # 强制交换, n 表示当前的步数

        if oneself.no_swap_exe and n == oneself.step_num:
            scheme = oneself.forced_exchange(now.ori_list)
            oneself.no_swap_exe = False
            now.ori_list[scheme[0]], now.ori_list[scheme[1]] = now.ori_list[scheme[1]], now.ori_list[scheme[0]]
            now.step = 0    # 强制交换后,从头搜索
            # 记录白块位置
            for i in range(len(now.ori_list)):
                if now.ori_list[i] < 0:
                    now.pos = i
                    break
            now.cost = now.cal_cost()  # 交换后重新计算代价
            oneself.maxdep = now.cost  # 重新计算最大深度
            oneself.init.ori_list = copy.deepcopy(now.ori_list)
            oneself.init.pos = now.pos
            oneself.init.step = now.step
            oneself.init.cost = now.cost
            oneself.swap_scheme = copy.deepcopy(scheme)  # 记录交换方案
            return True

        # 基于f值的强力剪枝
        if now.cost > oneself.maxdep:
            return False

        pos = now.pos
        step = now.step
        for i in range(4):
            # 方向不可走时
            if oneself.d[pos][i] == -1:
                continue
            # 0, 1, 2, 3
            # w, d, s, a
            # 上一步为向左,此步则不能向右走老路,其他方向同理。
            if (lastd == -1) or (lastd % 2) != (i % 2) or (lastd == i):
                ori_list = copy.deepcopy(now.ori_list)
                ori_list[pos], ori_list[oneself.d[pos][i]] = ori_list[oneself.d[pos][i]], ori_list[pos]
                # 构造函数形式:
                temp = Board(ori_list, oneself.d[pos][i], step + 1, now, oneself.index_to_direct[i])
                # 如果找到最短路径,递归地记录路径
                if oneself.dfs(temp, i, n+1):
                    oneself.path += temp.prepath
                    return True
        return False

    def IDA(oneself):
        oneself.maxdep = oneself.init.cost
        while not oneself.dfs(oneself.init, -1, 0):
            oneself.maxdep += 1
        #迭代加深
        tmp_path = oneself.path[::-1]
        oneself.path = ""
        if not oneself.find_sol:
            while not oneself.dfs(oneself.init, -1, 0):
                oneself.maxdep += 1
        oneself.path = tmp_path + oneself.path[::-1]
        return oneself.path

    # 在当前状态ori_list进行强制交换,若强制交换导致无解,则紧接着进行一次自由交换
    # 返回强制交换的方案
    def forced_exchange(oneself, ori_list):
        # 交换两个块
        tmp0 = copy.deepcopy(ori_list)
        if oneself.swap_scheme[0] != oneself.swap_scheme[1]:
            tmp0[oneself.swap_scheme[0]], tmp0[oneself.swap_scheme[1]] = tmp0[oneself.swap_scheme[1]], tmp0[oneself.swap_scheme[0]]
        # 若最初的强制交换不会造成无解,则返回
        if Judge_even(tmp0) == oneself.solvable:
            return oneself.swap_scheme
        # 否则,进行自由交换
        else:
            # 先要强制交换,在强制交换的基础上自由交换
            ori_list[oneself.swap_scheme[0]], ori_list[oneself.swap_scheme[1]] = ori_list[oneself.swap_scheme[1]], ori_list[oneself.swap_scheme[0]]
            # 双重循环,遍历可自由交换出的所有状态
            for i in range(8):
                for j in range(i+1, 9):
                    tmp = copy.deepcopy(tmp0)
                    tmp[i], tmp[j] = tmp[j], tmp[i]
                    if Judge_even(tmp) == oneself.solvable:
                        for k in range(len(tmp)):
                            if tmp[k] < 0:
                                break
                        tmp_board = Board(tmp, k)
                        cost_h = tmp_board.cost
                        # 以cost_h为键,交换方案为值,可能会有多个方案的cost_h相同的情况,但字典中只记录一个
                        oneself.swap_record[cost_h] = [i, j]
            m = min(oneself.swap_record)  # 找到最小的代价
            oneself.swap_scheme = copy.deepcopy(oneself.swap_record[m])
            return oneself.swap_scheme  # 返回最小代价对应的交换方案

最后有无解的判断就是逆序数对数量的判断,这里就不细说。
强制交换,我的思路是先判断再强制交换的步数前是否解完了,如果没有则进行强制交换同时判断是否有解,无解则对各种交换方式计算移动代价,最后取最小。

性能优化:
耗时一般:
在逆序数判断上由开始的双循环计数,变成了归并排序计数;
本来想对原图库的割图建立一个dict(而不是调用本地保存文件)来加速匹配,因为I/O接触较少,对于这方面的优化没有得到有效的提升,最后性能几乎没什么变化,也就放弃了。
github提交记录:

问题模块:
一开始对于强制交换理解错误,导致换的是编号而不是位置,一度卡关,后面被队友点醒;
对于IDA*的启发函数一开始设置不对,无法正确估值,影响判断其代价,后面通过询问对面宿舍的大佬解决了。
收获:
开发的过程,原来不只是代码,还有前期的原型设计等等方面也至关重要且有难度
评价队友:
WLT是个很努力的人,也很有当担,在二人犹豫分工的时候能站出来担起AI算法部分的担子,并且十分努力的在学习相关知识来写算法。
需要改进的部分:
我觉得我无权来评价队友需要改进的地方,因为这次的结对编程中,队友付出了极大的时间和精力,相反我却没有
个人学习进度及psp表格:

原文地址:https://www.cnblogs.com/AYuaner/p/13843297.html