赞
踩
本人萌新,花一天时间,教程+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; @override 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(); @override 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>; }); } @override void dispose() { // Dispose of the controller when the widget is disposed. _controller.dispose(); super.dispose(); } @override 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'); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。