Skip to content

American Gothic dans la palette de Mona Lisa : réarrangez les pixels

Solution :

Java - Interface graphique avec transformation progressive randomisée.

J'ai essayé BEAUCOUP de choses, dont certaines très compliquées, puis je suis finalement revenu à ce code relativement simple :

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Random;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.Timer;

@SuppressWarnings("serial")
public class CopyColors extends JFrame {
    private static final String SOURCE = "spheres";
    private static final String PALETTE = "mona";
    private static final int COUNT = 10000;
    private static final int DELAY = 20;
    private static final int LUM_WEIGHT = 10;

    private static final double[] F = {0.114, 0.587, 0.299};
    private final BufferedImage source;
    protected final BufferedImage dest;
    private final int sw;
    private final int sh;
    private final int n;
    private final Random r = new Random();
    private final JLabel l;

    public CopyColors(final String sourceName, final String paletteName) throws IOException {
        super("CopyColors by aditsu");
        source = ImageIO.read(new File(sourceName + ".png"));
        final BufferedImage palette = ImageIO.read(new File(paletteName + ".png"));
        sw = source.getWidth();
        sh = source.getHeight();
        final int pw = palette.getWidth();
        final int ph = palette.getHeight();
        n = sw * sh;
        if (n != pw * ph) {
            throw new RuntimeException();
        }
        dest = new BufferedImage(sw, sh, BufferedImage.TYPE_INT_RGB);
        for (int i = 0; i < sh; ++i) {
            for (int j = 0; j < sw; ++j) {
                final int x = i * sw + j;
                dest.setRGB(j, i, palette.getRGB(x % pw, x / pw));
            }
        }
        l = new JLabel(new ImageIcon(dest));
        add(l);
        final JButton b = new JButton("Save");
        add(b, BorderLayout.SOUTH);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(final ActionEvent e) {
                try {
                    ImageIO.write(dest, "png", new File(sourceName + "-" + paletteName + ".png"));
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        });
    }

    protected double dist(final int x, final int y) {
        double t = 0;
        double lx = 0;
        double ly = 0;
        for (int i = 0; i < 3; ++i) {
            final double xi = ((x >> (i * 8)) & 255) * F[i];
            final double yi = ((y >> (i * 8)) & 255) * F[i];
            final double d = xi - yi;
            t += d * d;
            lx += xi;
            ly += yi;
        }
        double l = lx - ly;
        return t + l * l * LUM_WEIGHT;
    }

    public void improve() {
        final int x = r.nextInt(n);
        final int y = r.nextInt(n);
        final int sx = source.getRGB(x % sw, x / sw);
        final int sy = source.getRGB(y % sw, y / sw);
        final int dx = dest.getRGB(x % sw, x / sw);
        final int dy = dest.getRGB(y % sw, y / sw);
        if (dist(sx, dx) + dist(sy, dy) > dist(sx, dy) + dist(sy, dx)) {
            dest.setRGB(x % sw, x / sw, dy);
            dest.setRGB(y % sw, y / sw, dx);
        }
    }

    public void update() {
        l.repaint();
    }

    public static void main(final String... args) throws IOException {
        final CopyColors x = new CopyColors(SOURCE, PALETTE);
        x.setSize(800, 600);
        x.setLocationRelativeTo(null);
        x.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        x.setVisible(true);
        new Timer(DELAY, new ActionListener() {
            @Override
            public void actionPerformed(final ActionEvent e) {
                for (int i = 0; i < COUNT; ++i) {
                    x.improve();
                }
                x.update();
            }
        }).start();
    }
}

Tous les paramètres pertinents sont définis comme des constantes au début de la classe.

Le programme copie d'abord l'image de la palette dans les dimensions de la source, puis choisit de manière répétée 2 pixels aléatoires et les échange si cela les rapproche de l'image source. "Plus proche" est défini à l'aide d'une fonction de distance de couleur qui calcule la différence entre les composantes r, g, b (pondérée par la luminance) ainsi que la différence luma totale, avec un poids plus important pour la luminance.

Il ne faut que quelques secondes pour que les formes se forment, mais un peu plus longtemps pour que les couleurs s'assemblent. Vous pouvez enregistrer l'image actuelle à tout moment. J'ai généralement attendu environ 1 à 3 minutes avant de sauvegarder.

Résultats :

Contrairement à certaines autres réponses, ces images ont toutes été générées en utilisant exactement les mêmes paramètres (autres que les noms de fichiers).

Palette gothique américaine

mona-gothiquecri

Palette Mona Lisa

monona gothiquecriersphères

Palette Nuit étoilée

mona-nuitnuit de crissphères

Palette The Scream

gêne gothiquemona-scindecrème nocturnesphères

Palette Sphères

Je pense que c'est le test le plus difficile et tout le monde devrait poster ses résultats avec cette palette :

sphères gothiquesmona-sphèrescris

Désolé, je n'ai pas trouvé l'image de la rivière très intéressante donc je ne l'ai pas incluse.

J'ai également ajouté une vidéo à https://www.youtube.com/watch?v=_-w3cKL5teM , elle montre ce que fait le programme (pas exactement en temps réel mais similaire) puis elle montre le mouvement progressif des pixels en utilisant le script python de Calvin. Malheureusement la qualité de la vidéo est significativement endommagée par l'encodage/compression de youtube.

Java

import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import javax.imageio.ImageIO;

/**
 *
 * @author Quincunx
 */
public class PixelRearranger {

    public static void main(String[] args) throws IOException {
        BufferedImage source = ImageIO.read(resource("American Gothic.png"));
        BufferedImage palette = ImageIO.read(resource("Mona Lisa.png"));
        BufferedImage result = rearrange(source, palette);
        ImageIO.write(result, "png", resource("result.png"));
        validate(palette, result);
    }

    public static class MInteger {
        int val;

        public MInteger(int i) {
            val = i;
        }
    }

    public static BufferedImage rearrange(BufferedImage source, BufferedImage palette) {
        BufferedImage result = new BufferedImage(source.getWidth(),
                source.getHeight(), BufferedImage.TYPE_INT_RGB);

        //This creates a list of points in the Source image.
        //Then, we shuffle it and will draw points in that order.
        List samples = getPoints(source.getWidth(), source.getHeight());
        System.out.println("gotPoints");

        //Create a list of colors in the palette.
        rgbList = getColors(palette);
        Collections.sort(rgbList, rgb);
        rbgList = new ArrayList<>(rgbList);
        Collections.sort(rbgList, rbg);
        grbList = new ArrayList<>(rgbList);
        Collections.sort(grbList, grb);
        gbrList = new ArrayList<>(rgbList);
        Collections.sort(gbrList, gbr);
        brgList = new ArrayList<>(rgbList);
        Collections.sort(brgList, brg);
        bgrList = new ArrayList<>(rgbList);
        Collections.sort(bgrList, bgr);

        while (!samples.isEmpty()) {
            Point currentPoint = samples.remove(0);
            int sourceAtPoint = source.getRGB(currentPoint.x, currentPoint.y);
            int bestColor = search(new MInteger(sourceAtPoint));
            result.setRGB(currentPoint.x, currentPoint.y, bestColor);
        }
        return result;
    }

    public static List getPoints(int width, int height) {
        HashSet points = new HashSet<>(width * height);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                points.add(new Point(x, y));
            }
        }
        List newList = new ArrayList<>();
        List corner1 = new LinkedList<>();
        List corner2 = new LinkedList<>();
        List corner3 = new LinkedList<>();
        List corner4 = new LinkedList<>();

        Point p1 = new Point(width / 3, height / 3);
        Point p2 = new Point(width * 2 / 3, height / 3);
        Point p3 = new Point(width / 3, height * 2 / 3);
        Point p4 = new Point(width * 2 / 3, height * 2 / 3);

        newList.add(p1);
        newList.add(p2);
        newList.add(p3);
        newList.add(p4);
        corner1.add(p1);
        corner2.add(p2);
        corner3.add(p3);
        corner4.add(p4);
        points.remove(p1);
        points.remove(p2);
        points.remove(p3);
        points.remove(p4);

        long seed = System.currentTimeMillis();
        Random c1Random = new Random(seed += 179426549); //The prime number pushes the first numbers apart
        Random c2Random = new Random(seed += 179426549); //Or at least I think it does.
        Random c3Random = new Random(seed += 179426549);
        Random c4Random = new Random(seed += 179426549);

        Dir NW = Dir.NW;
        Dir N = Dir.N;
        Dir NE = Dir.NE;
        Dir W = Dir.W;
        Dir E = Dir.E;
        Dir SW = Dir.SW;
        Dir S = Dir.S;
        Dir SE = Dir.SE;
        while (!points.isEmpty()) {
            putPoints(newList, corner1, c1Random, points, NW, N, NE, W, E, SW, S, SE);
            putPoints(newList, corner2, c2Random, points, NE, N, NW, E, W, SE, S, SW);
            putPoints(newList, corner3, c3Random, points, SW, S, SE, W, E, NW, N, NE);
            putPoints(newList, corner4, c4Random, points, SE, S, SW, E, W, NE, N, NW);
        }
        return newList;
    }

    public static enum Dir {
        NW(-1, -1), N(0, -1), NE(1, -1), W(-1, 0), E(1, 0), SW(-1, 1), S(0, 1), SE(1, 1);
        final int dx, dy;

        private Dir(int dx, int dy) {
            this.dx = dx;
            this.dy = dy;
        }

        public Point add(Point p) {
            return new Point(p.x + dx, p.y + dy);
        }
    }

    public static void putPoints(List newList, List listToAddTo, Random rand,
                                 HashSet points, Dir... adj) {
        List newPoints = new LinkedList<>();
        for (Iterator iter = listToAddTo.iterator(); iter.hasNext();) {
            Point p = iter.next();
            Point pul = adj[0].add(p);
            Point pu = adj[1].add(p);
            Point pur = adj[2].add(p);
            Point pl = adj[3].add(p);
            Point pr = adj[4].add(p);
            Point pbl = adj[5].add(p);
            Point pb = adj[6].add(p);
            Point pbr = adj[7].add(p);
            int allChosen = 0;
            if (points.contains(pul)) {
                if (rand.nextInt(5) == 0) {
                    allChosen++;
                    newPoints.add(pul);
                    newList.add(pul);
                    points.remove(pul);
                }
            } else {
                allChosen++;
            }
            if (points.contains(pu)) {
                if (rand.nextInt(5) == 0) {
                    allChosen++;
                    newPoints.add(pu);
                    newList.add(pu);
                    points.remove(pu);
                }
            } else {
                allChosen++;
            }
            if (points.contains(pur)) {
                if (rand.nextInt(3) == 0) {
                    allChosen++;
                    newPoints.add(pur);
                    newList.add(pur);
                    points.remove(pur);
                }
            } else {
                allChosen++;
            }
            if (points.contains(pl)) {
                if (rand.nextInt(5) == 0) {
                    allChosen++;
                    newPoints.add(pl);
                    newList.add(pl);
                    points.remove(pl);
                }
            } else {
                allChosen++;
            }
            if (points.contains(pr)) {
                if (rand.nextInt(2) == 0) {
                    allChosen++;
                    newPoints.add(pr);
                    newList.add(pr);
                    points.remove(pr);
                }
            } else {
                allChosen++;
            }
            if (points.contains(pbl)) {
                if (rand.nextInt(5) == 0) {
                    allChosen++;
                    newPoints.add(pbl);
                    newList.add(pbl);
                    points.remove(pbl);
                }
            } else {
                allChosen++;
            }
            if (points.contains(pb)) {
                if (rand.nextInt(3) == 0) {
                    allChosen++;
                    newPoints.add(pb);
                    newList.add(pb);
                    points.remove(pb);
                }
            } else {
                allChosen++;
            }
            if (points.contains(pbr)) {
                newPoints.add(pbr);
                newList.add(pbr);
                points.remove(pbr);
            }
            if (allChosen == 7) {
                iter.remove();
            }
        }
        listToAddTo.addAll(newPoints);
    }

    public static List getColors(BufferedImage img) {
        int width = img.getWidth();
        int height = img.getHeight();
        List colors = new ArrayList<>(width * height);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                colors.add(new MInteger(img.getRGB(x, y)));
            }
        }
        return colors;
    }

    public static int search(MInteger color) {
        int rgbIndex = binarySearch(rgbList, color, rgb);
        int rbgIndex = binarySearch(rbgList, color, rbg);
        int grbIndex = binarySearch(grbList, color, grb);
        int gbrIndex = binarySearch(gbrList, color, gbr);
        int brgIndex = binarySearch(brgList, color, brg);
        int bgrIndex = binarySearch(bgrList, color, bgr);

        double distRgb = dist(rgbList.get(rgbIndex), color);
        double distRbg = dist(rbgList.get(rbgIndex), color);
        double distGrb = dist(grbList.get(grbIndex), color);
        double distGbr = dist(gbrList.get(gbrIndex), color);
        double distBrg = dist(brgList.get(brgIndex), color);
        double distBgr = dist(bgrList.get(bgrIndex), color);

        double minDist = Math.min(Math.min(Math.min(Math.min(Math.min(
                distRgb, distRbg), distGrb), distGbr), distBrg), distBgr);

        MInteger ans;
        if (minDist == distRgb) {
            ans = rgbList.get(rgbIndex);
        } else if (minDist == distRbg) {
            ans = rbgList.get(rbgIndex);
        } else if (minDist == distGrb) {
            ans = grbList.get(grbIndex);
        } else if (minDist == distGbr) {
            ans = grbList.get(grbIndex);
        } else if (minDist == distBrg) {
            ans = grbList.get(rgbIndex);
        } else {
            ans = grbList.get(grbIndex);
        }
        rgbList.remove(ans);
        rbgList.remove(ans);
        grbList.remove(ans);
        gbrList.remove(ans);
        brgList.remove(ans);
        bgrList.remove(ans);
        return ans.val;
    }

    public static int binarySearch(List list, MInteger val, Comparator cmp){
        int index = Collections.binarySearch(list, val, cmp);
        if (index < 0) {
            index = ~index;
            if (index >= list.size()) {
                index = list.size() - 1;
            }
        }
        return index;
    }

    public static double dist(MInteger color1, MInteger color2) {
        int c1 = color1.val;
        int r1 = (c1 & 0xFF0000) >> 16;
        int g1 = (c1 & 0x00FF00) >> 8;
        int b1 = (c1 & 0x0000FF);

        int c2 = color2.val;
        int r2 = (c2 & 0xFF0000) >> 16;
        int g2 = (c2 & 0x00FF00) >> 8;
        int b2 = (c2 & 0x0000FF);

        int dr = r1 - r2;
        int dg = g1 - g2;
        int db = b1 - b2;
        return Math.sqrt(dr * dr + dg * dg + db * db);
    }

    //This method is here solely for my ease of use (I put the files under /Resources/ )
    public static File resource(String fileName) {
        return new File(System.getProperty("user.dir") + "/Resources/" + fileName);
    }

    static List rgbList;
    static List rbgList;
    static List grbList;
    static List gbrList;
    static List brgList;
    static List bgrList;
    static Comparator rgb = (color1, color2) -> color1.val - color2.val;
    static Comparator rbg = (color1, color2) -> {
        int c1 = color1.val;
        int c2 = color2.val;
        c1 = ((c1 & 0xFF0000)) | ((c1 & 0x00FF00) >> 8) | ((c1 & 0x0000FF) << 8);
        c2 = ((c2 & 0xFF0000)) | ((c2 & 0x00FF00) >> 8) | ((c2 & 0x0000FF) << 8);
        return c1 - c2;
    };
    static Comparator grb = (color1, color2) -> {
        int c1 = color1.val;
        int c2 = color2.val;
        c1 = ((c1 & 0xFF0000) >> 8) | ((c1 & 0x00FF00) << 8) | ((c1 & 0x0000FF));
        c2 = ((c2 & 0xFF0000) >> 8) | ((c2 & 0x00FF00) << 8) | ((c2 & 0x0000FF));
        return c1 - c2;
    };

    static Comparator gbr = (color1, color2) -> {
        int c1 = color1.val;
        int c2 = color2.val;
        c1 = ((c1 & 0xFF0000) >> 16) | ((c1 & 0x00FF00) << 8) | ((c1 & 0x0000FF) << 8);
        c2 = ((c2 & 0xFF0000) >> 16) | ((c2 & 0x00FF00) << 8) | ((c2 & 0x0000FF) << 8);
        return c1 - c2;
    };

    static Comparator brg = (color1, color2) -> {
        int c1 = color1.val;
        int c2 = color2.val;
        c1 = ((c1 & 0xFF0000) >> 8) | ((c1 & 0x00FF00) >> 8) | ((c1 & 0x0000FF) << 16);
        c2 = ((c2 & 0xFF0000) >> 8) | ((c2 & 0x00FF00) >> 8) | ((c2 & 0x0000FF) << 16);
        return c1 - c2;
    };

    static Comparator bgr = (color1, color2) -> {
        int c1 = color1.val;
        int c2 = color2.val;
        c1 = ((c1 & 0xFF0000) >> 16) | ((c1 & 0x00FF00)) | ((c1 & 0x0000FF) << 16);
        c2 = ((c2 & 0xFF0000) >> 16) | ((c2 & 0x00FF00)) | ((c2 & 0x0000FF) << 16);
        return c1 - c2;
    };

    public static void validate(BufferedImage palette, BufferedImage result) {
        List paletteColors = getTrueColors(palette);
        List resultColors = getTrueColors(result);
        Collections.sort(paletteColors);
        Collections.sort(resultColors);
        System.out.println(paletteColors.equals(resultColors));
    }

    public static List getTrueColors(BufferedImage img) {
        int width = img.getWidth();
        int height = img.getHeight();
        List colors = new ArrayList<>(width * height);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                colors.add(img.getRGB(x, y));
            }
        }
        Collections.sort(colors);
        return colors;
    }
}

Mon approche fonctionne en trouvant la couleur la plus proche de chaque pixel (enfin, probablement la plus proche), dans l'espace 3, puisque les couleurs sont en 3D.

Cela fonctionne en créant une liste de tous les points que nous devons remplir et une liste de toutes les couleurs possibles que nous pouvons utiliser. On randomise la liste des points (pour que l'image donne un meilleur résultat), puis on passe par chaque point et on obtient la couleur de l'image source.

Mise à jour : J'avais l'habitude de faire simplement une recherche binaire, donc le rouge correspondait mieux que le vert qui correspondait mieux que le bleu. Je l'ai maintenant changé pour faire six recherches binaires (toutes les permutations possibles), puis choisir la couleur la plus proche. Cela prend seulement ~6 fois plus de temps (c'est-à-dire 1 minute). Bien que les images soient encore granuleuses, les couleurs correspondent mieux.

Mise à jour 2 : Je ne randomise plus la liste. Au lieu de cela, je choisis 4 points en suivant la règle des tiers, puis je dispose aléatoirement les points, en privilégiant le remplissage du centre.

Note : Voir l'historique des révisions pour les anciennes images.

Mona Lisa -> Rivière :

Entrez la description de l'image ici

Mona Lisa -> American Gothic :

Entrez la description de l'image ici

Mona Lisa -> Sphères rayonnées :

Entrez la description de l'image ici

Nuit étoilée -> Joconde :

Entrez la description de l'image ici


Voici un Gif animé montrant comment l'image a été construite :

Entrez la description de l'image ici

Et montrant les pixels pris sur la Mona Lisa :

Entrez la description de l'image ici

Perl, avec l'espace couleur Lab et le dithering.

Note : Maintenant, j'ai aussi une solution en C.

Utilise une approche similaire à celle d'aditsu, (choisir deux positions aléatoires, et échanger les pixels à ces positions si cela rend l'image plus proche de l'image cible), avec deux améliorations majeures :

  1. Utilise la CIE L a b* pour comparer les couleurs - la métrique euclidienne sur cet espace est une très bonne approximation de la différence perceptive entre deux couleurs, donc les mappings de couleurs devraient être plus précis que RGB ou même HSV/HSL.
  2. Après une première passe mettant les pixels dans la meilleure position unique possible, il fait une passe supplémentaire avec un dither aléatoire. Au lieu de comparer les valeurs des pixels aux deux positions de swap, il calcule la valeur moyenne des pixels d'un voisinage 3x3 centré sur les positions de swap. Si un échange améliore les couleurs moyennes des voisinages, il est autorisé, même s'il rend les pixels individuels moins précis. Pour certaines paires d'images, cela a un effet douteux sur la qualité (et rend l'effet de palette moins frappant), mais pour certaines (comme les sphères -> n'importe quoi), cela aide beaucoup. Le facteur "détail" accentue le pixel central à un degré variable. En l'augmentant, on diminue la quantité globale de tremblement, mais on conserve plus de détails fins de l'image cible. L'optimisation du dithered est plus lente, c'est pourquoi nous l'exécutons sur la sortie de l'optimisation non-dithered comme point de départ.

Faire la moyenne des valeurs de laboratoire, comme le fait le dither, n'est pas... vraiment justifiée (elles devraient être converties en XYZ, moyennées, et reconverties) mais cela fonctionne très bien pour ces objectifs.

Ces images ont des limites de terminaison de 100 et 200 (fin de la première phase lorsque moins de 1 sur 5000 swaps est accepté, et de la seconde phase à 1 sur 2500), et un facteur de détail de tramage de 12 (un tramage un peu plus serré que l'ensemble précédent). Avec ce paramètre de très haute qualité, les images prennent beaucoup de temps à générer, mais avec la parallélisation, l'ensemble du travail se termine toujours en une heure sur ma machine à 6 cœurs. En augmentant les valeurs à 500 ou plus, les images sont générées en quelques minutes, mais elles sont un peu moins soignées. Je voulais montrer l'algorithme au mieux ici.

Le code n'est en aucun cas joli :

#!/usr/bin/perl
use strict;
use warnings;
use Image::Magick;
use Graphics::ColorObject 'RGB_to_Lab';
use List::Util qw(sum max);

my $source = Image::Magick->new;
$source->Read($ARGV[0]);
my $target = Image::Magick->new;
$target->Read($ARGV[1]);
my ($limit1, $limit2, $detail) = @ARGV[2,3,4];

my ($width, $height) = ($target->Get('width'), $target->Get('height'));

# Transfer the pixels of the $source onto a new canvas with the diemnsions of $target
$source->Set(magick => 'RGB');
my $img = Image::Magick->new(size => "${width}x${height}", magick => 'RGB', depth => 8);
$img->BlobToImage($source->ImageToBlob);

my ($made, $rejected) = (0,0);

system("rm anim/*.png");

my (@img_lab, @target_lab);
for my $x (0 .. $width) {
  for my $y (0 .. $height) {
    $img_lab[$x][$y] = RGB_to_Lab([$img->getPixel(x => $x, y => $y)], 'sRGB');
    $target_lab[$x][$y] = RGB_to_Lab([$target->getPixel(x => $x, y => $y)], 'sRGB');
  }
}

my $n = 0;
my $frame = 0;
my $mode = 1;

while (1) {
  $n++;

  my $swap = 0;
  my ($x1, $x2, $y1, $y2) = (int rand $width, int rand $width, int rand $height, int rand $height);
  my ($dist, $dist_swapped);

  if ($mode == 1) {
    $dist = (sum map { ($img_lab[$x1][$y1][$_] - $target_lab[$x1][$y1][$_])**2 } 0..2)
          + (sum map { ($img_lab[$x2][$y2][$_] - $target_lab[$x2][$y2][$_])**2 } 0..2);

    $dist_swapped = (sum map { ($img_lab[$x2][$y2][$_] - $target_lab[$x1][$y1][$_])**2 } 0..2)
                  + (sum map { ($img_lab[$x1][$y1][$_] - $target_lab[$x2][$y2][$_])**2 } 0..2);

  } else { # dither mode
    my $xoffmin = ($x1 == 0 || $x2 == 0 ? 0 : -1);
    my $xoffmax = ($x1 == $width - 1 || $x2 == $width - 1 ? 0 : 1);
    my $yoffmin = ($y1 == 0 || $y2 == 0 ? 0 : -1);
    my $yoffmax = ($y1 == $height - 1 || $y2 == $height - 1 ? 0 : 1);

    my (@img1, @img2, @target1, @target2, $points);
    for my $xoff ($xoffmin .. $xoffmax) {
      for my $yoff ($yoffmin .. $yoffmax) {
        $points++;
        for my $chan (0 .. 2) {
          $img1[$chan] += $img_lab[$x1+$xoff][$y1+$yoff][$chan];
          $img2[$chan] += $img_lab[$x2+$xoff][$y2+$yoff][$chan];
          $target1[$chan] += $target_lab[$x1+$xoff][$y1+$yoff][$chan];
          $target2[$chan] += $target_lab[$x2+$xoff][$y2+$yoff][$chan];
        }
      }
    }

    my @img1s = @img1;
    my @img2s = @img2;
    for my $chan (0 .. 2) {
      $img1[$chan] += $img_lab[$x1][$y1][$chan] * ($detail - 1);
      $img2[$chan] += $img_lab[$x2][$y2][$chan] * ($detail - 1);

      $target1[$chan] += $target_lab[$x1][$y1][$chan] * ($detail - 1);
      $target2[$chan] += $target_lab[$x2][$y2][$chan] * ($detail - 1);

      $img1s[$chan] += $img_lab[$x2][$y2][$chan] * $detail - $img_lab[$x1][$y1][$chan];
      $img2s[$chan] += $img_lab[$x1][$y1][$chan] * $detail - $img_lab[$x2][$y2][$chan];
    }

    $dist = (sum map { ($img1[$_] - $target1[$_])**2 } 0..2)
          + (sum map { ($img2[$_] - $target2[$_])**2 } 0..2);

    $dist_swapped = (sum map { ($img1s[$_] - $target1[$_])**2 } 0..2)
                  + (sum map { ($img2s[$_] - $target2[$_])**2 } 0..2);

  }

  if ($dist_swapped < $dist) {
    my @pix1 = $img->GetPixel(x => $x1, y => $y1);
    my @pix2 = $img->GetPixel(x => $x2, y => $y2);
    $img->SetPixel(x => $x1, y => $y1, color => @pix2);
    $img->SetPixel(x => $x2, y => $y2, color => @pix1);
    ($img_lab[$x1][$y1], $img_lab[$x2][$y2]) = ($img_lab[$x2][$y2], $img_lab[$x1][$y1]);
    $made ++;
  } else {
    $rejected ++;
  }

  if ($n % 50000 == 0) {
#    print "Made: $made Rejected: $rejectedn";
    $img->Write('png:out.png');
    system("cp", "out.png", sprintf("anim/frame%05d.png", $frame++));
    if ($mode == 1 and $made < $limit1) {
      $mode = 2;
      system("cp", "out.png", "nodither.png");
    } elsif ($mode == 2 and $made < $limit2) {
      last;
    }
    ($made, $rejected) = (0, 0);
  }
}

Résultats

Palette gothique américaine

Peu de différence ici avec le tramage ou non.

Palette Mona Lisa

Le tramage réduit le banding sur les sphères, mais n'est pas spécialement joli.

Palette Nuit étoilée

Mona Lisa conserve un peu plus de détails avec le tramage. Sphères est à peu près la même situation que la dernière fois.

Palette Scream

Starry Night sans dithering est la chose la plus géniale qui soit. Le dithering la rend plus fidèle à la photo, mais beaucoup moins intéressante.

Palette Sphères

Comme le dit Aditsu, le vrai test. Je pense que je réussis.

Le Dithering aide énormément avec American Gothic et Mona Lisa, en mélangeant certains gris et d'autres couleurs avec les pixels plus intenses pour produire des tons de peau semi-précis au lieu de taches horribles. The Scream est beaucoup moins affecté.

Camaro - Mustang

Images sources provenant du post de flawr.

Camaro :

Mustang :

Palette de Camaro

Ça a l'air plutôt bien sans tremblement.

Un dither "serré" (même facteur de détail que ci-dessus) ne change pas grand chose, il ajoute juste un peu de détail dans les hautes lumières sur le capot et le toit.

Un dither "lâche" (facteur de détail abaissé à 6) lisse vraiment la tonalité, et beaucoup plus de détails sont visibles à travers le pare-brise, mais les motifs de ditherng sont plus évidents partout.

Palette Mustang

Certaines parties de la voiture sont superbes, mais les pixels gris ont l'air glitchy. Pire, tous les pixels jaunes plus foncés ont été distribués sur la carrosserie rouge de la Camaro, et l'algorithme de non-dithering ne peut pas trouver quoi que ce soit à faire avec les plus clairs (les déplacer dans la voiture rendrait la correspondance pire, et les déplacer à un autre endroit sur l'arrière-plan ne fait aucune différence nette), donc il y a une Mustang fantôme à l'arrière-plan.

Le tramage est en mesure de répartir ces pixels jaunes supplémentaires afin qu'ils ne se touchent pas, les dispersant plus ou moins uniformément sur l'arrière-plan dans le processus. Les hautes lumières et les ombres sur la voiture ont l'air un peu mieux.

Encore une fois, le dither loose a la tonalité la plus régulière, révèle plus de détails sur les phares et le pare-brise. La voiture semble presque rouge à nouveau. L'arrière-plan est plus maladroit pour une raison quelconque. Pas sûr que j'aime ça.

Vidéo


(Lien HQ)



Utilisez notre moteur de recherche

Ricerca
Generic filters

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.