本人萌新,花一天时间,教程+ChatGPT+百度,搞了这么一个Demo出来,发出来互相学习。
我的目的是做实时图像识别,即我这边处理每一帧的相机预览流,发送给服务器,获得识别结果,将矩形框绘制到屏幕上。这篇博客只包含获取相机画面,转换成jpeg,压缩,旋转,编码至base64,json http post请求,这几部分。
主要点:
- 设置
CameraController
构造参数中的imageFormatGroup
为ImageFormatGroup.jpeg
- 配置预览图像流的订阅,
_streamSubscription = _controller.startImageStream()
; - 在预览流处理部分中,调用
convertImageToBase64()
进行jpeg,压缩,旋转,编码至base64这几步操作,构建map作为json数据的前身,作为参数使用httpPost()
请求服务器; - 若服务器接收不到数据,请先用
postman
这个软件对服务器做一下 post 测试,图中为json post请求页面。
注意点:x86虚拟机可能会有问题(闪退,我没深究原因),我的 arm64 真机完全没问题。我只会搞安卓,所以ios行不行我不清楚。运行总体来说比较流畅。
依赖部分:
dependencies:
flutter:
sdk: flutter
camera:
path:
path_provider:
image:
http:
代码部分:
最底下有一些封装的工具类函数
英文注释是ChatGPT生成的,不过内容都是对的,可以阅读一下。
import 'dart:async';
import 'dart:io';
// import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'dart:convert';
// import 'package:flutter_image_compress/flutter_image_compress.dart';
// import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';
import 'package:http/http.dart' as http;
Future<void> main() async {
// Ensure that plugin services are initialized so that `availableCameras()`
// can be called before `runApp()`
WidgetsFlutterBinding.ensureInitialized();
// Obtain a list of the available cameras on the device.
final cameras = await availableCameras();
// Get a specific camera from the list of available cameras.
final firstCamera = cameras.first;
runApp(
MaterialApp(
theme: ThemeData.dark(),
home: TakePictureScreen(
// Pass the appropriate camera to the TakePictureScreen widget.
camera: firstCamera,
),
),
);
}
// A screen that allows users to take a picture using a given camera.
class TakePictureScreen extends StatefulWidget {
const TakePictureScreen({
super.key,
required this.camera,
});
final CameraDescription camera;
TakePictureScreenState createState() => TakePictureScreenState();
}
class TakePictureScreenState extends State<TakePictureScreen> {
late CameraController _controller;
late Future<void> _initializeControllerFuture;
late StreamSubscription<CameraImage> _streamSubscription;
bool isProcessing = false;
// 手机与电脑处于同一个局域网,没有用模拟器,所以不用那个 10.开头的ip
final url = Uri.parse('http://192.168.2.151:5000/imageUpload');
// final GlobalKey cameraViewGlobalKey = GlobalKey();
void initState() {
super.initState();
// To display the current output from the Camera,
// create a CameraController.
_controller = CameraController(
// Get a specific camera from the list of available cameras.
widget.camera,
// Define the resolution to use.
ResolutionPreset.veryHigh,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.jpeg,
);
// Next, initialize the controller. This returns a Future.
_initializeControllerFuture = _controller.initialize().then((value) {
_streamSubscription = _controller.startImageStream(
(CameraImage image) async {
// 丢弃没能力处理的图像帧
if (isProcessing) return;
isProcessing = true;
// print("image size:[${image.width},${image.height}]");
String base64String = await convertImageToBase64(image);
// 将 base64 写入文件,方便测试
// await (await _getLocalFile()).writeAsString(base64String);
/// 定义一个map,用于向服务器发 json
Map<String, dynamic> data = {
'base64': base64String,
'PreSize': [720, 1280],
'ViewSize': [360, 640]
};
httpPost(data, url, handleRespones, handleErrors);
isProcessing = false;
},
) as StreamSubscription<CameraImage>;
});
}
void dispose() {
// Dispose of the controller when the widget is disposed.
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Take a picture')),
// You must wait until the controller is initialized before displaying the
// camera preview. Use a FutureBuilder to display a loading spinner until the
// controller has finished initializing.
body: FutureBuilder<void>(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return CameraPreview(_controller);
} else {
// Otherwise, display a loading indicator.
return const Center(child: CircularProgressIndicator());
}
},
),
);
}
}
// ----------------------------- Utils -----------------------------------------
/// 相机图像流 转 jpeg,压缩旋转后再转 base64
Future<String> convertImageToBase64(CameraImage image) async {
// 将CameraImage转换为Uint8List格式
final Uint8List bytes = _concatenatePlanes(image.planes);
// 使用flutter_image_compress库将图像压缩为JPEG格式
final compressedBytes = await FlutterImageCompress.compressWithList(
bytes,
format: CompressFormat.jpeg,
quality: 70, // JPEG图像的压缩质量(1-100)
rotate: 90, // 图片方向不对,需要旋转一下
minWidth: 640, // 图片还未旋转,高度与宽度是反的。 压缩后的像素大小
minHeight: 360,
);
final base64String = base64Encode(compressedBytes);
return base64String;
}
/// 辅助函数,将CameraImage的plane组合为Uint8List格式
Uint8List _concatenatePlanes(List<Plane> planes) {
final WriteBuffer allBytes = WriteBuffer();
for (Plane plane in planes) {
allBytes.putUint8List(plane.bytes);
}
return allBytes.done().buffer.asUint8List();
}
/// 获取 temp 目录,安卓为 "data/data/app-package/cache/"
Future<File> _getLocalFile() async {
// 获取应用目录
String dir = (await getTemporaryDirectory()).path;
return File('$dir/base64.txt');
}
/// 以 json 做参数,post 请求目标服务器;
/// data: 用 map 装的 json 数据;
/// url:服务器链接;
/// handleRespones:处理服务器返回数据;
/// handleErrors:处理 handleRespones 中抛出的异常
void httpPost(Map data, var url, Function(http.Response) handleRespones,
Function(Error) handleErrors) {
// 将Map对象编码为JSON格式的字符串
var body = json.encode(data);
http.post(url,
body: body,
headers: {'Content-Type': 'application/json'}).then((response) {
handleRespones(response);
}).catchError((error) {
handleErrors(error);
});
}
void handleRespones(http.Response response) {
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
}
void handleErrors(Error error) {
print('Error: $error');
}