Ultra-Wide Fundus AI - 超广角眼底多病种 AI 分析 Skill
概述
本 skill 封装了超广角眼底图像的 AI 分析完整流程,直接调用 HTTP API(HMAC-SHA256 签名鉴权)。
超广角眼底相机可一次性拍摄更大范围的眼底图像(通常可达 150°-200°),适用于周边视网膜病变的筛查。
- 上传接口(Base64):
POST https://pacs.qq.com/thirdparty/studyupload/v2/{appId}(≤5MB,支持多图) - 上传接口(文件):
POST https://pacs.qq.com/thirdparty/fileImageUpload/v1/{appId}(≤100MB,单图) - 查询接口:
POST https://pacs.qq.com/thirdparty/queryEyeAIResult/{appId} - APP-ID:
12719 - APP-TOKEN:
7b131f5c5a3e4af080fb9e70382244ba - hospitalId:与 APP-ID 相同,即
12719 - aiType:
12(超广角多病种 AI) - 鉴权方式:
signature = HMAC-SHA256(token, appId + timestamp),timestamp 为毫秒级 Unix 时间戳 - 请求头:
appId/signature/timestamp
执行流程(Agent 必须严格按此顺序执行)
Step 1:上传图片,获取 study_id
上传接口选择规则:
- 图片 ≤ 5MB → 使用 Base64 上传接口
studyupload/v2 - 图片 > 5MB → 使用文件上传接口
fileImageUpload/v1(无需压缩,支持 ≤100MB) - 需要一次上传多张图片 → 使用 Base64 上传接口
studyupload/v2(支持 images 数组一次传多张)
从文件名自动推断眼位:
- 文件名含
OD、_R、right→ descPosition=2(右眼) - 文件名含
OS、_L、left→ descPosition=1(左眼) - 无法判断 → descPosition=
0
1a. Base64 上传(图片 ≤5MB 或多图上传)
适用于:单张 ≤5MB 的图片,或需要一次上传多张图片(如左右眼同时上传)。
import hmac, hashlib, time, base64, json, requests
app_id = "12719"
token = "7b131f5c5a3e4af080fb9e70382244ba"
img_path = "<图片绝对路径>"
# 1. Base64 编码图片
with open(img_path, 'rb') as f:
img_base64 = base64.b64encode(f.read()).decode('utf-8')
# 2. 生成签名
timestamp = str(int(time.time() * 1000))
message = app_id + timestamp
signature = hmac.new(
token.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 3. 上传
upload_url = f"https://pacs.qq.com/thirdparty/studyupload/v2/{app_id}"
study_id = f"uw_{int(time.time())}"
payload = {
"studyId": study_id,
"studyName": "超广角眼底检查",
"studyDate": int(time.time()),
"studyType": 2,
"patientName": "患者",
"patientId": "p001",
"patientGender": "1",
"patientBirthday": "1980-01-01",
"images": [
{
"imageId": "img001",
"content": img_base64,
"descPosition": "2" # 1=左眼, 2=右眼, 0=未知
}
]
}
headers = {
'appId': app_id,
'signature': signature,
'timestamp': timestamp,
'Content-Type': 'application/json; charset=utf-8'
}
resp = requests.post(upload_url, headers=headers, json=payload, timeout=120)
result = resp.json()
# 成功: {"code":0,"message":"上传成功","requestId":"...","data":{}}
成功响应:{"code":0,"message":"上传成功","data":{}}
- 若
code != 0,停止并告知用户上传失败原因 - 所有图片总大小 ≤ 5MB
- 多图上传时,在
images数组中传入多张图片,分别设置descPosition
1b. 文件上传(图片 >5MB,单张)
适用于:单张图片超过 5MB 的大图,直接以文件形式上传,无需压缩。
import hmac, hashlib, time, json, requests
app_id = "12719"
token = "7b131f5c5a3e4af080fb9e70382244ba"
img_path = "<图片绝对路径>"
# 1. 生成签名
timestamp = str(int(time.time() * 1000))
message = app_id + timestamp
signature = hmac.new(
token.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 2. 上传(form-data 方式,请求头使用 god-portal-* 前缀)
upload_url = f"https://pacs.qq.com/thirdparty/fileImageUpload/v1/{app_id}"
study_id = f"uw_{int(time.time())}"
# 从文件名推断眼位
fname = img_path.upper()
if any(x in fname for x in ['OD', '_R', 'RIGHT']):
desc_position = "2"
elif any(x in fname for x in ['OS', '_L', 'LEFT']):
desc_position = "1"
else:
desc_position = "0"
headers = {
'god-portal-timestamp': timestamp,
'god-portal-signature': signature,
}
form_data = {
'studyId': study_id,
'studyName': '超广角眼底检查',
'studyDate': str(int(time.time())),
'studyType': '2',
'imageId': 'img001',
'descPosition': desc_position,
'cameraType': '2', # 2=超广角
}
files = {
'file': (img_path.split('/')[-1], open(img_path, 'rb'), 'image/jpeg')
}
resp = requests.post(upload_url, headers=headers, data=form_data, files=files, timeout=120)
result = resp.json()
# 成功: {"code":0,"message":"上传成功","requestId":"...","data":{}}
成功响应:{"code":0,"message":"上传成功","data":{}}
- 若
code != 0,停止并告知用户上传失败原因 - 注意:此接口请求头使用
god-portal-timestamp/god-portal-signature(与 Base64 接口的appId/signature/timestamp不同) cameraType=2标识为超广角图像- 此接口仅支持单张上传,如需多图请使用 Base64 上传接口
Step 2:查询超广角 AI 分析结果(轮询)
query_url = f"https://pacs.qq.com/thirdparty/queryEyeAIResult/{app_id}"
payload = {
"hospitalId": app_id,
"studyId": study_id,
"aiType": 12, # 12 = 超广角多病种
"needReport": 1 # 1 = 生成 PDF 报告
}
轮询策略:
- 间隔:10 秒
- 超时上限:5 分钟(30 次)
- 成功条件:
code=0且data.ultraWideResult != null - 处理中:
code=30008(继续等待) - 失败条件:其他非零 code
for i in range(30):
# 每次请求重新生成签名
timestamp = str(int(time.time() * 1000))
signature = hmac.new(token.encode('utf-8'), (app_id + timestamp).encode('utf-8'), hashlib.sha256).hexdigest()
headers = {
'appId': app_id,
'signature': signature,
'timestamp': timestamp,
'Content-Type': 'application/json; charset=utf-8'
}
resp = requests.post(query_url, headers=headers, json=payload, timeout=30)
result = resp.json()
if result['code'] == 0:
ultra_wide = result.get('data', {}).get('ultraWideResult')
if ultra_wide:
print("分析完成")
break
elif result['code'] == 30008:
print("处理中,继续等待...")
else:
print(f"错误: {result['message']}")
break
time.sleep(10)
Step 3:格式化输出超广角 AI 分析结果
超广角结果位于 data.ultraWideResult,包含四大类输出:
输出模板
## 🔬 超广角眼底 AI 分析结果
**图像**:<文件名>(<左眼/右眼>)
**状态**:<处理状态>
---
### 📋 推测诊断(Inferred Diagnoses)
| 疾病 | 左眼 | 右眼 | 说明 |
|------|------|------|------|
| 视网膜动脉阻塞 | <leftValue> | <rightValue> | |
| 视网膜脱离 | <leftValue> | <rightValue> | |
| 视网膜裂孔 | <leftValue> | <rightValue> | |
| 视网膜脉络膜肿物 | <leftValue> | <rightValue> | |
| 视网膜周边变性 | <leftValue> | <rightValue> | |
| 先天性视盘异常 | <leftValue> | <rightValue> | |
| 大视杯(C/D>0.3) | <leftValue> | <rightValue> | |
| 视神经萎缩 | <leftValue> | <rightValue> | |
| 黄斑前膜 | <leftValue> | <rightValue> | |
| 黄斑浆液性脱离 | <leftValue> | <rightValue> | |
| 黄斑裂孔 | <leftValue> | <rightValue> | |
| 星状玻璃体变性 | <leftValue> | <rightValue> | |
| 玻璃体后脱离 | <leftValue> | <rightValue> | |
| 其他玻璃体异常/出血/混浊 | <leftValue> | <rightValue> | |
| 糖尿病视网膜病变(PDR) | <leftValue> | <rightValue> | |
| 糖尿病视网膜病变(NPDR) | <leftValue> | <rightValue> | |
| 视网膜色素变性 | <leftValue> | <rightValue> | |
| 病理性近视 | <leftValue> | <rightValue> | |
| 视网膜中央静脉阻塞 | <leftValue> | <rightValue> | |
| 视网膜分支静脉阻塞 | <leftValue> | <rightValue> | |
| 湿性年龄相关性黄斑变性 | <leftValue> | <rightValue> | |
| 干性年龄相关性黄斑变性 | <leftValue> | <rightValue> | |
| VKH | <leftValue> | <rightValue> | |
### 🔍 体征判别(Eye Screening)
| 眼别 | 筛查结果 |
|------|----------|
| 左眼 | <left> |
| 右眼 | <right> |
### 🧬 体征检测与分割(Eye Detail)
**左眼体征**:
- 出血/渗出掩膜:`hemohedgeMask`
- 棉絮斑掩膜:`cottonWoolSpotMask`
- 硬性渗出掩膜:`hardExudateMask`
- 新生血管掩膜:`neovascularizationMask`
- 高度近视视盘:`highMyopiaOpticDisc`
- 黄斑前膜:`macularEpiretinalMembrane`
- 视网膜纤维膜:`retinalFibrousMembrane`
- 视网膜裂孔:`retinalHole`
- 视网膜脱离:`retinalDetachment`
- 陈旧色素病灶:`retinalOldPigmentLesion`
- 47维体征数组:`others[]`(1=有,0=无,-1=不确定)
**右眼体征**:同上(`rightDetail` 字段)
📄 [查看完整 AI 诊断报告 PDF](<reportUrl>)
结果图标规则:
leftValue/rightValue="1"→⚠️ 阳性(加粗)leftValue/rightValue="0"→✅ 阴性leftValue/rightValue="-1"→❓ 不确定
完整示例(Python 封装)
#!/usr/bin/env python3
"""超广角眼底 AI 分析完整流程"""
import hmac, hashlib, time, base64, json, os, requests
APP_ID = "12719"
TOKEN = "7b131f5c5a3e4af080fb9e70382244ba"
SIZE_THRESHOLD = 5 * 1024 * 1024 # 5MB,超过此大小使用文件上传接口
def generate_signature(app_id, token):
timestamp = str(int(time.time() * 1000))
message = app_id + timestamp
signature = hmac.new(
token.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
return signature, timestamp
def infer_eye_position(img_path):
"""从文件名推断眼位"""
fname = img_path.upper()
if any(x in fname for x in ['OD', '_R', 'RIGHT']):
return "2"
elif any(x in fname for x in ['OS', '_L', 'LEFT']):
return "1"
return "0"
def upload_image(img_path, app_id=APP_ID, token=TOKEN):
"""Step 1: 上传图片(自动选择上传接口)
- 图片 > 5MB → fileImageUpload/v1(文件上传,无需压缩,单张 ≤100MB)
- 图片 ≤ 5MB → studyupload/v2(Base64 上传)
"""
file_size = os.path.getsize(img_path)
desc_position = infer_eye_position(img_path)
study_id = f"uw_{int(time.time())}"
if file_size > SIZE_THRESHOLD:
# 使用文件上传接口(form-data)
signature, timestamp = generate_signature(app_id, token)
url = f"https://pacs.qq.com/thirdparty/fileImageUpload/v1/{app_id}"
headers = {
'god-portal-timestamp': timestamp,
'god-portal-signature': signature,
}
form_data = {
'studyId': study_id,
'studyName': '超广角眼底检查',
'studyDate': str(int(time.time())),
'studyType': '2',
'imageId': 'img001',
'descPosition': desc_position,
'cameraType': '2',
}
files = {
'file': (img_path.split('/')[-1], open(img_path, 'rb'), 'image/jpeg')
}
resp = requests.post(url, headers=headers, data=form_data, files=files, timeout=120)
else:
# 使用 Base64 上传接口
with open(img_path, 'rb') as f:
img_base64 = base64.b64encode(f.read()).decode('utf-8')
signature, timestamp = generate_signature(app_id, token)
url = f"https://pacs.qq.com/thirdparty/studyupload/v2/{app_id}"
payload = {
"studyId": study_id,
"studyName": "超广角眼底检查",
"studyDate": int(time.time()),
"studyType": 2,
"images": [{
"imageId": "img001",
"content": img_base64,
"descPosition": desc_position
}]
}
headers = {
'appId': app_id,
'signature': signature,
'timestamp': timestamp,
'Content-Type': 'application/json; charset=utf-8'
}
resp = requests.post(url, headers=headers, json=payload, timeout=120)
result = resp.json()
if result.get('code') != 0:
raise RuntimeError(f"上传失败: {result.get('message')}")
return study_id, desc_position
def upload_multiple_images(img_paths, app_id=APP_ID, token=TOKEN):
"""Step 1-alt: 多图上传(使用 Base64 接口,适合一次上传左右眼图片)
所有图片总大小需 ≤ 5MB,超限时需逐张使用 upload_image。
"""
signature, timestamp = generate_signature(app_id, token)
study_id = f"uw_{int(time.time())}"
images = []
for i, img_path in enumerate(img_paths):
with open(img_path, 'rb') as f:
img_base64 = base64.b64encode(f.read()).decode('utf-8')
images.append({
"imageId": f"img{i+1:03d}",
"content": img_base64,
"descPosition": infer_eye_position(img_path)
})
payload = {
"studyId": study_id,
"studyName": "超广角眼底检查",
"studyDate": int(time.time()),
"studyType": 2,
"images": images
}
headers = {
'appId': app_id,
'signature': signature,
'timestamp': timestamp,
'Content-Type': 'application/json; charset=utf-8'
}
url = f"https://pacs.qq.com/thirdparty/studyupload/v2/{app_id}"
resp = requests.post(url, headers=headers, json=payload, timeout=120)
result = resp.json()
if result.get('code') != 0:
raise RuntimeError(f"上传失败: {result.get('message')}")
return study_id
def query_ultra_wide_result(study_id, app_id=APP_ID, token=TOKEN, max_poll=30):
"""Step 2: 轮询超广角 AI 结果"""
url = f"https://pacs.qq.com/thirdparty/queryEyeAIResult/{app_id}"
for i in range(max_poll):
signature, timestamp = generate_signature(app_id, token)
headers = {
'appId': app_id,
'signature': signature,
'timestamp': timestamp,
'Content-Type': 'application/json; charset=utf-8'
}
payload = {
"hospitalId": app_id,
"studyId": study_id,
"aiType": 12,
"needReport": 1
}
resp = requests.post(url, headers=headers, json=payload, timeout=30)
result = resp.json()
if result.get('code') == 0:
data = result.get('data', {})
ultra_wide = data.get('ultraWideResult')
if ultra_wide:
return ultra_wide, data.get('reportUrl')
elif result.get('code') == 30008:
pass # 处理中,继续等待
else:
raise RuntimeError(f"查询失败: {result.get('message')}")
time.sleep(10)
raise TimeoutError("轮询超时,AI 分析未完成")
def format_result(ultra_wide, report_url, img_name, eye_side):
"""Step 3: 格式化输出"""
status = ultra_wide.get('status', 0)
inferred = ultra_wide.get('inferredDiagnoses', [])
screening = ultra_wide.get('eyeScreening', {})
print(f"## 🔬 超广角眼底 AI 分析结果\n")
print(f"**图像**:{img_name}({eye_side})")
print(f"**状态**:{'处理成功' if status == 200 else '处理中/异常'}\n")
if inferred:
print("### 📋 推测诊断\n")
print("| 疾病 | 左眼 | 右眼 |")
print("|------|------|------|")
for d in inferred:
lv = d.get('leftValue', '-')
rv = d.get('rightValue', '-')
lv_icon = '⚠️' if lv == '1' else ('✅' if lv == '0' else '❓')
rv_icon = '⚠️' if rv == '1' else ('✅' if rv == '0' else '❓')
print(f"| {d.get('name', d.get('disease'))} | {lv_icon} {lv} | {rv_icon} {rv} |")
if screening:
print("\n### 🔍 体征判别\n")
print(f"- 左眼:{screening.get('left', 'N/A')}")
print(f"- 右眼:{screening.get('right', 'N/A')}")
if report_url:
print(f"\n📄 [查看完整 AI 报告]({report_url})")
# 主流程
def analyze_ultra_wide_fundus(img_path):
study_id, desc_pos = upload_image(img_path)
ultra_wide, report_url = query_ultra_wide_result(study_id)
eye_side_map = {"0": "未知", "1": "左眼", "2": "右眼"}
eye_side = eye_side_map.get(desc_pos, "未知")
format_result(ultra_wide, report_url, img_path.split('/')[-1], eye_side)
return ultra_wide, report_url
if __name__ == "__main__":
import sys
analyze_ultra_wide_fundus(sys.argv[1])
注意事项
- 支持格式:JPEG / PNG / DICOM
- 上传接口选择:图片 ≤5MB 使用
studyupload/v2(Base64);图片 >5MB 使用fileImageUpload/v1(文件上传,≤100MB),无需压缩图片 - 多图上传:如需一次上传多张图片(如左右眼同时上传),建议使用 Base64 上传接口
studyupload/v2,在images数组中传入多张图片 - 超广角标识:通过
aiType=12区分;若使用fileImageUpload/v1接口,还可通过cameraType=2标识超广角 - 请求头差异:
studyupload/v2使用appId/signature/timestamp;fileImageUpload/v1使用god-portal-timestamp/god-portal-signature(签名算法相同) - 图片质量:超广角图像分辨率建议 ≥ 2000×2000,确保周边视网膜清晰可见
- 47维体征数组:
leftDetail.others/rightDetail.others为 47 维整数数组,1=阳性、0=阴性、-1=不确定,具体映射见 reference.md - Mask 字段:分割结果以 gzip + base64 编码的 JSON 格式返回,需解码后使用
- 服务权限:若返回「请联系 miying@tencent.com 开通服务」,说明当前 APP-ID 尚未开通超广角 AI 分析能力
微信扫一扫