在这篇文章中,我们将解释什么是 ArUco 标记,以及如何使用 OpenCV 将它们用于简单的增强现实任务。

ArUco 标记已经在增强现实、相机姿态估计和相机校准中使用了一段时间。让我们更多地了解它们。

1.什么是 ArUco 标记?

ArUco 代表科尔多瓦增强现实大学。那是它在西班牙开发的地方。下面是 ArUco 标记的一些示例。

aruco 标记是放置在被成像物体或场景上的基准标记。它是一个带有黑色背景和边界的二进制正方形,其中生成的白色图案唯一地标识了它。黑色的边界使它们更容易被发现。它们可以以各种大小生成。根据物体大小和场景来选择尺寸,以便成功检测。如果非常小的标记没有被检测到,只需增加它们的大小就可以使它们更容易被检测到。

这个想法是,你打印这些标记,并把它们放在现实世界中。您可以拍摄真实世界,并独特地检测这些标记。

如果您是初学者,您可能会想这有什么用?让我们看几个用例。

在我们分享的示例中,我们将印刷品和标记放在相框的角上。当我们唯一地识别标记时,我们就能够用任意视频或图像替换相框。当我们移动相机时,新图片具有正确的透视失真。

在机器人应用程序中,您可以将这些标记放置在配备摄像头的仓库机器人的路径上。当安装在机器人上的摄像头检测到这些标记中的一个时,它可以知道它在仓库中的精确位置,因为每个标记都有一个唯一的 ID,我们知道标记在仓库中的位置。

2.在 OpenCV 中生成 ArUco 标记

我们可以使用 OpenCV 非常轻松地生成这些标记。 OpenCV 中的 aruco 模块共有 25 个预定义的标记字典。字典中的所有标记包含相同数量的块或位(4×4、5×5、6×6 或 7×7),每个字典包含固定数量的标记(50、100、250 或 1000) .下面我们将展示如何在 C++ 和 Python 中生成和检测各种类型的 aruco 标记。

我们需要在代码中使用 aruco 模块。

下面的函数调用 getPredefinedDictionary 显示了如何加载 250 个标记的字典,其中每个标记包含一个 6×6 位二进制模式。
(1)Python

# generate_aruco_markers.py
import cv2 as cv
import numpy as np# 加载预定义字典
dictionary = cv.aruco.Dictionary_get(cv.aruco.DICT_6X6_250)# 生成标记
markerImage = np.zeros((200, 200), dtype=np.uint8)
markerImage = cv.aruco.drawMarker(dictionary, 33, 200, markerImage, 1);cv.imwrite("marker33.png", markerImage);

(2)C++

// generate_aruco_markers.cpp
#include <opencv2/highgui.hpp>
#include <opencv2/aruco.hpp>using namespace cv;int main(int argc, char *argv[]) {Mat markerImage;Ptr<cv::aruco::Dictionary> dictionary = aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);aruco::drawMarker(dictionary, 33, 200, markerImage, 1);imwrite("marker33.png", markerImage);}


上面的 drawMarker 函数让我们从 250 个标记的集合中选择具有给定 id(第二个参数 - 33)的标记,这些标记的 id 从 0 到 249。 drawMarker 函数的第三个参数决定生成的标记的大小。在上面的例子中,它会生成一个 200×200 像素的图像。第四个参数表示将存储生成的标记的对象(上面的markerImage)。最后,第五个参数是厚度参数,它决定了应该添加多少块作为生成的二进制模式的边界。在上面的示例中,将在 6×6 生成的模式周围添加 1 位边界,以在 200×200 像素图像中生成具有 7×7 位的图像。使用上述代码生成的标记如上图所示。

3.检测 Aruco 标记

使用 aruco 标记对场景进行成像后,我们需要检测它们并将其用于进一步处理。下面我们将展示如何检测标记。

// 加载用于生成标记的字典。
Ptr<Dictionary> dictionary = getPredefinedDictionary(DICT_6X6_250);// 使用默认值初始化检测器参数
Ptr<DetectorParameters> parameters = DetectorParameters::create();// 声明包含检测到的标记角和被拒绝的候选标记的向量
vector<vector<Point2f>> markerCorners, rejectedCandidates;// 检测到的标记的 id 存储在一个向量中
vector<int> markerIds;// 检测图像中的标记
detectMarkers(frame, dictionary, markerCorners, markerIds, parameters, rejectedCandidates);
#加载用于生成标记的字典。
dictionary = cv.aruco.Dictionary_get(cv.aruco.DICT_6X6_250)# 使用默认值初始化检测器参数
parameters =  cv.aruco.DetectorParameters_create()# 检测图像中的标记
markerCorners, markerIds, rejectedCandidates = cv.aruco.detectMarkers(frame, dictionary, parameters=parameters)

我们首先加载用于生成标记的相同字典。

使用 DetectorParameters::create() 初始化检测器参数。 OpenCV 允许我们在检测过程中更改多个参数。可以在此处找到可以调整的参数列表,包括自适应阈值。在大多数情况下,默认参数运行良好,OpenCV 建议使用这些参数。所以我们将坚持使用默认参数。

对于每个成功的标记检测,按从左上、右上、右下和左下的顺序检测标记的四个角点。在 C++ 中,这 4 个检测到的角点被存储为一个点向量,图像中的多个标记一起存储在一个点向量向量中。在 Python 中,它们存储为 Numpy 数组。

detectMarkers 函数用于检测和定位标记的角点。第一个参数是带有标记的场景图像。第二个参数是用于生成标记的字典。成功检测到的标记将存储在markerCorners 中,它们的id 存储在markerIds 中。之前初始化的 DetectorParameters 对象也作为参数传递。最后,被拒绝的候选人被存储在rejectedCandidates中。

在场景中打印、剪切和放置标记时,重要的是我们在标记的黑色边界周围保留一些白色边框,以便可以轻松检测到它们。

4.增强现实应用

ArUco 标记主要是为了解决包括增强现实在内的各种应用的相机姿态估计问题而开发的。 OpenCV 在其文档中详细描述了姿态估计过程。

在这篇文章中,我们将它们用于增强现实应用程序,让我们可以将任何新场景叠加到现有图像或视频上。我们在家里挑了一个有大相框的场景,我们想用新的替换相框中的图片,看看它们在墙上看起来如何。然后我们继续尝试在电影中插入视频。为此,我们将大的 aruco 标记打印、剪切并粘贴到图像区域的角落,如下图所示,然后捕获视频。捕获的视频位于帖子顶部的视频左侧。然后我们按顺序单独处理视频的每一帧。

对于每个图像,首先检测标记。下图显示了以绿色绘制的检测到的标记。第一个点用小红圈标记。可以通过顺时针遍历标记的边界来访问第二、第三和第四点。

输入图像和新场景图像中的四个对应点集用于计算单应性。给定场景不同视图中的对应点,单应性是一种将一个对应点映射到另一个对应点的变换。


在我们的例子中,单应矩阵用于将新场景图像扭曲成由我们捕获的图像中的标记定义的四边形。我们在下面的代码中展示了如何做到这一点。
(1)C++

// 从源点和目标点计算单应性
Mat h = cv::findHomography(pts_src, pts_dst);// 扭曲的图像
Mat warpedImage;// 基于单应性将源图像扭曲到目标
warpPerspective(im_src, warpedImage, h, frame.size(), INTER_CUBIC);// 准备一个表示区域的mask,以便从扭曲的图像复制到原始帧中。
Mat mask = Mat::zeros(frame.rows, frame.cols, CV_8UC1);
fillConvexPoly(mask, pts_dst, Scalar(255, 255, 255));// 腐蚀mask以不从变形中复制边界效果
Mat element = getStructuringElement( MORPH_RECT, Size(3,3) );
erode(mask, mask, element);// 将mask扭曲图像复制到mask区域中的原始帧中。
Mat imOut = frame.clone();
warpedImage.copyTo(imOut, mask);

(2)Python

# 计算单应性
h, status = cv.findHomography(pts_src, pts_dst)# 基于单应性将源图像扭曲到目标
warped_image = cv.warpPerspective(im_src, h, (frame.shape[1],frame.shape[0]))# 准备一个表示区域的mask,以便从扭曲的图像复制到原始帧中。
mask = np.zeros([frame.shape[0], frame.shape[1]], dtype=np.uint8);
cv.fillConvexPoly(mask, np.int32([pts_dst_m]), (255, 255, 255), cv.LINE_AA);# 腐蚀mask以不从变形中复制边界效果
element = cv.getStructuringElement(cv.MORPH_RECT, (3,3));
mask = cv.erode(mask, element, iterations=3);# 将mask复制到 3 个通道中。
warped_image = warped_image.astype(float)
mask3 = np.zeros_like(warped_image)
for i in range(0, 3):mask3[:,:,i] = mask/255# 将mask扭曲图像复制到蒙版区域中的原始帧中。
warped_image_masked = cv.multiply(warped_image, mask3)
frame_masked = cv.multiply(frame.astype(float), 1-mask3)
im_out = cv.add(warped_image_masked, frame_masked)

我们使用新的场景图像角点作为源点(pts_src),并将我们捕获的图像中相框内对应的图片角点作为目标点(dst_src)。 OpenCV 函数 findHomography 计算源点和目标点之间的单应函数 h。然后使用单应矩阵来扭曲新图像以适应目标帧。扭曲的图像被屏蔽并复制到目标帧中。在视频的情况下,这个过程在每一帧上重复。

5.完整代码

链接:https://pan.baidu.com/s/10OERAD4NQlg83eTuxj_LkQ
提取码:123a

(1)Python

# augmented_reality_with_aruco.py# Usage example
# python augmented_reality_with_aruco.py --image=test.jpg
# python augmented_reality_with_aruco.py --video=test.mp4import cv2 as cv
#from cv2 import aruco
import argparse
import sys
import os.path
import numpy as npparser = argparse.ArgumentParser(description='Augmented Reality using Aruco markers in OpenCV')
parser.add_argument('--image', help='Path to image file.', default="test.jpg")
parser.add_argument('--video', help='Path to video file.')
args = parser.parse_args()im_src = cv.imread("new_scenery.jpg");outputFile = "ar_out_py.avi"
if (args.image):# 打开图像文件if not os.path.isfile(args.image):print("Input image file ", args.image, " doesn't exist")sys.exit(1)cap = cv.VideoCapture(args.image)outputFile = args.image[:-4]+'_ar_out_py.jpg'
elif (args.video):# 打开视频文件if not os.path.isfile(args.video):print("Input video file ", args.video, " doesn't exist")sys.exit(1)cap = cv.VideoCapture(args.video)outputFile = args.video[:-4]+'_ar_out_py.avi'print("Storing it as :", outputFile)
else:# 网络摄像头输入cap = cv.VideoCapture(0)# 初始化视频编写器以保存输出视频
if (not args.image):vid_writer = cv.VideoWriter(outputFile, cv.VideoWriter_fourcc('M','J','P','G'), 28, (round(2*cap.get(cv.CAP_PROP_FRAME_WIDTH)),round(cap.get(cv.CAP_PROP_FRAME_HEIGHT))))winName = "Augmented Reality using Aruco markers in OpenCV"while cv.waitKey(1) < 0:try:# 从视频中获取帧hasFrame, frame = cap.read()# 如果到达视频结尾,则停止程序if not hasFrame:print("Done processing !!!")print("Output file is stored as ", outputFile)cv.waitKey(3000)break#加载用于生成标记的字典。dictionary = cv.aruco.Dictionary_get(cv.aruco.DICT_6X6_250)# 使用默认值初始化检测器参数parameters =  cv.aruco.DetectorParameters_create()# 检测图像中的标记markerCorners, markerIds, rejectedCandidates = cv.aruco.detectMarkers(frame, dictionary, parameters=parameters)index = np.squeeze(np.where(markerIds==25));refPt1 = np.squeeze(markerCorners[index[0]])[1];index = np.squeeze(np.where(markerIds==33));refPt2 = np.squeeze(markerCorners[index[0]])[2];distance = np.linalg.norm(refPt1-refPt2);scalingFac = 0.02;pts_dst = [[refPt1[0] - round(scalingFac*distance), refPt1[1] - round(scalingFac*distance)]];pts_dst = pts_dst + [[refPt2[0] + round(scalingFac*distance), refPt2[1] - round(scalingFac*distance)]];index = np.squeeze(np.where(markerIds==30));refPt3 = np.squeeze(markerCorners[index[0]])[0];pts_dst = pts_dst + [[refPt3[0] + round(scalingFac*distance), refPt3[1] + round(scalingFac*distance)]];index = np.squeeze(np.where(markerIds==23));refPt4 = np.squeeze(markerCorners[index[0]])[0];pts_dst = pts_dst + [[refPt4[0] - round(scalingFac*distance), refPt4[1] + round(scalingFac*distance)]];pts_src = [[0,0], [im_src.shape[1], 0], [im_src.shape[1], im_src.shape[0]], [0, im_src.shape[0]]];pts_src_m = np.asarray(pts_src)pts_dst_m = np.asarray(pts_dst)# 计算单应性h, status = cv.findHomography(pts_src_m, pts_dst_m)# 基于单应性将源图像扭曲到目标warped_image = cv.warpPerspective(im_src, h, (frame.shape[1],frame.shape[0]))# 准备一个表示区域的mask,以便从扭曲的图像复制到原始帧中。mask = np.zeros([frame.shape[0], frame.shape[1]], dtype=np.uint8);cv.fillConvexPoly(mask, np.int32([pts_dst_m]), (255, 255, 255), cv.LINE_AA);# 腐蚀mask以不从变形中复制边界效果element = cv.getStructuringElement(cv.MORPH_RECT, (3,3));mask = cv.erode(mask, element, iterations=3);# 将mask复制到 3 个通道中。warped_image = warped_image.astype(float)mask3 = np.zeros_like(warped_image)for i in range(0, 3):mask3[:,:,i] = mask/255# 将扭曲的图像复制到mask区域中的原始帧中。warped_image_masked = cv.multiply(warped_image, mask3)frame_masked = cv.multiply(frame.astype(float), 1-mask3)im_out = cv.add(warped_image_masked, frame_masked)# 并排显示原始图像和新的输出图像concatenatedOutput = cv.hconcat([frame.astype(float), im_out]);cv.imshow("AR using Aruco markers", concatenatedOutput.astype(np.uint8))# 用检测框写入帧图像if (args.image):cv.imwrite(outputFile, concatenatedOutput.astype(np.uint8));else:vid_writer.write(concatenatedOutput.astype(np.uint8))except Exception as inst:print(inst)cv.destroyAllWindows()
if 'vid_writer' in locals():vid_writer.release()print('Video writer released..')

(2)C++

// augmented_reality_with_aruco.cpp
// Usage
// ./augmented_reality_with_aruco.out --image=test.jpg
// ./augmented_reality_with_aruco.out --video=test.mp4
#include <fstream>
#include <sstream>
#include <iostream>#include <opencv2/aruco.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/calib3d.hpp>const char* keys =
"{help h usage ? | | Usage examples: \n\t\t./augmented_reality_with_aruco.out --image=test.jpg \n\t\t./augmented_reality_with_aruco.out --video=test.mp4}"
"{image i        |<none>| input image   }"
"{video v       |<none>| input video   }"
;
using namespace cv;
using namespace aruco;
using namespace std;int main(int argc, char** argv)
{CommandLineParser parser(argc, argv, keys);parser.about("Use this script to do Augmented Reality using Aruco markers in OpenCV.");if (parser.has("help")){parser.printMessage();return 0;}// 打开视频文件或图像文件或相机流。string str, outputFile;VideoCapture cap;VideoWriter video;Mat frame, blob;Mat im_src = imread("new_scenery.jpg");try {outputFile = "ar_out_cpp.avi";if (parser.has("image")){// 打开图像文件str = parser.get<String>("image");ifstream ifile(str);if (!ifile) throw("error");cap.open(str);str.replace(str.end()-4, str.end(), "_ar_out_cpp.jpg");outputFile = str;}else if (parser.has("video")){// 打开视频文件str = parser.get<String>("video");ifstream ifile(str);if (!ifile) throw("error");cap.open(str);str.replace(str.end()-4, str.end(), "_ar_out_cpp.avi");outputFile = str;}// 打开摄像头else cap.open(parser.get<int>("device"));}catch(...) {cout << "Could not open the input image/video stream" << endl;return 0;}// 初始化视频编写器以保存输出视频if (!parser.has("image")) {video.open(outputFile, VideoWriter::fourcc('M','J','P','G'), 28, Size(2*cap.get(CAP_PROP_FRAME_WIDTH), cap.get(CAP_PROP_FRAME_HEIGHT)));}// 创建一个窗口static const string kWinName = "Augmented Reality using Aruco markers in OpenCV";namedWindow(kWinName, WINDOW_NORMAL);// 处理帧while (waitKey(1) < 0){// 获得视频帧cap >> frame;try {// 如果到达视频结尾,则停止程序if (frame.empty()) {cout << "Done processing !!!" << endl;cout << "Output file is stored as " << outputFile << endl;waitKey(3000);break;}vector<int> markerIds;// 加载用于生成标记的字典。Ptr<Dictionary> dictionary = getPredefinedDictionary(DICT_6X6_250);// 声明将包含检测到的标记角点和被拒绝的标记候选者的向量vector<vector<Point2f>> markerCorners, rejectedCandidates;// 使用默认值初始化检测器参数Ptr<DetectorParameters> parameters = DetectorParameters::create();// 检测图像中的标记detectMarkers(frame, dictionary, markerCorners, markerIds, parameters, rejectedCandidates);// 使用检测到的标记,在将要显示新场景的目标帧上定位四边形。vector<Point> pts_dst;float scalingFac = 0.02;//0.015;Point refPt1, refPt2, refPt3, refPt4;// 找到目标四边形的左上角点std::vector<int>::iterator it = std::find(markerIds.begin(), markerIds.end(), 25);int index = std::distance(markerIds.begin(), it);refPt1 = markerCorners.at(index).at(1);// 找到目标四边形的右上角点it = std::find(markerIds.begin(), markerIds.end(), 33);index = std::distance(markerIds.begin(), it);refPt2 = markerCorners.at(index).at(2);float distance = norm(refPt1-refPt2);pts_dst.push_back(Point(refPt1.x - round(scalingFac*distance), refPt1.y - round(scalingFac*distance)));pts_dst.push_back(Point(refPt2.x + round(scalingFac*distance), refPt2.y - round(scalingFac*distance)));// 找到目标四边形的右下角点it = std::find(markerIds.begin(), markerIds.end(), 30);index = std::distance(markerIds.begin(), it);refPt3 = markerCorners.at(index).at(0);pts_dst.push_back(Point(refPt3.x + round(scalingFac*distance), refPt3.y + round(scalingFac*distance)));// 找到目标四边形的左下角点it = std::find(markerIds.begin(), markerIds.end(), 23);index = std::distance(markerIds.begin(), it);refPt4 = markerCorners.at(index).at(0);pts_dst.push_back(Point(refPt4.x - round(scalingFac*distance), refPt4.y + round(scalingFac*distance)));// 获取新场景图像的角点。vector<Point> pts_src;pts_src.push_back(Point(0,0));pts_src.push_back(Point(im_src.cols, 0));pts_src.push_back(Point(im_src.cols, im_src.rows));pts_src.push_back(Point(0, im_src.rows));// 从源点和目标点计算单应性Mat h = cv::findHomography(pts_src, pts_dst);// 扭曲的图像Mat warpedImage;// 基于单应性将源图像扭曲到目标warpPerspective(im_src, warpedImage, h, frame.size(), INTER_CUBIC);// 准备一个mask,表示要从扭曲的图像复制到原始帧的区域。Mat mask = Mat::zeros(frame.rows, frame.cols, CV_8UC1);fillConvexPoly(mask, pts_dst, Scalar(255, 255, 255), LINE_AA);// 腐蚀mask以不从变形中复制边界效果Mat element = getStructuringElement( MORPH_RECT, Size(5,5));
//            Mat element = getStructuringElement( MORPH_RECT, Size(3,3));erode(mask, mask, element);// 将扭曲的图像复制到mask区域中的原始帧中。Mat imOut = frame.clone();warpedImage.copyTo(imOut, mask);// 并排显示原始图像和新的输出图像Mat concatenatedOutput;hconcat(frame, imOut, concatenatedOutput);if (parser.has("image")) imwrite(outputFile, concatenatedOutput);else video.write(concatenatedOutput);imshow(kWinName, concatenatedOutput);}catch(const std::exception& e) {cout << endl << " e : " << e.what() << endl;cout << "Could not do homography !! " << endl;//        return 0;}}cap.release();if (!parser.has("image")) video.release();return 0;
}




参考目录

https://learnopencv.com/augmented-reality-using-aruco-markers-in-opencv-c-python/

OpenCV进阶(10)在 OpenCV 中使用 ArUco 标记的增强现实相关推荐

  1. [OpenCV实战]39 在OpenCV中使用ArUco标记的增强现实

    文章目录 1 什么是ArUco标记? 2 在OpenCV中生成ArUco标记 3 检测Aruco标记 4 增强现实应用 5 总结和代码 5.1 生成aruco标记 5.2 使用aruco增强现实 6 ...

  2. 增强现实入门实战,使用ArUco标记实现增强现实

    点击上方"AI算法修炼营",选择加星标或"置顶" 标题以下,全是干货 在本文中,我们将介绍ArUco标记以及如何使用OpenCV将其用于简单的增强现实任务,具体 ...

  3. 使用Python,OpenCV生成Aruco标记

    这篇博客将介绍Aruco标记是什么,以及如何使用Python,OpenCV生成Aruco标记.本文提供俩种生成方式:在线生成和代码生成: 使用OpenCV处理ArUco标签非常简单,因为OpenCV库 ...

  4. OpenCV 进阶应用,用编程手段搞定图像处理

    在我们生活中,常见的图像处理软件有 Adobe Photoshop.Adobe Illustrator 等.然而,并非只有软件才能实现图像处理,通过编程手段也是能实现的!通过编程手段实现图像处理,也就 ...

  5. opencv进阶学习笔记10:图像金字塔和图像梯度

    基础版笔记传送门: python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) 图像金字塔 变小 变大 原理 ...

  6. OpenCV进阶篇视频

    OpenCV进阶篇01 第14章 视频处理 OpenCV不仅能够处理图像,还能够处理视频.视频是由大量的图像构成的,这些图像以固定的时间间隔从视频中获取.这样,就能够使用图像处理的方法对这些图像进行处 ...

  7. opencv进阶学习笔记14:分水岭算法 实现图像分割

    基础版学习笔记目录: python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) 分水岭算法原理 分水岭算法 ...

  8. opencv进阶学习笔记13:图像形态学操作大全(膨胀,腐蚀,开闭,黑帽,顶帽,梯度)python版

    基础版学习笔记: python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) 基础版形态学: opencv学 ...

  9. opencv进阶学习笔记11:cannny边缘检测,直线检测,圆检测

    基础版笔记传送门 python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) cannny边缘检测 基础版边 ...

最新文章

  1. 一个妹子图应用客户端源码
  2. 转载,大佬关于虚拟内存与物理内存关系讲解。
  3. kalinux实现自适用全屏、与物理主机共享文件方法
  4. SmartCode 使用常见问题
  5. 腾讯云认证考试常见问题答疑
  6. Confluence 6 用户目录图例 - 使用 LDAP 授权,在用户第一次登陆时拷贝用户
  7. 关于联想oem系统激活方法尝试
  8. 平板Android调试打开log,Android 调试日志
  9. GML可视化学习(三)
  10. AD9的PCB技巧——环形焊盘的封装
  11. goland 注释模板_【精品】金工实习心得体会模板合集5篇
  12. xy轴坐标图数字表示_图纸上的坐标x,y轴上有好长的数字表示什么意思
  13. express跨域配置
  14. java考试真题6_JAVA认证考试历年真题:SCJP认证试题解析6
  15. 产品经理1.1_如何高效的开展产品需求评审会
  16. Dotween简单安装设置
  17. 《赵成的运维体系管理课》学习笔记(5)——故障管理
  18. 【毕业季·进击的技术er】一个读研学生的唠唠嗑
  19. html游戏转wxw,wf.html · wxw婉/flowChart - Gitee.com
  20. DapperPoco -- 基于Dapper的、轻量级的、高性能的、简单的、灵活的ORM框架

热门文章

  1. “守护花季,护航青春”青少年防性侵教育活动在兴隆社区顺利开展
  2. 我的世界java启动程下载_我的世界java启动器下载
  3. VB程序设计教程(第四版)龚沛曾-实验8-2
  4. Imply之HTTP推送
  5. PSP编程环境破解个人研究笔记——CYGWIN环境搭建
  6. 【苹果家庭群发推送】软件安装Apple推送是一种基于IMESSAGE平台的新信息推送功效的营销软件
  7. EventManager
  8. 理解WebKit和Chromium: WebKit和Chromium组成
  9. 寄充气娃娃怎么寄_我如何在5小时内寄出新设计作品集
  10. 解决问题帖--weston desktop pannel(桌面标题栏)去除方法