图片分割采用先填充在分割的方法,利用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 # 返回最小代价对应的交换方案
最后有无解的判断就是逆序数对数量的判断,这里就不细说。
强制交换,我的思路是先判断再强制交换的步数前是否解完了,如果没有则进行强制交换同时判断是否有解,无解则对各种交换方式计算移动代价,最后取最小。
|