This is started as a simple project, because I don't like software I'm using. Not that they are not good, but I wanted some custom stuff, so this little project started. This part of project is for live viewing while archiving is done with opencv (not shown here, I managed to get this work).
I am very new to python, but I managed to build this GUI. My question for you is: Is there anything I can improve?
I am satisfied with CPU and memory usage (15 cameras in view are using about 100 MiB of memory). First approach is made using opencv, but with this I lower the resource used. There is known bug on line 174, when one image is shown, picture not fit the view until 'right-click' (fitInView
action) is called by user. So solution for this will be helpful.
from PyQt4 import QtCore, QtGui
import urllib
import thread
import math
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
try:
_encoding = QtGui.QApplication.UnicodeUTF8
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig)
class PhotoViewer(QtGui.QGraphicsView):
def __init__(self, parent):
super(PhotoViewer, self).__init__(parent)
self.added = False
self._zoom = 0
self._scene = QtGui.QGraphicsScene(self)
self._photo = QtGui.QGraphicsPixmapItem()
self._scene.addItem(self._photo)
self.setScene(self._scene)
self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(255, 255, 255)))
def fitInView(self):
rect = QtCore.QRectF(self._photo.pixmap().rect())
if not rect.isNull():
unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
self.scale(1 / unity.width(), 1 / unity.height())
viewrect = self.viewport().rect()
scenerect = self.transform().mapRect(rect)
factor = min(viewrect.width() / scenerect.width(),
viewrect.height() / scenerect.height())
self.scale(factor, factor)
self.centerOn(rect.center())
self._zoom = 0
return True
return False
def setPhoto(self, pixmap=None):
if pixmap and not pixmap.isNull():
self.setDragMode(QtGui.QGraphicsView.ScrollHandDrag)
self._photo.setPixmap(pixmap)
if self.added is False:
self.added = True
while self.fitInView():
break
else:
self.setDragMode(QtGui.QGraphicsView.NoDrag)
self._photo.setPixmap(QtGui.QPixmap())
def zoomFactor(self):
return self._zoom
def wheelEvent(self, event):
if not self._photo.pixmap().isNull():
if event.delta() > 0:
factor = 1.35
self._zoom += 1
else:
factor = 0.7
self._zoom -= 1
if self._zoom > 0:
self.scale(factor, factor)
pass
elif self._zoom == 0:
self.fitInView()
else:
self._zoom = 0
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.gridLayout = QtGui.QGridLayout(self)
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
self.tabWidget = QtGui.QTabWidget(self)
self.tabWidget.setObjectName(_fromUtf8("tabWidget"))
self.tab = QtGui.QWidget()
self.tab.setObjectName(_fromUtf8("tab"))
self.gridLayout_2 = QtGui.QGridLayout(self.tab)
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
self.gridLayout_2.setSpacing(0)
self.gridLayout_2.setMargin(0)
self.reset_button = QtGui.QPushButton('Test', self.tab)
self.reset_button.clicked.connect(self.back_event)
self.gridLayout_2.addWidget(self.reset_button, 0, 0, 1, 1)
self.tabWidget.addTab(self.tab, _fromUtf8(""))
self.tab_2 = QtGui.QWidget()
self.tab_2.setObjectName(_fromUtf8("tab_2"))
self.tabWidget.addTab(self.tab_2, _fromUtf8(""))
self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1)
self.retranslateUi()
self.tabWidget.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(self)
def setup_camera(self, cameras):
"""Initialize camera.
"""
self.grid_position_x = 0
self.grid_position_y = 0
self.number_of_cameras = len(cameras)
self.images = [{}] * self.number_of_cameras
self.lock = [0] * self.number_of_cameras
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.display_video_stream)
# Display video stream every 100 ms, limit to 10 fps (for testing),
# can be more
self.timer.start(100)
def display_video_stream(self):
for frame in self.images:
try:
img = QtGui.QPixmap()
img.loadFromData(frame['image'])
frame['element'].setPhoto(img)
except (AttributeError, KeyError):
continue
def create_image_view(self, url, cam_id):
viewer = PhotoViewer(self)
viewer.installEventFilter(self)
# Automate placing views inside grids. This creates NxN grids by using
# square root of number of cameras
if cam_id % int(math.sqrt(self.number_of_cameras)) == 0:
self.grid_position_x = 0
self.grid_position_y += 1
else:
self.grid_position_x += 1
self.gridLayout_2.addWidget(
viewer, self.grid_position_y, self.grid_position_x, 1, 1)
thread.start_new_thread(self.CaptureImages, (url, cam_id, viewer))
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
# On right click, zoom out image
if event.button() == QtCore.Qt.RightButton:
source.fitInView()
# On middle click, show only that view in fool size
if event.button() == 4:
for label in self.images:
if label['element'] != source:
label['element'].hide()
# lock the camera, so no processing power is used while
# image is locked and not viewed by user
self.lock[label['id']] = 1
else:
pass
source.fitInView() # does not fit the image inside view, must work on this
return super(Window, self).eventFilter(source, event)
def back_event(self):
"""
The first loop is here to show the image view
The second fits those views. If content in second loop is placed inside first,
results are not correct, because not all views are shown, and fitInView will place
them wrongly
"""
for num, i in enumerate(self.images):
self.lock[num] = 0
i['element'].show()
for i in self.images:
i['element'].fitInView()
def CaptureImages(self, url, cam_id, element):
self.images[cam_id] = {"image": None, "element": element, "id": cam_id}
try:
stream = urllib.urlopen(url)
except:
while True:
stream = urllib.urlopen(url)
if stream:
break
bytes = ''
while True:
try:
bytes += stream.read(1024)
a = bytes.find('\xff\xd8')
b = bytes.find('\xff\xd9')
# If different camera is opened in fullscreen, stop proccess
# image for this camera
if self.lock[cam_id]:
if b != -1:
bytes = bytes[b + 2:]
continue
if a != -1 and b != -1:
jpg = bytes[a:b + 2]
bytes = bytes[b + 2:]
self.images[cam_id] = {
"image": jpg, "element": element, "id": cam_id}
except:
pass
def retranslateUi(self):
self.setWindowTitle(_translate("MainWindow", "MainWindow", None))
self.tabWidget.setTabText(
self.tabWidget.indexOf(
self.tab), _translate(
"MainWindow", "Online Monitor", None))
self.tabWidget.setTabText(
self.tabWidget.indexOf(
self.tab_2), _translate(
"MainWindow", "Archive", None))
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.showMaximized()
# list of MJPEG url's
# publicly available cameras for testing
cameras = ["http://96.10.1.168/mjpg/video.mjpg",
"http://92.255.239.225:80/mjpg/video.mjpg?COUNTER",
"http://195.189.135.6:80/mjpg/video.mjpg?COUNTER",
"http://128.104.138.26:80/mjpg/video.mjpg?COUNTER",
"http://75.151.65.9:80/mjpg/video.mjpg?COUNTER",
"http://210.249.39.236:80/mjpg/video.mjpg?COUNTER",
"http://91.144.150.75:80/mjpg/video.mjpg?COUNTER",
"http://91.144.174.140:80/mjpg/video.mjpg?COUNTER",
"http://83.56.31.69:80/mjpg/video.mjpg?COUNTER",
]
window.setup_camera(cameras)
for camera_id, camera_url in enumerate(cameras):
window.create_image_view(camera_url, camera_id)
window.show()
sys.exit(app.exec_())
I will separate files properly later, just testing this for now.