Обрезать изображение до минимального размера путем удаления прозрачных пикселей в Java - PullRequest
1 голос
/ 12 июля 2010

У меня есть лист спрайта, каждое изображение которого центрировано в ячейке 32x32.Фактические изображения не 32x32, но немного меньше.Я хотел бы взять ячейку и обрезать прозрачные пиксели, чтобы изображение было как можно меньше.

Как бы я это сделал в Java (JDK 6)?

Вот пример того, как я в настоящее время разбиваю лист плитки на ячейки:

BufferedImage tilesheet = ImageIO.read(getClass().getResourceAsStream("/sheet.png");
for (int i = 0; i < 15; i++) {
  Image img = tilesheet.getSubimage(i * 32, 0, 32, 32);
  // crop here..
}

Моя текущая идея состояла в том, чтобы проверить каждый пиксель из центра, работая на моем пути, чтобы убедиться, что он прозрачныйно мне было интересно, будет ли более быстрый / чистый способ сделать это.

Ответы [ 4 ]

5 голосов
/ 29 апреля 2016

image on transparent background

Существует тривиальное решение - сканировать каждый пиксель.Алгоритм ниже имеет постоянную производительность O(w•h).

private static BufferedImage trimImage(BufferedImage image) {
    int width = image.getWidth();
    int height = image.getHeight();
    int top = height / 2;
    int bottom = top;
    int left = width / 2 ;
    int right = left;
    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            if (image.getRGB(x, y) != 0){
                top    = Math.min(top, x);
                bottom = Math.max(bottom, x);
                left   = Math.min(left, x);
                right  = Math.max(right, x);
            }
        }
    }
    return image.getSubimage(left, top, right - left, bottom - top);
}

Но это гораздо эффективнее:

private static BufferedImage trimImage(BufferedImage image) {
    WritableRaster raster = image.getAlphaRaster();
    int width = raster.getWidth();
    int height = raster.getHeight();
    int left = 0;
    int top = 0;
    int right = width - 1;
    int bottom = height - 1;
    int minRight = width - 1;
    int minBottom = height - 1;

    top:
    for (;top < bottom; top++){
        for (int x = 0; x < width; x++){
            if (raster.getSample(x, top, 0) != 0){
                minRight = x;
                minBottom = top;
                break top;
            }
        }
    }

    left:
    for (;left < minRight; left++){
        for (int y = height - 1; y > top; y--){
            if (raster.getSample(left, y, 0) != 0){
                minBottom = y;
                break left;
            }
        }
    }

    bottom:
    for (;bottom > minBottom; bottom--){
        for (int x = width - 1; x >= left; x--){
            if (raster.getSample(x, bottom, 0) != 0){
                minRight = x;
                break bottom;
            }
        }
    }

    right:
    for (;right > minRight; right--){
        for (int y = bottom; y >= top; y--){
            if (raster.getSample(right, y, 0) != 0){
                break right;
            }
        }
    }

    return image.getSubimage(left, top, right - left + 1, bottom - top + 1);
}

Этот алгоритм следует идее pepan ответ (см. выше) и в 2-4 раза эффективнее.Разница в том, что он никогда не сканирует ни один пиксель дважды и пытается сократить диапазон поиска на каждом этапе.

Производительность метода в худшем случае равна O(w•h–a•b)

3 голосов
/ 02 июня 2014

Этот код работает для меня. Алгоритм прост, он перебирает слева / сверху / справа / снизу изображения и находит самый первый пиксель в столбце / строке, который не является прозрачным Затем он запоминает новый угол обрезанного изображения и, наконец, возвращает вспомогательное изображение исходного изображения.

Есть вещи, которые можно улучшить.

  1. Алгоритм ожидает, что в данных есть альфа-байт. Сбой индекса с исключением из массива, если его нет.

  2. Алгоритм предполагает, что на изображении есть хотя бы один непрозрачный пиксель. Это не удастся, если изображение полностью прозрачно.

    private static BufferedImage trimImage(BufferedImage img) {
    final byte[] pixels = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
    int width = img.getWidth();
    int height = img.getHeight();
    int x0, y0, x1, y1;                      // the new corners of the trimmed image
    int i, j;                                // i - horizontal iterator; j - vertical iterator
    leftLoop:
    for (i = 0; i < width; i++) {
        for (j = 0; j < height; j++) {
            if (pixels[(j*width+i)*4] != 0) { // alpha is the very first byte and then every fourth one
                break leftLoop;
            }
        }
    }
    x0 = i;
    topLoop:
    for (j = 0; j < height; j++) {
        for (i = 0; i < width; i++) {
            if (pixels[(j*width+i)*4] != 0) {
                break topLoop;
            }
        }
    }
    y0 = j;
    rightLoop:
    for (i = width-1; i >= 0; i--) {
        for (j = 0; j < height; j++) {
            if (pixels[(j*width+i)*4] != 0) {
                break rightLoop;
            }
        }
    }
    x1 = i+1;
    bottomLoop:
    for (j = height-1; j >= 0; j--) {
        for (i = 0; i < width; i++) {
            if (pixels[(j*width+i)*4] != 0) {
                break bottomLoop;
            }
        }
    }
    y1 = j+1;
    return img.getSubimage(x0, y0, x1-x0, y1-y0);
    

    }

2 голосов
/ 12 июля 2010

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

0 голосов
/ 12 июля 2010

Если на вашем листе уже есть прозрачные пиксели, то будет возвращено BufferedImage, возвращаемое getSubimage().Graphics2D составное правило по умолчанию - AlphaComposite.SRC_OVER, которое должно быть достаточно для drawImage().

Если у подизображений различный цвет фона, используйте LookupOp с четырехкомпонентным LookupTable, который устанавливает нулевой альфа-компонент для цветов, соответствующих фону.

Я бы пересмотрел растр пикселей только в крайнем случае.

Добавление: дополнительные прозрачные пиксели могут мешать обнаружению столкновений и т. Д. Чтобы обрезать их, потребуется напрямую работать с WritableRaster.Вместо того, чтобы работать по центру, я бы начал с границ, используя пару методов getPixels() / setPixels(), которые могут изменять строку или столбец одновременно.Если целая строка или столбец имеет нулевое значение альфа-канала, отметьте его для исключения при последующем получении подизображения.

...