抖动灰度图像


23

使用您自己的算法将灰度图像抖动为纯黑白图像。

准则:您必须提出自己的新算法。您不能使用预先存在的算法(例如Floyd-Steinburg),但可以使用常规技术。您的程序必须能够读取图像并产生相同大小的图像。这是一场人气竞赛,因此谁能产生最好的(最接近原始的)和最具创造力的(由投票决定)的胜利。如果代码简短,则可以加分,尽管这不是必需的。

您可以使用任何想要的灰度图像作为输入,它应该大于300x300。任何文件格式都可以。

输入示例:

小狗

输出示例:

抖动的

这是一项很好的工作,但是仍然有可见的线条和图案。


4
+1是一个有趣的挑战,但是我认为这可以作为[code-golf](带有规范)或其他一些完全客观的标准而更好。
门把手

2
代码大小,速度和内存使用情况的问题在于,您需要一个客观的阈值,以使结果必须是可识别的,才能使答案有效,这也是非常不可能的。普及竞赛确实是有道理的,但是对代码没有任何限制,人们没有动力去思考。我宁愿投票一个聪明的答案,也不愿给出最好的结果,因为它只是实现了现有的算法。但是您目前正在激励后者。
马丁·恩德

3
算法及其技术之间的界限太窄,无法确定某物掉到哪一边。
彼得·泰勒

2
我认为如果所有结果都显示同一图像的结果,则比较结果会容易得多。
joeytwiddle

3
您可以添加图片来源吗?(我不认为有人会生气在这里看到它的他/她的形象,但它是公平地注明来源)
AL

Answers:


16

Fortran

好的,我使用的是称为FITS的模糊图像格式,用于天文学。这意味着有一个用于读取和写入此类图像的Fortran库。另外,ImageMagick和Gimp都可以读取/写入FITS图像。

我使用的算法基于“ Sierra Lite”抖动,但有两个改进:
a)我将传播的误差减少了4/5。
b)我在扩散矩阵中引入随机变化,同时保持其总和不变。
这些几乎一起完全消除了OP示例中的模式。

假设您已安装CFITSIO库,请使用

gfortran -lcfitsio抖动.f90

文件名是硬编码的(无法解决此问题)。

码:

program dither
  integer              :: status,unit,readwrite,blocksize,naxes(2),nfound
  integer              :: group,npixels,bitpix,naxis,i,j,fpixel,un
  real                 :: nullval,diff_mat(3,2),perr
  real, allocatable    :: image(:,:), error(:,:)
  integer, allocatable :: seed(:)
  logical              :: anynull,simple,extend
  character(len=80)    :: filename

  call random_seed(size=Nrand)
  allocate(seed(Nrand))
  open(newunit=un,file="/dev/urandom",access="stream",&
       form="unformatted",action="read",status="old")
  read(un) seed
  close(un)
  call random_seed(put=seed)
  deallocate(seed)

  status=0
  call ftgiou(unit,status)
  filename='PUPPY.FITS'
  readwrite=0
  call ftopen(unit,filename,readwrite,blocksize,status)
  call ftgknj(unit,'NAXIS',1,2,naxes,nfound,status)
  call ftgidt(unit,bitpix,status)
  npixels=naxes(1)*naxes(2)
  group=1
  nullval=-999
  allocate(image(naxes(1),naxes(2)))
  allocate(error(naxes(1)+1,naxes(2)+1))
  call ftgpve(unit,group,1,npixels,nullval,image,anynull,status)
  call ftclos(unit, status)
  call ftfiou(unit, status)

  diff_mat=0.0
  diff_mat(3,1) = 2.0 
  diff_mat(1,2) = 1.0
  diff_mat(2,2) = 1.0
  diff_mat=diff_mat/5.0

  error=0.0
  perr=0
  do j=1,naxes(2)
    do i=1,naxes(1)
      p=max(min(image(i,j)+error(i,j),255.0),0.0)
      if (p < 127.0) then
        perr=p
        image(i,j)=0.0
      else
        perr=p-255.0
        image(i,j)=255.0
      endif
      call random_number(r)
      r=0.6*(r-0.5)
      error(i+1,j)=  error(i+1,j)  +perr*(diff_mat(3,1)+r)
      error(i-1,j+1)=error(i-1,j+1)+perr*diff_mat(1,2)
      error(i  ,j+1)=error(i ,j+1) +perr*(diff_mat(2,2)-r)
    end do
  end do

  call ftgiou(unit,status)
  blocksize=1
  filename='PUPPY-OUT.FITS'
  call ftinit(unit,filename,blocksize,status)
  simple=.true.
  naxis=2
  extend=.true.
  call ftphpr(unit,simple,bitpix,naxis,naxes,0,1,extend,status)
  group=1
  fpixel=1
  call ftppre(unit,group,fpixel,npixels,image,status)
  call ftclos(unit, status)
  call ftfiou(unit, status)

  deallocate(image)
  deallocate(error)
end program dither

OP帖子中小狗图像的
Dithered picture of puppy
示例输出:OP示例输出:
OPs dithered picture of puppy


这看起来非常好,可能是无与伦比的质量
aditsu

谢谢!我不知道它是无与伦比的,但是很难(非常主观)相对于其他好的算法进行判断。
2014年

1
我知道我通过滥用向后兼容性来学习高尔夫代码,但实际上看起来您是在滥用它。这段代码实际上让我哭了。
Kyle Kanos 2014年

@KyleKanos当我的代码使某人哭泣时,我总是很高兴:p在主题上,这里到底有什么恐怖呢?是的,我本可以使用“隐式无”,但是这样做的乐趣在哪里?我确实在工作中使用它进行认真的编码,但不是在打高尔夫球。而且我绝对同意CFITSIO库API完全可怕(ftppre()输出单精度实数的FITS图像,ftpprj()输出双精度数实的图像,等等),但是F77向后兼容。
2014年

1
好吧,所以其中大多数只是我草率。我改善了。建设性的批评总是受到赞赏:)
半外在的

34

GraphicsMagick / ImageMagick

订购的抖动:

magick B2DBy.jpg -gamma .45455 -ordered-dither [all] 4x4 ordered4x4g45.pbm

在抱怨我使用“已建立的算法”之前,请阅读2003年4月的GraphicsMagick和ImageMagick的ChangeLog,您会发现我在这些应用程序中实现了该算法。同样,“-gamma .45455”与“有序抖动”的组合是新的。

“ -gamma .45455”可确保图像太亮。只有GraphicsMagick才需要“ all”参数。

之所以会有条带,是因为4x4有序抖动图像中只有17个灰度级。通过使用具有65个电平的8x8有序抖动,可以减少出现带状的现象。

这是原始图像,4x4和8x8有序抖动输出和随机阈值输出: 在此处输入图片说明

我更喜欢有序抖动版本,但为了完整性起见,我还包括了随机阈值版本。

magick B2DBy.jpg -gamma .6 -random-threshold 10x90% random-threshold.pbm

“ 10x90%”表示将强度低于10%的像素渲染为纯黑色,将强度高于90%的像素渲染为纯白色,以避免在这些区域中出现一些孤独的斑点。

可能值得一提的是,两者都尽可能地提高了内存效率。也不进行任何扩散,因此即使编写有序抖动块,它们也一次工作一个像素,并且不需要了解任何有关相邻像素的信息。ImageMagick和GraphicsMagick一次处理一行,但是这些方法不是必需的。在我的旧x86_64计算机上,有序抖动转换的实时时间不到0.04秒。


31
“在抱怨我使用“已建立的算法”之前,请阅读2003年4月的GraphicsMagick和ImageMagick的ChangeLog,您会发现我在那些应用程序中实现了该算法。” +1为纯粹的脸颊。
Joe Z.

22

我为代码样式表示歉意,我使用我们刚刚在java类中构建的一些库将其整合在一起,并且它存在复制粘贴和幻数的不良情况。该算法选择图像中的随机矩形,并检查抖动图像或原始图像中的平均亮度是否更大。然后,它打开或关闭像素以使亮度更接近一致,优先选择与原始图像差异更大的像素。我认为这样做可以更好地表现出像小狗的头发这样的细小细节,但是图像噪声更大,因为即使在没有毛发的地方,它也会尝试表现出细节。

在此处输入图片说明

public void dither(){
    int count = 0;
    ditheredFrame.suspendNotifications();
    while(count < 1000000){
        count ++;
        int widthw = 5+r.nextInt(5);
        int heightw = widthw;
        int minx = r.nextInt(width-widthw);
        int miny = r.nextInt(height-heightw);



            Frame targetCropped = targetFrame.crop(minx, miny, widthw, heightw);
            Frame ditherCropped = ditheredFrame.crop(minx, miny, widthw, heightw);

            if(targetCropped.getAverage().getBrightness() > ditherCropped.getAverage().getBrightness() ){
                int x = 0;
                int y = 0;
                double diff = 0;

                for(int i = 1; i < ditherCropped.getWidth()-1; i ++){
                    for(int j = 1; j < ditherCropped.getHeight()-1; j ++){
                        double d = targetCropped.getPixel(i,  j).getBrightness() - ditherCropped.getPixel(i, j).getBrightness();
                        d += .005* targetCropped.getPixel(i+1,  j).getBrightness() - .005*ditherCropped.getPixel(i+1, j).getBrightness();

                        d += .005* targetCropped.getPixel(i-1,  j).getBrightness() - .005*ditherCropped.getPixel(i-1, j).getBrightness();

                        d += .005* targetCropped.getPixel(i,  j+1).getBrightness() -.005* ditherCropped.getPixel(i, j+1).getBrightness();

                        d += .005* targetCropped.getPixel(i,  j-1).getBrightness() - .005*ditherCropped.getPixel(i, j-1).getBrightness();

                        if(d > diff){
                            diff = d;
                            x = i;
                            y = j;
                        }
                    }
                    ditherCropped.setPixel(x,  y,  WHITE);
                }

            } else {
                int x = 0;
                int y = 0;
                double diff = 0;

                for(int i = 1; i < ditherCropped.getWidth()-1; i ++){
                    for(int j = 1; j < ditherCropped.getHeight()-1; j ++){
                        double d =  ditherCropped.getPixel(i, j).getBrightness() -targetCropped.getPixel(i,  j).getBrightness();
                        d += -.005* targetCropped.getPixel(i+1,  j).getBrightness() +.005* ditherCropped.getPixel(i+1, j).getBrightness();

                        d += -.005* targetCropped.getPixel(i-1,  j).getBrightness() +.005* ditherCropped.getPixel(i+1, j).getBrightness();

                        d += -.005* targetCropped.getPixel(i,  j+1).getBrightness() + .005*ditherCropped.getPixel(i, j+1).getBrightness();

                        d += -.005* targetCropped.getPixel(i,  j-1).getBrightness() + .005*ditherCropped.getPixel(i, j-1).getBrightness();



                        if(d > diff){
                            diff = d;
                            x = i;
                            y = j;
                        }
                    }
                    ditherCropped.setPixel(x,  y,  BLACK);
                }
            }


    }
    ditheredFrame.resumeNotifications();
}

我认为这是确定性的?如果是这样,速度有多快?
2014年

这是随机的,在我的计算机上大约需要3秒钟。
QuadmasterXLII

2
虽然也许不是最大保真度算法,但结果本身就是艺术。
AJMansfield 2014年

4
我真的很喜欢这种算法的外观!但是我认为它看起来如此好看,部分原因是它产生的纹理近似于皮毛,而这是一种有皮毛的动物。但是我不确定这是真的。您可以张贴其他图片,例如汽车吗?
2014年

1
无论是从算法的原创性还是从结果的出色程度来看,我认为这都是最佳答案。我也非常希望看到它也可以在其他图像上运行。
纳撒尼尔(Nathaniel)2014年

13

Ghostscript(在ImageMagick的帮助下)

远非我自己的“新算法”,但抱歉,无法抗拒它。

convert puppy.jpg puppy.pdf && \
convert puppy.jpg -depth 8 -colorspace Gray -resize 20x20! -negate gray:- | \
gs -q -sDEVICE=ps2write -o- -c \
    '<</Thresholds (%stdin) (r) file 400 string readstring pop 
       /HalftoneType 3 /Width 20 /Height 20
     >>sethalftone' \
    -f puppy.pdf | \
gs -q -sDEVICE=pngmono -o puppy.png -

在此处输入图片说明

当然,它在没有“相同大小”约束的情况下效果更好。


2
这真可笑。没有人对此沃霍尔风格的奇迹发表评论,这让我感到惊讶。
安德烈·科斯蒂卡(AndreïKostyrka)

10

爪哇

这是我的意见书。拍摄JPG图像,计算每个像素的像素亮度(由于这个 SO问题中的Bonan ),然后对照随机模式进行检查,以了解生成的像素是黑色还是白色。较暗的像素将始终为黑色,最亮的像素将始终为白色,以保留图像细节。

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Random;

import javax.imageio.ImageIO;

public class DitherGrayscale {

    private BufferedImage original;
    private double[] threshold = { 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31,
            0.32, 0.33, 0.34, 0.35, 0.36, 0.37, 0.38, 0.39, 0.4, 0.41, 0.42,
            0.43, 0.44, 0.45, 0.46, 0.47, 0.48, 0.49, 0.5, 0.51, 0.52, 0.53,
            0.54, 0.55, 0.56, 0.57, 0.58, 0.59, 0.6, 0.61, 0.62, 0.63, 0.64,
            0.65, 0.66, 0.67, 0.68, 0.69 };


    public static void main(String[] args) {
        DitherGrayscale d = new DitherGrayscale();
        d.readOriginal();
        d.dither();

    }

    private void readOriginal() {
        File f = new File("original.jpg");
        try {
            original = ImageIO.read(f);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void dither() {
        BufferedImage imRes = new BufferedImage(original.getWidth(),
                original.getHeight(), BufferedImage.TYPE_INT_RGB);
        Random rn = new Random();
        for (int i = 0; i < original.getWidth(); i++) {
            for (int j = 0; j < original.getHeight(); j++) {

                int color = original.getRGB(i, j);

                int red = (color >>> 16) & 0xFF;
                int green = (color >>> 8) & 0xFF;
                int blue = (color >>> 0) & 0xFF;

                double lum = (red * 0.21f + green * 0.71f + blue * 0.07f) / 255;

                if (lum <= threshold[rn.nextInt(threshold.length)]) {
                    imRes.setRGB(i, j, 0x000000);
                } else {
                    imRes.setRGB(i, j, 0xFFFFFF);
                }

            }
        }
        try {
            ImageIO.write(imRes, "PNG", new File("result.png"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

处理的图像

其他例子:

原版的 处理

也适用于全彩色图像:

彩色图像 结果


9

贾姆

lNl_~:H;:W;Nl;1N[{[{ri}W*]{2/{:+}%}:P~}H*]z{P}%:A;H{W{I2/A=J2/=210/[0X9EF]=I2%2*J2%+m>2%N}fI}fJ

95个字节:) 对于输入和输出,
它都使用ASCII PGM(P2)格式,没有注释行。

该方法非常基础:将2 * 2像素的平方相加,转换为0..4范围,然后使用4位的对应模式生成2 * 2黑白像素。
这也意味着宽度和高度必须相等。

样品:

确定性的小狗

仅有27个字节的随机算法:

lNl_~*:X;Nl;1N{ri256mr>N}X*

它使用相同的文件格式。

样品:

随机的小狗

最后是一种混合方法:随机抖动,偏向棋盘格模式;44个字节:

lNl_~:H;:W;Nl;1NH{W{ri128_mr\IJ+2%*+>N}fI}fJ

样品:

混合的小狗


2
第一个与Nintendo DSi的“ Flipnote Studio”应用程序相当。
BobTheAwesome

6

Java(1.4+)

我不确定我是否在这里重新发明轮子,但我认为它可能是独一无二的...

随机序列有限

随机序列有限

纯随机抖动

纯随机抖动

在此处输入图片说明

来自Averroes答案的城市形象

该算法使用局部发光能量和归一化以保留特征的概念。然后,初始版本使用随机抖动在类似亮度的区域上产生抖动外观。但是,它在视觉上并不那么吸引人。为了解决这个问题,将有限的一组有限的随机序列映射到原始输入像素的亮度,并反复使用样本并反复产生抖动的背景。

import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;

public class LocalisedEnergyDitherRepeatRandom {

    static private final float EIGHT_BIT_DIVISOR=1.0F/256;
    private static final int KERNEL_SIZE_DIV_2 =4;
    private static final double JITTER_MULTIPLIER=0.01;
    private static final double NO_VARIANCE_JITTER_MULTIPLIER=1.5;
    private static final int RANDOM_SEQUENCE_LENGTH=10;
    private static final int RANDOM_SEQUENCE_COUNT=20;
    private static final boolean USE_RANDOM_SEQUENCES=true;

    public static void main(String args[]) throws Exception
    {
        BufferedImage image = ImageIO.read(new File("data/dog.jpg"));
        final int width = image.getWidth();
        final int height = image.getHeight();
        float[][][] yuvImage =convertToYUVImage(image);
        float[][][] outputYUVImage = new float[width][height][3];
        double circularKernelLumaMean,sum,jitter,outputLuma,variance,inputLuma;
        int circularKernelPixelCount,y,x,kx,ky;
        double[][] randomSequences = new double[RANDOM_SEQUENCE_COUNT][RANDOM_SEQUENCE_LENGTH];
        int[] randomSequenceOffsets = new int[RANDOM_SEQUENCE_COUNT];

        // Generate random sequences
        for (y=0;y<RANDOM_SEQUENCE_COUNT;y++)
        {
            for (x=0;x<RANDOM_SEQUENCE_LENGTH;x++)
            {
                randomSequences[y][x]=Math.random();
            }
        }

        for (y=0;y<height;y++)
        {
            for (x=0;x<width;x++)
            {
                circularKernelLumaMean=0;
                sum=0;
                circularKernelPixelCount=0;

                /// calculate the mean of the localised surrounding pixels contained in 
                /// the area of a circle surrounding the pixel.
                for (ky=y-KERNEL_SIZE_DIV_2;ky<y+KERNEL_SIZE_DIV_2;ky++)
                {
                    if (ky>=0 && ky<height)
                    {
                        for (kx=x-KERNEL_SIZE_DIV_2;kx<x+KERNEL_SIZE_DIV_2;kx++)
                        {
                            if (kx>=0 && kx<width)
                            {
                                double distance= Math.sqrt((x-kx)*(x-kx)+(y-ky)*(y-ky));

                                if (distance<=KERNEL_SIZE_DIV_2)
                                {
                                    sum+=yuvImage[kx][ky][0];
                                    circularKernelPixelCount++;
                                }
                            }
                        }
                    }
                }

                circularKernelLumaMean=sum/circularKernelPixelCount;

                /// calculate the variance of the localised surrounding pixels contained in 
                /// the area of a circle surrounding the pixel.
                sum =0;

                for (ky=y-KERNEL_SIZE_DIV_2;ky<y+KERNEL_SIZE_DIV_2;ky++)
                {
                    if (ky>=0 && ky<height)
                    {
                        for (kx=x-KERNEL_SIZE_DIV_2;kx<x+KERNEL_SIZE_DIV_2;kx++)
                        {
                            if (kx>=0 && kx<width)
                            {
                                double distance= Math.sqrt((x-kx)*(x-kx)+(y-ky)*(y-ky));

                                if (distance<=KERNEL_SIZE_DIV_2)
                                {
                                    sum+=Math.abs(yuvImage[kx][ky][0]-circularKernelLumaMean);
                                }
                            }
                        }
                    }
                }

                variance = sum/(circularKernelPixelCount-1);

                if (variance==0)
                {
                    // apply some jitter to ensure there are no large blocks of single colour
                    inputLuma=yuvImage[x][y][0];
                    jitter = Math.random()*NO_VARIANCE_JITTER_MULTIPLIER;
                }
                else
                {
                    // normalise the pixel based on localised circular kernel
                    inputLuma = outputYUVImage[x][y][0]=(float) Math.min(1.0, Math.max(0,yuvImage[x][y][0]/(circularKernelLumaMean*2)));
                    jitter = Math.exp(variance)*JITTER_MULTIPLIER;
                }

                if (USE_RANDOM_SEQUENCES)
                {
                    int sequenceIndex =(int) (yuvImage[x][y][0]*RANDOM_SEQUENCE_COUNT);
                    int sequenceOffset = (randomSequenceOffsets[sequenceIndex]++)%RANDOM_SEQUENCE_LENGTH;
                    outputLuma=inputLuma+randomSequences[sequenceIndex][sequenceOffset]*jitter*2-jitter;
                }
                else
                {
                    outputLuma=inputLuma+Math.random()*jitter*2-jitter;
                }


                // convert 8bit luma to 2 bit luma
                outputYUVImage[x][y][0]=outputLuma<0.5?0:1.0f;
            }
        }

        renderYUV(image,outputYUVImage);
        ImageIO.write(image, "png", new File("data/dog.jpg.out.png"));
    }

      /**
       * Converts the given buffered image into a 3-dimensional array of YUV pixels
       * @param image the buffered image to convert
       * @return 3-dimensional array of YUV pixels
       */
      static private float[][][] convertToYUVImage(BufferedImage image)
      {
        final int width = image.getWidth();
        final int height = image.getHeight();
        float[][][] yuv = new float[width][height][3];
        for (int y=0;y<height;y++)
        {
          for (int x=0;x<width;x++)
          {
            int rgb = image.getRGB( x, y );
            yuv[x][y]=rgb2yuv(rgb);
          }
        }
        return yuv;
      }

      /**
       * Renders the given YUV image into the given buffered image.
       * @param image the buffered image to render to
       * @param pixels the YUV image to render.
       * @param dimension the
       */
      static private void renderYUV(BufferedImage image, float[][][] pixels)
      {
        final int height = image.getHeight();
        final int width = image.getWidth();
        int rgb;

        for (int y=0;y<height;y++)
        {
          for (int x=0;x<width;x++)
          {

            rgb = yuv2rgb( pixels[x][y] );
            image.setRGB( x, y,rgb );
          }
        }
      }

      /**
       * Converts a RGB pixel into a YUV pixel
       * @param rgb a pixel encoded as 24 bit RGB
       * @return array representing a pixel. Consisting of Y,U and V components
       */
      static float[] rgb2yuv(int rgb)
      {
        float red = EIGHT_BIT_DIVISOR*((rgb>>16)&0xFF);
        float green = EIGHT_BIT_DIVISOR*((rgb>>8)&0xFF);
        float blue = EIGHT_BIT_DIVISOR*(rgb&0xFF);

        float Y = 0.299F*red + 0.587F * green + 0.114F * blue;
        float U = (blue-Y)*0.565F;
        float V = (red-Y)*0.713F;

        return new float[]{Y,U,V};
      }

      /**
       * Converts a YUV pixel into a RGB pixel
       * @param yuv array representing a pixel. Consisting of Y,U and V components
       * @return a pixel encoded as 24 bit RGB
       */
      static int yuv2rgb(float[] yuv)
      {
        int red = (int) ((yuv[0]+1.403*yuv[2])*256);
        int green = (int) ((yuv[0]-0.344 *yuv[1]- 0.714*yuv[2])*256);
        int blue = (int) ((yuv[0]+1.77*yuv[1])*256);

        // clamp to 0-255 range
        red=red<0?0:red>255?255:red;
        green=green<0?0:green>255?255:green;
        blue=blue<0?0:blue>255?255:blue;

        return (red<<16) | (green<<8) | blue;
      }

}

3
非常好。到目前为止,它肯定会带来与其他答案不同的效果。
Geobits '16

@Geobits是的,这让我感到惊讶。但是,我不确定是否将它称为抖动,因为它会产生视觉上完全不同的输出
-Moogie

那看起来确实很独特。
qwr

5

蟒蛇

想法如下:将图像分成多个n x n图块。我们计算这些瓷砖的平均颜色。然后我们将颜色范围映射0 - 2550 - n*n给我们一个新值的范围v。然后,我们将该图块中的所有像素着色为黑色,并将v该图块中的像素随机着色为白色。它远非最佳,但仍可为我们带来可识别的结果。根据分辨率,通常在n=2或时效果最佳n=3。虽然在其中,n=2您已经可以从'模拟的颜色深度中找到伪像,以防万一n=3它可能已变得有些模糊。我假设图像应该保持相同的大小,但是您当然也可以使用此方法,只是将生成的图像的大小加倍/三倍以获取更多细节。

PS:我知道我参加聚会有点晚了,我记得当挑战开始时我没有任何想法,但是现在才有了脑波=)

from PIL import Image
import random
im = Image.open("dog.jpg") #Can be many different formats.
new = im.copy()
pix = im.load()
newpix = new.load()
width,height=im.size
print([width,height])
print(pix[1,1])

window = 3 # input parameter 'n'

area = window*window
for i in range(width//window):     #loop over pixels
    for j in range(height//window):#loop over pixels
        avg = 0
        area_pix = []
        for k in range(window):
            for l in range(window):
                area_pix.append((k,l))#make a list of coordinates within the tile
                try:
                    avg += pix[window*i+k,window*j+l][0] 
                    newpix[window*i+k,window*j+l] = (0,0,0) #set everything to black
                except IndexError:
                    avg += 255/2 #just an arbitrary mean value (when were outside of the image)
                                # this is just a dirty trick for coping with images that have
                                # sides that are not multiples of window
        avg = avg/area
        # val = v is the number of pixels within the tile that will be turned white
        val = round(avg/255 * (area+0.99) - 0.5)#0.99 due to rounding errors
        assert val<=area,'something went wrong with the val'
        print(val)
        random.shuffle(area_pix) #randomize pixel coordinates
        for m in range(val):
            rel_coords = area_pix.pop()#find random pixel within tile and turn it white
            newpix[window*i+rel_coords[0],window*j+rel_coords[1]] = (255,255,255)

new.save('dog_dithered'+str(window)+'.jpg')

结果:

n=2:

在此处输入图片说明

n=3:

在此处输入图片说明


3

您想要的任何文件格式都可以。

让我们为这个问题定义一个非常紧凑的理论文件格式,因为任何现有的文件格式都有太多的开销来写一个快速答案。

让图像文件的前四个字节分别定义图像的宽度和高度(以像素为单位):

00000001 00101100 00000001 00101100
width: 300 px     height: 300 px

随后是w * h0到255灰度值的字节:

10000101 10100101 10111110 11000110 ... [89,996 more bytes]

然后,我们可以用Python定义一段代码(145字节),以获取该图像并执行以下操作:

import random
f=open("a","rb");g=open("b","wb");g.write(f.read(4))
for i in f.read():g.write('\xff' if ord(i)>random.randint(0,255) else '\x00')

通过返回白色或黑色(其概率等于该像素的灰度值)来“抖动”。


应用于示例图像,它给出了类似以下内容:

抖动的狗

它虽然不太漂亮,但是在预览中按比例缩小时看起来确实很相似,对于只有145字节的Python,我认为您不会变得更好。


你可以分享一个例子吗?我相信这是随机抖动,但结果并不是最干净的。。。不错的个人资料图片
qwr

这确实是随机抖动,现在我以您的示例图片为例。
JoeZ。14年

2
我认为它可能会受益于对比度提升。我不了解python,但我假设random.randint(0,255)会选择一个介于0到255之间的随机数。尝试限制在例如55到200之间,这将强制超出该范围的任何阴影都是纯黑色或白色。使用许多图片,您可以得到一个很好的,醒目的图像,没有抖动,只是一个简单的阈值。(随机+对比度增强将使图像介于当前图像和简单阈值之间。)
Level River St

我认为随机抖动应称为盖革抖动(因为它看起来像盖革计数器的输出)。谁同意?
乔Z。14年

1
这几乎就是ImageMagick和GraphicsMagick对我几年前与“ ordered dither”(添加到我的答案)中添加的“ -random-threshold”选项所做的事情。同样,碰撞伽玛有助于获得正确的强度。我同意“盖革抖动”的建议。
Glenn Randers-Pehrson,2014年

3

眼镜蛇

接受24位或32位PNG / BMP文件(JPG产生的输出中带有一些灰色)。它也可以扩展到包含颜色的文件。

它使用速度优化的ELA将图像抖动为3位颜色,当给定测试图像时,它将返回为黑白。

我是否提到过它真的很快?

use System.Drawing
use System.Drawing.Imaging
use System.Runtime.InteropServices
use System.IO

class BW_Dither

    var path as String = Directory.getCurrentDirectory to !
    var rng = Random()

    def main
        file as String = Console.readLine to !
        image as Bitmap = Bitmap(.path+"\\"+file)
        image = .dither(image)
        image.save(.path+"\\test\\image.png")

    def dither(image as Bitmap) as Bitmap

        output as Bitmap = Bitmap(image)

        layers as Bitmap[] = @[ Bitmap(image), Bitmap(image), Bitmap(image),
                                Bitmap(image), Bitmap(image), Bitmap(image),
                                Bitmap(image)]

        layers[0].rotateFlip(RotateFlipType.RotateNoneFlipX)
        layers[1].rotateFlip(RotateFlipType.RotateNoneFlipY)
        layers[2].rotateFlip(RotateFlipType.Rotate90FlipX)
        layers[3].rotateFlip(RotateFlipType.Rotate90FlipY)
        layers[4].rotateFlip(RotateFlipType.Rotate90FlipNone)
        layers[5].rotateFlip(RotateFlipType.Rotate180FlipNone)
        layers[6].rotateFlip(RotateFlipType.Rotate270FlipNone)

        for i in layers.length, layers[i] = .dither_ela(layers[i])

        layers[0].rotateFlip(RotateFlipType.RotateNoneFlipX)
        layers[1].rotateFlip(RotateFlipType.RotateNoneFlipY)
        layers[2].rotateFlip(RotateFlipType.Rotate270FlipY)
        layers[3].rotateFlip(RotateFlipType.Rotate270FlipX)
        layers[4].rotateFlip(RotateFlipType.Rotate270FlipNone)
        layers[5].rotateFlip(RotateFlipType.Rotate180FlipNone)
        layers[6].rotateFlip(RotateFlipType.Rotate90FlipNone)

        vals = List<of List<of uint8[]>>()
        data as List<of uint8[]> = .getData(output)
        for l in layers, vals.add(.getData(l))
        for i in data.count, for c in 3
            x as int = 0
            for n in vals.count, if vals[n][i][c] == 255, x += 1
            if x > 3.5, data[i][c] = 255 to uint8
            if x < 3.5, data[i][c] = 0 to uint8

        .setData(output, data)
        return output

    def dither_ela(image as Bitmap) as Bitmap

        error as decimal[] = @[0d, 0d, 0d]
        rectangle as Rectangle = Rectangle(0, 0, image.width, image.height)
        image_data as BitmapData = image.lockBits(rectangle, ImageLockMode.ReadWrite, image.pixelFormat) to !
        pointer as IntPtr = image_data.scan0
        bytes as uint8[] = uint8[](image_data.stride * image.height)
        pfs as int = Image.getPixelFormatSize(image.pixelFormat) // 8
        Marshal.copy(pointer, bytes, 0, image_data.stride * image.height)

        for y as int in image.height, for x as int in image.width
            position as int = (y * image_data.stride) + (x * pfs)
            for i in 3
                error[i] -= bytes[position + i]
                if Math.abs(error[i] + 255 - bytes[position + i]) < Math.abs(error[i] - bytes[position + i])
                    bytes[position + i] = 255 to uint8
                    error[i] += 255
                else, bytes[position + i] = 0 to uint8

        Marshal.copy(bytes, 0, pointer, image_data.stride * image.height)
        image.unlockBits(image_data)
        return image

    def getData(image as Bitmap) as List<of uint8[]>

        rectangle as Rectangle = Rectangle(0, 0, image.width, image.height)
        image_data as BitmapData = image.lockBits(rectangle, ImageLockMode.ReadOnly, image.pixelFormat) to !
        pointer as IntPtr = image_data.scan0
        bytes as uint8[] = uint8[](image_data.stride * image.height)
        pixels as List<of uint8[]> = List<of uint8[]>()
        for i in image.width * image.height, pixels.add(nil)
        pfs as int = Image.getPixelFormatSize(image.pixelFormat) // 8
        Marshal.copy(pointer, bytes, 0, image_data.stride * image.height)

        count as int = 0
        for y as int in image.height, for x as int in image.width
            position as int = (y * image_data.stride) + (x * pfs)
            if pfs == 4, alpha as uint8 = bytes[position + 3]
            else, alpha as uint8 = 255
            pixels[count] = @[
                                bytes[position + 2], #red
                                bytes[position + 1], #green
                                bytes[position],     #blue
                                alpha]               #alpha
            count += 1

        image.unlockBits(image_data)
        return pixels

    def setData(image as Bitmap, pixels as List<of uint8[]>)
        if pixels.count <> image.width * image.height, throw Exception()
        rectangle as Rectangle = Rectangle(0, 0, image.width, image.height)
        image_data as BitmapData = image.lockBits(rectangle, ImageLockMode.ReadWrite, image.pixelFormat) to !
        pointer as IntPtr = image_data.scan0
        bytes as uint8[] = uint8[](image_data.stride * image.height)
        pfs as int = Image.getPixelFormatSize(image.pixelFormat) // 8
        Marshal.copy(pointer, bytes, 0, image_data.stride * image.height)

        count as int = 0
        for y as int in image.height, for x as int in image.width
            position as int = (y * image_data.stride) + (x * pfs)
            if pfs == 4, bytes[position + 3] = pixels[count][3] #alpha
            bytes[position + 2] = pixels[count][0]              #red
            bytes[position + 1] = pixels[count][1]              #green
            bytes[position] = pixels[count][2]                  #blue
            count += 1

        Marshal.copy(bytes, 0, pointer, image_data.stride * image.height)
        image.unlockBits(image_data)

狗

树木


为了减少重复,您是否考虑过创建一个临时变量col,并image.setPixel(x,y,col)一直保留到最后?
joeytwiddle 2014年

3
树木的图像是什么?
AJMansfield 2014年

它看起来不错,并提供了使用颜色的示例。
很好2014年

2

爪哇

使用PNGJ和噪声加法加基本扩散的低级代码。此实现需要灰度8位PNG源。

import java.io.File;
import java.util.Random;

import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.ImageLineInt;
import ar.com.hjg.pngj.PngReaderInt;
import ar.com.hjg.pngj.PngWriter;

public class Dither {

    private static void dither1(File file1, File file2) {
        PngReaderInt reader = new PngReaderInt(file1);
        ImageInfo info = reader.imgInfo;
        if( info.bitDepth != 8 && !info.greyscale ) throw new RuntimeException("only 8bits grayscale valid");
        PngWriter writer = new PngWriter(file2, reader.imgInfo);
        ImageLineInt line2 = new ImageLineInt(info);
        int[] y = line2.getScanline();
        int[] ye = new int[info.cols];
        int ex = 0;
        Random rand = new Random();
        while(reader.hasMoreRows()) {
            int[] x = reader.readRowInt().getScanline();
            for( int i = 0; i < info.cols; i++ ) {
                int t = x[i] + ex + ye[i];
                y[i] = t > rand.nextInt(256) ? 255 : 0;
                ex = (t - y[i]) / 2;
                ye[i] = ex / 2;
            }
            writer.writeRow(line2);
        }
        reader.end();
        writer.end();
    }

    public static void main(String[] args) {
        dither1(new File(args[0]), new File(args[1]));
        System.out.println("See output in " + args[1]);
    }

}

(加 如果要尝试,请将此jar到您的构建路径中)。

在此处输入图片说明

另外一个好处是:这在内存使用方面非常高效(它仅存储三行),因此可以用于巨大的图像。


Nitpick:我认为“用于大图像”不是那么重要(您见过大于8 GB的灰度PNG吗?),但是“用于例如嵌入式设备”是一个更为突出的观点。
2014年

我喜欢它,但是它的边​​缘看起来有点模糊,methinks。
BobTheAwesome

1

爪哇

只是一个基于RNG的简单算法,再加上一些用于处理彩色图像的逻辑。有概率b将给定像素设置为白色的,否则将其设置为黑色;其中b是该像素的原始亮度。

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Random;
import java.util.Scanner;

import javax.imageio.ImageIO;


public class Ditherizer {
    public static void main(String[]a) throws IOException{
        Scanner input=new Scanner(System.in);
        Random rand=new Random();
        BufferedImage img=ImageIO.read(new File(input.nextLine()));
        for(int x=0;x<img.getWidth();x++){
            for(int y=0;y<img.getHeight();y++){
                Color c=new Color(img.getRGB(x, y));
                int r=c.getRed(),g=c.getGreen(),b=c.getBlue();
                double bright=(r==g&&g==b)?r:Math.sqrt(0.299*r*r+0.587*g*g+0.114*b*b);
                img.setRGB(x,y,(rand.nextInt(256)>bright?Color.BLACK:Color.WHITE).getRGB());    
            }
        }
        ImageIO.write(img,"jpg",new File(input.nextLine()));
        input.close();
    }
}

这是狗图像的潜在结果:

在此处输入图片说明


你为什么不解释添加到顶部,而不是在那里没有人会读它的底部?我真的很喜欢这个主意=)
虚张声势
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.