教程 | 无需复杂深度学习算法,基于计算机视觉使用Python和OpenCV计算道路交通

机器之心 / 2018年04月14日 16:22

互联网+

选自hackernoon

参与:路雪、刘晓坤

本文介绍了不使用复杂的深度学习算法计算道路交通的方法。该方法基于计算机视觉,仅使用 Python 和 OpenCV,在背景提取算法的帮助下,使用简单的移动侦测来完成任务。

今天我们将学习如何在没有复杂深度学习算法的前提下基于计算机视觉计算道路交通。

该教程中,我们仅使用 Python 和 OpenCV,在背景提取算法的帮助下,使用简单的移动侦测来完成任务。

代码地址:https://github.com/creotiv/object_detection_projects/tree/master/opencv_traffic_counting

这里是我们的计划:

  1. 了解用于前景检测的背景提取算法的主要思想。

  2. OpenCV 图像过滤器。

  3. 基于轮廓的目标检测。

  4. 构建处理管道,用于进一步的数据处理。

视频中展示了结果:

背景提取算法

背景提取有很多不同算法,但是它们的主要思想非常简单。

我们来假设你有一个自己房间的视频,该视频的很多帧都没有人/宠物,因此基本上是静态的,我们称之为 background_layer。那么,要想获取视频中移动的物体,我们只需:

foreground_objects = current_frame - background_layer

但是有时候,我们无法获取静态帧,因为光线的变化、某些物体被移动或一直移动等。在这些情况下,我们保存某些帧,尝试找出它们中相同的像素,这些像素就是 background_layer 的一部分。区别通常在于我们获取 background_layer 和用于使选择更加准确的额外过滤的方式。

本教程中,我们将使用 MOG 算法进行背景提取。视频经算法处理后,如下图所示:

左侧是原始帧,右侧是使用 MOG(带有阴影检测)算法提取的背景

如图所示,前景模板仍存在一些噪声,我们将尝试使用标准过滤技术移除噪声。

代码如下:

  1. importos

  2. importlogging

  3. importlogging.handlers

  4. importrandom

  5. importnumpy asnp

  6. importskvideo.io

  7. importcv2

  8. importmatplotlib.pyplot asplt

  9. importutils

  10. # without this some strange errors happen

  11. cv2.ocl.setUseOpenCL(False)

  12. random.seed(123)

  13. # ============================================================================

  14. IMAGE_DIR = "./out"

  15. VIDEO_SOURCE = "input.mp4"

  16. SHAPE = (720, 1280) # HxW

  17. # ============================================================================

  18. deftrain_bg_subtractor(inst, cap, num=500):

  19. '''

  20. BG substractor need process some amount of frames to start giving result

  21. '''

  22. print('Training BG Subtractor...')

  23. i = 0

  24. forframe incap:

  25. inst.apply(frame, None, 0.001)

  26. i += 1

  27. ifi >= num:

  28. returncap

  29. defmain():

  30. log = logging.getLogger("main")

  31. # creting MOG bg subtractor with 500 frames in cache

  32. # and shadow detction

  33. bg_subtractor = cv2.createBackgroundSubtractorMOG2(

  34. history=500, detectShadows=True)

  35. # Set up image source

  36. # You can use also CV2, for some reason it not working for me

  37. cap = skvideo.io.vreader(VIDEO_SOURCE)

  38. # skipping 500 frames to train bg subtractor

  39. train_bg_subtractor(bg_subtractor, cap, num=500)

  40. frame_number = -1

  41. forframe incap:

  42. ifnotframe.any():

  43. log.error("Frame capture failed, stopping...")

  44. break

  45. frame_number += 1

  46. utils.save_frame(frame, "./out/frame_%04d.png"% frame_number)

  47. fg_mask = bg_subtractor.apply(frame, None, 0.001)

  48. utils.save_frame(frame, "./out/fg_mask_%04d.png"% frame_number)

  49. # ============================================================================

  50. if__name__ == "__main__":

  51. log = utils.init_logging()

  52. ifnotos.path.exists(IMAGE_DIR):

  53. log.debug("Creating image directory `%s`...", IMAGE_DIR)

  54. os.makedirs(IMAGE_DIR)

  55. main()

过滤

我们这种情况需要这些过滤器:Threshold(http://docs.opencv.org/3.1.0/d7/d4d/tutorial_py_thresholding.html)、Erode、Dilate、Opening 和 Closing(http://docs.opencv.org/3.1.0/d9/d61/tutorial_py_morphological_ops.html)。请打开链接并阅读,查看这些过滤器的工作方式(而不是简单的复制/粘贴)。

那么,现在我们将使用过滤器移除前景模板上的噪声。

首先,我们将使用 Closing 过滤器移除区域中的缝隙,然后使用 Opening 移除 1–2 个像素点,之后使用 Dilate 使物体更加清晰。

  1. deffilter_mask(img):

  2. kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2, 2))

  3. # Fill any small holes

  4. closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

  5. # Remove noise

  6. opening = cv2.morphologyEx(closing, cv2.MORPH_OPEN, kernel)

  7. # Dilate to merge adjacent blobs

  8. dilation = cv2.dilate(opening, kernel, iterations=2)

  9. # threshold

  10. th = dilation[dilation < 240] = 0

  11. returnth

处理后的前景如下图所示:

基于轮廓的目标检测

为达到目的,我们使用带有下列参数的标准 cv2.findContours 方法:

cv2.CV_RETR_EXTERNAL—get only outer contours.

cv2.CV_CHAIN_APPROX_TC89_L1 - use Teh-Chin chain approximation algorithm (faster)

  1. defget_centroid(x, y, w, h):

  2. x1 = int(w / 2)

  3. y1 = int(h / 2)

  4. cx = x + x1

  5. cy = y + y1

  6. return(cx, cy)

  7. defdetect_vehicles(fg_mask, min_contour_width=35, min_contour_height=35):

  8. matches = []

  9. # finding external contours

  10. im, contours, hierarchy = cv2.findContours(

  11. fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_L1)

  12. # filtering by with, height

  13. for(i, contour) inenumerate(contours):

  14. (x, y, w, h) = cv2.boundingRect(contour)

  15. contour_valid = (w >= min_contour_width) and(

  16. h >= min_contour_height)

  17. ifnotcontour_valid:

  18. continue

  19. # getting center of the bounding box

  20. centroid = get_centroid(x, y, w, h)

  21. matches.append(((x, y, w, h), centroid))

  22. returnmatches

很简单,对吧?

构建处理管道

你必须理解,在机器学习和计算机视觉领域中,没有一种魔术般的算法能够搞定一切,即使我们想象存在这样一种算法,我们仍然无法使用它,因为它在大规模应用时会无效。比如,几年前,Netflix 创办了一个比赛,最佳电影推荐算法奖励 300 万美元。有一支队伍创建了一个最佳算法,但问题是该算法无法大规模应用,因此对该公司没有用处。但是,Netflix 仍然奖励了他们 100 万。:)

那么,现在我们将构建简单的处理管道,该管道不是为了大规模使用,而是为了方便,但原理是一样的。

  1. classPipelineRunner(object):

  2. '''

  3. Very simple pipline.

  4. Just run passed processors in order with passing context from one to

  5. another.

  6. You can also set log level for processors.

  7. '''

  8. def__init__(self, pipeline=None, log_level=logging.DEBUG):

  9. self.pipeline = pipeline or[]

  10. self.context = {}

  11. self.log = logging.getLogger(self.__class__.__name__)

  12. self.log.setLevel(log_level)

  13. self.log_level = log_level

  14. self.set_log_level()

  15. defset_context(self, data):

  16. self.context = data

  17. defadd(self, processor):

  18. ifnotisinstance(processor, PipelineProcessor):

  19. raiseException(

  20. 'Processor should be an isinstance of PipelineProcessor.')

  21. processor.log.setLevel(self.log_level)

  22. self.pipeline.append(processor)

  23. defremove(self, name):

  24. fori, p inenumerate(self.pipeline):

  25. ifp.__class__.__name__ == name:

  26. delself.pipeline[i]

  27. returnTrue

  28. returnFalse

  29. defset_log_level(self):

  30. forp inself.pipeline:

  31. p.log.setLevel(self.log_level)

  32. defrun(self):

  33. forp inself.pipeline:

  34. self.context = p(self.context)

  35. self.log.debug("Frame #%d processed.", self.context['frame_number'])

  36. returnself.context

  37. classPipelineProcessor(object):

  38. '''

  39. Base class for processors.

  40. '''

  41. def__init__(self):

  42. self.log = logging.getLogger(self.__class__.__name__)

由于输入构造函数(input constructor)将使用一串处理器,它们将按顺序运行,每个处理器处理一部分工作。那么,现在我们就来创建一个轮廓检测处理器。

  1. classContourDetection(PipelineProcessor):

  2. '''

  3. Detecting moving objects.

  4. Purpose of this processor is to subtrac background, get moving objects

  5. and detect them with a cv2.findContours method, and then filter off-by

  6. width and height.

  7. bg_subtractor - background subtractor isinstance.

  8. min_contour_width - min bounding rectangle width.

  9. min_contour_height - min bounding rectangle height.

  10. save_image - if True will save detected objects mask to file.

  11. image_dir - where to save images(must exist).

  12. '''

  13. def__init__(self, bg_subtractor, min_contour_width=35, min_contour_height=35, save_image=False, image_dir='images'):

  14. super(ContourDetection, self).__init__()

  15. self.bg_subtractor = bg_subtractor

  16. self.min_contour_width = min_contour_width

  17. self.min_contour_height = min_contour_height

  18. self.save_image = save_image

  19. self.image_dir = image_dir

  20. deffilter_mask(self, img, a=None):

  21. '''

  22. This filters are hand-picked just based on visual tests

  23. '''

  24. kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2, 2))

  25. # Fill any small holes

  26. closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

  27. # Remove noise

  28. opening = cv2.morphologyEx(closing, cv2.MORPH_OPEN, kernel)

  29. # Dilate to merge adjacent blobs

  30. dilation = cv2.dilate(opening, kernel, iterations=2)

  31. returndilation

  32. defdetect_vehicles(self, fg_mask, context):

  33. matches = []

  34. # finding external contours

  35. im2, contours, hierarchy = cv2.findContours(

  36. fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_L1)

  37. for(i, contour) inenumerate(contours):

  38. (x, y, w, h) = cv2.boundingRect(contour)

  39. contour_valid = (w >= self.min_contour_width) and(

  40. h >= self.min_contour_height)

  41. ifnotcontour_valid:

  42. continue

  43. centroid = utils.get_centroid(x, y, w, h)

  44. matches.append(((x, y, w, h), centroid))

  45. returnmatches

  46. def__call__(self, context):

  47. frame = context['frame'].copy()

  48. frame_number = context['frame_number']

  49. fg_mask = self.bg_subtractor.apply(frame, None, 0.001)

  50. # just thresholding values

  51. fg_mask[fg_mask < 240] = 0

  52. fg_mask = self.filter_mask(fg_mask, frame_number)

  53. ifself.save_image:

  54. utils.save_frame(fg_mask, self.image_dir +

  55. "/mask_%04d.png"% frame_number, flip=False)

  56. context['objects'] = self.detect_vehicles(fg_mask, context)

  57. context['fg_mask'] = fg_mask

  58. returncontex

其实就是把背景提取、过滤和检测部分合并起来。

现在,我们来创建一个处理器,其将在不同帧上检测到的物体连接起来并创建路径,还能计算出口区的车辆数量。

  1. '''

  2. Counting vehicles that entered in exit zone.

  3. Purpose of this class based on detected object and local cache create

  4. objects pathes and count that entered in exit zone defined by exit masks.

  5. exit_masks - list of the exit masks.

  6. path_size - max number of points in a path.

  7. max_dst - max distance between two points.

  8. '''

  9. def__init__(self, exit_masks=[], path_size=10, max_dst=30, x_weight=1.0, y_weight=1.0):

  10. super(VehicleCounter, self).__init__()

  11. self.exit_masks = exit_masks

  12. self.vehicle_count = 0

  13. self.path_size = path_size

  14. self.pathes = []

  15. self.max_dst = max_dst

  16. self.x_weight = x_weight

  17. self.y_weight = y_weight

  18. defcheck_exit(self, point):

  19. forexit_mask inself.exit_masks:

  20. try:

  21. ifexit_mask[point[1]][point[0]] == 255:

  22. returnTrue

  23. except:

  24. returnTrue

  25. returnFalse

  26. def__call__(self, context):

  27. objects = context['objects']

  28. context['exit_masks'] = self.exit_masks

  29. context['pathes'] = self.pathes

  30. context['vehicle_count'] = self.vehicle_count

  31. ifnotobjects:

  32. returncontext

  33. points = np.array(objects)[:, 0:2]

  34. points = points.tolist()

  35. # add new points if pathes is empty

  36. ifnotself.pathes:

  37. formatch inpoints:

  38. self.pathes.append([match])

  39. else:

  40. # link new points with old pathes based on minimum distance between

  41. # points

  42. new_pathes = []

  43. forpath inself.pathes:

  44. _min = 999999

  45. _match = None

  46. forp inpoints:

  47. iflen(path) == 1:

  48. # distance from last point to current

  49. d = utils.distance(p[0], path[-1][0])

  50. else:

  51. # based on 2 prev points predict next point and calculate

  52. # distance from predicted next point to current

  53. xn = 2* path[-1][0][0] - path[-2][0][0]

  54. yn = 2* path[-1][0][1] - path[-2][0][1]

  55. d = utils.distance(

  56. p[0], (xn, yn),

  57. x_weight=self.x_weight,

  58. y_weight=self.y_weight

  59. )

  60. ifd < _min:

  61. _min = d

  62. _match = p

  63. if_match and_min <= self.max_dst:

  64. points.remove(_match)

  65. path.append(_match)

  66. new_pathes.append(path)

  67. # do not drop path if current frame has no matches

  68. if_match isNone:

  69. new_pathes.append(path)

  70. self.pathes = new_pathes

  71. # add new pathes

  72. iflen(points):

  73. forp inpoints:

  74. # do not add points that already should be counted

  75. ifself.check_exit(p[1]):

  76. continue

  77. self.pathes.append([p])

  78. # save only last N points in path

  79. fori, _ inenumerate(self.pathes):

  80. self.pathes[i] = self.pathes[i][self.path_size * -1:]

  81. # count vehicles and drop counted pathes:

  82. new_pathes = []

  83. fori, path inenumerate(self.pathes):

  84. d = path[-2:]

  85. if(

  86. # need at list two points to count

  87. len(d) >= 2and

  88. # prev point not in exit zone

  89. notself.check_exit(d[0][1]) and

  90. # current point in exit zone

  91. self.check_exit(d[1][1]) and

  92. # path len is bigger then min

  93. self.path_size <= len(path)

  94. ):

  95. self.vehicle_count += 1

  96. else:

  97. # prevent linking with path that already in exit zone

  98. add = True

  99. forp inpath:

  100. ifself.check_exit(p[1]):

  101. add = False

  102. break

  103. ifadd:

  104. new_pathes.append(path)

  105. self.pathes = new_pathes

  106. context['pathes'] = self.pathes

  107. context['objects'] = objects

  108. context['vehicle_count'] = self.vehicle_count

  109. self.log.debug('#VEHICLES FOUND: %s'% self.vehicle_count)

  110. returncontext

该教程有一点复杂,我们一部分一部分地过一遍。

下图中绿色的掩膜是出口区,我们在该区域计算车辆的数量。比如,我们将计算长度大于 3 个点(以移除噪声)的路径,其中第 4 个点就在绿色区域。

我们使用掩膜,因为它对很多操作都有效且比使用向量算法更简单。只需要使用二元和(binary and)运算检查该区域的点就可以了。下图显示了我们的设置方式:

  1. EXIT_PTS = np.array([

  2. [[732, 720], [732, 590], [1280, 500], [1280, 720]],

  3. [[0, 400], [645, 400], [645, 0], [0, 0]]

  4. ])

  5. base = np.zeros(SHAPE + (3,), dtype='uint8')

  6. exit_mask = cv2.fillPoly(base, EXIT_PTS, (255, 255, 255))[:, :, 0]

现在,我们连接路径中的点:

  1. new_pathes = []

  2. forpath inself.pathes:

  3. _min = 999999

  4. _match = None

  5. forp inpoints:

  6. iflen(path) == 1:

  7. # distance from last point to current

  8. d = utils.distance(p[0], path[-1][0])

  9. else:

  10. # based on 2 prev points predict next point and calculate

  11. # distance from predicted next point to current

  12. xn = 2* path[-1][0][0] - path[-2][0][0]

  13. yn = 2* path[-1][0][1] - path[-2][0][1]

  14. d = utils.distance(

  15. p[0], (xn, yn),

  16. x_weight=self.x_weight,

  17. y_weight=self.y_weight

  18. )

  19. ifd < _min:

  20. _min = d

  21. _match = p

  22. if_match and_min <= self.max_dst:

  23. points.remove(_match)

  24. path.append(_match)

  25. new_pathes.append(path)

  26. # do not drop path if current frame has no matches

  27. if_match isNone:

  28. new_pathes.append(path)

  29. self.pathes = new_pathes

  30. # add new pathes

  31. iflen(points):

  32. forp inpoints:

  33. # do not add points that already should be counted

  34. ifself.check_exit(p[1]):

  35. continue

  36. self.pathes.append([p])

  37. # save only last N points in path

  38. fori, _ inenumerate(self.pathes):

  39. self.pathes[i] = self.pathes[i][self.path_size * -1:]

在第一帧上,我们只需添加所有点作为新的路径。

接下来,如果 len(path) == 1,对于高速缓存中的每个路径,我们将尝试从新检测到的物体中找出点(质心),这些物体到路径最后一个点的欧几里得距离最短。

如果 len(path) > 1,我们将使用该路径中的最后两个点在同一条线上预测新的点,找出它和当前点之间的最小距离。

将最小距离的点添加至当前路径的末尾,然后将其从列表中移除。

如果还有剩下的点,我们将它们添加为新的路径。

我们还可以限制该路径中点的数量。

  1. # count vehicles and drop counted pathes:

  2. new_pathes = []

  3. fori, path inenumerate(self.pathes):

  4. d = path[-2:]

  5. if(

  6. # need at list two points to count

  7. len(d) >= 2and

  8. # prev point not in exit zone

  9. notself.check_exit(d[0][1]) and

  10. # current point in exit zone

  11. self.check_exit(d[1][1]) and

  12. # path len is bigger then min

  13. self.path_size <= len(path)

  14. ):

  15. self.vehicle_count += 1

  16. else:

  17. # prevent linking with path that already in exit zone

  18. add = True

  19. forp inpath:

  20. ifself.check_exit(p[1]):

  21. add = False

  22. break

  23. ifadd:

  24. new_pathes.append(path)

  25. self.pathes = new_pathes

  26. context['pathes'] = self.pathes

  27. context['objects'] = objects

  28. context['vehicle_count'] = self.vehicle_count

  29. self.log.debug('#VEHICLES FOUND: %s'% self.vehicle_count)

  30. returncontext

现在,我们尝试计算进入出口区的车辆的数量。我们需要观察路径中的最后两个点,并在出口区检查,是否其中靠后的一个在出口区,而靠前的不在,并确保 len(path) 比下限值要大。

之后的部分就是阻止新的点回联至出口区的点。

最后两个处理器是 CSV writer,可创建报告 CSV 文件和可视化文件,用于调试和输出更好的画面。

  1. classCsvWriter(PipelineProcessor):

  2. def__init__(self, path, name, start_time=0, fps=15):

  3. super(CsvWriter, self).__init__()

  4. self.fp = open(os.path.join(path, name), 'w')

  5. self.writer = csv.DictWriter(self.fp, fieldnames=['time', 'vehicles'])

  6. self.writer.writeheader()

  7. self.start_time = start_time

  8. self.fps = fps

  9. self.path = path

  10. self.name = name

  11. self.prev = None

  12. def__call__(self, context):

  13. frame_number = context['frame_number']

  14. count = _count = context['vehicle_count']

  15. ifself.prev:

  16. _count = count - self.prev

  17. time = ((self.start_time + int(frame_number / self.fps)) * 100

  18. + int(100.0/ self.fps) * (frame_number % self.fps))

  19. self.writer.writerow({'time': time, 'vehicles': _count})

  20. self.prev = count

  21. returncontext

  22. classVisualizer(PipelineProcessor):

  23. def__init__(self, save_image=True, image_dir='images'):

  24. super(Visualizer, self).__init__()

  25. self.save_image = save_image

  26. self.image_dir = image_dir

  27. defcheck_exit(self, point, exit_masks=[]):

  28. forexit_mask inexit_masks:

  29. ifexit_mask[point[1]][point[0]] == 255:

  30. returnTrue

  31. returnFalse

  32. defdraw_pathes(self, img, pathes):

  33. ifnotimg.any():

  34. return

  35. fori, path inenumerate(pathes):

  36. path = np.array(path)[:, 1].tolist()

  37. forpoint inpath:

  38. cv2.circle(img, point, 2, CAR_COLOURS[0], -1)

  39. cv2.polylines(img, [np.int32(path)], False, CAR_COLOURS[0], 1)

  40. returnimg

  41. defdraw_boxes(self, img, pathes, exit_masks=[]):

  42. for(i, match) inenumerate(pathes):

  43. contour, centroid = match[-1][:2]

  44. ifself.check_exit(centroid, exit_masks):

  45. continue

  46. x, y, w, h = contour

  47. cv2.rectangle(img, (x, y), (x + w - 1, y + h - 1),

  48. BOUNDING_BOX_COLOUR, 1)

  49. cv2.circle(img, centroid, 2, CENTROID_COLOUR, -1)

  50. returnimg

  51. defdraw_ui(self, img, vehicle_count, exit_masks=[]):

  52. # this just add green mask with opacity to the image

  53. forexit_mask inexit_masks:

  54. _img = np.zeros(img.shape, img.dtype)

  55. _img[:, :] = EXIT_COLOR

  56. mask = cv2.bitwise_and(_img, _img, mask=exit_mask)

  57. cv2.addWeighted(mask, 1, img, 1, 0, img)

  58. # drawing top block with counts

  59. cv2.rectangle(img, (0, 0), (img.shape[1], 50), (0, 0, 0), cv2.FILLED)

  60. cv2.putText(img, ("Vehicles passed: {total} ".format(total=vehicle_count)), (30, 30),

  61. cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 1)

  62. returnimg

  63. def__call__(self, context):

  64. frame = context['frame'].copy()

  65. frame_number = context['frame_number']

  66. pathes = context['pathes']

  67. exit_masks = context['exit_masks']

  68. vehicle_count = context['vehicle_count']

  69. frame = self.draw_ui(frame, vehicle_count, exit_masks)

  70. frame = self.draw_pathes(frame, pathes)

  71. frame = self.draw_boxes(frame, pathes, exit_masks)

  72. utils.save_frame(frame, self.image_dir +

  73. "/processed_%04d.png"% frame_number)

  74. returncontext

CSV writer 按时间保存数据,因为我们需要用它做进一步的分析。因此我使用下列公式向 unix 时间戳添加额外的帧计时:

time = ((self.start_time + int(frame_number / self.fps)) * 100

+ int(100.0 / self.fps) * (frame_number % self.fps))

在 start time=1 000 000 000 和 fps=10 的情况下,结果如下:

frame 1 = 1 000 000 000 010

frame 1 = 1 000 000 000 020

获取完整的 csv 报告后,你可以随意聚合这些数据。

该项目的完整代码地址:https://github.com/creotiv/object_detection_projects/tree/master/opencv_traffic_counting

结论

所以,这并不像人们想象的那么难。

但是,如果你运行该脚本,你会发现该解决方案并不完美,它存在一个问题——背景物体重叠,而且它还无法按类型进行车辆分类(实分析时你肯定需要)。但是,该方法拥有好的摄像头位置(道路上方),能够提供相当不错的准确率。这告诉我们即使简单的小算法用好了也能取得不错的结果。

那么我们要怎么做才能解决当前的问题呢?

一种方式是添加额外的过滤,使物体分离,以进行更好的检测。另一种方式是使用更复杂的算法,比如深度卷积网络。

原文地址:https://hackernoon.com/tutorial-making-road-traffic-counting-app-based-on-computer-vision-and-opencv-166937911660

本文为机器之心编译,转载请联系本公众号获得授权。返回搜狐,查看更多

责任编辑:

1.环球科技网遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.环球科技网的原创文章,请转载时务必注明文章作者和"来源:环球科技网",不尊重原创的行为环球科技网或将追究责任;3.作者投稿可能会经环球科技网编辑修改或补充。