下面介紹一種基于圖像模板的拼接方法坟奥,使用該方法有以下前提:
- 明確圖像之間存在重合區(qū)域。
- 圖像不存在明顯的尺度變化和畸變能耻。
我們可以考慮以下場景帆竹,一臺做網(wǎng)格化運動的攝像機绕娘,每若干毫秒在一個網(wǎng)格上拍攝一張照片,控制這些照片之間有視野重疊栽连。并且網(wǎng)格的數(shù)量在幾百乃至上千之間险领,使用特征匹配法性能有可能會達(dá)不到要求侨舆,但是可以嘗試下本文介紹的基于模板匹配的拼接方法。
兩張圖拼接方法:
假定圖像有先后順序绢陌,左圖在先挨下,右圖在后,并且兩張圖的重疊區(qū)域不小于下面將要設(shè)定的圖像模板的尺寸脐湾。
步驟如下:
- 從左圖右邊緣某個位置扣取一塊區(qū)域做為圖像模板臭笆。
- 使用模板匹配算法在右圖中執(zhí)行類卷積操作,計算相似性矩陣秤掌。
- 在相似性矩陣中定位最匹配的位置愁铺。
- 創(chuàng)建大圖,根據(jù)匹配位置闻鉴,分別將左圖和右圖粘貼到大圖上茵乱。
- 對重疊區(qū)域進行反向距離加權(quán)融合(待補充)。
測試代碼如下:
{
cv::Mat matImgL = cv::imread("cat01.png");
cv::Mat matImgR = cv::imread("cat02.png");
// 對右圖模擬光照變化孟岛、模糊度變化
//cv::convertScaleAbs(matImgR, matImgR, 1.0, 30);
//cv::GaussianBlur(matImgR, matImgR, cv::Size(3, 3), 0);
cv::Mat matImgLGray, matImgRGray;
cv::cvtColor(matImgL, matImgLGray, cv::COLOR_BGR2GRAY);
cv::cvtColor(matImgR, matImgRGray, cv::COLOR_BGR2GRAY);
int64 llTickStart = cv::getTickCount();
// 選取的圖像模板的尺寸
const int nTemplateWidth = 50;
const int nTemplateHeight = 50;
// 從左圖右邊緣中部扣取一塊50x50的區(qū)域瓶竭,做為圖像模板
cv::Rect rectTemplate(matImgLGray.cols - nTemplateWidth, matImgLGray.rows / 2 - nTemplateHeight/2, nTemplateWidth, nTemplateHeight);
cv::Mat matTemplate = matImgLGray(rectTemplate);
// 在右圖中執(zhí)行模板匹配
cv::Mat matResult;
cv::matchTemplate(matImgRGray, matTemplate, matResult, cv::TM_CCOEFF_NORMED);
// 定位匹配系數(shù)最高的位置
double fMaxVal = 0.0;
cv::Point ptMaxLoc;
cv::minMaxLoc(matResult, NULL, &fMaxVal, NULL, &ptMaxLoc);
cv::rectangle(matImgL, rectTemplate, cv::Scalar(0, 255, 0), 2);
cv::rectangle(matImgR, cv::Rect(ptMaxLoc.x, ptMaxLoc.y, nTemplateWidth, nTemplateHeight), cv::Scalar(0, 255, 0), 2);
// 創(chuàng)建大圖
cv::Mat matBig(matImgL.rows, rectTemplate.x + (matImgR.cols - ptMaxLoc.x), matImgL.type(), cv::Scalar::all(0));
// 選擇左圖全部位置貼到大圖指定位置
cv::Rect rectROILSrc(0, 0, matImgL.cols, matImgL.rows);
cv::Rect rectROILDst(0, 0, matImgL.cols, matImgL.rows);
matImgL(rectROILSrc).copyTo(matBig(rectROILDst));
// 選擇右圖部分位置貼到大圖指定位置
cv::Rect rectROIRSrc;
cv::Rect rectROIRDst;
if (ptMaxLoc.y - rectTemplate.y >= 0)
{
rectROIRSrc = cv::Rect(ptMaxLoc.x, (ptMaxLoc.y - rectTemplate.y), matImgR.cols - ptMaxLoc.x, matImgR.rows - (ptMaxLoc.y - rectTemplate.y));
rectROIRDst = cv::Rect(rectTemplate.x, 0, rectROIRSrc.width, rectROIRSrc.height);
}
else
{
rectROIRSrc = cv::Rect(ptMaxLoc.x, 0, matImgR.cols - ptMaxLoc.x, std::min(matBig.rows, matImgR.rows + (rectTemplate.y - ptMaxLoc.y)));
rectROIRSrc.height -= (rectTemplate.y - ptMaxLoc.y);
rectROIRDst = cv::Rect(rectTemplate.x, (rectTemplate.y - ptMaxLoc.y), rectROIRSrc.width, rectROIRSrc.height);
}
matImgR(rectROIRSrc).copyTo(matBig(rectROIRDst));
int64 llTickEnd = cv::getTickCount();
printf("use %fs \n", (llTickEnd - llTickStart)/cv::getTickFrequency());
cv::imwrite("matBig.png", matBig);
}
左圖和右圖分別為:
拼接后的大圖為:
網(wǎng)格化圖像組拼接方法:
拿前面的運動相機舉例,在網(wǎng)格化的運動系統(tǒng)作用下渠羞,攝像機不斷地拍取網(wǎng)格照片斤贰,并且這些照片之間有明顯的位置先后關(guān)系。因而我們可按倆張圖的拼接方法堵未,以串聯(lián)的方式腋舌,前面的圖先拼出結(jié)果,后面的圖再往前面結(jié)果上拼渗蟹。但是需要額外留意的是块饺,卡扣位置的選取,當(dāng)發(fā)生網(wǎng)格化換行時雌芽,卡扣應(yīng)取上一行的下邊緣授艰。
我們先將兩張圖的拼接過程封裝成函數(shù),在這些定義中世落,增加了水平和垂直拼接的實現(xiàn)淮腾。代碼如下:
namespace dakuang
{
// 兩張圖的位置關(guān)系
enum EClipRelation
{
CLIP_HORIZONTAL = 0, // 水平
CLIP_VERTICALITY // 垂直
};
// 卡扣位置信息
struct SClipInfo
{
cv::Rect rectFirst; // 第一張圖的卡扣位置
cv::Rect rectSecond; // 第二張圖的卡扣位置
};
// 計算兩張圖的卡扣位置信息
void calcClipInfo(const cv::Mat& matImgFirst, const cv::Mat& matImgSecond, const EClipRelation& emRelation, SClipInfo& stClipInfo)
{
cv::Mat matImgFirstGray, matImgSecondGray;
cv::cvtColor(matImgFirst, matImgFirstGray, cv::COLOR_BGR2GRAY);
cv::cvtColor(matImgSecond, matImgSecondGray, cv::COLOR_BGR2GRAY);
// 選取的圖像模板的尺寸
const int nTemplateWidth = 50;
const int nTemplateHeight = 50;
// 選取第一張圖的卡扣位置
cv::Rect rectTemplate;
if (emRelation == CLIP_HORIZONTAL)
{
// 水平拼接,選取右邊緣中部扣取一塊區(qū)域
rectTemplate = cv::Rect(matImgFirstGray.cols - nTemplateWidth, matImgFirstGray.rows / 2 - nTemplateHeight / 2, nTemplateWidth, nTemplateHeight);
}
else
{
// 垂直拼接屉佳,選取下邊緣中部扣取一塊區(qū)域
rectTemplate = cv::Rect(matImgFirstGray.cols/2 - nTemplateWidth/2, matImgFirstGray.rows - nTemplateHeight, nTemplateWidth, nTemplateHeight);
}
// 將卡扣位置的圖像做為模板
cv::Mat matTemplate = matImgFirstGray(rectTemplate);
// 在第二張圖中執(zhí)行模板匹配
cv::Mat matResult;
cv::matchTemplate(matImgSecondGray, matTemplate, matResult, cv::TM_CCOEFF_NORMED);
// 定位匹配系數(shù)最高的位置
double fMaxVal = 0.0;
cv::Point ptMaxLoc;
cv::minMaxLoc(matResult, NULL, &fMaxVal, NULL, &ptMaxLoc);
// 輸出卡扣位置信息
stClipInfo.rectFirst = rectTemplate;
stClipInfo.rectSecond = cv::Rect(ptMaxLoc.x, ptMaxLoc.y, nTemplateWidth, nTemplateHeight);
}
// 使用卡扣信息谷朝,拼接兩張圖
void joinTwoImage(const cv::Mat& matImgFirst, const cv::Mat& matImgSecond, const EClipRelation& emRelation, const SClipInfo& stClipInfo, cv::Mat& matResult)
{
// 水平拼接
if (emRelation == CLIP_HORIZONTAL)
{
// 創(chuàng)建大圖(高度以第一張圖為準(zhǔn))
cv::Mat matBig(matImgFirst.rows, stClipInfo.rectFirst.x + (matImgSecond.cols - stClipInfo.rectSecond.x), matImgFirst.type(), cv::Scalar::all(0));
// 選擇第一張圖全部位置貼到大圖指定位置
cv::Rect rectROIFirstSrc(0, 0, matImgFirst.cols, matImgFirst.rows);
cv::Rect rectROIFirstDst(0, 0, matImgFirst.cols, matImgFirst.rows);
matImgFirst(rectROIFirstSrc).copyTo(matBig(rectROIFirstDst));
// 計算第二張圖部分位置貼到大圖指定位置
cv::Rect rectROISecondSrc;
cv::Rect rectROISecondDst;
if (stClipInfo.rectSecond.y - stClipInfo.rectFirst.y >= 0)
{
rectROISecondSrc = cv::Rect(stClipInfo.rectSecond.x, (stClipInfo.rectSecond.y - stClipInfo.rectFirst.y),
(matImgSecond.cols - stClipInfo.rectSecond.x), std::min(matBig.rows, matImgSecond.rows - (stClipInfo.rectSecond.y - stClipInfo.rectFirst.y)));
rectROISecondDst = cv::Rect(stClipInfo.rectFirst.x, 0, rectROISecondSrc.width, rectROISecondSrc.height);
}
else
{
rectROISecondSrc = cv::Rect(stClipInfo.rectSecond.x, 0,
(matImgSecond.cols - stClipInfo.rectSecond.x), std::min(matBig.rows, matImgSecond.rows + (stClipInfo.rectFirst.y - stClipInfo.rectSecond.y)));
rectROISecondSrc.height -= (stClipInfo.rectFirst.y - stClipInfo.rectSecond.y);
// 另一種表示法
//rectROISecondSrc = cv::Rect(stClipInfo.rectSecond.x, 0, (matImgSecond.cols - stClipInfo.rectSecond.x), 0);
//int nMoreHeight = (stClipInfo.rectFirst.y - stClipInfo.rectSecond.y) + matImgSecond.rows - matBig.rows;
//rectROISecondSrc.height = (nMoreHeight > 0) ? (matImgSecond.rows - nMoreHeight) : matImgSecond.rows;
rectROISecondDst = cv::Rect(stClipInfo.rectFirst.x, (stClipInfo.rectFirst.y - stClipInfo.rectSecond.y), rectROISecondSrc.width, rectROISecondSrc.height);
}
matImgSecond(rectROISecondSrc).copyTo(matBig(rectROISecondDst));
matResult = matBig;
}
// 垂直拼接
else
{
// 創(chuàng)建大圖(寬度以第一張圖為準(zhǔn))
cv::Mat matBig(stClipInfo.rectFirst.y + (matImgSecond.rows - stClipInfo.rectSecond.y), matImgFirst.cols, matImgFirst.type(), cv::Scalar::all(0));
// 選擇第一張圖全部位置貼到大圖指定位置
cv::Rect rectROIFirstSrc(0, 0, matImgFirst.cols, matImgFirst.rows);
cv::Rect rectROIFirstDst(0, 0, matImgFirst.cols, matImgFirst.rows);
matImgFirst(rectROIFirstSrc).copyTo(matBig(rectROIFirstDst));
// 計算第二張圖部分位置貼到大圖指定位置
cv::Rect rectROISecondSrc;
cv::Rect rectROISecondDst;
if (stClipInfo.rectSecond.x - stClipInfo.rectFirst.x >= 0)
{
rectROISecondSrc = cv::Rect((stClipInfo.rectSecond.x - stClipInfo.rectFirst.x), stClipInfo.rectSecond.y,
std::min(matBig.cols, matImgSecond.cols - (stClipInfo.rectSecond.x - stClipInfo.rectFirst.x)), (matImgSecond.rows - stClipInfo.rectSecond.y));
rectROISecondDst = cv::Rect(0, stClipInfo.rectFirst.y, rectROISecondSrc.width, rectROISecondSrc.height);
}
else
{
rectROISecondSrc = cv::Rect(0, stClipInfo.rectSecond.y,
std::min(matBig.cols, matImgSecond.cols + (stClipInfo.rectFirst.x - stClipInfo.rectSecond.x)), (matImgSecond.rows - stClipInfo.rectSecond.y));
rectROISecondSrc.width -= (stClipInfo.rectFirst.x - stClipInfo.rectSecond.x);
// 另一種表示法
//rectROISecondSrc = cv::Rect(0, stClipInfo.rectSecond.y, 0, (matImgSecond.rows - stClipInfo.rectSecond.y));
//int nMoreWidth = (stClipInfo.rectFirst.x - stClipInfo.rectSecond.x) + matImgSecond.cols - matBig.cols;
//rectROISecondSrc.width = (nMoreWidth > 0) ? (matImgSecond.cols - nMoreWidth) : matImgSecond.cols;
rectROISecondDst = cv::Rect((stClipInfo.rectFirst.x - stClipInfo.rectSecond.x), stClipInfo.rectFirst.y, rectROISecondSrc.width, rectROISecondSrc.height);
}
matImgSecond(rectROISecondSrc).copyTo(matBig(rectROISecondDst));
matResult = matBig;
}
}
}
接下來我們找一張圖,把它分割為3x3的小圖武花,并模擬網(wǎng)格化拍攝圆凰,保證每張圖有相同的視野,相同的重疊區(qū)域体箕。分割的代碼如下:
{
cv::Mat matImgBase = cv::imread("full.jpg");
// 模擬相機专钉,切分出3x3的視野圖
int nPerKernelWidth = (matImgBase.cols - 400) / 3;
int nPerKernelHeight = (matImgBase.rows - 400) / 3;
// 截圖
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
{
cv::Rect rectROI;
rectROI.x = 100 * (j + 1) + j * nPerKernelWidth - 100;
rectROI.y = 100 * (i + 1) + i * nPerKernelHeight - 100;
rectROI.width = 100 + nPerKernelWidth + 100;
rectROI.height = 100 + nPerKernelHeight + 100;
char sbuf[32] = { 0 };
snprintf(sbuf, sizeof(sbuf), "part%dx%d.jpg", i, j);
cv::imwrite(sbuf, matImgBase(rectROI));
}
}
}
大圖如下:
分割的小圖效果如下:
然后應(yīng)用卡扣拼接算法挑童,對這3x3的小圖進行拼接,代碼如下:
{
// 加載網(wǎng)格化小圖
std::vector< std::vector<cv::Mat> > matrixMat;
matrixMat.resize(3);
for (int i = 0; i < 3; ++i)
{
matrixMat[i].resize(3);
for (int j = 0; j < 3; ++j)
{
char sbuf[32] = { 0 };
snprintf(sbuf, sizeof(sbuf), "part%dx%d.jpg", i, j);
matrixMat[i][j] = cv::imread(sbuf);
}
}
// 網(wǎng)格化遍歷
cv::Mat matFull;
for (int i = 0; i < 3; ++i)
{
cv::Mat matLine;
// 行內(nèi)遍歷
for (int j = 0; j < 3; ++j)
{
if (matLine.empty())
{
matLine = matrixMat[i][j];
continue;
}
// 行內(nèi)水平卡扣
dakuang::SClipInfo stClipInfo;
dakuang::calcClipInfo(matLine, matrixMat[i][j], dakuang::CLIP_HORIZONTAL, stClipInfo);
dakuang::joinTwoImage(matLine, matrixMat[i][j], dakuang::CLIP_HORIZONTAL, stClipInfo, matLine);
}
if (matFull.empty())
{
matFull = matLine;
continue;
}
// 行間垂直卡扣
dakuang::SClipInfo stClipInfo;
dakuang::calcClipInfo(matFull, matLine, dakuang::CLIP_VERTICALITY, stClipInfo);
dakuang::joinTwoImage(matFull, matLine, dakuang::CLIP_VERTICALITY, stClipInfo, matFull);
}
cv::imwrite("full2.jpg", matFull);
}
拼接后的效果如下:
性能優(yōu)化:
1. 關(guān)于雙圖拼接時的卡扣計算優(yōu)化
在卡扣計算函數(shù)calcClipInfo()中,如果第一張圖的尺寸過大,如水平拼接時長度過長充易,會消耗無端的資源來計算灰度值,可以優(yōu)化為先扣出模板尽楔,再對模板進行灰度運算,并且對第二張圖玉雾,也只對左端/上端感興趣區(qū)域進行灰度運算翔试,然后執(zhí)行模板匹配轻要。
2. 關(guān)于雙圖拼接過程中圖像矩陣的內(nèi)存分配問題
隨著拼接過程不斷地進行复旬,會反復(fù)執(zhí)行下面的過程:
a) 創(chuàng)建兩張圖的合并矩陣
b) 復(fù)制第一張圖到合并矩陣
c) 復(fù)制第二張圖到合并矩陣
圖示如下:
在這些過程中,每個過程都會重新創(chuàng)建新的矩陣內(nèi)存冲泥,而且隨著拼接的進行驹碍,內(nèi)存會越來越大,系統(tǒng)開銷會越來越重凡恍。優(yōu)化思路為:
一次性創(chuàng)建出所有合并后大圖尺寸志秃,由于不清楚真實的尺寸大小,可以創(chuàng)建得大一些嚼酝,以后每次的合并結(jié)果不再創(chuàng)建新的矩陣浮还,而是在這個大矩陣上做ROI映射。因此需修改calcClipInfo()和joinTwoImage()這兩個函數(shù)闽巩,傳能時同時傳入第一張圖的ROI矩陣和整個大矩陣钧舌,并且在拼接處理時就不再需要復(fù)制第一張圖了,只需要把第二張圖復(fù)制上去涎跨,最后返回合并后的感興趣矩陣洼冻。
圖示如下:
這種方法也可以推廣到整個網(wǎng)格大圖上,大致思路是先分配網(wǎng)格大矩陣隅很,然后逐行分配行矩陣執(zhí)行行拼接撞牢,一行完成后再粘巾到網(wǎng)格大內(nèi)存上。
內(nèi)存優(yōu)化:
在上面的過程中叔营,由于將整個網(wǎng)格完整地分配了內(nèi)存屋彪,如果網(wǎng)格數(shù)量比較大,且網(wǎng)格圖的分辨率比較高绒尊,例如2048x2048三通道格式畜挥,網(wǎng)格數(shù)為100x100,粗略計算需要120G以上的內(nèi)存垒酬,這是非常嚇人的數(shù)字砰嘁。有什么辦法可以解決這個問題呢件炉?
我們可以想想在做網(wǎng)絡(luò)協(xié)議解幀的情景,每當(dāng)緩沖區(qū)收到新的數(shù)據(jù)時矮湘,我們就檢查是否收滿一幀斟冕,如果有就立即提取這個幀,使緩沖區(qū)永遠(yuǎn)保持在低水平長度缅阳。借鑒這個方法磕蛇,我們也可以檢查拼圖過程中,當(dāng)前已經(jīng)完成的拼接圖高度是否滿足一定條件十办,若滿足則把這部分提走秀撇,也使后續(xù)拼圖的高度維持在低水位。按照這個方法計算向族,如果設(shè)定每拼出完整一行就提取走呵燕,那么對內(nèi)存的要求將下降到3G以內(nèi)。