0.2.0: lo que la ventanita de la cámara le hace a la detección de diapositivas
Hoy publicamos en npm la versión 0.2.0 de video-slide-extractor. El paquete es un detector que compara la imagen por bloques: toma muestras del video a intervalos fijos y decide, fotograma a fotograma, si el contenido en pantalla ha pasado a la diapositiva siguiente. Es el mismo detector que Video2Any ejecuta dentro de tu navegador. El código es abierto, no tiene dependencias, usa licencia MIT y funciona tanto en el navegador como en Node.
La versión 0.1.x que publicamos en julio era una copia del código de producto tomada el 16 de julio. Acierta con una grabación de pantalla limpia, pero se equivoca en buena parte de las grabaciones que los usuarios tienen de verdad. Hay dos situaciones que la hacen fallar, el producto resolvió las dos durante el mes siguiente y el paquete no las conocía. La 0.2.0 incorpora esas dos.
A continuación explicamos tres cosas, por orden: cómo eran realmente los videos que subió la gente, qué conclusión daba ya el banco de pruebas antes de que tocáramos nada, y qué medimos después del cambio.
En esta página
Cómo eran realmente los videos que subió la gente
Video2Any registra los eventos de extracción desde el 16 de julio. Hasta hoy, 96 personas han lanzado 411 conversiones; 354 llegaron al final y produjeron 291 archivos: 188 PowerPoint, 95 PDF y 8 paquetes de imágenes. Casi todo son archivos locales: en 398 de las 411 conversiones, el usuario eligió un video que ya estaba en el mismo equipo donde tenía abierta la pestaña. (Los dos días en los que pasamos nuestras pruebas automáticas quedan fuera de todas las cifras de este artículo.)
En las 106 conversiones cuyo archivo declaraba su duración, el video medía 53 minutos de media. No son demos de producto de cinco minutos: son clases, reuniones y grabaciones de cursos. El detector de julio nunca se había medido con este tipo de video.
Lo que merece más atención es cuántas diapositivas salen en cada conversión. Las conversiones terminadas dieron 64 de media, una cifra que parece normal y que oculta la distribución: 64 conversiones superaron las 100 diapositivas, siete superaron las 300 y cuatro devolvieron exactamente 900. 900 es el número máximo de muestras que puede tomar el barrido, así que en esas cuatro cada muestra se consideró una diapositiva nueva y el detector no descartó ninguna. Uno de esos videos duraba 30 minutos.
El ritmo de salida tampoco es un valor fijo: con el mismo ajuste, los videos de menos de una hora dieron unas 6.5 diapositivas por minuto y los de más de una hora unas 1.0. Parte de esa diferencia es real, porque una reunión de dos horas cambia de pantalla menos veces por minuto que una demo corta. El problema es que la cifra por sí sola no dice qué lado tiene razón. Las cuatro conversiones que llegaron al tope sí lo dicen: si se conservan todas las muestras, el detector no está decidiendo nada.
El banco de pruebas ya daba esa conclusión
Antes de arreglar nada, la 0.2.0 añadió al paquete el directorio bench/. Los videos de prueba se generan a partir de una secuencia de diapositivas conocida, de modo que el segundo exacto en que ocurre cada cambio queda fijado al renderizar y nadie tiene que etiquetarlo a mano. Cada presentación se renderiza de tres formas: una versión limpia, una comprimida a crf 45 para tener ruido de compresión y una con la ventanita de la cámara animada en una esquina.
En la presentación del MIT (46 diapositivas, una muestra cada 2 segundos a 160×90), la versión limpia obtuvo un F1 de 0.966. Al superponer la cámara, el F1 baja a 0.538: el detector sacó 125 capturas de un contenido de 46 diapositivas, la precisión fue de 0.368 y el 63% de lo que devolvió era una diapositiva que ya había capturado antes.
El motivo es aritmético, no cuestión de suerte. La condición por defecto es que cambie más del 2% de los bloques. Una ventanita de cámara, un cursor o un logotipo animado en bucle ocupan por sí solos más del 2% de los bloques mientras siguen moviéndose. Así que cada fotograma muestreado supera el umbral aunque no haya cambiado ninguna diapositiva. Al final, el número de diapositivas que recibes depende de si quien hablaba tenía la cámara encendida, y no de cuántas páginas tiene la presentación.
Publicamos esa fila tal cual. Las cuatro conversiones de 900 diapositivas son esa misma fila vista sobre material real, y arreglarla es lo que hace la 0.2.0.
Qué añade la 0.2.0
Tres funciones nuevas exportadas, una opción y las declaraciones de tipos de todo ello.
buildActivityMask
Esta función recibe los fotogramas muestreados, localiza los bloques que cambian en casi todos los pares de fotogramas consecutivos —la ventanita, el cursor, el reloj de la esquina— y devuelve una máscara que los excluye por completo de la comparación. Si la máscara fuese a cubrir la mayor parte del cuadro, la función devuelve
nullen lugar de una máscara. Esa rama importa: cuando casi toda la imagen se mueve, lo que se mueve es precisamente el contenido que hay que mirar, y excluirlo equivale a renunciar a detectar.chooseThreshold
Un mismo umbral fijo no puede servir para una presentación y para un primer plano de alguien hablando. La presentación está quieta, así que un cambio real se distingue con claridad del ruido. La imagen de cámara se mueve todo el rato y ahí 0.02 convierte cada fotograma en una diapositiva nueva. Esta función analiza la distribución del cambio entre fotogramas y devuelve
{ changedRatio, mode }. El campomodeestá expuesto a propósito: valebimodal,static,motionodefaulte indica qué tipo de material cree el detector que está mirando, de forma que si el resultado es malo puedas ver en qué se basó en lugar de suponerlo.analyzeSamples
Esta función ejecuta los dos pasos anteriores sobre los mismos fotogramas y devuelve
{ mask, choice }. Es lo que necesita la mayoría de quien la llama: tienes unas muestras y quieres saber con qué parámetros detectar, sin tener que enterarte antes de que «con qué parámetros» son en realidad dos decisiones independientes.frameDiff({ collect: true })
Con esta opción,
frameDiffdevuelve ademásflags: un byte por bloque que indica en qué zonas cambió ese fotograma. Sirve para dibujar las zonas que el detector consideró cambiadas. Solo se devuelve si pasascollect, porque un campo que normalmente no existe se usa mejor que un campo que casi siempre vale null.
Puesto en el orden en que lo llamarías, esto es todo:
import { analyzeSamples, createSlideDetector } from 'video-slide-extractor';
// frames: muestras RGBA, una cada ~2 s, reducidas a 160x90
const { mask, choice } = analyzeSamples(frames, 160, 90);
console.log(choice.mode); // 'bimodal' | 'static' | 'motion' | 'default'
const detect = createSlideDetector(160, 90, {
mask,
changedRatio: choice.changedRatio
});
const kept = [];
frames.forEach((frame, i) => {
if (detect(frame).keep) kept.push(i); // muestras a capturar a resolución completa
});Cuánto mejora, medido
Mismos videos de prueba y mismo protocolo: presentación del MIT, 46 diapositivas, versión con cámara, una muestra de 160×90 cada 2 s, y damos por acertada una detección si cae a menos de ±2.5 s del cambio etiquetado.
| Detección | Capturas | Precisión | Exhaustividad | F1 | Duplicados |
|---|---|---|---|---|---|
| Valores de 0.1.x (changedRatio 0.02) | 125 | 0.368 | 1.000 | 0.538 | 0.632 |
| Con máscara de actividad | 54 | 0.815 | 0.957 | 0.880 | 0.185 |
| Con máscara y umbral automático | 35 | 1.000 | 0.761 | 0.864 | 0.000 |
Solo con añadir la máscara, las capturas bajan de 125 a 54, los duplicados del 63% al 19% y el F1 sube de 0.538 a 0.880. Además no afecta al material que no la necesita: en las versiones limpia y con ruido devuelve null, así que esas dos filas dan exactamente el mismo resultado que la 0.1.1.
El umbral, en cambio, tiene ventajas y desventajas, y para explicarlo conviene empezar por la fila que sale mal: en la presentación limpia del MIT, el umbral calculado es más conservador que la constante anterior y la exhaustividad baja de 0.935 a 0.761. Por eso DEFAULTS no ha cambiado en esta versión. analyzeSamples sirve cuando no sabes de qué tipo es el material que te han dado. Si lo sabes, las constantes siguen ahí y las decides tú.
Medimos las dos filas nuevas con la herramienta del banco de pruebas, sobre el commit de la publicación. El RESULTS.md publicado todavía informa solo del conjunto de métodos de la 0.1.x; incorporar ahí la máscara como método propio es lo siguiente que haremos en bench/.
Lo que hemos dejado fuera a propósito
La unificación global de duplicados y el suavizado de transiciones no están en el paquete y no vamos a añadirlos. Las dos cosas exigen ver el video entero de una vez: qué plano volvió más tarde, o si tres capturas venían en realidad de un mismo fundido. Eso es trabajo de la tubería de procesado. Un detector que solo responde si la imagen ha cambiado no debería además guardar el video completo.
La densidad de diapositivas tampoco entra aquí: cuántas debería dar una clase de 90 minutos es una decisión de producto, no de detección. Video2Any tiene su criterio y ofrece un control para ajustarlo. Una biblioteca no tiene por qué tomar esa decisión por ti; le basta con decirte dónde ha cambiado la imagen.
Tres lecturas relacionadas:
- Demasiadas diapositivas de un solo video — cómo trata Video2Any la densidad de diapositivas
- Decodificar video en paralelo, en una pestaña — la tubería completa en la que va montado este detector
- De grabación de Zoom a PowerPoint — la ventanita de la cámara aparece sobre todo en este tipo de grabación
Qué viene ahora
- Un tiempo mínimo de permanencia. El contenido nuevo tendrá que quedarse quieto un par de segundos antes de contar como diapositiva. Esto es lo que acaba de verdad con las conversiones de 900: la máscara quita la ventanita, pero un fundido rápido o un arrastre de la barra de tiempo siguen disparando la condición. En Video2Any ya funciona. Llegará al paquete cuando su interfaz esté resuelta, porque un tiempo de permanencia no significa nada sin una cadencia de muestreo, y el paquete no sabe a propósito cada cuánto muestreaste.
- Sustituir el material sintético por una cámara real. El material de prueba actual es una animación renderizada en una esquina. Una webcam que graba a una persona real es una prueba más dura y más parecida a lo que ocurre, y la máscara debería medirse con eso.
- El fallo que no resuelve ninguno de los dos métodos. Cuando dos diapositivas seguidas solo se diferencian en una línea añadida, a 160×90 esa diferencia se queda pegada al mínimo que dispara la condición, así que las diapositivas que se construyen por partes se pierden enteras. El banco de pruebas lo muestra incluso con un renderizado sintético. Es un problema de resolución y de criterio de puntuación, y
docs/evaluation.mdconvierte ahora la forma de contar esas construcciones en un parámetro que hay que declarar y publicar, en lugar de dejarlo a la interpretación de cada evaluador. - Dónde debería calibrarse. Video2Any reparte el video entre cuatro workers y cada uno calibra su umbral sobre su propio trozo, de modo que el mismo archivo da presentaciones algo distintas en equipos con distinto número de núcleos. No es una propiedad deseable. La solución evidente sería calibrar una sola vez, de forma global. La construimos y la medimos: en una reunión de 29 minutos donde ffmpeg cuenta siete cortes reales, la calibración por trozos encontró seis y la global solo cuatro. Calibrar globalmente no es necesariamente mejor, así que el paquete no lo pondrá por defecto hasta que exista una versión que lo sea.
Pruébalo
La instalación es un solo comando, no tiene dependencias y funciona con cualquier grabación que tengas a mano:
npm i video-slide-extractorSi construyes algo con él, o tienes material que lo hace fallar, envíalo al repositorio. Un video que deja al detector sin respuesta nos vale más que una estrella: así es como acabó existiendo el material de prueba con cámara.