Non-sidereal tracking and continuous autofocus for equatorial mounts.
赤道仪天生只会做一件事:以恒星速率(15.041"/s)绕极轴转动,好让星星钉在视场里。 SkyHook 让它学会做另一件事——追上会动的东西:从头顶掠过的飞机、LEO 卫星、气球、无人机, 并且在目标距离不断变化的同时,把焦点一直咬住。
核心是标准库实现(零第三方依赖),天文算法与硬件协议全部可选可插拔。
skyhook sim --scenario satellite --duration 120
SkyHook 0.2.0 -- simulating 'satellite' for 120 s of target time
site: site (39.90420, 116.40740, 50 m)
start: RA 272.9886 Dec +30.2797 el 21.5 range 1245205 m
predictor: model + bias
results
cycles 600
tracked samples 582
error mean 5.7" <- 而仿真器自身注入了 14" 周期误差 + 22" 齿隙
error rms 9.1"
error p95 18.9"
max RA rate 2999"/s
range 991414 .. 1230331 m
focuser 20003 .. 20004
Important
先读 docs/HARDWARE.md 里的速率上限一节。 这不是软件能绕过去的限制:一架 10 km 外的客机需要约 0.5°/s 的轴速度, 过天顶的 LEO 卫星需要 1°/s 以上,而很多消费级赤道仪的自定义跟踪速率上限远低于此。 追不上不是 bug,是机械现实。
追一个移动目标,困难不在"知道它在哪",而在知道命令抵达时它会在哪。
三份延迟会叠加:
| 延迟来源 | 典型值 | 后果 |
|---|---|---|
| 数据源陈旧 | ADS-B 位置约 1 s 更新一次,读到时已过期 | 900 km/h 的飞机偏出 ~250 m |
| 指令传输 | 10–150 ms(串口最慢) | 按当前位置发命令,永远慢半拍 |
| 机械响应 | 轴速率一阶收敛,0.1–0.3 s | 新速率要过一会儿才建立 |
SkyHook 的处理方式:
- 按数据源给出的时间戳与速度做前向外推(ADS-B 的
seen_pos直接用上); - 卡尔曼滤波估计位置/速度/加速度,并带野值剔除——一次错误报文不该把望远镜甩飞;
- 提前量(lead):速率指令按
t + lead时刻的目标状态计算,命令生效时正好合上; - 读数外推:望远镜回传的位置本身也是旧的,用它自己的速率推回"现在"再比误差;
- 输出的是速率而非"转到某点"——每 200 ms 发一次 GoTo 会让赤道仪一直在启停,图像抖动、机械磨损。
不需要任何硬件,不需要装任何包。
git clone https://github.com/aronsirius/skyhook
cd skyhook
# 纯标准库,连 pip install 都不需要
python -m skyhook sim --scenario aircraft --duration 120
python -m skyhook sim --scenario satellite --duration 120
# 加上真实感的噪声:25 m 位置误差、0.35 s 时间抖动、5% 丢包
python -m skyhook sim --scenario aircraft --duration 120 --noise
# 对焦表:不同距离该把调焦座拧到哪
python -m skyhook focus --distances 500 3000 30000 1e9
# 自检
python -m skyhook selftest
python -m skyhook info仿真用的是虚拟时钟:120 秒的目标时间通常 1 秒内跑完,因此 CI 里也能跑回归测试。
想看实时曲线:
python -m skyhook track --config config/skyhook.toml --dashboard
# 打开 http://127.0.0.1:8765/ ADS-B / TLE / synthetic 相机(可选)
| |
v v
+----------------+ +----------------+
| TargetSource | | sharpness / |
| 统一输出视位置 | | centroid |
+----------------+ +----------------+
| |
| ra, dec, rate, range | 清晰度 / 像面偏移
v |
+-------------------------------------------+
| TargetPredictor |
| 物理模型(可选) + 卡尔曼残差 -> t+lead |
+-------------------------------------------+
|
v ra, dec, dRA/dt, dDec/dt
+-------------------------------------------+
| TrackingLoop |
| 误差 = 目标(现在) - 望远镜(外推到现在) |
| 指令 = 前馈速率 + Kp*误差 + Ki*∫误差 |
+-------------------------------------------+
| |
v v
+----------------+ +--------------------+
| Mount | | FocusController |
| 绝对速率 arcsec/s| | 距离前馈 + 温度 |
+----------------+ | + 清晰度爬山 |
+--------------------+
硬件接口大多要的是相对恒星速的偏移,而控制律天然产生绝对速率。
这个转换统一放在驱动边界(mount/base.py):
offset_ra = absolute_ra - 15.0410686 # ASCOM Alpaca / INDI 都是这个语义
offset_dec = absolute_dec # 赤纬轴没有恒星分量另外:RA 轴速率是沿赤经轴的速率,不是天空角速度,所以全项目不乘 cos(dec)。
对卫星,TLE 传播器能给出长期正确的轨迹,但绝对精度只有公里级; 让多项式去外推整段过境会发散。所以:
- 模型负责长期行为(
propagate/kepler.py或 skyfield/SGP4) - 滤波器只估计残差(TLE 误差、折射残差、时间偏差),残差变化缓慢,CV 模型足够
飞机没有可信的长期模型,就退回滤波器独立外推(含加速度项)。
| 赤道仪 | 驱动 | 非恒星速率 | 备注 |
|---|---|---|---|
| ASCOM Alpaca | alpaca |
✅ 原生 | 推荐。跨平台、无 COM、无 32/64 位坑 |
| INDI | indi |
✅ 原生 | Raspberry Pi + Ekos 的默认选择 |
| LX200 串口 | lx200 |
协议本身没有连续速率命令,见下 | |
| 任何 GoTo(应急) | lx200 |
用导星脉冲补差,会有几角秒锯齿 | |
| 仿真 | sim |
✅ | 延迟、齿隙、周期误差、限幅全都有 |
LX200 的现实:标准命令集没有"设置连续跟踪速率"这一条。驱动提供两种模式——
pulse(用方向键脉冲补差,通用但有几角秒锯齿)和 custom
(填你固件的私有命令模板,例如 rate_command_ra = ":RA{rate:+.5f}#")。
真要做高精度非恒星跟踪,请走 Alpaca 或 INDI。
| 源 | 说明 |
|---|---|
adsb |
dump1090 / readsb / tar1090 的 aircraft.json;本地文件或 URL 都行 |
satellite |
TLE 文件;装了 skyfield 自动升级为完整 SGP4 |
synthetic |
合成轨迹,用于仿真、演示、回归测试 |
| 源 | 说明 | 依赖 |
|---|---|---|
sim |
内置光学模型,随调焦座位置改变模糊 | 无 |
alpaca |
ASCOM Alpaca 相机 | 无 |
opencv |
USB / 网络摄像头 | opencv-python |
directory |
回放 PGM/PPM 序列,适合离线调参 | 无(其他格式需 Pillow) |
skyhook sim 的数字,仿真器注入了 14" 周期误差 + 22" 齿隙 + 1.1" 抖动 + 0.08 s 指令延迟:
| 场景 | 误差均值 | RMS | P95 | 峰值轴速率 |
|---|---|---|---|---|
| 客机(无噪声) | 3.1" | 6.0" | 5.7" | ~1600"/s |
| 客机(25 m 噪声 + 0.35 s 抖动 + 5% 丢包) | 114" | 144" | 287" | ~4800"/s |
| LEO 卫星(model + bias) | 5.7" | 9.1" | 18.9" | ~3000"/s |
| LEO 卫星(仅滤波器) | < 60" | — | — | ~3000"/s |
噪声场景的 114" 值得解释:ADS-B 在 30 km 外 25 m 的位置误差本身就是 ~170", 加上 0.35 s 抖动 × 240 m/s ≈ 84 m ≈ 580"。滤波器把它们压到 114"(约 16 m), 但压不到零——这是数据源的物理极限,不是算法问题。 想再进一步只能靠图像闭环测目标质心(见路线图)。
薄透镜关系决定了全部设计:
1/f = 1/u + 1/v => Δv = v - f = f²/(u - f)
- 1000 mm 望远镜拍 3 km 外的飞机:调焦座要外移 0.33 mm。按 2.5 µm/步算是 130 步—— 数值不大,但远大于景深,不能不管。
- 500 km 外的卫星:0.000002 mm。等价于无穷远,所以卫星只需要温度补偿。
控制器按可信度排序使用三种输入:
- 距离前馈(主力)——斜距已知时直接算位置;飞机 20 秒内距离能减半,必须跟上。
- 温度补偿——碳纤/铝管的每度漂移,不补的话前两项白做。
- 清晰度爬山(兜底)——对目标周围 ROI 的拉普拉斯方差做爬山,
只在一个有界偏移量内微调,吸收模型不知道的东西(滤镜轮后截距变化、镜筒形变、
steps_at_infinity标定误差)。
$ python -m skyhook focus --distances 500 3000 30000 1e9
optics: 1000 mm f/5, 4000 steps/mm (0.25 um/step)
hyperfocal distance: 10,001 m (beyond this, infinity focus is fine)
distance offset mm steps delta depth of field
500 2.0040 28016 +8016 476 .. 526
3,000 0.3334 21334 +1334 2,308 .. 4,285
30,000 0.0333 20133 +133 7,501 .. inf
1,000,000,000 0.0000 20000 +0 10,001 .. inf
注意 30 km 那一行:远近界变成了 7501 .. inf——超过了超焦距,
远处全部清晰,于是再远的目标都共用无穷远对焦。这就是为什么卫星不用调焦。
上面三项里,前两项(距离、温度)不需要图像就能算;第三项要。没有相机时
sharpness_fn 是空的,爬山不启动——对焦仍然工作,但只靠模型,对后截距变化、
镜筒形变这类"模型不知道的事"无能为力。
# 先看一眼相机和指标是否正常:扫描应当是一条干净的单峰
python -m skyhook cam --kind sim --sweep --sweep-from 19800 --sweep-to 20200 --sweep-step 50
# 全程跟踪 + 真实像素驱动对焦
python -m skyhook sim --camera sim --duration 60支持四种源:sim(内置光学模型)、alpaca(ASCOM 相机)、opencv(USB/网络摄像头)、
directory(回放 PGM/PPM 序列,适合离线调参)。
ROI 是这里唯一容易设错的东西。 清晰度指标是在裁剪窗口上取的平均值, 噪声的贡献是每像素固定的、不随窗口缩小而减少,而目标的信号集中在十几个像素里。 窗口开得越大,信号被摊薄、噪声不减:实测 90×90 时峰底比只有 1.1(等于没有信号), 16×16 时是 23.5。所以窗口要紧紧框住目标,而不是"大一点保险"。
TOML(Python 3.11+ 自带 tomllib,所以依然零依赖)。从 config/skyhook.toml 抄一份改。
最小可用配置:
[site]
lat_deg = 39.9042
lon_deg = 116.4074
height_m = 50.0
[mount]
kind = "sim"
[source]
kind = "synthetic"
[loop]
cycle_s = 0.2
lead_s = 0.45lead_s 是最值得调的一个数:它应该等于指令延迟 + 机械响应时间常数。
Alpaca 一般 0.3–0.5 s;串口 LX200 可以到 0.6 s。调小了会滞后,调大了会超前震荡。
kind = "sim"先跑通,确认站址、时间、目标源都对。- 换
alpaca(或indi),先不接相机,只看error_total_as能不能稳定。 - 标定对焦:对准一颗亮星,手动调到最锐,把步数填进
steps_at_infinity; 再用两个已知距离的目标(或一次--direction试错)确定direction与steps_per_mm。 - 第一次实战挑慢目标:远处高空客机(角速度 100–300"/s)比头顶飞过的近距飞机容易得多。
细节见 docs/HARDWARE.md,公式见 docs/MATH.md。
python -m unittest discover -s tests # 81 项,约 2 秒,不需要硬件
python -m skyhook selftest测试里最有价值的是 tests/test_control.py:它把整条闭环跑起来,
断言误差量级。如果有人改坏了提前量补偿或速率单位换算,这些数字会涨一个数量级——
比断言某个内部变量有意义得多。
python -m pip install -e ".[dev]" # 开发
python -m pip install -e ".[sat]" # 完整 SGP4
python -m pip install -e ".[serial]" # LX200 串口
python -m pip install -e ".[all]"诚实清单:
- 轴速率上限是硬约束。见 HARDWARE.md。软件只能检测和降级,不能突破机械。
- 天极奇异:目标接近天极时所需 RA 速率发散。环会限幅并标记
degraded,但物理上追不上。 - 赤道仪 vs 经纬仪:追快速移动目标,Alt-Az 其实是更合适的机构(没有场旋问题要另说, 但至少不会撞上赤经轴的奇异)。用赤道仪追飞机是"用手头的设备做难的事"。
- 内置传播器是 J2 长期项 + 开普勒,精度公里级(见
propagate/kepler.py文档字符串)。 要真追卫星请装 skyfield:pip install "skyhook[sat]"。 - LX200 脉冲模式有几角秒锯齿。
- 图像闭环尚未内置:清晰度评价函数已有(
focus/metrics.py),质心测量还没接。 目前 ADS-B/TLE 提供粗引导,精度受数据源限制。 - 仿真相机的噪声模型是单像素高斯,没有建模热像素、坏列、读出噪声的非均匀性。
真机上建议给采样器加一点
blur_px抑制热像素。
- 永远不要把激光指向飞机或卫星。在多数国家这是刑事犯罪,且真的会伤到机组。
- 在机场附近飞行/拍摄有额外法规;请遵守当地规定。
- 拍摄航空器本身在绝大多数地区是合法的爱好(planespotting),但公开发布时注意隐私与当地法律。
- 望远镜指向太阳会造成永久失明与设备损毁。本项目的仰角限制不是太阳安全保护, 别用它对着太阳。
- 详情见 docs/SAFETY.md。
- 图像质心闭环(
optical_fn),用相机精跟踪吃掉数据源误差 - 多目标调度(一架落地自动切下一架)
- 场旋补偿(Alt-Az + 消旋镜)
- 完整 SGP4 内置实现(去掉 skyfield 依赖)
- 过境预报 CLI(
skyhook pass) - Web 控制面板(目标列表、手动接管)
MIT。见 LICENSE。