要么改变世界,要么适应世界

借助OpenCV拼接特定背景图

2022-01-21 10:25:22
53
目录

前言

之前就一直有一个想法,提供大量图片以及一张背景图,然后将这些图片组成一个个小网格拼接起来,然后效果和原背景图差不多,简而言之就是将原背景图马赛克化,只不过每一个马赛克是用提供的图片组成,现在放寒假了终于有时间开干啦,哈哈哈哈哈。

先看下效果:

原背景图

拼接结果图

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)
历史评论
开始评论