「论一个随机图片api的养成计划」
前段时间,书樱用Python稍微抓取了 Pixiv 的图片,总共大概有三万张左右。因为爬取时的爬虫只负责下载了,出现了很多重复的图片以及图片缩略图。最近想搞个图片api,正好可以用上这些二次元图片,所以这个系列暂且叫做「论一个随机图片api的养成计划,于是便有了这篇文章。
系列文章合集 -> 「论一个随机图片api的养成计划」
如果您觉得我写的东西对您有所帮助的话,不妨请我喝杯咖啡。=> 赞助
序言
这是这个系列文章的第一篇,因为下一篇涉及的内容会非常多,信息量太大(完全不是因为想水文章),今天把相对简单的内容单独列成一篇,作为开胃小菜。
话不宜迟,进入正题。
分类
思路
图片分类,顾名思义,就是根据各自在图像信息中所反映的不同特征,把不同类别的目标区分开来的图像处理方法。它利用计算机对图像进行定量分析,把图像或图像中的每个像元或区域划归为若干个类别中的某一种,以代替人的视觉判读。[1] 所以,自然而然,能供我们分类的信息也有很多。
有机会用DL写一个识别天依的程序
当然,和广泛定义中的图片分类不一样,这将近三万张图片的分类呢,书樱只想按照长宽比来分类,原因很简单。首先,因为都是来自 Pixiv 的图片,所以不需要对图片内容进行分类。其次,不同设备屏幕长宽比不同,所需要显示图片的长宽比也不同,书樱希望在 api 中设置一个参数,来规范图片返回的长宽比,以适应不同的设备。因此,按尺寸分类图片也就顺理成章了。
算法
很好,现在想法有了,让我们来构思下这个小程序。
- 首先,我们需要从一个文件夹里读取出图片。
- 接着,读取出图片的长宽信息并计算出长宽比。
- 然后,再按照长宽比对图片进行分类。
- 最后,把分类好的图片转移到新文件夹,按照长宽比分类。
代码
下面是根据我们想法写代码。
分类图片中,书樱使用到了os
、shutil
和Opencv/Pillow
这两(三)个模块。
os
(多种操作系统接口)这个模块不用多说,os
提供了非常丰富的方法用来处理文件和目录,功能可以说是非常强大。[2]
shutil
(高阶文件操作)模块提供了一系列对文件和文件集合的高阶操作。特别是提供了一些支持文件拷贝和删除的函数。[3]
关于Opencv
和Pillow (PIL)
,这里我选择的是Opencv
,因为Opencv
在各方面速度都比PIL
要快上不少,对于庞大数量的图片,这可以给我们省下很多时间,不过对于单线程读取来说,瓶颈在这,当然我也留下PIL
的代码。[4][5]
Opencv
和PIL
本身就是非常强大的媒体处理库,这方面的内容可谓是相当丰富,特别是在深度学习方面。书樱在这里也只是使用了一些非常简单的功能,只是“冰山半角”,如果有兴趣的朋友可以Google搜索相关文档阅读。
下面是导入 Python 模块,import cv2
是导入Opencv
模块。如果是使用PIL
,因为我们使用的函数在Image
中,则需要from PIL import Image
从PIL
库中导入我们需要的Image
模块(可选)。
# 导入模块
import os
import cv2
import shutil
# from PIL import Image
请根据你使用的图片处理库导入相应的模块。
导入了我们所需要的模块之后,是时候来处理图片了。
首先,我们需要让Python从文件夹中读取图片。
# 修改这里的路径
path = 'path\to\pic'
# 对于 path 里的每一个 file 文件
for file in os.listdir(path):
print(file)
os.chdir(path) # 此处的os.chdir()是必要的
我们使用os.listdir()
这个函数,传递一个路径path
,程序将会依次输出当前目录下的所有文件(夹)。
[mdx_fold title=提示 open=false]对于os.listdir()本身是无法区分文件和文件夹的,必须使用os.path.isdir()判断是否为文件夹或使用os.path.isfile()判断是否是文件。为了代码的简便,建议您将待分类的图片单独放在一个文件夹中。[/collapse]
首先,定义一个path
,用来存储我们待分类的图片的路径。
# 修改这里的路径,此路径为绝对路径
path = 'path\to\pic'
利用for
可以依次将读取出文件名存入file
中
# 对于 path 里的每一个 file 文件
for file in os.listdir(path):
print(file)
os.chdir(path) # 此处的os.chdir()是必要的
我们先打印出文件名,以便让我们知道系统当前处理的文件。
我们使用了os.chdir()
改变当前Python的工作目录,此处的os.chdir()
是必要的,原因我会在后文讲到。
注意:os.chdir()
中的路径必须存在,否则会丢出FileNotFoundError
错误。
有了文件名,现在可以让Opencv读取图片了。
# 修改这里的路径
path = 'path\to\pic'
# 对于 path 里的每一个 file 文件
for file in os.listdir(path):
print(file)
os.chdir(path) # 此处的os.chdir()是必要的
im = cv2.imread(file) # Opencv代码
h, w = im.shape[0:2]
# im = Image.open(file) # Pillow代码
# h, w = im.size[0:2]
使用cv2.imread(file)
让Opencv读取图片为对象并保存到im
中。
im.shape[0:2]
读取出该文件的图片信息,并返回一个元组。我们将返回的数据存入h,w变量中。
[mdx_fold title=cv2.shape open=false]cv.shape操作返回的是一个元组,(h, w, c),三个参数分别是图片的高度、图片的宽度、颜色通道数,所以在这里我们只需要前两个参数即可[/collapse]
对于PIL
,只需要把cv2.imread()
改成Image.open()
,im.shape
改成im.size
。原理不变。
接着是计算长宽比。所谓长宽比,即一个影像的宽度除以高度的比例。[6]
所以,很自然的,长宽比就是w/h
。不过大部分长宽比都是无限小数,所以书樱用了round()
函数四舍五入,此函数基本用法为:round (数字,保留位数)
。
当然round()
保留之后输出的是浮点数,所以还需要用str()
转化成字符串。
# 修改这里的路径
path = 'path\to\pic'
# 对于 path 里的每一个 file 文件
for file in os.listdir(path):
print(file)
os.chdir(path) # 此处的os.chdir()是必要的
im = cv2.imread(file) # Opencv代码
h, w = im.shape[0:2]
# im = Image.open(file) # Pillow代码
# h, w = im.size[0:2]
size = str(round(w/h, 1)) # 计算长宽比并保留一位小数
对于分类好的图片,我们需要让 Python 自动创建文件夹并把图片复制进来。
所以我们定义一个函数mkdir()
,传递一个参数name
作为文件夹名,让Python自动创建文件夹。
这里书樱直接使用图片长宽比size
做为文件夹名。
os.path.exists()
用于判断路径是否存在并返回布尔值。
def mkdir(name):
# 修改这里的路径,为保存分类后图片的路径
os.chdir('path\to\save\pic')
if os.path.exists(name):
os.chdir(name)
else:
os.mkdir(name)
os.chdir(name)
我们先用os.chdir()
切换到我们保存图片的路径,请修改此路径为你自己的路径。
如果传入的name
目录存在,我们则直接切换到该路径下;如果传入的name
路径不存在,Python将会使用os.mkdir()
创建名为name
的文件夹,并用os.chdir()
切换到该路径下。
函数创建好了,我们直接调用并创建一个名字为size的文件夹,不过注意此函数要在程序入口之前定义。
# 修改这里的路径
path = 'path\to\pic'
# 对于 path 里的每一个 file 文件
for file in os.listdir(path):
print(file)
os.chdir(path) # 此处的os.chdir()是必要的
im = cv2.imread(file) # Opencv代码
h, w = im.shape[0:2]
# im = Image.open(file) # Pillow代码
# h, w = im.size[0:2]
size = str(round(w/h, 1)) # 计算并保留一位小数
mkdir(size)
最后则将图片按长宽比分类,这里使用的是shutil.copy()
复制图片,此函数的用法是shutil.copy(路径前, 路径后)
,把copy
方法替换成move
则是移动此图片。
import shutil
import os
import cv2
# 修改这里的路径
path = 'path\to\pic'
# 对于 path 里的每一个 file 文件
for file in os.listdir(path):
print(file)
os.chdir(path) # 此处的os.chdir()是必要的
im = cv2.imread(file) # Opencv代码
h, w = im.shape[0:2]
# im = Image.open(file) # Pillow代码
# h, w = im.size[0:2]
size = str(round(w/h, 1)) # 计算并保留一位小数
mkdir(size)
shutil.copy(path + '\\' + file, file)
# 当操作为移动是此处为shutil.move
大功告成!程序会在目标文件夹下生成一系列文件夹。
下面是完整的代码
# 导入模块
# from PIL import Image
def mkdir(name):
# 修改这里的路径,为保存分类后图片的路径
os.chdir('path\to\save\pic')
if os.path.exists(name):
os.chdir(name)
else:
os.mkdir(name)
os.chdir(name)
path = 'path\to\pic'
# 对于 path 里的每一个 file 文件
for file in os.listdir(path):
print(file)
os.chdir(path) # 此处的os.chdir()是必要的
im = cv2.imread(file) # Opencv代码
h, w = im.shape[0:2]
# im = Image.open(file) # Pillow代码
# h, w = im.size[0:2]
size = str(round(w/h, 1)) # 计算并保留一位小数
mkdir(size)
shutil.copy(path + '\\' + file, file)
# 当操作为移动是此处为shutil.move
测试
关于PIL
和Opencv
对于图片的处理速度,书樱做了个简单的测试,此为测试图片,来自Pixiv,画师id:418969,作品:#VOCALOIDCHINA Espejo
图片放置在内存盘中,所以应该不存在所谓的读取瓶颈,测试十次,每次读取图片100次,求得平均值。
下面是Opencv
的测试代码
import time
import cv2
import numpy
times = 100
data = []
for i in range(0, 10):
start = time.time()
for j in range(0, times):
cv2.imread('Z:\\0.png')
t = times / (time.time() - start)
data.append(t)
print(f"第{i+1}次测试,时间:{t}")
print(numpy.average(data))
输出如下:
第1次测试,时间:11.179286012576398
第2次测试,时间:11.03766301872993
第3次测试,时间:10.768910462691446
第4次测试,时间:11.260747744295276
第5次测试,时间:11.387417728544236
第6次测试,时间:11.409427301319251
第7次测试,时间:11.391379203206606
第8次测试,时间:11.386520601823856
第9次测试,时间:11.26953684781443
第10次测试,时间:11.196677950982393
11.228756687198382
所以对于Opencv
来说,每秒读取了11.2张图片,此测试图片像素为2048*1152,所以Opencv
,每秒读取了大约26,491,960个像素。
再来看看Pillow
的测试,以下为Pillow
的测试代码
import time
from PIL import Image
import numpy
times = 100
data = []
for i in range(0, 10):
start = time.time()
for j in range(0, times):
Image.open('Z:\\0.png')
t = times / (time.time() - start)
data.append(t)
print(f"第{i+1}次测试,时间:{t}")
print(numpy.average(data))
输出如下:
第1次测试,时间:968.2477463439408
第2次测试,时间:1785.6384450660094
第3次测试,时间:1886.9801508035055
第4次测试,时间:1724.1211483442403
第5次测试,时间:1886.7255044848093
第6次测试,时间:1850.8501202479977
第7次测试,时间:1814.4200030281402
第8次测试,时间:1818.2426662158236
第9次测试,时间:1886.8358427840733
第10次测试,时间:1886.8867725058146
1750.8948399824355
所以对于Pillow
来说,每秒读取了1750张图片,所以Pillow
,每秒读取了大约4,130,879,192个像素。
结果让人有点出乎意料,看来Pillow
完胜Opencv
啊,不过这个数据有点太离谱了,数据大约差了一千倍。这部分内容待定,欢迎纠正。
总结
总结一下,简单来说,运行顺序如下:
- 程序从文件夹里读取出文件名如
114514.png
,存入file
中,并打印出来。 - 切换工作目录至
path
下,使用cv2.imread()
让Opencv
读取图片,此时的im
是个类。 - 使用
im.shape[0:2]
将Opencv
读取出cv2.shape
元组赋值到h
,w
。 - 计算出图片长宽比(宽度/高度)
size
,使用round()
保留一位小数,并用str()
转化为字符串,此时的size
是个字符串。 - 调用函数
mkdir()
创建并切换至size
文件夹,将图片复制(移动)到新文件夹。 - 循环以上步骤直到所有文件都被处理完成。
这样一个分类的小程序算是写好了,还算是比较简单的一个Python程序。因为需要进行IO操作,为了能简单一点,所以书樱在这里也没有使用多线程运行,不过以上操作的速度还是很快的,除非是巨量图片,否则多线程的提升应该也不会很大,那时候瓶颈应该是在IO读写上了而不是CPU上了。
参考资料
- 图像分类_百度百科
- os — 多种操作系统接口 — Python 3.10.2 文档
- shutil — 高阶文件操作 — Python 3.10.2 文档
- 0.伏笔:图像读取方式以及效率对比 – 知乎
- Python Pillow 和 cv2 图片 resize 速度的比较 – 知乎
- 长宽比 (影像) – 维基百科,自由的百科全书
To be continue -> 【二】imagehash算法解析【Python】