IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 人工智能 -> OpenCV进阶(10)在 OpenCV 中使用 ArUco 标记的增强现实 -> 正文阅读

[人工智能]OpenCV进阶(10)在 OpenCV 中使用 ArUco 标记的增强现实

在这篇文章中,我们将解释什么是 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.mp4

import cv2 as cv
#from cv2 import aruco
import argparse
import sys
import os.path
import numpy as np

parser = 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/

  人工智能 最新文章
2022吴恩达机器学习课程——第二课(神经网
第十五章 规则学习
FixMatch: Simplifying Semi-Supervised Le
数据挖掘Java——Kmeans算法的实现
大脑皮层的分割方法
【翻译】GPT-3是如何工作的
论文笔记:TEACHTEXT: CrossModal Generaliz
python从零学(六)
详解Python 3.x 导入(import)
【答读者问27】backtrader不支持最新版本的
上一篇文章      下一篇文章      查看所有文章
加:2021-10-13 11:27:11  更:2021-10-13 11:27:21 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 10:17:41-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码