Dedicated large inspection art for graphics cards

192x128 drawings made for the magnifier tray instead of enlarging the
48x32 item sprite: nine-blade fans, fin stack, heat pipes, power connector,
PCB parts, per-contact gold fingers; back with brushed backplate, QR and
barcode sticker, seal. Appearance overlays share coordinates with the base.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-03 12:21:40 +08:00
1 parent c05fe41ca9
commit dd95aad7aa
37 files changed
+450 -2

No files matched your search

Binary file not shown.
Binary file not shown.
+443
View File
@@ -0,0 +1,443 @@
#!/usr/bin/env python3
"""显卡检视大图(192×128):放大镜检视台里用的细节图,不是小图的放大版。
正面:七叶风扇、鳍片、热管、供电口、PCB 元件与逐根金手指;背面:拉丝背板、丝印凹槽、二维码与条码贴纸、封条。
叠加图与底图同尺寸、坐标一致,只画变化的像素。"""
import math
import random
from sprites import Canvas, C, key, MODELS
BW, BH = 192, 128
FANS = ((68, 51), (142, 51))
FR = 30 # 风扇半径
SH0, SH1 = (24, 14), (186, 96) # 散热罩 左上 / 右下
FING_Y0, FING_Y1 = 109, 122
NOTCH_X = 112
# 5×7 像素字(丝印与品牌字)
F57 = {
'R': ['11110', '10001', '10001', '11110', '10100', '10010', '10001'],
'Z': ['11111', '00001', '00010', '00100', '01000', '10000', '11111'],
'X': ['10001', '10001', '01010', '00100', '01010', '10001', '10001'],
'P': ['11110', '10001', '10001', '11110', '10000', '10000', '10000'],
'M': ['10001', '11011', '10101', '10101', '10001', '10001', '10001'],
'D': ['11110', '10001', '10001', '10001', '10001', '10001', '11110'],
'T': ['11111', '00100', '00100', '00100', '00100', '00100', '00100'],
'i': ['00100', '00000', '01100', '00100', '00100', '00100', '01110'],
'O': ['01110', '10001', '10001', '10001', '10001', '10001', '01110'],
'B': ['11110', '10001', '10001', '11110', '10001', '10001', '11110'],
'I': ['01110', '00100', '00100', '00100', '00100', '00100', '01110'],
'A': ['01110', '10001', '10001', '11111', '10001', '10001', '10001'],
'0': ['01110', '10001', '10011', '10101', '11001', '10001', '01110'],
'1': ['00100', '01100', '00100', '00100', '00100', '00100', '01110'],
'2': ['01110', '10001', '00001', '00110', '01000', '10000', '11111'],
'3': ['11110', '00001', '00001', '01110', '00001', '00001', '11110'],
'4': ['00010', '00110', '01010', '10010', '11111', '00010', '00010'],
'5': ['11111', '10000', '11110', '00001', '00001', '10001', '01110'],
'6': ['00110', '01000', '10000', '11110', '10001', '10001', '01110'],
'7': ['11111', '00001', '00010', '00100', '01000', '01000', '01000'],
'8': ['01110', '10001', '10001', '01110', '10001', '10001', '01110'],
'9': ['01110', '10001', '10001', '01111', '00001', '00010', '01100'],
}
def text57(c, x, y, s, color, scale=1, skew=False, gap=1):
i = 0
for ch in s:
if ch == ' ':
x += 3 * scale
continue
dy = (i % 2) * scale if skew else 0
for r, row in enumerate(F57[ch]):
for k, bit in enumerate(row):
if bit == '1':
c.rect(x + k * scale, y + r * scale + dy, x + k * scale + scale - 1, y + r * scale + scale - 1 + dy, color)
x += (5 + gap) * scale + (scale if skew and i % 3 == 2 else 0)
i += 1
def outline(c, col='O'):
solid = lambda v: v is not None
get = lambda x, y: c.px[y][x] if 0 <= x < c.w and 0 <= y < c.h else None
edge = [(x, y) for y in range(c.h) for x in range(c.w)
if solid(get(x, y)) and any(not solid(get(x + dx, y + dy)) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)))]
for x, y in edge:
c.put(x, y, col)
def bracket(c):
"""左端 I/O 挡板:折边、三个接口(HDMI 与两个 DP,内腔与触片)、散热网孔、底部螺丝。"""
c.rect(2, 8, 22, 112, 'b')
c.rect(2, 8, 4, 112, 'l'); c.rect(2, 8, 2, 112, 'h') # 受光折边
c.rect(19, 8, 22, 112, 'd'); c.rect(21, 8, 22, 112, 'g') # 背光折边
for x in range(5, 19): # 拉丝
for y in range(9, 112, 2):
c.put(x, y, 'b' if (x + y) % 5 else 'l')
for i, y in enumerate((16, 36, 56)): # 三个接口
c.rect(6, y, 17, y + 13, 'k')
c.rect(6, y, 17, y, 'g'); c.rect(6, y, 6, y + 13, 'g')
c.rect(7, y + 1, 16, y + 12, 'n')
if i == 0: # HDMI:两侧斜角
for k in range(3):
c.put(7 + k, y + 12 - k, 'k'); c.put(16 - k, y + 12 - k, 'k')
c.rect(9, y + 4, 14, y + 6, 'z')
else: # DP:一边切角
c.put(16, y + 12, 'k'); c.put(15, y + 12, 'k'); c.put(16, y + 11, 'k')
c.rect(9, y + 4, 14, y + 5, 'z')
for x in range(8, 16, 2):
c.put(x, y + 9, 'Y')
for y in range(78, 104, 4): # 散热网孔
for x in range(7, 17, 3):
c.rect(x, y, x + 1, y + 2, 'k')
c.put(x, y, 'x')
c.rect(9, 106, 14, 110, 'm'); c.rect(10, 107, 13, 109, 'g') # 底部螺丝
c.put(11, 108, 'k'); c.put(12, 108, 'k'); c.put(11, 107, 'b')
c.rect(9, 10, 14, 12, 'k')
def fan(c, cx, cy, accent, accent_dark):
"""风扇:九片弯曲扇叶(前缘高光、叶面渐变、叶尖暗边),外圈带四个固定螺丝,轴心带品牌圆标。"""
R = FR
for y in range(cy - R - 1, cy + R + 2):
for x in range(cx - R - 1, cx + R + 2):
dx, dy = x - cx, y - cy
r = math.hypot(dx, dy)
if r > R + 0.5:
continue
ang = (math.atan2(dy, dx) / (2 * math.pi)) % 1.0
light = -(dx + dy) / (1.4 * R) # 左上为正
if r >= R - 3.2: # 外圈
c.put(x, y, 'h' if light > 0.5 else 'l' if light > 0.1 else 'b' if light > -0.4 else 'm')
elif r >= R - 5.5: # 风道阴影
c.put(x, y, 'n' if light < 0.2 else 'k')
elif r <= 6.5: # 轴心
if r <= 4.3:
c.put(x, y, accent if light < 0.3 else accent_dark if r > 3 else 'w')
else:
c.put(x, y, 'h' if light > 0.2 else 'm' if light > -0.3 else 'd')
else:
t = (ang * 9 + (r - 7) * 0.060) % 1.0
if t < 0.62:
if t < 0.06:
col = 'h'
elif t < 0.40:
col = 'l' if light > 0.25 else 'b' if light > -0.2 else 'm'
else:
col = 'b' if light > 0.2 else 'm' if light > -0.3 else 'g'
if r > R - 8:
col = 'g' if col in ('m', 'g') else 'm'
c.put(x, y, col)
else:
c.put(x, y, 'n' if t < 0.9 else 'k')
for k in range(4): # 外圈固定螺丝
a = math.pi / 4 + k * math.pi / 2
sx, sy = int(cx + math.cos(a) * (R - 1.5)), int(cy + math.sin(a) * (R - 1.5))
c.rect(sx - 1, sy - 1, sx + 1, sy + 1, 'k')
c.put(sx, sy, 'g')
def gpu_front(accent, accent_dark, brand_text, burnt=False):
c = Canvas(BW, BH)
bracket(c)
x0, y0 = SH0
x1, y1 = SH1
# 散热罩:石墨底,顶部受光亮边,左缘高光,右下阴影;右上角切斜
for y in range(y0, y1 + 1):
for x in range(x0, x1 + 1):
if x > x1 - 8 and y < y0 + (x - (x1 - 8)): # 右上切角
continue
base = 'z'
if y < y0 + 3: base = 'h' if x < x1 - 40 else 'l'
elif y < y0 + 6: base = 'l' if x < x1 - 40 else 'b'
elif y < y0 + 9: base = 'x'
elif y > y1 - 4: base = 'n'
c.put(x, y, base)
for y in range(y0 + 3, y1): # 左缘
c.put(x0, y, 'l'); c.put(x0 + 1, y, 'b'); c.put(x0 + 2, y, 'x')
# 右端鳍片
for x in range(x1 - 18, x1 + 1):
for y in range(y0 + 10, y1 - 4):
if x > x1 - 8 and y < y0 + (x - (x1 - 8)) + 2:
continue
c.put(x, y, 'x' if (x - (x1 - 18)) % 3 == 0 else 'g' if (x - (x1 - 18)) % 3 == 1 else 'n')
c.rect(x1 - 20, y0 + 10, x1 - 19, y1 - 4, 'x')
# 风扇之间的中筋
c.rect(105, y0 + 8, 106, y1 - 5, 'k'); c.rect(107, y0 + 8, 107, y1 - 5, 'x')
for cx, cy in FANS:
fan(c, cx, cy, accent, accent_dark)
# 前沿斜切装饰条:品牌色 + 品牌字
for x in range(x0 + 4, x1 - 20):
c.rect(x, y1 - 5, x, y1 - 3, accent if x % 11 else accent_dark)
for k in range(18):
c.rect(x0 + 4 + k, y1 - 5 - k // 2, x0 + 4 + k, y1 - 3 - k // 2 + 1, accent)
text57(c, 112, 84, brand_text, 'h')
# 顶沿:四根铜热管端头与 8 针供电口
for i in range(4):
x = 38 + i * 9
c.rect(x, 7, x + 6, y0 - 1, 'p'); c.rect(x, 7, x + 1, y0 - 1, 'h' if i == 0 else 'p')
c.rect(x + 5, 7, x + 6, y0 - 1, 'q'); c.rect(x, 7, x + 6, 7, 'q')
c.rect(112, 5, 154, y0 - 1, 'n'); c.rect(112, 5, 154, 6, 'z')
for i in range(8):
c.rect(114 + i * 5, 7, 116 + i * 5, 9, 'k'); c.put(115 + i * 5, 8, 'Y')
# PCB:阻焊绿、边缘高光、元件与走线
for y in range(y1 + 1, FING_Y0):
for x in range(x0, x1 + 1):
c.put(x, y, 'G' if y < y1 + 4 else 'g2')
for x in range(x0, x1 + 1):
c.put(x, y1 + 1, 'T') if False else None
random.seed(7)
for i in range(14):
sx = x0 + 6 + i * 11
c.rect(sx, y1 + 3, sx + 3, y1 + 5, 'k' if i % 3 else 'b')
c.rect(sx, y1 + 3, sx + 3, y1 + 3, 'x')
for k in range(4):
c.put(sx + k, y1 + 7, 'Y' if k % 2 == 0 else 'g2')
# 金手指:逐根(3 像素宽,1 像素缝),PCIe 键位缺口
for x in range(x0 + 2, x1 - 2):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 or (x - x0) % 4 == 3:
continue
for y in range(FING_Y0, FING_Y1 + 1):
c.put(x, y, 'h' if y == FING_Y0 else 'Y' if (x - x0) % 4 != 0 else 'p')
c.put(x, FING_Y1, 'e')
c.rect(x0, FING_Y0, x1, FING_Y0, 'g2')
for y in range(FING_Y0 + 1, FING_Y1 + 1): # 缝里露出 PCB
for x in range(x0 + 2, x1 - 2):
if c.px[y][x] is None and not (NOTCH_X - 2 <= x <= NOTCH_X + 3):
c.put(x, y, 'g2')
if burnt:
for cx, cy in FANS:
for dx, dy in ((0, 0), (5, -5), (-6, 6), (8, 4), (-9, -3), (2, 10)):
c.rect(cx + dx - 2, cy + dy - 2, cx + dx + 2, cy + dy + 2, 'c')
for x in range(x0 + 4, x1 - 20):
c.rect(x, y1 - 5, x, y1 - 3, 'e')
for dx, dy in ((30, 70), (90, 40), (150, 80), (60, 30), (120, 88)):
c.rect(dx, dy, dx + 4, dy + 3, 'c')
outline(c)
return c
def screw(c, x, y):
c.rect(x - 3, y - 3, x + 3, y + 3, 'm')
c.rect(x - 3, y - 3, x - 1, y - 3, 'h')
c.rect(x - 2, y - 2, x + 2, y + 2, 'b')
c.rect(x - 2, y, x + 2, y, 'k'); c.rect(x, y - 2, x, y + 2, 'k')
c.rect(x + 2, y + 2, x + 3, y + 3, 'd')
def qr(c, x, y, seed):
rnd = random.Random(seed)
n = 21
c.rect(x - 1, y - 1, x + n, y + n, 'w')
for j in range(n):
for i in range(n):
on = rnd.random() < 0.5
for fx, fy in ((0, 0), (n - 7, 0), (0, n - 7)):
if fx <= i < fx + 7 and fy <= j < fy + 7:
li, lj = i - fx, j - fy
on = li in (0, 6) or lj in (0, 6) or (2 <= li <= 4 and 2 <= lj <= 4)
elif fx - 1 <= i <= fx + 7 and fy - 1 <= j <= fy + 7:
on = False
if on:
c.put(x + i, y + j, 'n')
def sticker(c, seed):
"""序列号贴纸(右下):二维码、条码、两行小字。"""
c.rect(86, 64, 172, 100, 'w')
c.rect(86, 64, 172, 65, 'h'); c.rect(86, 99, 172, 100, 'l')
c.rect(86, 64, 87, 100, 'h'); c.rect(171, 64, 172, 100, 'l')
qr(c, 91, 70, seed)
for x in range(118, 166):
w = 1 + ((x * 7 + seed) % 3)
if (x * 5 + seed) % 4 != 0:
c.rect(x, 70, x, 84, 'n')
text57(c, 118, 88, "0X5A70", 'n')
c.rect(150, 88, 166, 94, 'm')
def seal(c):
"""封条(红底白字):压住贴纸右边缘。"""
c.rect(174, 60, 182, 104, 'R')
c.rect(174, 60, 175, 104, 'r2'); c.rect(181, 60, 182, 104, 'r2')
for y in range(66, 100, 8):
c.rect(176, y, 180, y + 3, 'w')
c.put(178, y + 1, 'R')
def gpu_back(accent, accent_dark, burnt=False):
c = Canvas(BW, BH)
bracket(c)
x0, y0 = SH0
x1, y1 = SH1
for y in range(y0, y1 + 12):
for x in range(x0, x1 + 1):
c.put(x, y, 'd' if y % 2 else 'm' if (x // 3) % 4 else 'd')
for y in range(y0, y1 + 12):
c.put(x0, y, 'l'); c.put(x0 + 1, y, 'b'); c.put(x1, y, 'k'); c.put(x1 - 1, y, 'z')
for x in range(x0, x1 + 1):
c.put(x, y0, 'h'); c.put(x, y0 + 1, 'l'); c.put(x, y0 + 2, 'b')
# 丝印凹槽(型号叠加图写在这里)
c.rect(36, 24, 178, 52, 'z')
c.rect(36, 24, 178, 25, 'k'); c.rect(36, 24, 37, 52, 'k')
c.rect(36, 52, 178, 52, 'x'); c.rect(178, 24, 178, 52, 'x')
# 品牌色斜条
for x in range(x0 + 4, x1 - 4):
c.rect(x, 56, x, 59, accent)
c.put(x, 60, accent_dark)
for k in range(20):
c.rect(x1 - 6 - k, 55 - k // 2, x1 - 6 - k, 55 - k // 2 + 1, accent_dark)
# 通风槽(左下)
for y in range(70, 100, 6):
c.rect(x0 + 10, y, x0 + 50, y + 3, 'k')
c.rect(x0 + 10, y + 4, x0 + 50, y + 4, 'x')
# 四角螺丝
for sx, sy in ((x0 + 12, y0 + 10), (x1 - 12, y0 + 10), (x0 + 12, y1 - 2), (x1 - 12, y1 - 2)):
screw(c, sx, sy)
sticker(c, 5)
seal(c)
# 背面金手指(暗一档)
for x in range(x0 + 2, x1 - 2):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 or (x - x0) % 4 == 3:
continue
for y in range(y1 + 14, FING_Y1 + 1):
c.put(x, y, 'Y' if (x - x0) % 4 else 'p')
for y in range(y1 + 12, y1 + 14):
for x in range(x0, x1 + 1):
c.put(x, y, 'g2')
if burnt:
for dx, dy in ((60, 30), (130, 40), (50, 80), (100, 70), (150, 90)):
c.rect(dx, dy, dx + 6, dy + 4, 'c')
outline(c)
return c
def new():
return Canvas(BW, BH)
# ---------- 叠加图 ----------
def ov_fan(kind):
c = new()
rnd = random.Random(11 + kind)
for cx, cy in FANS:
if kind == 1: # 积灰:叶片上一层灰点和灰絮
for _ in range(190):
a = rnd.random() * math.tau
r = 8 + rnd.random() * (FR - 12)
x, y = int(cx + math.cos(a) * r), int(cy + math.sin(a) * r)
c.put(x, y, 'l' if rnd.random() < 0.6 else 'h')
if rnd.random() < 0.3:
c.put(x + 1, y, 'l')
for _ in range(10): # 絮状长条
a = rnd.random() * math.tau
r = 10 + rnd.random() * (FR - 16)
x, y = int(cx + math.cos(a) * r), int(cy + math.sin(a) * r)
for k in range(5):
c.put(x + k, y + (k // 2), 'w' if k % 2 == 0 else 'l')
else: # 缺角:一片叶尖缺一块,露出风道黑底
a0 = 0.9 if cx < 100 else 3.2
for r in range(10, FR - 3):
for da in (-0.20, -0.17, -0.13, -0.10, -0.06, -0.02, 0.02, 0.06, 0.10, 0.14):
if r > FR - 14 + (da * 30) and r > 17:
c.put(int(cx + math.cos(a0 + da) * r), int(cy + math.sin(a0 + da) * r), 'n')
for r in range(17, FR - 3):
c.put(int(cx + math.cos(a0 - 0.22) * r), int(cy + math.sin(a0 - 0.22) * r), 'h')
return c
def ov_fingers(kind):
c = new()
rnd = random.Random(21 + kind)
x0, x1 = SH0[0], SH1[0]
for x in range(x0 + 2, x1 - 2):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 or (x - x0) % 4 == 3:
continue
for y in range(FING_Y0 + 1, FING_Y1 + 1):
if kind == 1: # 发暗:触点变褐
c.put(x, y, 'e' if (x + y) % 3 else 'q')
else: # 氧化:发绿的斑块
if rnd.random() < 0.65:
c.put(x, y, 'g2' if rnd.random() < 0.5 else 'G')
return c
def ov_bracket(kind):
c = new()
rnd = random.Random(31)
if kind == 1: # 划痕
for _ in range(9):
x, y = rnd.randint(5, 17), rnd.randint(14, 100)
for k in range(rnd.randint(5, 12)):
c.put(x + k // 2, y + k, 'h' if k % 2 else 'k')
else: # 接口数不对:第三个接口被板子封死
c.rect(6, 56, 17, 69, 'b')
c.rect(6, 56, 17, 56, 'h'); c.rect(6, 56, 6, 69, 'l'); c.rect(17, 56, 17, 69, 'd')
for x in range(7, 17, 2):
c.put(x, 62, 'm')
return c
def ov_text(model, skew):
c = new()
text57(c, 46, 31, model, 'l' if skew else 'h', scale=2, skew=skew, gap=1)
return c
def ov_sticker(kind):
c = new()
if kind == 1: # 手写序列号
c.rect(86, 64, 172, 100, 'w')
c.rect(86, 64, 172, 65, 'h'); c.rect(86, 99, 172, 100, 'l')
c.rect(86, 64, 87, 100, 'h'); c.rect(171, 64, 172, 100, 'l')
rnd = random.Random(41)
for row in (74, 84, 92):
x = 92
while x < 164:
w = rnd.randint(3, 7)
for k in range(w):
c.put(x + k, row + int(math.sin(k + x) * 1.5), 'O')
c.put(x + k, row + 1 + int(math.sin(k + x) * 1.5), 'O')
x += w + rnd.randint(2, 4)
else: # 封条缺失:残留的胶痕与撕口
c.rect(174, 60, 182, 104, 'd')
for y in range(60, 105, 3):
c.rect(174, y, 178 + (y % 5), y + 1, 'm')
for y in range(62, 104, 6):
c.rect(179, y, 181, y + 1, 'x')
return c
BIG = {}
for _b, (_a, _ad, _t) in {'probidia': ('G', 'g2', 'PROBIDIA'), 'pmd': ('R', 'r2', 'PMD')}.items():
BIG['gpu_big_%s_f' % _b] = (lambda a=_a, ad=_ad, t=_t: gpu_front(a, ad, t))
BIG['gpu_big_%s_b' % _b] = (lambda a=_a, ad=_ad: gpu_back(a, ad))
BIG['gpu_big_burnt_f'] = lambda: gpu_front('e', 'c', 'PROBIDIA', burnt=True)
BIG['gpu_big_burnt_b'] = lambda: gpu_back('e', 'c', burnt=True)
BIG_OV = {
'gpu_big_ov_fan1': lambda: ov_fan(1), 'gpu_big_ov_fan2': lambda: ov_fan(2),
'gpu_big_ov_fin1': lambda: ov_fingers(1), 'gpu_big_ov_fin2': lambda: ov_fingers(2),
'gpu_big_ov_brk1': lambda: ov_bracket(1), 'gpu_big_ov_brk2': lambda: ov_bracket(2),
'gpu_big_ov_stk1': lambda: ov_sticker(1), 'gpu_big_ov_stk2': lambda: ov_sticker(2),
}
for _m in MODELS:
BIG_OV['gpu_big_ov_txt_' + key(_m)] = (lambda m=_m: ov_text(m, False))
BIG_OV['gpu_big_ov_txs_' + key(_m)] = (lambda m=_m: ov_text(m, True))
if __name__ == '__main__':
import sys, os
if len(sys.argv) == 3 and sys.argv[1] == '--preview':
d = sys.argv[2]
def stack(base, *ovs):
b = BIG[base]()
for o in ovs:
oc = BIG_OV[o]()
for y in range(oc.h):
for x in range(oc.w):
if oc.px[y][x] is not None:
b.px[y][x] = oc.px[y][x]
return b
bg = (0x6A, 0x72, 0x70)
stack('gpu_big_probidia_f').png(os.path.join(d, 'big_f.png'), scale=5, bg=bg)
stack('gpu_big_pmd_f', 'gpu_big_ov_fan1', 'gpu_big_ov_fin2', 'gpu_big_ov_brk2').png(os.path.join(d, 'big_f_fake.png'), scale=5, bg=bg)
stack('gpu_big_pmd_b', 'gpu_big_ov_txs_rz6800', 'gpu_big_ov_stk1').png(os.path.join(d, 'big_b_fake.png'), scale=5, bg=bg)
stack('gpu_big_probidia_b', 'gpu_big_ov_txt_rzx80ti').png(os.path.join(d, 'big_b.png'), scale=5, bg=bg)
+3
View File
@@ -568,4 +568,7 @@ if __name__ == '__main__':
canvas.png(os.path.join(out, name + '.png'))
for name, make in OVERLAYS.items():
make().png(os.path.join(out, name + '.png'))
import big
for name, make in {**big.BIG, **big.BIG_OV}.items():
make().png(os.path.join(out, name + '.png'))
print(out)