Hoofdstuk 11 Databestanden en Bitmaps Databestanden kunnen op heel veel verschillende manieren zijn gecodeerd. Bijvoorbeeld, meetwaarden kunnen allemaal gesorteerd staan per experimentele variabele, per meetsessie, of anderszins. Soms zijn de datafiles eenvoudig leesbaar, bijvoorbeeld 3 getallen per regel die in een bepaald format zijn weggeschreven en onderling gescheiden zijn door komma's. Soms zijn de data weggeschreven in een zogenaamde binaire file, die vaak alleen maar geï nterpreteerd kunnen worden door een bepaald type applicatie. Voor plaatjes gecodeerd in GIF, JPG of BMP formaat gebruik je in het algemeen een viewer. Het is geen enkel probleem om zo'n bestand in te lezen, en correct te interpreteren als je maar weet wat de structuur van de file is! We zullen in dit hoofdstuk zo'n geval aanpakken, namelijk het geschikt maken voor numerieke bewerking van data in een speciaal type binary file waarin bijv. een digitale foto is opgeslagen. Het formaat dat we bespreken is de bitmap, maar dat is eigenlijk niet zo relevant. Bitmap bestanden worden in het algemeen voorzien van de extensie ‘ .BMP’ Natuurlijk . zijn er andere en meestal ook efficië ntere manieren om foto's of tekeningen te coderen, maar daar gaat het in dit hoofdstuk niet om. In dit hoofdstuk leer je hoe je zo'n bitmapbestand kunt inlezen en interpreteren in Mathematica en hoe je de inhoud geschikt kunt maken voor analyse. Met deze kennis moet je in principe in staat zijn ook data die met elk ander coderingssysteem zijn opgeslagen, geschikt te maken voor analyse. In dit hoofdstuk is voor de bitmapcodering gekozen omdat dit een vrij inzichtelijk coderingssysteem is. Tevens kun je de opgedane kennis uit vorige hoofdstukken goed oefenen.
11.1 Inleiding en achtergronden Bitmaps In een bitmap is elk "punt" van een plaatje apart gecodeerd. Zo bestaat een digitale foto uit een aantal rijen en een aantal kolommen. We zouden van een nm matrix kunnen spreken. Elk element uit die matrix noemen we een beeldpunt ofwel een pixel. Elk pixel is in de bitmap met één getal weergegeven. Al die getallen vormen een lange rij met "datapunten", die een plaatje doorlopen van links naar rechts, en van beneden naar boven. Een pixel kan met verschillende nauwkeurigheden beschreven worden. De codering die het minste ruimte vergt is een codering waarbij voor elk pixel maar één (1) bit (1/8 byte, 1 byte = 8 bits) wordt gebruikt. Eén bit kan maar twee waarden aannemen, aan of uit. Het beeldpunt kan dus ook alleen maar aan of uit zijn; kleur informatie is dus niet aanwezig: de foto is monochroom en de pixels zijn dichotoom (aan/uit). Het hieronder besproken voorbeeld is monochroom blauw. Een meer complexe codering is een codering met drie bytes per pixel (24 bits). Voor elk pixel is één byte (28 = 256 waarden) voor Rood, één byte voor Groen en één byte voor Blauw. Met deze drie bytes kan een beeldpunt 256 * 256 * 256 = 16.8 miljoen verschillende waarden aannemen. Men spreekt dan wel van 16.8 miljoen kleuren, hoewel daarin ook een aantal dezelfde kleuren maar met verschillende intensiteit in zitten. 11.1.1 De filestructuur De header van een ‘ .BMP’bestand is opgebouwd volgens een vast patroon. In onderstaande tabel staat welke posities in de bitmapfile gereserveerd zijn voor de informatie die nodig is om de bitmap goed te coderen en hoeveel bytes deze plaatsen in beslag nemen. Header general: variabele Signature Filesize Reserved DataOffset Infoheader: InfoSize Width Height Planes BitCount
Compression
lengte (bytes) 2 4 4 4
beginpositie 0 2 6 10
beschrijving "BM" lengte van het bestand (bytes) n.v.t. beginpositie bitmapdata
4 4 4 2 2
14 18 22 26 28
4
30
grootte van de infoheader (bytes) breedte bitmap (pixels) hoogte bitmap (pixels) n.v.t. aantal bits per pixel: 1: NumColors = 2 4: NumColors = 16 CP-cursus, 2007-2008 8: NumColors = 256 24: NumColors = 16.8 miljoen type compressie; 0 = niet gecomprimeerd)
142
Hoofdstuk11.nb
Header general: variabele Signature Filesize Reserved DataOffset Infoheader: InfoSize Width Height Planes BitCount
Compression ImageSize XPixelsPerM YPixelsPerM ColorsUsed ColorsImportant ColorIndex:
lengte (bytes) 2 4 4 4
beginpositie 0 2 6 10
beschrijving "BM" lengte van het bestand (bytes) n.v.t. beginpositie bitmapdata
4 4 4 2 2
14 18 22 26 28
4 4 4 4 4 4
30 34 38 42 46 50
grootte van de infoheader (bytes) breedte bitmap (pixels) hoogte bitmap (pixels) n.v.t. aantal bits per pixel: 1: NumColors = 2 4: NumColors = 16 8: NumColors = 256 24: NumColors = 16.8 miljoen type compressie; 0 = niet gecomprimeerd) grootte van bitmap (bytes) horizontale resolutie (pixels / meter) verticale resolutie (pixels / meter) aantal kleuren dat echt wordt gebruikt aantal belangrijke kleuren
4
54
4 ... 4
specificatie eerste kleur (met index 0) R-G-B-waarden van elke kleur, tussen 0 en 255, staan in het derde, tweede en eerste byte, resp! 58 specificatie tweede kleur (met index 1) ... ... 54 + 4*ColorsUsed specificatie laatste kleur (met index ColorsUsed-1) Tabel 1 De header van een .BMP file
In ColorIndex wordt vastgelegd welke kleuren precies worden gebruikt van de maximaal 256 256 256 mogelijkheden. De data staat pixel voor pixel van links naar rechts en van beneden naar boven in het datasegment van de bitmapfile dat direct volgt op de header. Afhankelijk van het aantal kleuren in de bitmap neemt elk pixel een vaste hoeveelheid fileruimte in beslag. Dit kan zo weinig zijn als 1 bit (bij 2 kleuren), 4 bits bij 16 kleuren of 1 byte bij 256 kleuren. Eén rij van Width pixels neemt echter altijd een veelvoud van 4 bytes in, zodat soms niet alle bits worden gebruikt voor codering.
11.2 Lezen en interpreteren van een gecodeerde file Met de standaard functie ReadList zouden we in principe heel makkelijk alle bytes in de hele file in één keer kunnen inlezen. We kiezen hier meestal niet voor, omdat er soms characters in één byte verstopt zitten, soms een integer in 2 bytes of soms een integer in 4 bytes. Maar als je de file toch als een reeks bytes inleest krijg je een lijst met bytes (een lijst met gehele getallen tussen 0 en 255). Bijvoorbeeld, voor het bijzonder eenvoudige test bestand "circles.bmp" die op elke Windows computer wel aanwezig is (hoop ik), krijg je zo een lijst van 190 bytes. Alle files die we in deze opgave inlezen staan in één directory op je F-schijf op de practicumcomputers (maar ze staan ook op BlackBoard). Om niet steeds het volledige pad te hoeven opgeven doen we bijv. SetDirectory@"F:DataCpCursus"D filenaam = "circles.bmp";
Nu kunnen we eenvoudig als volgt alle bytes in de file circles.bmp bekijken. Gelukkig zijn dat er niet zo veel.
CP-cursus, 2007-2008
Hoofdstuk11.nb
143
dataInFile = ReadList@filenaam, ByteD 866, 77, 190, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 40, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 128, 0, 0, 0, 19, 11, 0, 0, 19, 11, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 128, 0, 0, 0, 255, 0, 0, 0, 0, 112, 0, 56, 0, 240, 0, 60, 0, 224, 28, 28, 1, 224, 34, 30, 135, 192, 65, 15, 255, 128, 65, 7, 255, 0, 65, 3, 252, 0, 34, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 254, 0, 0, 3, 1, 143, 128, 4, 0, 112, 96, 8, 0, 96, 16, 16, 0, 144, 8, 32, 0, 136, 8, 32, 1, 8, 4, 64, 1, 4, 4, 64, 1, 4, 4, 64, 1, 4, 4, 64, 1, 4, 4, 64, 0, 132, 8, 252, 0, 132, 8, 255, 0, 68, 19, 255, 128, 56, 103, 167, 192, 15, 143, 17, 224, 16, 30, 8, 224, 32, 28, 4, 240, 64, 60, 3, 113, 128, 56, 0, 254, 0, 56, 0, 112, 0, 56<
We kiezen er echter voor om de file in kleine gedeeltes, en dan steeds een relevante naam aan de ingelezen items te geven. Voor we starten met inlezen openen we de file met OpenRead voor lezen. De optie geeft aan dat we een binaire file willen kraken. stream = OpenRead@filenaam, BinaryFormat ® TrueD
[email protected], 352D
Het eerste dat we nu gaan inlezen is 2 maal een byte, die we met de functie FromCharacterCode direct interpreteren als characters. Dat doen we omdat we in de bovenstaande tabel kunnen zien dat de eerste 2 bytes in een bitmap file altijd de hoofdletters "BM" moeten zijn. signatur = ReadList@stream, Byte, 2D FromCharacterCode BM
Dat ziet er goed uit! Het testen of deze string van 2 letters identiek is aan de string "BM" is nu heel eenvoudig. Merk in onderstaande expressie het verschil in betekenis op tussen de operatoren = en . testBMfile = signatur "BM" True
Volgens de tabel zouden er nu in de header van een bitmap file 6 items van 4 bytes volgen. Deze 6 items lezen we in waarbij we ze interpreteren als 4-byte integers (in de header staat n.l. geen enkel floating-point getal). Zo'n getal heeft dus 32 bits en heet daarom in Mathematica type "Integer32". Informatie over dit type en alle mogelike binaire types vind je in de documentatie van de functie BinaryRead of BinaryReadList. Bij het inlezen zit soms een addertje onder het gras. Normaal zou je verwachten dat je in een binair getal de meest significante bytes eerst krijgt, net zoals je een numeriek getal leest van links naar rechts: het belangrijkste cijfer eerst. In deze bitmap file staat echter het minst significante byte vooraan! Je kon dit al zien aan de de hele file-inhoud dataInFile). Het derde byte van de file heeft de waarde 190, de lengte van het bestand. De lengte van het bestand is volgens de tabel echter opgeslagen in byte 3 tot en met byte 6. Als het meest significante byte vooraan had gestaan, dan zou byte 6 dus de waarde 190 hebben, en byte 3, 4 en 5 zouden gelijk aan nul zijn. (Voor de geï nteresseerden: op de practicum PC's is de waarde van de systeem parameter $ByteOrdering gelijk aan -1, dit is in overeenstemming met de wijze waarop de (Windows) bitmapbestanden zijn gecodeerd en derhalve hoef je de waarde van deze parameter niet aan te passen). Dan lezen we nu de volgende zes items van elke 4 bytes. 8filesize, reserved, dataOffset, infoSize, width, height< = BinaryReadList@stream, "Integer32", 6D 8190, 0, 62, 40, 32, 32<
Vervolgens komen er (om duistere redenen) ineens twee items met een lengte van slechts 2 bytes (16 bits). 8planes, bitCount< = BinaryReadList@stream, "Integer16", 2D numColors = 2 ^ bitCount 81, 1< 2
Als er maar 1 bit per pixel wordt gebruikt, kun je maximaal 2 kleuren weergeven. Deze twee kleuren hoeven niet noodzakelijkerwijs zwart en wit te zijn.
CP-cursus, 2007-2008
144
Hoofdstuk11.nb
Als er maar 1 bit per pixel wordt gebruikt, kun je maximaal 2 kleuren weergeven. Deze twee kleuren hoeven niet noodzakelijkerwijs zwart en wit te zijn. Tot slot volgt dan nog een zestal items van elk 4 bytes in de Info header. 8compression , imageSize, xPixelsPerM, yPixelsPerM, colorsUsed, colorsImportant< = BinaryReadList@stream, "Integer32", 6D 80, 128, 2835, 2835, 2, 2<
Hierna volgt de kleurenindex. Elke kleur heeft een index. Als er bijv. 24 bits per pixel worden gebruikt kun je 16.8 miljoen kleuren weergeven. Het is dan niet zo handig om 67.2 MB aan ruimte te verspillen om een triviale kleurenindex op te slaan, en dan heb je nog niet eens de informatie van het plaatje zelf. Dat doe je dus niet! Meestal zitten er maar "een paar" bijv. 256 van de 16.8 miljoen mogelijke kleuren in een plaatje. Dan kun je van elke gebruikte kleur de RGB waarden (3 gehele getallen tussen 0 en 255) opslaan in een index. De bitmap tenslotte is dan niets anders dan een lijst waarin voor elk pixel (rij na rij van linksonder naar rechtsboven) de kleurenindex staat. Het kleur-index getal van een kleur is de positie van die kleur (-1) in de index. De eerste kleur heeft dus (kleuren-)index 0, de tweede index 1, etc. Voor de naam van dat bestand in dit voorbeeld kiezen we rgbWaarden (zie hieronder), maar we lezen nu wel eerst colorIndex in volgens de specificatie in de tabel. colorIndex = BinaryReadList@stream, "Byte" , 4 colorsUsedD 8128, 0, 0, 0, 255, 0, 0, 0<
Ook hier heb je te maken met het feit dat het minst significante van de 4 bytes eerst komt. Met andere woorden, voor RGB heb je de volgende situatie: het eerste byte is de blauwwaarde van de kleur, het tweede de groenwaarde en het derde de roodwaarde (allen van 0 tot en met 255). Het vierde byte heeft geen betekenis. De volgende Mathematica opdracht zet de kleurbytes om in een lijst met RGB-waarden tussen 0 en 1, die later zullen worden gebruikt om er een RGBColor graphics directive van te maken. rgbWaarden = Block@ 8x1, x2, x3, x4<, x1 = Partition@colorIndex, 4D; x2 = Transpose@x1D; x3 = Drop@x2, - 1D; x4 = Reverse@x3D; Transpose@x4D 255D 128 ::0, 0,
>, 80, 0, 1<> 255
Dit had als volgt, maar dan met gebruik van een Slot (#) , een pure Function (&), en de functie Map (/@) ook in één regel gekund. rgbWaarden = Reverse@Drop@ð, - 1DD & Partition@colorIndex, 4D 255 128 ::0, 0,
>, 80, 0, 1<> 255
De bitmap wordt nu uiteindelijk verkregen door het restant van de file als bytes in te lezen. bitmapLijst = BinaryReadList@stream, "Byte"D;
In dit geval zijn er 128 bytes in de bitmap (controleer met Length). Beter is het om direct voordat je de bitmap inleest eerst de filepointer naar de juiste positie dataOffset te laten wijzen. Deze methode is veel robuuster omdat je nu niet afhankelijk bent van aspecten als compressie, het al dan niet aanwezig zijn van de index, etc.
CP-cursus, 2007-2008
Hoofdstuk11.nb
145
SetStreamPosition@stream, dataOffsetD; bitmapLijst = BinaryReadList@stream, "Byte", imageSizeD 80, 112, 0, 56, 0, 240, 0, 60, 0, 224, 28, 28, 1, 224, 34, 30, 135, 192, 65, 15, 255, 128, 65, 7, 255, 0, 65, 3, 252, 0, 34, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 254, 0, 0, 3, 1, 143, 128, 4, 0, 112, 96, 8, 0, 96, 16, 16, 0, 144, 8, 32, 0, 136, 8, 32, 1, 8, 4, 64, 1, 4, 4, 64, 1, 4, 4, 64, 1, 4, 4, 64, 1, 4, 4, 64, 0, 132, 8, 252, 0, 132, 8, 255, 0, 68, 19, 255, 128, 56, 103, 167, 192, 15, 143, 17, 224, 16, 30, 8, 224, 32, 28, 4, 240, 64, 60, 3, 113, 128, 56, 0, 254, 0, 56, 0, 112, 0, 56<
De ingelezen bitmapLijst is echter nog niet bruikbaar, en wel om twee redenen. Ten eerste zitten er maar 128 elementen in de lijst, terwijl de bitmap volgens de variabelen width en height een afmeting van 32 bij 32, dus in totaal 1024 pixels heeft. We moeten dus alle 1024 bits in die 128 bytes berekenen! Ten tweede hebben we een lijst terwijl de pixels in een plaatje een matrix vormen. De volgende functie bepaalt met behulp van IntegerDigits de waarde van al de 8 bits in een byte. Deze functie is handig omdat we te maken hebben met een 1 bits bitmap, waarin elk pixel maar 1 bit aan geheugenruimte kost. ByteBits@a_ ; 0 £ a £ 255D := IntegerDigits@a, 2, 8D
Het kleurenpatroon van het plaatje, in dit geval gaat het om een 32 bij 32 pixel-plaatje, wordt nu gegeven in een 32 bij 32 indexmatrix met (de 2) kleurenindices. Het weergeven van de getallen in deze 32 bij 32 matrix is niet bijster interessant. Vandaar de ; achter de volgende expressie. (Maar je bent natuurlijk vrij om te kijken wat er na evaluatie staat). indexMatrix = Partition@ByteBits bitmapLijst Flatten , widthD ;
11.2.1 Gebruik van de functie Raster zonder kleureninformatie. Het plaatje, maar met onjuiste kleurcodering, kun je bijvoorbeeld met de functie Raster, op je scherm toveren. Show@indexMatrix Raster Graphics , AspectRatio ® 1D
11.2.2 Gebruik van ListDensityPlot met specificatie van een kleurenfunctie. Met behulp van de optie ColorFunction, die toegestaan is voor de meeste plot-functies, is het mogelijk om aan elke index (=geheel getal) in indexMatrix de juiste kleur te koppelen. Dat gebeurt hieronder met Function, Apply (@@) en RGBColor. Belangrijk is dat je de optie ColorFunctionScaling®False gebruikt, want anders gaat Mathematica zelf zitten rommelen aan de zorgvuldig geconstrueerde ColorFunction.
CP-cursus, 2007-2008
146
Hoofdstuk11.nb
ArrayPlot@indexMatrix, ColorFunction ® HRGBColor rgbWaardenPð1 + 1T &L, ColorFunctionScaling ® False, Mesh ® FalseD
Het laten zien van de figuur in de twee oorspronkelijke blauwtinten op papier is onmogelijk als je alleen maar grijstinten gebruikt. Je krijgt dan n.l. een vrijwel zwart plaatje. Merk op dat de elementen van rgbWaarden beginnnen te tellen bij 1, terwijl de getallen in indexMatrix beginnen te tellen bij 0. In bovenstaande cel wordt de waarde 0 in de indexMatrix gekoppeld aan het eerste element van rgbWaarden. 11.2.3 Controle We kunnen de bitmap file natuurlijk gewoon importeren in Mathematica en het vervolgens laten zien. Dit ter controle. Je kunt het plaatje altijd opschalen door er op te klikken en het vervolgens "op te rekken". Natuurlijk kun je onder Windows ook de file opzoeken en er op dubbel klikken. Er zal vast wel een applicatie zijn die het plaatje laat zien. fig = Import@filenaamD
11.3 Opdracht Û Tip: Kopieer elke opgave eerst naar een eigen opgaven-notebook, en begin dan pas met de uitwerking ervan.
Opdracht 1 Voeg aan het in het bovenstaande voorbeeld ingelezen bestand "circles.bmp" een rand toe met rode pixels, en laat het resultaat zien op het beeldscherm. Hiervoor gebruik je natuurlijk niet het originele bestand, maar de informatie waarmee je kunt rekenen: indexMatrix en rgbWaarden.
CP-cursus, 2007-2008
Hoofdstuk11.nb
147
Opdracht 2 Bewerk het plaatje "Circles.bmp" zo dat er in beide richtingen 2 maal zoveel pixels zijn als in de originele figuur, maar in een 2 bij 2 schaakbord patroon in de kleuren rood en blauw en met witte ringen. Hierbij blijft de grootte van de ringen (gemeten in pixels) onveranderd. Het is wellicht handig, maar niet per se noodzakelijk, om hiervoor de functie ArrayFlatten te gebruiken. Opdracht 3 De file "Bubbles.bmp" heeft een iets andere codering omdat er 4 kleuren worden gebruikt en niet 2. Hoeveel bits per pixel worden er in deze file gebruikt? Waar moet je wijzigingen aanbrengen in het bovenstaande voorbeeld opdat ook voor deze file op een correcte manier de grootheden indexMatrix en rgbWaarden worden berekend? Laat het resultaat daadwerkelijk zien met een ListDensityPlot. Je mag hiervoor dus best de hele sectie 11.2 kopië ren naar een eigen notebook, en vervolgens op de vereiste plaatsen wijzigingen aanbrengen. Opdracht 4 Maak een functie BMDimension die als input een characterstring met de naam van de file verwacht. Als output verschijnt dan een lijstje met 2 elementen: het aantal pixels in de x- en in de y-richting. Als de file niet bestaat, als het geen bitmap file is, of als de lengte van de file kleiner is dan 54 bytes moet er een foutmelding verschijnen en mag de functie dus ook geen resultaat geven, maar moet slechts een Message geven. Hint: gebruik FileType om te testen of de file bestaat, gebruik FileByteCount om te testen of or wel genoeg bytes in de files zitten. Lees bij voorkeur niet de hele file in, maar alleen de header. Merk op dat de minimale afmeting van een niet corrupte header gelijk is aan 54 bytes.
CP-cursus, 2007-2008