Java – pokročilá grafika (operace s obrázky)

16. prosince 2003

Třída BufferedImage slouží k vytváření bitmapových obrázků, které pak můžeme v paměti různě upravovat. V tomto článku si ukážeme, jak takové obrázky zaostřovat, rozmazávat, otáčet, měnit jejich velikost a podobně. Velkou pozornost budeme věnovat hlavně využití algoritmu konvoluce.

Obrázky BufferedImage je obecně vzato možné upravovat buď prostřednictvím objektu Graphics, respektive Graphics2D, nebo pomocí objektů Raster. Ani jednou z těchto možností se tentokrát přímo zabývat nebudeme, ukážeme si snazší způsob úpravy obrázků spočívající ve využití toho, co už pro nás připravili jiní. Tím něčím budou filtry z balíčku java.awt.image, které implementují rozhraní BufferedImageOp. Kromě nich existují ještě filtry implementující rozhraní ImageFilter. Těmi jsme se ale už částečně zabývali v předchozím článku.

Pro nás nejpodstatnější metodou definovanou v rozhraní BufferedImageOp je metoda public BufferedImage filter(BufferedImage zdroj, BufferedImage cíl), po jejímž zavolání dojde k vlastnímu filtrování obrázku. Výsledný obrázek se pak uloží buď do objektu „cíl“ předaného jako parametr, nebo do nově vytvořeného obrázku, pokud bude parametr „cíl“ roven „null“.

Pro potřeby tohoto článku si vytvoříme jednoduchou konzolovou aplikaci, která načte obrázek, jehož URL jí bude předána jako argument při spuštění, aplikuje na něj filtr, jehož název jí bude rovněž předán jako argument, a obrázek nakonec uloží. Načtením a uložením obrázku jsme se zabývali už v článku o tom, jak v Javě vytvořit jednoduchý bitmapový editor, takže nám už zbývá jen říci si, jak je to s vytvářením instancí tříd jen na základě jejich jména. K tomu slouží mechanismus reflexe, který je jistě mnohým z vás důvěrně známý a který neumožňuje jenom vytváření nových instancí předem neznámých tříd, ale i mnoho dalšího, čímž se ale nyní nebudeme zabývat. K vytvoření nové instance nám totiž stačí pouze volat statickou metodu forName třídy Class, která vrací objekt Class, a pak volat na takto vzniklém objektu Class metodu newInstance. Metoda main naší konzolové aplikace následuje na výpisu:

public static void main(String[] args) {
   System.out.println(„Vytvářím instanci filtru.“);
   DemoImgOp dio = null;
   BufferedImage src = null;
   try {
     dio = (DemoImgOp)Class.forName(args[1]).newInstance();
   } catch (Exception e){
     System.err.println(„Nepodařilo se vytvořit instanci třídy: „+args[1]);
     System.err.println(e);
     System.exit(0);
   }
   System.out.println(„Načítám obrázek ze souboru: „+args[0]);
   try {
     src = loadImage(args[0]);
   } catch (Exception e){
     System.err.println(„Nepodařilo se načíst obrázek“);
     System.err.println(e);
     System.exit(0);
   }
   System.out.println(„Aplikuji filtr na obrázek“);
   BufferedImage buf = dio.filter(src);
   System.out.println(„Ukládám obrázek do souboru: „+args[0]+“_filtered.jpg“);
   try {
     saveImage(args[0]+“_filtered.jpg“, buf);
   } catch (IOException e){
     System.err.println(„Nepodařilo se uložit obrázek.“);
     System.err.println(e);
     System.exit(0);
   }
   System.out.println(„Hotovo“);
}

Spustit ji pak budeme moci například následujícím způsobem: java interval.graphics2d.Filters Obrazek.jpg interval.graphics2d.GaussianBlur, kde GaussianBlur je název nějaké třídy.

Jak jste si asi všimli, pracuje naše aplikace s objekty DemoImgOp. Rozhraní DemoImgOp si implementujeme za účelem udělat věci co nejjednoduššími, takže v něm definujeme pouze jedinou metodu, jak můžete vidět na dalším výpisu. Tato metoda prostě vezme obrázek, provede na něm nějakou operaci a pak jej vrátí jako návratovou hodnotu.

package interval.graphics2d;
import java.awt.image.*;
public interface DemoImgOp {
   public BufferedImage filter(BufferedImage src);
}

Konvoluce

Asi nejpoužívanější operací prováděnou na obrázcích je konvoluce, což je algoritmus, který počítá výsledné pixely jako vážené součty pixelů v jejich okolí, což dělá pro každou barevnou složku zvlášť. Je až neuvěřitelné, co je s tak jednoduchým algoritmem možné provádět za kouzla.

Třída ConvolveOp právě takovou konvoluci implementuje, a to většinou prostřednictvím nativního kódu, díky čemuž bude vždy rychlejší než řešení, která bychom si vytvářeli sami. (Pokud bychom ovšem nevyužili taktéž nativní kód a k tomu ještě nenahradili konvoluci pro určité speciální případy – gaussovské rozmazání – několika „snazšími“ konvolucemi, což by však pochopitelně nešlo provést obecně pro konvoluci jako takovou.)

Konstruktor vypadá následovně: ConvolveOp(Kernel kernel, int operaceNaOkraje, RenderingHints vlastnosti), kde kernel je matice obsahující váhy pixelů a příznak operaceNaOkraje určuje, zda se mají okraje nahradit nulami (EDGE_ZERO_FILL) nebo zda se mají nechat, jak jsou (EDGE_NO_OP). Objekt RenderingHints pak obsahuje informace o tom, jak má být vše provedeno, může však být i null, čehož využijeme i my. Ještě zbývá ukázat si, jak vytvořit objekt Kernel. Konstruktor Kernel(int šířka, int výška, float[] matice) očekává tři parametry: výšku a šířku matice a matici v podobě jednorozměrného pole, jehož délka musí pochopitelně být šířka*výška.

Suchá teorie není nic zajímavého, takže si pojďme ukázat několik filtrů implementujících operace jako zaostřování nebo rozmazávání. Pokročilí uživatelé grafických editorů, kteří již s konvolucí měli tu čest, mohou následující část popisující několik konvolučních filtrů s klidným svědomím přeskočit.

Rozmazání zprůměrováním

Nejprve si ukážeme, jak obrázek rozmazat. Nejpřímočařejším způsobem, jak rozmazat obrázek, je prostě zprůměrovat několik pixelů. Třída, která toto provádí, je na následujícím výpisu:

package interval.graphics2d;
import java.awt.image.*;
public class Averaging implements DemoImgOp{
   float matrix[] = {
   1f/9,1f/9f,1f/9f,
   1f/9,1f/9f,1f/9f,
   1f/9,1f/9f,1f/9f};
   protected ConvolveOp cop;
   public Averaging() {
     Kernel kernel = new Kernel(3,3,matrix);
     cop = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
   }
   public BufferedImage filter(BufferedImage src){
     return cop.filter(src, null);
   }
}

A toto je výsledek spuštění programu Filters s filtrem interval.graphics2d.Averaging:

původní obrázek obrázek rozmazaný zprůměrováním

Gaussovské rozmazání

Další možností, jak rozmazat obrázek, je použít matici, v níž váhy pixelů odpovídají gaussovskému rozložení. Zde se často s výhodou využívá členů Pascalova trojúhelníku (krajní sloupce a řádky). Gaussovská matice 5×5 prvků pak může vypadat například takto:

1/256 4/256 6/256 4/256 1/256
4/256 16/256 24/256 16/256 4/256
6/256 24/256 36/256 24/256 6/256
4/256 16/256 24/256 16/256 4/256
1/256 4/256 6/256 4/256 1/256

A zde je výsledek:

Gaussovské rozmazání

Zaostření

Obrázek můžeme ale také zaostřovat. Principem této operace je mírné zvýraznění hran. Matice k tomu určená může vypadat například takto:

-1 -1 -1
-1 9 -1
-1 -1 -1

Tato matice způsobí dost drastické vytažení kontur, snadno si však můžete najít matici, která bude vyhovovat vašim potřebám.

zaostření

Detekce hran

Další z operací, které si uvedeme, je detekce hran. Matice pro detekci hran může vypadat následovně…

-1 -1 -1
-1 8 -1
-1 -1 -1

…ovšem lepšího efektu dosáhneme s následující maticí:

-2 -2 -2 -2 -2
-2 -3 -3 -3 -2
-2 -3 53 -3 -2
-2 -3 -3 -3 -2
-2 -2 -2 -2 -2

Sami můžete porovnat jejich výsledky:

detekce hran (1) detekce hran (2)

Další filtry

Přestože konvoluce je skutečně asi nejpoužívanější operací prováděnou s obrázky, neznamená to, že by neexistovaly i jiné způsoby, jak obrázky upravovat. Opomeneme-li nyní změnu jasu, odstínu a kontrastu, stále nám tu zůstanou například různé transformace, jako je roztahování nebo otáčení, nebo třeba „prohazování“ barev.

LookupOp

Nejprve se pojďme podívat na záměnu barev, k čemuž lze použít třídu LookupOp. Konstruktoru LookupOp(LookupTable table, RenderingHints hints) se předává objekt LookupTable, v němž jsou uloženy nové hodnoty pro jednotlivé složky barvy. Máme-li například pixel s červenou složkou 123, bude tomuto pixelu přiřazena hodnota uložená v tabulce na indexu 123. Následující kód demonstruje použití této třídy:

package interval.graphics2d;
import java.awt.image.*;
public class ReverseLookUp implements DemoImgOp{
   protected LookupOp lookup;
   protected ByteLookupTable table;
   public ReverseLookUp() {
     byte array[] = new byte[256];
     for (int i = 0; i < 256; i++)
      array[i] = (byte)(255-i);
      table = new ByteLookupTable(0, array);
      lookup = new LookupOp(table, null);
   }
   public BufferedImage filter(BufferedImage src) {
     return lookup.filter(src, null);
   }
}

Výsledkem je následující obrázek, ve kterém byly původní barvy nahrazeny barvami doplňkovými.

záměna barev

V tomto případě používáme stejnou tabulku pro všechny složky barvy. Pokud potřebujete mít více tabulek, podívejte se do dokumentace, zde se zabýváme jen nejjednodušším případem.

AffineTransformOp

Další běžnou operací, prováděnou s obrázky, jsou úpravy jejich rozměrů či jejich otáčení. K tomu se využívá třída AffineTransformOp, jejíž konstruktor vypadá takto: AffineTransformOp(AffineTransform at, RenderingHints hints). Pokud si ještě vzpomínáte, třídou AffineTransform jsme se zabývali v prvním článku o pokročilé grafice, takže pokud jste pozapomněli, jak se s ní pracuje, můžete se k článku vrátit.

Nyní se na chvíli věnujme malému příkladu. Naším cílem tentokrát nebude nic těžšího, než otočit obrázek o 90°. Zase tak jednoduché to ale nebude. Musíme totiž vykonat následující kroky: vytvořit prázdný obrázek s šířkou rovnou výšce původního a s výškou rovnou šířce původního. Pak musíme vytvořit transformační matici, nastavit do ní posunutí (obrázek by se nám totiž jinak během otáčení dostal mimo nový obrázek a výsledkem by byla jenom černá plocha) a pak teprve rotaci.

import java.awt.geom.AffineTransform;
import java.awt.image.*;
public class Rotation90 implements DemoImgOp{
   public BufferedImage filter(BufferedImage src) {
     AffineTransform at = new AffineTransform();
     BufferedImage bif = new BufferedImage(src.getHeight(), src.getWidth(), src.getType());
     at.translate(src.getHeight(), 0);
     at.rotate(Math.PI/2);
     AffineTransformOp ato = new AffineTransformOp(at, null);
     ato.filter(src, bif);
     return bif;
   }
}

Podobně bychom postupovali i při překlápění nebo změně rozměrů.

otočený obrázek

RescaleOp

Další z palety operací, které nám standardní třídy umožňují provádět s obrázky, je operace, kterou lze popsat následujícím předpisem: vezmi pixel, vynásob jeho každou barevnou složku příslušným parametrem „scale“ a pak k ní přičti parametr „offset“ – pokud je výsledek větší než maximální hodnota, použij maximální hodnotu.

S využitím této třídy a třídy ConvolveOp vytvoříme filtr podobný filtru známému pod anglickým označením emboss, který způsobuje jakoby prostorové vystoupnutí objektů, k čemuž se využívá matice, která je podobná matici pro detekci hran, ale liší se od ní především tím, že není symetrická. Po provedení konvoluce s touto maticí pak k jednotlivým barevným složkám všech pixelů přičteme hodnotu 128.

Uvedený postup se od klasického „embossu“ liší především tím, že tam se k pixelu přidává hodnota 128 už během konvoluce, díky čemuž se neztrácí informace o hodnotách menších než nula, které se v našem filtru prostě všechny nahradí nulami a následně hodnotami 128.

Konstruktor, který použijeme, má tvar RescaleOp(float scale, float offset, RenderingHints hints), přičemž parametr „hints“ může být i „null“. Kód třídy implementující filtr emboss následuje. (Opět používáme pouze jednodušší verzi se stejnými hodnotami scale a offset pro všechny barevné složky pixelu.)

package interval.graphics2d;
import java.awt.color.ColorSpace;
import java.awt.image.*;
public class LikeEmboss implements DemoImgOp{
   float matrix[] = {
   2f,0f,0f,
   0f,-1f,0f,
   0f, 0f,-1f};
   protected ConvolveOp cop;
   protected RescaleOp rescale;
   public LikeEmboss() {
     Kernel kernel = new Kernel(3,3,matrix);
     cop = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
     rescale = new RescaleOp(1, 128, null);
   }
   public BufferedImage filter(BufferedImage src){
     BufferedImage b = cop.filter(src, null);
     return rescale.filter(b, null);
   }
}

emboss

A to je vše. Tímto článkem končí seriál o pokročilé grafice v Javě. Mnoha témat se dotkl jen ve zkratce nebo je zcela pominul. Na druhou stranu však nebylo a ani nemohlo být jeho účelem probrat vše, co je v Javě v oblasti grafiky možné udělat, od toho tu jsou podrobné tutoriály.

Přílohy ke stažení

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Předchozí článek Jak budovat a rozvíjet e-shop
Další článek gradua.cz
Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *