Trama una imagen en escala de grises

23

Trama una imagen en escala de grises en blanco y negro puro con tu propio algoritmo.

Pautas: debe crear su propio algoritmo nuevo. No puede usar algoritmos preexistentes (ej. Floyd-Steinburg) pero puede usar la técnica general. Su programa debe poder leer una imagen y producir una imagen del mismo tamaño. Este es un concurso de popularidad, por lo que el que produce el mejor (más cercano al original) y el más creativo (determinado por los votos) gana. Bonificación si el código es corto, aunque esto no es necesario.

Puede usar cualquier imagen en escala de grises que desee como entrada, debe ser mayor que 300x300. Cualquier formato de archivo está bien.

Entrada de ejemplo:

perrito

Salida de ejemplo:

sacudido

Este es un trabajo bastante bueno, pero todavía hay líneas y patrones visibles.

qwr
fuente
44
+1 para un desafío interesante, pero creo que esto sería mucho mejor como [código-golf] (con una especificación) o algún otro criterio completamente objetivo.
Pomo de la puerta
2
El problema con el tamaño del código, la velocidad y el uso de la memoria es que necesitaría un umbral objetivo de cuán reconocible debe ser el resultado para que la respuesta sea válida, lo que también es bastante imposible. El concurso de popularidad tiene sentido, pero sin ninguna restricción en el código no hay ningún incentivo para que la gente piense fuera de la caja. Prefiero votar una respuesta inteligente que dar el mejor resultado porque solo implementó un algoritmo existente. Pero actualmente estás incentivando a este último.
Martin Ender
3
La línea entre un algoritmo y su técnica es demasiado delgada para determinar de qué lado cae algo.
Peter Taylor
2
Creo que sería mucho más fácil comparar los resultados si todos mostraran resultados de la misma imagen.
joeytwiddle
3
¿Puedes agregar la fuente de la imagen? (No creo que alguien se enoje al ver su imagen aquí, pero es justo citar la fuente)
AL

Respuestas:

16

Fortran

De acuerdo, estoy usando un formato de imagen oscuro llamado FITS que se usa para astronomía. Esto significa que hay una biblioteca Fortran para leer y escribir tales imágenes. Además, ImageMagick y Gimp pueden leer / escribir imágenes FITS.

El algoritmo que uso se basa en el tramado "Sierra Lite", pero con dos mejoras:
a) Reduzco el error propagado en un factor 4/5.
b) Introduzco una variación aleatoria en la matriz de difusión mientras mantengo su suma constante.
Juntos, estos eluyen casi por completo los patrones vistos en el ejemplo de OP.

Suponiendo que tiene instalada la biblioteca CFITSIO, compile con

gfortran -lcfitsio dither.f90

Los nombres de los archivos están codificados (no se pudo molestar en arreglar esto).

Código:

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

Ejemplo de salida para la imagen del cachorro en OPs post:
Imagen borrosa de cachorro
OPs ejemplo de salida:
Operadores de imagen borrosa de cachorro

semi-extrínseco
fuente
Esto se ve muy bien, podría ser inmejorable para la calidad
Aditsu
¡Gracias! No sé si es inmejorable, pero va a ser difícil (muy subjetivo) juzgar esto contra otros buenos algoritmos.
semi-extrínseco
1
Sé que uso el código de golf al abusar de la compatibilidad con versiones anteriores, pero en realidad parece que abusas de él como estándar. Este código en realidad me está haciendo llorar.
Kyle Kanos
@KyleKanos Siempre estoy feliz cuando mi código hace llorar a alguien: p Sin embargo, sobre el tema, ¿qué es específicamente horrible aquí? Sí, podría haber usado "ninguno implícito", pero ¿dónde está la diversión en eso? Lo uso para codificación seria en el trabajo, pero no para jugar al golf. Y definitivamente estoy de acuerdo en que la API de la biblioteca CFITSIO es completamente horrible (ftppre () emite una imagen FITS con precisión real única, ftpprj () emite una imagen con precisión de doble entero, etc.) pero esa es la compatibilidad con F77 para usted.
semi-extrínseco
1
De acuerdo, la mayoría de ellos eran solo yo siendo descuidado. Lo mejoré. La crítica constructiva siempre es apreciada :)
semi-extrínseca
34

GraphicsMagick / ImageMagick

Dither ordenado:

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

Antes de quejarse de que use un "algoritmo establecido", lea el ChangeLog para GraphicsMagick e ImageMagick de abril de 2003, donde verá que implementé el algoritmo en esas aplicaciones. Además, la combinación de "-gamma .45455" con "-order-dither" es nueva.

El "-gamma .45455" se encarga de que la imagen sea demasiado clara. El parámetro "todos" solo es necesario con GraphicsMagick.

Hay bandas porque solo hay 17 niveles de grayle en una imagen de tramado ordenado de 4x4. La aparición de bandas puede reducirse mediante el uso de un tramado ordenado de 8x8 que tiene 65 niveles.

Aquí están la imagen original, la salida diferida ordenada 4x4 y 8x8 y la salida de umbral aleatorio: ingrese la descripción de la imagen aquí

Prefiero la versión de tramado ordenado, pero incluyo la versión de umbral aleatorio para completar.

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

El "10x90%" significa hacer que los píxeles de intensidad por debajo del 10 por ciento sean de color negro puro y por encima del 90 por ciento de color blanco puro, para evitar tener algunas manchas solitarias en esas áreas.

Probablemente valga la pena señalar que ambos son tan eficientes en cuanto a memoria como pueden ser. Tampoco difunde, por lo que trabajan un píxel a la vez, incluso cuando se escriben bloques ordenados de interpolación, y no necesitan saber nada sobre los píxeles vecinos. ImageMagick y GraphicsMagick procesan una fila a la vez, pero no es necesario para estos métodos. Las conversiones de tramado ordenado toman menos de .04 segundos en tiempo real en mi vieja computadora x86_64.

Glenn Randers-Pehrson
fuente
31
"Antes de quejarse de que use un" algoritmo establecido ", lea ChangeLog for GraphicsMagick e ImageMagick para abril de 2003, donde verá que implementé el algoritmo en esas aplicaciones". +1 por pura mejilla.
Joe Z.
22

Pido disculpas por el estilo de código, intente esto usando algunas bibliotecas que acabamos de construir en mi clase de Java, y tiene un mal caso de copiar y pegar números mágicos. El algoritmo selecciona rectángulos aleatorios en la imagen y verifica si el brillo promedio es mayor en la imagen difuminada o en la imagen original. Luego activa o desactiva un píxel para acercar los brillos en línea, eligiendo preferentemente los píxeles que son más diferentes de la imagen original. Creo que hace un mejor trabajo al resaltar detalles finos como el pelo del cachorro, pero la imagen es más ruidosa porque trata de resaltar detalles incluso en áreas sin ninguno.

ingrese la descripción de la imagen aquí

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();
}
QuadmasterXLII
fuente
¿Supongo que esto es determinista? Si es así, ¿qué tan rápido es?
Precioso
Es aleatorio y toma alrededor de 3 segundos en mi computadora.
QuadmasterXLII
2
Aunque quizás no sea el algoritmo de mayor fidelidad, los resultados son arte por sí mismos.
AJMansfield
44
¡Realmente me gusta el aspecto de este algoritmo! Pero creo que quizás se ve tan bien en parte porque produce una textura más o menos similar al pelaje, y este es un animal con pelaje. Pero no estoy completamente seguro de que esto sea cierto. ¿Podría publicar otra imagen de, por ejemplo, un automóvil?
semi-extrínseco
1
Creo que esta es la mejor respuesta, tanto en términos de originalidad del algoritmo como en términos de resultados impresionantes. También me gustaría verlo correr en algunas otras imágenes también.
Nathaniel
13

Ghostscript (con poca ayuda de ImageMagick)

Lejos de ser mi 'nuevo algoritmo', pero, lo siento, no pude resistirlo.

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 -

ingrese la descripción de la imagen aquí

Por supuesto, funciona mejor sin restricciones del 'mismo tamaño'.

usuario2846289
fuente
2
Esto es muy gracioso. Estoy sorprendido por el hecho de que nadie haya comentado sobre esta maravilla de estilo Warhol.
Andreï Kostyrka
10

JAVA

Aquí está mi presentación. Toma una imagen JPG, calcula la luminosidad de píxel por píxel (gracias a Bonan en esta pregunta SO) y luego la compara con un patrón aleatorio para saber si el píxel resultante será blanco o negro. Los píxeles más oscuros siempre serán negros y los píxeles más brillantes siempre serán blancos para preservar los detalles de la imagen.

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();
        }
    }

}

Imagen procesada

Otros ejemplos:

Original Procesada

También funciona con imágenes a todo color:

Imagen en color Resultado

Averroes
fuente
9

CJam

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 bytes :)
Utiliza el formato ASCII PGM (P2) sin línea de comentario, tanto para entrada como para salida.

El método es muy básico: suma cuadrados de 2 * 2 píxeles, los convierte al rango 0..4, luego usa un patrón correspondiente de 4 bits para generar 2 * 2 píxeles en blanco y negro.
Eso también significa que el ancho y la altura deben ser uniformes.

Muestra:

cachorro determinista

Y un algoritmo aleatorio en solo 27 bytes:

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

Utiliza el mismo formato de archivo.

Muestra:

cachorro al azar

Y finalmente un enfoque mixto: tramado aleatorio con un sesgo hacia un patrón de tablero de ajedrez; 44 bytes:

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

Muestra:

cachorro mixto

aditsu
fuente
2
El primero es comparable a la aplicación "Flipnote Studio" de Nintendo DSi.
BobTheAwesome
6

Java (1.4+)

No estoy seguro de si estoy reinventando la rueda aquí, pero creo que puede ser única ...

con secuencias aleatorias limitadas

Con secuencias aleatorias limitadas

Dithering puro al azar

Dithering puro al azar

ingrese la descripción de la imagen aquí

Imagen de la ciudad de la respuesta de Averroes

El algoritmo utiliza el concepto de energía de luminosidad localizada y normalización para retener características. La versión inicial luego usó un jitter aleatorio para producir un aspecto difuso sobre áreas de luminosidad similar. Sin embargo, no era tan visualmente atractivo. Para contrarrestar esto, se asigna un conjunto limitado de secuencias aleatorias limitadas a la luminosidad del píxel de entrada sin procesar y las muestras se usan de forma iterativa y repetida para obtener fondos de aspecto difuso.

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;
      }

}
Moogie
fuente
3
Muy agradable. Definitivamente tiene un efecto diferente al de las otras respuestas hasta ahora.
Geobits
@Geobits Sí, me sorprendió lo efectivo que es. Sin embargo, no estoy seguro de si lo llamaría un tramado ya que produce una salida visualmente diferente
Moogie
Eso se ve bastante único.
qwr
5

Pitón

La idea es la siguiente: la imagen se divide en n x nmosaicos. Calculamos el color promedio de cada uno de esos mosaicos. Luego asignamos el rango de color 0 - 255al rango 0 - n*nque nos da un nuevo valor v. Luego coloreamos todos los píxeles de ese mosaico en negro, y coloreamos al azar los vpíxeles dentro de ese mosaico en blanco. Está lejos de ser óptimo, pero aún nos da resultados reconocibles. Dependiendo de la resolución, generalmente funciona mejor en n=2o n=3. Si bien n=2ya puede encontrar artefactos de la 'profundidad de color simulada, en caso de n=3que ya pueda ponerse algo borrosa. Supuse que las imágenes deberían permanecer del mismo tamaño, pero por supuesto, también puede usar este método y simplemente duplicar / triplicar el tamaño de la imagen generada para obtener más detalles.

PD: Sé que llego un poco tarde a la fiesta, recuerdo que no tenía ninguna idea cuando comenzó el desafío, pero ahora solo tenía esta onda cerebral =)

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

Resultados:

n=2:

ingrese la descripción de la imagen aquí

n=3:

ingrese la descripción de la imagen aquí

falla
fuente
3

Cualquier formato de archivo que desee está bien.

Definamos un formato de archivo teórico muy compacto para esta pregunta, ya que cualquiera de los formatos de archivo existentes tiene demasiada sobrecarga para escribir una respuesta rápida.

Deje que los primeros cuatro bytes del archivo de imagen definan el ancho y el alto de la imagen en píxeles, respectivamente:

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

seguido de w * hbytes de valores de escala de grises de 0 a 255:

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

Luego, podemos definir un fragmento de código en Python (145 bytes) que tomará esta imagen y hará:

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

que "se tambalea" al devolver blanco o negro con una probabilidad igual al valor de escala de grises de ese píxel.


Aplicado en la imagen de muestra, da algo como esto:

perro teñido

No es demasiado bonito, pero se ve muy similar cuando se reduce en una vista previa, y por solo 145 bytes de Python, no creo que pueda mejorar mucho.

Joe Z.
fuente
¿Puedes compartir un ejemplo? Creo que esto es oscilación aleatoria, y los resultados no son las más limpias ... buena imagen de perfil, aunque
QWR
Esto es de hecho un tramado aleatorio, y estoy haciendo un ejemplo de su imagen de muestra en este momento.
Joe Z.
2
Creo que podría beneficiarse de un aumento de contraste. No conozco Python, pero supongo que random.randint (0,255) está eligiendo un número aleatorio entre 0 y 255. Intenta limitarlo a, digamos, 55 y 200, lo que obligará a cualquier tono fuera de ese rango a ser blanco o negro puro. Con muchas imágenes, puede obtener una buena imagen impactante sin tramado, solo un umbral simple. (Aleatorio + aumento de contraste daría una imagen intermedia entre su imagen actual y el umbral simple.)
Level River St
Creo que el tramado aleatorio debería llamarse tramado Geiger (porque parece la salida de un contador Geiger). ¿Quién está de acuerdo?
Joe Z.
1
Eso es casi exactamente lo que hacen ImageMagick y GraphicsMagick con la opción "-random-umbral" que agregué junto con "-order-dither" hace años (agregado a mi respuesta). Nuevamente, golpear el gamma ayuda a obtener la intensidad correcta. Estoy de acuerdo con la sugerencia de "Geiger dithering".
Glenn Randers-Pehrson
3

Cobra

Toma un archivo PNG / BMP de 24 o 32 bits (JPG produce una salida con algunos grises). También es extensible a archivos que contienen color.

Utiliza ELA de velocidad optimizada para oscurecer la imagen en color de 3 bits, que volverá a ser blanco / negro cuando se le dé su imagen de prueba.

¿Mencioné que es realmente rápido?

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)

Perro

Arboles

Οurous
fuente
Para reducir la repetición, ¿ha considerado crear una variable temporal coly dejarla image.setPixel(x,y,col)hasta el final?
joeytwiddle
3
¿Qué pasa con la imagen de los árboles?
AJMansfield
Se ve bien, y proporciona un ejemplo de este trabajo con colores también.
Agradable
2

Java

Código de bajo nivel, usando PNGJ y una adición de ruido más difusión básica. Esta implementación requiere una fuente PNG de 8 bits en escala de grises.

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

}

(Agregue este jar a su ruta de compilación si desea probarlo).

ingrese la descripción de la imagen aquí

Como beneficio adicional: esto es extremadamente eficiente en el uso de memoria (solo almacena tres filas), por lo que podría usarse para imágenes enormes.

leonbloy
fuente
Nitpick: Creo que "usado para grandes imágenes" no es tan importante (¿alguna vez has visto un PNG en escala de grises de> 8 GB?), Pero "usado en, por ejemplo, dispositivos integrados" es un punto mucho más destacado.
semi-extrínseco
Me gusta, pero parece un poco borroso en los bordes, creo.
BobTheAwesome
1

Java

Solo un algoritmo simple basado en RNG, más algo de lógica para manejar imágenes en color. Tiene probabilidad b de establecer cualquier píxel dado en blanco, de lo contrario lo establece en negro; donde b es el brillo original de ese píxel.

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();
    }
}

Aquí hay un resultado potencial para la imagen del perro:

ingrese la descripción de la imagen aquí

SuperJedi224
fuente
¿Por qué no agrega la explicación en la parte superior en lugar de en la parte inferior donde nadie la va a leer? Realmente me gusta esa idea =)
falla