【一】使用OpenCV、Pillow库分类图片【Python】
本文最后更新于 1236 天前,其中的信息可能已经有所发展或是发生改变。

「论一个随机图片api的养成计划」

前段时间,书樱用Python稍微抓取了 Pixiv 的图片,总共大概有三万张左右。因为爬取时的爬虫只负责下载了,出现了很多重复的图片以及图片缩略图。最近想搞个图片api,正好可以用上这些二次元图片,所以这个系列暂且叫做「论一个随机图片api的养成计划,于是便有了这篇文章。

系列文章合集 -> 「论一个随机图片api的养成计划」

如果您觉得我写的东西对您有所帮助的话,不妨请我喝杯咖啡。=> 赞助

序言

这是这个系列文章的第一篇,因为下一篇涉及的内容会非常多,信息量太大(完全不是因为想水文章),今天把相对简单的内容单独列成一篇,作为开胃小菜。

话不宜迟,进入正题。

分类

思路

图片分类,顾名思义,就是根据各自在图像信息中所反映的不同特征,把不同类别的目标区分开来的图像处理方法。它利用计算机对图像进行定量分析,把图像或图像中的每个像元或区域划归为若干个类别中的某一种,以代替人的视觉判读。[1] 所以,自然而然,能供我们分类的信息也有很多。

有机会用DL写一个识别天依的程序

当然,和广泛定义中的图片分类不一样,这将近三万张图片的分类呢,书樱只想按照长宽比来分类,原因很简单。首先,因为都是来自 Pixiv 的图片,所以不需要对图片内容进行分类。其次,不同设备屏幕长宽比不同,所需要显示图片的长宽比也不同,书樱希望在 api 中设置一个参数,来规范图片返回的长宽比,以适应不同的设备。因此,按尺寸分类图片也就顺理成章了。

算法

很好,现在想法有了,让我们来构思下这个小程序。

  • 首先,我们需要从一个文件夹里读取出图片。
  • 接着,读取出图片的长宽信息并计算出长宽比。
  • 然后,再按照长宽比对图片进行分类。
  • 最后,把分类好的图片转移到新文件夹,按照长宽比分类。

代码

下面是根据我们想法写代码。

分类图片中,书樱使用到了osshutilOpencv/Pillow这两(三)个模块。

os(多种操作系统接口)这个模块不用多说,os提供了非常丰富的方法用来处理文件和目录,功能可以说是非常强大。[2]

shutil(高阶文件操作)模块提供了一系列对文件和文件集合的高阶操作。特别是提供了一些支持文件拷贝和删除的函数。[3]

关于OpencvPillow (PIL),这里我选择的是Opencv,因为Opencv在各方面速度都比PIL要快上不少,对于庞大数量的图片,这可以给我们省下很多时间,不过对于单线程读取来说,瓶颈在这,当然我也留下PIL的代码。[4][5]

OpencvPIL本身就是非常强大的媒体处理库,这方面的内容可谓是相当丰富,特别是在深度学习方面。书樱在这里也只是使用了一些非常简单的功能,只是“冰山半角”,如果有兴趣的朋友可以Google搜索相关文档阅读。

下面是导入 Python 模块,import cv2是导入Opencv模块。如果是使用PIL,因为我们使用的函数在Image中,则需要from PIL import ImagePIL库中导入我们需要的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

测试

关于PILOpencv对于图片的处理速度,书樱做了个简单的测试,此为测试图片,来自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元组赋值到hw
  • 计算出图片长宽比(宽度/高度)size,使用round()保留一位小数,并用str()转化为字符串,此时的size是个字符串。
  • 调用函数mkdir()创建并切换至size文件夹,将图片复制(移动)到新文件夹。
  • 循环以上步骤直到所有文件都被处理完成。

这样一个分类的小程序算是写好了,还算是比较简单的一个Python程序。因为需要进行IO操作,为了能简单一点,所以书樱在这里也没有使用多线程运行,不过以上操作的速度还是很快的,除非是巨量图片,否则多线程的提升应该也不会很大,那时候瓶颈应该是在IO读写上了而不是CPU上了。

参考资料

  1. 图像分类_百度百科
  2. os --- 多种操作系统接口 — Python 3.10.2 文档
  3. shutil --- 高阶文件操作 — Python 3.10.2 文档
  4. 0.伏笔:图像读取方式以及效率对比 - 知乎
  5. Python Pillow 和 cv2 图片 resize 速度的比较 - 知乎
  6. 长宽比 (影像) - 维基百科,自由的百科全书

To be continue -> 【二】imagehash算法解析【Python】

本文作者:SakuraPuare
本文链接:https://blog.sakurapuare.com/archives/2021/01/resort-pic-with-opencv-and-pillow/
版权声明:本文采用 CC BY-NC-SA 4.0 CN 协议进行许可
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇