迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
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<SendPort>();
|
||||
final _ready = Completer<void>();
|
||||
|
||||
SendPort? _port;
|
||||
|
||||
/// 在途帧数(主 isolate 侧计数,用于丢帧)
|
||||
int _inFlight = 0;
|
||||
bool _dead = false;
|
||||
|
||||
/// 结果回调:结果 / 运动区域 / 新颖区域 / 旋转角 / 图宽 / 图高 / 处理耗时 ms
|
||||
void Function(List<DetectionResult>, List<MotionRegion>, List<MotionRegion>,
|
||||
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<DetectorWorker?> 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<void> _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<String>());
|
||||
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<Uint8List>();
|
||||
final strides = (frame[1] as List).cast<int>();
|
||||
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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user