|
|
@@ -0,0 +1,189 @@
|
|
|
+#!/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()
|