feat: 数据真实化、企业展示墙、全局手势识别与 3D 模型完善

- 数据大屏改用真实数据(xls 统计表 + 宣传册口径);TOKEN/工具调用基于真实锚点测算
- 新增企业展示墙页(/wall,39 家全量档案 + MQTT/admin/ESP32 控制文档)
- 新增全局手势识别(GlobalVision 全局挂载 + dpm:gesture 事件分发)
- 3D 模型墙面逐间修复、去除门/过道顶棚/调试标签、房间资产不穿墙、会议室多小桌
- 侧栏企业分布扩展至 39 家完整信息,分区按官方四大孵化区域
- 语音页多轮对话展示优化、去除页脚与检测横条、背景对齐
This commit is contained in:
Pine
2026-08-18 05:48:56 +08:00
parent 1581b62154
commit 0af159a175
21 changed files with 1690 additions and 588 deletions
+188 -102
View File
@@ -1,138 +1,224 @@
# -*- coding: utf-8 -*-
"""园区数据模拟引擎 —— 由前端 parkData.js 移植,后端统一产生数据
前端通过 GET /api/dashboard/snapshot 获取,或订阅 MQTT opc/dashboard/tick
"""园区数据引擎(后端)—— 真实数据 + 测算演示
数据来源(用户指定口径):
· 《昆明市大学生创业园运行情况统计表(2026年7月).xls》(官方月报)
· 《昆明市创业园宣传册.pdf》
测算口径(用户确认):
· TOKEN:真实业务锚点月均 200 亿 tokens → 日均 ≈ 6.67 亿(200亿/30),
实时速率 ≈ 7,700 t/s(日均/86400s),近30日累计 = 200 亿
· 工具调用:按平均每次调用消耗约 2 万 tokens 折算(今日 ≈ 3.3 万次)
其余统计指标为真实静态值;TOKEN/工具仅做轻微演示波动
"""
import random
import threading
import time
MODEL_NAMES = ["DeepSeek-V3", "通义千问", "智谱 GLM-4", "豆包", "讯飞星火"]
TOOL_NAMES = ["文档生成", "数据查询", "图像创作", "代码执行", "语音合成"]
COMPANY_NAMES = [
"云南派音人工智能科技", "米勒克尔蓝宝石珠宝", "中泰研学合作", "云南宸中低空经济",
"昆明智海银高文化科技", "云南廷秀文旅康养", "瀚颖AI+教育信息咨询",
"仰光客厅", "云南上古绝学文化", "中越生物医疗", "酷享野农AI农业",
"滇缅国际设计", "昆明舒诺生物科技", "达岸教育管理", "花仙子园艺肥料",
"研X同行者网络", "鬼才明AI创意工作室", "朵哈·玫瑰特色产业链", "昆明云韵体育",
"南菌优培食用菌", "五华区丽裳文化", "云南星瑞航空", "综合直播私域平台",
"昆明屿澈电商", "蓝智科技", "云品出滇·纸享万家", "启元人工智能科技",
# ---------- 真实锚点(仅来自 xls 统计表 + 宣传册) ----------
REAL = {
"as_of": "2026-07",
"token_monthly_yi": 200, # 月均消耗 200 亿 tokens(真实业务锚点)
"token_daily_wan": 66666.7, # 日均 6.67 亿 tokens(200亿/30 ≈ 66666.7 万)
"token_rate": 7700, # 实时 t/s(6.67e8 / 86400 ≈ 7,716)
"tools_per_call_tokens": 150000, # 平均每次工具调用消耗 tokens(真实测算:12.5M输入 ÷ 81步 ≈ 15.4万)
"occupied": 39, # 园区实际入驻企业数(信息表名单 39 家,统一口径)
"capacity": 49, # 园区可容纳企业个数(统计表)
"invested": 560, # 开园以来累计投入运营资金(万元,统计表)
"jobs": 213, # 入驻企业累计带动就业人数(统计表)
"revenue_total": 252.15, # 入驻企业累计生产经营总额(万元,统计表)
"revenue_tax": 1.03, # 入驻企业累计上缴税利总额(万元,统计表)
"area": 3000, # 园区建筑面积(㎡,统计表+宣传册)
"founded": 2009, # 成立年份(宣传册)
"province_level": 2012, # 获评省级创业园年份(宣传册)
"address": "昆明市民航路229号(人力资源中心1-2楼)",
"phone": "18087174891",
"email": "714066050@qq.com",
}
# 入驻企业名录(《入驻企业信息表》39 家全称,与在园数统一口径)
COMPANIES = [
"云南派音人工智能科技有限公司", "米勒克尔蓝宝石珠宝产业化(项目)", "中泰研学合作(项目)",
"云南宸中低空经济有限公司", "昆明智海银高文化科技有限公司", "人工智能机器人大模型训练(项目)",
"云南廷秀文旅康养服务(项目)", "瀚颖AI+教育信息咨询(项目)", "云南大学AI+联合创业服务平台(项目)",
"仰光客厅(项目)", "云南上古绝学文化发展有限公司", "千楣之路(项目)",
"中越生物医疗(项目)", "中越文旅交流(项目)", "绿态环保科技项目",
"联翼航空技术低空经济应用国际化拓展(项目)", "酷享野农AI农业(项目)", "滇缅国际设计(项目)",
"昆明舒诺生物科技有限责任公司", "达岸教育管理(云南)有限公司", "昆明滇油石化有限责任公司",
"花仙子园艺肥料(云南)有限公司", "Facebook越南市场普洱茶与建水紫陶壶跨境电商(项目)",
"研X——同行者网络(项目)", "鬼才明AI创意工作室(项目)", "朵哈·玫瑰特色产业链(项目)",
"昆明云韵体育有限责任公司", "昆明舒护安养老服务有限责任公司",
"南菌优培—食用菌专用生物制剂创新与高原特色野生菌高值化全链开发(项目)",
"五华区丽裳文化艺术工作室", "云南星瑞航空(项目)", "综合性云南直播私域服务平台",
"昆明屿澈电子商务有限公司", "中外青少年研学(项目)", "星杭商务信息咨询昆明有限责任公司",
"蓝智科技(项目)", "云品出滇·纸享万家(项目)",
"云迹轻旅—昆明本土一站式轻量化文旅服务工作室(项目)", "启元人工智能科技(项目)",
]
# 宣传册四大孵化区域
ZONES = [
{"name": "OPC创业空间", "desc": "AI 技术支撑 · 数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域", "period": "拎包入驻 · 最长2年"},
{"name": "创业苗圃区", "desc": "创意阶段 · 未注册企业团队 · 开放式共享工位", "period": "最长2年"},
{"name": "孵化加速区", "desc": "已工商注册初创企业 · 独立办公空间", "period": "基础2年 · 可延1年(≤3年)"},
{"name": "国际创客区", "desc": "归国留学生 · 外籍留学生 · 国际化配套服务", "period": "参照加速区"},
]
# 园区企业行业分布(按《入驻企业信息表》39 家企业简介分类统计)
INDUSTRY_MIX = [
{"name": "人工智能", "value": 7},
{"name": "跨境电商", "value": 5},
{"name": "教育培训", "value": 4},
{"name": "文旅康养", "value": 4},
{"name": "文化创意", "value": 4},
{"name": "生物医药", "value": 4},
{"name": "低空经济", "value": 3},
{"name": "现代农业", "value": 3},
{"name": "绿色环保", "value": 2},
{"name": "企业服务", "value": 2},
{"name": "新材料", "value": 1},
]
# 园区实时动态(真实事件:园区官方公示 + 宣传册活动 + 互联网公开信息,最新在前)
FEED = [
{"id": 24, "icon": "dna", "text": "园内企业「昆明舒诺生物科技」完成GEO服务体系建设(生成式引擎优化,聚焦宠物营养品牌数字化)", "time": "2026-08"},
{"id": 1, "icon": "activity", "text": "2026年7月运行情况统计表填报完成:累计投入运营资金560万元、带动就业213人", "time": "2026-07-31"},
{"id": 2, "icon": "radio", "text": "2026年第三期入驻创业企业(项目)评审结果在昆明市人社局官网公示", "time": "2026-07-02"},
{"id": 3, "icon": "graduation", "text": "云南师范大学就业创业实践基地落地园区 · 云南旅游职业学院开展就业创业交流", "time": "2026-06-03"},
{"id": 23, "icon": "radio", "text": "「PineSound」完成 ICP 备案(滇ICP备2026008517号)与公安备案,注册地址昆明市民航路301-307号", "time": "2026"},
{"id": 4, "icon": "rocket", "text": "\"滇水逐梦·雨林创航\"昆明·西双版纳创业路演PK赛成功举办", "time": "2026-05-20"},
{"id": 5, "icon": "flag", "text": "昆明市青年企业家协会创新创业基地在园区揭牌", "time": "2026-05-13"},
{"id": 6, "icon": "users", "text": "\"聚力赋能·共创未来\"高校创新创业交流暨项目路演活动圆满举行", "time": "2026-05-08"},
{"id": 7, "icon": "trophy", "text": "\"创赢未来\"2026创业大赛昆明选拔赛暨马兰花创业培训讲师大赛举行(13个项目参赛)", "time": "2026-04-22"},
{"id": 8, "icon": "broadcast", "text": "2026年第二期创业企业(项目)入驻招募公告发布(官网 www.kmyc.gov.cn)", "time": "2026-04-15"},
{"id": 21, "icon": "sparkles", "text": "「PineSound」AI驱动音频管理平台上线 pinesound.cn:智能配乐与音效生成,集成100W+音效库、50W+配乐库,全球版权授权(官网)", "time": "2026"},
{"id": 22, "icon": "medal", "text": "「PineSound」发布企业标准 Q/YNPY 001-2026《数字内容创作 配乐通用分类标准》", "time": "2026"},
{"id": 9, "icon": "award", "text": "2026年第一期入驻评审结果在昆明市人社局官网公示(23个优质项目正式入驻)", "time": "2026-03-27"},
{"id": 10, "icon": "users", "text": "2026年第一期招募集中评审完成:38个申请,23个优质项目正式入驻", "time": "2026-03-24"},
{"id": 19, "icon": "sparkles", "text": "园内企业「云南派音人工智能科技(PineSound)」专注多模态音频技术研发:自研Pine系列模型覆盖音频识别、向量嵌入、音效生成、配乐创作", "time": "2026-04"},
{"id": 20, "icon": "bolt", "text": "「PineSound」2026年4月注册成立,获北京投资支持并吸纳就业4人,入驻加速区(入驻企业信息表)", "time": "2026-04"},
{"id": 11, "icon": "cpu", "text": "云南首个人工智能OPC创新人才基地落地昆明,填补省内个体AI创业培育空白(公开报道)", "time": "2026-03-23"},
{"id": 12, "icon": "medal", "text": "第九届\"春城创业荟\"创业创新大赛圆满闭幕,获奖项目名单公布", "time": "2025-09-30"},
{"id": 13, "icon": "trophy", "text": "\"春城创业荟\"初赛落幕,147个项目晋级复赛", "time": "2025-04-28"},
{"id": 14, "icon": "building", "text": "园内企业「花仙子园艺肥料(云南)」成立,注册资本118万元(公开报道)", "time": "2025-04-28"},
{"id": 15, "icon": "radio", "text": "2025年入园项目评审结果公示(昆明市人社局官网)", "time": "2025-04-22"},
{"id": 16, "icon": "atom", "text": "研X平台入选\"创客中国\"项目库(一站式研究生学术成长平台)", "time": "2025-01"},
{"id": 17, "icon": "leaf", "text": "行业动态:园内「朵哈·玫瑰」原料产地昆明八街食用玫瑰行情上涨,玫瑰经济走强", "time": "2025-01"},
{"id": 18, "icon": "heart", "text": "行业动态:「千楣之路」主营檀娜卡——泰国天然护肤品牌Thanaka正式进入中国市场", "time": "2025-01"},
]
def _pick(arr):
return arr[random.randrange(len(arr))]
def _day_secs():
"""当日零点起已过秒数(时钟驱动,保证数值单向持续增长、重启连续)"""
now = time.time()
lt = time.localtime(now)
day_start = time.mktime((lt.tm_year, lt.tm_mon, lt.tm_mday, 0, 0, 0, 0, 0, -1))
return max(0.0, now - day_start)
def _now_time():
return time.strftime("%H:%M:%S")
def _today_values(prev_noise=None):
"""按真实速率推算当日累计值(单向增长 + 工具调用波动):
平台实时速率 = 单会话 157 tok/s × 49 路并发 ≈ 7,700 t/s
今日 TOKEN(万)= 秒数 × 7,700 / 10000 = 秒数 × 0.77
今日工具调用 = 今日 TOKEN / 15万 tokens 每次(真实基线)
+ 随机游走噪声(±10,波浪式波动,不单调)
"""
secs = _day_secs()
today_wan = round(secs * 0.77) # 万 tokens
base_tools = secs * 0.77 * 10000 / REAL["tools_per_call_tokens"] # 真实基线(浮点)
if prev_noise is None:
noise = 0.0
else:
noise = max(-10.0, min(10.0, prev_noise + (random.random() - 0.5) * 6.0))
today_tools = max(0, round(base_tools + noise)) # 次
return today_wan, today_tools, noise
def make_series(base, growth, noise, n=30):
def _make_series(base, noise, n=30):
"""生成围绕日均锚点的演示曲线(单位:万 tokens),尾点=今日实时值"""
out = []
v = base
for _ in range(n):
v = v * (1 + growth) + (random.random() - 0.5) * noise
out.append(round(max(1, v * 100) / 100, 2))
for _ in range(n - 1):
v = base + (random.random() - 0.5) * noise
out.append(round(max(1, v), 1))
out.append(round(base, 1))
return out
def gen_event():
def _live_values(prev=None):
"""TOKEN 面板实时运行指标(围绕真实基准动态波动,会话持续推进)"""
r = random.random
pool = [
{"icon": "bolt", "text": f"AI 推理任务完成 · 消耗 {round(800 + r() * 9000):,} tokens({_pick(MODEL_NAMES)})"},
{"icon": "wrench", "text": f"工具「{_pick(TOOL_NAMES)}」被调用 {round(10 + r() * 90)} 次"},
{"icon": "building", "text": f"「{_pick(COMPANY_NAMES)}」提交入驻申请 · 进入评审流程"},
{"icon": "users", "text": f"「{_pick(COMPANY_NAMES)}」新增招聘岗位 {round(1 + r() * 5)} 个"},
{"icon": "coin", "text": f"园区企业完成一笔 ¥{(0.5 + r() * 9):.1f}万 交易"},
{"icon": "robot", "text": f"「{_pick(MODEL_NAMES)}」模型完成一次微调任务"},
]
e = _pick(pool)
return {"id": f"{int(time.time()*1000)}-{random.random()}", "icon": e["icon"], "text": e["text"], "time": _now_time()}
def init_feed():
return [
{"id": 1, "icon": "bolt", "text": "云南派音AI 完成音频向量嵌入任务 · 消耗 12,480 tokens", "time": _now_time()},
{"id": 2, "icon": "building", "text": "「启元人工智能科技」通过评审 · 正式入驻 OPC 创业空间", "time": _now_time()},
{"id": 3, "icon": "users", "text": "「昆明舒护安养老服务」新增招聘岗位 2 个", "time": _now_time()},
{"id": 4, "icon": "coin", "text": "园区企业完成一笔 ¥3.6万 交易", "time": _now_time()},
]
steps = (prev or {}).get("steps", 81)
steps += 1 if r() < 0.3 else 0
return {
"firstToken": round(1.3 + (r() - 0.5) * 0.2, 2), # 首 token 平均 1.3s 基准 ±0.1
"throughput": round(157 + (r() - 0.5) * 14), # 吞吐 157 tok/s 基准 ±7
"cacheHit": round(98 + (r() - 0.5) * 1.2, 1), # 缓存命中 98% 基准 ±0.6
"inputM": round((prev or {}).get("inputM", 12.5) + 0.004 + r() * 0.008, 2), # 会话输入持续推进
"outputM": round((prev or {}).get("outputM", 2.0) + 0.001 + r() * 0.002, 2), # 会话输出持续推进
"rounds": max(2, steps // 40), # 每 40 步一轮
"steps": steps, # 步数持续增长
}
def init_snapshot():
today_wan, today_tools, noise = _today_values()
return {
"t": 0,
"token": {"today": 128.64, "total": 12840, "rate": 84.6, "series": make_series(82, 0.012, 9)},
"tools": {"today": 3568, "total": 365204, "success": 98.7},
"projects": {"inPark": 158, "cum": 208, "todayNew": 2},
"jobs": {"total": 2186, "todayNew": 3},
"revenue": {"today": 38.6, "total": 20800, "growth": 8.2, "series": make_series(30, 0.006, 4)},
"park": {"devices": 98.6, "energy": 386, "people": 127, "desk": 76, "meeting": 3, "nodes": 12},
"feed": init_feed(),
"as_of": REAL["as_of"],
"_toolsNoise": noise,
"live": _live_values(),
"token": {
"today": today_wan, # 万 tokens(当日累计,实时增长)
"total": REAL["token_monthly_yi"] * 10000 + today_wan, # 万 tokens(近30日 = 200亿 + 今日增量)
"rate": REAL["token_rate"], # t/s(157 tok/s × 49 并发 ≈ 7,700)
"series": _make_series(REAL["token_daily_wan"], REAL["token_daily_wan"] * 0.16),
},
"tools": {
"today": today_tools, # 次(随 TOKEN 实时联动)
"total": round(REAL["token_monthly_yi"] * 100000000 / REAL["tools_per_call_tokens"]) + today_tools, # 次
"success": 98.5, # 成功率(演示)
"nodes": 12, # AI 服务节点(演示)
},
"projects": {
"inPark": REAL["occupied"], # 实际入驻 39 家
"capacity": REAL["capacity"], # 可容纳 49 个
"invested": REAL["invested"], # 累计投入 560 万元
},
"jobs": {"total": REAL["jobs"]}, # 带动就业 213 人
"revenue": {"total": REAL["revenue_total"], "tax": REAL["revenue_tax"]}, # 万元
"park": {
"area": REAL["area"],
"founded": REAL["founded"],
"provinceLevel": REAL["province_level"],
"address": REAL["address"],
"phone": REAL["phone"],
"email": REAL["email"],
},
"zones": ZONES,
"companies": COMPANIES,
"industryMix": INDUSTRY_MIX,
"feed": FEED,
}
def next_snapshot(s):
r = random.random
t_delta = round(0.26 + r() * 0.34, 2)
"""时钟驱动:按真实速率重算当日累计(TOKEN 单向增长,工具调用波浪波动),其余真实静态"""
today_wan, today_tools, noise = _today_values(s.get("_toolsNoise"))
token = {
"today": round(s["token"]["today"] + t_delta, 2),
"total": round(s["token"]["total"] + t_delta, 2),
"rate": round(76 + r() * 20, 1),
"today": today_wan,
"total": REAL["token_monthly_yi"] * 10000 + today_wan,
"rate": REAL["token_rate"],
"series": s["token"]["series"][:-1] + [today_wan],
}
tool_delta = round(13 + r() * 22)
tools = {
"today": s["tools"]["today"] + tool_delta,
"total": s["tools"]["total"] + tool_delta,
"success": round(98.1 + r() * 1.2, 1),
}
rev_delta = round(0.6 + r() * 1.7, 1)
revenue = {
"today": round(s["revenue"]["today"] + rev_delta, 1),
"total": round(s["revenue"]["total"] + rev_delta, 1),
"growth": round(7.2 + r() * 2.2, 1),
}
park = {
"devices": round(97.6 + r() * 1.6, 1),
"energy": round(320 + r() * 130),
"people": round(80 + r() * 95),
"desk": round(62 + r() * 24),
"meeting": round(2 + r() * 4),
"today": today_tools,
"total": round(REAL["token_monthly_yi"] * 100000000 / REAL["tools_per_call_tokens"]) + today_tools,
"success": 98.5,
"nodes": 12,
}
projects = dict(s["projects"])
if r() < 0.055:
projects["todayNew"] += 1
if r() < 0.035:
projects["inPark"] += 1
projects["cum"] += 1
jobs = dict(s["jobs"])
if r() < 0.08:
jobs["todayNew"] += 1
if r() < 0.05:
jobs["total"] += 1
t_series = list(s["token"]["series"])
t_series[-1] = round(t_series[-1] + t_delta, 2)
if s["t"] % 12 == 11:
t_series = t_series[1:] + [round(72 + r() * 30, 2)]
token["series"] = t_series
r_series = list(s["revenue"]["series"])
r_series[-1] = round(r_series[-1] + rev_delta, 1)
if s["t"] % 12 == 11:
r_series = r_series[1:] + [round(26 + r() * 9, 1)]
revenue["series"] = r_series
feed = [gen_event()] + s["feed"][:6] if s["t"] % 3 == 2 else s["feed"]
return {
"t": s["t"] + 1,
"token": token, "tools": tools, "revenue": revenue,
"park": park, "projects": projects, "jobs": jobs, "feed": feed,
**s,
"_toolsNoise": noise,
"live": _live_values(s.get("live")),
"token": token,
"tools": tools,
}
+68 -19
View File
@@ -34,12 +34,17 @@ KP_LEFT_ELBOW = 7
KP_RIGHT_ELBOW = 8
KP_LEFT_WRIST = 9
KP_RIGHT_WRIST = 10
RAISE_LIFT = 0.12 # 举手:手腕高于对应肩 12% 画面高
RAISE_LIFT = 0.06 # 举手:手腕高于对应肩 6% 画面高(降低阈值,更易稳定激活)
FIST_ELBOW_LIFT = 0.05 # 举拳:手腕高于肘 5% 画面高(前臂上举)
FIST_SHOULDER_GAP = 0.15 # 举拳:拳不高于肩 15% 画面高(收在胸前)
FIST_CHEST_DIST = 0.25 # 举拳:拳与肩水平距离 < 25% 画面宽(贴近躯干)
KP_CONF_MIN = 0.3 # 关键点置信度过滤
# ── 扩展手势几何阈值(基于 COCO 17 关键点) ──
POINT_REACH_X = 0.30 # 指向:手腕水平伸出距肩 ≥30% 画面宽(手臂向前/侧伸)
POINT_Y_RANGE = 0.28 # 指向:手腕与肩同高 ±28% 画面高(排除高举/下垂)
HANDS_CLOSE_DIST = 0.18 # 双手合十/靠近:双腕欧氏距离 <18% 画面宽
_face_model = None
_pose_model = None
@@ -64,15 +69,22 @@ def get_pose_model():
return _pose_model
def _parse_pose(results):
"""解析姿态结果 → persons(17 关键点归一化 [x,y,conf])+ raised(举手)+ fists(举拳)"""
def _parse_pose(results, W, H):
"""解析姿态结果 → persons + raised(举手) + fists(举拳)
+ both_up(双臂举起) + pointing(指向) + hands_close(双手合十)
+ hands(每手几何摘要,供前端挥手/手势跟随时序判定)
注意:ultralytics keypoints.data 为【原图像素坐标】,
此处统一归一化为 [0,1](x/W, y/H)后再判定阈值与输出
"""
persons = []
raised = []
fists = []
pointing = []
hands = []
for r in results:
if r.keypoints is None:
continue
kps = r.keypoints.data # [N,17,3] 归一化坐标
kps = r.keypoints.data # [N,17,3] 像素坐标
for i in range(kps.shape[0]):
kp = kps[i]
person = [
@@ -81,6 +93,7 @@ def _parse_pose(results):
]
persons.append(person)
# 每只手臂:举手(腕明显高于肩)或 举拳(前臂上举、拳收胸前),两者互斥
arm = {}
for side, wrist_i, elbow_i, shoulder_i in (
("left", KP_LEFT_WRIST, KP_LEFT_ELBOW, KP_LEFT_SHOULDER),
("right", KP_RIGHT_WRIST, KP_RIGHT_ELBOW, KP_RIGHT_SHOULDER),
@@ -90,22 +103,53 @@ def _parse_pose(results):
s = kp[shoulder_i]
if float(w[2]) < KP_CONF_MIN or float(s[2]) < KP_CONF_MIN or float(e[2]) < KP_CONF_MIN:
continue
wx, wy = float(w[0]), float(w[1])
sx, sy = float(s[0]), float(s[1])
ey = float(e[1])
if wy < sy - RAISE_LIFT:
raised.append({"side": side, "x": round(wx, 3), "y": round(wy, 3)})
elif (
wy < ey - FIST_ELBOW_LIFT # 前臂上举(腕高于肘)
and wy >= sy - FIST_SHOULDER_GAP # 拳不高过肩太多(收在胸前)
and abs(wx - sx) < FIST_CHEST_DIST # 拳贴近躯干中线
# 像素 → 归一化 [0,1]
wx, wy = float(w[0]) / W, float(w[1]) / H
sx, sy = float(s[0]) / W, float(s[1]) / H
ey = float(e[1]) / H
is_raised = wy < sy - RAISE_LIFT
is_fist = (
wy < ey - FIST_ELBOW_LIFT
and wy >= sy - FIST_SHOULDER_GAP
and abs(wx - sx) < FIST_CHEST_DIST
)
if is_raised:
raised.append({"side": side, "x": round(wx, 4), "y": round(wy, 4)})
elif is_fist:
fists.append({"side": side, "x": round(wx, 4), "y": round(wy, 4)})
# 指向:手腕水平伸出距肩较远、且与肩同高区间(前伸/侧伸,排除高举与下垂)
if (
abs(wx - sx) > POINT_REACH_X
and abs(wy - sy) < POINT_Y_RANGE
):
fists.append({"side": side, "x": round(wx, 3), "y": round(wy, 3)})
return persons, raised, fists
pointing.append({"side": side, "x": round(wx, 4), "y": round(wy, 4)})
arm[side] = {
"side": side,
"wx": round(wx, 4), "wy": round(wy, 4),
"sx": round(sx, 4), "sy": round(sy, 4),
"raised": is_raised, "fist": is_fist,
}
if arm:
hands.append(arm)
# 双臂举起:左、右腕都高于各自肩
both_up = False
if len(hands) >= 1:
h0 = hands[0]
if "left" in h0 and "right" in h0:
both_up = h0["left"]["raised"] and h0["right"]["raised"]
# 双手合十/靠近:同一人的双腕欧氏距离 < 阈值
hands_close = False
if len(hands) >= 1:
h0 = hands[0]
if "left" in h0 and "right" in h0:
dx = h0["left"]["wx"] - h0["right"]["wx"]
dy = h0["left"]["wy"] - h0["right"]["wy"]
hands_close = (dx * dx + dy * dy) ** 0.5 < HANDS_CLOSE_DIST
return persons, raised, fists, both_up, pointing, hands_close, hands
def predict_jpeg(jpeg_bytes: bytes):
"""人脸检测 + 姿态估计 → {faces, boxes, pose, raised, latency_ms}"""
"""人脸检测 + 姿态估计 → {faces, boxes, pose, raised, fists, both_up, pointing, hands_close, hands, latency_ms}"""
t0 = time.time()
img = Image.open(io.BytesIO(jpeg_bytes)).convert("RGB")
@@ -121,13 +165,14 @@ def predict_jpeg(jpeg_bytes: bytes):
boxes.append({"box": xyxy, "conf": conf})
# 姿态
W, H = img.size # 原图尺寸(关键点像素坐标 → 归一化基准)
pose_res = get_pose_model().predict(img, conf=CONF_THRESHOLD, imgsz=IMGSZ, verbose=False, device="cpu")
persons, raised, fists = _parse_pose(pose_res)
persons, raised, fists, both_up, pointing, hands_close, hands = _parse_pose(pose_res, W, H)
latency_ms = round((time.time() - t0) * 1000, 1)
log.info(
"vision: 人脸 %d 姿态 %d 举手 %d 举拳 %d · %.0fms",
len(boxes), len(persons), len(raised), len(fists), latency_ms,
"vision: 人脸 %d 姿态 %d 举手 %d 举拳 %d 指向 %d 合十 %s · %.0fms",
len(boxes), len(persons), len(raised), len(fists), len(pointing), hands_close, latency_ms,
)
return {
"faces": len(boxes),
@@ -135,6 +180,10 @@ def predict_jpeg(jpeg_bytes: bytes):
"pose": persons,
"raised": raised,
"fists": fists,
"both_up": both_up,
"pointing": pointing,
"hands_close": hands_close,
"hands": hands,
"latency_ms": latency_ms,
}
+15
View File
@@ -130,11 +130,26 @@
<div class="mqtt-group-title">页面导航</div>
<div class="mqtt-row">
<button class="page-switch-btn" data-mqtt='{"action":"navigate","params":{"page":"/voice"}}'>语音对话</button>
<button class="page-switch-btn" data-mqtt='{"action":"navigate","params":{"page":"/wall"}}'>企业展示墙</button>
<button class="page-switch-btn" data-mqtt='{"action":"navigate_rel","params":{"delta":-1}}'>← 向左</button>
<button class="page-switch-btn" data-mqtt='{"action":"navigate_rel","params":{"delta":1}}'>向右 →</button>
</div>
</div>
<!-- 企业展示墙 -->
<div class="mqtt-group">
<div class="mqtt-group-title">企业展示墙(/wall)</div>
<div class="mqtt-row">
<button class="page-switch-btn" data-mqtt='{"action":"wall_pause","params":{}}'>暂停滚动</button>
<button class="page-switch-btn" data-mqtt='{"action":"wall_resume","params":{}}'>继续滚动</button>
</div>
<div class="mqtt-row">
<button class="page-switch-btn" data-mqtt='{"action":"wall_speed","params":{"speed":"slow"}}'>慢速</button>
<button class="page-switch-btn" data-mqtt='{"action":"wall_speed","params":{"speed":"normal"}}'>标准</button>
<button class="page-switch-btn" data-mqtt='{"action":"wall_speed","params":{"speed":"fast"}}'>快速</button>
</div>
</div>
<!-- 视觉识别 -->
<div class="mqtt-group">
<div class="mqtt-group-title">人物识别(全局)</div>