1
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.