拉普拉斯金字塔融合是多圖融合相關算法裏最簡單和最容易實現的一種,我們在看網絡上大部分的文章都是在拿那個蘋果和橙子融合在一起,變成一個果橙的效果作爲例子說明。在這方面確實融合的比較好。但是本文我們主要講下這個在圖像增強方面的運用。
首先我們還是來講下這個融合的過程和算法優化。
算法第一步:輸入兩個相同大小,位深的圖像,通過拉普拉斯分解得到各自的拉普拉斯金字塔數據A和B。
算法第二步:選擇下低頻部分的融合規則,這裏的低頻部分,其實就是高斯金字塔最頂層那裏的數據,這個數據相當於是原圖像的一個高斯模糊的下采樣版本,反應了基本的圖像輪廓和信息。
通常情況下,融合規則有三種:
(1)選擇A;
(2)選擇B;
(3)選擇A和B的平均值。
算法第三步:選擇高頻部分的融合規則。高頻代表了圖像的邊緣和細節,當然也可能是噪音。高頻部分的數據保存在各自拉普拉斯金字塔的除最頂層外的層中(最頂層和高斯金字塔的最頂層共享數據)。
這裏的融合規則就有多種,常用的比如如下幾種:
(1)選擇A和B中絕對值最大的;
(2)選擇A和B領域中絕對值最大的。
第一種規則比較容易理解,絕對值大(拉普拉斯金字塔數據有可能是負值得),表示這裏的邊緣強度越高,細節越豐富。
第二種也好理解,通常我們選擇3*3領域。A的3*3領域的絕對值最大值如果大於B的3*3領域最大值,則選擇A,否則選擇B,這種做法的道理就是用領域去除一定的噪音影響。通常伴隨着該種方法的還有一個叫做一致性檢測的過程,即如果中心位置的融合係數選自原圖像A變換的係數,而其周圍領域內的融合係數大部分都選取自原圖像B變換的係數 ,則把中心位置的融合係數修改爲圖像B變換後的係數,反之亦然。
那還有一種基於基於3X3窗口內相似性測度,獲取拉普拉斯金字塔融合結果的方法,這個可以參考: https://wenku.baidu.com/view/c8ae11adf61fb7360b4c65c4.html ,這種融合規則由於考慮了與相鄰像素間的相關性,降低了對邊緣的敏感性,能夠有效減少融合像素的錯誤選取,在一定程度上顯著提高了融合算法的魯棒性,從而提高了融合效果。 實測這種也還可以,但是代碼比較麻煩,這裏不描述。
算法第四步:高頻和低頻都已經處理好後,則重構圖像得到結果值。
一個簡單的描述過程如下:
int IM_LaplacePyramidFusion(unsigned char *SrcA, unsigned char *SrcB, unsigned char *Dest, int Width, int Height, int Stride, LowFrequencyFusionRule Low, HighFrequencyFusionRule High, int Level)
{
int Channel = Stride / Width;
if ((SrcA == NULL) || (SrcB == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != 1) && (Channel != 3) && (Channel != 4)) return IM_STATUS_INVALIDPARAMETER;
int Status = IM_STATUS_OK;
Level = IM_ClampI(Level, 1, IM_GetMaxPyramidLevel(Width, Height, 1)); // 經過測試如果直接處理到最小爲1個像素的金子塔,效果並不好
Pryamid *GaussPyramid = (Pryamid *)calloc(Level, sizeof(Pryamid)); // 必須用calloc,不然在後面的釋放函數中可能存在野指針釋放問題,高斯金字塔可以用同一個內存
Pryamid *LaplacePyramidA = (Pryamid *)calloc(Level, sizeof(Pryamid)); // 圖A的拉普拉斯金字塔
Pryamid *LaplacePyramidB = (Pryamid *)calloc(Level, sizeof(Pryamid)); // 圖B的拉普拉斯金字塔
Pryamid *GaussPyramidD, *LaplacePyramidD;
if ((GaussPyramid == NULL) || (LaplacePyramidA == NULL) || (LaplacePyramidB == NULL))
{
Status = IM_STATUS_OUTOFMEMORY;
goto FreeMemory;
}
Status = IM_AllocatePyramidMemory(GaussPyramid, Width, Height, Stride, Level, true, sizeof(unsigned char)); // 分配內存,高斯金字塔的塔底就是原數據
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_AllocatePyramidMemory(LaplacePyramidA, Width, Height, Stride, Level, false, sizeof(unsigned char));
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_AllocatePyramidMemory(LaplacePyramidB, Width, Height, Stride, Level, false, sizeof(unsigned char));
if (Status != IM_STATUS_OK) goto FreeMemory;
GaussPyramid[0].Data = SrcA;
for (int Y = 1; Y < Level; Y++) // 各級高斯金字塔
{
Status = IM_DownSample8U((unsigned char *)GaussPyramid[Y - 1].Data, (unsigned char *)GaussPyramid[Y].Data, GaussPyramid[Y - 1].Width, GaussPyramid[Y - 1].Height, GaussPyramid[Y - 1].Stride, GaussPyramid[Y].Width, GaussPyramid[Y].Height, GaussPyramid[Y].Stride, Channel);
if (Status != IM_STATUS_OK) goto FreeMemory;
}
// 拉普拉斯金子塔的最頂層和高斯金字塔的是一樣的額
memcpy(LaplacePyramidA[Level - 1].Data, GaussPyramid[Level - 1].Data, LaplacePyramidA[Level - 1].Height * LaplacePyramidA[Level - 1].Stride);
for (int Y = Level - 2; Y >= 0; Y--)
{
Status = IM_UpSampleSub8U((unsigned char *)GaussPyramid[Y + 1].Data, (unsigned char *)GaussPyramid[Y].Data, (unsigned char *)LaplacePyramidA[Y].Data, GaussPyramid[Y + 1].Width, GaussPyramid[Y + 1].Height, GaussPyramid[Y + 1].Stride, GaussPyramid[Y].Width, GaussPyramid[Y].Height, GaussPyramid[Y].Stride, Channel);
if (Status != IM_STATUS_OK) goto FreeMemory;
}
GaussPyramid[0].Data = SrcB;
for (int Y = 1; Y < Level; Y++) // 各級高斯金字塔
{
Status = IM_DownSample8U((unsigned char *)GaussPyramid[Y - 1].Data, (unsigned char *)GaussPyramid[Y].Data, GaussPyramid[Y - 1].Width, GaussPyramid[Y - 1].Height, GaussPyramid[Y - 1].Stride, GaussPyramid[Y].Width, GaussPyramid[Y].Height, GaussPyramid[Y].Stride, Channel);
if (Status != IM_STATUS_OK) goto FreeMemory;
}
// 拉普拉斯金子塔的最頂層和高斯金字塔的是一樣的額
memcpy(LaplacePyramidB[Level - 1].Data, GaussPyramid[Level - 1].Data, LaplacePyramidB[Level - 1].Height * LaplacePyramidB[Level - 1].Stride);
for (int Y = Level - 2; Y >= 0; Y--)
{
Status = IM_UpSampleSub8U((unsigned char *)GaussPyramid[Y + 1].Data, (unsigned char *)GaussPyramid[Y].Data, (unsigned char *)LaplacePyramidB[Y].Data, GaussPyramid[Y + 1].Width, GaussPyramid[Y + 1].Height, GaussPyramid[Y + 1].Stride, GaussPyramid[Y].Width, GaussPyramid[Y].Height, GaussPyramid[Y].Stride, Channel);
if (Status != IM_STATUS_OK) goto FreeMemory;
}
LaplacePyramidD = LaplacePyramidA;
// 低頻部分的融合
PyramidLowFreqFusion((unsigned char *)LaplacePyramidA[Level - 1].Data, (unsigned char *)LaplacePyramidB[Level - 1].Data, (unsigned char *)LaplacePyramidD[Level - 1].Data, LaplacePyramidA[Level - 1].Width, LaplacePyramidA[Level - 1].Height, LaplacePyramidA[Level - 1].Stride, Low);
for (int Y = 0; Y < Level - 1; Y++) // 高頻部分的融合
{
if (High == SinglePixelAbsMax)
PyramidHighFreqFusion_AbsMax((unsigned char *)LaplacePyramidA[Y].Data, (unsigned char *)LaplacePyramidB[Y].Data, (unsigned char *)LaplacePyramidD[Y].Data, LaplacePyramidA[Y].Width, LaplacePyramidA[Y].Height, LaplacePyramidA[Y].Stride);
else if (High == LocalAbsMaxWithConsistencyCheck)
PyramidHighFreqFusion_3X3MaxAbsValue((unsigned char *)LaplacePyramidA[Y].Data, (unsigned char *)LaplacePyramidB[Y].Data, (unsigned char *)LaplacePyramidD[Y].Data, LaplacePyramidA[Y].Width, LaplacePyramidA[Y].Height, LaplacePyramidA[Y].Stride);
// else
//PyramidHighFreqFusion_3X3Similarity(LaplacePyramidA[Y], LaplacePyramidB[Y], LaplacePyramidD[Y], PryamidW[Y], PryamidH[Y], Channel);*/
}
GaussPyramidD = GaussPyramid;
memcpy(GaussPyramidD[Level - 1].Data, LaplacePyramidD[Level - 1].Data, LaplacePyramidD[Level - 1].Height * LaplacePyramidD[Level - 1].Stride);
for (int Y = Level - 2; Y >= 0; Y--) // 重構拉普拉斯金子塔
{
if (Y != 0)
IM_UpSampleAdd8U((unsigned char *)GaussPyramidD[Y + 1].Data, (unsigned char *)LaplacePyramidD[Y].Data, (unsigned char *)GaussPyramidD[Y].Data, GaussPyramidD[Y + 1].Width, GaussPyramidD[Y + 1].Height, GaussPyramidD[Y + 1].Stride, GaussPyramidD[Y].Width, GaussPyramidD[Y].Height, GaussPyramidD[Y].Stride, Channel);
else
IM_UpSampleAdd8U((unsigned char *)GaussPyramidD[Y + 1].Data, (unsigned char *)LaplacePyramidD[Y].Data, Dest, GaussPyramidD[Y + 1].Width, GaussPyramidD[Y + 1].Height, GaussPyramidD[Y + 1].Stride, GaussPyramidD[Y].Width, GaussPyramidD[Y].Height, GaussPyramidD[Y].Stride, Channel);
}
FreeMemory:
IM_FreeGaussPyramid(GaussPyramid, Level, true);
IM_FreeLaplacePyramid(LaplacePyramidA, Level);
IM_FreeLaplacePyramid(LaplacePyramidB, Level);
if (GaussPyramid != NULL) free(GaussPyramid);
if (LaplacePyramidA != NULL) free(LaplacePyramidA);
if (LaplacePyramidB != NULL) free(LaplacePyramidB);
return Status;
}
我們上面的所有的高斯或者拉普拉斯金字塔數據都是用unsigned char類型來描述的, 爲什麼可以這樣做呢,做個簡單的分析。第一,高斯金字塔用byte是毫無疑問的,第二,前面說了,嚴格的拉普拉斯金字塔是有負數的,但是我們考慮到一個這個負數大於-127的可能性是非常小的,這種情況可能會在二值圖像中出現,而二值圖的處理算法中能用到金字塔嗎,我似乎沒聽說過,所以我們可以把拉普拉斯金字塔的數據加上127,讓整體在0和255之間,這樣有很多算法都直接調用了。
在我們的高頻或者低頻的選取過程中,因爲都不存在新的數據出來,也就是沒有啥幾何乘積計算,因此,用byte保存也不存在啥大問題。
PyramidLowFreqFusion低頻部分的融合代碼非常簡單,如下所示:
/// 低頻部分的融合,一般有三種方式。1、取圖像A的係數; 2、取圖像B的係數;3、取圖像A和個B係數的平均值。
/// 其實這裏應該用高斯金字塔的最頂層數據,只是由於拉普拉斯和高斯金字塔共享這一層數據,所以也可以直接這樣寫
int PyramidLowFreqFusion(unsigned char *LaplacePyramidA, unsigned char *LaplacePyramidB, unsigned char *LaplacePyramidD, int Width, int Height, int Stride, LowFrequencyFusionRule Low)
{
int Channel = Stride / Width;
if ((LaplacePyramidA == NULL) || (LaplacePyramidB == NULL) || (LaplacePyramidD == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != 1) && (Channel != 3) && (Channel != 4)) return IM_STATUS_INVALIDPARAMETER;
if (Low == SrcA)
memcpy(LaplacePyramidD, LaplacePyramidA, Height * Stride);
else if (Low == SrcB)
memcpy(LaplacePyramidD, LaplacePyramidB, Height * Stride);
else
{
// 也可以考慮某一副圖佔比高一點
//int BlockSize = 16, Block = (Height * Stride) / BlockSize;
//for (int Y = 0; Y < Block * BlockSize; Y += BlockSize)
//{
// __m128i SrcA = _mm_loadu_si128((__m128i *)(LaplacePyramidA + Y));
// __m128i SrcB = _mm_loadu_si128((__m128i *)(LaplacePyramidB + Y));
// __m128i Dst1 = _mm_srli_epi16(_mm_add_epi16(_mm_mullo_epi16(_mm_cvtepu8_epi16(SrcA), _mm_set1_epi16(3)), _mm_cvtepu8_epi16(SrcA)), 2);
// __m128i Dst2 = _mm_srli_epi16(_mm_add_epi16(_mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_srli_si128(SrcA, 8)), _mm_set1_epi16(3)), _mm_cvtepu8_epi16(_mm_srli_si128(SrcA, 8))), 2);
// _mm_storeu_si128((__m128i *)(LaplacePyramidD + Y), _mm_packus_epi16(Dst1, Dst2));
//}
//for (int Y = Block * BlockSize; Y < Height * Stride; Y++)
//{
// LaplacePyramidD[Y] = (LaplacePyramidA[Y] * 3 + LaplacePyramidB[Y]) >> 2; // 最高層(低頻)係數取平均
//}
// **************************** 真正意義上的平均值 *************************************
int BlockSize = 16, Block = (Height * Stride) / BlockSize;
for (int Y = 0; Y < Block * BlockSize; Y += BlockSize)
{
_mm_storeu_si128((__m128i *)(LaplacePyramidD + Y), _mm_avg_epu8(_mm_loadu_si128((__m128i *)(LaplacePyramidA + Y)), _mm_loadu_si128((__m128i *)(LaplacePyramidB + Y))));
}
for (int Y = BlockSize * BlockSize; Y < Height * Stride; Y++)
{
LaplacePyramidD[Y] = (LaplacePyramidA[Y] + LaplacePyramidB[Y]) >> 1; // 最高層(低頻)係數取平均
}
}
return IM_STATUS_OK;
}
取平均直接用_mm_avg_epu8就可以了。
高頻部分如果選擇絕對值最大的方案代碼也是很簡單的:
/// 基於係數絕對值取大的融合策略進行拉普拉斯金字塔圖像融合。
int PyramidHighFreqFusion_AbsMax(unsigned char *LaplacePyramidA, unsigned char *LaplacePyramidB, unsigned char *LaplacePyramidD, int Width, int Height, int Stride)
{
int Channel = Stride / Width;
if ((LaplacePyramidA == NULL) || (LaplacePyramidB == NULL) || (LaplacePyramidD == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != 1) && (Channel != 3) && (Channel != 4)) return IM_STATUS_INVALIDPARAMETER;
int BlockSize = 16, Block = (Height * Stride) / BlockSize;
__m128i C127 = _mm_set1_epi8(127);
for (int Y = 0; Y < Block * BlockSize; Y += BlockSize)
{
__m128i SrcA = _mm_loadu_si128((__m128i *)(LaplacePyramidA + Y));
__m128i SrcB = _mm_loadu_si128((__m128i *)(LaplacePyramidB + Y));
__m128i Flag = _mm_cmpgt_epu8(_mm_absdiff_epu8(SrcA, C127), _mm_absdiff_epu8(SrcB, C127));
_mm_storeu_si128((__m128i *)(LaplacePyramidD + Y), _mm_blendv_epi8(SrcB, SrcA, Flag));
}
for (int Y = Block * BlockSize; Y < Height * Stride; Y++)
{
if (IM_Abs(LaplacePyramidA[Y] - 127) > IM_Abs(LaplacePyramidB[Y] - 127))
LaplacePyramidD[Y] = LaplacePyramidA[Y];
else
LaplacePyramidD[Y] = LaplacePyramidB[Y];
}
return IM_STATUS_OK;
}
其中的_mm_absdiff_epu8函數如下所示:
// 返回8位字節數數據的差的絕對值
inline __m128i _mm_absdiff_epu8(__m128i a, __m128i b)
{
return _mm_or_si128(_mm_subs_epu8(a, b), _mm_subs_epu8(b, a));
}
這裏要減去127的主要原因是前面所說的再計算拉普拉斯金字塔時我們增加了127,而這裏計算時我們需要真正的拉普拉斯金子塔數據。
用_mm_blendv_epi8可以方便的解決後續的抉擇問題,有點相當於C語言的裏的三目運算符。
關於PyramidHighFreqFusion_3X3MaxAbsValue這個函數就要複雜很多了,首先這種3*3的領域計算,我還是推薦我在sobel邊緣算子優化一文中提到的那種優化結構,可以支持In-Place操作,同時還可以完美處理邊緣。算法的流程是標準化的。就是求出各自3*3領域的絕對值的最大值,然後進行比較,爲了後續的一致性檢測,比較的結果需要寫入個臨時內存,在實現時,我們做了如下處理:
_mm_storeu_si128((__m128i *)(LinePF + X), _mm_cmpgt_epu8(MaxA, MaxB));
其中的MaxA和MaxB爲領域的最大值,這裏也就是說A>B,對應的Flag位置設置爲255,否則設置爲0(也是用的unsigned char內存保存的)。
爲什麼這樣做,有兩個好處,第一,我們在後續的一致性檢測裏,可以充分利用這個數據的特殊性。在一致性檢測裏,我們要判斷周邊的是不是大部分都和中心點來自同一個數據源,一種處理方式就是把周邊的8個點的數據都相加,如果中心點爲0,周邊的和大於255*4,則表明周邊和中心不太一致,需要把中心的點改爲255,如果中心點爲255,而周邊的點的和小於255*4,則中心點要改爲0,用代碼表示如下:
int Sum = First[X + 0] + First[X + 1] + First[X + 2] + Second[X + 0] + Second[X + 2] + Third[X + 0] + Third[X + 1] + Third[X + 2];
if (LinePD[X] == 0 && Sum > 255 * 4) // 如果當前點爲0,並且周邊8個點中至少有5個點爲1,則把當前點的值修改爲1。 LinePD[X] = 255; else if (LinePD[X] == 255 && Sum < 255 * 4) // 如果當前點爲1,並且周邊8個點中至少有5個點爲0,則把當前點的值修改爲0。 LinePD[X] = 0;
第二,在SSE優化時,這個特殊性是可以幫上大忙的,主要體現在兩個方面,一時如上的Sum計算過程,我們如果直接按照255相加,則8個數會超出8位所表達的範圍,這樣就要轉換到16位的空間進行計算了,但是如果我們把epu8看成epi8,這個時候255就編程了-1,此時的加法我使用_mm_add_epi8,則結果能在epi8的範圍內,這樣一次性就可以處理16個像素了。二是後續我們需要根據這個Flag對組中的輸出結果做明示,這個時候我們就可以直接使用這個Flag做mask供_mm_blendv_epi8調用。
我們在倆看看上面的判斷部分如何用SSE處理,因爲SSE不善於做分支,所以我們需要想辦法,這樣做,我們看看下面的代碼是不是和上面的一個意思:
// if ((LinePD[X] == 0 && Sum > 255 * 4) || ((LinePD[X] == 255 && Sum < 255 * 4))) // LinePD[X] = 255 - LinePD[X];
但是這裏是有不同的,我們可以很方便的對上述代碼SSE處理:
__m128i FlagA = _mm_and_si128(_mm_cmpeq_epi8(Current, _mm_setzero_si128()), _mm_cmplt_epi8(Sum, _mm_set1_epi8(-4))); __m128i FlagB = _mm_and_si128(_mm_cmpeq_epi8(Current, _mm_set1_epi8(255)), _mm_cmpgt_epi8(Sum, _mm_set1_epi8(-4))); __m128i FlagAB = _mm_or_si128(FlagA, FlagB); _mm_storeu_si128((__m128i *)(LinePD + X), _mm_blendv_epi8(Current, _mm_subs_epu8(_mm_set1_epi8(255), Current), FlagAB)); // 局部取反
這樣效率可以大大的提高。
優化方面基本講完了,當然,由於我沒有共享代碼,大部分其實是寫給自己看的,因爲我怕時間長了自己都不知道爲什麼要這樣寫。
下面我們測試下算法效果和性能:
SrcA SrcB
低頻=SrcA,高頻=3*3領域, Level = 10 低頻=SrcB,高頻=3*3領域, Level = 10
低頻=(SrcA + SrcB) / 2,高頻=3*3領域, Level = 10 低頻=(SrcA + SrcB) / 2,高頻=3*3領域, Level =5
上述原圖B是某個增強算法處理後的結果,很明顯,改算法對圖像右下角的暗部的增強效果很好,但是同時圖像上部的天空區域已經完全過曝了,天空的雲消失不見了,這在很多增強算法中都會出現類似的情況,而在原圖中天空的細節本身就已經比較好了,因此,我們嘗試用不同的選項對這兩幅圖做拉普拉斯融合,如果高頻選擇SrcA,則整體融合後的圖像暗部增強的不明顯,選擇SrcB,則天空恢復的不夠好,選擇(SrcA + SrcB) / 2則能對天空和暗部都有較好的恢復。
另外,金字塔的層數對結果也有一定的影響,在最後兩張圖中,我們可以看到Level=5時的效果要比爲10時的稍微好一點,我們一般也不建議高斯金字塔的最頂層取得太小,通常,我們取5層金字塔應該能獲得較爲滿意的效果。
效率方面,一般1920*1080的彩色圖像,這種混合大概需要20-30ms左右(取決於選擇的參數),一半的時間用於了金字塔的構建。
其他說明:
1、在PyramidLowFreqFusion函數中,我們註釋掉了一部分,這部分註釋的代碼的本意是低頻的算法我們不一定一定要是取平均值,也可以根據實際的情況更加強調某一對象,比如假如SrcB是處理後的部分,我們可以把他的權重設置爲75%,而SrcA的權重設置爲25%。
2、對於彩色圖像,如果三個通道獨立寫,則對每個像素,有可能每個通道的高頻或低頻部分會選自不同的來源,這樣有可能導致結果出現異常的彩色,一種解決方案是採用高頻或低頻部分的灰度信號作爲判斷源。
3、金子塔融合的基本原理還是保留更多的細節,因此,如果對一個原始圖像進行了類似銳化方面的處理後,這個圖在和原圖進行融合,那基本上不會有什麼變化的,柔和的結果必然是靠近銳化後的結果圖。這個大家可以自己做實驗。
4、這個融合的過程基本不需要外接的參數接入,我們可以考慮把他作爲某些算法的最後一個默認步驟。
5、對於任意兩幅大小相同的圖,這個算法融合的結果也是蠻有意思的,如下:
當然,這種融合還是有一定的限制的,下一節,我們將討論基於蒙版的金字塔融合,那裏可以更加智能的獲取更好的融合效果。
提供一個DEMO供測試效果:極度優化版本工程:https://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,見MultiImage->LaplacePyramidFusion菜單。