• 结队编程作业


    1 博客相关

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

    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提交记录(每次做完都忘记提交github,所以只能找找之前版本在一天提交):

    问题模块:
    一开始对于强制交换理解错误,导致换的是编号而不是位置,一度卡关,后面被队友点醒;
    对于IDA*的启发函数一开始设置不对,无法正确估值,影响判断其代价,后面通过询问对面宿舍的大佬解决了。
    收获:
    果然还是不够行,对于这种知识的掌握总感觉半斤八两,没有真的get到点上,很感谢帮助我的各位大佬以及我的队友。
    评价队友:
    林源:一个憨憨的但是很有耐心,很上进的人,他管理时间的方法值得我学习,任务进行也高效,我的算法也受了他很大帮助。
    个人学习进度及psp表格:

  • 相关阅读:
    weblogic 的应用 常见问题处理 db2 链接不上(转载)
    WebLogic简单的备份和恢复
    linux 切换用户之后变成-bash-x.x$的解决方法
    weblogic迁移随手记
    生产环境下Centos 6.5优化配置 (装载)
    查看db2表空间使用率
    Linux 之 rsyslog 系统日志转发(转载)
    windows下定时利用bat脚本实现ftp上传和下载
    rsyslog同步history日志(转载)
    linux比较指令comm、diff、grep -Ff
  • 原文地址:https://www.cnblogs.com/yydswlt/p/13838392.html
Copyright © 2020-2023  润新知