Roztrząsaj obraz w skali szarości

23

Roztrząsaj obraz w skali szarości na czysty czarno-biały z własnym algorytmem.

Wskazówki: Musisz wymyślić własny nowy algorytm. Nie możesz użyć wcześniej istniejących algorytmów (np. Floyd-Steinburg), ale możesz użyć ogólnej techniki. Twój program musi być w stanie odczytać obraz i wygenerować obraz tego samego rozmiaru. To konkurs popularności, więc wygrywa ten, kto wyprodukuje najlepszy (najbliższy oryginałowi) i najbardziej kreatywny (określony głosami). Bonus, jeśli kod jest krótki, ale nie jest to konieczne.

Jako wejścia możesz użyć dowolnego obrazu w skali szarości, powinien on być większy niż 300 x 300. Każdy format pliku jest w porządku.

Przykładowe dane wejściowe:

szczeniak

Przykładowe dane wyjściowe:

porzucony

To całkiem niezła robota, ale nadal widoczne są linie i wzory.

qwr
źródło
4
+1 za ciekawe wyzwanie, ale myślę, że byłoby znacznie lepiej jako [golf-golf] (ze specyfikacją) lub jakieś inne całkowicie obiektywne kryterium.
Klamka
2
Problem z rozmiarem kodu, szybkością i zużyciem pamięci polega na tym, że potrzebujesz obiektywnego progu określającego, jak rozpoznawalny musi być wynik, aby odpowiedź była prawidłowa, co jest również całkiem niemożliwe. Konkurs popularności ma sens, ale bez żadnych ograniczeń w kodzie ludzie nie są skłonni myśleć nieszablonowo. Wolę głosować za mądrą odpowiedzią, niż jedna daje najlepszy wynik, ponieważ właśnie zaimplementował on istniejący algorytm. Ale obecnie zachęcasz to drugie.
Martin Ender
3
Linia między algorytmem a jego techniką jest zbyt cienka, aby określić, po której stronie coś spada.
Peter Taylor
2
Myślę, że dużo łatwiej byłoby porównać wyniki, gdyby wszystkie pokazały wyniki z tego samego obrazu.
joeytwiddle
3
Czy możesz dodać źródło obrazu? (Nie sądzę, aby ktoś był zły, widząc tutaj jego / jej wizerunek, ale można zacytować źródło)
AL

Odpowiedzi:

16

Fortran

Okej, używam niejasnego formatu obrazu o nazwie FITS, który jest używany w astronomii. Oznacza to, że istnieje biblioteka Fortran do odczytu i zapisu takich obrazów. ImageMagick i Gimp mogą także odczytywać / zapisywać obrazy FITS.

Algorytm, którego używam, opiera się na ditheringu „Sierra Lite”, ale z dwoma ulepszeniami:
a) Zmniejszam błąd propagacji o czynnik 4/5.
b) Wprowadzam losową zmienność w matrycy dyfuzyjnej, utrzymując stałą jej sumę.
Razem prawie całkowicie eliminują wzorce widoczne na przykładzie PO.

Zakładając, że masz zainstalowaną bibliotekę CFITSIO, skompiluj ją

gfortran -lcfitsio dither.f90

Nazwy plików są zakodowane na stałe (nie było problemu, aby to naprawić).

Kod:

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

Przykładowe dane wyjściowe dla zdjęcia szczeniaka w PO: Post
Porzucone zdjęcie szczeniaka
przykładowe wyniki:
OP porzucił zdjęcie szczeniaka

pół-zewnętrzny
źródło
Wygląda to naprawdę dobrze, może być nie do pobicia pod względem jakości
aditsu
Dzięki! Nie wiem, czy jest to nie do pokonania, ale trudno będzie (bardzo subiektywnie) ocenić to na podstawie innych dobrych algorytmów.
pół-zewnętrzny
1
Wiem, że koduję w golfa, nadużywając wstecznej kompatybilności, ale wygląda na to, że nadużywasz go jako standard. Ten kod powoduje, że płaczę.
Kyle Kanos
@KyleKanos Zawsze się cieszę, gdy mój kod powoduje, że ktoś płacze: p Ale co konkretnie tutaj jest okropne? Tak, mogłem użyć „ukrytego braku”, ale gdzie jest w tym zabawa? Używam go do poważnego kodowania w pracy, ale nie do gry w golfa. I zdecydowanie zgadzam się, że API biblioteki CFITSIO jest całkowicie okropne (ftppre () generuje obraz FITS z pojedynczą rzeczywistą precyzją, ftpprj () generuje obraz z podwójną dokładnością całkowitą itp.), Ale to jest kompatybilność wsteczna F77.
pół-zewnętrzny zewnętrzny
1
Okej, więc większość z nich była po prostu niechlujna. Poprawiłem to. Konstruktywna krytyka jest zawsze doceniana :)
pół-zewnętrzny
34

GraphicsMagick / ImageMagick

Dithering zamówiony:

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

Zanim narzekam na to, że używam „ustalonego algorytmu”, przeczytaj ChangeLog dla GraphicsMagick i ImageMagick z kwietnia 2003 r., Gdzie zobaczysz, że zaimplementowałem algorytm w tych aplikacjach. Również kombinacja „-gamma .45455” z „-ordered-dither” jest nowa.

„-Gamma .45455” dba o to, aby obraz był zbyt jasny. Parametr „wszystko” jest potrzebny tylko w GraphicsMagick.

Jest pasowanie, ponieważ na obrazie z 4x4 uporządkowanym ditheringiem jest tylko 17 gravevels. Wygląd pasm można zmniejszyć, stosując uporządkowany drenaż 8x8, który ma 65 poziomów.

Oto oryginalny obraz, uporządkowane dane wyjściowe 4x4 i 8x8 oraz dane losowe: wprowadź opis zdjęcia tutaj

Wolę wersję z ditheringiem uporządkowanym, ale dołączam wersję z progiem losowym dla kompletności.

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

„10 x 90%” oznacza renderowanie pikseli o intensywności poniżej 10 procent jako czystej czerni i powyżej 90 procent jako czystej bieli, aby uniknąć posiadania kilku samotnych plamek w tych obszarach.

Prawdopodobnie warto zauważyć, że oba są tak wydajne pod względem pamięci, jak to tylko możliwe. Nie ma też żadnej dyfuzji, więc działają one jeden piksel na raz, nawet przy pisaniu bloków uporządkowanego ditheringu i nie muszą nic wiedzieć o sąsiednich pikselach. ImageMagick i GraphicsMagick przetwarzają wiersz po kolei, ale dla tych metod nie jest to konieczne. Konwersja uporządkowanego roztrząsania zajmuje mniej niż 0,04 sekundy w czasie rzeczywistym na moim starym komputerze x86_64.

Glenn Randers-Pehrson
źródło
31
„Przed złożeniem skargi na moje użycie„ ustalonego algorytmu ”przeczytaj ChangeLog for GraphicsMagick i ImageMagick z kwietnia 2003 r., Gdzie zobaczysz, że zaimplementowałem algorytm w tych aplikacjach.” +1 za czysty policzek.
Joe Z.
22

Przepraszam za styl kodu, rzuciłem to razem za pomocą bibliotek, które właśnie zbudowaliśmy w mojej klasie Java, i ma zły przypadek kopiowania-wklejania i magicznych liczb. Algorytm wybiera losowe prostokąty na obrazie i sprawdza, czy średnia jasność jest większa na skrawku obrazu lub na oryginalnym obrazie. Następnie włącza lub wyłącza piksel, aby zbliżyć jasność do siebie, wybierając piksele, które bardziej różnią się od oryginalnego obrazu. Myślę, że lepiej jest wydobyć cienkie detale, takie jak sierść szczeniaka, ale obraz jest głośniejszy, ponieważ próbuje wydobyć szczegóły nawet w obszarach, w których nie ma żadnych.

wprowadź opis zdjęcia tutaj

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
źródło
Rozumiem, że to jest deterministyczne? Jeśli tak, to jak szybko to jest?
tragiczny
Jest losowy i zajmuje około 3 sekund na moim komputerze.
QuadmasterXLII
2
Chociaż być może nie jest to algorytm największej wierności, wyniki same w sobie są sztuką.
AJMansfield
4
Naprawdę podoba mi się wygląd tego algorytmu! Ale myślę, że może wygląda tak dobrze częściowo, ponieważ tworzy teksturę podobną do futra, a to jest zwierzę z futrem. Ale nie jestem do końca pewien, czy to prawda. Czy możesz zamieścić inne zdjęcie np. Samochodu?
pół-zewnętrzny zewnętrzny
1
Myślę, że to najlepsza odpowiedź, zarówno pod względem oryginalności algorytmu, jak i pod względem niesamowitości wyników. Chciałbym też zobaczyć, jak działa na innych obrazach.
Nathaniel,
13

Ghostscript (przy niewielkiej pomocy ImageMagick)

Daleki od bycia moim „nowym algorytmem”, ale niestety nie mogłem się mu oprzeć.

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 -

wprowadź opis zdjęcia tutaj

Oczywiście działa lepiej bez ograniczeń „tego samego rozmiaru”.

użytkownik 2846289
źródło
2
To przezabawne. Jestem oszołomiony faktem, że nikt nie skomentował tego cudu w stylu Warhola.
Andreï Kostyrka
10

JAWA

Oto moje zgłoszenie. Pobiera obraz JPG, oblicza jasność pikseli na piksel (dzięki Bonanowi w tym pytaniu SO), a następnie porównuje go z przypadkowym wzorem, aby dowiedzieć się, czy wynikowy piksel będzie czarny czy biały. Najciemniejsze piksele będą zawsze czarne, a najjaśniejsze piksele będą zawsze białe, aby zachować szczegóły obrazu.

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

}

Przetworzony obraz

Inne przykłady:

Oryginalny Obrobiony

Działa również z kolorowymi obrazami:

Kolorowy obraz Wynik

Averroes
źródło
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 bajtów :)
Używa formatu ASCII PGM (P2) bez wiersza komentarza, zarówno dla wejścia, jak i wyjścia.

Metoda jest bardzo podstawowa: sumuje kwadraty 2 * 2 pikseli, konwertuje do zakresu 0..4, a następnie wykorzystuje odpowiedni wzór 4 bitów do wygenerowania 2 * 2 czarno-białych pikseli.
Oznacza to również, że szerokość i wysokość muszą być równe.

Próba:

deterministyczny szczeniak

I losowy algorytm tylko w 27 bajtach:

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

Używa tego samego formatu pliku.

Próba:

losowy szczeniak

I wreszcie podejście mieszane: losowe dithering z nastawieniem na wzór szachownicy; 44 bajty:

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

Próba:

mieszany szczeniak

aditsu
źródło
2
Pierwszy jest porównywalny do aplikacji „Flipnote Studio” Nintendo DSi.
BobTheAwesome
6

Java (1.4+)

Nie jestem pewien, czy wymyślam tutaj koło, ale myślę, że może być wyjątkowy ...

z ograniczonymi losowymi sekwencjami

Z ograniczoną liczbą losowych sekwencji

Czysty losowy dithering

Czysty losowy dithering

wprowadź opis zdjęcia tutaj

Obraz miasta z odpowiedzi Averroesa

Algorytm wykorzystuje koncepcję zlokalizowanej energii świetlnej i normalizacji w celu zachowania cech. Pierwotna wersja wykorzystywała następnie przypadkowe drgania, aby uzyskać sparaliżowane spojrzenie na obszarach o podobnej jasności. Nie było to jednak tak atrakcyjne wizualnie. Aby temu przeciwdziałać, ograniczony zestaw ograniczonych losowych sekwencji jest odwzorowywany na surową jasność pikseli wejściowych, a próbki są używane iteracyjnie i wielokrotnie dają rozczochrane tła.

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
źródło
3
Bardzo dobrze. To zdecydowanie daje inny efekt niż inne dotychczasowe odpowiedzi.
Geobits
@Geobits Tak, zaskoczyło mnie to, jak skuteczny jest. Nie jestem jednak pewien, czy nazwałbym to ditheringiem, ponieważ generuje całkiem odmienną wydajność
Moogie
To wygląda całkiem wyjątkowo.
qwr
5

Pyton

Pomysł jest następujący: obraz dzieli się na n x nkafelki. Średni kolor każdego z tych kafelków obliczamy. Następnie mapujemy zakres kolorów 0 - 255na zakres, 0 - n*nktóry daje nam nową wartość v. Następnie kolorujemy wszystkie piksele z tego kafelka na czarno, a losowo vpiksele w kafelku są białe. Jest daleki od optymalnego, ale wciąż daje nam rozpoznawalne wyniki. W zależności od rozdzielczości działa zwykle najlepiej w trybie n=2lub n=3. Podczas gdy n=2możesz już znaleźć artefakty z „symulowanej głębi kolorów”, na wypadek n=3, gdyby stało się już trochę rozmazane. Zakładałem, że obrazy powinny pozostać tego samego rozmiaru, ale oczywiście możesz również użyć tej metody i po prostu podwoić / potroić rozmiar generowanego obrazu, aby uzyskać więcej szczegółów.

PS: Wiem, że jestem trochę spóźniony na imprezę, pamiętam, że nie miałem żadnych pomysłów, kiedy wyzwanie się zaczęło, ale teraz miałem falę mózgową =)

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

Wyniki:

n=2:

wprowadź opis zdjęcia tutaj

n=3:

wprowadź opis zdjęcia tutaj

wada
źródło
3

Dowolny format pliku jest w porządku.

Zdefiniujmy bardzo zwarty, teoretyczny format pliku dla tego pytania, ponieważ którykolwiek z istniejących formatów plików ma zbyt duży narzut, aby napisać szybką odpowiedź.

Niech pierwsze cztery bajty pliku obrazu określają odpowiednio szerokość i wysokość obrazu w pikselach:

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

po których następują w * hbajty wartości skali szarości od 0 do 255:

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

Następnie możemy zdefiniować fragment kodu w Pythonie (145 bajtów), który weźmie ten obraz i wykona:

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

który „ditheruje”, zwracając biały lub czarny z prawdopodobieństwem równym wartości skali szarości tego piksela.


Nałożony na przykładowy obraz daje coś takiego:

porzucony pies

Nie jest zbyt ładny, ale wygląda bardzo podobnie po zmniejszeniu w podglądzie, a dla zaledwie 145 bajtów Pythona nie wydaje mi się, żeby można było znacznie poprawić.

Joe Z.
źródło
Czy możesz podać przykład? Wierzę, że jest to przypadkowe dithering, a wyniki nie są najczystsze ... ładne zdjęcie profilowe
qwr
To jest rzeczywiście przypadkowe dithering i robię teraz przykład twojego przykładowego zdjęcia.
Joe Z.
2
Myślę, że może skorzystać z wzmocnienia kontrastu. Nie znam Pythona, ale zakładam, że random.randint (0,255) wybiera losową liczbę od 0 do 255. Spróbuj ograniczyć się do powiedzmy od 55 do 200, co spowoduje, że wszystkie odcienie spoza tego zakresu będą czysto czarne lub białe. Przy wielu zdjęciach można uzyskać dobry, efektowny obraz bez roztrząsania, tylko prosty próg. (Losowe + zwiększenie kontrastu dałoby obraz pośredni między twoim obecnym obrazem a prostym progiem.)
Level River St
Myślę, że losowe ditherowanie powinno się nazywać ditheringiem Geigera (ponieważ wygląda jak wyjście licznika Geigera). Kto się zgadza?
Joe Z.
1
To prawie dokładnie to, co ImageMagick i GraphicsMagick robią z opcją „-random-próg”, którą dodałem wraz z „-ordered-dither” lata temu (dodałem do mojej odpowiedzi). Ponownie uderzenie gamma pomaga uzyskać odpowiednią intensywność. Zgadzam się z sugestią „ditheringu Geigera”.
Glenn Randers-Pehrson
3

Kobra

Pobiera 24-bitowy lub 32-bitowy plik PNG / BMP (JPG wytwarza dane wyjściowe z pewnymi szarościami). Jest również rozszerzalny na pliki zawierające kolory.

Wykorzystuje ELA o zoptymalizowanej prędkości do roztrząsania obrazu w 3-bitowym kolorze, który powróci jako czarno-biały po otrzymaniu obrazu testowego.

Czy wspominałem, że jest naprawdę szybki?

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)

Pies

Drzewa

Obrzydliwe
źródło
Czy w celu zmniejszenia liczby powtórzeń rozważałeś utworzenie zmiennej tymczasowej coli pozostawienie jej image.setPixel(x,y,col)do samego końca?
joeytwiddle
3
Co z wizerunkiem drzew?
AJMansfield
Wygląda ładnie i stanowi przykład tego działania z kolorami.
Οurous
2

Jawa

Kod niskopoziomowy, wykorzystujący PNGJ i dodatek szumu plus podstawowa dyfuzja. Ta implementacja wymaga 8-bitowego źródła PNG w skali szarości.

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

}

(Dodaj ten słoik do ścieżki kompilacji, jeśli chcesz go wypróbować).

wprowadź opis zdjęcia tutaj

Jako bonus: jest niezwykle wydajny w użyciu pamięci (przechowuje tylko trzy rzędy), dzięki czemu można go używać do dużych obrazów.

leonbloy
źródło
Nitpick: Myślę, że „używane do dużych obrazów” nie jest tak ważne (czy kiedykolwiek widziałeś PNG w skali szarości> 8 GB?), Ale „używane na np. Urządzeniach osadzonych” jest znacznie bardziej istotne.
pół-zewnętrzny zewnętrzny
Podoba mi się, ale wygląda trochę rozmazany na brzegach.
BobTheAwesome
1

Jawa

Po prostu prosty algorytm oparty na RNG oraz logika do obsługi kolorowych obrazów. Ma prawdopodobieństwo b ustawienia dowolnego danego piksela na biały, w przeciwnym razie ustawia go na czarny; gdzie b to oryginalna jasność tego piksela.

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

Oto potencjalny wynik obrazu psa:

wprowadź opis zdjęcia tutaj

SuperJedi224
źródło
Dlaczego nie dodasz wyjaśnienia na górze zamiast na dole, gdzie nikt go nie przeczyta? Naprawdę podoba mi się ten pomysł =)
flawr