Sanshruth commited on
Commit
b58c110
·
verified ·
1 Parent(s): 6e3fd3f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +170 -144
app.py CHANGED
@@ -1,26 +1,10 @@
1
- # Maximize CPU usage
2
- import multiprocessing
3
- import cv2
4
-
5
- # Get the number of CPU cores
6
- cpu_cores = multiprocessing.cpu_count()
7
-
8
- # Set OpenCV to use all available cores
9
- cv2.setNumThreads(cpu_cores)
10
-
11
- # Print the number of threads being used (optional)
12
- print(f"OpenCV using {cv2.getNumThreads()} threads out of {cpu_cores} available cores")
13
-
14
- ##############
15
  import cv2
16
  import gradio as gr
17
  import numpy as np
18
  from PIL import Image, ImageDraw
19
  from ultralytics import YOLO
20
- from ultralytics.utils.plotting import Annotator, colors
21
  import logging
22
  import math
23
- import torch
24
 
25
  # Set up logging
26
  logging.basicConfig(level=logging.INFO)
@@ -29,199 +13,241 @@ logger = logging.getLogger(__name__)
29
  # Global variables to store line coordinates and line equation
30
  start_point = None
31
  end_point = None
32
- line_params = None # Stores (start_point, end_point)
33
-
34
- # Load model once globally
35
- model = YOLO("yolo11n.pt")
36
- device = 'cuda' if torch.cuda.is_available() else 'cpu'
37
- model = model.to(device)
38
-
39
- def liang_barsky(line, bbox):
40
- """Optimized line-rectangle intersection check using Liang-Barsky algorithm"""
41
- x1, y1 = line[0]
42
- x2, y2 = line[1]
43
- xmin, ymin, xmax, ymax = bbox
44
-
45
- dx = x2 - x1
46
- dy = y2 - y1
47
- p = [-dx, dx, -dy, dy]
48
- q = [x1 - xmin, xmax - x1, y1 - ymin, ymax - y1]
49
- u1 = 0.0
50
- u2 = 1.0
51
-
52
- for i in range(4):
53
- if p[i] == 0:
54
- if q[i] < 0:
55
- return False
56
- continue
57
- t = q[i] / p[i]
58
- if p[i] < 0:
59
- if t > u1:
60
- u1 = t
61
- else:
62
- if t < u2:
63
- u2 = t
64
-
65
- return u1 <= u2
66
 
67
  def extract_first_frame(stream_url):
68
- """Extracts the first available frame from the IP camera stream"""
69
- logger.info("Extracting first frame...")
 
 
70
  cap = cv2.VideoCapture(stream_url)
71
  if not cap.isOpened():
 
72
  return None, "Error: Could not open stream."
73
 
74
  ret, frame = cap.read()
75
  cap.release()
76
 
77
  if not ret:
78
- return None, "Error: Could not read frame."
 
79
 
 
80
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
81
- return Image.fromarray(frame_rgb), "First frame extracted successfully."
 
 
 
82
 
83
  def update_line(image, evt: gr.SelectData):
84
- """Handles line drawing interactions"""
 
 
85
  global start_point, end_point, line_params
86
 
 
87
  if start_point is None:
88
  start_point = (evt.index[0], evt.index[1])
 
 
89
  draw = ImageDraw.Draw(image)
90
- draw.ellipse((start_point[0]-5, start_point[1]-5, start_point[0]+5, start_point[1]+5),
91
- fill="blue", outline="blue")
 
 
 
92
  return image, f"Line Coordinates:\nStart: {start_point}, End: None"
93
 
 
94
  end_point = (evt.index[0], evt.index[1])
95
- line_params = (start_point, end_point)
96
-
 
 
 
 
 
 
 
 
 
97
  draw = ImageDraw.Draw(image)
98
  draw.line([start_point, end_point], fill="red", width=2)
99
- draw.ellipse((end_point[0]-5, end_point[1]-5, end_point[0]+5, end_point[1]+5),
100
- fill="green", outline="green")
 
 
 
 
 
101
 
 
102
  start_point = None
103
- return image, f"Line Coordinates:\nStart: {line_params[0]}, End: {line_params[1]}"
 
 
104
 
105
  def reset_line():
106
- """Resets line coordinates"""
 
 
107
  global start_point, end_point, line_params
108
- start_point = end_point = line_params = None
 
 
109
  return None, "Line reset. Click to draw a new line."
110
 
111
  def is_object_crossing_line(box, line_params):
112
- """Optimized line crossing check using Liang-Barsky algorithm"""
113
- if not line_params:
114
- return False
115
-
116
- line_start, line_end = line_params
 
117
  x1, y1, x2, y2 = box
118
- return liang_barsky((line_start, line_end), (x1, y1, x2, y2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  def draw_angled_line(image, line_params, color=(0, 255, 0), thickness=2):
121
- """Draws the user-defined line on the frame"""
122
- start, end = line_params
123
- cv2.line(image, start, end, color, thickness)
 
 
124
 
125
  def process_video(confidence_threshold=0.5, selected_classes=None, stream_url=None):
126
- """Main video processing function with optimizations"""
 
 
127
  global line_params
 
128
  errors = []
129
 
130
- if not line_params:
131
- errors.append("Error: No line drawn.")
132
- if not selected_classes:
133
- errors.append("Error: No classes selected.")
134
- if not stream_url:
135
  errors.append("Error: No stream URL provided.")
 
136
  if errors:
137
  return None, "\n".join(errors)
138
 
139
- # Convert class names to indices once
140
- selected_class_indices = {i for i, name in model.names.items() if name in selected_classes}
141
-
142
  cap = cv2.VideoCapture(stream_url)
143
- cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Reduce buffer size
144
  if not cap.isOpened():
145
- return None, "Error: Could not open stream."
 
146
 
147
- crossed_objects = {}
148
- max_tracked_objects = 1000
149
 
 
150
  while cap.isOpened():
151
  ret, frame = cap.read()
152
  if not ret:
 
153
  break
154
 
155
- # Optimized inference
156
- results = model.track(
157
- frame,
158
- persist=True,
159
- conf=confidence_threshold,
160
- half=True,
161
- device=device,
162
- verbose=False
163
- )
164
 
165
- if results[0].boxes.id is not None:
166
- boxes = results[0].boxes
167
- track_ids = boxes.id.int().cpu().tolist()
168
- clss = boxes.cls.cpu().tolist()
169
 
170
- for box, cls, t_id in zip(boxes.xyxy.cpu(), clss, track_ids):
171
- if cls in selected_class_indices and t_id not in crossed_objects:
172
- if is_object_crossing_line(box.numpy(), line_params):
173
- crossed_objects[t_id] = True
174
- if len(crossed_objects) > max_tracked_objects:
175
- crossed_objects.clear()
 
176
 
177
- # Visualization
178
  annotated_frame = results[0].plot()
179
- draw_angled_line(annotated_frame, line_params)
180
-
181
- # Draw count
 
 
182
  count = len(crossed_objects)
183
- (w, h), _ = cv2.getTextSize(f"COUNT: {count}", cv2.FONT_HERSHEY_SIMPLEX, 1, 2)
184
- cv2.rectangle(annotated_frame, (10, 10), (20 + w, 40 + h), (0, 0, 0), -1)
185
- cv2.putText(annotated_frame, f"COUNT: {count}", (20, 40),
186
- cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
 
 
 
 
 
187
 
 
 
 
 
188
  yield annotated_frame, ""
189
 
190
  cap.release()
 
191
 
192
- # Gradio interface remains unchanged
193
  with gr.Blocks() as demo:
194
- gr.Markdown("<h1>Real-time monitoring, object tracking, and line-crossing detection for CCTV camera streams.</h1>")
195
  gr.Markdown("## https://github.com/SanshruthR/CCTV_SENTRY_YOLO11")
196
-
197
- stream_url = gr.Textbox(
198
- label="IP Camera Stream URL",
199
- value="https://s104.ipcamlive.com/streams/68idokwtondsqpmkr/stream.m3u8",
200
- visible=False
201
- )
202
-
203
- # First frame extraction
204
  first_frame, status = extract_first_frame(stream_url.value)
205
- image = gr.Image(value=first_frame, label="First Frame", type="pil") if first_frame else gr.Markdown(f"**Error:** {status}")
206
- line_info = gr.Textbox(label="Line Coordinates", value="Line Coordinates:\nStart: None, End: None")
207
- image.select(update_line, inputs=image, outputs=[image, line_info])
208
-
209
- # Class selection
210
- class_names = list(model.names.values())
211
- selected_classes = gr.CheckboxGroup(choices=class_names, label="Select Classes to Detect")
212
-
213
- # Confidence threshold
214
- confidence_threshold = gr.Slider(0.0, 1.0, value=0.2, label="Confidence Threshold")
215
-
216
- # Process button
217
- process_button = gr.Button("Process Stream")
218
- output_image = gr.Image(label="Processed Frame", streaming=True)
219
- error_box = gr.Textbox(label="Errors/Warnings", interactive=False)
220
-
221
- process_button.click(
222
- process_video,
223
- inputs=[confidence_threshold, selected_classes, stream_url],
224
- outputs=[output_image, error_box]
225
- )
 
 
 
 
 
 
 
 
 
226
 
 
227
  demo.launch(debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import cv2
2
  import gradio as gr
3
  import numpy as np
4
  from PIL import Image, ImageDraw
5
  from ultralytics import YOLO
 
6
  import logging
7
  import math
 
8
 
9
  # Set up logging
10
  logging.basicConfig(level=logging.INFO)
 
13
  # Global variables to store line coordinates and line equation
14
  start_point = None
15
  end_point = None
16
+ line_params = None # Stores (slope, intercept) of the line
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  def extract_first_frame(stream_url):
19
+ """
20
+ Extracts the first available frame from the IP camera stream and returns it as a PIL image.
21
+ """
22
+ logger.info("Attempting to extract the first frame from the stream...")
23
  cap = cv2.VideoCapture(stream_url)
24
  if not cap.isOpened():
25
+ logger.error("Error: Could not open stream.")
26
  return None, "Error: Could not open stream."
27
 
28
  ret, frame = cap.read()
29
  cap.release()
30
 
31
  if not ret:
32
+ logger.error("Error: Could not read the first frame.")
33
+ return None, "Error: Could not read the first frame."
34
 
35
+ # Convert the frame to a PIL image
36
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
37
+ pil_image = Image.fromarray(frame_rgb)
38
+
39
+ logger.info("First frame extracted successfully.")
40
+ return pil_image, "First frame extracted successfully."
41
 
42
  def update_line(image, evt: gr.SelectData):
43
+ """
44
+ Updates the line based on user interaction (click and drag).
45
+ """
46
  global start_point, end_point, line_params
47
 
48
+ # If it's the first click, set the start point and show it on the image
49
  if start_point is None:
50
  start_point = (evt.index[0], evt.index[1])
51
+
52
+ # Draw the start point on the image
53
  draw = ImageDraw.Draw(image)
54
+ draw.ellipse(
55
+ (start_point[0] - 5, start_point[1] - 5, start_point[0] + 5, start_point[1] + 5),
56
+ fill="blue", outline="blue"
57
+ )
58
+
59
  return image, f"Line Coordinates:\nStart: {start_point}, End: None"
60
 
61
+ # If it's the second click, set the end point and draw the line
62
  end_point = (evt.index[0], evt.index[1])
63
+
64
+ # Calculate the slope (m) and intercept (b) of the line: y = mx + b
65
+ if start_point[0] != end_point[0]: # Avoid division by zero
66
+ slope = (end_point[1] - start_point[1]) / (end_point[0] - start_point[0])
67
+ intercept = start_point[1] - slope * start_point[0]
68
+ line_params = (slope, intercept, start_point, end_point) # Store slope, intercept, and points
69
+ else:
70
+ # Vertical line (special case)
71
+ line_params = (float('inf'), start_point[0], start_point, end_point)
72
+
73
+ # Draw the line and end point on the image
74
  draw = ImageDraw.Draw(image)
75
  draw.line([start_point, end_point], fill="red", width=2)
76
+ draw.ellipse(
77
+ (end_point[0] - 5, end_point[1] - 5, end_point[0] + 5, end_point[1] + 5),
78
+ fill="green", outline="green"
79
+ )
80
+
81
+ # Return the updated image and line info
82
+ line_info = f"Line Coordinates:\nStart: {start_point}, End: {end_point}\nLine Equation: y = {line_params[0]:.2f}x + {line_params[1]:.2f}"
83
 
84
+ # Reset the points for the next interaction
85
  start_point = None
86
+ end_point = None
87
+
88
+ return image, line_info
89
 
90
  def reset_line():
91
+ """
92
+ Resets the line coordinates.
93
+ """
94
  global start_point, end_point, line_params
95
+ start_point = None
96
+ end_point = None
97
+ line_params = None
98
  return None, "Line reset. Click to draw a new line."
99
 
100
  def is_object_crossing_line(box, line_params):
101
+ """
102
+ Determines if an object's bounding box is fully intersected by the user-drawn line.
103
+ """
104
+ _, _, line_start, line_end = line_params
105
+
106
+ # Get the bounding box coordinates
107
  x1, y1, x2, y2 = box
108
+
109
+ # Define the four edges of the bounding box
110
+ box_edges = [
111
+ ((x1, y1), (x2, y1)), # Top edge
112
+ ((x2, y1), (x2, y2)), # Right edge
113
+ ((x2, y2), (x1, y2)), # Bottom edge
114
+ ((x1, y2), (x1, y1)) # Left edge
115
+ ]
116
+
117
+ # Count the number of intersections between the line and the bounding box edges
118
+ intersection_count = 0
119
+ for edge_start, edge_end in box_edges:
120
+ if intersect(line_start, line_end, edge_start, edge_end):
121
+ intersection_count += 1
122
+
123
+ # Only count the object if the line intersects the bounding box at least twice
124
+ return intersection_count >= 2
125
 
126
  def draw_angled_line(image, line_params, color=(0, 255, 0), thickness=2):
127
+ """
128
+ Draws the user-defined line on the frame.
129
+ """
130
+ _, _, start_point, end_point = line_params
131
+ cv2.line(image, start_point, end_point, color, thickness)
132
 
133
  def process_video(confidence_threshold=0.5, selected_classes=None, stream_url=None):
134
+ """
135
+ Processes the IP camera stream to count objects of the selected classes crossing the line.
136
+ """
137
  global line_params
138
+
139
  errors = []
140
 
141
+ if line_params is None:
142
+ errors.append("Error: No line drawn. Please draw a line on the first frame.")
143
+ if selected_classes is None or len(selected_classes) == 0:
144
+ errors.append("Error: No classes selected. Please select at least one class to detect.")
145
+ if stream_url is None or stream_url.strip() == "":
146
  errors.append("Error: No stream URL provided.")
147
+
148
  if errors:
149
  return None, "\n".join(errors)
150
 
151
+ logger.info("Connecting to the IP camera stream...")
 
 
152
  cap = cv2.VideoCapture(stream_url)
 
153
  if not cap.isOpened():
154
+ errors.append("Error: Could not open stream.")
155
+ return None, "\n".join(errors)
156
 
157
+ model = YOLO(model="yolov8n.pt")
158
+ crossed_objects = set() # Use a set to store unique object IDs (if available)
159
 
160
+ logger.info("Starting to process the stream...")
161
  while cap.isOpened():
162
  ret, frame = cap.read()
163
  if not ret:
164
+ errors.append("Error: Could not read frame from the stream.")
165
  break
166
 
167
+ # Perform object detection (no tracking)
168
+ results = model.predict(frame, conf=confidence_threshold)
 
 
 
 
 
 
 
169
 
170
+ for result in results:
171
+ boxes = result.boxes.xyxy.cpu().numpy()
172
+ clss = result.boxes.cls.cpu().numpy()
173
+ confs = result.boxes.conf.cpu().numpy()
174
 
175
+ for box, cls, conf in zip(boxes, clss, confs):
176
+ if conf >= confidence_threshold and model.names[int(cls)] in selected_classes:
177
+ # Check if the object crosses the line
178
+ if is_object_crossing_line(box, line_params):
179
+ # Use the bounding box center as a unique identifier
180
+ center = ((box[0] + box[2]) / 2, (box[1] + box[3]) / 2)
181
+ crossed_objects.add(tuple(center)) # Add the center to the set
182
 
183
+ # Visualize the results with bounding boxes
184
  annotated_frame = results[0].plot()
185
+
186
+ # Draw the angled line on the frame
187
+ draw_angled_line(annotated_frame, line_params, color=(0, 255, 0), thickness=2)
188
+
189
+ # Display the count on the frame
190
  count = len(crossed_objects)
191
+ (text_width, text_height), _ = cv2.getTextSize(f"COUNT: {count}", cv2.FONT_HERSHEY_SIMPLEX, 1, 2)
192
+
193
+ # Calculate the position for the middle of the top
194
+ margin = 10 # Margin from the top
195
+ x = (annotated_frame.shape[1] - text_width) // 2 # Center-align the text horizontally
196
+ y = text_height + margin # Top-align the text
197
+
198
+ # Draw the black background rectangle
199
+ cv2.rectangle(annotated_frame, (x - margin, y - text_height - margin), (x + text_width + margin, y + margin), (0, 0, 0), -1)
200
 
201
+ # Draw the text
202
+ cv2.putText(annotated_frame, f"COUNT: {count}", (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
203
+
204
+ # Yield the annotated frame to Gradio
205
  yield annotated_frame, ""
206
 
207
  cap.release()
208
+ logger.info("Stream processing completed.")
209
 
210
+ # Define the Gradio interface
211
  with gr.Blocks() as demo:
212
+ gr.Markdown("<h1>Real-time monitoring, object tracking, and line-crossing detection for CCTV camera streams.</h1></center>")
213
  gr.Markdown("## https://github.com/SanshruthR/CCTV_SENTRY_YOLO11")
214
+
215
+ # Step 1: Enter the IP Camera Stream URL
216
+ stream_url = gr.Textbox(label="Enter IP Camera Stream URL", value="https://s104.ipcamlive.com/streams/68idokwtondsqpmkr/stream.m3u8", visible=False)
217
+
218
+ # Step 1: Extract the first frame from the stream
219
+ gr.Markdown("### Step 1: Click on the frame to draw a line, the objects crossing it would be counted in real-time.")
 
 
220
  first_frame, status = extract_first_frame(stream_url.value)
221
+ if first_frame is None:
222
+ gr.Markdown(f"**Error:** {status}")
223
+ else:
224
+ # Image component for displaying the first frame
225
+ image = gr.Image(value=first_frame, label="First Frame of Stream", type="pil")
226
+
227
+ line_info = gr.Textbox(label="Line Coordinates", value="Line Coordinates:\nStart: None, End: None")
228
+ image.select(update_line, inputs=image, outputs=[image, line_info])
229
+
230
+ # Step 2: Select classes to detect
231
+ gr.Markdown("### Step 2: Select Classes to Detect")
232
+ model = YOLO(model="yolov8n.pt") # Load the model to get class names
233
+ class_names = list(model.names.values()) # Get class names
234
+ selected_classes = gr.CheckboxGroup(choices=class_names, label="Select Classes to Detect")
235
+
236
+ # Step 3: Adjust confidence threshold
237
+ gr.Markdown("### Step 3: Adjust Confidence Threshold (Optional)")
238
+ confidence_threshold = gr.Slider(minimum=0.0, maximum=1.0, value=0.2, label="Confidence Threshold")
239
+
240
+ # Process the stream
241
+ process_button = gr.Button("Process Stream")
242
+
243
+ # Output image for real-time frame rendering
244
+ output_image = gr.Image(label="Processed Frame", streaming=True)
245
+
246
+ # Error box to display warnings/errors
247
+ error_box = gr.Textbox(label="Errors/Warnings", interactive=False)
248
+
249
+ # Event listener for processing the video
250
+ process_button.click(process_video, inputs=[confidence_threshold, selected_classes, stream_url], outputs=[output_image, error_box])
251
 
252
+ # Launch the interface
253
  demo.launch(debug=True)