Compressão de arte ASCII com perda

21

fundo

PICASCII é uma ferramenta elegante que converte imagens em arte ASCII.

Atinge diferentes graus de brilho usando os dez caracteres ASCII a seguir:

@#+';:,.` 

Diremos que esses charxels (elementos de caractere) têm brilho de 1 (sinal de arroba) a 10 (espaço).

Abaixo, você pode ver os resultados da conversão de um pequeno código, a bandeira galesa, um fractal estendido, uma truta grande e um pouco de golfe, exibidos com a fonte correta:

Arte ASCII

Você pode ver as imagens neste violino e fazer o download delas no Google Drive .

Tarefa

Embora os resultados finais do PICASCII sejam visualmente agradáveis, todas as cinco imagens combinadas pesam 153.559 bytes. Quanto essas imagens podem ser compactadas se estivermos dispostos a sacrificar parte de sua qualidade?

Sua tarefa é escrever um programa que aceite uma imagem artística ASCII como as anteriores e uma qualidade mínima como entrada e imprima uma compactação com perdas da imagem - na forma de um programa completo ou de uma função retornando uma única sequência - que satisfaça a exigência de qualidade.

Isso significa que você não consegue escrever um descompressor separado; ele deve estar embutido em cada uma das imagens compactadas.

A imagem original consistirá em charxels com brilho entre 1 e 10, separados por feeds de linha em linhas do mesmo comprimento. A imagem compactada deve ter as mesmas dimensões e usar o mesmo conjunto de caracteres.

Para uma imagem não compactada que consiste em n charxels, a qualidade de uma versão compactada da imagem é definida como

fórmula de qualidade

onde c i é o brilho do i- ésimo charxel da saída da imagem compactada e u é o brilho do i- ésimo charxel da imagem não compactada.

Pontuação

Seu código será executado com as cinco imagens acima como entrada e configurações de qualidade mínima de 0,50, 0,60, 0,70, 0,80 e 0,90 para cada uma das imagens.

Sua pontuação é a média geométrica dos tamanhos de todas as imagens compactadas, ou seja, a vigésima quinta raiz do produto, dos comprimentos de todas as vinte e cinco imagens compactadas.

A pontuação mais baixa vence!

Regras adicionais

  • Seu código precisa funcionar para imagens arbitrárias, não apenas para as usadas na pontuação.

    Espera-se que você otimize seu código para os casos de teste, mas um programa que nem tente compactar imagens arbitrárias não receberá um voto positivo de mim.

  • O seu compressor pode usar compressores de fluxo de bytes embutidos (por exemplo, gzip), mas você mesmo deve implementá-los para as imagens compactadas.

    Bulit-ins normalmente usados ​​em descompressores de fluxo de bytes (por exemplo, conversão de base, decodificação no comprimento da execução) são permitidos.

  • Compressor e imagens compactadas não precisam estar no mesmo idioma.

    No entanto, você deve escolher um único idioma para todas as imagens compactadas.

  • Para cada imagem compactada, aplicam-se regras de código padrão de golfe.

Verificação

Criei um script CJam para verificar facilmente todos os requisitos de qualidade e calcular a pontuação de um envio.

Você pode fazer o download do interpretador Java aqui ou aqui .

e# URLs of the uncompressed images.
e# "%s" will get replaced by 1, 2, 3, 4, 5.

"file:///home/dennis/codegolf/53199/original/image%s.txt"

e# URLs of the compressed images (source code).
e# "%s-%s" will get replaced by "1-50", "1-60", ... "5-90".

"file:///home/dennis/codegolf/53199/code/image%s-%s.php"

e# URLs of the compressed images (output).

"file:///home/dennis/codegolf/53199/output/image%s-%s.txt"

e# Code

:O;:C;:U;5,:)
{
    5,5f+Af*
    {
        C[IQ]e%g,X*:X;
        ISQS
        [U[I]e%O[IQ]e%]
        {g_W=N&{W<}&}%
        _Nf/::,:=
        {
            {N-"@#+';:,.` "f#}%z
            _::m2f#:+\,81d*/mq1m8#
            _"%04.4f"e%S
            @100*iQ<"(too low)"*
        }{
            ;"Dimension mismatch."
        }?
        N]o
    }fQ
}fI
N"SCORE: %04.4f"X1d25/#e%N

Exemplo

Bash → PHP, pontuação 30344.0474

cat

Atinge 100% de qualidade para todas as entradas.

$ java -jar cjam-0.6.5.jar vrfy.cjam
1 50 1.0000 
1 60 1.0000 
1 70 1.0000 
1 80 1.0000 
1 90 1.0000 
2 50 1.0000 
2 60 1.0000 
2 70 1.0000 
2 80 1.0000 
2 90 1.0000 
3 50 1.0000 
3 60 1.0000 
3 70 1.0000 
3 80 1.0000 
3 90 1.0000 
4 50 1.0000 
4 60 1.0000 
4 70 1.0000 
4 80 1.0000 
4 90 1.0000 
5 50 1.0000 
5 60 1.0000 
5 70 1.0000 
5 80 1.0000 
5 90 1.0000 

SCORE: 30344.0474
Dennis
fonte
Estou tendo problemas para entender esta parte: se alguém escolher q = 0,5, cada caractere no arquivo de entrada deverá ser substituído pelo caractere com metade do brilho na saída, certo? Obviamente, excluir o espaço em branco, pois isso atrapalharia a imagem inteira.
Nicolás Siplis 16/07/2015
11
É tudo muito confuso e brega. Como você interrompe uma entrada mattmahoney.net/dc/barf.html ? O descompactador também pode ler qualquer arquivo que não seja a imagem compactada? Você pode fornecer um script python ou algo que realmente verifique a qualidade de uma imagem e calcule uma pontuação para que também não haja duvidas nessa parte? Etc.
Will
11
@ Confuso? Talvez. Mas não acho que seja uma bobagem. Cada imagem compactada deve ser um programa ou função, de modo que piadas sem graça como BARF são excluídas automaticamente. Não conheço Python, mas vou pensar em algo simples para verificação.
Dennis
8
"Eu criei um script CJam para verificar facilmente todos os requisitos de qualidade e calcular a pontuação de um envio". As pessoas realmente usam essa coisa para fazer scripts normais? Caro senhor ...
Fatalize

Respostas:

4

Java → CJam, pontuação 174417.89

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import net.aditsu.cjam.CJam;

public class Compress {
    protected static final char[] DIGITS = "0123456789ABCDEFGHIJK".toCharArray();
    protected static final String CHARS = "@#+';:,.` ";
    protected static final char[] CHR = CHARS.toCharArray();

    private static class Img {
        public final int rows;
        public final int cols;
        public final int[][] a;

        public Img(final int rows, final int cols) {
            this.rows = rows;
            this.cols = cols;
            a = new int[rows][cols];
        }

        public Img(final List<String> l) {
            rows = l.size();
            cols = l.get(0).length();
            a = new int[rows][cols];
            for (int i = 0; i < rows; ++i) {
                for (int j = 0; j < cols; ++j) {
                    a[i][j] = CHARS.indexOf(l.get(i).charAt(j));
                }
            }
        }

        public static Img read(final Reader r) {
            try {
                final BufferedReader br = new BufferedReader(r);
                final List<String> l = new ArrayList<>();
                while (true) {
                    final String s = br.readLine();
                    if (s == null || s.isEmpty()) {
                        break;
                    }
                    l.add(s);
                }
                br.close();
                return new Img(l);
            }
            catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        public static Img read(final File f) {
            try {
                return read(new FileReader(f));
            }
            catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        public Img scaleDown(final int fr, final int fc) {
            final int r1 = (rows + fr - 1) / fr;
            final int c1 = (cols + fc - 1) / fc;
            final Img x = new Img(r1, c1);
            final int[][] q = new int[r1][c1];
            for (int i = 0; i < rows; ++i) {
                for (int j = 0; j < cols; ++j) {
                    x.a[i / fr][j / fc] += a[i][j];
                    q[i / fr][j / fc]++;
                }
            }
            for (int i = 0; i < r1; ++i) {
                for (int j = 0; j < c1; ++j) {
                    x.a[i][j] /= q[i][j];
                }
            }
            return x;
        }

        public Img scaleUp(final int fr, final int fc) {
            final int r1 = rows * fr;
            final int c1 = cols * fc;
            final Img x = new Img(r1, c1);
            for (int i = 0; i < r1; ++i) {
                for (int j = 0; j < c1; ++j) {
                    x.a[i][j] = a[i / fr][j / fc];
                }
            }
            return x;
        }

        public Img crop(final int r, final int c) {
            if (r == rows && c == cols) {
                return this;
            }
            final Img x = new Img(r, c);
            for (int i = 0; i < r; ++i) {
                for (int j = 0; j < c; ++j) {
                    x.a[i][j] = a[i][j];
                }
            }
            return x;
        }

        public Img rescale(final int fr, final int fc) {
            return scaleDown(fr, fc).scaleUp(fr, fc).crop(rows, cols);
        }

        public double quality(final Img x) {
            if (x.rows != rows || x.cols != cols) {
                throw new IllegalArgumentException();
            }
            double t = 0;
            for (int i = 0; i < rows; ++i) {
                for (int j = 0; j < cols; ++j) {
                    final int y = a[i][j] - x.a[i][j];
                    t += y * y;
                }
            }
            t /= 81 * rows * cols;
            t = 1 - Math.sqrt(t);
            return Math.pow(t, 8);
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            for (int i = 0; i < rows; ++i) {
                for (int j = 0; j < cols; ++j) {
                    sb.append(CHR[a[i][j]]);
                }
                sb.append('\n');
            }
            return sb.toString();
        }

        public Array toArray() {
            final Array x = new Array(rows * cols);
            int k = 0;
            for (int i = 0; i < rows; ++i) {
                for (int j = 0; j < cols; ++j) {
                    x.a[k++] = a[i][j];
                }
            }
            return x;
        }

        public String compress(final double quality) {
            int bi = 1;
            int bj = 1;
            int bs = rows * cols;
            Img bx = this;

            for (int i = 1; i < 3; ++i) {
                for (int j = 1; j < 3; ++j) {
                    Img x = rescale(i, j);
                    if (quality(x) >= quality) {
                        x = scaleDown(i, j);
                        if (x.rows * x.cols < bs) {
                            bi = i;
                            bj = j;
                            bs = x.rows * x.cols;
                            bx = x;
                        }
                    }
                }
            }

            Array a = bx.toArray();
            int bf = 0;
            for (int i = 1; i <= 20; ++i) {
                final int t = a.rle11(i).n;
                if (t < bs) {
                    bs = t;
                    bf = i;
                }
            }

            int b = 10;
            if (bf > 0) {
                b = 11;
                a = a.rle11(bf);
            }

            String s = null;
            for (int i = 92; i < 97; ++i) {
                for (char c = ' '; c < '$'; ++c) {
                    final String t = a.cjamBase(b, i, c);
                    boolean ok = true;
                    for (int j = 0; j < t.length(); ++j) {
                        if (t.charAt(j) > '~') {
                            ok = false;
                            break;
                        }
                    }
                    if (!ok) {
                        continue;
                    }
                    if (s == null || t.length() < s.length()) {
                        s = t;
                    }
                }
            }

            if (bf > 0) {
                s += "{(_A={;()";
                if (bf > 1) {
                    s += DIGITS[bf] + "*";
                }
                s += "\\(a@*}&\\}h]e_";
            }
            if (bi * bj == 1) {
                return s + '"' + CHARS + "\"f=" + cols + "/N*";
            }
            s += bx.cols + "/";
            if (bi > 1) {
                s += bi + "e*";
                if (rows % 2 == 1) {
                    s += "W<";
                }
            }
            if (bj > 1) {
                s += bj + "fe*";
                if (cols % 2 == 1) {
                    s += "Wf<";
                }
            }
            return s + '"' + CHARS + "\"ff=N*";
        }

        public void verify(final String s, final double quality) {
            final String t = CJam.run(s, "");
            final Img x = read(new StringReader(t));
            final double q = quality(x);
            if (q < quality) {
                throw new RuntimeException(q + " < " + quality);
            }
//          System.out.println(q + " >= " + quality);
        }
    }

    private static class Array {
        public final int[] a;
        public final int n;

        public Array(final int n) {
            this.n = n;
            a = new int[n];
        }

        public Array(final int[] a) {
            this.a = a;
            n = a.length;
        }

        public String join() {
            final StringBuilder sb = new StringBuilder();
            for (int x : a) {
                sb.append(x).append(' ');
            }
            sb.setLength(sb.length() - 1);
            return sb.toString();
        }

//      public String cjamStr() {
//          final StringBuilder sb = new StringBuilder("\"");
//          for (int x : a) {
//              sb.append(DIGITS[x]);
//          }
//          sb.append("\":~");
//          return sb.toString();
//      }

        public String cjamBase(final int m, final int b, final char c) {
            final boolean zero = a[0] == 0;
            String s = join();
            if (zero) {
                s = "1 " + s;
            }
            s = CJam.run("q~]" + m + "b" + b + "b'" + c + "f+`", s);
            s += "'" + c + "fm" + b + "b" + DIGITS[m] + "b";
            if (zero) {
                s += "1>";
            }
            return s;
        }

        public Array rle11(final int f) {
            final int[] b = new int[n];
            int m = 0;
            int x = -1;
            int k = 0;
            for (int i = 0; i <= n; ++i) {
                final int t = i == n ? -2 : a[i];
                if (t == x && m < 11 * f) {
                    m++;
                }
                else {
                    if (m >= f && m > 3) {
                        b[k++] = 10;
                        b[k++] = m / f - 1;
                        b[k++] = x;
                        for (int j = 0; j < m % f; ++j) {
                            b[k++] = x;
                        }
                    }
                    else {
                        for (int j = 0; j < m; ++j) {
                            b[k++] = x;
                        }
                    }
                    m = 1;
                    x = t;
                }
            }
            return new Array(Arrays.copyOf(b, k));
        }
    }

    private static void score() {
        double p = 1;
        for (int i = 1; i < 6; ++i) {
            final File f = new File("image" + i + ".txt");
            final Img img = Img.read(f);
            final int n = (int) f.length();
            for (int j = 5; j < 10; ++j) {
                final double q = j / 10.0;
                final String s = img.compress(q);
                System.out.println(f.getName() + ", " + q + ": " + n + " -> " + s.length());
                img.verify(s, q);
                p *= s.length();
            }
        }
        System.out.println(Math.pow(p, 1 / 25.0));
    }

    public static void main(final String... args) {
        if (args.length != 2) {
            score();
            return;
        }
        final String fname = args[0];
        final double quality = Double.parseDouble(args[1]);
        try {
            final Img img = Img.read(new File(fname));
            final String s = img.compress(quality);
            img.verify(s, quality);
            final FileWriter fw = new FileWriter(fname + ".cjam");
            fw.write(s);
            fw.close();
        }
        catch (IOException e) {
            throw new RuntimeException();
        }
    }
}

Requer o jar CJam no caminho de classe. Se você fornecer dois argumentos de linha de comando (nome e qualidade do arquivo), ele anexará ".cjam" ao nome do arquivo e gravará a imagem compactada. Caso contrário, ele calcula sua pontuação nas 5 imagens de teste, que são consideradas no diretório atual. O programa também verifica todas as imagens compactadas automaticamente. Convém verificar novamente o cálculo da pontuação, caso haja alguma discrepância.

As técnicas usadas (até agora) são: escalar para metade (horizontal, vertical ou ambos) se não reduzir muito a qualidade, um RLE com código personalizado e conversão básica para compactar mais dados em cada caractere enquanto permanece no gama ASCII imprimível.

aditsu
fonte
Você poderia me dar uma breve visão geral de como executar isso? Eu o compilei (com êxito, eu acho) com javac -cp cjam-0.6.5.jar Compress.java, mas java -cp cjam-0.6.5.jar Compressdiz Error: Could not find or load main class Compresse java Compressnão encontra a classe CJam.
Dennis
@Dennis Você precisa adicionar o diretório que contém Compress.class ao caminho de classe (-cp). Se é no diretório atual, o uso -cp .:cjam-0.6.5.jar(em windoze Eu acho que você precisa de um ponto e vírgula em vez de dois pontos)
aditsu
Isso fez o truque, obrigado.
Dennis
2

Python 3.5 (principal e de saída) (atualmente não competitivo)

Feliz Aniversário, Desafio! Aqui está o seu presente: uma resposta!

EDIT: Saída convertida em código python, taxa de compactação aprimorada (ligeiramente) EDIT2: Imprimiu em bruto quando size é 1. Pontuação aprimorada, mas a pontuação precisa ser calculada novamente. EDIT3: @Dennis apontou que eu ainda tenho bugs para corrigir, então marquei a resposta como não-competitiva

Código:

import sys
LIST = [' ','`','.',',',':',';',"'",'+','#','@']

def charxel_to_brightness(charxel):
    return LIST.index(charxel)

def brightness_to_charxel(bright):
    return LIST[bright]

def image_to_brightness(imagetext):
    return [list(map(charxel_to_brightness,line)) for line in imagetext.split("\n")]

def brightness_to_image(brightarray):
    return '\n'.join([''.join(map(brightness_to_charxel,line)) for line in brightarray])

def split_into_parts(lst,size):
    return [lst[x:x+size] for x in range(0, len(lst), size)]

def gen_updown(startxel,endxel,size):
    return [[int((size-r)*(endxel-startxel)/size+startxel) for c in range(size)] for r in range(size)]

def gen_leftright(startxel,endxel,size):
    return [[int((size-c)*(endxel-startxel)/size+startxel) for c in range(size)] for r in range(size)]

def gen_tlbr(startxel,endxel,size):
    return [[int((2*size-r-c)/2*(endxel-startxel)/size+startxel) for c in range(size)] for r in range(size)]

def gen_bltr(startxel,endxel,size):
    return [[int((size-r+c)/2*(endxel-startxel)/size+startxel) for c in range(size)] for r in range(size)]

def gen_block(code,startxel,endxel,size):
    if code==0:return gen_updown(startxel,endxel,size)
    if code==1:return gen_leftright(startxel,endxel,size)
    if code==2:return gen_bltr(startxel,endxel,size)
    if code==3:return gen_tlbr(startxel,endxel,size)

def vars_to_data(code,startxel,endxel):
    acc=endxel
    acc+=startxel<<4
    acc+=code<<8
    return acc

def data_to_vars(data):
    code=data>>8
    startxel=(data>>4)&15
    endxel=data&15
    return code,startxel,endxel

def split_into_squares(imgarray,size):
    rows = split_into_parts(imgarray,size)
    allsquares = []
    for rowblock in rows:
        splitrows = []
        for row in rowblock:
            row = split_into_parts(row,size)
            splitrows.append(row)
        rowdict = []
        for row in splitrows:
            for x in range(len(row)):
                if len(rowdict)<=x:
                    rowdict.append([])
                rowdict[x].append(row[x])
        allsquares.append(rowdict)
    return allsquares

def calc_quality(imgarray,comparray):
    acc=0
    for row in range(len(imgarray)):
        for col in range(len(imgarray[row])):
            acc+=pow(imgarray[row][col]-comparray[row][col],2)
    return (1-(acc/81.0/sum([len(row) for row in imgarray]))**.5)**8

def fuse_squares(squarray):
    output=[]
    counter=0
    scounter=0
    sqrow=0
    while sqrow<len(squarray):
        if scounter<len(squarray[sqrow][0]):
            output.append([])
            for square in squarray[sqrow]:
                output[counter].extend(square[scounter])
            scounter+=1
            counter+=1
        else:
            scounter=0
            sqrow+=1
    return output

def main_calc(imgarray,threshold):
    imgarray = image_to_brightness(imgarray)
    size = 9
    quality = 0
    compimg=[]
    datarray=[]
    testdata = [vars_to_data(c,s,e) for c in range(4) for s in range(10) for e in range(10)]
    while quality<threshold:
        squares = split_into_squares(imgarray,size)
        compimg = []
        datarray = []
        testblock = [gen_block(c,s,e,size) for c in range(4) for s in range(10) for e in range(10)]
        for row in squares:
            comprow = []
            datrow=[]
            for square in row:
                quality_values = [calc_quality(square,block) for block in testblock]
                best_quality = quality_values.index(max(quality_values))
                comprow.append(testblock[best_quality])
                datrow.append(testdata[best_quality])
            compimg.append(comprow)
            datarray.append(datrow)
        compimg = fuse_squares(compimg)
        quality = calc_quality(imgarray,compimg)
        print("Size:{} Quality:{}".format(size,quality))
        size-=1
    return brightness_to_image(compimg),datarray,size+1

template = '''def s(d,s,e,z):
 x=range(z)
 return d<1 and[[int((z-r)*(e-s)/z+s)for c in x]for r in x]or d==1 and[[int((z-c)*(e-s)/z+s)for c in x]for r in x]or d==2 and[[int((2*z-r-c)/2*(e-s)/z+s)for c in x]for r in x]or d>2 and[[int((z-r+c)/2*(e-s)/z+s)for c in x] for r in x]
i=lambda a:'\\n'.join([''.join(map(lambda r:" `.,:;'+#@"[r],l))for l in a])
def f(a):
 o=[];c=0;s=0;r=0
 while r<len(a):
  if s<len(a[r][0]):
   o.append([])
   for q in a[r]:
    o[c].extend(q[s])
   s+=1;c+=1
  else:
   s=0;r+=1
 return o
t={};z={}
print(i(f([[s(D>>8,(D>>4)&15,D&15,z)for D in R]for R in t])))'''

template_size_1 = '''print("""{}""")'''   

def main(filename,threshold):
    print(filename+" "+str(threshold))
    file = open(filename,'r')
    compimg,datarray,size = main_calc(file.read(),threshold)
    file.close()
    textoutput = open(filename.split(".")[0]+"-"+str(threshold*100)+".txt",'w')
    textoutput.write(compimg)
    textoutput.close()
    compoutput = open(filename.split(".")[0]+"-"+str(threshold*100)+".py",'w')
    datarray = str(datarray).replace(" ","")
    code = ""
    if size==1:
        code = template_size_1.format(compimg)
    else:
        code= template.format(datarray,str(size))
    compoutput.write(code)
    compoutput.close()
    print("done")

if __name__ == "__main__":
    main(sys.argv[1],float(sys.argv[2]))

Esta resposta pode usar muito melhorias, então provavelmente estarei trabalhando nisso mais no fim de semana.

Como isso funciona:

  • Divida a imagem em blocos de tamanho size .
  • Encontre o melhor bloco correspondente
    • Blocos podem ter gradiente agora!
  • Calcular a qualidade (de acordo com a fórmula) para a imagem inteira.
  • Se correto, escreva a imagem compactada no arquivo.
  • Caso contrário, diminua sizee tente novamente.

Esse algoritmo funciona bem para baixa qualidade (0,5, 0,6), mas não funciona muito bem nas imagens de alta qualidade (na verdade, são infladas). Também é muito lento.

Aqui eu tenho todos os arquivos gerados, para que você não precise gerá-los novamente.

Azul
fonte
Finalmente, uma resposta! No entanto, é tecnicamente não-competitivo, desde que criei o Bubblegum depois de postar esse desafio ... Executarei o script de pontuação mais tarde e talvez o leve para um idioma menos esotérico.
Dennis
@ Dennis Ah, bem, não deve ser muito difícil portar a saída para um script python. Obrigado pelo heads-up
Blue
Acabei de reler o meu desafio (depois de um ano, fiquei um pouco confuso com os detalhes), e diz: Seu compressor pode usar compressores de fluxo de bytes embutidos (por exemplo, gzip), mas você deve implementá-los você mesmo. imagens compactadas. Isso significa que Bubblegum está fora de qualquer maneira.
Dennis
Finalmente me lembrei de ter prometido marcar isso; Desculpe o atraso. Seu código parece ter um erro de digitação ( compingdeveria ser compimg), que eu corrigi para executar o programa. A menos que eu tenha cometido um erro ao executar seu código, as dimensões de algumas das imagens geradas estão incorretas (por exemplo, image2.txtpossui 33.164 bytes, mas image2-50.0.txtpossui 33.329) e outras não geram o mesmo arquivo ao executar os programas gerados ( image3-50.0.txttem uma qualidade de 0,5110 , mas a execução do programa gerado resulta em uma qualidade de 0,4508 ).
Dennis
Adendo: baixei image3-50.0.pydo seu Dropbox e ele corresponde ao arquivo que eu gerei.
Dennis