实施照片马赛克
介绍
照片马赛克是分割成矩形网格的图像,每个矩形都替换为与目标匹配的另一个图像(您最终希望出现在照片马赛克中的图像)。换句话说,如果你从远处看一个照片马赛克,你看到的是目标图像;但如果你走近一点,你会发现图像实际上由许多较小的图像组成。这是因为人眼的工作方式。
有两种马赛克,这取决于匹配的方式。在更简单的类型中,目标图像的每个部分都被平均为一种颜色。每个库图像也被简化为单一颜色。然后,目标图像的每一部分都被替换为库中的一个,其中这些颜色尽可能相似。实际上,目标图像的分辨率会降低(通过下采样),然后将每个生成的像素替换为平均颜色与该像素匹配的图像。
在更高级的摄影马赛克中,目标图像没有被下采样,匹配是通过将矩形中的每个像素与每个库图像中的相应像素进行比较来完成的。然后将目标中的矩形替换为最小化总差异的库图像。这需要比简单类型更多的计算,但结果可能要好得多,因为逐像素匹配可以保持目标图像的分辨率。
如何创建照片马赛克?
- 读取平铺图像,这将替换原始图像中的平铺。
- 读取目标图像并将其拆分为 M×N 的瓦片网格。
- 对于每个图块,从输入图像中找到最佳匹配。
- 通过将选定的输入图像排列在 M×N 网格中来创建最终的马赛克。
将图像分割成图块
现在让我们看看如何从这个网格计算单个图块的坐标。索引为 (i, j) 的图块的左上角坐标为 (i*w, i*j),右下角坐标为 ((i+1)*w, (j+1)*h ),其中 w 和 h 分别代表图块的宽度和高度。这些可以与 PIL 一起使用来裁剪并从此图像创建图块。
平均颜色值
图像中的每个像素都有一种颜色,可以用它的红色、绿色和蓝色值来表示。在本例中,您使用的是 8 位图像,因此这些组件中的每一个都具有 [0, 255] 范围内的 8 位值。给定一张总共有 N 个像素的图像,平均 RGB 计算如下:
匹配图像
对于目标图像中的每个图块,您需要从用户指定的输入文件夹中的图像中找到匹配的图像。要确定两个图像是否匹配,请使用平均 RGB 值。最接近的匹配是具有最接近平均 RGB 值的图像。
最简单的方法是计算像素中 RGB 值之间的距离,以找到输入图像之间的最佳匹配。您可以对几何体的 3D 点使用以下距离计算:
现在让我们尝试编写代码
Python3
#Importing the required libraries
import os, random, argparse
from PIL import Image
import imghdr
import numpy as np
def getAverageRGBOld(image):
"""
Given PIL Image, return average value of color as (r, g, b)
"""
# no. of pixels in image
npixels = image.size[0]*image.size[1]
# get colors as [(cnt1, (r1, g1, b1)), ...]
cols = image.getcolors(npixels)
# get [(c1*r1, c1*g1, c1*g2),...]
sumRGB = [(x[0]*x[1][0], x[0]*x[1][1], x[0]*x[1][2]) for x in cols]
# calculate (sum(ci*ri)/np, sum(ci*gi)/np, sum(ci*bi)/np)
# the zip gives us [(c1*r1, c2*r2, ..), (c1*g1, c1*g2,...)...]
avg = tuple([int(sum(x)/npixels) for x in zip(*sumRGB)])
return avg
def getAverageRGB(image):
"""
Given PIL Image, return average value of color as (r, g, b)
"""
# get image as numpy array
im = np.array(image)
# get shape
w,h,d = im.shape
# get average
return tuple(np.average(im.reshape(w*h, d), axis=0))
def splitImage(image, size):
"""
Given Image and dims (rows, cols) returns an m*n list of Images
"""
W, H = image.size[0], image.size[1]
m, n = size
w, h = int(W/n), int(H/m)
# image list
imgs = []
# generate list of dimensions
for j in range(m):
for i in range(n):
# append cropped image
imgs.append(image.crop((i*w, j*h, (i+1)*w, (j+1)*h)))
return imgs
def getImages(imageDir):
"""
given a directory of images, return a list of Images
"""
files = os.listdir(imageDir)
images = []
for file in files:
filePath = os.path.abspath(os.path.join(imageDir, file))
try:
# explicit load so we don't run into resource crunch
fp = open(filePath, "rb")
im = Image.open(fp)
images.append(im)
# force loading image data from file
im.load()
# close the file
fp.close()
except:
# skip
print("Invalid image: %s" % (filePath,))
return images
def getImageFilenames(imageDir):
"""
given a directory of images, return a list of Image file names
"""
files = os.listdir(imageDir)
filenames = []
for file in files:
filePath = os.path.abspath(os.path.join(imageDir, file))
try:
imgType = imghdr.what(filePath)
if imgType:
filenames.append(filePath)
except:
# skip
print("Invalid image: %s" % (filePath,))
return filenames
def getBestMatchIndex(input_avg, avgs):
"""
return index of best Image match based on RGB value distance
"""
# input image average
avg = input_avg
# get the closest RGB value to input, based on x/y/z distance
index = 0
min_index = 0
min_dist = float("inf")
for val in avgs:
dist = ((val[0] - avg[0])*(val[0] - avg[0]) +
(val[1] - avg[1])*(val[1] - avg[1]) +
(val[2] - avg[2])*(val[2] - avg[2]))
if dist < min_dist:
min_dist = dist
min_index = index
index += 1
return min_index
def createImageGrid(images, dims):
"""
Given a list of images and a grid size (m, n), create
a grid of images.
"""
m, n = dims
# sanity check
assert m*n == len(images)
# get max height and width of images
# ie, not assuming they are all equal
width = max([img.size[0] for img in images])
height = max([img.size[1] for img in images])
# create output image
grid_img = Image.new('RGB', (n*width, m*height))
# paste images
for index in range(len(images)):
row = int(index/n)
col = index - n*row
grid_img.paste(images[index], (col*width, row*height))
return grid_img
def createPhotomosaic(target_image, input_images, grid_size,
reuse_images=True):
"""
Creates photomosaic given target and input images.
"""
print('splitting input image...')
# split target image
target_images = splitImage(target_image, grid_size)
print('finding image matches...')
# for each target image, pick one from input
output_images = []
# for user feedback
count = 0
batch_size = int(len(target_images)/10)
# calculate input image averages
avgs = []
for img in input_images:
avgs.append(getAverageRGB(img))
for img in target_images:
# target sub-image average
avg = getAverageRGB(img)
# find match index
match_index = getBestMatchIndex(avg, avgs)
output_images.append(input_images[match_index])
# user feedback
if count > 0 and batch_size > 10 and count % batch_size is 0:
print('processed %d of %d...' %(count, len(target_images)))
count += 1
# remove selected image from input if flag set
if not reuse_images:
input_images.remove(match)
print('creating mosaic...')
# draw mosaic to image
mosaic_image = createImageGrid(output_images, grid_size)
# return mosaic
return mosaic_image
# Gather our code in a main() function
def main():
# Command line args are in sys.argv[1], sys.argv[2] ..
# sys.argv[0] is the script name itself and can be ignored
# parse arguments
parser = argparse.ArgumentParser
(description='Creates a photomosaic from input images')
# add arguments
parser.add_argument('--target-image', dest='target_image', required=True)
parser.add_argument('--input-folder', dest='input_folder', required=True)
parser.add_argument('--grid-size', nargs=2, dest='grid_size', required=True)
parser.add_argument('--output-file', dest='outfile', required=False)
args = parser.parse_args()
###### INPUTS ######
# target image
target_image = Image.open(args.target_image)
# input images
print('reading input folder...')
input_images = getImages(args.input_folder)
# check if any valid input images found
if input_images == []:
print('No input images found in %s. Exiting.' % (args.input_folder, ))
exit()
# shuffle list - to get a more varied output?
random.shuffle(input_images)
# size of grid
grid_size = (int(args.grid_size[0]), int(args.grid_size[1]))
# output
output_filename = 'mosaic.png'
if args.outfile:
output_filename = args.outfile
# re-use any image in input
reuse_images = True
# resize the input to fit original image size?
resize_input = True
##### END INPUTS #####
print('starting photomosaic creation...')
# if images can't be reused, ensure m*n <= num_of_images
if not reuse_images:
if grid_size[0]*grid_size[1] > len(input_images):
print('grid size less than number of images')
exit()
# resizing input
if resize_input:
print('resizing images...')
# for given grid size, compute max dims w,h of tiles
dims = (int(target_image.size[0]/grid_size[1]),
int(target_image.size[1]/grid_size[0]))
print("max tile dims: %s" % (dims,))
# resize
for img in input_images:
img.thumbnail(dims)
# create photomosaic
mosaic_image = createPhotomosaic(target_image, input_images, grid_size,
reuse_images)
# write out mosaic
mosaic_image.save(output_filename, 'PNG')
print("saved output to %s" % (output_filename,))
print('done.')
# Standard boilerplate to call the main() function to begin
# the program.
if __name__ == '__main__':
main()
python test.py --target-image test-data/a.jpg --input-folder test-data/set1/ --grid-size 128 128
输出: