Java - ще один підхід до картографування
Редагувати 1: Після того, як це було поділено в середовищі "математики" на G +, ми, здається, всі використовуємо відповідні підходи з різними способами обійти складність.
Редагування 2: Я переплутав зображення на своєму диску Google і перезапустив, тому старі посилання більше не працюють. Вибачте, я все ще працюю над більшою репутацією для отримання більше посилань.
Правка 3: Читаючи інші публікації, я отримав натхнення. Зараз я швидше отримав програму і вклав деякий час у процесор, щоб змінити зміни залежно від місця розташування цільового зображення.
Редагувати 4: Нова версія програми. Швидше! Спеціальна обробка обох областей із різкими кутами та дуже плавними змінами (дуже допомагає при простеженні променів, але дає Мона Лізі час від часу червоні очі)! Можливість генерування проміжних кадрів з анімації!
Мені дуже сподобалася ідея, і рішення Quincunx наче мене заінтригувало. Тому я подумав, що, можливо, я можу додати свої 2 євро цента.
Ідея полягала в тому, що нам очевидно потрібно (якось близько) зіставлення між двома кольоровими палітрами.
З цією ідеєю я провів першу ніч, намагаючись налаштувати стабільний алгоритм шлюбу для швидкого запуску та з пам’яттю мого ПК на 123520 кандидатів. Поки я потрапив у діапазон пам'яті, я виявив проблему виконання нерозв'язною.
Другу ніч я вирішив трохи далі і зануритися в угорський алгоритм, який обіцяв надати рівне наближення властивостей, тобто мінімальну відстань між кольорами в будь-якому зображенні. На щастя, я знайшов 3 готових для цього підключення Java (не рахуючи багатьох завдань напівфабрикатів, які починають ускладнювати Google для елементарних алгоритмів). Але, як можна було б очікувати, угорські алгоритми ще гірші за часом роботи та використанням пам'яті. Ще гірше, що всі 3 тестування, які я протестував, приводили до випадкових неправильних результатів. Я тремчу, коли я думаю про інші програми, які можуть бути засновані на них.
Третій підхід (кінець другої ночі) був легким, швидким, швидким і зрештою не таким вже й поганим: сортуйте кольори в обох зображеннях за яскравістю та простою картою за рейтингом, тобто найтемнішою до найтемнішої, другою - найтемнішою до другої темної. Це одразу створює чітку чорно-білу реконструкцію з деяким випадковим кольором, нанесеним навколо.
* Підхід 4 та остаточний поки що (ранок другої ночі) починається з вищевказаного відображення світності та додає до нього локальні виправлення, застосовуючи алгоритми Угорщини до різних послідовностей пікселів, що перекриваються. Таким чином я покращив картинг і вирішив як складність проблеми, так і помилки в реалізації.
Отже, ось якийсь код Java, деякі частини можуть виглядати схожим на інший Java-код, розміщений тут. Угорська мова - це виправлена версія Джона Міллера, спочатку в проекті онтологіїСіміларі. Це був найшвидший шлях, який я знайшов і показав найменшу кількість помилок.
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
Зразки, які я хотів показати першим:
Люди -> Mona Lisa http://goo.gl/mGvq9h
Програма відслідковує всі координати точок, але я зараз відчуваю себе виснаженим і наразі не планую робити анімацію. Якби я витрачав більше часу, я міг би самостійно зробити угорський алгоритм або змінити графік локальної оптимізації моєї програми.