Як розділити зображення на символи програмою ZennoPoster

Користувачі програми ZennoPoster займаються автоматизацією – пишуть ботів, які щось роблять в інтернеті. Власники сайтів встановлюють на своїх сайтах captcha щоб обмежити ботам доступ до деяких функцій, наприклад для відправки коментаря. Але автоматизаторів не завжди це зупиняє. І сьогодні я продовжу розповідати про те, що можна дальше робити з зображенням, яке було переведено раніше у чорний та білий кольори.

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

Щоб одержати доступ до кольорів кожного пікселя на зображенні потрібно використовувати тип даних Bitmap. Тому файл спочатку потрібно прочитати, і перевести його у Bitmap. Але часто, після внесення змін в одному блоці проєкту потрібно продовжити роботу в іншому блоці. Так як в проєктах ZennoPoster всі локальні змінні мають текстовий тип даних, то буває зручним після зчитування зображення переводити його в base64. А потім коли необхідно – переводити в Bitmap і при виході з блока – знову переводити в base64. Це я вказав тому, щоб було зрозуміло, чому я частіше буду використовувати саме base64, а не зчитування з файла – вважаючи, що воно було прочитано раніше і поміщено в локальну змінну у вигляді текстового рядка base64. Також зазначу, що в даному випадку я маю справу з маленькими зображеннями, розміром 300 пікселів в ширину і 100 в висоту. Тому вони займають мало місця всередині локальних змінних ZennoPoster. Але коли мені прийшлось би мати справу з великими зображеннями, наприклад 10 000 пікселів в ширину і 5000 в висоту – то скоріш за все я не зберігав би їх всередині локальних змінних, тому що вони займали би багато пам’яті.

Так от, маючи Bitmap з зображенням, я без проблем одержую колір пікселя визиваючи метод GetPixel, а також я можу записати в піксель необхідний колір, використовуючи метод SetPixel. Але, доступ до пікселів зображення може займати багато часу (тому що пікселів на зображенні багато). Тому, знаючи що кожен піксель білий або чорний – мені здається зручнішим прочитати їх один раз в масив масивів (int[,]), і білий колір позначити як 0, а чорний – як 1. Після чого я зможу дуже швидко отримувати доступ до кожного значення, рахувати їх, змінювати. І після внесення всіх необхідних змін, я можу просто на новому Bitmap намалювати зображення уже використовуючи значення із масиву масивів. Знаю що дехто використовує для подібного табличку в ZennoPoster, дехто список списків. Кому як зручніше.

Сама функція, яку я буду використовувати для перетворення Bitmap в числовий масив масивів буде виглядати так:

Func<Bitmap,int[,]> BitmapToArray = (Bitmap input) =>{
	int[,] array = new int[input.Height, input.Width];
	for(int y=0;y<input.Height;y++) { 
	    for(int x=0;x<input.Width;x++) {
			array[y,x] = input.GetPixel(x, y).B > 0 ? 0 : 1;
		} 
	}
	return array;
};

project.Context["bintoarray"] = BitmapToArray;

Тоді визвати цю функцію я зможу так, як показано на фрагменті C# коду нижче:

var BitmapToArray = project.Context["bintoarray"];
string path_input = Path.Combine(project.Directory, "b.jpg");

using(var bmp = new Bitmap(path_input)){
	var array = BitmapToArray(bmp);
	project.SendInfoToLog( Global.ZennoLab.Json.JsonConvert.SerializeObject(array,  Global.ZennoLab.Json.Formatting.None));
}

Там є вивід в лог значення, яке було одержано в результаті у вигляді текстового json рядка.
Звичайно, у проєкті, який буде виконувати роботу я приберу цей рядок.
Проте, для того, щоб можна було глянути що саме відбувається, мені зручно виводити в лог.

Наступний крок – це виділення фігур на зображенні. Заглибитись в теорію обробки зображень і розпізнавання образів на зображеннях можна здійснивши пошук за ключовими словами: алгоритми виділення зв’язаних областей, сегментація зображення, дискретизація. Проте, для того щоб скористатись фрагментом коду і виділити букви на зображенні немає необхідності вивчати всю теорію. Я сам взяв за основу інформацію з публікації 2011 року, коли мені вперше приходилось виділяти окремі букви на зображенні. Я адаптував описаний там алгоритм для роботи в ZennoPoster спочатку на блоках, потім на блоках і MySQL, а потім на C#. Тому буду вважати, що зображення яке було переведено в масив масивів є таким, як його розглядає і автор публікації.

Наступних декілька абзаців я розкажу про те, як я генерував html табличку з одержаного раніше масиву масивів, щоб получити збільшене зображення пікселів, в цілому щоб в будь-який момент часу можна було подивитись як виглядають розмічені області. Звичайно, що після того, як завдання було вирішено, візуалізація процесів перестала бути необхідною – і ці фрагменти коду в проєктах не використовувались (хоча дуже допомогли розібратись в темі).

Одержаний масив масивів бінарного зображення буде мати приблизно такий вигляд:

int[,] array = new int[,] {
	{0,0,0,0,0,0,0,0,0,0,0},
	{0,0,0,0,0,0,0,0,0,0,0},
	{0,0,0,0,0,0,0,1,0,0,0},
	{0,0,0,1,1,0,0,1,0,0,0},
	{0,0,0,1,1,0,0,1,1,0,0},
	{0,1,1,1,1,1,0,1,1,1,1},
	{0,1,1,1,1,0,0,0,0,0,0},
	{0,1,1,1,0,0,0,0,0,0,0},
	{0,0,0,0,0,0,0,0,0,0,0},
	{0,0,0,0,0,0,0,1,1,0,0},
	{0,0,0,0,0,0,1,1,1,0,0},
	{0,1,1,0,0,1,1,1,1,0,0},
	{0,1,1,0,0,1,1,1,1,1,1},
	{0,1,1,1,0,1,1,0,1,1,0},
	{0,0,0,0,0,0,0,0,0,0,0}
};

Щоб подивитись на нього в браузері ZennoPoster я добавив блок який містить стиль:

  <style>
    body { text-align: center; }
    table {  margin: 0 auto; text-align: center; }
    td { font-family:courier; width: 25px; height: 25px; background-color:#99CCFF; border: 1px solid black; }
    
 .white { background-color: #99CCFF;  color: black; }
 .black { background-color:  #7DA647; color: black;}
 .red { background-color: red; color: blue; }
  </style>

Після чого згенерував html табличку (в прикладі тільки 2 клітинки, в реальності для кожного елементу масиву масивів створювалась окрема клітинка), яка використовує описані вище стилі:

  <table id="img">
    <tbody>
      <tr>
        <td class="white">0</td>
        <td class="black">1</td>
      </tr>
    </tbody>
  </table>

Створив функцію генерації html таблички, структура якої описана вище:

 Func<int[,], Dictionary<int, string>,string> GenerateHtmlTable = (int[,] img, Dictionary<int, string> colorMap) => {
    var htmlBuilder = new StringBuilder();
    htmlBuilder.AppendLine("<table id='img'>");
    for (int i = 0; i < img.GetLength(0); i++) {
        htmlBuilder.AppendLine("<tr>");
		
        for (int j = 0; j < img.GetLength(1); j++) {
            int value = img[i, j];
            string color = colorMap.ContainsKey(value) ? colorMap[value] : "black";
            htmlBuilder.AppendLine(string.Format("<td class='{0}'>{1}</td>", color, value));
        }
        htmlBuilder.AppendLine("</tr>");
    }
    htmlBuilder.AppendLine("</table>");
    return htmlBuilder.ToString();
};

var colorMap1 = new Dictionary<int, string>  {
    {0, "white"},  
    {1, "black"}
};

Після чого, вже готову згенеровану табличку добавив для відображення в браузері ZennoPoster:

string htmlTable = GenerateHtmlTable(array, colorMap1);
string centeredHtml =string.Format("<div style='margin: 0 auto; text-align: center;'>{0}</div>",htmlTable);
HtmlElement he = instance.ActiveTab.FindElementByAttribute("body", "fulltag", "body", "text", 0);
he.SetAttribute("innerhtml", centeredHtml);     

В результаті, побачив табличку, як на картинці нижче:

Тепер можна пройтись по кожному пікселю і визначити до якої області зв’язаних пікселів він відноситься. Нагадаю, що в даний момент у нас всього дві області – фон – позначений як 0, і всі інші закрашені області – помічені як 1.
Тому, ми будемо вважати, що новий об’єкт буде поміченим значенням 2, потім наступний значенням 3, тобто кожній потенційно новій області пікселів будемо задавати нове значення.

Згідно публікації автора, те, яким номером буде помічений конкретний піксель, залежить виключно від пікселя, який находиться над ним, і пікселя який находиться зліва (у випадках, коли по прохід по пікселям зображення відбувається зверху-вниз і зліва-направо). Для зручності мислення над завданням конкретний піксель який в даний момент перевіряється автор позначає буквою A, піксель над ним буквою C, і піксель зліва – буквою B. А позиції цих трьох пікселів називає маскою.

Так от взяли ми конкретний піксель, наприклад з координатами A(x)=10, A(y)=10 і у нього значення 0, то ми можемо сказати, що елемент маски A(10,10) = 0.
Вище елемента A знаходиться елемент маски С.
Координати елемента С будуть C(x)=A(x), C(y)=A(y)-1.
Тобто елемент C(10,9) і він буде мати якесь значення, наприклад 0;
І залишився елемент B, який знаходиться зліва від елемента A.
У нього B(x)=A(x)-1, B(у)=A(y), і значення, наприклад 0.
Тобто B(9,10) = 0

Так от, у випадках, коли ми розглядаємо самий верхній ряд пікселів, або самий лівий ряд пікселів – то в цей момент одна із координат елементів маски C чи B, чи навіть обидві (при пікселі з координатами 0,0) будуть мати від’ємні значення, і ми не зможемо прочитати значення цих пікселів з зображення. Тому, вважається, що за межами зображення об’єктів немає, і при від’ємних координатах елемент маски одержує значення 0 за замовчуванням.

Тепер трішки потрібно відійти від координат і розглянути власне самі значення маски.
Маска у нашому випадку має три елементи, і вони мають якісь значення, і воно буде співпадати з номером об’єкта області зв’язності.
Якщо значення встановлене, то воно буде мати більше 0, будь-яке число більше 0 нижче позначу як 1.
Випишу таблицю варіантів:

A B C
P! = 0 0 0 – вважаємо що це фон, переходимо до наступного пікселя

P2 = 0 0 1 – знаходимось внизу під об’єктом на пікселі фону, переходимо до наступного пікселя

P3 = 0 1 0 – знаходимось справа від об’єкта на пікселі фону, переходимо до наступного пікселя

P4 = 0 1 1 – знаходимось справа внизу від об’єкта на пікселі фону, переходимо до наступного пікселя

P5 = 1 0 0 – вважаємо що піксель – це новий об’єкт – записуємо нову мітку значення A, переходимо до наступного пікселя

P6 = 1 1 0 – вважаємо що A це продовження об’єкта з міткою B, помічаємо A значенням B і переходимо до наступного пікселя

P7 = 1 0 1 – вважаємо що A це продовження об’єкта з міткою С, помічаємо A значенням С і переходимо до наступного пікселя

P8 = 1 1 1 – вважаємо що ABC зв’язані, тобто це один об’єкт, тому якщо B != C тоді всі С = B, після чого помічаємо A значенням з B і переходимо до наступного пікселя

Я спеціально зробив скриншоти всіх можливих варіантів, тому що коли я читав першоджерело, там був скриншот, який позначав позиції маски в тих чи інших випадках. Автор посилався на це зображення в коді, і мені було зовсім не зрозуміло те, що на зображенні піксель A являється фоновим, а автор описує що його чомусь потрібно перекрасити, помітивши його фігурою. І коли я відобразив всі варіанти, тоді зрозумів, що автор помилився (в тексті він говорить правильно, що якщо елемент A не розмічений, то ми переходимо до наступного пікселя, проте на скриншоті відображені саме варіанти в яких потрібно пропустити піксель).

Func<int[,], int, int, int> GetElementA = (int[,] array, int A_x, int A_y) => {
  int element = array[A_y, A_x];
  return element;
};

Func<int[,], int, int, int>  GetElementB = (int[,] array, int A_x, int A_y) => {
  int element = 0;
  int element_y = A_y-1;
  if (element_y <= 0) { element_y = 1; element = 0; }
  else { element = array[element_y, A_x];}	
  return element;
};

Func<int[,], int, int, int>  GetElementC = (int[,] array, int A_x, int A_y) => {
  int element = 0;
  int element_x = A_x-1;
  if (element_x <= 0) { element_x = 1; element = 0; }
  else { element = array[A_y, element_x];}	
  return element;
};

project.Context["A"]=GetElementA;
project.Context["B"]=GetElementB;
project.Context["C"]=GetElementC;

Для того, щоб визначити в якій саме позиції находиться маска в даний момент, я вирішив описати всі перевірки окремо в функції, і добавити їх в контекст, щоб можна було визивати їх в необхідний момент:

Func<int, int, int, bool> CheckP1 = (int ElementA, int ElementB, int ElementC) => {
	return ElementA == 0 && ElementB == 0 && ElementC == 0;
};

Func<int, int, int, bool> CheckP2 = (int ElementA, int ElementB, int ElementC) => {
	return ElementA == 0 && ElementB == 0 && ElementC != 0;
};

Func<int, int, int, bool> CheckP3 = (int ElementA, int ElementB, int ElementC) => {
	return ElementA == 0 && ElementB != 0 && ElementC == 0;
};

Func<int, int, int, bool> CheckP4 = (int ElementA, int ElementB, int ElementC) => {
	return ElementA == 0 && ElementB != 0 && ElementC != 0;
};


Func<int, int, int, bool> CheckP5 = (int ElementA, int ElementB, int ElementC) => {
	return ElementA != 0 && ElementB == 0 && ElementC == 0;
};

Func<int, int, int, bool> CheckP6 = (int ElementA, int ElementB, int ElementC) => {
	return ElementA != 0 && ElementB != 0 && ElementC == 0;
};

Func<int, int, int, bool> CheckP7 = (int ElementA, int ElementB, int ElementC) => {
	return ElementA != 0 && ElementB == 0 && ElementC != 0;
};

Func<int, int, int, bool> CheckP8 = (int ElementA, int ElementB, int ElementC) => {
	return ElementA != 0 && ElementB != 0 && ElementC != 0;
};

var dic = new Dictionary<int, Func<int, int, int, bool>>();

dic.Add(1, CheckP1);
dic.Add(2, CheckP2);
dic.Add(3, CheckP3);
dic.Add(4, CheckP4);
dic.Add(5, CheckP5);
dic.Add(6, CheckP6);
dic.Add(7, CheckP7);
dic.Add(8, CheckP8);

project.Context["check"] = dic;

В фрагменті коду, який находиться вище я описав функції, і записав їх в словник, а уже словник помістив у контекст, і тепер визивати будь-яку перевірку я зможу у вигляді:

var dic_check = project.Context["check"];
return dic_check[1](0,0,0);

Замість 1 я укажу номер позиції маски, які описані вище і зображені на скриншотах, а замість 0, 0, 0 я буду вказувати реальні значення елементів A, B, C. Результатом виконання перевірки буде True або False. В залежності від результату, я буду виконувати необхідну дію, а саме помічати піксель новим значенням, переходити до наступного пікселя чи помічати область зв’язності з маркером C у міткою поміщеною в B.

Щоб бачити зміни в браузері ZennoPoster описав також функцію, яка буде відмальовувати значення в табличці:

Action<HtmlElement, int> Draw = (HtmlElement td, int num) => {
	if(num > 0) td.SetAttribute("class", td.GetAttribute("class") + " blue");	
	td.SetAttribute("innerText",num.ToString());
};

project.Context["Draw"] = Draw;

Також, коли необхідно в позиції 8 замінити всі зв’язані мітки з однієї на іншу – також рішив описати у вигляді функції, тому що цей функціонал ще пригодиться, коли потрібно буде кожну фігуру відмалювати окремо:

Func<int[,], int, int, int[,]> ReplaceC2B = (int[,] array, int ElementB, int ElementC) => {
	int width = array.GetLength(1);
	int height = array.GetLength(0);
	int[,] matrix = new int[height,width];
	for (int i = 0; i < height; i++){
	    for (int j = 0; j <width; j++) {
	        matrix[i, j] = array[i,j] == ElementC ? ElementB : array[i,j];		
	    }
	}
	return matrix;
};

project.Context["ReplaceC2B"] = ReplaceC2B;

Тепер, використовуючи функції, які вже були описані вище можна сформувати логіку, яка буде розмічати зв’язані області на зображенні. Варіант, який я використовував з відображенням даних в таблиці в інстансі браузера ZennoPoster буде виглядати приблизно так:

var dic_check = project.Context["check"];
var ReplaceC2B = project.Context["ReplaceC2B"];
var Draw = project.Context["Draw"];
int A = 0;
int B = 0;
int C = 0;
int cur = 10;

HtmlElement tb = instance.ActiveTab.FindElementById("img");
for(int m=0; m<img1.GetLength(0);m++){
	HtmlElementCollection trs = tb.FindChildrenByTags("tr");
	HtmlElementCollection tds = trs.Elements[m].FindChildrenByTags("td");
	
	for(int n=0; n<img1.GetLength(1);n++){		
		HtmlElement td = tds.Elements[n];
		Draw(td, img1[m,n]);
 		
		A = project.Context["A"](img1, n, m);
		B = project.Context["B"](img1, n, m);
		C = project.Context["C"](img1, n, m);
		
		bool check =  false;
		
		check = dic_check[1]( A, B, C );
		if(check) {
			Draw(td, img1[m,n]);
			continue; // 0 0 0
		}
		
		check =  dic_check[2]( A, B, C );
		if(check) {
			Draw(td, img1[m,n]);
			continue; // 0 0 1
		}
		
		check =  dic_check[3]( A, B, C );
		if(check){
			Draw(td, img1[m,n]);
			continue; // 0 1 0
		}
		
		check =  dic_check[4]( A, B, C );
		if(check) {
			Draw(td, img1[m,n]);
			continue; // 0 1 1
		}
		
		check =  dic_check[5]( A, B, C );
		if(check) {
			cur++;
		 	img1[m,n] = cur;
			Draw(td, img1[m,n]);
			continue; // 1 0 0
		}
		
		check =  dic_check[6]( A, B, C );
		if(check) {
			img1[m,n] = B;
			Draw(td, img1[m,n]);
			continue; // 1 1 0
		}
		
		check =  dic_check[7]( A, B, C );
		if(check) {
			img1[m,n] = C;
			Draw(td, img1[m,n]);
			continue; // 1 0 1
		}
		
		check =  dic_check[8]( A, B, C );
		if(check) {
			if(B != C) { 
				img1 = ReplaceC2B(img1, B, C); // змінюємо всі С на B
			}
			img1[m,n] = B; 
			Draw(td, img1[m,n]);
			continue; // 1 1 1
		}
	}
	for(int i=0;i<tds.Count;i++) Draw(tds.Elements[i], img1[m,i]);
}

Після виконання цього фрагменту, в масиві масивів img1 уже будуть дані про кожну фігуру, різні фігури будуть мати різні номери. В браузері буде відображена табличка, яка буде містити значення масиву масивів img1:

Але, в коді, який буде використовуватись не в навчальних цілях, а для виконання обробки великої кількості зображень краще змінити код, а саме видалити рядки, які займаються візуалізацією процесу. Ось фрагмент без візуалізації:

var dic_check = project.Context["check"]; // словник з функціями перевірки
var ReplaceC2B = project.Context["ReplaceC2B"]; // функція для заміни номера фігури

int A = 0; // Значення пікселя з яким проводиться робота
int B = 0; // Піксель який знаходиться зліва
int C = 0; // Піксель який находиться вище
int cur = 10; // Початковий номер фігури

for(int m=0; m<img1.GetLength(0);m++){
	for(int n=0; n<img1.GetLength(1);n++){		
        A = project.Context["A"](img1, n, m);
		B = project.Context["B"](img1, n, m);
		C = project.Context["C"](img1, n, m);
		
		bool check =  false;
		
		check = dic_check[1]( A, B, C );
		if(check)  continue; // 0 0 0
		 
		
		check =  dic_check[2]( A, B, C );
		if(check) continue; // 0 0 1
		 
		
		check =  dic_check[3]( A, B, C );
		if(check) continue; // 0 1 0
				
		check =  dic_check[4]( A, B, C );
		if(check) continue; // 0 1 1
				
		check =  dic_check[5]( A, B, C );
		if(check) {
			cur++;
		 	img1[m,n] = cur;
			continue; // 1 0 0
		}
		
		check =  dic_check[6]( A, B, C );
		if(check) {
			img1[m,n] = B;
			continue; // 1 1 0
		}
		
		check =  dic_check[7]( A, B, C );
		if(check) {
			img1[m,n] = C;
			continue; // 1 0 1
		}
		
		check =  dic_check[8]( A, B, C );
		if(check) {
			if(B != C) { 
				img1 = ReplaceC2B(img1, B, C); // змінюємо всі С на B
			}
			img1[m,n] = B; 
			continue; // 1 1 1
		}
	}
}

Тепер, можна написати функцію, яка візьме у список номера всіх відомих фігур розмічених в нашому масиві масивів:

Func<int[,], List<int>> GetFigureNumbers = (int[,] array) => {
	HashSet<int> list = new HashSet<int>();
	int width = array.GetLength(1);
	int height = array.GetLength(0);
	for (int i = 0; i < height; i++){
	    for (int j = 0; j <width; j++) {
			list.Add(array[i,j]);
	    }
	}
	return list.ToList();;
};


project.Context["GetFigureNumbers"] = GetFigureNumbers;

Тепер можна визвати функцію після розмітки зображення на зв’язані області:

var GetFigureNumbers = project.Context["GetFigureNumbers"];
List<int> figures = GetFigureNumbers(img1);
return string.Join(", ",figures.Select(ii => ii.ToString())); //тільки щоб подивитись в лог
// 0, 11, 12, 14, 16

В результаті буде видно номера фігур, які знаходяться на зображенні.
Наступне, що може знадобитись – це перемалювати кожну фігуру на окреме зображення.
Для цього, потрібно створити словник, ключами якого будуть номери фігур, а значеннями – новий масив масивів, до якого буде примінена функція, яка всі елементи значення яких еквівалентні номеру фігури позначить значенням 1, а всі інші – значенням 0, тим самим результатом вже буде масив масивів у такому ж вигляді, як був поданий на вхід на початку цієї публікації, тільки на ньому буде міститись тільки одна фігура.

Сам код функції, яка залишить за зображенні тільки фон і одну вказану фігуру:

Func<int[,], int, int[,]> NewFigure = (int[,] array, int Element) => {
	int width = array.GetLength(1);
	int height = array.GetLength(0);
	int[,] matrix = new int[height,width];
	for (int i = 0; i < height; i++){
	    for (int j = 0; j <width; j++) {
	        matrix[i, j] = array[i,j] == Element ? 1 : 0;
		
	    }
	}
	return matrix;
};

project.Context["NewFigure"] = NewFigure;

Ось так буде виглядати заповнення словника номерами фігур і їх зображеннями на окремих картинках:

var GetFigureNumbers = project.Context["GetFigureNumbers"];
List<int> figures = GetFigureNumbers(img1);

Dictionary<int, int[,]> dic_figures = new Dictionary<int, int[,]>();
var NewFigure = project.Context["NewFigure"];
foreach(int f in figures) {
	dic_figures[f] = NewFigure(img1, f);
}

А коли виконаю фрагмент коду, який знаходиться нижче, то у вікні інстанса браузера ZennoPoster побачу по черзі таблички, які будуть відповідати знайденим фігурам:

foreach(var k in dic_figures){
	htmlTable = GenerateHtmlTable(k.Value, colorMap1);
	centeredHtml =string.Format("<div style='margin: 0 auto; text-align: center;'>{0}</div>",htmlTable);
	he = instance.ActiveTab.FindElementByAttribute("body", "fulltag", "body", "text", 0);
	he.SetAttribute("innerhtml", centeredHtml);     

	tb = instance.ActiveTab.FindElementById("img");
	for(int m=0; m<k.Value.GetLength(0);m++){
		HtmlElementCollection trs = tb.FindChildrenByTags("tr");
		HtmlElementCollection tds = trs.Elements[m].FindChildrenByTags("td");
		for(int i=0;i<tds.Count;i++) Draw(tds.Elements[i], k.Value[m,i]);
	}
	Thread.Sleep(5*1000);
}

Як бачимо, фон також вважається за фігуру:

Потім йде сама верхня і сама ліва фігура:

Потім йде та, що чуть нижче, але максимально зліва:

Потім та що справа, але находиться вижче:

И остання, яка найнижче зліва:

Залишилась остання функція, яка повинна масив масивів перевести в зображення Bitmap:

Func<int[,], Bitmap> Bin2Bitmap = (int[,] array) => {
	int width = array.GetLength(1);
	int height = array.GetLength(0);
	var output = new Bitmap(width, height);  
	for (int i = 0; i < height; i++){
	    for (int j = 0; j <width; j++) {
			var color = array[i,j] == 1 ? Color.Black : Color.White;
			output.SetPixel(j,i, color);
	    }
	}
	return output;
};

project.Context["Bin2Bitmap"] = Bin2Bitmap;

Фрагмент коду вище займається створенням нового зображення Bitmap із нулів і одиничок які зберігаються в масиві масивів. Після чого одержані фігури можна відправляти на розпізнавання. Хоча, коли мені приходилось займатись подібною роботою – то наступний крок був вирівнювання всіх фігур в одну горизонтальну лінію і відмальовував я фігури так, щоб між ними було декілька пікселів фонового кольору. Власне дальше продовжується творча робота над одержаним зображенням (проте всю математику, наприклад підрахувати площу фігури, координати описаного прямокутника навколо фігури і подібне краще робити саме з масивом масивів, а переведення в зображення – це вже останній крок).

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *