Если Вы хотите научиться создавать программы для Windows, но совсем не знаете с чего начать, то с этой статьи.
Сегодня мы напишем программу, в которой человеку надо нарисовать два рисунка, а компьютер скажет насколько они похожи друг на друга.
Для этого урока у Вас должна быть установлена среда разработки Visual Studio, а также Вы должны знать основы синтаксиса языка C# (об этом и была статься, в которой говорилось с чего начать). Об элементах управления графического интерфейса, методах и классах пространства имён System.Windows.Forms я рассказывал в этой статье (там мы рисовали свои символы и печатали ими). Но сейчас мы научимся создавать свои классы: на основе существующих и полностью свои. Также там кратко сказал про то, что такое классы и методы. В этом примере возьмём их на вооружение и применим.
Шаг 1. Подумать над задачей
Задачу можно разделить на несколько последовательных этапов. Сначала человек рисует два рисунка. Про то какие это должны быть рисунки нам ничего не сказали поэтому сделаем два небольших рисунка на панелях рядом друг с другом: так будет наглядно и мы используем знания, полученные в предыдущем примере.
Затем программа должна сравнить рисунки. По каким критериям она должна сравнивать нам ничего не сказали, поэтому будем считать, что поворот, растяжение или фрагментацию рисунка учитывать не надо. Зато учтём характер изображений, психологию и наше желание научиться, поэтому не будем использовать известные алгоритмы сравнения изображений, а изобретём велосипед свой способ.
Так как у нас линии толщиной в 1 пиксель, а рисовать пользователю будет скорее всего лень, то предположим наличие всего нескольких не особо извилистых линий, которые могут пересекаться. Моя программа будет сравнивать точки окончания и пересечения этих линий. Если таких точек для сравнения будет слишком мало (например изображение будет состоять из не пересекающихся кругов), то добавим ещё несколько точек, принадлежащих самим линиям. Получатся два небольших массива точек с уникальными характеристиками, которые и будут сравниваться.
Следующим этапом программа должна принять решение. Для этого надо подсчитать степень близости каждой точки одного изображения к другому и наоборот (так как возможно что одно из изображений является всего лишь фрагментом другого), а затем взять худший результат и сравнить с пороговым значением.
Оценивать близость точки надо по трём характеристикам: координате, виду точки (точка линии, точка конца, точка пересечения) и направлению линий в точке. Так как мы игнорируем аффинные преобразования, то искать надо наиболее близкую по всем трём характеристикам точку в некоторой окрестности точки.
Шаг 2. Продумать интерфейс программы
Так как два рисунка будут рядом, то надо соответственно две панели рядом. С ними в коде программы свяжем объекты типа Graphics. Также нужна будет кнопка, нажатием которой пользователь даст знать, что он закончил рисовать и пора сравнивать.
Результат следует сообщить в виде сообщения с вердиктом ПОХОЖ или НЕ ПОХОЖ.
Следует помнить, что интерфейс всегда можно переделать под задачу или удобство использования, поэтому зацикливаться на чём-то определённо не стоит.
Шаг 3. Написать код программы
Про рисование и типы Graphics и Bitmap писал в предыдущем примере.
Далее полученные рисунки надо проанализировать и выделить самые ценные точки: окончаний и пересечений, если их будет не хватать, то точки линий. Так как точки это не просто точки, а обладающие особыми характеристиками объекты, то какой-то из стандартных классов не подойдёт, а следовательно придётся придумать свой.
class xPoint { public int x, y, //координата
t, //тип:0 - точка ,1 - Х, 2 - окончание, 3 - часть линии
v; //направление - по кругу: 1, 2 ,3 ,4 ,5 ,6 ,7, 8
public double r = 1d; //от наиболее похожей точки
//для разных видов точек разные конструкторы
public xPoint(int _x, int _y, int _t) { x = _x; y = _y; t = _t; }
public xPoint(int _x, int _y, int _t, int _v) { x = _x; y = _y; t = _t; v = _v; }
//метод для примера: вызывает сообщение с координатами и r
public void GetInfo() {
MessageBox.Show($"координаты: {x};{y}, расстояние: {r}"); } }
Зачем нужны классы и методы я уже раньше рассказывал. Официальная документация про классы тут и тут, а тут про методы. Поэтому сейчас просто скажу пару слов по нашему примеру. А к документации следует обратиться, именно за тем, чтобы узнать как пользоваться по-другому, для какой-то отличающейся задачи.
Только что мы объявили класс xPoint. Объект этого класса будет иметь координаты, а также хранить информацию о направлении линий в ней и о том, насколько похожая точка есть в другом изображении. Метод класса, который создаёт его новый экземпляр называется конструктором. Конструктор вызывается после ключевого слова new. Если конструктор не определить, то экземпляр класса создаётся с помощью конструктора по-умолчанию new xPoint(); .
Метод GetInfo() не имеет входных параметров (в круглых скобках) и не возвращает значения (тип void), его можно использовать не только внутри класса (модификатор public).
Чуть дальше приведу примеры использования этих конструкторов и метода класса.
Области видимости и модификаторы
Обычно переменные и методы можно использовать только в том контексте (области видимости), где они были объявлены. Например, переменная, объявленная внутри цикла не доступна после выхода их него.
Поля (переменные) класса и его методы могут иметь модификаторы. Например использованный выше модификатор public перед объявлением поля x является модификатором доступа, который указывает, что поле x может быть доступно для использования снаружи класса, а не только в своём контексте, ограниченном фигурными скобками.
Свойства классов (это такие методы, которые устанавливают значение полей или возвращают его в зависимости от применения) их ещё называют геттеры-сеттеры также могут иметь модификаторы, причём для блоков get и set можно изменить модификатор.
Кроме модификаторов доступа есть ещё модификатор static, который указывает на то, что такое поле можно использовать без создания экземпляра класса. При этом для этого поля будет заранее выделена память, и какие бы изменения не делались с этими полями в различных экземплярах этого класса - они все будут менять значение, хранящееся в этой выделенной области памяти, а не каждое своё.
Вернёмся к написанию программы и напишем метод задающий параметры точки.
Этим методом будет обработчик кнопки, по которой будем осуществлять сравнение. В него напишем оставшуюся часть программы.
Сделаем по коллекции типа List<T> для каждого рисунка так: List<xPoint> i1 = new List<xPoint>(); (для второго рисунка соответственно i2).
Эти коллекции будем заполнять особыми точками: i1.Add(new xPoint(_x, _y, 0)); или так i1.Add(new xPoint(75, 16, 2, 7)); . Программа сама поймёт какой конструктор выбрать и какие значения им заполнить. Как только в коллекции i1 появятся значения, мы можем вызвать метод GetInfo() для конкретного экземпляра, например для последнего добавленного так: i1.Last().GetInfo(); .
Небольшое отступление про пространства имён
Видите как точка позволяет заглянуть внутрь класса. Так же точка позволяет заглянуть в пространство имён. Например Color может быть в разных пространствах имён, поэтому в начале программы строкой using System.Drawing; мы объявляем, что System.Drawing.Color это то же, что просто Color. Если нам понадобится использовать Color из другого пространства имён, то придётся это писать полностью.
И немного про ссылочные и значимые типы
В прошлом примере мы уже столкнулись с тем, что надо учитывать случаи когда при изменении копии переменной изменяется оригинал. Если в метод передаются простые типы (вроде int), то в памяти создаётся копия с которой работают, а если массив или что сложнее, то тогда изменяется начальный вариант.
В документации есть много информации про типы и ссылки, но повторюсь, что считаю, учебник надо пробежать просто для ознакомления с возможностями языка, а к документации обращаться когда столкнётесь с проблемой.
Вернёмся к написанию программы
Циклом определим цвет для каждой точки изображения, и если он будет чёрный, то узнаем особенная ли эта точка. Просто сравнить цвет (b1.GetPixel(x, y) == Color.Black) нельзя, так как для сравнения структур в C# требуется перегрузка операторов == и != . Структуру Color создавали не мы, поэтому лучше написать отдельный метод для определения чёрный ли это цвет.
private bool isBlack(Color cl) {
bool Result = false;
if (cl.A == 255 && cl.R == 0 && cl.G == 0 && cl.B == 0) Result = true;
return Result; }
Воспользуемся нашим методом например так: if (isBlack(b1.GetPixel(_x, _y))){MessageBox.Show($"точка {_x} {_y} чёрная");}. Обратите внимание, что если в строку надо вставить переменную, то перед строкой следует поставить знак $, а переменную взять в фигурные скобки. Конечно можно было бы каждый раз полностью сравнивать все четыре члена структуры, но нам эта операция понадобится очень часто, поэтому красивее и быстрее написать свой метод.
Далее надо проанализировать каждую найденную чёрную точку, чтобы понять какого она вида.
На иллюстрации видно, что просто считая количество пикселей рядом нельзя отличить линию от перекрёстка. Если просто запомнить модели какими бывают линии и перекрёстки, то в случае правого изображения все тёмные пиксели будут считаться перекрёстками.
Задача кажется сложной, потому что в одно действие не решается. Минуту поразмышляв, в голову придёт верная мысль, что надо считать не количество пикселей рядом, а количество смен цвета с белого на чёрный. Тогда оба рисунка однозначно будут расценены как точка линии, но в правом это один пиксель линии между двумя перекрёстками. Вторым действием надо сделать костыль цикл, который склеит пересечения, находящиеся на расстоянии одного пикселя друг от друга.
Более сложным случаям является когда вокруг точки 7 или 8 других чёрных пикселей. Чтобы такого не было, следует обработать изображения каким-то методом скелетизации. После него линии уменьшатся по толщине до 1 пикселя, я воспользуюсь алгоритмом секлетизации Зонга-Суня. Многие задачи имеют оптимальный алгоритм решения, знать их не обязательно, но может быть полезно, например на собеседованиях на работу (не важно на каком языке программирования) частенько просят написать реализацию классического алгоритма сортировки массива пузырьком или нахождения редакционного расстояния.
Принятие решения
Для каждой точки найдём лучший результат по формуле:
r = max(коэффициент_ценности_одинакового_типа(тип) * если_окончание_коэффициент(разница_направления_точки) * коэффициент_ценности_просто_расстояний(расстояние))
Самым худшим результатом посчитаем точки разного вида, находящиеся в разных концах рисунка. Это будет единица (1 * 1 *1). Коэффициенты подберём эмпирически.
Если при переборе всех пикселей изображения было удобнее использовать цикл for (int _x = 1; _x < b1.Width; _x++), то для перебора коллекций удобнее использовать цикл foreach (xPoint i in i1).
Есть ещё масса путей для совершенствования разработанного в этой статье алгоритма сравнения, вроде подбора оптимальных коэффициентов для разных характеристик точек и учёта аффинных преобразований. Но и в таком виде программа выполнила свою задачу: позволила применить знание классов и методов в C# .
_______________________________
Если эта статья Вам понравилась, то рекомендую взять на вооружение использование истории изменений и коллективную разработку в среде Visual Studio.
В следующей статье цикла рассказывается про асинхронную, многопоточную и параллельную работу. В рассмотренном примере (программе для проверки доступности ресурса в сети) применяются такие конструкции попытки как try-catch и using, а также делегаты и лямбды.