在之前的做题中了解过几次这个库,只是了解了一点皮毛,不过随着做题的增多越来越发现这个库的重要性,所以重点学习一下。

库简介

1
pip install PIL

PIL库是python中最常用的图像处理库。PIL库共包括多个与图片相关的类,这些类可以被看做是子库或PIL库中的模块,Image是最常用的类。

1
2
3
4
5
import PIL

from PIL import *

from PIL import Image

库函数

根据参数加载图片文件Image.open(filename)

将图片加载之后可以读取图片的一些属性:

1
2
3
4
5
from PIL import Image

img=Image.open('attach.png')
print(img.mode,img.format,img.size)
#RGB PNG (208, 208)

根据参数创建一个新的图像Image.new(mode,size,color)

图像的组成元素是像素pixel,每一个像素都有明确的位置和被分配的色彩数值。

对于图片模式mode,常用的有:“L”为灰色图像、“RGB”为真彩色图像、“CMYK”为出版图像。

RGB模式下每个像素储存需要三个字节,分别记录R,G,B三个通到的数值。

如果R=G=B,图片像素的颜色即为从纯黑色到纯白色,图片视觉上为白色到黑色之间的过渡色,所以只需要一个字节就能存储色彩数值,这就是灰度模式。

1
2
3
4
width = 200
height = 200
img = Image.new('RGB', (width, height))
img.show()#预览图片,得到黑色小正方形

根据像素点date创建图像Image.frombytes(mode,size,date)

我用不同的模式来举个简单的例子:

1
2
3
4
5
6
7
from PIL import Image

tobytes = b'xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1'
img = Image.frombytes("RGB", (4, 1), tobytes)
print(list(img.getdata()))
#[(120, 100, 56), (225, 183, 235), (168, 229, 32), (210, 183, 225)]
img.show()

这里我们有一份12长度的字节,在RGB模式中,我们只能转为记录四个像素的字节,预览图片之后能得到四像素点的彩图(四个小方块)。

1
2
3
4
5
6
7
from PIL import Image

tobytes = b'xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1'
img = Image.frombytes("L", (4, 3), tobytes)
print(list(img.getdata()))
#[120, 100, 56, 225, 183, 235, 168, 229, 32, 210, 183, 225]
img.show()

但是,如果改为灰度模式,就可以生成12个像素的灰色图片。(注意对图片使用getdata后得到的数组区别。)

记录像素时一行一行从左往右记录。

修改图像像素putpixel((x,y),(r,g,b,a))

获取图像像素img.getpixel((x, y))

绘图:函数声明:draw = ImageDraw.Draw(image)

后加相应属性为绘图样式:例如书写文字text: drawObject.text(position,text,fill = None,font = None,anchor = None,spacing = 0, align =“left”,direction = None,features = None,language = None),无特殊声明的属性值可以省略。

复制:使用img.crop((a,b,c,d))进行复制,其中a和b组合是复制左上角的点,c和d组合是复制左下角的点。

扩展:matplotlib库

通常进行数据处理绘图分析时需要使用到另一个库matplotlib库,比如说我们如果需要绘制一个坐标图,提供了对应的x坐标数组和y坐标数组,就可以直接绘制一个图表:

1
2
3
4
5
6
import matplotlib.pyplot as plt

x_data = [...]
y_data = [...]
plt.scatter(x_data, y_data)
plt.show()

[NCTF 2022]coloratura

这道题就是利用了PIL库,实质是随机数预测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from Crypto.Util.number import long_to_bytes
from PIL import Image, ImageDraw
from random import getrandbits

width = 208
height = 208
flag = open('flag.txt').read()


def makeSourceImg():
colors = long_to_bytes(getrandbits(width * height * 24))[::-1]
img = Image.new('RGB', (width, height))
x = 0
for i in range(height):
for j in range(width):
img.putpixel((j, i), (colors[x], colors[x + 1], colors[x + 2]))
x += 3
return img


def makeFlagImg():
img = Image.new("RGB", (width, height))
draw = ImageDraw.Draw(img)
draw.text((5, 5), flag, fill=(255, 255, 255))
return img


if __name__ == '__main__':
img1 = makeSourceImg()
img2 = makeFlagImg()
img3 = Image.new("RGB", (width, height))
for i in range(height):
for j in range(width):
p1, p2 = img1.getpixel((j, i)), img2.getpixel((j, i))
img3.putpixel((j, i), tuple([(p1[k] ^ p2[k]) for k in range(3)]))
img3.save('attach.png')

提供了一张attach图片:

看一遍代码,这道题首先利用随机数生成的方式生成图片1,再生成图片2并将flag文字打在图片上,最后将图片1和图片2的每一个像素位对应异或运算得到图片3,题目就提供了图片3。

首先先了解一下makeSourceImg()

1
2
3
4
5
6
7
8
9
def makeSourceImg():
colors = long_to_bytes(getrandbits(width * height * 24))[::-1]
img = Image.new('RGB', (width, height))
x = 0
for i in range(height):
for j in range(width):
img.putpixel((j, i), (colors[x], colors[x + 1], colors[x + 2]))
x += 3
return img

题目首先随机生成图片颜色,生成之后对所有字节都进行了倒排列,由于生成随机数时先生成的放在最后面,所以倒排列之后相当于把先生成的放在了前面,目的还是让我们找到624个连续的32位随机数序列进行随机数预测。

再来看makeFlagImg()

1
2
3
4
5
def makeFlagImg():
img = Image.new("RGB", (width, height))
draw = ImageDraw.Draw(img)
draw.text((5, 5), flag, fill=(255, 255, 255))
return img

我使用题目给的代码随机生成了一些文字:

可以看到除了flag为纯白色,其他全为黑色(0,0,0),有:draw.text((5, 5), flag, fill=(255, 255, 255))从第五行第五列个像素点开始写文字。

先计算一下需要多少个像素点才满足624个32位随机数:
$$
624×32÷24=832
$$
一行有208个像素点,四行刚好为832个像素点,而这四行像素点全为纯黑,和0异或的结果保持不变,所以题目给的图片前四行的像素对应的RGB值刚好就足够624个32位随机数,提取出来之后进行随机数预测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from PIL import Image
import random
from Crypto.Util.number import *

def invert_right(m, l, val=''):
length = 32
mx = 0xffffffff
if val == '':
val = mx
i, res = 0, 0
while i * l < length:
mask = (mx << (length - l) & mx) >> i * l
tmp = m & mask
m = m ^ tmp >> l & val
res += tmp
i += 1
return res

def invert_left(m, l, val):
length = 32
mx = 0xffffffff
i, res = 0, 0
while i * l < length:
mask = (mx >> (length - l) & mx) << i * l
tmp = m & mask
m ^= tmp << l & val
res |= tmp
i += 1
return res

def invert_temper(m):
m = invert_right(m, 18)
m = invert_left(m, 15, 4022730752)
m = invert_left(m, 7, 2636928640)
m = invert_right(m, 11)
return m

def clone_mt(record):
state = [invert_temper(i) for i in record]
gen = random.Random()
gen.setstate((3, tuple(state + [0]), None))
return gen

def makeSourceImg():
colors = long_to_bytes(g.getrandbits(width * height * 24))[::-1]
img = Image.new('RGB', (width, height))
x = 0
for i in range(height):
for j in range(width):
img.putpixel((j, i), (colors[x], colors[x + 1], colors[x + 2]))
x += 3
return img

height, width = 208, 208

img = Image.open('attach.png')
a = []
for i in range(4):
for j in range(208):
p = img.getpixel((j, i))
for pi in p:
a.append(pi)
M = []
for i in range(len(a) // 4):
px = (a[4 * i + 3] << 24) + (a[4 * i + 2] << 16) + (a[4 * i + 1] << 8) + a[4 * i + 0]
M.append(px)

g = clone_mt(M)

img1 = makeSourceImg()
img2 = Image.open('attach.png')
img3 = Image.new("RGB", (width, height))
for i in range(height):
for j in range(width):
p1, p2 = img1.getpixel((j, i)), img2.getpixel((j, i))
img3.putpixel((j, i), tuple([(p1[k] ^ p2[k]) for k in range(3)]))
img3.save('flag.png')

这里我直接用脚本恢复了最初的状态state,让他重新生成一遍随机数得到未知的img1,得到img1之后直接利用题目原始代码和attach.png异或之后就能复原得到img2,得到flag。