Pārlūkot izejas kodu

Change echo image

AlexSidorov 1 gadu atpakaļ
vecāks
revīzija
a03c060d29

+ 2 - 2
module/CleanerImage.py

@@ -1,7 +1,7 @@
 from .WorkWithDB import connect, list_propusk
 from sqlalchemy import select
 from sqlalchemy.exc import OperationalError
-from MyMessageBox import show_dialog
+from module.MyMessageBox import show_dialog
 from PySide6.QtWidgets import QMessageBox
 import os, sys
 from logger import logger
@@ -44,7 +44,7 @@ class CleanerImage:
     def clear(self) -> None:
         _, _, files = os.walk(self._path).__next__()
         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))
                 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
-from logger import logger
 from datetime import datetime
+
 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:
@@ -39,3 +44,13 @@ def cupture_face(path_photo: str, new_path: str) -> None:
             cv2.imwrite(new_path, img)
     except cv2.error as 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
         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: 
-            image = convert_from_path(pdf, dpi=600)[0].toqimage()
+            image = convert_from_path(pdf, dpi=300)[0].toqimage()
 
         image = QPixmap.fromImage(image)
         image = image.scaledToWidth(self.printer.width(), Qt.SmoothTransformation)

+ 37 - 28
module/WorkWithDB.py

@@ -1,31 +1,13 @@
 from sqlalchemy import (MetaData, Table, Column, Integer,
                         DateTime, String, Text, ForeignKey,
                         create_engine, func)
-from sqlalchemy.exc import ProgrammingError
+from sqlalchemy.exc import ProgrammingError, OperationalError
 import os
 from module.MyMessageBox import show_dialog
 from PySide6.QtWidgets import QMessageBox
 from logger import logger
 
 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,
                     Column('id', Integer, primary_key=True),
                     Column('type', Integer, nullable=False),
@@ -58,8 +40,10 @@ list_propusk = Table("list_propusk", meta,
                      Column("id_propusk", Integer, nullable=False),
                      Column("date_from", 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("purpose_visite", 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,
                            Column("id", Integer, primary_key=True),
                            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("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'
 
+
+
 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)
 
@@ -92,8 +97,12 @@ def connect():
 def check_error_sql(func):
     def wrapper(*arg, **args):
         try:
-            return func(arg, args)
+            return func(*arg, **args)
         except ProgrammingError as pe:
+            show_dialog(QMessageBox.Icon.Critical, 'Ошибка', 'Произошла ошибка в работе БД')
             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
 from module import create_filename
 from module.ImageTool import cupture_face
+from widgets import ImageViewer
 from logger import logger
 
 
@@ -75,9 +76,9 @@ class IPCam(Thread):
     def __del__(self) -> None:
         self.stop_cam()
         
-    def cupture_image(self, qLabel: QLabel) -> str:
+    def cupture_image(self, image_viewer: ImageViewer) -> str:
         name_file = create_filename()
-        qLabel.setPixmap(self.__scaled_img)
+        image_viewer.setImage(self.__scaled_img)
         self.__scaled_img.save(name_file, 'jpg')
         self.stop_cam()
         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.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.QtMultimedia import (
     QMediaDevices, QCamera, QImageCapture, QMediaCaptureSession)
 from itertools import groupby
 from logger import logger
 from module import create_filename
-from module.ImageTool import cupture_face
+from widgets import ImageViewer
+
 
 if not os.environ.get("PHOTO_DIR"):
     logger.error("Не задана локальная переменная PHOTO_DIR")
@@ -23,19 +24,19 @@ class USBCam:
     _camera: QCamera
     _camera_info: list[QMediaDevices]
     _image_capture: QImageCapture
-    _current_preview = QImage()
+    _current_preview: QImage = QImage()
     _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._video_widget = q_Video_Widget
+        self._video_widget = video_widget
 
         self._camera_info = get_object_cam_by_name(name_cam)
         if not self._camera_info:
             logger.error(
-                "Не нашли камеру QMediaDevices.videoInputs(). убидитесь, что у вас есть камера")
-            raise IndexError(
-                "Не нашли камеру QMediaDevices.videoInputs(). убидитесь, что у вас есть камера")
+                "Не нашли камеру убидитесь, что у вас есть камера")
+            sys.exit(0)
 
     def start_cam(self):
         self._camera = QCamera(self._camera_info)
@@ -66,8 +67,8 @@ class USBCam:
     def __del__(self) -> None:
         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._image_capture.captureToFile(self._file_name)
         logger.info(F"Создаем файл {self._file_name}")
@@ -92,26 +93,20 @@ class USBCam:
 
     @Slot(int, str)
     def image_saved(self, id, fileName):
-        load_image(self._label, fileName)
+        self._image_viewer.setImage(fileName)
         self.stop_cam()
         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(
         [x.description() for x in QMediaDevices.videoInputs()]
     )]
 
-
 def get_object_cam_by_name(name_cam: str) -> QMediaDevices:
     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 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
 
 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.QtCore import QSize
 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
+from logger import logger
 
 
 class PStackedWidget(QStackedWidget):
@@ -21,7 +23,7 @@ class PStackedWidget(QStackedWidget):
         self.image.resize(event.size())
         self.video.resize(event.size())
         
-        self.image.setPixmap(QPixmap.fromImage(
+        self.image.setImage(QPixmap.fromImage(
             self.__scaled_image(self.image, event)                                       
         ))
         
@@ -30,7 +32,6 @@ class PStackedWidget(QStackedWidget):
                 self.__scaled_image(self.video, event)
             ))
         
-        # print(type(self.video))
 
     def __setup_image_stacked(self) -> None:
         self.page_image = QWidget()
@@ -39,7 +40,7 @@ class PStackedWidget(QStackedWidget):
         self.layout_image = QGridLayout(self.page_image)
         self.layout_image.setObjectName("layout_image")
 
-        self.image = QLabel(self.page_image)
+        self.image = ImageViewer(self.page_image)
         self.image.setObjectName("image")
         self.layout_image.addWidget(self.image)
 
@@ -63,12 +64,15 @@ class PStackedWidget(QStackedWidget):
         self.addWidget(self.page_video)
 
     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:
         self.setCurrentIndex(0)
 
     def to_video(self) -> None:
+        
         self.setCurrentIndex(1)
 
     def __scaled_image(self, label: QLabel, event: QResizeEvent) -> QImage:

+ 2 - 1
widgets/__init__.py

@@ -1,8 +1,9 @@
 from .PStackedWidget import *
 from .PLineEdit 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:
     widget = PCamChecked(obj, mode=mode)