Як навколо об’єкта на зображенні намалювати червоний прямокутник в ZennoPoster

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

Значить публікації з якими попередньо потрібно ознайомитись знаходяться тут:
Як перевести кольорове зображення в чорнобіле
Як знайти символи на зображенні

Після чого, візьму для прикладу зображення годинника. Таке зображення captcha використовувалось колись на сервісі tutanota. Пригадую у 2019 році на форумі користувач indigo666 розміщав на третьому конкурсі шаблонів простенький реєстратор аккаунтів, а в жовтні 2022 року користувач LInewort просив, щоб йому допомогти розпізнати такі зображення. І хоча користувач з логіном LInewort навіть дякую не сказав, після того як одержав готовий код, який розпізнавав такі зображення – всеодно мені здається завдання цікавим саме з навчальною метою. Ось відео, в якому показано який одержується результат:

Власне робота буде проводитись приблизно над такими зображеннями:
captcha tutanota з зображеним годинником

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

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

Func<int[,], int, List<int>> SumX = (int[,] array, int element) => {
	List<int> list = new List<int>();
	int width = array.GetLength(1);
	int height = array.GetLength(0);
	for (int i = 0; i < height; i++){
		int sum = 0;
		for (int j = 0; j <width; j++) {
			if(element == array[i,j])sum+=array[i,j];
		}
		list.Add(sum);
	}
	return list.ToList();;
};
project.Context["SumX"] = SumX;

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

int[,] img1 =project.Context["item"];
var SumX = project.Context["SumX"];
List<int> list_sumX = SumX(img1, 1);
project.SendInfoToLog(Global.ZennoLab.Json.JsonConvert.SerializeObject(list_sumX,  Global.ZennoLab.Json.Formatting.None));

В фрагменті коду я одержаний результат вивів в лог в форматі JSON, для того, щоб можна було побачити значення:

[
0,0,0,0,19,27,24,18,16,14,
20,17,14,17,20,14,18,10,10,8,
8,8,8,10,14,10,8,8,8,6,
8,8,8,12,13,23,24,31,37,31,
32,23,13,13,14,12,14,13,12,11,
14,10,8,8,8,8,10,10,8,10,
20,17,14,17,16,18,20,18,24,27,
23,15,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
]

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

Func<List<int>, int> Top = (List<int> list) => {
	int top = 0;
	for (int i = 0; i < list.Count; i++){
		if(list[i] !=0 ) {
			top = i;
			break;		
		}
	}
	return top;
};
project.Context["Top"] = Top;

Func<List<int>, int> Bottom = (List<int> list) => {
	int bottom = 0;
	for (int i = list.Count-1; i > 0; --i){
		if(list[i] !=0 ) {
			bottom = i;
			break;		
		}
	}
	return bottom;
};
project.Context["Bottom"] = Bottom;

Тепер, я можу визивати ці функції в інших блоках свого проєкту в програмі ZennoPoster:

var SumX = project.Context["SumX"];
var Top = project.Context["Top"];
var Bottom = project.Context["Bottom"];

int[,] img1 =project.Context["item"];

List<int> list_sumX = SumX(img1, 1);

int top = Top(list_sumX);
int bottom = Bottom(list_sumX);

project.SendInfoToLog(string.Format("top: {0} bottom: {1}", top, bottom),true);

Результат я відправив в лог, тому там я побачу приблизно такий рядок:

top: 4 bottom: 71

Якщо відняти від bottom top – одержимо висоту: 71-4 = 67 пікселів.
Не дивно, що розрахунок менший на 1 ніж я описав вище – справа в тому, що індекси в списках і масивах починаються не з 1 а з 0. Але це не страшно, тому що в подальшому ці дані будуть передані в інший код, і там також перебиратись масив буде з індексу 0.

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

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



Func<List<int>, int> Left = (List<int> list) => {
	int top = 0;
	for (int i = 0; i < list.Count; i++){
		if(list[i] !=0 ) {
			top = i;
			break;		
		}
	}
	return top;
};

Func<List<int>, int> Right = (List<int> list) => {
	int bottom = 0;
	for (int i = list.Count-1; i > 0; --i){
		if(list[i] !=0 ) {
			bottom = i;
			break;		
		}
	}
	return bottom;
};

project.Context["SumY"] = SumY;
project.Context["Left"] = Left;
project.Context["Right"] = Right;

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

var SumY = project.Context["SumY"];
var Left =project.Context["Left"];
var Right = project.Context["Right"];

List<int> list_sumY = SumY(img1, 1);
int left = Top(list_sumY);
int right = Bottom(list_sumY);

project.SendInfoToLog(string.Format("left: {0} right: {1}", left, right),true);

Вивід в лог показує приблизно такі дані:

left: 180 right: 248

Якщо відняти від правої границі ліву – одержимо ширину годинника: right-left=248-180=68 пікселів.

Тобто, співставивши одержані дані, ми повинні одержати 4 координати:

top_left: x=180, y=4;
bottom_left: x=180, y=71;
top_right: x=248,  y=4;
bottom_right: x=248, y=71;

Ширина 68 пікселів і Висота – 67 пікселів.

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

Func<int[,], int, int, int, int, int[,]> CreateNewImage = (int[,] array, int top, int bottom, int left, int right) => {
	int[,] array_out = new int[bottom-top, right-left];	
	for (int i = top; i < bottom; i++){
		for (int j = left; j < right; j++) {
			array_out[i-top,j-left] = array[i,j];
		}	 
	}
	return array_out;
};

project.Context["CreateNewImage"] = CreateNewImage;

Тепер, коли масив масивів буде переводитись в зображення, можна для всіх пікселів з мінімальним і максимальним значенням x і y встановити червоний колір, і у цьому випадку годинник буде вписаним в червоний прямокутник.
Але у цьому випадку, будуть “затерті” крайні пікселі об’єкта. Тому, при визові описаної вище функції необхідно змістити границі на 1 піксель вверх, на 1 піксель вниз, на 1 піксель вліво і на 1 піксель вправо. Нижче приклад коду, який змістить границі:

var CreateNewImage = project.Context["CreateNewImage"];
var img2 = CreateNewImage(img1, top-1, bottom+1, left-1, right+1);
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++) {
        	if(i == 0 )  output.SetPixel(j,i,  Color.Red);
			else if(j == 0)   output.SetPixel(j,i,  Color.Red);
			else if(i == height-1)  output.SetPixel(j,i,  Color.Red);
			else if(j == width-1)  output.SetPixel(j,i,  Color.Red);
			else {
            	var color = array[i,j] == 1 ? Color.Black : Color.White;
            	output.SetPixel(j,i,  color);
			}
        }
    }
    return output;
};
project.Context["Bin2Bitmap"] = Bin2Bitmap;

Визиваю функцію, яка перетворить масив в Bitmap, і зберігаю зображення поряд з проєктом ZennoPoster:

var img2 = project.Context["item"];
string path_input = Path.Combine(project.Directory, "a.jpg");

using(var bmp = Bin2Bitmap(img2)){
	bmp.Save(path_input); 
}

Результатом виконання буде приблизно таке зображення годинника в червоній однопіксельній рамочці розміром 70×70 пікселів:
годинник з captcha з червоною рамочкою розміром 1 піксель

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

Власне місія виконана – об’єкт вирізаний, намальований на новому зображенні, і навколо об’єкта намальована червона рамочка.

2 коментаря до “Як навколо об’єкта на зображенні намалювати червоний прямокутник в ZennoPoster”

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

  2. Спасибі, Роман!
    Я розумію спосіб який Ви описуєте.
    Скоріше за все у мене просто недостатньо досвіду щоб ним скористатись.

    Способом який я описав в публікації нічого не помішає мені обвести декілька об’єктів на зображенні рамочкою.
    Також, замість рамочки, я можу обвести об’єкт наприклад по периметру.
    І в мене є чітке бачення як саме я це можу зробити швидко.
    А способом який Ви пропонуєте – я не знаю як вирішувати такі завдання…

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

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