Готовый перевод Mastering OpenCV with Practical Computer Vision Projects / Опановуюємо OpenCV через практичні проекти комп'ютерного зору: Розділ 1. Ефект мультиплікації та зміна кольору шкіри для Android
У цьому розділі ви дізнаєтесь, як запрограмувати деякі фільтри для обробки зображень на смартфонах і планшетах з ОС Android, використавши спочатку код для Desktop (С/C++), а потім перенісши його на Android (з тим же кодом С/С++, але графічним інтерфейсом користувача JAVA). Це рекомендований сценарій під час розробки програм для мобільних пристроїв. У цьому розділі ви дізнаєтесь:
Як конвертувати фотографію у схематичне зображення
Як змішати фотографію і схематичне зображення, отримавши ефект мультиплікації
Як перетворити добрих персонажів на злих, активувавши страхітливий режим "зла"
Як визначити шкіру та замінити її колір, щоб надати кому-небудь зелений "інопланетний" вигляд
Як перенести проєкт на мобільні пристрої
Наступний скріншот зображує результат запуску програми Cartoonifier на планшеті з ОС Android:
Нам потрібно, аби реальні фотографії не відрізнялися від мультиплікації. Основа ідеї полягає в тому, щоб заповнити однотонні частини зображення деяким кольором, а потім намалювати товсті лінії на їх межах. Іншими словами, однотонні фрагменти мають стати більш однотонними, а їх межі - більш акцентованими. Ми знайдемо межі та згладимо однотонні фрагменти, а потім підсилимо межі для того, щоб сформувати ефект мультиплікації чи коміксу.
При розробці програм комп'ютерного бачення для мобільних пристроїв спочатку варто компілювати повністю робочу версію для ПК, адже її легше запрограмувати та налагодити, аніж мобільний застосунок! Тому цей розділ почнемо з повної настільної версії програми Cartoonifier, яку можна створити у будь-якій з ваших улюблених середовищ для розробки (наприклад, Visual Studio, XCode, Eclipse, Qt Creator і подібні). Коли вона працюватиме як треба, перенесемо її на Android (можливо навіть iOS), використавши при цьому Eclipse. Оскільки ми плануємо створити два різні проєкти, що матимуть спільний код логіки, але різні графічні інтерфейси, ми могли б створити бібліотеку і прив'язати її до кожного, але для простоти викладу матеріалу розмістимо обидва проєкти поряд і налаштуємо доступ версії для Android до деяких файлів (cartoon.cpp та cartoon.h, у яких знаходиться весь код обробки зображень) з каталогу Desktop. Наприклад:
C:\Cartoonifier_Desktop\cartoon.cpp
C:\Cartoonifier_Desktop\cartoon.h
C:\Cartoonifier_Desktop\main_desktop.cpp
C:\Cartoonifier_Android\...
Прикладна програма для ПК використовує вікно OpenCV GUI, налаштовує камеру і для кожного кадру камери викликає функцію cartoonifyImage(), що включає більшість коду цього розділу. Потім оброблене зображення засобами GUI виводиться на поверхні вікна. Подібним чином, програма для Android використовує власне вікно GUI, налаштовує камеру методами Java, і для кожного кадру камери викликає ту ж функцію cartoonifyImage(), але з меню і системою вводу Android.
У цьому розділі пояснюється, як створити програму для ПК з нуля, а також для Android - на основі одного з прикладів OpenCV Android. Таким чином, спочатку ви маєте запрограмувати версію для настільних пристроїв в улюбленому IDE, файл main_desktop.cpp має зберігати код графічного інтерфейсу, поданий в наступних частинах, такі як головний цикл, функціональність вебкамери та ввід з клавіатури, також потрібно створити файл cartoon.cpp, який використають обидва проєкти. Більшість коду розділу знаходитиметься у функції cartoonifyImage() з файлу cartoon.cpp.
Отримуємо доступ до вебкамери
Щоб отримати доступ до комп'ютерної чи мобільної камери, досить викликати метод cv::VideoCapture::open() і передати "0" в якості параметру (індекс камери за замовчуванням). Деякі комп'ютери мають декілька приєднаних камер або не використовують камеру 0; тому слід дозволяти користувачу передавати бажаний індекс як аргумент командної стрічки, на випадок, якщо він забажає спробувати камеру 1, 2, чи, наприклад, -1. Ми також спробуємо встановити роздільну здатність у значення 640х480 за допомогою методу cv::VideoCapture::set(), щоб програма працювала швидше з камерами з високою роздільною здатністю.
Примітка
У залежності від моделі камери, драйверу чи системи, OpenCV може не змінювати властивості вашої камери. Для цього проєкту це неважливо, тож не хвилюйтесь, якщо з вашою камерою виявляться ті ж проблеми. Помістіть цей код у функцію main() файлу main_desktop.cpp:
int cameraNumber = 0;
if (argc > 1)
cameraNumber = atoi(argv[1]);
// Отримаємо доступ до камери.
cv::VideoCapture camera;
camera.open(cameraNumber);
if (!camera.isOpened()) {
std::cerr << "ПОМИЛКА: Не вдалося отримати доступ до камери чи відео!" <<
std::endl;
exit(1);
}
// Спробуємо встановити роздільну здатність камери.
camera.set(cv::CV_CAP_PROP_FRAME_WIDTH, 640);
camera.set(cv::CV_CAP_PROP_FRAME_HEIGHT, 480);
Після налаштування вебкамери можна отримати поточне зображення у вигляді об'єкта cv::Mat (контейнер OpenCV для роботи з кадрами). Використавши потоковий оператор C++, кожен кадр можна виокремити з об'єкта cv::VideoCapture і помістити в об'єкт cv::Mat, подібно до отримання ввідних даних з консолі.
Примітка
Бібліотека OpenCV дозволяє дуже легко завантажувати відеофайли (у форматах AVI чи MPG) та використовувати їх замість вебкамери. Єдина різниця у коді полягає в тому, щоб створити об'єкт cv::VideoCapture з назвою файлу, наприклад camera.open("my_video.avi"), а не індексом камери, як у випадку camera.open(0). Обидва методи формують об'єкт cv::VideoCapture, з яким можна взаємодіяти подібним чином.
Головний цикл взаємодії з камерою програми для ПК
Щоб зобразити вікно методами OpenCV, для кожного зображення необхідно викликати cv::imshow(), але у цьому випадку знадобиться також cv::waitKey() один раз на кадр, інакше вікна зовсім не оновляться! Якщо викликати функцію cv::waitKey(0), програма чекатиме, поки користувач не натисне клавішу, а позитивне значення, як у випадку cv::waitKey(20) чи вище, змусить потік почекати як мінімум цю кількість мілісекунд.
Помістіть цей головний цикл у файл main_desktop.cpp, це буде основа для вашої програми з камерою:
while (true) {
// Отримаємо наступний кадр камери
cv::Mat cameraFrame;
camera >> cameraFrame;
if (cameraFrame.empty()) {
std::cerr << "ПОМИЛКА: Не можна отримати кадр камери." <<
std::endl;
exit(1);
}
// Створюємо пусте вивідне зображення, на якому будемо малювати.
cv::Mat displayedFrame(cameraFrame.size(), cv::CV_8UC3);
// Оброблюємо кадр камери фільтром мультиплікації.
cartoonifyImage(cameraFrame, displayedFrame);
// Виводимо оброблене зображення на екрані.
imshow("Cartoonifier", displayedFrame);
// ВАЖЛИВО: Чекаємо як мінімум 20 мілісекунд,
// щоб зображення з'явилося на екрані!
// Також перевіряємо, чи не натиснута клавіша.
// Майте на увазі, якщо необхідна підтримка Linux, це має бути символ.
char keypress = cv::waitKey(20); // Це необхідно, щоб побачити зображення!
if (keypress == 27) { // Код клавіші Escape
// Завершити роботу з програмою!
break;
}
}//кінець циклу while
Генеруємо чорно-білий скетч
Щоб отримати скетч (чорно-біле зображення) на основі кадру з камери, використаємо фільтр розпізнавання меж; для формування кольорового малюнку використаємо двопрохідний фільтр, щоб мати можливість згладити однотонні фрагменти, не змінивши при цьому межі. Наклавши схематичне зображення на кольоровий малюнок, отримаємо ефект мультиплікації, показаний раніше на скріншоті фінальної версії програми.
Існує багато різних фільтрів розпізнавання меж, наприклад Собеля, Щарра, Лапласа або детектор меж Кенні. Зупинимось на фільтрі Лапласа, бо він знаходить межі, найбільше схожі на ручні скетчі в порівнянні з алгоритмами Собеля чи Щарра, та є більш точним за детектор меж Кенні, що формує дуже плавні малюнки з ліній, але при цьому страждає від випадкових шумів на кадрах, через що зображення змінюватиметься надто сильно між кадрами.
Хай там що, нам все ще необхідно зменшити шум на зображенні до фільтра Лапласа. Використаємо фільтр Медіани, адже він добре видаляє шум, залишаючи межі при цьому гострими; крім того, він не настільки повільний як двопрохідний. Оскільки фільтрам Лапласа потрібні зображення у відтінках сірого, необхідно конвертувати базовий формат OpenCV BGR у Grayscale. В пустому файлі cartoon.cpp помістіть цей код зверху, щоб отримати доступ до OpenCV та стандартних функцій С++ без повсюдного використання cv:: та std::.
// Підключаємо С++ інтерфейс бібліотеки OpenCV
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;
Помістіть цей і решту коду в функцію cartoonifyImage() в файлі cartoon.cpp:
Mat gray;
cvtColor(srcColor, gray, CV_BGR2GRAY);
const int MEDIAN_BLUR_FILTER_SIZE = 7;
medianBlur(gray, gray, MEDIAN_BLUR_FILTER_SIZE);
Mat edges;
const int LAPLACIAN_FILTER_SIZE = 5;
Laplacian(gray, edges, CV_8U, LAPLACIAN_FILTER_SIZE);
Фільтр Лапласа продукує межі різної яскравості, тому, щоб вони виглядали більш схожими на скетч, застосуємо функцію бінарного порогу, що робить межі або білими, або чорними:
Mat mask;
const int EDGES_THRESHOLD = 80;
threshold(edges, mask, EDGES_THRESHOLD, 255, THRESH_BINARY_INV);
Наступний малюнок презентує оригінальне зображення (зліва) і згенеровану маску меж (справа), що виглядає дуже схожою до скетчу. Коли ми сформуємо кольорову картину (дізнаємось як саме), цю маску розмістимо зверху, щоб отримати зображення з чорними лініями меж.
Генеруємо кольорову картину та ефект мультиплікації
Могутній двопрохідний фільтр згладжує однотонні фрагменти та залишає межі гострими, що неабияк підходить для створення автоматичного мультиплікаційного чи картинного фільтра, хоча він дуже повільний (час виконання вимірюється в секундах чи навіть хвилинах, а не в мілісекундах!). Тому ми використаємо деякі трюки, щоб отримати пристойний ефект мультиплікації, що виконуватиметься з прийнятною швидкістю. Найважливіший трюк - це те, що ми можемо виконати двопрохідну фільтрацію на зображеннях зниженої роздільної здатності. Ефект буде той же, що і для вихідного варіанту, але швидкість - значно вищою. Знизимо загальну кількість пікселів в чотири рази (зменшимо ширину і висоту вдвічі):
Size size = srcColor.size();
Size smallSize;
smallSize.width = size.width/2;
smallSize.height = size.height/2;
Mat smallImg = Mat(smallSize, CV_8UC3);
resize(srcColor, smallImg, smallSize, 0,0, INTER_LINEAR);
Замість повного двопрохідного фільтра використаємо багато неповних, щоб отримати гарний ефект мультиплікації за менший час. Обмежимо фільтр (ознайомтесь з наступним малюнком), щоб замість повного розміру (наприклад, 21х21, тобто крива дзвону займає 21 піксель ширини), він використовував лише мінімальний, необхідний для переконливого результату (наприклад, 9х9 для кривої дзвону шириною 21 піксель). Обмежений фільтр опрацює більшу його частину (сіру зону), не витрачаючи час на меншу (біла зона під кривою), тому виконуватиметься у декілька разів швидше.
Існує чотири параметри, що контролюють двопрохідну фільтрацію: сила кольору, сила позиції, розмір і кількість повторень. Нам знадобиться тимчасовий об'єкт Mat, адже bilateralFilter() не може змінювати вхідні дані (про що ще говорять як про "обробку на місці"), але ми можемо застосувати один фільтр і зберегти результат у тимчасовому Mat, а потім ще один, що повертає дані у вихідний об'єкт Mat.
Mat tmp = Mat(smallSize, CV_8UC3);
int repetitions = 7; // Кількість повторень для пристойного ефекту мультиплікації.
for (int i=0; i<repetitions; i++) {
int ksize = 9; // Розмір фільтру. Значно впливає на швидкість.
double sigmaColor = 9; // Сила кольору.
double sigmaSpace = 7; // Сила позиції. Впливає на швидкість.
bilateralFilter(smallImg, tmp, ksize, sigmaColor, sigmaSpace);
bilateralFilter(tmp, smallImg, ksize, sigmaColor, sigmaSpace);
}
Пам'ятайте, що цей алгоритм застосовано до зменшеного зображення, тому не забудьте розширити його до початкових розмірів. Після цього ми зможемо розмістити зверху знайдену раніше маску меж. Щоб накласти "скетч", тобто маску меж, на "картину", отриману за допомогою двопрохідного фільтра (ліва частина наступного малюнку), почнемо з чорного фону і скопіюємо пікселі "картини", що не є межами в масці "скетча":
Mat bigImg;
resize(smallImg, bigImg, size, 0,0, INTER_LINEAR);
dst.setTo(0);
bigImg.copyTo(dst, mask);
Результат - це оброблене зображення з ефектом мультиплікації, як показано на правій частині малюнка, на якому маска "скетча" накладається на "картину":
Активуємо режим "зла" з використанням фільтрів меж
Мультиплікація та комікси завжди мали добрих та злих персонажів. Правильно комбінуючи фільтри меж, можна згенерувати страхітливе зображення навіть найневиннішої особи у світі! Суть полягає у тому, щоб застосувати фільтр для малих меж, яких знайдеться чимало, а потім з'єднати їх невеликим Медіанним фільтром.
Цей процес виконаємо на однотонному зображенні зі зниженим рівнем шуму, тому попередній код для конвертації оригінального зображення у відтінки сірого та обробки останнього Медіанним фільтром 7х7 буде використаний знову (перше зображення на наступному малюнку зображає вивід результату однотонного Медіанного розмиття). Далі, замість фільтра Лапласа та бінарного порогу, отримаємо більш страшний вигляд, якщо застосуємо градієнтний фільтр Щарра розміром 3х3 вздовж осей x та y (друге зображення), а потім активуємо функцію бінарного порогу з дуже малим відрізком (третє зображення) та Медіанний фільтр 3х3, що сформує остаточну маску "зла" (четверте зображення).
Mat gray;
cvtColor(srcColor, gray, CV_BGR2GRAY);
const int MEDIAN_BLUR_FILTER_SIZE = 7;
medianBlur(gray, gray, MEDIAN_BLUR_FILTER_SIZE);
Mat edges, edges2;
Scharr(srcGray, edges, CV_8U, 1, 0);
Scharr(srcGray, edges2, CV_8U, 1, 0, -1);
edges += edges2; // Об'єднуємо межі на осі x з межами на осі y.
const int EVIL_EDGE_THRESHOLD = 12;
threshold(edges, mask, EVIL_EDGE_THRESHOLD, 255, THRESH_BINARY_INV);
medianBlur(mask, mask, 3);
Отримавши маску "зла", можемо розмістити її над обробленою ефектом мультиплікації "картиною", подібно до зробленого за допомогою звичайної маски "скетчу" раніше. Результат видно з правого боку наступного малюнку:
Генеруємо режим "прибульця" з використанням алгоритму розпізнавання шкіри
Дотепер ми створили режим скетчу, мультиплікації (картина + схематична маска) і жаху (картина + маска зла), а тепер спробуємо дещо складніше: режим "прибульця", визначивши фрагменти шкіри на обличчі та змінивши колір шкіри на зелений.
Алгоритм розпізнавання шкіри
Існує багато різних технік, що використовуються для визначення фрагментів шкіри, від простих порогів кольору для значень RGB (червоний, зелений, синій) чи HSV (відтінок, насиченість, яскравість), обчислення та репроєкції гістограми кольорів, до складних алгоритмів машинного навчання (як-от змішування моделей), що потребують калібрування камер в кольоровому просторі CIELab та тренування на багатьох прикладах облич, і так далі.
Але навіть складні методи не обов'язково однаково ефективні для різних камер, умов світла та типів шкіри. Оскільки алгоритм розпізнавання шкіри має виконуватися на мобільних пристроях без калібрування чи тренування, а ми використаємо його лише для "смішного" фільтру зображення, для нас достатньо застосувати простий метод розпізнавання шкіри. Однак, колір, отриманий від крихітних сенсорів камери мобільного пристрою, має тенденцію досить значно змінюватись, до того ж ми бажаємо підтримувати розпізнавання шкіри для людей з будь-яким його кольором без жодного калібрування, тому нам потрібно дещо краще, аніж прості пороги кольорів.
Наприклад, простий HSV детектор шкіри може позначати будь-який піксель як шкіру, якщо його відтінок червонуватий, насиченість досить висока, але не занадто, а яскравість ні надто низька, ні надто висока. Але мобільні камери часто мають проблеми з балансом білого, тому шкіра людини може мати злегка синюватий відтінок замість червоного тощо, і саме це є головною проблемою для простого HSV порогу.
Більш ефективне рішення полягає в тому, щоб виконати розпізнавання обличчя за допомогою каскадного класифікатора Хаара чи LBP (показано в розділі 8 "Розпізнавання облич за допомогою алгоритму Eigenface") а потім розглянути діапазон кольорів для пікселів посередині знайденого обличчя, адже саме вони мають являти собою шкіру людини. Після цього можна просканувати все зображення чи сусідні фрагменти та знайти пікселі тих же кольорів. У цьому є значний плюс, адже дуже вірогідно, що алгоритм знайде як мінімум частину необхідного фрагменту зображення будь-якої людини, неважливо якого кольору її шкіра, синювата вона чи червонувата.
На жаль, розпізнавання шкіри з використанням каскадних класифікаторів досить повільне на сучасних мобільних пристроях, тому цей метод може не підходити для мобільних застосунків реального часу. З іншого боку, можна скористатися тим фактом, що, працюючи з мобільними програмами, користувач триматиме камеру навпроти свого обличчя на близькій відстані, а оскільки він тримає камеру у власній руці, яку можна легко перемістити у просторі, можна попросити його розмістити своє обличчя у певній позиції та на визначеній відстані, що дозволить не шукати місцеположення та розмір відповідного фрагменту. Це основа багатьох програм для мобільних пристроїв, в яких користувач розміщує своє обличчя в певній позиції чи, можливо, вручну переміщує точки на екрані, щоб показати, де знаходяться його межі на фотографії. Тому просто намалюємо контур в центрі екрану і вкажемо користувачу, якою має бути позиція і розмір його обличчя.
Вказуємо користувачу, де розмістити його обличчя
Як тільки-но завантажиться режим прибульця, намалюємо контур обличчя поверх кадру камери, щоб користувач знав, де розмістити своє обличчя. Намалюємо великий еліпс, що займатиме 70 відсотків висоти зображення з фіксованим відношенням сторін 0.72, щоб обличчя не виявилось занадто вузьким чи широким, на що впливають відповідні характеристики камери:
// Малюємо кольорове обличчя на чорному фоні.
Mat faceOutline = Mat::zeros(size, CV_8UC3);
Scalar color = CV_RGB(255,255,0); // Жовтий.
int thickness = 4;
// Використаємо 70% висоти екрану як висоту обличчя.
int sw = size.width;
int sh = size.height;
int faceH = sh/2 * 70/100; // Радіус еліпса.
// Змінюємо ширину обличчя, щоб вона залишалась однаковою для будь-якої ширини екрана.
int faceW = faceH * 72/100;
// Малюємо контур обличчя.
ellipse(faceOutline, Point(sw/2, sh/2), Size(faceW, faceH),
0, 0, 360, color, thickness, CV_AA);
Щоб користувач зрозумів, що це контур обличчя, намалюємо також контури двох очей. Замість того, щоб малювати око як єдиний еліпс, зробимо його більш реальним (дивіться наступний малюнок), намалювавши обмежений еліпс для верхньої частини ока і ще один - для нижньої, адже, використовуючи функцію ellipse(), ми маємо змогу визначати початковий та кінцевий кути.
// Малюємо контури очей як дві дуги на кожне око.
int eyeW = faceW * 23/100;
int eyeH = faceH * 11/100;
int eyeX = faceW * 48/100;
int eyeY = faceH * 13/100;
Size eyeSize = Size(eyeW, eyeH);
// Встановлюємо кут та відстань напівеліпсів від центра ока.
int eyeA = 15; // кут в градусах.
int eyeYshift = 11;
// Малюємо верхню частину правого ока.
ellipse(faceOutline, Point(sw/2 - eyeX, sh/2 – eyeY),
eyeSize, 0, 180+eyeA, 360-eyeA, color, thickness, CV_AA);
// Малюємо нижню частину правого ока.
ellipse(faceOutline, Point(sw/2 - eyeX, sh/2 - eyeY – eyeYshift),
eyeSize, 0, 0+eyeA, 180-eyeA, color, thickness, CV_AA);
// Малюємо верхню частину лівого ока.
ellipse(faceOutline, Point(sw/2 + eyeX, sh/2 - eyeY),
eyeSize, 0, 180+eyeA, 360-eyeA, color, thickness, CV_AA);
// Малюємо нижню частину лівого ока.
ellipse(faceOutline, Point(sw/2 + eyeX, sh/2 - eyeY – eyeYshift),
eyeSize, 0, 0+eyeA, 180-eyeA, color, thickness, CV_AA);
Подібним чином малюємо нижню губу рота:
// Малюємо нижню губу рота.
int mouthY = faceH * 48/100;
int mouthW = faceW * 45/100;
int mouthH = faceH * 6/100;
ellipse(faceOutline, Point(sw/2, sh/2 + mouthY), Size(mouthW,
mouthH), 0, 0, 180, color, thickness, CV_AA);
Щоб користувач точно зрозумів, куди слід помістити обличчя, виведемо екран повідомлення!
// Напишемо текст з ефектом згладжування.
int fontFace = FONT_HERSHEY_COMPLEX;
float fontScale = 1.0f;
int fontThickness = 2;
char *szMsg = "Помістіть ваше обличчя тут";
putText(faceOutline, szMsg, Point(sw * 23/100, sh * 10/100),
fontFace, fontScale, color, fontThickness, CV_AA);
Маючи контур обличчя, ми можемо розмістити його над зображенням за допомогою альфа змішування та об'єднати мультиплікацію з намальованим контуром:
addWeighted(dst, 1.0, faceOutline, 0.7, 0, dst, CV_8UC3);
Результати цього процесу можна побачити на наступному малюнку, на якому зображається, де користувач має розмістити своє обличчя, щоб нам не довелося розпізнавати його місцеположення:
Реалізація зміни кольору шкіри
Замість того, щоб розпізнавати колір шкіри та фрагменти з ним, можна використати функцію OpenCV floodFill(), яка діє аналогічно до інструменту заливки кольором у багатьох програмах редагування зображень. Ми знаємо, що фрагменти в центрі екрану повинні мати пікселі шкіри (адже ми просимо користувача розмістити своє обличчя посередині), тому, щоб змінити колір всього обличчя на зелений, ми можемо просто застосувати заливку зеленим кольором на центральному пікселі, що в будь-якому випадку змінить колір як мінімум частини обличчя. Насправді колір, насиченість та яскравість вірогідно відрізнятимуться для різних фрагментів обличчя, тому заливка навряд чи покриє всі пікселі шкіри, якщо поріг не настільки низький, що воно покриє навіть небажані пікселі за межами обличчя. Тому, замість виконання єдиної заливки в центрі зображення, застосуємо її в 6 різних точках, які мають бути пікселями шкіри.
Цікавою особливістю функції floodFill() бібліотеки OpenCV є те, що вона може здійснювати заливку у вихідне зображення, не змінюючи при цьому вхідне. Завдяки цьому ми зможемо отримати маску для керування кольором пікселів без обов'язкової зміни яскравості чи насиченості, формуючи більш реалістичне зображення, аніж у випадку однакового зеленого кольору для всіх пікселів шкіри (через що важливі деталі обличчя буде втрачено).
Зміна кольору шкіри не так ефективна у просторі кольорів RGB. Нам важливо, аби яскравість мала змогу змінюватися залежно від позиції на обличчі, а відтінок залишався тим же. Простір RGB не відокремлює
яскравість від кольору. По-перше, можна використати простір HSV (відтінок, насиченість, яскравість), адже він розмежовує ці характеристики. На жаль, простір HSV надбудовує значення відтінку над червоною компонентою, а оскільки шкіра має здебільшого рожевий колір, нам доведеться працювати з відтінками менше ніж 10 і більше ніж 90 відсотків, адже і ті, й інші червоні. Тому замість цього ми використаємо простір Y'CrCb (варіант YUV, що підтримується OpenCV), тому що він виокремлює яскравість та має єдиний діапазон значень для типового кольору шкіри, а не два. Майте на увазі, що більшість камер, зображень та відео на ділі використовують деякі види просторів YUV до того, як будуть конвертовані в RGB, тому у багатьох випадках ви отримаєте зображення YUV без необхідності конвертувати його власноруч.
Оскільки ми бажаємо, щоб режим прибульця був схожим на мультиплікацію, ми застосуємо фільтр вже після того, як матимемо зображення з даним ефектом; іншими словами, матимемо доступ до зменшеного кольорового зображення, створеного двопрохідним фільтром та повнорозмірною маскою меж. Алгоритм розпізнавання шкіри часто працює краще у випадках низької роздільної здатності (або низькочастотних сигналів замість високочастотних з шумом). Тому роботу здійснюватимемо над тим же зменшеним зображенням, як і двопрохідний фільтр (половина ширини і половина висоти). Конвертуємо картину в простір кольорів YUV:
Mat yuv = Mat(smallSize, CV_8UC3);
cvtColor(smallImg, yuv, CV_BGR2YCrCb);
Ми також маємо зменшити маску меж, щоб вона мала ті ж розміри, що й зображення картини. Є певна складність у роботі з функцією floodFill() бібліотеки OpenCV, а саме, коли відбувається запис маски в окреме зображення, воно повинно також мати границю шириною 1 піксель, тому якщо на вхід подано кадр місткістю WxH пікселів, маска має містити (W+2)x(H+2) пікселів. Але функція floodFill() дозволяє також налаштувати маску межами, які її алгоритм ні в якому разі не перетне. Скористаємось цією особливістю в надії, що заливка не вийде за контури обличчя.
Таким чином, нам необхідно сформувати два зображення: маску меж з розмірами WxH і таку ж, але розмірами (W+2)x(H+2), бо вона включатиме границі навколо неї. При роботі з бібліотекою можливо мати декілька об'єктів cv::Mat (чи заголовків) що відносяться до тих же даних, чи навіть один об'єкт cv::Mat, що позначає частину основного cv::Mat. Тому замість виділення двох окремих кадрів чи копіювання пікселів меж, виділимо єдине зображення маски включно з границею і створимо додатковий об'єкт cv::Mat розмірами WxH (який лише вказує на зону інтересу в масці заливки без границі). Іншими словами, існує єдиний масив пікселів розмірами (W+2)x(H+2), але два об'єкти cv::Mat, один з яких відповідає цілісному зображенню (W+2)x(H+2), а інше - фрагменту WxH в його центрі:
int sw = smallSize.width;
int sh = smallSize.height;
Mat mask, maskPlusBorder;
maskPlusBorder = Mat::zeros(sh+2, sw+2, CV_8UC1);
mask = maskPlusBorder(Rect(1,1,sw,sh)); // Маска в maskPlusBorder.
resize(edge, mask, smallSize); // Розміщуємо межі в обох зображеннях.
Маска меж (показана з лівого боку наступного малюнку) повна гострих і слабких меж; але нам потрібні лише гострі, тому застосуємо бінарний поріг (результатом якого є середнє зображення). Щоб з'єднати прогалини між межами, ми скомбінуємо морфологічні оператори dilate() та erode(), інакше кажучи, виконаємо оператор "замикання", що сформує праву частину малюнку:
const int EDGES_THRESHOLD = 80;
threshold(mask, mask, EDGES_THRESHOLD, 255, THRESH_BINARY);
dilate(mask, mask, Mat());
erode(mask, mask, Mat());
Як зазначено раніше, нам необхідно виконати заливку в багатьох точках обличчя, щоб упевнитися, що включені різні можливі кольори та відтінки. Виберемо 6 точок на носі, щоках і лобі, як показано з лівого боку наступного малюнку. Зауважте, ці значення залежать від раніше намальованого контуру обличчя:
int const NUM_SKIN_POINTS = 6;
Point skinPts[NUM_SKIN_POINTS];
skinPts[0] = Point(sw/2, sh/2 - sh/6);
skinPts[1] = Point(sw/2 - sw/11, sh/2 - sh/6);
skinPts[2] = Point(sw/2 + sw/11, sh/2 - sh/6);
skinPts[3] = Point(sw/2, sh/2 + sh/16);
skinPts[4] = Point(sw/2 - sw/9, sh/2 + sh/16);
skinPts[5] = Point(sw/2 + sw/9, sh/2 + sh/16);
Тепер треба знайти слушні нижні та верхні границі для заливки. Майте на увазі, цей процес виконується в просторі кольорів Y'CrCb, тому по суті ми вирішуємо, наскільки може змінюватися яскравість, червоний та синій компоненти. Яскравість має змінюватися досить сильно, щоб включати тіні, освітлення та відблиск, але кольори повинні залишатися приблизно тими ж:
const int LOWER_Y = 60;
const int UPPER_Y = 80;
const int LOWER_Cr = 25;
const int UPPER_Cr = 15;
const int LOWER_Cb = 20;
const int UPPER_Cb = 15;
Scalar lowerDiff = Scalar(LOWER_Y, LOWER_Cr, LOWER_Cb);
Scalar upperDiff = Scalar(UPPER_Y, UPPER_Cr, UPPER_Cb);
Ми використаємо floodFill() зі значеннями по замовчуванню, за винятком того, що ми хочемо зберегти результат у зовнішній масці, тому використаємо FLOODFILL_MASK_ONLY:
const int CONNECTED_COMPONENTS = 4; // Щоб заповнити по діагоналі, використаємо 8.
const int flags = CONNECTED_COMPONENTS | FLOODFILL_FIXED_RANGE | FLOODFILL_MASK_ONLY;
Mat edgeMask = mask.clone(); // Збережемо копію маски меж.
// В maskPlusBorder додаємо межі, за які floodFill() не зможе вийти.
for (int i=0; i< NUM_SKIN_POINTS; i++) {
floodFill(yuv, maskPlusBorder, skinPts[i], Scalar(), NULL, lowerDiff, upperDiff, flags);
}
Ліве зображення наступного малюнку зображає 6 позицій для заливки (показані у вигляді синіх кіл), на правому можна бачити згенеровану маску, у якій шкіра має сірий колір, а межі - білий. Майте на увазі, що зображення з правого боку модифіковане для цієї книги таким чином, щоб пікселі шкіри (зі значенням 1) можна було роздивитися.
Зображення маски (показане з правого боку попереднього малюнку) відтепер має пікселі з такими значеннями:
255 для меж
1 для шкіри
0 для інших фрагментів
Тим часом edgeMask зберігає лише пікселі меж (255). Тому, щоб отримати тільки пікселі шкіри, можна відняти їх від усієї маски:
mask -= edgeMask;
Тепер зображення маски зберігає лише одиниці для пікселів шкіри та нулі для інших фрагментів. Щоб змінити колір шкіри та яскравість оригінального кадра, можна використати функцію cv::add() для маски шкіри, підсиливши таким чином зелений компонент на BGR зображенні:
int Red = 0;
int Green = 70;
int Blue = 0;
add(smallImgBGR, CV_RGB(Red, Green, Blue), smallImgBGR, mask);
Далі зображені початковий кадр з лівого боку та прибулець з ефектом мультиплікації з правого, на якому мінімум 6 частин обличчя відтепер зелені! Пам'ятайте, ми зробили шкіру не тільки зеленою, але й більш яскравою (аби бути схожими на прибульця, що світиться у темряві). Якщо потрібно змінити лише колір, без жодних втручань у яскравість, використайте інші методи, наприклад додайте 70 до зеленої компоненти й відніміть те ж значення від червоної та синьої, або конвертуйте зображення в HSV за допомогою cvtColor(src, dst, "CV_BGR2HSV_FULL") та налаштуйте відтінок і насиченість.
От і все! Перевірте програму в різних режимах, поки не будете готові перенести її на смартфон.
Переносимо настільну версію програми на Android
Застосунок працює як треба на ПК, тому приступимо до створення версії для Android чи iOS. Подані нижче деталі специфічні для Android, але їх можна використати на iOS, для Apple iPhone, iPad та інших подібних пристроїв. При розробці для Android користуватися OpenCV можна напряму з Java, але результат навряд чи буде настільки ж ефективним, як нативний код C/C++, ще й не дозволить запускати той же код на мобільному. Тому рекомендовано користуватися C/C++ для розробки більшості програм OpenCV для Android (читачі, які бажають користуватися виключно Java, можуть звернути увагу на JavaCV (автор Samuel Audet), доступну за адресою http://code.google.com/p/javacv/, це дасть змогу запускати на ПК той же код, що й на Android).
Примітка
Android проєкт використовує камеру для живого вводу, тому не спрацює в емуляторі. Він потребує мобільного пристрою з камерою, на основі ОС Android 2.2 (Froyo) чи більш пізньої.
Користувацький інтерфейс має бути запрограмований на Java, але для обробки зображення використаємо той же файл cartoon.cpp, що й для ПК. Щоб мати можливість запускати код С/C++ на Android, необхідна NDK (нативний набір для розробки), основою якого є JNI (нативний інтерфейс Java). Ми створимо JNI обгортку для функції cartoonifyImage(), щоб її потім використати на Android.
Налаштування Android-проєкту, що використовує OpenCV
Портування OpenCV на Android кожного року суттєво змінюється, як і метод для доступу до камери, тому книга - не найкращий спосіб дізнатися, як її правильно налаштувати. Читачеві краще знайти інструкції на сторінці http://opencv.org/platforms/android.html, щоб відрегулювати та збудувати нативний (з використанням NDK) Android-застосунок з OpenCV. В бібліотеці є приклад проєкту з назвою Sample3Native, що отримує доступ до камери та виводить модифіковане зображення. Цей приклад корисний як основа застосунку, що буде розроблений далі, тому читачам варто з ним ознайомитися (зараз доступний за адресою http://docs.opencv.org/doc/tutorials/introduction/android_binary_package/android_binary_package_using_with_NDK.html). Потім ми модифікуємо основу так, щоб він зміг оброблювати кадри ефектом мультиплікації та виводити результат.
Якщо розробка OpenCV програм для Android здається вам складною, наприклад ви отримуєте помилки компіляції чи камера видає чорні кадри, спробуйте знайти рішення на цих сайтах:
1. Уроки роботи з бінарним пакетом Android NDK, що був зазначений раніше.
2. Офіційна група Google, присвячена Android OpenCV (https://groups.google.com/forum/?
fromgroups#!forum/android-opencv).
3. Питання та відповіді на OpenCV (http://answers.opencv.org).
4. Питання та відповіді на StackOverflow (http://stackoverflow.com/questions/tagged/opencv+android).
5. Мережа (наприклад, http://www.google.com).
6. Якщо ви все ще не можете виправити проблему, спробувавши вище зазначені кроки, можна запостити питання з деталями помилки в групі Google, і так далі.
Формати кольору для обробки зображень на Android
Під час розробки для ПК ми мали справу з форматом пікселя BGR, адже вхідні дані з камери, зображення чи відеофайлу мали саме цей формат, вихідні, до речі, теж. Але при розробці для мобільних пристроїв доведеться конвертувати нативні формати самостійно.
Формат кольору для вводу з камери
У коді прикладу jni\jni_part.cpp змінна myuv позначає кольорове зображення у форматі камери Android за замовчуванням: "NV21" YUV420sp. Перша частина - це масив пікселів у відтінках сірого, за яким слідує вдвічі менший, розділений на канали U та V. Тому, якщо потрібно лише отримати доступ до зображення у відтінках сірого, його можна отримати з першої частини напівплоского кадру YUV420sp без жодних конверсій. Але якщо є потреба в кольоровому зображенні (наприклад, BGR чи BGRA), знадобиться конвертувати формат через cvtColor().
Формат кольору для виводу на екран
У коді Sample3Native змінна mbgra позначає кольорове зображення, що виводитиметься на пристрої Android, у форматі BGRA. Формат OpenCV за замовчуванням - це BGR (зворотний порядок байтів, аніж при RGB), а BGRA всього лише додає невикористаний байт на кінці пікселя, тобто кожен з них зберігається у вигляді "синій, зелений, червоний, невикористаний". Можна або виконати обробку в форматі BGR, а потім конвертувати BGR в BGRA до того, як вивести його на екран, або відрегулювати код обробки так, щоб він міг підтримувати формат BGRA замість або в доповнення до BGR. Досить часто це легко зробити в OpenCV, багато з його функцій здатні оброблювати BGRA зображення, але треба впевнитися, що ви створюєте зображення з тією ж кількістю каналів, що й вхідне, визначивши за допомогою функції Mat::channels() його значення. Крім цього, якщо у коді ви працюєте з пікселями напряму, доведеться відокремити код обробки 3-канального BGR та 4-канальних зображень BGRA.
Примітка
Деякі операції OpenCV працюють швидше з пікселями BGRA (бо вони вирівнені по 32 біти), а інші краще пристосовані до BGR (адже він потребує менше пам'яті для зчитування та запису), тому для максимальної ефективності краще підтримати обидва формати й протестувати, який з них підійде застосунку. Почнемо з простого: отримаємо доступ до кадру камери OpenCV, але не будемо його оброблювати, а просто виведемо на екран. Це легко зробити за допомогою коду Java, але важливо знати, як це зробити, користуючись лише OpenCV.
Як зазначено раніше, кадр камери прибуває до коду у форматі YUV420sp, а відбувати має як BGRA. Тому, якщо ми підготуємо об'єкти cv::Mat для вводу та виводу, нам доведеться лише конвертувати YUV420sp в BGRA з використанням cvtColor(). Щоб мати можливість користуватися кодом C/C++ в застосунку Java для Android, потрібні особливі імена функцій JNI, що збігаються з іменами класу та пакету Java, що використає функцію JNI, у такому форматі:
JNIEXPORT <Return> JNICALL Java_<Package>_<Class>_<Function>(JNIEnv* env, jobject, <Args>)
Почнемо з функції C/C++ ShowPreview(), яку потім використаємо з класу Java CartoonifierView в пакеті Cartoonifier. Додайте її в jni\jni_part.cpp:
// Показати звичайний кадр камери без модифікацій.
JNIEXPORT void JNICALL
Java_com_Cartoonifier_CartoonifierView_ShowPreview(
JNIEnv* env, jobject,
jint width, jint height, jbyteArray yuv, jintArray bgra)
{
jbyte* _yuv = env->GetByteArrayElements(yuv, 0);
jint* _bgra = env->GetIntArrayElements(bgra, 0);
Mat myuv = Mat(height + height/2, width, CV_8UC1, (uchar *)_yuv);
Mat mbgra = Mat(height, width, CV_8UC4, (uchar *)_bgra);
// Конвертуємо формат камери NV21 "YUV420sp"
// в кольорове зображення Android BGRA.
cvtColor(myuv, mbgra, CV_YUV420sp2BGRA);
// OpenCV відтепер може отримати доступ до / змінювати зображення BGRA "mbgra" ...
env->ReleaseIntArrayElements(bgra, _bgra, 0);
env->ReleaseByteArrayElements(yuv, _yuv, 0);
}
Хоча код виглядає досить складним, на перший погляд, перші дві стрічки функції лише дають нативний доступ до даних масивів Java, наступні дві будують об'єкти cv::Mat над заданими буферами пікселів (таким чином, вони не розміщують нові зображення, а дозволяють myuv отримати доступ до пікселів в масиві _yuv), а останні дві звільняють нативну прив'язку, яку ми встановили для масивів Java. Єдиною реальною роботою є конвертація формату YUV у BGRA, тому ця функція є лише основою, яку можна використати для створення нових. Тепер розширимо код аналізом та модифікацією BGRA об'єкта cv::Mat до виводу на екран.
Примітка
Приклад jni\jni_part.cpp в OpenCV v2.4.2 використовує наступний код:
cvtColor(myuv, mbgra, CV_YUV420sp2BGR, 4);
Це схоже на конвертацію в 3-канальний формат BGR (за замовчуванням), але оскільки використано параметр "4", конвертація відбувається в 4-канальний BGRA (формат виводу Android)! Виклик ідентичний до нижче вказаного коду, більш зрозумілого:
cvtColor(myuv, mbgra, CV_YUV420sp2BGRA);
Оскільки маємо вхідне та вихідне зображення у форматі BGRA замість основного BGR бібліотеки OpenCV, залишається два варіанти обробки:
Конвертувати зображення з формату BGRA в BGR до етапу обробки, виконати алгоритм над форматом BGR, а потім повернути результат в BGRA, яким можна скористатися в Android.
Модифікувати код таким чином, щоб мати змогу оброблювати формат BGRA в доповнення (або замість) BGR і відмовитись від необхідності здійснювати повільні конвертації між ними.
Щоб спростити викладання матеріалу, застосуємо перетворення BGRA в BGR і навпаки замість підтримки обох форматів одночасно. Створюючи програму реального часу, варто додати можливість працювати з 4-канальним BGRA задля потенційного підвищення швидкості виконання. Зробимо ще одну корисну річ, яка прискорить справи: конвертуємо вхід з YUV420sp в BGRA, а потім з BGRA в BGR, що дасть змогу конвертувати YUV420sp в BGR!
Спершу краще компілювати й перевірити показану раніше функцію ShowPreview() на ПК, щоб мати можливість повернутися у випадку більш пізніх проблем з кодом C/C++. Щоб викликати її з Java, додамо нативне оголошення поряд з Java-версією CartoonifyImage() в нижній частині CartoonifyView.java:
public native void ShowPreview(int width, int height, byte[] yuv, int[] rgba);
Потім зможемо викликати її подібно до прикладу FindFeatures() бібліотеки. Помістіть цей фрагмент всередині функції processFrame() файлу CartoonifierView.java:
ShowPreview(getFrameWidth(), getFrameHeight(), data, rgba);
Збудуйте рішення та запустіть програму на своєму мобільному пристрої, щоб просто побачити кадри з камери в реальному часі.
Використовуємо код ефекту мультиплікації в застосунку Android NDK
Маємо під'єднати модуль cartoon.cpp, використаний раніше для ПК. Файл jni\Android.mk налаштовує вихідні файли C/C++/Assembly, шляхи до заголовків, нативних бібліотек, та властивості компілятора GCC для проєкту:
1. Додайте cartoon.cpp (та ImageUtils_0.7.cpp, якщо потрібні інструменти для налагодження) в LOCAL_SRC_FILES, але врахуйте, що вони знаходяться в каталозі desktop замість jni за замовчуванням. Тому включіть це після LOCAL_SRC_FILES := jni_part.cpp:
LOCAL_SRC_FILES += ../../Cartoonifier_Desktop/cartoon.cpp
LOCAL_SRC_FILES += ../../Cartoonifier_Desktop/ImageUtils_0.7.cpp
2. Додайте шлях для пошуку заголовків, аби NDK зміг знайти cartoon.h:
LOCAL_C_INCLUDES += $(LOCAL_PATH)/../../Cartoonifier_Desktop
3. У верхній частині файлу jni\jni_part.cpp замініть #include <vector> на:
#include "cartoon.h" // ефект мультиплікації
#include "ImageUtils.h" // опціональні функції для налагодження OpenCV
4. Додайте у файл JNI-функцію CartoonifyImage(), код обробки ефектом мультиплікації. Почнемо з дублювання створеної раніше функції ShowPreview(), яка лише виводить на екран кадри з камери без модифікацій. Примітьте, що конвертація відбувається з формату YUV420sp в BGR, адже ми не бажаємо працювати із зображеннями BGRA:
Модифікуємо зображення камери фільтром мультиплікації.
JNIEXPORT void JNICALL
Java_com_Cartoonifier_CartoonifierView_CartoonifyImage(
JNIEnv* env, jobject,
jint width, jint height, jbyteArray yuv, jintArray bgra)
{
// Отримуємо нативний доступ до заданих масивів Java.
jbyte* _yuv = env->GetByteArrayElements(yuv, 0);
jint* _bgra = env->GetIntArrayElements(bgra, 0);
// Формуємо надбудови OpenCV над вхідними та вихідними даними.
Mat myuv(height + height/2, width, CV_8UC1, (uchar *)_yuv);
Mat mbgra(height, width, CV_8UC4, (uchar *)_bgra);
// Конвертуємо формат кольору з напівплоского формату YUV420sp камери
// у формат OpenCV's за замовчуванням.
Mat mbgr(height, width, CV_8UC3); // Виділяємо буфер для нового зображення.
cvtColor(myuv, mbgr, CV_YUV420sp2BGR);
// OpenCV може змінювати BGR-зображення "mbgr", і повинен
// зберігати результат як BGR-зображення "displayedFrame".
Mat displayedFrame(mbgr.size(), CV_8UC3);
// ТИМЧАСОВО: Показати лише кадри з камери без модифікацій
displayedFrame = mbgr;
// Конвертувати вивід з формату BGR бібліотеки OpenCV у формат
// BGRA ОС Android.
cvtColor(displayedFrame, mbgra, CV_BGR2BGRA);
// Звільняємо нативну прив'язку масивів Java.
env->ReleaseIntArrayElements(bgra, _bgra, 0);
env->ReleaseByteArrayElements(yuv, _yuv, 0);
}
5. Попередній код не змінює зображення, однак ми бажаємо застосувати фільтр мультиплікації, розроблений раніше у цьому розділі. Тому доєднаємо виклик наявної функції cartoonifyImage() з файлу cartoon.cpp застосунку для ПК. Замініть тимчасову стрічку коду displayedFrame = mbgr на:
cartoonifyImage(mbgr, displayedFrame);
6. Це все! Збудуйте рішення (Eclipse має скомпілювати код C/C++, використовуючи ndk-build) та запустіть його на мобільному пристрої. Ви маєте отримати робочу версію програми Cartoonifier для Android (на початку розділу є приклад, як має виглядати результат)! Якщо виявляться проблеми з компіляцією чи запуском, поверніться на попередні кроки та виправте помилки (можете ознайомитись з кодом, що йде разом з книжкою). Перевірте наступні кроки, коли впораєтесь з цим.
Огляд програми для Android
Робота програми на мобільному пристрої швидко виявить чотири проблеми:
Вона дуже повільна; потребує багато секунд на кожен кадр! Тому ми маємо лише вивести зображення з камери без змін, а його обробка має відбутися винятково при дотику користувача до екрана.
Вона має працювати з вводом даних, такими як зміна режимів між скетчем, картиною, виглядом страховидла чи прибульця. Додамо їх в меню Android.
Було б чудово, якби існувала можливість зберігати оброблене зображення та ділитися ним з друзями. Коли користувач дотикнеться до екрана, щоб отримати фотографію з ефектом мультиплікації, запишемо результат як файл на SD-карту та виведемо її в галереї Android.
Детектор меж скетчу формує багато випадкового шуму. Щоб справитися з проблемою, створимо особливий фільтр зменшення "перцевого" його виду.
Надаємо зображенню ефект мультиплікації у відповідь на дотик до екрана
Щоб показати зображення з камери (поки користувач не забажає застосувати ефект мультиплікації до обраного кадру), можна просто викликати створену раніше JNI-функцію ShowPreview(). До застосування фільтру також знадобиться почекати на події дотику. Нам потрібно обробити зображення, коли користувач дотикнеться до екрана, тому зробимо мітку, що вкаже на потребу застосувати фільтр до наступного кадру, і лише коли її буде звільнено, вивід на екран продовжиться. Це значитиме, що оброблене зображення виведеться лише на долі секунди, а потім зміниться на наступний кадр камери. Тому знадобиться інша мітка, завдяки якій зображення не зникатиме декілька секунд до того, як наступні змінять його; зрештою, це дасть змогу користувачу роздивитися подібні кадри.
1. Додаємо наступний заголовок у верхній частині файлу CartoonifierApp.java з
теки src\com\Cartoonifier:
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.MotionEvent;
2. Модифікуємо визначення класу у верхній частині файлу CartoonifierApp.java:
public class CartoonifierApp
extends Activity implements OnTouchListener {
3. Вміщуємо наступний код в нижню частину функції onCreate():
// Викликаємо функцію зворотного виклику onTouch() у
// відповідь на дотик користувача до екрана.
mView.setOnTouchListener(this);
4. Додаємо функцію onTouch() для обробки подій дотику:
public boolean onTouch(View v, MotionEvent m) {
// Ігноруємо подію переміщення пальця, нам лише потрібно дізнатись,
// коли палець вперше дотикається до екрана.
if (m.getAction() != MotionEvent.ACTION_DOWN) {
return false; // Не використовуємо цю подію дотику.
}
Log.i(TAG, "onTouch down event");
// Сигналізуємо, що наступний кадр має бути обробленим
// та збереженим у файл замість звичного виводу.
mView.nextFrameShouldBeSaved(getBaseContext());
return true;
}
5. Тепер визначимо функцію nextFrameShouldBeSaved() в файлі CartoonifierView.java:
// Оброблюємо наступний кадр камери та зберігаємо його замість виводу на екран
protected void nextFrameShouldBeSaved(Context context) {
bSaveThisFrame = true;
}
6. Запишемо наступні змінні у верхній частині класу CartoonifierView:
private boolean bSaveThisFrame = false;
private boolean bFreezeOutput = false;
private static final int FREEZE_OUTPUT_MSECS = 3000;
7. Функція processFrame() класу CartoonifierView тепер здатна перемикати режим між звичайними та обробленими зображеннями, але потрібно впевнитися, що немодифіковані кадри виводитимуться лише якщо наразі не показана версія з ефектом мультиплікації. Тому змінимо функцію processFrame() так:
@Override
protected Bitmap processFrame(byte[] data) {
// Збережемо вихідне зображення у внутрішній змінній класу rgba.
int[] rgba = mRGBA;
// Проводитимемо роботу з камерою та оновлюватимемо екран тільки
// якщо не потрібно лише вивести результат фільтру.
if (bFreezeOutput) {
// Змінна має бути визначена лише один раз.
bFreezeOutput = false;
// Почекати декілька секунд!
try {
wait(FREEZE_OUTPUT_MSECS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
if (!bSaveThisFrame) {
ShowPreview(getFrameWidth(), getFrameHeight(), data,
rgba);
}
else {
// Зробити лише раз, а тоді повернутися в режим перегляду.
bSaveThisFrame = false;
// Не оновлювати екран деякий час, щоб користувач міг
// побачити результат фільтру мультиплікації.
bFreezeOutput = true;
CartoonifyImage(getFrameWidth(), getFrameHeight(), data,
rgba, m_sketchMode, m_alienMode, m_evilMode,
m_debugMode);
}
// Помістимо оброблене зображення в об'єкт Bitmap,
// який повернемо для виводу на екран.
Bitmap bmp = mBitmap;
bmp.setPixels(rgba, 0, getFrameWidth(), 0, 0, getFrameWidth(),
getFrameHeight());
return bmp;
}
8. Тепер програма має скомпілюватися та запуститися. Перевіримо, чи добре вона працює.
Зберігаємо зображення у файл та виводимо в галереї Android
Ми збережемо вихідний результат у вигляді файлу PNG та покажемо його в галереї зображень Android. Остання розроблена для використання з файлами JPEG, але даний формат не підходить для мультиплікації з чіткими кольорами та межами, тому ми застосуємо цікавий спосіб додати зображення PNG до галереї. Створимо Java функцію savePNGImageToGallery(), що допоможе виконати це завдання. В нижній частині функції processFrame(), показаної раніше, бачимо створення об'єкту Android Bitmap із вхідних даних; тому нам потрібен спосіб зберегти його у вигляді файлу PNG. Функція imwrite() OpenCV Java API може бути використана з цією метою, але це вимагатиме прив'язки обох версій API (Java і C/C++, як робить це приклад "tutorial-4-mixed" бібліотеки OpenCV4Android). Оскільки OpenCV Java API не потрібен нам для будь-чого іншого, наступний код покаже лише як зберегти файли PNG, використовуючи Android API замість OpenCV Java API:
1. Клас Bitmap для Android здатний зберігати файли у форматі PNG, тому використаємо його. Також підберемо ім'я файлу. Використаємо поточні дату і час, що дозволить зберегти багато зображень та вказати користувачу, коли саме вони створені. Вмістіть це точно перед стрічкою "return bmp" функції processFrame():
if (bFreezeOutput) {
// Отримаємо поточні дату та час
SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd, HH-mm-ss");
String timestamp = s.format(new Date());
String baseFilename = "Cartoon" + timestamp + ".png";
// Збережемо оброблене зображення у вигляді файлу PNG на SD-карті
// та покажемо її в галереї Android.
savePNGImageToGallery(bmp, mContext, baseFilename);
}
2. Додамо цей код до верхньої частини CartoonifierView.java:
// Необхідно для збереження Bitmap у файлі та галереї Android.
import android.graphics.Bitmap.CompressFormat;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.text.format.DateFormat;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
3. Помістіть цей код в класі CartoonifierView, у верхній частині:
private static final String TAG = "CartoonifierView";
private Context mContext; // Щоб отримати доступ до галереї Android.
4. Додайте це у функцію nextFrameShouldBeSaved() класу CartoonifierView:
mContext = context; // Зберегти контекст Android для доступу до графічного інтерфейсу.
5. Реалізуйте функцію savePNGImageToGallery() в класі CartoonifierView:
// Збережемо оброблене зображення у вигляді файлу PNG на SD-карті
// та покажемо його в галереї Android.
protected void savePNGImageToGallery(Bitmap bmp, Context
context,
String baseFilename)
{
try {
// Отримаємо шлях до файлу на SD-карті.
String baseFolder = \
Environment.getExternalStoragePublicDirectory( \
Environment.DIRECTORY_PICTURES).getAbsolutePath() \
+ "/";
File file = new File(baseFolder + baseFilename);
Log.i(TAG, "Зберігаємо оброблене зображення у файл [" + \
file.getAbsolutePath() + "]");
// Відкриваємо файл.
OutputStream out = new BufferedOutputStream(
new FileOutputStream(file));
// Зберігаємо файл зображення у форматі PNG.
bmp.compress(CompressFormat.PNG, 100, out);
// Переконаємось, що він збережений,
// бо ми збираємось додати його до галереї.
out.flush();
out.close();
// Додаємо файл PNG до галереї Android.
ContentValues image = new ContentValues();
image.put(Images.Media.TITLE, baseFilename);
image.put(Images.Media.DISPLAY_NAME, baseFilename);
image.put(Images.Media.DESCRIPTION,
"Оброблений програмою Cartoonifier");
image.put(Images.Media.DATE_TAKEN,
System.currentTimeMillis()); // мілісекунд з 1970 року, UTC.
image.put(Images.Media.MIME_TYPE, "image/png");
image.put(Images.Media.ORIENTATION, 0);
image.put(Images.Media.DATA, file.getAbsolutePath());
Uri result = context.getContentResolver().insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, image);
}
catch (Exception e) {
e.printStackTrace();
}
}
6. Застосункам Android потрібен дозвіл на встановлення, якщо вони планують зберігати файли на пристрої. Для цього помістіть таку стрічку у файл AndroidManifest.xml поряд з подібною, що запитує дозвіл на доступ до камери:
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
7. Скомпілюйте та запустіть програму! Якщо ви торкнетесь екрану, щоб зберегти фотографію, через деякий час оброблене фільтром зображення з'явиться на екрані (можливо, через 5 чи 10 секунд). Як тільки-но воно буде показано, це значитиме, що відбувся запис на SD-карту та в галерею. Завершіть роботу з програмою Cartoonifier, відкрийте галерею Android та перегляньте альбом. Ви маєте побачити оброблені зображення у форматі PNG повної роздільної здатності екрану.
Показуємо повідомлення Android про збережене зображення
Якщо треба показати повідомлення, коли нове зображення буде записане на SD-карту та в галерею Android, виконайте наступні кроки, інакше пропустіть цю частину:
1. Додайте наступний код у верхню частину файлу CartoonifierView.java:
// Для показу оповіщення, коли файл буде збережено.
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentValues;
import android.content.Intent;
2. Додайте це поряд з верхньою частиною файлу CartoonifierView:
private int mNotificationID = 0; // Щоб показати єдине оповіщення.
3. Помістіть це всередині блоку if під викликом функції savePNGImageToGallery() в processFrame():
showNotificationMessage(mContext, baseFilename);
4. Реалізуйте функцію showNotificationMessage() в класі CartoonifierView:
// Покажіть оповіщення, що вкаже на нове збережене зображення.
protected void showNotificationMessage(Context context,
String filename)
{
// Виведемо оповіщення у вікні стану Android.
// Щоб повідомлення виводилося для кожного
// зображення, але не більше, ніж по одному,
// використовуйте різний ID кожного разу,
// але видаляйте попередні, перш ніж створити нові.
final NotificationManager mgr = (NotificationManager) \
context.getSystemService(Context.NOTIFICATION_SERVICE);
// Закриємо попередні оповіщення, щоб виводити їх
// по одному за раз, але все ще для всіх зображень.
if (mNotificationID > 0)
mgr.cancel(mNotificationID);
mNotificationID++;
Notification notification = new Notification(R.drawable.icon,
"Зберігаємо в галерею ("image " + mNotificationID + ") ...",
System.currentTimeMillis());
Intent intent = new Intent(context, CartoonifierView.class);
// Ініціюємо закриття, якщо користувач натисне на оповіщення.
notification.flags |= Notification.FLAG_AUTO_CANCEL;
PendingIntent pendingIntent =
PendingIntent.getActivity(context,
0, intent, 0);
notification.setLatestEventInfo(context, "Cartoonifier зберіг "
+
mNotificationID + " зображень до галереї", "Збережено під назвою '" +
filename + "'", pendingIntent);
mgr.notify(mNotificationID, notification);
}
5. Знову ж таки, скомпілюємо та запустимо застосунок! Тепер, якщо ви натиснете на екран, щоб зберегти наступний кадр, буде показано оповіщення. Якщо повідомлення має з'явитися до обробки зображення, а не після цього тривалого процесу, здійсніть виклик функції showNotificationMessage() перед cartoonifyImage(), та перемістіть код для генерації рядків дати й часу, щоб для оповіщення та назви реального файлу залишалися однаковими.
Змінюємо режим мультиплікації через панель меню Android
Дозволимо користувачу змінювати режим через меню:
1. Додаємо наступні заголовки у верхній частині файлу
src\com\Cartoonifier\CartoonifierApp.java:
import android.view.Menu;
import android.view.MenuItem;
2. Розміщуємо наступні внутрішні змінні в клас CartoonifierApp:
// Елементи на панелі меню Android.
private MenuItem mMenuAlien;
private MenuItem mMenuEvil;
private MenuItem mMenuSketch;
private MenuItem mMenuDebug;
3. Додайте код наступних функцій в CartoonifierApp:
/** Викликається під час створення панелі меню системою Android. */
public boolean onCreateOptionsMenu(Menu menu) {
Log.i(TAG, "onCreateOptionsMenu");
mMenuSketch = menu.add("Скетч чи картина");
mMenuAlien = menu.add("Прибулець чи людина");
mMenuEvil = menu.add("Зло чи добро");
mMenuDebug = menu.add("[Режим налагодження]");
return true;
}
/** Викликається, коли користувач натискає на елемент панелі меню. */
public boolean onOptionsItemSelected(MenuItem item) {
Log.i(TAG, "Обрано елемент меню: " + item);
if (item == mMenuSketch)
mView.toggleSketchMode();
else if (item == mMenuAlien)
mView.toggleAlienMode();
else if (item == mMenuEvil)
mView.toggleEvilMode();
else if (item == mMenuDebug)
mView.toggleDebugMode();
return true;
}
4. Помістіть наступні внутрішні змінні в CartoonifierView:
private boolean m_sketchMode = false;
private boolean m_alienMode = false;
private boolean m_evilMode = false;
private boolean m_debugMode = false;
5. Додайте наступні функції в CartoonifierView:
protected void toggleSketchMode() {
m_sketchMode = !m_sketchMode;
}
protected void toggleAlienMode() {
m_alienMode = !m_alienMode;
}
protected void toggleEvilMode() {
m_evilMode = !m_evilMode;
}
protected void toggleDebugMode() {
m_debugMode = !m_debugMode;
}
6. Значення режимів потрібно передати в JNI-код функції cartoonifyImage(), тому відправимо їх як аргументи. Модифікуємо оголошення Java-функції CartoonifyImage() в CartoonifierView:
public native void CartoonifyImage(int width, int
height, byte[] yuv,
int[] rgba, boolean sketchMode, boolean alienMode,
boolean evilMode, boolean debugMode);
7. Тепер змінимо код Java таким чином, щоб передати поточні значення режимів в processFrame():
CartoonifyImage(getFrameWidth(), getFrameHeight(), data, rgba,
m_sketchMode, m_alienMode, m_evilMode, m_debugMode);
8. JNI оголошення функції CartoonifyImage() у файлі jni\jni_part.cpp тепер матиме вигляд:
JNIEXPORT void JNICALL
Java_com_Cartoonifier_CartoonifierView_CartoonifyImage(
JNIEnv* env, jobject, jint width, jint height,
jbyteArray yuv, jintArray bgra, jboolean sketchMode,
jboolean alienMode, jboolean evilMode, jboolean debugMode)
9. Тепер маємо передати режим в код C/C++ файлу cartoon.cpp з JNI-функції в jni\jni_part.cpp. Під час розробки для Android в кожен момент часу можна показувати одне вікно, на відміну від ПК, коли зручно оперувати кількома для налагодження. Тому замість булевої мітки вкажемо номер, що становитиме 0 для звичайного режиму програми, 1 для налагодження на мобільних пристроях (створення GUI OpenCV завжди викличе проблеми!) та 2 для налагодження версії для ПК (у якої можна створювати стільки вікон, скільки забажається):
int debugType = 0;
if (debugMode)
debugType = 1;
cartoonifyImage(mbgr, displayedFrame, sketchMode, alienMode,
evilMode, debugType);
10. Оновлюємо реалізацію C/C++ у файлі cartoon.cpp:
void cartoonifyImage(Mat srcColor, Mat dst, bool sketchMode,
bool alienMode, bool evilMode, int debugType)
{
11.Також змінюємо оголошення C/C++ у файлі cartoon.h:
void cartoonifyImage(Mat srcColor, Mat dst, bool sketchMode,
bool alienMode, bool evilMode, int debugType);
12. Компілюємо та запускаємо код; потім тестуємо кнопки меню в нижній частині вікна. Майте на увазі, скетч зображений в режимі реального часу, а картина формується зі значною затримкою через використання двопрохідного фільтра.
Зменшуємо випадковий перцевий шум на скетчі
Більшість камер сучасних смартфонів і планшетів створюють значну кількість шуму. Частіше за все це некритично, але у випадку застосування фільтру Лапласових меж 5х5 вплив надто сильний. Маска меж (показана у вигляді режиму скетчу) матиме тисячі невеликих скупчень, які називають перцевим шумом, кожна частка якого складатиметься з кількох чорних пікселів, розміщених поряд на білому фоні. Дотепер використано Медіанний фільтр, який непогано видаляє перцевий шум у більшості випадків, але в нашому він не достатньо ефективний. Маска меж переважно має чистий білий фон (зі значенням 255) з деякими чорними межами (0) та точки шуму (того ж рівня). Можна використати стандартний морфологічний оператор, але він видалить також і межі. Замість цього застосуємо ручний фільтр, що працюватиме з малими чорними фрагментами, повністю оточеними білими пікселями. Це дозволить спекатися більшості шуму, не сильно змінюючи власне межі.
Знайдемо на зображенні чорні пікселі, і для кожного з них перевіримо квадрат розмірами 5х5 навколо нього, щоб перевірити, чи не є границя повністю білою. Якщо це дійсно так, маємо острівець чорного шуму, тому заповнимо поточний блок білими пікселями й таким чином видалимо його. Для простоти, в нашому фільтрі 5х5 проігноруємо два граничні пікселі навколо зображення та залишимо їх як є.
Наступний малюнок зображає оригінальний кадр з планшета Android з лівого боку, режим скетчу посередині (на якому можна помітити малі чорні точки перцевого шуму), а з правого боку - результат обробки, на якому шкіра виглядає чистішою:
Наступний блок коду можна назвати функцією removePepperNoise(). Для простоти вона редагує вхідне зображення:
void removePepperNoise(Mat &mask)
{
for (int y=2; y<mask.rows-2; y++) {
// Отримаємо доступ до кожного з 5 рядків поблизу пікселя.
uchar *pUp2 = mask.ptr(y-2);
uchar *pUp1 = mask.ptr(y-1);
uchar *pThis = mask.ptr(y);
uchar *pDown1 = mask.ptr(y+1);
uchar *pDown2 = mask.ptr(y+2);
// Пропускаємо перші і останні 2 пікселі кожного рядка.
pThis += 2;
pUp1 += 2;
pUp2 += 2;
pDown1 += 2;
pDown2 += 2;
for (int x=2; x<mask.cols-2; x++) {
uchar value = *pThis; // Отримуємо значення пікселя (від 0 до 255).
// Перевіряємо, чи це не чорний піксель, що оточений
// білими (точніше, чи не острівець, складений з них).
if (value == 0) {
bool above, left, below, right, surroundings;
above = *(pUp2 - 2) && *(pUp2 - 1) && *(pUp2) &&
*(pUp2 + 1) && *(pUp2 + 2);
left = *(pUp1 - 2) && *(pThis - 2) && *(pDown1 - 2);
below = *(pDown2 - 2) && *(pDown2 - 1) && *(pDown2) &&
*(pDown2 + 1) && *(pDown2 + 2);
right = *(pUp1 + 2) && *(pThis + 2) && *(pDown1 + 2);
surroundings = above && left && below && right;
if (surroundings == true) {
// Заповнюємо весь блок білим кольором. Оскільки знаємо,
// що границі 5x5 вже білі, потрібно заповнити лише
// внутрішній регіон 3x3.
*(pUp1 - 1) = 255;
*(pUp1 + 0) = 255;
*(pUp1 + 1) = 255;
*(pThis - 1) = 255;
*(pThis + 0) = 255;
*(pThis + 1) = 255;
*(pDown1 - 1) = 255;
*(pDown1 + 0) = 255;
*(pDown1 + 1) = 255;
// Оскільки ми тільки-но заповнили весь блок 5x5
// білим кольором, знаємо, що наступні 2 пікселі
// не будуть чорними, тому пропускаємо їх.
pThis += 2;
pUp1 += 2;
pUp2 += 2;
pDown1 += 2;
pDown2 += 2;
}
}
// Переміщуємось до наступного пікселя справа.
pThis++;
pUp1++;
pUp2++;
pDown1++;
pDown2++;
}
}
}
Показуємо FPS у застосунку
Якщо існує необхідність показати кількість кадрів в секунду (FPD) на екрані, що не настільки важливо для повільного застосунку, як цей, але все ще корисно, слідуйте наступним крокам:
1. Скопіюйте файл src\org\opencv\samples\imagemanipulations\FpsMeter.java
з теки прикладу ImageManipulations в OpenCV (наприклад, C:\OpenCV-
2.4.1\samples\android\image-manipulations) до вашої теки src\com\Cartoonifier.
2. Замініть назву пакету у верхній частині FpsMeter.java на com.Cartoonifier.
3. У файлі CartoonifierViewBase.java оголосіть змінну типу FpsMeter після "private byte[] mBuffer;":
private FpsMeter mFps;
4. Налаштуйте об'єкт FpsMeter у конструкторі CartoonifierViewBase(), після виклику "mHolder.addCallback(this);":
mFps = new FpsMeter();
mFps.init();
5. Виміряйте швидкість кожного кадру у функції run() після блоку try/catch:
mFps.measure();
6. Виведіть FPS на екран на кожному кадрі, у функції run() після
виклику canvas.drawBitmap():
mFps.draw(canvas, (canvas.getWidth() - bmp.getWidth()) /2, 0);
Змінюємо роздільну здатність камери
Якщо необхідно підвищити швидкість роботи програми, при цьому зниження якості не має значення, вочевидь треба або запросити з камери кадри меншого розміру, або зменшити отримані зображення. Код прикладу, покладеного в основу Cartoonifier, базується на використанні найближчої до висоти екрану роздільної здатності камери. Тому, якщо прилад має 5-мегапіксельну камеру, а екран - 640х480 пікселів, можна використати зображення розмірами 720х480. Щоб мати можливість обирати роздільну здатність, можна модифікувати параметри виклику setupCamera() у функції surfaceChanged() файлу CartoonifierViewBase.java. Наприклад:
public void surfaceChanged(SurfaceHolder _holder, int format,
int width, int height) {
Log.i(TAG, "Розмір екрана: " + width + "x" + height);
// Роздільна здатність рівна приблизно половині розміру екрана.
setupCamera(width/2, height/2);
}
Простий спосіб отримати зображення найвищої роздільної здатності з камери - передати велике значення розміру, наприклад 10000х10000, у результаті чого алгоритм обере максимально доступну (врахуйте, що це стосується характеристик попереднього перегляду, тобто відеопотоку камери, які часто значно нижчі, ніж для простих зображень). Або, якщо вимагається найбільша швидкість, передайте 1х1 в якості параметру, це дозволить знайти найнижчу роздільну здатність (наприклад, 160х120).
Підлаштовуємо застосунок
Створивши програму Cartoonifier для Android, ви маєте розуміти, що лежить в основі її роботи та для чого потрібні окремі частини. Тепер підлаштуйте її під власні потреби! Змініть графічний інтерфейс, поведінку та потік програми, константи фільтру мультиплікатора, алгоритм пошуку шкіри чи замініть код власними ідеями.
Алгоритм розпізнавання шкіри можна покращити безліччю способів, шляхом використання складніших варіантів (наприклад, тренованих моделей Гауса з недавніх записів конференцій CVPR та ICCV, що знаходяться за адресою http://www.cvpapers.com) чи застосування процесу розпізнавання (перегляньте відповідну частину розділу 8 "Розпізнавання облич за допомогою алгоритму Eigenfaces"), щоб існувала можливість знаходити обличчя, а не просити користувача помістити його в центр екрана. Майте на увазі, розпізнавання може зайняти тривалий час на деяких пристроях чи камерах з високою роздільною здатністю, тому цей підхід обмежений порівняно невисокою швидкістю обробки. Хоча швидкість смартфонів та планшетів значно зростає кожного року, тому з часом це стане меншою проблемою.
Найефективніший спосіб підвищити швидкість роботи мобільних програм комп'ютерного зору - максимально зменшити роздільну здатність камери (0.5 мегапікселя замість 5), рідше виділяти та звільняти пам'ять для зображень, проводити менше конвертацій (наприклад, шляхом підтримки формату BGRA в усьому коді). Можна також пошукати способи оптимізувати обробку зображень чи математичні бібліотеки від постачальника CPU вашого пристрою (як-от, NVIDIA Tegra, Texas Instruments OMAP, Samsung Exynos, Apple Ax чи QualComm Snapdragon) або для сімейства (наприклад, ARM Cortex-A9). Пам'ятайте, може існувати оптимізована версія OpenCV для цільової машини.
Щоб полегшити підлаштування NDK та коду обробки зображень для ПК, до книги додаються файли ImageUtils.cpp та ImageUtils.h. Вони включають корисні функції, як-от printMatInfo(), що виводить багато інформації про об'єкт cv::Mat, завдяки чому полегшується налагодження коду OpenCV. Також доступні макроси для профілювання, що дають змогу легко отримати детальну статистику С/C++ коду. Наприклад:
DECLARE_TIMING(myFilter);
void myImageFunction(Mat img) {
printMatInfo(img, "вхідне зображення");
START_TIMING(myFilter);
bilateralFilter(img, …);
STOP_TIMING(myFilter);
SHOW_TIMING(myFilter, "профілювання myFilter");
}
В консолі ви отримаєте щось подібне:
вхідне зображення: 800w600h 3ch 8bpp, range[19,255][17,243][47,251]
профілювання myFilter: time: 213ms (ave=215ms min=197ms max=312ms, across 57
runs).
Користь стає очевидною, коли код OpenCV працює не так, як слід; особливо під час розробки для мобільних пристроїв, коли часто неможливо використати налагоджувач IDE, а положення printf() не працюють. Однак, функції ImageUtils однаково добре підходять для мобільних та стаціонарних пристроїв.
Висновки
У цьому розділі показано декілька типів фільтрів обробки зображень, що можуть бути використані для створення різноманітних ефектів мультиплікації: простий режим скетчу, що схожий на малюнок олівцем, картини, яка виглядає як твір мистецтва, та мультиплікації, що розміщує скетч над картиною та робить зображення більш схожим на мультик. Також пояснюється як отримати інші смішні ефекти, наприклад режим "зла", що значно загострює шумні межі, та прибульця, який змушує шкіру виглядати яскраво зеленою.
Існує багато комерційних програм для смартфонів, які формують схожий смішний ефект на обличчі користувача, як-от фільтри мультиплікації та зміни кольору шкіри. Доступні також професійні інструменти, що базуються на схожих концепціях. Серед них інструменти для постобробки та згладження, які намагаються покращити вигляд жіночих облич, згладжуючи шкіру та залишаючи межі й інші фрагменти гострими, щоб їхні обличчя видавалися молодшими. У розділі пояснюється, як перенести настільну програму у варіант для Android, слідуючи спочатку рекомендованим орієнтирам розробки робочої версії для ПК, здійснивши портування та створивши графічний інтерфейс, що підходить мобільній програмі. Код обробки зображення використовується двома проєктами, тому читач має можливість модифікувати фільтри мультиплікації для настільного застосунку та, перебудовуючи версію для мобільних пристроїв, автоматично отримувати зміни в застосунку Android.
Спосіб використання OpenCV4Android часто змінюються, та й розробка для Android не стоїть на місці; тому в розділі пояснюється, як збудувати програму для Android, просто додаючи функціональність до одного з проєктів-прикладів OpenCV. Очікується, що читач може додати подібні можливості в еквівалентний проєкт у майбутніх версіях OpenCV4Android.
Книга включає вихідний код обох проєктів: для настільних та мобільних пристроїв.
Внимание! Этот перевод, возможно, ещё не готов.
Его статус: перевод редактируется
Переведено на Нотабеноиде
http://notabenoid.org/book/81521/498897
Переводчики: Atronax
Настройки
Готово:
