Files
observer/flutter_app/lib/camera/frame_analyzer.dart
T
2026-09-03 17:50:23 +08:00

148 lines
4.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:typed_data';
import 'package:camera/camera.dart';
import '../detection/detection_result.dart';
import '../detection/detector_worker.dart';
import 'camera_view_model.dart';
/// 抽帧节流 + 后台推理(对应 Kotlin FrameAnalyzer)。
/// 推理在后台 isolateDetectorWorker)执行,主 isolate 只投递帧与收结果。
class FrameAnalyzer {
/// 连续检测:100ms 一帧
int intervalMs = 100;
DetectorWorker? _worker;
/// 当前推理 worker(null = 仅预览不分析);相机启动后可用 [attachWorker]
/// 原地替换(相机帧流回调闭包捕获的是本 analyzer 对象,无需重启相机)
DetectorWorker? get worker => _worker;
final CameraViewModel viewModel;
int _lastDetectMs = 0;
/// 诊断计数:推理调用/异常次数
int detectCalls = 0;
int detectErrors = 0;
String? lastError;
/// 最近一次完整处理(预处理+推理+运动检测)耗时 ms(worker 侧)
int lastProcessMs = 0;
/// 图像流回调是否到达(诊断用)
int framesReceived = 0;
FrameAnalyzer({DetectorWorker? worker, required this.viewModel}) {
attachWorker(worker);
}
/// 替换推理 workernull = 停识别仅预览)。旧 worker 在此释放;若相机已
/// 启动则新 worker 立即接管后续帧——重启相机会重新 initialize
/// (iOS ~1s+)且预览闪断,原地替换即时生效(2026-09-03)。
void attachWorker(DetectorWorker? w) {
if (identical(_worker, w)) return;
final old = _worker;
_worker = w;
w?.onResult = _onResult;
w?.onError = _onError;
old?.dispose();
_lastDetectMs = 0;
}
void recordStreamError(String msg) {
lastError = msg;
detectErrors++;
}
void _onResult(
List<DetectionResult> results,
List<MotionRegion> motion,
List<MotionRegion> novelty,
int rotation,
int width,
int height,
int processMs,
String yuvDiag) {
detectCalls++;
lastProcessMs = processMs;
viewModel.onFramesAnalyzed(
results,
rotation,
width,
height,
motion,
novelty,
detectCalls: detectCalls,
detectErrors: detectErrors,
lastError: lastError,
framesReceived: framesReceived,
lastProcessMs: lastProcessMs,
yuvDiag: yuvDiag,
);
}
void _onError(String msg) {
detectErrors++;
lastError = msg;
viewModel.onFramesAnalyzed(
const [],
90,
0,
0,
const [],
const [],
detectCalls: detectCalls,
detectErrors: detectErrors,
lastError: lastError,
framesReceived: framesReceived,
lastProcessMs: lastProcessMs,
);
}
/// 节流与 busy 丢帧判定(两入口共用);通过后才允许投递
bool _canSend() {
framesReceived++;
final w = worker;
if (w == null) return false;
final now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastDetectMs < intervalMs) return false;
_lastDetectMs = now;
if (w.busy) return false; // 上一帧未返回则丢帧,避免在途积压
return true;
}
void analyze(CameraImage image, int rotationDegrees,
{bool rgbaOrder = false}) {
if (!_canSend()) return;
worker!.analyze(image, rotationDegrees, rgbaOrder: rgbaOrder);
}
/// 原生相机通道帧(Android):字节已在 Kotlin 侧旋转成竖屏,rotation=0。
/// isBgra=true + rgbaOrder 与插件路径同语义:false=BGRA(rOff=2)/true=RGBA(rOff=0)
void analyzeRaw({
required List<Uint8List> planes,
required List<int> strides,
required int width,
required int height,
required bool isBgra,
required bool rgbaOrder,
int rotationDegrees = 0,
}) {
if (!_canSend()) return;
worker!.analyzeRaw(
planes: planes,
strides: strides,
width: width,
height: height,
isBgra: isBgra,
rgbaOrder: rgbaOrder,
rotationDegrees: rotationDegrees,
);
}
void reset() => _lastDetectMs = 0;
void dispose() => _worker?.dispose();
}