Python小项目—照片马赛克

项目内容

该项目用Python创建照片马赛克,将目标图像划分成较小图像的网络,并用适当的图像替换网络中的每一小块,创建原始图像的照片的马赛克
在这里插入图片描述

项目知识点

  • 用Python图像库(PIL)创建图像
  • 计算图像的平均RGB值
  • 剪切图像
  • 通过粘贴另一张图像来替代原图像的一部分
  • 利用平均距离测量来比较RGB值

工作原理

照片马赛克的实质:将一张图像分割成长方形的网络,每个长方形由另一张匹配“目标”的图像替代。
创建照片马赛克,从目标图像的块状低分配率版本开始(图像的分配率决定马赛克的维度MXN)。

  1. 读入一些小块图像,它们将取代原始图像中的小块
  2. 读入目标图像,将它们分隔成M*N的小块网络
  3. 对于每个小块,从输入的小块图像中找到最佳匹配
  4. 将选择的输入图像安排在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代码,一口气执行

附加B——课后练习

在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w要变强

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值