题目:图片华容道
一、
我的博客
我的GitHub
苏哥的博客
苏哥的GitHub
具体分工
p-主要工作;s-协助工作;0-无工作
分工内容 | 我 | 队友 |
---|---|---|
原型设计 | 0 | p |
基本UI界面 | p | s |
UI细节优化 | s | p |
AI | 0 | p |
接口 | p | 0 |
题目设计 | p | 0 |
二、原型设计
2.1设计说明
首先,是关于模型的设计,我们的想法是:先做个好看点的游戏开始界面,两个按钮“开始游戏”、“结束游戏”。点开始游戏之后,进入可以玩的游戏窗口。左边是拼图区块,右边加些功能型按钮,比如“重新开始”、“退出游戏”、“选择拼图”等,再显示步数、最快步数等内容。
拿到题目的时候我就想用用pycharm实现,因为pycharm有个pygame的库,我觉得做这游戏效果会好些。
最开始就是用pygame设计出游戏窗口,写一些颜色的RGB值,用纯色填充游戏背景,导入图片。对九个拼图进行编号,利用random库的shuffle函数进行随机排序,从图片中挖出一个空白格,用空白格做文章,监听键盘进行移动,当序号还原到未被打乱的状态时,即完成拼图,然后会在界面上显示完整的拼图。
之后优化了进入的界面,做了个稍帅一点的背景图,放了几个按钮,点击“开始游戏”进入游戏界面,游戏界面增加了几个功能按钮:“重新开始”,“退出游戏”,“AI解题”,“上次步数显示”,“当前步数显示”。点击AI解题后会在pycharm的运行面板里显示出后续的打发,根据“0”的移动来确定空白块的移动。
2.2使用的原型模型设计工具:MockPlus
2.3共同面对之结对照片
“非摆拍照片” 我寻思这不摆拍该怎么拍
2.4遭遇的困难及解决方法
- 困难描述:①不知道怎么和队友交接代码 ②一开始不知道怎么起步,迷茫了一个星期 ③设想着两个窗口,点击第一个窗口的选项然后切换到第二个游玩窗口,但是不知道两个窗口之间要怎么切换。
- 解决尝试:两个人在同一个宿舍搞代码还是比较方便的,讨论也比较方便,然后就py文件发来发去,就安排上了。迷茫完了还是得面对,第二个星期就开始慢慢着手做事了,从pygame的学习开始,然后就递归递归递归学习······
- 问题基本上解决了。
- 收获:团队作业应该多分函数写在不同的python file上,调用起来方便,彼此沟通也快,找代码段落也快,对方修改了某个函数、某个类,也不需要大动干戈的修改。
三、AI与原型设计实现
3.1代码实现思路:
1.网络接口的使用:
# 从Postman获取题目,将图片保存至文件夹
pic_url = requests.get('http://47.102.118.1:8089/api/problem?stuid=031802423')
image_code = base64.b64decode(pic_url.json()['img'])
file_like = BytesIO(image_code)
image = Image.open(file_like)
image.save('image.JPG')
2.代码组织与内部实现设计:
3.关键的算法与关键实现部分流程图
比较关键的算法有:从文件夹中寻找与切块后的拼图拥有相同部分的图片的算法
4.比较重要的代码片段
- 将base64编码转为图片,并将图片保存到文件夹的代码:
pic_url = requests.get('http://47.102.118.1:8089/api/problem?stuid=031802423')
image_code = base64.b64decode(pic_url.json()['img'])
file_like = BytesIO(image_code)
image = Image.open(file_like)
image.save('image.JPG')
- 将图片切块、保存的代码:
#切图
def cut_image(image) :
item_width = 300
box_list = []
for i in range(0, 3) : # 两重循环,生成9张图片基于原图的位置
for j in range(0, 3) :
box = (j*item_width, i*item_width, (j+1)*item_width, (i+1)*item_width)
box_list.append(box)
image_list = [image.crop(box) for box in box_list]
return image_list
# 保存拼图
def save_images(image_list) :
index = 0
for image in image_list :
image.save(str(index) + '.jpg')
index += 1
gameImage2 = Image.open('./zifu/H_.jpg')
image_list = cut_image(gameImage2)
save_images(image_list)
- 比较两章图片是否相同的代码:
#对比是否为相同图片,如果相同,则返回True
def compare_image_with_hash(image_file_name_1, image_file_name_2, max_dif=0) :
ImageFile.LOAD_TRUNCATED_IMAGES = True
hash_1 = None
hash_2 = None
with open(image_file_name_1, 'rb') as fp :
hash_1 = imagehash.average_hash(Image.open(fp))
with open(image_file_name_2, 'rb') as fp :
hash_2 = imagehash.average_hash(Image.open(fp))
dif = hash_1 - hash_2
if dif < 0 :
dif = -dif
if dif <= max_dif :
return True
else :
return False
- 判断文件夹中的图片切块是否与选出的拼图块相同的代码:
for pic_name in os.listdir("D://python_work//pycharmWork//huarong//zifu"):
img = Image.open("D://python_work//pycharmWork//huarong//zifu" + "/" +pic_name)
image_list1 = cut_image(img)
for image in image_list1 :
image.save('cankao' + '.jpg')
if compare_image_with_hash('cankao.jpg', 'tu.jpg', 0):
img.save('cankao1.jpg')
break
- 绘制每次移动后的拼图的代码:
for i in range(blocks):
row = int(i/rows)
col = int(i%rows)
rectDst = pygame.Rect(col*width, row*height, width, height)
if gameBoard[i] == -1:
continue
rowArea = int(gameBoard[i]/rows)
colArea = int(gameBoard[i]%rows)
rectArea = pygame.Rect(colArea*width, rowArea*height, width, height)
windowSurface.blit(gameImage, rectDst, rectArea)
5.性能分析与改进
(这里是队友写的)布置完题目后,在一节人工智能课上听到了老师讲解使用A算法解决八数码问题便反应过来可以运用到此次的结对编程作业上。A算法其实是Dijkstra最短路径算法的一种扩展,由于效率高,被广泛应用于电脑游戏中的路径规划问题中。此算法中最核心的就是启发是函数与对open表和close表的维护。
启发式函数 f(n) = g(n) + h(n) 可以理解为从原点到目标点的所需要消耗的总代价f(n),这个总代价可以分成两个部分,从原点到中间节点(搜索的中间状态)已经消耗的实际代价g(n),和从中间节点到目标点的预测h(n)。
open表:可以认为是一个未搜索节点的表
close表:可以认为是一个已完成搜索的节点的表
在每一次搜索中两个表中的元素可以互相转换。
在此次作业中,我把启发式函数含义定为:当前格局与目的格局相比,其位置不符的将牌数目。
def A_star(s):
global openlist # 全局变量可以让open表进行时时更新
openlist = [s]
while (openlist): # 当open表不为空
get = openlist[0] # 取出open表的首节点
if (get.node == goal.node).all(): # 判断是否与目标节点一致
return get
openlist.remove(get) # 将get移出open表
# 判断此时状态的空格位置
for a in range(len(get.node)):
for b in range(len(get.node[a])):
if get.node[a][b] == 0:
break
if get.node[a][b] == 0:
break
# 开始移动
for i in range(len(get.node)):
for j in range(len(get.node[i])):
c = get.node.copy()
if (i + j - a - b) ** 2 == 1:
c[a][b] = c[i][j]
c[i][j] = 0
new = State(c)
new.father = get # 此时取出的get节点成为新节点的父亲节点
new.g = get.g + 1 # 新节点与父亲节点的距离
new.h = h(new) # 新节点的启发函数值
new.f = new.g + new.h # 新节点的估价函数值
openlist.append(new) # 加入open表中
list_sort(openlist) # 排序
6.改进的思路
对于启发式函数的定义可以再进行改进,比如将其定义为:各将牌移到目的位置所需移动的距离的总和。
7.性能分析展示
-
性能分析图
-
程序中消耗最大的函数
8.展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路
- (1)对空白块上下左右移动的测试,确保移动时不会跑到界外去,在完成拼图时能够显示出原来完整的图片。对空白块移动的单元测试代码如下:
import pygame, sys, random
from pygame.locals import *
# 一些常量
window_length = 500
background = (255, 255, 255)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
rows = 3
blocks = rows * rows
# 随机生成游戏盘面
def newGameBoard() :
board = []
for i in range(blocks) :
board.append(i)
blackCell = blocks - 1
# 将图片的一块挖空
board[blackCell] = -1
# 将空白块进行随机移动,确保每次打乱都能够有解
for i in range(50) :
direction = random.randint(0, 3)
if (direction == 0) :
blackCell = moveLeft(board, blackCell)
elif (direction == 1) :
blackCell = moveRight(board, blackCell)
elif (direction == 2) :
blackCell = moveUp(board, blackCell)
elif (direction == 3) :
blackCell = moveDown(board, blackCell)
return board, blackCell
# 若空白图像块不在最左边,则将空白块左边的块移动到空白块位置
def moveRight(board, blackCell) :
if blackCell % rows == 0 :
return blackCell
board[blackCell - 1], board[blackCell] = board[blackCell], board[blackCell - 1]
return blackCell - 1
# 若空白图像块不在最右边,则将空白块右边的块移动到空白块位置
def moveLeft(board, blackCell) :
if blackCell % rows == rows - 1 :
return blackCell
board[blackCell + 1], board[blackCell] = board[blackCell], board[blackCell + 1]
return blackCell + 1
# 若空白图像块不在最上边,则将空白块上边的块移动到空白块位置
def moveDown(board, blackCell) :
if blackCell < rows :
return blackCell
board[blackCell - rows], board[blackCell] = board[blackCell], board[blackCell - rows]
return blackCell - rows
# 若空白图像块不在最下边,则将空白块下边的块移动到空白块位置
def moveUp(board, blackCell) :
if blackCell >= blocks - rows :
return blackCell
board[blackCell + rows], board[blackCell] = board[blackCell], board[blackCell + rows]
return blackCell + rows
# 是否完成
def isFinished(board, blackCell) :
for i in range(blocks - 1) :
if board[i] != i :
return False
return True
# 初始化
pygame.init()
# 加载图片
gameImage = pygame.image.load('wu.jpg')
gameRect = gameImage.get_rect()
# 设置窗口
screen = pygame.display.set_mode((gameRect.width, gameRect.height))
pygame.display.set_caption('拼图')
# 每块拼图的大小
cellWidth = int(gameRect.width / rows)
cellHeight = int(gameRect.height / rows)
finish = False
gameBoard, blackCell = newGameBoard()
# 游戏主循环
while True :
for event in pygame.event.get() :
if event.type == QUIT :
pygame.quit()
sys.exit()
if finish :
continue
if event.type == KEYDOWN :
if event.key == K_d :
blackCell = moveLeft(gameBoard, blackCell)
if event.key == K_a :
blackCell = moveRight(gameBoard, blackCell)
if event.key == K_s :
blackCell = moveUp(gameBoard, blackCell)
if event.key == K_w :
blackCell = moveDown(gameBoard, blackCell)
if (isFinished(gameBoard, blackCell)) :
gameBoard[blackCell] = blocks - 1
finish = True
screen.fill(background)
#绘制拼图
for i in range(blocks) :
rowDst = int(i / rows)
colDst = int(i % rows)
rectDst = pygame.Rect(colDst * cellWidth, rowDst * cellHeight, cellWidth, cellHeight)
if gameBoard[i] == -1 :
continue
rowArea = int(gameBoard[i] / rows)
colArea = int(gameBoard[i] % rows)
rectArea = pygame.Rect(colArea * cellWidth, rowArea * cellHeight, cellWidth, cellHeight)
screen.blit(gameImage, rectDst, rectArea)
pygame.display.update()
构造思路:刚起步的时候,取出main.py中的部分代码,进行测试,再进行改进。
3.2 GitHub的代码签入记录:
3.3 遇到的代码模块异常或结对困难及解决方法
- 困难描述:①如何从Postman中导入图片的base64编码,将其转换成对应的图片。②如何识别出原图作为参考图片进行后续拼图。 ③打开图片的时候总是会出现一些错误。 ④AI结题有的时候会崩溃
- 解决尝试:问题①通过网上资料查询比较容易找到代码并进行修改;问题②我们先将打乱的拼图切成九块,取其中一块,判断它是不是空白块或是全黑块,若都不是,则顺序打开文件夹中的图片,进行切块,并依次对比,得到相同的,则是对应的未打乱的图片。 问题③我们选择保存这些后面需要用到的图片,再从文件夹中调用这些图片。
- 问题①③完美解决;问题②判断C和e会出现冲突;问题④的崩溃偶尔还是会发生。
- 收获:问题②的解决思路是从“拼图的每一块都是独一无二的”得到的,将未打乱的图片和打乱的图片都切块,对比得到。学会了怎么从文件夹打开图片,学会了怎么使用Postman的链接。pygame的图片调用跟TK的画布一起使用的话会出现两个窗口,然后会神奇的自动关闭掉一个。。也许是代码打错了(逃)
3.4评价队友
苏哥强就完事了,连着熬了几天夜修改AI的算法,第二天还能正常起床学习。随叫随到然后开始就疯狂讨论,然后讨论出个差不多的思路,继续按照这个思路写代码。
需要改进的地方:我们两人都拖了有点久才开始着手写代码做原型,拖久了很多地方都没时间完成,没时间优化,感觉还有很多地方没做好。年轻人尽量少熬夜~
3.5
PSP表
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 500 | 720 |
· Design Spec | · 生成设计文档 | 30 | 60 |
· Design Review | · 设计复审 | 40 | 50 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
· Design | · 具体设计 | 40 | 60 |
· Coding | · 具体编码 | 360 | 1440 |
· Code Review | · 代码复审 | 30 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 180 | 360 |
Reporting | 报告 | ||
· Test Repor | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
· 合计 | 1380 | 2980 |
学习进度条
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 迷茫的一周 |
2 | 0 | 0 | 6 | 6 | 复习了pygame的用法 |
3 | 200 | 200 | 20 | 26 | 通过练习掌握了pygame的用法,学会了PIL的使用 |
4 | 400 | 600 | 32 | 58 | 熟悉了imagehash的用法,学会了文字的设置和显示 |