189 lines
5.4 KiB
Dart
189 lines
5.4 KiB
Dart
import 'dart:async';
|
||
import 'dart:typed_data';
|
||
|
||
import 'package:camera/camera.dart';
|
||
|
||
import '../detection/detection_result.dart';
|
||
import '../detection/detector_worker.dart';
|
||
import '../feedback/false_target_capture.dart';
|
||
import 'camera_view_model.dart';
|
||
|
||
/// 抽帧节流 + 后台推理(对应 Kotlin FrameAnalyzer)。
|
||
/// 推理在后台 isolate(DetectorWorker)执行,主 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;
|
||
|
||
/// 假目标上报快照:armed 时下一帧(节流前)拷贝单平面像素并交付。
|
||
/// Completer 泛型必须可空:运行时 future 类型决定 .timeout(onTimeout) 的
|
||
/// 回调签名,非空 Completer 会让 `() => null` 触发运行时子类型错误
|
||
Completer<FrameSnapshot?>? _snapCompleter;
|
||
|
||
FrameAnalyzer({DetectorWorker? worker, required this.viewModel}) {
|
||
attachWorker(worker);
|
||
}
|
||
|
||
/// 取下一帧快照(假目标上报用;在节流判定之前捕获,~33ms 内必有帧)
|
||
Future<FrameSnapshot?> takeSnapshot() {
|
||
final existing = _snapCompleter;
|
||
if (existing != null && !existing.isCompleted) {
|
||
return existing.future;
|
||
}
|
||
final c = Completer<FrameSnapshot?>();
|
||
_snapCompleter = c;
|
||
return c.future;
|
||
}
|
||
|
||
/// 帧到达(节流判定前):armed 则拷贝像素完成快照
|
||
void _captureIfNeeded(
|
||
Uint8List plane, int bytesPerRow, int width, int height, bool bgra) {
|
||
final c = _snapCompleter;
|
||
if (c == null || c.isCompleted) return;
|
||
_snapCompleter = null;
|
||
c.complete(FrameSnapshot(
|
||
plane: Uint8List.fromList(plane),
|
||
bytesPerRow: bytesPerRow,
|
||
width: width,
|
||
height: height,
|
||
bgra: bgra,
|
||
));
|
||
}
|
||
|
||
/// 替换推理 worker(null = 停识别仅预览)。旧 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}) {
|
||
final p = image.planes.isNotEmpty ? image.planes.first : null;
|
||
if (p != null) {
|
||
_captureIfNeeded(
|
||
p.bytes, p.bytesPerRow, image.width, image.height, !rgbaOrder);
|
||
}
|
||
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 (planes.isNotEmpty) {
|
||
_captureIfNeeded(planes.first, strides.first, width, height, isBgra);
|
||
}
|
||
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();
|
||
}
|