Fredag Q&A 31-08-2012: Få fram og tolke bilde data
Cocoa kommer med noen gode abtraksjoner for å arbeide med bilder. NSImage lar deg behandle et bilde som ugjennomsiktlig boble som du kan trekke hvor du vil. Bilde kjerner slår sammen en rekke med bilde prosessering i en brukervennlig API som lar deg slippe å bekymre deg for hvordan indiviudelle bilder er representert. Uansett, noen ganger ønsker du bare å komme til kjerne pixel data koden. Scott Luther kom med forslaget til dagens tema: å hente og manipulere denne kjerne pixel dataen.
Teorien
Den enkleste bilde framstillingen er et flatt bitmap. Dette er en tabell av bits, en per pixel, som indikerer hvor vidt det er svart eller hvitt. Tabellen inneholder rader av piksler etter hver, slik at det totale antallet bits er tilsvarende med bredden av bilde multiplisert med høyden. Her følger et eksempel på et smilefjes:
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
Rent sort og hvitt er ikke et veldig utrykksfult virkemiddel, selvsagt, og det å aksessere individuelle bits i en tabell er en liten utfordring. La oss forflytte oss et steg oppover og bruke en byte per piksel, som tillater bruk av gråfarge (vi kan la 0 være sort, 255 hvit, og nummerene i mellom ulike grader av gråhet) og gjøre det enklere å aksessere elementene i tillegg.
Nok en gang, vi bruker en tabell med bytes med sekvensielle rader. Her er noe eksempel kode for å tildele minne for bildet:
uint8_t *AllocateImage(int width, int height)
{
return malloc(width * height);
}
For å få en viss piksel ved (x, y), må vi forflytte oss ned y rader, og langs den raden med x piksler. Siden radene blir lagt ut sekvensielt, forflytter vi oss ned y rader ved å bevege oss y * bredden bytes gjennom tabellen. Indeksen for en viss piksel er dermed x + y * bredden. Basert på dette, her er to funksjoner for å få og å angi en gråfarge piksel ved en viss koordinat:
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;
}
Gråfargen er ikke så interesann i de fleste tilfeller, og vi ønsker å være i stand til å representere farge. Den typiske måten å representere fargede piksler på er ved en kombinasjon av 3 verdier for røde, grønne og blåe komponenter. Alle nuller resulterer i sort, mens andre verdier ved å blande de tre fargene sammen for å forme hvilke farge som trengs. Det er vanlig å benytte 8 bits per farge, som resulterer i 24 bits per piksel. Noen ganger er disse pakket sammen, og noen ganger er de fylt med 8 bits som et tomt tillegg for å få 32 bits per piksel, som er enklere å jobbe med etter som datamaskiner normalt sett er gode til å manipulere 32-bit verdier.
Gjennomsiktighet, eller alfa, kan også være praktisk å representere i et bilde. 8 bits med gjennomsiktighet passer flott inn i de 8 bitsene med fyll i en 32 bits piksel, og å benytte 32 bit med piksel for å ha rød, grønn, blå, og alfa er sannsynligvis det mest vanlige piksel formatet i bruk.
Det er to måter å pakke disse pikslene sammen. Den vanlige måten er å bare kjøre dem sammen i sekvens, så du ville ha en byte av rød, en byte av grønn, en byte av blå, og en byte av alfa ved siden av hverandre. Så ville du hatt rød, grønn, blå, og alfa for neste piksel, og så videre. Hver piksel tar opp fire bytes av sammenhengende minne.

Det er også mulig å lagre hver farge i en seperat tykk skive av minne. Hver tykk skive kalles et plan og dette formatet kalles "planar". I dette tilfellet, har du essensielt tre eller fire (avhengig av hvor vidt alfa er representert) regioner av minne, som hver er lagt ut nøyaktig som pikslene i gråfarge eksempelet over. Piksel fargen er en kombinasjon av verdiene til alle planene. Dette kan noen gang være mer passende å arbeide med, men er ofte saktere, på grunn av dårlig referanse lokasjon, og ofte mer komplekst å arbeide med, så det er mye mindre vanlig format.

Den eneste andre tingen å finne ut er hvordan fargene er ordnet. RGBA (rød, grønn, blå, så alfa) ordning er den mest vanlige på Mac, men ordninger som ARGB og BGRA dukker opp noen ganger også. Det er ingen spesiell grunn for å velge en framfor en annen, annet enn kompabilitet og fart. For å unnga kostbare format konverteringer, er det best å finne det formatet som brukes til det du skal tegne til, lagre til, eller laste fra, når mulig.
Å hente piksel data
Cocoa klassen som inneholder og sørger for piksel data er NSBitmapImageRep. Dette er en underklasse av NSImageRep, som er en abstrakt klasse for en enkelt "representasjon" av et bilde.NSImage er en beholder for en eller flere NSImageRep instanser. I tilfellet hvor det er mere enn en representant, kan de gjerne representere ulike størrelser, oppløsninger, farge områder, etc., og NSImage vil velge den beste for den aktuelle konteksten for tegning.

Med det som forbehold, virker det som det skulle bli temmelig enkelt å få bilde dataen fra et NSImage: finn en NSBitmapImageRep i dets representasjon, og spør så den representasjonen for dets piksel data.

Det er to problemer med dette. For det første, bilde har kanskje ikke en NSBitmapImageRep i det hele tatt. Det finnes representasjoner som ikke er bitmap. For eksempel, et NSImage som representerer PDF vil inneholder vektor data, ikke bitmap data, og benytte en annen type bilde representasjon. For det andre, selv om bilde har en NSBitmapImageRep, er det ikke noe informasjon om hva piksel formatet av den representasjonen vil bli. Det er ikke praktisk å skrive kode for å håndtere ethvert mulig piksel format, særlig da det ville bli vanskelig å prøve ut de fleste av tilfellene.

Det finnes mye kode der ute som gjør det på denne måten. Man kommer seg unna med det ved å gjøre seg antakelser om innholdet av NSImage og piksel formatet til NSBitmapImageRep. Dette er ikke til å stole på, og bør unngås.

Hvordan får man så piksel data på en pålitelig måte ? Du kan tegne et NSImage på en pålitelig måte, og du kan tegne til en NSBitmapImageRep ved å benytte en NSGraphicsContext klasse, og du kan få piksel data fra den NSBitmapImageRep. Føy så alt sammen, og du kan få piksel data.

Her kommer noe kode for å håndtere denne sekvensen. Den første tingen den gjør er å finne ut piksel bredden og høyden til bitmap representasjonen. Dette er ikke nødvendigvis åpenpart, da en NSImage's størrelse ikke må korrespondere med piksel dimensjoner. Koden vil uansett bruke størrelse, men avhengig av situasjonen din, så vil du kanskje benytte en annen måte å finne ut størrelsen:
NSBitmapImageRep *ImageRepFromImage(NSImage *image)
{
int width = [image size].width;
int height = [image size].height;
if(width < 1 || height < 1)
return nil;
Deretter, oppretter vi NSBitmapImageRep. Dette involverer bruken av en virkelig lang oppstarts metode som ser noe skremmende ut, men jeg skal gå i gjennom alle parameterene i detalj:
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]
La oss se på disse parameterne en etter en. Det første argumentetBitmapDataPlanes, gir deg mulighet til å spesifisere minnet hvor piksel dataen vil bli lagret. Å sette inn NULL her, som denne koden gjør, gir beskjed til NSBitmapImageRep å til ordne dens egen minne internt, som vanligvis er den mest passende måten å håndtere dette på.
Deretter, spesifiserer koden antall piksler i bredden og høyden, som er databehandlet tidligere. Det bare tar imot de verdiene forpikselvidde og pikselhøyde.
Nå begynner vi å gå inn i det faktiske piksel formatet. Jeg nevnte tidligere at 32-bit RGBA (hvor rød, grønn og blå og alfa hver tar opp en byte og er lagt ut sammenhengende i minne) er et vanlig piksel format, og det er det vi skal benytte. Da hvert eksemplar er en byte, så setter koden 8 for bitsPerSample:. Parameteret samplesPerPixel: refererer til antallet ulike komponenter som er brukt i bildet. Vi har 4 komponenter (R, G, B og A) og koden tar dermed 4 her.
RGBA formatet har alfa, så vi angir YES for hasAlpha. Vi ønsker ikke et planar format, så vi angir NO for isPlanar. Vi ønsker en RGB farge rom, så vi angir NSCalibratedRGBColorSpace.

Deretter, vil NSBitmapImageRep vite hvor mange bytes som tar opp hver rad av bildet. Dette er benyttet i fall oppfylling er ønsket. Noen ganger benytter en bilde rekke mer enn det som strengt tatt er minimum antall bytes, normalt for ytelses grunner, for å holde ting pent oppstilt. Vi ønsker ikke å rote det til med oppfylling, så vi angir det minimume antallet bytes som som trengs for en rad av piksler, som er ganske enkelt bredden * 4.

Til slutt, spørs det etter antallet av bits per pixel. Ved 8 bits per komponent og 4 komponenter, dette er bare 32.

Nå har vi NSBitmapImageRep med formatet vi ønsker, men hvordan tegner vi inn det ? Det første steget er å lage en NSGraphicsContext med det:
NSGraphicsContext *ctx = [NSGraphicsContext graphicsContextWithBitmapImageRep: rep];
En viktig merknad når man feilsøker: ikke alle parametre for et NSBitmapImageRep er akseptable når man oppretter en NSGraphicsContext. Om denne linjen klager over et ikke støttet format, så betyr det at en av parameterne som ble benyttet til å opprette NSBitmapImageRep ikke var slik systemet likte, så gå tilbake og dobbeltsjekk disse.

Det neste steget er å sette denne konteksten som den nåværende grafiske konteksten. For å være sikker på at vi ikke roter til noen andre grafiske aktiviteter som kan være i gang, så lagrer vi først den nåværende grafiske tilstanden, slik at vi kan gjenopprette det senere:
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext: ctx];
På dette stadiet, så vil all tegning vi gjør gå inn i vår nylige opprettede NSBitmapImageRep. Den neste steget er enkelt og greit tegnet bilde:
[image drawAtPoint: NSZeroPoint fromRect: NSZeroRect operation: NSCompositeCopy fraction: 1.0];
NSZeroRect er simpel then en passende snarvei som forteller at NSImage skal tegne hele bildet.

Nå som bildet er tegnet, så spyler vi ut det grafiske innholdet for å forsikre oss om at ingen ting av dette fremdeles står i kø, gjenoppretter den grafiske tilstanden, og returnerer dette bitmapet:
[ctx flushGraphics];
[NSGraphicsContext restoreGraphicsState];

return rep;
}
Ved å benytte denne teknikken, så kan du få hva som helst som Cocoa er i stand til å tegne inn en passende 32-bit RGBA bitmap.
Å tolke Pixel Data
Nå som vi har piksel dataen, hva gjør vi så med denne ? Nøyaktig hva du vil gjøre med den er opp til deg, men la oss se på hvordan vi faktisk skal komme til piksel dataen.

La oss starte ved å definere en struktur for å representere en individuell piksel:
struct Pixel { uint8_t r, g, b, a; };
Dette vil stilles opp med RGBA piksel dataen lagret i NSBitmapImageRep. Vi kan dra ut en peker av det for å benytte:
struct Pixel *pixels = (struct Pixel *)[rep bitmapData];
Å aksessere en spesifikk piksel ved (X, Y) fungerer akkurat som det forrige eksempelet for gråfarge bilder:
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);
Pass på at x og y er plassert innenfor bilde verdi grensene før du gjør dette, ellers kan veldig morsomme resultater følge. Om du er heldig vil overskredet grenseverdi koordinater kanskje kræsje.

For å gå gjennom alle pikslene i et bilde vil et par enkle looper gjøre nytten:
for(int y = 0; y < height; y++)
for(int x = 0; x < width; x++)
{
int index = x + y * width;
// Use pixels[index] here
}
Legg merke til hvordan y loopen er det ytterste, selv om x først ville være den naturlige rekkefølgen. Dette er fordi det er mye raskere å gå gjennom pikslene i samme rekkefølgen som de er lagret i minne i, slik at de tilstøtende pikslene er aksessert sekvensielt. Å putte x på innsiden gjør dette, og den resulterende koden er mye mer behageligere for hurtigminnet og minnekontrollerne som er laget for å håndtere sekvensiell aksess.

En moderne kompilator vil sannsynligvis generere god kode for det ovenfor, men i tilfelle du er paranoid og vil forsikre deg om at kompilatoren ikke vil multiplisere og lage en tabell indeks for hver loop gjentakelse, så kan du iterere ved å benytte en aritmetisk peker istedet:
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++;
}
Til slutt, merk at denne dataen er formerbar. Om du skulle ønske det, så kan du faktisk modifisere r, g, b, og a, og NSBitmapImageRep vil reflektere disse forandringene.
Konklusjon
Å drive med kjerne data piksler er ikke noe vi normalt må gjøre, men om du trenger det, så gjør Cocoa det relativt enkelt. Teknikkene er litt karusell, men ved å tegne inn i en NSBitmapImageRep med et valgt piksel format, så kan du få piksel data i det formatet du velger deg. Med det samme du har den piksel dataen, er det kun en enkelt spørsmål om å indeksere inn i det for å hente de individuelle piksel verdiene.

Det var alt for i dag! Fredags Q&A er som vanlig drevet av leser ideer, så om du har noen forslag til temaer som du kunne likt å se dekket i et framtidig program, vennlist send dem inn.
Likte du denne artikkelen? Jeg selger hele bøker fulle av dem! Volum II og III er nå ute! De er tilgjengelige som ePub, PDF, trykket skrift, og på iBooks og Kindle. Trykk her for mer informasjon.