diff --git a/README.md b/README.md
index 06d8407..7b03b1d 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# People-Counting-in-Real-Time
People Counting in Real-Time using live video stream/IP camera in OpenCV.
-> This repo is an improvement/modification to https://www.pyimagesearch.com/2018/08/13/opencv-people-counter/
+> This is an improvement/modification to https://www.pyimagesearch.com/2018/08/13/opencv-people-counter/
> Refer to added [Features](#features). Also, added support for an IP camera.
@@ -13,6 +13,7 @@ People Counting in Real-Time using live video stream/IP camera in OpenCV.
- The primary aim is to use the project as a business perspective, ready to scale.
- Use case: counting the number of people in the stores/buildings/shopping malls etc., in real-time.
- Sending an alert to the staff if the people are way over the limit.
+- Automating features and optimising the real-time stream for better performance.
- Acts as a measure towards footfall analysis and in a way to tackle COVID-19.
---
@@ -48,12 +49,11 @@ pip install -r requirements.txt
python run.py --prototxt mobilenet_ssd/MobileNetSSD_deploy.prototxt --model mobilenet_ssd/MobileNetSSD_deploy.caffemodel --input videos/example_01.mp4
```
> To run inference on an IP camera:
-- Setup your camera url in 'run.py':
+- Setup your camera url in 'mylib/config.py':
+
```
-# the following is an ip camera url example
-# just enter your camera url and it should work
-url = 'http://191.138.0.100:8040/video'
-vs = VideoStream(url).start()
+# Enter the ip camera url (e.g., url = 'http://191.138.0.100:8040/video')
+url = ''
```
- Then run with the command:
```
@@ -61,26 +61,24 @@ python run.py --prototxt mobilenet_ssd/MobileNetSSD_deploy.prototxt --model mobi
```
## Features
-The following are some of the added features. Note: You can easily on/off them in the config. options (mylib>config.py):
+The following are the added features. Note: You can easily on/off them in the config. options (mylib/config.py):
-
+
***1. Real-Time alert:***
- If selected, we send an email alert in real-time. Use case: If the total number of people (say 30) exceeded in a store/building, we simply alert the staff.
- This is pretty useful considering the COVID-19 scenario.
-
+
-- Note: To setup the sender email, please refer the instructions inside 'mylib/mailer.py'. Setup receiver email at the start of 'run.py'.
+- Note: To setup the sender email, please refer the instructions inside 'mylib/mailer.py'. Setup receiver email in the config.
***2. Threading:***
-- Multi-Threading is implemented in 'Thread.py'. If you ever see a lag/delay in your real-time stream, consider running it.
-- Threaing removes OpenCV's internal buffer (which stores the frames yet to be processed) and thus reduces the lag.
-- It is most preferred for complex real-time applications. Use the command:
+- Multi-Threading is implemented in 'mylib/thread.py'. If you ever see a lag/delay in your real-time stream, consider using it.
+- Threaing removes OpenCV's internal buffer (which stores the frames yet to be processed) and thus reduces the lag/increases fps.
+- It is most suitable for solid performance on complex real-time applications. To use threading:
-```
-python thread.py --prototxt mobilenet_ssd/MobileNetSSD_deploy.prototxt --model mobilenet_ssd/MobileNetSSD_deploy.caffemodel
-```
+``` set Thread = True in config. ```
***3. Scheduler:***
@@ -106,7 +104,7 @@ if Timer:
break
```
-***4. Simple log:***
+***5. Simple log:***
- Logs all data at end of the day.
- Useful for footfall analysis.
diff --git a/Run.py b/Run.py
index 98dbfef..11ccc4a 100644
--- a/Run.py
+++ b/Run.py
@@ -3,7 +3,7 @@ from mylib.trackableobject import TrackableObject
from imutils.video import VideoStream
from imutils.video import FPS
from mylib.mailer import Mailer
-from mylib import config
+from mylib import config, thread
import time, schedule, csv
import numpy as np
import argparse, imutils
@@ -12,7 +12,6 @@ from itertools import zip_longest
t0 = time.time()
-
def run():
# construct the argument parse and parse the arguments
@@ -45,14 +44,12 @@ def run():
# if a video path was not supplied, grab a reference to the ip camera
if not args.get("input", False):
print("[INFO] Starting the live stream..")
- # the following is an ip camera url example
- # just enter your camera url and it should work
- url = 'http://191.138.0.100:8040/video'
- vs = VideoStream(url).start()
+ vs = VideoStream(config.url).start()
time.sleep(2.0)
# otherwise, grab a reference to the video file
else:
+ print("[INFO] Starting the video..")
vs = cv2.VideoCapture(args["input"])
# initialize the video writer (we'll instantiate later if need be)
@@ -82,6 +79,9 @@ def run():
# start the frames per second throughput estimator
fps = FPS().start()
+ if config.Thread:
+ vs = thread.ThreadingClass(config.url)
+
# loop over frames from the video stream
while True:
# grab the next frame and handle if we are reading from either
@@ -97,7 +97,7 @@ def run():
# resize the frame to have a maximum width of 500 pixels (the
# less data we have, the faster we can process it), then convert
# the frame from BGR to RGB for dlib
- frame = imutils.resize(frame, width=500)
+ frame = imutils.resize(frame, width = 500)
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# if the frame dimensions are empty, set them
@@ -319,13 +319,13 @@ def run():
print("[INFO] approx. FPS: {:.2f}".format(fps.fps()))
- # if we are not using a video file, stop the camera video stream
- if not args.get("input", False):
- vs.stop()
-
- # otherwise, release the video file pointer
- else:
- vs.release()
+ # # if we are not using a video file, stop the camera video stream
+ # if not args.get("input", False):
+ # vs.stop()
+ #
+ # # otherwise, release the video file pointer
+ # else:
+ # vs.release()
# close any open windows
cv2.destroyAllWindows()
diff --git a/Thread.py b/Thread.py
deleted file mode 100644
index 0f7dfea..0000000
--- a/Thread.py
+++ /dev/null
@@ -1,339 +0,0 @@
-from mylib.centroidtracker import CentroidTracker
-from mylib.trackableobject import TrackableObject
-from imutils.video import VideoStream
-from imutils.video import FPS
-from mylib.mailer import Mailer
-from mylib import config
-import time, schedule, csv
-import numpy as np
-import argparse, imutils, queue, threading
-import time, dlib, cv2, datetime
-from itertools import zip_longest
-
-t0 = time.time()
-
-# construct the argument parse and parse the arguments
-ap = argparse.ArgumentParser()
-ap.add_argument("-p", "--prototxt", required=False,
- help="path to Caffe 'deploy' prototxt file")
-ap.add_argument("-m", "--model", required=True,
- help="path to Caffe pre-trained model")
-ap.add_argument("-i", "--input", type=str,
- help="path to optional input video file")
-ap.add_argument("-o", "--output", type=str,
- help="path to optional output video file")
-# confidence default 0.4
-ap.add_argument("-c", "--confidence", type=float, default=0.4,
- help="minimum probability to filter weak detections")
-ap.add_argument("-s", "--skip-frames", type=int, default=30,
- help="# of skip frames between detections")
-args = vars(ap.parse_args())
-
-# initialize the list of class labels MobileNet SSD was trained to
-# detect
-CLASSES = ["background", "aeroplane", "bicycle", "bird", "boat",
- "bottle", "bus", "car", "cat", "chair", "cow", "diningtable",
- "dog", "horse", "motorbike", "person", "pottedplant", "sheep",
- "sofa", "train", "tvmonitor"]
-
-# load our serialized model from disk
-net = cv2.dnn.readNetFromCaffe(args["prototxt"], args["model"])
-
-
-class VideoCapture:
- # initiate threading
- def __init__(self, name):
- self.cap = cv2.VideoCapture(name)
- self.q = queue.Queue()
- t = threading.Thread(target=self._reader)
- t.daemon = True
- t.start()
-
- # read frames as soon as they are available
- # this approach removes OpenCV's internal buffer and reduces the frame lag
- def _reader(self):
- while True:
- ret, frame = self.cap.read()
- if not ret:
- break
- if not self.q.empty():
- try:
- self.q.get_nowait()
- except queue.Empty:
- pass
- self.q.put(frame)
-
- def read(self):
- return self.q.get()
-
-
-# initialize the video writer (we'll instantiate later if need be)
-writer = None
-
-# initialize the frame dimensions (we'll set them as soon as we read
-# the first frame from the video)
-W = None
-H = None
-
-# instantiate our centroid tracker, then initialize a list to store
-# each of our dlib correlation trackers, followed by a dictionary to
-# map each unique object ID to a TrackableObject
-ct = CentroidTracker(maxDisappeared=40, maxDistance=50)
-trackers = []
-trackableObjects = {}
-
-# initialize the total number of frames processed thus far, along
-# with the total number of objects that have moved either up or down
-totalFrames = 0
-totalDown = 0
-totalUp = 0
-x = []
-empty=[]
-empty1=[]
-
-# start the frames per second throughput estimator
-fps = FPS().start()
-
-print("[INFO] Starting the live stream..")
-url = 'http://192.134.0.102:8290/video'
-cap = VideoCapture(url)
-
-# loop over frames from the video stream
-while True:
- # grab the next frame and handle if we are reading from either
- # VideoCapture or VideoStream
- frame = cap.read()
- frame = frame[1] if args.get("input", False) else frame
-
- # if we are viewing a video and we did not grab a frame then we
- # have reached the end of the video
- if args["input"] is not None and frame is None:
- break
-
- # resize the frame to have a maximum width of 500 pixels (the
- # less data we have, the faster we can process it), then convert
- # the frame from BGR to RGB for dlib
- frame = imutils.resize(frame, width=500)
- rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
-
- # if the frame dimensions are empty, set them
- if W is None or H is None:
- (H, W) = frame.shape[:2]
-
- # if we are supposed to be writing a video to disk, initialize
- # the writer
- if args["output"] is not None and writer is None:
- fourcc = cv2.VideoWriter_fourcc(*"MJPG")
- writer = cv2.VideoWriter(args["output"], fourcc, 30,
- (W, H), True)
-
- # initialize the current status along with our list of bounding
- # box rectangles returned by either (1) our object detector or
- # (2) the correlation trackers
- status = "Waiting"
- rects = []
-
- # check to see if we should run a more computationally expensive
- # object detection method to aid our tracker
- if totalFrames % args["skip_frames"] == 0:
- # set the status and initialize our new set of object trackers
- status = "Detecting"
- trackers = []
-
- # convert the frame to a blob and pass the blob through the
- # network and obtain the detections
- blob = cv2.dnn.blobFromImage(frame, 0.007843, (W, H), 127.5)
- net.setInput(blob)
- detections = net.forward()
-
- # loop over the detections
- for i in np.arange(0, detections.shape[2]):
- # extract the confidence (i.e., probability) associated
- # with the prediction
- confidence = detections[0, 0, i, 2]
-
- # filter out weak detections by requiring a minimum
- # confidence
- if confidence > args["confidence"]:
- # extract the index of the class label from the
- # detections list
- idx = int(detections[0, 0, i, 1])
-
- # if the class label is not a person, ignore it
- if CLASSES[idx] != "person":
- continue
-
- # compute the (x, y)-coordinates of the bounding box
- # for the object
- box = detections[0, 0, i, 3:7] * np.array([W, H, W, H])
- (startX, startY, endX, endY) = box.astype("int")
-
-
- # construct a dlib rectangle object from the bounding
- # box coordinates and then start the dlib correlation
- # tracker
- tracker = dlib.correlation_tracker()
- rect = dlib.rectangle(startX, startY, endX, endY)
- tracker.start_track(rgb, rect)
-
- # add the tracker to our list of trackers so we can
- # utilize it during skip frames
- trackers.append(tracker)
-
- # otherwise, we should utilize our object *trackers* rather than
- # object *detectors* to obtain a higher frame processing throughput
- else:
- # loop over the trackers
- for tracker in trackers:
- # set the status of our system to be 'tracking' rather
- # than 'waiting' or 'detecting'
- status = "Tracking"
-
- # update the tracker and grab the updated position
- tracker.update(rgb)
- pos = tracker.get_position()
-
- # unpack the position object
- startX = int(pos.left())
- startY = int(pos.top())
- endX = int(pos.right())
- endY = int(pos.bottom())
-
- # add the bounding box coordinates to the rectangles list
- rects.append((startX, startY, endX, endY))
-
- # draw a horizontal line in the center of the frame -- once an
- # object crosses this line we will determine whether they were
- # moving 'up' or 'down'
- cv2.line(frame, (0, H // 2), (W, H // 2), (0, 0, 0), 3)
- cv2.putText(frame, "-Prediction border - Entrance-", (10, H - ((i * 20) + 200)),
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
-
- # use the centroid tracker to associate the (1) old object
- # centroids with (2) the newly computed object centroids
- objects = ct.update(rects)
-
- # loop over the tracked objects
- for (objectID, centroid) in objects.items():
- # check to see if a trackable object exists for the current
- # object ID
- to = trackableObjects.get(objectID, None)
-
- # if there is no existing trackable object, create one
- if to is None:
- to = TrackableObject(objectID, centroid)
-
- # otherwise, there is a trackable object so we can utilize it
- # to determine direction
- else:
- # the difference between the y-coordinate of the *current*
- # centroid and the mean of *previous* centroids will tell
- # us in which direction the object is moving (negative for
- # 'up' and positive for 'down')
- y = [c[1] for c in to.centroids]
- direction = centroid[1] - np.mean(y)
- to.centroids.append(centroid)
-
- # check to see if the object has been counted or not
- if not to.counted:
- # if the direction is negative (indicating the object
- # is moving up) AND the centroid is above the center
- # line, count the object
- if direction < 0 and centroid[1] < H // 2:
- totalUp += 1
- empty.append(totalUp)
- to.counted = True
-
- # if the direction is positive (indicating the object
- # is moving down) AND the centroid is below the
- # center line, count the object
- elif direction > 0 and centroid[1] > H // 2:
- totalDown += 1
- empty1.append(totalDown)
- #print(empty1[-1])
- x = []
- # compute the sum of total people inside
- x.append(len(empty1)-len(empty))
- #print("Total people inside:", x)
- # Optimise number below: 10, 50, 100, etc., indicate the max. people inside limit
- # if the limit exceeds, send an email alert
- people_limit = 10
- if sum(x) == people_limit:
- if config.ALERT:
- print("[INFO] Sending email alert..")
- Mailer().send(config.MAIL)
- print("[INFO] Alert sent")
-
- to.counted = True
-
-
- # store the trackable object in our dictionary
- trackableObjects[objectID] = to
-
- # draw both the ID of the object and the centroid of the
- # object on the output frame
- text = "ID {}".format(objectID)
- cv2.putText(frame, text, (centroid[0] - 10, centroid[1] - 10),
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
- cv2.circle(frame, (centroid[0], centroid[1]), 4, (255, 255, 255), -1)
-
- # construct a tuple of information we will be displaying on the
- info = [
- ("Exit", totalUp),
- ("Enter", totalDown),
- ("Status", status),
- ]
-
- info2 = [
- ("Total people inside", x),
- ]
-
- # Display the output
- for (i, (k, v)) in enumerate(info):
- text = "{}: {}".format(k, v)
- cv2.putText(frame, text, (10, H - ((i * 20) + 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
-
- for (i, (k, v)) in enumerate(info2):
- text = "{}: {}".format(k, v)
- cv2.putText(frame, text, (265, H - ((i * 20) + 60)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
-
- # Initiate a simple log to save data at end of the day
- if config.Log:
- datetimee = [datetime.datetime.now()]
- d = [datetimee, empty1, empty, x]
- export_data = zip_longest(*d, fillvalue = '')
-
- with open('Log.csv', 'w', newline='') as myfile:
- wr = csv.writer(myfile, quoting=csv.QUOTE_ALL)
- wr.writerow(("End Time", "In", "Out", "Total Inside"))
- wr.writerows(export_data)
-
-
- # show the output frame
- cv2.imshow("Real-Time Monitoring/Analysis Window", frame)
- key = cv2.waitKey(1) & 0xFF
-
- # if the `q` key was pressed, break from the loop
- if key == ord("q"):
- break
-
- # increment the total number of frames processed thus far and
- # then update the FPS counter
- totalFrames += 1
- fps.update()
-
- if config.Timer:
- # Automatic timer to stop the live stream. Set to 8 hours (28800s).
- t1 = time.time()
- num_seconds=(t1-t0)
- if num_seconds > 28800:
- break
-
-# stop the timer and display FPS information
-fps.stop()
-print("[INFO] elapsed time: {:.2f}".format(fps.elapsed()))
-print("[INFO] approx. FPS: {:.2f}".format(fps.fps()))
-
-
-# close any open windows
-cv2.destroyAllWindows()
diff --git a/mylib/config.py b/mylib/config.py
index 771cbee..1add681 100644
--- a/mylib/config.py
+++ b/mylib/config.py
@@ -4,9 +4,13 @@
# Enter mail below to receive real-time email alerts
# e.g., 'email@gmail.com'
MAIL = ''
+# Enter the ip camera url (e.g., url = 'http://191.138.0.100:8040/video')
+url = ''
# ON/OFF for mail feature. Enter True to turn on the email alert feature.
ALERT = False
+# Threading ON/OFF
+Thread = False
# Simple log to log the counting data
Log = False
diff --git a/mylib/thread.py b/mylib/thread.py
new file mode 100644
index 0000000..cb3af34
--- /dev/null
+++ b/mylib/thread.py
@@ -0,0 +1,28 @@
+import cv2, threading, queue
+
+class ThreadingClass:
+ # initiate threading class
+ def __init__(self, name):
+ self.cap = cv2.VideoCapture(name)
+ # define an empty queue and thread
+ self.q = queue.Queue()
+ t = threading.Thread(target=self._reader)
+ t.daemon = True
+ t.start()
+
+ # read the frames as soon as they are available
+ # this approach removes OpenCV's internal buffer and reduces the frame lag
+ def _reader(self):
+ while True:
+ ret, frame = self.cap.read() # read the frames and ---
+ if not ret:
+ break
+ if not self.q.empty():
+ try:
+ self.q.get_nowait()
+ except queue.Empty:
+ pass
+ self.q.put(frame) # --- store them in a queue (instead of the buffer)
+
+ def read(self):
+ return self.q.get() # fetch frames from the queue one by one