| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- 生成布里斯托大便分类法(Bristol Stool Scale)类型 1-7 示意图。
- 输出:cfc-frontend/static/health/bristol/bristol-{1..7}.png(200x200 透明底 PNG)
- 用法:python3 scripts/gen-bristol-stool-images.py
- 依赖:Pillow
- 风格:暖棕配色(#8C6444 主体 / #684830 描边),与问卷暖色主题一致;
- 4 倍超采样后 LANCZOS 缩放,保证小程序端边缘平滑。
- """
- import math
- import os
- import random
- from PIL import Image, ImageDraw
- OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "static", "health", "bristol")
- SUPER = 4 # 超采样倍数
- SIZE = 200 # 最终尺寸
- S = SIZE * SUPER # 绘制画布尺寸
- BODY = (140, 100, 68, 255) # 主体
- BODY_LIGHT = (176, 136, 100, 255) # 高光
- EDGE = (104, 72, 48, 255) # 描边
- CRACK = (94, 66, 44, 255) # 裂痕
- random.seed(20261018)
- def new_canvas():
- img = Image.new("RGBA", (S, S), (0, 0, 0, 0))
- return img, ImageDraw.Draw(img)
- def outlined_ellipse(draw, cx, cy, rx, ry, outline=5):
- """带描边的椭圆"""
- draw.ellipse([cx - rx - outline, cy - ry - outline, cx + rx + outline, cy + ry + outline], fill=EDGE)
- draw.ellipse([cx - rx, cy - ry, cx + rx, cy + ry], fill=BODY)
- def outlined_chain(draw, pts, r, outline=5):
- """圆形链:先画描边层再画主体层,内部不出现多余描边"""
- for (x, y) in pts:
- draw.ellipse([x - r - outline, y - r - outline, x + r + outline, y + r + outline], fill=EDGE)
- for (x, y) in pts:
- draw.ellipse([x - r, y - r, x + r, y + r], fill=BODY)
- def outlined_polygon(draw, pts, outline=5):
- """多边形:等比放大后作描边层"""
- cx = sum(p[0] for p in pts) / len(pts)
- cy = sum(p[1] for p in pts) / len(pts)
- scale = 1.0 + outline / 60.0
- big = [(cx + (x - cx) * scale, cy + (y - cy) * scale) for (x, y) in pts]
- draw.polygon(big, fill=EDGE)
- draw.polygon(pts, fill=BODY)
- def ragged_pts(cx, cy, rx, ry, n=48, noise=0.10):
- """不规则(毛边)轮廓点集"""
- pts = []
- for i in range(n):
- a = 2 * math.pi * i / n
- k = 1 + noise * math.sin(a * 3.1) + noise * 0.7 * math.sin(a * 7.7)
- pts.append((cx + rx * k * math.cos(a), cy + ry * k * math.sin(a)))
- return pts
- def curve(p0, p1, p2, steps=40):
- """二次贝塞尔采样,用于平滑弯曲的肠状轮廓"""
- out = []
- for i in range(steps + 1):
- t = i / steps
- x = (1 - t) ** 2 * p0[0] + 2 * (1 - t) * t * p1[0] + t ** 2 * p2[0]
- y = (1 - t) ** 2 * p0[1] + 2 * (1 - t) * t * p1[1] + t ** 2 * p2[1]
- out.append((x, y))
- return out
- def highlight(draw, cx, cy, rx, ry):
- """柔和高光(半透明,模拟体积)"""
- layer = Image.new("RGBA", (S, S), (0, 0, 0, 0))
- ld = ImageDraw.Draw(layer)
- ld.ellipse([cx - rx, cy - ry, cx + rx, cy + ry], fill=(255, 245, 235, 70))
- return Image.alpha_composite(draw._image, layer)
- def save(img, n):
- out = os.path.join(OUT_DIR, "bristol-%d.png" % n)
- img.resize((SIZE, SIZE), Image.LANCZOS).save(out, "PNG", optimize=True)
- print("已生成", os.path.relpath(out), os.path.getsize(out), "bytes")
- # ---- Type 1:一粒粒硬球(很难排出) ----
- def type1():
- img, d = new_canvas()
- for (cx, cy, r) in [(300, 300, 88), (500, 268, 76), (338, 512, 72), (536, 500, 80)]:
- outlined_ellipse(d, cx, cy, r, r * 0.94)
- return img
- # ---- Type 2:香肠状,表面凹凸(由小块拼成) ----
- def type2():
- img, d = new_canvas()
- pts = []
- radii = [46, 62, 52, 68, 50, 66, 54, 70, 48, 60, 42]
- for i, r in enumerate(radii):
- t = i / float(len(radii) - 1)
- x = 110 + t * 580
- y = 400 + 34 * math.sin(t * math.pi * 1.5) + (16 if i % 2 else -14)
- pts.append((x, y, r))
- for (x, y, r) in pts:
- d.ellipse([x - r - 5, y - r - 5, x + r + 5, y + r + 5], fill=EDGE)
- for (x, y, r) in pts:
- d.ellipse([x - r, y - r, x + r, y + r], fill=BODY)
- return img
- # ---- Type 3:香肠状,表面有裂痕(正常偏干) ----
- def type3():
- img, d = new_canvas()
- path = curve((130, 430), (400, 300), (670, 410))
- outlined_chain(d, [(x, y) for (x, y) in path], 78)
- # 沿肠体分布的不规则裂痕(垂直于走向,略带角度)
- for t, ang, length in ((0.22, -16, 118), (0.38, 12, 132), (0.56, -10, 108), (0.72, 18, 124), (0.88, -8, 96)):
- i = int(t * (len(path) - 1))
- x, y = path[i]
- i2 = min(len(path) - 1, i + 1)
- dx = path[i2][0] - x
- dy = path[i2][1] - y
- base = math.atan2(dy, dx) + math.pi / 2 + math.radians(ang)
- hx = math.cos(base) * length / 2.0
- hy = math.sin(base) * length / 2.0
- d.line([x - hx, y - hy, x + hx, y + hy], fill=CRACK, width=9)
- d.ellipse([x - hx - 4, y - hy - 4, x - hx + 4, y - hy + 4], fill=CRACK)
- d.ellipse([x + hx - 4, y + hy - 4, x + hx + 4, y + hy + 4], fill=CRACK)
- return img
- # ---- Type 4:光滑柔软(理想:像香肠或蛇) ----
- def type4():
- img, d = new_canvas()
- path = curve((120, 470), (400, 250), (690, 430), steps=60)
- outlined_chain(d, path, 74)
- # 背部高光带
- for i in range(6, len(path) - 6, 3):
- x, y = path[i]
- d.ellipse([x - 26, y - 44, x + 26, y - 16], fill=BODY_LIGHT)
- return img
- # ---- Type 5:软块状,边缘清晰(缺乏纤维) ----
- def type5():
- img, d = new_canvas()
- for (cx, cy, rx, ry) in [(268, 300, 86, 72), (512, 288, 78, 68),
- (240, 520, 74, 66), (470, 528, 92, 74), (660, 420, 66, 60)]:
- outlined_ellipse(d, cx, cy, rx, ry)
- return img
- # ---- Type 6:糊状,边缘不清(蓬松) ----
- def type6():
- img, d = new_canvas()
- for (cx, cy, rx, ry) in [(300, 320, 130, 100), (560, 300, 110, 88),
- (330, 540, 118, 92), (590, 540, 126, 96)]:
- outlined_polygon(d, ragged_pts(cx, cy, rx, ry, noise=0.14))
- return img
- # ---- Type 7:水样,无固形物(腹泻) ----
- def type7():
- img, d = new_canvas()
- puddle = ragged_pts(400, 430, 290, 132, n=72, noise=0.09)
- outlined_polygon(d, puddle)
- outlined_ellipse(d, 250, 190, 46, 38)
- outlined_ellipse(d, 566, 168, 34, 28)
- return img
- def main():
- os.makedirs(OUT_DIR, exist_ok=True)
- for n, fn in enumerate([type1, type2, type3, type4, type5, type6, type7], start=1):
- save(fn(), n)
- if __name__ == "__main__":
- main()
|