Звичайно, що намалювати червоний прямокутник навколо об’єкта не важко – потрібно просто на зображенні змінити колір пікселів з білого (фонового) на червоний. Проте важливим є те, як на практиці вирішити це завдання. Тому, використовуючи інформацію з попередніх публікацій продовжу писати C# код.
Значить публікації з якими попередньо потрібно ознайомитись знаходяться тут:
– Як перевести кольорове зображення в чорнобіле
– Як знайти символи на зображенні
Після чого, візьму для прикладу зображення годинника. Таке зображення captcha використовувалось колись на сервісі tutanota. Пригадую у 2019 році на форумі користувач indigo666 розміщав на третьому конкурсі шаблонів простенький реєстратор аккаунтів, а в жовтні 2022 року користувач LInewort просив, щоб йому допомогти розпізнати такі зображення. І хоча користувач з логіном LInewort навіть дякую не сказав, після того як одержав готовий код, який розпізнавав такі зображення – всеодно мені здається завдання цікавим саме з навчальною метою. Ось відео, в якому показано який одержується результат:
Власне робота буде проводитись приблизно над такими зображеннями:
Так як зображення крім чорного та білого кольорів містить також відтінки сірого – приміняю до нього функції, які спочатку переведуть зображення в чорний та білий кольори, а потім з зображення буде переведено в масив масивів з значеннями нулів і одиниць. В цьому випадку немає необхідності визначати області зв’язності, так як на рисунку знаходиться один об’єкт – годинник. На всіх подібних зображеннях він має один розмір. Тому завдання в тому, щоб вирізати цього годинника і розмістити його на іншому зображенні, з розміром самого годинника. А щоб продемонструвати процес, зручно намалювати спочатку на зображенні червоний описаний прямокутник, тим самим візуально впевнитись що об’єкт був знайдений вірно.
Щоб знайти верхню і нижню границю годинника, який зображений на рисунку, я описав функцію, яка на вхід буде приймати масив масивів на значення елемента, яке потрібно порахувати. Для підрахунку чорних пікселів буду передавати значення 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 пікселів:
Уважний читач напевне замітив, що в публікації частково використовується код із публікацій, на які я посилався вище. Справа в тому, я подумав це краще, ніж відправляти читача повторно йти і шукати необхідну функцію там, де я їх описував перший раз. Крім того, я міг в деяких місцях внести зміни. З іншої сторони, використовувати код написаний одного разу багато разів – це хороша звичка.
Власне місія виконана – об’єкт вирізаний, намальований на новому зображенні, і навколо об’єкта намальована червона рамочка.
Друг во первых поддержу твой блог комментарием, и постараюсь его посещать чаще. Ты многим помог и скорее всего не плохой человек. По теме, все намного проще, если конкретно нужно рамку, то создаем полотно чуть больше оригинального изображения, и накладываем друг на друга.
Спасибі, Роман!
Я розумію спосіб який Ви описуєте.
Скоріше за все у мене просто недостатньо досвіду щоб ним скористатись.
Способом який я описав в публікації нічого не помішає мені обвести декілька об’єктів на зображенні рамочкою.
Також, замість рамочки, я можу обвести об’єкт наприклад по периметру.
І в мене є чітке бачення як саме я це можу зробити швидко.
А способом який Ви пропонуєте – я не знаю як вирішувати такі завдання…