如何使用PIL从100张图片中获取平均图片?

27
例如,我有100张分辨率相同的图片,我想将它们合并成一张图片。 对于最终的图片,每个像素的RGB值是该位置100张图片的平均值。 我知道在这种情况下getdata函数可以工作,但是否有更简单,更快速的方法在PIL(Python图像库)中实现这一点?

1
你考虑过同时使用NumPy吗? - Ignacio Vazquez-Abrams
@SteveBarnes的回答https://dev59.com/B2Qm5IYBdhLWcg3w2R9J#17291771在我看来是最好的。构建大量图像列表是一种不必要的、巨大的、耗费时间的内存浪费。 - Mark Setchell
6个回答

52

假设你的所有图像都是 .png 文件,并且它们都存储在当前工作目录中。下面的 Python 代码会完成你想要的事情。正如Ignacio所建议的那样,在这里使用numpy和PIL是关键。当构建平均像素强度时,你只需要小心在整数数组和浮点数数组之间切换。

import os, numpy, PIL
from PIL import Image

# Access all PNG files in directory
allfiles=os.listdir(os.getcwd())
imlist=[filename for filename in allfiles if  filename[-4:] in [".png",".PNG"]]

# Assuming all images are the same size, get dimensions of first image
w,h=Image.open(imlist[0]).size
N=len(imlist)

# Create a numpy array of floats to store the average (assume RGB images)
arr=numpy.zeros((h,w,3),numpy.float)

# Build up average pixel intensities, casting each image as an array of floats
for im in imlist:
    imarr=numpy.array(Image.open(im),dtype=numpy.float)
    arr=arr+imarr/N

# Round values in array and cast as 8-bit integer
arr=numpy.array(numpy.round(arr),dtype=numpy.uint8)

# Generate, save and preview final image
out=Image.fromarray(arr,mode="RGB")
out.save("Average.png")
out.show()

上面的图像是使用上述代码从一系列高清视频帧生成的。

高清视频帧的平均值


1
将图像转换为数字数组似乎有些粗糙。特别是因为这是一个相当标准的图像处理操作。PIL/Pillow没有本地功能来使用不同混合模式叠加图像吗? - Matt
26
这个意思其实也就是图像的定义。 - Chris Jones
如果图像是调色板格式,代码有可能会抛出错误。不过,问题可以很简单地得到解决: https://stackoverflow.com/questions/60681654/cannot-convert-pil-image-from-paint-net-to-numpy-array - user6276743
通过整数求和后进行一次最终除法运算,可以实现更快速、更精确的平均值计算。 - Jan Heldal
哈哈,@Matt,如果不用整数表示RGB值,你是如何表示光栅图像的呢? - Virgiliu
浮点数是一种选择,但那不是我认为很“恶心”的方式。让人感到“恶心”的是需要将图像对象转换为矩阵数学对象以执行图像操作。这就像将字符串转换为字节数组,然后进行字节运算以将所有字母转换为大写一样荒谬。是的,从底层来看,最终就是这么做的,但我认为“image.alphaOver()”不存在和“string.toUpper()”不存在一样愚蠢。 - Matt

14

我觉得很难想象在这里会出现内存问题,但是如果你绝对不能够负担起创建所需的浮点数数组(参见原回答),你可以使用PIL的混合函数,如@mHurley建议的那样(点击查看)

# Alternative method using PIL blend function
avg=Image.open(imlist[0])
for i in xrange(1,N):
    img=Image.open(imlist[i])
    avg=Image.blend(avg,img,1.0/float(i+1))
avg.save("Blend.png")
avg.show()

你可以从PIL混合函数的定义开始,推导出正确的alpha值序列:
out = image1 * (1.0 - alpha) + image2 * alpha

考虑将该函数递归应用于数字向量(而不是图像),以获得向量的平均值。对于长度为N的向量,您需要N-1个混合操作,使用N-1个不同的alpha值。
但是,直观地思考这些操作可能更容易。在每一步中,您希望平均图像包含先前步骤的源图像的相等比例。在混合第一个和第二个源图像时,alpha应为1/2,以确保相等的比例。在将第三个图像与前两个的平均值混合时,您希望新图像由第三个图像的1/3组成,其余部分由先前图像的平均值(当前avg值)组成,依此类推。
原则上,基于混合的新答案应该很好。但是我不知道混合函数的工作原理。这让我担心每次迭代后像素值是如何舍入的。
下面的图像是使用我的原始答案中的代码从288个源图像生成的:

Averaged, original answer

另一方面,这张图片是通过反复应用PIL的混合功能到相同的288张图像上生成的:

Blended, using Image.blend

我希望您能够看到这两个算法的输出明显不同。我认为这是由于在重复应用Image.blend时累积了小的舍入误差所致。
我强烈推荐我的原始答案,而不是这个替代方案。

8

可以使用numpy的平均函数进行平均。代码看起来更好,运行速度更快。

这里是对700张嘈杂的灰度人脸图像进行计时和结果比较:

def average_img_1(imlist):
    # Assuming all images are the same size, get dimensions of first image
    w,h=Image.open(imlist[0]).size
    N=len(imlist)

    # Create a numpy array of floats to store the average (assume RGB images)
    arr=np.zeros((h,w),np.float)

    # Build up average pixel intensities, casting each image as an array of floats
    for im in imlist:
        imarr=np.array(Image.open(im),dtype=np.float)
        arr=arr+imarr/N
    out = Image.fromarray(arr)
    return out

def average_img_2(imlist):
    # Alternative method using PIL blend function
    N = len(imlist)
    avg=Image.open(imlist[0])
    for i in xrange(1,N):
        img=Image.open(imlist[i])
        avg=Image.blend(avg,img,1.0/float(i+1))
    return avg

def average_img_3(imlist):
    # Alternative method using numpy mean function
    images = np.array([np.array(Image.open(fname)) for fname in imlist])
    arr = np.array(np.mean(images, axis=(0)), dtype=np.uint8)
    out = Image.fromarray(arr)
    return out

average_img_1() 
100 loops, best of 3: 362 ms per loop

average_img_2()
100 loops, best of 3: 340 ms per loop

average_img_3()
100 loops, best of 3: 311 ms per loop

顺便说一下,取平均值的结果相当不同。我认为第一种方法在取平均值时丢失了信息。而第二种方法则有一些人工制品。

average_img_1

enter image description here

平均图像_2

enter image description here

平均图像_3

enter image description here


1
这些图片看起来非常有趣!我很惊讶img_1和img_3看起来如此不同。需要注意的是,使用numpy.mean的img_3方法需要一次性将所有数据加载到内存中,这可能会对大批量的图像造成很大问题。如果原始数据是公开可用的,我很想弄清楚img_1和img_3之间的差异。 - CnrL

5

如果有人对numpy的蓝图解决方案感兴趣(我实际上正在寻找这个),这里是代码:

mean_frame = np.mean(([frame for frame in frames]), axis=0)

3

我建议创建一个x乘y的整数数组,从(0, 0, 0)开始,然后对于每个文件中的每个像素,将RGB值添加到数组中,除以图像数量,然后从其中创建图像 - 您可能会发现numpy很有帮助。


1
这是目前为止最好的答案,超出其他答案不少。混合图像不准确而且慢。在内存中构建所有图像的大型列表会极大浪费RAM,尤其是涉及100多张图片时。将图像汇总为一个聚合图像,然后除以图像数量。 - Mark Setchell

1
我尝试采用被接受的方法时遇到了内存错误。我找到了一种优化方式,似乎能够产生相同的结果。基本上,您需要逐个混合每个图像,而不是将它们全部加起来再除以数量。
N=len(images_to_blend)
avg = Image.open(images_to_blend[0])
for im in images_to_blend: #assuming your list is filenames, not images
    img = Image.open(im)
    avg = Image.blend(avg, img, 1/N)
avg.save(blah)

这样做有两个好处,一是在将图片转化为数组时,可以避免拥有两个非常密集的图像副本,二是完全不需要使用64位浮点数。你可以获得相似高精度,但数字更小。结果看起来是一样的,虽然我希望有人能检查我的计算。

1
很遗憾,这是不正确的。要使这样的方案起作用,您必须在每个步骤中使用不同的不透明度混合。即使您正确计算了不透明度向量,每个步骤也会存在舍入误差。 - CnrL
谢谢!你能带我走一遍这个数学问题吗?我总是对这种事情感到困难...我认为解决方案可能是“对于n从1到n,混合量= 1 / n”。我不太担心舍入误差,而是担心MemoryError会完全阻止其他算法运行。 - Matt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接