import 'dart:async'; import 'dart:isolate'; import 'dart:typed_data'; import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart' show debugPrint; import 'package:flutter/services.dart' show rootBundle; import '../camera/motion_detector.dart'; import 'background_model.dart'; import 'detection_result.dart'; import 'tflite_detector.dart'; import 'visual_prior.dart'; /// 推理工作单元:模型加载与检测全部在后台 isolate 执行, /// 主 isolate 只投递帧数据、接收结果,UI 不被推理阻塞(iOS 真机卡顿根因)。 class DetectorWorker { static const String modelAsset = 'assets/model.tflite'; static const String labelsAsset = 'assets/labels.txt'; final Isolate _isolate; final ReceivePort _responses; final _controlPort = Completer(); final _ready = Completer(); SendPort? _port; /// 在途帧数(主 isolate 侧计数,用于丢帧) int _inFlight = 0; bool _dead = false; /// 结果回调:结果 / 运动区域 / 新颖区域 / 旋转角 / 图宽 / 图高 / 处理耗时 ms void Function(List, List, List, int, int, int, int)? onResult; /// 单帧处理异常回调(不影响相机流) void Function(String)? onError; /// 最近一次创建失败的诊断原因(UI 展示用) static String? lastLoadError; /// worker 最近上报的执行步骤(诊断用) static String? lastLog; DetectorWorker._(this._isolate, this._responses) { _responses.listen(_onMessage, onDone: () { _dead = true; if (!_ready.isCompleted) { _ready.completeError(StateError('推理进程异常退出')); } onError?.call('推理进程异常退出'); }); } /// 读取模型资产并启动后台推理 isolate;加载失败返回 null(App 降级为仅预览)。 static Future create() async { try { final data = await rootBundle.load(modelAsset); final modelBytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); final labels = (await rootBundle.loadString(labelsAsset)) .split('\n') .where((l) => l.trim().isNotEmpty) .toList(); final responses = ReceivePort(); final isolate = await Isolate.spawn(_workerMain, responses.sendPort); final worker = DetectorWorker._(isolate, responses); final port = await worker._controlPort.future .timeout(const Duration(seconds: 10), onTimeout: () => throw TimeoutException('worker port timeout')); worker._port = port; port.send(['load', modelBytes, labels]); await worker._ready.future .timeout(const Duration(seconds: 20), onTimeout: () { throw TimeoutException('model load timeout'); }); return worker; } catch (e) { lastLoadError = e.toString(); debugPrint('[DetectorWorker] create failed: $e'); return null; } } /// 是否忙(上一帧尚未返回):忙则丢帧,避免在途积压 bool get busy => _inFlight > 0; void analyze(CameraImage image, int rotationDegrees) { final port = _port; if (port == null || _dead) return; _inFlight++; port.send([ 'frame', [ image.planes.map((p) => p.bytes).toList(), image.planes.map((p) => p.bytesPerRow).toList(), image.width, image.height, image.format.group == ImageFormatGroup.bgra8888, rotationDegrees, ], ]); } void _onMessage(dynamic msg) { final list = msg as List; switch (list[0] as String) { case 'port': _controlPort.complete(list[1] as SendPort); break; case 'ready': _ready.complete(); break; case 'load-error': _ready.completeError(StateError( list.length > 1 ? list[1] as String : 'model load failed')); break; case 'result': _inFlight--; final dets = (list[4] as List).map((d) { final v = d as List; return DetectionResult( label: v[0] as String, score: v[1] as double, left: v[2] as double, top: v[3] as double, right: v[4] as double, bottom: v[5] as double, ); }).toList(); final motion = (list[5] as List) .map((m) => m as List) .map((v) => MotionRegion( v[0] as double, v[1] as double, v[2] as double, v[3] as double)) .toList(); final novelty = (list[6] as List) .map((m) => m as List) .map((v) => MotionRegion( v[0] as double, v[1] as double, v[2] as double, v[3] as double)) .toList(); onResult?.call(dets, motion, novelty, list[1] as int, list[2] as int, list[3] as int, list[7] as int); break; case 'log': lastLog = list[1] as String; debugPrint('[DetectorWorker] $lastLog'); break; case 'error': _inFlight--; onError?.call(list[1] as String); } } /// 相机切换/场景变化后重置运动与背景参考 void reset() { final port = _port; if (port == null || _dead) return; port.send(['reset']); } void dispose() { _dead = true; _isolate.kill(priority: Isolate.immediate); _responses.close(); } } /// 后台 isolate 入口:串行处理 load / frame / reset 命令。 /// 所有回发必须走 [mainPort](主 isolate 的端口);control 是 worker 自己的 /// 收件箱,往 control.sendPort 发消息等于发给自己,主 isolate 永远收不到。 Future _workerMain(SendPort mainPort) async { final control = ReceivePort(); mainPort.send(['port', control.sendPort]); mainPort.send(['log', 'worker-start']); TfliteDetector? detector; MotionDetector? motion; BackgroundModel? background; await for (final msg in control) { try { final list = msg as List; switch (list[0] as String) { case 'load': mainPort.send(['log', 'load-received']); try { detector = await TfliteDetector.fromBuffer( list[1] as Uint8List, (list[2] as List).cast()); if (detector == null) { mainPort.send(['load-error', 'fromBuffer 返回 null']); } else { mainPort.send(['log', 'fromBuffer-ok']); motion = MotionDetector(); background = BackgroundModel(); mainPort.send(['ready']); } } catch (e) { mainPort.send(['load-error', '$e']); } break; case 'frame': final d = detector; final m = motion; final b = background; if (d == null || m == null || b == null) break; final frame = list[1] as List; final planes = (frame[0] as List).cast(); final strides = (frame[1] as List).cast(); final width = frame[2] as int; final height = frame[3] as int; final isBgra = frame[4] as bool; final rotation = frame[5] as int; final sw = Stopwatch()..start(); var results = d.detectRaw( planes: planes, strides: strides, width: width, height: height, isBgra: isBgra, ); // 低分野鸡框过视觉先验(颜色/位置),减少户外误报 results = VisualPrior.filter( results, planes: planes, strides: strides, width: width, height: height, isBgra: isBgra, ); final motionRegions = m.detectMotionRaw( planes[0], strides[0], width, height); final noveltyRegions = b.updateRaw(planes[0], strides[0], width, height); sw.stop(); mainPort.send([ 'result', rotation, width, height, results .map((r) => [r.label, r.score, r.left, r.top, r.right, r.bottom]) .toList(), motionRegions .map((mr) => [mr.left, mr.top, mr.right, mr.bottom]) .toList(), noveltyRegions .map((mr) => [mr.left, mr.top, mr.right, mr.bottom]) .toList(), sw.elapsedMilliseconds, ]); break; case 'reset': motion?.reset(); background?.reset(); } } catch (e) { mainPort.send(['error', '$e']); } } }