Browse Source

Change echo image

AlexSidorov 1 year ago
parent
commit
a03c060d29

+ 2 - 2
module/CleanerImage.py

@@ -1,7 +1,7 @@
 from .WorkWithDB import connect, list_propusk
 from .WorkWithDB import connect, list_propusk
 from sqlalchemy import select
 from sqlalchemy import select
 from sqlalchemy.exc import OperationalError
 from sqlalchemy.exc import OperationalError
-from MyMessageBox import show_dialog
+from module.MyMessageBox import show_dialog
 from PySide6.QtWidgets import QMessageBox
 from PySide6.QtWidgets import QMessageBox
 import os, sys
 import os, sys
 from logger import logger
 from logger import logger
@@ -44,7 +44,7 @@ class CleanerImage:
     def clear(self) -> None:
     def clear(self) -> None:
         _, _, files = os.walk(self._path).__next__()
         _, _, files = os.walk(self._path).__next__()
         for file in files:
         for file in files:
-            if file not in self._files_in_db:
+            if file not in self._files_in_db and 'jpg' in file:
                 os.remove(os.path.join(self._path, file))
                 os.remove(os.path.join(self._path, file))
                 logger.info(F"Удален файл: {os.path.join(self._path, file)}")
                 logger.info(F"Удален файл: {os.path.join(self._path, file)}")
 
 

+ 17 - 2
module/ImageTool.py

@@ -1,8 +1,13 @@
-from PIL import Image, UnidentifiedImageError
 import os
 import os
-from logger import logger
 from datetime import datetime
 from datetime import datetime
+
 import cv2
 import cv2
+from PIL import Image, UnidentifiedImageError
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QImage, QPixmap
+from PySide6.QtWidgets import QLabel
+
+from logger import logger
 
 
 
 
 def rotate_image(path_file: str, gradus: int = -90) -> str:
 def rotate_image(path_file: str, gradus: int = -90) -> str:
@@ -39,3 +44,13 @@ def cupture_face(path_photo: str, new_path: str) -> None:
             cv2.imwrite(new_path, img)
             cv2.imwrite(new_path, img)
     except cv2.error as err:
     except cv2.error as err:
         logger.error(err)
         logger.error(err)
+
+
+def load_image(qlabel: QLabel, path_file: str) -> None:
+    logger.info(F"Set image to label: {path_file}")
+    qlabel.setPixmap(QPixmap(QImage(path_file)).scaled(
+        qlabel.width()-4,
+        qlabel.height(),
+        Qt.AspectRatioMode.KeepAspectRatio,
+        Qt.TransformationMode.FastTransformation
+    ))

+ 2 - 2
module/Printer.py

@@ -21,9 +21,9 @@ class Print:
 
 
         image = None
         image = None
         if system() in 'Windows':
         if system() in 'Windows':
-            image = convert_from_path(pdf, dpi=600, poppler_path=r'poppler\bin')[0].toqimage()
+            image = convert_from_path(pdf, dpi=300, poppler_path=r'poppler\bin')[0].toqimage()
         else: 
         else: 
-            image = convert_from_path(pdf, dpi=600)[0].toqimage()
+            image = convert_from_path(pdf, dpi=300)[0].toqimage()
 
 
         image = QPixmap.fromImage(image)
         image = QPixmap.fromImage(image)
         image = image.scaledToWidth(self.printer.width(), Qt.SmoothTransformation)
         image = image.scaledToWidth(self.printer.width(), Qt.SmoothTransformation)

+ 37 - 28
module/WorkWithDB.py

@@ -1,31 +1,13 @@
 from sqlalchemy import (MetaData, Table, Column, Integer,
 from sqlalchemy import (MetaData, Table, Column, Integer,
                         DateTime, String, Text, ForeignKey,
                         DateTime, String, Text, ForeignKey,
                         create_engine, func)
                         create_engine, func)
-from sqlalchemy.exc import ProgrammingError
+from sqlalchemy.exc import ProgrammingError, OperationalError
 import os
 import os
 from module.MyMessageBox import show_dialog
 from module.MyMessageBox import show_dialog
 from PySide6.QtWidgets import QMessageBox
 from PySide6.QtWidgets import QMessageBox
 from logger import logger
 from logger import logger
 
 
 meta = MetaData()
 meta = MetaData()
-
-# /home/asidorov/Документы/propusk_db
-
-FILE_NAME = None
-
-if os.environ.get("DB_DIR"):
-    FILE_NAME = os.path.join(os.environ.get("DB_DIR"), "propusk.db")
-else:
-    if os.environ.get("DEFAULT_PATH"):
-        FILE_NAME = os.path.join(os.environ.get("DEFAULT_PATH"), "propusk.db")
-    else:
-        logger.error("Не правильно указан путь к базе данных, или вообще отсутствует")
-        show_dialog(
-            QMessageBox.Icon.Critical,
-            "Путь к бд",
-            "Не правильно указан путь к базе данных, или вообще отсутствует"
-        )
-    
 cam_setting = Table("сam_setting", meta,
 cam_setting = Table("сam_setting", meta,
                     Column('id', Integer, primary_key=True),
                     Column('id', Integer, primary_key=True),
                     Column('type', Integer, nullable=False),
                     Column('type', Integer, nullable=False),
@@ -58,8 +40,10 @@ list_propusk = Table("list_propusk", meta,
                      Column("id_propusk", Integer, nullable=False),
                      Column("id_propusk", Integer, nullable=False),
                      Column("date_from", DateTime, nullable=False),
                      Column("date_from", DateTime, nullable=False),
                      Column("date_to", DateTime, nullable=False),
                      Column("date_to", DateTime, nullable=False),
-                     Column("personal", Integer, ForeignKey("list_personal.id"), nullable=False),
-                     Column("place", Integer, ForeignKey("list_place.id"), nullable=False),
+                     Column("personal", Integer, ForeignKey(
+                         "list_personal.id"), nullable=False),
+                     Column("place", Integer, ForeignKey(
+                         "list_place.id"), nullable=False),
                      Column("receiving_man", Text, nullable=False),
                      Column("receiving_man", Text, nullable=False),
                      Column("purpose_visite", Text, nullable=False),
                      Column("purpose_visite", Text, nullable=False),
                      Column("face", Text, nullable=False),
                      Column("face", Text, nullable=False),
@@ -71,16 +55,37 @@ list_propusk = Table("list_propusk", meta,
 list_ussued_passes = Table("list_ussued_passes", meta,
 list_ussued_passes = Table("list_ussued_passes", meta,
                            Column("id", Integer, primary_key=True),
                            Column("id", Integer, primary_key=True),
                            Column("used_pass", Integer, nullable=False),
                            Column("used_pass", Integer, nullable=False),
-                           Column("id_propusk", Integer, ForeignKey("list_propusk.id_propusk"), nullable=False),
+                           Column("id_propusk", Integer, ForeignKey(
+                               "list_propusk.id_propusk"), nullable=False),
                            Column("created", DateTime, default=func.now()),
                            Column("created", DateTime, default=func.now()),
                            Column("update", DateTime, default=func.now(), onupdate=func.current_timestamp()))
                            Column("update", DateTime, default=func.now(), onupdate=func.current_timestamp()))
 
 
-engine = create_engine(F"sqlite:///{FILE_NAME}", echo=False)
+def checking_path_db() -> str | None:
+    if os.environ.get("DB_DIR"):
+        return os.path.join(os.environ.get("DB_DIR"), "propusk.db")
+    else:
+        if os.environ.get("DEFAULT_PATH"):
+            return os.path.join(
+                os.environ.get("DEFAULT_PATH"), "propusk.db")
+        else:
+            logger.error(
+                "Не правильно указан путь к базе данных, или вообще отсутствует")
+            # show_dialog(
+            #     QMessageBox.Icon.Critical,
+            #     "Путь к бд",
+            #     "Не правильно указан путь к базе данных, или вообще отсутствует"
+            # )
+
+
+engine = create_engine(F"sqlite:///{checking_path_db()}", echo=False)
 engine.logging_name = 'PropuskLogger'
 engine.logging_name = 'PropuskLogger'
 
 
+
+
 def init_db():
 def init_db():
-    if not os.path.exists(os.path.dirname(FILE_NAME)):
-        os.mkdir(os.path.dirname(FILE_NAME))
+    path = checking_path_db()
+    if not os.path.exists(os.path.dirname(path)):
+        os.mkdir(os.path.dirname(path))
 
 
     meta.create_all(engine)
     meta.create_all(engine)
 
 
@@ -92,8 +97,12 @@ def connect():
 def check_error_sql(func):
 def check_error_sql(func):
     def wrapper(*arg, **args):
     def wrapper(*arg, **args):
         try:
         try:
-            return func(arg, args)
+            return func(*arg, **args)
         except ProgrammingError as pe:
         except ProgrammingError as pe:
+            show_dialog(QMessageBox.Icon.Critical, 'Ошибка', 'Произошла ошибка в работе БД')
             logger.error(pe)
             logger.error(pe)
-            
-    return wrapper
+        except OperationalError as oe:
+            show_dialog(QMessageBox.Icon.Critical, 'Ошибка', 'Произошла ошибка в работе БД')
+            logger.error(oe)
+
+    return wrapper

+ 3 - 2
module/cam/IPCam.py

@@ -5,6 +5,7 @@ from PySide6.QtGui import QImage, QPixmap
 import cv2
 import cv2
 from module import create_filename
 from module import create_filename
 from module.ImageTool import cupture_face
 from module.ImageTool import cupture_face
+from widgets import ImageViewer
 from logger import logger
 from logger import logger
 
 
 
 
@@ -75,9 +76,9 @@ class IPCam(Thread):
     def __del__(self) -> None:
     def __del__(self) -> None:
         self.stop_cam()
         self.stop_cam()
         
         
-    def cupture_image(self, qLabel: QLabel) -> str:
+    def cupture_image(self, image_viewer: ImageViewer) -> str:
         name_file = create_filename()
         name_file = create_filename()
-        qLabel.setPixmap(self.__scaled_img)
+        image_viewer.setImage(self.__scaled_img)
         self.__scaled_img.save(name_file, 'jpg')
         self.__scaled_img.save(name_file, 'jpg')
         self.stop_cam()
         self.stop_cam()
         face_file_name = create_filename('face')
         face_file_name = create_filename('face')

+ 21 - 26
module/cam/USBCam.py

@@ -1,15 +1,16 @@
-import os
+import os, sys
 
 
 from PySide6.QtMultimediaWidgets import QVideoWidget
 from PySide6.QtMultimediaWidgets import QVideoWidget
-from PySide6.QtCore import Slot, Qt
-from PySide6.QtGui import QImage, QPixmap
+from PySide6.QtCore import Slot
+from PySide6.QtGui import QImage
 from PySide6.QtWidgets import QLabel
 from PySide6.QtWidgets import QLabel
 from PySide6.QtMultimedia import (
 from PySide6.QtMultimedia import (
     QMediaDevices, QCamera, QImageCapture, QMediaCaptureSession)
     QMediaDevices, QCamera, QImageCapture, QMediaCaptureSession)
 from itertools import groupby
 from itertools import groupby
 from logger import logger
 from logger import logger
 from module import create_filename
 from module import create_filename
-from module.ImageTool import cupture_face
+from widgets import ImageViewer
+
 
 
 if not os.environ.get("PHOTO_DIR"):
 if not os.environ.get("PHOTO_DIR"):
     logger.error("Не задана локальная переменная PHOTO_DIR")
     logger.error("Не задана локальная переменная PHOTO_DIR")
@@ -23,19 +24,19 @@ class USBCam:
     _camera: QCamera
     _camera: QCamera
     _camera_info: list[QMediaDevices]
     _camera_info: list[QMediaDevices]
     _image_capture: QImageCapture
     _image_capture: QImageCapture
-    _current_preview = QImage()
+    _current_preview: QImage = QImage()
     _label: QLabel
     _label: QLabel
-
-    def __init__(self, q_Video_Widget: QVideoWidget, name_cam: str) -> None:
+    _video_widget: QVideoWidget
+    
+    def __init__(self, video_widget: QVideoWidget, name_cam: str) -> None:
         self._create_dirs()
         self._create_dirs()
-        self._video_widget = q_Video_Widget
+        self._video_widget = video_widget
 
 
         self._camera_info = get_object_cam_by_name(name_cam)
         self._camera_info = get_object_cam_by_name(name_cam)
         if not self._camera_info:
         if not self._camera_info:
             logger.error(
             logger.error(
-                "Не нашли камеру QMediaDevices.videoInputs(). убидитесь, что у вас есть камера")
-            raise IndexError(
-                "Не нашли камеру QMediaDevices.videoInputs(). убидитесь, что у вас есть камера")
+                "Не нашли камеру убидитесь, что у вас есть камера")
+            sys.exit(0)
 
 
     def start_cam(self):
     def start_cam(self):
         self._camera = QCamera(self._camera_info)
         self._camera = QCamera(self._camera_info)
@@ -66,8 +67,8 @@ class USBCam:
     def __del__(self) -> None:
     def __del__(self) -> None:
         self.stop_cam()
         self.stop_cam()
 
 
-    def cupture_image(self, label: QLabel) -> str:
-        self._label = label
+    def cupture_image(self, image_viewer: ImageViewer) -> str:
+        self._image_viewer = image_viewer
         self._file_name = create_filename()
         self._file_name = create_filename()
         self._image_capture.captureToFile(self._file_name)
         self._image_capture.captureToFile(self._file_name)
         logger.info(F"Создаем файл {self._file_name}")
         logger.info(F"Создаем файл {self._file_name}")
@@ -92,26 +93,20 @@ class USBCam:
 
 
     @Slot(int, str)
     @Slot(int, str)
     def image_saved(self, id, fileName):
     def image_saved(self, id, fileName):
-        load_image(self._label, fileName)
+        self._image_viewer.setImage(fileName)
         self.stop_cam()
         self.stop_cam()
         return fileName
         return fileName
 
 
+def get_first_cam() -> str:
+    try:
+        return QMediaDevices.videoInputs()[0].description()
+    except IndexError:
+        pass
 
 
-def load_image(qlabel: QLabel, path_file: str) -> None:
-    logger.info(F"Set image to label: {path_file}")
-    qlabel.setPixmap(QPixmap(QImage(path_file)).scaled(
-        qlabel.width()-4,
-        qlabel.height(),
-        Qt.AspectRatioMode.KeepAspectRatio,
-        Qt.TransformationMode.FastTransformation
-    ))
-
-
-def get_list_name_cam() -> list:
+def get_list_name_cam() -> list[str]:
     return [x for x, _ in groupby(
     return [x for x, _ in groupby(
         [x.description() for x in QMediaDevices.videoInputs()]
         [x.description() for x in QMediaDevices.videoInputs()]
     )]
     )]
 
 
-
 def get_object_cam_by_name(name_cam: str) -> QMediaDevices:
 def get_object_cam_by_name(name_cam: str) -> QMediaDevices:
     return [x for x in QMediaDevices.videoInputs() if x.description() == name_cam][0]
     return [x for x in QMediaDevices.videoInputs() if x.description() == name_cam][0]

+ 0 - 8
module/lang/ru.py

@@ -1,8 +0,0 @@
-
-start_cam = "Запустить камеру"
-stop_cam = "Остановить камеру"
-
-warring_cams = dict(
-    title = "Камеры не настроины",
-    body = "Настройте камеры \nНастройки -> Настройки камеры"
-)

+ 433 - 0
widgets/ImageViewer.py

@@ -0,0 +1,433 @@
+""" ImageViewer.py: PyQt image viewer widget based on QGraphicsView with mouse croping.
+"""
+
+import os.path
+
+from PySide6.QtCore import Qt, QRectF, QRect, QPoint, QPointF, QEvent, QSize, Signal
+from PySide6.QtGui import QImage, QPixmap, QPainterPath, QMouseEvent, QPainter, QPen
+from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QFileDialog, QSizePolicy, QMdiArea
+
+# numpy is optional: only needed if you want to display numpy 2d arrays as images.
+try:
+    import numpy as np
+except ImportError:
+    np = None
+
+# qimage2ndarray is optional: useful for displaying numpy 2d arrays as images.
+# !!! qimage2ndarray requires PyQt5.
+#     Some custom code in the viewer appears to handle the conversion from numpy 2d arrays,
+#     so qimage2ndarray probably is not needed anymore. I've left it here just in case.
+try:
+    import qimage2ndarray
+except ImportError:
+    qimage2ndarray = None
+
+__author__ = "Alex Sidorov <alex.sidorof@ya.ru>"
+__version__ = '0.1'
+
+
+class ImageViewer(QGraphicsView):
+    """ PyQt image viewer widget based on QGraphicsView with mouse croping.
+    Image File:
+    -----------
+    Calling set_file() 
+    Image:
+    ------
+    Use the setImage(im) method to set the image data in the viewer.
+        - im can be a QImage, QPixmap, or NumPy 2D array (the later requires the package qimage2ndarray).
+        For display in the QGraphicsView the image will be converted to a QPixmap.
+    Some useful image format conversion utilities:
+        qimage2ndarray: NumPy ndarray <==> QImage    (https://github.com/hmeine/qimage2ndarray)
+        ImageQt: PIL Image <==> QImage  (https://github.com/python-pillow/Pillow/blob/master/PIL/ImageQt.py)
+    Mouse:
+    ------
+    Mouse interactions for cropted and panning is fully customizable by simply setting the desired button interactions:
+    e.g.,
+        regionZoomButton = Qt.LeftButton  # Drag a zoom box.
+        zoomOutButton = Qt.RightButton  # Pop end of zoom stack (double click clears zoom stack).
+        panButton = Qt.MiddleButton  # Drag to pan.
+        wheelZoomFactor = 1.25  # Set to None or 1 to disable mouse wheel zoom.
+    To disable any interaction, just disable its button.
+    e.g., to disable panning:
+        panButton = None
+    """
+
+    # Mouse button signals emit image scene (x, y) coordinates.
+    # !!! For image (row, column) matrix indexing, row = y and column = x.
+    # !!! These signals will NOT be emitted if the event is handled by an interaction such as zoom or pan.
+    # !!! If aspect ratio prevents image from filling viewport, emitted position may be outside image bounds.
+    leftMouseButtonPressed = Signal(float, float)
+    leftMouseButtonReleased = Signal(float, float)
+    middleMouseButtonPressed = Signal(float, float)
+    middleMouseButtonReleased = Signal(float, float)
+    rightMouseButtonPressed = Signal(float, float)
+    rightMouseButtonReleased = Signal(float, float)
+    leftMouseButtonDoubleClicked = Signal(float, float)
+    rightMouseButtonDoubleClicked = Signal(float, float)
+
+    # Emitted upon zooming/panning.
+    viewChanged = Signal()
+
+    # Emitted on mouse motion.
+    # Emits mouse position over image in image pixel coordinates.
+    # !!! setMouseTracking(True) if you want to use this at all times.
+    mousePositionOnImageChanged = Signal(QPoint)
+
+    def __init__(self, parent=None):
+        super(ImageViewer, self).__init__(parent)
+
+        # Image is displayed as a QPixmap in a QGraphicsScene attached to this QGraphicsView.
+        self.scene = QGraphicsScene()
+        self.setScene(self.scene)
+        
+        
+        # Better quality pixmap scaling?
+        # self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
+
+        # Displayed image pixmap in the QGraphicsScene.
+        self._image: QPixmap = None
+
+        # Image aspect ratio mode.
+        #   Qt.IgnoreAspectRatio: Scale image to fit viewport.
+        #   Qt.KeepAspectRatio: Scale image to fit inside viewport, preserving aspect ratio.
+        #   Qt.KeepAspectRatioByExpanding: Scale image to fill the viewport, preserving aspect ratio.
+        self.aspectRatioMode = Qt.AspectRatioMode.KeepAspectRatio
+
+        # Scroll bar behaviour.
+        #   Qt.ScrollBarAlwaysOff: Never shows a scroll bar.
+        #   Qt.ScrollBarAlwaysOn: Always shows a scroll bar.
+        #   Qt.ScrollBarAsNeeded: Shows a scroll bar only when zoomed.
+        self.setHorizontalScrollBarPolicy(
+            Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+
+        # Interactions (set buttons to None to disable interactions)
+        # !!! Events handled by interactions will NOT emit *MouseButton* signals.
+        #     Note: regionZoomButton will still emit a *MouseButtonReleased signal on a click (i.e. tiny box).
+        self.regionZoomButton = Qt.MouseButton.LeftButton  # Drag a zoom box.
+        # Pop end of zoom stack (double click clears zoom stack).
+        self.zoomOutButton = Qt.MouseButton.RightButton
+        self.panButton = Qt.MouseButton.MiddleButton  # Drag to pan.
+        # Set to None or 1 to disable mouse wheel zoom.
+        self.wheelZoomFactor = 1.25
+
+        # Stack of QRectF zoom boxes in scene coordinates.
+        # !!! If you update this manually, be sure to call updateViewer() to reflect any changes.
+        self.zoomStack: list[QRectF] = []
+        
+        #Stack of image
+        self.imageStack: list[QPixmap] = []
+
+        # Flags for active zooming/panning.
+        self._isZooming: bool = False
+        self._isPanning: bool = False
+
+        # Store temporary position in screen pixels or scene units.
+        self._pixelPosition = QPoint()
+        self._scenePosition = QPointF()
+
+        # Track mouse position. e.g., For displaying coordinates in a UI.
+        # self.setMouseTracking(True)
+
+        self.setSizePolicy(QSizePolicy.Policy.Expanding,
+                           QSizePolicy.Policy.Expanding)
+
+    def sizeHint(self):
+        return QSize(900, 600)
+
+    def hasImage(self):
+        """ Returns whether the scene contains an image pixmap.
+        """
+        return self._image is not None
+
+    def clearImage(self):
+        """ Removes the current image pixmap from the scene if it exists.
+        """
+        if self.hasImage():
+            self.scene.removeItem(self._image)
+            self._image = None
+
+    def pixmap(self):
+        """ Returns the scene's current image pixmap as a QPixmap, or else None if no image exists.
+        :rtype: QPixmap | None
+        """
+        if self.hasImage():
+            return self._image.pixmap()
+        return None
+
+    def image(self):
+        """ Returns the scene's current image pixmap as a QImage, or else None if no image exists.
+        :rtype: QImage | None
+        """
+        if self.hasImage():
+            return self._image.pixmap().toImage()
+        return None
+
+    def setImage(self, image):
+        """ Set the scene's current image pixmap to the input QImage or QPixmap.
+        Raises a RuntimeError if the input image has type other than QImage or QPixmap.
+        :type image: QImage | QPixmap
+        """
+        if type(image) is QPixmap:
+            pixmap = image
+        elif type(image) is QImage:
+            pixmap = QPixmap.fromImage(image)
+        elif type(image) is str:
+            pixmap = QPixmap.fromImage(QImage(image))
+        elif (np is not None) and (type(image) is np.ndarray):
+            if qimage2ndarray is not None:
+                qimage = qimage2ndarray.array2qimage(image, True)
+                pixmap = QPixmap.fromImage(qimage)
+            else:
+                image = image.astype(np.float32)
+                image -= image.min()
+                image /= image.max()
+                image *= 255
+                image[image > 255] = 255
+                image[image < 0] = 0
+                image = image.astype(np.uint8)
+                height, width = image.shape
+                bytes = image.tobytes()
+                qimage = QImage(bytes, width, height,
+                                QImage.Format.Format_Grayscale8)
+                pixmap = QPixmap.fromImage(qimage)
+        else:
+            raise RuntimeError(
+                "ImageViewer.setImage: Argument must be a QImage, QPixmap, or numpy.ndarray.")
+
+        if self.hasImage():
+            self._image.setPixmap(pixmap)
+        else:
+            self._image = self.scene.addPixmap(pixmap)
+
+        self.imageStack.append(pixmap)
+        # Better quality pixmap scaling?
+        # !!! This will distort actual pixel data when zoomed way in.
+        #     For scientific image analysis, you probably don't want this.
+        self._image.setTransformationMode(Qt.SmoothTransformation)
+
+        # Set scene size to image size.
+        self.setSceneRect(QRectF(pixmap.rect()))
+        self.updateViewer()
+
+    def updateViewer(self):
+        """ Show current zoom (if showing entire image, apply current aspect ratio mode).
+        """
+        if not self.hasImage():
+            return
+        if len(self.imageStack) > 1:
+            # Show zoomed rect.
+            # self.fitInView(self.zoomStack[-1], self.aspectRatioMode)
+            self._image.setPixmap(self.imageStack[-1])
+        else:
+            # Show entire image.
+            self._image.setPixmap(self.imageStack[0])
+            # self.fitInView(self.sceneRect(), self.aspectRatioMode)
+
+    def clearZoom(self):
+        if len(self.zoomStack) > 0:
+            self.zoomStack = []
+            self.imageStack = self.imageStack[0]
+            self.updateViewer()
+            self.viewChanged.emit()
+
+    def resizeEvent(self, event):
+        """ Maintain current zoom on resize.
+        """
+        self.updateViewer()
+
+    def mousePressEvent(self, event):
+        """ Start mouse pan or zoom mode.
+        """
+        # Ignore dummy events. e.g., Faking pan with left button ScrollHandDrag.
+        dummyModifiers = Qt.KeyboardModifier(Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier
+                                             | Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier)
+        if event.modifiers() == dummyModifiers:
+            QGraphicsView.mousePressEvent(self, event)
+            event.accept()
+            return
+        
+        # Start dragging a region zoom box?
+        if (self.regionZoomButton is not None) and (event.button() == self.regionZoomButton):
+            self._pixelPosition = event.pos()  # store pixel position
+            self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
+            QGraphicsView.mousePressEvent(self, event)
+            event.accept()
+            self._isZooming = True
+            return
+
+        if (self.zoomOutButton is not None) and (event.button() == self.zoomOutButton):
+            if len(self.zoomStack):
+                self.zoomStack.pop()
+                self.imageStack.pop()
+                self.updateViewer()
+                self.viewChanged.emit()
+            event.accept()
+            return
+
+        # Start dragging to pan?
+        if (self.panButton is not None) and (event.button() == self.panButton):
+            self._pixelPosition = event.pos()  # store pixel position
+            self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
+            if self.panButton == Qt.MouseButton.LeftButton:
+                QGraphicsView.mousePressEvent(self, event)
+            else:
+                # ScrollHandDrag ONLY works with LeftButton, so fake it.
+                # Use a bunch of dummy modifiers to notify that event should NOT be handled as usual.
+                self.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
+                dummyModifiers = Qt.KeyboardModifier(Qt.KeyboardModifier.ShiftModifier
+                                                     | Qt.KeyboardModifier.ControlModifier
+                                                     | Qt.KeyboardModifier.AltModifier
+                                                     | Qt.KeyboardModifier.MetaModifier)
+                dummyEvent = QMouseEvent(QEvent.Type.MouseButtonPress, QPointF(event.pos()), Qt.MouseButton.LeftButton,
+                                         event.buttons(), dummyModifiers)
+                self.mousePressEvent(dummyEvent)
+            sceneViewport = self.mapToScene(
+                self.viewport().rect()).boundingRect().intersected(self.sceneRect())
+            self._scenePosition = sceneViewport.topLeft()
+            event.accept()
+            self._isPanning = True
+            return
+
+        scenePos = self.mapToScene(event.pos())
+        if event.button() == Qt.MouseButton.LeftButton:
+            self.leftMouseButtonPressed.emit(scenePos.x(), scenePos.y())
+        elif event.button() == Qt.MouseButton.MiddleButton:
+            self.middleMouseButtonPressed.emit(scenePos.x(), scenePos.y())
+        elif event.button() == Qt.MouseButton.RightButton:
+            self.rightMouseButtonPressed.emit(scenePos.x(), scenePos.y())
+
+        QGraphicsView.mousePressEvent(self, event)
+
+    def mouseReleaseEvent(self, event):
+        """ Stop mouse pan or zoom mode (apply zoom if valid).
+        """
+        # Ignore dummy events. e.g., Faking pan with left button ScrollHandDrag.
+        dummyModifiers = Qt.KeyboardModifier(Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier
+                                             | Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier)
+        if event.modifiers() == dummyModifiers:
+            QGraphicsView.mouseReleaseEvent(self, event)
+            event.accept()
+            return
+
+        # Finish dragging a region zoom box?
+        if (self.regionZoomButton is not None) and (event.button() == self.regionZoomButton):
+            QGraphicsView.mouseReleaseEvent(self, event)
+            zoomRect = self.scene.selectionArea().boundingRect().intersected(self.sceneRect())
+            # Clear current selection area (i.e. rubberband rect).
+            self.scene.setSelectionArea(QPainterPath())
+            self.setDragMode(QGraphicsView.DragMode.NoDrag)
+            # If zoom box is 3x3 screen pixels or smaller, do not zoom and proceed to process as a click release.
+            zoomPixelWidth = abs(event.pos().x() - self._pixelPosition.x())
+            zoomPixelHeight = abs(event.pos().y() - self._pixelPosition.y())
+            if zoomPixelWidth > 3 and zoomPixelHeight > 3:
+                if zoomRect.isValid() and (zoomRect != self.sceneRect()):
+                    self.zoomStack.append(zoomRect)
+                    self.crop_image()
+                    self.updateViewer()
+                    self.viewChanged.emit()
+                    event.accept()
+                    self._isZooming = False
+                    return
+
+        # Finish panning?
+        if (self.panButton is not None) and (event.button() == self.panButton):
+            if self.panButton == Qt.MouseButton.LeftButton:
+                QGraphicsView.mouseReleaseEvent(self, event)
+            else:
+                # ScrollHandDrag ONLY works with LeftButton, so fake it.
+                # Use a bunch of dummy modifiers to notify that event should NOT be handled as usual.
+                self.viewport().setCursor(Qt.CursorShape.ArrowCursor)
+                dummyModifiers = Qt.KeyboardModifier(Qt.KeyboardModifier.ShiftModifier
+                                                     | Qt.KeyboardModifier.ControlModifier
+                                                     | Qt.KeyboardModifier.AltModifier
+                                                     | Qt.KeyboardModifier.MetaModifier)
+                dummyEvent = QMouseEvent(QEvent.Type.MouseButtonRelease, QPointF(event.pos()),
+                                         Qt.MouseButton.LeftButton, event.buttons(), dummyModifiers)
+                self.mouseReleaseEvent(dummyEvent)
+            self.setDragMode(QGraphicsView.DragMode.NoDrag)
+            if len(self.zoomStack) > 0:
+                sceneViewport = self.mapToScene(
+                    self.viewport().rect()).boundingRect().intersected(self.sceneRect())
+                delta = sceneViewport.topLeft() - self._scenePosition
+                self.zoomStack[-1].translate(delta)
+                self.zoomStack[-1] = self.zoomStack[-1].intersected(
+                    self.sceneRect())
+                self.viewChanged.emit()
+            event.accept()
+            self._isPanning = False
+            return
+
+        scenePos = self.mapToScene(event.pos())
+        if event.button() == Qt.MouseButton.LeftButton:
+            self.leftMouseButtonReleased.emit(scenePos.x(), scenePos.y())
+        elif event.button() == Qt.MouseButton.MiddleButton:
+            self.middleMouseButtonReleased.emit(scenePos.x(), scenePos.y())
+        elif event.button() == Qt.MouseButton.RightButton:
+            self.rightMouseButtonReleased.emit(scenePos.x(), scenePos.y())
+
+        QGraphicsView.mouseReleaseEvent(self, event)
+
+    def mouseDoubleClickEvent(self, event):
+        """ Show entire image.
+        """
+        # Zoom out on double click?
+        if (self.zoomOutButton is not None) and (event.button() == self.zoomOutButton):
+            self.clearZoom()
+            event.accept()
+            return
+
+        scenePos = self.mapToScene(event.pos())
+        if event.button() == Qt.MouseButton.LeftButton:
+            self.leftMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
+        elif event.button() == Qt.MouseButton.RightButton:
+            self.rightMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
+
+        QGraphicsView.mouseDoubleClickEvent(self, event)
+
+
+    def mouseMoveEvent(self, event):
+        # Emit updated view during panning.
+        if self._isPanning:
+            QGraphicsView.mouseMoveEvent(self, event)
+            if len(self.zoomStack) > 0:
+                sceneViewport = self.mapToScene(
+                    self.viewport().rect()).boundingRect().intersected(self.sceneRect())
+                delta = sceneViewport.topLeft() - self._scenePosition
+                self._scenePosition = sceneViewport.topLeft()
+                self.zoomStack[-1].translate(delta)
+                self.zoomStack[-1] = self.zoomStack[-1].intersected(
+                    self.sceneRect())
+                self.updateViewer()
+                self.viewChanged.emit()
+
+        scenePos = self.mapToScene(event.pos())
+        if self.sceneRect().contains(scenePos):
+            # Pixel index offset from pixel center.
+            x = int(round(scenePos.x() - 0.5))
+            y = int(round(scenePos.y() - 0.5))
+            imagePos = QPoint(x, y)
+        else:
+            # Invalid pixel position.
+            imagePos = QPoint(-1, -1)
+        self.mousePositionOnImageChanged.emit(imagePos)
+
+        QGraphicsView.mouseMoveEvent(self, event)
+
+    def enterEvent(self, event):
+        pass
+        # self.setCursor(Qt.CursorShape.CrossCursor)
+
+    def leaveEvent(self, event):
+        pass
+        # self.setCursor(Qt.CursorShape.ArrowCursor)
+
+    def crop_image(self) -> QPixmap:
+        if self.hasImage():
+            rect: QRect = self.zoomStack[-1].toRect()
+            image = self.imageStack[-1].copy(rect)
+            self._image.setPixmap(image)
+            self.imageStack.append(image)
+
+    def save_image(self, path_file: str) -> None:
+        self._image.save(path_file)

+ 3 - 2
widgets/PCamChecked.py

@@ -1,7 +1,8 @@
 
 
 from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QComboBox, QPushButton, QMessageBox
 from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QComboBox, QPushButton, QMessageBox
-from widgets.PStackedWidget import PStackedWidget
-from module.cam import IPCam, USBCam, get_list_name_cam
+from widgets import PStackedWidget
+from module.cam import IPCam, USBCam
+from module.cam.USBCam import get_list_name_cam
 from logger import logger
 from logger import logger
 
 
 class PCamChecked(QWidget):
 class PCamChecked(QWidget):

+ 9 - 5
widgets/PStackedWidget.py

@@ -2,8 +2,10 @@ from PySide6.QtWidgets import QStackedWidget, QWidget, QLabel, QGridLayout
 from PySide6.QtGui import QResizeEvent, QPixmap, QImage
 from PySide6.QtGui import QResizeEvent, QPixmap, QImage
 from PySide6.QtCore import QSize
 from PySide6.QtCore import QSize
 from PySide6.QtMultimediaWidgets import QVideoWidget
 from PySide6.QtMultimediaWidgets import QVideoWidget
-from module.cam.USBCam import load_image
+from .ImageViewer import ImageViewer
+# from module.cam.USBCam import load_image
 import os
 import os
+from logger import logger
 
 
 
 
 class PStackedWidget(QStackedWidget):
 class PStackedWidget(QStackedWidget):
@@ -21,7 +23,7 @@ class PStackedWidget(QStackedWidget):
         self.image.resize(event.size())
         self.image.resize(event.size())
         self.video.resize(event.size())
         self.video.resize(event.size())
         
         
-        self.image.setPixmap(QPixmap.fromImage(
+        self.image.setImage(QPixmap.fromImage(
             self.__scaled_image(self.image, event)                                       
             self.__scaled_image(self.image, event)                                       
         ))
         ))
         
         
@@ -30,7 +32,6 @@ class PStackedWidget(QStackedWidget):
                 self.__scaled_image(self.video, event)
                 self.__scaled_image(self.video, event)
             ))
             ))
         
         
-        # print(type(self.video))
 
 
     def __setup_image_stacked(self) -> None:
     def __setup_image_stacked(self) -> None:
         self.page_image = QWidget()
         self.page_image = QWidget()
@@ -39,7 +40,7 @@ class PStackedWidget(QStackedWidget):
         self.layout_image = QGridLayout(self.page_image)
         self.layout_image = QGridLayout(self.page_image)
         self.layout_image.setObjectName("layout_image")
         self.layout_image.setObjectName("layout_image")
 
 
-        self.image = QLabel(self.page_image)
+        self.image = ImageViewer(self.page_image)
         self.image.setObjectName("image")
         self.image.setObjectName("image")
         self.layout_image.addWidget(self.image)
         self.layout_image.addWidget(self.image)
 
 
@@ -63,12 +64,15 @@ class PStackedWidget(QStackedWidget):
         self.addWidget(self.page_video)
         self.addWidget(self.page_video)
 
 
     def set_default_image(self) -> None:
     def set_default_image(self) -> None:
-        load_image(self.image, os.environ.get("NO_MEDIA_IMAGE"))
+        self.to_image()
+        self.image.setImage(os.environ.get("NO_MEDIA_IMAGE"))
+        # load_image(self.image, os.environ.get("NO_MEDIA_IMAGE"))
 
 
     def to_image(self) -> None:
     def to_image(self) -> None:
         self.setCurrentIndex(0)
         self.setCurrentIndex(0)
 
 
     def to_video(self) -> None:
     def to_video(self) -> None:
+        
         self.setCurrentIndex(1)
         self.setCurrentIndex(1)
 
 
     def __scaled_image(self, label: QLabel, event: QResizeEvent) -> QImage:
     def __scaled_image(self, label: QLabel, event: QResizeEvent) -> QImage:

+ 2 - 1
widgets/__init__.py

@@ -1,8 +1,9 @@
 from .PStackedWidget import *
 from .PStackedWidget import *
 from .PLineEdit import *
 from .PLineEdit import *
 from .PCamChecked import *
 from .PCamChecked import *
+from .ImageViewer import ImageViewer
 
 
-
+__all__ = ['PStackedWidget', 'PLineEdit', 'PCamChecked', 'ImageViewer']
 
 
 def create_widget_cam_shecked(obj, layout, name_object: str, mode: str = 'video') -> PCamChecked:
 def create_widget_cam_shecked(obj, layout, name_object: str, mode: str = 'video') -> PCamChecked:
     widget = PCamChecked(obj, mode=mode)
     widget = PCamChecked(obj, mode=mode)