Mastering OpenCV with Practical Computer Vision Projects / Опановуюємо OpenCV через практичні проекти комп'ютерного зору: Розділ 1. Ефект мультиплікації та зміна кольору шкіри для Android

← →
Английский оригинал Перевод на украинский

Сожалеем, но текст оригинала доступен только зарегистрированным пользователям.

Надаємо зображенню ефект мультиплікації у відповідь на дотик до екрана

Atronax 14.02.21 в 11:42

0

Щоб показати зображення з камери (поки користувач не забажає застосувати ефект мультиплікації до обраного кадру), можна просто викликати створену раніше JNI-функцію ShowPreview(). До застосування фільтру також знадобиться почекати на події дотику. Нам потрібно обробити зображення, коли користувач дотикнеться до екрана, тому зробимо мітку, що вкаже на потребу застосувати фільтр до наступного кадру, і лише коли її буде звільнено, вивід на екран продовжиться. Це значитиме, що оброблене зображення виведеться лише на долі секунди, а потім зміниться на наступний кадр камери. Тому знадобиться інша мітка, завдяки якій зображення не зникатиме декілька секунд до того, як наступні змінять його; зрештою, це дасть змогу користувачу роздивитися подібні кадри.

Atronax 14.02.21 в 12:34

0

1. Додаємо наступний заголовок у верхній частині файлу CartoonifierApp.java з
теки src\com\Cartoonifier:
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.MotionEvent;

Atronax 14.02.21 в 12:35

0

2. Модифікуємо визначення класу у верхній частині файлу CartoonifierApp.java:
public class CartoonifierApp
extends Activity implements OnTouchListener {

Atronax 14.02.21 в 12:36

0

3. Вміщуємо наступний код в нижню частину функції onCreate():
// Викликаємо функцію зворотного виклику onTouch() у
// відповідь на дотик користувача до екрана.
mView.setOnTouchListener(this);

Atronax 14.02.21 в 12:38

0

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;
}

Atronax 14.02.21 в 12:44

0

5. Тепер визначимо функцію nextFrameShouldBeSaved() в файлі CartoonifierView.java:

Atronax 14.02.21 в 12:44

0

// Оброблюємо наступний кадр камери та зберігаємо його замість виводу на екран
protected void nextFrameShouldBeSaved(Context context) {
bSaveThisFrame = true;
}

Atronax 14.02.21 в 12:46

0

6. Запишемо наступні змінні у верхній частині класу CartoonifierView:
private boolean bSaveThisFrame = false;
private boolean bFreezeOutput = false;
private static final int FREEZE_OUTPUT_MSECS = 3000;

Atronax 14.02.21 в 12:48

0

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;
}

Atronax 14.02.21 в 12:55

0

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;
}

Atronax 14.02.21 в 13:21

0

8. Тепер програма має скомпілюватися та запуститися. Перевіримо, чи добре вона працює.

Atronax 14.02.21 в 13:22

0

Зберігаємо зображення у файл та виводимо в галереї Android

Atronax 14.02.21 в 13:23

0

Ми збережемо вихідний результат у вигляді файлу 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:

Atronax 14.02.21 в 13:46

0

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);
}

Atronax 14.02.21 в 13:57

0

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;

Atronax 14.02.21 в 14:02

0

3. Помістіть цей код в класі CartoonifierView, у верхній частині:
private static final String TAG = "CartoonifierView";
private Context mContext; // Щоб отримати доступ до галереї Android.

Atronax 14.02.21 в 14:04

0

4. Додайте це у функцію nextFrameShouldBeSaved() класу CartoonifierView:
mContext = context; // Зберегти контекст Android для доступу до графічного інтерфейсу.

Atronax 14.02.21 в 14:05

0

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() + "]");

Atronax 14.02.21 в 14:09

0

// Відкриваємо файл.
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();
}
}

Atronax 14.02.21 в 14:15

0

6. Застосункам Android потрібен дозвіл на встановлення, якщо вони планують зберігати файли на пристрої. Для цього помістіть таку стрічку у файл AndroidManifest.xml поряд з подібною, що запитує дозвіл на доступ до камери:
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Atronax 14.02.21 в 14:21

0

7. Скомпілюйте та запустіть програму! Якщо ви торкнетесь екрану, щоб зберегти фотографію, через деякий час оброблене фільтром зображення з'явиться на екрані (можливо, через 5 чи 10 секунд). Як тільки-но воно буде показано, це значитиме, що відбувся запис на SD-карту та в галерею. Завершіть роботу з програмою Cartoonifier, відкрийте галерею Android та перегляньте альбом. Ви маєте побачити оброблені зображення у форматі PNG повної роздільної здатності екрану.

Atronax 14.02.21 в 14:36

0

Показуємо повідомлення Android про збережене зображення
Якщо треба показати повідомлення, коли нове зображення буде записане на SD-карту та в галерею Android, виконайте наступні кроки, інакше пропустіть цю частину:

Atronax 14.02.21 в 14:38

0

1. Додайте наступний код у верхню частину файлу CartoonifierView.java:
// Для показу оповіщення, коли файл буде збережено.
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentValues;
import android.content.Intent;

Atronax 14.02.21 в 14:39

0

2. Додайте це поряд з верхньою частиною файлу CartoonifierView:
private int mNotificationID = 0; // Щоб показати єдине оповіщення.

Atronax 14.02.21 в 14:41

0

3. Помістіть це всередині блоку if під викликом функції savePNGImageToGallery() в processFrame():
showNotificationMessage(mContext, baseFilename);

Atronax 14.02.21 в 14:44

0

4. Реалізуйте функцію showNotificationMessage() в класі CartoonifierView:
// Покажіть оповіщення, що вкаже на нове збережене зображення.
protected void showNotificationMessage(Context context,
String filename)
{
// Виведемо оповіщення у вікні стану Android.
// Щоб повідомлення виводилося для кожного
// зображення, але не більше, ніж по одному,
// використовуйте різний ID кожного разу,
// але видаляйте попередні, перш ніж створити нові.
final NotificationManager mgr = (NotificationManager) \
context.getSystemService(Context.NOTIFICATION_SERVICE);

Atronax 14.02.21 в 14:50

0

// Закриємо попередні оповіщення, щоб виводити їх
// по одному за раз, але все ще для всіх зображень.
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);
}

Atronax 14.02.21 в 14:57

0

5. Знову ж таки, скомпілюємо та запустимо застосунок! Тепер, якщо ви натиснете на екран, щоб зберегти наступний кадр, буде показано оповіщення. Якщо повідомлення має з'явитися до обробки зображення, а не після цього тривалого процесу, здійсніть виклик функції showNotificationMessage() перед cartoonifyImage(), та перемістіть код для генерації рядків дати й часу, щоб для оповіщення та назви реального файлу залишалися однаковими.

Atronax 14.02.21 в 15:08

0

Змінюємо режим мультиплікації через панель меню Android

Atronax 14.02.21 в 15:09

0

Дозволимо користувачу змінювати режим через меню:
1. Додаємо наступні заголовки у верхній частині файлу
src\com\Cartoonifier\CartoonifierApp.java:
import android.view.Menu;
import android.view.MenuItem;

Atronax 14.02.21 в 15:10

0

2. Розміщуємо наступні внутрішні змінні в клас CartoonifierApp:
// Елементи на панелі меню Android.
private MenuItem mMenuAlien;
private MenuItem mMenuEvil;
private MenuItem mMenuSketch;
private MenuItem mMenuDebug;

Atronax 14.02.21 в 15:11

0

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;
}

Atronax 14.02.21 в 15:19

0

4. Помістіть наступні внутрішні змінні в CartoonifierView:
private boolean m_sketchMode = false;
private boolean m_alienMode = false;
private boolean m_evilMode = false;
private boolean m_debugMode = false;

Atronax 14.02.21 в 15:20

0

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;
}

Atronax 14.02.21 в 15:21

0

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);

Atronax 14.02.21 в 15:23

0

7. Тепер змінимо код Java таким чином, щоб передати поточні значення режимів в processFrame():
CartoonifyImage(getFrameWidth(), getFrameHeight(), data, rgba,
m_sketchMode, m_alienMode, m_evilMode, m_debugMode);

Atronax 14.02.21 в 15:25

0

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)

Atronax 14.02.21 в 15:26

0

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);

Atronax 14.02.21 в 15:38

0

10. Оновлюємо реалізацію C/C++ у файлі cartoon.cpp:
void cartoonifyImage(Mat srcColor, Mat dst, bool sketchMode,
bool alienMode, bool evilMode, int debugType)
{

Atronax 14.02.21 в 15:39

0

11.Також змінюємо оголошення C/C++ у файлі cartoon.h:
void cartoonifyImage(Mat srcColor, Mat dst, bool sketchMode,
bool alienMode, bool evilMode, int debugType);

Atronax 14.02.21 в 15:40

0

12. Компілюємо та запускаємо код; потім тестуємо кнопки меню в нижній частині вікна. Майте на увазі, скетч зображений в режимі реального часу, а картина формується зі значною затримкою через використання двопрохідного фільтра.

Atronax 14.02.21 в 15:43

0

Зменшуємо випадковий перцевий шум на скетчі

Atronax 15.02.21 в 08:37

0

Більшість камер сучасних смартфонів і планшетів створюють значну кількість шуму. Частіше за все це некритично, але у випадку застосування фільтру Лапласових меж 5х5 вплив надто сильний. Маска меж (показана у вигляді режиму скетчу) матиме тисячі невеликих скупчень, які називають перцевим шумом, кожна частка якого складатиметься з кількох чорних пікселів, розміщених поряд на білому фоні. Дотепер використано Медіанний фільтр, який непогано видаляє перцевий шум у більшості випадків, але в нашому він не достатньо ефективний. Маска меж переважно має чистий білий фон (зі значенням 255) з деякими чорними межами (0) та точки шуму (того ж рівня). Можна використати стандартний морфологічний оператор, але він видалить також і межі. Замість цього застосуємо ручний фільтр, що працюватиме з малими чорними фрагментами, повністю оточеними білими пікселями. Це дозволить спекатися більшості шуму, не сильно змінюючи власне межі.

Atronax 15.02.21 в 08:38

0

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

Atronax 15.02.21 в 09:04

0

Наступний малюнок зображає оригінальний кадр з планшета Android з лівого боку, режим скетчу посередині (на якому можна помітити малі чорні точки перцевого шуму), а з правого боку - результат обробки, на якому шкіра виглядає чистішою:

Atronax 15.02.21 в 09:08

0

Наступний блок коду можна назвати функцією removePepperNoise(). Для простоти вона редагує вхідне зображення:

Atronax 15.02.21 в 09:09

0

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).

Atronax 15.02.21 в 09:10

0

// Перевіряємо, чи це не чорний піксель, що оточений
// білими (точніше, чи не острівець, складений з них).
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;

Atronax 15.02.21 в 09:16

0

// Оскільки ми тільки-но заповнили весь блок 5x5
// білим кольором, знаємо, що наступні 2 пікселі
// не будуть чорними, тому пропускаємо їх.
pThis += 2;
pUp1 += 2;
pUp2 += 2;
pDown1 += 2;
pDown2 += 2;
}
}
// Переміщуємось до наступного пікселя справа.
pThis++;
pUp1++;
pUp2++;
pDown1++;
pDown2++;
}
}
}

Atronax 15.02.21 в 09:20

0
Минутку...

← Предыдущая страница

Следующая страница →