Java - Outra Abordagem de Mapeamento
Edit 1: Depois que isso foi compartilhado em um ambiente de "matemática" no G +, todos nós parecemos usar abordagens correspondentes de várias maneiras para contornar a complexidade.
Edição 2: eu estraguei as imagens no meu google drive e reiniciei, para que os links antigos não funcionem mais. Desculpe, ainda estou trabalhando em mais reputação por mais links.
Edição 3: Lendo as outras postagens, recebi algumas inspirações. Eu peguei o programa mais rápido agora e reinvesti algum tempo de CPU, para fazer algumas alterações, dependendo da localização da imagem de destino.
Edição 4: Nova versão do programa. Mais rápido! Tratamento especial de ambas as áreas com cantos afiados e mudanças muito suaves (ajuda muito no traçado de raios, mas dá ocasionalmente à Mona Lisa olhos vermelhos)! Capacidade de gerar quadros intermediários a partir de animações!
Eu realmente amei a idéia e a solução Quincunx me intrigou. Por isso, pensei que poderia adicionar meus 2 centavos de euro.
A idéia era que, obviamente, precisamos de um mapeamento (de alguma forma próximo) entre duas paletas de cores.
Com essa idéia, passei a primeira noite tentando criar um algoritmo estável de casamento para rodar rápido e com a memória do meu PC em 123520 candidatos. Enquanto eu chegava ao intervalo de memória, achei o problema de tempo de execução insolúvel.
Segunda noite, eu decidi me aprofundar e mergulhar no algoritmo húngaro, que prometia fornecer propriedades de aproximação uniformes, ou seja, distância mínima entre as cores em qualquer imagem. Felizmente, eu encontrei três prontas para implementar implementações Java disso (sem contar muitas tarefas semi-acabadas dos alunos, que começam a dificultar muito o google para algoritmos elementares). Mas, como seria de esperar, os algoritmos húngaros são ainda piores em termos de tempo de execução e uso de memória. Pior ainda, todas as três implementações que testei, retornaram resultados errados ocasionais. Tremo quando penso em outros programas, que podem ser baseados neles.
Terceira abordagem (final da segunda noite) foi fácil, rápida, rápida e, afinal, não é tão ruim assim: classifique as cores nas duas imagens por luminosidade e simples mapa por classificação, ou seja, mais escuro para mais escuro, segundo mais escuro para o segundo mais escuro. Isso cria imediatamente uma reconstrução em preto e branco de aparência nítida, com algumas cores aleatórias espalhadas.
* A abordagem 4 e final até agora (manhã da segunda noite) começa com o mapeamento de luminosidade acima e adiciona correções locais, aplicando algoritmos húngaros a várias seqüências de pixels sobrepostas. Dessa maneira, obtive um mapeamento melhor e trabalhei com a complexidade do problema e os bugs nas implementações.
Então, aqui está um código Java, algumas partes podem parecer semelhantes a outros códigos Java postados aqui. O húngaro usado é uma versão remendada de John Millers, originalmente no projeto ontologySimilariy. Esta foi a maneira mais rápida que encontrei e mostrou o menor número de bugs.
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import javax.imageio.ImageIO;
/**
*
*/
public class PixelRearranger {
private final String mode;
public PixelRearranger(String mode)
{
this.mode = mode;
}
public final static class Pixel {
final BufferedImage img;
final int val;
final int r, g, b;
final int x, y;
public Pixel(BufferedImage img, int x, int y) {
this.x = x;
this.y = y;
this.img = img;
if ( img != null ) {
val = img.getRGB(x,y);
r = ((val & 0xFF0000) >> 16);
g = ((val & 0x00FF00) >> 8);
b = ((val & 0x0000FF));
} else {
val = r = g = b = 0;
}
}
@Override
public int hashCode() {
return x + img.getWidth() * y + img.hashCode();
}
@Override
public boolean equals(Object o) {
if ( !(o instanceof Pixel) ) return false;
Pixel p2 = (Pixel) o;
return p2.x == x && p2.y == y && p2.img == img;
}
public double cd() {
double x0 = 0.5 * (img.getWidth()-1);
double y0 = 0.5 * (img.getHeight()-1);
return Math.sqrt(Math.sqrt((x-x0)*(x-x0)/x0 + (y-y0)*(y-y0)/y0));
}
@Override
public String toString() { return "P["+r+","+g+","+b+";"+x+":"+y+";"+img.getWidth()+":"+img.getHeight()+"]"; }
}
public final static class Pair
implements Comparable<Pair>
{
public Pixel palette, from;
public double d;
public Pair(Pixel palette, Pixel from)
{
this.palette = palette;
this.from = from;
this.d = distance(palette, from);
}
@Override
public int compareTo(Pair e2)
{
return sgn(e2.d - d);
}
@Override
public String toString() { return "E["+palette+from+";"+d+"]"; }
}
public static int sgn(double d) { return d > 0.0 ? +1 : d < 0.0 ? -1 : 0; }
public final static int distance(Pixel p, Pixel q)
{
return 3*(p.r-q.r)*(p.r-q.r) + 6*(p.g-q.g)*(p.g-q.g) + (p.b-q.b)*(p.b-q.b);
}
public final static Comparator<Pixel> LUMOSITY_COMP = (p1,p2) -> 3*(p1.r-p2.r)+6*(p1.g-p2.g)+(p1.b-p2.b);
public final static class ArrangementResult
{
private List<Pair> pairs;
public ArrangementResult(List<Pair> pairs)
{
this.pairs = pairs;
}
/** Provide the output image */
public BufferedImage finalImage()
{
BufferedImage target = pairs.get(0).from.img;
BufferedImage res = new BufferedImage(target.getWidth(),
target.getHeight(), BufferedImage.TYPE_INT_RGB);
for(Pair p : pairs) {
Pixel left = p.from;
Pixel right = p.palette;
res.setRGB(left.x, left.y, right.val);
}
return res;
}
/** Provide an interpolated image. 0 le;= alpha le;= 1 */
public BufferedImage interpolateImage(double alpha)
{
BufferedImage target = pairs.get(0).from.img;
int wt = target.getWidth(), ht = target.getHeight();
BufferedImage palette = pairs.get(0).palette.img;
int wp = palette.getWidth(), hp = palette.getHeight();
int w = Math.max(wt, wp), h = Math.max(ht, hp);
BufferedImage res = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
int x0t = (w-wt)/2, y0t = (h-ht)/2;
int x0p = (w-wp)/2, y0p = (h-hp)/2;
double a0 = (3.0 - 2.0*alpha)*alpha*alpha;
double a1 = 1.0 - a0;
for(Pair p : pairs) {
Pixel left = p.from;
Pixel right = p.palette;
int x = (int) (a1 * (right.x + x0p) + a0 * (left.x + x0t));
int y = (int) (a1 * (right.y + y0p) + a0 * (left.y + y0t));
if ( x < 0 || x >= w ) System.out.println("x="+x+", w="+w+", alpha="+alpha);
if ( y < 0 || y >= h ) System.out.println("y="+y+", h="+h+", alpha="+alpha);
res.setRGB(x, y, right.val);
}
return res;
}
}
public ArrangementResult rearrange(BufferedImage target, BufferedImage palette)
{
List<Pixel> targetPixels = getColors(target);
int n = targetPixels.size();
System.out.println("total Pixels "+n);
Collections.sort(targetPixels, LUMOSITY_COMP);
final double[][] energy = energy(target);
List<Pixel> palettePixels = getColors(palette);
Collections.sort(palettePixels, LUMOSITY_COMP);
ArrayList<Pair> pairs = new ArrayList<>(n);
for(int i = 0; i < n; i++) {
Pixel pal = palettePixels.get(i);
Pixel to = targetPixels.get(i);
pairs.add(new Pair(pal, to));
}
correct(pairs, (p1,p2) -> sgn(p2.d*p2.from.b - p1.d*p1.from.b));
correct(pairs, (p1,p2) -> sgn(p2.d*p2.from.r - p1.d*p1.from.r));
// generates visible circular artifacts: correct(pairs, (p1,p2) -> sgn(p2.d*p2.from.cd() - p1.d*p1.from.cd()));
correct(pairs, (p1,p2) -> sgn(energy[p2.from.x][p2.from.y]*p2.d - energy[p1.from.x][p1.from.y]*p1.d));
correct(pairs, (p1,p2) -> sgn(p2.d/(1+energy[p2.from.x][p2.from.y]) - p1.d/(1+energy[p1.from.x][p1.from.y])));
// correct(pairs, null);
return new ArrangementResult(pairs);
}
/**
* derive an energy map, to detect areas of lots of change.
*/
public double[][] energy(BufferedImage img)
{
int n = img.getWidth();
int m = img.getHeight();
double[][] res = new double[n][m];
for(int x = 0; x < n; x++) {
for(int y = 0; y < m; y++) {
int rgb0 = img.getRGB(x,y);
int count = 0, sum = 0;
if ( x > 0 ) {
count++; sum += dist(rgb0, img.getRGB(x-1,y));
if ( y > 0 ) { count++; sum += dist(rgb0, img.getRGB(x-1,y-1)); }
if ( y < m-1 ) { count++; sum += dist(rgb0, img.getRGB(x-1,y+1)); }
}
if ( x < n-1 ) {
count++; sum += dist(rgb0, img.getRGB(x+1,y));
if ( y > 0 ) { count++; sum += dist(rgb0, img.getRGB(x+1,y-1)); }
if ( y < m-1 ) { count++; sum += dist(rgb0, img.getRGB(x+1,y+1)); }
}
if ( y > 0 ) { count++; sum += dist(rgb0, img.getRGB(x,y-1)); }
if ( y < m-1 ) { count++; sum += dist(rgb0, img.getRGB(x,y+1)); }
res[x][y] = Math.sqrt((double)sum/count);
}
}
return res;
}
public int dist(int rgb0, int rgb1) {
int r0 = ((rgb0 & 0xFF0000) >> 16);
int g0 = ((rgb0 & 0x00FF00) >> 8);
int b0 = ((rgb0 & 0x0000FF));
int r1 = ((rgb1 & 0xFF0000) >> 16);
int g1 = ((rgb1 & 0x00FF00) >> 8);
int b1 = ((rgb1 & 0x0000FF));
return 3*(r0-r1)*(r0-r1) + 6*(g0-g1)*(g0-g1) + (b0-b1)*(b0-b1);
}
private void correct(ArrayList<Pair> pairs, Comparator<Pair> comp)
{
Collections.sort(pairs, comp);
int n = pairs.size();
int limit = Math.min(n, 133); // n / 1000;
int limit2 = Math.max(1, n / 3 - limit);
int step = (2*limit + 2)/3;
for(int base = 0; base < limit2; base += step ) {
List<Pixel> list1 = new ArrayList<>();
List<Pixel> list2 = new ArrayList<>();
for(int i = base; i < base+limit; i++) {
list1.add(pairs.get(i).from);
list2.add(pairs.get(i).palette);
}
Map<Pixel, Pixel> connection = rematch(list1, list2);
int i = base;
for(Pixel p : connection.keySet()) {
pairs.set(i++, new Pair(p, connection.get(p)));
}
}
}
/**
* Glue code to do an hungarian algorithm distance optimization.
*/
public Map<Pixel,Pixel> rematch(List<Pixel> liste1, List<Pixel> liste2)
{
int n = liste1.size();
double[][] cost = new double[n][n];
Set<Pixel> s1 = new HashSet<>(n);
Set<Pixel> s2 = new HashSet<>(n);
for(int i = 0; i < n; i++) {
Pixel ii = liste1.get(i);
for(int j = 0; j < n; j++) {
Pixel ij = liste2.get(j);
cost[i][j] = -distance(ii,ij);
}
}
Map<Pixel,Pixel> res = new HashMap<>();
int[] resArray = Hungarian.hungarian(cost);
for(int i = 0; i < resArray.length; i++) {
Pixel ii = liste1.get(i);
Pixel ij = liste2.get(resArray[i]);
res.put(ij, ii);
}
return res;
}
public static List<Pixel> getColors(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
List<Pixel> colors = new ArrayList<>(width * height);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
colors.add(new Pixel(img, x, y));
}
}
return colors;
}
public static List<Integer> getSortedTrueColors(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
List<Integer> 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;
}
public static void main(String[] args) throws Exception {
int i = 0;
String mode = args[i++];
PixelRearranger pr = new PixelRearranger(mode);
String a1 = args[i++];
File in1 = new File(a1);
String a2 = args[i++];
File in2 = new File(a2);
File out = new File(args[i++]);
//
BufferedImage target = ImageIO.read(in1);
BufferedImage palette = ImageIO.read(in2);
long t0 = System.currentTimeMillis();
ArrangementResult result = pr.rearrange(target, palette);
BufferedImage resultImg = result.finalImage();
long t1 = System.currentTimeMillis();
System.out.println("took "+0.001*(t1-t0)+" s");
ImageIO.write(resultImg, "png", out);
// Check validity
List<Integer> paletteColors = getSortedTrueColors(palette);
List<Integer> resultColors = getSortedTrueColors(resultImg);
System.out.println("validate="+paletteColors.equals(resultColors));
// In Mode A we do some animation!
if ( "A".equals(mode) ) {
for(int j = 0; j <= 50; j++) {
BufferedImage stepImg = result.interpolateImage(0.02 * j);
File oa = new File(String.format("anim/%s-%s-%02d.png", a1, a2, j));
ImageIO.write(stepImg, "png", oa);
}
}
}
}
O tempo de execução atual é de 20 a 30 segundos por par de imagens acima, mas há muitos ajustes para torná-lo mais rápido ou talvez obter um pouco mais de qualidade.
Parece que minha reputação de novato não é suficiente para tantos links / imagens, então aqui está um atalho de texto para minha pasta do Google drives para obter exemplos de imagens: http://goo.gl/qZHTao
As amostras que eu queria mostrar primeiro:
Pessoas -> Mona Lisa http://goo.gl/mGvq9h
O programa acompanha todas as coordenadas dos pontos, mas agora me sinto exausto e não pretendo fazer animações por enquanto. Se eu gastasse mais tempo, eu mesmo poderia executar um algoritmo húngaro ou ajustar a programação de otimização local do meu programa.