132 lines
3.3 KiB
Dart
132 lines
3.3 KiB
Dart
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)。
|
||
/// 推理在后台 isolate(DetectorWorker)执行,主 isolate 只投递帧与收结果。
|
||
class FrameAnalyzer {
|
||
/// 连续检测:100ms 一帧
|
||
int intervalMs = 100;
|
||
|
||
/// null = 模型加载失败,仅预览不分析
|
||
final DetectorWorker? worker;
|
||
final CameraViewModel viewModel;
|
||
|
||
int _lastDetectMs = 0;
|
||
|
||
/// 诊断计数:推理调用/异常次数
|
||
int detectCalls = 0;
|
||
int detectErrors = 0;
|
||
String? lastError;
|
||
|
||
/// 最近一次完整处理(预处理+推理+运动检测)耗时 ms(worker 侧)
|
||
int lastProcessMs = 0;
|
||
|
||
/// 图像流回调是否到达(诊断用)
|
||
int framesReceived = 0;
|
||
|
||
FrameAnalyzer({required this.worker, required this.viewModel}) {
|
||
worker?.onResult = _onResult;
|
||
worker?.onError = _onError;
|
||
}
|
||
|
||
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();
|
||
}
|