Java 中的快速高斯模糊實現
1. 引言
高斯模糊是一種廣泛使用的影像處理技術,用於降低影像雜訊和保留細節。然而,實現精確的高斯模糊計算量可能很大,尤其對於具有較大模糊半徑的高解析度影像而言。
在本教程中,我們將探索更快、運算效率更高的 Java 高斯模糊實作方案。
2. 問題概述
我們透過將灰階影像與二維高斯核進行卷積來實現標準的高斯模糊。在此過程中,對於影像中的每個像素,演算法首先檢查其周圍的像素網格(即核)。然後,它將每個像素的強度乘以一個特定的權重。最後,將所有乘積相加。
對於一個包含n像素、模糊半徑為r的影像,我們的捲積核大小為(2r + 1) x (2r + 1) 。因此,標準的二維卷積演算法的時間複雜度為O(nr 2 ) 。隨著模糊半徑r增大,效能呈現二次方下降。因此,它並不適用於大圖像的即時處理。
3. 快速近似演算法
我們的快速近似方法對標準的二維高斯模糊方法進行了兩個關鍵改進。
3.1. 可分離過濾器
二維高斯核可以看成是兩個一維高斯向量。換句話說,我們可以將它們分解為水平向量和垂直向量。因此,與其對影像的每個像素應用二維矩陣,不如先對所有行應用水平方向的一維模糊,再對所有列應用垂直方向的一維模糊,即可達到相同的效果。
這種簡單的分離將我們的平均情況時間複雜度從O(nr 2 )降低到O(nr) 。
3.2. 重複框模糊
我們重複進行特定的模糊操作(盒狀模糊)以達到所需的效果。
我們將盒狀模糊定義為最簡單的模糊形式,即模糊半徑內的每個像素權重相等。然後,我們利用中心極限定理來近似一個有效且匹配的高斯分佈。根據中心極限定理,多次應用簡單的盒狀模糊可以很好地近似一個精確的高斯分佈。我們連續運行五次盒狀模糊,最終得到與嚴格高斯模糊在視覺上無法區分的結果,而計算開銷卻大大降低。
此外,我們將 Box Blur 中的所有權重設為相等,因此無需對每個像素重新求和整個半徑。這使我們能夠自由地使用滑動視窗或移動平均。當視窗向右移動一個像素時,我們減去離開的像素並加上進入的像素。這使得平均時間複雜度降至O(n) ,從而使效能完全與模糊半徑r!
4. 解決方案
讓我們來看解決方案。
4.1. 滑動視窗框模糊
首先,我們定義一個函數horizontalBoxBlur() ,它使用滑動視窗技術執行 1D 水平模糊。
這裡,我們逐行處理圖像。我們不會在每個位置重新計算像素總和。相反,我們初始化每行第一個像素的windowSum值。從左到右,它減去半徑後緣之後像素的值,並加上半徑前緣像素的值。
最後,我們對邊緣進行箝制,以確保如果半徑超出影像邊界,我們的邏輯會複製邊緣像素,而不是拋出ArrayIndexOutOfBoundsException異常。
private static void horizontalBoxBlur(int[] source, int[] target, int width, int height, int radius) {
double scale = 1.0 / (radius * 2 + 1);
for (int y = 0; y < height; y++) {
int windowSum = 0;
int offset = y * width;
for (int x = -radius; x <= radius; x++) {
int safeX = Math.min(Math.max(x, 0), width - 1);
windowSum += source[offset + safeX];
}
for (int x = 0; x < width; x++) {
target[offset + x] = (int) Math.round(windowSum * scale);
int leftX = Math.max(x - radius, 0);
int rightX = Math.min(x + radius + 1, width - 1);
windowSum -= source[offset + leftX];
windowSum += source[offset + rightX];
}
}
}
verticalBoxBlur()的邏輯與此相同,但這裡我們遍歷的是列而不是行(從上到下) :
private static void verticalBoxBlur(int[] source, int[] target, int width, int height, int radius) {
double scale = 1.0 / (radius * 2 + 1);
for (int x = 0; x < width; x++) {
int windowSum = 0;
for (int y = -radius; y <= radius; y++) {
int safeY = Math.min(Math.max(y, 0), height - 1);
windowSum += source[safeY * width + x];
}
for (int y = 0; y < height; y++) {
target[y * width + x] = (int) Math.round(windowSum * scale);
int topY = Math.max(y - radius, 0);
int bottomY = Math.min(y + radius + 1, height - 1);
windowSum -= source[topY * width + x];
windowSum += source[bottomY * width + x];
}
}
}
4.2 集成
這是我們的統籌者。
利用中心極限定理,我們對資料進行三次水平和垂直方向的盒狀模糊處理(可配置)。這可以平滑尖峰區域,從而產生接近真實高斯鐘形曲線的完美近似值。它使用臨時數組( temp )在處理過程中安全地傳遞數據,而不會損壞來源資料:
public static int[] applyFastGaussianBlur(int[] source, int numPasses, int width, int height, int radius) {
int[] target = new int[source.length];
int[] temp = new int[source.length];
System.arraycopy(source, 0, target, 0, source.length);
for (int i = 0; i < numPasses; i++) {
horizontalBoxBlur(target, temp, width, height, radius);
verticalBoxBlur(temp, target, width, height, radius);
}
return target;
}
5. 測試
5.1. 使用脈衝影像進行測試
首先,我們使用脈衝影像(僅包含一個黑色像素的人工影像)來測試這種方法。我們的主要目標是驗證我們的演算法是否能夠正確處理邊緣並應用平滑效果:
void givenSharpImage_whenAppliedBlur_thenCenterIsSmoothed() {
int width = 5;
int height = 5;
int numPasses = 5;
int[] image = new int[width * height];
image[12] = 255;
int[] blurredImage = FastGaussianBlur.applyFastGaussianBlur(image, numPasses, width, height, 1);
assertTrue(blurredImage[12] < 255);
assertTrue(blurredImage[12] > 0);
assertTrue(blurredImage[11] > 0); // Left neighbor
assertTrue(blurredImage[13] > 0); // Right neighbor
}
我們首先建立一個 5×5 的影像(25 像素),並將所有像素值設為 0,使其變為純黑色。然後,我們將影像中心像素(索引 12)的值設為 255,使其變為純白色。這個尖銳的點就是我們的脈衝或峰值。接下來,我們應用半徑為 1 的快速模糊演算法。
我們的主要論點是,在正確應用模糊效果後,中心像素的亮度會向外擴散到周圍的黑色像素。此外, assertTrue(blurredImage[12] < 255)證實了中心像素變暗是因為它分享了中心像素的亮度;而assertTrue(blurredImage[11] > 0) ` 和assertTrue(blurredImage[13] > 0)則證實了緊鄰的像素兩側的像素(兩者皆為黑色亮度)。
5.2. 使用真實影像進行測試
我們選取一張開源圖片,並使用我們的演算法對其進行模糊處理。以下是範例圖片:
請查看模糊版本:
我們使用單通道整數值作為脈衝影像(灰階)測試案例。本用例使用真實圖像。每張真實影像都包含 32 位元 ARGB(Alpha、Red、Green、Blue)資料。因此,我們首先提取 ARGB 通道,然後分別對每個 RGB 通道進行模糊處理,最後將它們重新組合在一起:
public static BufferedImage blurRealImage(@Nonnull BufferedImage image, int radius, int numPasses) {
int width = image.getWidth();
int height = image.getHeight();
int[] pixels = image.getRGB(0, 0, width, height, null, 0, width);
int[] a = new int[pixels.length];
int[] r = new int[pixels.length];
int[] g = new int[pixels.length];
int[] b = new int[pixels.length];
for (int i = 0; i < pixels.length; i++) {
a[i] = (pixels[i] >> 24) & 0xff;
r[i] = (pixels[i] >> 16) & 0xff;
g[i] = (pixels[i] >> 8) & 0xff;
b[i] = pixels[i] & 0xff;
}
r = FastGaussianBlur.applyFastGaussianBlur(r, width, height, radius, numPasses);
g = FastGaussianBlur.applyFastGaussianBlur(g, width, height, radius, numPasses);
b = FastGaussianBlur.applyFastGaussianBlur(b, width, height, radius, numPasses);
int[] resultPixels = new int[pixels.length];
for (int i = 0; i < pixels.length; i++) {
resultPixels[i] = (a[i] << 24) | (r[i] << 16) | (g[i] << 8) | b[i];
}
BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
result.setRGB(0, 0, width, height, resultPixels, 0, width);
return result;
}
6. 結論
本文介紹了一種更快的 Java 高斯模糊方法。我們對標準的高斯模糊實作進行了兩項關鍵改進,使其時間複雜度達到線性。首先,我們將二維卷積矩陣分解為可分離的一維數組;其次,我們使用了移動平均滑動窗口,並進行了三次盒狀模糊處理。因此,我們的解決方案具有恆定的處理時間,且與模糊半徑無關。
和往常一樣,完整的程式碼範例可以在 GitHub 上找到。