OpenCV實現簡單套索工具

Photoshop中的套索工具通過鼠標多次點擊可以選中一個任意多邊形的區域,然後單獨對這塊區域進行編輯,下面就使用OpenCV實現一個簡單的功能,模擬Photoshop中的套索工具。

這裡的套索工具通過鼠標左鍵在圖片上多次點擊創建任意多個點,右鍵點擊後將這些點連成封閉的多邊形,形成一塊待編輯的區域,鍵盤方向鍵控制該區域的移動,從而將該區域內的圖像復制到原圖像的其他地方。

首先定義下列全局變量

const char* winName = "TaoSuoTool";//窗口名稱
cv::Mat resultImg;//最終在OpenCV窗口上顯示的圖像
cv::Mat foregroundImg;//編輯前的圖像
cv::Mat areaMask;//蒙版,多邊形區域實際繪制在該蒙版上
cv::Point maskLocation;//蒙版位置,通過方向鍵移動後蒙版位置隨之變化
std::vector<cv::Point> drawingPoints;//區域完成前正在點擊的所有點
std::vector<cv::Point> areaPoints;//區域完成後其多邊形頂點

main函數

int main(int argc, char **arv)
{
    foregroundImg = cv::imread("test.jpg");
    foregroundImg.copyTo(resultImg);
    areaMask = cv::Mat::zeros(foregroundImg.size(), CV_8U);
    cv::imshow(winName, resultImg);
 
    maskLocation.x = maskLocation.y = 0;
    cv::setMouseCallback(winName, OnMouseEvent);
    int key = cv::waitKeyEx(0);
    while (key != VK_ESCAPE)
    {
        key = cv::waitKeyEx(0);
    }
    return 0;
}

在鼠標回調函數OnMouseEvent中處理三個消息:鼠標左鍵按下,鼠標右鍵按下和鼠標移動

void OnMouseEvent(int event, int x, int y, int flags, void* userdata)
{
    if (event == cv::EVENT_LBUTTONDOWN)
    {
        OnLeftMouseButtonDown(x,y);
    }
    else if (event == cv::EVENT_RBUTTONDOWN)
    {
        OnRightMouseButtonDown(x,y);
    }
    if (event == cv::EVENT_MOUSEMOVE)
    {
        OnMouseMove(x,y);
    }
}

在編寫鼠標事件前先定義一個函數

void OnCompleteArea(bool bDrawOutline);

它表示完成當前區域的編輯,包括右鍵點擊完成封閉多邊形、移動區域以及合成最終圖片。參數bDrawOutline表示繪制區域多邊形的外輪廓,右鍵點擊完成封閉多邊形和移動區域過程中都要顯示輪廓(bDrawOutline=true),合成最終圖片後就不需要顯示輪廓瞭(bDrawOutline=false)。
鼠標左鍵按下事件:先判斷是否有前一個區域存在,存在則先完成前一個區域並且不顯示區域輪廓,然後開始繪制新的區域多邊形的點,點與點之間用藍色線連接,點位置處繪制一個4X4的紅色矩形。

void OnLeftMouseButtonDown(int x,int y)
{
    if (drawingPoints.empty() && areaPoints.size() > 0)
    {
        OnCompleteArea(false);
    }
    drawingPoints.push_back(cv::Point(x, y));
    cv::rectangle(resultImg, cv::Rect(x - 2, y - 2, 4, 4), CV_RGB(255, 0, 0), -1);
    if (drawingPoints.size() >= 2)
    {
        cv::line(resultImg, drawingPoints[drawingPoints.size() - 2], cv::Point(x, y), CV_RGB(0, 0, 255), 1);
    }
    cv::imshow(winName, resultImg);
}

鼠標移動事件:判斷drawingPoints是否為空,如果已經存在點則繪制線和點,並且還要繪制一根連接到鼠標當前位置的線。

void OnMouseMove(int x,int y)
{
    if (drawingPoints.size() > 0)
    {
        foregroundImg.copyTo(resultImg);
        for (int i = 0; i < drawingPoints.size() - 1; i++)
        {
            cv::rectangle(resultImg, cv::Rect(drawingPoints[i].x - 2, drawingPoints[i].y - 2, 4, 4), CV_RGB(255, 0, 0), -1);
            cv::line(resultImg, drawingPoints[i], drawingPoints[i + 1], CV_RGB(0, 0, 255), 1);
        }
        cv::rectangle(resultImg, cv::Rect(drawingPoints[drawingPoints.size() - 1].x - 2, drawingPoints[drawingPoints.size() - 1].y - 2, 4, 4), CV_RGB(255, 0, 0), -1);
        cv::line(resultImg, drawingPoints[drawingPoints.size() - 1], cv::Point(x, y), CV_RGB(0, 0, 255), 1);
        cv::imshow(winName, resultImg);
    }
}

鼠標右鍵按下事件:如果點個數少於2,不能形成有效區域則不做處理(不考慮多個點共線),否則就在蒙版Area上繪制一個多邊形區域,然後調用OnCompleteArea完成區域編輯,這裡需要畫多邊形輪廓,參數傳入true。

void OnRightMouseButtonDown(int x,int y)
{
    if (drawingPoints.size() >= 3)
    {
        areaPoints = drawingPoints;
        std::vector<std::vector<cv::Point>> polys;
        polys.push_back(areaPoints);
        cv::fillPoly(areaMask, polys, cv::Scalar::all(255));
        OnCompleteArea(true);
    }
    else
    {
        foregroundImg.copyTo(resultImg);
    }
    drawingPoints.clear();
    cv::imshow(winName, resultImg);
}

下面是OnCompleteArea函數的實現,其中MergeImages函數通過蒙版以及蒙版的位置合成最終的圖像,蒙版中區域內的像素值大於0,其他像素值都為0,默認圖像是三通道(destImg.at<cv::Vec3b>)

void MergeImages(cv::Mat& destImg, const cv::Mat& srcImg, const cv::Mat& maskImg, int x, int y)
{
    int top = y > 0 ? y : 0;
    int left = x > 0 ? x : 0;
    int right = destImg.cols > x + srcImg.cols ? x + srcImg.cols : destImg.cols;
    int bottom = destImg.rows > y + srcImg.rows ? y + srcImg.rows : destImg.rows;
    for (int i = top; i < bottom; i++)
    {
        for (int j = left; j < right; j++)
        {
            int destIndex = i * destImg.cols + j;
            int srcIndex = (i - top)*srcImg.cols + j - left;
            int channel = destImg.channels();
            if (maskImg.at<uchar>(i - y, j - x) > 0)
            {
                destImg.at<cv::Vec3b>(i, j)[0] = srcImg.at<cv::Vec3b>(i - y, j - x)[0];
                destImg.at<cv::Vec3b>(i, j)[1] = srcImg.at<cv::Vec3b>(i - y, j - x)[1];
                destImg.at<cv::Vec3b>(i, j)[2] = srcImg.at<cv::Vec3b>(i - y, j - x)[2];
            }
        }
    }
}
void OnCompleteArea(bool bDrawOutline)
{
    foregroundImg.copyTo(resultImg);
    MergeImages(resultImg, foregroundImg, areaMask, maskLocation.x, maskLocation.y);
    if (bDrawOutline)
    {
        if (areaPoints.size() >= 3)
        {
            std::vector<std::vector<cv::Point>> polys;
            polys.push_back(areaPoints);
            cv::polylines(resultImg, polys, true, cv::Scalar::all(255));
        }
    }
    else
    {
        resultImg.copyTo(foregroundImg);
        areaPoints.clear();
        memset(areaMask.data, 0, areaMask.rows*areaMask.cols*areaMask.elemSize());
        maskLocation.x = maskLocation.y = 0;
    }
}

繪制區域之後就可以通過方向按鍵控制區域圖像的移動瞭(也可以實現為鼠標左鍵按下拖動來移動區域),移動主要是更新maskLocation和areaPoints的坐標值,然後調用OnCompleteArea(true),依然顯示區域的輪廓。

void OnDirectionKeyDown(short keyCode)
{
    int x = keyCode == VK_LEFT ? -2 : (keyCode == VK_RIGHT ? 2 : 0);
    int y = keyCode == VK_UP ? -2 : (keyCode == VK_DOWN ? 2 : 0);
    maskLocation.x += x;
    maskLocation.y += y;
    for (int i = 0; i < areaPoints.size(); i++)
    {
        areaPoints[i].x += x;
        areaPoints[i].y += y;
    }
    OnCompleteArea(true);
    cv::imshow(winName, resultImg);
}

將上面函數在主函數的按鍵循環中調用,方向按鍵通過key的高16位判斷,在Windows下可以使用虛擬鍵碼宏表示。 同時為瞭能看到最終合成的圖片加入Enter按鍵消息處理,將圖像合成並去掉輪廓。

int key = cv::waitKeyEx(0);
short lowKey = key;
short highKey = key >> 16;
while (key != VK_ESCAPE)
{
    if (key == VK_RETURN)//Enter
    {
        OnCompleteArea(false);
        cv::imshow(winName, resultImg);
    }
    else if (lowKey == 0 && (highKey == VK_UP || highKey == VK_DOWN || highKey == VK_LEFT || highKey == VK_RIGHT))
    {
        OnDirectionKeyDown(highKey);
    }
    key = cv::waitKeyEx(0);
    lowKey = key;
    highKey = key >> 16;
}

這樣一個簡單的套索工具功能就做好瞭(上面的代碼都是簡化處理,還有很多可以優化的地方,從而使編輯更加流暢)

完整代碼

#include<iostream>
#include"opencv2/opencv.hpp"
#include<windows.h>
const char* winName = "TaoSuoTool";
cv::Point maskLocation;
cv::Mat resultImg;
cv::Mat foregroundImg;
cv::Mat areaMask;
std::vector<cv::Point> drawingPoints;
std::vector<cv::Point> areaPoints;
void MergeImages(cv::Mat& destImg, const cv::Mat& srcImg, const cv::Mat& maskImg, int x, int y)
{
    int top = y > 0 ? y : 0;
    int left = x > 0 ? x : 0;
    int right = destImg.cols > x + srcImg.cols ? x + srcImg.cols : destImg.cols;
    int bottom = destImg.rows > y + srcImg.rows ? y + srcImg.rows : destImg.rows;
    for (int i = top; i < bottom; i++)
    {
        for (int j = left; j < right; j++)
        {
            int destIndex = i * destImg.cols + j;
            int srcIndex = (i - top)*srcImg.cols + j - left;
            int channel = destImg.channels();
            if (maskImg.at<uchar>(i - y, j - x) > 0)
            {
                destImg.at<cv::Vec3b>(i, j)[0] = srcImg.at<cv::Vec3b>(i - y, j - x)[0];
                destImg.at<cv::Vec3b>(i, j)[1] = srcImg.at<cv::Vec3b>(i - y, j - x)[1];
                destImg.at<cv::Vec3b>(i, j)[2] = srcImg.at<cv::Vec3b>(i - y, j - x)[2];
            }
        }
    }
}
 
void OnCompleteArea(bool bDrawOutline)
{
    foregroundImg.copyTo(resultImg);
    MergeImages(resultImg, foregroundImg, areaMask, maskLocation.x, maskLocation.y);
    if (bDrawOutline)
    {
        if (areaPoints.size() >= 3)
        {
            std::vector<std::vector<cv::Point>> polys;
            polys.push_back(areaPoints);
            cv::polylines(resultImg, polys, true, cv::Scalar::all(255));
        }
    }
    else
    {
        resultImg.copyTo(foregroundImg);
        areaPoints.clear();
        memset(areaMask.data, 0, areaMask.rows*areaMask.cols*areaMask.elemSize());
        maskLocation.x = maskLocation.y = 0;
    }
}
void OnLeftMouseButtonDown(int x,int y)
{
    if (drawingPoints.empty() && areaPoints.size() > 0)
    {
        OnCompleteArea(false);
    }
    drawingPoints.push_back(cv::Point(x, y));
    cv::rectangle(resultImg, cv::Rect(x - 2, y - 2, 4, 4), CV_RGB(255, 0, 0), -1);
    if (drawingPoints.size() >= 2)
    {
        cv::line(resultImg, drawingPoints[drawingPoints.size() - 2], cv::Point(x, y), CV_RGB(0, 0, 255), 1);
    }
    cv::imshow(winName, resultImg);
}
void OnRightMouseButtonDown(int x,int y)
{
    if (drawingPoints.size() >= 3)
    {
        areaPoints = drawingPoints;
        std::vector<std::vector<cv::Point>> polys;
        polys.push_back(areaPoints);
        cv::fillPoly(areaMask, polys, cv::Scalar::all(255));
        OnCompleteArea(true);
    }
    else
    {
        foregroundImg.copyTo(resultImg);
    }
    drawingPoints.clear();
    cv::imshow(winName, resultImg);
}
void OnMouseMove(int x,int y)
{
    if (drawingPoints.size() > 0)
    {
        foregroundImg.copyTo(resultImg);
        for (int i = 0; i < drawingPoints.size() - 1; i++)
        {
            cv::rectangle(resultImg, cv::Rect(drawingPoints[i].x - 2, drawingPoints[i].y - 2, 4, 4), CV_RGB(255, 0, 0), -1);
            cv::line(resultImg, drawingPoints[i], drawingPoints[i + 1], CV_RGB(0, 0, 255), 1);
        }
        cv::rectangle(resultImg, cv::Rect(drawingPoints[drawingPoints.size() - 1].x - 2, drawingPoints[drawingPoints.size() - 1].y - 2, 4, 4), CV_RGB(255, 0, 0), -1);
        cv::line(resultImg, drawingPoints[drawingPoints.size() - 1], cv::Point(x, y), CV_RGB(0, 0, 255), 1);
        cv::imshow(winName, resultImg);
    }
}
void OnMouseEvent(int event, int x, int y, int flags, void* userdata)
{
    if (event == cv::EVENT_LBUTTONDOWN)
    {
        OnLeftMouseButtonDown(x,y);
    }
    else if (event == cv::EVENT_RBUTTONDOWN)
    {
        OnRightMouseButtonDown(x,y);
    }
    if (event == cv::EVENT_MOUSEMOVE)
    {
        OnMouseMove(x,y);
    }
}
 
void OnDirectionKeyDown(short keyCode)
{
    int x = keyCode == VK_LEFT ? -2 : (keyCode == VK_RIGHT ? 2 : 0);
    int y = keyCode == VK_UP ? -2 : (keyCode == VK_DOWN ? 2 : 0);
    maskLocation.x += x;
    maskLocation.y += y;
    for (int i = 0; i < areaPoints.size(); i++)
    {
        areaPoints[i].x += x;
        areaPoints[i].y += y;
    }
    OnCompleteArea(true);
    cv::imshow(winName, resultImg);
}
int main(int argc, char **arv)
{
    foregroundImg = cv::imread("test.jpg");
    foregroundImg.copyTo(resultImg);
    areaMask = cv::Mat::zeros(foregroundImg.size(), CV_8U);
    cv::imshow(winName, resultImg);
 
    maskLocation.x = maskLocation.y = 0;
    cv::setMouseCallback(winName, OnMouseEvent);
    int key = cv::waitKeyEx(0);
    short lowKey = key;
    short highKey = key >> 16;
    while (key != VK_ESCAPE)
    {
        if (key == VK_RETURN)//Enter
        {
            OnCompleteArea(false);
            cv::imshow(winName, resultImg);
        }
        else if (lowKey == 0 && (highKey == VK_UP || highKey == VK_DOWN || highKey == VK_LEFT || highKey == VK_RIGHT))
        {
            OnDirectionKeyDown(highKey);
        }
        key = cv::waitKeyEx(0);
        lowKey = key;
        highKey = key >> 16;
    }
    return 0;
}

以上就是本文的全部內容,希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: