#Stitcher类与detail命名空间
OpenCV提供了高级别的函数封装在Stitcher类中,使用很方便,不用考虑太多的细节。

低级别函数封装在detail命名空间中,展示了OpenCV算法实现的很多步骤和细节,使熟悉如下拼接流水线的用户,方便自己定制。

可见OpenCV图像拼接模块的实现是十分精密和复杂的,拼接的结果很完善,但同时也是费时的,完全不能够实现实时应用。

我在研究detail源码时,由于水平有限,并不能自由灵活地对各种部件取其所需,取舍随意。

官方提供的stitching和stitching_detailed使用示例,分别是高级别和低级别封装这两种方式正确地使用示例。两种结果产生的拼接结果相同,后者却可以允许用户,在参数变量初始化时,选择各项算法。如下所示:

这涉及到以下算法流程:

  1. 命令行调用程序,输入源图像以及程序的参数

  2. 特征点检测,判断是使用surf还是orb,默认是surf。

  3. 对图像的特征点进行匹配,使用最近邻和次近邻方法,
    将两个最优的匹配的置信度保存下来。

  4. 对图像进行排序以及将置信度高的图像保存到同一个集合中,
    删除置信度比较低的图像间的匹配,得到能正确匹配的图像序列。
    这样将置信度高于门限的所有匹配合并到一个集合中。

  5. 对所有图像进行相机参数粗略估计,然后求出旋转矩阵

  6. 使用光束平均法进一步精准的估计出旋转矩阵。

  7. 波形校正,水平或者垂直

  8. 拼接

  9. 融合,多频段融合,光照补偿,


#Stitcher类使用示例

#include <iostream>
#include <fstream>
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/stitching/stitcher.hpp"
#include<io.h>using namespace std;
using namespace cv;bool try_use_gpu = false;
vector<Mat> imgs;
string result_name = "result.jpg";void getFiles(string path, vector<string>& files)
{//文件句柄  long   hFile = 0;//文件信息  struct _finddata_t fileinfo;string p;if ((hFile = _findfirst(p.assign(path).append("/*").c_str(), &fileinfo)) != -1){do{//如果是目录,迭代之  //如果不是,加入列表  if ((fileinfo.attrib &  _A_SUBDIR)){if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)getFiles(p.assign(path).append("/").append(fileinfo.name), files);}else{files.push_back(p.assign(path).append("/").append(fileinfo.name));}} while (_findnext(hFile, &fileinfo) == 0);_findclose(hFile);}
}int main(int argc, char* argv[])
{vector<string> filesName;getFiles("E:/workspace/iamge/dataset/",filesName);for (string fileName:filesName){Mat img = imread(fileName,1);imgs.push_back(img);}Mat pano;Stitcher stitcher = Stitcher::createDefault(try_use_gpu);Stitcher::Status status = stitcher.stitch(imgs, pano);if (status != Stitcher::OK){cout << "Can't stitch images, error code = " << int(status) << endl;return -1;}imwrite(result_name, pano);imshow(result_name,pano);waitKey(0);return 0;
}

其中的getfiles()函数的功能是获取一个目录下的所有文件地址。这使得可以在windows下批量的读取图像的地址。


#stitching_detailed使用示例

#include <iostream>
#include <fstream>
#include <string>
#include "opencv2/opencv_modules.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/stitching/detail/autocalib.hpp"
#include "opencv2/stitching/detail/blenders.hpp"
#include "opencv2/stitching/detail/camera.hpp"
#include "opencv2/stitching/detail/exposure_compensate.hpp"
#include "opencv2/stitching/detail/matchers.hpp"
#include "opencv2/stitching/detail/motion_estimators.hpp"
#include "opencv2/stitching/detail/seam_finders.hpp"
#include "opencv2/stitching/detail/util.hpp"
#include "opencv2/stitching/detail/warpers.hpp"
#include "opencv2/stitching/warpers.hpp"using namespace std;
using namespace cv;
using namespace cv::detail;//
#define ENABLE_LOG 1// Default command line args
vector<string> img_names;
bool preview = false;
bool try_gpu = true;
double work_megapix = 0.6;
double seam_megapix = 0.1;
double compose_megapix = -1;
float conf_thresh = 1.f;
string features_type = "surf";
string ba_cost_func = "ray";
string ba_refine_mask = "xxxxx";
bool do_wave_correct = true;
WaveCorrectKind wave_correct = detail::WAVE_CORRECT_HORIZ;
bool save_graph = false;
std::string save_graph_to;
string warp_type = "spherical";
int expos_comp_type = ExposureCompensator::GAIN_BLOCKS;
float match_conf = 0.3f;
string seam_find_type = "gc_color";
int blend_type = Blender::MULTI_BAND;
float blend_strength = 5;
string result_name = "result.jpg";int main(int argc, char* argv[])
{//读入图像double ttt = getTickCount();img_names.push_back("E:/workspace/iamge/dataset/yard1.jpg");img_names.push_back("E:/workspace/iamge/dataset/yard2.jpg");img_names.push_back("E:/workspace/iamge/dataset/yard3.jpg");img_names.push_back("E:/workspace/iamge/dataset/yard4.jpg");#if ENABLE_LOGint64 app_start_time = getTickCount();
#endifcv::setBreakOnError(true);/*int retval = parseCmdArgs(argc, argv);if (retval)return retval;*/// Check if have enough imagesint num_images = static_cast<int>(img_names.size());if (num_images < 2){LOGLN("Need more images");return -1;}double work_scale = 1, seam_scale = 1, compose_scale = 1;bool is_work_scale_set = false, is_seam_scale_set = false, is_compose_scale_set = false;LOGLN("Finding features...");
#if ENABLE_LOGint64 t = getTickCount();
#endifPtr<FeaturesFinder> finder;if (features_type == "surf"){#if defined(HAVE_OPENCV_NONFREE) && defined(HAVE_OPENCV_GPU)if (try_gpu && gpu::getCudaEnabledDeviceCount() > 0)finder = new SurfFeaturesFinderGpu();else
#endiffinder = new SurfFeaturesFinder();}else if (features_type == "orb"){finder = new OrbFeaturesFinder();}else{cout << "Unknown 2D features type: '" << features_type << "'.\n";return -1;}Mat full_img, img;vector<ImageFeatures> features(num_images);vector<Mat> images(num_images);vector<Size> full_img_sizes(num_images);double seam_work_aspect = 1;for (int i = 0; i < num_images; ++i){full_img = imread(img_names[i]);full_img_sizes[i] = full_img.size();if (full_img.empty()){LOGLN("Can't open image " << img_names[i]);return -1;}if (work_megapix < 0){img = full_img;work_scale = 1;is_work_scale_set = true;}else{if (!is_work_scale_set){work_scale = min(1.0, sqrt(work_megapix * 1e6 / full_img.size().area()));is_work_scale_set = true;}resize(full_img, img, Size(), work_scale, work_scale);}if (!is_seam_scale_set){seam_scale = min(1.0, sqrt(seam_megapix * 1e6 / full_img.size().area()));seam_work_aspect = seam_scale / work_scale;is_seam_scale_set = true;}(*finder)(img, features[i]);features[i].img_idx = i;LOGLN("Features in image #" << i+1 << ": " << features[i].keypoints.size());resize(full_img, img, Size(), seam_scale, seam_scale);images[i] = img.clone();}finder->collectGarbage();full_img.release();img.release();LOGLN("Finding features, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec");LOG("Pairwise matching");
#if ENABLE_LOGt = getTickCount();
#endifvector<MatchesInfo> pairwise_matches;BestOf2NearestMatcher matcher(try_gpu, match_conf);matcher(features, pairwise_matches);matcher.collectGarbage();LOGLN("Pairwise matching, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec");// Check if we should save matches graphif (save_graph){LOGLN("Saving matches graph...");ofstream f(save_graph_to.c_str());f << matchesGraphAsString(img_names, pairwise_matches, conf_thresh);}// Leave only images we are sure are from the same panoramavector<int> indices = leaveBiggestComponent(features, pairwise_matches, conf_thresh);vector<Mat> img_subset;vector<string> img_names_subset;vector<Size> full_img_sizes_subset;for (size_t i = 0; i < indices.size(); ++i){img_names_subset.push_back(img_names[indices[i]]);img_subset.push_back(images[indices[i]]);full_img_sizes_subset.push_back(full_img_sizes[indices[i]]);}images = img_subset;img_names = img_names_subset;full_img_sizes = full_img_sizes_subset;// Check if we still have enough imagesnum_images = static_cast<int>(img_names.size());if (num_images < 2){LOGLN("Need more images");return -1;}HomographyBasedEstimator estimator;vector<CameraParams> cameras;estimator(features, pairwise_matches, cameras);for (size_t i = 0; i < cameras.size(); ++i){Mat R;cameras[i].R.convertTo(R, CV_32F);cameras[i].R = R;LOGLN("Initial intrinsics #" << indices[i]+1 << ":\n" << cameras[i].K());}Ptr<detail::BundleAdjusterBase> adjuster;if (ba_cost_func == "reproj") adjuster = new detail::BundleAdjusterReproj();else if (ba_cost_func == "ray") adjuster = new detail::BundleAdjusterRay();else{cout << "Unknown bundle adjustment cost function: '" << ba_cost_func << "'.\n";return -1;}adjuster->setConfThresh(conf_thresh);Mat_<uchar> refine_mask = Mat::zeros(3, 3, CV_8U);if (ba_refine_mask[0] == 'x') refine_mask(0,0) = 1;if (ba_refine_mask[1] == 'x') refine_mask(0,1) = 1;if (ba_refine_mask[2] == 'x') refine_mask(0,2) = 1;if (ba_refine_mask[3] == 'x') refine_mask(1,1) = 1;if (ba_refine_mask[4] == 'x') refine_mask(1,2) = 1;adjuster->setRefinementMask(refine_mask);(*adjuster)(features, pairwise_matches, cameras);// Find median focal lengthvector<double> focals;for (size_t i = 0; i < cameras.size(); ++i){LOGLN("Camera #" << indices[i]+1 << ":\n" << cameras[i].K());focals.push_back(cameras[i].focal);}sort(focals.begin(), focals.end());float warped_image_scale;if (focals.size() % 2 == 1)warped_image_scale = static_cast<float>(focals[focals.size() / 2]);elsewarped_image_scale = static_cast<float>(focals[focals.size() / 2 - 1] + focals[focals.size() / 2]) * 0.5f;if (do_wave_correct){vector<Mat> rmats;for (size_t i = 0; i < cameras.size(); ++i)rmats.push_back(cameras[i].R.clone());waveCorrect(rmats, wave_correct);for (size_t i = 0; i < cameras.size(); ++i)cameras[i].R = rmats[i];}LOGLN("Warping images (auxiliary)... ");
#if ENABLE_LOGt = getTickCount();
#endifvector<Point> corners(num_images);vector<Mat> masks_warped(num_images);vector<Mat> images_warped(num_images);vector<Size> sizes(num_images);vector<Mat> masks(num_images);// Preapre images masksfor (int i = 0; i < num_images; ++i){masks[i].create(images[i].size(), CV_8U);masks[i].setTo(Scalar::all(255));}// Warp images and their masksPtr<WarperCreator> warper_creator;
#if defined(HAVE_OPENCV_GPU)if (try_gpu && gpu::getCudaEnabledDeviceCount() > 0){if (warp_type == "plane") warper_creator = new cv::PlaneWarperGpu();else if (warp_type == "cylindrical") warper_creator = new cv::CylindricalWarperGpu();else if (warp_type == "spherical") warper_creator = new cv::SphericalWarperGpu();}else
#endif{if (warp_type == "plane") warper_creator = new cv::PlaneWarper();else if (warp_type == "cylindrical") warper_creator = new cv::CylindricalWarper();else if (warp_type == "spherical") warper_creator = new cv::SphericalWarper();else if (warp_type == "fisheye") warper_creator = new cv::FisheyeWarper();else if (warp_type == "stereographic") warper_creator = new cv::StereographicWarper();else if (warp_type == "compressedPlaneA2B1") warper_creator = new cv::CompressedRectilinearWarper(2, 1);else if (warp_type == "compressedPlaneA1.5B1") warper_creator = new cv::CompressedRectilinearWarper(1.5, 1);else if (warp_type == "compressedPlanePortraitA2B1") warper_creator = new cv::CompressedRectilinearPortraitWarper(2, 1);else if (warp_type == "compressedPlanePortraitA1.5B1") warper_creator = new cv::CompressedRectilinearPortraitWarper(1.5, 1);else if (warp_type == "paniniA2B1") warper_creator = new cv::PaniniWarper(2, 1);else if (warp_type == "paniniA1.5B1") warper_creator = new cv::PaniniWarper(1.5, 1);else if (warp_type == "paniniPortraitA2B1") warper_creator = new cv::PaniniPortraitWarper(2, 1);else if (warp_type == "paniniPortraitA1.5B1") warper_creator = new cv::PaniniPortraitWarper(1.5, 1);else if (warp_type == "mercator") warper_creator = new cv::MercatorWarper();else if (warp_type == "transverseMercator") warper_creator = new cv::TransverseMercatorWarper();}if (warper_creator.empty()){cout << "Can't create the following warper '" << warp_type << "'\n";return 1;}Ptr<RotationWarper> warper = warper_creator->create(static_cast<float>(warped_image_scale * seam_work_aspect));for (int i = 0; i < num_images; ++i){Mat_<float> K;cameras[i].K().convertTo(K, CV_32F);float swa = (float)seam_work_aspect;K(0,0) *= swa; K(0,2) *= swa;K(1,1) *= swa; K(1,2) *= swa;corners[i] = warper->warp(images[i], K, cameras[i].R, INTER_LINEAR, BORDER_REFLECT, images_warped[i]);sizes[i] = images_warped[i].size();warper->warp(masks[i], K, cameras[i].R, INTER_NEAREST, BORDER_CONSTANT, masks_warped[i]);}vector<Mat> images_warped_f(num_images);for (int i = 0; i < num_images; ++i)images_warped[i].convertTo(images_warped_f[i], CV_32F);LOGLN("Warping images, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec");Ptr<ExposureCompensator> compensator = ExposureCompensator::createDefault(expos_comp_type);compensator->feed(corners, images_warped, masks_warped);Ptr<SeamFinder> seam_finder;if (seam_find_type == "no")seam_finder = new detail::NoSeamFinder();else if (seam_find_type == "voronoi")seam_finder = new detail::VoronoiSeamFinder();else if (seam_find_type == "gc_color"){#if defined(HAVE_OPENCV_GPU)if (try_gpu && gpu::getCudaEnabledDeviceCount() > 0)seam_finder = new detail::GraphCutSeamFinderGpu(GraphCutSeamFinderBase::COST_COLOR);else
#endifseam_finder = new detail::GraphCutSeamFinder(GraphCutSeamFinderBase::COST_COLOR);}else if (seam_find_type == "gc_colorgrad"){#if defined(HAVE_OPENCV_GPU)if (try_gpu && gpu::getCudaEnabledDeviceCount() > 0)seam_finder = new detail::GraphCutSeamFinderGpu(GraphCutSeamFinderBase::COST_COLOR_GRAD);else
#endifseam_finder = new detail::GraphCutSeamFinder(GraphCutSeamFinderBase::COST_COLOR_GRAD);}else if (seam_find_type == "dp_color")seam_finder = new detail::DpSeamFinder(DpSeamFinder::COLOR);else if (seam_find_type == "dp_colorgrad")seam_finder = new detail::DpSeamFinder(DpSeamFinder::COLOR_GRAD);if (seam_finder.empty()){cout << "Can't create the following seam finder '" << seam_find_type << "'\n";return 1;}seam_finder->find(images_warped_f, corners, masks_warped);// Release unused memoryimages.clear();images_warped.clear();images_warped_f.clear();masks.clear();LOGLN("Compositing...");
#if ENABLE_LOGt = getTickCount();
#endifMat img_warped, img_warped_s;Mat dilated_mask, seam_mask, mask, mask_warped;Ptr<Blender> blender;//double compose_seam_aspect = 1;double compose_work_aspect = 1;for (int img_idx = 0; img_idx < num_images; ++img_idx){LOGLN("Compositing image #" << indices[img_idx]+1);// Read image and resize it if necessaryfull_img = imread(img_names[img_idx]);if (!is_compose_scale_set){if (compose_megapix > 0)compose_scale = min(1.0, sqrt(compose_megapix * 1e6 / full_img.size().area()));is_compose_scale_set = true;// Compute relative scales//compose_seam_aspect = compose_scale / seam_scale;compose_work_aspect = compose_scale / work_scale;// Update warped image scalewarped_image_scale *= static_cast<float>(compose_work_aspect);warper = warper_creator->create(warped_image_scale);// Update corners and sizesfor (int i = 0; i < num_images; ++i){// Update intrinsicscameras[i].focal *= compose_work_aspect;cameras[i].ppx *= compose_work_aspect;cameras[i].ppy *= compose_work_aspect;// Update corner and sizeSize sz = full_img_sizes[i];if (std::abs(compose_scale - 1) > 1e-1){sz.width = cvRound(full_img_sizes[i].width * compose_scale);sz.height = cvRound(full_img_sizes[i].height * compose_scale);}Mat K;cameras[i].K().convertTo(K, CV_32F);Rect roi = warper->warpRoi(sz, K, cameras[i].R);corners[i] = roi.tl();sizes[i] = roi.size();}}if (abs(compose_scale - 1) > 1e-1)resize(full_img, img, Size(), compose_scale, compose_scale);elseimg = full_img;full_img.release();Size img_size = img.size();Mat K;cameras[img_idx].K().convertTo(K, CV_32F);// Warp the current imagewarper->warp(img, K, cameras[img_idx].R, INTER_LINEAR, BORDER_REFLECT, img_warped);// Warp the current image maskmask.create(img_size, CV_8U);mask.setTo(Scalar::all(255));warper->warp(mask, K, cameras[img_idx].R, INTER_NEAREST, BORDER_CONSTANT, mask_warped);// Compensate exposurecompensator->apply(img_idx, corners[img_idx], img_warped, mask_warped);img_warped.convertTo(img_warped_s, CV_16S);img_warped.release();img.release();mask.release();dilate(masks_warped[img_idx], dilated_mask, Mat());resize(dilated_mask, seam_mask, mask_warped.size());mask_warped = seam_mask & mask_warped;if (blender.empty()){blender = Blender::createDefault(blend_type, try_gpu);Size dst_sz = resultRoi(corners, sizes).size();float blend_width = sqrt(static_cast<float>(dst_sz.area())) * blend_strength / 100.f;if (blend_width < 1.f)blender = Blender::createDefault(Blender::NO, try_gpu);else if (blend_type == Blender::MULTI_BAND){MultiBandBlender* mb = dynamic_cast<MultiBandBlender*>(static_cast<Blender*>(blender));mb->setNumBands(static_cast<int>(ceil(log(blend_width)/log(2.)) - 1.));LOGLN("Multi-band blender, number of bands: " << mb->numBands());}else if (blend_type == Blender::FEATHER){FeatherBlender* fb = dynamic_cast<FeatherBlender*>(static_cast<Blender*>(blender));fb->setSharpness(1.f/blend_width);LOGLN("Feather blender, sharpness: " << fb->sharpness());}blender->prepare(corners, sizes);}// Blend the current imageblender->feed(img_warped_s, mask_warped, corners[img_idx]);}Mat result, result_mask;blender->blend(result, result_mask);LOGLN("Compositing, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec");imwrite(result_name, result);result.convertTo(result,CV_8UC1);imshow("stitch",result);ttt = ((double)getTickCount() - ttt) / getTickFrequency();cout << "总的拼接时间:" << ttt << endl;waitKey(0);LOGLN("Finished, total time: " << ((getTickCount() - app_start_time) / getTickFrequency()) << " sec");return 0;
}

#拼接结果

输入4张图像,每张分辨率为327*245,总的拼接时间为9.25s。

图像拼接(十):OPenCV stitching和stitching_detailed相关推荐

  1. OpenCV图像拼接之Stitching和Stitching_detailed

    Stitcher类与detail命名空间 OpenCV提供了高级别的函数封装在Stitcher类中,使用很方便,不用考虑太多的细节. 低级别函数封装在detail命名空间中,展示了opencv算法实现 ...

  2. OPenCV 图像拼接之------stitching和stitching_detailed

    Stitcher类与detail命名空间 OpenCV提供了高级别的函数封装在Stitcher类中,使用很方便,不用考虑太多的细节. 低级别函数封装在detail命名空间中,展示了opencv算法实现 ...

  3. 图像拼接|OpenCV3.4 stitching源码分析(一)续

    图像拼接|OpenCV3.4 stitching源码分析(一)续 前言 OpenCV与VLFeat的SIFT实现之对比 opencv vlfeat 参考 前言 图像拼接|--OpenCV3.4 sti ...

  4. 图像拼接--Seam-Driven Image Stitching

    缝合线驱动的图像拼接 Seam-Driven Image Stitching Eurographics (EG) 2013 上图对比了 传统图像拼接方法 和 缝合线驱动的图像拼接 Traditiona ...

  5. 图像拼接|OpenCV3.4 stitching源码分析(一)

    图像拼接|OpenCV3.4 stitching源码分析(一) 前言 特征点检测 源码 应用 前言 图像拼接|--OpenCV3.4 stitching模块分析(一)特征点检测 参考opencv_赵春 ...

  6. 《OpenCv视觉之眼》Python图像处理十 :Opencv图像形态学处理之开运算、闭运算和梯度运算原理及方法

    本专栏主要介绍如果通过OpenCv-Python进行图像处理,通过原理理解OpenCv-Python的函数处理原型,在具体情况中,针对不同的图像进行不同等级的.不同方法的处理,以达到对图像进行去噪.锐 ...

  7. OpenCV图像拼接-Stitcher类-Stitching detailed使用与参数介绍

    关于OpenCV图像拼接的方法,如果不熟悉的话,可以先看看我整理的如下四篇博客: OpenCV常用图像拼接方法(一):直接拼接(硬拼) OpenCV常用图像拼接方法(二):基于模板匹配拼接 OpenC ...

  8. OpenCV Stitching 工程搭建

     转自http://www.tuicool.com/articles/fMbUfaF Opencv中提供Stitcher类,实现了多图像自动拼接,Opencv是开源的,程序实现的源代码都在Open ...

  9. opencv stitching算法分析

    原文地址:http://blog.csdn.net/skeeee/article/details/19480693?utm_source=tuicool 一.stitching_detail程序运行流 ...

最新文章

  1. android jni 调用 java_Android与JNI(二) ---- Java调用C++ 动态调用
  2. Linux 操作系统原理 — 文件系统 —文件
  3. QT的QGraphicsLinearLayout类的使用
  4. libxml中用到的Xpath语法说明
  5. 每天一道LeetCode-----计算整型数二进制中1的个数/返回二进制翻转后的结果
  6. html5游戏引擎-Pharse.js学习笔记(一)
  7. 如何查看jsplumb.js的API文档(YUIdoc的基本使用)#华为云·寻找黑马程序员#
  8. Error:java: Invalid additional meta-data in ‘META-INF/spring-configuration-metadata.json‘: End of in
  9. 【java学习之路】(数据结构篇)004.递归和二叉搜索树
  10. java里面的pai_Java - ZhangPai - 博客园
  11. sql常用语句之DDL
  12. 一加手机刷入第三方Rec
  13. linux控制风扇转速的命令,Cputroller:一款Linux下查看调节CPU的策略、风扇转速的工具...
  14. LCN分布式事务(Java)
  15. 遇到“无法浏览网页”教你十招解决疑难杂症
  16. css中找不到bordercolor,CSS里bordercolor要怎样使用
  17. 同步传输和异步传输_同步和异步传输| 数据通讯
  18. EDVR和FastDVD
  19. PHP 通过单号查询快递( 申通、EMS、顺丰、圆通、中通、韵达、天天、汇通、全峰、德邦、宅急送)
  20. 端口扫描程序设计c语言,主机端口扫描程序设计.doc

热门文章

  1. Android KK平台的一个bug----在收到内容只有一个“=”的信息后,手机自动重启
  2. 并查集训练题解(F-J)
  3. EI检索的国际会议有这些
  4. 非科班学python就业_非科班出身自学Python,这些实用方法学习方法你知道吗!
  5. 五.抽象接口与依赖反转(C面向对象开发)
  6. 信用卡套现千万别触碰这两条红线,否则银行会盯上你!
  7. 通达信经典实用选股公式
  8. 设计模式八(享元模式)
  9. python -itchat实现把文件传输助手当作linux的shell
  10. 启明医疗完成对Keystone Heart有限公司的收购