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

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

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

Після чого текст на зображенні можна розпізнати вручну, використовуючи додаткові бібліотеки чи відправити на сервіс розпізнавання. Так як в даному випадку може бути важливим регістр букв і від однієї неправильно розпізнаної букви посилання на Google Table буде не корректним – рішив що розпізнавати цього разу буду в ручному режимі (крім того, не часто буває така необхідність, і в цілому розпізнавати треба тільки не велику кількість символів).

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

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

// Перший блок коду з функцією переведення зображення в чорнобілий колір
Func<Bitmap,double,Bitmap> BitmapBinary = (Bitmap input, double brightness) =>{
	var output = new Bitmap(input.Width,input.Height); // створив нове зображення 
	for(int y=0;y<input.Height;y++) { // прохід по висоті
	    for(int x=0;x<input.Width;x++) { // прохід по шинині
			bool check = input.GetPixel(x, y).GetBrightness() > brightness; // перевірка яркості пікселя відносно границі
			var color = check ? Color.White : Color.Black; // визначаю колір майбутнього пікселя
			output.SetPixel(x,y, color); // малюю піксель на новому зображенні
	    } 
	}
	return output; // повертаю нове зображення
};

project.Context["bin"] = BitmapBinary;

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

var BitmapBinary = project.Context["bin"];
double avg = 0.75f; // границя яркості пікселя
string path_input = Path.Combine(project.Directory, "a.jpg"); // вхідне зображення
string path_output = Path.Combine(project.Directory, "b.jpg"); // вихідне зображення

using(var bmp = new Bitmap(path_input)){ // прочитав зображення
	using(var birary = BitmapBinary(bmp, avg)){ // перекрасив пікселі в чорний і білий в залежності від яскравості
		birary.Save(path_output);// зберіг результат на диск
	}
}

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

А нижче показані зображення, які получаються при зміні границі яркості пікселя:
double avg = 0.1f;
зображення після обробки з границею яскравості double avg = 0.1f

double avg = 0.2f;
зображення після обробки з границею яскравості double avg = 0.2f

double avg = 0.3f;
зображення після обробки з границею яскравості double avg = 0.3f

double avg = 0.4f;
зображення після обробки з границею яскравості double avg = 0.4f

double avg = 0.5f;
зображення після обробки з границею яскравості double avg = 0.5f

double avg = 0.6f;
зображення після обробки з границею яскравості double avg = 0.6f

double avg = 0.7f;
зображення після обробки з границею яскравості double avg = 0.7f

double avg = 0.8f;
зображення після обробки з границею яскравості double avg = 0.8f

double avg = 0.85f;
зображення після обробки з границею яскравості double avg = 0.85f
double avg = 0.9f;
зображення після обробки з границею яскравості double avg = 0.9f

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

В інтернеті також ходить фрагмент коду, який використовуючи System.Windows.Forms.ToolStripRenderer.CreateDisabledImage також зменшує кількість кольорів на зображенні. Проте результат получається в градаціяї сірого, що може не дуже підходити для завдання розпізнавання символів на зображенні. Хоча в деяких випадках можливо варто перевести зображення спочатку в градації сірого, а уже потім використовуючи фрагмент коду наведений вище переводити тільки у два кольори – чорний і білий. Ось приклад який переведе зображення в градації сірого:

string path_input = Path.Combine(project.Directory, "a.jpg");
string path_output = Path.Combine(project.Directory, "b.jpg");

using(var bmp = new Bitmap(path_input)){
	using(var img = System.Windows.Forms.ToolStripRenderer.CreateDisabledImage(bmp)){
		img.Save(path_output);
	}
}

Нижче приведу також результат, який одержується з використанням цього фрагменту:

зображення після обробки ToolStripRenderer.CreateDisabledImage

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

Інколи метод GetBrightness видає значення, яке далеке від реальності. І тоді приходиться вираховувати яскравість пікселя перемноживши кожний канал кольору на константи. В такому випадку функція, яку я описав вище може мати наступний вигляд:

Func<Bitmap,int,Bitmap> BitmapBinary2 = (Bitmap input, int brightness) =>{
	var output = new Bitmap(input.Width,input.Height);  
	for(int y=0;y<input.Height;y++) { 
	    for(int x=0;x<input.Width;x++) {
			var pixelColor = input.GetPixel(x, y);
			int pixelBrightness = (int)(0.299 * pixelColor.R + 0.587 * pixelColor.G + 0.114 * pixelColor.B);
			bool check = pixelBrightness > brightness;
			var color = check ? Color.White : Color.Black;
			output.SetPixel(x,y, color);
	    } 
	}
	return output;
};

project.Context["bin2"] = BitmapBinary2;

І тепер вже границю яскравості потрібно задавати у вигляді числа від 0-255.

var BitmapBinary2 = project.Context["bin2"];
int avg = 195;
string path_input = Path.Combine(project.Directory, "a.jpg");
string path_output = Path.Combine(project.Directory, "b.jpg");

using(var bmp = new Bitmap(path_input)){
	using(var birary = BitmapBinary2(bmp, avg)){
		birary.Save(path_output);
	}
}

Методом підбору, в цьому випадку із значенням границі яркості 195 в мене получився такий результат:
зображення після обробки з границею яскравості 195

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

Повертаючись до завдання яке стояло на початку публікації, а саме розпізнати вручну текст із зображення – то це вирішує наступний код визиваючи першу функцію з цієї публікації використовуючи границю яскравості 0.85f:

var BitmapBinary = project.Context["bin"];
double avg = 0.85f;
string path_input = Path.Combine(project.Directory, "a.jpg");

string base64 = string.Empty;
using(var bmp = new Bitmap(path_input)){
  using(var birary = BitmapBinary(bmp, avg)){
    using (var ms = new MemoryStream()){
      birary.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
      base64 = Convert.ToBase64String(ms.GetBuffer());
    }
  }
}

System.Windows.Forms.Clipboard.SetText(ZennoPoster.CaptchaRecognition("MonkeyEnter.dll", base64, string.Empty));

Тут замість зберігання результату в файл я переводжу його в base64 і відправляю на розпізнавання у MonkeyEnter.dll. В цей момент ZennoPoster відображає мені вікно в якому буде зображення і поле для вводу. Після введення символів з зображення текст буде поміщений у буфер обміну.

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

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

В моменти, коли потрібно продовжити працювати з зображенням, проводити сегментацію, тобто помічати зв’язані області на зображенні, визначати потім їхню площу, периметр, область описаного прямокутника, центр мас буває зручно привести це зображення до набору нулів і одиничок, які будуть зберігатись в масиві масивів. З однієї сторони, пришвидшується доступ до кожного пікселя (тому що елемент з масиву зчитується швидше, ніж колір колір пікселя, точно так і з встановленням). Для цього я використовую приблизно таку функцію:

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;

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

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;

Таким чином, в цій публікації я зосередив інформацію, яка позволить перевести зображення в два кольори, а також результат перевести в масив нулів і одиничок.

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

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