Guía para el uso de la cámara en Flutter

Guía para el uso de la cámara en Flutter

En la actualidad, las aplicaciones no se limitan solo a mostrar información estática. La capacidad de interactuar con el entorno se ha convertido en una parte integral de la experiencia del usuario. En este contexto, la integración de la cámara en las aplicaciones desempeña un papel fundamental, permitiendo una gama diversa de funciones, desde simples capturas de fotos y videos hasta el uso de algoritmos de visión por computadora para la clasificación de objetos.

En este artículo, exploraremos el uso de diferentes funciones de la cámara de los smartphones con el lenguaje Dart y el framework Flutter.

Configuraciones iniciales

El primer paso es la creación de un proyecto Flutter. Si aún no tienes configurado el entorno de desarrollo, sigue las instrucciones en la documentación oficial de Flutter para instalar el Flutter SDK y configurar tus herramientas. Después de esto, puedes crear un nuevo proyecto usando el siguiente comando:

flutter create nome_do_projeto

Este comando creará una estructura básica de carpetas para tu proyecto, incluyendo el archivo "lib/main.dart", que es el punto de entrada de tu aplicación. Y dentro del archivo "pubspec.yaml" es donde colocaremos las bibliotecas del proyecto. Con el proyecto iniciado, añade las bibliotecas camera e image_picker dentro de "pubspec.yaml" debajo de dependencies:

dependencies:
  camera: ^0.10.5+2
  path_provider: ^2.1.0
  permission_handler: ^10.4.3

Después, dentro del archivo "main.dart" en la carpeta lib, importa los módulos necesarios:

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';

Ahora necesitamos una estructura básica para la aplicación, que contenga la función main, que es el punto de entrada, un StatelessWidget que devolverá el MaterialApp on el menú de la cámara como home (esta parte es especialmente importante, ya que más adelante necesitaremos obtener el size del MaterialApp para definir el tamaño del preview de la cámara) y, por último, un StatefulWidget que devolverá un Scaffold:

void main() {
  runApp(const MyCameraApp());
}


class MyCameraApp extends StatelessWidget {
  const MyCameraApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CameraMenu(),
    );
  }
}

class CameraMenu extends StatefulWidget {
  const CameraMenu({Key? key}) : super(key: key);

  @override
  State<CameraMenu> createState() => _CameraMenuState();
}

class _CameraMenuState extends State<CameraMenu> {

  @override
  build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Camera App'),
      ),
    );
  }
}

Obteniendo permisos de la cámara en Android

Para utilizar ciertos recursos del dispositivo, es necesario pedir el permiso del usuario. La biblioteca permission_handler vai facilitará este proceso, ya que debemos diseñar la aplicación para que responda de acuerdo con la interacción del usuario. Esto significa que, si acepta la solicitud, la cámara se abrirá normalmente; si la rechaza, aparecerá un mensaje en la pantalla diciendo que el permiso de la cámara es necesario; y si la rechaza nuevamente o de forma permanente, se abrirán las configuraciones de la aplicación en el dispositivo para que él modifique los permisos desde allí.

Para configurar la cámara, primero se debe cambiar la versión mínima del SDK a 21 en el archivo build.gradle dentro de la carpeta android/app:

defaultConfig {
  minSdkVersion 21
}

Dentro del estado del CameraMenu, primero creamos una función llamada requestPermission, que es asíncrona y solicitará al dispositivo que la cámara pueda ser utilizada. Luego, el estado del Widget será actualizado con el nuevo valor del estado para la variable _permissionStatus:

PermissionStatus _permissionStatus = Permission.denied;

Future<void> requestPermission() async {
  final status = await Permission.camera.request();
  setState(() {
  _permissionStatus = status;
});

Una función será responsable de llamar el openAppSettings de la biblioteca permission_handler para abrir las configuraciones, y al inicio del estado llamamos a la función requestPermission:

void _openAppSettings() {
  openAppSettings();
}

@override
void initState() {
  super.initState();
  requestPermission();
}

Ahora dentro del Scaffold, añadimos un Widget Center, dentro de él una columna que contenga un texto con el valor de la variable _permissionStatus para saber el estado actual del permiso de acceso a la cámara, un botón elevado que al ser presionado llama la función requestPermission y otro texto y botón que se mostrarán si el valor del Status es permanentlyDenied. Este botón llamará la función _openAppSettings:

body: Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Text(
        'Camera Permission Status: $_permissionStatus',
        style: const TextStyle(fontSize: 18),
      ),
      const SizedBox(height: 20),
      ElevatedButton(
        onPressed: requestPermission,
        child: const Text('Request Camera Permission'),
      ),
      if (_permissionStatus == PermissionStatus.permanentlyDenied)
        Column(
          children: [
            const SizedBox(height: 20),
            const Text('Please allow camera access to use this feature.',
                    style: TextStyle(fontSize: 16),),
            const SizedBox(height: 10),
            ElevatedButton(
                onPressed: _openAppSettings,
                child: const Text('Open App Settings'),
            ),
          ],
        ),
    ],
  ),
),

Hecho esto, la aplicación ya tiene la funcionalidad de pedir el permiso del usuario para utilizar la cámara y reaccionar de acuerdo con su respuesta. El siguiente paso es utilizar la biblioteca camera para aprovechar las funciones de la cámara. Para esto, al inicio del estado, primero creamos una variable para listar las cámaras disponibles y otra para el controlador de la cámara:

late List<CameraDescription> cameras;
late CameraController cameraController;

Después, es necesario iniciar la cámara cuando el elemento sea iniciado y liberarla cuando el elemento sea destruido. Una función asíncrona específica obtendrá las cámaras disponibles, instanciará e iniciará el controlador, cambiando el estado de la variable de la cámara inicializada a true. La inicialización del controlador debe colocarse dentro de un bloque try/catch, ya que si el usuario no permite el uso de la cámara o ocurre algún error al intentar obtener la lista de cámaras disponibles, se devolverá null y se producirá un error en la aplicación.

  bool isCameraInitialized = false;
 
  @override
  void initState() {
    super.initState();
    requestPermission();
  }

  void dispose() {
    cameraController.dispose();
    super.dispose();
  }

  void startCamera() async {
    cameras = await availableCameras();
    cameraController = CameraController(
      cameras[0],
      ResolutionPreset.high,
      enableAudio: false,
    );
    try {
      await cameraController.initialize();
      if (!mounted) return;
      setState(() {
        isCameraInitialized = true;
      });
    } catch (e) {
      print(e);
    }
  }

La función “CameraController” es la que inicializa el controlador de la cámara. En ella se pasa primero la cámara a seleccionar, luego la resolución (que puede ser baja, media o alta), el parámetro enableAudio (que, si se coloca en false, solo habilitará la cámara para capturar videos sin audio), el parámetro para definir el formato de las imágenes (por ejemplo, jpeg), entre otros.

Ahora, necesitamos una función que devuelva un Widget para que podamos modificar en el body. Comenzará verificando si el permiso de la cámara está habilitado. Si no lo está, devolverá lo que ya teníamos en el body; si está habilitado, verificará si la cámara aún no ha sido inicializada para llamar a la función startCamera y devolver un widget con texto informando que la cámara se está iniciando. Cuando finalice la carga, el estado de la aplicación se actualizará y la condición pasará a ser verdadera, devolviendo así el widget que contiene el CameraPreview:

Widget cameraElement() {
    if (_permissionStatus == PermissionStatus.granted) {
      if (isCameraInitialized == false) {
        startCamera();
        return const Center(
          child: Text('Starting Camera...'),
        );
      } else {
        return Container(
          child: CameraPreview(cameraController),
        );
      }
    } else {
      return Center(...); // Adicionar o que tinha no body
  }
}

Ahora, basta con sustituir lo que había en el body del Scaffold por la llamada a la función cameraElement:

body: cameraElement()

Cambiar el tamaño del preview

Para cambiar el tamaño del preview, es necesario colocarlo dentro de un contenedor que tenga width y height. Se pueden agregar otros elementos sobre el preview si este contenedor está dentro de un stack como el primer elemento; entonces, los elementos que estén debajo en el stack quedarán encima del preview. De esta manera, puedes agregar botones, crear menús, colocar las bounding boxes para indicar la posición de elementos en el preview con algoritmos de visión por computadora ¡y mucho más!

Si deseas que el preview tenga el tamaño de la pantalla, el stack deberá estar dentro del scaffold y este dentro del MaterialApp. Tomamos el tamaño de la pantalla con el context del MaterialApp y luego lo pasamos como parámetro para el width y height del contenedor donde está el preview:

Size? size;

@override
build(BuildContext context) {
    size = MediaQuery.of(context).size;
    return Scaffold(
        body: Stack(
            children: [
                Container(
                    width: size!.width,
                    height: size!.height,
                    child: CameraPreview(controller!)
                )
            ]
        )
    );
}

Cambiar Configuraciones Predeterminadas

Después de que el controlador de la cámara se inicializa, se pueden llamar algunas funciones para modificar las configuraciones predeterminadas de la cámara, como el flash, el enfoque y el zoom :

cameraController.setFlashMode(FlashMode.off);
cameraController.setFocusMode(FocusMode.locked);
cameraController.setZoomLevel(4.0);

Tomar fotos con la cámara

Para tomar una foto, necesitas usar la función takePicture del controlador, que puede colocarse en un MaterialButton, y el onPressed debe ser asíncrono, ya que para tomar una foto, las acciones necesarias para el hardware pueden demorar un tiempo impredecible, pues involucra varios factores, como iniciar el hardware de la cámara, esperar el enfoque automático, tomar la foto y guardarla en un archivo. Estas tareas pueden verse influenciadas por la capacidad de procesamiento del dispositivo.

MaterialButton(
    color: Colors.red,
    child: const Icon(
        Icons.camera_alt,
        color: Colors.white,
    ),
    onPressed: () async {
        final image = await cameraController!.takePicture();
        print(image.path);
    }
),

Capturar el stream de imágenes de la cámara

Capturar el stream de imágenes de la cámara permite analizar en tiempo real utilizando algoritmos de visión por computadora lo que está siendo capturado por la cámara y reaccionar dibujando bounding boxes en la pantalla para indicar que un objeto está siendo identificado. Para capturar este stream, en la función de inicialización del controlador de la cámara (que es asíncrona), cuando esté listo, se debe llamar a la función startImageStream y cuando el Widget sea desmontado, el stream debe detenerse:

@override
void dispose() {
    cameraController?.stopImageStream();
    cameraController?.dispose();
    super.dispose();
}
...
try {
  await cameraController?.initialize().then((_) => {
  controller!.startImageStream((image) => print(Datetime.now().microsecondsSinceEpoch))});
} catch (e) {
  print(e);
} // Colocar este bloco na parte da inicialização do controlador

Es importante notar que el imageFormatGroup no debe especificarse en la inicialización del controlador de la cámara cuando se va a utilizar el ImageStream, ya que causará un error y el stream no se iniciará. En este ejemplo, si todo funciona correctamente, la hora actual se estará registrando en el terminal en tiempo real.

Cuando el stream esté funcionando, se puede crear una variable del tipo CameraImage para almacenar la imagen que está siendo retornada por el ImageStream:

late CameraImage _cameraImage;
...
cameraController!.startImageStream((image) => _cameraImage = image)

Conclusión

En este artículo, abordamos desde las configuraciones iniciales para instalar las bibliotecas en la aplicación y obtener los permisos necesarios para utilizar los recursos de hardware de los smartphones hasta la creación del controlador y el uso de algunas de las funcionalidades de la cámara, como modificar el enfoque, hacer zoom, activar el flash, iniciar el preview dentro de un widget, tomar fotos y capturar el stream de imágenes. Todavía existen muchas funcionalidades para mejorar la experiencia del usuario en la aplicación al utilizar la cámara, que podremos explorar en un próximo artículo, como la posibilidad de grabar videos, guardar imágenes y videos en la galería del dispositivo, además de la integración del stream de imágenes con TensorFlow Lite para crear una aplicación capaz de reconocer objetos en tiempo real.

El lenguaje Dart y el framework Flutter, junto con las diversas bibliotecas que existen para trabajar con los recursos de hardware e inteligencia artificial, crean una abstracción y abren la posibilidad de crear tecnologías avanzadas multiplataforma con un esfuerzo relativamente pequeño, comparado con la necesidad de desarrollar todo de manera nativa, lo que proporciona una excelente experiencia de desarrollo. La demanda por tecnologías portátiles y eficientes sigue creciendo en diferentes mercados, y aquellos profesionales que logren especializarse para entrar en este mercado ciertamente tendrán grandes oportunidades en un futuro cercano.

💡
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Listopro.

Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.