nate-griff/enf
GitHub: nate-griff/enf
从音视频文件中提取电网频率特征并与参考数据比对以验证录制时间的数字取证分析工具。
Stars: 0 | Forks: 0
# enf
电网频率分析工具
## 概述
本仓库包含一个本地电网频率 (ENF) 分析工具,可从录音和录像中提取 ENF 特征,然后将其与来自北美四大电网之一的已知电网频率参考数据进行比较:
- **EI** (东部互联电网)
- **WECC** (西部电力协调委员会)
- **ERCOT** (德克萨斯州电力可靠性委员会)
- **Quebec** (魁北克)
该工具主要面向研究,设计为一种辅助调查工具,用于缩小供人工审查的合理时间范围,而不是一个独立的证明系统。
## 快速开始
### 设置
```
# 创建并激活虚拟环境
python -m venv .venv
.\.venv\Scripts\activate # Windows
source .venv/bin/activate # Linux/macOS
# 安装依赖
pip install -r requirements.txt
```
### 基本工作流程
**1. 从音频/视频中提取 ENF:**
```
python enf_extract.py --input recording.wav --output trace.csv
python enf_extract.py --input recording.mp4 --output trace.csv # video auto-converts
```
**2. 与电网参考数据进行比较:**
```
python enf_compare.py \
--trace trace.csv \
--grid-dir source_data/grid_data \
--region EI \
--date 2026-04-20 \
--top-n 5 \
--plot
```
**3. 在 GUI 中检查匹配结果:**
```
python enf_view.py --results results.json
```
## 工具
### `enf_extract.py` — 从音频/视频中提取 ENF
使用二次插值 FFT (QIFFT) 提取电网频率。
**用法:**
```
python enf_extract.py --input FILE [--output OUTPUT.csv] [options]
```
**主要参数:**
- `--input` (必需):音频或视频文件
- `--output`:输出 CSV 路径 (默认: `{input_stem}_enf.csv`)
- `--nominal`:标称电网频率 (默认: 60 Hz)
- `--harmonic`:要提取的谐波次数 (默认: 2 — 120 Hz 处的二次谐波)
- 推荐使用二次谐波 — 结果更干净,噪声污染更少
- 结果会自动除回基频 (60 Hz)
- `--bandwidth`:目标周围的单边带宽,单位为 Hz (默认: 0.5)
- `--frame-sec`:帧时长,单位为秒 (默认: 1.0)
- `--overlap`:帧重叠比例 0–1 (默认: 0.5)
- `--pad-factor`:FFT 的零填充乘数 (默认: 16)
- `--median-window`:中值滤波器窗口大小 (默认: 3,设为 0 禁用)
**输出 CSV 列:**
- `offset_seconds`:距录制开始的秒数
- `frequency_hz`:估计的 ENF 频率
**示例:**
```
python enf_extract.py --input fan.wav --output fan_enf.csv --harmonic 2 --bandwidth 0.5
```
### `enf_compare.py` — 电网匹配
在连续的参考区段上,使用 FFT 加速的皮尔逊相关,将提取的 ENF 轨迹与电网参考数据进行比较。
**用法:**
```
python enf_compare.py --trace TRACE.csv --grid-dir DIR --region REGION [options]
```
**主要参数:**
- `--trace` (必需):来自 `enf_extract.py` 的 ENF 轨迹 CSV
- `--grid-dir` (必需):包含每日电网 CSV 文件的目录
- `--region` (必需):电网区域 (EI, WECC, ERCOT, 或 Quebec)
- `--date`:按特定日期过滤电网数据 (YYYY-MM-DD,逗号分隔)
- `--top-n`:返回的最佳匹配数 (默认: 3)
- `--min-separation-sec`:返回的匹配起始时间之间的最小间隔,单位为秒 (默认: 5,使用 0 允许相邻的近似重复项)
- `--threshold`:“足够接近”评分的 Hz 阈值 (默认: 0.01)
- `--output`:JSON 输出路径 (默认: `{trace_stem}_results.json`)
- `--plot`:为每个最佳匹配生成叠加 PNG
- `--recording-time`:用于偏移显示的已知 UTC 开始时间 (ISO 格式)
- 参考数据仅在较短的观测运行内进行插值。大范围停电或缺失天数的间隔会被分割为独立的区段,不参与匹配。
**输出 JSON:**
包含排名后的匹配项,包含:
- `rank`:匹配顺序
- `ref_start_utc`:参考窗口开始时间
- `ref_end_utc`:参考窗口结束时间
- `correlation`:皮尔逊相关系数 (0–1)
- `threshold_coverage`:在阈值 Hz 内的样本比例 (0–1)
- `composite_score`:加权得分 (40% 相关性 + 60% 覆盖率)
**评分:**
综合得分结合了:
- **皮尔逊相关** (40%):形状相似性,对偏移不变
- **阈值覆盖率** (60%):绝对频率接近度
评分后,匹配结果将进行贪心过滤,使返回的 `ref_start_utc` 值至少相隔 `--min-separation-sec`。这会抑制 1 秒偏移的近似重复窗口,同时仍会回填更靠后的候选者,直到找到 `top-n` 个不同的匹配项。
**示例:**
```
python enf_compare.py \
--trace fan_enf.csv \
--grid-dir source_data/grid_data \
--region EI \
--date 2026-04-20 \
--top-n 5 \
--min-separation-sec 5 \
--threshold 0.01 \
--plot \
--recording-time "2026-04-20T16:36:00"
```
### `enf_view.py` — GUI 叠加查看器
用于可视化检查 ENF 匹配结果的交互式 tkinter + matplotlib 查看器。
**用法:**
```
# 从 results JSON 加载
python enf_view.py --results results.json
# 或者手动加载
python enf_view.py --trace trace.csv --grid-dir source_data/grid_data --region EI
```
**特性:**
- **叠加显示**:查询轨迹 (蓝色) vs. 匹配的参考轨迹 (橙色)
- **匹配步进**:上一个/下一个按钮可循环切换最佳匹配项
- **滚动/缩放**:对数比例的缩放滑块和时间位置滚动
- **分数显示**:显示相关性、覆盖率百分比和综合得分
- **UTC 信息**:在绘图标题中显示参考时间窗口
**控制:**
- **匹配组合框**:跳转到任何最佳匹配项
- **滚动滑块**:在轨迹上移动时间窗口
- **缩放滑块**:更改可见时间范围 (对数刻度,窄 ← → 宽)
- **上一个/下一个按钮**:逐步浏览排名后的匹配项
## 项目结构
```
.
├── enf_extract.py # ENF extraction (audio/video → CSV)
├── enf_compare.py # Grid matching (CSV → JSON results)
├── enf_view.py # GUI viewer (JSON → overlay display)
├── freqgauge_view_csv.py # CSV viewer for grid reference data
├── freqgauge_extract.py # Extract grid data from FNET images
├── collect_freqgauge_service.py # Continuous FNET image collection
├── requirements.txt # Python dependencies
├── Project-Plan.md # Detailed technical plan
└── source_data/
├── audio_samples/ # Test recordings
├── grid_data/ # Daily grid CSVs from FNET
└── scraped_images/ # FNET frequency gauge images (from collector)
```
## 技术细节
### ENF 提取方法
`enf_extract.py` 脚本使用**二次插值 FFT (QIFFT)** 获得子频段的频率精度:
1. **带通滤波器**:目标频率周围的 4 阶巴特沃斯滤波器
2. **加窗**:对每一帧应用汉宁窗
3. **FFT**:零填充 (默认 16×) 以获得精细的频率间隔
4. **峰值查找**:在预期频率范围内定位最大幅度
5. **QIFFT 插值**:对峰值及其相邻点进行二次拟合以实现子频段精度
6. **聚合**:平均每秒的多个估计值以匹配电网节奏
7. **平滑**:用于降噪的可选中值滤波器
**公式:** 对于峰值 `k` 处的幅度频率槽 `α`、`β`、`γ`:
```
δ = 0.5 × (α - γ) / (α - 2β + γ)
f_est = (k + δ) × (fs / N)
```
### 匹配算法
`enf_compare.py` 脚本使用 FFT 加速的滑动皮尔逊相关:
1. **加载与分段**:电网数据按时间戳排序,并在较大的间隔处进行拆分,这样停电和缺失的日期就永远不会成为合成的匹配窗口
2. **重采样**:每个连续的区段独立重采样为规则的 1 秒间隔
3. **稳定的皮尔逊相关**:查询窗口和候选窗口去均值后,通过 FFT 互相关加上滚动窗口统计量计算相关性
4. **候选选择**:从每个连续区段中保留前 50 个相关候选者
5. **阈值覆盖率**:计算这些候选者中在 Hz 阈值内的样本数
6. **综合评分**:0.4 × 相关性 + 0.6 × 覆盖率
7. **去重过滤**:保留起始时间至少相隔配置的间距窗口的最高得分匹配项
8. **排名**:按得分顺序返回前 N 个不同的匹配项
### 默认设置
- **谐波**:2 (120 Hz 处的二次谐波,比基波干净得多)
- **带宽**:0.5 Hz
- **帧大小**:1.0 秒
- **重叠率**:50% (0.5 秒的跳跃)
- **零填充**:16× (48 kHz × 1s = 48000 点 → 768000 点)
- **中值滤波器**:3 个样本的窗口
- **阈值**:0.01 Hz
- **参考数据间隔切分**:5 秒
- **综合权重**:40% 相关性,60% 覆盖率
## 依赖项
```
numpy>=1.24.0
pandas>=2.0.0
scipy>=1.11.0
matplotlib>=3.7.0
opencv-python>=4.8.0 # For image extraction (freqgauge_extract.py)
requests>=2.28.0 # For image collection (collect_freqgauge_service.py)
```
## 数据来源
### 参考电网数据
每日 CSV 文件是通过处理 FNET 频率仪表图像生成的。每个 CSV 包含:
```
timestamp_utc,region,frequency_hz
2026-04-20 16:36:12.457984+00:00,EI,59.980379
```
**电网区域:**
- **EI**:东部互联电网 (美国东部)
- **WECC**:西部电力协调委员会 (美国西部)
- **ERCOT**:德克萨斯州电力可靠性委员会 (德克萨斯州)
- **Quebec**:魁北克水电系统 (魁北克/加拿大东部)
### 图像收集
使用 `collect_freqgauge_service.py` 持续下载 FNET 仪表图像:
```
python collect_freqgauge_service.py \
--outdir source_data/scraped_images \
--interval 38.6
```
### 图像处理
从收集到的图像中提取频率轨迹:
```
python freqgauge_extract.py \
--input source_data/scraped_images \
--output source_data/grid_data/merged.csv \
-j 8
```
查看和浏览提取的数据:
```
python freqgauge_view_csv.py source_data/grid_data/merged.csv
```
## 验证与测试
该工具的端到端验证使用了:
- **测试录音**:`fan.wav` — 340 秒,48 kHz 立体声,录制于 2026-04-20 下午 12:36 (美国东部时间)
- **参考数据**:2026-04-20 的 EI 电网数据
- **结果**:在 **16:36:05 UTC** 找到最佳匹配 (与真实时间相差不到 5 秒)
- 相关性:0.713
- 覆盖率:57%
- 综合得分:0.629
- 所有前 5 个匹配项都在正确时间的 ±7 秒范围内
## 未来工作
来自项目计划:
- 扩展到 50 Hz 电网 (国际支持)
- 自动地理电网检测
- 基于网页的部署
- 大规模基准数据集
- 取证级置信度指标
- 针对大型数据集的 GPU 加速匹配
## 数据来源(详情)
数据爬取自 FNET 的实时电网数据
爬取图像
使用 `collect_freqgauge_service.py` 从以下地址持续下载当前图像: `https://fnetpublic.utk.edu/freqgauge.php` ### 它的作用 - 每 38.6 秒下载一张图像(默认) - 将图像保存在 UTC 日期文件夹(`YYYY-MM-DD`)下 - 为每个文件名添加 UTC 时间戳 - 将失败和状态消息记录到日志文件和标准输出中 ### 安装依赖 ``` python3 -m pip install requests ``` ### 手动运行 ``` python3 collect_freqgauge_service.py \ --outdir /var/lib/freqgauge/images \ --log-file /var/log/freqgauge/collector.log ``` 可选标志: - `--interval 50` (轮询之间的秒数) - `--timeout 20` (HTTP 超时) - `--once` (下载一张图像后退出) - `--verbose` (调试日志) ### systemd 服务设置 1. 创建服务用户(可选但建议): ``` sudo useradd --system --no-create-home --shell /usr/sbin/nologin freqgauge ``` 2. 创建目录并设置权限: ``` sudo mkdir -p /var/lib/freqgauge/images sudo mkdir -p /var/log/freqgauge sudo chown -R freqgauge:freqgauge /var/lib/freqgauge /var/log/freqgauge ``` 3. 创建 `/etc/systemd/system/freqgauge-collector.service`: ``` [Unit] Description=FNET Frequency Gauge Collector After=network-online.target Wants=network-online.target [Service] Type=simple User=freqgauge Group=freqgauge WorkingDirectory=/opt/enf ExecStart=/usr/bin/python3 /opt/enf/collect_freqgauge_service.py --outdir /var/lib/freqgauge/images --log-file /var/log/freqgauge/collector.log --interval 50 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` 4. 启用并启动: ``` sudo systemctl daemon-reload sudo systemctl enable freqgauge-collector sudo systemctl start freqgauge-collector ``` 5. 检查状态/日志: ``` sudo systemctl status freqgauge-collector sudo journalctl -u freqgauge-collector -f ```处理图像
### 将轨迹提取到 CSV (`freqgauge_extract.py`) 将图像处理工具栈安装到您用于爬取的同一虚拟环境中: ``` .\.venv\Scripts\pip.exe install -r requirements.txt ``` 处理单张图像(每个 x 列 × 四个区域输出一行数据): ``` .\.venv\Scripts\python.exe freqgauge_extract.py ` --input testdata\freqgauge_2026-03-20T22-39-56.541331Z.png ` --output out\sample.csv ``` 处理整个目录树(递归处理 `freqgauge_*.png` / `.jpg`,包括采集器生成的 `YYYY-MM-DD` 日期文件夹)。对于**两张或更多**图像,`--dedupe-ms` 会对时间戳进行分箱,并计算重叠窗口内 `frequency_hz` 的平均值: ``` .\.venv\Scripts\python.exe freqgauge_extract.py ` --input path\to\images ` --output out\merged.csv ` --dedupe-ms 1000 ``` **调试叠加层**(裁剪后的绘图 + 二值掩码并排显示)用于调整颜色检测和边缘: ``` .\.venv\Scripts\python.exe freqgauge_extract.py ` --input testdata\some.png ` --debug-dir out\debug ``` 实用标志:`--window-seconds` (默认 55),如果分辨率发生变化可使用 `--skip-shape-check`,使用 `--morphology 0` 禁用掩码清理。对于大批量处理,`-j` / `--jobs N` 会以 **N 个并行进程**运行提取 (默认 1);在多核机器上尝试 4–8 — 每个工作进程会在内存中保存一张完整的图像,而 `--debug-dir` 仍然会在进程池完成后按顺序运行。默认情况下 CSV 列为 `timestamp_utc`、`region`、`frequency_hz`;添加 `--verbose-csv` 可包含 `pixel_x` 和 `source_path`。 **时间轴:** 列从左侧的 `(capture_time − window)` 到右侧的 `capture_time` 进行线性映射,使用文件名中的 UTC 时间戳。**频率:** 内部图表底部为 59.95 Hz,顶部为 60.05 Hz(脚本中的 `FREQ_MIN_HZ` / `REQ_MAX_HZ`)。 ### 查看提取的 CSV (`freqgauge_view_csv.py`) 需要 `matplotlib`(包含在 `requirements.txt` 中)。查看器需要 `timestamp_utc`、`region` 和 `frequency_hz` 列(忽略额外的列,例如 `pixel_x` / `source_path`)。 ``` .\.venv\Scripts\python.exe freqgauge_view_csv.py .\.venv\Scripts\python.exe freqgauge_view_csv.py out\merged.csv ``` 使用 **Open CSV**(或在命令行中传递路径),每次选择**一个区域**,通过**下拉菜单**(常见宽度)、**对数比例宽度滑块**和/或 **−** / **+** 来设置**时间缩放**。当滑块与预设不匹配时,下拉菜单会切换为 **Custom**。**Scroll time** 会沿 UTC 轴移动可见窗口。**Reset view** 显示完整的时间范围。 ### 绘图详情(校准) **区域** ``` PLOT_REGIONS = { "EI": {"x1": 100, "x2": 1180, "y1": 43, "y2": 220}, "WECC": {"x1": 100, "x2": 1180, "y1": 342, "y2": 520}, "ERCOT": {"x1": 100, "x2": 1180, "y1": 642, "y2": 820}, "Quebec": {"x1": 100, "x2": 1180, "y1": 942, "y2": 1120}, } ``` **颜色代码 (RGB)** EI: 5.1, 55.7, 87.1 WECC: 2.0, 58.5, 16.9 ERCOT: 88.6, 26.3, 11.8 Quebec: 82.0, 1.6, 79.2标签:ENF提取, FFmpeg, FFT, GUI可视化, NumPy, OpenCV, Python, QIFFT, SciPy, 信号处理, 北美电网, 录音鉴定, 数字取证, 无后门, 时序分析, 时间戳验证, 电力系统, 电网频率分析, 科学研究, 自动化脚本, 视频取证, 调查辅助工具, 逆向工具, 音频取证, 音频处理, 频谱分析