借助OpenCV拼接特定背景图
前言
之前就一直有一个想法,提供大量图片以及一张背景图,然后将这些图片组成一个个小网格拼接起来,然后效果和原背景图差不多,简而言之就是将原背景图马赛克化,只不过每一个马赛克是用提供的图片组成,现在放寒假了终于有时间开干啦,哈哈哈哈哈。
先看下效果:
原背景图
拼接结果图
emmm,勉强能看,有点抽象
只能看缩略图了
不过只要你电脑配置够好,时间也充足,可以将图片分割成更多的小方格,效果也更佳。
基本原理
基本原理其实很简单:就是首先计算每一张素材图片的颜色平均值,然后计算背景图中的每一个小方格的颜色平均值,然后两两匹配就行了。
下面说一下具体操作
对于素材,也就是上面所提到的“大量图片”,我觉得最快速的获取方式就是视频,毕竟一个视频里边每一帧都可以作为一张图片,如果视频是24
帧每秒,一分钟就可以提供24*60=1440
张图片,对于我这个一年拍的生活照都不超过200张的蓝人来说,1440
难以想象。但是对于视频来说,每一帧都分析的话,花费时间太多,并且其实有一些邻近帧的平均颜色相差不多,为了加快分析可以取一定间隔进行分析,OpenCV
也提供了提取指定位置的帧的方法:
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
但是这个性能上面比较慢,我们可以顺序读取每一帧,然后根据间隔再分析:
# 视频总帧数
frameNum = cap.get(7)
# 提取帧数的步长
step = frameNum // needNum
cnt, idx = 0, 0
imgList = []
while (True):
ret, frame = cap.read()
if ret:
print('\r' + '-' * (int(idx / frameNum * 100)) + str(int(idx / frameNum * 100)) + '%', end='', flush=True)
if idx % step == 0:
aver = frame.mean(axis=0).mean(axis=0)
averHex = BGR2HEX(aver)
# 先将暂存帧保存起来
cv2.imwrite(PICTURES_DIR + 'cache-' + str(cnt) + '.jpg', frame)
imgList.append([averHex, PICTURES_DIR + 'cache-' + str(cnt) + '.jpg'])
cnt += 1
idx += 1
else:
break
imgList.sort(key=lambda x: x[0])
return imgList
经过实测,花费时间减少到原来方法的一半!
对imgList
进行排序是方便后面匹配的时候使用二分查找。
程序也支持自己提供素材图片,不过自己能提供几千张图片以上估计很难吧??
然后进行背景图的分割,理论上背景图分割得越精细越好,但是如此一来需要匹配的次数就越多,程序运行时间越多,而且拼接出来的图片也很大,但是分割数目太少,拼接出来的图片与原图相差甚远,因此要挑选好要切割的行数和列数。
匹配过程比较好理解,我们实现把背景图切割成m*n
个小宫格,计算每一个小宫格的平均颜色,然后遍历每一个小宫格,在imgList
中使用二分查找与之相近的图片,并拿到该图片的名字再读取进来(我们并不在imgList
中保存每一张图片,因为如果我们可能有几千到上万的图片,如果每一张图都有1MB
大小,那我们就需要好几个GB
的内存,而现在我们imgList
中每一个元素都是包含了该图片的名字和平均颜色值), 而OpenCV
读取的图片返回值是矩阵,图片的拼接也就是对应矩阵的拼接,不过要注意的是区分按列拼接和按行拼接。
当然了为了防止输出的文件过大,拼接过程还应该将素材图片进行缩放。
代码实现
'''
Author : YaleXin
Email :
LastEditors : YaleXin
'''
import os
import cv2
import numpy as np
import bisect
import time
# 视频链接
VIDEO_URL = "data/mouseAndCat.mp4"
# 素材图片文件夹
PICTURES_DIR = "materialPics/"
# 背景图
BACKGROUND_URL = "bgr.jpg"
# 输出图片结果名
OUTPUT_PICTURE = "result.jpg"
# 缩放比例
ZOOM_FACTOR = 4
# 切割图片的行数、列数
ROW, COL = 41, 41
cap = cv2.VideoCapture(VIDEO_URL)
# 将指定图片进行裁剪 m n 分别是行数列数
def cutBackgroundImg(img, m=21, n=21):
# 背景图片的高度和宽度
h, w = img.shape[0], img.shape[1]
grid_h = int(h * 1.0 / (m - 1) + 0.5)
grid_w = int(w * 1.0 / (n - 1) + 0.5)
# 满足整除关系时的高、宽
h = grid_h * (m - 1)
w = grid_w * (n - 1)
# 图像缩放
img_re = cv2.resize(img, (w, h),
cv2.INTER_LINEAR) # 也可以用img_re=skimage.transform.resize(img, (h,w)).astype(np.uint8)
gx, gy = np.meshgrid(np.linspace(0, w, n), np.linspace(0, h, m))
gx = gx.astype(np.int)
gy = gy.astype(np.int)
# 前面两维代表位置 i行j列 后三维代表图片信息(像素位置、颜色信息)
divide_image = np.zeros([m - 1, n - 1, grid_h, grid_w, 3],
np.uint8)
for i in range(m - 1):
for j in range(n - 1):
divide_image[i, j, ...] = img_re[
gy[i][j]:gy[i + 1][j + 1], gx[i][j]:gx[i + 1][j + 1], :]
return divide_image
pass
# 分析分割后的图片 divideImages 是背景图分割后的一系列图片 materialIndexColor是包含颜色均值的一些列图片(从视频中提取或者用户提供)
def handleBlocks(divideImages, materialIndexColor):
print('--- handleBlocks begin ---')
beginTime = time.time()
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
res0, frame0 = cap.read()
originX, originY = frame0.shape[0:2]
sortedAverColor = np.array(materialIndexColor)[:, 0].astype("int32")
m, n = divideImages.shape[0], divideImages.shape[1]
cnt, total, listLen = 0, (m + 1) * (n + 1), len(materialIndexColor)
resImg = np.array([])
for i in range(m):
rowItem = np.array([])
for j in range(n):
aver = divideImages[i, j].mean(axis=0).mean(axis=0)
averHex = BGR2HEX(aver)
# 在有序序列中寻找接近该 块(来自背景图) 的图片
left = bisect.bisect_left(sortedAverColor, averHex)
left = max(0, left)
left = min(left, listLen - 1)
imgFilename = materialIndexColor[left][1]
frame = cv2.imread(imgFilename)
if rowItem.shape == (0,):
# 拼接列
rowItem = cv2.resize(frame, (originY // ZOOM_FACTOR, originX // ZOOM_FACTOR))
else:
rowItem = np.concatenate((rowItem, cv2.resize(frame, (originY // ZOOM_FACTOR, originX // ZOOM_FACTOR))),
1)
print('\r' + '+' * (int(cnt / total * 100)) + str(int(cnt / total * 100)) + '%', end='', flush=True)
cnt += 1
if resImg.shape == (0,):
resImg = rowItem
else:
# 拼接行
resImg = np.concatenate((resImg, rowItem))
cv2.imwrite(OUTPUT_PICTURE, resImg)
cv2.destroyAllWindows()
# 删除中间生成的文件
for item in materialIndexColor:
os.remove(item[1])
endTime = time.time()
print('\n---- handleMaterial2 finished and cost {} --------'.format(endTime - beginTime))
print('\n--- handleBlocks end ---')
# 将BGR数组转为 RGB16进制形式
def BGR2HEX(BGR):
return (int(BGR[2]) << 16) + (int(BGR[1]) << 8) + int(BGR[0])
def handleMaterial(needNum=500):
print('--- handleMaterial begin ---')
# 视频总帧数
frameNum = cap.get(7)
# 提取帧数的步长
step = frameNum / needNum
idx = 0
imgList = []
# 按照步长分析每一帧
for cnt in range(needNum):
print('\r' + '-' * (cnt * 100 // needNum) + str(cnt * 100 // needNum) + '%', end='', flush=True)
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
res, frame = cap.read()
aver = frame.mean(axis=0).mean(axis=0)
averHex = BGR2HEX(aver)
# cnt 是标记该元素在imgList中的下标,idx是标记该帧在原视频中的下标
imgList.append([cnt, averHex, idx])
idx += step
print('\n---- handleMaterial finished! --------')
imgList.sort(key=lambda x: x[1])
return imgList
def handlePictures():
print('--- handlePictures begin ---')
imgList = []
if os.path.exists(PICTURES_DIR):
imgsFiles = os.listdir(PICTURES_DIR)
fileLen = len(imgsFiles)
cnt = 0
for file in imgsFiles:
fileType = os.path.splitext(file)[1]
# 常见的图片格式
if fileType == '.jpg' or fileType == '.jpeg' or fileType == '.png' or fileType == '.bmp' or fileType == '.svg':
img = cv2.imread(PICTURES_DIR + file)
# 判断图片是否读取成功
if isinstance(img, np.ndarray):
aver = img.mean(axis=0).mean(axis=0)
averHex = BGR2HEX(aver)
cv2.imwrite(PICTURES_DIR + 'cache-' + file, img)
imgList.append([averHex, PICTURES_DIR + 'cache-' + file])
print('\r' + '+' * (int(cnt / fileLen * 100)) + str(int(cnt / fileLen * 100)) + '%', end='',
flush=True)
cnt += 1
else:
print('this path not exist')
imgList.sort(key=lambda x: x[0])
print('\n--- handlePictures end ---')
return imgList
def handleVideoMaterial(needNum):
print('--- handleMaterial2 begin ---')
beginTime = time.time()
# 视频总帧数
frameNum = cap.get(7)
# 提取帧数的步长
step = frameNum // needNum
cnt, idx = 0, 0
imgList = []
while (True):
ret, frame = cap.read()
if ret:
print('\r' + '-' * (int(idx / frameNum * 100)) + str(int(idx / frameNum * 100)) + '%', end='', flush=True)
if idx % step == 0:
aver = frame.mean(axis=0).mean(axis=0)
averHex = BGR2HEX(aver)
cv2.imwrite(PICTURES_DIR + 'cache-' + str(cnt) + '.jpg', frame)
imgList.append([averHex, PICTURES_DIR + 'cache-' + str(cnt) + '.jpg'])
cnt += 1
idx += 1
else:
break
endTime = time.time()
print('\n---- handleMaterial2 finished! way 2 cost {} --------'.format(endTime - beginTime))
imgList.sort(key=lambda x: x[0])
return imgList
if __name__ == '__main__':
img = cv2.imread(BACKGROUND_URL)
way = int(input('请选择你的方式:1.从视频中截取 2.从含有图片的文件夹中提取'))
if way == 1:
divideImages = cutBackgroundImg(img, m=ROW, n=COL)
materialList = handleVideoMaterial(needNum=3000)
handleBlocks(divideImages, materialList)
elif way == 2:
divideImages = cutBackgroundImg(img, m=ROW, n=COL)
materialList = handlePictures()
handleBlocks(divideImages, materialList)
本文由「黄阿信」创作,创作不易,请多支持。
如果您觉得本文写得不错,那就点一下「赞赏」请我喝杯咖啡~
商业转载请联系作者获得授权,非商业转载请附上原文出处及本链接。
关注公众号,获取最新动态!