如何方形裁剪 Flutter 相机预览

Posted

技术标签:

【中文标题】如何方形裁剪 Flutter 相机预览【英文标题】:How to square crop a Flutter camera preview 【发布时间】:2018-12-23 04:40:54 【问题描述】:

我正在尝试在我的应用中拍摄方形照片。我正在使用camera 包,我正在尝试显示CameraPreview 小部件的中心方形裁剪版本。

我的目标是显示预览的中心正方形(全宽),从顶部和底部裁剪均匀。

我很难让它发挥作用,所以我使用固定图像创建了一个最小示例。 (为我坐在椅子上的沉闷照片道歉):

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      title: 'Example',
      theme: ThemeData(),
      home: Scaffold(
        body: Example(),
      ),
    );
  


class Example extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return Column(
      children: <Widget>[
        CroppedCameraPreview(),

        // Something to occupy the rest of the space
        Expanded(
          child: Container(),
        )
      ],
    );
  


class CroppedCameraPreview extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    // We will pretend this is a camera preview (to make demo easier)
    var cameraImage = Image.network("https://i.imgur.com/gZfg4jm.jpg");
    var aspectRatio = 1280 / 720;

    return Container(
      width: MediaQuery.of(context).size.width,
      height: MediaQuery.of(context).size.width,
      child: ClipRect(
        child: new OverflowBox(
          alignment: Alignment.center,
          child: FittedBox(
            fit: BoxFit.fitWidth,
            child: cameraImage,
          ),
        ),
      ),
    );
  

这很好 - 我得到一个全屏宽度的图像,中心被裁剪并推到我的应用程序的顶部。

但是,如果我将此代码放入现有的应用程序中并将cameraImage 替换为CameraPreview,则会出现很多布局错误:

flutter: ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
flutter: The following assertion was thrown during performResize():
flutter: TextureBox object was given an infinite size during layout.
flutter: This probably means that it is a render object that tries to be as big as possible, but it was put
flutter: inside another render object that allows its children to pick their own size.
flutter: The nearest ancestor providing an unbounded width constraint is:
flutter:   RenderFittedBox#0bd54 NEEDS-LAYOUT NEEDS-PAINT
flutter:   creator: FittedBox ← OverflowBox ← ClipRect ← ConstrainedBox ← Container ← Stack ← ConstrainedBox
flutter:   ← Container ← CameraWidget ← Column ← CameraPage ← MediaQuery ← ⋯
flutter:   parentData: offset=Offset(0.0, 0.0) (can use size)
flutter:   constraints: BoxConstraints(w=320.0, h=320.0)
flutter:   size: MISSING
flutter:   fit: fitWidth
flutter:   alignment: center
flutter:   textDirection: ltr
flutter: The nearest ancestor providing an unbounded height constraint is:
flutter:   RenderFittedBox#0bd54 NEEDS-LAYOUT NEEDS-PAINT
flutter:   creator: FittedBox ← OverflowBox ← ClipRect ← ConstrainedBox ← Container ← Stack ← ConstrainedBox
flutter:   ← Container ← CameraWidget ← Column ← CameraPage ← MediaQuery ← ⋯
flutter:   parentData: offset=Offset(0.0, 0.0) (can use size)
flutter:   constraints: BoxConstraints(w=320.0, h=320.0)
flutter:   size: MISSING
flutter:   fit: fitWidth
flutter:   alignment: center
flutter:   textDirection: ltr
flutter: The constraints that applied to the TextureBox were:
flutter:   BoxConstraints(unconstrained)
flutter: The exact size it was given was:
flutter:   Size(Infinity, Infinity)
flutter: See https://flutter.io/layout/ for more information.

谁能告诉我为什么我在预览中遇到错误以及如何避免这些错误?

【问题讨论】:

【参考方案1】:

我通过给我的CameraPreview 实例指定一个特定的大小,将它包装在Container 中来解决这个问题:

  var size = MediaQuery.of(context).size.width;

  // ...

  Container(
    width: size,
    height: size,
    child: ClipRect(
      child: OverflowBox(
        alignment: Alignment.center,
        child: FittedBox(
          fit: BoxFit.fitWidth,
          child: Container(
            width: size,
            height:
                size / widget.cameraController.value.aspectRatio,
            child: camera, // this is my CameraPreview
          ),
        ),
      ),
    ),
  );

为了回应 Luke 的评论,我使用此代码对生成的图像进行了方形裁剪。 (因为即使预览是方形的,抓拍出来的还是标准比例)。

  Future<String> _resizePhoto(String filePath) async 
      ImageProperties properties =
          await FlutterNativeImage.getImageProperties(filePath);

      int width = properties.width;
      var offset = (properties.height - properties.width) / 2;

      File croppedFile = await FlutterNativeImage.cropImage(
          filePath, 0, offset.round(), width, width);

      return croppedFile.path;
  

这使用https://github.com/btastic/flutter_native_image。自从我使用此代码已经有一段时间了 - 认为它目前仅适用于纵向图像,但应该可以轻松扩展以处理横向。

【讨论】:

您知道如何将输出裁剪为相机预览的大小吗? 知道如何将自定义画家的坐标映射到 stack() 小部件中其下方的相机预览的坐标吗?我正在使用 ML 视觉来检测相机预览中的东西,但在不同的手机上,我似乎无法找到一种方法将从 ML 视觉返回的边界框(这是全尺寸预览的绝对坐标)映射到将自定义画家堆叠在上面。【参考方案2】:

我有一个类似于答案中使用的代码 sn-p。

同答案,支持相机纵横比与屏幕纵横比不同的情况。

虽然我的版本有一些不同:它不需要 MediaQuery 来获取设备大小,因此它会适合任何父级的宽度(不仅仅是全屏宽度)

          ....

          return AspectRatio(
            aspectRatio: 1,
            child: ClipRect(
              child: Transform.scale(
                scale: 1 / _cameraController.value.aspectRatio,
                child: Center(
                  child: AspectRatio(
                    aspectRatio: _cameraController.value.aspectRatio,
                    child: CameraPreview(_cameraController),
                  ),
                ),
              ),
            ),
          );

要对图像进行中心正方形裁剪,请参见下面的 sn-p。

它同样适用于纵向和横向的图像。它还允许选择性地镜像图像(如果您想保留自拍相机的原始镜像外观,它会很有用)

import 'dart:io';
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:image/image.dart' as IMG;

class ImageProcessor 
  static Future cropSquare(String srcFilePath, String destFilePath, bool flip) async 
    var bytes = await File(srcFilePath).readAsBytes();
    IMG.Image src = IMG.decodeImage(bytes);

    var cropSize = min(src.width, src.height);
    int offsetX = (src.width - min(src.width, src.height)) ~/ 2;
    int offsetY = (src.height - min(src.width, src.height)) ~/ 2;

    IMG.Image destImage =
      IMG.copyCrop(src, offsetX, offsetY, cropSize, cropSize);

    if (flip) 
        destImage = IMG.flipVertical(destImage);
    

    var jpg = IMG.encodeJpg(destImage);
    await File(destFilePath).writeAsBytes(jpg);
  

此代码需要 图像 包。将其添加到 pubspec.yaml

  dependencies:
      image: ^2.1.4

【讨论】:

是否为每一帧调用 ImageProcessor.cropSquare()? 我真的需要知道这一点。可以为每一帧更改 offsetX 或 Y 吗?创建一个移动的裁剪区域? 很棒的答案。如何对图像的顶部进行类似的裁剪?【参考方案3】:

经过一些测试,我建议将 CameraPreview(不是图像本身)裁剪为任何矩形:

 Widget buildCameraPreview(CameraController cameraController) 
    final double previewAspectRatio = 0.7;
    return AspectRatio(
      aspectRatio: 1 / previewAspectRatio,
      child: ClipRect(
        child: Transform.scale(
          scale: cameraController.value.aspectRatio / previewAspectRatio,
          child: Center(
            child: CameraPreview(cameraController),
          ),
        ),
      ),
    );
  

优点类似于@VeganHunter 的解决方案,但可以通过使用 previewAspectRatio 扩展到任何矩形形状。 previewAspectRatio 的值为 1 将生成正方形,值为 0.5 将是其宽度一半高度的矩形,而 cameraController.value.aspectRatio 的值将是完整大小的预览。

另外,这里解释一下为什么会这样:

Flutter 的一件事是,通常不可能让子级溢出其父级(因为在布局阶段计算小部件大小的方式)。 Transform.scale 的使用在这里至关重要,因为根据文档:

这个对象在绘制之前应用它的变换,这意味着在计算这个小部件的孩子(以及这个小部件)消耗多少空间时,不考虑变换。

这意味着小部件在放大时将能够溢出它的容器,我们可以使用 CLipRect 剪辑(从而隐藏)溢出的部分,从而将预览的整体大小限制为其父大小。仅使用 Container 无法达到相同的效果,因为它们会在布局阶段根据可用空间进行缩放,并且不会溢出,因此无需裁剪。

选择比例 (cameraController.value.aspectRatio * previewAspectRatio) 以使预览的宽度与其父级的宽度匹配。

如果此解决方案对您不起作用,请尝试将所有 cameraController.value.aspectRatio 替换为其倒数 (1 / cameraController.value.aspectRatio)。

这仅针对纵向模式进行了测试,横向可能需要进行一些调整。

【讨论】:

【参考方案4】:

我就是这样解决的。

SizedBox(
    width: width,
    height: height,
    child: SingleChildScrollView(
      child: AspectRatio(
        aspectRatio: _cameraController.value.aspectRatio,
        child: CameraPreview(_cameraController),
      ),
    ),
  )

【讨论】:

请解释一下您的代码,以便有人可以从中学到一些东西。【参考方案5】:

简单的裁剪方法

凭记忆:

Container(
  width: 40.0,
  height: 40.0,
  decoration: BoxDecoration(
    image: DecorationImage(
      image: MemoryImage(photo),
      fit: BoxFit.cover,
    ),
  ),
)

来自资产:

Container(
  width: 40.0,
  height: 40.0,
  decoration: BoxDecoration(
    image: DecorationImage(
      image: AssetImage('assets/images/photo.jpg'),
      fit: BoxFit.cover,
    ),
  ),
)

【讨论】:

以上是关于如何方形裁剪 Flutter 相机预览的主要内容,如果未能解决你的问题,请参考以下文章

TextureView 的裁剪相机预览

是否可以裁剪相机预览?

如何设置 3:4 纵横比 Flutter 相机预览

裁剪 MediaCapture 的视频预览以生成方形纵横比

如何在不使用内部相机裁剪的情况下裁剪图像

裁剪/剪辑相机预览以仅显示图像的上半部分