Cómo creé un filtro que no corrompe la imagen incluso después de un millón de ejecuciones

Después de completar la creación de la arquitectura web para nuestro nuevo cómic web Meow the Infinite , decidí que era hora de escribir algunos artículos técnicos muy atrasados. Este artículo se centrará en un filtro que desarrollé hace varios años. Nunca se ha discutido en el campo de la compresión de video, aunque me parece que vale la pena hacerlo.

En 2011, desarrollé el "filtro de media capa". Este es un tipo especial de filtro que toma una imagen entrante y muestra de manera convincente cómo se vería la imagen cuando se desplaza exactamente medio píxel .

Probablemente se esté preguntando por qué tal filtro puede ser necesario. De hecho, son bastante comunes en los códecs de video modernos. Los códecs de video usan filtros similares para tomar fragmentos de cuadros anteriores y usarlos en cuadros posteriores. Los códecs más antiguos movieron los datos del cuadro solo un píxel completo a la vez, pero los nuevos códecs fueron más allá y permitieron un desplazamiento de medio o incluso un cuarto de píxel para transmitir mejor pequeños movimientos.

Al analizar el comportamiento de los algoritmos de compensación de movimiento en los filtros de halfpel tradicionales, Jeff Roberts descubrió que cuando se aplican repetidamente a cuadros secuenciales, se degradan rápidamente, obligando a otras partes del compresor de video a usar más datos de los necesarios para corregir los artefactos. Si deshabilita estas correcciones y observa los resultados "en bruto" del filtro de halfpel, esta es la imagen original:


se convierte en esto:


solo un segundo después el video. Como debería, se desplaza hacia un lado, porque cada cuadro desplazó la imagen medio píxel. Pero el resultado no parece una versión desplazada de la imagen original, está muy distorsionada.

Durante el "video de un segundo", el filtro se aplica muchas veces, 60 si el video se reproduce a una frecuencia de 60 cuadros por segundo. Pero idealmente, necesitamos filtros que sean resistentes a tales distorsiones. Si los tuviéramos, los videos de desplazamiento suave no habrían sido codificados con tantas correcciones de artefactos, lo que los habría hecho menos, o mejor, o ambos.

Si está familiarizado con el campo de la compresión de video, es posible que se pregunte por qué incluso necesitamos usar el filtro de halfpel más de una vez. Al final, si aplicamos el filtro de halfpel dos veces, entonces ya moveremos un píxel completo, entonces, ¿por qué no usar los datos de dos cuadros hacia atrás y simplemente tomarlos?

La respuesta no es tan simple. En primer lugar, cuantos más datos necesitemos para codificarlos, menos compresión obtendremos. Por lo tanto, si comenzamos a codificar sin la necesidad de demasiados datos, como "de qué fotograma tomar datos", el video no se comprimirá muy bien.

Pero esto no es lo más importante. El principal problema es que si necesitamos tomar información de marcos anteriores, tendremos que almacenarlos . Para preservar los dos cuadros anteriores, en lugar de uno, debe adivinar que tiene el doble de memoria. Para las CPU modernas, este no es un problema especial, tienen mucha memoria y tal problema no les molesta. Pero esto es un problema para usted si desea crear un formato de video rápido, portátil y ampliamente utilizado que debería funcionar en dispositivos con una pequeña cantidad de memoria (teléfonos móviles, dispositivos electrónicos integrados, etc.).

Realmente no queremos almacenar varios cuadros para compensar el movimiento, simplemente para no usar un filtro de halfpel. Por lo tanto, se me indicó que averiguara qué está sucediendo exactamente aquí y que averigüe si puedo crear un filtro que no tenga tales problemas.

Antes de eso, nunca había trabajado con filtros y no tenía idea de cómo se desarrollan habitualmente. Por extraño que parezca, resultó estar a mi favor, porque tuve que mirar este problema sin prejuicios.

Los fundamentos


Rápidamente me di cuenta de que los filtros halfpel más populares tienen una estructura similar: para cada píxel en la imagen de salida, se toman de 2 a 8 píxeles de la imagen de entrada, que se muestrean y mezclan con ciertos coeficientes. Los diferentes filtros difieren solo en el número de píxeles de origen muestreados (a menudo en la jerga de los desarrolladores de filtros se les llama tap) y los factores de mezcla de píxeles. Estos coeficientes a menudo se denominan "núcleo de filtro" y eso es todo lo que se necesita para describir completamente el filtro.

Si está familiarizado con algún tipo de muestreo o remuestreo de imágenes (por ejemplo, escalar imágenes), entonces esto debería estar claro para usted. Esencialmente, los filtros hacen lo mismo. Dado que la compresión de video es un área extensa en la que se llevan a cabo varios estudios, es obvio que hay muchas otras formas de compensar el movimiento además del simple filtrado. Pero los códecs comunes usualmente usan procedimientos de compensación de movimiento con filtros de halfpel, que son esencialmente idénticos a los filtros de escala de imagen: simplemente toman los píxeles originales, los multiplican por algunos pesos, los agregan y obtienen los píxeles de salida.

La necesidad de "nitidez"


Entonces, necesitamos cambiar la imagen por medio píxel. Si usted es un programador de gráficos, pero no está particularmente familiarizado con el filtrado, podría pensar: "También tengo un problema, solo use un filtro bilineal". Este es un proceso estándar para trabajar con gráficos, cuando necesitamos calcular valores intermedios entre dos elementos de datos entrantes, como sucede aquí.

El siguiente núcleo de filtro puede describir fácilmente un filtro bilineal para mover exactamente medio píxel:

// NOTE(casey): Simple bilinear filter BilinearKernel[] = {1.0/2.0, 1.0/2.0}; 

Esto funcionará, pero no sin problemas. Si su objetivo es imágenes de alta calidad, y en el caso de la compresión de video, el objetivo es solo eso, entonces un filtro bilineal no es la mejor solución, ya que agrega más desenfoque al resultado del necesario. No es tanto , pero crea más que otros filtros.

Para mostrar esto claramente, aquí hay una imagen aproximada del ojo de la morsa de la imagen original después de una sola aplicación de los filtros más comunes:


A la izquierda está el original, a la derecha está el filtrado bilineal. Entre ellos se encuentran los filtros halfpel más utilizados de códecs de video. Si observa de cerca, puede ver que casi todas las imágenes son similares, excepto una bilineal, que es un poco más borrosa. Aunque el desenfoque no es tanto, si su objetivo principal es la calidad de imagen, esto es suficiente para preferir un filtro diferente al filtro bilineal.

Entonces, ¿cómo otros filtros "mantienen" la nitidez y evitan el desenfoque? Recordemos cómo se ve el núcleo del desenfoque bilineal:

 BilinearKernel[] = {1.0/2.0, 1.0/2.0}; 

Es muy simple Para cambiar la imagen a medio píxel, tomamos un píxel y lo mezclamos al 50% con su vecino. Eso es todo Uno puede imaginar cómo esto "difumina" la imagen, porque en aquellos lugares donde el píxel blanco brillante está adyacente al negro oscuro, estos dos píxeles se promedian durante el filtrado bilineal, creando un píxel gris que "suaviza" el borde. Esto sucede con cada píxel, así que literalmente cada área donde hay una clara diferencia en color o brillo. suavizado

Es por eso que en los códecs de alta calidad, el filtrado bilineal no se usa para la compensación de movimiento (aunque puede usarse en otros casos). En cambio, se utilizan filtros que conservan la nitidez, por ejemplo, como:

 // NOTE(casey): Half-pel filters for the industry-standard h.264 and HEVC video codecs h264Kernel[] = {1.0/32.0, -5.0/32.0, 20.0/32.0, 20.0/32.0, -5.0/32.0, 1.0/32.0}; HEVCKernel[] = {-1.0/64.0, 4.0/64.0, -11.0/64.0, 40.0/64.0, 40/64.0, -11.0/64.0, 4.0/64.0, -1.0/64.0}; 

Como puede ver, donde el filtrado bilineal tuvo en cuenta solo dos píxeles, estos filtros tienen en cuenta seis (h.264) o incluso ocho (HEVC) píxeles. Además, no solo calculan los valores promedio ponderados habituales de estos píxeles, sino que usan pesos negativos para algunos píxeles para restar estos píxeles de otros valores.

¿Por qué están haciendo esto?

En realidad, no es difícil entender esto: utilizando valores positivos y negativos, y también considerando una "ventana" más amplia, el filtro puede tener en cuenta la diferencia entre píxeles adyacentes y simular la nitidez de los dos píxeles más cercanos en relación con sus vecinos más lejanos. Esto le permite mantener la nitidez del resultado de la imagen en aquellos lugares donde los píxeles difieren significativamente de sus vecinos, mientras que el promedio todavía se usa para crear valores creíbles de cambios de "medio píxel", que necesariamente deben reflejar la combinación de píxeles de la imagen entrante.

Filtrado inestable


Entonces, ¿se resuelve el problema? Sí, es posible, pero si solo necesita hacer un desplazamiento de medio píxel. Sin embargo, estos filtros de "afilado" (y uso este término aquí intencionalmente) en realidad hacen algo peligroso, esencialmente similar a lo que hace el filtrado bilineal. Simplemente saben cómo esconderlo.

Cuando el filtrado bilineal reduce la nitidez de la imagen, estos filtros estándar la aumentan , como la operación de nitidez en algún tipo de programa gráfico. La cantidad de nitidez es muy pequeña, por lo que si ejecutamos el filtro solo una vez, no lo notaremos. Pero si el filtrado se realiza varias veces, esto puede volverse muy notable.

Y, desafortunadamente, dado que este enfoque es de procedimiento y depende de la diferencia entre los píxeles, crea un ciclo de retroalimentación que continuará enfocando el mismo borde una y otra vez hasta que destruya la imagen. Puede mostrar esto con ejemplos específicos.

Arriba, la imagen original, abajo, con filtrado bilineal, realizado en más de 60 cuadros:



Como es de esperar, el desenfoque simplemente continúa reduciendo la nitidez de la imagen hasta que se vuelve bastante borrosa. Ahora el original estará en la parte superior y el filtro de halfpel de códec h.264 que se ejecutará durante 60 fotogramas en la parte inferior:



¿Ves toda esta basura? El filtro hizo lo mismo que el efecto de "desenfoque" del filtrado bilineal, pero viceversa : "aumentó la nitidez de la imagen" para que todas las partes donde los detalles se convirtieron en patrones de luz / oscuridad fuertemente distorsionados.

¿El códec HEVC que usa 8 píxeles se comporta mejor? Bueno, definitivamente funciona mejor que h.264:


pero si aumentamos el tiempo de 60 cuadros (1 segundo) a 120 cuadros (2 segundos), todavía veremos que hay comentarios y la imagen se destruye:


Por el bien de aquellos a quienes les gusta el procesamiento de señales, agregaré un filtro de ventana sinc (llamado filtro Lanczos) como referencia:

 // NOTE(casey): Traditional 6-tap Lanczos filter LanczosKernel[] = {0.02446, -0.13587, 0.61141, 0.61141, -0.13587, 0.02446}; 

No explicaré en este artículo por qué alguien podría estar interesado en la "ventana sinc", pero basta con decir que este filtro es popular por razones teóricas, así que mire cómo se ve al procesar 60 cuadros (1 segundo):


y al procesar 120 cuadros (2 segundos):


Mejor que h.264, y casi lo mismo que HEVC.

Filtrado estable


¿Cómo podemos lograr mejores resultados que h.264, HEVC y sinc en ventana? ¿Y cuánto mejor pueden ser?

Esperaría ver preguntas similares en la literatura sobre compresión de video y deberían ser bien conocidas por los especialistas en compresión, pero de hecho (al menos para 2011) no encontré a nadie que al menos dijera que esto era un problema. Así que tuve que encontrar una solución solo.

Afortunadamente, el enunciado del problema es muy simple: cree un filtro que pueda aplicarse tantas veces como sea posible para que la imagen tenga el mismo aspecto que al principio.

Llamo a esta definición "filtrado estable" porque, en mi opinión, puede considerarse una propiedad de filtro. Un filtro es "estable" si no cae en su ciclo de retroalimentación, es decir, puede aplicarse repetidamente sin crear artefactos. Un filtro es "inestable" si crea artefactos que se amplifican con el uso repetido y eventualmente destruyen la imagen.

Repito, no entiendo por qué este tema no se considera en la literatura sobre códecs de video o procesamiento de imágenes. Quizás usa una terminología diferente, pero no la he cumplido. El concepto de "retroalimentación" está bien establecido en el campo del trabajo con sonido. pero no es un problema importante en el procesamiento de imágenes. ¿Quizás porque los filtros deberían aplicarse solo una vez?

Si fuera un especialista en este campo, lo más probable es que tuviera una opinión sobre este tema, y ​​tal vez incluso conocería esos rincones de la literatura especializada donde ya hay soluciones a este problema, que pocos conocen. Pero, como dije al comienzo del artículo, nunca antes había podido crear filtros, así que busqué solo en artículos conocidos (aunque vale la pena señalar que hay al menos una persona conocida en la literatura que tampoco ha escuchado algo así) )

Entonces, en la mañana me dijeron que necesitábamos este filtro, y todo el día intenté crearlo. Mi enfoque era simple: creé un programa que ejecutaba el filtro cientos de veces y al final produje una imagen para poder ver el resultado de largas ejecuciones. Luego experimenté con diferentes coeficientes de filtro y observé los resultados. Fue literalmente un proceso direccional de prueba y error.

Aproximadamente una hora después, recogí los mejores coeficientes de filtro adecuados para esta tarea (pero tenían un defecto, que discutiremos en la segunda parte del artículo):

 MyKernel[] = {1.0/32.0, -4.0/32.0, 19.0/32.0, 19.0/32.0, -4.0/32.0, 1.0/32.0}; 

Este núcleo está a punto de afilarse y difuminarse. Dado que la nitidez siempre conduce a la retroalimentación que crea artefactos vívidos y obvios, este núcleo de filtro prefiere un poco de desenfoque para que la imagen se vea un poco más "opaca".

Así es como se ve después de 60 cuadros. Como referencia, mostré todos los filtros en este orden: la imagen original (sin filtrado), mi filtro, bilineal, Lanczos, h.264, HEVC:







Como puede ver, mi filtro proporciona resultados ligeramente más borrosos que los filtros de nitidez, pero no tiene artefactos de nitidez inaceptables después de 60 cuadros. Sin embargo, es posible que prefiera desenfocar los artefactos para agudizar los artefactos, por lo que puede elegir entre el mejor filtro de nitidez (Lanczos) y el mío. Sin embargo, si aumentamos el número a 120 cuadros, entonces mi filtro está fuera de competencia:







Después de 300 cuadros, todos los filtros, excepto el mío, se convierten en una broma de mal gusto:







Después de 600 fotogramas, la broma se vuelve aún más cruel:







Ni siquiera tiene que decir qué sucede después de 900 fotogramas:







¿Qué tan estable es?


En esta etapa, naturalmente se preguntará: ¿mi filtro es realmente estable o es simplemente un desenfoque muy lento, mucho más lento que el filtrado bilineal? ¿Quizás después de miles de repeticiones, mi filtro borrará gradualmente la imagen?

Sorprendentemente, la respuesta parece ser negativa. Aunque se agrega un poco de desenfoque en el transcurso de aproximadamente cien de las primeras superposiciones, parece que el filtro converge en una representación estable de la imagen, que nunca se degrada. Aquí hay otra imagen ampliada de un ojo de morsa:


De izquierda a derecha: la imagen original, mi filtro aplicado 60 veces, 120 veces, 300 veces, 600 y 900 veces. Como puede ver, el desenfoque converge a un estado estable, que ya no se degrada incluso después de cientos de superposiciones de filtros. Por el contrario, compare esto con la sincronización en ventana para el mismo número de muestras (toque), y vea qué tan malo (¡y rápido!) Los artefactos forman la retroalimentación y crean un resultado inútil:


Mi filtro parece muy estable y, en comparación con todos los filtros que he visto, crea los mejores resultados después de un uso repetido. Parece que tiene una cierta propiedad "asintótica", en la que los datos convergen rápidamente a una imagen suavizada (limitada), y luego esta imagen suavizada se guarda y no realiza una degradación ilimitada para completar la basura.

Incluso intenté aplicar el filtro un millón de veces, y parece que después de los primeros cientos de superposiciones no se degrada más. Sin un mejor análisis matemático (y aún no he encontrado una solución matemática que pueda probarlo exactamente, pero estoy seguro de que está en algún lugar), no puedo decir con certeza que en algún lugar después de miles de millones o billones de superposiciones que -no se romperá. Dentro de las pruebas razonables, no pude detectar una mayor degradación.

¿Es el mejor filtro Halfpel estable para seis toques?


En esta etapa, sería lógico hacer la pregunta: ¿es esto realmente lo mejor que se puede encontrar? La intuición nos dice que no, porque no tenía absolutamente ningún conocimiento sobre el desarrollo de filtros y casi no busqué en la literatura, recogí este filtro en solo una hora. Al menos se puede suponer que después de un estudio tan breve, no habría encontrado un filtro definitivo, el mejor, el mejor conquistador.

¿Es cierto este supuesto? Y si es cierto, ¿cuál será el mejor filtro final? Discutiré esto con más detalle en la segunda parte del artículo.

Source: https://habr.com/ru/post/468501/


All Articles