Écrire un encodeur GIF

9

Oui, le bon vieux GIF. Aimé pour sa polyvalence, détesté pour ses brevets et en partie obsolète en raison de ses limites (et brevets), GIF se compose, au cœur, d'une palette de couleurs et d'une image indexée de palette compressée à l'aide de l'algorithme LZW.

Votre tâche consiste à écrire un programme qui lit une image au format ASCII PPM (nombre magique "P3") à partir de l'entrée standard et écrit la même image (pixel par pixel identique) au format GIF sur la sortie standard. La sortie peut être soit sous forme binaire, soit en texte ASCII, chaque octet étant représenté par un nombre compris entre 0 et 255 (inclus), séparé par des espaces.

L'image d'entrée est garantie de ne pas avoir plus de 256 couleurs différentes.

Notation:

Votre programme sera testé sur 3 échantillons d'images et votre score sera calculé comme suit:
taille du programme + somme (taille de sortie - taille de référence pour chaque image d'échantillon)
Le score le plus bas l'emporte.

Exigences:

  • Votre programme doit fonctionner avec des types d'images similaires de différentes tailles et ne pas être limité aux exemples d'images. Vous pouvez, par exemple, restreindre les dimensions à des multiples de 2 ou supposer que la couleur maximale en ppm est de 255, mais cela devrait toujours fonctionner avec une grande variété d'images d'entrée.
  • La sortie doit être un fichier GIF valide qui peut être chargé avec n'importe quel programme compatible (après la reconversion en binaire si vous utilisez l'option de sortie ASCII).
  • Vous ne pouvez utiliser aucune fonction de traitement d'image (intégrée ou tierce), votre programme doit contenir tout le code pertinent.
  • Votre programme doit être exécutable sous Linux en utilisant un logiciel disponible gratuitement.
  • Le code source doit utiliser uniquement des caractères ASCII.

Exemples d'images:

Voici les 3 exemples d'images qui seront utilisés pour la notation. Vous pouvez télécharger une archive zip avec les fichiers ppm (utilisez le bouton de téléchargement en haut de cette page). Ou vous pouvez les convertir à partir des images png ci-dessous, en utilisant ImageMagick avec la commande suivante:

convert file.png -compress none file.ppm

Je fournis également les sommes de contrôle MD5 des fichiers ppm pour confirmation.

1. ambre

amber.png

Taille de référence: 38055 Somme de
contrôle MD5 de ppm: d1ad863cb556869332074717eb278080

2. yeux bleus

blueeyes.png

Taille de référence: 28638 Somme de
contrôle MD5 de ppm: e9ad410057a5f6c25a22a534259dcf3a

3. poivrons

peppers.png

Taille de référence: 53586 Somme de
contrôle MD5 de ppm: 74112dbdbb8b7de5216f9e24c2e1a627

aditsu quitte parce que SE est MAL
la source
1
Remarque du modérateur: les commentaires hors sujet / obsolètes ont été supprimés. Veuillez consulter la méta pour une discussion sur les exemples d'images dans cette question.
Poignée de porte
Il semble que la deuxième image ait été traitée avec quelque chose comme ceci: websiteoptimization.com/speed/tweak/lossy qui expliquerait un meilleur taux de compression et une sensibilité aux réglages de l'encodeur LZW.
nutki
1
"Le code source ne doit utiliser que des caractères ASCII." - donc, en d'autres termes, nous ne sommes pas autorisés à relever ce défi en APL?
FUZxxl
@FUZxxl true, mais vous pouvez utiliser J. Vous n'êtes pas non plus autorisé à utiliser Aheui ou à effectuer des astuces de conversion de base dans GolfScript / CJam.
aditsu quitte car SE est EVIL

Réponses:

4

Perl, 515 + -2922 + 0 + -2571 = -4978

Une autre approche. Cette fois, j'essaie d'enregistrer l'image dans des tuiles de taille 64xH. C'est bien selon les spécifications, mais certains logiciels peuvent afficher uniquement la première tuile ou une animation. Les tuiles se compriment mieux grâce à une meilleure localisation spatiale. Je fais également une compression normale pour la deuxième image et je choisis celle qui est la plus courte. Comme cela comprime deux fois l'image, elle est deux fois plus lente que ma solution précédente.

#!perl -n0
sub r{$r.=1&"@_">>$_ for 0..log(@d|256)/log 2}
@k=/(\d+) (\d+)/;
@l=map{$$_||=(push(@t,split$"),++$i)}/\d+ \d+ \d+/g;
print+GIF89a,pack(vvCxxC768,@k,~8,@t);
sub v{($w,$h)=@_;for$k(0.."@k"/$w-1){
$k*=$w;$r='';@d=();@p=grep+($z++-$k)%"@k"<$w,@l;
$"=' ';$_="@p ";$"='|';while(/./){
r 256;%d=map{($_,$_-1)}@d=1..256;
$d{$&}=@d+2,r$d{$1},unshift@d,$&while@d<4095&&s/^(@d) (\d*)/$2/}
r 257;$_=pack"b*",$r;
$h.=pack"Cv4n(C/a)*",44,$k,0,$w,$k[1],8,/.{0,255}/gs
}$b=$h if!$b||length$b>length$h}
"@k"%64||v 64;v"@k";print"$b;"

Perl, 354 + 12 + 0 + -1 = 365 418 9521 51168 56639

Soit il y a un bug dans mon code, soit la deuxième image est optimisée pour un encodeur spécifique car un changement apparemment insignifiant a réduit la taille à la référence exactement. Prend environ 30s-60s par image.

Version golfée.

#!perl -n0
sub r{$r.=1&"@_">>$_ for 0..log(@d|256)/log 2}
@k=/(\d+) (\d+)/;
@p=map{$$_||=(push(@t,split$"),++$i)}/\d+ \d+ \d+/g;
$_="@p ";$"='|';while(/./){
r 256;%d=map{($_,$_-1)}@d=1..256;
$d{$&}=@d+2,r$d{$1},unshift@d,$&while@d<4095&&s/^(@d) (\d*)/$2/}
r 257;$_=pack"b*",$r;
print+GIF89a,pack(vvCxxC768,@k,~8,@t),
pack("Cx4vvn(C/a)*",44,@k,8,/.{0,255}/gs),';'

La seule décision qu'un compresseur GIF peut prendre est de réinitialiser le dictionnaire LZW. En général, en raison de la façon dont les images pour cette tâche ont été choisies, le meilleur moment pour le faire est chaque 4096 codes, c'est-à-dire le moment où le dictionnaire déborderait. Avec une telle limite, le dictionnaire ne déborde jamais, ce qui économise quelques octets dans l'implémentation. Voici comment cela fonctionne en détail:

#!perl -n0
# function to add one codeword to the output stream @r.
# the current codeword length is based on the dictionary size/
sub r{push@r,map"@_">>$_,0..log(@d|256)/log 2}
# get the dimensions into @k
@k=/(\d+) (\d+)/;
# get pixel indexes to @p and palette to @t
@p=map{$$_||=(push(@t,split$"),++$i)}/\d+ \d+ \d+/g;
# convert index table into space separated string 
$_="@p ";$"='|';
# LZW encoder; while something to encode
while(/\S/){
# output reset code
r 256;
# reset code dictionary $d is the last code number,
# %d is the map of codes and @d list of codes
$d=257;%d=map{($_,$_-1)}@d=1..256;
# find codes using regexp, stop at dictionary overflow
while($d<4096&&s/^(@d) (\d*)/$2/){
unshift@d,$&;$d{$&}=++$d;r$d{$1}}}
# end LZW encoder; output end code
r 257;
# convert bit string @r to bytes $f
vec($f,$j++,1)=$_ for@r;
# output header up to the color table
print+GIF89a,pack(vvCvC768,@k,~8,0,@t),
# output rest of the header
pack(Cv4CC,44,0,0,@k,0,8),
# output the LZW compressed data $f slicing into sub-blocks
$f=~s/.{0,255}/chr(length$&).$&/egsr,';'

Perl, 394 + -8 + 0 + -12 = 374

L'ajout d'une heuristique pour deviner le point de réinitialisation améliore un peu la compression mais pas assez pour justifier le code supplémentaire:

#!perl -n0
sub r{$r.=1&"@_">>$_ for 0..log(@d|256)/log 2}
@k=/(\d+) (\d+)/;
@p=map{$$_||=(push(@t,split$"),++$i)}/\d+ \d+ \d+/g;
$_="@p ";$"='|';while(/./){
r 256;%d=map{($_,$_-1)}@d=1..256;
$d{$&}=@d+2,r$d{$1},unshift@d,$&while
(@d<4001||(/((@d) ){11}/,$&=~y/ //>12))&@d<4095&&s/^(@d) (\d*)/$2/}
r 257;$_=pack"b*",$r;
print+GIF89a,pack(vvCxxC768,@k,~8,@t),
pack("Cx4vvn(C/a)*",44,@k,8,/.{0,255}/gs),';'
nutki
la source
Très agréable! Bien que cela prenne beaucoup plus de 30 secondes par image ici. J'ai été assez impressionné par le -30 de la version précédente, je me demande si vous pouvez combiner les méthodes et obtenir un score inférieur. Pourriez-vous également écrire un peu sur ce que fait le programme?
aditsu quitte car SE est EVIL
Exiger que la largeur soit un multiple de 64 semble un peu extrême ...
aditsu quitte car SE est EVIL
@aditsu, Ce n'est pas obligatoire, si la largeur n'est pas multiple de 64, la méthode de tuilage n'est pas essayée et une compression régulière est utilisée. Bien sûr, au coût d'environ 100 caractères supplémentaires, je pourrais créer la dernière taille variable de tuile.
nutki
Très lent, et les images en mosaïque apparaissent sous forme d'animations, mais .. bien fait, c'est assez impressionnant de voir que vous pouvez vraiment les réduire.
aditsu quitte car SE est EVIL
2

CJam, score 155 + 35306 + 44723 + 21518 = 101702

Juste une implémentation de référence stupide. C'est lent, ne fait aucune compression réelle et ce n'est pas du golf.

"GIF89a":iS*SqN/S*S%1>:i3/:M0=2<256f{md\}S*:ZS247S0S0SM1>_|:PL*_,768\m0a*+S*S44S0S0S0S0SZS0S8SM1>Pf{\a#}254/256a*{512+2b1>W%}%:+8/{W%2b}%255/{_,S@S*S}/0S59
aditsu quitte parce que SE est MAL
la source