Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -79,6 +81,7 @@ options:
<img src = "ss/2.PNG">
<img src = "ss/3.PNG">
<img src = "ss/4.PNG">
<img src = "ss/5.PNG">

## Todo

Expand Down
100 changes: 78 additions & 22 deletions app.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -689,36 +698,65 @@ 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:
# Video ended or no frame available
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

Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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_())
56 changes: 20 additions & 36 deletions gauge.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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([
Expand All @@ -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 = []
Expand All @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
Binary file added ss/5.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.