资讯专栏INFORMATION COLUMN

树莓派视觉小车 -- 小球追踪(颜色追踪)(OpenCV色彩空间HSV)

developerworks / 1576人阅读

摘要:一般调高饱和度会降低中相对较低的数值,凸显主要颜色的纯度。对当前唯一的方法霍夫梯度法,它表示在检测阶段圆心的累加器阈值。第八个参数,类型的有默认值,表示圆半径的最小值。

目录

效果展示 

基础理论(HSV)

为什么用HSV空间而不是RGB空间?

HSV

1、Hue(色相)

2、Value(明度)

3、Saturation(饱和度)

一、初始化

滑动条初始化

1、创建回调函数

2、窗口设置(名称) 

3、滑动条设置

代码

二、运动函数

三、舵机控制

四、在HSV空间下获取二值图

1、获取滑动条的值

2、转HSV(三通道:H、S、V)

3、转二值图(单通道阈值处理)

4、结合HSV三个通道,得到最终二值图

代码

五、图像处理(总)

1、打开摄像头

2、获取HSV色彩空间得到的二值图

3、高斯滤波

4、开运算去噪

5、闭运算

6、霍夫圆检测

原理

API

 6-1、霍夫圆检测

6-2、获取圆心和半径坐标

6-3、画圆

代码

六、运动控制

总代码


效果展示 

 

 

 

基础理论(HSV)

为什么用HSV空间而不是RGB空间?

因为RGB通道并不能很好地反映出物体具体的颜色信息

HSV空间能够非常直观的表达色彩的明暗、色调、以及鲜艳程度,方便进行颜色之间的对比。

(RGB受光线影响很大,所以采取HSV

这里用HSV的目的:得到合适的二值图

HSV

Hue(H):色调、色相(具体的颜色)

Saturation(S):饱和度、色彩纯净度

Value(V):明度

Hue范围是[0,179],饱和范围是[0,255],值范围是[0,255]

(写代码的时候,犯了蠢错误:把3个都设成了179,发现死活调不对)

1、Hue(色相)

Hue:色相(具体的颜色)


2、Value(明度)

明度:色彩的明亮程度,单通道亮度(并不等同于整体发光量)。

(明度越高越白,越低越黑,一般提高明度会同时提高R、G、B三通道的数值)

3、Saturation(饱和度)

Saturation:饱和度、色彩纯度(越低越灰,越高越纯)  。

(一般调高饱和度会降低RGB中相对较低的数值,凸显主要颜色的纯度。 )

B站视频讲解:

短动画慢语速1分钟讲清影视调色中色彩形成原理基础——RGB与HSV

一、初始化

滑动条初始化

这次的初始化,除了电机、舵机、窗口等等的初始化,还有滑动条的初始设置。 

在创建滑动条之前:

1、需要设置窗口名称,不然窗口都没有,自然也就无法设置滑动条了。

2、创建回调函数。

1、创建回调函数

def nothing(*arg):    pass

2、窗口设置(名称) 

def Trackbar_Init():    # 1 create windows    cv2.namedWindow("h_binary")    cv2.namedWindow("s_binary")    cv2.namedWindow("v_binary")

3、滑动条设置

# 2 Create Trackbar    cv2.createTrackbar("hmin", "h_binary", 6, 179, nothing)      cv2.createTrackbar("hmax", "h_binary", 26, 179, nothing)      cv2.createTrackbar("smin", "s_binary", 110, 255, nothing)    cv2.createTrackbar("smax", "s_binary", 255, 255, nothing)    cv2.createTrackbar("vmin", "v_binary", 140, 255, nothing)    cv2.createTrackbar("vmax", "v_binary", 255, 255, nothing)    #   创建滑动条     滑动条值名称 窗口名称   滑动条值 滑动条阈值 回调函数

代码

def Motor_Init():    global L_Motor, R_Motor    L_Motor= GPIO.PWM(l_motor,100)    R_Motor = GPIO.PWM(r_motor,100)    L_Motor.start(0)    R_Motor.start(0)def Direction_Init():    GPIO.setup(left_back,GPIO.OUT)    GPIO.setup(left_front,GPIO.OUT)    GPIO.setup(l_motor,GPIO.OUT)        GPIO.setup(right_front,GPIO.OUT)    GPIO.setup(right_back,GPIO.OUT)    GPIO.setup(r_motor,GPIO.OUT)def Servo_Init():    global pwm_servo    pwm_servo=Adafruit_PCA9685.PCA9685()def Trackbar_Init():    # 1 create windows    cv2.namedWindow("h_binary")    cv2.namedWindow("s_binary")    cv2.namedWindow("v_binary")    # 2 Create Trackbar    cv2.createTrackbar("hmin", "h_binary", 6, 179, nothing)      cv2.createTrackbar("hmax", "h_binary", 26, 179, nothing)      cv2.createTrackbar("smin", "s_binary", 110, 255, nothing)    cv2.createTrackbar("smax", "s_binary", 255, 255, nothing)    cv2.createTrackbar("vmin", "v_binary", 140, 255, nothing)    cv2.createTrackbar("vmax", "v_binary", 255, 255, nothing)    #   创建滑动条     滑动条值名称 窗口名称   滑动条值 滑动条阈值 回调函数def Init():    GPIO.setwarnings(False)     GPIO.setmode(GPIO.BCM)    Direction_Init()    Servo_Init()    Motor_Init()    Trackbar_Init()# 回调函数def nothing(*arg):    pass

二、运动函数

常规操作:向前、向后、向左、向右、停止。 

def Front(speed):    L_Motor.ChangeDutyCycle(speed)    GPIO.output(left_front,1)   #left_front    GPIO.output(left_back,0)    #left_back    R_Motor.ChangeDutyCycle(speed)    GPIO.output(right_front,1)  #right_front    GPIO.output(right_back,0)   #right_back         def Back(speed):    L_Motor.ChangeDutyCycle(speed)    GPIO.output(left_front,0)   #left_front    GPIO.output(left_back,1)    #left_back    R_Motor.ChangeDutyCycle(speed)    GPIO.output(right_front,0)  #right_front    GPIO.output(right_back,1)   #right_backdef Left(speed):    L_Motor.ChangeDutyCycle(speed)    GPIO.output(left_front,0)   #left_front    GPIO.output(left_back,1)    #left_back    R_Motor.ChangeDutyCycle(speed)    GPIO.output(right_front,1)  #right_front    GPIO.output(right_back,0)   #right_backdef Right(speed):    L_Motor.ChangeDutyCycle(speed)    GPIO.output(left_front,1)   #left_front    GPIO.output(left_back,0)    #left_back    R_Motor.ChangeDutyCycle(speed)    GPIO.output(right_front,0)  #right_front    GPIO.output(right_back,1)   #right_backdef Stop():    L_Motor.ChangeDutyCycle(0)    GPIO.output(left_front,0)   #left_front    GPIO.output(left_back,0)    #left_back    R_Motor.ChangeDutyCycle(0)    GPIO.output(right_front,0)  #right_front    GPIO.output(right_back,0)   #right_back    

三、舵机控制

def set_servo_angle(channel,angle):    angle=4096*((angle*11)+500)/20000    pwm_servo.set_pwm_freq(50)                #frequency==50Hz (servo)    pwm_servo.set_pwm(channel,0,int(angle))
set_servo_angle(4, 110)     #top servo     lengthwise    #0:back    180:front        set_servo_angle(5, 90)     #bottom servo  crosswise    #0:left    180:right  

四、在HSV空间下获取二值图

1、获取滑动条的值

# 1 get trackbar"s value    hmin = cv2.getTrackbarPos("hmin", "h_binary")    hmax = cv2.getTrackbarPos("hmax", "h_binary")    smin = cv2.getTrackbarPos("smin", "s_binary")    smax = cv2.getTrackbarPos("smax", "s_binary")    vmin = cv2.getTrackbarPos("vmin", "v_binary")    vmax = cv2.getTrackbarPos("vmax", "v_binary")

2、转HSV(三通道:H、S、V)

# 2 to HSV    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)    cv2.imshow("hsv", hsv)    h, s, v = cv2.split(hsv)

 

3、转二值图(单通道阈值处理)

分别对H、S、V三个道阈值处理:

# 3 set threshold (binary image)    # if value in (min, max):white; otherwise:black    h_binary = cv2.inRange(np.array(h), np.array(hmin), np.array(hmax))    s_binary = cv2.inRange(np.array(s), np.array(smin), np.array(smax))    v_binary = cv2.inRange(np.array(v), np.array(vmin), np.array(vmax))# 5 Show    cv2.imshow("h_binary", h_binary)    cv2.imshow("s_binary", s_binary)    cv2.imshow("v_binary", v_binary)

4、结合HSV三个通道,得到最终二值图

对H、S、V三个通道与操作。(H&&S&&V)

# 4 get binary    binary = cv2.bitwise_and(h_binary, cv2.bitwise_and(s_binary, v_binary))

代码

# 在HSV色彩空间下得到二值图def Get_HSV(image):    # 1 get trackbar"s value    hmin = cv2.getTrackbarPos("hmin", "h_binary")    hmax = cv2.getTrackbarPos("hmax", "h_binary")    smin = cv2.getTrackbarPos("smin", "s_binary")    smax = cv2.getTrackbarPos("smax", "s_binary")    vmin = cv2.getTrackbarPos("vmin", "v_binary")    vmax = cv2.getTrackbarPos("vmax", "v_binary")        # 2 to HSV    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)    cv2.imshow("hsv", hsv)    h, s, v = cv2.split(hsv)        # 3 set threshold (binary image)    # if value in (min, max):white; otherwise:black    h_binary = cv2.inRange(np.array(h), np.array(hmin), np.array(hmax))    s_binary = cv2.inRange(np.array(s), np.array(smin), np.array(smax))    v_binary = cv2.inRange(np.array(v), np.array(vmin), np.array(vmax))        # 4 get binary(对H、S、V三个通道分别与操作)    binary = cv2.bitwise_and(h_binary, cv2.bitwise_and(s_binary, v_binary))        # 5 Show    cv2.imshow("h_binary", h_binary)    cv2.imshow("s_binary", s_binary)    cv2.imshow("v_binary", v_binary)    cv2.imshow("binary", binary)        return binary

五、图像处理(总)

1、打开摄像头

# 1 Capture the frames    ret, frame = camera.read()    image = frame    cv2.imshow("frame", frame)

2、获取HSV色彩空间得到的二值图

(Get_HSV(frame)其实就是步骤四的HSV处理)

# 2 get HSV    binary = Get_HSV(frame)

3、高斯滤波

# 3 Gausi blur    blur = cv2.GaussianBlur(binary,(9,9),0)

4、开运算去噪

# 4 Open    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9,9))    Open = cv2.morphologyEx(blur, cv2.MORPH_OPEN, kernel)    cv2.imshow("Open",Open)

5、闭运算

# 5 Close    Close = cv2.morphologyEx(Open, cv2.MORPH_CLOSE, kernel)    cv2.imshow("Close",Close)

 

6、霍夫圆检测

原理

霍夫变换圆检测是基于图像梯度实现:

圆心检测的原理︰圆心是圆周法线的交汇处设置一个阈值在某点的相交的直线的条数大于这个阈值就认为该交汇点为圆心
圆半径确定原理:圆心到圆周上的距离〔半径)是相同的设置一个阈值只要相同距离的数量大于该阈值就认为该距离是该圆心的半径

API

def HoughCircles(image: Any,                 method: Any,                 dp: Any,                 minDist: Any,                 circles: Any = None,                 param1: Any = None,                 param2: Any = None,                 minRadius: Any = None,                 maxRadius: Any = None) -> None

参数: 

  • 第二个参数,int类型的method,即使用的检测方法,目前OpenCV中就霍夫梯度法一种可以使用,它的标识符为CV_HOUGH_GRADIENT,在此参数处填这个标识符即可。
  • 第三个参数,double类型的dp,用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。上述文字不好理解的话,来看例子吧。例如,如果dp= 1时,累加器和输入图像具有相同的分辨率。如果dp=2,累加器便有输入图像一半那么大的宽度和高度。
  • 第四个参数,double类型的minDist,为霍夫变换检测到的圆的圆心之间的最小距离,即让我们的算法能明显区分的两个不同圆之间的最小距离。这个参数如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之,这个参数设置太大的话,某些圆就不能被检测出来了。
  • 第五个参数,InputArray类型的circles,经过调用HoughCircles函数后此参数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量(x, y, radius)表示。
  • 第六个参数,double类型的param1,有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。
  • 第七个参数,double类型的param2,也有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小的话,就可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了
  • 第八个参数,int类型的minRadius,有默认值0,表示圆半径的最小值
  • 第九个参数,int类型的maxRadius,也有默认值0,表示圆半径的最大值

 6-1、霍夫圆检测

# 6 Hough Circle detect    circles = cv2.HoughCircles(Close,cv2.HOUGH_GRADIENT,2,120,param1=120,param2=20,minRadius=20,maxRadius=0)    #                                                                     param2:决定圆能否被检测到(越少越容易检测到圆,但相应的也更容易出错)

6-2、获取圆心和半径坐标

# 1 获取圆的圆心和半径        x, y, r = int(circles[0][0][0]),int(circles[0][0][1]),int(circles[0][0][2])        print(x, y, r)

6-3、画圆

# 2 画圆        cv2.circle(image, (x, y), r, (255,0,255),5)        cv2.imshow("image", image)

代码

# 图像处理def Image_Processing():    global h, s, v    # 1 Capture the frames    ret, frame = camera.read()    image = frame    cv2.imshow("frame", frame)        # 2 get HSV    binary = Get_HSV(frame)        # 3 Gausi blur    blur = cv2.GaussianBlur(binary,(9,9),0)        # 4 Open    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9,9))    Open = cv2.morphologyEx(blur, cv2.MORPH_OPEN, kernel)    cv2.imshow("Open",Open)    # 5 Close    Close = cv2.morphologyEx(Open, cv2.MORPH_CLOSE, kernel)    cv2.imshow("Close",Close)    # 6 Hough Circle detect    circles = cv2.HoughCircles(Close,cv2.HOUGH_GRADIENT,2,120,param1=120,param2=20,minRadius=20,maxRadius=0)    #                                                                     param2:决定圆能否被检测到(越少越容易检测到圆,但相应的也更容易出错)    # judge if circles is exist    if circles is not None:        # 1 获取圆的圆心和半径        x, y, r = int(circles[0][0][0]),int(circles[0][0][1]),int(circles[0][0][2])        print(x, y, r)        # 2 画圆        cv2.circle(image, (x, y), r, (255,0,255),5)        cv2.imshow("image", image)    else:        (x,y),r = (0,0), 0            return (x,y), r

六、运动控制

根据检测到的圆,获取到的坐标、半径,进行运动控制。

这里可以做到跟踪小球,前进和后退相配合,“敌进我退,敌退我进”。

# 运动控制(这里可以做到跟踪小球,前景和后退相配合,“敌进我退,敌退我进”)def Move((x,y), r):    low_xlimit = width/4    high_xlimit = 0.75 * width    #low_ylimit = 3/4 * height    ylimit = 0.75 * height    print(high_xlimit, ylimit)    # 没检测到,停止不动    if x==0:        Stop()    # 检测到在图片0.75以上的区域(距离正常)    elif x>low_xlimit and xlow_xlimit and x=ylimit:        Back(60)    # 在左0.25区域,向左跟踪    elif xhigh_xlimit:        Right(60)

总代码

#Ball Tracking(HSV)import  RPi.GPIO as GPIOimport timeimport Adafruit_PCA9685import numpy as npimport cv2#set capture windowwidth, height = 320, 240camera = cv2.VideoCapture(0)camera.set(3,width) camera.set(4,height) l_motor = 18left_front   =  22left_back   =  27r_motor = 23right_front   = 25right_back  =  24def Motor_Init():    global L_Motor, R_Motor    L_Motor= GPIO.PWM(l_motor,100)    R_Motor = GPIO.PWM(r_motor,100)    L_Motor.start(0)    R_Motor.start(0)def Direction_Init():    GPIO.setup(left_back,GPIO.OUT)    GPIO.setup(left_front,GPIO.OUT)    GPIO.setup(l_motor,GPIO.OUT)        GPIO.setup(right_front,GPIO.OUT)    GPIO.setup(right_back,GPIO.OUT)    GPIO.setup(r_motor,GPIO.OUT)def Servo_Init():    global pwm_servo    pwm_servo=Adafruit_PCA9685.PCA9685()def Trackbar_Init():    # 1 create windows    cv2.namedWindow("h_binary")    cv2.namedWindow("s_binary")    cv2.namedWindow("v_binary")    # 2 Create Trackbar    cv2.createTrackbar("hmin", "h_binary", 6, 179, nothing)      cv2.createTrackbar("hmax", "h_binary", 26, 179, nothing)      cv2.createTrackbar("smin", "s_binary", 110, 255, nothing)    cv2.createTrackbar("smax", "s_binary", 255, 255, nothing)    cv2.createTrackbar("vmin", "v_binary", 140, 255, nothing)    cv2.createTrackbar("vmax", "v_binary", 255, 255, nothing)    #   创建滑动条     滑动条值名称 窗口名称   滑动条值 滑动条阈值 回调函数def Init():    GPIO.setwarnings(False)     GPIO.setmode(GPIO.BCM)    Direction_Init()    Servo_Init()    Motor_Init()    Trackbar_Init()def Front(speed):    L_Motor.ChangeDutyCycle(speed)    GPIO.output(left_front,1)   #left_front    GPIO.output(left_back,0)    #left_back    R_Motor.ChangeDutyCycle(speed)    GPIO.output(right_front,1)  #right_front    GPIO.output(right_back,0)   #right_back         def Back(speed):    L_Motor.ChangeDutyCycle(speed)    GPIO.output(left_front,0)   #left_front    GPIO.output(left_back,1)    #left_back    R_Motor.ChangeDutyCycle(speed)    GPIO.output(right_front,0)  #right_front    GPIO.output(right_back,1)   #right_backdef Left(speed):    L_Motor.ChangeDutyCycle(speed)    GPIO.output(left_front,0)   #left_front    GPIO.output(left_back,1)    #left_back    R_Motor.ChangeDutyCycle(speed)    GPIO.output(right_front,1)  #right_front    GPIO.output(right_back,0)   #right_backdef Right(speed):    L_Motor.ChangeDutyCycle(speed)    GPIO.output(left_front,1)   #left_front    GPIO.output(left_back,0)    #left_back    R_Motor.ChangeDutyCycle(speed)    GPIO.output(right_front,0)  #right_front    GPIO.output(right_back,1)   #right_backdef Stop():    L_Motor.ChangeDutyCycle(0)    GPIO.output(left_front,0)   #left_front    GPIO.output(left_back,0)    #left_back    R_Motor.ChangeDutyCycle(0)    GPIO.output(right_front,0)  #right_front    GPIO.output(right_back,0)   #right_backdef set_servo_angle(channel,angle):    angle=4096*((angle*11)+500)/20000    pwm_servo.set_pwm_freq(50)                #frequency==50Hz (servo)    pwm_servo.set_pwm(channel,0,int(angle))# 回调函数def nothing(*arg):    pass# 在HSV色彩空间下得到二值图def Get_HSV(image):    # 1 get trackbar"s value    hmin = cv2.getTrackbarPos("hmin", "h_binary")    hmax = cv2.getTrackbarPos("hmax", "h_binary")    smin = cv2.getTrackbarPos("smin", "s_binary")    smax = cv2.getTrackbarPos("smax", "s_binary")    vmin = cv2.getTrackbarPos("vmin", "v_binary")    vmax = cv2.getTrackbarPos("vmax", "v_binary")        # 2 to HSV    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)    cv2.imshow("hsv", hsv)    h, s, v = cv2.split(hsv)        # 3 set threshold (binary image)    # if value in (min, max):white; otherwise:black    h_binary = cv2.inRange(np.array(h), np.array(hmin), np.array(hmax))    s_binary = cv2.inRange(np.array(s), np.array(smin), np.array(smax))    v_binary = cv2.inRange(np.array(v), np.array(vmin), np.array(vmax))        # 4 get binary(对H、S、V三个通道分别与操作)    binary = cv2.bitwise_and(h_binary, cv2.bitwise_and(s_binary, v_binary))        # 5 Show    cv2.imshow("h_binary", h_binary)    cv2.imshow("s_binary", s_binary)    cv2.imshow("v_binary", v_binary)    cv2.imshow("binary", binary)        return binary# 图像处理def Image_Processing():    global h, s, v    # 1 Capture the frames    ret, frame = camera.read()    image = frame    cv2.imshow("frame", frame)        # 2 get HSV    binary = Get_HSV(frame)        # 3 Gausi blur    blur = cv2.GaussianBlur(binary,(9,9),0)        # 4 Open    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9,9))    Open = cv2.morphologyEx(blur, cv2.MORPH_OPEN, kernel)    cv2.imshow("Open",Open)    # 5 Close    Close = cv2.morphologyEx(Open, cv2.MORPH_CLOSE, kernel)    cv2.imshow("Close",Close)    # 6 Hough Circle detect    circles = cv2.HoughCircles(Close,cv2.HOUGH_GRADIENT,2,120,param1=120,param2=20,minRadius=20,maxRadius=0)    #                                                                     param2:决定圆能否被检测到(越少越容易检测到圆,但相应的也更容易出错)    # judge if circles is exist    if circles is not None:        # 1 获取圆的圆心和半径        x, y, r = int(circles[0][0][0]),int(circles[0][0][1]),int(circles[0][0][2])        print(x, y, r)        # 2 画圆        cv2.circle(image, (x, y), r, (255,0,255),5)        cv2.imshow("image", image)    else:        (x,y),r = (0,0), 0            return (x,y), r# 运动控制(这里可以做到跟踪小球,前景和后退相配合,“敌进我退,敌退我进”)def Move((x,y), r):    low_xlimit = width/4    high_xlimit = 0.75 * width    #low_ylimit = 3/4 * height    ylimit = 0.75 * height    print(high_xlimit, ylimit)    # 没检测到,停止不动    if x==0:        Stop()    # 检测到在图片0.75以上的区域(距离正常)    elif x>low_xlimit and xlow_xlimit and x=ylimit:        Back(60)    # 在左0.25区域,向左跟踪    elif xhigh_xlimit:        Right(60)    if __name__ == "__main__":    Init()        set_servo_angle(4, 110)     #top servo     lengthwise    #0:back    180:front        set_servo_angle(5, 90)     #bottom servo  crosswise    #0:left    180:right          while 1:        # 1 Image Process        (x,y), r = Image_Processing()                # 2 Move        Move((x,y), r)                # must include this codes(otherwise you can"t open camera successfully)        if cv2.waitKey(1) & 0xFF == ord("q"):            Stop()            GPIO.cleanup()                break

这里的HSV是根据我自己当前的情况调节的,更改场景以后可能需要重新调节H、S、V三通道的阈值(max && min)

基础的视觉检测+运动,没有太多的运动控制算法(PID等等都没有涉及到)。有好的想法和建议欢迎交流讨论,Thanks♪(・ω・)ノ

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/119986.html

相关文章

  • 分享 | 撞坏遥控车后,有个技术大牛爸爸是种怎样的体验

    摘要:在我已经制作完成一辆可以运行的遥控车时,公司发布了一个自驾车项目,来展示自动驾驶汽车的工作原理。需要注意的是,这里用的都是语言而非,其主要原因有两个一方面,近来似乎已成为运用机器学习技术时实际使用的语言。 最近,Mapbox 的 Android 工程师 Antonio 使用计算机视觉和机器学习技术,为他的女儿 Violeta 重新制作了一台遥控车。接下来我们看看 Antonio 是如何...

    ConardLi 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<