J'ai essayé de compter les voitures lors du franchissement de la ligne et cela fonctionne, mais le problème est qu'il compte une voiture plusieurs fois, ce qui est ridicule car il devrait être compté une fois
Voici le code que j'utilise:
import cv2
import numpy as np
bgsMOG = cv2.BackgroundSubtractorMOG()
cap = cv2.VideoCapture("traffic.avi")
counter = 0
if cap:
while True:
ret, frame = cap.read()
if ret:
fgmask = bgsMOG.apply(frame, None, 0.01)
cv2.line(frame,(0,60),(160,60),(255,255,0),1)
# To find the countours of the Cars
contours, hierarchy = cv2.findContours(fgmask,
cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
try:
hierarchy = hierarchy[0]
except:
hierarchy = []
for contour, hier in Zip(contours, hierarchy):
(x, y, w, h) = cv2.boundingRect(contour)
if w > 20 and h > 20:
cv2.rectangle(frame, (x,y), (x+w,y+h), (255, 0, 0), 1)
#To find centroid of the Car
x1 = w/2
y1 = h/2
cx = x+x1
cy = y+y1
## print "cy=", cy
## print "cx=", cx
centroid = (cx,cy)
## print "centoid=", centroid
# Draw the circle of Centroid
cv2.circle(frame,(int(cx),int(cy)),2,(0,0,255),-1)
# To make sure the Car crosses the line
## dy = cy-108
## print "dy", dy
if centroid > (27, 38) and centroid < (134, 108):
## if (cx <= 132)and(cx >= 20):
counter +=1
## print "counter=", counter
## if cy > 10 and cy < 160:
cv2.putText(frame, str(counter), (x,y-5),
cv2.FONT_HERSHEY_SIMPLEX,
0.5, (255, 0, 255), 2)
## cv2.namedWindow('Output',cv2.cv.CV_WINDOW_NORMAL)
cv2.imshow('Output', frame)
## cv2.imshow('FGMASK', fgmask)
key = cv2.waitKey(60)
if key == 27:
break
cap.release()
cv2.destroyAllWindows()
et la vidéo est sur ma page github @ https://github.com/Tes3awy/MatLab-Tutorials appelée traffic.avi, et c'est aussi une vidéo intégrée dans la bibliothèque Matlab
Une aide pour que chaque voiture soit comptée une fois?
EDIT: Les images individuelles de la vidéo se présentent comme suit:
Afin de comprendre ce qui se passe et finalement de résoudre notre problème, nous devons d'abord améliorer un peu le script.
J'ai ajouté la journalisation des étapes importantes de votre algorithme, refactorisé un peu le code et ajouté la sauvegarde du masque et des images traitées, ajouté la possibilité d'exécuter le script en utilisant les images de trame individuelles, ainsi que d'autres modifications.
Voici à quoi ressemble le script à ce stade:
import logging
import logging.handlers
import os
import time
import sys
import cv2
import numpy as np
from vehicle_counter import VehicleCounter
# ============================================================================
IMAGE_DIR = "images"
IMAGE_FILENAME_FORMAT = IMAGE_DIR + "/frame_%04d.png"
# Support either video file or individual frames
CAPTURE_FROM_VIDEO = False
if CAPTURE_FROM_VIDEO:
IMAGE_SOURCE = "traffic.avi" # Video file
else:
IMAGE_SOURCE = IMAGE_FILENAME_FORMAT # Image sequence
# Time to wait between frames, 0=forever
WAIT_TIME = 1 # 250 # ms
LOG_TO_FILE = True
# Colours for drawing on processed frames
DIVIDER_COLOUR = (255, 255, 0)
BOUNDING_BOX_COLOUR = (255, 0, 0)
CENTROID_COLOUR = (0, 0, 255)
# ============================================================================
def init_logging():
main_logger = logging.getLogger()
formatter = logging.Formatter(
fmt='%(asctime)s.%(msecs)03d %(levelname)-8s [%(name)s] %(message)s'
, datefmt='%Y-%m-%d %H:%M:%S')
handler_stream = logging.StreamHandler(sys.stdout)
handler_stream.setFormatter(formatter)
main_logger.addHandler(handler_stream)
if LOG_TO_FILE:
handler_file = logging.handlers.RotatingFileHandler("debug.log"
, maxBytes = 2**24
, backupCount = 10)
handler_file.setFormatter(formatter)
main_logger.addHandler(handler_file)
main_logger.setLevel(logging.DEBUG)
return main_logger
# ============================================================================
def save_frame(file_name_format, frame_number, frame, label_format):
file_name = file_name_format % frame_number
label = label_format % frame_number
log.debug("Saving %s as '%s'", label, file_name)
cv2.imwrite(file_name, frame)
# ============================================================================
def get_centroid(x, y, w, h):
x1 = int(w / 2)
y1 = int(h / 2)
cx = x + x1
cy = y + y1
return (cx, cy)
# ============================================================================
def detect_vehicles(fg_mask):
log = logging.getLogger("detect_vehicles")
MIN_CONTOUR_WIDTH = 21
MIN_CONTOUR_HEIGHT = 21
# Find the contours of any vehicles in the image
contours, hierarchy = cv2.findContours(fg_mask
, cv2.RETR_EXTERNAL
, cv2.CHAIN_APPROX_SIMPLE)
log.debug("Found %d vehicle contours.", len(contours))
matches = []
for (i, contour) in enumerate(contours):
(x, y, w, h) = cv2.boundingRect(contour)
contour_valid = (w >= MIN_CONTOUR_WIDTH) and (h >= MIN_CONTOUR_HEIGHT)
log.debug("Contour #%d: pos=(x=%d, y=%d) size=(w=%d, h=%d) valid=%s"
, i, x, y, w, h, contour_valid)
if not contour_valid:
continue
centroid = get_centroid(x, y, w, h)
matches.append(((x, y, w, h), centroid))
return matches
# ============================================================================
def filter_mask(fg_mask):
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
# Fill any small holes
closing = cv2.morphologyEx(fg_mask, cv2.MORPH_CLOSE, kernel)
# Remove noise
opening = cv2.morphologyEx(closing, cv2.MORPH_OPEN, kernel)
# Dilate to merge adjacent blobs
dilation = cv2.dilate(opening, kernel, iterations = 2)
return dilation
# ============================================================================
def process_frame(frame_number, frame, bg_subtractor, car_counter):
log = logging.getLogger("process_frame")
# Create a copy of source frame to draw into
processed = frame.copy()
# Draw dividing line -- we count cars as they cross this line.
cv2.line(processed, (0, car_counter.divider), (frame.shape[1], car_counter.divider), DIVIDER_COLOUR, 1)
# Remove the background
fg_mask = bg_subtractor.apply(frame, None, 0.01)
fg_mask = filter_mask(fg_mask)
save_frame(IMAGE_DIR + "/mask_%04d.png"
, frame_number, fg_mask, "foreground mask for frame #%d")
matches = detect_vehicles(fg_mask)
log.debug("Found %d valid vehicle contours.", len(matches))
for (i, match) in enumerate(matches):
contour, centroid = match
log.debug("Valid vehicle contour #%d: centroid=%s, bounding_box=%s", i, centroid, contour)
x, y, w, h = contour
# Mark the bounding box and the centroid on the processed frame
# NB: Fixed the off-by one in the bottom right corner
cv2.rectangle(processed, (x, y), (x + w - 1, y + h - 1), BOUNDING_BOX_COLOUR, 1)
cv2.circle(processed, centroid, 2, CENTROID_COLOUR, -1)
log.debug("Updating vehicle count...")
car_counter.update_count(matches, processed)
return processed
# ============================================================================
def main():
log = logging.getLogger("main")
log.debug("Creating background subtractor...")
bg_subtractor = cv2.BackgroundSubtractorMOG()
log.debug("Pre-training the background subtractor...")
default_bg = cv2.imread(IMAGE_FILENAME_FORMAT % 119)
bg_subtractor.apply(default_bg, None, 1.0)
car_counter = None # Will be created after first frame is captured
# Set up image source
log.debug("Initializing video capture device #%s...", IMAGE_SOURCE)
cap = cv2.VideoCapture(IMAGE_SOURCE)
frame_width = cap.get(cv2.cv.CV_CAP_PROP_FRAME_WIDTH)
frame_height = cap.get(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT)
log.debug("Video capture frame size=(w=%d, h=%d)", frame_width, frame_height)
log.debug("Starting capture loop...")
frame_number = -1
while True:
frame_number += 1
log.debug("Capturing frame #%d...", frame_number)
ret, frame = cap.read()
if not ret:
log.error("Frame capture failed, stopping...")
break
log.debug("Got frame #%d: shape=%s", frame_number, frame.shape)
if car_counter is None:
# We do this here, so that we can initialize with actual frame size
log.debug("Creating vehicle counter...")
car_counter = VehicleCounter(frame.shape[:2], frame.shape[0] / 2)
# Archive raw frames from video to disk for later inspection/testing
if CAPTURE_FROM_VIDEO:
save_frame(IMAGE_FILENAME_FORMAT
, frame_number, frame, "source frame #%d")
log.debug("Processing frame #%d...", frame_number)
processed = process_frame(frame_number, frame, bg_subtractor, car_counter)
save_frame(IMAGE_DIR + "/processed_%04d.png"
, frame_number, processed, "processed frame #%d")
cv2.imshow('Source Image', frame)
cv2.imshow('Processed Image', processed)
log.debug("Frame #%d processed.", frame_number)
c = cv2.waitKey(WAIT_TIME)
if c == 27:
log.debug("ESC detected, stopping...")
break
log.debug("Closing video capture device...")
cap.release()
cv2.destroyAllWindows()
log.debug("Done.")
# ============================================================================
if __name__ == "__main__":
log = init_logging()
if not os.path.exists(IMAGE_DIR):
log.debug("Creating image directory `%s`...", IMAGE_DIR)
os.makedirs(IMAGE_DIR)
main()
Ce script est responsable du traitement du flux d'images et de l'identification de tous les véhicules dans chaque cadre - je les appelle matches
dans le code.
Le comptage des véhicules détectés est délégué à la classe VehicleCounter
. La raison pour laquelle j'ai choisi d'en faire un cours deviendra évidente à mesure que nous progressons. Je n'ai pas implémenté votre algorithme de comptage de véhicules, car il ne fonctionnera pas pour des raisons qui deviendront à nouveau évidentes au fur et à mesure que nous creusons.
Fichier vehicle_counter.py
contient le code suivant:
import logging
# ============================================================================
class VehicleCounter(object):
def __init__(self, shape, divider):
self.log = logging.getLogger("vehicle_counter")
self.height, self.width = shape
self.divider = divider
self.vehicle_count = 0
def update_count(self, matches, output_image = None):
self.log.debug("Updating count using %d matches...", len(matches))
# ============================================================================
Enfin, j'ai écrit un script qui assemblera toutes les images générées, il est donc plus facile de les inspecter:
import cv2
import numpy as np
# ============================================================================
INPUT_WIDTH = 160
INPUT_HEIGHT = 120
OUTPUT_TILE_WIDTH = 10
OUTPUT_TILE_HEIGHT = 12
TILE_COUNT = OUTPUT_TILE_WIDTH * OUTPUT_TILE_HEIGHT
# ============================================================================
def stitch_images(input_format, output_filename):
output_shape = (INPUT_HEIGHT * OUTPUT_TILE_HEIGHT
, INPUT_WIDTH * OUTPUT_TILE_WIDTH
, 3)
output = np.zeros(output_shape, np.uint8)
for i in range(TILE_COUNT):
img = cv2.imread(input_format % i)
cv2.rectangle(img, (0, 0), (INPUT_WIDTH - 1, INPUT_HEIGHT - 1), (0, 0, 255), 1)
# Draw the frame number
cv2.putText(img, str(i), (2, 10)
, cv2.FONT_HERSHEY_PLAIN, 0.7, (255, 255, 255), 1)
x = i % OUTPUT_TILE_WIDTH * INPUT_WIDTH
y = i / OUTPUT_TILE_WIDTH * INPUT_HEIGHT
output[y:y+INPUT_HEIGHT, x:x+INPUT_WIDTH,:] = img
cv2.imwrite(output_filename, output)
# ============================================================================
stitch_images("images/frame_%04d.png", "stitched_frames.png")
stitch_images("images/mask_%04d.png", "stitched_masks.png")
stitch_images("images/processed_%04d.png", "stitched_processed.png")
Afin de résoudre ce problème, nous devons avoir une idée des résultats que nous attendons. Nous devrions également étiqueter toutes les voitures distinctes dans la vidéo, il est donc plus facile d'en parler.
Si nous exécutons notre script et assemblons les images, nous obtenons un certain nombre de fichiers utiles pour nous aider à analyser le problème:
En les inspectant, un certain nombre de problèmes deviennent évidents:
Notre vidéo est assez courte, seulement 120 images. Avec un taux d'apprentissage de 0.01
, il faudra une partie substantielle de la vidéo pour que le détecteur d'arrière-plan se stabilise.
Heureusement, la dernière image de la vidéo (numéro d'image 119) est complètement dépourvue de véhicules, et nous pouvons donc l'utiliser comme image de fond initiale. (D'autres options pour obtenir une image appropriée sont mentionnées dans les notes et commentaires.)
Pour utiliser cette image d'arrière-plan initiale, nous la chargeons simplement et apply
sur le soustracteur d'arrière-plan avec le facteur d'apprentissage 1.0
:
bg_subtractor = cv2.BackgroundSubtractorMOG()
default_bg = cv2.imread(IMAGE_FILENAME_FORMAT % 119)
bg_subtractor.apply(default_bg, None, 1.0)
Lorsque nous regardons la nouvelle mosaïque de masques nous pouvons voir que nous obtenons moins de bruit et que la détection des véhicules fonctionne mieux dans les premières images.
Une approche simple pour améliorer notre masque de premier plan consiste à appliquer quelques transformations morphologiques .
def filter_mask(fg_mask):
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
# Fill any small holes
closing = cv2.morphologyEx(fg_mask, cv2.MORPH_CLOSE, kernel)
# Remove noise
opening = cv2.morphologyEx(closing, cv2.MORPH_OPEN, kernel)
# Dilate to merge adjacent blobs
dilation = cv2.dilate(opening, kernel, iterations = 2)
return dilation
En inspectant les masques , trames traitées et les fichier journal générés avec le filtrage, nous pouvons voir que nous détectons maintenant les véhicules de manière plus fiable et avons atténué la problème de détection de différentes parties d'un véhicule en tant qu'objets distincts.
À ce stade, nous devons parcourir notre fichier journal et collecter toutes les coordonnées du centroïde pour chaque véhicule. Cela nous permettra de tracer et d'inspecter le chemin que chaque véhicule trace à travers l'image et de développer un algorithme pour le faire automatiquement. Pour rendre ce processus plus facile, nous pouvons créer un log réduit en récupérant les entrées pertinentes.
Les listes de coordonnées centroïdes:
traces = {
'A': [(112, 36), (112, 45), (112, 52), (112, 54), (112, 63), (111, 73), (111, 86), (111, 91), (111, 97), (110, 105)]
, 'B': [(119, 37), (120, 42), (121, 54), (121, 55), (123, 64), (124, 74), (125, 87), (127, 94), (125, 100), (126, 108)]
, 'C': [(93, 23), (91, 27), (89, 31), (87, 36), (85, 42), (82, 49), (79, 59), (74, 71), (70, 82), (62, 86), (61, 92), (55, 101)]
, 'D': [(118, 30), (124, 83), (125, 90), (116, 101), (122, 100)]
, 'E': [(77, 27), (75, 30), (73, 33), (70, 37), (67, 42), (63, 47), (59, 53), (55, 59), (49, 67), (43, 75), (36, 85), (27, 92), (24, 97), (20, 102)]
, 'F': [(119, 30), (120, 34), (120, 39), (122, 59), (123, 60), (124, 70), (125, 82), (127, 91), (126, 97), (128, 104)]
, 'G': [(88, 37), (87, 41), (85, 48), (82, 55), (79, 63), (76, 74), (72, 87), (67, 92), (65, 98), (60, 106)]
, 'H': [(124, 35), (123, 40), (125, 45), (127, 59), (126, 59), (128, 67), (130, 78), (132, 88), (134, 93), (135, 99), (135, 107)]
, 'I': [(98, 26), (97, 30), (96, 34), (94, 40), (92, 47), (90, 55), (87, 64), (84, 77), (79, 87), (74, 93), (73, 102)]
, 'J': [(123, 60), (125, 63), (125, 81), (127, 93), (126, 98), (125, 100)]
}
Traces individuelles de véhicules tracées en arrière-plan:
Image agrandie combinée de toutes les traces du véhicule:
Afin d'analyser le mouvement, nous devons travailler avec des vecteurs (c'est-à-dire la distance et la direction déplacées). Le diagramme suivant montre comment les angles correspondent au mouvement des véhicules dans l'image.
Nous pouvons utiliser la fonction suivante pour calculer le vecteur entre deux points:
def get_vector(a, b):
"""Calculate vector (distance, angle in degrees) from point a to point b.
Angle ranges from -180 to 180 degrees.
Vector with angle 0 points straight down on the image.
Values increase in clockwise direction.
"""
dx = float(b[0] - a[0])
dy = float(b[1] - a[1])
distance = math.sqrt(dx**2 + dy**2)
if dy > 0:
angle = math.degrees(math.atan(-dx/dy))
Elif dy == 0:
if dx < 0:
angle = 90.0
Elif dx > 0:
angle = -90.0
else:
angle = 0.0
else:
if dx < 0:
angle = 180 - math.degrees(math.atan(dx/dy))
Elif dx > 0:
angle = -180 - math.degrees(math.atan(dx/dy))
else:
angle = 180.0
return distance, angle
Une façon de rechercher des modèles qui pourraient être utilisés pour classer les mouvements comme valides/invalides consiste à créer un nuage de points (angle par rapport à la distance):
distance = -0.008 * angle**2 + 0.4 * angle + 25.0
distance = 10.0
Nous pouvons utiliser la fonction suivante pour catégoriser les vecteurs de mouvement:
def is_valid_vector(a):
distance, angle = a
threshold_distance = max(10.0, -0.008 * angle**2 + 0.4 * angle + 25.0)
return (distance <= threshold_distance)
NB: Il y a une valeur aberrante, qui se produit en raison de notre piste perdue du véhicule [~ # ~] d [~ # ~] dans les images 43. .48.
Nous utiliserons la classe Vehicle
pour stocker des informations sur chaque véhicule suivi:
La classe VehicleCounter
stockera une liste des véhicules actuellement suivis et gardera une trace du nombre total. Sur chaque cadre, nous utiliserons la liste des boîtes englobantes et les positions des véhicules identifiés (liste candidate) pour mettre à jour l'état de VehicleCounter
:
Vehicle
s: Vehicle
pour toutes les correspondances restantesNous pouvons réutiliser le script principal avec la version finale de vehicle_counter.py
, contenant l'implémentation de notre algorithme de comptage:
import logging
import math
import cv2
import numpy as np
# ============================================================================
CAR_COLOURS = [ (0,0,255), (0,106,255), (0,216,255), (0,255,182), (0,255,76)
, (144,255,0), (255,255,0), (255,148,0), (255,0,178), (220,0,255) ]
# ============================================================================
class Vehicle(object):
def __init__(self, id, position):
self.id = id
self.positions = [position]
self.frames_since_seen = 0
self.counted = False
@property
def last_position(self):
return self.positions[-1]
def add_position(self, new_position):
self.positions.append(new_position)
self.frames_since_seen = 0
def draw(self, output_image):
car_colour = CAR_COLOURS[self.id % len(CAR_COLOURS)]
for point in self.positions:
cv2.circle(output_image, point, 2, car_colour, -1)
cv2.polylines(output_image, [np.int32(self.positions)]
, False, car_colour, 1)
# ============================================================================
class VehicleCounter(object):
def __init__(self, shape, divider):
self.log = logging.getLogger("vehicle_counter")
self.height, self.width = shape
self.divider = divider
self.vehicles = []
self.next_vehicle_id = 0
self.vehicle_count = 0
self.max_unseen_frames = 7
@staticmethod
def get_vector(a, b):
"""Calculate vector (distance, angle in degrees) from point a to point b.
Angle ranges from -180 to 180 degrees.
Vector with angle 0 points straight down on the image.
Values increase in clockwise direction.
"""
dx = float(b[0] - a[0])
dy = float(b[1] - a[1])
distance = math.sqrt(dx**2 + dy**2)
if dy > 0:
angle = math.degrees(math.atan(-dx/dy))
Elif dy == 0:
if dx < 0:
angle = 90.0
Elif dx > 0:
angle = -90.0
else:
angle = 0.0
else:
if dx < 0:
angle = 180 - math.degrees(math.atan(dx/dy))
Elif dx > 0:
angle = -180 - math.degrees(math.atan(dx/dy))
else:
angle = 180.0
return distance, angle
@staticmethod
def is_valid_vector(a):
distance, angle = a
threshold_distance = max(10.0, -0.008 * angle**2 + 0.4 * angle + 25.0)
return (distance <= threshold_distance)
def update_vehicle(self, vehicle, matches):
# Find if any of the matches fits this vehicle
for i, match in enumerate(matches):
contour, centroid = match
vector = self.get_vector(vehicle.last_position, centroid)
if self.is_valid_vector(vector):
vehicle.add_position(centroid)
self.log.debug("Added match (%d, %d) to vehicle #%d. vector=(%0.2f,%0.2f)"
, centroid[0], centroid[1], vehicle.id, vector[0], vector[1])
return i
# No matches fit...
vehicle.frames_since_seen += 1
self.log.debug("No match for vehicle #%d. frames_since_seen=%d"
, vehicle.id, vehicle.frames_since_seen)
return None
def update_count(self, matches, output_image = None):
self.log.debug("Updating count using %d matches...", len(matches))
# First update all the existing vehicles
for vehicle in self.vehicles:
i = self.update_vehicle(vehicle, matches)
if i is not None:
del matches[i]
# Add new vehicles based on the remaining matches
for match in matches:
contour, centroid = match
new_vehicle = Vehicle(self.next_vehicle_id, centroid)
self.next_vehicle_id += 1
self.vehicles.append(new_vehicle)
self.log.debug("Created new vehicle #%d from match (%d, %d)."
, new_vehicle.id, centroid[0], centroid[1])
# Count any uncounted vehicles that are past the divider
for vehicle in self.vehicles:
if not vehicle.counted and (vehicle.last_position[1] > self.divider):
self.vehicle_count += 1
vehicle.counted = True
self.log.debug("Counted vehicle #%d (total count=%d)."
, vehicle.id, self.vehicle_count)
# Optionally draw the vehicles on an image
if output_image is not None:
for vehicle in self.vehicles:
vehicle.draw(output_image)
cv2.putText(output_image, ("%02d" % self.vehicle_count), (142, 10)
, cv2.FONT_HERSHEY_PLAIN, 0.7, (127, 255, 255), 1)
# Remove vehicles that have not been seen long enough
removed = [ v.id for v in self.vehicles
if v.frames_since_seen >= self.max_unseen_frames ]
self.vehicles[:] = [ v for v in self.vehicles
if not v.frames_since_seen >= self.max_unseen_frames ]
for id in removed:
self.log.debug("Removed vehicle #%d.", id)
self.log.debug("Count updated, tracking %d vehicles.", len(self.vehicles))
# ============================================================================
Le programme dessine maintenant les chemins historiques de tous les véhicules actuellement suivis dans l'image de sortie, ainsi que le nombre de véhicules. Chaque véhicule se voit attribuer 1 des 10 couleurs.
Notez que le véhicule D finit par être suivi deux fois, mais il n'est compté qu'une seule fois, car nous le perdons de vue avant de traverser le séparateur. Des idées sur la façon de résoudre ce problème sont mentionnées dans l'annexe.
Basé sur la dernière image traitée générée par le script
le nombre total de véhicules est 10 . C'est un résultat correct.
Plus de détails peuvent être trouvés dans la sortie générée par le script:
cv2.drawContours
avec CV_FILLED
?BackgroundSubtractorMOG
dans Python (au moins dans OpenCV 2.4.x), mais il existe un moyen de faites-le avec un peu de travail.