• PPLiteSeg实时语义分割预测结果输出控制无人车转向角度方向实现沿车道无人驾驶


    一、前言

    承接上文已经完成对数据集的训练和对框架的修改实现实时输出mask的预测结果,但是仅仅做到这些是无法实现无人驾驶的。

    PPLiteSeg训练自己的数据集实现自动驾驶并爆改制作成API可供其他Python程序调用实时语义分割(超低延时)_Leonard2021的博客-CSDN博客

    百度飞浆EISeg高效交互式标注分割软件的使用教程_Leonard2021的博客-CSDN博客

    整体思路:实时语义分割的输出预测mask,将整体预测分割中的车道线分割结果mask独立读取出来,再通过opencv进行图像二值化Canny边缘检测后得到车道线的边缘后,再通过HoughLinesP霍尔线变化取得车道线上的大量零散坐标,在这些零散坐标中求得起点和终点的车道线中点后,设置逻辑判断车道线是通向前方、左方、还是右方,求得输出车道区域的中点和起点车道区域的中点之间的角度,设置逻辑将改输出角度同步到无人车的前方转向轮的方向角度控制,就可使小车沿车道无人车自动驾驶。

    数据集大概长这样:

     

    二、实战

    训练函数部分,我在上一篇文章里已经提及,这里不再重复,直接开始修改和使用调用的部分。

    新建  visualize_myself.py  :

    1. # Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
    2. #
    3. # Licensed under the Apache License, Version 2.0 (the "License");
    4. # you may not use this file except in compliance with the License.
    5. # You may obtain a copy of the License at
    6. #
    7. # http://www.apache.org/licenses/LICENSE-2.0
    8. #
    9. # Unless required by applicable law or agreed to in writing, software
    10. # distributed under the License is distributed on an "AS IS" BASIS,
    11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12. # See the License for the specific language governing permissions and
    13. # limitations under the License.
    14. import os
    15. import cv2
    16. import numpy as np
    17. from PIL import Image as PILImage
    18. def visualize(image, result, color_map, save_dir=None, weight=0.6):
    19. """
    20. Convert predict result to color image, and save added image.
    21. Args:
    22. image (str): The path of origin image.
    23. result (np.ndarray): The predict result of image.
    24. color_map (list): The color used to save the prediction results.
    25. save_dir (str): The directory for saving visual image. Default: None.
    26. weight (float): The image weight of visual image, and the result weight is (1 - weight). Default: 0.6
    27. Returns:
    28. vis_result (np.ndarray): If `save_dir` is None, return the visualized result.
    29. """
    30. color_map = [color_map[i:i + 3] for i in range(0, len(color_map), 3)]
    31. color_map = np.array(color_map).astype("uint8")
    32. # Use OpenCV LUT for color mapping
    33. c1 = cv2.LUT(result, color_map[:, 0])
    34. c2 = cv2.LUT(result, color_map[:, 1])
    35. c3 = cv2.LUT(result, color_map[:, 2])
    36. pseudo_img = np.dstack((c3, c2, c1))
    37. #im = cv2.imread(image)
    38. im = image.copy()
    39. vis_result = cv2.addWeighted(im, weight, pseudo_img, 1 - weight, 0)
    40. if save_dir is not None:
    41. if not os.path.exists(save_dir):
    42. os.makedirs(save_dir)
    43. image_name = os.path.split(image)[-1]
    44. out_path = os.path.join(save_dir, image_name)
    45. cv2.imwrite(out_path, vis_result)
    46. else:
    47. return vis_result
    48. def get_pseudo_color_map(pred, color_map=None):
    49. """
    50. Get the pseudo color image.
    51. Args:
    52. pred (numpy.ndarray): the origin predicted image.
    53. color_map (list, optional): the palette color map. Default: None,
    54. use paddleseg's default color map.
    55. Returns:
    56. (numpy.ndarray): the pseduo image.
    57. """
    58. pred_mask = PILImage.fromarray(pred.astype(np.uint8), mode='P')
    59. if color_map is None:
    60. color_map = get_color_map_list(256)
    61. pred_mask.putpalette(color_map)
    62. #print(len(pred_mask.split()), type(pred_mask))
    63. #image = pred_mask.convert("RGB")#单通道转化为3通道
    64. #img = cv2.cvtColor(np.asarray(image), cv2.COLOR_BGR2GRAY)#转化为cv的格式返回
    65. return pred_mask
    66. def get_color_map_list(num_classes, custom_color=None):
    67. """
    68. Returns the color map for visualizing the segmentation mask,
    69. which can support arbitrary number of classes.
    70. Args:
    71. num_classes (int): Number of classes.
    72. custom_color (list, optional): Save images with a custom color map. Default: None, use paddleseg's default color map.
    73. Returns:
    74. (list). The color map.
    75. """
    76. num_classes += 1
    77. color_map = num_classes * [0, 0, 0]
    78. for i in range(0, num_classes):
    79. j = 0
    80. lab = i
    81. while lab:
    82. color_map[i * 3] |= (((lab >> 0) & 1) << (7 - j))
    83. color_map[i * 3 + 1] |= (((lab >> 1) & 1) << (7 - j))
    84. color_map[i * 3 + 2] |= (((lab >> 2) & 1) << (7 - j))
    85. j += 1
    86. lab >>= 3
    87. color_map = color_map[3:]
    88. if custom_color:
    89. color_map[:len(custom_color)] = custom_color
    90. return color_map
    91. def paste_images(image_list):
    92. """
    93. Paste all image to a image.
    94. Args:
    95. image_list (List or Tuple): The images to be pasted and their size are the same.
    96. Returns:
    97. result_img (PIL.Image): The pasted image.
    98. """
    99. assert isinstance(image_list,
    100. (list, tuple)), "image_list should be a list or tuple"
    101. assert len(
    102. image_list) > 1, "The length of image_list should be greater than 1"
    103. pil_img_list = []
    104. for img in image_list:
    105. if isinstance(img, str):
    106. assert os.path.exists(img), "The image is not existed: {}".format(
    107. img)
    108. img = PILImage.open(img)
    109. img = np.array(img)
    110. elif isinstance(img, np.ndarray):
    111. img = PILImage.fromarray(img)
    112. pil_img_list.append(img)
    113. sample_img = pil_img_list[0]
    114. size = sample_img.size
    115. for img in pil_img_list:
    116. assert size == img.size, "The image size in image_list should be the same"
    117. width, height = sample_img.size
    118. result_img = PILImage.new(sample_img.mode,
    119. (width * len(pil_img_list), height))
    120. for i, img in enumerate(pil_img_list):
    121. result_img.paste(img, box=(width * i, 0))
    122. return result_img

    新建  predict_with_api.py  : 修改过的predict,制作成了可实时运行的API接口

    1. import cv2
    2. import numpy as np
    3. import paddle
    4. from paddleseg.core import infer
    5. from paddleseg.utils import visualize
    6. import visualize_myself
    7. from PIL import Image as PILImage
    8. def preprocess(im_path, transforms):
    9. data = {}
    10. data['img'] = im_path
    11. data = transforms(data)
    12. data['img'] = data['img'][np.newaxis, ...]
    13. data['img'] = paddle.to_tensor(data['img'])
    14. return data
    15. def predict(model,
    16. model_path,
    17. transforms,
    18. image_list,
    19. aug_pred=False,
    20. scales=1.0,
    21. flip_horizontal=True,
    22. flip_vertical=False,
    23. is_slide=False,
    24. stride=None,
    25. crop_size=None,
    26. custom_color=None
    27. ):
    28. # 加载模型权重
    29. para_state_dict = paddle.load(model_path)
    30. model.set_dict(para_state_dict)
    31. # 设置模型为评估模式
    32. model.eval()
    33. # 读取图像
    34. im = image_list.copy()
    35. color_map = visualize.get_color_map_list(256, custom_color=custom_color)
    36. with paddle.no_grad():
    37. data = preprocess(im, transforms)
    38. # 是否开启多尺度翻转预测
    39. if aug_pred:
    40. pred, _ = infer.aug_inference(
    41. model,
    42. data['img'],
    43. trans_info=data['trans_info'],
    44. scales=scales,
    45. flip_horizontal=flip_horizontal,
    46. flip_vertical=flip_vertical,
    47. is_slide=is_slide,
    48. stride=stride,
    49. crop_size=crop_size)
    50. else:
    51. pred, _ = infer.inference(
    52. model,
    53. data['img'],
    54. trans_info=data['trans_info'],
    55. is_slide=is_slide,
    56. stride=stride,
    57. crop_size=crop_size)
    58. # 将返回数据去除多余的通道,并转为uint8类型,方便保存为图片
    59. pred = paddle.squeeze(pred)
    60. pred = pred.numpy().astype('uint8')
    61. # 展示结果
    62. added_image = visualize_myself.visualize(image= im,result= pred,color_map=color_map, weight=0.6)
    63. #cv2.imshow('image_predict', added_image)
    64. # save pseudo color prediction
    65. pred_mask = visualize_myself.get_pseudo_color_map(pred, color_map)
    66. #cv2.waitKey(0)
    67. #cv2.destroyAllWindows()
    68. return added_image,pred_mask

    新建  seg_read.py  :  主要功能为将整体预测的输出改为指定输出某个种类的分割结果,这里我主要只输出车道线分割的预测部分

    1. from PIL import Image
    2. import numpy as np
    3. import cv2
    4. #生成染色版
    5. def get_voc_palette(num_classes):
    6. n = num_classes
    7. palette = [0]*(n*3)
    8. for j in range(0,n):
    9. lab = j
    10. palette[j*3+0] = 0
    11. palette[j*3+1] = 0
    12. palette[j*3+2] = 0
    13. i = 0
    14. while (lab > 0):
    15. palette[j*3+0] |= (((lab >> 0) & 1) << (7-i))
    16. palette[j*3+1] |= (((lab >> 1) & 1) << (7-i))
    17. palette[j*3+2] |= (((lab >> 2) & 1) << (7-i))
    18. i = i + 1
    19. lab >>= 3
    20. return palette
    21. #染色代码
    22. def colorize_mask(mask, palette):
    23. zero_pad = 256 * 3 - len(palette)
    24. for i in range(zero_pad):
    25. palette.append(0)
    26. new_mask = Image.fromarray(mask.astype(np.uint8)).convert('P')
    27. new_mask.putpalette(palette)
    28. return new_mask
    29. #生成指定类别的语义分割预测结果
    30. def generate_img_with_choice_class(img,classes:list,num_classes:int):
    31. #传入 图像路径 和 需要预测的类别 总共的类别
    32. #img = Image.open(img)#
    33. img = np.asarray(img)
    34. f_img = img.copy()
    35. for idx,c in enumerate(classes):
    36. f_img[np.where(img==c)] = 0 #将对应位置置零
    37. f_img = colorize_mask(f_img,get_voc_palette(num_classes)) # 进行染色处理
    38. image = f_img.convert("RGB")
    39. #image.save('output/process_img.png')
    40. img = cv2.cvtColor(np.asarray(image), cv2.COLOR_BGR2GRAY)
    41. return img
    42. if __name__ == '__main__':
    43. ''''''
    44. img_path = r'data/annotations/000026.png'
    45. choice_list = [0,2]
    46. img = generate_img_with_choice_class(img_path,choice_list,3)
    47. cv2.imshow('image',img)
    48. #cv2.imwrite('output/3.jpg', img)
    49. #cv2.waitKey(0)

    新建    opencv手动阈值分割.py   :通过滑块手动调整canny边缘检测的阈值

    1. import cv2
    2. #载入图片
    3. img_original=cv2.imread('output/1.jpg')
    4. #设置窗口
    5. cv2.namedWindow('Canny')
    6. #定义回调函数
    7. def nothing(x):
    8. pass
    9. #创建两个滑动条,分别控制threshold1,threshold2
    10. cv2.createTrackbar('threshold1','Canny',50,400,nothing)
    11. cv2.createTrackbar('threshold2','Canny',100,400,nothing)
    12. while(1):
    13. #返回滑动条所在位置的值
    14. threshold1=cv2.getTrackbarPos('threshold1','Canny')
    15. threshold2=cv2.getTrackbarPos('threshold2','Canny')
    16. #Canny边缘检测
    17. img_edges=cv2.Canny(img_original,threshold1,threshold2)
    18. #显示图片
    19. cv2.imshow('original',img_original)
    20. cv2.imshow('Canny',img_edges)
    21. if cv2.waitKey(1)==ord('q'):
    22. break
    23. cv2.destroyAllWindows()

    新建 HoughLines_return.py  :主要功能通过canny特征检测提取车道线上的大量零散坐标,再抽取边缘(包括:最上方、最左方、最下方、最右方)的各4个最接近边缘的输出坐标,求平均得到4个的方向的中点坐标,若最左方的车道线边缘中点靠近左侧上方且不上方接触则输出角度为左侧边缘的中点与下方边缘中点之间的角度,右侧和上方的同理。输出后便可通过串口通讯实时同步无人车的前方转向轮的方向角度。

    1. import cv2
    2. import matplotlib.pyplot as plt
    3. import numpy as np
    4. from collections import Counter
    5. import math
    6. def sort_max(list):
    7. list1 = list.copy()#复制一份,不破坏原来的列表
    8. list1.sort()#从小到大排序
    9. max1 = list1[-1]#最大
    10. max2 = list1[-2]#第二大
    11. max3 = list1[-3]#第3大
    12. max4 = list1[-4]#第4大
    13. return max1,max2,max3,max4
    14. def sort_min(list):
    15. list1 = list.copy() # 复制一份,不破坏原来的列表
    16. list1.sort() # 从小到大排序
    17. min1 = list1[0] # 最大
    18. min2 = list1[1] # 第二大
    19. min3 = list1[2] # 第3大
    20. min4 = list1[3] # 第4大
    21. return min1, min2, min3, min4
    22. def azimuthangle(x1, y1, x2, y2):
    23. """ 已知两点坐标计算角度 -
    24. :param x1: 原点横坐标值
    25. :param y1: 原点纵坐标值
    26. :param x2: 目标点横坐标值
    27. :param y2: 目标纵坐标值
    28. """
    29. angle = 0.0
    30. dx = x2 - x1
    31. dy = y2 - y1
    32. if x2 == x1:
    33. angle = math.pi / 2.0
    34. if y2 == y1:
    35. angle = 0.0
    36. elif y2 < y1:
    37. angle = 3.0 * math.pi / 2.0
    38. elif x2 > x1 and y2 > y1:
    39. angle = math.atan(dx / dy)
    40. elif x2 > x1 and y2 < y1:
    41. angle = math.pi / 2 + math.atan(-dy / dx)
    42. elif x2 < x1 and y2 < y1:
    43. angle = math.pi + math.atan(dx / dy)
    44. elif x2 < x1 and y2 > y1:
    45. angle = 3.0 * math.pi / 2.0 + math.atan(dy / -dx)
    46. return angle * 180 / math.pi
    47. def get_index(l, x, n):
    48. # 函数作用: 获取某个元素第n次出现在列表的下标
    49. # 参数列表: 第一个参数为可迭代对象, 第二个参数为要查找的数, 第三个参数为要查找第几个出现的x
    50. l_count = l.count(x)
    51. result = None
    52. if n <= l_count:
    53. num = 0
    54. for item in enumerate(l):
    55. if item[1] == x:
    56. num += 1
    57. if num == n:
    58. result = item[0]
    59. break
    60. else:
    61. print("列表里总共有{}个{}".format(l_count, x))
    62. return result
    63. def get_top_mid(x_list,y_list,judge):#真为上方,假为下方
    64. if judge ==True:
    65. min1_y, min2_y, min3_y, min4_y = sort_min(y_list)
    66. else:
    67. min1_y, min2_y, min3_y, min4_y = sort_max(y_list)
    68. #print("min1:", min1_y, "min2:", min2_y, "min3:", min3_y, "min4:", min4_y)
    69. min_list_y = [min1_y, min2_y, min3_y, min4_y]
    70. b = dict(Counter(min_list_y))
    71. same = [key for key, value in b.items() if value > 1] # 只展示重复元素
    72. min_list_x = []
    73. i_1 = 1
    74. i_2 = 1
    75. for max in min_list_y:
    76. if len(same)==0:
    77. y_index = get_index(y_list, max, 1)
    78. elif len(same)==1:
    79. if max == same[0]:
    80. y_index = get_index(y_list, max, i_1)
    81. i_1 =i_1+1
    82. else:
    83. y_index = get_index(y_list, max, 1)
    84. elif len(same)==2:
    85. if max == same[0]:
    86. y_index = get_index(y_list, max, i_1)
    87. i_1 =i_1+1
    88. elif max == same[1]:
    89. y_index = get_index(y_list, max, i_2)
    90. i_2 =i_2+1
    91. else:
    92. y_index = get_index(y_list, max, 1)
    93. min_list_x.append(x_list[y_index])
    94. #print("min_list_x", min_list_x)
    95. top_mid = [sum(min_list_x) / len(min_list_x), sum(min_list_y) / len(min_list_y)]
    96. #print("top_mid:", top_mid)
    97. return top_mid
    98. def get_side_mid(x_list,y_list,judge):#真为左,假为右
    99. if judge ==True:
    100. m1_x, m2_x, m3_x, m4_x = sort_min(x_list)
    101. else:
    102. m1_x, m2_x, m3_x, m4_x = sort_max(x_list)
    103. #print("min1:", m1_x, "min2:", m2_x, "min3:", m3_x, "min4:", m4_x)
    104. min_list_x = [m1_x, m2_x, m3_x, m4_x]
    105. b = dict(Counter(min_list_x))
    106. same = [key for key, value in b.items() if value > 1] # 只展示重复元素
    107. #print("same:", same, len(same))
    108. min_list_y = []
    109. i_1 = 1
    110. i_2 = 1
    111. for max in min_list_x:
    112. if len(same)==0:
    113. x_index = get_index(x_list, max, 1)
    114. elif len(same) == 1:
    115. if max == same:
    116. x_index = get_index(x_list, max, i_1)
    117. i_1 = i_1 + 1
    118. else:
    119. x_index = get_index(x_list, max, 1)
    120. #print("x_index_1:", type(x_index), x_index)
    121. elif len(same)==2:
    122. if max == same[0]:
    123. x_index = get_index(x_list, max, i_1)
    124. i_1 = i_1 + 1
    125. elif max == same[1]:
    126. x_index = get_index(x_list, max, i_2)
    127. i_2 = i_2 + 1
    128. else:
    129. x_index = get_index(x_list, max, 1)
    130. #print("x_index_1:", type(x_index), x_index)
    131. min_list_y.append(y_list[x_index])
    132. #print("min_list_x", min_list_y)
    133. side_mid = [sum(min_list_x) / len(min_list_x), sum(min_list_y) / len(min_list_y)]
    134. #print("side_mid:", side_mid)
    135. return side_mid
    136. def HL_return(src):
    137. img = src.copy()
    138. # 二值化图像(Canny边缘检测)
    139. # gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    140. dst_img = cv2.Canny(img, 11, 16)
    141. cv2.imshow('Canny', dst_img)
    142. #cv2.waitKey(0)
    143. lines = cv2.HoughLinesP(dst_img, 1, np.pi / 180, 20)
    144. x_list = []
    145. y_list = []
    146. for line in lines:
    147. for x1, y1, x2, y2 in line:
    148. x_list.append(x1)
    149. x_list.append(x2)
    150. y_list.append(y1)
    151. y_list.append(y2)
    152. cv2.line(img, (x1, y1), (x2, y2), (0, 255, 0), 3)
    153. cv2.imshow("HoughLinesP", img)
    154. #cv2.waitKey(0)
    155. top_mid = get_top_mid(x_list,y_list,True)#得到上面的中点
    156. down_mid = get_side_mid(x_list,y_list,False)#得到下面的中点
    157. left_side_mid = get_side_mid(x_list,y_list,True)#得到左边的中点
    158. right_side_mid = get_side_mid(x_list,y_list,False)#得到右边的中点
    159. print("top_mid:",top_mid,"dowm_mid:",down_mid,"left_mid:",left_side_mid,"right_mid:",right_side_mid)
    160. sp = src.shape
    161. sz1 = sp[0] # height(rows) of image
    162. sz2 = sp[1] # width(colums) of image
    163. sz3 = sp[2] # the pixels value is made up of three primary colors
    164. if left_side_mid[1]<(sp[0]*0.5):
    165. angle =azimuthangle (left_side_mid[0],left_side_mid[1],down_mid[0],down_mid[1])
    166. elif right_side_mid[1]<(sp[0]*0.5):
    167. angle = azimuthangle(right_side_mid[0], right_side_mid[1], down_mid[0], down_mid[1])
    168. else:
    169. angle = azimuthangle(top_mid[0], top_mid[1], down_mid[0], down_mid[1])
    170. #print("angle:", type(angle), angle)
    171. #cv2.waitKey(0)
    172. return angle
    173. if __name__ == '__main__':
    174. src = cv2.imread(r"D:\pyCharmdata\PPLiteSeg_demo\output\2.jpg")
    175. angle =HL_return(src)
    176. if (angle - 90) > 0:
    177. print("请右转:", math.fabs(angle - 90), "度")
    178. elif (angle - 90) < 0:
    179. print("请左转:", math.fabs(angle - 90), "度")
    180. elif angle == 90:
    181. print("正在保持90度直行")

     新建 demo_run_API.py  :调用实时分割API并实时输出转向角度

    1. import cv2
    2. from predict_with_api import predict
    3. from paddleseg.models import PPLiteSeg
    4. from paddleseg.models.backbones import STDC1
    5. import paddleseg.transforms as T
    6. from seg_read import generate_img_with_choice_class
    7. from HoughLines_return import HL_return
    8. import math
    9. backbone = STDC1()
    10. model = PPLiteSeg(num_classes=13,
    11. backbone= backbone,
    12. arm_out_chs = [32, 64, 128],
    13. seg_head_inter_chs = [32, 64, 64],
    14. pretrained=None)
    15. transforms = T.Compose([
    16. T.Resize(target_size=(512, 512)),
    17. T.RandomHorizontalFlip(),
    18. T.Normalize()
    19. ])
    20. model_path = 'output/best_model/model.pdparams'
    21. cap=cv2.VideoCapture(0)# 0
    22. if __name__ == '__main__':
    23. while True:
    24. rec,img = cap.read()
    25. added_image,pred_mask = predict(model=model,model_path=model_path, transforms=transforms,image_list=img)
    26. cv2.imshow('image_predict', added_image)#显示原图像与分割预测的合并图
    27. #
    28. choice_list = [0, 2]#只展示第2个类型的分割结果
    29. img = generate_img_with_choice_class(pred_mask, choice_list, 3)# PIL图像输入、展示第几个类型的分割、总的分割种类
    30. cv2.imshow('image', img)#将返回的指定分割类型的灰度图显示出来
    31. angle = HL_return(img)
    32. if (angle - 90) > 0:
    33. print("请右转:", math.fabs(angle - 90), "度")
    34. elif (angle - 90) < 0:
    35. print("请左转:", math.fabs(angle - 90), "度")
    36. elif angle == 90:
    37. print("正在保持90度直行")
    38. if cv2.waitKey(1)==ord('q'):
    39. break

    三、总结

    总的来说,虽然是完成的整体开发工作,但是由于使用的是数据集,且使用的数据集数量较少,训练程度较低。尚未落地进行实验,可能存在许多调试和逻辑上的问题有待发现和解决,且判断车道线的流向方向的阈值是手动自己估计的,实际的阈值的差值有待商榷。逻辑控制方面上相对大厂的算法显得不太成熟和简单。将输出角度与转向轮方向角度同步的部分,使用pyserial与下位机进行串口通讯,下位机再控制电机的转向轮到指定角度。

    再加上目标检测部分,就形成了一个简易的无人驾驶的视觉部分。

    爆改YOLOV7的detect.py制作成API接口供其他python程序调用(超低延时)_Leonard2021的博客-CSDN博客

    本人完全独立自制的小玩意,简易版本的无人驾驶的视觉部分,精度肯定不如大厂的成品算法,如果有时间的精力的话,会继续改进的更新的。

    本项目的架构,有需要可以自取:

    PPLiteSeg_AutoDrive_demo.zip-深度学习文档类资源-CSDN下载

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    本文到此结束,如果对你有帮助,欢迎一键三连!!!

  • 相关阅读:
    Appium学习日记(三)——Windows系统测试桌面应用
    Mysql整理-主从复制
    Java 基础常见知识点&面试题总结(下),2022 最新版!
    源码分析RocketMQ之broker-文件恢复
    Java后端学习路线
    计算机毕业设计SSM电竞资讯网站【附源码数据库】
    分享一个图片转换工具XnConvert
    【CMU15-445 Part-4】DatabaseStorage ii
    联邦学习模型分类
    五、Kafka日志存储
  • 原文地址:https://blog.csdn.net/weixin_51331359/article/details/126223158