一起用代码吸猫!本文正在参与【喵星人征文活动】。
先看效果
实现
拍照界面
布局
拍照界面使用依赖的camera
库中的CameraPreview
实现自定义相机界面,通过层叠布局Stack
将猫咪头像覆盖在相机上达到遮罩层效果。
CameraPreview(
controller!,
),
Container(
padding: EdgeInsets.only(top: 150.w),
child: Align(
alignment: Alignment.topCenter,
child: Image.asset(
"assets/images/cat.png",
width: 280.w,
height: 280.w,
)),
),
拍照
点击拍照时,调用cameraView控制器的takePicture
方法,通过转换uint8list就是生成的一张当前相机拍摄的图片。
XFile xFile = await controller!.takePicture();
Uint8List uint8list = await xFile.readAsBytes();
截取头像蒙层部分
上面的拍照其实和使用系统相机拍摄一张图片是相同的,而猫咪头像蒙层的设计就是为了获取更为精准位置的图像信息,排除主图中间外其他因素的干扰,所以这里需要从整个照片中扣取头像部分。实现方式是通过画布实现对ui.image
对象的截取绘制,再使用CustomPaint
组件将截取的图片绘制到界面上,最后通过RepaintBoundary
组件获取到截图并保存。注意这里使用层叠布局将截取的图像绘制到界面的最底层,也就是只是为了截图用的,并不会被用户看到。
/// 自定义图片裁剪区域
class ImageClipper extends CustomPainter {
final ui.Image image;
ImageClipper(this.image);
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint();
canvas.drawImageRect(
image,
Rect.fromLTRB(image.width * 0.12, image.height * 0.25,
image.width * 0.88, image.height * 0.25 + (image.width * 0.76)),
Rect.fromLTWH(0, 0, size.width, size.height),
paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
截图组件:
RepaintBoundary(
key: _globalKey,
// 用于截取方框位置图片的Widget,对用户来说不可见
child: Container(
width: 200,
height: 200,
child: CustomPaint(
painter: ImageClipper(image!),
),
),
)
点击拍照:
/// 点击拍照 首先获取拍摄的完整照片 其次截取头像框部分图像
void _takePhoto() async{
if (!(controller?.value.isInitialized ?? false)) {
return;
}
if (controller!.value.isTakingPicture) {
return;
}
XFile xFile = await controller!.takePicture();
Uint8List uint8list = await xFile.readAsBytes();
ui.Codec codec = await ui.instantiateImageCodec(uint8list);
ui.FrameInfo fi = await codec.getNextFrame();
setState(() {
image = fi.image;
});
/// 延时截取方框中的图片 防止截取不到
Future.delayed(Duration(milliseconds: 100), () async {
RenderRepaintBoundary boundary = _globalKey.currentContext!
.findRenderObject() as RenderRepaintBoundary;
var image = await boundary.toImage(pixelRatio: 3.0);
ByteData? byteData =
await image.toByteData(format: ImageByteFormat.png);
Uint8List? _photoBytes = byteData?.buffer.asUint8List();
if(_photoBytes == null){
return;
}
Directory directory = await getTemporaryDirectory();
Directory tempDirectory = Directory(directory.path + "/catIdentifyTemp");
bool hasCreateDirectory = tempDirectory.existsSync();
// 创建文件夹
if (!hasCreateDirectory) {
tempDirectory.createSync();
}
String targetPath = directory.path + "/${DateTime.now().millisecondsSinceEpoch}.png";
File tempFile = File(targetPath);
bool hasFile = tempFile.existsSync();
// 创建临时文件
if (!hasFile) {
tempFile.createSync();
}
// 将截图存到本地缓存文件夹
File newFile = await tempFile.writeAsBytes(_photoBytes);
String bs64 = base64Encode(await newFile.readAsBytes());
Navigator.of(context).pop(bs64);
});
// var result = await FlutterImageCompress.compressWithList(
// uint8list,
// quality: 60,
// );
}
踩坑:在拍照界面时,APP处于后台之后再回到前台时会抛出异常,所以这里监听APP的前后台切换,当处于后台时释放相机,回到前台时重新初始化相机。
@override
void didChangeAppLifecycleState(ui.AppLifecycleState state) {
// App state changed before we got the chance to initialize.
// 设置生命周期监听的目的是 切换到后台释放相机,切回前台后重新初始化相机,否则会出现无法拍照问题
if (controller == null || !controller!.value.isInitialized) {
return;
}
if (state == AppLifecycleState.inactive) {
controller?.dispose();
} else if (state == AppLifecycleState.resumed) {
if (controller != null) {
onNewCameraSelected(controller!.description);
}
}
super.didChangeAppLifecycleState(state);
}
图像识别
软件中使用的图像识别是百度图像识别Api,Api中提供的接口是通过传递图片base64
或者图片网络地址url进行传参,所以软件中使用base64传参,上面的代码也展示了生成base64图片的方式。
在调用图像识别接口前是先要申请Api调用token
的,由于token有效期是30天,所以这里只在App启动的时候请求了一次,后面就一直使用此token。
识别结果展示页则根据接口返回的数据集合进行展示,数据中包括了匹配度,百度百科介绍等字段。
另外,这个百度接口是针对于动物识别的,不仅仅可以识别猫咪,还可以识别其他动物,如下是对狗类的识别:
完整代码
项目地址→
注意,clone下来的同学需要自己在百度AI平台添加应用,获取到APP的key和密钥粘贴到项目中的ApiConstants
类中。