平滑锯齿状路径

前几天我正在参与线程图像/图形到一个形状 ,并通过迭代地向Area添加一个Rectangle来尝试获取图像的轮廓。 那非常慢。

此示例改为构建GeneralPath并从GP创建Area 。 快多了。

用于处理的样本图像

左上角的图像是“源图像”。 右边的两个是处理轮廓的各个阶段。 它们都在圆周围并沿着三角形的倾斜边缘具有锯齿状边缘。

我希望获得一种能够消除或减少锯齿状的形状。

在ASCII艺术中。

情况1:

  1234 1 ** 2 ** 3 *** 4 *** 5 **** 6 **** 

角落在:

  • (2,3)内角
  • (3,3)
  • (3,5)内角
  • (4,5)

案例2:

  1234 1 **** 2 **** 3 ** 4 ** 5 **** 6 **** 

角落在:

  • (4,2)
  • (2,2)内角
  • (2,5)内角
  • (4,5)

假设我们的路径具有显示的形状和列出的点,我想删除第一组的“内角”点,同时保留“内部角落”(图像中的一对)第2位。


  • 任何人都可以建议一些聪明的内置方法来完成这项工作的繁重工作吗?
  • 如果做不到这一点,那么识别内角的位置和性质(对/单)的好方法是什么? (我猜我可以得到一个PathIterator并构建一个新的GeneralPath删除奇异的内角 – 如果我能想出如何识别它们的话!)。

以下是要使用的代码:

 import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.awt.geom.*; import javax.swing.*; import javax.swing.border.*; import javax.swing.event.*; /* Gain the outline of an image for further processing. */ class ImageOutline { private BufferedImage image; private TwoToneImageFilter twoToneFilter; private BufferedImage imageTwoTone; private JLabel labelTwoTone; private BufferedImage imageOutline; private Area areaOutline = null; private JLabel labelOutline; private JLabel targetColor; private JSlider tolerance; private JProgressBar progress; private SwingWorker sw; public ImageOutline(BufferedImage image) { this.image = image; imageTwoTone = new BufferedImage( image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); } public void drawOutline() { if (areaOutline!=null) { Graphics2D g = imageOutline.createGraphics(); g.setColor(Color.WHITE); g.fillRect(0,0,imageOutline.getWidth(),imageOutline.getHeight()); g.setColor(Color.RED); g.setClip(areaOutline); g.fillRect(0,0,imageOutline.getWidth(),imageOutline.getHeight()); g.setColor(Color.BLACK); g.setClip(null); g.draw(areaOutline); g.dispose(); } } public Area getOutline(Color target, BufferedImage bi) { // construct the GeneralPath GeneralPath gp = new GeneralPath(); boolean cont = false; int targetRGB = target.getRGB(); for (int xx=0; xx<bi.getWidth(); xx++) { for (int yy=0; yy<bi.getHeight(); yy++) { if (bi.getRGB(xx,yy)==targetRGB) { if (cont) { gp.lineTo(xx,yy); gp.lineTo(xx,yy+1); gp.lineTo(xx+1,yy+1); gp.lineTo(xx+1,yy); gp.lineTo(xx,yy); } else { gp.moveTo(xx,yy); } cont = true; } else { cont = false; } } cont = false; } gp.closePath(); // construct the Area from the GP & return it return new Area(gp); } public JPanel getGui() { JPanel images = new JPanel(new GridLayout(2,2,2,2)); JPanel gui = new JPanel(new BorderLayout(3,3)); JPanel originalImage = new JPanel(new BorderLayout(2,2)); final JLabel originalLabel = new JLabel(new ImageIcon(image)); targetColor = new JLabel("Target Color"); targetColor.setForeground(Color.RED); targetColor.setBackground(Color.WHITE); targetColor.setBorder(new LineBorder(Color.BLACK)); targetColor.setOpaque(true); JPanel controls = new JPanel(new BorderLayout()); controls.add(targetColor, BorderLayout.WEST); originalLabel.addMouseListener( new MouseAdapter() { @Override public void mouseEntered(MouseEvent me) { originalLabel.setCursor( Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); } @Override public void mouseExited(MouseEvent me) { originalLabel.setCursor(Cursor.getDefaultCursor()); } @Override public void mouseClicked(MouseEvent me) { int x = me.getX(); int y = me.getY(); Color c = new Color( image.getRGB(x,y) ); targetColor.setBackground( c ); updateImages(); } }); originalImage.add(originalLabel); tolerance = new JSlider( JSlider.HORIZONTAL, 0, 255, 104 ); tolerance.addChangeListener( new ChangeListener() { public void stateChanged(ChangeEvent ce) { updateImages(); } }); controls.add(tolerance, BorderLayout.CENTER); gui.add(controls,BorderLayout.NORTH); images.add(originalImage); labelTwoTone = new JLabel(new ImageIcon(imageTwoTone)); images.add(labelTwoTone); images.add(new JLabel("Smoothed Outline")); imageOutline = new BufferedImage( image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB ); labelOutline = new JLabel(new ImageIcon(imageOutline)); images.add(labelOutline); updateImages(); progress = new JProgressBar(); gui.add(images, BorderLayout.CENTER); gui.add(progress, BorderLayout.SOUTH); return gui; } private void updateImages() { if (sw!=null) { sw.cancel(true); } sw = new SwingWorker() { @Override public String doInBackground() { progress.setIndeterminate(true); adjustTwoToneImage(); labelTwoTone.repaint(); areaOutline = getOutline(Color.BLACK, imageTwoTone); drawOutline(); return ""; } @Override protected void done() { labelOutline.repaint(); progress.setIndeterminate(false); } }; sw.execute(); } public void adjustTwoToneImage() { twoToneFilter = new TwoToneImageFilter( targetColor.getBackground(), tolerance.getValue()); Graphics2D g = imageTwoTone.createGraphics(); g.drawImage(image, twoToneFilter, 0, 0); g.dispose(); } public static void main(String[] args) throws Exception { int size = 150; final BufferedImage outline = new BufferedImage(size,size,BufferedImage.TYPE_INT_RGB); Graphics2D g = outline.createGraphics(); g.setColor(Color.WHITE); g.fillRect(0,0,size,size); g.setRenderingHint( RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Polygon p = new Polygon(); p.addPoint(size/2, size/10); p.addPoint(size-10, size-10); p.addPoint(10, size-10); Area a = new Area(p); Rectangle r = new Rectangle(size/4, 8*size/10, size/2, 2*size/10); a.subtract(new Area(r)); int radius = size/10; Ellipse2D.Double c = new Ellipse2D.Double( (size/2)-radius, (size/2)-radius, 2*radius, 2*radius ); a.subtract(new Area(c)); g.setColor(Color.BLACK); g.fill(a); ImageOutline io = new ImageOutline(outline); JFrame f = new JFrame("Image Outline"); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.add(io.getGui()); f.pack(); f.setResizable(false); f.setLocationByPlatform(true); f.setVisible(true); } } class TwoToneImageFilter implements BufferedImageOp { Color target; int tolerance; TwoToneImageFilter(Color target, int tolerance) { this.target = target; this.tolerance = tolerance; } private boolean isIncluded(Color pixel) { int rT = target.getRed(); int gT = target.getGreen(); int bT = target.getBlue(); int rP = pixel.getRed(); int gP = pixel.getGreen(); int bP = pixel.getBlue(); return( (rP-tolerance<=rT) && (rT<=rP+tolerance) && (gP-tolerance<=gT) && (gT<=gP+tolerance) && (bP-tolerance<=bT) && (bT<=bP+tolerance) ); } public BufferedImage createCompatibleDestImage( BufferedImage src, ColorModel destCM) { BufferedImage bi = new BufferedImage( src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_RGB); return bi; } public BufferedImage filter( BufferedImage src, BufferedImage dest) { if (dest==null) { dest = createCompatibleDestImage(src, null); } for (int x=0; x<src.getWidth(); x++) { for (int y=0; y<src.getHeight(); y++) { Color pixel = new Color(src.getRGB(x,y)); Color write = Color.BLACK; if (isIncluded(pixel)) { write = Color.WHITE; } dest.setRGB(x,y,write.getRGB()); } } return dest; } public Rectangle2D getBounds2D(BufferedImage src) { return new Rectangle2D.Double(0, 0, src.getWidth(), src.getHeight()); } public Point2D getPoint2D( Point2D srcPt, Point2D dstPt) { // no co-ord translation return srcPt; } public RenderingHints getRenderingHints() { return null; } } 

这是一个很大的主题。 您可能会发现Johannes Kopf和Dani Lischinski的Depixelizing Pixel Art 1非常有用:它最近可读,包括以前工作的摘要,并详细解释了它们的方法。

另请参阅包含类似背景和video的 幻灯片 (!) 。

  1. 以下是“最近邻居”与“他们的技术”文档中的一些截图。 最近的邻居他们的结果

该问题的最一般版本是大多数计算机视觉管道的初始阶段之一。 它被称为图像分割。 它将图像分成视觉上相同的像素区域。 这些区域由“轮廓”分开(参见例如本文 ),这相当于沿着像素边界运行的图像的路径。

有一个简单的递归算法,用于将轮廓表示为定义的折线,使得其中的任何点都不会超过您选择的某个固定量(例如max_dev )。 通常它是1/2到2像素。

 function getPolyline(points [p0, p1, p2... pn] in a contour, max_dev) { if n <= 1 (there are only one or two pixels), return the whole contour Let pi, 0 <= i <= n, be the point farthest from the line segment p0<->pn if distance(pi, p0<->pn) < max_dev return [ p0 -> pn ] else return concat(getPolyline [ p0, ..., pi ], getPolyline [ pi, ..., pn] ) 

这背后的想法是你似乎有类似卡通的图像已经分段。 因此,如果您编写一个将边缘像素组合成链的简单搜索,您可以使用上面的算法将它们转换为平滑的线段链。 它们甚至可以用抗锯齿绘制。

如果您已经知道段或边缘尝试使用高斯或平均值或您自己的内核之一模糊并移动到您想要平滑的边缘。 这是一个快速的解决方案,可能不适合复杂的图像,但对于自绘,它很好。

Interesting Posts