码迷,mamicode.com
首页 > 数据库 > 详细

实现multibandblend

时间:2017-10-10 09:53:01      阅读:1242      评论:0      收藏:0      [点我收藏+]

标签:scala   原理   过渡   rspec   做了   toe   技巧   uil   合并   

       multibandblend是目前图像融和方面比较好的方法。原始论文为《a multivesolution spline with application to image mosaics 》,可以用百度学术找到。原始论文中采用的方法是直接对带拼接的两个图片进行拉普拉斯金字塔分解,而后一半对一半进行融合。国人也有许多改进,比如“首先采用拉普拉斯分辨率金字塔结构,将输入的图像分解成一系列不同频率上的图像,而后在每个频段上面进行加权平均,把这些不同频率上的图像最后合并一个图像”。这个说法来源于下面这篇论文,根据其他获得的一些知识,应该是可行的。
      在opencv内部已经实现了multibandblend,但是耦合度比较高,想拿出来单独研究要对代码有深入理解;网络上面也有这样的实现,这个下文提及[]。为了深入理解并运用,还是要自己实现,再复用别人的代码
技术分享
一、首先实现laplacian金字塔的分解和重建
#include "stdafx.h"
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
using namespace std;
using namespace cv;
int _tmain(int argc_TCHARargv[])
{    
    Mat src = imread("Lena.jpg");    
    src.convertTo(src,CV_32F,1.0/255);
    imshow("src",src);
    Mat src2src.clone();
    Mat dst;
    Mat lastmat;
    vector<MatvecMats;
    Mat tmp;
    for (int i=0;i<4;i++)
    {
        pyrDown(src2,src2);
        pyrUp(src2,tmp);
        resize(tmp,tmp,src.size());
        tmp = src - tmp;
        vecMats.push_back(tmp);
        src = src2;
    }
    lastmat = src;
    //重建
    
    for (int i=3;i>=0;i--)
    {
        pyrUp(lastmat,lastmat);
        resize(lastmat,lastmat,vecMats[i].size());
        lastmat = lastmat + vecMats[i];
    }
    imshow("dst",lastmat);
    waitKey();
    return 0;
}
技术分享
使用工具比对也是完全一样的
技术分享
二、实现每个金字塔层面的linearblend融合
#include "stdafx.h"
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
using namespace std;
using namespace cv;
int _tmain(int argc_TCHARargv[])
{    
    Mat srcLeft = imread("apple.png");    
    Mat srcRightimread("orange.png");
    srcLeft.convertTo(srcLeft,CV_32F,1.0/255);
    srcRight.convertTo(srcRight,CV_32F,1.0/255);
    Mat rawLeft = srcLeft.clone();
    Mat rawRight=srcRight.clone();
    imshow("srcRight",srcRight);
    imshow("srcLeft",srcLeft);
    Mat srcLeft2srcLeft.clone();
    Mat srcRight2=srcRight.clone();
    Mat lastmatLeftMat lastmatRight;
    vector<MatvecMatsLeftvector<MatvecMatsRight;
    Mat tmp;
    for (int i=0;i<4;i++)
    {
        pyrDown(srcLeft2,srcLeft2);
        pyrUp(srcLeft2,tmp);
        resize(tmp,tmp,srcLeft.size());
        tmp = srcLeft - tmp;
        vecMatsLeft.push_back(tmp);
        srcLeft = srcLeft2;
    }
    lastmatLeft = srcLeft;
    for (int i=0;i<4;i++)
    {
        pyrDown(srcRight2,srcRight2);
        pyrUp(srcRight2,tmp);
        resize(tmp,tmp,srcRight.size());
        tmp = srcRight - tmp;
        vecMatsRight.push_back(tmp);
        srcRight = srcRight2;
    }
    lastmatRight = srcRight;
    //每一层都要对准并融合
    int ioffset =vecMatsLeft[0].cols - 100;//这里-100 的操作是linearblend的小技巧
    int istep = 1;
    double dblend = 0.0;
    vector<MatvecMatResult;//保存结果
    Mat tmpResult
    Mat roi;
    for (int i=0;i<4;i++)
    {
        //对准
        tmpResult = Mat::zeros(vecMatsLeft[i].rows,vecMatsLeft[i].cols*2,vecMatsLeft[i].type());
        
        roi     = tmpResult(Rect(ioffset,0,vecMatsLeft[i].cols,vecMatsLeft[i].rows));
        vecMatsRight[i].copyTo(roi);
        roi = tmpResult(Rect(0,0,vecMatsLeft[i].cols,vecMatsLeft[i].rows));
        vecMatsLeft[i].copyTo(roi);
        //融合 
        for (int j = 0;j<(100/istep);j++)
        {              
            tmpResult.col(ioffset + j)= tmpResult.col(ioffset+j)*(1-dblend) + vecMatsRight[i].col(j)*dblend;
            dblend = dblend +0.01*istep;
        }
        //结尾
        dblend = 0.0;
        ioffset = ioffset/2;
        istep = istep*2;    
        vecMatResult.push_back(tmpResult);
    }
    Mat latmatresult = Mat::zeros(lastmatLeft.rows,lastmatLeft.cols*2,lastmatLeft.type());
    roi = latmatresult(Rect(0,0,lastmatLeft.cols,lastmatLeft.rows));
    lastmatLeft.copyTo(roi);
    roi = latmatresult(Rect(ioffset,0,lastmatLeft.cols,lastmatLeft.rows));
    lastmatRight.copyTo(roi);
    for (int j=0;j<(100/istep);j++)
    {
        latmatresult.col(ioffset + j)= latmatresult.col(ioffset+j)*(1-dblend) + lastmatRight.col(j)*dblend;
        dblend = dblend +0.01*istep;
    }
    //重构
    for (int i=3;i>=0;i--)
    {
        pyrUp(lastmatLeft,lastmatLeft);
        resize(lastmatLeft,lastmatLeft,vecMatsLeft[i].size());
        lastmatLeft = lastmatLeft + vecMatsLeft[i];
    }
    for (int i=3;i>=0;i--)
    {
        pyrUp(lastmatRight,lastmatRight);
        resize(lastmatRight,lastmatRight,vecMatsRight[i].size());
        lastmatRight = lastmatRight + vecMatsRight[i];
    }
    for (int i=3;i>=0;i--)
    {
        pyrUp(latmatresult,latmatresult);
        resize(latmatresult,latmatresult,vecMatResult[i].size());
        latmatresult = latmatresult + vecMatResult[i];
    }
    imshow("lastmatLeft",lastmatLeft);
    lastmatLeft.convertTo(lastmatLeft,CV_8U,255);
    imwrite("lastmatleft.png",lastmatLeft);
    imshow("lastmatRight",lastmatRight);
    imshow("multibandblend",latmatresult);
    waitKey();
    return 0;
}
观看细节,可以看到过渡的地方一点都不违和,苹果上面的白色小斑点都过渡了过来。这个结果应该是非常接近论文上面的要求了。
技术分享
三、改造现有的图像拼接程序,并且运用于图像拼接
//使用Multiblend进行图像融合
#include "stdafx.h"
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include "matcher.h"
using namespace cv;
int main()
{
    // Read input images 这里的命名最好为imageleft和imageright
    cv::Mat image1cv::imread("Univ4.jpg",1);
    cv::Mat image2cv::imread("Univ5.jpg",1);
    if (!image1.data || !image2.data)
        return 0; 
     
    // Prepare the matcher
    RobustMatcher rmatcher;
    rmatcher.setConfidenceLevel(0.98);
    rmatcher.setMinDistanceToEpipolar(1.0);
    rmatcher.setRatio(0.65f);
    cv::Ptr<cv::FeatureDetectorpfdnew cv::SurfFeatureDetector(10); 
    rmatcher.setFeatureDetector(pfd);
    // Match the two images
    std::vector<cv::DMatchmatches;
    std::vector<cv::KeyPointkeypoints1keypoints2;
    cv::Mat fundementalrmatcher.match(image1,image2,matcheskeypoints1keypoints2);
    // draw the matches
    cv::Mat imageMatches;
    cv::drawMatches(image1,keypoints1,  // 1st image and its keypoints
                    image2,keypoints2,  // 2nd image and its keypoints
                    matches,            // the matches
                    imageMatches,        // the image produced
                    cv::Scalar(255,255,255)); // color of the lines
    // Convert keypoints into Point2f
    std::vector<cv::Point2fpoints1points2;
    for (std::vector<cv::DMatch>::const_iterator itmatches.begin();
         it!= matches.end(); ++it) {
             // Get the position of left keypoints
             float xkeypoints1[it->queryIdx].pt.x;
             float ykeypoints1[it->queryIdx].pt.y;
             points1.push_back(cv::Point2f(x,y));
             // Get the position of right keypoints
             xkeypoints2[it->trainIdx].pt.x;
             ykeypoints2[it->trainIdx].pt.y;
             points2.push_back(cv::Point2f(x,y));
    }
    std::cout << points1.size() << " " << points2.size() << std::endl
    // Find the homography between image 1 and image 2
    std::vector<ucharinliers(points1.size(),0);
    cv::Mat homographycv::findHomography(
        cv::Mat(points1),cv::Mat(points2), // corresponding points
        inliers,    // outputed inliers matches 
        CV_RANSAC,    // RANSAC method
        1.);        // max distance to reprojection point
    // Warp image 1 to image 2
    cv::Mat result;
    cv::warpPerspective(image1// input image
        result,            // output image
        homography,        // homography
        cv::Size(2*image1.cols,image1.rows)); // size of output image
    cv::Mat resultback;
    result.copyTo(resultback);
    // Copy image 1 on the first half of full image
    cv::Mat half(result,cv::Rect(0,0,image2.cols,image2.rows));
    image2.copyTo(half);
    // Display the warp image
    cv::namedWindow("After warping");
    cv::imshow("After warping",result);
    //需要注意的一点是,原始文件的图片是按照从右至左边进行移动的。
    //进行Multiblend的融合,融合的输入图像为image2(左)和resultback(右)
    Mat srcLeft = image2.clone();
    Mat srcRightresultback.clone();
    srcLeft.convertTo(srcLeft,CV_32F,1.0/255);
    srcRight.convertTo(srcRight,CV_32F,1.0/255);
    Mat rawLeft = srcLeft.clone();
    Mat rawRight=srcRight.clone();
    Mat srcLeft2srcLeft.clone();
    Mat srcRight2=srcRight.clone();
    Mat lastmatLeftMat lastmatRight;
    vector<MatvecMatsLeftvector<MatvecMatsRight;
    Mat tmp;
    for (int i=0;i<4;i++)
    {
        pyrDown(srcLeft2,srcLeft2);
        pyrUp(srcLeft2,tmp);
        resize(tmp,tmp,srcLeft.size());
        tmp = srcLeft - tmp;
        vecMatsLeft.push_back(tmp);
        srcLeft = srcLeft2;
    }
    lastmatLeft = srcLeft;
    for (int i=0;i<4;i++)
    {
        pyrDown(srcRight2,srcRight2);
        pyrUp(srcRight2,tmp);
        resize(tmp,tmp,srcRight.size());
        tmp = srcRight - tmp;
        vecMatsRight.push_back(tmp);
        srcRight = srcRight2;
    }
    lastmatRight = srcRight;
    //每一层都要对准并融合
    int ioffset =vecMatsLeft[0].cols - 100;//这里-100 的操作是linearblend的小技巧
    int istep = 1;
    double dblend = 0.0;
    vector<MatvecMatResult;//保存结果
    Mat tmpResult
    Mat roi;
    for (int i=0;i<4;i++)
    {
        //对准
        tmpResult = Mat::zeros(vecMatsLeft[i].rows,vecMatsLeft[i].cols*2,vecMatsLeft[i].type());
        roi     = tmpResult(Rect(0,0,vecMatsRight[i].cols,vecMatsRight[i].rows));
        vecMatsRight[i].copyTo(roi);
        roi = tmpResult(Rect(0,0,vecMatsLeft[i].cols,vecMatsLeft[i].rows));
        vecMatsLeft[i].copyTo(roi);
        //融合 
        for (int j = 0;j<(128/istep);j++)
        {              
            tmpResult.col(ioffset + j)= tmpResult.col(ioffset+j)*(1-dblend) + vecMatsRight[i].col(ioffset+j)*dblend;
            dblend = dblend +0.0078125*istep;
        }
        //结尾
        dblend = 0.0;
        ioffset = ioffset/2;
        istep = istep*2;    
        vecMatResult.push_back(tmpResult);
    }
    Mat latmatresult = Mat::zeros(lastmatLeft.rows,lastmatLeft.cols*2,lastmatLeft.type());
    roi = latmatresult(Rect(0,0,lastmatRight.cols,lastmatRight.rows));
    lastmatRight.copyTo(roi);
    roi = latmatresult(Rect(0,0,lastmatLeft.cols,lastmatLeft.rows));
    lastmatLeft.copyTo(roi);
    for (int j=0;j<(128/istep);j++)
    {
        latmatresult.col(ioffset+j)= latmatresult.col(ioffset+j)*(1-dblend) + lastmatRight.col(ioffset+j)*dblend;
        dblend = dblend +0.00725*istep;
    }
    //重构
    for (int i=3;i>=0;i--)
    {
        pyrUp(lastmatLeft,lastmatLeft);
        resize(lastmatLeft,lastmatLeft,vecMatsLeft[i].size());
        lastmatLeft = lastmatLeft + vecMatsLeft[i];
    }
    for (int i=3;i>=0;i--)
    {
        pyrUp(lastmatRight,lastmatRight);
        resize(lastmatRight,lastmatRight,vecMatsRight[i].size());
        lastmatRight = lastmatRight + vecMatsRight[i];
    }
    for (int i=3;i>=0;i--)
    {
        pyrUp(latmatresult,latmatresult);
        resize(latmatresult,latmatresult,vecMatResult[i].size());
        latmatresult = latmatresult + vecMatResult[i];
    }
    cv::waitKey();
    return 0;
}
可以看到纹理过渡不违和,但是光照变化剧烈
技术分享
四、改造现有代码
上文提及了已经实现的代码,这个代码写的还是相当不错的,其原理完全按照《a multivesolution spline with application to image mosaics 》来实现,为了应用于实际的图像拼接,做了一些修改
//使用Multiblend进行图像融合
//代码重构
#include "stdafx.h"
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include "matcher.h"
using namespace cv;
class LaplacianBlending {  
private:  
    Mat_<Vec3ftop;  
    Mat_<Vec3fdown;  
    Mat_<floatblendMask;  
    vector<Mat_<Vec3f> > topLapPyr,downLapPyr,resultLapPyr;//Laplacian Pyramids   
    Mat topHighestLeveldownHighestLevelresultHighestLevel;  
    vector<Mat_<Vec3f> > maskGaussianPyramid//masks are 3-channels for easier multiplication with RGB   
    int levels;  
    //创建金字塔
    void buildPyramids() {  
        //参数的解释 top就是top ,topLapPyr就是top的laplacian的pyr,而topHighestLevel保存的是最高端的高斯金字塔
        buildLaplacianPyramid(top,topLapPyr,topHighestLevel);   
        buildLaplacianPyramid(down,downLapPyr,downHighestLevel);  
        buildGaussianPyramid();  
    }  
    //创建gauss金字塔
    void buildGaussianPyramid() {//金字塔内容为每一层的掩模   
        assert(topLapPyr.size()>0);  
        maskGaussianPyramid.clear();  
        Mat currentImg;  
        //blendMask就是结果所在
        cvtColor(blendMaskcurrentImgCV_GRAY2BGR);//store color img of blend mask into maskGaussianPyramid   
        maskGaussianPyramid.push_back(currentImg); //0-level   
        currentImg = blendMask;  
        for (int l=1; l<levels+1; l++) {  
            Mat _down;  
            if (topLapPyr.size() > l)  
                pyrDown(currentImg_downtopLapPyr[l].size());  
            else  
                pyrDown(currentImg_downtopHighestLevel.size()); //lowest level   
            Mat down;  
            cvtColor(_downdownCV_GRAY2BGR);  
            maskGaussianPyramid.push_back(down);//add color blend mask into mask Pyramid   
            currentImg = _down;  
        }  
    }  
    //创建laplacian金字塔
    void buildLaplacianPyramid(const Matimgvector<Mat_<Vec3f> >& lapPyrMatHighestLevel) {  
        lapPyr.clear();  
        Mat currentImg = img;  
        for (int l=0; l<levelsl++) {  
            Mat down,up;  
            pyrDown(currentImgdown);  
            pyrUp(downup,currentImg.size());  
            Mat lap = currentImg - up;  //存储的就是残差
            lapPyr.push_back(lap);  
            currentImg = down;  
        }  
        currentImg.copyTo(HighestLevel);  
    }  
    Mat_<Vec3freconstructImgFromLapPyramid() {  
        //将左右laplacian图像拼成的resultLapPyr金字塔中每一层   
        //从上到下插值放大并相加,即得blend图像结果   
        Mat currentImg = resultHighestLevel;  
        for (int l=levels-1; l>=0; l--) {  
            Mat up;  
            pyrUp(currentImgupresultLapPyr[l].size());  
            currentImg = up + resultLapPyr[l];  
        }  
        return currentImg;  
    }  
    void blendLapPyrs() {  
        //获得每层金字塔中直接用左右两图Laplacian变换拼成的图像resultLapPyr   
        resultHighestLevel = topHighestLevel.mul(maskGaussianPyramid.back()) +  
            downHighestLevel.mul(Scalar(1.0,1.0,1.0) - maskGaussianPyramid.back());  
        for (int l=0; l<levelsl++) {  
            Mat A = topLapPyr[l].mul(maskGaussianPyramid[l]);  
            Mat antiMask = Scalar(1.0,1.0,1.0) - maskGaussianPyramid[l];  
            Mat B = downLapPyr[l].mul(antiMask);  
            Mat_<Vec3fblendedLevel = A + B;  
            resultLapPyr.push_back(blendedLevel);  
        }  
    }  
public:  
    LaplacianBlending(const Mat_<Vec3f>& _topconst Mat_<Vec3f>& _downconst Mat_<float>& _blendMaskint _levels)://缺省数据,使用 LaplacianBlending lb(l,r,m,4);   
        top(_top),down(_down),blendMask(_blendMask),levels(_levels)  
    {  
        assert(_top.size() == _down.size());  
        assert(_top.size() == _blendMask.size());  
        buildPyramids();  //创建laplacian金字塔和gauss金字塔 
        blendLapPyrs();   //将左右金字塔融合成为一个图片   
    };  
    Mat_<Vec3fblend() {  
        return reconstructImgFromLapPyramid();//reconstruct Image from Laplacian Pyramid   
    }  
};  
Mat_<Vec3fLaplacianBlend(const Mat_<Vec3f>& tconst Mat_<Vec3f>& dconst Mat_<float>& m) {  
    LaplacianBlending lb(t,d,m,4);  
    return lb.blend();  
}  
int main()
{
    // Read input images 这里的命名最好为imageleft和imageright
    cv::Mat image1cv::imread("Univ3.jpg",1);
    cv::Mat image2cv::imread("Univ4.jpg",1);
    if (!image1.data || !image2.data)
        return 0; 
     
    // Prepare the matcher
    RobustMatcher rmatcher;
    rmatcher.setConfidenceLevel(0.98);
    rmatcher.setMinDistanceToEpipolar(1.0);
    rmatcher.setRatio(0.65f);
    cv::Ptr<cv::FeatureDetectorpfdnew cv::SurfFeatureDetector(10); 
    rmatcher.setFeatureDetector(pfd);
    // Match the two images
    std::vector<cv::DMatchmatches;
    std::vector<cv::KeyPointkeypoints1keypoints2;
    cv::Mat fundementalrmatcher.match(image1,image2,matcheskeypoints1keypoints2);
    // draw the matches
    cv::Mat imageMatches;
    cv::drawMatches(image1,keypoints1,  // 1st image and its keypoints
                    image2,keypoints2,  // 2nd image and its keypoints
                    matches,            // the matches
                    imageMatches,        // the image produced
                    cv::Scalar(255,255,255)); // color of the lines
    // Convert keypoints into Point2f
    std::vector<cv::Point2fpoints1points2;
    for (std::vector<cv::DMatch>::const_iterator itmatches.begin();
         it!= matches.end(); ++it) {
             // Get the position of left keypoints
             float xkeypoints1[it->queryIdx].pt.x;
             float ykeypoints1[it->queryIdx].pt.y;
             points1.push_back(cv::Point2f(x,y));
             // Get the position of right keypoints
             xkeypoints2[it->trainIdx].pt.x;
             ykeypoints2[it->trainIdx].pt.y;
             points2.push_back(cv::Point2f(x,y));
    }
    std::cout << points1.size() << " " << points2.size() << std::endl
    // Find the homography between image 1 and image 2
    std::vector<ucharinliers(points1.size(),0);
    cv::Mat homographycv::findHomography(
        cv::Mat(points1),cv::Mat(points2), // corresponding points
        inliers,    // outputed inliers matches 
        CV_RANSAC,    // RANSAC method
        1.);        // max distance to reprojection point
    // Warp image 1 to image 2
    cv::Mat result;
    cv::warpPerspective(image1// input image
        result,            // output image
        homography,        // homography
        cv::Size(2*image1.cols,image1.rows)); // size of output image
    cv::Mat resultback;
    result.copyTo(resultback);
    // Copy image 1 on the first half of full image
    cv::Mat half(result,cv::Rect(0,0,image2.cols,image2.rows));
    image2.copyTo(half);
    // Display the warp image
    cv::namedWindow("After warping");
    cv::imshow("After warping",result);
    //需要注意的一点是,原始文件的图片是按照从右至左边进行移动的。
    //进行Multiblend的融合,融合的输入图像为image2(左)和resultback(右)
    Mat srcLeft = image2.clone();
    Mat srcRightresultback.clone();
    int ioffset = srcLeft.cols -100;
    Mat imageL = srcLeft(Rect(ioffset,0,100,srcLeft.rows)).clone();
    Mat imageR = srcRight(Rect(ioffset,0,100,srcLeft.rows)).clone();
    Mat_<Vec3ftimageL.convertTo(t,CV_32F,1.0/255.0);//Vec3f表示有三个通道,即 l[row][column][depth]   
    Mat_<Vec3fdimageR.convertTo(d,CV_32F,1.0/255.0);  
 
    Mat_<floatm(t.rows,d.cols,0.0);                 //将m全部赋值为0   
    m(Range::all(),Range(0,m.cols/2)) = 1.0;    //取m全部行&[0,m.cols/2]列,赋值为1.0   
    Mat_<Vec3fmatblend = LaplacianBlend(t,dm);  
    Mat re;
    matblend.convertTo(re,CV_8UC3,255);
    
    Mat roi = srcLeft(Rect(ioffset,0,100,srcLeft.rows));
    re.copyTo(roi);
    roi = resultback(Rect(0,0,srcLeft.cols,srcLeft.rows));
    srcLeft.copyTo(roi);
    cv::waitKey();
    return 0;
}
五、结果比较
我写的multibandblend
技术分享
liner
技术分享
multibendblend
技术分享
技术分享
六、小结
应该说我采用的算法,虽然也有一定程度上效果,但是“鬼影”并没有减少。采用linearblend的方法,不管分不分金字塔,都是“鬼影”产生的原因。研究问题,还是应该读原始论文,找到好的代码,真正地解决问题。
这个论文背后的原理是什么?
这个代码是谁写的,来自哪里?
搞明白这些问题。




附件列表

     

    实现multibandblend

    标签:scala   原理   过渡   rspec   做了   toe   技巧   uil   合并   

    原文地址:http://www.cnblogs.com/jsxyhelu/p/7643855.html

    (0)
    (0)
       
    举报
    评论 一句话评论(0
    登录后才能评论!
    © 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
    迷上了代码!