Какие карты лежат на столе?

Задача и требования:

— Дано множество картинок
— Необходимо написать программу на Java, которая распознает, какие карты лежат на столе (только по центру картинки). Например, на этой картинке:

В данном примере, на столе лежат карты 6s5h10h

Или вот в другом примере по ссылке, на столе лежат карты 4hQd7s
— Тестирование программы будет осуществляться на аналогичных картинках, которых нет в исходном множестве
— Допускаются ошибки в распознавании не более 3% от общего количества распознанных карт
— Нельзя использовать готовые библиотеки для распознавания текста. Необходимо написать свой алгоритм распознавания карт
— На распознавание одного файла не должно уходить более 1 секунды
— Исходный код решения задачи не должен быть длиннее 500 строк с нормальным форматированием

— Программу нужно предоставить в виде, готовом к запуску на Windows десктопе. Файл run.bat параметром принимает путь до папки с картинками. В консоль распечатывается результат в виде «имя файла — карты» для всех файлов папки
— Программу нужно предоставить с исходными файлами
— В исходных файлах должен быть ВЕСЬ код, который был использован для решения задачи

Рекомендации:

— У автора этой задачи решение заняло 100 строк кода. У лучшего на данный момент кандидата — 160 строк. Ничего страшного, если ваше решение занимает 500 строк. Однако, если больше и это — не комментарии, то стоит задуматься

Для решения задачи рекомендуется использовать следующие функции, встроенные в Java:

— BufferedImage img = ImageIO.read(f); зачитка картинки из файла
— ImageIO.write(img, «png», f); запись картинки в файл
— img.getWidth(); img.getHeight(); рамеры картинки
— BufferedImage img1 = img.getSubimage(x, y, w, h); взятие области в картинке
— img.getRGB(x, y);взятие цвета точки по координате
— Color c = new Color(img.getRGB(x, y)); c.getRed(); c.getGreen(); c.getBlue(); c.equals(c1) работа с цветом точки

 

Приступим к решению

С чего можно начать?

Прежде всего, любую задачу можно разбить на более мелкие логические блоки.

Первое с чего начнем, это с идеи определения — что за карты лежат на столе.

И тут напрашивается самая простая идея — можно определить что именно нарисовано в тех местах где нарисованы карты. А так как карты отображаются на одних и тех же местах, то можно определять масть и уровень карты с помощью сканирования цвета пикселей в зоне отображения масти и уровня карты. Воспользуемся методом сверки с шаблоном, это позволит быстро определять что же нарисовано на картинке. Правда для этого, нужно будет обучать систему вариантам шаблонов.

Второе — это немного разобьем на этапы нашу работу:

После написания метода определения масти и уровня карты в указанной области, нам необходимо будет считывать список картинок в указанном каталоге. Далее, нам нужен будет метод считывания картинки в объект, из которого можно будет читать цвет пикселей.

На этапе проверки участка изображения с шаблоном, нам понадобится локальная база шаблонов ассоциаций. В момент проверки, может оказаться, что похожего шаблона нет. Поэтому необходимо будет обучать систему, спрашивая пользователя что же нарисовано на одном из пяти мест (какая масть или уровень еще не известен). Таким образом, база знаний будет пополняться.

 

И так начнем. Метод для определения того что нарисовано в указанной области.

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

static BufferedImage img = null;


public static int kodKarta(int x, int y) {
    int kod=0;
    int kodf=0;
    for(int iy = y; iy <y+30; iy++){
        for(int ix = x; ix <x+40; ix++){
            Color c = new Color(img.getRGB(ix, iy));
            if (c.getGreen()<120)
                kod= kod+ix*iy;
            else
                kodf++;
        }
    }
    if (kodf<10)
        kod=0;
    return kod;
}

На что тут обратить внимание:

img — это объект представляющий из себя загруженную картинку.

Карты представляют собой изображения знаков масти и символы уровня, красного и черного цветов. При этом сама карта белая на сером столе. Если в искомом изображении нет серого цвета, значит тут явно есть символ который нужно оценить. А для этого мы сделаем его «слепок». В этом и заключается суть данного метода.

 

Продолжаем

Для работы нашего примера понадобиться использовать параметры командной строки windows. Программа должна принимать параметром путь до папки с картинками.

Главный метод main принимает эту строку. Поэтому в нашей программе мы сможем использовать путь к папке с картинками, указанный таким образом.

public static void main(String[] args) {
    if (args.length>0) {

Но запускать программу с использованием командной строки удобно лишь уже рабочую версию. А вот для того, что бы запускаться в таком режиме в среде IDE нужно добавить одну возможность:

Параметры запуска через IntelliJ IDEA:
Run -> Edit Configuration -> Aplication (main) в поле Program Arguments нужно указать саму строку. В нашем примере это путь к папке imgs

Теперь как это выглядит в bat файле. Создадим файл start.bat

И добавим туда одну строку:

java -jar out\artifacts\testKarta_jar\testKarta.jar %1

А вот для того чтобы запускался этот bat , необходимо существование jar файла. Для того чтобы IntelliJ IDEA создавала jar файл, необходимо проделать пару действий в главном меню:

File -> Project Structure -> Artifacts + jar (From modules….)
Потом Build -> Build Artifact

 

Идем дальше

Теперь перейдем в главный метод Main .

Вот что нам понадобится:

public static void main(String[] args) {
    if (args.length>0) {
        String fileNameIn = args[0];
        File dir = new File(fileNameIn);
        File[] files = dir.listFiles();        
...

fileNameIn — это имя папки, получаемой из командной строки

Далее мы получаем список файлов находящихся в папке, создав объект-список files

 

Как говорилось ранее, для определения ассоциативных изображений символов, нам необходима локальная база. Поэтому будем хранить ассоциативный список в локальном файле dm.txt

Далее в методе main читаем эту базу ассоциаций использую метод loadM предварительно созданного класса figura (детально об этом классе будет ниже):

Figura.loadM("dm.txt");

Формат файла выглядит как пара имени и кода:

2-28877304
A-26796104
J-17995515
K-19362417
J-15719680
K-35396788
A-18215045
4-21330143
3-15651879

Имя символа к примеру К (король) А (туз) 2 (двойка) — ассоциируется с кодом, получаемым методом kodKarta() описанным выше.

В нашем тестовом примере, расположение карт на любой из скринов(картинок), всегда в одном и том же месте. Первая карта всегда на своем месте, как любая из пяти карт.

Поэтому, мы можем точно указать координаты левого верхнего угла небольшой области изображения. Координаты X для пяти карт 146, 217, 289, 360, 432 (они совпадают с изображениями масти и изображениями уровня)

int[] kX = {146, 217, 289, 360, 432};

А координата Y для изображения масти = 589. А для изображения уровня = 589+30.

Исходя из этого, будем работать с пятью возможными картами, читая одну из картинок в объект bufferedReader

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

System.out.print(nameFile + " - ");
for (int i = 0; i < 5; i++) {
   -----> сюда вставим обработку изображений с мастью и уровнем карты
}
System.out.println();

— участки с изображением масти :

int kodU = kodKarta(kX[i], 589);
if (kodU > 0){
    int iU = Figura.searchM(kodU);
    if (iU < 0){
        System.out.print('\n' + "You need training" + '\n' + "In this file: " + nameFile + '\n' + "what level is " + (i + 1) + " card? - ");
        try {
            String sU = bufferedReader.readLine();
            Figura.addM(sU, kodU);
            Figura.saveM("dm.txt");
        } catch (IOException e) {
        }
    } else {
        System.out.print(Figura.getM(iU));
    }
}

— участки с изображением уровня карты:

int kodM = kodKarta(kX[i], 589 + 30);
if (kodM > 0) {
    int iM = Figura.searchM(kodM);
    if (iM < 0) {
        System.out.print('\n' + "You need training" + '\n' + "In this file: " + nameFile + '\n' + "what suit is " + (i + 1) + " card? - ");
        try {
            String sM = bufferedReader.readLine();
            Figura.addM(sM, kodM);
            Figura.saveM("dm.txt");
        } catch (IOException e) {
        }
    } else {
        System.out.print(Figura.getM(iM));
    }
}

Предвартильно обернув эти действия с пятью картами

String nameFile = null;
for (int iImage = 0; iImage < files.length; iImage++) {
    try {
        nameFile = files[iImage].toString();
        img = ImageIO.read(new File(nameFile));
    } catch (IOException e) {
    }
-----> тут и вставляем описанный код выше.
}

 

Теперь стоит обратить внимание на используемые методы класса figura :

Figura.searchM();
Figura.addM();
Figura.saveM();
Figura.addM();
Figura.getM();

В завершении задачи

Добавим в наш пример класс Figura :

private static class Figura {
    private static ArrayList<String> listM = new ArrayList<>();
    private static ArrayList<Integer> listMk = new ArrayList<>();
}

Добавим в класс несколько методов.

Метод addM() добавляет ассоциацию имени с кодом:

public static void addM(String n, int k) {
    listM.add(n);
    listMk.add(k);
}

Метод getM() получает номер записи в списке ассоциаций

public static String getM(int i) {
    return listM.get(i);
}

Метод searchM() позволяет найти код в списке ассоциаций, совпадающий с искомым кодом обрабатываемого изображения

public static int searchM(int k) {
    int r=-1;
    for (int i=0; i<listMk.size(); i++){
        if (k==listMk.get(i))
            r=i;
    }
    return r;
}

Метод saveM() позволяет сохранить наш список ассоциаций в файл dm.txt

public static void saveM(String namefile) {
    try(FileWriter writer = new FileWriter(namefile, false))
    {
        int i=0;
        while (i<listM.size())
        {
            writer.write(listM.get(i)+'-');
            writer.write(Integer.toString(listMk.get(i))+'\n');
            i++;
        }
        writer.flush();
    } catch (Exception ex){}
}

И еще один метод позволяющий считывать список ассоциаций из нашей локальной базы данных, файла dm.txt

public static void loadM(String namefile) {
    try(FileReader reader = new FileReader(namefile))
    {
        int c;
        String s="";
        String n=null;
        int k;
        while((c=reader.read())!=-1){
            if (c=='-') {
                n = s;
                s="";
            } else
            if (c=='\n') {
                k= new Integer(s);
                listM.add(n);
                listMk.add(k);
                s="";
            } else
            s+= ((char)c);
        }
    } catch (Exception ex){}
}

Заключение или резюме

Результат работы этого приложения — вполне себе шустро получаем список распознанных карт.

Было бы конечно интересно посмотреть на более короткую или более оптимальную версию этой задачи.

Исходный код решения этой задачи на Git

 

P.S.

Кто будет пробовать этот пример на практике, не забудьте распаковать архив с картиками imgs