返回 Skill 列表
extension
分类: 其它需要 API Key

腾讯健康-觅影-基于超广角眼底彩照的多病种/病灶 AI 分析

超广角眼底多病种 AI 分析服务。当用户需要上传超广角眼底图像进行 AI 诊断、分析超广角眼底照片时使用。支持推测诊断、体征判别、体征检测和体征分割四类 AI 能力。触发词:超广角眼底、超广角AI、广角眼底分析、ultra-wide fundus、超广角诊断。

person作者: u_a4bd9e57hubenterprise

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-ID12719
  • APP-TOKEN7b131f5c5a3e4af080fb9e70382244ba
  • hospitalId:与 APP-ID 相同,即 12719
  • aiType12(超广角多病种 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_Rright → descPosition=2(右眼)
  • 文件名含 OS_Lleft → 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=0data.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 / timestampfileImageUpload/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 分析能力