本 skill 封装了超广角眼底图像的 AI 分析完整流程,直接调用 HTTP API(HMAC-SHA256 签名鉴权)。
超广角眼底相机可一次性拍摄更大范围的眼底图像(通常可达 150°-200°),适用于周边视网膜病变的筛查。
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}127197b131f5c5a3e4af080fb9e70382244ba1271912(超广角多病种 AI)signature = HMAC-SHA256(token, appId + timestamp),timestamp 为毫秒级 Unix 时间戳appId / signature / timestamp上传接口选择规则:
studyupload/v2fileImageUpload/v1(无需压缩,支持 ≤100MB)studyupload/v2(支持 images 数组一次传多张)从文件名自动推断眼位:
OD、_R、right → descPosition=2(右眼)OS、_L、left → descPosition=1(左眼)0适用于:单张 ≤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,停止并告知用户上传失败原因images 数组中传入多张图片,分别设置 descPosition适用于:单张图片超过 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 标识为超广角图像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 报告
}
轮询策略:
code=0 且 data.ultraWideResult != nullcode=30008(继续等待)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)
超广角结果位于 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" → ❓ 不确定#!/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])
studyupload/v2(Base64);图片 >5MB 使用 fileImageUpload/v1(文件上传,≤100MB),无需压缩图片studyupload/v2,在 images 数组中传入多张图片aiType=12 区分;若使用 fileImageUpload/v1 接口,还可通过 cameraType=2 标识超广角studyupload/v2 使用 appId / signature / timestamp;fileImageUpload/v1 使用 god-portal-timestamp / god-portal-signature(签名算法相同)leftDetail.others / rightDetail.others 为 47 维整数数组,1=阳性、0=阴性、-1=不确定,具体映射见 reference.md共 1 个版本