有损ASCII艺术压缩


21

背景

PICASCII是一种灵巧的工具,可将图像转换为ASCII艺术作品。

通过使用以下十个ASCII字符,可以实现不同程度的亮度:

@#+';:,.` 

我们将说这些像素(字符元素)的亮度从1(符号)到10(空格)。

在下面,您可以看到以正确的字体显示的一些代码转换,威尔士旗帜,分形,大鳟鱼和高尔夫球的结果:

ASCII艺术

您可以看到此小提琴中的图像,然后从Google云端硬盘下载它们。

任务

尽管PICASCII的最终结果在视觉上令人愉悦,但所有五个图像的总重为153,559字节。如果我们愿意牺牲部分图像质量,这些图像将被压缩多少?

您的任务是编写一个程序,该程序接受上述格式的ASCII艺术图像并以最低质量作为输入,并以完整程序或返回单个字符串的函数的形式打印图像的有损压缩,该压缩应满足以下要求:质量要求。

这意味着您不必编写单独的解压缩器;它必须内置在每个压缩图像中。

原始图像将由亮度介于1到10之间的像素像素组成,并通过换行分隔成相同长度的行。压缩的图像必须具有相同的尺寸并使用相同的字符集。

对于包含n个像素的未压缩图像,图像的压缩版本的质量定义为

质量公式

其中Ç 是的亮度和压缩后的图像的输出的charxel ü 的亮度未压缩图像的charxel。

计分

您的代码将以上方的五个图像作为输入,每个图像的最低质量设置分别为0.50、0.60、0.70、0.80和0.90。

您的分数是所有压缩图像大小的几何平均值,即所有25个压缩图像的长度乘积的第二十五根。

最低分获胜!

附加规则

  • 您的代码必须适用于任意图像,而不仅仅是用于评​​分的图像。

    期望您针对测试用例优化代码,但是什至不尝试压缩任意图像的程序也不会被我接受。

  • 您的压缩器可能使用内置的字节流压缩器(例如gzip),但是您必须自己为压缩图像实现它们。

    允许在字节流解压缩器中正常使用的Bulit-ins(例如,基本转换,行程解码)。

  • 压缩器和压缩图像不必使用相同的语言。

    但是,您必须为所有压缩图像选择一种语言。

  • 对于每个压缩图像,均适用标准代码高尔夫规则。

验证

我制作了一个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,那么应该用输出一半亮度的char替换输入文件中的每个char,对吗?显然排除空格,因为那样会弄乱整个图像。
尼古拉斯Siplis

1
它太令人困惑和漏洞。如何停止mattmahoney.net/dc/barf.html条目?解压缩器也可以读取压缩图像以外的任何文件吗?您能否提供python脚本或某种可以实际检查图像质量并计算分数的东西,以便在该方面也不会出现任何疑问?等等
威尔

1
@会感到困惑吗?也许。但是我不认为这是漏洞。每个压缩的图像都必须是一个程序或功能,因此会自动排除诸如BARF之类的有趣笑话。我不了解Python,但我会想到一些简单的验证方法。
丹尼斯

8
“我制作了一个CJam脚本,可以轻松地验证所有质量要求并计算提交的分数。” 人们真的使用这个东西来制作普通脚本吗?尊敬的主...
致命

Answers:


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 Compress没有找到CJam类。
丹尼斯

@Dennis您需要将包含Compress.class的目录添加到类路径(-cp)。如果它在当前目录中,请使用-cp .:cjam-0.6.5.jar(在windoze中,我认为您需要用分号代替冒号)
aditsu

做到了,谢谢。
丹尼斯

2

Python 3.5(主要和输出)(当前处于非竞争状态)

生日快乐,挑战!这是您的礼物:答案!

编辑:将输出转换为python代码,改善了压缩率(略)编辑2:在size为1 时将其打印为原始图像。改进了分数,但需要再次计算分数。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
  • 查找最佳匹配块
    • 方块现在可以渐变了!
  • 计算整个图像的质量(根据公式)。
  • 如果正确,则将压缩图像写入文件。
  • 否则,请递减并重size试。

该算法适用于低质量(0.5、0.6),但不适用于较高质量的图像(实际上是膨胀的)。这也真的很慢。

这里有所有生成的文件,因此您无需再次重新生成它们。


最后一个答案!但是,从技术上讲,这是没有竞争的,因为我在发布此挑战后创建了Bubblegum ...我将稍后运行计分脚本,并可能将其移植到较不深奥的语言中。
丹尼斯

@Dennis Ah,将输出移植到python脚本应该不难。感谢您的单挑
Blue Blue

我只是重新阅读了我的挑战(一年后,我在细节上有点模糊),它说您的压缩器可能使用内置的字节流压缩器(例如gzip),但是您必须自己实现它们压缩图像。这意味着Bubblegum仍然存在。
丹尼斯

我终于想起我曾经答应打分。抱歉耽搁了。您的代码似乎有一个错字(comping应该是compimg),我已修复该错字来运行程序。除非我在运行代码时犯了错误,否则某些生成的图像的尺寸是不正确的(例如,image2.txt具有33,164字节,但image2-50.0.txt具有33,329 字节),而其他图像在运行生成的程序时不会生成相同的文件(image3-50.0.txt质量为0.5110),但运行生成的程序的质量为0.4508)。
丹尼斯

附录:我是image3-50.0.py从您的Dropbox 下载的,它与我生成的文件匹配。
丹尼斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.