8

在我的 2D 游戏中,我使用图形工具来创建以黑色表示的漂亮、平滑的地形: 在此处输入图像描述

用 java 编写的简单算法每 15 个像素查找一次黑色,创建以下一组线条(灰色):

在此处输入图像描述

如您所见,有些地方的映射非常糟糕,有些地方非常好。在其他情况下,不需要每 15 个像素采样一次,例如。如果地形平坦。

使用尽可能少的点将这条曲线转换为一组点 [线] 的最佳方法是什么?每 15 像素采样一次 = 55 FPS,10 像素 = 40 FPS

以下算法正在完成这项工作,从右到左采样,将可粘贴输出到代码数组中:

public void loadMapFile(String path) throws IOException {
    File mapFile = new File(path);
    image = ImageIO.read(mapFile);
    boolean black;
    System.out.print("{ ");

    int[] lastPoint = {0, 0};

    for (int x = image.getWidth()-1; x >= 0; x -= 15) {
        for (int y = 0; y < image.getHeight(); y++) {
            black = image.getRGB(x, y) == -16777216 ? true : false;

            if (black) {
                lastPoint[0] = x;
                lastPoint[1] = y;
                System.out.print("{" + (x) + ", " + (y) + "}, ");
                break;
            }

        }
    }

    System.out.println("}");
}

我在 Android 上开发,使用 Java 和 AndEngine

4

3 回答 3

2

这个问题与信号(例如声音)的数字化问题几乎相同,其基本规律是输入中的频率对于采样率而言太高的信号将不会反映在数字化输出中。所以问题是,如果您检查 30 个像素,然后按照 bmorris591 的建议测试中间,您可能会错过采样点之间的 7 个像素孔。这表明,如果您不能错过 10 个像素特征,则需要每 5 个像素扫描一次:您的采样率应该是信号中最高频率的两倍。

有助于改进算法的一件事是更好的 y 维搜索。目前您正在线性搜索天空和地形之间的交点,但二分搜索应该更快

int y = image.getHeight()/2; // Start searching from the middle of the image
int yIncr = y/2;
while (yIncr>0) {
    if (image.getRGB(x, y) == -16777216) {
        // We hit the terrain, to towards the sky
        y-=yIncr;
    } else {
        // We hit the sky, go towards the terrain
        y+=yIncr;
    }
    yIncr = yIncr/2;
}
// Make sure y is on the first terrain point: move y up or down a few pixels
// Only one of the following two loops will execute, and only one or two iterations max
while (image.getRGB(x, y) != -16777216) y++; 
while (image.getRGB(x, y-1) == -16777216) y--;

其他优化也是可能的。如果你知道你的地形没有悬崖,那么你只需要搜索从 lastY+maxDropoff 到 lastY-maxDropoff 的窗口。此外,如果您的地形永远不能与整个位图一样高,您也不需要搜索位图的顶部。这应该有助于释放一些 CPU 周期,您可以将其用于地形的更高分辨率 x 扫描。

于 2013-03-09T16:07:57.327 回答
2

我建议找到存在于白色和深色像素之间边界上的边界点。之后,我们可以将这些点数字化。为此,我们应该定义DELTA哪些指定我们应该跳过哪个点以及我们应该将哪个点添加到结果列表中。

DELTA = 3, Number of points = 223

在此处输入图像描述

DELTA = 5, Number of points = 136

在此处输入图像描述

DELTA = 10, Number of points = 70

在此处输入图像描述

下面,我放了源代码,它打印图像并寻找点。我希望,您将能够阅读它并找到解决问题的方法。

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class Program {

    public static void main(String[] args) throws IOException {
        BufferedImage image = ImageIO.read(new File("/home/michal/Desktop/FkXG1.png"));
        PathFinder pathFinder = new PathFinder(10);
        List<Point> borderPoints = pathFinder.findBorderPoints(image);
        System.out.println(Arrays.toString(borderPoints.toArray()));
        System.out.println(borderPoints.size());

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(new ImageBorderPanel(image, borderPoints));
        frame.pack();
        frame.setMinimumSize(new Dimension(image.getWidth(), image.getHeight()));
        frame.setVisible(true);
    }
}

class PathFinder {

    private int maxDelta = 3;

    public PathFinder(int delta) {
        this.maxDelta = delta;
    }

    public List<Point> findBorderPoints(BufferedImage image) {
        int width = image.getWidth();
        int[][] imageInBytes = convertTo2DWithoutUsingGetRGB(image);
        int[] borderPoints = findBorderPoints(width, imageInBytes);

        List<Integer> indexes = dwindlePoints(width, borderPoints);
        List<Point> points = new ArrayList<Point>(indexes.size());
        for (Integer index : indexes) {
            points.add(new Point(index, borderPoints[index]));
        }
        return points;
    }

    private List<Integer> dwindlePoints(int width, int[] borderPoints) {
        List<Integer> indexes = new ArrayList<Integer>(width);
        indexes.add(borderPoints[0]);
        int delta = 0;
        for (int index = 1; index < width; index++) {
            delta += Math.abs(borderPoints[index - 1] - borderPoints[index]);
            if (delta >= maxDelta) {
                indexes.add(index);
                delta = 0;
            }
        }
        return indexes;
    }

    private int[] findBorderPoints(int width, int[][] imageInBytes) {
        int[] borderPoints = new int[width];
        int black = Color.BLACK.getRGB();
        for (int y = 0; y < imageInBytes.length; y++) {
            int maxX = imageInBytes[y].length;
            for (int x = 0; x < maxX; x++) {
                int color = imageInBytes[y][x];
                if (color == black && borderPoints[x] == 0) {
                    borderPoints[x] = y;
                }
            }
        }
        return borderPoints;
    }

    private int[][] convertTo2DWithoutUsingGetRGB(BufferedImage image) {
        final byte[] pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
        final int width = image.getWidth();
        final int height = image.getHeight();
        final boolean hasAlphaChannel = image.getAlphaRaster() != null;

        int[][] result = new int[height][width];
        if (hasAlphaChannel) {
            final int pixelLength = 4;
            for (int pixel = 0, row = 0, col = 0; pixel < pixels.length; pixel += pixelLength) {
                int argb = 0;
                argb += (((int) pixels[pixel] & 0xff) << 24); // alpha
                argb += ((int) pixels[pixel + 1] & 0xff); // blue
                argb += (((int) pixels[pixel + 2] & 0xff) << 8); // green
                argb += (((int) pixels[pixel + 3] & 0xff) << 16); // red
                result[row][col] = argb;
                col++;
                if (col == width) {
                    col = 0;
                    row++;
                }
            }
        } else {
            final int pixelLength = 3;
            for (int pixel = 0, row = 0, col = 0; pixel < pixels.length; pixel += pixelLength) {
                int argb = 0;
                argb += -16777216; // 255 alpha
                argb += ((int) pixels[pixel] & 0xff); // blue
                argb += (((int) pixels[pixel + 1] & 0xff) << 8); // green
                argb += (((int) pixels[pixel + 2] & 0xff) << 16); // red
                result[row][col] = argb;
                col++;
                if (col == width) {
                    col = 0;
                    row++;
                }
            }
        }

        return result;
    }
}

class ImageBorderPanel extends JPanel {

    private static final long serialVersionUID = 1L;

    private BufferedImage image;
    private List<Point> borderPoints;

    public ImageBorderPanel(BufferedImage image, List<Point> borderPoints) {
        this.image = image;
        this.borderPoints = borderPoints;
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(image, 0, 0, null);

        Graphics2D graphics2d = (Graphics2D) g;

        g.setColor(Color.YELLOW);
        for (Point point : borderPoints) {
            graphics2d.fillRect(point.x, point.y, 3, 3);
        }
    }
}

在我的源代码中,我使用了这个问题的示例:

于 2013-03-09T16:51:02.497 回答
2

最有效的解决方案(相对于所需的点)是允许沿 X 轴的点之间的间距可变。这样,一个大的平坦部分将需要很少的点/样本,而复杂的地形将使用更多。

在 3D 网格处理中,有一个很好的网格简化算法,名为“二次边缘崩溃”,您可以根据自己的问题进行调整。

这是这个想法,转化为您的问题 - 它实际上比原始 3D 算法简单得多:

  1. 用太多的点来表示你的曲线。
  2. 对于每个点,如果您删除它,则测量误差(即与平滑地形的差异)。
  3. 删除给出最小误差的点。
  4. 重复直到您将点数减少到足够多或错误变得太大。

更准确地说,关于第 2 步:给定点P, Q, R,误差Q是用两条直线近似地形P->QQ->R和仅用一条直线近似地形之间的差异P->R

请注意,当一个点被删除时,只有它的邻居需要更新它们的错误值。

于 2013-03-09T16:19:59.180 回答