WeChall - Training - Caterpillar

Challenge

I have drawn a picture of a caterpillar and hid some text in it. I am sure you can figure it out, as it is not too hard :)

Solution

下载 caterpillar.png(220x55, RGBA)

标准 stegano 分析(zsteg, bit plane 扫描, LSB/MSB 全通道)无结果。PNG 只有 IHDR/sRGB/bKGD/pHYs/IDAT/IEND 六个 chunk,无附加数据。

数据不在 RGB 裸值里,而是藏在其他色彩空间的 Hue 值中。

RGB 是加色混色——R, G, B 各 0-255。毛虫的主色是黄绿,以 RGB(135,194,41) 为例:

  • R=135:不在可打印 ASCII 范围(32-126)外,chr(135) 是扩展字符
  • G=194:同上,超出范围
  • B=41:chr(41) = ),无意义

直接用 RGB 值当 ASCII 只能得到一堆乱码。

色彩空间可以互相转换

RGB 和 HSV 描述的是同一个颜色,只是坐标系不同——就像经纬度 (116.4°E, 39.9°N) 和北京市地址是同一地点。

1
2
3
4
5
6
7
8
9
10
11
import colorsys

r, g, b = 135, 194, 41

# RGB -> HSV
h, s, v = colorsys.rgb_to_hsv(r/255, g/255, b/255)
# h = 83° (色相角度), s = 79% (饱和度), v = 76% (明度)

# HSV -> RGB (可逆)
r2, g2, b2 = colorsys.hsv_to_rgb(h, s, v)
# (r2,g2,b2) == (135,194,41) # 无损还原

转换公式是解析的,无精度损失。

解法

毛虫的 8 个主 body segment,每个用了一种特定的颜色。RGB→HSV 后,H(色相角度)落在可打印 ASCII 范围内:

1
2
3
4
5
6
7
8
9
10
segment 1 (x=  0- 22)  #e1f0c9    H=83° -> 'S'
segment 2 (x= 23- 46) #e8f4d7 H=85° -> 'U'
腿阴影 (x= 47- 69) #6e7d4d H=79° -> 'O'
腿阴影 (x= 70- 92) #465512 H=73° -> 'I'
segment 3 (x= 93-115) #a9bb27 H=67° -> 'C'
segment 4 (x=116-140) #557315 H=79° -> 'O'
segment 5 (x=141-165) #7dad30 H=83° -> 'S'
segment 6 (x=166-187) #eaf4d0 H=77° -> 'M'
头部 (x=188-219) #e06478 H=350° -> 不可打印 (结束标记)
...
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
#!/usr/bin/env python3
from PIL import Image
import colorsys
from collections import Counter

img = Image.open('caterpillar.png').convert('RGBA')
w, h = img.size

# 逐列取最常见的非白色 Hue
x_hues = {}
for x in range(w):
hues = []
for y in range(h):
r, g, b, a = img.getpixel((x, y))
if (r, g, b) == (255, 255, 255):
continue
h_val, _, _ = colorsys.rgb_to_hsv(r/255, g/255, b/255)
hues.append(round(h_val * 360))
if hues:
x_hues[x] = Counter(hues).most_common(1)[0][0]

# 合并连续相同 Hue 为段,只保留稳定段(>=10 列宽)
result = []
run_h, run_start, run_len = None, None, 0
for x in range(w):
h = x_hues.get(x)
if h == run_h:
run_len += 1
else:
if run_len >= 10 and 32 <= run_h <= 126:
result.append(chr(run_h))
run_h, run_start, run_len = h, x, 1

if run_len >= 10 and 32 <= run_h <= 126:
result.append(chr(run_h))

decoded = ''.join(result)
print(f'Decoded Hue sequence: {decoded}')
print(f'Answer (last word): {decoded.split()[-1]}')
COLOR-SHEMES