2014-10-24 2 views
3

(Извините за длинный пост ... по крайней мере, у него есть фотографии?)Java 8 + Swing: Как рисовать Промывать многоугольники

Я написал алгоритм, который создает мозаику из изображения статистически генераций N выпуклые многоугольники, которые покрывают изображение без перекрытия. Эти полигоны имеют расстояние между 3-8 сторонами, а каждая сторона имеет угол, кратный 45 градусам. Эти многоугольники хранятся внутри как прямоугольник с смещениями для каждого угла. Ниже изображение, которое объясняет, как это работает:

enter image description here

getRight() возвращает x + width - 1 и getBottom() возвращается y + height - 1. Класс предназначен для поддержания жесткой ограничивающей рамки вокруг заполненных пикселей, поэтому координаты, показанные на этом изображении, являются правильными. Обратите внимание, что width >= ul + ur + 1, width >= ll + lr + 1, height >= ul + ll + 1 и height >= ur + ul + 1, или на стороне будут пустые пиксели. Обратите также внимание на то, что смещение угла может быть равным 0, что указывает на то, что все пиксели заполнены в этом углу. Это позволяет этому представлению хранить 3-8-сторонние выпуклые многоугольники, каждая из сторон которых имеет по крайней мере один пиксель в длину.

Хотя приятно математически представлять эти регионы, я хочу нарисовать их, чтобы я мог их видеть. Используя простую лямбду и метод, который выполняет итерацию по каждому пикселю в многоугольнике, я могу сделать изображение идеально. В качестве примера ниже приведено значение Claude Monet's Woman with a Parasol с использованием 99 полигонов, разрешающих все направления разделения.

enter image description here

Код, который делает это изображение выглядит следующим образом:

public void drawOnto(Graphics graphics) { 
    graphics.setColor(getColor()); 
    forEach(
     (i, j) -> { 
      graphics.fillRect(x + i, y + j, 1, 1); 
     } 
    ); 
} 

private void forEach(PerPixel algorithm) { 
    for (int j = 0; j < height; ++j) { 
     int nj = height - 1 - j; 

     int minX; 
     if (j < ul) { 
      minX = ul - j; 
     } else if (nj < ll) { 
      minX = ll - nj; 
     } else { 
      minX = 0; 
     } 

     int maxX = width; 
     if (j < ur) { 
      maxX -= ur - j; 
     } else if (nj < lr) { 
      maxX -= lr - nj; 
     } 

     for (int i = minX; i < maxX; ++i) { 
      algorithm.perform(i, j); 
     } 
    } 
} 

Однако, это не является идеальным по многим причинам. Во-первых, концепция графического представления многоугольника теперь является частью самого класса; лучше разрешить другим классам, чья задача состоит в том, чтобы представить эти полигоны. Во-вторых, это влечет за собой много звонков на fillRect(), чтобы нарисовать один пиксель. Наконец, я хочу иметь возможность разрабатывать другие методы рендеринга этих полигонов, чем рисовать их как-есть (например, performing weighted interpolation over the Voronoi tessellation represented by the polygons' centers).

Все это указывает на генерацию java.awt.Polygon, которая представляет собой вершины многоугольника (который я назвал Region, чтобы отличать класс Polygon). Нет проблем; Я написал метод, чтобы сгенерировать Polygon, который имеет вышеуказанные углы без каких-либо дубликатов для обработки случаев, что смещение равно 0 или что сторона имеет только один пиксель на нем:

public Polygon getPolygon() { 
    int[] xes = { 
     x + ul, 
     getRight() - ur, 
     getRight(), 
     getRight(), 
     getRight() - lr, 
     x + ll, 
     x, 
     x 
    }; 
    int[] yes = { 
     y, 
     y, 
     y + ur, 
     getBottom() - lr, 
     getBottom(), 
     getBottom(), 
     getBottom() - ll, 
     y + ul 
    }; 

    int[] keptXes = new int[8]; 
    int[] keptYes = new int[8]; 
    int length = 0; 
    for (int i = 0; i < 8; ++i) { 
     if (
      length == 0 || 
      keptXes[length - 1] != xes[i] || 
      keptYes[length - 1] != yes[i] 
     ) { 
      keptXes[length] = xes[i]; 
      keptYes[length] = yes[i]; 
      length++; 
     } 
    } 

    return new Polygon(keptXes, keptYes, length); 
} 

Проблема заключается в том, что, когда я попробуйте использовать такой Polygon с методом Graphics.fillPolygon(), он не заполняет все пиксели! Ниже та же мозаика визуализации с этим другим способом:

enter image description here

Так у меня есть несколько взаимосвязанных вопросов по поводу этого поведения:

  1. Почему Polygon класса не заполняет во всех этих пикселях, хотя углы простые кратные 45 градусов?

  2. Как я могу последовательно кодировать этот дефект (насколько мое приложение) в моих средствах визуализации, чтобы я мог использовать мой метод getPolygon() как есть? Я не хочу менять вершины, которые он выводит, потому что мне нужно, чтобы они были точными для вычисления центра масс.


MCE

Если вышеуказанные фрагменты кода и изображения не достаточно, чтобы объяснить эту проблему, я добавил Minimal, полный и Проверяемость пример, который демонстрирует поведение я описал выше.

package com.sadakatsu.mce; 

import java.awt.Color; 
import java.awt.Graphics; 
import java.awt.Polygon; 
import java.awt.image.BufferedImage; 
import java.io.File; 
import java.io.IOException; 

import javax.imageio.ImageIO; 

public class Main { 
    @FunctionalInterface 
    private static interface PerPixel { 
     void perform(int x, int y); 
    } 

    private static class Region { 
     private int height; 
     private int ll; 
     private int lr; 
     private int width; 
     private int ul; 
     private int ur; 
     private int x; 
     private int y; 

     public Region(
      int x, 
      int y, 
      int width, 
      int height, 
      int ul, 
      int ur, 
      int ll, 
      int lr 
     ) { 
      if (
       width < 0 || width <= ll + lr || width <= ul + ur || 
       height < 0 || height <= ul + ll || height <= ur + lr || 
       ul < 0 || 
       ur < 0 || 
       ll < 0 || 
       lr < 0 
      ) { 
       throw new IllegalArgumentException(); 
      } 

      this.height = height; 
      this.ll = ll; 
      this.lr = lr; 
      this.width = width; 
      this.ul = ul; 
      this.ur = ur; 
      this.x = x; 
      this.y = y; 
     } 

     public Color getColor() { 
      return Color.BLACK; 
     } 

     public int getBottom() { 
      return y + height - 1; 
     } 

     public int getRight() { 
      return x + width - 1; 
     } 

     public Polygon getPolygon() { 
      int[] xes = { 
       x + ul, 
       getRight() - ur, 
       getRight(), 
       getRight(), 
       getRight() - lr, 
       x + ll, 
       x, 
       x 
      }; 
      int[] yes = { 
       y, 
       y, 
       y + ur, 
       getBottom() - lr, 
       getBottom(), 
       getBottom(), 
       getBottom() - ll, 
       y + ul 
      }; 

      int[] keptXes = new int[8]; 
      int[] keptYes = new int[8]; 
      int length = 0; 
      for (int i = 0; i < 8; ++i) { 
       if (
        length == 0 || 
        keptXes[length - 1] != xes[i] || 
        keptYes[length - 1] != yes[i] 
       ) { 
        keptXes[length] = xes[i]; 
        keptYes[length] = yes[i]; 
        length++; 
       } 
      } 

      return new Polygon(keptXes, keptYes, length); 
     } 

     public void drawOnto(Graphics graphics) { 
      graphics.setColor(getColor()); 
      forEach(
       (i, j) -> { 
        graphics.fillRect(x + i, y + j, 1, 1); 
       } 
      ); 
     } 

     private void forEach(PerPixel algorithm) { 
      for (int j = 0; j < height; ++j) { 
       int nj = height - 1 - j; 

       int minX; 
       if (j < ul) { 
        minX = ul - j; 
       } else if (nj < ll) { 
        minX = ll - nj; 
       } else { 
        minX = 0; 
       } 

       int maxX = width; 
       if (j < ur) { 
        maxX -= ur - j; 
       } else if (nj < lr) { 
        maxX -= lr - nj; 
       } 

       for (int i = minX; i < maxX; ++i) { 
        algorithm.perform(i, j); 
       } 
      } 
     } 
    } 

    public static void main(String[] args) throws IOException { 
     int width = 10; 
     int height = 8; 

     Region region = new Region(0, 0, 10, 8, 2, 3, 4, 1); 

     BufferedImage image = new BufferedImage(
      width, 
      height, 
      BufferedImage.TYPE_3BYTE_BGR 
     ); 
     Graphics graphics = image.getGraphics(); 
     graphics.setColor(Color.WHITE); 
     graphics.fillRect(0, 0, width, height); 
     region.drawOnto(graphics); 
     ImageIO.write(image, "PNG", new File("expected.png")); 

     image = new BufferedImage(
      width, 
      height, 
      BufferedImage.TYPE_3BYTE_BGR 
     ); 
     graphics = image.getGraphics(); 
     graphics.setColor(Color.WHITE); 
     graphics.fillRect(0, 0, width, height); 
     graphics.setColor(Color.BLACK); 
     graphics.fillPolygon(region.getPolygon()); 
     ImageIO.write(image, "PNG", new File("got.png")); 
    } 
} 
+0

Это самая крутая вещь, которую я видел весь день :-) +1 –

+0

Это смотрит на меня, как будто определение интерьер многоугольника в * вашем * коде отличается от кода java.awt.Polygon; оба правильны в * своей * системе отсчета, они просто разные определения. Вам нужно будет преобразовать свое определение в Polygon; никоим образом не обойти это, если вы хотите использовать Polygon ... – Durandal

+0

@Durandal: Это правда. Моя проблема в том, что я еще не нашел объяснений, как работает этот подход. Если я смогу научиться рифме и причине объяснить, почему иногда она генерирует одни и те же результаты, а другие - нет, я смогу добиться прогресса. До сих пор я не нашел этого объяснения. – sadakatsu

ответ

0

Я провел весь день, работая над ним, и у меня, похоже, есть исправление для этого. Ключ был найден в документации для Shape класса, который гласит:

Определение insideness: Точка считается лежать внутри Shape, если и только если:

  • лежит полностью внутри границы раздела или

  • он расположен точно на границе формы, а пространство, непосредственно прилегающее к точке в возрастающем направлении X, полностью находится внутри границы или

  • он расположен точно на горизонтальном граничном сегменте, а пространство, непосредственно смежное с точкой в ​​возрастающем направлении Y, находится внутри границы.

На самом деле, этот текст немного вводит в заблуждение; третий случай переопределяет второй (т. е. даже если пиксель в горизонтальном граничном сегменте на дне Shape имеет заполненную точку справа, он все равно не будет заполнен). Представлял изобразительно, то Polygon ниже не нарисует х «Ed из пикселей:

enter image description here

красный, зеленый и синий пиксели являются частью Polygon; остальные - нет. Синие пиксели подпадают под первый случай, зеленые пиксели подпадают под второй случай, а красные пиксели подпадают под третий случай. Обратите внимание, что все правые и нижние пиксели вдоль выпуклого корпуса НЕ нарисованы. Чтобы заставить их рисовать, вам нужно переместить вершины на оранжевые пиксели, как показано, чтобы сделать новую самую правую/самую нижнюю часть выпуклого корпуса.

Самый простой способ сделать это - использовать метод camickr: используйте как fillPolygon(), так и drawPolygon(). По крайней мере, в случае моих выпуклых корпусов с 45-градусным краем, drawPolygon() точно рисует линии до вершин (и, вероятно, для других случаев), и таким образом заполняет пиксели, которые пропускают fillPolygon(). Тем не менее, ни fillPolygon(), ни drawPolygon() не нарисуют одноточечный Polygon, поэтому для этого нужно закодировать специальный случай.

Фактическое решение, которое я разработал в попытке понять insideness определения выше было создать другую Polygon с измененными углами, как показано на рисунке. Он имеет преимущество (?) Вызова библиотеки чертежей только один раз и автоматически обрабатывает специальный случай. Это, вероятно, на самом деле не является оптимальным, но вот код, который я использовал для чьего рассмотрения:

package com.sadakatsu.mosaic.renderer; 

import java.awt.Polygon; 
import java.util.Arrays; 

import com.sadakatsu.mosaic.Region; 

public class RegionPolygon extends Polygon { 
    public RegionPolygon(Region region) { 
     int bottom = region.getBottom(); 
     int ll = region.getLL(); 
     int lr = region.getLR(); 
     int right = region.getRight(); 
     int ul = region.getUL(); 
     int ur = region.getUR(); 
     int x = region.getX(); 
     int y = region.getY(); 

     int[] xes = { 
      x + ul, 
      right - ur + 1, 
      right + 1, 
      right + 1, 
      right - lr, 
      x + ll + 1, 
      x, 
      x 
     }; 

     int[] yes = { 
      y, 
      y, 
      y + ur, 
      bottom - lr, 
      bottom + 1, 
      bottom + 1, 
      bottom - ll, 
      y + ul 
     }; 

     npoints = 0; 
     xpoints = new int[xes.length]; 
     ypoints = new int[xes.length]; 
     for (int i = 0; i < xes.length; ++i) { 
      if (
       i == 0 || 
       xpoints[npoints - 1] != xes[i] || 
       ypoints[npoints - 1] != yes[i] 
      ) { 
       addPoint(xes[i], yes[i]); 
      } 
     } 
    } 
} 
Смежные вопросы