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