Lossy ASCIIアート圧縮


21

バックグラウンド

PICASCIIは、画像をASCIIアートに変換するきちんとしたツールです。

次の10個のASCII文字を使用して、さまざまな明るさを実現します。

@#+';:,.` 

これらの文字(文字要素)の明るさは1(アットマーク)から10(スペース)までです。

以下に、正しいフォントで表示された小さなコード、ウェールズの旗、オーバーハンドフラクタル、大きなマス、小さなゴルフの変換結果を示します。

アスキーアート

このフィドルの画像を表示して、Googleドライブからダウンロードできます。

仕事

PICASCIIの最終結果は視覚的には満足のいくものですが、合計5つの画像の重量は153,559バイトです。品質の一部を犠牲にする場合、これらの画像をどれだけ圧縮できますか?

あなたの仕事は、上記のようなASCIIアートイメージと最低品質を入力として受け入れ、完全なプログラムまたは単一の文字列を返す関数の形式で、イメージの非可逆圧縮を印刷するプログラムを作成することです。品質要件。

これは、別個の解凍プログラムを作成できないことを意味します。各圧縮イメージに組み込まれている必要があります。

元の画像は、1〜10の明るさの文字で構成され、同じ長さの行に改行で区切られます。圧縮されたイメージは同じサイズで、同じ文字セットを使用する必要があります。

n個の charxelsで構成される非圧縮画像の場合、画像の圧縮バージョンの品質は次のように定義されます。

品質フォーミュラ

ここで、cはの輝度であるI 番目の圧縮された画像の出力のcharxel及びU Iの輝度I 番目の非圧縮の画像のcharxel。

得点

コードは、入力として上記の5つの画像を使用して実行され、各画像の最小品質設定は0.50、0.60、0.70、0.80、0.90です。

スコアは、すべての圧縮画像のサイズの幾何平均です。つまり、25個すべての圧縮画像の長さの積の25番目のルートです。

最低スコアが勝ちます!

追加のルール

  • コードは、スコアリングに使用される画像だけでなく、任意の画像に対して機能する必要があります。

    テストケースに向けてコードを最適化することが期待されますが、任意の画像を圧縮しようとさえしないプログラムは、私から賛成を得ることはありません。

  • コンプレッサーは、組み込みのバイトストリームコンプレッサー(gzipなど)を使用する場合がありますが、圧縮イメージ用に自分で実装する必要があります。

    通常、バイトストリームデコンプレッサ(ベース変換、ランレングスデコードなど)で使用されるブリットインが許可されます。

  • 圧縮プログラムと圧縮イメージは同じ言語である必要はありません。

    ただし、すべての圧縮画像に対して単一の言語を選択する必要があります。

  • 各圧縮画像には、標準コードのゴルフ規則が適用されます。

検証

CJamスクリプトを作成して、すべての品質要件を簡単に検証し、提出のスコアを計算しました。

Javaインタープリターは、こちらまたはこちらからダウンロードできます。

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

Bash→PHP、スコア30344.0474

cat

すべての入力に対して100%の品質を実現します。

$ 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

この部分を理解するのに苦労しています:q = 0.5を選択した場合、入力ファイルの各文字は、出力の輝度が半分の文字に置き換えられるはずですよね?空白は画像全体を台無しにするため、明らかに除外します。
ニコラスシプリス

1
それはあまりにも混乱し、抜け穴です。mattmahoney.net/dc/barf.htmlエントリをどのように停止しますか?圧縮解除プログラムは、圧縮されたイメージ以外のファイルも読み取ることができますか?pythonスクリプトや、画像の品質を実際にチェックしてスコアを計算するものを提供して、その面でも問題が発生しないようにできますか?など
ウィル

1
@混乱しますか?多分。しかし、私はそれが抜け穴だとは思わない。各圧縮画像はプログラムまたは機能である必要があるため、BARFのような不愉快なジョークは自動的に除外されます。Pythonについては知りませんが、簡単に検証できるものを考えます。
デニス

8
「CJamスクリプトを作成して、すべての品質要件を簡単に検証し、提出のスコアを計算しました。」人々は本当にこのことを使って通常のスクリプトを実行しますか?親愛なる主…
15

回答:


4

Java→CJam、スコア≈4417.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();
        }
    }
}

クラスパスにCJam jarが必要です。2つのコマンドライン引数(ファイル名と品質)を指定すると、ファイル名に「.cjam」が追加され、そこに圧縮イメージが書き込まれます。それ以外の場合は、現在のディレクトリにあると想定される5つのテストイメージでスコアを計算します。また、プログラムはすべての圧縮画像を自動的に検証します。不一致がある場合は、スコアの計算を再確認することをお勧めします。

(これまでに)使用されている手法は、品質をあまり低下させない場合に半分(水平、垂直、または両方)にスケーリングすること、カスタムコーディングされたRLE、およびベースコンバージョンです。印刷可能なASCII範囲。


これを実行する方法の概要を教えてください。私は(成功し、私は思う)、それをコンパイルしjavac -cp cjam-0.6.5.jar Compress.javaますが、java -cp cjam-0.6.5.jar Compress言うError: Could not find or load main class Compressjava CompressCJamクラスを見つけることができません。
デニス

@Dennis Compress.classを含むディレクトリをクラスパス(-cp)に追加する必要があります。現在のディレクトリにある場合は、-cp .:cjam-0.6.5.jar(windozeではコロンではなくセミコロンが必要だと思います)
aditsu

それはトリックでした、ありがとう。
デニス

2

Python 3.5(メインおよび出力)(現在競合していません)

ハッピーバースデー、チャレンジ!これがあなたのプレゼントです。答えです!

編集:出力をpythonコードに変換し、圧縮率を(わずかに)改善しましたEDIT2: sizeました。EDIT3:@Dennisは私がまだ修正すべきバグを抱えていることを指摘したので、私は答えを非競合としてマークしました

コード:

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]))

この答えはたくさん使うことができますの改善を必要とする可能性があるので、週末にかけてもっと作業するでしょう。

仕組み:

  • 画像をサイズのブロックに分割する size
  • 最適なブロックを見つける
    • ブロックは今勾配を持つことができます!
  • 画像全体の品質を(式に従って)計算します。
  • 正しい場合は、zip形式のイメージをファイルに書き込みます。
  • それ以外の場合は、デクリメントsizeして再試行してください。

このアルゴリズムは低品質(0.5、0.6)ではうまく機能しますが、高品質の画像(実際には膨張する)ではあまり機能しません。また、本当に遅いです。

ここにすべての生成されたファイルがあるので、それらを再生成する必要はありません。


最後に、答え!ただし、このチャレンジを投稿した後にBubblegumを作成したため、技術的に競合していません。スコアリングスクリプトを後で実行し、より難解な言語に移植する可能性があります。
デニス

@Dennisああ、出力をPythonスクリプトに移植するのはそれほど難しくないはずです。ヘッズアップをありがとう
ブルー

私は自分の課題を読み直しました(1年後、詳細が少し曖昧になりました)。コンプレッサーは組み込みのバイトストリームコンプレッサー(gzipなど)を使用する場合がありますが、圧縮画像。それはバブルガムがとにかく出ていることを意味します。
デニス

私はついに、これを獲得することを約束したことを思い出しました。遅れて申し訳ありません。あなたのコードでは、タイプミス(持っていると思われるcompingべきであるcompimg私は、プログラムを実行するために固定しました)、。コードを実行するときに間違えない限り、生成された画像の一部の寸法は正しくなく(たとえば、image2.txt33,164バイトですが、image2-50.0.txt33,329 があります)、他は生成されたプログラムを実行するときに同じファイルを生成しません(image3-50.0.txt品質は0.5110です)、ただし、生成されたプログラムを実行すると、品質は0.4508になります)。
デニス

補遺:image3-50.0.pyDropboxからダウンロードし、生成したファイルと一致しています。
デニス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.