使用Flutter实现一个猫脸识别APP,看看你家喵是啥品种?

2,932 阅读3分钟

一起用代码吸猫!本文正在参与【喵星人征文活动】

先看效果

实现

拍照界面

布局

拍照界面使用依赖的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。 识别结果展示页则根据接口返回的数据集合进行展示,数据中包括了匹配度,百度百科介绍等字段。

image.png

另外,这个百度接口是针对于动物识别的,不仅仅可以识别猫咪,还可以识别其他动物,如下是对狗类的识别:

完整代码

项目地址→ 注意,clone下来的同学需要自己在百度AI平台添加应用,获取到APP的key和密钥粘贴到项目中的ApiConstants类中。