Skip to content

Commit 73093f2

Browse files
Merge pull request #4 from barpavel/ui-fixes
Several improvements - UI, camera, code warnings, refactoring.
2 parents 1488541 + 2724dc5 commit 73093f2

File tree

4 files changed

+107
-64
lines changed

4 files changed

+107
-64
lines changed

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ Welcome to the Car Dashboard (HMI) prototype project! This simple yet intuitive
2424

2525
7. **Camera Streaming**
2626
- Access live camera feeds for improved awareness and safety.
27+
- Handles camera failures (camera is unavailable or gets disconnected during use).
2728

2829
8. **Prerecorded Video Streaming**
2930
- When camera is unavailable (i.e., during development or demos), you can play video instead.
31+
- Handles video file failures (file doesn't exist or video stream gets interrupted/corrupted).
3032

3133
## Development
3234

@@ -49,28 +51,28 @@ $ python3 -m venv .venv
4951

5052
Install the mandatory dependencies using the following command:
5153
```bash
52-
$ pip install -r requirements.txt
54+
(.venv) $ pip install -r requirements.txt
5355
```
5456

5557
## Execute
5658
Run the application:
5759
```bash
58-
$ python app.py
60+
(.venv) $ python app.py
5961
```
6062
or
6163
```bash
62-
$ python app.py --play-video /path/to/your/video.mp4
64+
(.venv) $ python app.py --play-video /path/to/your/video.mp4
6365
```
6466
Use `--help` to display the available options
6567
```console
66-
$ python app.py --help
68+
(.venv) $ python app.py --help
6769
usage: app.py [-h] [--play-video path]
6870
6971
Smart Car Dashboard GUI
7072
7173
options:
72-
-h, --help show this help message and exit
73-
--play-video path [Optional] path to video file to play instead of camera
74+
-h, --help show this help message and exit
75+
--play-video path [Optional] path to video file to play instead of camera
7476
```
7577

7678
## Screenshot
@@ -79,6 +81,7 @@ options:
7981
<img src = "ss/2.PNG">
8082
<img src = "ss/3.PNG">
8183
<img src = "ss/4.PNG">
84+
<img src = "ss/5.PNG">
8285

8386
## Todo
8487

app.py

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
# Developed By Sihab Sahariar
1+
__author__ = "Sihab Sahariar"
2+
__contact__ = "www.github.com/sihabsahariar"
3+
__credits__ = ["Pavel Bar"]
4+
__version__ = "1.0.1"
5+
26
import io
37
import sys
8+
import time
49
import argparse
510

611
# import OpenCV module
712
import cv2
813

9-
import folium # pip install folium
14+
import folium
1015

1116
# PyQt5 imports - Core
1217
from PyQt5.QtCore import QRect, QSize, QTimer, Qt, QCoreApplication, QMetaObject
1318
# PyQt5 imports - GUI
14-
from PyQt5.QtGui import QPixmap, QImage, QFont
19+
from PyQt5.QtGui import QPixmap, QImage, QFont, QPainter, QPen
1520
# PyQt5 imports - Widgets
1621
from PyQt5.QtWidgets import (
1722
QApplication, QWidget, QHBoxLayout, QLabel, QFrame, QPushButton,
@@ -27,21 +32,25 @@
2732

2833

2934
class Ui_MainWindow(object):
35+
# Main window dimensions constants
36+
WINDOW_WIDTH = 1117
37+
WINDOW_HEIGHT = 636
38+
3039
# Webcam widget dimensions constants
3140
WEBCAM_WIDTH = 321
3241
WEBCAM_HEIGHT = 331
33-
42+
3443
def __init__(self, video_path=None):
3544
self.video_path = video_path
36-
45+
3746
def setupUi(self, MainWindow):
3847
MainWindow.setObjectName("MainWindow")
39-
MainWindow.setFixedSize(1117, 636)
48+
MainWindow.setFixedSize(Ui_MainWindow.WINDOW_WIDTH, Ui_MainWindow.WINDOW_HEIGHT)
4049
MainWindow.setStyleSheet("background-color: rgb(30, 31, 40);")
4150
self.centralwidget = QWidget(MainWindow)
4251
self.centralwidget.setObjectName("centralwidget")
4352
self.label = QLabel(self.centralwidget)
44-
self.label.setGeometry(QRect(0, 0, 1111, 651))
53+
self.label.setGeometry(QRect(0, 0, Ui_MainWindow.WINDOW_WIDTH, Ui_MainWindow.WINDOW_HEIGHT))
4554
self.label.setText("")
4655
self.label.setPixmap(QPixmap(":/bg/Untitled (1).png"))
4756
self.label.setScaledContents(True)
@@ -671,7 +680,7 @@ def setupUi(self, MainWindow):
671680

672681
self.webcam = QLabel(self.frame_map)
673682
self.webcam.setObjectName(u"webcam")
674-
self.webcam.setGeometry(QRect(500, 40, self.WEBCAM_WIDTH, self.WEBCAM_HEIGHT))
683+
self.webcam.setGeometry(QRect(500, 40, Ui_MainWindow.WEBCAM_WIDTH, Ui_MainWindow.WEBCAM_HEIGHT))
675684

676685
MainWindow.setCentralWidget(self.centralwidget)
677686
self.show_dashboard()
@@ -689,36 +698,65 @@ def setupUi(self, MainWindow):
689698
)
690699
self.label_km.setAlignment(Qt.AlignCenter)
691700

692-
def _read_video_frame(self):
701+
def display_error_message(self, message):
702+
"""Display error message in the video area with proper styling."""
703+
# Create a QPixmap with the same dimensions as the webcam area
704+
error_pixmap = QPixmap(Ui_MainWindow.WEBCAM_WIDTH, Ui_MainWindow.WEBCAM_HEIGHT)
705+
error_pixmap.fill(Qt.black) # Black background to match the UI
706+
707+
# Draw the error message on the pixmap
708+
painter = QPainter(error_pixmap)
709+
painter.setPen(QPen(Qt.red, 2))
710+
painter.setFont(QFont("Arial", 12, QFont.Bold))
711+
712+
# Draw border
713+
painter.drawRect(2, 2, Ui_MainWindow.WEBCAM_WIDTH - 4, Ui_MainWindow.WEBCAM_HEIGHT - 4)
714+
715+
# Draw error message in center
716+
painter.setPen(QPen(Qt.white, 1))
717+
text_rect = error_pixmap.rect()
718+
text_rect.adjust(10, 0, -10, 0) # Add some margin
719+
painter.drawText(text_rect, Qt.AlignCenter | Qt.TextWordWrap, message)
720+
721+
painter.end()
722+
723+
# Set the error pixmap to the webcam label
724+
self.webcam.setPixmap(error_pixmap)
725+
726+
@staticmethod
727+
def _read_video_frame():
693728
"""Read and validate a video frame from the capture device.
694-
729+
695730
Returns:
696731
numpy.ndarray: Valid image frame, or None if no valid frame available
697732
"""
698733
ret, image = cap.read()
699-
734+
700735
# Validate frame
701736
if not ret or image is None or image.size == 0:
702737
return None
703-
738+
704739
return image
705740

706741
def view_video(self):
707-
image = self._read_video_frame()
742+
"""Displays camera / video stream and handles errors."""
743+
image = Ui_MainWindow._read_video_frame()
708744

709745
# Check if frame is valid
710746
if image is None:
711747
# Video ended or no frame available
712748
if self.video_path:
713749
# For video files, restart from beginning (loop)
714750
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
715-
image = self._read_video_frame()
751+
image = Ui_MainWindow._read_video_frame()
716752
if image is None:
717-
# If still no frame, stop the timer
753+
# If still no frame, show error and stop the timer
754+
self.display_error_message("Video file is unavailable or corrupted!\n\nPlease check video file.")
718755
self.quit_video()
719756
return
720757
else:
721-
# For camera, stop the timer
758+
# For camera, show error and stop the timer
759+
self.display_error_message("Camera is unavailable!\n\nPlease check camera connection.")
722760
self.quit_video()
723761
return
724762

@@ -729,8 +767,8 @@ def view_video(self):
729767
height, width, channel = image.shape
730768

731769
# Calculate scaling to fit within target area while maintaining aspect ratio
732-
scale_w = self.WEBCAM_WIDTH / width
733-
scale_h = self.WEBCAM_HEIGHT / height
770+
scale_w = Ui_MainWindow.WEBCAM_WIDTH / width
771+
scale_h = Ui_MainWindow.WEBCAM_HEIGHT / height
734772
scale = min(scale_w, scale_h) # Use smaller scale to fit entirely
735773

736774
# Calculate new dimensions
@@ -762,6 +800,8 @@ def controlTimer(self):
762800
cap = cv2.VideoCapture(self.video_path)
763801
else:
764802
cap = cv2.VideoCapture(0)
803+
# Give camera time to initialize for better robustness
804+
time.sleep(0.1)
765805
self.timer.start(20)
766806

767807
def retranslateUi(self, MainWindow):
@@ -885,10 +925,26 @@ def progress(self):
885925
parser = argparse.ArgumentParser(description='Smart Car Dashboard GUI')
886926
parser.add_argument('--play-video', metavar='path', type=str, help='[Optional] path to video file to play instead of camera')
887927
args = parser.parse_args()
888-
928+
929+
# Enable automatic high DPI scaling
930+
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
931+
# Enable crisp rendering on high DPI displays
932+
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
933+
# Disable window context help button
934+
QApplication.setAttribute(Qt.AA_DisableWindowContextHelpButton, True)
935+
889936
app = QApplication(sys.argv)
890-
MainWindow = QMainWindow()
937+
main_app_window = QMainWindow()
891938
ui = Ui_MainWindow(video_path=args.play_video)
892-
ui.setupUi(MainWindow)
893-
MainWindow.show()
939+
ui.setupUi(main_app_window)
940+
941+
# Center window on screen
942+
screen = app.primaryScreen()
943+
screen_geometry = screen.geometry()
944+
window_geometry = main_app_window.frameGeometry()
945+
center_point = screen_geometry.center()
946+
window_geometry.moveCenter(center_point)
947+
main_app_window.move(window_geometry.topLeft())
948+
949+
main_app_window.show()
894950
sys.exit(app.exec_())

gauge.py

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
# Sihab Sahariar (Fixed, 2023)
1+
__author__ = "Sihab Sahariar"
2+
__contact__ = "www.github.com/sihabsahariar"
3+
__credits__ = ["Pavel Bar"]
4+
__version__ = "1.0.1"
25

36
import math
47

@@ -14,11 +17,7 @@
1417

1518

1619
class AnalogGaugeWidget(QWidget):
17-
"""Fetches rows from a Bigtable.
18-
Args:
19-
none
20-
21-
"""
20+
"""Fetches rows from a Bigtable."""
2221
valueChanged = pyqtSignal(int)
2322

2423
def __init__(self, parent=None):
@@ -113,10 +112,7 @@ def __init__(self, parent=None):
113112
self.rescale_method()
114113

115114
def rescale_method(self):
116-
if self.width() <= self.height():
117-
self.widget_diameter = self.width()
118-
else:
119-
self.widget_diameter = self.height()
115+
self.widget_diameter = min(self.width(), self.height())
120116

121117
ypos = - int(self.widget_diameter / 2 * self.needle_scale_factor)
122118
self.change_value_needle_style([QPolygon([
@@ -140,9 +136,6 @@ def rescale_method(self):
140136
self.scale_fontsize = self.initial_scale_fontsize * self.widget_diameter // 400
141137
self.value_fontsize = self.initial_value_fontsize * self.widget_diameter // 400
142138

143-
def creator(self):
144-
print("Sihab Sahariar | www.github.com/sihabsahariar")
145-
146139
def change_value_needle_style(self, design):
147140
# prepared for multiple needle instrument
148141
self.value_needle = []
@@ -152,12 +145,8 @@ def change_value_needle_style(self, design):
152145
self.update()
153146

154147
def update_value(self, value):
155-
if value <= self.value_min:
156-
self.value = self.value_min
157-
elif value >= self.value_max:
158-
self.value = self.value_max
159-
else:
160-
self.value = value
148+
# Clamp value between min and max limits
149+
self.value = max(self.value_min, min(value, self.value_max))
161150
self.valueChanged.emit(int(value))
162151

163152
if not self.use_timer_event:
@@ -270,31 +259,26 @@ def set_enable_fine_scaled_marker(self, enable = True):
270259
self.update()
271260

272261
def set_scala_main_count(self, count):
273-
if count < 1:
274-
count = 1
275-
self.scala_main_count = count
262+
# Ensure count is at least 1
263+
self.scala_main_count = max(count, 1)
276264

277265
if not self.use_timer_event:
278266
self.update()
279267

280-
def set_MinValue(self, min):
281-
if self.value < min:
282-
self.value = min
283-
if min >= self.value_max:
284-
self.value_min = self.value_max - 1
285-
else:
286-
self.value_min = min
268+
def set_MinValue(self, new_value_min):
269+
# Ensure value is not below the new minimum
270+
self.value = max(self.value, new_value_min)
271+
# Update the minimum value, but ensure it stays below the current maximum
272+
self.value_min = min(new_value_min, self.value_max - 1)
287273

288274
if not self.use_timer_event:
289275
self.update()
290276

291-
def set_MaxValue(self, max):
292-
if self.value > max:
293-
self.value = max
294-
if max <= self.value_min:
295-
self.value_max = self.value_min + 1
296-
else:
297-
self.value_max = max
277+
def set_MaxValue(self, new_value_max):
278+
# Ensure value doesn't exceed the new maximum
279+
self.value = min(self.value, new_value_max)
280+
# Update the maximum value, but ensure it stays above the current minimum
281+
self.value_max = max(new_value_max, self.value_min + 1)
298282

299283
if not self.use_timer_event:
300284
self.update()

ss/5.PNG

175 KB
Loading

0 commit comments

Comments
 (0)