Programmering

Billedbehandling med Java 2D

Billedbehandling er kunsten og videnskaben ved at manipulere digitale billeder. Det står med den ene fod fast i matematik og den anden i æstetik og er en kritisk komponent i grafiske computersystemer. Hvis du nogensinde har gidet dig med at oprette dine egne billeder til websider, vil du uden tvivl sætte pris på vigtigheden af ​​Photoshops billedmanipuleringsfunktioner til oprydning af scanninger og rydning af mindre end optimale billeder.

Hvis du lavede noget billedbehandlingsarbejde i JDK 1.0 eller 1.1, husker du sandsynligvis, at det var lidt stump. Den gamle model af billeddataproducenter og forbrugere er uhåndterlig til billedbehandling. Før JDK 1.2 var billedbehandling involveret MemoryImageSources, PixelGrabbers og andre sådanne arcana. Java 2D giver dog en renere, lettere at bruge model.

Denne måned undersøger vi algoritmerne bag flere vigtige billedbehandlingsoperationer (ops) og vise dig, hvordan de kan implementeres ved hjælp af Java 2D. Vi viser dig også, hvordan disse ops bruges til at påvirke billedets udseende.

Fordi billedbehandling er en virkelig nyttig, enkeltstående applikation af Java 2D, har vi bygget denne måneds eksempel, ImageDicer, til at være så genanvendelig som muligt til dine egne applikationer. Dette enkelt eksempel viser alle de billedbehandlingsteknikker, vi dækker i denne måneds kolonne.

Bemærk, at kort før denne artikel blev offentliggjort, frigav Sun Java 1.2 Beta 4-udviklingssættet. Beta 4 ser ud til at give bedre ydeevne til vores eksempler på billedbehandlingsoperationer, men det tilføjer også nogle nye fejl, der involverer grænsekontrol af ConvolveOps. Disse problemer påvirker kantdetektering og slibningseksempler, vi bruger i vores diskussion.

Vi synes, at disse eksempler er værdifulde, så i stedet for at udelade dem helt kompromitterede vi: for at sikre, at den kører, afspejler eksempelkoden Beta 4-ændringerne, men vi har bevaret tallene fra 1.2 Beta 3-udførelsen, så du kan se operationerne fungerer korrekt.

Forhåbentlig vil Sun adressere disse fejl inden den endelige Java 1.2-udgivelse.

Billedbehandling er ikke raketvidenskab

Billedbehandling behøver ikke at være vanskelig. Faktisk er de grundlæggende begreber ret enkle. Et billede er trods alt bare et rektangel med farvede pixels. Behandling af et billede er simpelthen et spørgsmål om at beregne en ny farve for hver pixel. Den nye farve på hver pixel kan være baseret på den eksisterende pixelfarve, farven på de omgivende pixels, andre parametre eller en kombination af disse elementer.

2D API introducerer en ligefrem billedbehandlingsmodel, der hjælper udviklere med at manipulere disse billedpixels. Denne model er baseret på java.awt.image.BufferedImage klasse og billedbehandlingshandlinger som f.eks sammenfald og tærskelværdi er repræsenteret ved implementeringer af java.awt.image.BufferedImageOp interface.

Implementeringen af ​​disse ops er relativt ligetil. Antag for eksempel, at du allerede har kildebilledet som en BufferedImage hedder kilde. At udføre den handling, der er illustreret i figuren ovenfor, vil kun tage nogle få linier kode:

001 kort [] tærskel = ny kort [256]; 002 for (int i = 0; i <256; i ++) 003 tærskel [i] = (i <128)? (kort) 0: (kort) 255; 004 BufferedImageOp-tærskelOp = 005 ny LookupOp (ny ShortLookupTable (0, tærskel), null); 006 BufferedImage destination = thresholdOp.filter (kilde, null); 

Det er virkelig alt der er ved det. Lad os nu se nærmere på trinnene:

  1. Instantier billedfunktionen efter eget valg (linjer 004 og 005). Her brugte vi en OpslagOp, som er en af ​​de billedoperationer, der er inkluderet i Java 2D-implementeringen. Som enhver anden billedoperation implementerer den BufferedImageOp interface. Vi taler mere om denne operation senere.

  2. Ring til operationen filter() metode med kildebilledet (linje 006). Kilden behandles, og destinationsbilledet returneres.

Hvis du allerede har oprettet en BufferedImage der holder destinationsbilledet, kan du sende det som den anden parameter til filter(). Hvis du passerer nul, som vi gjorde i eksemplet ovenfor, en ny destination BufferedImage er oprettet.

2D API inkluderer en håndfuld af disse indbyggede billedoperationer. Vi diskuterer tre i denne kolonne: foldning,opslagstabeller, og tærskelværdi. Se Java 2D-dokumentationen for information om de resterende operationer, der er tilgængelige i 2D API (Resources).

Konvolution

EN sammenfald Betjening giver dig mulighed for at kombinere farverne på en kilde-pixel og dens naboer for at bestemme farven på en destinations-pixel. Denne kombination specificeres ved hjælp af en kerne, en lineær operator, der bestemmer andelen af ​​hver kilde-pixelfarve, der bruges til at beregne destinationspixelfarven.

Tænk på kernen som en skabelon, der er overlejret på billedet for at udføre en foldning på en pixel ad gangen. Når hver pixel er viklet, flyttes skabelonen til den næste pixel i kildebilledet, og konvolutionsprocessen gentages. En kildekopi af billedet bruges til inputværdier til sammenfaldet, og alle outputværdier gemmes i en destinationskopi af billedet. Når konvolutionsoperationen er afsluttet, returneres destinationsbilledet.

Kernens centrum kan betragtes som overlejring af kildepixel, der er indviklet. For eksempel har en konvolutionshandling, der bruger følgende kerne, ingen indvirkning på et billede: hver destinationspixel har den samme farve som den tilsvarende kildepixel.

 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 

Hovedreglen til oprettelse af kerner er, at elementerne alle skal tilføjes op til 1, hvis du vil bevare billedets lysstyrke.

I 2D API er en opløsning repræsenteret af en java.awt.image.ConvolveOp. Du kan konstruere en ConvolveOp ved hjælp af en kerne, der er repræsenteret af en forekomst af java.awt.billede.Kerne. Den følgende kode konstruerer a ConvolveOp ved hjælp af kernen præsenteret ovenfor.

001 float [] identityKernel = {002 0.0f, 0.0f, 0.0f, 003 0.0f, 1.0f, 0.0f, 004 0.0f, 0.0f, 0.0f 005}; 006 BufferedImageOp identity = 007 new ConvolveOp (new Kernel (3, 3, identityKernel)); 

Konvolutionsoperationen er nyttig til at udføre flere almindelige operationer på billeder, som vi detaljerer om et øjeblik. Forskellige kerner producerer radikalt forskellige resultater.

Nu er vi klar til at illustrere nogle billedbehandlingskerner og deres effekter. Vores umodificerede image er Lady Agnew af Lochnaw, malet af John Singer Sargent i 1892 og 1893.

Den følgende kode opretter en ConvolveOp der kombinerer lige store mængder af hver kilde-pixel og dens naboer. Denne teknik resulterer i en slørende effekt.

001 niende flyde = 1.0f / 9.0f; 002 float [] blurKernel = {003 niende, niende, niende, 004 niende, niende, niende, 005 niende, niende, niende 006}; 007 BufferedImageOp blur = new ConvolveOp (new Kernel (3, 3, blurKernel)); 

En anden almindelig opløsningskerne understreger kanterne i billedet. Denne operation kaldes almindeligvis kantregistrering. I modsætning til de andre kerner, der præsenteres her, tilføjes denne kernes koefficienter ikke op til 1.

001 float [] edgeKernel = {002 0.0f, -1.0f, 0.0f, 003 -1.0f, 4.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005}; 006 BufferedImageOp edge = new ConvolveOp (new Kernel (3, 3, edgeKernel)); 

Du kan se, hvad denne kerne gør ved at se på koefficienterne i kernen (linier 002-004). Tænk et øjeblik på, hvordan kantdetekteringskernen bruges til at fungere i et område, der udelukkende har en farve. Hver pixel ender uden farve (sort), fordi farven på omgivende pixels annullerer kildepixelens farve. Lyse pixels omgivet af mørke pixels forbliver lyse.

Bemærk, hvor meget mørkere det behandlede billede er sammenlignet med originalen. Dette sker, fordi elementerne i kantdetekteringskernen ikke tilføjes op til 1.

En simpel variation på kanedetektion er slibning kerne. I dette tilfælde tilføjes kildebilledet til en kantdetekteringskerne som følger:

 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 -1.0 4.0 -1.0 + 0.0 1.0 0.0 = -1.0 5.0 -1.0 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 

Slibekernen er faktisk kun en mulig kerne, der skærper billeder.

Valget af en 3 x 3 kerne er noget vilkårlig. Du kan definere kerner af enhver størrelse, og formodentlig behøver de ikke engang at være firkantede. I JDK 1.2 Beta 3 og 4 producerede en ikke-kvadratisk kerne imidlertid et applikationsnedbrud, og en 5 x 5-kerne tyggede billeddataene op på en mest mærkelig måde. Medmindre du har en overbevisende grund til at afvige fra 3 x 3 kerner, anbefaler vi det ikke.

Du undrer dig måske også over, hvad der sker i kanten af ​​billedet. Som du ved, tager konvolutionsoperationen hensyn til en kilde-pixels naboer, men kilde-pixels i billedets kanter har ikke naboer på den ene side. Det ConvolveOp klasse inkluderer konstanter, der angiver, hvad adfærd skal være ved kanterne. Det EDGE_ZERO_FILL konstant angiver, at kanterne på destinationsbilledet er indstillet til 0. EDGE_NO_OP konstant angiver, at kildepixel langs billedets kant kopieres til destinationen uden at blive ændret. Hvis du ikke angiver en kantadfærd, når du konstruerer en ConvolveOp, EDGE_ZERO_FILL anvendes.

Følgende eksempel viser, hvordan du kan oprette en slibningsoperator, der bruger EDGE_NO_OP Herske (NO_OP overføres som en ConvolveOp parameter i linje 008):

001 flyde [] sharpKernel = {002 0.0f, -1.0f, 0.0f, 003 -1.0f, 5.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005}; 006 BufferedImageOp sharpen = new ConvolveOp (007 new Kernel (3, 3, sharpKernel), 008 ConvolveOp.EDGE_NO_OP, null); 

Opslagstabeller

En anden alsidig billedoperation involverer brug af en opslagstabel. Til denne handling oversættes kilde-pixelfarver til destinationspixelfarver ved hjælp af en tabel. En farve, husk, er sammensat af røde, grønne og blå komponenter. Hver komponent har en værdi fra 0 til 255. Tre tabeller med 256 poster er tilstrækkelige til at oversætte enhver kildefarve til en destinationsfarve.

Det java.awt.image.LookupOp og java.awt.image.LookupTable klasser indkapsler denne handling. Du kan definere separate tabeller for hver farvekomponent eller bruge en tabel til alle tre. Lad os se på et simpelt eksempel, der inverterer farverne på hver komponent. Alt hvad vi skal gøre er at oprette en matrix, der repræsenterer tabellen (linier 001-003). Så opretter vi en Opslagstabel fra arrayet og a OpslagOp fra Opslagstabel (linje 004-005).

001 kort [] invert = ny kort [256]; 002 for (int i = 0; i <256; i ++) 003 invertere [i] = (kort) (255 - i); 004 BufferedImageOp invertOp = ny LookupOp (005 ny ShortLookupTable (0, invert), null); 

Opslagstabel har to underklasser, ByteLookupTable og ShortLookupTable, der indkapsler byte og kort arrays. Hvis du opretter en Opslagstabel der ikke har en indtastning for nogen inputværdi, kastes en undtagelse.

Denne handling skaber en effekt, der ligner en farvenegativ i konventionel film. Bemærk også, at anvendelsen af ​​denne handling to gange gendanner det originale billede; du tager dybest set et negativt af det negative.

Hvad hvis du kun ville påvirke en af ​​farvekomponenterne? Let. Du konstruerer en Opslagstabel med separate tabeller for hver af de røde, grønne og blå komponenter. Følgende eksempel viser, hvordan man opretter en OpslagOp der kun inverterer den blå komponent i farven. Som med den tidligere inversionsoperatør gendanner det originale billede to gange ved at anvende denne operator.

001 kort [] invert = ny kort [256]; 002 kort [] lige = ny kort [256]; 003 for (int i = 0; i <256; i ++) {004 invertere [i] = (kort) (255 - i); 005 lige [i] = (kort) i; 006} 007 kort [] [] blueInvert = ny kort [] [] {lige, lige, inverter}; 008 BufferedImageOp blueInvertOp = 009 ny LookupOp (ny ShortLookupTable (0, blueInvert), null); 

Posterisering er en anden god effekt, du kan anvende ved hjælp af en OpslagOp. Posterisering indebærer at reducere antallet af farver, der bruges til at vise et billede.

EN OpslagOp kan opnå denne effekt ved hjælp af en tabel, der kortlægger inputværdier til et lille sæt outputværdier. Følgende eksempel viser, hvordan inputværdier kan kortlægges til otte specifikke værdier.

001 kort [] posterize = ny kort [256]; 002 for (int i = 0; i <256; i ++) 003 posterize [i] = (kort) (i - (i% 32)); 004 BufferedImageOp posterizeOp = 005 ny LookupOp (ny ShortLookupTable (0, posterize), null); 

Tærskelværdi

Den sidste billedoperation, vi undersøger, er tærskelværdi. Tærskelværdi gør farveændringer på tværs af en programmørbestemt "grænse" eller tærskel, mere indlysende (svarende til hvordan konturlinjerne på et kort gør højdegrænser mere indlysende). Denne teknik bruger en specificeret tærskelværdi, minimumsværdi og maksimumsværdi til at kontrollere farvekomponentværdierne for hver pixel i et billede. Farveværdier under tærsklen tildeles minimumsværdien. Værdier over tærsklen tildeles den maksimale værdi.