本文說明如何通過實現 BufferedImageOp 接口來編寫自定義 Java 2D 圖像處理類。它使用一個 2D 細胞自動機(CA),即循環空間,來構造圖像處理應用程序。CA 會 “操作” 圖像(例如,一個 PEG 文件),使圖像不斷地按有趣的方式轉換。我希望本文能開闊您的視野,使您能編寫一個全新的圖像處理應用程序類。
2D 細胞自動機
2D 細胞自動機由分布在 2D 網格(通常稱為布局)中的細胞 組成。每個細胞都有一個狀態,可以是 0 到 n 之間的任意整數。清單 1 顯示了如何用 Java 代碼聲明一個細胞自動機布局:
清單 1. 定義 TwoDCellularAutomaton.universe
protected int[][] universe;
|
所有細胞每個時刻都同時更新狀態。一個細胞的新狀態取決于該細胞的當前狀態和它相鄰細胞的當前狀態,狀態的轉換根據特定的規則進行。清單 2 更新了下一時刻的布局:
清單 2. TwoDCellularAutomaton 類(部分清單)
public void update() { int[][] newUniverse = new int[rowCount][colCount]; for (int row = 0; row < rowCount; row++) { for (int col = 0; col < colCount; col++) { newUniverse[row][col] = updateCell(row, col); } } for (int row = 0; row < rowCount; row++) { for (int col = 0; col < colCount; col++) { universe[row][col] = newUniverse[row][col]; } } }
protected abstract int updateCell(int row, int col);
|
不同類型的 CA 更新單個細胞所用的規則不相同。規則的定義由子類完成。
循環空間
 |
|
循環空間是由麥迪遜市威斯康星大學數學系的 David Griffeath 發現的,并由 A. K. Dewdney 在 Scientific American 的一個專欄中推廣。
|
|
在循環空間中,每個細胞都有一個狀態,它是 n 種狀態中的一種。每個細胞的初始狀態通常是隨機定義的,也就是說,是 0 和 n - 1(包括 0 和 n - 1)之間的一個隨機數字。細胞的鄰居定義為 von Neumann 鄰居:包括它的上下左右 4 個鄰近細胞。
清單 3 通過給出每個細胞鄰居和細胞本身的不同坐標來定義該細胞的 von Neumann 鄰居:
清單 3. 定義 TwoDCellularAutomaton.VON_NEUMANN_NEIGHBORHOOD
protected static final int[][] VON_NEUMANN_NEIGHBORHOOD = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
|
循環空間由以下規則定義:
如果一個細胞的狀態是 k,它有一個鄰居的狀態是 k + 1,那么該狀態在下一時刻將會有一個新的狀態 k + 1。否則,該細胞的狀態將保持不變。
這個規則是循環的,因此,如果一個細胞處于狀態 n - 1,而且有一個狀態為 0 的鄰居,那么該細胞在下一時刻的狀態將為 0。
 |
ConvolveOp 算是一個細胞自動機
Java 2D API 的 ConvolveOp 類代表一個空間螺旋:每個目標像素的顏色通過對應的源像素及其鄰居像素的顏色來確定。
您是不是覺得這個定義很熟悉呢?這與 2D 細胞自動機基本上是同一個東西,但不盡相同。例如,狀態(顏色)是連續的而不是分散的(也不完全如此:RGB 值的個數是無限的,但很接近連續)。這使得該類更像是一個連續自動機。而且您不能使用像 CA 那樣細的粒度控制基于細胞及其鄰居細胞當前狀態的新狀態。
因此,您無法使用一個 ConvolveOp 定義循環空間,但它仍然是很有趣的。它是查看 ConvolveOp 的另一種方式。
|
|
這個簡單規則會導致意想不到的復雜行為。清單 4 實現了在循環空間中更新細胞的規則:
清單 4. 定義 CyclicSpace.updateCell(int, int)
protected int updateCell(int row, int col) { int[] neighborStates = getNeighborStates(row, col, neighborhood); int currentState = universe[row][col]; for (int i = 0; i < neighborStates.length; i++) { int neighborState = neighborStates[i]; if (neighborState == (currentState + 1) % n) { return neighborState; } }
return currentState; }
|
我曾說過,循環空間布局的初始狀態是隨機的。細胞會被 “更大的” 細胞 “吃掉”,最后會再次循環回到狀態 0。在這個過程中,區域自行組織并展開,成為波浪形。最后,會出現一個穩定的波浪圖案。這些波浪呈對角線在布局中移動,看上去有點像紙風車。
創建圖像操作器
java.awt.image.BufferedImageOp 接口允許您創建自己的圖像操作器(也稱為過濾器)。本文只討論 BufferedImageOp 的一個方法:
BufferedImage filter(BufferedImage src, BufferedImage dest)
|
src 和 dest 是 2D 像素網格。實現此方法時,您可以按任意方式從 src 構建 dest。普遍做法是在 src 中迭代像素,并按照一定規則在 dest 中創建相應的像素。這就是在圖像處理應用程序中需要做的事情,我根據著名的法國畫家 Georges-Pierre Seurat 將它命名為 Seurat(參見 下載 獲取完整的示例代碼)。
Seurat 應用程序
您可能知道圖像像素與 CA 中的細胞存在映射關系。它們都以 2D 網格形式存在,每個都有狀態,對于像素就是它的紅綠藍(RGB)值。我將在 filter(BufferedImage src, BufferedImage dest) 實現中探討這種映射關系。對于 src 中的每個像素,我會根據一定規則將該像素的 RGB 值與 CA 中相應細胞的狀態組合起來,創建 dest 中相應像素的新 RGB 值。這個規則將定義一個過濾器。
清單 5 顯示如何迭代 src 中的所有像素并在 dest 中構建像素。抽象方法 getNewRGB(Color) 由單獨的過濾器定義。它為輸入顏色計算并返回經過過濾的 RGB 值。
清單 5. CellularAutomataFilter 類(部分清單)
public BufferedImage filter(BufferedImage src, BufferedImage dest) { if (dest == null) dest = createCompatibleDestImage(src, null);
int srcHeight = src.getHeight(); int srcWidth = src.getWidth(); for (int y = 0; y < srcHeight; y++) { for (int x = 0; x < srcWidth; x++) { // Get the pixel in the original image. int origRGB = src.getRGB(x, y); Color origColor = new Color(origRGB);
// Get the new RGB values from the filter. int[] newRGB = getNewRGB(origColor);
// Convert the pixel coordinates to the CA coordinates by // scaling. int cAY = (int) ((double) twoDCellularAutomaton .getRowCount() / (double) srcHeight * y); int cAX = (int) ((double) twoDCellularAutomaton .getColCount() / (double) srcWidth * x); // Get the state of the corresponding CA cell. int state = twoDCellularAutomaton.getState(cAY, cAX); // Determine the weight of the filtered RGB values depending on // the state. double filterProportion = (double) state / (double) twoDCellularAutomaton.getN();
// Determine the weighted average between the filtered RGB // values and the image RGB values. int weightedRed = (int) Math.round(newRGB[0] * filterProportion + origColor.getRed() * (1.0 - filterProportion)); int weightedBlue = (int) Math.round(newRGB[1] * filterProportion + origColor.getBlue() * (1.0 - filterProportion)); int weightedGreen = (int) Math.round(newRGB[2] * filterProportion + origColor.getGreen() * (1.0 - filterProportion));
// Set the pixel in dest with this weighted average. dest.setRGB(x, y, new Color(weightedRed, weightedBlue, weightedGreen).getRGB()); } }
return dest; }
abstract protected int[] getNewRGB(Color color);
|
您可能會發現我沒有利用圖像中的像素與 CA 中的細胞之間的一對一映射關系。更確切地講,CA 是粗粒度的(至少大多數情況如此)。我最初這樣做是出于性能考慮。但是,使用不同大小的 CA 布局可以獲得有趣的像素效果。
清單 6 顯示了 getNewRGB(Color) 的一種特殊實現。它計算 “RGB 互補(complement)”,但這不是實際的顏色互補(計算真正顏色互補的過濾器也很有趣,但將其編寫成代碼則沒有這么簡單)。
清單 6. RGBComplementFilter 類(部分清單)
protected int[] getNewRGB(Color c) { int red = c.getRed(); int newRed = getComplement(red); int green = c.getGreen(); int newGreen = getComplement(green); int blue = c.getBlue(); int newBlue = getComplement(blue);
return new int[] { newRed, newGreen, newBlue }; }
private int getComplement(int colorVal) { // 'Reflect' colorVal across the mid-point 128. int maxDiff = colorVal >= 128 ? -colorVal : 255 - colorVal; // Divide by 2.0 to make the effect more subtle. Could also just use // maxDiff for a more garish effect. int diff = (int) Math.round(maxDiff / 2.0); int newColorVal = colorVal + diff;
return newColorVal; }
|
我已經擴展 getNewRGB(Color),使其不僅可以傳入要轉換的像素顏色,而且可以傳入 8 個鄰居像素的顏色。這允許我創建某些效果,比如模糊效果或檢測邊緣,其中過濾的像素顏色取決于它的鄰居的顏色。這將是一個很好的增強功能。
最后,我將配合 CA 時鐘更新圖像來動畫圖像。為此,我使用了一個 javax.swing.Timer(這是制作變化圖像動畫的簡單方式,但不是最好的方式。Jonathan Knudsen 的著作 Java 2D Graphics 提供了一種更好更復雜的方式來創建動畫。
運行 Seurat
圖 1 是 Georges Seurat 于 1884 年創作的點畫法名作 “A Sunday Afternoon on the Island of La Grand Jatte” 的照片:
圖 1. Georges Seurat 的 “A Sunday Afternoon on the Island of La Grand Jatte”

現在我將使用 RGB 互補過濾器在 Seurat 圖畫上運行 Seurat 應用程序。圖 2 顯示了過濾后的圖畫,此時循環空間處于它的初始隨機狀態:
圖 2. 使用循環空間過濾圖畫的無組織隨機狀態

圖 3 顯示了過濾后的圖畫,此時循環空間開始進入有序模式,但仍然帶有很大的隨機性:
圖 3. 使用循環空間過濾圖畫的中間狀態

圖 4 顯示了過濾圖畫的最終穩定狀態:
 |
過程藝術與算法藝術
為編寫 Seurat(我最初稱它為 “Blue Poles”),我閱讀了大量有關 Jackson Pollock 的資料。我總是會碰到術語過程藝術。我天真地認為它表示基于算法或規則的藝術,藝術家據此設計一組規則并根據某些初始條件運行它們,從而創作出藝術作品,如畫作。但我發現過程藝術與藝術世界中最普通的含義大不相同。根據 Guggenheim Collection 術語,它的含義如下:
“
過程藝術強調藝術創作的 ‘過程’(而不是預訂的作品或計劃)以及變化和稍縱即逝的效果,如一些藝術家在他們作品中闡述的那樣,包括 Lynda
Benglis、Eva Hesse、Robert Morris、Bruce Nauman、Alan Saret、Richard
Serra、Robert Smithson 和 Keith Sonnier?!?/p>
因此,如果算法(或程序)的中間狀態具有藝術趣味,那么基于算法和規則的藝術可以被認為是一種過程藝術形式,而 算法則是運行過程 。
作為程序員,我們只適合創建過程藝術或幫助藝術家創建它。
|
|
圖 4. 使用循環空間在穩定狀態下過濾的圖畫
不過,靜態圖片不能真正實現過濾器/CA(畢竟,這個應用程序是為動畫 靜態圖像而編寫的)。我建議您運行實際的 Java applet 來查看運行中的過濾器/CA(請參閱 參考資料,獲得即時 demo 的鏈接)。
審美注意事項
一
些人可能會認為在 “A Sunday Afternoon on the Island of La Grand Jatte”
之類的偉大作品上運行圖像過濾器應用程序是一種褻瀆。我當然很贊同此觀點。但我只是以這幅畫為例子。我的主要目標是展示如何使用一種簡單的細胞自動機器,
以有趣而復雜的方式來制作圖像動畫,以一副熟悉的名畫作為例子會比較好。
我曾在許多類型的畫上運行過
Seurat,在抽象藝術和具象藝術方面都得到了有趣的結果。但是,似乎在現代藝術 — 特別是流行藝術方面效果更好。例如,當您在 Jasper
Johns 的 “Flag” 畫上運行 Seurat 時,會出現有趣的圖案。循環空間的對角線能根據 “Flag” 畫中的直線很好地工作。在
Jackson Pollock 的水滴畫中,運行 Seurat 時也會產生有趣的結果。例如,隨著循環空間 CA 越過 Pollock 的
“Blue Poles”,它會隱藏、顯示、再隱藏這幅復雜畫作的細節,讓您在不同時間集中注意不同的部位。這對照片同樣適用。我喜歡在 Ralph
Eugene Meatyard 超現實主義的照片上運行 Seurat。
在運行 Seurat 這樣的應用程序時,您有
3 種選擇:2D 細胞自動機類型、過濾器和原始圖像。在這篇文章中,我只使用了循環空間,但是也可以使用其他類型的 2D 細胞自動機(如
Hodgepodge)。只要發揮您的想象力,就能編寫出各種過濾器程序。我主要實踐了操作顏色的過濾器,但更改圖像空間關系的過濾器也很有趣。例如,您
可以編寫一個歪曲圖像表面的過濾器程序,創建類似于披頭士的 Rubber Soul 專輯的封面那種效果。最后,您可以使用任意圖像,比如照片。對于給定的圖像,各種過濾器和 CA 類型的組合可以生成更好或更差的結果。我希望本文能鼓起您體驗的欲望。
致謝
我對 Julia Braswell 在視覺藝術方面的幫助表示衷心的感謝!
關于作者
 |
|

|
 |
Paul
Reiners 是一位 Sun 認證的 Java 程序員和開發人員。他參與開發了幾個開放源碼程序,包括 Automatous
Monk、Twisted Life 和 Leipzig。Reiners 于 1991 年 5 月獲得伊利諾斯大學
Urbana-Champaign
分校的應用數學(計算理論)碩士學位。他目前住在明尼蘇達州,在業余時間喜歡演奏低音電吉他,并且是一個爵士樂隊的成員。
|