2012-01-11 16:38:00 +02:00
|
|
|
|
package main;
|
|
|
|
|
|
|
|
|
|
import java.awt.*;
|
|
|
|
|
import java.awt.image.BufferedImage;
|
|
|
|
|
import java.util.Random;
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Класс обработки изображений
|
2012-01-11 16:38:00 +02:00
|
|
|
|
* @author aNNiMON
|
|
|
|
|
*/
|
|
|
|
|
public class ImageUtils {
|
|
|
|
|
|
2012-01-11 16:55:25 +02:00
|
|
|
|
/* За сколько точек мы будем узнавать преобладающий фон */
|
2012-01-11 16:38:00 +02:00
|
|
|
|
private static final int MAX_COLOR_POINTS = 50;
|
|
|
|
|
|
2012-01-11 16:55:25 +02:00
|
|
|
|
/* Чувствительность к поиску кнопок */
|
2012-01-11 16:38:00 +02:00
|
|
|
|
private static final int FIND_BUTTON_TOLERANCE = 20;
|
|
|
|
|
|
2012-01-11 16:55:25 +02:00
|
|
|
|
/* Изображение окна */
|
2012-01-11 16:38:00 +02:00
|
|
|
|
private BufferedImage image;
|
2012-01-11 16:55:25 +02:00
|
|
|
|
/* Размер изображения */
|
2012-01-11 16:38:00 +02:00
|
|
|
|
private int w, h;
|
2012-01-11 16:55:25 +02:00
|
|
|
|
/* Размерность поля */
|
2012-01-11 16:38:00 +02:00
|
|
|
|
private int boardSize;
|
2012-01-11 16:55:25 +02:00
|
|
|
|
/* Размер ячеек */
|
2012-01-11 16:38:00 +02:00
|
|
|
|
private int cellSize;
|
2012-01-11 16:55:25 +02:00
|
|
|
|
/* Координата угла игрового поля */
|
2012-01-11 16:38:00 +02:00
|
|
|
|
private Point board;
|
2012-01-11 16:55:25 +02:00
|
|
|
|
/* Монохромное представление изображения */
|
2012-01-11 16:38:00 +02:00
|
|
|
|
private boolean[] monochrome;
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Конструктор для определения настроек
|
2012-01-11 16:38:00 +02:00
|
|
|
|
* @param image
|
|
|
|
|
* @param boardSize
|
|
|
|
|
*/
|
|
|
|
|
public ImageUtils(BufferedImage image, int boardSize) {
|
|
|
|
|
this.image = image;
|
|
|
|
|
this.boardSize = boardSize;
|
|
|
|
|
w = image.getWidth();
|
|
|
|
|
h = image.getHeight();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Конструктор для проверки настроек
|
2012-01-11 16:38:00 +02:00
|
|
|
|
* @param image
|
|
|
|
|
* @param boardSize
|
|
|
|
|
* @param cellSize
|
|
|
|
|
* @param x
|
|
|
|
|
* @param y
|
|
|
|
|
*/
|
|
|
|
|
public ImageUtils(BufferedImage image, int boardSize, int cellSize, int x, int y) {
|
|
|
|
|
this.image = image;
|
|
|
|
|
this.boardSize = boardSize;
|
|
|
|
|
this.cellSize = cellSize;
|
|
|
|
|
this.board = new Point(x, y);
|
|
|
|
|
w = image.getWidth();
|
|
|
|
|
h = image.getHeight();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Получить размер ячейки
|
2012-01-11 16:38:00 +02:00
|
|
|
|
* @return
|
|
|
|
|
*/
|
|
|
|
|
public int getCellSize() {
|
|
|
|
|
return cellSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Получить координаты игрового поля
|
|
|
|
|
* @return точка с координатами левого верхнего угла поля
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
public Point getBoardParameters() {
|
|
|
|
|
int[] pixels = new int[w * h];
|
|
|
|
|
image.getRGB(0, 0, w, h, pixels, 0, w);
|
|
|
|
|
monochrome = threshold(pixels, 64);
|
|
|
|
|
board = getBoardXY(boardSize);
|
|
|
|
|
return board;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Получить изображение игрового поля
|
|
|
|
|
* @return картинка игрового поля
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
public BufferedImage getBoardImage() {
|
|
|
|
|
int size = cellSize * boardSize;
|
|
|
|
|
try {
|
|
|
|
|
return image.getSubimage(board.x, board.y, size, size);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
return image;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Получить координаты кнопок для автоматического нажатия
|
|
|
|
|
* @param colors массив цветов, по которым будем искать кнопки
|
|
|
|
|
* @return массив координат с точками, или null - если не удалось найти
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
public Point[] getButtons(int[] colors) {
|
|
|
|
|
Point[] out = new Point[colors.length];
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Размер игрового поля в пикселах
|
2012-01-11 16:38:00 +02:00
|
|
|
|
int size = boardSize * cellSize;
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Размеры частей изображения, на которых будем искать кнопки
|
2012-01-11 16:38:00 +02:00
|
|
|
|
Rectangle[] partsOfImage = new Rectangle[] {
|
2012-01-11 16:55:25 +02:00
|
|
|
|
new Rectangle(0, board.y, board.x, size), // слева от поля
|
|
|
|
|
new Rectangle(0, 0, w, board.y), // сверху от поля
|
2012-01-11 16:38:00 +02:00
|
|
|
|
new Rectangle(board.x+size, board.y,
|
2012-01-11 16:55:25 +02:00
|
|
|
|
w-board.x-size, size), // справа от поля
|
2012-01-11 16:38:00 +02:00
|
|
|
|
new Rectangle(0, board.y+size,
|
2012-01-11 16:55:25 +02:00
|
|
|
|
w, h-board.x-size) // снизу от поля
|
2012-01-11 16:38:00 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < partsOfImage.length; i++) {
|
|
|
|
|
Rectangle rect = partsOfImage[i];
|
|
|
|
|
BufferedImage part = image.getSubimage(rect.x, rect.y, rect.width, rect.height);
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Вырезаем часть изображения, в котором будем искать
|
2012-01-11 16:38:00 +02:00
|
|
|
|
boolean found = true;
|
|
|
|
|
for (int j = 0; j < colors.length; j++) {
|
|
|
|
|
if (colors[i] == -1) continue;
|
|
|
|
|
Point pt = findButton(part, colors[j]);
|
|
|
|
|
if (pt != null) {
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Учитываем смещения относительно частей картинок
|
2012-01-11 16:38:00 +02:00
|
|
|
|
pt.translate(rect.x, rect.y);
|
|
|
|
|
out[j] = pt;
|
|
|
|
|
} else {
|
|
|
|
|
found = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (found) return out;
|
|
|
|
|
}
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Не удалось найти все точки
|
2012-01-11 16:38:00 +02:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Преобразовать массив цветов в графический вид
|
|
|
|
|
* @param ids массив идентификаторов последовательности
|
|
|
|
|
* @param palette массив палитры цветов
|
|
|
|
|
* @return изображение последовательности цветов
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
public BufferedImage sequenceToImage(byte[] ids, int[] palette) {
|
2012-01-11 16:55:25 +02:00
|
|
|
|
final int size = 20; // размер каждой ячейки
|
|
|
|
|
// Разбивать будем по 10 клеток на строку
|
2012-01-11 16:38:00 +02:00
|
|
|
|
final int CELLS_IN_ROW = 10;
|
|
|
|
|
int width = CELLS_IN_ROW * size;
|
|
|
|
|
if (width == 0) width = size;
|
|
|
|
|
int rows = ids.length / CELLS_IN_ROW;
|
|
|
|
|
|
|
|
|
|
BufferedImage out = new BufferedImage(width, (rows*size)+size, BufferedImage.TYPE_INT_RGB);
|
|
|
|
|
Graphics G = out.getGraphics();
|
|
|
|
|
for (int i = 0; i < ids.length; i++) {
|
|
|
|
|
G.setColor(new Color(palette[ids[i]]));
|
|
|
|
|
G.fillRect(i % CELLS_IN_ROW * size,
|
|
|
|
|
i / CELLS_IN_ROW * size,
|
|
|
|
|
size, size);
|
|
|
|
|
}
|
|
|
|
|
G.dispose();
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Преобразовать цветное изображение в монохромное.
|
|
|
|
|
* Нужно также учесть, что если поле расположено на светлом
|
|
|
|
|
* фоне, то необходимо инвертировать изображение, чтобы
|
|
|
|
|
* получить сплошную белую область на месте поля.
|
|
|
|
|
* @param pixels массив пикселей изображения
|
|
|
|
|
* @param value разделяющее значение
|
|
|
|
|
* @return массив boolean, true - белый, false - чёрный
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
private boolean[] threshold(int[] pixels, int value) {
|
|
|
|
|
boolean inverse = isBackgroundLight(MAX_COLOR_POINTS);
|
|
|
|
|
if (inverse) value = 255 - value;
|
|
|
|
|
boolean[] bw = new boolean[pixels.length];
|
|
|
|
|
for (int i = 0; i < pixels.length; i++) {
|
|
|
|
|
int brightNess = getBrightness(pixels[i]);
|
|
|
|
|
bw[i] = (brightNess >= value) ^ inverse;
|
|
|
|
|
}
|
|
|
|
|
return bw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Получение состояния яркости фона.
|
|
|
|
|
* @param numPoints сколько точек нужно для определения.
|
|
|
|
|
* @return true - фон светлый, false - тёмный
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
private boolean isBackgroundLight(int numPoints) {
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Получаем numPoints случайных точек
|
2012-01-11 16:38:00 +02:00
|
|
|
|
Random rnd = new Random();
|
|
|
|
|
int[] colors = new int[numPoints];
|
|
|
|
|
for (int i = 0; i < numPoints; i++) {
|
|
|
|
|
int x = rnd.nextInt(w);
|
|
|
|
|
int y = rnd.nextInt(h);
|
|
|
|
|
colors[i] = image.getRGB(x, y);
|
|
|
|
|
}
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Находим среднюю яркость всех numPoints точек
|
2012-01-11 16:38:00 +02:00
|
|
|
|
long sum = 0;
|
|
|
|
|
for (int i = 0; i < numPoints; i++) {
|
|
|
|
|
int brightness = getBrightness(colors[i]);
|
|
|
|
|
sum = sum + brightness;
|
|
|
|
|
}
|
|
|
|
|
sum = sum / numPoints;
|
|
|
|
|
return (sum > 128);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Определить координаты левой верхней ячейки игрового поля.
|
|
|
|
|
* @param boardSize размерность поля (10x10, 14x14 и т.д.)
|
|
|
|
|
* @return координата левого верхнего прямоугольника
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
private Point getBoardXY(int boardSize) {
|
|
|
|
|
/*
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Сначала подсчитаем количество белых точек по горизонтали и вертикали
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
int[] horizontal = new int[h];
|
|
|
|
|
for (int i = 0; i < h; i++) {
|
|
|
|
|
int count = 0;
|
|
|
|
|
for (int j = 0; j < w; j++) {
|
|
|
|
|
if (getBWPixel(j, i)) count++;
|
|
|
|
|
}
|
|
|
|
|
horizontal[i] = count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int[] vertical = new int[w];
|
|
|
|
|
for (int i = 0; i < w; i++) {
|
|
|
|
|
int count = 0;
|
|
|
|
|
for (int j = 0; j < h; j++) {
|
|
|
|
|
if (getBWPixel(i, j)) count++;
|
|
|
|
|
}
|
|
|
|
|
vertical[i] = count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Затем "отфильтруем" лишнее: подсчитаем среднее значение
|
|
|
|
|
* и на его основе уберём малозначимые строки и столбцы.
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
horizontal = filterByMean(horizontal);
|
|
|
|
|
vertical = filterByMean(vertical);
|
|
|
|
|
|
|
|
|
|
/*
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Ищем наибольшую ненулевую последовательность.
|
|
|
|
|
* Индексы границ последовательности и будут граничными точками поля.
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
int[] vParam = getParamsFromSequence(horizontal);
|
|
|
|
|
int[] hParam = getParamsFromSequence(vertical);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int outX = hParam[0];
|
|
|
|
|
int outY = vParam[0];
|
|
|
|
|
int outWidth = hParam[1];
|
|
|
|
|
int outHeight = vParam[1];
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Подсчет размера ячейки
|
2012-01-11 16:38:00 +02:00
|
|
|
|
cellSize = Math.max((outWidth / boardSize), (outHeight / boardSize));
|
|
|
|
|
return new Point(outX, outY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Фильтр последовательности от малозначимых значений.
|
|
|
|
|
* @param source последовательность вхождений цвета
|
|
|
|
|
* @return отфильтрованный массив со значениями 0 и 1
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
private int[] filterByMean(int[] source) {
|
|
|
|
|
long mean = 0;
|
|
|
|
|
for (int i = 0; i < source.length; i++) {
|
|
|
|
|
mean += source[i];
|
|
|
|
|
}
|
|
|
|
|
mean = mean / source.length;
|
|
|
|
|
for (int i = 0; i < source.length; i++) {
|
|
|
|
|
source[i] = (source[i] > mean) ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
return source;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Поиск самой длинной последовательности в массиве.
|
|
|
|
|
* @param source входная последовательность из нулей и единиц
|
|
|
|
|
* @return массив параметров - индекс начала последовательности и её длина
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
private int[] getParamsFromSequence(int[] source) {
|
|
|
|
|
int maxStart = 0, start = 0;
|
|
|
|
|
int maxLength = 0, length = 0;
|
|
|
|
|
for (int i = 1; i < source.length; i++) {
|
|
|
|
|
if (source[i] == 0) {
|
|
|
|
|
start = 0;
|
|
|
|
|
length = 0;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (source[i] == source[i-1]) {
|
|
|
|
|
length++;
|
|
|
|
|
if (maxLength < length) {
|
|
|
|
|
maxStart = start;
|
|
|
|
|
maxLength = length;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Если предыдущий элемент был нулевым - начинаем новую последовательность
|
2012-01-11 16:38:00 +02:00
|
|
|
|
start = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return new int[] {maxStart, maxLength};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Поиск координаты кнопки с цветом template
|
|
|
|
|
* @param img изображение, на котором будем искать
|
|
|
|
|
* @param template шаблон цвета
|
|
|
|
|
* @return координата X. Y, или null если не нашли
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
private Point findButton(BufferedImage img, int template) {
|
|
|
|
|
int h2 = img.getHeight() / 2;
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Искать будем с середины по вертикали, так быстрее найдём
|
2012-01-11 16:38:00 +02:00
|
|
|
|
for (int y = 0; y < h2; y++) {
|
|
|
|
|
for (int x = 0; x < img.getWidth(); x++) {
|
|
|
|
|
int color = img.getRGB(x, h2 - y);
|
|
|
|
|
if (isEquals(color, template, FIND_BUTTON_TOLERANCE)) {
|
|
|
|
|
return new Point(x, h2 - y);
|
|
|
|
|
}
|
|
|
|
|
color = img.getRGB(x, h2 + y);
|
|
|
|
|
if (isEquals(color, template, FIND_BUTTON_TOLERANCE)) {
|
|
|
|
|
return new Point(x, h2 + y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2012-01-11 16:55:25 +02:00
|
|
|
|
// Не нашли
|
2012-01-11 16:38:00 +02:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Проверка на соответствие цветов друг другу
|
|
|
|
|
* @param color1 первый цвет
|
|
|
|
|
* @param color2 второй цвет
|
|
|
|
|
* @param tolerance чувствительность
|
|
|
|
|
* @return true - соответствуют, false - нет
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
private boolean isEquals(int color1, int color2, int tolerance) {
|
|
|
|
|
if (tolerance < 2) return color1 == color2;
|
|
|
|
|
|
|
|
|
|
int r1 = (color1 >> 16) & 0xff;
|
|
|
|
|
int g1 = (color1 >> 8) & 0xff;
|
|
|
|
|
int b1 = color1 & 0xff;
|
|
|
|
|
int r2 = (color2 >> 16) & 0xff;
|
|
|
|
|
int g2 = (color2 >> 8) & 0xff;
|
|
|
|
|
int b2 = color2 & 0xff;
|
|
|
|
|
return (Math.abs(r1 - r2) <= tolerance) &&
|
|
|
|
|
(Math.abs(g1 - g2) <= tolerance) &&
|
|
|
|
|
(Math.abs(b1 - b2) <= tolerance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Получение яркости цвета
|
|
|
|
|
* @param color исходный цвет
|
|
|
|
|
* @return яркость (0..255)
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
private int getBrightness(int color) {
|
|
|
|
|
int qr = (color >> 16) & 0xff;
|
|
|
|
|
int qg = (color >> 8) & 0xff;
|
|
|
|
|
int qb = color & 0xff;
|
|
|
|
|
return (qr + qg + qb) / 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
2012-01-11 16:55:25 +02:00
|
|
|
|
* Получение цвета из монохромного изображения.
|
|
|
|
|
* return true - белый, false - чёрный
|
2012-01-11 16:38:00 +02:00
|
|
|
|
*/
|
|
|
|
|
private boolean getBWPixel(int x, int y) {
|
|
|
|
|
if ((x < 0) || (y < 0) || (x >= w) || (y >= h)) return false;
|
|
|
|
|
return monochrome[y * w + x];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|