视觉分析(模拟)app

This commit is contained in:
lzx
2026-01-07 16:14:34 +08:00
commit 40991e6c86
136 changed files with 6568 additions and 0 deletions

View File

@@ -0,0 +1,462 @@
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:image_picker/image_picker.dart';
import 'package:gal/gal.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:permission_handler/permission_handler.dart';
import 'package:camera_app/services/yolo_service.dart';
import 'package:camera_app/screens/result_screen.dart';
class CameraScreen extends StatefulWidget {
const CameraScreen({super.key});
@override
State<CameraScreen> createState() => _CameraScreenState();
}
class _CameraScreenState extends State<CameraScreen> {
CameraController? _controller;
List<CameraDescription>? _cameras;
bool _isInitialized = false;
bool _isCapturing = false;
int _photoCount = 0;
Timer? _timer;
List<String> _savedPhotoPaths = [];
bool _isWebOrDesktop = false;
final YOLOService _yoloService = YOLOService();
@override
void initState() {
super.initState();
_initializeCamera();
}
Future<void> _initializeCamera() async {
try {
// Web 和桌面平台使用 image_picker
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) {
if (mounted) {
setState(() {
_isWebOrDesktop = true;
_isInitialized = true; // Web/桌面平台直接显示界面
});
}
return;
}
// 移动平台使用 camera 包
await _requestPermissions();
_cameras = await availableCameras();
if (_cameras == null || _cameras!.isEmpty) {
_showError('未找到可用的相机');
return;
}
_controller = CameraController(
_cameras![0],
ResolutionPreset.high,
enableAudio: false,
);
await _controller!.initialize();
if (mounted) {
setState(() {
_isInitialized = true;
});
}
} catch (e) {
_showError('初始化相机失败: $e');
}
}
Future<void> _requestPermissions() async {
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) {
return; // Web/桌面平台权限由浏览器/系统处理
}
final cameraStatus = await Permission.camera.request();
if (!cameraStatus.isGranted) {
_showError('需要相机权限才能使用此功能');
return;
}
if (Platform.isAndroid) {
final photosStatus = await Permission.photos.request();
if (photosStatus.isDenied || photosStatus.isPermanentlyDenied) {
final storageStatus = await Permission.storage.request();
if (!storageStatus.isGranted) {
_showError('需要存储权限才能保存图片');
return;
}
}
} else if (Platform.isIOS) {
final photosStatus = await Permission.photos.request();
if (!photosStatus.isGranted) {
_showError('需要照片权限才能保存图片');
return;
}
}
}
Future<void> _startCapturing() async {
if (_isWebOrDesktop) {
_showError('Web/桌面平台不支持自动连续拍照,请使用移动设备');
return;
}
// 移动平台:自动拍照模式
if (!_isInitialized || _controller == null || !_controller!.value.isInitialized) {
_showError('相机未初始化');
return;
}
setState(() {
_isCapturing = true;
_photoCount = 0;
_savedPhotoPaths.clear();
});
// 立即拍摄第一张
await _takePhoto();
// 然后每2秒拍摄一张直到点击停止
_timer = Timer.periodic(const Duration(seconds: 2), (timer) async {
if (_isCapturing) {
await _takePhoto();
} else {
timer.cancel();
}
});
}
Future<void> _stopCapturing() async {
setState(() {
_isCapturing = false;
});
_timer?.cancel();
if (_savedPhotoPaths.isEmpty) {
_showError('没有拍摄照片');
return;
}
// 显示分析中对话框
if (!mounted) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('正在分析照片...'),
],
),
);
},
);
try {
// 使用YOLO分析照片
final analysisResults = await _yoloService.analyzeImages(_savedPhotoPaths);
if (!mounted) return;
Navigator.of(context).pop(); // 关闭分析中对话框
// 导航到结果页面
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => ResultScreen(
photoPaths: _savedPhotoPaths,
analysisResults: analysisResults,
),
),
);
} catch (e) {
if (!mounted) return;
Navigator.of(context).pop(); // 关闭分析中对话框
_showError('分析照片失败: $e');
}
}
Future<void> _takePhoto() async {
if (_controller == null || !_controller!.value.isInitialized) {
return;
}
try {
final XFile photo = await _controller!.takePicture();
await _savePhoto(photo.path);
if (mounted) {
setState(() {
_photoCount++;
});
}
} catch (e) {
if (mounted) {
_showError('拍照失败: $e');
}
}
}
Future<void> _savePhoto(String photoPath) async {
try {
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
// 移动平台:保存到相册
await Gal.putImage(photoPath);
} else {
// Web/桌面平台:保存到下载目录
final appDir = await getApplicationDocumentsDirectory();
final fileName = 'photo_${DateTime.now().millisecondsSinceEpoch}.jpg';
final savedPath = path.join(appDir.path, fileName);
await File(photoPath).copy(savedPath);
}
_savedPhotoPaths.add(photoPath);
} catch (e) {
// 保存失败不影响流程
_savedPhotoPaths.add(photoPath);
}
}
void _showError(String message) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
}
@override
void dispose() {
_timer?.cancel();
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) {
return Scaffold(
backgroundColor: Colors.black,
body: const Center(
child: CircularProgressIndicator(color: Colors.white),
),
);
}
// Web/桌面平台界面
if (_isWebOrDesktop) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.camera_alt,
size: 100,
color: Colors.white,
),
const SizedBox(height: 32),
Text(
'$_photoCount',
style: const TextStyle(
color: Colors.white,
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
const Text(
'Web/桌面平台不支持自动连续拍照',
style: TextStyle(
color: Colors.white70,
fontSize: 18,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
tooltip: '返回',
),
],
),
),
),
);
}
// 移动平台:显示相机预览
if (_controller == null || !_controller!.value.isInitialized) {
return Scaffold(
backgroundColor: Colors.black,
body: const Center(
child: Text(
'相机初始化中...',
style: TextStyle(color: Colors.white),
),
),
);
}
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
SizedBox.expand(child: CameraPreview(_controller!)),
SafeArea(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
if (_isCapturing) {
_stopCapturing();
}
Navigator.of(context).pop();
},
),
if (_isCapturing)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.8),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
'$_photoCount',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
child: _isCapturing
? Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: _stopCapturing,
icon: const Icon(Icons.stop, size: 28),
label: const Text(
'停止拍照',
style: TextStyle(fontSize: 18),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
),
const SizedBox(height: 16),
Text(
'已拍摄 $_photoCount 张照片',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
)
: ElevatedButton.icon(
onPressed: _startCapturing,
icon: const Icon(Icons.camera_alt, size: 28),
label: const Text(
'开始拍照',
style: TextStyle(fontSize: 18),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,294 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:camera_app/services/yolo_service.dart';
class ResultScreen extends StatefulWidget {
final List<String> photoPaths;
final Map<String, YOLOAnalysisResult> analysisResults;
const ResultScreen({
super.key,
required this.photoPaths,
required this.analysisResults,
});
@override
State<ResultScreen> createState() => _ResultScreenState();
}
class _ResultScreenState extends State<ResultScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('分析结果'),
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
elevation: 0,
),
body: Column(
children: [
// 统计信息卡片
Container(
width: double.infinity,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.photo_library, color: Colors.blue.shade700),
const SizedBox(width: 8),
Text(
'共拍摄 ${widget.photoPaths.length} 张照片',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade900,
),
),
],
),
const SizedBox(height: 12),
Text(
'已分析 ${widget.analysisResults.length}',
style: TextStyle(
fontSize: 14,
color: Colors.blue.shade700,
),
),
],
),
),
// 分析结果列表
Expanded(
child: widget.analysisResults.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.info_outline,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'暂无分析结果',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: widget.photoPaths.length,
itemBuilder: (context, index) {
final photoPath = widget.photoPaths[index];
final result = widget.analysisResults[photoPath];
return _buildPhotoAnalysisCard(photoPath, result, index);
},
),
),
// 完成按钮
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: ElevatedButton(
onPressed: () {
// 返回欢迎页面
Navigator.of(context).popUntil((route) => route.isFirst);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'完成',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
Widget _buildPhotoAnalysisCard(
String photoPath,
YOLOAnalysisResult? result,
int index,
) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 图片预览
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: Image.file(
File(photoPath),
width: double.infinity,
height: 200,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: double.infinity,
height: 200,
color: Colors.grey.shade200,
child: const Icon(Icons.broken_image, size: 64),
);
},
),
),
// 分析结果
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.image, color: Colors.blue.shade700),
const SizedBox(width: 8),
Text(
'照片 ${index + 1}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
],
),
const SizedBox(height: 12),
if (result == null)
const Text(
'分析中...',
style: TextStyle(color: Colors.grey),
)
else if (result.error != null)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade200),
),
child: Row(
children: [
Icon(Icons.warning, color: Colors.orange.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(
result.error!,
style: TextStyle(color: Colors.orange.shade900),
),
),
],
),
)
else if (result.objects.isEmpty)
const Text(
'未检测到物体',
style: TextStyle(color: Colors.grey),
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'检测到 ${result.objects.length} 个物体:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
...result.objects.map((obj) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: Row(
children: [
Icon(Icons.label, color: Colors.green.shade700),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
obj.label,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green.shade900,
),
),
Text(
'置信度: ${(obj.confidence * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
),
),
],
),
),
],
),
),
)),
],
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'camera_screen.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.blue.shade400,
Colors.blue.shade700,
],
),
),
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 图标
Icon(
Icons.camera_alt,
size: 100,
color: Colors.white,
),
const SizedBox(height: 30),
// 标题
const Text(
'sight-identification',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 20),
// 说明文字
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Text(
'点击按钮开始自动拍照\n每2秒拍摄一次',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.9),
height: 1.5,
),
),
),
const SizedBox(height: 60),
// 开始按钮
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CameraScreen(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.blue.shade700,
padding: const EdgeInsets.symmetric(
horizontal: 50,
vertical: 18,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 5,
),
child: const Text(
'开始拍照',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
),
);
}
}