\documentclass[a4paper,fontsize=14bp]{article}

\input{../common-preamble}
\input{../bmstu-preamble}
\input{../fancy-listings-preamble}
\numerationTop

\begin{document}
\thispagestyle{empty}
\makeBMSTUHeader

% ... работе, номер, тема, предмет, ?а, кто
\makeReportTitle{лабораторной}{2}{Распознавание объекта по цвету}{Цифровая обработка изображений}{}{Большаков В.Э.}
\newpage
\thispagestyle{empty}
\tableofcontents
\newpage
\pagestyle{fancy}
\sloppy
\section{Цель}
Основной целью лабораторной работы является изучение методов распознания объекта по цвету с помощью библиотеки цифровой обработки изображений scikit-image, а также разработка программы распознавания блюд на подносе.
\section{Задание}
\begin{itemize}
\item Подготовить выборку 10 цветных цифровых изображений блюд бауманской столовой (по аналогии с примером).
\item В среде Spyder (сборка Anaconda) на языке Python (3.5 и старше) создать проект и подключить библиотеку scikit-image.
\item Из л.р. 1 взять модуль загрузки цветного цифрового изображения и модуль обработки пикселей.
\item Запрограммировать формулу перевода цветного цифрового изображения в цветовое пространство в соответствии с вариантом (В1: RGB, В2: HSV, В3: CMYK, В4: YUV, В5: Hough Circle+HSV, В6: Hough Circle +HSL)
\item Создать классификацию блюд из цифровых изображений.
\item Для каждого блюда определить цветовые характеристики. Задать распределение значений каждого цвета.
\item Провести эксперимент по распознаванию блюд с визуализацией результатов.
\end{itemize}

Вариант (по номеру в списке группы) 9, то есть цветовое пространство CMYK. Для распознавания разрешается использовать только цвет.
\section{Теоретическая часть}
\subsection{Цветовое пространство CMYK}
Система CMYK создана и используется для типографической печати. Аббревиатура \textbf{CMYK} означает названия основных красок, использующихся для четырехцветной печати: голубой (Сyan), пурпурный (Мagenta) и жёлтый (Yellow). Буквой К обозначают черную краску (BlacK), позволяющую добиться насыщенного черного цвета при печати. Используется последняя, а не первая буква слова, чтобы не путать CMYK Black и RGB Blue. На рисунке \hrf{pic:cmyk-rgb} показана цветовая палитра CMYK вместе с RGB.
\begin{figure}[H]
  \centering
  \includegraphics[width=12cm]{02-dip-lab02-cmyk-rgb.png}  \caption{Цветовые пространства CMYK и RGB}
  \label{pic:cmyk-rgb}
\end{figure}

\subsection{Особенность формирования цвета}
Каждое из чисел, определяющее цвет в CMYK, представляет собой процент краски данного цвета, составляющей цветовую комбинацию. Например, для получения тёмно-оранжевого цвета следует смешать 30\% голубой краски, 45\% пурпурной краски, 80\% жёлтой краски и 5\% чёрной. Это можно обозначить следующим образом: CMYK(30/45/80/5).

\subsection{Перевод из RGB в CMYK}
Для того, чтобы перевести пиксель RGB в пиксель CMYK можно применить формулу, описанную на языке Java и представленную в таблице \hrf{code:rgb-cmyk}.
\begin{lstlisting}[language=Java, style=JCodeStyle, caption={Формула перевода из RGB в CMYK}, label={code:rgb-cmyk}]
  int black = Math.min(Math.min(255 - red, 255 - green), 255 - blue);
  if (black!=255) {
    int cyan    = (255-red-black)/(255-black);
    int magenta = (255-green-black)/(255-black);
    int yellow  = (255-blue-black)/(255-black);
    return new int[] {cyan,magenta,yellow,black};
  } else {
    int cyan = 255 - red;
    int magenta = 255 - green;
    int yellow = 255 - blue;
    return new int[] {cyan,magenta,yellow,black};
  }
\end{lstlisting}
Пример полученного изображения с использованием преобразования пикселей представлен на рисунке \hrf{pic:trans}. При преобразовании таким методом исходного изображения (\hrf{pic:trans-rgb}) будет получено изображение не естественных цветов (\hrf{pic:trans-cmyk}).

\begin{figure}[H]
  \centering
  \begin{subfigure}[b]{0.4\textwidth}
    \centering
    \includegraphics[width=\textwidth]{02-dip-lab02-trans-rgb.png}
    \caption{Исходное изображение в пространстве RGB}
    \label{pic:trans-rgb}
  \end{subfigure}
  \hfill
  \begin{subfigure}[b]{0.4\textwidth}
    \centering
    \includegraphics[width=\textwidth]{02-dip-lab02-trans-cmyk.png}
    \caption{Преобразованное изображение CMYK}
    \label{pic:trans-cmyk}
  \end{subfigure}
  \caption{Пример попиксельного перевода изображения}
  \label{pic:trans}
\end{figure}

Данные результаты не являются правильными. Для более точного преобразования следует использовать ICC профили, описывающие как должен отображаться цвет на мониторе и бумаге.

\subsection{Цветовое профилирование}
Для достоверного управления цветом необходимы ICC-совместимые профили всех цветовоспроизводящих устройств. Например, без точного профиля сканера хорошо отсканированное изображение может отображаться в другой программе неправильно из-за различий между алгоритмами отображения, используемыми сканером и программой. Недостоверность цветопередачи может привести к внесению в хорошее изображение ненужных и, возможно, вредных "улучшений". При наличии точного профиля программа, импортирующая изображение, способна скорректировать разницу с устройством и воспроизвести достоверные цвета отсканированного изображения.

Профили документов описывают конкретное цветовое пространство, используемое в документе. Путем назначения профиля, или пометки документа профилем, приложение определяет фактические цвета документа. Например, запись R = 127, G = 12, B = 107 — это просто набор чисел, которые разные устройства будут отображать по-разному. Однако при пометке цветовым пространством Adobe RGB эти числа определяют фактический цвет или длину световой волны. На рисунке \hrf{pic:icc-profile} представлена назначение профилей ICC.
\begin{figure}[H]
  \centering
  \includegraphics[width=5cm]{02-dip-lab02-icc-profile.png}
  \caption{Пример использования профилей ICC}
  \label{pic:icc-profile}
\end{figure}

При использовании профилей преобразованные изображения получаются более правдоподобными. На рисунке \hrf{pic:icc-trans} представлено такое преобразование, \hrf{pic:icc-trans-rgb} пространство RGB, \hrf{pic:icc-trans-cmyk} пространство CMYK.
\begin{figure}[H]
  \centering
  \begin{subfigure}[b]{0.4\textwidth}
    \centering
    \includegraphics[width=\textwidth]{02-dip-lab02-trans-rgb.png}
    \caption{Исходное изображение в пространстве RGB}
    \label{pic:icc-trans-rgb}
  \end{subfigure}
  \hfill
  \begin{subfigure}[b]{0.4\textwidth}
    \centering
    \includegraphics[width=\textwidth]{02-dip-lab02-trans-rgb.png}
    \caption{Преобразованное изображение CMYK}
    \label{pic:icc-trans-cmyk}
  \end{subfigure}
  \caption{Пример преобразования с использованием профилей}
  \label{pic:icc-trans}
\end{figure}

\section{Практическая часть}
\subsection{Состав исполняемых файлов}
Программа по распознаванию изображений состоит из двух скриптов:
\begin{itemize}
\item основного скрипта для выполнения программы \code{main.py};
\item скрипта для обработки изображений \code{colorFoodView.py}.
\end{itemize}

Основной скрипт для тренировки принимает 2 аргумента для запуска:
\begin{itemize}
\item \code{-c}, файл конфигурации программы в формате json;
\item \code{-f}, файл описывающий блюда и их цвета.
\end{itemize}

Например, скрипт для запуска тренировки может выглядеть следующим образом:
\begin{lstlisting}[style=CCodeStyle]
main.py -c "./configs.json" -f "./foods.json"
\end{lstlisting}

\subsection{Конфигурационные файлы}
Для корректной работы программы, ей необходимо передать два файла в формате json:
\begin{enumerate}
\item файл конфигурации (далее \code{config.json});
\item файл описания блюд (далее \code{food.json}).
\end{enumerate}

Параметры файла \code{config.json} представлены в таблице \hrf{table:config-json}, а пример заполненного файла в листинге \hrf{code:conf-json}.
\begin{table}[H]
  \centering
  \begin{tabular}{|l|p{100mm}|}
    \hline
    Наименование параметра & Описание \\ \hline
    \code{RGB_profile} & Путь до профиля ICC RGB описывающего цветовое пространство исходного изображения \\ \hline
    \code{CMYK_profile} & Путь до профиля ICC CMYK в чье цветовое пространство должно быть переведено изображение \\ \hline
    \code{Path_to_food} & Путь до папки с изображениями \\ \hline
    \code{Delta_for_color} & Процент погрешности для пикселей \\ \hline
    \code{Color_swap} & Цвет в формате CMYK в который необходимо преобразовать найденные пиксели \\ \hline
    \code{Mode} & Режим работы программы. Если режим работы Test, то в консоль будет выводиться количество найденных пикселей для искомого блюда\\
    \hline
  \end{tabular}
  \caption{Параметры конфигурации}
  \label{table:config-json}
\end{table}

\begin{lstlisting}[language=C,style=CCodeStyle, caption={Пример настроек параметров в программе}, label={code:conf-json}]
  {
    "RGB_profile": "./AdobeICCProfiles/RGB/AdobeRGB1998.icc",
    "CMYK_profile": "./AdobeICCProfiles/CMYK/WebCoatedSWOP2006Grade5.icc",
    "Path_to_food": "./WorkFood",
    "Delta_for_color": 5,
    "Color_swap": [100, 78, 0, 0],
    "Mode": "Product"
  }
\end{lstlisting}

Параметры файла \code{food.json} представлены в таблице \hrf{table:food-json}, а пример заполненного файла в листинге \hrf{code:food-json}. Файл описывает список блюд, а именно параметры каждого из элементов списка.
\begin{table}[H]
  \centering
  \begin{tabular}{|l|p{100mm}|}
    \hline
    Наименование параметра & Описание \\ \hline
    Name & Наименование блюда \\ \hline
    CMYK & Пиксели которые описывают данное блюдо \\ \hline
    Count & Минимальное количество пикселей, которое должно быть у изображения для того, чтобы можно было говорить о том, что на изображении присутствует искомое блюдо. Данное значение можно получить в тестовом режиме \\
    \hline
  \end{tabular}
  \caption{Параметры конфигурации}
  \label{table:food-json}
\end{table}

\begin{lstlisting}[language=C,style=CCodeStyle, caption={Пример настроек параметров в программе}, label={code:food-json}]
{
  "Foods": [
    {
      "Name": "Банан в шоколаде",
      "CMYK": [
        { "C": 23, "M": 29, "Y": 44, "K": 7 },
        { "C": 29, "M": 40, "Y": 57, "K": 11 },
        { "C": 34, "M": 61, "Y": 63, "K": 40 }
      ],
      "Count": 2000
    },
    {
      "Name": "Сосиска",
      "CMYK": [
        { "C": 27, "M": 65, "Y": 76, "K": 25 },
        { "C": 14, "M": 47, "Y": 67, "K": 3 },
        { "C": 24, "M": 65, "Y": 85, "K": 20 },
        { "C": 28, "M": 83, "Y": 99, "K": 30 }
      ],
      "Count": 9000
    },
    //...
  ]
}
\end{lstlisting}

\subsection{Описание алгоритма работы программы}
Алгоритм работы основной программы заключается в загрузке и настройки конфигурации и передаче их в скрипт обработки изображения. После выполнения поиска блюд и перед началом нового цикла работа можно скорректировать \code{json} файлы конфигурации и изменения будут применены без необходимости перезапуска программы.
В скрипте обработки изображения используется следующий алгоритм работы:
\begin{itemize}
\item преобразовать все изображения в цветовое пространство, описанное профилем CMYK;
\item для каждого изображения найти пиксели из выбранного блюда;
\item если пиксель найден, то его цвет меняется на цвет, заданный в настройках;
\item проверяется количество найденных пикселей, если их меньше порогового значения, то изображение отбрасывается;
\item изображения, прошедшие проверку, возвращаются в основную программу для дальнейшей работы.
\end{itemize}

\subsection{Выборка для тестирования}
Для тестирования была подготовлена выборка из десяти подносов с едой. Каждая картинка была приведена к разрешению 400х300 пикселей. Изначальный размер изображений (более 4000х3000 пикселей) не позволяет добиться приемлемых результатов с использованием только цвета как характеристики для поиска изображений на фото. На рисунке \hrf{pic:food-src} представлена подготовленная для работы выборка.

\begin{figure}[H]
  \centering
  \includegraphics[width=3cm]{01-dip-lab02-rgb1.jpg}
  \includegraphics[width=3cm]{01-dip-lab02-rgb2.jpg}
  \includegraphics[width=3cm]{01-dip-lab02-rgb3.jpg}
  \includegraphics[width=3cm]{01-dip-lab02-rgb4.jpg}
  \includegraphics[width=3cm]{01-dip-lab02-rgb5.jpg}
  \includegraphics[width=3cm]{01-dip-lab02-rgb6.jpg}
  \includegraphics[width=3cm]{01-dip-lab02-rgb7.jpg}
  \includegraphics[width=3cm]{01-dip-lab02-rgb8.jpg}
  \includegraphics[width=3cm]{01-dip-lab02-rgb9.jpg}
  \includegraphics[width=3cm]{01-dip-lab02-rgb10.jpg}
  \caption{Исходная выборка изображений}
  \label{pic:food-src}
\end{figure}

\subsection{Подготовка данных}
Для анализа изображений на предмет присутствия искомых пикселей они должны быть правильно заданы. Для этого используется скрипт \code{converter.py} преобразующий рабочее изображение в пространство CMYK. В дальнейшем с помощью дополнительного инструментария\footnote{онлайн пикер цвета https://www.ginifab.com/feeds/pms/color\_picker\_from\_image.php}, возможно узнать цвет пикселей в пространстве CMYK и внести в \code{food.json}.

\section{Результат работы}
\subsection{Листинг}
Исходный код скриптов \code{main.py}, \code{colorFoodView.py} и \code{converter.py} представлены в приложениях \hrf{app:main}, \hrf{app:find} и \hrf{app:conv} соответственно.

\subsection{Интерфейс и измерения}
Программа использует терминальный текстовый интерфейс для взаимодействия с пользователем. На основе содержания \code{food.json} строятся возможные варианты выбора в основном меню. Предусмотрен контроль ошибок ввода. В результате измерений программа вернет изображения где, по её мнению, присутствуют искомые продукты. Найденные пиксели будут закрашены в заданный в конфигурации цвет. Результат работы для поиска яичницы приведен на рисунке \hrf{pic:result}.

\begin{figure}[H]
  \centering
  \includegraphics[width=12cm]{01-dip-lab02-result.png}
  \caption{Результат работы программы}
  \label{pic:result}
\end{figure}

\subsection{Ошибки}
В ходе вычислений программа допускает некоторое количество ошибок. Чем качественнее изображение, тем больше шансов что искомый пиксель будет присутствовать на изображении в неожиданных местах. Рабочая выборка подверглась минимальной предварительной обработке и часто в искомую область попадают объекты, не связанные с продуктами, такие как стол или поднос. В таблице \hrf{table:errors} представлены ошибки первого и второго рода для измерений.
\begin{table}[H]
  \centering
  \begin{tabular}{||r|c|c||} 
    \hline
    \multirow{2}{*}{Результаты} & \multicolumn{2}{c|}{Верная гипотеза} \\
    \cline{2-3}
                                & H0 & H1 \\
    \hline
    H0 & 27 & 36 \\
    \hline
    H1 & 0  & 524 \\
    \hline
  \end{tabular}
  \caption{Ошибка первого и второго рода}
  \label{table:errors}
\end{table}
\begin{itemize}
\item [] Ошибка первого рода: $0\%$
\item [] Ошибка второго рода: $36 / 524 * 200 = 7\%$
\end{itemize}
Данные результаты легко объяснить. Коэффициенты для срабатывания определения блюда настроены таким образом, чтобы находить блюдо в наборе. И программа хорошо справляется с этой задачей. Ошибки второго рода могут возникнуть, когда цвет блюда очень общий для изображения и срабатывает в нем во многих местах. Таких ситуаций не много, однако они имеют место быть. Основная масса ошибок второго рода отсекаются настраиваемым коэффициентом для каждого блюда.

\section{Заключение}
В ходе выполнения задания были изучены методы по обработке изображения и работы с цветом. Исходя из результатов тестирования можно сделать вывод, что использование только цвета для поиска объектов на картинке не является достаточным, а подготовке рабочей выборки нужно уделять большое внимание. Разный источник света и разные условия сьемки сильно изменяют цвета, которые можно использовать для идентификации объектов.

\newpage
\appendix
\section*{Приложения}
\addcontentsline{toc}{section}{Приложения}
\renewcommand{\thesubsection}{\Asbuk{subsection}}

\subsection{Основная программа}
\label{app:main}
\begin{lstlisting}[language=Python,style=PyCodeStyle]
import argparse

# Парсим аргументы вызова скрипта
import codecs
import json
import sys

from ColorFoodView import find_food


def create_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument('-c', '--configs')
    parser.add_argument('-f', '--foods')
    return parser


def menu(foods):
    print("Допустимый выбор:")
    print("0 : Выход")
    i = 1
    for food in foods["Foods"]:
        print(i, ":", food["Name"])
        i += 1
    print("Введите команду:")
    i -= 1
    while 1:
        try:
            chose = int(input())
            if 0 > chose or chose > i:
                print("Данной команды не существует. Выберите из предложенного набора и попробуйте еще раз.")
                continue
            return chose
        except:
            print("Необходимо ввести число соответсвующее вашему выбору. Попробуйте еще раз")


# поиск продуктов
def food_name(foods, chose):
    return foods["Foods"][chose - 1]


namespace = create_parser().parse_args(sys.argv[1:])

while 1:
    # получение конфигурационных данных
    configs = json.loads(codecs.open(namespace.configs, 'r', encoding='utf-8').read())
    # получение данных для справочника продуктов
    foods = json.loads(codecs.open(namespace.foods, 'r', encoding='utf-8').read())
    # меню выбора
    chose = menu(foods)
    if chose == 0:
        break
    # запрос на поиск продуктов
    images_food = find_food(configs, food_name(foods, chose))
    for image in images_food:
        image.show()
    print("Нажмите любую кнопку чтобы продолжить.")
    input()
\end{lstlisting}

\subsection{Поиск объектов по цвету}
\label{app:find}
\begin{lstlisting}[language=Python,style=PyCodeStyle]
import os

import re

from PIL import Image
from PIL.ImageCms import profileToProfile

color_swap = [0, 0, 0, 255]
delta_for_color = 0
mode = "Product"


# Возвращает список изображений в цветовом пространтстве CMYK
def get_list_cmyk_image(path, rgb_profile, cmyk_profile):
    list_image = []
    for d, dirs, files in os.walk(path):  # Достаем все каталоги по пути
        for f in files:  # Достаем все файлы
            if re.search(".jpg", f):  # Сортировка по типу
                path = os.path.join(d, f)  # Получаем путь до изображения
                im = Image.open(path)
                picture_cmyk = profileToProfile(im, rgb_profile, cmyk_profile, outputMode='CMYK')
                list_image.append(picture_cmyk)  # Добавляем полученное изображение в список
    return list_image


# переводит список процентов в соответствующее значение [0..255]
def percent_to_scale_list(percent):
    list = []
    for p in percent:
        list.append(percent_to_scale(p))
    return list


# преобразует процент в соответветсвующее значение [0..255]
def percent_to_scale(percent):
    return int(percent / 100 * 255)


# получение минимум
# значение пикселя - процент погрешности
def get_min(param):
    result = param - delta_for_color
    if result < 0:
        return 0
    else:
        return result


# получение максимума
# значение пикселя + процент погрешности
def get_max(param):
    result = param + delta_for_color
    if result > 255:
        return 255
    else:
        return result


# проверка входит ли значение пикселя в заданные границы
def pixel_equal(pixel, mask_pixel):
    min_c = get_min(pixel)
    max_c = get_max(pixel)
    return min_c <= mask_pixel <= max_c


# проверка пикселя на соответсвие искомого
def range_equal(pixel, mask_pixel):
    c = pixel_equal(pixel[0], mask_pixel[0])
    m = pixel_equal(pixel[1], mask_pixel[1])
    y = pixel_equal(pixel[2], mask_pixel[2])
    k = pixel_equal(pixel[3], mask_pixel[3])

    if c and m and y and k:
        return 1
    else:
        return 0


# ищем пиксели из изображения и сравниваем с искомыми
# если поиск был успешен, то заменяем его на заданные в конфигурации цвет
def swap_pixel(image, mask_pixels, color_count):
    count = 0
    for i in range(image.width):
        for j in range(image.height):
            pixel = image.getpixel((i, j))
            for mask_pixel in mask_pixels:
                if range_equal(pixel, mask_pixel):
                    image.putpixel((i, j), tuple(color_swap))
                    count += 1
    if mode == "Test":
        print(count)

    if count > color_count:
        return image
    else:
        return None


# Получение списка пикселей для дальшейшего их поиска в изображении
def get_mask_pixel(food_name):
    list_tuple = []
    for pixel in food_name["CMYK"]:
        c = percent_to_scale(pixel["C"])
        m = percent_to_scale(pixel["M"])
        y = percent_to_scale(pixel["Y"])
        k = percent_to_scale(pixel["K"])
        list_tuple.append(tuple([c, m, y, k]))
    return list_tuple


# поиск блюд на изображении
def find_food_on_image(cmyk_image_list, food_name):
    list_found_image = []
    for image in cmyk_image_list:
        mask_pixel = get_mask_pixel(food_name)
        image_swap_pixel = swap_pixel(image, mask_pixel, food_name["Count"])
        if image_swap_pixel is not None:
            list_found_image.append(image_swap_pixel)
    return list_found_image


# точка входа в скрипт, настройка параметров и вызов функций
def find_food(configs, food_name):
    global delta_for_color, color_swap, mode
    delta_for_color = percent_to_scale(configs["Delta_for_color"])
    color_swap = percent_to_scale_list(configs["Color_swap"])
    mode = configs["Mode"]
    cmyk_image_list = get_list_cmyk_image(configs["Path_to_food"], configs["RGB_profile"], configs["CMYK_profile"])
    food_on_image_list = find_food_on_image(cmyk_image_list, food_name)
    return food_on_image_list
\end{lstlisting}

\subsection{Конвертер изображений}
\label{app:conv}
\begin{lstlisting}[language=Python,style=PyCodeStyle]
from PIL import Image
from PIL.ImageCms import profileToProfile

RGB_profile = "./AdobeICCProfiles/RGB/AdobeRGB1998.icc"
CMYK_profile = "./AdobeICCProfiles/CMYK/WebCoatedSWOP2006Grade5.icc"

for i in range(1, 11):
    image_path = "./WorkFood/" + str(i) + ".jpg"
    im = Image.open(image_path)
    converted_image = profileToProfile(im, RGB_profile, CMYK_profile, outputMode='CMYK')
    converted_image.save("./CMYK_Food/" + str(i) + ".jpg")
\end{lstlisting}


\end{document}

