diff --git a/README.md b/README.md index 712db9b..2059e68 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,11 @@ Welcome to the Car Dashboard (HMI) prototype project! This simple yet intuitive 7. **Camera Streaming** - Access live camera feeds for improved awareness and safety. + - Handles camera failures (camera is unavailable or gets disconnected during use). 8. **Prerecorded Video Streaming** - When camera is unavailable (i.e., during development or demos), you can play video instead. + - Handles video file failures (file doesn't exist or video stream gets interrupted/corrupted). ## Development @@ -49,28 +51,28 @@ $ python3 -m venv .venv Install the mandatory dependencies using the following command: ```bash -$ pip install -r requirements.txt +(.venv) $ pip install -r requirements.txt ``` ## Execute Run the application: ```bash -$ python app.py +(.venv) $ python app.py ``` or ```bash -$ python app.py --play-video /path/to/your/video.mp4 +(.venv) $ python app.py --play-video /path/to/your/video.mp4 ``` Use `--help` to display the available options ```console -$ python app.py --help +(.venv) $ python app.py --help usage: app.py [-h] [--play-video path] Smart Car Dashboard GUI options: - -h, --help show this help message and exit - --play-video path [Optional] path to video file to play instead of camera + -h, --help show this help message and exit + --play-video path [Optional] path to video file to play instead of camera ``` ## Screenshot @@ -79,6 +81,7 @@ options: + ## Todo diff --git a/app.py b/app.py index 7582fa9..979dfab 100644 --- a/app.py +++ b/app.py @@ -1,17 +1,22 @@ -# Developed By Sihab Sahariar +__author__ = "Sihab Sahariar" +__contact__ = "www.github.com/sihabsahariar" +__credits__ = ["Pavel Bar"] +__version__ = "1.0.1" + import io import sys +import time import argparse # import OpenCV module import cv2 -import folium # pip install folium +import folium # PyQt5 imports - Core from PyQt5.QtCore import QRect, QSize, QTimer, Qt, QCoreApplication, QMetaObject # PyQt5 imports - GUI -from PyQt5.QtGui import QPixmap, QImage, QFont +from PyQt5.QtGui import QPixmap, QImage, QFont, QPainter, QPen # PyQt5 imports - Widgets from PyQt5.QtWidgets import ( QApplication, QWidget, QHBoxLayout, QLabel, QFrame, QPushButton, @@ -27,21 +32,25 @@ class Ui_MainWindow(object): + # Main window dimensions constants + WINDOW_WIDTH = 1117 + WINDOW_HEIGHT = 636 + # Webcam widget dimensions constants WEBCAM_WIDTH = 321 WEBCAM_HEIGHT = 331 - + def __init__(self, video_path=None): self.video_path = video_path - + def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") - MainWindow.setFixedSize(1117, 636) + MainWindow.setFixedSize(Ui_MainWindow.WINDOW_WIDTH, Ui_MainWindow.WINDOW_HEIGHT) MainWindow.setStyleSheet("background-color: rgb(30, 31, 40);") self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.label = QLabel(self.centralwidget) - self.label.setGeometry(QRect(0, 0, 1111, 651)) + self.label.setGeometry(QRect(0, 0, Ui_MainWindow.WINDOW_WIDTH, Ui_MainWindow.WINDOW_HEIGHT)) self.label.setText("") self.label.setPixmap(QPixmap(":/bg/Untitled (1).png")) self.label.setScaledContents(True) @@ -671,7 +680,7 @@ def setupUi(self, MainWindow): self.webcam = QLabel(self.frame_map) self.webcam.setObjectName(u"webcam") - self.webcam.setGeometry(QRect(500, 40, self.WEBCAM_WIDTH, self.WEBCAM_HEIGHT)) + self.webcam.setGeometry(QRect(500, 40, Ui_MainWindow.WEBCAM_WIDTH, Ui_MainWindow.WEBCAM_HEIGHT)) MainWindow.setCentralWidget(self.centralwidget) self.show_dashboard() @@ -689,22 +698,49 @@ def setupUi(self, MainWindow): ) self.label_km.setAlignment(Qt.AlignCenter) - def _read_video_frame(self): + def display_error_message(self, message): + """Display error message in the video area with proper styling.""" + # Create a QPixmap with the same dimensions as the webcam area + error_pixmap = QPixmap(Ui_MainWindow.WEBCAM_WIDTH, Ui_MainWindow.WEBCAM_HEIGHT) + error_pixmap.fill(Qt.black) # Black background to match the UI + + # Draw the error message on the pixmap + painter = QPainter(error_pixmap) + painter.setPen(QPen(Qt.red, 2)) + painter.setFont(QFont("Arial", 12, QFont.Bold)) + + # Draw border + painter.drawRect(2, 2, Ui_MainWindow.WEBCAM_WIDTH - 4, Ui_MainWindow.WEBCAM_HEIGHT - 4) + + # Draw error message in center + painter.setPen(QPen(Qt.white, 1)) + text_rect = error_pixmap.rect() + text_rect.adjust(10, 0, -10, 0) # Add some margin + painter.drawText(text_rect, Qt.AlignCenter | Qt.TextWordWrap, message) + + painter.end() + + # Set the error pixmap to the webcam label + self.webcam.setPixmap(error_pixmap) + + @staticmethod + def _read_video_frame(): """Read and validate a video frame from the capture device. - + Returns: numpy.ndarray: Valid image frame, or None if no valid frame available """ ret, image = cap.read() - + # Validate frame if not ret or image is None or image.size == 0: return None - + return image def view_video(self): - image = self._read_video_frame() + """Displays camera / video stream and handles errors.""" + image = Ui_MainWindow._read_video_frame() # Check if frame is valid if image is None: @@ -712,13 +748,15 @@ def view_video(self): if self.video_path: # For video files, restart from beginning (loop) cap.set(cv2.CAP_PROP_POS_FRAMES, 0) - image = self._read_video_frame() + image = Ui_MainWindow._read_video_frame() if image is None: - # If still no frame, stop the timer + # If still no frame, show error and stop the timer + self.display_error_message("Video file is unavailable or corrupted!\n\nPlease check video file.") self.quit_video() return else: - # For camera, stop the timer + # For camera, show error and stop the timer + self.display_error_message("Camera is unavailable!\n\nPlease check camera connection.") self.quit_video() return @@ -729,8 +767,8 @@ def view_video(self): height, width, channel = image.shape # Calculate scaling to fit within target area while maintaining aspect ratio - scale_w = self.WEBCAM_WIDTH / width - scale_h = self.WEBCAM_HEIGHT / height + scale_w = Ui_MainWindow.WEBCAM_WIDTH / width + scale_h = Ui_MainWindow.WEBCAM_HEIGHT / height scale = min(scale_w, scale_h) # Use smaller scale to fit entirely # Calculate new dimensions @@ -762,6 +800,8 @@ def controlTimer(self): cap = cv2.VideoCapture(self.video_path) else: cap = cv2.VideoCapture(0) + # Give camera time to initialize for better robustness + time.sleep(0.1) self.timer.start(20) def retranslateUi(self, MainWindow): @@ -885,10 +925,26 @@ def progress(self): parser = argparse.ArgumentParser(description='Smart Car Dashboard GUI') parser.add_argument('--play-video', metavar='path', type=str, help='[Optional] path to video file to play instead of camera') args = parser.parse_args() - + + # Enable automatic high DPI scaling + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + # Enable crisp rendering on high DPI displays + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + # Disable window context help button + QApplication.setAttribute(Qt.AA_DisableWindowContextHelpButton, True) + app = QApplication(sys.argv) - MainWindow = QMainWindow() + main_app_window = QMainWindow() ui = Ui_MainWindow(video_path=args.play_video) - ui.setupUi(MainWindow) - MainWindow.show() + ui.setupUi(main_app_window) + + # Center window on screen + screen = app.primaryScreen() + screen_geometry = screen.geometry() + window_geometry = main_app_window.frameGeometry() + center_point = screen_geometry.center() + window_geometry.moveCenter(center_point) + main_app_window.move(window_geometry.topLeft()) + + main_app_window.show() sys.exit(app.exec_()) diff --git a/gauge.py b/gauge.py index ddac19e..9f63dcf 100644 --- a/gauge.py +++ b/gauge.py @@ -1,4 +1,7 @@ -# Sihab Sahariar (Fixed, 2023) +__author__ = "Sihab Sahariar" +__contact__ = "www.github.com/sihabsahariar" +__credits__ = ["Pavel Bar"] +__version__ = "1.0.1" import math @@ -14,11 +17,7 @@ class AnalogGaugeWidget(QWidget): - """Fetches rows from a Bigtable. - Args: - none - - """ + """Fetches rows from a Bigtable.""" valueChanged = pyqtSignal(int) def __init__(self, parent=None): @@ -113,10 +112,7 @@ def __init__(self, parent=None): self.rescale_method() def rescale_method(self): - if self.width() <= self.height(): - self.widget_diameter = self.width() - else: - self.widget_diameter = self.height() + self.widget_diameter = min(self.width(), self.height()) ypos = - int(self.widget_diameter / 2 * self.needle_scale_factor) self.change_value_needle_style([QPolygon([ @@ -140,9 +136,6 @@ def rescale_method(self): self.scale_fontsize = self.initial_scale_fontsize * self.widget_diameter // 400 self.value_fontsize = self.initial_value_fontsize * self.widget_diameter // 400 - def creator(self): - print("Sihab Sahariar | www.github.com/sihabsahariar") - def change_value_needle_style(self, design): # prepared for multiple needle instrument self.value_needle = [] @@ -152,12 +145,8 @@ def change_value_needle_style(self, design): self.update() def update_value(self, value): - if value <= self.value_min: - self.value = self.value_min - elif value >= self.value_max: - self.value = self.value_max - else: - self.value = value + # Clamp value between min and max limits + self.value = max(self.value_min, min(value, self.value_max)) self.valueChanged.emit(int(value)) if not self.use_timer_event: @@ -270,31 +259,26 @@ def set_enable_fine_scaled_marker(self, enable = True): self.update() def set_scala_main_count(self, count): - if count < 1: - count = 1 - self.scala_main_count = count + # Ensure count is at least 1 + self.scala_main_count = max(count, 1) if not self.use_timer_event: self.update() - def set_MinValue(self, min): - if self.value < min: - self.value = min - if min >= self.value_max: - self.value_min = self.value_max - 1 - else: - self.value_min = min + def set_MinValue(self, new_value_min): + # Ensure value is not below the new minimum + self.value = max(self.value, new_value_min) + # Update the minimum value, but ensure it stays below the current maximum + self.value_min = min(new_value_min, self.value_max - 1) if not self.use_timer_event: self.update() - def set_MaxValue(self, max): - if self.value > max: - self.value = max - if max <= self.value_min: - self.value_max = self.value_min + 1 - else: - self.value_max = max + def set_MaxValue(self, new_value_max): + # Ensure value doesn't exceed the new maximum + self.value = min(self.value, new_value_max) + # Update the maximum value, but ensure it stays above the current minimum + self.value_max = max(new_value_max, self.value_min + 1) if not self.use_timer_event: self.update() diff --git a/ss/5.PNG b/ss/5.PNG new file mode 100644 index 0000000..9cedfd7 Binary files /dev/null and b/ss/5.PNG differ