https://mikeash.com/pyblog/friday-qa-2012-08-31-obtaining-and-interpreting-image-data.html
Mikeash.com: просто цей хлопець, ти знаєш?

П'ятниця Q & A 2012-08-31: Отримання та інтерпретація даних зображень
від Майк Аш

Какао надає деякі чудові абстракції для роботи з зображеннями. NSImage дозволяє розглядати зображення як непрозорий згусток , що ви можете просто намалювати , де ви хочете. Core Image обробляє велику кількість оброблених зображень у простий у використанні API, що звільняє вас від турбот про те, як відображаються окремі пікселі. Тим не менш, іноді ви дійсно просто хочете потрапити на сирі піксельні дані в коді. Скотт Лютер запропонував сьогоднішню тему: отримання та маніпулювання цим сировинними піксельними даними.

Теорія
Найпростіше представлення зображення - це простий растровий малюнок. Це масив бітів, один на піксель, який вказує, чи є він чорним або білим. Масив містить рядки пікселів один за одним, так що загальна кількість бітів дорівнює ширині зображення, помноженому на висоту. Ось приклад растрового зображення посмішки:

    0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0
    0 0 1 0 0 1 0 0
    0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0
    0 1 0 0 0 0 1 0
    0 0 1 1 1 1 0 0
    0 0 0 0 0 0 0 0

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

Ще раз ми будемо використовувати масив байтів із послідовними рядками. Ось приклад коду для виділення пам'яті для зображення:

    uint8_t *AllocateImage(int width, int height)
    {
        return malloc(width * height);
    }

Для того, щоб дістатися до конкретного пікселя в (x, y) , ми повинні рухатися вниз y рядків, а потім по цьому рядку на x пікселів. Так як рядки викладаються послідовно, ми рухаємося вниз y рядків шляхом переміщення через масив по y * width байтів. Індекс для конкретного пікселя , то x + y * width . Виходячи з цього, тут є дві функції для отримання та налаштування пікселя сірого на певній координаті:

    uint8_t ReadPixel(uint8_t *image, int width, int x, int y)
    {
        int index = x + y * width;
        return image[index];
    }

    void SetPixel(uint8_t *image, int width, int x, int y, uint8_t value)
    {
        int index = x + y * width;
        image[index] = value;
    }

Темне відтінки сірого все ще не так вже й цікаво у багатьох випадках, і ми хочемо, щоб він міг представляти колір. Типовим способом представлення кольорових пікселів є комбінація трьох значень для червоних, зелених та синіх компонентів. Усі нулі призводять до появи чорного кольору, з іншими значеннями, що змішують три кольори, щоб сформувати потрібний колір. Це типово для використання 8 біт на колір, що призводить до 24 біта на піксель. Іноді вони упаковані разом, а іноді вони доповнюються з додатковими 8 бітами порожнечі , щоб дати 32 біт на піксель, що є краще працювати з так комп'ютери, як правило , добре маніпулюють 32 значень -розрядним.

Прозорість або альфа, також може бути зручним для представлення на зображенні. 8 біт прозорості вписується в 8 бітів заповнення в 32 бітового пікселя, і з допомогою 32 біт пікселів тримає червоний, зелений, синій, і альфа, ймовірно , є найбільш поширеним форматом пікселя використовується в даний час.

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

Також можна зберігати кожен колір у окремій частині пам'яті. Кожен кусок називається літаком, і цей формат називається "плоским". У цьому випадку у вас, по суті, є три або чотири (в залежності від наявності альфа) області пам'яті, кожен з яких викладено точно так, як пікселі із прикладу сірого кольору зверху. Колір пікселя - це комбінація значень з усіх площин. Іноді це може бути більш зручним для роботи, але часто повільніше, з - за поганий локальності посилань, а часто і більш складним для роботи, так що це набагато менш поширений формат.

Єдина річ, щоб з'ясувати, як замовлені кольори. Замовлення RGBA (червоний, зелений, синій, а потім альфа) найпоширеніше на Mac, але іноді також з'являються замовлення, як-от ARGB та BGRA. Немає особливої ​​причини вибрати одне над іншим, окрім сумісності чи швидкості. Щоб уникнути дорогих перетворень формату, найкраще відповідати формату, який використовується незалежно від того, що ви будете малювати, зберігати чи завантажувати з, коли можливо.

Отримання піксельних даних
Клас какао , який містить і надає дані пікселів NSBitmapImageRep . Це підклас NSImageRep , який є абстрактним класом для одного «репрезентації» образу. NSImage є контейнер для одного або декількох NSImageRep примірників. У тому випадку , коли існує більш ніж одне подання, вони можуть представляти різні розміри, дозвіл, колірні простору і т.д., і NSImage буде вибрати кращий для поточного контексту , коли малюнок.

З огляду на , що, здається , що це має бути досить легко отримати дані зображення з NSImage : знайти NSBitmapImageRep в своїх уявленнях, то запитайте , що уявлення для своїх піксельних даних.

Є дві проблеми з цим. По- перше, зображення може не мати NSBitmapImageRep взагалі. Є типи представлення, які не є растровими зображеннями. Наприклад, NSImage , що представляє PDF буде містити векторні дані, а НЕ растрові дані, і використовувати інший тип представлення зображення. По- друге, навіть якщо зображення дійсно є NSBitmapImageRep , там ніхто не знає , що формат пікселя цього подання буде. Непрактично писати код для обробки всіх можливих форматів пікселів, особливо тому, що більшість випадків буде важко перевірити.

Там дуже багато коду там що робить це anyway. Вона сходить з рук, роблячи припущення про зміст NSImage і формату пікселя NSBitmapImageRep . Це не є надійним і його слід уникати.

Як ви надійно отримати піксельні дані, а потім? Ви можете намалювати NSImage надійно, і ви можете зробити в NSBitmapImageRep використовуючи NSGraphicsContext клас, і ви можете отримати піксельні дані від NSBitmapImageRep . Це все це разом, і ви можете отримати піксельні дані.

Ось код для обробки цієї послідовності. Перше, що він робить, це з'ясувати ширину і висоту пікселів растрового зображення. Це не обов'язково очевидно, так як NSImage «s size не повинен відповідати розмірам пікселів. Цей код буде використовувати size в будь-якому випадку, але в залежності від ситуації, ви можете використовувати інший спосіб , щоб з'ясувати розмір:

    NSBitmapImageRep *ImageRepFromImage(NSImage *image)
    {
        int width = [image size].width;
        int height = [image size].height;

        if(width < 1 || height < 1)
            return nil;

Далі ми створюємо NSBitmapImageRep . Це передбачає використання дуже довгого метод ініціалізатор , який виглядає начебто страшно, але я пройду всі параметри в деталях:

        NSBitmapImageRep *rep = [[NSBitmapImageRep alloc]
                                 initWithBitmapDataPlanes: NULL
                                 pixelsWide: width
                                 pixelsHigh: height
                                 bitsPerSample: 8
                                 samplesPerPixel: 4
                                 hasAlpha: YES
                                 isPlanar: NO
                                 colorSpaceName: NSCalibratedRGBColorSpace
                                 bytesPerRow: width * 4
                                 bitsPerPixel: 32]

Давайте подивимось на ці параметри один за іншим. Перший аргумент, BitmapDataPlanes , дозволяє визначити пам'ять , в якій будуть зберігатися дані пікселів. Передача NULL тут, як цей код, каже NSBitmapImageRep виділити свою власну пам'ять всередині, яка, як правило , найбільш зручний спосіб впоратися з цим.

Далі код вказує кількість пікселів, широких і високих, які він розраховував раніше. Він просто передає ці значення протягом pixelsWide і pixelsHigh .

Тепер ми починаємо потрапляти у фактичний формат пікселів. Я вже згадував раніше, що 32-розрядний RGBA (де червоний, зелений, синій і альфа кожен займають один байт і розташовуються незмінно в пам'яті) є загальним форматом пікселів, і це ми збираємося використовувати. Так як кожен зразок займає один байт, код проходить 8 для bitsPerSample: . samplesPerPixel: параметр відноситься до числа різних компонентів , які використовуються в зображенні. У нас є чотири компоненти (R, G, B і A) і тому код проходить 4 тут.

Формат RGBA має альфа, тому ми переходимо YES для hasAlpha . Ми не хочемо , плоский формат, тому ми переходимо NO для isPlanar. Ми хочемо RGB колірного простору, так що ми проходимо NSCalibratedRGBColorSpace .

Далі, NSBitmapImageRep хоче знати , скільки байт становить кожен рядок зображення. Це використовується у випадку, якщо бажано заповнення. Іноді рядок зображень використовує більше, ніж строго мінімальна кількість байтів, зазвичай з міркувань продуктивності, щоб краще вирівняти їх. Ми не хочемо , щоб возитися з прокладкою, тому ми передаємо мінімальну кількість байт , необхідних для одного рядка пікселів, що тільки width * 4 .

Нарешті, він запитує кількість бітів на піксель. На 8 біт на компонент і 4 компонентів, це всього лише 32 .

Тепер у нас є NSBitmapImageRep з форматом ми хочемо, але як же ми малюємо в нього? Перший крок , щоб зробити NSGraphicsContext з ним:

        NSGraphicsContext *ctx = [NSGraphicsContext graphicsContextWithBitmapImageRep: rep];

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

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

        [NSGraphicsContext saveGraphicsState];
        [NSGraphicsContext setCurrentContext: ctx];

На даний момент, будь-який малюнок ми будемо вдаватися в нашій новоявленої NSBitmapImageRep . Наступним кроком є ​​просто малювати зображення.

        [image drawAtPoint: NSZeroPoint fromRect: NSZeroRect operation: NSCompositeCopy fraction: 1.0];

NSZeroRect це просто зручний ярлик , який говорить NSImage намалювати все зображення.

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

        [ctx flushGraphics];
        [NSGraphicsContext restoreGraphicsState];

        return rep;
    }

Використовуючи цю техніку, ви можете отримати що - небудь , що какао здатний зробити в зручний 32 бітове RGBA растрового зображення.

Інтерпретація даних пікселів
Тепер, коли у нас є дані пікселя, що ми з ним робити? Точно що з цим робити залежно від вас, але давайте подивимося, як насправді потрапити на дані пікселів.

Почнемо з визначення структури для представлення окремих пікселів:

    struct Pixel { uint8_t r, g, b, a; };

Це буде вибудовуватися з даними RGBA пікселів , що зберігаються в NSBitmapImageRep . Ми можемо захопити вказівник з нього для використання:

    struct Pixel *pixels = (struct Pixel *)[rep bitmapData];

Доступ до конкретної піксель (x, y) працює так само , як і в попередньому прикладі коду для зображень в відтінках сірого:

    int index = x + y * width;
    NSLog(@"Pixel at %d, %d: R=%u G=%u B=%u A=%u",
          x, y
          pixels[index].r,
          pixels[index].g,
          pixels[index].b,
          pixels[index].a);

Переконайтеся , що x і y знаходяться в межах зображень , перш ніж робити це, або ж веселі результати можуть наступити. Якщо вам пощастить, координати поза межі будуть аварійно завершені.

Щоб переглянути всі пікселі на зображенні, виконайте просту пару циклів:

    for(int y = 0; y < height; y++)
        for(int x = 0; x < width; x++)
        {
            int index = x + y * width;
            // Use pixels[index] here
        }

Зверніть увагу на те, як y петлі зовнішній один, навіть якщо x перший буде природний порядок. Це тому, що він набагато швидше ітератує пікселі в тому ж порядку, що вони зберігаються в пам'яті, так що суміжні пікселі доступні послідовно. Вважаючи x на внутрішній стороні робить це, і отриманий код набагато більш дружелюбним до кеш - пам'яті і контролерам , які побудовані для обробки послідовного доступу.

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

    struct Pixel *cursor = pixels;
    for(int y = 0; y < height; y++)
        for(int x = 0; x < width; x++)
        {
            // Use cursor->r, cursor->g, etc.
            cursor++;
        }

Нарешті, слід зазначити , що ці дані мінливе. Якщо ви повинні таке бажання, ви можете змінити r, g, b, і та a NSBitmapImageRep буде відображати зміни.

Висновок
Робота з сировими піксельними даними - це не те, що вам зазвичай потрібно робити, але якщо вам це потрібно, Какао робить це відносно легко. Техніка трохи обхідним, але малюнок в NSBitmapImageRep з обраним форматом пікселів, ви можете отримати піксельні дані в форматі вашого вибору. Після того, як ви отримаєте ці піксельні дані, просте питання індексації в нього, щоб отримати окремі значення пікселів.

Ось це і сьогодні! П'ятниця Q & А рухає читач ідеї , як завжди, так що якщо у вас є якісь - або пропозиції по темам , які ви хотіли б бачити покриті майбутню партію, будь ласка , надішліть їх .

Вам сподобалася ця стаття? Я продаю повну книгу з них. Доступно для iBooks та Kindle, а також пряме завантаження у форматі PDF і EPUB. Він також доступний у паперовому вигляді для старомодних. Натисніть тут для отримання додаткової інформації .