Привіт, makkals,
Ця публікація є частиною серії статей про WebAssembly. Ознайомтесь з іншими частинами серії тут
Оригінальна публікація — https://hemath.dev/blog/webassembly/build-image-processor-application-with-webassembly
На цьому етапі ми маємо достатньо знань для роботи з WebAssembly. Час створити щось достатньо цікаве, щоб зрозуміти більше про WebAssembly.
У цій публікації ми створимо додаток, який конвертує кольорові зображення в відтінки сірого без використання бібліотек.
Ось швидкий перегляд додатку, який ми побудуємо. Він бере кольорове зображення та перетворює його в відтінки сірого.
Ви можете отримати вихідний код цього проекту з цього репозиторію — https://github.com/djhemath/Webassembly-demos/tree/main/grayscale
Ідея перетворення зображення в відтінки сірого
Перед тим як почати писати код, давайте спершу зрозуміємо логіку перетворення кольорового зображення в відтінки сірого.
Кожне зображення складається з пікселів. Кожен піксель має колір. Кожен колір можна представити 4 значеннями:
- R — червоний
- G — зелений
- B — синій
- A — альфа / прозорість
Зображення з https://tsumutake.com/photo-quality/chapter1-structure-of-photo
Отже, якщо ми зможемо змінити значення R, G та B кожного пікселя, ми зможемо перетворити зображення в відтінки сірого.
Для цього ми повинні помножити значення R, G та B на певні коефіцієнти, що зменшують світність.
Ми розіб'ємо світність на 3 частини,
0.299 + 0.587 + 0.114 = 1
Чому саме ці числа?
Ці коефіцієнти відображають чутливість людського ока до різних кольорів:
- Зелений (
0.587
): Людське око найбільше чутливе до зеленого світла, тому цей колір має найвищу вагу. - Червоний (
0.299
): Око менш чутливе до червоного світла, тому він має помірну вагу. - Синій (
0.114
): Око найменше чутливе до синього світла, тому цей колір має найменшу вагу.
Отже, щоб перетворити будь-який піксель у сірий, ми можемо використовувати наступну формулу,
gray = (r * 0.299) + (g * 0.587) + (b * 0.114)
Отримання пікселів зображення в JavaScript
Наступне завдання — це зчитати зображення та отримати дані пікселів з нього. На щастя, у нас є Canvas в Web. Ідея полягає в тому, щоб намалювати зображення, вибране користувачем, на канвасі та отримати дані зображення з канваса.
Функція CanvasRenderingContext2D.getImageData
дає нам одномірний масив беззнакових цілих чисел 8-біт.
Оскільки це одномірний масив, дані кожного пікселя займають 4 місця в масиві.
Наприклад,
[137, 243, 92, 255, 98, 223, 148, …]
Тут дані для
- першого пікселя зберігаються в індексах 0, 1, 2 та 3
- другого пікселя — в індексах 4, 5, 6 та 7
- і так далі
Це фактично плоский масив.
Отже, працюючи з цим масивом, ми переважно ітерацією обробляємо 4 елементи за раз.
У цьому масиві,
- перший елемент представляє R
- другий елемент представляє G
- третій елемент представляє B
- четвертий елемент представляє A
Щоб отримати дані зображення в JavaScript,
```
const imageInput = document.getElementById('image');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
imageInput.addEventListener('change', (e) => {
// Отримуємо файл з об'єкта події
const file = e.target.files[0];
// Створюємо новий FileReader
const reader = new FileReader();
// Зчитуємо файл як data URL
reader.readAsDataURL(file);
reader.onload = () => {
// Створюємо об'єкт зображення
const image = new Image();
// Завантажуємо результат у зображення
image.src = reader.result;
image.onload = () => {
// Встановлюємо ширину та висоту канвасу рівними розмірам зображення
canvas.width = image.width;
canvas.height = image.height;
// Малюємо зображення на канвасі
ctx.drawImage(image, 0, 0);
// Отримуємо дані зображення
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Отримуємо масив пікселів
const pixels = imageData.data;
}
}
});
Перепрошую за "пекло колбеків", я навмисно спростив код, щоб ми могли побачити потік з верхньої частини до нижньої одночасно.
Напишемо логіку для перетворення зображення в відтінки сірого
Тепер, коли ми маємо формулу для конвертації та дані пікселів, час написати функцію WebAssembly, яка перетворюватиме кожен піксель у сірий. Створимо просту функцію на C++,
// grayscale.cpp
#include
#include
extern "C" {
EMSCRIPTEN_KEEPALIVE
void applyGrayscale(uint8_t* data, int length) {
for (int i = 0; i < length; i += 4) {
uint8_t r = data[i];
uint8_t g = data[i + 1];
uint8_t b = data[i + 2];
uint8_t gray = static_cast<uint8_t>(0.299 * r + 0.587 * g + 0.114 * b);
data[i] = data[i + 1] = data[i + 2] = gray; // Встановлюємо R, G, B у значення сірого
}
}
}
Це проста функція на C++, яка отримує вхідний вказівник на беззнакове 8-бітне ціле число. Вона потім застосовує формулу для кожного пікселя.
Якщо подивитися уважно, ця функція безпосередньо змінює значення вказівника замість того, щоб повертати змінений масив.
Це зроблено навмисно, оскільки повернення нового масиву означає передачу даних з WASM до JS. І така передача даних є витратною і займає час. Щоб уникнути цього, ми можемо створити спільну пам'ять між JavaScript та C++. Ми помістимо дані зображення в пам'ять і передамо вказівник на ці дані. А функція C++ працюватиме з даними безпосередньо.
Тепер давайте скомпілюємо код і згенеруємо WASM бінарний файл та JavaScript код зв'язки.
emcc grayscale.cpp -o grayscale.js -O3 -s EXPORTED_FUNCTIONS='["_applyGrayscale", "_malloc", "_free"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' -s ALLOW_MEMORY_GROWTH=1
Це згенерує два файли:
- grayscale.wasm — реальний бінарний файл WASM
- grayscale.js — код зв'язки на JS
Використання згенерованого WASM з JavaScript
Для наступних частин вам потрібно бути трохи знайомим з:
- Пам'яттю стека і купи
- malloc та free
Перш за все, нам потрібно створити і налаштувати дані зображення в пам'яті WASM.
Ми можемо зробити це за допомогою функції _malloc
та об'єкта HEAP8
.
// Створюємо необхідну пам'ять
const imageDataPointer = Module._malloc(pixles.length * pixles.BYTES_PER_ELEMENT);
// Встановлюємо масив пікселів у пам'ять
Module.HEAP8.set(pixles, imageDataPointer / input.BYTES_PER_ELEMENT);
Тут Module._malloc
створює необхідну кількість пам'яті. BYTES_PER_ELEMENT
вказує, скільки байт займає один елемент у масиві pixels
. Помноживши довжину масиву пікселів на байти на елемент, ми отримуємо загальний розмір пам'яті, необхідний для зображення.
У нашому випадку масив пікселів — це масив 8-бітних цілих чисел. Кожен елемент займає 8 біт або 1 байт пам'яті. Отже, якщо масив пікселів має 1000 елементів, то потрібна пам'ять становить 1000 байт.
Пам'ятайте, що кожен піксель представлений 4 цілими числами. Це означає, що кожен піксель займає 4 байти пам'яті.
Отже, якщо зображення містить 25 x 25 пікселів, то це означає,
25 x 25 = 125 пікселів всього
125 x 4 = 500 елементів у масиві
500 x 1 = 500 байт
Після того, як ми виділимо місце та встановимо дані, ми можемо викликати функцію applyGrayscale
, визначену в WASM.
// Обгортка JS навколо функції WASM
const applyGrayscale = Module.cwrap("applyGrayscale", null, ["number", "number"]);
// Викликаємо функцію WASM з необхідними даними
applyGrayscale(imageDataPointer, pixels.length);
Після того, як функція WASM завершить виконання, пам'ять тепер буде зміненою.
Щоб отримати змінені дані з пам'яті, ми повинні отримати buffer
з HEAP8
і створити масив беззнакових 8-бітних цілих чисел.
// Отримуємо змінені дані з пам'яті
const grayPixels = new Uint8Array(Module.HEAP8.buffer, imageDataPointer, pixels.length);
// Встановлюємо змінені дані в оригінальний масив пікселів
pixels.set(grayPixels);
Після всього цього, ми МУСТИмо очистити пам'ять, яку виділили. Пам'ятайте, що навіть хоча JavaScript має збірник сміття, C++ — ні!
Module._free(imageDataPointer);
Об'єднуємо все разом
Досі ми розглядали кожен компонент окремо.
Тепер давайте об'єднаємо всі ці частини.
JS
WASM
```
## Висновок
Отже, нарешті ми створили працюючий додаток в WebAssembly. Якщо подивитись уважно, ми лише перемістили обчислювально інтенсивну частину в C++ і зберегли інші активності, пов'язані з інтерфейсом користувача, в JavaScript. Це гарний приклад того, як WebAssembly та JavaScript можуть працювати разом.
Ви можете отримати вихідний код цього проекту з цього репозиторію — [https://github.com/djhemath/Webassembly-demos/tree/main/grayscale](https://github.com/djhemath/Webassembly-demos/tree/main/grayscale)
Хоча ми створили додаток для реального використання, це все ж невеликий проект. Є багато інших речей, які можна зробити за допомогою WebAssembly. Ми також можемо використовувати бібліотеки, написані іншими мовами, у браузерах.
У наступній публікації ми розглянемо, як використати відому бібліотеку [FFmpeg](https://www.ffmpeg.org) (написану на C++) для створення конвертера відео в GIF. Слідкуйте за оновленнями!
_Оригінально опубліковано на_ [_https://hemath.dev_](https://hemath.dev/blog/webassembly/build-image-processor-application-with-webassembly)_._
Перекладено з: [Build an image processor application with webassembly](https://medium.com/@djhemath/build-an-image-processor-application-with-webassembly-5831df06a78a)