Java – Detección de Rostros con OpenCV

La versión anterior de esta página explicaba cómo instalar OpenCV 3.1 en Windows, configurarlo a mano en Eclipse y detectar caras con la cámara. Hoy no hace falta instalar nada: una dependencia de Maven trae OpenCV con sus bibliotecas nativas para Windows, Linux y macOS.

La dependencia

<dependency>
  <groupId>org.openpnp</groupId>
  <artifactId>opencv</artifactId>
  <version>4.9.0-0</version>
</dependency>

Es un empaquetado de OpenCV 4.9 para Java que incluye dentro del jar las bibliotecas nativas de cada sistema y las carga solo, con una llamada. Es lo que sustituye a descargar OpenCV, descomprimirlo y apuntar Eclipse a la carpeta de las DLL, que era la mitad de la página anterior.

OpenCV está escrito en C++, y su versión para Java es una capa por encima que necesita esas bibliotecas nativas. Si ves UnsatisfiedLinkError, es que no se han cargado: falta la llamada a OpenCV.loadLocally() o se está usando otro jar sin los nativos.

Dos detectores, la misma foto

OpenCV ofrece dos formas de encontrar caras. Las cascadas Haar, las que usaba esta página y casi todos los tutoriales, y YuNet, un detector basado en redes neuronales. Las probamos las dos sobre la foto oficial de la tripulación del Apolo 11:

package ejemplo;

import nu.pattern.OpenCV;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.CascadeClassifier;
import org.opencv.objdetect.FaceDetectorYN;

public class Caras {
    public static void main(String[] args) {
        OpenCV.loadLocally();
        System.out.println("OpenCV " + Core.VERSION);

        Mat imagen = Imgcodecs.imread("tripulacion.jpg");
        if (imagen.empty()) throw new IllegalStateException("no se pudo leer tripulacion.jpg");

        // --- YuNet: el detector basado en redes neuronales
        FaceDetectorYN yunet = FaceDetectorYN.create("yunet.onnx", "", imagen.size(), 0.8f, 0.3f, 5000);
        Mat caras = new Mat();
        yunet.detect(imagen, caras);
        System.out.println("YuNet: " + caras.rows() + " caras");
        Mat conYunet = imagen.clone();
        for (int i = 0; i < caras.rows(); i++) {
            float[] c = new float[15];
            caras.get(i, 0, c);
            System.out.printf("  en (%d, %d), %dx%d | confianza %.2f%n",
                    (int) c[0], (int) c[1], (int) c[2], (int) c[3], c[14]);
            Imgproc.rectangle(conYunet, new Rect((int) c[0], (int) c[1], (int) c[2], (int) c[3]),
                    new Scalar(255, 100, 30), 3);
            for (int p = 0; p < 5; p++)
                Imgproc.circle(conYunet, new Point(c[4 + 2 * p], c[5 + 2 * p]), 5, new Scalar(60, 200, 60), -1);
        }
        Imgcodecs.imwrite("resultado_yunet.jpg", conYunet);

        // --- Cascada Haar: el método de los tutoriales antiguos
        CascadeClassifier haar = new CascadeClassifier("haarcascade_frontalface_default.xml");
        System.out.println("cascada cargada: " + !haar.empty());
        Mat gris = new Mat();
        Imgproc.cvtColor(imagen, gris, Imgproc.COLOR_BGR2GRAY);
        MatOfRect rects = new MatOfRect();
        haar.detectMultiScale(gris, rects, 1.1, 5, 0, new Size(60, 60));
        System.out.println("Haar: " + rects.toArray().length + " caras");
        for (Rect r : rects.toArray()) System.out.printf("  en (%d, %d), %dx%d%n", r.x, r.y, r.width, r.height);
    }
}
salidaOpenCV 4.9.0
YuNet: 3 caras
  en (247, 324), 105x132 | confianza 0.94
  en (532, 159), 93x123 | confianza 0.93
  en (823, 338), 96x127 | confianza 0.92
cascada cargada: true
Haar: 4 caras
  en (523, 164), 114x114
  en (806, 354), 106x106
  en (248, 332), 121x121
  en (683, 596), 101x101
La foto oficial de la tripulación del Apolo 11 procesada dos veces: a la izquierda, el detector Haar marca las tres caras y además un bolsillo del traje; a la derecha, YuNet marca solo las tres caras y cinco puntos en cada una
Tres personas, cuatro «caras» con la cascada: la cuarta es un bolsillo del traje. La imagen es de la versión en Python de este ejemplo; las coordenadas que da Java son exactamente las mismas. Foto: NASA, dominio público.

Lo que hace falta descargar

ArchivoPara quéDe dónde
face_detection_yunet_2023mar.onnxel modelo de YuNet, unos 230 KB, licencia MITopencv_zoo
haarcascade_frontalface_default.xmlla cascada Haarel repositorio de OpenCV

En el ejemplo están en la carpeta desde la que se ejecuta el programa. La versión anterior cargaba la cascada desde C:/Users/decodigo/Documents/opencv/…, una ruta que solo existía en un ordenador.

Cómo leer el resultado de YuNet

detect devuelve una matriz con una fila por cara y 15 números por fila: los cuatro primeros son el recuadro —x, y, ancho y alto—; los diez siguientes, cinco puntos —los dos ojos, la nariz y las dos comisuras de la boca—, y el último, la confianza. Se leen con caras.get(i, 0, c) sobre un float[15]. El tamaño que se pasa a create tiene que ser el de la imagen; si procesas imágenes de tamaños distintos, se cambia antes con setInputSize.

Por qué falla la cascada

Una cascada Haar busca patrones de luces y sombras con forma de cara: una franja oscura para los ojos sobre otra más clara, un hueco claro para la nariz. Un bolsillo con su sombra los cumple lo bastante bien. Ajustando sus parámetros se cambia un error por otro: con menos exigencia aparecen más falsos positivos, y con más, se escapan caras. YuNet aprendió a reconocer caras a partir de miles de ejemplos y da una confianza con la que decidir.

Si la ruta de la cascada está mal, OpenCV no lanza ninguna excepción. Escribe un aviso por consola —Can't open file: 'no_existe.xml' in read mode— y crea un clasificador vacío, y el fallo llega después, al detectar. Por eso el ejemplo comprueba haar.empty() justo al cargarla.

Con la cámara web

package ejemplo;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import nu.pattern.OpenCV;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.FaceDetectorYN;
import org.opencv.videoio.VideoCapture;

public class Camara {
    public static void main(String[] args) throws Exception {
        OpenCV.loadLocally();

        VideoCapture camara = new VideoCapture(0);
        if (!camara.isOpened()) throw new IllegalStateException("no hay cámara disponible");

        Mat cuadro = new Mat();
        camara.read(cuadro);
        FaceDetectorYN detector = FaceDetectorYN.create("yunet.onnx", "", cuadro.size(), 0.8f, 0.3f, 5000);

        JLabel pantalla = new JLabel();
        JFrame ventana = new JFrame("Detección de caras");
        ventana.add(pantalla);
        ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ventana.setSize(cuadro.width() + 20, cuadro.height() + 40);
        ventana.setVisible(true);

        Mat caras = new Mat();
        while (ventana.isDisplayable() && camara.read(cuadro)) {
            detector.detect(cuadro, caras);
            for (int i = 0; i < caras.rows(); i++) {
                float[] c = new float[15];
                caras.get(i, 0, c);
                Imgproc.rectangle(cuadro, new Rect((int) c[0], (int) c[1], (int) c[2], (int) c[3]),
                        new Scalar(255, 100, 30), 2);
            }
            BufferedImage imagen = aImagen(cuadro);
            SwingUtilities.invokeLater(() -> pantalla.setIcon(new ImageIcon(imagen)));
        }
        camara.release();
    }

    static BufferedImage aImagen(Mat m) throws IOException {
        MatOfByte jpg = new MatOfByte();
        Imgcodecs.imencode(".jpg", m, jpg);
        return ImageIO.read(new ByteArrayInputStream(jpg.toArray()));
    }
}

Es lo mismo, aplicado a cada fotograma, con una ventana de Swing para mostrar el resultado. La conversión de la imagen de OpenCV a una de Java se hace pasándola por JPEG con imencode: no es lo más rápido, pero son tres líneas, frente a la clase de cuarenta que usaba la versión anterior.

Este programa compila y se ha ejecutado, pero sin cámara: el contenedor de pruebas no tiene. En ese caso falla con su propio mensaje, que es lo que debe hacer:

salida[ WARN:0@0.006] global cap_v4l.cpp:997 open VIDEOIO(V4L2:/dev/video0): can't open camera by index
Exception in thread "main" java.lang.IllegalStateException: no hay cámara disponible
Capturas de la versión anterior del ejemplo detectando caras con la cámara web
Así se veía el ejemplo con la cámara en la versión de 2016 de esta página.

Un aviso de Java 25

salidaWARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by nu.pattern.OpenCV$LocalLoader in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module

Sale al cargar las bibliotecas nativas. No es un error y el programa funciona; para que no aparezca, se arranca con java --enable-native-access=ALL-UNNAMED ….

Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.

12 comentarios en «Java – Detección de Rostros con OpenCV»

  1. Hola me podrias compartir tu archivo, ya que me sale este error

    utilice opencv 3.0.0

    Error: no se ha encontrado o cargado la clase principal C:\Program

    Bonita explicacion…te estaria agradecido si me pudieras apoyar con ese tema

    Responder
  2. Buenas Tardes, yo ejecuto este programa y siempre me sale este error.

    Error de lectura.
    OpenCV Error: Assertion failed (!empty()) in cv::CascadeClassifier::detectMultiScale, file C:\builds\master_PackSlaveAddon-win64-vc12-static\opencv\modules\objdetect\src\cascadedetect.cpp, line 1639
    Exception in thread «main» CvException [org.opencv.core.CvException: cv::Exception: C:\builds\master_PackSlaveAddon-win64-vc12-static\opencv\modules\objdetect\src\cascadedetect.cpp:1639: error: (-215) !empty() in function cv::CascadeClassifier::detectMultiScale
    ]
    at org.opencv.objdetect.CascadeClassifier.detectMultiScale_1(Native Method)
    at org.opencv.objdetect.CascadeClassifier.detectMultiScale(CascadeClassifier.java:159)
    at Principal.java.DetectorRostros.detecta(Principal.java:78)
    at Principal.java.Principal.main(Principal.java:118)

    Responder
  3. Hola amigo, tengo una duda, que tipo de entrenamiento realiza tu programa, entiendo que todo algoritmo debe realizar un entrenamiento antes para proceder a realizar la detección, exactamente en que punto se realiza el entrenamiento.

    Responder

Deja un comentario