项目内容
该项目用Python创建照片马赛克,将目标图像划分成较小图像的网络,并用适当的图像替换网络中的每一小块,创建原始图像的照片的马赛克
项目知识点
- 用Python图像库(PIL)创建图像
- 计算图像的平均RGB值
- 剪切图像
- 通过粘贴另一张图像来替代原图像的一部分
- 利用平均距离测量来比较RGB值
工作原理
照片马赛克的实质:将一张图像分割成长方形的网络,每个长方形由另一张匹配“目标”的图像替代。
创建照片马赛克,从目标图像的块状低分配率版本开始(图像的分配率决定马赛克的维度MXN)。
- 读入一些小块图像,它们将取代原始图像中的小块
- 读入目标图像,将它们分隔成M*N的小块网络
- 对于每个小块,从输入的小块图像中找到最佳匹配
- 将选择的输入图像安排在M*N的网络中,创建最终的照片马赛克
1.分割目标图像
- x 轴表示网格的列,y 轴表示网格的行。
- 下标为 (i,j) 的小块,左上角坐标为 (iw,ij) ,右下角坐标为 ((i+1)∗w,(j+1)∗h)
- w 和 h 分别是小块的宽度和高度
2.平均颜色值
图像中的每个像素颜色由红、绿、蓝值表示。若一幅图像共有N个像素,平均RGB(三元组):
计算平均RGB目的:匹配图像小块和目标图像
3.匹配图像
匹配图像:对于目标图像的每个小块,需要在用户指定的输入文件夹的一些图像中,找到一副匹配图像
确定两图像是否匹配:最接近的匹配就是最接近平均的RGB值的图像,故需计算一个像素中的RGB值之间的距离
(三维图像两点之间的距离:)
项目code
- 读取小块图像
#os.listdir()方法将imageDir目录中的文件放入一个列表
#os.path()和os.path.join()获取图像的完整文件名
def getImages(imageDir) :
"""
given a directory of images, return a list of Images
"""
files = os.listdir(imageDir) #os.listdir()方法将imageDir目录中的文件放入一个列表
images = []
for file in files : #遍历列表中的每个文件
filePath = os.path.abspath(os.path.join(imageDir, file)) #os.path()和os.path.join()获取图像的完整文件名
try :
# explicit load so we don't run into resource crunch
fp = open(filePath, "rb") #打开每个小块图像
im = Image.open(fp) # Image.open()方法将文件句柄fp传入PIL()
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 getAverageRGB(image) :
"""
Given PIL Image, return average value of color as (r, g, b)
"""
# get image as numpy array
im = np.array(image) #用nunmpy将每个Image对象转换为数据数组,返回的numpy数组形为(w,h,d)对应(R,G,B)
# get shape
w, h, d = im.shape #保存shape元组,然后计算平均值,reshape():将数组形式变为形状(w * h, d)
# get average
return tuple(np.average(im.reshape(w * h, d), axis=0)) #average():计算RGB平均值
- 将目标图像分割成网络
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)))#image.crop():分割并将每一小块保存为单独的图像
return imgs
- 寻找小块的最佳匹配
#img.paste(images[index], (col * width, row * height)):第一个参数要粘贴的对象,第二个参数左上角的坐标
def getBestMatchIndex(input_avg, avgs) :# input_avg:最匹配平均RGB值;avgs:小块图像平均值的RGB列表
"""
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#最接近匹配下标初始化为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
- 创建图像网络
#img.paste(images[index], (col * width, row * height)):第一个参数要粘贴的对象,第二个参数左上角的坐标
def createImageGrid(images, dims) :
"""
Given a list of images and a grid size (m, n), create
a grid of images.
"""
m, n = dims #取得图像尺寸大小,并用assert检车提供给的图像的数量是否符合网络的大小
# sanity check
assert m * n == len(images) # assert():检查代码中的假定
# 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)) #创建一个空的Image,大小符合网络中的所有图像
# paste images
for index in range(len(images)) :#遍历选定的图像(小块在图像网络 中的下表有N*rol+col给出)
row = int(index / n)#行
col = index - n * row#列
grid_img.paste(images[index], (col * width, row * height))#img.paste():粘贴到相应的网络中
return grid_img
- 创建照片马赛克
#createPhotomosaic(target_image, input_images, grid_size,
reuse_images=True)
参数:目标图像、输入图像列表、生成照片马赛克的大、表明图像是否可以复用的标志
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)# batch_size设置为小块总数的1/10
# calculate input image averages
avgs = []
for img in input_images :#为文件夹中的每个图像计算平均RGB值,并保存在 avgs 列表中
avgs.append(getAverageRGB(img))
for img in target_images :#迭代目标图像网络中的平均RGB值,并将值保存在avg
# target sub-image average
avg = getAverageRGB(img)
# find match index
#在输入图像的平均值列表中寻找最佳匹配
match_index = getBestMatchIndex(avg, avgs)#返回的是个下标
output_images.append(input_images[match_index])#取Images对象并保存在列表
# 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
- 添加命令行选项
#此段代码包含3个必要的命令行参数:目标图像的名称、输入文件夹的名称、网络的尺寸//4:可选的文件名
# 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)
- 控制照片的马赛克大小
#若基于目标图像中匹配的小块,盲目将图像粘在一起,得到巨大的照片马赛克,比目标图像大得多,故需要控制照片的马赛克大小
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) #thumbnail()调整图像
完整代码
"""
photomosaic.py
Creates a photomosaic given a target image and a folder of input images
Author: Mahesh Venkitachalam
"""
import sys, 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__':
实验素材
- 本实验包括一个 .py 文件和若干图片组成
- 所有的图片都放置在 test-data 文件夹下
- test-data/a.jpg是目标图像
- test-data/set1/ 文件夹下存放的是小块图像
实验效果
运行照片马赛克生成程序
$ python photomosaic.py --target-image test-data/a.jpg --input-folder test-data/set1/ --grid-size 128 128
效果图
原图:
效果图
参考资料
《Python极客项目编程》
github项目代码
实验楼Python创建照片马赛克
NeroChang
附加A——python两种编程方式:
-
交互式编程——写一行python语句马上运行一行并显示效果
-
脚本式编程——现在相关的文本文件中,写好相关的python代码,一口气执行