V pátek Q&A 2012-08-31: Získání a Interpretace Obrazových Dat

link: https://mikeash.com/pyblog/friday-qa-2012-08-31-obtaining-and-interpreting-image-data.html

Od Mike Ash  

Kakao poskytuje některé skvělé abstrakce pro práci s obrázky. NSImageUmožňuje zacházet s obrázkem jako s neprůhledným blobem, který můžete kreslit pouze tam, kde ho chcete. Core Image obklopuje mnoho zpracování obrazu v snadno použitelném rozhraní API, které vás osvobozuje od obav o tom, jak jednotlivé pixely jsou zastoupeny. Někdy se ovšem opravdu jen chcete dostat na surové pixelové údaje v kódu. Scott Luther navrhl dnešní téma: načtení a manipulaci se surovinovými obrazovými daty.

Teorie
Nejjednodušší zobrazení obrázku je obyčejný rastrový obrázek. Jedná se o řadu bitů, jeden na pixel, což znamená, zda je černá nebo bílá. Pole obsahuje řady pixelů za sebou, takže celkový počet bitů se rovná šířce obrazu vynásobenému výškou. Zde je příklad bitmapy smajlíka:

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

Čistá černá a bílá není samozřejmě velmi výrazné médium a přístup k jednotlivým bitům v poli je trochu hádka. Pojďme posunout krok k použití jednoho bajtu na pixel, který dovoluje stupně šedi (můžeme mít nulu černou, 255bílou a čísla mezi různými odstíny šedé) a usnadňuje přístup k prvkům také.

Opět použijeme množinu bajtů s po sobě jdoucími řádky. Zde je příklad kódu pro přidělení paměti pro obrázek:

 Uint8_t  * AllocateImage ( int  width ,  int  height ) 
    { 
        return  malloc (šířka * výška ); 
    }

Chcete-li se dostat k určitému pixelu (x, y), musíme přesunout yřádky dolů a potom přes tento řádek o xpixely. Vzhledem k tomu, že jsou řádky uspořádány postupně, přesouváme yřádky přesunem pole po y * widthbajtech. Index pro konkrétní pixel je pak x + y * width. Na základě toho jsou zde dvě funkce pro získání a nastavení pixelu ve stupních šedi v určité souřadnici:

 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  * šířka; 
        Image [ index ]  = význam ; 
    }

Stupně šedi stále nejsou v mnoha případech zajímavé a my chceme být schopni reprezentovat barvu. Typický způsob zobrazení barevných pixelů je kombinace tří hodnot pro červené, zelené a modré komponenty. Všechna nula vedou k černé barvě, přičemž další hodnoty míchají tři barvy dohromady, aby vytvořily jakoukoliv barvu. Je typické používat 8bity na jednu barvu, což vede k 24bitům na pixel. Někdy jsou nabité dohromady a někdy jsou vycpané dalšími 8kouskami prázdnoty, aby dávaly 32bitům na pixel, což je lepší pracovat, protože počítače jsou obvykle dobré při manipulaci s 32bitovými hodnotami.

Transparentnost, nebo alfa, může být také užitečná, aby reprezentovala obraz. 8Bity průhlednosti se skvěle hodí do 8bitů polstrování v 32bitovém pixelu a pomocí 32bitových pixelů držících červenou, zelenou, modrou a alfa je pravděpodobně nejběžnější formát pixelů, který se právě používá.

Existují dva způsoby, jak tyto pixely spojit dohromady. Spolehlivá cesta je, že je všechny spouští dohromady, takže byste měli jeden bajt červeného, jeden byte zeleně, jeden byte modré a jeden byte alfa vedle sebe. Pak budete mít červený, zelený, modrý a alfa pro další pixel a tak dále. Každý pixel zaujímá čtyři bajty souvislé paměti.

Je také možné ukládat jednotlivé barvy do samostatného bloku paměti. Každý kus se nazývá rovina a tento formát se nazývá „planární“. V tomto případě máte v podstatě tři nebo čtyři (v závislosti na tom, zda je alfa přítomen) oblasti paměti, z nichž každá je umístěna přesně jako pixely z příkladu stupňů šedi shora. Barva pixelů je kombinací hodnot ze všech rovin. Někdy to může být mnohem pohodlnější, ale je často pomalejší, kvůli špatné referenční lokalitě a často je složitější pracovat, takže je to mnohem méně obvyklý formát.

Jediná věc, kterou je třeba zjistit, je, jak jsou barvy uspořádány. RGBA (červená, zelená, modrá, pak alfa) je nejčastější objednávka na Macu, ale objednávky jako ARGB a BGRA se občas zobrazují také. Neexistuje žádný zvláštní důvod, proč si vybrat jiný než jiný než kompatibilita nebo rychlost. Chcete-li se vyhnout nákladným konverzi formátů, je nejlepší, aby odpovídal formátu použitému všem, na které budete kreslit, ukládat nebo načíst, pokud je to možné.

Získání obrazových dat
Třída kakaa, která obsahuje a poskytuje obrazová data, je NSBitmapImageRep. Toto je podtřída NSImageRep, která je abstraktní třídou pro jediné „reprezentaci“ obrazu. NSImageJe kontejner pro jednu nebo více NSImageRepinstancí. V případě, že existuje více než jedna reprezentace, mohou reprezentovat různé velikosti, rozlišení, barevné prostory apod. A NSImagepři kreslení zvolí to nejlepší pro aktuální kontext.

Vzhledem k tomu, že se zdá, že by mělo být velmi snadné získat obrazová data z NSImage: najděte jej NSBitmapImageRepv jeho reprezentacích a pak se zeptejte, že reprezentace pro jeho pixel data.

Existují dva problémy s tím. Za prvé, obraz nemusí mít NSBitmapImageRepvůbec. Existují typy reprezentace, které nejsou bitmapami. Například NSImagereprezentace PDF bude obsahovat vektorová data, nikoli bitmapová data, a použít jiný typ reprezentace obrazu. Zadruhé, i když má obrázek nějaký obraz NSBitmapImageRep, není možné říci, jaký bude pixelový formát této reprezentace. Není praktické psát kód pro zpracování všech možných formátů pixelů, zejména proto, že většinu případů bude těžké otestovat.

Je tam spousta kódu, který to stejně dělá. Odchází to tím, že předkládá předpoklady o obsahu NSImagea pixelovém formátu NSBitmapImageRep. To není spolehlivé a je třeba se vyhnout.

Jak se vám spolehlivě získat data obrazových bodů, pak? Můžete čerpatNSImage spolehlivě, a můžete čerpat doNSBitmapImageRep používat NSGraphicsContexttřídu, a můžete získat data obrazových bodů z které NSBitmapImageRep. Řetězejte to dohromady a můžete získat pixelová data.

Zde je nějaký kód pro zpracování této sekvence. První věc, kterou dělá, je zjistit šířku a výšku pixelů bitmapové reprezentace. Je to není nutně zřejmé, jak NSImageto sizenemusí odpovídat rozměry v obrazových bodech. Tento kód bude sizestejně používat , avšak v závislosti na vaší situaci, můžete použít jiný způsob, jak zjistit velikost:

NSBitmapImageRep  * ImageRepFromImage ( NSImage  * image ) 
    { 
        int  width  =  [velikost obrázku ]. Šířka; Int height = [ velikost obrázku].Výška; If ( width < 1 || height < 1 ) return nil ;

Dále vytvoříme NSBitmapImageRep. To zahrnuje použití skutečně dlouhé inicializační metody, která vypadá trochu děsivě, ale projdu podrobně všechny parametry:

        NSBitmapImageRep  * rep  =  [[ NSBitmapImageRep  Alloc ] 
                                 initWithBitmapDataPlanes:  NULL 
                                 pixelsWide:  šířka 
                                 pixelsHigh:  výška 
                                 bitsPerSample:  8 
                                 samplesPerPixel:  4 
                                 hasAlpha:  ANO 
                                 isPlanar:  NE 
                                 colorSpaceName:  NSCalibratedRGBColorSpace 
                                 bytesPerRow:  šířka  *  4 
                                 bitsPerPixel:  32 ]

Podívejme se na tyto parametry jeden po druhém. První argument,, BitmapDataPlanesumožňuje zadat paměť, kde budou data pixelů uložena. Při předávání NULL, jak tento kód dělá, říká, NSBitmapImageRepže má vlastní paměť přidělit interně, což je obvykle nejvhodnější způsob, jak to zvládnout.

Dále kód určuje počet pixelů široký a vysoký, který předtím vypočítal. Prostě prochází tyto hodnoty ve pro pixelsWidepixelsHigh.

Teď se začneme dostat do skutečného pixelového formátu. Již dříve jsem se zmínil o tom, že 32bitová RGBA (kde červená, zelená, modrá a alfa každá zabírají jeden byte a jsou rozložena souvisle v paměti) je společný pixelový formát a to je to, co budeme používat. Vzhledem k tomu, každý vzorek je jeden bajt, kód projde 8pro bitsPerSample:samplesPerPixel:Parametr se vztahuje k počtu různých složek použitých v obraze. Máme čtyři komponenty (R, G, B a A), takže kód prochází 4tady.

Formát RGBA má alfa, takže míjíme YESpro hasAlpha. Nechceme plošný formát, takže NOpro isPlanar předáme. Chceme barevný prostor RGB, takže procházíme NSCalibratedRGBColorSpace.

Dále NSBitmapImageRepchce vědět, kolik bytů tvoří každý řádek obrazu. To se používá v případě, že je požadováno vycpávky. Někdy řádek obrázků používá více než striktně minimální počet bajtů, obvykle z důvodů výkonu, aby se věci dobře přizpůsobily. Nechceme se obtěžovat s polstrováním, a proto předáváme minimální počet bajtů potřebných pro jeden řádek pixelů, což je správné width * 4.

Konečně se zeptá na počet bitů na pixel. U 8bitů na komponent a 4 komponent je to jenom 32.

Máme nyní NSBitmapImageRepformu, kterou chceme, ale jak se k tomu dostaneme? Prvním krokem je udělat NSGraphicsContexts ním:

NSGraphicsContext  * ctx  =  [ NSGraphicsContext  graphicsContextWithBitmapImageRep :  rep ];

Důležitá poznámka při odstraňování problémů: ne všechny parametry pro jeden NSBitmapImageRepjsou přijatelné při vytváření NSGraphicsContext. Pokud tento řádek stěžuje na nepodporovaný formát, znamená to, že jeden z parametrů použitých při vytváření NSBitmapImageRepnebyl podle systémových možností, takže se vraťte a zkontrolujte je.

Dalším krokem je nastavení tohoto kontextu jako aktuálního grafického kontextu. Abychom se ujistili, že nemáme problémy s jinou grafickou aktivitou, která by se mohla dělat, nejprve uložíme aktuální stav grafiky, abychom jej mohli později obnovit:

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

V tomto okamžiku bude každá kresba, kterou uděláme, do naší nově vyřezané NSBitmapImageRep. Dalším krokem je jednoduché kreslení obrázku.

        [ Obrázek  drawAtPoint :  NSZeroPoint  fromRect :  NSZeroRect  operace :  NSCompositeCopy  frakce :  1,0 ];

NSZeroRectJe jednoduše pohodlná klávesová zkratka, která říká, NSImageže nakreslí celý obraz.

Nyní, když je obrázek nakreslen, vyprázdníme grafický kontext, abychom zajistili, že žádná z těchto věcí není stále ve frontě, obnoví stav grafiky a nevrátí bitmapu:

 [ CTX  flushGraphics ]; 
        [ NSGraphicsContext  restoreGraphicsState ]; 

        Return  rep ; 
    }

Pomocí této techniky můžete získat vše , co Cocoa dokáže kreslit do praktické 32bitové RGBA bitmapy.

Interpretace pixelových dat
Nyní, když máme pixelová data, co s nimi děláme ? Přesně to, co s tím dělat, je na vás, ale podíváme se, jak se skutečně dostat k datům pixelů.

Začněme tím, že definujeme strukturu, která reprezentuje jednotlivé pixely:

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

To se vyrovná s daty RGBA pixelů uloženými v adresáři NSBitmapImageRep. Můžeme z něj vyndat ukazatel a použít:

    Struktura  pixelu  * pixels  =  ( struktura  pixelu  * ) [ rep  bitmapData ];

Přístup k určitému pixelu (x, y)funguje stejně jako předchozí kód pro obrázky v odstínech šedi:

 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 , 
          pixely [ index ]. a );

Ujistěte se, že xyjsou umístěny v obrazových mezích, než dělat to, jinak povedené výsledky mohou následovat. Máte-li štěstí, dojde k selhání souřadnic mimo hranice.

Chcete-li iterovat přes všechny pixely v obraze, provede se jednoduchý pár pro smyčky:

    pro ( int  y  =  0 ,  y  <  výšky ;  y ++ ) 
        pro ( int  x  =  0 ;  x  <  šířka ;  x ++ ) 
        { 
            int  index  =  x  +  y  *  šířky ; 
            // Pomocí pixelů [index] zde 
        }

Všimněte si, jak je ysmyčka nejvzdálenější, přestože xprvní by byl přirozený pořádek. Je to proto, že je mnohem rychlejší opakování pixelů ve stejném pořadí, v jakém jsou uloženy v paměti, takže přístupové pixely jsou postupně přístupné. Uvedení xna vnitřní straně to udělá a výsledný kód je mnohem přívětivější pro vyrovnávací paměti a paměťové řadiče, které jsou postaveny tak, aby zvládly sekvenční přístup.

Moderní kompilátor pravděpodobně vygeneruje dobrý kód pro výše uvedené, ale v případě, že jste paranoidní a chcete se ujistit, že kompilátor nebude generovat násobek a index pole pro každou iteraci smyčky, můžete iterovat pomocí ukazatele aritmetiky místo:

    Struct  pixel  * kurzor  =  pixely ; 
    Pro ( int  y  =  0 ;  y  <  výška ;  y ++ ) 
        pro ( int  x  =  0 ;  x  <  width ;  x ++ ) 
        { 
            // Použijte kurzor-> r, kurzor-> g atd. 
            Kurzor ++ ; 
        }

Nakonec si všimněte, že tato data jsou měnitelná . Pokud byste si to přejí, můžete skutečně změnit rgba, a NSBitmapImageRepbude odrážet změny.

Závěr
Jednání se surovými pixelovými daty není něco, co obvykle musíte dělat, ale pokud to potřebujete, kakao je poměrně snadné. Technika je trochu kruhový objezd, ale kreslením do NSBitmapImageRepzvoleného pixelového formátu můžete získat pixelová data ve formátu dle vašeho výběru. Jakmile získáte data pixelů, je to jednoduchá záležitost indexování do ní, aby se získaly jednotlivé pixelové hodnoty.

To je dnes! Pátek a čtvrtek jsou motivovány nápady čtenářů jako vždy, takže pokud máte nějaké návrhy ohledně témat, které byste chtěli vidět v budoucí splátce, pošlete je prosím .

Líbí se vám tento článek? Prodávám celou knihu plnou z nich. Je k dispozici pro iBooks a Kindle, a to přímo ve formátu PDF a ePub. Je k dispozici také v papíře pro staromódní. Klikněte zde pro více informací .