Java - еще один картографический подход
Редактировать 1: После того, как это было распространено в «математической» среде на G +, мы все, кажется, используем подходы с различными способами, чтобы обойти сложность.
Изменить 2: я испортил изображения на моем диске Google и перезапустил, поэтому старые ссылки больше не работают. Извините, я все еще работаю над созданием репутации для большего количества ссылок.
Редактировать 3: Читая другие посты, я получил некоторые вдохновения. Теперь я получил программу быстрее и реинвестировал некоторое время процессора, чтобы внести некоторые изменения в зависимости от расположения целевого образа.
Изменить 4: Новая версия программы. Быстрее! Специальная обработка обеих областей с острыми углами и очень плавными изменениями (очень помогает при трассировке лучей, но иногда дает Моне Лизе красные глаза)! Возможность генерировать промежуточные кадры из анимации!
Мне очень понравилась идея, и решение Quincunx заинтриговало меня. Поэтому я подумал, что смогу добавить свои 2 евроцента.
Идея заключалась в том, что нам, очевидно, необходимо (как-то близкое) сопоставление двух цветовых палитр.
С этой идеей я провел первую ночь, пытаясь настроить алгоритм стабильного брака, чтобы он работал быстро и с памятью моего компьютера на 123520 кандидатов. Пока я попал в область памяти, я обнаружил, что проблема времени выполнения неразрешима.
Вторую ночь я решил пойти еще дальше и погрузиться в венгерский алгоритм, который обещал обеспечить даже свойства аппроксимации, то есть минимальное расстояние между цветами в любом изображении. К счастью, я нашел 3 готовых к реализации Java-реализации этого (не считая многих полу-законченных студенческих заданий, которые начинают усложнять поиск элементарных алгоритмов в Google). Но, как можно было ожидать, венгерские алгоритмы еще хуже с точки зрения времени работы и использования памяти. Хуже того, все 3 реализации, которые я тестировал, иногда возвращали неверные результаты. Я дрожу, когда думаю о других программах, которые могут основываться на них.
Третий подход (конец второй ночи) был легким, быстрым, быстрым и, в конце концов, не таким уж и плохим: сортировка цветов на обоих изображениях по яркости и простой карте по ранжированию, т.е. от самого темного к самому темному, от второго темного к второму темному. Это сразу создает четкую черно-белую реконструкцию с разбрызгиванием случайного цвета.
* Подход 4 и последний к настоящему моменту (утро второй ночи) начинается с приведенного выше отображения яркости и добавляет к нему локальные поправки, применяя венгерские алгоритмы к различным перекрывающимся последовательностям пикселей. Таким образом, я получил лучшее отображение и работал над сложностью проблемы и ошибками в реализациях.
Итак, вот некоторый Java-код, некоторые части могут выглядеть аналогично другому Java-коду, опубликованному здесь. Используемый венгерский язык является исправленной версией Джона Миллерса, первоначально в проекте ontologyS Similariy. Это был самый быстрый способ, который я нашел и показал наименьшее количество ошибок.
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);
}
}
}
}
Текущее время работы составляет от 20 до 30 секунд для каждой пары изображений, но есть множество настроек, которые заставляют его работать быстрее или, возможно, получить немного более качественное изображение.
Похоже, что моей репутации новичка недостаточно для такого количества ссылок / изображений, поэтому вот текстовый ярлык к моей папке на дисках Google для образцов изображений: http://goo.gl/qZHTao
Сначала я хотел показать образцы:
Люди -> Мона Лиза http://goo.gl/mGvq9h
Программа отслеживает все координаты точек, но сейчас я чувствую себя измотанным и пока не планирую делать анимацию. Если бы я потратил больше времени, я мог бы сам сделать венгерский алгоритм или настроить график локальной оптимизации своей программы.