最近处理一些规格不一的照片,需要修改成指定尺寸便于打印。为便于分类排列,还需要能够在照片上显示日期信息。如果只是单独几张照片,完全可以使用Photoshop解决。然而面对成百上千张的照片,照片的日期信息也不尽相同,Photoshop便显得难以胜任了。
考虑到修改图片尺寸也好,添加日期文本到图片上也好,这些对图片的修改任务都是程序式的,完成一次和完成一百次在步骤上并没有什么变化,因此编写脚本程序进行处理无疑是一劳永逸。而Python的标准库以及图像处理库Pillow恰能完美完成本项任务。
这里选择十张大小不一的图片,使用Python+Pillow对整个图片批处理过程进行模拟。主要流程是编写两个方法,一个用于修改尺寸,一个用于为图片添加日期信息,最后在每张图片上都分别应用这两个方法,达到批处理的目的。
1.素材准备
因为要处理大量图片,首要任务是收集待处理图片。对于相机拍摄的照片,可以直接使用原图,这样做有两个优点:一是保证图片在尺寸修改之前不会有画质损失;二是原始图片中包含很多有用的信息,比如有规则的文件名,拍摄时间等,这在本文的为图片添加文本信息部分会详细介绍。
用于本文模拟的图片素材下载自网络,并不包含类似于照片的拍摄信息。不过因为对照片处理的任务只需要使用照片的拍摄日期信息,而恰好日期信息在照片文件名中有所体现,所以完全可以把素材图片文件名修改为和待处理照片相似的格式。待处理照片的文件名格式为“IMG_(datetime)xxxxx”,有用的部信息为“_”之后的拍摄日期。将模拟素材照稍加处理之后,得如下图所示的一些文件:
修改文件名后的图片素材及文件信息
这些素材经微信转发一次,文件名会被修改成固定的格式,再将日期信息稍加修改即得到我们期望的素材。此外,我们也能看到图片的分辨率信息,这些图片的长宽比都不一致,一般来说相机拍摄照片的长宽比更为固定,不过使用这些尺寸变化较大的素材,更能体现出使用代码进行图片处理在应对不同尺寸素材时的灵活性。现在已经得到了模拟处理过程所需的全部素材,接下来便可以逐步对素材进行处理。
2.修改图片尺寸
任何一个图像处理库都会包含一些最基本图像处理操作,其中最常用的为仿射变换,包括旋转,平移,缩放,剪切等。修改一张图片至目标尺寸,缩放与剪切正是我们所需要的。为保持图片的原始比例,并不能够直接将图片变换到目标尺寸,需要在等比例缩放后对图片进行一些修剪。所以我们只需要在图像处理库中找到缩放和修剪图片相关的方法,再进一步代码实现缩放和修剪过程即可。
Pillow是Python中最为流行的图像处理库,支持一系列的图形处理功能。我们使用Pillow完成图片尺寸修剪的需求。查阅Pillow库文档,可以在Image
模组中找到名为resize
的方法,使用这个方法可以调整一张图片的尺寸,最巧妙的是可以在调整尺寸后输出指定区域,因此在参数设置合理的情况下,只需要这一个方法就能够完成我们的全部需求。下面用一张图描绘resize
方法重定义图像尺寸的等效的过程:
重定义图片尺寸过程可视化模拟图
图中所有非红色蒙版区域尺寸均等于目标尺寸。resize
方法对图片尺寸调整的效果相当于流程(1)→(3)→(4)。(1)中目标尺寸大于素材尺寸,所以将素材等比例放大至与目标尺寸等高,再与目标尺寸中心对齐后修建去目标尺寸之外的像素,便得到我们所需要图片。(2)中描绘素材缩放后与目标尺寸左对齐的效果,如果在此基础上修剪冗余部分,图像信息损失会集中在右侧,为使修改后图片相对均衡,本次批处理使用中心对齐并修剪的方案。
需要注意,缩放过程中素材与目标尺寸等高或者等宽,可以保证图片在缩放和剪切后损失最少的信息。至于缩放至等长还是等宽的选择取决于素材尺寸与目标尺寸的相对长宽比,例如目标尺寸相对素材尺寸较宽时--上图对resize
方法效果的模拟情况下--需要缩放至等宽,如果缩放至等长,显然目标尺寸中会有部分区域未被填充,我们并不希望获得一张包含过多空白区域的图片。
了解到resize
方法实现的效果后,只需要为resize
方法提供合适的参数,就可以将一张图片的尺寸灵活地调整到目标尺寸。这些参数的具体计算过程以及对resize
方法的使用封装在文末程序代码的img_resize
方法中,我们只需要提供修改图片的目标尺寸。
3.为图片添加文本信息
为图片添加文本信息的过程比较简单,就是一个将大象装进冰箱的问题:①首先获取欲添加的文本信息 ②把文本添加到待处理图片上。
这里的待处理图片指的是尺寸调整之后达到需求尺寸的图片,只有在图片尺寸确定之后才能够在正确的位置添加文本信息。
在素材准备部分中将素材文件名修改为规则的带有日期信息的格式正是为了方便地获取①步骤中欲添加的日期文本,在实际的批处理中照片文件名也是这种带有日期信息的规则格式。观察文件名,可以发现日期信息从“_”之后开始,由8位数字组成,使用正则表达式可以直接提取这个字符串,再将年、月、日拆分,转换到常用日期格式,便得到待添加的文本。
把文本添加到图片上的操作在常用的办公软件上相当简单,可以直接添加文本框。不过在代码层面上这个操作要复杂一些,在获取文本后还需要将其与待处理图片混合,输出一张完整的图片。在数字图像中,可以将图像分为四个通道:RGBA。RGB三个通道包含图片基本的颜色信息,而A(α)通道携带的则是图片的透明度信息,决定一张图片的透明程度。在图片上添加文本的原理类似于将带有文字的“贴纸”贴在待处理图片上,这张“贴纸”实际上就是一张图片,图片中除文本部分外α通道为全透明,在设置“贴纸”上的文字与文字位置信息后,将“贴纸”与素材图片的α通道混合得到一张新的图片,就实现了图片上文本信息的添加。
使用Pillow库进行代码实现流程与上述基本一致:①创建一张与待处理图片尺寸一致的空图片 ②在这张空图片合适的位置上绘制文本,使用ImageDraw.text
方法,该方法可以直接得到一张“贴纸”形式的图片 ③使用 Image.alpha_composite
方法混合“贴纸”与待处理图片的α通道,得到我们所需要的图片。
到这里我们已经获得了所需要的一张图片,下一步的批处理步骤则不断重复修改图片尺寸和为图片添加文本信息这两个过程,大大提高了处理图片的效率。
4.批量处理
使用python标准库的os
模块,可以方便地获取文件夹中的文件路径,这是我们得以对图片进行批处理的基础:在获取素材图片路径中图片的文件名后,一方面可以对路径下的每张图片进行尺寸修改,另一方面在通过文件名解析日期信息,便可以为图片添加文本。
既是批处理过程,首先需要得知的是需要处理那些素材,我们可以将遍历图片路径得到的文件名储存到一个列表中,再对这些图片进行统一的处理。在具体的代码实现中,获取文件名的过程可以和解析日期的过程同时进行,我们将获得两组信息,一组是文件名,另一组是文件名中包含的日期信息,这两组信息是一一对应的,可以创建一个字典保存这些信息。代码中通过get_pathtime_dict
这个方法实现。
余下的操作就比较清晰了,只需要将上述步骤整合起来,便可完成我们的图片批处理过程:获取待处理的文件名及日期信息字典,遍历这个字典,通过文件名找到图片并修改尺寸,而后将日期信息添加到重定尺寸后的图片上,最后把处理后的图片导出。批量处理后的单张图片效果与批处理后的新图片的文件信息如下:
单张素材原图(左)&处理后的效果图(右)
素材批处理后的文件信息
从两张图中可以看出,日期信息被成功地添加到素材图片当中;而经过批处理后的图片在尺寸上已经完全一致,并且保持了素材图片的矩形较长边的方向。处理前后图片的文件格式由jpg转换为了png,这是由于在添加文本的过程中涉及到α混合,图片保存有α通道信息,可以直接储存为png格式,jpg格式则不包含α通道信息,所以欲保存为jpg格式还需要进一步转换。因为两种图片格式都非常常用,能够适用于各种场景,也就没有将png格式再刻意地转换回jpg格式。
5.总结
使用上述方法对图片进行批量处理,修改一张图片只需要不到一秒钟的时间,这种效率是使用Photoshop等软件无法企及的。此外使用代码进行图片批处理还极具灵活性,如果需要修改图片至不同的尺寸、添加外的信息等,只需简单地修改几处参数。
篇幅原因本文没有介绍一些python使用细节,代码中方法内部的涉及的小算法也没有详细的解析。本文最主要的目的是分析图片批处理过程的整体思路,python是解决这个问题的一个工具,使用其它编程语言同样可以完美完成,不过可能不会有python来得方便。
对于图片的批处理,也可以发挥更多的想象力,完成更高级的任务,比如封装成应用程序的形式、不通过文件名获取文件的生成日期信息、识别图片中的关键点并以此为中心进行缩放等,这些任务都非常有趣,不过既然是追求效率,那就贯彻到底,又快又好的完成我们的需求就足够了。
完整代码:
from PIL import Image, ImageDraw, ImageFont
import os
import re
font = ImageFont.truetype(r'C:\Windows\Fonts\msyh.ttc', size=80)
def get_pathtime_dict(path_root):
raw_paths = os.listdir(path_root)
path_time_dict = {}
pattern = re.compile(r'_(\d{8})\d*')
for path in raw_paths:
if path[-4:] != '.jpg':
continue
timeinfo = pattern.findall(path)
if timeinfo:
label = timeinfo[0][:4]+'/' + \
timeinfo[0][4:6] + '/' + timeinfo[0][6:]
path_time_dict[path] = label
return path_time_dict
def text2img(img, text, font=font):
rgba_img = img.convert('RGBA')
# Create an overlay for the texts
text_overlay = Image.new('RGBA', rgba_img.size, (0, 255, 0, 0))
image_draw = ImageDraw.Draw(text_overlay)
# Set the properties of the texts
text_wdth, text_hgt = image_draw.textsize(text, font=font)
text_sz = (rgba_img.size[0] - text_wdth - 50,
rgba_img.size[1] - text_hgt - 50)
image_draw.text(text_sz, text, font=font, fill=(255, 0, 0, 180))
img_with_text = Image.alpha_composite(rgba_img, text_overlay)
return img_with_text
def img_resize(img, trgtsize):
'''
Zoom an image based on the center of the image to fit the target size, with the redundant section trimmed
:param trgtsize: tatget size for the img normalization
'''
trgtsize = tuple(trgtsize)
img_wdth, img_hgt = img.size
if img_wdth > img_hgt:
trgtsize = (max(trgtsize), min(trgtsize))
else:
trgtsize = (min(trgtsize), max(trgtsize))
target_wdth, target_ght = trgtsize
# Fit the source image to the target size with the redundant part trimmed
center_px = (img_wdth / 2, img_hgt / 2)
zoom_fct = min(img_wdth / target_wdth, img_hgt / target_ght)
box_wdth, box_hgt = target_wdth*zoom_fct, target_ght*zoom_fct
box_start_pt = (int(center_px[0]-box_wdth/2), int(center_px[1]-box_hgt/2))
box_end_pt = (int(center_px[0]+box_wdth/2), int(center_px[1]+box_hgt/2))
resized_img = img.resize(trgtsize, box=(
box_start_pt[0], box_start_pt[1], box_end_pt[0], box_end_pt[1]))
return resized_img
def main():
path_root = r'C:\Users\Windy\Desktop\Python照片批处理\images_we'
path_time_dict = get_pathtime_dict(path_root)
nrml_sz = (1051, 1500)
for path, text in path_time_dict.items():
img_pre = Image.open(
r'C:\Users\Windy\Desktop\Python照片批处理\images_we\{}'.format(path))
img_nrml = img_resize(img_pre, nrml_sz)
img_pos = text2img(img_nrml, text)
img_pos.save( # remove the '.jpg' suffix from an image file's path
r'C:\Users\Windy\Desktop\Python照片批处理\images_we\new\_{}.png'.format(path[:-4]))
print('image {} done!'.format(path))
img_pre.close()
main()
print('OK~')