选自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. import os
  2. import logging
  3. import logging.handlers
  4. import random
  5. import numpy as np
  6. import skvideo.io
  7. import cv2
  8. import matplotlib.pyplot as plt
  9. import utils
  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. def train_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. for frame in cap:
  25.        inst.apply(frame, None, 0.001)
  26.        i += 1
  27. if i >= num:
  28. return cap
  29. def main():
  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. for frame in cap:
  42. ifnot frame.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. ifnot os.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. def filter_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. return th
处理后的前景如下图所示:
基于轮廓的目标检测
为达到目的,我们使用带有下列参数的标准 cv2.findContours 方法:
cv2.CV_RETR_EXTERNAL—get only outer contours.
cv2.CV_CHAIN_APPROX_TC89_L1 - use Teh-Chin chain approximation algorithm (faster)
  1. def get_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. def detect_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) in enumerate(contours):
  14.        (x, y, w, h) = cv2.boundingRect(contour)
  15.        contour_valid = (w >= min_contour_width) and (
  16.            h >= min_contour_height)
  17. ifnot contour_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. return matches
在出口区,我们通过高度、宽度添加过滤,并添加质心。
很简单,对吧?
构建处理管道
你必须理解,在机器学习和计算机视觉领域中,没有一种魔术般的算法能够搞定一切,即使我们想象存在这样一种算法,我们仍然无法使用它,因为它在大规模应用时会无效。比如,几年前,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. def set_context(self, data):
  16.        self.context = data
  17. def add(self, processor):
  18. ifnot isinstance(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. def remove(self, name):
  24. for i, p in enumerate(self.pipeline):
  25. if p.__class__.__name__ == name:
  26. del self.pipeline[i]
  27. returnTrue
  28. returnFalse
  29. def set_log_level(self):
  30. for p in self.pipeline:
  31.            p.log.setLevel(self.log_level)
  32. def run(self):
  33. for p in self.pipeline:
  34.            self.context = p(self.context)
  35.        self.log.debug("Frame #%d processed.", self.context['frame_number'])
  36. return self.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. def filter_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. return dilation
  32. def detect_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) in enumerate(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. ifnot contour_valid:
  42. continue
  43.            centroid = utils.get_centroid(x, y, w, h)
  44.            matches.append(((x, y, w, h), centroid))
  45. return matches
  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. if self.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. return contex
其实就是把背景提取、过滤和检测部分合并起来。
现在,我们来创建一个处理器,其将在不同帧上检测到的物体连接起来并创建路径,还能计算出口区的车辆数量。
  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. def check_exit(self, point):
  19. for exit_mask in self.exit_masks:
  20. try:
  21. if exit_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. ifnot objects:
  32. return context
  33.        points = np.array(objects)[:, 0:2]
  34.        points = points.tolist()
  35. # add new points if pathes is empty
  36. ifnot self.pathes:
  37. for match in points:
  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. for path in self.pathes:
  44.                _min = 999999
  45.                _match = None
  46. for p in points:
  47. if len(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. if d < _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. if len(points):
  73. for p in points:
  74. # do not add points that already should be counted
  75. if self.check_exit(p[1]):
  76. continue
  77.                    self.pathes.append([p])
  78. # save only last N points in path
  79. for i, _ in enumerate(self.pathes):
  80.            self.pathes[i] = self.pathes[i][self.path_size * -1:]
  81. # count vehicles and drop counted pathes:
  82.        new_pathes = []
  83. for i, path in enumerate(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. not self.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. for p in path:
  100. if self.check_exit(p[1]):
  101.                        add = False
  102. break
  103. if add:
  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. return context
该教程有一点复杂,我们一部分一部分地过一遍。
下图中绿色的掩膜是出口区,我们在该区域计算车辆的数量。比如,我们将计算长度大于 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. for path in self.pathes:
  3.    _min = 999999
  4.    _match = None
  5. for p in points:
  6. if len(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. if d < _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. if len(points):
  32. for p in points:
  33. # do not add points that already should be counted
  34. if self.check_exit(p[1]):
  35. continue
  36.        self.pathes.append([p])
  37. # save only last N points in path
  38. for i, _ in enumerate(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. for i, path in enumerate(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. not self.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. for p in path:
  20. if self.check_exit(p[1]):
  21.                add = False
  22. break
  23. if add:
  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. return context
现在,我们尝试计算进入出口区的车辆的数量。我们需要观察路径中的最后两个点,并在出口区检查,是否其中靠后的一个在出口区,而靠前的不在,并确保 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. if self.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. return context
  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. def check_exit(self, point, exit_masks=[]):
  28. for exit_mask in exit_masks:
  29. if exit_mask[point[1]][point[0]] == 255:
  30. returnTrue
  31. returnFalse
  32. def draw_pathes(self, img, pathes):
  33. ifnot img.any():
  34. return
  35. for i, path in enumerate(pathes):
  36.            path = np.array(path)[:, 1].tolist()
  37. for point in path:
  38.                cv2.circle(img, point, 2, CAR_COLOURS[0], -1)
  39.                cv2.polylines(img, [np.int32(path)], False, CAR_COLOURS[0], 1)
  40. return img
  41. def draw_boxes(self, img, pathes, exit_masks=[]):
  42. for (i, match) in enumerate(pathes):
  43.            contour, centroid = match[-1][:2]
  44. if self.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. return img
  51. def draw_ui(self, img, vehicle_count, exit_masks=[]):
  52. # this just add green mask with opacity to the image
  53. for exit_mask in exit_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. return img
  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. return context
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
本文为机器之心编译,转载请联系本公众号获得授权
✄------------------------------------------------
加入机器之心(全职记者/实习生):[email protected]
投稿或寻求报道:[email protected]
广告&商务合作:[email protected]
继续阅读
阅读原文