Comprima una imagen en una vista previa de 4 KiB

30

En este desafío, creará un algoritmo de compresión de vista previa de imagen. Su objetivo es reducir un archivo de imagen arbitrario a una imagen de vista previa de 4 KiB, que se puede utilizar para identificar rápidamente imágenes con muy poco ancho de banda.

Debe escribir dos programas (o un programa combinado): un compresor y un descompresor. Ambos deben tomar un archivo o stdin como entrada y salida a un archivo o stdout. El compresor debe aceptar una imagen en el formato de imagen principal sin pérdida de elección (por ejemplo, PNG, BMP, PPM), y generar un archivo de 4096 bytes como máximo . El descompresor debe aceptar cualquier archivo generado por el compresor y mostrar una imagen lo más cerca posible de la entrada. Tenga en cuenta que no hay límite de tamaño de código fuente para el codificador / decodificador, por lo que puede ser creativo en su algoritmo.

Restricciones

  • No hay "trampa". Sus programas no pueden usar entradas ocultas, almacenar datos en Internet, etc. También se le prohíbe incluir características / datos relacionados únicamente con el conjunto de imágenes de puntuación.

  • Para las bibliotecas / herramientas / muebles empotrados que está permitido el uso de genéricos operaciones de procesamiento de imágenes (escalamiento, visión borrosa, color de transformación del espacio, etc), pero no de decodificación de imágenes / codificación / compresión de las operaciones (excepto para la entrada del compresor y descompresor de salida). La compresión / descompresión genérica también está prohibida . Se pretende que implemente su propia compresión para este desafío.

  • Las dimensiones de la salida de imagen por el descompresor deben coincidir exactamente con las del archivo original dado al compresor. Puede suponer que las dimensiones de la imagen no superan los 2 16 en ninguna dirección.

  • Su compresor debe funcionar en una PC de consumo promedio en menos de 5 minutos, y el descompresor debe funcionar en menos de 10 segundos para cualquier imagen en el conjunto a continuación.

Tanteo

Para ayudar a la verificación rápida y la comparación visual, incluya un álbum de imágenes sin pérdida del corpus de prueba después de la compresión con su respuesta.

Su compresor será probado usando el siguiente corpus de imágenes :

estrellado fuente habitación arco iris
margen llama niño julia

Puede descargar todas las imágenes en un archivo zip aquí .

Su puntaje será el índice de similitud estructural promedio para su compresor en todas las imágenes. Utilizaremos el código abierto dssimpara este desafío. Se construye fácilmente desde la fuente, o si estás en Ubuntu también tiene un PPA. Es preferible que obtenga su propia respuesta, pero si no sabe cómo crear aplicaciones C y no ejecuta Debian / Ubuntu, puede dejar que alguien más lo haga por usted.dssimespera entrada / salida en PNG, por lo tanto, convierta su salida a PNG primero si sale en un formato diferente.

Para que la puntuación sea indolora, aquí hay un script de Python de ayuda rápida, uso python score.py corpus_dir compressed_dir:

import glob, sys, os, subprocess

scores = []
for img in sorted(os.listdir(sys.argv[1])):
    ref, preview = (os.path.join(sys.argv[i], img) for i in (1, 2))
    sys.stdout.write("Comparing {} to {}... ".format(ref, preview))
    out = subprocess.check_output(["dssim", ref, preview]).decode("utf-8").split()[0]
    print(out)
    scores.append(float(out))

print("Average score: {:.6f}".format(sum(scores) / len(scores)))

La puntuación más baja gana.

orlp
fuente
¿La imagen comprimida tiene que ser visible?
Eumel
2
@Eumel Puede considerar el descompresor como un visor. Así que no, su formato comprimido puede ser arbitrario y depende completamente de usted. Solo después de la descompresión, debe aparecer una imagen visible.
orlp
77
You may assume that the image dimensions do not exceed 2^32 in either direction.¿No es esto un poco excesivo? Esto significa que tengo que usar hasta 16 bytes para almacenar un par de coordenadas (x, y). Pocos archivos de imagen tienen dimensiones de más de 2 ^ 16 (65536) píxeles en cualquier dirección, y 2 ^ 11 es suficiente para todas las imágenes en el corpus.
Peter Olson
@PeterOlson lo cambiaré a 2^16.
orlp
@orlp Las reglas establecen que el descompresor debe tomar menos de diez segundos para decodificar una imagen. La idea que tengo potencialmente tomará unos minutos para generar archivos de referencia que se utilizarán en llamadas posteriores al descompresor. es decir, es una actividad única similar a "instalar" una aplicación. ¿Esta solución sería descalificada?
Moogie

Respuestas:

8

Python con PIL, puntaje 0.094218

Compresor:

#!/usr/bin/env python
from __future__ import division
import sys, traceback, os
from PIL import Image
from fractions import Fraction
import time, io

def image_bytes(img, scale):
    w,h = [int(dim*scale) for dim in img.size]
    bio = io.BytesIO()
    img.resize((w,h), Image.LANCZOS).save(bio, format='PPM')
    return len(bio.getvalue())

def compress(img):
    w,h = img.size
    w1,w2 = w // 256, w % 256
    h1,h2 = h // 256, h % 256
    n = w*h
    total_size = 4*1024 - 8 #4 KiB minus 8 bytes for
                            # original and new sizes
    #beginning guess for the optimal scaling
    scale = Fraction(total_size, image_bytes(img, 1))
    #now we do a binary search for the optimal dimensions,
    # with the restriction that we maintain the scale factor
    low,high = Fraction(0),Fraction(1)
    best = None
    start_time = time.time()
    iter_count = 0
    while iter_count < 100: #scientifically chosen, not arbitrary at all
        #make sure we don't take longer than 5 minutes for the whole program
        #10 seconds is more than reasonable for the loading/saving
        if time.time() - start_time >= 5*60-10:
            break
        size = image_bytes(img, scale)
        if size > total_size:
            high = scale
        elif size < total_size:
            low = scale
            if best is None or total_size-size < best[1]:
                best = (scale, total_size-size)
        else:
            break
        scale = (low+high)/2
        iter_count += 1
    w_new, h_new = [int(dim*best[0]) for dim in (w,h)]
    wn1,wn2 = w_new // 256, w_new % 256
    hn1, hn2 = h_new // 256, h_new % 256
    i_new = img.resize((w_new, h_new), Image.LANCZOS)
    bio = io.BytesIO()
    i_new.save(bio, format='PPM')
    return ''.join(map(chr, (w1,w2,h1,h2,wn1,wn2,hn1,hn2))) + bio.getvalue()

if __name__ == '__main__':
    for f in sorted(os.listdir(sys.argv[1])):
        try:
            print("Compressing {}".format(f))
            with Image.open(os.path.join(sys.argv[1],f)) as img:
                with open(os.path.join(sys.argv[2], f), 'wb') as out:
                    out.write(compress(img.convert(mode='RGB')))
        except:
            print("Exception with {}".format(f))
            traceback.print_exc()
            continue

Descompresor:

#!/usr/bin/env python
from __future__ import division
import sys, traceback, os
from PIL import Image
from fractions import Fraction
import io

def process_rect(rect):
    return rect

def decompress(compressed):
    w1,w2,h1,h2,wn1,wn2,hn1,hn2 = map(ord, compressed[:8])
    w,h = (w1*256+w2, h1*256+h2)
    wc, hc = (wn1*256+wn2, hn1*256+hn2)
    img_bytes = compressed[8:]
    bio = io.BytesIO(img_bytes)
    img = Image.open(bio)
    return img.resize((w,h), Image.LANCZOS)


if __name__ == '__main__':
    for f in sorted(os.listdir(sys.argv[1])):
        try:
            print("Decompressing {}".format(f))
            with open(os.path.join(sys.argv[1],f), 'rb') as img:
                decompress(img.read()).save(os.path.join(sys.argv[2],f))
        except:
            print("Exception with {}".format(f))
            traceback.print_exc()
            continue

Ambas secuencias de comandos toman entrada a través de argumentos de línea de comando, como dos directorios (entrada y salida), y convierten todas las imágenes en el directorio de entrada.

La idea es encontrar un tamaño que se ajuste a menos de 4 KiB y tenga la misma relación de aspecto que el original, y usar un filtro Lanczos para obtener la mayor calidad posible de la imagen muestreada.

Imgur álbum de imágenes comprimidas, después de cambiar el tamaño a las dimensiones originales

Resultado del script de puntuación:

Comparing corpus/1 - starry.png to test/1 - starry.png... 0.159444
Comparing corpus/2 - source.png to test/2 - source.png... 0.103666
Comparing corpus/3 - room.png to test/3 - room.png... 0.065547
Comparing corpus/4 - rainbow.png to test/4 - rainbow.png... 0.001020
Comparing corpus/5 - margin.png to test/5 - margin.png... 0.282746
Comparing corpus/6 - llama.png to test/6 - llama.png... 0.057997
Comparing corpus/7 - kid.png to test/7 - kid.png... 0.061476
Comparing corpus/8 - julia.png to test/8 - julia.png... 0.021848
Average score: 0.094218
Mego
fuente
Me acabo de dar cuenta de que su solución usa WebP, lo cual no está permitido. Tu solución no es válida.
orlp
@orlp Ahora está arreglado para usar PPM, que es un formato sin comprimir.
Mego
Bien. Sin embargo, este desafío expone bastante debilidad de DSSIM. Yo diría que la mayoría de las imágenes de Moogie se ven sustancialmente mejores.
orlp
@orlp Se ven bien como miniaturas. Cuando explotan a sus dimensiones originales (usando Lanczos), se ven de la misma calidad o peor. Estoy trabajando para subir las miniaturas de mi salida.
Mego
7

Java (vainilla, debería funcionar con java 1.5+), puntaje 0.672

No genera puntajes dssim particularmente buenos pero, a mi parecer, produce imágenes más amigables para los humanos ...

estrellado fuente habitación arco iris
margen llama niño julia

Álbum: http://imgur.com/a/yL31U

Resultado del script de puntuación:

Comparing corpus/1 - starry.png to test/1 - starry.png... 2.3521
Comparing corpus/2 - source.png to test/2 - source.png... 1.738
Comparing corpus/3 - room.png to test/3 - room.png... 0.1829
Comparing corpus/4 - rainbow.png to test/4 - rainbow.png... 0.0633
Comparing corpus/5 - margin.png to test/5 - margin.png... 0.4224
Comparing corpus/6 - llama.png to test/6 - llama.png... 0.204
Comparing corpus/7 - kid.png to test/7 - kid.png... 0.36335
Comparing corpus/8 - julia.png to test/8 - julia.png... 0.05
Average score: 0.672

La cadena de compresión:

1. if filter data has already been generated goto step 6
2. create sample images from random shapes and colours
3. take sample patches from these sample images
4. perform k-clustering of patches based on similarity of luminosity and chomanosity.
5. generate similarity ordered lists for each patch as compared to the other patches.
6. read in image
7. reduce image size to current multiplier * blocksize
8. iterate over image comparing each blocksize block against the list of clustered luminosity patches and chromanosity patches, selecting the closest match
9. output the index of the closet match from the similarity ordered list (of the previous block) (repeat 8 for chromanosity)
10. perform entropy encoding using deflate.
11. if output size is < 4096 bytes then increment current multiplier and repeat step 7
12. write previous output to disk.

Para descomprimir, inflar y luego leer los índices de bloque y enviar el parche correspondiente al archivo de salida, luego cambiar el tamaño a las dimensiones originales.

Lamentablemente, el código es demasiado grande para stackoverflow, por lo que puede encontrarlo en https://gist.github.com/anonymous/989ab8a1bb6ec14f6ea9

Correr:

Usage: 
       For single image compression: java CompressAnImageToA4kibPreview -c <INPUT IMAGE> [<COMPRESSED IMAGE>]
       For multiple image compression: java CompressAnImageToA4kibPreview -c <INPUT IMAGES DIR> [<COMPRESSED IMAGE DIR>]
       For single image decompression: java CompressAnImageToA4kibPreview -d <COMPRESSED IMAGE> [<DECOMPRESSED IMAGE>
       For multiple image decompression: java CompressAnImageToA4kibPreview -d <COMPRESSED IMAGE DIR> [<DECOMPRESSED IMAGES DIR>]

If optional parameters are not set then defaults will be used:
       For single image compression, compressed image will be created in same directory are input image and have '.compressed' file extension.
       For multiple image compression, compressed images will be created in a new 'out' sub directory of <INPUT IMAGES DIR> and have '.compressed' file extensions.
       For single image decompression, decompressed image will be created in same directory are input image and have '.out.png' file extension.
       For multiple image decompression, decompressed images will be created a new 'out' sub directory of <COMPRESSED IMAGE DIR> and have '.png' file extensions.

La primera vez que se ejecuta esta aplicación, los archivos necesarios se generarán y guardarán en un directorio relativo al directorio de trabajo de ejecución. Esto puede tomar unos pocos minutos. Para ejecuciones posteriores, no será necesario realizar este paso.

Moogie
fuente
Esto se ve asombroso. Solo para confirmar, ¿los pasos 1-6 no dependen en absoluto del corpus? Además, ¿le importaría si vuelvo a alojar su código en gist.github.com?
orlp
Correcto, no utiliza ninguno de los archivos de corpus como entrada. puede ver las imágenes que produce para generar la compra de parches cambiando la constante "OUTPUT_SAMPLE_IMAGES" a verdadero.
Producirá
@orlp ahora usa gist.github
Moogie
Los resultados son sorprendentes, pero ¿el uso de desinflar / inflar no infringe la regla de no permitir la compresión / descompresión genérica?
algmyr
@algmyr Ha pasado un tiempo, pero creo que había interpretado que la regla de compresión no genérica significaba que no había compresión genérica de 'imagen' ... es decir, jpeg, etc. Pero puedo haberlo interpretado incorrectamente, en cuyo caso, sí, esto La presentación debe ser descalificada.
Moogie