首頁 > 軟體

OpenCV  iOS 影象處理程式設計入門詳細教學

2022-07-27 14:01:54

OpenCV簡介

OpenCV(Open Source Computer Vision Library) 是一個廣受歡迎的開源的跨平臺計算機視覺庫,它實現了影象處理和計算機視覺方面的很多通用演演算法,演演算法從最基本的濾波到高階的物體檢測皆有涵蓋。

多語言介面

OpenCV 使用 C/C++ 開發,同時也提供了 Python、Java、MATLAB 等其他語言的介面。

跨平臺

OpenCV 是跨平臺的,可以在 Windows、Linux、Mac OS、Android、iOS 等作業系統上執行。

應用領域廣泛

OpenCV 的應用領域非常廣泛,包括影象拼接、影象降噪、產品質檢、人機互動、臉部辨識、動作識別、動作跟蹤、無人駕駛等。OpenCV 還提供了機器學習模組,你可以使用正態貝葉斯、K最近鄰、支援向量機、決策樹、隨機森林、人工神經網路等機器學習演演算法。

整合OpenCV

1、首先建立一個Xcode 工程, 在Build Settings 設定BuEnable Bitcode 為NO。

2、使用 cocoaPads 設定OpenCV。開啟終端,cd到專案的目錄,執行pod init 命令初始化工程,建立工程對應的Podfile檔案。使用 vim Podfile 新增 pod 'OpenCV' ,'~> 4.3.0',最後執行pod install安裝OpenCV。

3、所參照到OpenCV 的類檔案,需要將m檔案改成.mm,告訴編譯器有C++。

基礎影象容器Mat

影象表示

通常我們拍攝的現實世界中的真實的影象,在轉化到電子裝置中時,記錄的卻是影象中的每個點的數值。

一副尺寸為A x B的影象,可以用AxB的矩陣來表示,矩陣元素的值表示這個位置上的畫素的亮度,一般來說畫素值越大表示該點越亮。

一般情況,灰度圖用 2 維矩陣表示,彩色(多通道)影象用 3 維矩陣(M × N × 3)表示。對於影象顯示來說,目前大部分裝置都是用無符號 8 位整數(型別為 CV_8U)表示畫素亮度。

影象資料在計算機記憶體中的儲存順序為以影象最左上點(也可能是最左下 點)開始,如果是多通道影象,比如 RGB 影象,則每個 畫素用三個位元組表示。在 OpenCV 中,RGB 影象的通道順序為 BGR 。

Mat類關鍵屬性及定義

其中關鍵的屬性如下:

/* flag引數中包含許多關於矩陣的資訊,如: -Mat 的標識

-資料是否連續 -深度 -通道數目

*/

int flags;

//矩陣的維數,取值應該大於或等於 2

int dims;

//矩陣的行數和列數,如果矩陣超過 2 維,這兩個變數的值都為-1

int rows, cols;

//指向資料的指標

uchar* data;

//指向參照計數的指標 //如果資料是由使用者分配的,則為 NULL

int* refcount;

Mat定義如下:

class CV_EXPORTS Mat
{
public:
    Mat();
    Mat(int rows, int cols, int type);
    Mat(Size size, int type);
    Mat(int rows, int cols, int type, const Scalar& s);
    Mat(Size size, int type, const Scalar& s);
    Mat(int ndims, const int* sizes, int type);
    Mat(const std::vector<int>& sizes, int type);
    Mat(int ndims, const int* sizes, int type, const Scalar& s);
    Mat(const std::vector<int>& sizes, int type, const Scalar& s);
    Mat(const Mat& m);
    Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP);
    Mat(Size size, int type, void* data, size_t step=AUTO_STEP);
    .......
    .........................
    ................................
          /*! includes several bit-fields:
         - the magic signature
         - continuity flag
         - depth
         - number of channels
     */
    int flags;
    //! the matrix dimensionality, >= 2
    int dims;
    //! the number of rows and columns or (-1, -1) when the matrix has more than 2 dimensions
    int rows, cols;
    //! pointer to the data
    uchar* data;

    //! helper fields used in locateROI and adjustROI
    const uchar* datastart;
    const uchar* dataend;
    const uchar* datalimit;

    //! custom allocator
    MatAllocator* allocator;
    //! and the standard allocator
    static MatAllocator* getStdAllocator();
    static MatAllocator* getDefaultAllocator();
    static void setDefaultAllocator(MatAllocator* allocator);

    //! internal use method: updates the continuity flag
    void updateContinuityFlag();

    //! interaction with UMat
    UMatData* u;

    MatSize size;
    MatStep step;

protected:
    template<typename _Tp, typename Functor> void forEach_impl(const Functor& operation);
};

建立Mat物件

Mat 是一個非常優秀的影象類,它同時也是一個通用的矩陣類,可以用來建立和操作多維矩陣。有多種方法建立一個 Mat 物件。

對於二維多通道影象,首先要定義其尺寸,即行數和列數。而後需要指定儲存元素的資料型別以及每個矩陣點的通道數。為此,定義規則如下:

CV_【位數】【帶符號與否】【型別字首】C【通道數】

例:CV_8UC3:表示使用8位元的unsigned char型別,每個畫素有三個元素組成三通道。而預先定義的通道數可以多達四個。Scalar 是個short 型別的向量,能使用指定的客製化化值來初始化矩陣,它還可以表示顏色。

1、建構函式方法建立Mat

Mat M(5,8, CV_8UC3, Scalar(255,0,0));

建立一個高度為5,寬度為8的影象,影象元素為8位元無符號型別,且有3個通道。影象的所有畫素值被初始化為(255,0,0)。因 OpenCV 中預設的顏色順序為 BGR,因此這是一個純藍色的影象【RGB為:(0,0,255)】

//建立行數為 rows,列數為 col,型別為 type 的影象;
Mat::Mat(int rows, int cols, int type);
//建立大小為 size,型別為 type 的影象;
Mat::Mat(Size size, int type)
//建立行數為 rows,列數為 col,型別為 type 的影象,並將所有元素初始 化為值 s;
Mat::Mat(int rows, int cols, int type, const Scalar& s);
//建立大小為 size,型別為 type 的影象,並將所有元素初始化為值 s;
Mat::Mat(Size size, int type, const Scalar& s);

2、利用Create()函數建立Mat

Mat mat; 
mat.create(2, 2, CV_8UC3);

常用資料結構和函數

Point類

用於表示點。Point類資料結構表示了二維座標系下的點,即由其影象座標x和y指定的2D點。

使用方式如下:

Point point;
point.x=2;
point.y=5;
或
Point point=Point(2,5);

Scalar類

用於表示顏色的。Scalar()表示具有4個元素的陣列,在OpenCV中被大量用於傳遞畫素值,如RGB顏色值。RGB顏色值為三個引數,對於Scalar()來說,第四個引數為可選,用不到則無需寫出,只寫三個引數,則OpenCV會認為我們就想表示三個引數。

例:

 Scalar scalar=Scalar(0,2,255);

定義的RGB顏色值為:0:藍色分量,2:綠色分量,255:紅色分量。

Scalar類的源頭為Scalar_類,而Scalar_類為Vec4x 的一個變種,常用的Scalar其實就是 Scalar_<double> ,這也是為啥很多函數的引數可以輸入Mat,也可以是Scalar。

//Vec 是Matx的一個派生類,一個一維的Matx,和vector很類似。Matx是個輕量級的Mat,必須在使用前規定好大小。
template<typename _Tp> class Scalar_ : public Vec<_Tp, 4>
{
public:
    //! default constructor
    Scalar_();
    Scalar_(_Tp v0, _Tp v1, _Tp v2=0, _Tp v3=0);
    Scalar_(_Tp v0);

    Scalar_(const Scalar_& s);
    Scalar_(Scalar_&& s) CV_NOEXCEPT;

    Scalar_& operator=(const Scalar_& s);
    Scalar_& operator=(Scalar_&& s) CV_NOEXCEPT;

    template<typename _Tp2, int cn>
    Scalar_(const Vec<_Tp2, cn>& v);

    //! returns a scalar with all elements set to v0
    static Scalar_<_Tp> all(_Tp v0);

    //! conversion to another data type
    template<typename T2> operator Scalar_<T2>() const;

    //! per-element product
    Scalar_<_Tp> mul(const Scalar_<_Tp>& a, double scale=1 ) const;

    //! returns (v0, -v1, -v2, -v3)
    Scalar_<_Tp> conj() const;

    //! returns true iff v1 == v2 == v3 == 0
    bool isReal() const;
};

typedef Scalar_<double> Scalar;

Size類

用於表示尺寸。Size類部分原始碼如下:

typedef Size_<int> Size2i;
typedef Size_<int64> Size2l;
typedef Size_<float> Size2f;
typedef Size_<double> Size2d;
typedef Size2i Size;

其中Size_是模版類,在此,Size_<int> 表示其類內部的模版所代表的型別為int。意思是:首先給已知的資料型別Size_<int> 起個新名字為Size2i,然後又給已知的資料型別Size2i 起個新名字Size。因此,Size_<int> 、Size2i、Size三個型別名等價。

Size_模版定義如下:

template<typename _Tp> class Size_
{
public:
    typedef _Tp value_type;

    //! default constructor
  //建構函式
    Size_();
    Size_(_Tp _width, _Tp _height);
    Size_(const Size_& sz);
    Size_(Size_&& sz) CV_NOEXCEPT;
    Size_(const Point_<_Tp>& pt);

    Size_& operator = (const Size_& sz);
    Size_& operator = (Size_&& sz) CV_NOEXCEPT;
    //! the area (width*height)
  //區域(width*height)
    _Tp area() const;
    //! aspect ratio (width/height)
    double aspectRatio() const;
    //! true if empty
    bool empty() const;

    //! conversion of another data type.
  //轉化為另一種資料型別
    template<typename _Tp2> operator Size_<_Tp2>() const;
     //常用屬性,模版型別的寬度和高度
    _Tp width; //!< the width 寬度
    _Tp height; //!< the height高度
};

Size_模版類內部又過載了一些建構函式,使用度最高的建構函式如下:

Size_(_Tp _width, _Tp _height);

於是我們可以用xx.width和xx.height 來分別表示寬和高。

例:Size(2,3);構造出的Size寬為2,高為3。即 size.width=2, size.height=3。

 Size size=Size(2,3);
 size.width;
 size.height;

Rect類

用於表示矩形。Rect 類的成員變數有x,y,width,height,分別為左上角點點座標和矩形的寬和高。常用的成員函數有Size(),返回值為Size;area()返回矩形的面積;contains(Point)判斷點是否位於矩形內;inside(Rect)函數判斷矩形是否在該矩形內;tl()返回左上角點座標;br()返回右下角點座標。如想求兩個矩形的交集和並集,可如下這麼寫:

  Rect rect1=Rect(0,0,100,120);
  Rect rect2=Rect(10,10,100,120);
  Rect rect=rect1|rect2;
  Rect rect3=rect1&rect2;

若想讓矩形進行平移或縮放操作,可這樣:

  Rect rect=Rect(10,10,100,120);
  Rect rect1=rect+point;
  Rect rect2=rect+size;

cvtColor類

用於顏色空間轉換。cvtColor()函數是OpenCV裡的顏色空間轉換函數,可以實現RGB向HSV 、HSI等顏色空間的轉換,可以轉換為灰度影象。

cvtColor()函數定義如下:

CV_EXPORTS_W void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 );

第1個引數src為輸入影象,第2個引數dst為輸出影象,第3個引數code為顏色空間轉換的識別符號,第4個引數dstCn為目標影象的通道數,若引數是0,表示目標影象取源影象的通道數。

例:轉換源圖片為灰度圖片

 cvtColor(matInput, grayMat,COLOR_BGR2GRAY);

顏色空間轉換識別符號在OpenCV 庫中的imgproc.hpp 中的ColorConversionCodes 列舉中定義了很多識別符號。

/** the color conversion codes
@see @ref imgproc_color_conversions
@ingroup imgproc_color_conversions
 */
enum ColorConversionCodes {
    COLOR_BGR2BGRA     = 0, //!< add alpha channel to RGB or BGR image
    COLOR_RGB2RGBA     = COLOR_BGR2BGRA,

    COLOR_BGRA2BGR     = 1, //!< remove alpha channel from RGB or BGR image
    COLOR_RGBA2RGB     = COLOR_BGRA2BGR,

    COLOR_BGR2RGBA     = 2, //!< convert between RGB and BGR color spaces (with or without alpha channel)
    COLOR_RGB2BGRA     = COLOR_BGR2RGBA,

    COLOR_RGBA2BGR     = 3,
    COLOR_BGRA2RGB     = COLOR_RGBA2BGR,
    ...........
    ...................
    ..............................
        //! Demosaicing with alpha channel
    COLOR_BayerBG2BGRA = 139,
    COLOR_BayerGB2BGRA = 140,
    COLOR_BayerRG2BGRA = 141,
    COLOR_BayerGR2BGRA = 142,

    COLOR_BayerBG2RGBA = COLOR_BayerRG2BGRA,
    COLOR_BayerGB2RGBA = COLOR_BayerGR2BGRA,
    COLOR_BayerRG2RGBA = COLOR_BayerBG2BGRA,
    COLOR_BayerGR2RGBA = COLOR_BayerGB2BGRA,

    COLOR_COLORCVT_MAX  = 143
};

影象處理技術

存取影象中的畫素

我們已經瞭解到影象矩陣的大小取決於所用的顏色模型,準確的說取決於所用的通道數。如果是灰度影象,矩陣如下:

 c 0c 1c ...c m
R 00,00,1...0,m
R 11,01,1...1,m
R ......,0...,1......,m
R nn,0n,1n,..n,m

多通道圖來說,矩陣中列會包含多個子列,其子列個數與通道數相等。

例:下面表示RGB 顏色模型的矩陣。

 c 0c 1c ...c m
R 00,0 0,0 0,00,1 0,1 0,1... ... ...0,m 0,m 0,m
R 11,0 1,0 1,01,1 1,1 1,1... ... ...1,m 1,m 1,m
R ......,0 ...,0 ...,0...,1 ...,1 ...,1... ... ......,m ...,m ...,m
R nn,0 n,0 n,0n,1 n,1 n,1n,.. n,.. n,..n,m n,m n,m

值得注意的是,OpenCV中子列的通道順序是反過來的,是BGR 而不是RGB。

任何影象處理演演算法,都是從操作每個畫素開始的。OpenCV中提供了三種存取每個畫素的方法。

指標存取:

指標存取畫素利用的是C語言中的操作符[]。這種最快。

//顏色空間縮減
void colorReduce(Mat& matInput,Mat& matoutput,int div){
    //複製輸入影象
    matoutput=matInput.clone();
    int rows=matoutput.rows;//行數,高度
    int cols=matoutput.cols*matoutput.channels();//列數*通道數=每一行元素的個數
    //遍歷影象矩陣//寬度
    for (int i=0; i<rows; i++) {//行迴圈
        uchar *data=matoutput.ptr<uchar>(i);//取出第i行首地址
        for (int j=0; j<cols; j++) {//列迴圈
            data[j]=data[j]/div*div+div/2;//開始處理每一個畫素
        }
    }
}

Mat類中的公有成員變數rows為影象的高度,cols是寬度。channels()函數返回影象的通道數。灰度通道是1,彩色通道數為3,含alpha的為4。ptr<uchar>(i)可以得到影象任意行的首地址。ptr是模版函數,返回第i行的首地址。

迭代器iterator:

在迭代法中,僅僅需要獲取影象矩陣的begin和end,然後增加迭代從begin到end。將(*it)帶星操作符新增到迭代指標前,即可存取當前指向的內容。相比指標直接存取可能出現越界的問題,迭代器絕對是非常安全的方法。

//顏色空間縮減
void colorReduceIterator(Mat& matInput,Mat& matoutput,int div){
    //複製輸入影象
    matoutput=matInput.clone();
    //初始位置迭代器
    Mat_<Vec3b>::iterator it=matoutput.begin<Vec3b>();
    //終止位置的迭代器
    Mat_<Vec3b>::iterator itend =matoutput.end<Vec3b>();
    //遍歷影象矩陣
    for (;it!=itend;++it) {
        //處理每一個畫素
        (*it)[0]=(*it)[0]/div*div+div/2;
        (*it)[1]=(*it)[1]/div*div+div/2;
        (*it)[2]=(*it)[2]/div*div+div/2;
    }
}

動態地址計算:

使用動態地址來計算操作畫素,需配合at方法的colorReduce 函數。這種方法簡潔明瞭。但不是最快。

//顏色空間縮減
void colorReduceVec(Mat& matInput,Mat& matoutput,int div){
    //引數準備
    matoutput=matInput.clone();
    int rows=matoutput.rows;//行數
    int cols=matoutput.cols;//列數
    for (int i=0; i<rows; i++) {
        for (int j=0; j<cols; j++) {//處理每一個畫素
            //藍色通道
            matoutput.at<Vec3b>(i,j)[0]=matoutput.at<Vec3b>(i,j)[0]/div*div+div/2;
             //綠色通道
            matoutput.at<Vec3b>(i,j)[1]=matoutput.at<Vec3b>(i,j)[1]/div*div+div/2;
            //紅色通道
            matoutput.at<Vec3b>(i,j)[2]=matoutput.at<Vec3b>(i,j)[2]/div*div+div/2;
        }
    }
}

at<Vec3b>(i,j)函數可以用來存取影象元素,但是在編譯期必須知道影象的資料型別。務必保證指定的資料型別和矩陣中的資料型別相符合,因at方法本身不對任何資料型別進行轉換。

彩色影象

每個畫素由三個部分構成,藍色通道、綠色通道、紅色通道 【BGR】 。

若帶Alpha通道,則每個畫素由三個部分構成,藍色通道、綠色通道、紅色通道 Alpha通道【BGRA】 。

三通道影象

是指具有RGB三種通道的影象,簡單來說就是彩色影象。R:紅色,G:綠色,B:藍色。比如紅色為(255,0,0)

四通道影象

是在三通道的基礎上加上了一個Alpha通道,Alpha用來衡量一個畫素或影象的透明度。比如Alpha為0時,該畫素完全透明,Alpha為255時,該畫素是完全不透明。 一個包含彩色影象的Mat,會返回一個由3個8位元陣列成的向量。OpenCV中將此型別的向量定義為Vec3b,即由3個unsigned char 組成的向量。若帶有alpha通道,則會返回一個由4個8位元陣列成的向量,OpenCV中將此型別的向量定義為為Vec4b。所以我們可以這樣使用:matoutput.at<Vec3b>(i,j)[0]索引值0標明瞭顏色的通道號為0。代表該點的B分量(藍色)。

影象置灰

//置灰
-(UIImage *)grayInPutImage:(UIImage *)inputImage{
    cv::Mat matInput=[[CVUtil sharedInstance]cvMatFromUIImage:inputImage];
    cv:: Mat grayMat;
    cv::cvtColor(matInput, grayMat,cv::COLOR_BGR2GRAY);
    UIImage *imag=[[CVUtil sharedInstance]UIImageFromCVMat:grayMat];
    return imag;
}

方框濾波

//方框濾波操作
-(UIImage *)boxFilterInPutImage:(UIImage *)inputImage value:(int)value{
    Mat matInput=[[CVUtil sharedInstance]cvMatFromUIImage:inputImage];
    Mat boxFilterMat;
    boxFilter(matInput, boxFilterMat, -1,cv::Size(value+1,value+1));
    UIImage *imag=[[CVUtil sharedInstance]UIImageFromCVMat:boxFilterMat];
    return imag;
}

均值濾波

//均值濾波操作
-(UIImage *)blurInPutImage:(UIImage *)inputImage value:(int)value{
    Mat matInput=[[CVUtil sharedInstance]cvMatFromUIImage:inputImage];
    Mat blurMat;
    blur(matInput, blurMat, cv::Size(value+1,value+1),cv::Point(-1,-1));
    UIImage *imag=[[CVUtil sharedInstance]UIImageFromCVMat:blurMat];
    return imag;
}

高斯濾波

//高斯濾波操作
-(UIImage *)gaussianBlurInPutImage:(UIImage *)inputImage value:(int)value{
    Mat matInput=[[CVUtil sharedInstance]cvMatFromUIImage:inputImage];
    Mat gaussianBlurMat;
    GaussianBlur(matInput, gaussianBlurMat, cv::Size(value*2+1,value*2+1), 0,0);
    UIImage *imag=[[CVUtil sharedInstance]UIImageFromCVMat:gaussianBlurMat];
    return imag;
}

中值濾波

//中值濾波操作
-(UIImage *)medianBlurInPutImage:(UIImage *)inputImage value:(int)value{
    Mat matInput=[[CVUtil sharedInstance]cvMatFromUIImage:inputImage];
    Mat medianBlurMat;
    medianBlur(matInput, medianBlurMat,value*2+1);
    UIImage *imag=[[CVUtil sharedInstance]UIImageFromCVMat:medianBlurMat];
    return imag;
}

雙邊濾波

//雙邊濾波操作
-(UIImage *)bilateralFilterInPutImage:(UIImage *)inputImage value:(int)value{
    Mat matInput=[[CVUtil sharedInstance]cvMatFromUIImage:inputImage];
    Mat bilateralFilterMat;
    Mat grayMat;
    cvtColor(matInput, grayMat,cv::COLOR_BGR2GRAY);
    bilateralFilter(grayMat, bilateralFilterMat, value, (double)value*2, (double)value/2);
    UIImage *imag=[[CVUtil sharedInstance]UIImageFromCVMat:bilateralFilterMat];
    return imag;
}

腐蝕

//腐蝕操作
- (UIImage *)erodeInPutImage:(UIImage *)inputImage value:(int)value{
    Mat matInput=[[CVUtil sharedInstance]cvMatFromUIImage:inputImage];
    Mat element;
    element=cv::getStructuringElement(MORPH_RECT, cv::Size(2*value+1,2*value+1),cv::Point(value,value));
    Mat desimg;
    erode(matInput,desimg,element);
    UIImage *imag=[[CVUtil sharedInstance]UIImageFromCVMat:desimg];
    return imag;
}

膨脹

//膨脹操作
- (UIImage *)dilateInPutImage:(UIImage *)inputImage value:(int)value{
    Mat matInput=[[CVUtil sharedInstance]cvMatFromUIImage:inputImage];
    Mat element;
    element=cv::getStructuringElement(MORPH_RECT, cv::Size(2*value+1,2*value+1),cv::Point(value,value));
    Mat desimg;
    dilate(matInput,desimg,element);
    UIImage *imag=[[CVUtil sharedInstance]UIImageFromCVMat:desimg];
    return imag;
}

邊緣檢測

//邊緣檢測
-(UIImage *)cannyInPutImage:(UIImage *)inputImage value:(int)value{
    if (value==0) {
        return inputImage;
    }
    Mat srcImage=[[CVUtil sharedInstance]cvMatFromUIImage:inputImage];
    Mat destImage;
    destImage.create(srcImage.size(), srcImage.type());
    Mat grayImage;
    cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);
    Mat edge;
    blur(grayImage,edge,cv::Size(value,value));
    Canny(edge, edge, 13, 9 ,3);
    destImage=Scalar::all(0);
    srcImage.copyTo(destImage, edge);
    UIImage *imag=[[CVUtil sharedInstance]UIImageFromCVMat:destImage];
    return imag;
}

影象對比度和亮度調整

//調整對比度和亮度
-(UIImage *)contrasAndBrightInPutImage:(UIImage *)inputImage alpha:(NSInteger)alpha beta:(NSInteger)beta{
    Mat g_srcImage=[[CVUtil sharedInstance]cvMatFromUIImage:inputImage];
    if(g_srcImage.empty()){
        return nil;
    }
    Mat g_dstImage=Mat::zeros(g_srcImage.size(),g_srcImage.type());
    int height=g_srcImage.rows;
    int width=g_srcImage.cols;
    for (int row=0; row<height; row++) {
        for (int col=0; col<width; col++) {
            for (int c=0; c<4; c++) {//4通道BGRA影象
                g_dstImage.at<Vec4b>(row,col)[c]=saturate_cast<uchar>((alpha*0.01)*(g_srcImage.at<Vec4b>(row,col)[c])+beta);
            }
        }
    }
    UIImage *imag=[[CVUtil sharedInstance]UIImageFromCVMat:g_dstImage];
    return imag;
}

總結

OpenCV 的應用領域非常廣泛,對於影象處理、人機互動及機器學習演演算法感興趣的可以選擇一個方向進行深入的研究。

到此這篇關於OpenCV- iOS 影象處理程式設計入門的文章就介紹到這了,更多相關OpenCV  iOS 影象處理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com