798 lines
34 KiB
Python
798 lines
34 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Colorful diagram assets for defense PPT v2."""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
|
|
|
OUT = Path(__file__).resolve().parent / "assets_v2"
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Colorful palette
|
|
C = {
|
|
"navy": (15, 32, 90),
|
|
"blue": (37, 99, 235),
|
|
"cyan": (6, 182, 212),
|
|
"teal": (20, 184, 166),
|
|
"green": (34, 197, 94),
|
|
"lime": (132, 204, 22),
|
|
"yellow": (234, 179, 8),
|
|
"orange": (249, 115, 22),
|
|
"coral": (251, 113, 133),
|
|
"pink": (236, 72, 153),
|
|
"purple": (168, 85, 247),
|
|
"violet": (139, 92, 246),
|
|
"indigo": (99, 102, 241),
|
|
"slate": (51, 65, 85),
|
|
"dark": (15, 23, 42),
|
|
"muted": (100, 116, 139),
|
|
"light": (248, 250, 252),
|
|
"white": (255, 255, 255),
|
|
"soft": (241, 245, 249),
|
|
}
|
|
|
|
|
|
def font(size, bold=False):
|
|
cands = [
|
|
r"C:\Windows\Fonts\msyhbd.ttc" if bold else r"C:\Windows\Fonts\msyh.ttc",
|
|
r"C:\Windows\Fonts\simhei.ttf",
|
|
r"C:\Windows\Fonts\arial.ttf",
|
|
]
|
|
for p in cands:
|
|
try:
|
|
return ImageFont.truetype(p, size)
|
|
except OSError:
|
|
continue
|
|
return ImageFont.load_default()
|
|
|
|
|
|
def rr(d, xy, r, fill, outline=None, w=2):
|
|
d.rounded_rectangle(xy, radius=r, fill=fill, outline=outline, width=w)
|
|
|
|
|
|
def tc(d, xy, text, f, fill):
|
|
b = d.textbbox((0, 0), text, font=f)
|
|
d.text((xy[0] - (b[2] - b[0]) // 2, xy[1] - (b[3] - b[1]) // 2), text, font=f, fill=fill)
|
|
|
|
|
|
def tl(d, xy, text, f, fill):
|
|
d.text(xy, text, font=f, fill=fill)
|
|
|
|
|
|
def gradient(w, h, c1, c2, horizontal=False):
|
|
img = Image.new("RGB", (w, h), c1)
|
|
px = img.load()
|
|
for y in range(h):
|
|
for x in range(w):
|
|
t = (x / max(w - 1, 1)) if horizontal else (y / max(h - 1, 1))
|
|
px[x, y] = tuple(int(c1[i] * (1 - t) + c2[i] * t) for i in range(3))
|
|
return img
|
|
|
|
|
|
def cover():
|
|
w, h = 1920, 1080
|
|
img = gradient(w, h, (15, 23, 72), (8, 100, 140))
|
|
ov = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(ov)
|
|
colors = [C["cyan"], C["purple"], C["coral"], C["teal"], C["orange"], C["indigo"]]
|
|
for i, (cx, cy, r) in enumerate([(200, 180, 200), (1700, 200, 240), (300, 900, 180), (1600, 880, 220), (960, 540, 380)]):
|
|
col = colors[i % len(colors)]
|
|
d.ellipse((cx - r, cy - r, cx + r, cy + r), outline=(*col, 70), width=4)
|
|
pts = [(280, 300), (520, 220), (800, 400), (1100, 280), (1400, 450), (1650, 320), (1200, 700), (700, 750)]
|
|
for i in range(len(pts) - 1):
|
|
d.line([pts[i], pts[i + 1]], fill=(*C["cyan"], 90), width=3)
|
|
for i, p in enumerate(pts):
|
|
col = colors[i % len(colors)]
|
|
d.ellipse((p[0] - 10, p[1] - 10, p[0] + 10, p[1] + 10), fill=(*col, 200))
|
|
# glass panel
|
|
d.rounded_rectangle((160, 200, 1760, 880), radius=36, fill=(10, 25, 60, 150), outline=(*C["cyan"], 120), width=3)
|
|
# colorful bars
|
|
for i, col in enumerate([C["coral"], C["orange"], C["yellow"], C["teal"], C["blue"], C["purple"]]):
|
|
d.rounded_rectangle((220 + i * 250, 780, 440 + i * 250, 820), radius=10, fill=(*col, 220))
|
|
img = Image.alpha_composite(img.convert("RGBA"), ov).convert("RGB")
|
|
img.save(OUT / "cover.png", quality=95)
|
|
|
|
|
|
def dual_chain():
|
|
w, h = 1500, 700
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (40, 20), "双核心业务链路", font(28, True), C["dark"])
|
|
chains = [
|
|
(70, "链路 A · 医学影像 AI 读片", C["blue"], C["cyan"],
|
|
[("影像检查", "X光/CT/MRI/超声"), ("YOLO 检测", "框选+置信度"), ("结构化报告", "所见/印象/建议")]),
|
|
(390, "链路 B · 电子病历辅助决策", C["purple"], C["pink"],
|
|
[("病历录入", "主诉→诊断→用药"), ("RAG + LLM", "知识检索生成"), ("决策建议", "治疗/护理/随访")]),
|
|
]
|
|
for y, title, c1, c2, nodes in chains:
|
|
rr(d, (30, y, w - 30, y + 280), 20, C["white"], c1, 3)
|
|
d.rectangle((30, y, 48, y + 280), fill=c1)
|
|
tl(d, (60, y + 18), title, font(24, True), c1)
|
|
bw, bh = 360, 120
|
|
gap = 40
|
|
for i, (a, b) in enumerate(nodes):
|
|
x = 70 + i * (bw + gap)
|
|
col = c1 if i % 2 == 0 else c2
|
|
rr(d, (x, y + 90, x + bw, y + 90 + bh), 16, col)
|
|
tc(d, (x + bw // 2, y + 125), a, font(22, True), C["white"])
|
|
tc(d, (x + bw // 2, y + 165), b, font(16), (240, 250, 255))
|
|
if i < 2:
|
|
d.polygon([(x + bw + 8, y + 150), (x + bw + 32, y + 140), (x + bw + 32, y + 160)], fill=c1)
|
|
img.save(OUT / "dual_chain.png", quality=95)
|
|
|
|
|
|
def goals_radar():
|
|
"""Five goal cards as colorful strip."""
|
|
w, h = 1500, 320
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
goals = [
|
|
("业务闭环", "登录·患者·影像\n病历·预约·用户", C["blue"]),
|
|
("AI 可演示", "YOLO 实检\nLLM/模板降级", C["purple"]),
|
|
("前后端分离", "Vue3 + Boot\n+ FastAPI", C["teal"]),
|
|
("可离线实训", "默认 H2\nAI 可降级", C["orange"]),
|
|
("安全可讲", "JWT·角色\nBCrypt·双端", C["coral"]),
|
|
]
|
|
for i, (t, desc, col) in enumerate(goals):
|
|
x = 25 + i * 295
|
|
rr(d, (x, 30, x + 280, 290), 18, C["white"], col, 3)
|
|
rr(d, (x, 30, x + 280, 100), 18, col)
|
|
d.rectangle((x, 80, x + 280, 100), fill=col)
|
|
tc(d, (x + 140, 65), t, font(20, True), C["white"])
|
|
for j, ln in enumerate(desc.split("\n")):
|
|
tc(d, (x + 140, 150 + j * 36), ln, font(16), C["dark"])
|
|
img.save(OUT / "goals.png", quality=95)
|
|
|
|
|
|
def scenes():
|
|
w, h = 1500, 380
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
cards = [
|
|
("影像科", "上传胸片→AI诊断\n标注框·置信度·报告", C["cyan"]),
|
|
("临床医生", "病历关键词→建议\n治疗·护理·随访·RAG", C["blue"]),
|
|
("管理员", "LLM配置·知识库\nYOLO权重·用户", C["purple"]),
|
|
("挂号窗口", "预约状态流转\n预约→确认→完成", C["orange"]),
|
|
]
|
|
for i, (t, desc, col) in enumerate(cards):
|
|
x = 20 + i * 370
|
|
rr(d, (x, 25, x + 350, 350), 20, C["white"], col, 3)
|
|
rr(d, (x, 25, x + 350, 110), 20, col)
|
|
d.rectangle((x, 90, x + 350, 110), fill=col)
|
|
tc(d, (x + 175, 70), t, font(24, True), C["white"])
|
|
d.ellipse((x + 145, 135, x + 205, 195), fill=col)
|
|
tc(d, (x + 175, 165), str(i + 1), font(22, True), C["white"])
|
|
for j, ln in enumerate(desc.split("\n")):
|
|
tc(d, (x + 175, 230 + j * 32), ln, font(15), C["dark"])
|
|
img.save(OUT / "scenes.png", quality=95)
|
|
|
|
|
|
def pain_transform():
|
|
w, h = 1400, 520
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (30, 15), "从教学原型到三端 AI 混合架构", font(24, True), C["dark"])
|
|
rr(d, (40, 70, 520, 480), 18, C["white"], C["coral"], 3)
|
|
rr(d, (40, 70, 520, 130), 18, C["coral"])
|
|
d.rectangle((40, 110, 520, 130), fill=C["coral"])
|
|
tc(d, (280, 100), "早期痛点", font(22, True), C["white"])
|
|
for i, t in enumerate(["业务割裂 · 缺 360° 档案", "AI 假数据 · 无真实检测", "Thymeleaf 前后端耦合", "Session 权限薄弱", "依赖失败整链中断"]):
|
|
tl(d, (70, 160 + i * 55), "● " + t, font(18), C["dark"])
|
|
|
|
# arrow
|
|
d.polygon([(560, 250), (680, 220), (680, 240), (780, 240), (780, 260), (680, 260), (680, 280)], fill=C["teal"])
|
|
|
|
rr(d, (820, 70, 1360, 480), 18, C["white"], C["teal"], 3)
|
|
rr(d, (820, 70, 1360, 130), 18, C["teal"])
|
|
d.rectangle((820, 110, 1360, 130), fill=C["teal"])
|
|
tc(d, (1090, 100), "当前实现", font(22, True), C["white"])
|
|
for i, t in enumerate(["患者 360° 统一视图", "YOLO 实检 + LLM/RAG", "Vue3 SPA + REST", "JWT + 角色双端守卫", "AI 宕机可规则降级"]):
|
|
tl(d, (850, 160 + i * 55), "● " + t, font(18), C["dark"])
|
|
img.save(OUT / "pain.png", quality=95)
|
|
|
|
|
|
def priority():
|
|
w, h = 1500, 780
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (30, 15), "功能需求优先级看板", font(26, True), C["dark"])
|
|
cols = [
|
|
("P0 必须具备", C["coral"], [
|
|
"F-01 登录/改密 JWT+BCrypt",
|
|
"F-02 患者 CRUD + 360°",
|
|
"F-03 影像 CRUD + 上传",
|
|
"F-04 AI 诊断状态机",
|
|
"F-05 病历 + 辅助决策",
|
|
"F-06 角色权限双端",
|
|
]),
|
|
("P1 重要增强", C["orange"], [
|
|
"F-07 预约挂号全流程",
|
|
"F-08 仪表盘可视化",
|
|
"F-09 AI 对话助手",
|
|
"F-10 知识库 RAG",
|
|
"F-11 AI/LLM 配置",
|
|
"F-12 YOLO 权重管理",
|
|
]),
|
|
("P2 体验健壮", C["teal"], [
|
|
"F-13 AI 不可用降级",
|
|
"F-14 弹窗 append-to-body",
|
|
"F-15 报告分段可读",
|
|
"F-16 路由过渡体验",
|
|
]),
|
|
]
|
|
for i, (title, col, items) in enumerate(cols):
|
|
x = 30 + i * 490
|
|
rr(d, (x, 70, x + 460, h - 30), 18, C["white"], col, 3)
|
|
rr(d, (x, 70, x + 460, 140), 18, col)
|
|
d.rectangle((x, 120, x + 460, 140), fill=col)
|
|
tc(d, (x + 230, 105), title, font(22, True), C["white"])
|
|
yy = 170
|
|
for it in items:
|
|
rr(d, (x + 20, yy, x + 440, yy + 70), 12, C["soft"], col, 1)
|
|
tl(d, (x + 40, yy + 22), it, font(15), C["dark"])
|
|
yy += 85
|
|
img.save(OUT / "priority.png", quality=95)
|
|
|
|
|
|
def tech_stack():
|
|
w, h = 1500, 620
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (30, 15), "三层技术栈(版本对齐文档)", font(26, True), C["dark"])
|
|
layers = [
|
|
("前端 SPA :5173", C["blue"], "Vue 3.5.10 · Vite 5.4.8 · Element Plus 2.8.4 · Pinia · Router · Axios · ECharts · marked"),
|
|
("业务后端 :8080", C["purple"], "Spring Boot 3.3.4 · Java 17 · Security + jjwt 0.12.6 · JPA · H2/MySQL · Lombok · Maven"),
|
|
("AI 微服务 :8001", C["teal"], "FastAPI ≥0.110 · Ultralytics YOLO · OpenCV · LangChain ≥0.2 · DeepSeek 等 OpenAI 兼容"),
|
|
]
|
|
for i, (t, col, desc) in enumerate(layers):
|
|
y = 70 + i * 170
|
|
rr(d, (40, y, w - 40, y + 150), 18, C["white"], col, 4)
|
|
rr(d, (40, y, 280, y + 150), 18, col)
|
|
d.rectangle((220, y, 280, y + 150), fill=col)
|
|
tc(d, (160, y + 75), t, font(18, True), C["white"])
|
|
tl(d, (310, y + 55), desc, font(16), C["dark"])
|
|
img.save(OUT / "tech.png", quality=95)
|
|
|
|
|
|
def architecture():
|
|
w, h = 1600, 900
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (40, 20), "系统逻辑架构", font(28, True), C["dark"])
|
|
|
|
def box(x, y, bw, bh, title, lines, col):
|
|
rr(d, (x, y, x + bw, y + bh), 16, C["white"], col, 3)
|
|
rr(d, (x, y, x + bw, y + 48), 16, col)
|
|
d.rectangle((x, y + 30, x + bw, y + 48), fill=col)
|
|
tc(d, (x + bw // 2, y + 24), title, font(18, True), C["white"])
|
|
yy = y + 65
|
|
for ln in lines:
|
|
tc(d, (x + bw // 2, yy), ln, font(14), C["dark"])
|
|
yy += 24
|
|
|
|
box(560, 70, 480, 110, "浏览器 Vue SPA", ["localhost:5173 · Element Plus · ECharts"], C["blue"])
|
|
d.line([(800, 180), (800, 230)], fill=C["slate"], width=3)
|
|
d.polygon([(800, 240), (790, 225), (810, 225)], fill=C["slate"])
|
|
tl(d, (820, 200), "/api Vite 代理", font(14), C["muted"])
|
|
|
|
box(480, 250, 640, 130, "Spring Boot 业务端", ["8080 · JWT / JPA / 文件 · 鉴权落库 · 降级"], C["purple"])
|
|
d.line([(640, 380), (640, 450)], fill=C["blue"], width=3)
|
|
d.line([(1000, 380), (1000, 450)], fill=C["teal"], width=3)
|
|
d.line([(640, 450), (1000, 450)], fill=C["muted"], width=2)
|
|
d.polygon([(640, 465), (630, 450), (650, 450)], fill=C["blue"])
|
|
d.polygon([(1000, 465), (990, 450), (1010, 450)], fill=C["teal"])
|
|
|
|
box(280, 480, 420, 150, "H2 / MySQL", ["默认 H2 内存 · 可选 MySQL", "DataInitializer 种子数据"], C["orange"])
|
|
box(900, 480, 480, 170, "FastAPI AI 服务", ["8001 · YOLO · RAG · LLM", "失败可降级到模板规则"], C["teal"])
|
|
|
|
box(820, 720, 200, 100, "权重 pt", ["best.pt 等"], C["indigo"])
|
|
box(1040, 720, 200, 100, "知识 MD", ["高血压/肺炎…"], C["pink"])
|
|
box(1260, 720, 220, 100, "外部 LLM", ["DeepSeek 兼容"], C["green"])
|
|
d.line([(1140, 650), (920, 720)], fill=C["muted"], width=2)
|
|
d.line([(1140, 650), (1140, 720)], fill=C["muted"], width=2)
|
|
d.line([(1140, 650), (1370, 720)], fill=C["muted"], width=2)
|
|
|
|
rr(d, (40, 700, 760, 860), 14, C["white"], C["violet"], 2)
|
|
tl(d, (60, 720), "调用原则", font(18, True), C["violet"])
|
|
for i, p in enumerate(["1. 浏览器只访问业务后端", "2. AI 能力集中在 FastAPI", "3. 配置单向同步管理端→AI", "4. 失败可降级保证演示"]):
|
|
tl(d, (60, 760 + i * 24), p, font(14), C["dark"])
|
|
img.save(OUT / "architecture.png", quality=95)
|
|
|
|
|
|
def feature_panorama():
|
|
w, h = 1500, 800
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (30, 15), "智慧医院功能全景", font(26, True), C["dark"])
|
|
cols = [
|
|
("身份与权限", C["blue"], ["登录 / JWT", "修改密码", "角色菜单", "头像上传"]),
|
|
("业务主数据", C["purple"], ["患者管理", "用户管理", "知识库文档"]),
|
|
("诊疗业务", C["teal"], ["影像检查", "电子病历", "预约挂号", "仪表盘统计"]),
|
|
("智能能力", C["coral"], ["YOLO 影像检测", "AI 诊断报告", "EMR 决策+RAG", "AI 对话助手", "LLM/YOLO 管理"]),
|
|
]
|
|
for i, (title, col, items) in enumerate(cols):
|
|
x = 30 + i * 370
|
|
rr(d, (x, 70, x + 350, h - 30), 18, C["white"], col, 3)
|
|
rr(d, (x, 70, x + 350, 140), 18, col)
|
|
d.rectangle((x, 120, x + 350, 140), fill=col)
|
|
tc(d, (x + 175, 105), title, font(20, True), C["white"])
|
|
yy = 170
|
|
for it in items:
|
|
rr(d, (x + 20, yy, x + 330, yy + 70), 12, C["soft"])
|
|
d.ellipse((x + 40, yy + 22, x + 66, yy + 48), fill=col)
|
|
tl(d, (x + 80, yy + 22), it, font(16), C["dark"])
|
|
yy += 85
|
|
img.save(OUT / "features.png", quality=95)
|
|
|
|
|
|
def _arrow(d, x1, y1, x2, y2, fill, width=3):
|
|
"""Orthogonal-friendly straight segment with arrow head at end."""
|
|
d.line([(x1, y1), (x2, y2)], fill=fill, width=width)
|
|
# arrow head
|
|
if x2 == x1 and y2 > y1: # down
|
|
d.polygon([(x2, y2), (x2 - 7, y2 - 12), (x2 + 7, y2 - 12)], fill=fill)
|
|
elif x2 == x1 and y2 < y1: # up
|
|
d.polygon([(x2, y2), (x2 - 7, y2 + 12), (x2 + 7, y2 + 12)], fill=fill)
|
|
elif y2 == y1 and x2 > x1: # right
|
|
d.polygon([(x2, y2), (x2 - 12, y2 - 7), (x2 - 12, y2 + 7)], fill=fill)
|
|
elif y2 == y1 and x2 < x1: # left
|
|
d.polygon([(x2, y2), (x2 + 12, y2 - 7), (x2 + 12, y2 + 7)], fill=fill)
|
|
|
|
|
|
def _ortho(d, path, fill, width=3, arrow=True):
|
|
"""Draw polyline path [(x,y),...]; optional arrow on last segment."""
|
|
for i in range(len(path) - 1):
|
|
x1, y1 = path[i]
|
|
x2, y2 = path[i + 1]
|
|
if i == len(path) - 2 and arrow:
|
|
_arrow(d, x1, y1, x2, y2, fill, width)
|
|
else:
|
|
d.line([(x1, y1), (x2, y2)], fill=fill, width=width)
|
|
|
|
|
|
def imaging_flow():
|
|
"""Left-to-right pipeline with orthogonal connectors only."""
|
|
w, h = 1600, 880
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (30, 15), "AI 影像诊断核心流程", font(26, True), C["dark"])
|
|
|
|
# notes card top-right (no lines through it)
|
|
rr(d, (980, 50, 1560, 220), 14, C["white"], C["violet"], 2)
|
|
tl(d, (1000, 65), "技术要点", font(18, True), C["violet"])
|
|
for i, t in enumerate([
|
|
"· AsyncConfig 线程池,避免阻塞 HTTP",
|
|
"· 前端轮询有上限,卸载时清理",
|
|
"· AI 不可用 → Spring 本地规则降级",
|
|
"· 报告分段:所见 / 印象 / 建议 / 声明",
|
|
]):
|
|
tl(d, (1000, 105 + i * 28), t, font(14), C["dark"])
|
|
|
|
# Compact pipeline with guaranteed gaps between boxes
|
|
bh = 88
|
|
# (id, x, y, w, text, color)
|
|
spec = [
|
|
(0, 40, 300, 175, "新建影像\n上传图片", C["blue"]),
|
|
(1, 255, 300, 155, "PENDING", C["muted"]),
|
|
(2, 450, 300, 170, "点击AI诊断", C["orange"]),
|
|
(3, 660, 300, 170, "ANALYZING", C["coral"]),
|
|
(4, 870, 300, 170, "Spring\n@Async", C["purple"]),
|
|
(5, 1080, 300, 180, "FastAPI\nanalyze", C["teal"]),
|
|
(6, 900, 500, 180, "YOLO 检测\n标注图", C["indigo"]),
|
|
(7, 1160, 500, 180, "报告生成\nLLM/模板", C["cyan"]),
|
|
(8, 780, 700, 180, "写结果\n更新记录", C["green"]),
|
|
(9, 1020, 700, 190, "COMPLETED\n/ ERROR", C["green"]),
|
|
(10, 1270, 700, 200, "前端轮询\n报告弹窗", C["blue"]),
|
|
]
|
|
boxes = {}
|
|
for i, x, y, wbox, text, col in spec:
|
|
rr(d, (x, y, x + wbox, y + bh), 14, col)
|
|
lines = text.split("\n")
|
|
for j, ln in enumerate(lines):
|
|
tc(d, (x + wbox // 2, y + (bh // 2 - 12) + j * 26), ln, font(14, True), C["white"])
|
|
boxes[i] = (x, y, wbox, bh)
|
|
|
|
def mid_right(i):
|
|
x, y, wbox, bh_ = boxes[i]
|
|
return x + wbox, y + bh_ // 2
|
|
|
|
def mid_left(i):
|
|
x, y, wbox, bh_ = boxes[i]
|
|
return x, y + bh_ // 2
|
|
|
|
def mid_bottom(i):
|
|
x, y, wbox, bh_ = boxes[i]
|
|
return x + wbox // 2, y + bh_
|
|
|
|
def mid_top(i):
|
|
x, y, wbox, bh_ = boxes[i]
|
|
return x + wbox // 2, y
|
|
|
|
# main chain
|
|
for a, b in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]:
|
|
x1, y1 = mid_right(a)
|
|
x2, y2 = mid_left(b)
|
|
gap = x2 - x1
|
|
if gap > 16:
|
|
_arrow(d, x1 + 3, y1, x2 - 3, y2, C["slate"], 3)
|
|
|
|
# FastAPI -> split bus -> YOLO / Report
|
|
fx, fy = mid_bottom(5)
|
|
bus_y = 450
|
|
d.line([(fx, fy), (fx, bus_y)], fill=C["slate"], width=3)
|
|
yx, _ = mid_top(6)
|
|
rx, _ = mid_top(7)
|
|
d.line([(min(yx, fx), bus_y), (max(rx, fx), bus_y)], fill=C["slate"], width=3)
|
|
_arrow(d, yx, bus_y, yx, mid_top(6)[1] - 2, C["slate"], 3)
|
|
_arrow(d, rx, bus_y, rx, mid_top(7)[1] - 2, C["slate"], 3)
|
|
|
|
# merge YOLO + Report -> 写结果
|
|
merge_y = 640
|
|
d.line([(mid_bottom(6)[0], mid_bottom(6)[1]), (mid_bottom(6)[0], merge_y)], fill=C["slate"], width=3)
|
|
d.line([(mid_bottom(7)[0], mid_bottom(7)[1]), (mid_bottom(7)[0], merge_y)], fill=C["slate"], width=3)
|
|
d.line([(mid_top(8)[0], merge_y), (mid_bottom(7)[0], merge_y)], fill=C["slate"], width=3)
|
|
_arrow(d, mid_top(8)[0], merge_y, mid_top(8)[0], mid_top(8)[1] - 2, C["slate"], 3)
|
|
|
|
# bottom chain
|
|
for a, b in [(8, 9), (9, 10)]:
|
|
x1, y1 = mid_right(a)
|
|
x2, y2 = mid_left(b)
|
|
if x2 - x1 > 12:
|
|
_arrow(d, x1 + 3, y1, x2 - 3, y2, C["slate"], 3)
|
|
|
|
img.save(OUT / "imaging_flow.png", quality=95)
|
|
|
|
|
|
def decision_flow():
|
|
w, h = 1500, 500
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (30, 15), "电子病历辅助决策流程", font(24, True), C["dark"])
|
|
nodes = [
|
|
("保存病历", C["blue"]),
|
|
("Decision\nSupport", C["purple"]),
|
|
("FastAPI\n/report/decision", C["teal"]),
|
|
("RAG+LLM\n或模板回退", C["orange"]),
|
|
("七类建议\nDialog 展示", C["coral"]),
|
|
]
|
|
boxes = []
|
|
for i, (t, col) in enumerate(nodes):
|
|
x = 40 + i * 290
|
|
rr(d, (x, 70, x + 250, 200), 16, col)
|
|
for j, ln in enumerate(t.split("\n")):
|
|
tc(d, (x + 125, 120 + j * 30), ln, font(16, True), C["white"])
|
|
boxes.append((x, 70, 250, 130))
|
|
if i < 4:
|
|
# edge-to-edge arrow in the gap only
|
|
x1 = x + 250 + 6
|
|
x2 = x + 290 - 6
|
|
midy = 70 + 65
|
|
_arrow(d, x1, midy, x2, midy, C["slate"], 3)
|
|
|
|
# vertical from last node down to output row bus
|
|
last = boxes[-1]
|
|
lx = last[0] + last[2] // 2
|
|
d.line([(lx, last[1] + last[3]), (lx, 300)], fill=C["slate"], width=3)
|
|
d.line([(50, 300), (1450, 300)], fill=C["slate"], width=3)
|
|
|
|
outs = [("治疗", C["blue"]), ("用药", C["purple"]), ("护理", C["teal"]), ("随访", C["green"]),
|
|
("风险", C["orange"]), ("冲突", C["coral"]), ("RAG", C["indigo"])]
|
|
for i, (o, col) in enumerate(outs):
|
|
x = 50 + i * 205
|
|
_arrow(d, x + 90, 300, x + 90, 330, C["slate"], 2)
|
|
rr(d, (x, 335, x + 180, 450), 12, col)
|
|
tc(d, (x + 90, 392), o, font(16, True), C["white"])
|
|
img.save(OUT / "decision.png", quality=95)
|
|
|
|
|
|
def state_machine():
|
|
w, h = 1400, 280
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (30, 15), "影像诊断状态机", font(22, True), C["dark"])
|
|
states = [
|
|
("PENDING", "已登记待诊断", C["muted"]),
|
|
("ANALYZING", "诊断进行中", C["orange"]),
|
|
("COMPLETED", "成功可看报告", C["green"]),
|
|
("ERROR", "失败", C["coral"]),
|
|
]
|
|
for i, (n, desc, col) in enumerate(states):
|
|
x = 50 + i * 340
|
|
rr(d, (x, 70, x + 280, 220), 18, col)
|
|
tc(d, (x + 140, 120), n, font(20, True), C["white"])
|
|
tc(d, (x + 140, 170), desc, font(14), C["white"])
|
|
if i < 3:
|
|
d.polygon([(x + 290, 140), (x + 325, 130), (x + 325, 150)], fill=C["slate"])
|
|
img.save(OUT / "state.png", quality=95)
|
|
|
|
|
|
def yolo_concept():
|
|
w, h = 1000, 560
|
|
img = Image.new("RGB", (w, h), (20, 28, 48))
|
|
d = ImageDraw.Draw(img)
|
|
tc(d, (250, 30), "原始影像(示意)", font(18, True), C["cyan"])
|
|
tc(d, (750, 30), "YOLO 标注(示意)", font(18, True), C["coral"])
|
|
rr(d, (40, 60, 460, 500), 12, (30, 40, 60))
|
|
rr(d, (540, 60, 960, 500), 12, (30, 40, 60))
|
|
d.ellipse((90, 120, 220, 400), outline=(160, 190, 210), width=2)
|
|
d.ellipse((250, 120, 380, 400), outline=(160, 190, 210), width=2)
|
|
d.ellipse((590, 120, 720, 400), outline=(160, 190, 210), width=2)
|
|
d.ellipse((750, 120, 880, 400), outline=(160, 190, 210), width=2)
|
|
d.rectangle((600, 180, 700, 270), outline=C["teal"], width=3)
|
|
tl(d, (600, 155), "findings 0.87", font(14), C["teal"])
|
|
d.rectangle((780, 250, 870, 330), outline=C["orange"], width=3)
|
|
tl(d, (760, 225), "nodule 0.76", font(14), C["orange"])
|
|
tl(d, (40, 520), "仅教学演示概念图,非真实患者数据", font(13), C["muted"])
|
|
img.save(OUT / "yolo.png", quality=95)
|
|
|
|
|
|
def team_overview():
|
|
"""Six members in 2x3 grid — orthogonal tree, no diagonals through cards."""
|
|
w, h = 1500, 740
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (30, 12), "六人协作总览(按工程模块划分)", font(26, True), C["dark"])
|
|
|
|
# hub
|
|
hub = (450, 50, 1050, 115)
|
|
rr(d, hub, 16, C["navy"])
|
|
tc(d, (750, 82), "Smart Hospital · 6 人三端协作", font(20, True), C["white"])
|
|
|
|
# column centers and card geometry
|
|
cols_x = [40, 520, 1000] # left of cards
|
|
card_w, card_h = 460, 200
|
|
top_y, bot_y = 175, 460
|
|
cxs = [x + card_w // 2 for x in cols_x] # 270, 750, 1230
|
|
|
|
# tree connectors (draw first, under cards visually by being outside card bodies)
|
|
# hub bottom center down to bus
|
|
bus_y = 145
|
|
d.line([(750, 115), (750, bus_y)], fill=C["slate"], width=3)
|
|
d.line([(cxs[0], bus_y), (cxs[2], bus_y)], fill=C["slate"], width=3)
|
|
for cx in cxs:
|
|
d.line([(cx, bus_y), (cx, top_y)], fill=C["slate"], width=3)
|
|
|
|
# vertical between top and bottom cards in each column
|
|
for cx, x in zip(cxs, cols_x):
|
|
d.line([(cx, top_y + card_h), (cx, bot_y)], fill=C["slate"], width=3)
|
|
|
|
members = [
|
|
(cols_x[0], top_y, "成员A", "前端基础", "布局 · 登录 · 权限 · Pinia", C["blue"], "前端层"),
|
|
(cols_x[1], top_y, "成员B", "前端业务", "仪表盘 · 患者360° · 预约", C["cyan"], "前端层"),
|
|
(cols_x[2], top_y, "成员C", "前端智能", "影像 · 病历决策 · AI助手", C["purple"], "前端层"),
|
|
(cols_x[0], bot_y, "成员D", "后端主数据", "JWT · 患者 · 用户 · 预约", C["orange"], "业务后端"),
|
|
(cols_x[1], bot_y, "成员E", "后端 AI 桥", "异步诊断 · Client · 降级", C["coral"], "业务后端"),
|
|
(cols_x[2], bot_y, "成员F", "AI 微服务", "YOLO · RAG · LLM · 权重", C["teal"], "AI 服务"),
|
|
]
|
|
for x, y, name, role, desc, col, layer in members:
|
|
rr(d, (x, y, x + card_w, y + card_h), 18, C["white"], col, 3)
|
|
rr(d, (x, y, x + card_w, y + 58), 18, col)
|
|
d.rectangle((x, y + 42, x + card_w, y + 58), fill=col)
|
|
tc(d, (x + card_w // 2, y + 30), f"{name} · {role}", font(17, True), C["white"])
|
|
tc(d, (x + card_w // 2, y + 105), desc, font(15), C["dark"])
|
|
rr(d, (x + 140, y + 145, x + 320, y + 178), 10, col)
|
|
tc(d, (x + card_w // 2, y + 161), layer, font(13, True), C["white"])
|
|
|
|
tl(d, (40, 690), "树状正交连线:总枢纽 → 三列 → 上下两排;连线只走卡片外空隙,不穿过文字", font(14), C["muted"])
|
|
img.save(OUT / "team.png", quality=95)
|
|
|
|
|
|
def team_swimlane():
|
|
"""Wide short swimlane (fits full PPT width without looking tiny)."""
|
|
w, h = 1600, 360 # ~4.4:1 — scales to full slide width
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (20, 8), "六人分工泳道 · 交付物对照(全宽)", font(20, True), C["dark"])
|
|
rows = [
|
|
("A 前端基础", C["blue"], "BasicLayout · 登录 · 路由守卫 · Pinia · 个人中心"),
|
|
("B 前端业务", C["cyan"], "Dashboard · Patients 360° · Appointments · ECharts"),
|
|
("C 前端智能", C["purple"], "Imaging 报告弹窗 · EMR 决策 Dialog · AI 助手 · 知识库页"),
|
|
("D 后端主数据", C["orange"], "Security/JWT · User · Patient · Appointment · Result"),
|
|
("E 后端 AI 桥", C["coral"], "AIDiagnosis 异步 · AiServiceClient · 决策对接 · 降级"),
|
|
("F AI 微服务", C["teal"], "YOLO · /imaging/analyze · RAG · LLM · 权重/配置"),
|
|
]
|
|
row_h, gap, top = 48, 6, 42
|
|
for i, (name, col, desc) in enumerate(rows):
|
|
y = top + i * (row_h + gap)
|
|
rr(d, (20, y, 280, y + row_h), 10, col)
|
|
tc(d, (150, y + row_h // 2), name, font(15, True), C["white"])
|
|
rr(d, (295, y, 1580, y + row_h), 10, C["white"], col, 2)
|
|
tl(d, (315, y + 12), desc, font(15), C["dark"])
|
|
img.save(OUT / "team_lane.png", quality=95)
|
|
|
|
|
|
def module_tree():
|
|
w, h = 1400, 620
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (30, 15), "仓库三工程结构", font(24, True), C["dark"])
|
|
roots = [
|
|
(40, "frontend/", C["blue"], ["api/ HTTP", "views/ 页面", "router 守卫", "stores Pinia"]),
|
|
(480, "smart-hospital/", C["purple"], ["controller/service", "security JWT", "uploads 文件", "data/ai 配置"]),
|
|
(920, "ai-service/", C["teal"], ["api imaging/rag", "yolo · llm", "knowledge MD", "weights pt"]),
|
|
]
|
|
for x, title, col, items in roots:
|
|
rr(d, (x, 70, x + 400, 580), 18, C["white"], col, 3)
|
|
rr(d, (x, 70, x + 400, 140), 18, col)
|
|
d.rectangle((x, 120, x + 400, 140), fill=col)
|
|
tc(d, (x + 200, 105), title, font(20, True), C["white"])
|
|
yy = 170
|
|
for it in items:
|
|
rr(d, (x + 30, yy, x + 370, yy + 80), 12, C["soft"])
|
|
tl(d, (x + 55, yy + 25), it, font(16), C["dark"])
|
|
yy += 95
|
|
img.save(OUT / "modules.png", quality=95)
|
|
|
|
|
|
def demo_path():
|
|
w, h = 1500, 300
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (20, 10), "推荐演示剧本(7 步)", font(20, True), C["dark"])
|
|
steps = [
|
|
("1", "doctor1\n登录", C["blue"]),
|
|
("2", "患者\n360°", C["cyan"]),
|
|
("3", "影像\nAI诊断", C["purple"]),
|
|
("4", "病历\n决策", C["teal"]),
|
|
("5", "预约\n流转", C["orange"]),
|
|
("6", "admin\n配置", C["coral"]),
|
|
("7", "降级\n验证", C["indigo"]),
|
|
]
|
|
for i, (n, t, col) in enumerate(steps):
|
|
x = 40 + i * 210
|
|
d.ellipse((x + 55, 55, x + 115, 115), fill=col)
|
|
tc(d, (x + 85, 85), n, font(20, True), C["white"])
|
|
for j, ln in enumerate(t.split("\n")):
|
|
tc(d, (x + 85, 140 + j * 28), ln, font(14), C["dark"])
|
|
if i < 6:
|
|
d.line([(x + 125, 85), (x + 195, 85)], fill=C["muted"], width=3)
|
|
img.save(OUT / "demo.png", quality=95)
|
|
|
|
|
|
def closing():
|
|
w, h = 1920, 1080
|
|
img = gradient(w, h, (30, 20, 80), (10, 90, 120))
|
|
ov = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(ov)
|
|
for i, col in enumerate([C["cyan"], C["purple"], C["coral"], C["teal"], C["orange"], C["blue"]]):
|
|
ang = i * 60
|
|
import math
|
|
cx = 960 + int(280 * math.cos(math.radians(ang)))
|
|
cy = 480 + int(200 * math.sin(math.radians(ang)))
|
|
d.ellipse((cx - 40, cy - 40, cx + 40, cy + 40), fill=(*col, 160))
|
|
d.ellipse((860, 380, 1060, 580), outline=(*C["cyan"], 180), width=5)
|
|
d.rounded_rectangle((940, 430, 980, 530), radius=6, fill=(*C["teal"], 220))
|
|
d.rounded_rectangle((910, 460, 1010, 500), radius=6, fill=(*C["teal"], 220))
|
|
img = Image.alpha_composite(img.convert("RGBA"), ov).convert("RGB")
|
|
img.save(OUT / "closing.png", quality=95)
|
|
|
|
|
|
def nfr_icons():
|
|
w, h = 1500, 280
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
items = [
|
|
("性能", "@Async 异步\n有限轮询", C["blue"]),
|
|
("可用性", "H2 开箱\n种子数据", C["teal"]),
|
|
("安全", "JWT·BCrypt\nCORS", C["purple"]),
|
|
("可维护", "Result<T>\n全局异常", C["orange"]),
|
|
("可扩展", "AI 独立\n进程解耦", C["coral"]),
|
|
("兼容", "OpenAI\n协议可切换", C["indigo"]),
|
|
]
|
|
for i, (t, desc, col) in enumerate(items):
|
|
x = 20 + i * 245
|
|
rr(d, (x, 25, x + 230, 250), 16, C["white"], col, 3)
|
|
rr(d, (x, 25, x + 230, 90), 16, col)
|
|
d.rectangle((x, 70, x + 230, 90), fill=col)
|
|
tc(d, (x + 115, 55), t, font(18, True), C["white"])
|
|
for j, ln in enumerate(desc.split("\n")):
|
|
tc(d, (x + 115, 130 + j * 35), ln, font(14), C["dark"])
|
|
img.save(OUT / "nfr.png", quality=95)
|
|
|
|
|
|
def er_mini():
|
|
"""
|
|
Clean ER with orthogonal bus connectors — lines never cross through card text.
|
|
Layout (文档 8.1 / 8.4):
|
|
User
|
|
______|______
|
|
| | |
|
|
Imaging EMR Appointment
|
|
| | |
|
|
+------+------+
|
|
|
|
|
Patient
|
|
Imaging
|
|
|
|
|
AIResult
|
|
"""
|
|
w, h = 1200, 640
|
|
img = Image.new("RGB", (w, h), C["light"])
|
|
d = ImageDraw.Draw(img)
|
|
tl(d, (24, 12), "核心实体关系(正交连线)", font(22, True), C["dark"])
|
|
|
|
def ent(x, y, bw, title, fields, col):
|
|
bh = 44 + 22 * len(fields) + 14
|
|
rr(d, (x, y, x + bw, y + bh), 12, C["white"], col, 2)
|
|
rr(d, (x, y, x + bw, y + 40), 12, col)
|
|
d.rectangle((x, y + 28, x + bw, y + 40), fill=col)
|
|
tc(d, (x + bw // 2, y + 20), title, font(16, True), C["white"])
|
|
yy = y + 52
|
|
for f in fields:
|
|
tl(d, (x + 16, yy), f, font(13), C["dark"])
|
|
yy += 22
|
|
# return box edges
|
|
return {"x": x, "y": y, "w": bw, "h": bh, "cx": x + bw // 2, "cy": y + bh // 2,
|
|
"top": (x + bw // 2, y), "bottom": (x + bw // 2, y + bh),
|
|
"left": (x, y + bh // 2), "right": (x + bw, y + bh // 2)}
|
|
|
|
BW = 220
|
|
user = ent(490, 50, BW, "User", ["ADMIN/DOCTOR/RADIO", "BCrypt 密码"], C["purple"])
|
|
imaging = ent(120, 250, BW, "Imaging", ["类型/状态", "图像 URL"], C["teal"])
|
|
emr = ent(490, 250, BW, "EMR", ["主诉→随访", "决策关联"], C["orange"])
|
|
appt = ent(860, 250, BW, "Appointment", ["状态机", "按日筛选"], C["indigo"])
|
|
patient = ent(490, 450, BW, "Patient", ["档案字段", "既往史"], C["blue"])
|
|
ai = ent(120, 450, BW, "AIResult", ["置信度/检测框", "完整报告"], C["coral"])
|
|
|
|
line_c = C["slate"]
|
|
|
|
# User bottom -> horizontal bus -> drop to each mid entity top
|
|
bus_y = 200
|
|
ux, uy = user["bottom"]
|
|
d.line([(ux, uy), (ux, bus_y)], fill=line_c, width=3)
|
|
# bus spans imaging.cx to appt.cx
|
|
d.line([(imaging["cx"], bus_y), (appt["cx"], bus_y)], fill=line_c, width=3)
|
|
for box in (imaging, emr, appt):
|
|
_arrow(d, box["cx"], bus_y, box["cx"], box["top"][1] - 2, line_c, 3)
|
|
|
|
# Mid entities bottom -> lower bus -> Patient top
|
|
bus2_y = 400
|
|
for box in (imaging, emr, appt):
|
|
d.line([(box["cx"], box["bottom"][1]), (box["cx"], bus2_y)], fill=line_c, width=3)
|
|
d.line([(imaging["cx"], bus2_y), (appt["cx"], bus2_y)], fill=line_c, width=3)
|
|
_arrow(d, patient["cx"], bus2_y, patient["cx"], patient["top"][1] - 2, line_c, 3)
|
|
|
|
# Imaging → AIResult: continuous vertical (bus2 crosses same x — OK for tree)
|
|
ix = imaging["cx"]
|
|
d.line([(ix, imaging["bottom"][1]), (ix, ai["top"][1] - 2)], fill=line_c, width=3)
|
|
_arrow(d, ix, ai["top"][1] - 14, ix, ai["top"][1] - 2, line_c, 3)
|
|
# 1:1 label placed to the LEFT of the stem, clear of bus
|
|
tl(d, (ix - 48, (imaging["bottom"][1] + bus2_y) // 2 - 6), "1:1", font(12, True), C["coral"])
|
|
|
|
# relationship captions on buses (right side, clear of arrows)
|
|
tl(d, (740, 178), "医生创建", font(12), C["muted"])
|
|
tl(d, (740, 378), "归属患者", font(12), C["muted"])
|
|
|
|
# legend
|
|
rr(d, (860, 480, 1160, 600), 12, C["white"], C["soft"], 2)
|
|
tl(d, (880, 495), "关系说明", font(14, True), C["dark"])
|
|
tl(d, (880, 525), "User → 影像/病历/预约", font(12), C["muted"])
|
|
tl(d, (880, 550), "Patient ← 三类业务记录", font(12), C["muted"])
|
|
tl(d, (880, 575), "Imaging → AIResult (1:1)", font(12), C["muted"])
|
|
|
|
img.save(OUT / "er.png", quality=95)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cover()
|
|
dual_chain()
|
|
goals_radar()
|
|
scenes()
|
|
pain_transform()
|
|
priority()
|
|
tech_stack()
|
|
architecture()
|
|
feature_panorama()
|
|
imaging_flow()
|
|
decision_flow()
|
|
state_machine()
|
|
yolo_concept()
|
|
team_overview()
|
|
team_swimlane()
|
|
module_tree()
|
|
demo_path()
|
|
closing()
|
|
nfr_icons()
|
|
er_mini()
|
|
print("assets_v2 done ->", OUT)
|