1
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert' show jsonEncode;
|
||||
import 'dart:ui' as ui show PlatformDispatcher;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../auth/session_store.dart';
|
||||
import '../detection/detection_result.dart';
|
||||
import '../detection/detector_worker.dart';
|
||||
import '../feedback/feedback_api.dart';
|
||||
import '../feedback/false_target_capture.dart';
|
||||
import '../models/model_manager.dart';
|
||||
import '../reminder/reminder.dart';
|
||||
import 'app_camera_controller.dart';
|
||||
@@ -55,6 +62,11 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||
/// 胶囊与顶部横幅显示「识别准备中」,不误报「模型加载失败」
|
||||
bool _workerPending = false;
|
||||
|
||||
/// 假目标上报进行中(防重复触发)
|
||||
bool _reporting = false;
|
||||
|
||||
static const _falseTargetConsent = FlutterSecureStorage();
|
||||
|
||||
void _onDevTaps() {
|
||||
final now = DateTime.now();
|
||||
_devTaps.add(now);
|
||||
@@ -196,6 +208,99 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 假目标上报:一键上报当前画面——按一次快照帧(与喂给 YOLO 的帧同源同尺寸)
|
||||
/// + 当前全部检测框快照,单独同意(首次)→ isolate 内整帧 jpg 重编码
|
||||
/// (剥离全部元数据)→ 上传。无任何额外选择步骤,取消/失败不影响识别。
|
||||
Future<void> _startFalseTargetReport() async {
|
||||
final analyzer = _analyzer;
|
||||
final vm = _viewModel;
|
||||
if (_reporting || analyzer == null || vm == null || !vm.state.modelReady) {
|
||||
return;
|
||||
}
|
||||
_reporting = true;
|
||||
try {
|
||||
final snap = await analyzer
|
||||
.takeSnapshot()
|
||||
.timeout(const Duration(seconds: 2), onTimeout: () => null);
|
||||
if (!mounted || snap == null) {
|
||||
_toast('未获取到画面,请稍后重试');
|
||||
return;
|
||||
}
|
||||
final ok = await _ensureFalseTargetConsent();
|
||||
if (!mounted || !ok) return;
|
||||
_toast('正在上报…');
|
||||
final boxes = List<DetectionResult>.of(vm.state.results)
|
||||
.where((b) => b.width > 0 && b.height > 0)
|
||||
.toList();
|
||||
final detections = jsonEncode([
|
||||
for (final b in boxes)
|
||||
{
|
||||
'label': b.label,
|
||||
'class': b.classId,
|
||||
'score': double.parse(b.score.toStringAsFixed(4)),
|
||||
'model': b.modelName,
|
||||
'cx': double.parse(b.centerX.toStringAsFixed(6)),
|
||||
'cy': double.parse(b.centerY.toStringAsFixed(6)),
|
||||
'w': double.parse(b.width.toStringAsFixed(6)),
|
||||
'h': double.parse(b.height.toStringAsFixed(6)),
|
||||
},
|
||||
]);
|
||||
final jpeg = await compute(encodeFrameJpeg, snap);
|
||||
await FeedbackApi(sessionStore: SessionStore()).reportFalseTarget(
|
||||
jpeg: jpeg,
|
||||
detectionsJson: detections,
|
||||
sourceW: snap.width,
|
||||
sourceH: snap.height,
|
||||
);
|
||||
_toast('感谢反馈!我们会用它改进识别');
|
||||
} catch (e) {
|
||||
_toast('上报失败: $e');
|
||||
} finally {
|
||||
_reporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 单独同意(PIPL):首次上报前弹说明,可拒绝且不影响识别;同意后记录不再弹
|
||||
Future<bool> _ensureFalseTargetConsent() async {
|
||||
if (await _falseTargetConsent.read(key: falseTargetConsentKey) == '1') {
|
||||
return true;
|
||||
}
|
||||
if (!mounted) return false;
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('上报说明'),
|
||||
content: const Text(
|
||||
'将上传当前画面一帧(与识别使用的画面一致,上传前已在本地重新编码、'
|
||||
'移除位置等全部照片信息)及当时的识别框,用于改进识别模型。\n\n'
|
||||
'该功能完全自愿,拒绝不影响任何其他功能。已上传的图片可联系客服删除。',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('暂不上报'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text('同意并继续'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok == true) {
|
||||
await _falseTargetConsent.write(key: falseTargetConsentKey, value: '1');
|
||||
}
|
||||
return ok == true;
|
||||
}
|
||||
|
||||
void _toast(String msg) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(
|
||||
content: Text(msg), duration: const Duration(seconds: 2)));
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -508,6 +613,34 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// 假目标上报入口:识别就绪且有框时出现(完全自愿,可忽略)
|
||||
if (vm.state.modelReady && vm.state.results.isNotEmpty && !_workerPending)
|
||||
Positioned(
|
||||
left: 12,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 72,
|
||||
child: Material(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: _startFalseTargetReport,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.flag_outlined,
|
||||
color: Colors.orangeAccent, size: 16),
|
||||
SizedBox(width: 5),
|
||||
Text('误报上报',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
left: 8,
|
||||
right: 8,
|
||||
|
||||
Reference in New Issue
Block a user