This commit is contained in:
rattatwinko
2025-05-25 20:42:58 +02:00
commit 8e9b2568b2
4902 changed files with 1152638 additions and 0 deletions

0
mucapy/__init__.py Normal file
View File

498
mucapy/main.py Normal file
View File

@@ -0,0 +1,498 @@
import os
import sys
import cv2
import numpy as np
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
QWidget, QLabel, QPushButton, QComboBox, QSpinBox,
QFileDialog, QMessageBox, QMenu, QAction, QMenuBar,
QActionGroup, QSizePolicy, QGridLayout, QGroupBox)
from PyQt5.QtCore import Qt, QTimer, QDir
from PyQt5.QtGui import QImage, QPixmap
class MultiCamYOLODetector:
def __init__(self):
self.cameras = []
self.net = None
self.classes = []
self.colors = []
self.target_fps = 10
self.last_frame_time = 0
self.frame_interval = 1.0 / self.target_fps
self.available_cameras = []
self.model_dir = ""
self.cuda_available = self.check_cuda()
def check_cuda(self):
"""Check if CUDA is available"""
try:
count = cv2.cuda.getCudaEnabledDeviceCount()
return count > 0
except:
return False
def scan_for_cameras(self, max_to_check=10):
"""Check for available cameras on Linux with better error handling"""
self.available_cameras = []
# Check standard video devices
for i in range(max_to_check):
try:
cap = cv2.VideoCapture(i, cv2.CAP_V4L2)
if cap.isOpened():
self.available_cameras.append(str(i))
cap.release()
except:
continue
# Also check direct device paths
v4l_paths = [f"/dev/video{i}" for i in range(max_to_check)]
v4l_paths += [f"/dev/v4l/video{i}" for i in range(max_to_check)]
for path in v4l_paths:
if os.path.exists(path):
try:
cap = cv2.VideoCapture(path, cv2.CAP_V4L2)
if cap.isOpened():
if path not in self.available_cameras:
self.available_cameras.append(path)
cap.release()
except:
continue
return self.available_cameras
def load_yolo_model(self, model_dir):
"""Load YOLO model from selected directory with better error handling"""
self.model_dir = model_dir
try:
# Find model files in the directory
weights = [f for f in os.listdir(model_dir) if f.endswith(('.weights', '.onnx'))]
configs = [f for f in os.listdir(model_dir) if f.endswith('.cfg')]
classes = [f for f in os.listdir(model_dir) if f.endswith('.names')]
if not weights or not configs or not classes:
return False
# Use the first found files
weights_path = os.path.join(model_dir, weights[0])
config_path = os.path.join(model_dir, configs[0])
classes_path = os.path.join(model_dir, classes[0])
self.net = cv2.dnn.readNet(weights_path, config_path)
# Set backend based on availability
if self.cuda_available:
try:
self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
except:
# Fall back to CPU if CUDA fails
self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
else:
self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
with open(classes_path, 'r') as f:
self.classes = f.read().strip().split('\n')
np.random.seed(42)
self.colors = np.random.randint(0, 255, size=(len(self.classes), 3), dtype='uint8')
return True
except Exception as e:
print(f"Error loading YOLO model: {e}")
return False
def connect_cameras(self, camera_paths):
"""Connect to multiple cameras with better error handling"""
self.disconnect_cameras()
for cam_path in camera_paths:
try:
if isinstance(cam_path, str) and cam_path.startswith('/dev/'):
cap = cv2.VideoCapture(cam_path, cv2.CAP_V4L2)
else:
cap = cv2.VideoCapture(int(cam_path), cv2.CAP_V4L2)
if not cap.isOpened():
print(f"Warning: Could not open camera {cam_path}")
continue
# Try to set properties but continue if they fail
try:
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
cap.set(cv2.CAP_PROP_FPS, self.target_fps)
cap.set(cv2.CAP_PROP_AUTOFOCUS, 0)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
except:
pass
self.cameras.append((cam_path, cap))
except Exception as e:
print(f"Error opening camera {cam_path}: {e}")
return len(self.cameras) > 0
def disconnect_cameras(self):
"""Disconnect all cameras"""
for _, cam in self.cameras:
try:
cam.release()
except:
pass
self.cameras = []
def get_detections(self, frame):
"""Perform YOLO object detection on a frame with error handling"""
if self.net is None:
return frame
try:
blob = cv2.dnn.blobFromImage(frame, 1/255.0, (416, 416), swapRB=True, crop=False)
self.net.setInput(blob)
# Get output layer names compatible with different OpenCV versions
try:
layer_names = self.net.getLayerNames()
output_layers = [layer_names[i - 1] for i in self.net.getUnconnectedOutLayers()]
except:
output_layers = self.net.getUnconnectedOutLayersNames()
outputs = self.net.forward(output_layers)
boxes = []
confidences = []
class_ids = []
for output in outputs:
for detection in output:
scores = detection[5:]
class_id = np.argmax(scores)
confidence = scores[class_id]
if confidence > 0.5:
box = detection[0:4] * np.array([frame.shape[1], frame.shape[0],
frame.shape[1], frame.shape[0]])
(centerX, centerY, width, height) = box.astype('int')
x = int(centerX - (width / 2))
y = int(centerY - (height / 2))
boxes.append([x, y, int(width), int(height)])
confidences.append(float(confidence))
class_ids.append(class_id)
indices = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
if len(indices) > 0:
for i in indices.flatten():
(x, y, w, h) = boxes[i]
color = [int(c) for c in self.colors[class_ids[i]]]
cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2)
text = f"{self.classes[class_ids[i]]}: {confidences[i]:.2f}"
cv2.putText(frame, text, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
except Exception as e:
print(f"Detection error: {e}")
return frame
def get_frames(self):
"""Get frames from all cameras with error handling"""
frames = []
for i, (cam_path, cam) in enumerate(self.cameras):
try:
ret, frame = cam.read()
if not ret:
print(f"Warning: Could not read frame from camera {cam_path}")
frame = np.zeros((720, 1280, 3), dtype=np.uint8)
else:
frame = self.get_detections(frame)
frames.append(frame)
except:
frame = np.zeros((720, 1280, 3), dtype=np.uint8)
frames.append(frame)
return frames
def get_frames(self):
"""Get frames from all cameras with detections"""
frames = []
for i, (cam_path, cam) in enumerate(self.cameras):
ret, frame = cam.read()
if not ret:
print(f"Warning: Could not read frame from camera {cam_path}")
frame = np.zeros((720, 1280, 3), dtype=np.uint8)
else:
frame = self.get_detections(frame)
frames.append(frame)
return frames
class CameraDisplay(QLabel):
"""Custom QLabel for displaying camera feed"""
def __init__(self, parent=None):
super().__init__(parent)
self.setAlignment(Qt.AlignCenter)
self.setText("No camera feed")
self.setStyleSheet("background-color: black; color: white;")
self.setMinimumSize(320, 240)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Multi-Camera YOLO Detection")
self.setGeometry(100, 100, 1200, 800)
self.detector = MultiCamYOLODetector()
self.camera_settings = {} # Store camera-specific settings
self.create_menus()
self.init_ui()
self.init_timer()
def create_menus(self):
"""Create the menu bar with model and camera menus"""
menubar = self.menuBar()
# Model menu
model_menu = menubar.addMenu('Model')
load_model_action = QAction('Load Model Directory...', self)
load_model_action.triggered.connect(self.load_model_directory)
model_menu.addAction(load_model_action)
# Camera menu
self.camera_menu = menubar.addMenu('Cameras')
self.refresh_cameras_action = QAction('Refresh Camera List', self)
self.refresh_cameras_action.triggered.connect(self.populate_camera_menu)
self.camera_menu.addAction(self.refresh_cameras_action)
self.camera_menu.addSeparator()
self.camera_action_group = QActionGroup(self)
self.camera_action_group.setExclusive(False)
self.populate_camera_menu()
def populate_camera_menu(self):
"""Populate the camera menu with available cameras"""
# Clear existing camera actions (except refresh)
for action in self.camera_menu.actions()[2:]:
self.camera_menu.removeAction(action)
available_cams = self.detector.scan_for_cameras()
for cam_path in available_cams:
# Display friendly name
if cam_path.startswith('/dev/'):
cam_name = os.path.basename(cam_path)
else:
cam_name = f"Camera {cam_path}"
action = QAction(cam_name, self, checkable=True)
action.setData(cam_path)
self.camera_action_group.addAction(action)
self.camera_menu.addAction(action)
if not available_cams:
no_cam_action = QAction('No cameras found', self)
no_cam_action.setEnabled(False)
self.camera_menu.addAction(no_cam_action)
def load_model_directory(self):
"""Open file dialog to select model directory"""
model_dir = QFileDialog.getExistingDirectory(
self,
"Select Model Directory",
QDir.homePath(),
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
)
if model_dir:
if self.detector.load_yolo_model(model_dir):
self.model_label.setText(f"Model: {os.path.basename(model_dir)}")
QMessageBox.information(self, "Success", "Model loaded successfully!")
else:
QMessageBox.critical(self, "Error", "Failed to load model from selected directory")
def init_ui(self):
"""Initialize the user interface"""
main_widget = QWidget()
main_layout = QVBoxLayout()
# Control panel
control_panel = QGroupBox("Controls")
control_layout = QHBoxLayout()
# Model info
self.model_label = QLabel("Model: Not loaded")
control_layout.addWidget(self.model_label)
# Selected cameras label
self.cameras_label = QLabel("Selected Cameras: None")
control_layout.addWidget(self.cameras_label)
# FPS control
self.fps_spin = QSpinBox()
self.fps_spin.setRange(1, 60)
self.fps_spin.setValue(10)
control_layout.addWidget(QLabel("FPS:"))
control_layout.addWidget(self.fps_spin)
# Camera layout selection
self.layout_combo = QComboBox()
self.layout_combo.addItems(["1 Camera", "2 Cameras", "3 Cameras", "4 Cameras", "Grid Layout"])
self.layout_combo.currentIndexChanged.connect(self.change_camera_layout)
control_layout.addWidget(QLabel("Layout:"))
control_layout.addWidget(self.layout_combo)
# Buttons
self.start_btn = QPushButton("Start Detection")
self.start_btn.clicked.connect(self.start_detection)
control_layout.addWidget(self.start_btn)
self.stop_btn = QPushButton("Stop Detection")
self.stop_btn.clicked.connect(self.stop_detection)
self.stop_btn.setEnabled(False)
control_layout.addWidget(self.stop_btn)
control_panel.setLayout(control_layout)
main_layout.addWidget(control_panel)
# Camera display area
self.display_area = QWidget()
self.display_layout = QGridLayout()
self.camera_displays = []
# Initially create 4 camera displays
for i in range(4):
display = CameraDisplay()
self.camera_displays.append(display)
self.display_layout.addWidget(display, i//2, i%2)
self.display_area.setLayout(self.display_layout)
main_layout.addWidget(self.display_area)
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
def change_camera_layout(self, index):
"""Change the camera display layout"""
# Clear the layout
for i in reversed(range(self.display_layout.count())):
self.display_layout.itemAt(i).widget().setParent(None)
num_cameras = index + 1 if index < 4 else 4
if index == 4: # Grid layout
rows = 2
cols = 2
for i in range(4):
self.display_layout.addWidget(self.camera_displays[i], i//cols, i%cols)
else:
if num_cameras == 1:
self.display_layout.addWidget(self.camera_displays[0], 0, 0, 1, 2)
elif num_cameras == 2:
self.display_layout.addWidget(self.camera_displays[0], 0, 0)
self.display_layout.addWidget(self.camera_displays[1], 0, 1)
elif num_cameras == 3:
self.display_layout.addWidget(self.camera_displays[0], 0, 0)
self.display_layout.addWidget(self.camera_displays[1], 0, 1)
self.display_layout.addWidget(self.camera_displays[2], 1, 0, 1, 2)
elif num_cameras == 4:
for i in range(4):
self.display_layout.addWidget(self.camera_displays[i], i//2, i%2)
# Hide unused displays
for i, display in enumerate(self.camera_displays):
display.setVisible(i < num_cameras)
def init_timer(self):
"""Initialize the timer for updating camera feeds"""
self.timer = QTimer()
self.timer.timeout.connect(self.update_feeds)
def start_detection(self):
"""Start the detection process"""
if not self.detector.model_dir:
QMessageBox.critical(self, "Error", "No model directory selected!")
return
# Get selected cameras
selected_cameras = []
for action in self.camera_action_group.actions():
if action.isChecked():
selected_cameras.append(action.data())
if not selected_cameras:
QMessageBox.critical(self, "Error", "No cameras selected!")
return
# Set FPS
self.detector.target_fps = self.fps_spin.value()
self.detector.frame_interval = 1.0 / self.detector.target_fps
# Connect to cameras
if not self.detector.connect_cameras(selected_cameras):
QMessageBox.critical(self, "Error", "Failed to connect to cameras!")
return
# Update UI
self.update_selection_labels()
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.fps_spin.setEnabled(False)
# Start timer
self.timer.start(int(1000 / self.detector.target_fps))
def stop_detection(self):
"""Stop the detection process"""
self.timer.stop()
self.detector.disconnect_cameras()
# Update UI
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.fps_spin.setEnabled(True)
# Clear displays
for display in self.camera_displays:
display.setText("No camera feed")
display.setStyleSheet("background-color: black; color: white;")
def update_selection_labels(self):
"""Update the model and camera selection labels"""
# Update cameras label
selected_cams = []
for action in self.camera_action_group.actions():
if action.isChecked():
selected_cams.append(action.text())
self.cameras_label.setText(f"Selected Cameras: {', '.join(selected_cams) or 'None'}")
def update_feeds(self):
"""Update the camera feeds in the display"""
frames = self.detector.get_frames()
for i, frame in enumerate(frames):
if i >= len(self.camera_displays):
break
# Convert frame to QImage
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape
bytes_per_line = ch * w
qt_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
# Scale while maintaining aspect ratio
pixmap = QPixmap.fromImage(qt_image)
display = self.camera_displays[i]
display.setPixmap(pixmap.scaled(display.width(), display.height(),
Qt.KeepAspectRatio, Qt.SmoothTransformation))
def closeEvent(self, event):
"""Handle window close event"""
self.stop_detection()
super().closeEvent(event)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

80
mucapy/models/coco.names Normal file
View File

@@ -0,0 +1,80 @@
person
bicycle
car
motorbike
aeroplane
bus
train
truck
boat
traffic light
fire hydrant
stop sign
parking meter
bench
bird
cat
dog
horse
sheep
cow
elephant
bear
zebra
giraffe
backpack
umbrella
handbag
tie
suitcase
frisbee
skis
snowboard
sports ball
kite
baseball bat
baseball glove
skateboard
surfboard
tennis racket
bottle
wine glass
cup
fork
knife
spoon
bowl
banana
apple
sandwich
orange
broccoli
carrot
hot dog
pizza
donut
cake
chair
sofa
pottedplant
bed
diningtable
toilet
tvmonitor
laptop
mouse
remote
keyboard
cell phone
microwave
oven
toaster
sink
refrigerator
book
clock
vase
scissors
teddy bear
hair drier
toothbrush

View File

@@ -0,0 +1,294 @@
[net]
# Testing
#batch=1
#subdivisions=1
# Training
batch=64
subdivisions=1
width=416
height=416
channels=3
momentum=0.9
decay=0.0005
angle=0
saturation = 1.5
exposure = 1.5
hue=.1
learning_rate=0.00261
burn_in=1000
max_batches = 2000200
policy=steps
steps=1600000,1800000
scales=.1,.1
#weights_reject_freq=1001
#ema_alpha=0.9998
#equidistant_point=1000
#num_sigmas_reject_badlabels=3
#badlabels_rejection_percentage=0.2
[convolutional]
batch_normalize=1
filters=32
size=3
stride=2
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=64
size=3
stride=2
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
[route]
layers=-1
groups=2
group_id=1
[convolutional]
batch_normalize=1
filters=32
size=3
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=32
size=3
stride=1
pad=1
activation=leaky
[route]
layers = -1,-2
[convolutional]
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=leaky
[route]
layers = -6,-1
[maxpool]
size=2
stride=2
[convolutional]
batch_normalize=1
filters=128
size=3
stride=1
pad=1
activation=leaky
[route]
layers=-1
groups=2
group_id=1
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
[route]
layers = -1,-2
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[route]
layers = -6,-1
[maxpool]
size=2
stride=2
[convolutional]
batch_normalize=1
filters=256
size=3
stride=1
pad=1
activation=leaky
[route]
layers=-1
groups=2
group_id=1
[convolutional]
batch_normalize=1
filters=128
size=3
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=128
size=3
stride=1
pad=1
activation=leaky
[route]
layers = -1,-2
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[route]
layers = -6,-1
[maxpool]
size=2
stride=2
[convolutional]
batch_normalize=1
filters=512
size=3
stride=1
pad=1
activation=leaky
##################################
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=3
stride=1
pad=1
activation=leaky
[convolutional]
size=1
stride=1
pad=1
filters=255
activation=linear
[yolo]
mask = 3,4,5
anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319
classes=80
num=6
jitter=.3
scale_x_y = 1.05
cls_normalizer=1.0
iou_normalizer=0.07
iou_loss=ciou
ignore_thresh = .7
truth_thresh = 1
random=0
resize=1.5
nms_kind=greedynms
beta_nms=0.6
#new_coords=1
#scale_x_y = 2.0
[route]
layers = -4
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[upsample]
stride=2
[route]
layers = -1, 23
[convolutional]
batch_normalize=1
filters=256
size=3
stride=1
pad=1
activation=leaky
[convolutional]
size=1
stride=1
pad=1
filters=255
activation=linear
[yolo]
mask = 1,2,3
anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319
classes=80
num=6
jitter=.3
scale_x_y = 1.05
cls_normalizer=1.0
iou_normalizer=0.07
iou_loss=ciou
ignore_thresh = .7
truth_thresh = 1
random=0
resize=1.5
nms_kind=greedynms
beta_nms=0.6
#new_coords=1
#scale_x_y = 2.0

Binary file not shown.