add first version
This commit is contained in:
14
.claude/settings.local.json
Normal file
14
.claude/settings.local.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cd:*)",
|
||||
"Bash(cd /c/Users/xdedmi/Desktop/aisee/aisee_flutter && /c/src/flutter/bin/flutter pub get 2>&1)",
|
||||
"Bash(cmd.exe:*)",
|
||||
"Bash(cd /c/Users/xdedmi/Desktop/aisee/aisee_flutter && /c/Users/xdedmi/flutter/bin/flutter pub get 2>&1)",
|
||||
"Bash(cd /c/Users/xdedmi/Desktop/aisee/aisee_flutter && /c/Users/xdedmi/flutter/bin/flutter devices 2>&1)",
|
||||
"Bash(cd /c/Users/xdedmi/Desktop/aisee/aisee_flutter && /c/Users/xdedmi/flutter/bin/flutter emulators 2>&1)",
|
||||
"Bash(adb devices:*)",
|
||||
"Bash(/c/Users/xdedmi/flutter/bin/flutter doctor:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
103
aisee_flutter/Android设备准备.md
Normal file
103
aisee_flutter/Android设备准备.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# 快速启动 Android 模拟器
|
||||
|
||||
## 当前状态
|
||||
|
||||
✅ Flutter 已安装:`C:\Users\xdedmi\flutter`
|
||||
✅ 项目依赖已安装
|
||||
❌ 没有 Android 模拟器
|
||||
|
||||
## 创建 Android 模拟器
|
||||
|
||||
### 方法 1:使用 Android Studio(推荐)
|
||||
|
||||
1. **打开 Android Studio**
|
||||
|
||||
2. **打开 Device Manager**
|
||||
- 点击右上角的设备图标
|
||||
- 或 Tools -> Device Manager
|
||||
|
||||
3. **创建新设备**
|
||||
- 点击 "Create Device"
|
||||
- 选择设备:Pixel 6 Pro
|
||||
- 选择系统镜像:API 34 (Android 14)
|
||||
- 点击 "Finish"
|
||||
|
||||
4. **启动模拟器**
|
||||
- 点击播放按钮启动模拟器
|
||||
- 等待 2-3 分钟启动完成
|
||||
|
||||
### 方法 2:使用命令行
|
||||
|
||||
```bash
|
||||
# 1. 查看可用的系统镜像
|
||||
C:\Users\xdedmi\flutter\bin\flutter doctor
|
||||
|
||||
# 2. 如果 Android Studio 已安装,创建模拟器
|
||||
# 打开 Android Studio -> Device Manager -> Create Device
|
||||
```
|
||||
|
||||
## 启动项目(Android)
|
||||
|
||||
模拟器启动后:
|
||||
|
||||
```bash
|
||||
cd C:\Users\xdedmi\Desktop\aisee\aisee_flutter
|
||||
|
||||
# 检查设备
|
||||
C:\Users\xdedmi\flutter\bin\flutter devices
|
||||
|
||||
# 运行项目
|
||||
C:\Users\xdedmi\flutter\bin\flutter run
|
||||
```
|
||||
|
||||
## 使用真机(更快)
|
||||
|
||||
如果有 Android 手机:
|
||||
|
||||
1. **开启开发者选项**
|
||||
- 设置 -> 关于手机
|
||||
- 连续点击"版本号" 7 次
|
||||
|
||||
2. **开启 USB 调试**
|
||||
- 设置 -> 开发者选项
|
||||
- 开启 "USB 调试"
|
||||
|
||||
3. **连接手机**
|
||||
- USB 连接手机到电脑
|
||||
- 手机上允许 USB 调试授权
|
||||
|
||||
4. **运行项目**
|
||||
```bash
|
||||
C:\Users\xdedmi\flutter\bin\flutter devices
|
||||
C:\Users\xdedmi\flutter\bin\flutter run
|
||||
```
|
||||
|
||||
## 当前运行状态
|
||||
|
||||
项目正在 Windows 桌面模式编译运行,你可以先看到界面效果。
|
||||
|
||||
**注意**:相机功能需要 Android 设备才能使用。
|
||||
|
||||
## 快捷脚本
|
||||
|
||||
我已经创建了启动脚本,但需要更新 Flutter 路径。
|
||||
|
||||
编辑 `install.bat` 和 `run.bat`,在开头添加:
|
||||
|
||||
```batch
|
||||
@echo off
|
||||
set PATH=C:\Users\xdedmi\flutter\bin;%PATH%
|
||||
```
|
||||
|
||||
然后就可以双击运行了。
|
||||
|
||||
## 下一步
|
||||
|
||||
1. ⏳ 等待 Windows 版本编译完成(首次约 5-10 分钟)
|
||||
2. 📱 创建 Android 模拟器或连接真机
|
||||
3. 🚀 在 Android 设备上运行项目
|
||||
4. 📷 测试相机功能
|
||||
|
||||
---
|
||||
|
||||
需要帮助创建模拟器吗?
|
||||
@@ -1,12 +1,109 @@
|
||||
# aisee_flutter
|
||||
# AISee Flutter 实时相机
|
||||
|
||||
A new Flutter project.
|
||||
实时相机图像捕获和传输演示项目。
|
||||
|
||||
## Getting Started
|
||||
## 功能特性
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
- ✅ 实时相机预览
|
||||
- ✅ 图像自动压缩(640x480,质量 85%)
|
||||
- ✅ 定时捕获(每 500ms 一帧)
|
||||
- ✅ 异步上传到后端
|
||||
- ✅ 单张拍照并分析
|
||||
- ✅ 前后摄像头切换
|
||||
- ✅ 实时状态显示
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
### 2. 配置后端地址
|
||||
|
||||
编辑 `lib/utils/app_config.dart`:
|
||||
|
||||
```dart
|
||||
// Android 模拟器
|
||||
static const String apiBaseUrl = 'http://10.0.2.2:8000';
|
||||
|
||||
// 真机(改为你的电脑 IP)
|
||||
static const String apiBaseUrl = 'http://192.168.1.100:8000';
|
||||
```
|
||||
|
||||
### 3. 运行项目
|
||||
|
||||
```bash
|
||||
flutter run
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart # 入口和首页
|
||||
├── screens/
|
||||
│ └── camera_screen.dart # 相机页面
|
||||
├── services/
|
||||
│ ├── camera_service.dart # 相机服务
|
||||
│ └── api_service.dart # API 服务
|
||||
└── utils/
|
||||
└── app_config.dart # 配置
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. **打开相机**:点击首页"打开相机"按钮
|
||||
2. **拍照分析**:拍摄单张照片并上传分析
|
||||
3. **开始传输**:每 500ms 自动捕获并上传
|
||||
4. **停止传输**:停止实时捕获
|
||||
5. **切换摄像头**:前后摄像头切换
|
||||
|
||||
## 配置参数
|
||||
|
||||
在 `lib/utils/app_config.dart` 中调整:
|
||||
|
||||
```dart
|
||||
static const int imageMaxWidth = 640; // 图像宽度
|
||||
static const int imageMaxHeight = 480; // 图像高度
|
||||
static const int imageQuality = 85; // JPEG 质量
|
||||
static const Duration captureInterval = Duration(milliseconds: 500); // 捕获间隔
|
||||
```
|
||||
|
||||
## 后端 API
|
||||
|
||||
需要实现以下端点:
|
||||
|
||||
```
|
||||
POST /api/v1/images/upload # 上传图像
|
||||
POST /api/v1/analysis/analyze # 分析图像
|
||||
```
|
||||
|
||||
详细文档见项目根目录的技术方案文档。
|
||||
|
||||
## 依赖
|
||||
|
||||
- camera: ^0.10.5+9
|
||||
- image: ^4.1.7
|
||||
- dio: ^5.4.1
|
||||
- permission_handler: ^11.3.0
|
||||
- provider: ^6.1.1
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 相机权限被拒绝?**
|
||||
A: 在手机设置中授予相机权限并重启应用。
|
||||
|
||||
**Q: 网络连接失败?**
|
||||
A: 检查后端服务是否启动,确认 IP 地址配置正确。
|
||||
|
||||
**Q: 上传速度慢?**
|
||||
A: 降低图像分辨率或增加捕获间隔。
|
||||
|
||||
---
|
||||
|
||||
更多信息请查看项目文档。
|
||||
|
||||
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 添加权限 -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
|
||||
<application
|
||||
android:label="aisee_flutter"
|
||||
android:name="${applicationName}"
|
||||
|
||||
13
aisee_flutter/install.bat
Normal file
13
aisee_flutter/install.bat
Normal file
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
set PATH=C:\Users\xdedmi\flutter\bin;%PATH%
|
||||
echo 正在安装依赖...
|
||||
cd /d %~dp0
|
||||
flutter pub get
|
||||
echo.
|
||||
echo 依赖安装完成!
|
||||
echo.
|
||||
echo 下一步:
|
||||
echo 1. 连接 Android 设备或启动模拟器
|
||||
echo 2. 双击 run.bat 运行项目
|
||||
echo.
|
||||
pause
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'screens/camera_screen.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@@ -7,116 +8,96 @@ void main() {
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
title: 'AISee Camera',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
home: const HomeScreen(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
title: const Text('AISee'),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
//
|
||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||
// action in the IDE, or press "p" in the console), to see the
|
||||
// wireframe for each widget.
|
||||
mainAxisAlignment: .center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('You have pushed the button this many times:'),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
const Icon(
|
||||
Icons.visibility,
|
||||
size: 100,
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'AISee 视觉辅助',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'实时相机图像传输演示',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CameraScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.camera_alt, size: 28),
|
||||
label: const Text(
|
||||
'打开相机',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
'功能说明:\n'
|
||||
'• 拍照分析:拍摄单张照片并上传分析\n'
|
||||
'• 开始传输:每 500ms 自动捕获并上传\n'
|
||||
'• 切换:切换前后摄像头',
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black87,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
290
aisee_flutter/lib/screens/camera_screen.dart
Normal file
290
aisee_flutter/lib/screens/camera_screen.dart
Normal file
@@ -0,0 +1,290 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import '../services/camera_service.dart';
|
||||
import '../services/api_service.dart';
|
||||
|
||||
class CameraScreen extends StatefulWidget {
|
||||
const CameraScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CameraScreen> createState() => _CameraScreenState();
|
||||
}
|
||||
|
||||
class _CameraScreenState extends State<CameraScreen> {
|
||||
late CameraService _cameraService;
|
||||
late ApiService _apiService;
|
||||
bool _isProcessing = false;
|
||||
String _statusMessage = '准备就绪';
|
||||
int _frameCount = 0;
|
||||
int _uploadCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cameraService = CameraService();
|
||||
_apiService = ApiService();
|
||||
_initializeCamera();
|
||||
}
|
||||
|
||||
Future<void> _initializeCamera() async {
|
||||
// 请求相机权限
|
||||
final status = await Permission.camera.request();
|
||||
if (!status.isGranted) {
|
||||
setState(() {
|
||||
_statusMessage = '相机权限被拒绝';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化相机
|
||||
await _cameraService.initialize();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = '相机已就绪';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始实时传输
|
||||
void _startStreaming() {
|
||||
if (_cameraService.isStreaming) return;
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '实时传输中...';
|
||||
_frameCount = 0;
|
||||
_uploadCount = 0;
|
||||
});
|
||||
|
||||
_cameraService.startStreaming((imageBytes) async {
|
||||
setState(() {
|
||||
_frameCount++;
|
||||
});
|
||||
|
||||
// 上传到后端(异步,不阻塞捕获)
|
||||
_uploadToBackend(imageBytes);
|
||||
});
|
||||
}
|
||||
|
||||
/// 停止实时传输
|
||||
void _stopStreaming() {
|
||||
_cameraService.stopStreaming();
|
||||
setState(() {
|
||||
_statusMessage = '已停止传输';
|
||||
});
|
||||
}
|
||||
|
||||
/// 上传图像到后端
|
||||
Future<void> _uploadToBackend(imageBytes) async {
|
||||
if (_isProcessing) return; // 防止并发上传
|
||||
|
||||
_isProcessing = true;
|
||||
|
||||
try {
|
||||
// 上传图像
|
||||
final result = await _apiService.uploadImage(imageBytes);
|
||||
|
||||
setState(() {
|
||||
_uploadCount++;
|
||||
_statusMessage = '已上传 $_uploadCount 帧';
|
||||
});
|
||||
|
||||
print('上传成功: ${result['image_id']}');
|
||||
} catch (e) {
|
||||
print('上传失败: $e');
|
||||
setState(() {
|
||||
_statusMessage = '上传失败: $e';
|
||||
});
|
||||
} finally {
|
||||
_isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 拍摄单张照片并分析
|
||||
Future<void> _captureAndAnalyze() async {
|
||||
setState(() {
|
||||
_statusMessage = '正在拍照...';
|
||||
});
|
||||
|
||||
final imageBytes = await _cameraService.takePicture();
|
||||
if (imageBytes == null) {
|
||||
setState(() {
|
||||
_statusMessage = '拍照失败';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '正在分析...';
|
||||
});
|
||||
|
||||
try {
|
||||
// 上传并分析
|
||||
final result = await _apiService.uploadAndAnalyze(imageBytes);
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '分析完成';
|
||||
});
|
||||
|
||||
// 显示结果
|
||||
_showAnalysisResult(result);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_statusMessage = '分析失败: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示分析结果
|
||||
void _showAnalysisResult(Map<String, dynamic> result) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('分析结果'),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(result.toString()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cameraService.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('实时相机'),
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
body: _cameraService.isInitialized
|
||||
? _buildCameraView()
|
||||
: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCameraView() {
|
||||
return Stack(
|
||||
children: [
|
||||
// 相机预览
|
||||
Positioned.fill(
|
||||
child: CameraPreview(_cameraService.controller!),
|
||||
),
|
||||
|
||||
// 状态信息
|
||||
Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_statusMessage,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'捕获帧数: $_frameCount | 上传帧数: $_uploadCount',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 控制按钮
|
||||
Positioned(
|
||||
bottom: 32,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// 拍照并分析按钮
|
||||
_buildControlButton(
|
||||
icon: Icons.camera_alt,
|
||||
label: '拍照分析',
|
||||
onPressed: _captureAndAnalyze,
|
||||
color: Colors.blue,
|
||||
),
|
||||
|
||||
// 开始/停止实时传输按钮
|
||||
_buildControlButton(
|
||||
icon: _cameraService.isStreaming ? Icons.stop : Icons.play_arrow,
|
||||
label: _cameraService.isStreaming ? '停止传输' : '开始传输',
|
||||
onPressed: _cameraService.isStreaming ? _stopStreaming : _startStreaming,
|
||||
color: _cameraService.isStreaming ? Colors.red : Colors.green,
|
||||
),
|
||||
|
||||
// 切换摄像头按钮
|
||||
_buildControlButton(
|
||||
icon: Icons.flip_camera_android,
|
||||
label: '切换',
|
||||
onPressed: () => _cameraService.switchCamera(),
|
||||
color: Colors.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlButton({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onPressed,
|
||||
required Color color,
|
||||
}) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
onPressed: onPressed,
|
||||
backgroundColor: color,
|
||||
child: Icon(icon, size: 32),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black,
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
79
aisee_flutter/lib/services/api_service.dart
Normal file
79
aisee_flutter/lib/services/api_service.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../utils/app_config.dart';
|
||||
|
||||
class ApiService {
|
||||
late final Dio _dio;
|
||||
|
||||
ApiService() {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: AppConfig.apiBaseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
));
|
||||
|
||||
// 添加日志拦截器
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestBody: false, // 不打印请求体(图片太大)
|
||||
responseBody: true,
|
||||
error: true,
|
||||
));
|
||||
}
|
||||
|
||||
/// 上传图像到后端
|
||||
Future<Map<String, dynamic>> uploadImage(Uint8List imageBytes) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
'file': MultipartFile.fromBytes(
|
||||
imageBytes,
|
||||
filename: 'image_${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||||
),
|
||||
});
|
||||
|
||||
final response = await _dio.post(
|
||||
'/api/v1/images/upload',
|
||||
data: formData,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
print('上传图像失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 请求 AI 分析
|
||||
Future<Map<String, dynamic>> analyzeImage(String imageUrl) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/api/v1/analysis/analyze',
|
||||
data: {'image_url': imageUrl},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
print('分析图像失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 一步到位:上传并分析
|
||||
Future<Map<String, dynamic>> uploadAndAnalyze(Uint8List imageBytes) async {
|
||||
try {
|
||||
// 1. 上传图像
|
||||
final uploadResult = await uploadImage(imageBytes);
|
||||
final imageUrl = uploadResult['url'];
|
||||
|
||||
// 2. 请求分析
|
||||
final analysisResult = await analyzeImage(imageUrl);
|
||||
|
||||
return analysisResult;
|
||||
} catch (e) {
|
||||
print('上传并分析失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
aisee_flutter/lib/services/camera_service.dart
Normal file
158
aisee_flutter/lib/services/camera_service.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import '../utils/app_config.dart';
|
||||
|
||||
class CameraService extends ChangeNotifier {
|
||||
CameraController? _controller;
|
||||
List<CameraDescription> _cameras = [];
|
||||
bool _isInitialized = false;
|
||||
bool _isStreaming = false;
|
||||
Timer? _captureTimer;
|
||||
|
||||
CameraController? get controller => _controller;
|
||||
bool get isInitialized => _isInitialized;
|
||||
bool get isStreaming => _isStreaming;
|
||||
|
||||
/// 初始化相机
|
||||
Future<void> initialize() async {
|
||||
try {
|
||||
_cameras = await availableCameras();
|
||||
if (_cameras.isEmpty) {
|
||||
print('没有可用的相机');
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用后置摄像头
|
||||
final camera = _cameras.firstWhere(
|
||||
(camera) => camera.lensDirection == CameraLensDirection.back,
|
||||
orElse: () => _cameras.first,
|
||||
);
|
||||
|
||||
_controller = CameraController(
|
||||
camera,
|
||||
ResolutionPreset.medium, // 中等分辨率,平衡质量和性能
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.jpeg,
|
||||
);
|
||||
|
||||
await _controller!.initialize();
|
||||
_isInitialized = true;
|
||||
notifyListeners();
|
||||
|
||||
print('相机初始化成功');
|
||||
} catch (e) {
|
||||
print('相机初始化失败: $e');
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 拍摄单张照片
|
||||
Future<Uint8List?> takePicture() async {
|
||||
if (!_isInitialized || _controller == null) return null;
|
||||
|
||||
try {
|
||||
final XFile image = await _controller!.takePicture();
|
||||
final bytes = await image.readAsBytes();
|
||||
|
||||
// 压缩图像
|
||||
final compressedBytes = await _compressImage(bytes);
|
||||
return compressedBytes;
|
||||
} catch (e) {
|
||||
print('拍照失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始实时捕获(定时拍照)
|
||||
void startStreaming(Function(Uint8List) onImageCaptured) {
|
||||
if (_isStreaming) return;
|
||||
|
||||
_isStreaming = true;
|
||||
notifyListeners();
|
||||
|
||||
_captureTimer = Timer.periodic(AppConfig.captureInterval, (timer) async {
|
||||
if (!_isStreaming) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
final imageBytes = await takePicture();
|
||||
if (imageBytes != null) {
|
||||
onImageCaptured(imageBytes);
|
||||
}
|
||||
});
|
||||
|
||||
print('开始实时捕获,间隔: ${AppConfig.captureInterval.inMilliseconds}ms');
|
||||
}
|
||||
|
||||
/// 停止实时捕获
|
||||
void stopStreaming() {
|
||||
_isStreaming = false;
|
||||
_captureTimer?.cancel();
|
||||
_captureTimer = null;
|
||||
notifyListeners();
|
||||
print('停止实时捕获');
|
||||
}
|
||||
|
||||
/// 压缩图像
|
||||
Future<Uint8List> _compressImage(Uint8List bytes) async {
|
||||
return await compute(_compressImageIsolate, bytes);
|
||||
}
|
||||
|
||||
/// 在独立 Isolate 中压缩图像(避免阻塞 UI)
|
||||
static Uint8List _compressImageIsolate(Uint8List bytes) {
|
||||
// 解码图像
|
||||
img.Image? image = img.decodeImage(bytes);
|
||||
if (image == null) return bytes;
|
||||
|
||||
// 调整大小
|
||||
if (image.width > AppConfig.imageMaxWidth ||
|
||||
image.height > AppConfig.imageMaxHeight) {
|
||||
image = img.copyResize(
|
||||
image,
|
||||
width: AppConfig.imageMaxWidth,
|
||||
height: AppConfig.imageMaxHeight,
|
||||
);
|
||||
}
|
||||
|
||||
// 压缩为 JPEG
|
||||
final compressed = img.encodeJpg(image, quality: AppConfig.imageQuality);
|
||||
|
||||
print('图像压缩: ${bytes.length} bytes -> ${compressed.length} bytes');
|
||||
return Uint8List.fromList(compressed);
|
||||
}
|
||||
|
||||
/// 切换摄像头
|
||||
Future<void> switchCamera() async {
|
||||
if (_cameras.length < 2) return;
|
||||
|
||||
final currentLens = _controller?.description.lensDirection;
|
||||
final newCamera = _cameras.firstWhere(
|
||||
(camera) => camera.lensDirection != currentLens,
|
||||
orElse: () => _cameras.first,
|
||||
);
|
||||
|
||||
await _controller?.dispose();
|
||||
|
||||
_controller = CameraController(
|
||||
newCamera,
|
||||
ResolutionPreset.medium,
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.jpeg,
|
||||
);
|
||||
|
||||
await _controller!.initialize();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
@override
|
||||
void dispose() {
|
||||
stopStreaming();
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
13
aisee_flutter/lib/utils/app_config.dart
Normal file
13
aisee_flutter/lib/utils/app_config.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
class AppConfig {
|
||||
// API 配置
|
||||
static const String apiBaseUrl = 'http://10.0.2.2:8000'; // Android 模拟器访问本机
|
||||
// 如果使用真机,改为你的电脑 IP,例如:'http://192.168.1.100:8000'
|
||||
|
||||
// 图像配置
|
||||
static const int imageMaxWidth = 640;
|
||||
static const int imageMaxHeight = 480;
|
||||
static const int imageQuality = 85;
|
||||
|
||||
// 实时传输配置
|
||||
static const Duration captureInterval = Duration(milliseconds: 500); // 每 500ms 捕获一帧
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.9"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -17,6 +25,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
camera:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: camera
|
||||
sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.6"
|
||||
camera_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_android
|
||||
sha256: "27761a1e317bc072266dc1975f3a04c7fa02ff6123f18b1d89d9968951a4f002"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.10+15"
|
||||
camera_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_avfoundation
|
||||
sha256: "11b4aee2f5e5e038982e152b4a342c749b414aa27857899d20f4323e94cb5f0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.23+2"
|
||||
camera_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_platform_interface
|
||||
sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
camera_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_web
|
||||
sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+3"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -33,6 +81,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -41,6 +97,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -49,6 +121,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.2"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -57,6 +145,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -70,11 +174,56 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.33"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -107,6 +256,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -131,6 +288,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.4"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -139,6 +328,150 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.22"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.4.0"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.7"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3+5"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -168,6 +501,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -192,6 +533,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.9"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -208,6 +557,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
flutter: ">=3.38.4"
|
||||
|
||||
@@ -35,6 +35,24 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
# 相机
|
||||
camera: ^0.10.5+9
|
||||
|
||||
# 图像处理
|
||||
image: ^4.1.7
|
||||
|
||||
# 网络请求
|
||||
dio: ^5.4.1
|
||||
|
||||
# 权限管理
|
||||
permission_handler: ^11.3.0
|
||||
|
||||
# 状态管理
|
||||
provider: ^6.1.1
|
||||
|
||||
# 路径
|
||||
path_provider: ^2.1.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
21
aisee_flutter/run.bat
Normal file
21
aisee_flutter/run.bat
Normal file
@@ -0,0 +1,21 @@
|
||||
@echo off
|
||||
set PATH=C:\Users\xdedmi\flutter\bin;%PATH%
|
||||
echo ========================================
|
||||
echo AISee Flutter 启动脚本
|
||||
echo ========================================
|
||||
echo.
|
||||
cd /d %~dp0
|
||||
|
||||
echo 正在检查设备...
|
||||
flutter devices
|
||||
echo.
|
||||
|
||||
echo 如果看到 Android 设备,按任意键继续运行...
|
||||
echo 如果没有设备,请先启动模拟器或连接真机
|
||||
pause
|
||||
|
||||
echo.
|
||||
echo 正在启动应用...
|
||||
flutter run
|
||||
|
||||
pause
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
permission_handler_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
253
aisee_flutter/启动指南.md
Normal file
253
aisee_flutter/启动指南.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# 启动 AISee Flutter 项目指南
|
||||
|
||||
## 前提条件检查
|
||||
|
||||
### 1. 检查 Flutter 是否安装
|
||||
|
||||
打开命令提示符(CMD)或 PowerShell,运行:
|
||||
|
||||
```bash
|
||||
flutter --version
|
||||
```
|
||||
|
||||
**如果显示版本信息**,说明 Flutter 已安装,跳到步骤 2。
|
||||
|
||||
**如果提示"命令不存在"**,需要先安装 Flutter,参考:`Flutter开发环境搭建.md`
|
||||
|
||||
### 2. 检查设备连接
|
||||
|
||||
```bash
|
||||
flutter devices
|
||||
```
|
||||
|
||||
应该看到:
|
||||
- Android 模拟器,或
|
||||
- 连接的 Android 真机
|
||||
|
||||
**如果没有设备**:
|
||||
- 启动 Android 模拟器(Android Studio -> Device Manager)
|
||||
- 或连接真机并开启 USB 调试
|
||||
|
||||
## 启动步骤
|
||||
|
||||
### 方法 1:使用脚本(推荐)
|
||||
|
||||
1. **安装依赖**
|
||||
- 双击 `install.bat`
|
||||
- 等待依赖下载完成
|
||||
|
||||
2. **运行项目**
|
||||
- 双击 `run.bat`
|
||||
- 等待编译和安装
|
||||
|
||||
### 方法 2:使用命令行
|
||||
|
||||
打开命令提示符,进入项目目录:
|
||||
|
||||
```bash
|
||||
cd C:\Users\xdedmi\Desktop\aisee\aisee_flutter
|
||||
|
||||
# 1. 安装依赖
|
||||
flutter pub get
|
||||
|
||||
# 2. 检查设备
|
||||
flutter devices
|
||||
|
||||
# 3. 运行项目
|
||||
flutter run
|
||||
```
|
||||
|
||||
### 方法 3:使用 VS Code
|
||||
|
||||
1. 用 VS Code 打开项目文件夹
|
||||
2. 按 `F5` 或点击右上角的运行按钮
|
||||
3. 选择设备(模拟器或真机)
|
||||
4. 等待编译和安装
|
||||
|
||||
## 配置后端地址
|
||||
|
||||
在运行前,需要配置后端 API 地址。
|
||||
|
||||
编辑文件:`lib/utils/app_config.dart`
|
||||
|
||||
```dart
|
||||
class AppConfig {
|
||||
// 如果使用 Android 模拟器
|
||||
static const String apiBaseUrl = 'http://10.0.2.2:8000';
|
||||
|
||||
// 如果使用真机(改为你的电脑 IP 地址)
|
||||
// static const String apiBaseUrl = 'http://192.168.1.100:8000';
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 如何获取电脑 IP 地址?
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
ipconfig
|
||||
# 查找 "IPv4 地址",例如:192.168.1.100
|
||||
```
|
||||
|
||||
**确保手机和电脑在同一 WiFi 网络!**
|
||||
|
||||
## 首次运行
|
||||
|
||||
首次运行需要:
|
||||
1. 下载 Gradle 依赖(约 5-10 分钟)
|
||||
2. 编译 Android 应用(约 2-3 分钟)
|
||||
3. 安装到设备
|
||||
|
||||
**请耐心等待,不要中断!**
|
||||
|
||||
## 运行成功标志
|
||||
|
||||
看到以下信息说明成功:
|
||||
|
||||
```
|
||||
✓ Built build/app/outputs/flutter-apk/app-debug.apk.
|
||||
Installing build/app/outputs/flutter-apk/app.apk...
|
||||
Syncing files to device...
|
||||
Flutter run key commands.
|
||||
r Hot reload.
|
||||
R Hot restart.
|
||||
```
|
||||
|
||||
应用会自动在设备上打开。
|
||||
|
||||
## 使用应用
|
||||
|
||||
1. **授予权限**
|
||||
- 首次打开会请求相机权限
|
||||
- 点击"允许"
|
||||
|
||||
2. **打开相机**
|
||||
- 点击首页的"打开相机"按钮
|
||||
|
||||
3. **开始传输**
|
||||
- 点击"开始传输"按钮
|
||||
- 观察状态栏的帧数统计
|
||||
|
||||
4. **查看效果**
|
||||
- 捕获帧数:相机捕获的总帧数
|
||||
- 上传帧数:成功上传到后端的帧数
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. Flutter 命令不存在
|
||||
|
||||
**问题**:`'flutter' 不是内部或外部命令`
|
||||
|
||||
**解决**:
|
||||
1. 检查 Flutter 是否安装
|
||||
2. 检查环境变量是否配置
|
||||
3. 重启命令提示符
|
||||
4. 参考 `Flutter开发环境搭建.md`
|
||||
|
||||
### 2. 没有可用设备
|
||||
|
||||
**问题**:`No devices found`
|
||||
|
||||
**解决**:
|
||||
- 启动 Android 模拟器
|
||||
- 或连接真机并开启 USB 调试
|
||||
|
||||
### 3. Gradle 下载慢
|
||||
|
||||
**问题**:卡在 `Running Gradle task 'assembleDebug'...`
|
||||
|
||||
**解决**:
|
||||
1. 配置国内镜像(见环境搭建文档)
|
||||
2. 使用代理
|
||||
3. 耐心等待(首次需要 10-30 分钟)
|
||||
|
||||
### 4. 编译错误
|
||||
|
||||
**问题**:`FAILURE: Build failed with an exception`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 清理项目
|
||||
flutter clean
|
||||
|
||||
# 重新获取依赖
|
||||
flutter pub get
|
||||
|
||||
# 重新运行
|
||||
flutter run
|
||||
```
|
||||
|
||||
### 5. 相机权限被拒绝
|
||||
|
||||
**问题**:应用显示"相机权限被拒绝"
|
||||
|
||||
**解决**:
|
||||
1. 进入手机设置 -> 应用管理 -> aisee_flutter
|
||||
2. 权限 -> 相机 -> 允许
|
||||
3. 重启应用
|
||||
|
||||
### 6. 网络连接失败
|
||||
|
||||
**问题**:上传图像失败
|
||||
|
||||
**解决**:
|
||||
1. 确保后端服务已启动
|
||||
2. 检查 `app_config.dart` 中的 IP 地址
|
||||
3. 模拟器用 `10.0.2.2`,真机用电脑 IP
|
||||
4. 确保手机和电脑在同一网络
|
||||
5. 关闭防火墙(测试时)
|
||||
|
||||
## 热重载
|
||||
|
||||
代码修改后,无需重新编译:
|
||||
|
||||
```bash
|
||||
# 在运行的终端按:
|
||||
r # 热重载(保留状态)
|
||||
R # 热重启(重置状态)
|
||||
q # 退出
|
||||
```
|
||||
|
||||
或在 VS Code 中:
|
||||
- `Ctrl + S` 保存后自动热重载
|
||||
- `Ctrl + F5` 热重启
|
||||
|
||||
## 调试
|
||||
|
||||
### 查看日志
|
||||
|
||||
```bash
|
||||
flutter logs
|
||||
```
|
||||
|
||||
### 查看性能
|
||||
|
||||
```bash
|
||||
flutter run --profile
|
||||
```
|
||||
|
||||
### 查看设备信息
|
||||
|
||||
```bash
|
||||
flutter doctor -v
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
项目运行成功后:
|
||||
|
||||
1. 测试拍照功能
|
||||
2. 测试实时传输
|
||||
3. 调整配置参数(分辨率、帧率)
|
||||
4. 开发后端 API
|
||||
5. 集成 AI 模型
|
||||
|
||||
---
|
||||
|
||||
**需要帮助?**
|
||||
|
||||
查看项目根目录的其他文档:
|
||||
- `Flutter开发环境搭建.md` - 环境配置
|
||||
- `Flutter项目初始化.md` - 项目结构
|
||||
- `技术方案.md` - 技术架构
|
||||
104
aisee_flutter/手机连接问题.md
Normal file
104
aisee_flutter/手机连接问题.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# 手机连接问题解决方案
|
||||
|
||||
## 问题诊断
|
||||
|
||||
✅ 手机已连接并开启 USB 调试
|
||||
❌ Flutter 无法识别 Android 设备
|
||||
❌ 原因:Android SDK 未安装或未配置
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 方案 1:安装 Android Studio(推荐)
|
||||
|
||||
1. **下载 Android Studio**
|
||||
- 官网:https://developer.android.com/studio
|
||||
- 下载最新版本
|
||||
|
||||
2. **安装 Android Studio**
|
||||
- 双击安装包
|
||||
- 选择 "Standard" 安装
|
||||
- 等待下载 Android SDK(约 2-3GB)
|
||||
|
||||
3. **配置 Flutter**
|
||||
```bash
|
||||
# Android Studio 安装完成后,Flutter 会自动检测
|
||||
C:\Users\xdedmi\flutter\bin\flutter doctor
|
||||
```
|
||||
|
||||
4. **重新连接手机**
|
||||
- 拔掉 USB 重新连接
|
||||
- 手机上重新授权 USB 调试
|
||||
- 运行:`flutter devices`
|
||||
|
||||
### 方案 2:仅安装 Android SDK 命令行工具(快速)
|
||||
|
||||
1. **下载 SDK 命令行工具**
|
||||
- https://developer.android.com/studio#command-tools
|
||||
- 下载 "Command line tools only"
|
||||
|
||||
2. **解压到固定位置**
|
||||
```
|
||||
C:\Android\cmdline-tools\latest\
|
||||
```
|
||||
|
||||
3. **配置环境变量**
|
||||
```
|
||||
ANDROID_HOME=C:\Android
|
||||
Path 添加:
|
||||
C:\Android\cmdline-tools\latest\bin
|
||||
C:\Android\platform-tools
|
||||
```
|
||||
|
||||
4. **安装必要组件**
|
||||
```bash
|
||||
sdkmanager "platform-tools" "platforms;android-34"
|
||||
```
|
||||
|
||||
5. **配置 Flutter**
|
||||
```bash
|
||||
C:\Users\xdedmi\flutter\bin\flutter config --android-sdk C:\Android
|
||||
```
|
||||
|
||||
## 快速验证
|
||||
|
||||
安装完成后运行:
|
||||
|
||||
```bash
|
||||
# 1. 检查环境
|
||||
C:\Users\xdedmi\flutter\bin\flutter doctor
|
||||
|
||||
# 2. 检查设备
|
||||
C:\Users\xdedmi\flutter\bin\flutter devices
|
||||
|
||||
# 3. 应该看到你的手机
|
||||
# 例如:SM G9980 (mobile) • xxxxxx • android-arm64 • Android 14
|
||||
```
|
||||
|
||||
## 临时解决方案:使用 Web 版本
|
||||
|
||||
如果暂时无法配置 Android,可以先在浏览器中测试(但相机功能不可用):
|
||||
|
||||
```bash
|
||||
cd C:\Users\xdedmi\Desktop\aisee\aisee_flutter
|
||||
C:\Users\xdedmi\flutter\bin\flutter run -d chrome
|
||||
```
|
||||
|
||||
## 我的建议
|
||||
|
||||
**推荐安装 Android Studio**,因为:
|
||||
1. 一键安装所有必要组件
|
||||
2. 包含模拟器管理器
|
||||
3. 提供完整的开发工具
|
||||
4. 自动配置环境变量
|
||||
|
||||
安装时间:约 30-60 分钟(包括下载)
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 选择一个方案开始安装
|
||||
2. 安装完成后告诉我
|
||||
3. 我会帮你验证并启动项目
|
||||
|
||||
---
|
||||
|
||||
你想选择哪个方案?我推荐方案 1(Android Studio)。
|
||||
Reference in New Issue
Block a user