---
title: WingLens：从相机快门到观鸟记录，一张照片的完整旅程
slug: winglens-bird-recognition
description: 用 Sony A7C2 拍鸟后，照片如何自动传输到 iPhone、在 Mac 上完成 AI 识别，最终变成一条可追溯的观鸟记录。兼谈 1000+ 鸟种、35000+ 张训练数据背后的实践与教训。
date: 2026-07-20
category: Projects
tags:
  - 鸟类识别
  - Sony A7C II
  - YOLO
  - BioCLIP
  - SwiftUI
  - OrniVision
draft: false
featured: true
hidden: false
archived: false
project: winglens
---

去年秋天，我在北京的沙河水库拍到了一只不认识的鸟。

回家后我把照片导入电脑、裁切、上传到识鸟小程序，等了十几秒——结果是"白骨顶"。但翻看了几张示例图后，我又不确定了：它的额头没有白骨顶那么白，体型也偏瘦。可能是黑水鸡的亚成体，也可能是某种我完全没听过的秧鸡科鸟类。

那一刻我突然意识到：**拍鸟和识别鸟之间的那段路，太远了。**

相机里有几百张照片，每一张要经历：挑图 → 裁切 → 上传 → 等待 → 对比 → 记录。一天拍下来，整理比拍摄更累。

于是 WingLens 这个项目开始了。它的目标不是做一个"拍照识物"的 App——那种东西已经有很多了。我想做的是：**让相机拍下鸟的那一刻，就已经自动开始记录时间、地点、鸟种和置信度，最后只需要你点一下"确认"。**

---

## 一张照片的完整旅程

整个过程可以用一条线串起来：

```d2
direction: right

相机拍照: {
  shape: circle
  style.fill: "#282840"
  style.stroke: "#CBA6F7"
  style.stroke-width: 2
  width: 100
  height: 100
}

iPhone 取图: {
  shape: rectangle
  style.fill: "#1E1E2E"
  style.stroke: "#89B4FA"
  style.stroke-width: 1
  style.border-radius: 8
  width: 140
}

Mac 识别: {
  label: "Mac mini M4\nYOLO + BioCLIP"
  shape: class
  style.fill: "#282840"
  style.stroke: "#FAB387"
  style.stroke-width: 2
  style.border-radius: 12
  width: 200
}

确认记录: {
  shape: rectangle
  style.fill: "#1E1E2E"
  style.stroke: "#A6E3A1"
  style.stroke-width: 1
  style.border-radius: 8
  width: 130
}

数据回流: {
  shape: circle
  style.fill: "#282840"
  style.stroke: "#F5C2E7"
  style.stroke-width: 2
  width: 100
  height: 100
}

相机拍照 -> iPhone 取图: Wi-Fi / USB { style.stroke: "#6C7086"; style.stroke-width: 2 }
iPhone 取图 -> Mac 识别: 局域网 { style.stroke: "#6C7086"; style.stroke-width: 2 }
Mac 识别 -> 确认记录: Top-5 结果 { style.stroke: "#6C7086"; style.stroke-width: 2 }
确认记录 -> 数据回流: 标注数据 { style.stroke: "#F5C2E7"; style.stroke-width: 2; style.stroke-dash: 4 }
```

不复杂，但每个环节都要跑通，才能让"拍完即记录"成为现实。

---

## 第一环：相机和 iPhone 之间

Sony A7C II 是一台很好的拍鸟相机——对焦快、长焦够用、画质细腻。但相机不是手机，它不会自己把照片传出去。

相机和 iPhone 之间有三条路：

```d2
direction: right

相机: {
  shape: circle
  style.fill: "#282840"
  style.stroke: "#CBA6F7"
  style.stroke-width: 2
}

Wi-Fi PTP/IP: {
  label: "Wi-Fi\nPTP/IP 协议"
  shape: rectangle
  style.fill: "#1E1E2E"
  style.stroke: "#89B4FA"
  style.stroke-width: 1
  style.border-radius: 8
  width: 140
}

USB PTP: {
  label: "USB\n有线直连"
  shape: rectangle
  style.fill: "#1E1E2E"
  style.stroke: "#A6E3A1"
  style.stroke-width: 1
  style.border-radius: 8
  width: 140
}

SD 卡: {
  label: "SD 卡\n手动导入"
  shape: rectangle
  style.fill: "#1E1E2E"
  style.stroke: "#FAB387"
  style.stroke-width: 1
  style.border-radius: 8
  width: 140
}

iPhone: {
  shape: circle
  style.fill: "#282840"
  style.stroke: "#A6E3A1"
  style.stroke-width: 2
}

相机 -> Wi-Fi PTP/IP -> iPhone: 无线取图 { style.stroke: "#6C7086" }
相机 -> USB PTP -> iPhone: 有线取图 { style.stroke: "#6C7086" }
相机 -> SD 卡 -> iPhone: 手动导入 { style.stroke: "#6C7086" }
```

**Wi-Fi PTP/IP** 是索尼相机自带的协议。相机创建热点，iPhone 连接后可以通过 PTP 协议获取照片列表、下载原图、甚至遥控拍摄。Sony 官方提供了 `CameraRemoteCommand` SDK 参考实现（Linux 版），我基于它的 PTP 3 协议文档，用 Swift 实现了一个跨平台版本——**SonyCameraCross**，可以在 macOS/iPadOS/iOS 上编译运行。

**USB PTP** 连线更稳定，速度更快。macOS 上可以通过 `libusb` 走 Bulk PTP 通道；iOS 端目前只支持 Wi-Fi。

**SD 卡**是最传统的方案，但在户外拍鸟时不方便——你需要拔卡、插读卡器、等导入，鸟可能就飞走了。

第一版优先跑通 Wi-Fi 路径。拍照后 iPhone 自动从相机拉取最新照片，用户不需要任何操作。

---

## 第二环：AI 识别——找鸟，然后认鸟

图片到了 iPhone，下一步是识别。这是一个典型的"检测 + 分类"两阶段问题。

为什么不能直接把整张图丢给分类模型？因为照片里可能什么都没有——大片的天空、水面、树枝；也可能有多只鸟，分布在画面不同位置，主体只占几十个像素。

所以需要先"找到鸟"，再"认鸟"。

```d2
direction: right

原图: {
  shape: circle
  style.fill: "#282840"
  style.stroke: "#CBA6F7"
  style.stroke-width: 2
  width: 90
  height: 90
}

YOLO11s: {
  label: "YOLO11s 检测\n找鸟框 · ≥0.60"
  shape: class
  style.fill: "#282840"
  style.stroke: "#FAB387"
  style.stroke-width: 2
  style.border-radius: 12
  width: 190
}

裁剪: {
  shape: rectangle
  style.fill: "#1E1E2E"
  style.stroke: "#585B70"
  style.border-radius: 8
  width: 100
}

BioCLIP2: {
  label: "BioCLIP2 提特征\n768 维向量"
  shape: class
  style.fill: "#282840"
  style.stroke: "#89B4FA"
  style.stroke-width: 2
  style.border-radius: 12
  width: 190
}

分类头: {
  label: "线性分类头\n768 → 1009 种"
  shape: class
  style.fill: "#1E1E2E"
  style.stroke: "#A6E3A1"
  style.stroke-width: 1
  style.border-radius: 12
  width: 180
}

Top-5: {
  shape: circle
  style.fill: "#282840"
  style.stroke: "#F5C2E7"
  style.stroke-width: 2
  width: 90
  height: 90
}

原图 -> YOLO11s: { style.stroke: "#6C7086"; style.stroke-width: 2 }
YOLO11s -> 裁剪: { style.stroke: "#6C7086" }
裁剪 -> BioCLIP2: { style.stroke: "#6C7086"; style.stroke-width: 2 }
BioCLIP2 -> 分类头: { style.stroke: "#6C7086"; style.stroke-width: 2 }
分类头 -> Top-5: { style.stroke: "#F5C2E7"; style.stroke-width: 2 }
```

### 为什么用 YOLO11s？

YOLO（You Only Look Once）是一个经典的目标检测模型家族。YOLO11 是目前最新的版本，`s` 代表 small。它可以在一张图上同时找到多个目标的位置，并给出每个目标的置信度。

我选择 YOLO11s 而不是更小的 YOLO11n，是因为鸟在画面中可能很小——有时候只有几十个像素——需要更高的检测精度。代价是模型大了几 MB，推理慢了十几毫秒，在 Mac 上几乎感觉不到。

### 为什么用 BioCLIP 而不是自己训分类模型？

这是整个项目中最重要的一个决策。

BioCLIP 是由 Imageomics 研究所训练的"生物视觉基础模型"。它在 450,000 个物种、80 万张图片上训练过，涵盖了从昆虫到哺乳动物的大量生物类别。关键是：**它见过很多鸟。**

如果你自己从零训练一个鸟类分类器，需要海量标注数据——每种鸟至少几百张高质量图片，还要覆盖不同角度、光线、季节、姿态。对于 1000+ 种鸟来说，这个数据量是不可承受的。

BioCLIP 的思路是"站在巨人的肩膀上"：

1. **BioCLIP 主干固定不训练**：它负责把裁剪后的鸟图转换成一个 768 维的数学向量（embedding）。这个向量编码了鸟的形态、颜色、纹理等视觉信息。
2. **只训练一个线性分类头**：这是一个很小的矩阵（768 × 1009），把 BioCLIP 的特征向量映射到具体的鸟种编号。训练它只需要几百 MB 显存、几十分钟。

这就像 BioCLIP 已经学会了"看鸟"这件事，你只需要告诉它"你看到的这只具体叫什么名字"。

实际验证下来，BioCLIP 2（ViT-L/14，768 维）是效果和成本的最佳平衡点：
- 原版 BioCLIP（ViT-B/16，512 维）：最轻量，但细粒度识别不够准
- BioCLIP 2（ViT-L/14，768 维）：✅ 当前主力
- BioCLIP 2.5 Huge（ViT-H/14，1024 维）：理论效果更好，但模型更大、推理更慢

---

## 第三环：服务端——Mac mini 里的"鸟识别工厂"

iPhone 不适合直接跑 BioCLIP。一是模型太大（1.7GB），二是推理需要 PyTorch + Apple MPS 加速。所以我把识别服务部署在一台 Mac mini M4 上，放在家里的局域网里。

但其实这个架构是"云端"的——只不过云长这样：

```d2
direction: right

iPhone: {
  shape: rectangle
  style.fill: "#282840"
  style.stroke: "#CBA6F7"
  style.stroke-width: 2
  style.border-radius: 8
  width: 140
}

API: {
  label: "FastAPI\n鉴权 · 分发"
  shape: class
  style.fill: "#282840"
  style.stroke: "#89B4FA"
  style.stroke-width: 2
  style.border-radius: 12
  width: 160
}

Redis: {
  shape: cylinder
  style.fill: "#1E1E2E"
  style.stroke: "#FAB387"
  style.stroke-width: 1
  width: 100
}

YOLO Worker: {
  label: "YOLO Worker\n检测 + 裁剪"
  shape: class
  style.fill: "#1E1E2E"
  style.stroke: "#A6E3A1"
  style.stroke-width: 1
  style.border-radius: 12
  width: 160
}

BioCLIP Worker: {
  label: "BioCLIP Worker\n特征 + 分类"
  shape: class
  style.fill: "#1E1E2E"
  style.stroke: "#F5C2E7"
  style.stroke-width: 1
  style.border-radius: 12
  width: 170
}

存储: {
  label: "PostgreSQL + MinIO\n元数据 · 文件"
  shape: class
  style.fill: "#1E1E2E"
  style.stroke: "#585B70"
  style.stroke-width: 1
  style.border-radius: 12
  width: 190
}

iPhone -> API: 上传 { style.stroke: "#6C7086"; style.stroke-width: 2 }
API -> Redis: 投递 { style.stroke: "#6C7086" }
Redis -> YOLO Worker: 检测 { style.stroke: "#6C7086" }
Redis -> BioCLIP Worker: 分类 { style.stroke: "#6C7086" }
YOLO Worker -> 存储: 裁剪图 { style.stroke: "#585B70" }
BioCLIP Worker -> 存储: 结果 { style.stroke: "#585B70" }
存储 -> API: 查询 { style.stroke: "#585B70" }
```

整套系统命名为 **OrniVision-Server**。技术栈是：

| 组件 | 技术 | 说明 |
|------|------|------|
| API | FastAPI + Uvicorn | 接收图片、返回结果、Web 测试台 |
| 队列 | Redis | 异步任务分发，解耦 API 和 Worker |
| 检测 | YOLO11s | 找鸟框、裁剪鸟图 |
| 分类 | BioCLIP 2 + 线性头 | 768 维特征 → 1009 种鸟 |
| 元数据 | PostgreSQL | 任务、结果、用户、图片索引 |
| 文件 | MinIO | 原图、裁剪图、识别结果 JSON |
| 监控 | Grafana + Loki | 日志、吞吐、延迟 |

为什么用 Redis 队列？因为一次识别可能耗时几秒（BioCLIP 推理 + 前后处理），不能让 HTTP 请求一直等着。iPhone 上传照片后立即拿到一个 `task_id`，然后轮询结果。这种异步模式也方便未来扩展到多台 Worker 并行处理。

基础设施用 OrbStack 在本机跑——PostgreSQL、Redis、MinIO 都是容器化的，数据挂载到外接 SSD。

从外部访问怎么办？用 frp 做了内网穿透：Mac mini 通过 frpc 连接到一台有公网 IP 的轻量服务器，iPhone 在外网也能调 API。

---

## 数据：最难的不是模型，是"找到对的图"

很多人以为 AI 项目的瓶颈是模型，其实往往是数据。

WingLens 目前覆盖 **1,009 种中国鸟类**，共 **35,275 张训练图片**。平均每种鸟约 33 张，中位数 28 张。

这些数据从哪来？

1. **birdreport.cn**：中国观鸟记录中心的公开数据，包含大量鸟种、地点、时间的标注图片。我写了脚本按鸟种目录批量下载整理。
2. **Android ADB 半自动采集**：用小红书搜索鸟名，通过 ADB 控制 Android 手机截图选图，再拉回电脑按目录保存。听起来很土，但效率比手动高不少。
3. **标注纠正**：在 OrniVision 标注工作台上，用户可以手动确认或纠正模型的 Top-5 结果，错误标注的图片会被记录下来，后续改进。

数据质量比数量更重要。训练脚本中有几个关键机制：
- **YOLO 裁剪后再训练**：不是用整张原图，而是先用 YOLO 找出鸟的准确位置，裁出鸟图，再去训练分类头。这样模型不会被背景干扰。
- **数据审阅服务**：训练完一版后，启动一个本地 Web 服务（端口 8765），逐鸟种看训练样本——有些图可能根本不是鸟，或者鸟种标错了。标记后重新裁剪、重新训练。
- **label_map**：目录名如 `白骨顶_4230`，下划线后的数字是鸟种唯一编号。训练和推理都基于这个编号，保证数据可追溯。

当前数据的短板也很明显：
- 很多鸟种只有 10-20 张图，对模型来说太少
- 部分图片质量不高——模糊、遮挡、角度刁钻
- 稀有鸟种的数据尤其稀少，模型容易把它们认成常见鸟种

---

## iPhone 端：WingLens App

iPhone 端目前是 SwiftUI 开发的横屏应用，核心交互是：

**左侧主图区域**：显示最新照片 + 检测框 + 鸟名标签。每只鸟的框用不同颜色标记。

**右侧结果面板**：按鸟展示 Top-5 候选，每只鸟一张卡片，颜色与主图检测框对应。

![App 主界面：左侧实时画面，右侧识别结果](@media/posts/winglens-bird-recognition/app-screenshot.png)

第一版的设计原则是"少操作"：拍完照后自动识别，用户只需要扫一眼 Top-5，点确认。不需要手动裁切、不需要输入鸟名、不需要打开其他 App。

---

## OrniVision：标注工作台

除了给 iPhone 用的 OrniVision-Server，还有一个独立的标注 Web 应用——**OrniVision**。

它的定位是"数据标注工作台"：

- 上传图片 → 自动识别所有鸟框 → 每个框展示 Top-5
- 用户可以从 Top-5 中选择正确的鸟种，或手动输入
- 确认后图片按鸟种目录保存（`白骨顶_4230/`），自动成为训练数据
- 需要登录（支持多账号和 API Key）
- 可以切换 BioCLIP 原版 / BioCLIP 2 / BioCLIP 2.5 三种分类头

这是一个关键的"数据闭环"工具：用户在使用过程中纠正的错误，会变成下一版模型的训练素材。就像数字员工的日会——每天复盘哪些认错了，下次做得更好。

---

## 踩过的坑

### 1. 模型加载和首次推理太慢

BioCLIP 2 的权重大约 1.7GB，首次加载需要几秒。解决方案是在 Worker 启动时预加载模型到内存，保持常驻，避免每次请求都重新加载。MPS（Apple Silicon 的 GPU 加速）比纯 CPU 快 3-5 倍。

### 2. 相机连接不稳定

索尼相机的 Wi-Fi 热点偶尔会断开，PTP 协议有些边缘情况会超时。处理方式是加重试机制、连接状态指示器、以及降级到 SD 卡的手动流程。

### 3. BioCLIP 2.5 太大，不适合本地

BioCLIP 2.5 Huge 效果确实更好一点，但权重大约 3.9GB，推理时间是 BioCLIP 2 的 2 倍。对于实时场景（用户等几秒看结果），BioCLIP 2 是更好的折中。

### 4. 鸟种之间的混淆

有些鸟种长得非常像。比如"白骨顶"和"黑水鸡"在光线不好时很难区分；"红嘴鸥"和"黑嘴鸥"的冬羽也容易混淆。这类情况下模型给出的置信度通常较低（40-60%），需要用户人工判断。这也是为什么 Top-5 比 Top-1 更实用。

### 5. 吸管喝水式的边缘情况

类比我的另一个项目"智能水杯秤"：如果用户用吸管喝水，杯子始终不离开底座，传感器无法判断。WingLens 也有类似的边缘情况——如果鸟只露出半个头、或者在画面边缘、或者被树叶遮挡超过 70%，YOLO 可能根本检测不到。目前的对策是：提高检测阈值（≥0.60），宁可漏检也不要误检太多。

---

## 当前进展

| 模块 | 状态 | 说明 |
|------|------|------|
| 相机连接 | ✅ Wi-Fi PTP/IP 已跑通 | SonyCameraCross Swift 实现 |
| YOLO 检测 | ✅ YOLO11s | 在 Mac mini M4 上跑 MPS 加速 |
| BioCLIP 分类 | ✅ BioCLIP 2 | 768 维特征 + 线性头，1009 种鸟 |
| 训练数据 | ✅ 35K 张 | 1009 种，平均 33 张/种 |
| 标注工作台 | ✅ OrniVision Web | 支持 3 种 backbone 切换 |
| 服务端 API | ✅ OrniVision-Server | Redis 队列 + 异步 Worker |
| iPhone App | 🚧 开发中 | SwiftUI 横屏，已跑通主流程 |
| 公网访问 | ✅ frp 穿透 | Mac mini → 轻量服务器 |
| 鉴权与付费 | 🔜 P4 | license_token + StoreKit 2 |
| 鸟声识别 | 🔜 远期 | 麦克风 + 声纹模型 |

---

## 写在最后

从沙河水库那只认不出来的鸟开始，到 1000+ 种鸟的训练数据、一台 Mac mini 跑着识别服务、一个横屏 App 正在变完整——这条路比我想象的长得多。

但每一次模型认出一只我没见过的鸟，每一次标注数据又多了一个物种，我都会觉得这件事是对的。

WingLens 不是要替代你的观鸟图鉴，也不是要让你放下相机去用手机拍。它只是想在你按下快门之后，把后面的麻烦事都省掉：

**让它自己去找鸟、认鸟、记下来。你只需要继续看下一只。**

---

## 参考资源

- 项目仓库（AI_Birds）：包含训练脚本、数据流水线、OrniVision 标注工具
- OrniVision-Server：Mac mini M4 上的异步识别服务（FastAPI + Redis + BioCLIP2）
- SonyCameraCross：基于 PTP 3 协议的 Swift 相机控制库
- WingLens iOS App：SwiftUI 横屏识别工作台
- 训练方案：YOLO 裁剪 + BioCLIP 特征 + 线性分类头（详见 `training/bioclip_head/方案介绍.md`）
