프로젝트

일반

사용자정보

통계
| 개정판:

hytos / DTI_PID / DTI_PID / QtImageViewer.py @ 02ed19e4

이력 | 보기 | 이력해설 | 다운로드 (14.1 KB)

1
# coding: utf-8
2

    
3
import os.path
4
try:
5
    from PyQt5.QtCore import Qt, QRectF, pyqtSignal, QT_VERSION_STR, QPointF
6
    from PyQt5.QtGui import QImage, QPixmap, QPainterPath, QPainter, QCursor, QPen, QBrush, QColor
7
    from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QFileDialog
8
except ImportError:
9
    try:
10
        from PyQt4.QtCore import *
11
        from PyQt4.QtGui import *
12
    except ImportError:
13
        raise ImportError("ImageViewerQt: Requires PyQt5 or PyQt4.")
14

    
15

    
16
__author__ = "Marcel Goldschen-Ohm <marcel.goldschen@gmail.com>"
17
__version__ = '0.9.0'
18

    
19

    
20
class QtImageViewer(QGraphicsView):
21
    """ PyQt image viewer widget for a QPixmap in a QGraphicsView scene with mouse zooming and panning.
22
    Displays a QImage or QPixmap (QImage is internally converted to a QPixmap).
23
    To display any other image format, you must first convert it to a QImage or QPixmap.
24
    Some useful image format conversion utilities:
25
        qimage2ndarray: NumPy ndarray <==> QImage    (https://github.com/hmeine/qimage2ndarray)
26
        ImageQt: PIL Image <==> QImage  (https://github.com/python-pillow/Pillow/blob/master/PIL/ImageQt.py)
27
    Mouse interaction:
28
        Left mouse button drag: Pan image.
29
        Right mouse button drag: Zoom box.
30
        Right mouse button doubleclick: Zoom to show entire image.
31
    """
32

    
33
    # Mouse button signals emit image scene (x, y) coordinates.
34
    # !!! For image (row, column) matrix indexing, row = y and column = x.
35
    leftMouseButtonPressed = pyqtSignal(float, float)
36
    rightMouseButtonPressed = pyqtSignal(float, float)
37
    leftMouseButtonMoved = pyqtSignal(float, float)
38
    rightMouseButtonMoved = pyqtSignal(float, float)
39
    leftMouseButtonReleased = pyqtSignal(float, float)
40
    rightMouseButtonReleased = pyqtSignal(float, float)
41
    leftMouseButtonDoubleClicked = pyqtSignal(float, float)
42
    rightMouseButtonDoubleClicked = pyqtSignal(float, float)
43

    
44
    def __init__(self):
45
        QGraphicsView.__init__(self)
46

    
47
        # Image is displayed as a QPixmap in a QGraphicsScene attached to this QGraphicsView.
48
        self.command = None
49
        self.scene = QGraphicsScene()
50
        self.setScene(self.scene)
51
        self.scene.setBackgroundBrush(Qt.gray)
52
        self.crosshairPos = None
53

    
54
        self.isPressCtrl = False
55
        self.scaleFactor = 1.0
56
        self.numScheduledScalings = 0
57
        self.isOriginalPointSelected = False
58
        #self.currentMenuTool = self.MENU_HAND_TOOL
59

    
60
        # Store a local handle to the scene's current image pixmap.
61
        self._pixmapHandle = None
62

    
63
        # Image aspect ratio mode.
64
        # !!! ONLY applies to full image. Aspect ratio is always ignored when zooming.
65
        #   Qt.IgnoreAspectRatio: Scale image to fit viewport.
66
        #   Qt.KeepAspectRatio: Scale image to fit inside viewport, preserving aspect ratio.
67
        #   Qt.KeepAspectRatioByExpanding: Scale image to fill the viewport, preserving aspect ratio.
68
        self.aspectRatioMode = Qt.KeepAspectRatio
69

    
70
        # Scroll bar behaviour.
71
        #   Qt.ScrollBarAlwaysOff: Never shows a scroll bar.
72
        #   Qt.ScrollBarAlwaysOn: Always shows a scroll bar.
73
        #   Qt.ScrollBarAsNeeded: Shows a scroll bar only when zoomed.
74
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
75
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
76

    
77
        # Stack of QRectF zoom boxes in scene coordinates.
78
        self.zoomStack = []
79

    
80
        self.setRenderHint(QPainter.Antialiasing)
81

    
82
        # Flags for enabling/disabling mouse interaction.
83
        self.canZoom = True
84
        self.canPan = True
85
        self.setMouseTracking(True)
86
        self.command = None
87

    
88
    def hasImage(self):
89
        """ Returns whether or not the scene contains an image pixmap.
90
        """
91
        return self._pixmapHandle is not None
92

    
93
    def clearImage(self):
94
        """ Removes the current image pixmap from the scene if it exists.
95
        """
96
        if self.hasImage():
97
            self.scene.removeItem(self._pixmapHandle)
98
            self._pixmapHandle = None
99

    
100
    def pixmap(self):
101
        """ Returns the scene's current image pixmap as a QPixmap, or else None if no image exists.
102
        :rtype: QPixmap | None
103
        """
104
        if self.hasImage():
105
            return self._pixmapHandle.pixmap()
106
        return None
107

    
108
    def image(self):
109
        """ Returns the scene's current image pixmap as a QImage, or else None if no image exists.
110
        :rtype: QImage | None
111
        """
112
        if self.hasImage():
113
            return self._pixmapHandle.pixmap().toImage()
114
        return None
115

    
116
    def setImage(self, image):
117
        """ Set the scene's current image pixmap to the input QImage or QPixmap.
118
        Raises a RuntimeError if the input image has type other than QImage or QPixmap.
119
        :type image: QImage | QPixmap
120
        """
121
        if type(image) is QPixmap:
122
            pixmap = image
123
        elif type(image) is QImage:
124
            pixmap = QPixmap.fromImage(image)
125
        else:
126
            raise RuntimeError("ImageViewer.setImage: Argument must be a QImage or QPixmap.")
127
        if self.hasImage():
128
            self._pixmapHandle.setPixmap(pixmap)
129
        else:
130
            self._pixmapHandle = self.scene.addPixmap(pixmap)
131

    
132
        self.setSceneRect(QRectF(pixmap.rect()))  # Set scene size to image size.
133
        self.updateViewer()
134

    
135
    def loadImageFromFile(self, fileName=""):
136
        """ Load an image from file.
137
        Without any arguments, loadImageFromFile() will popup a file dialog to choose the image file.
138
        With a fileName argument, loadImageFromFile(fileName) will attempt to load the specified image file directly.
139
        """
140

    
141
        if len(fileName) == 0:
142
            options = QFileDialog.Options()
143
            options |= QFileDialog.DontUseNativeDialog
144
            if QT_VERSION_STR[0] == '4':
145
                fileName = QFileDialog.getOpenFileName(self, "Open image file", os.getcwd(), "Image files(*.png *.jpg)", options=options)
146
            elif QT_VERSION_STR[0] == '5':
147
                fileName, dummy = QFileDialog.getOpenFileName(self, "Open image file", os.getcwd(), "Image files(*.png *.jpg)", options=options)
148
        if len(fileName) and os.path.isfile(fileName):
149
            image = QImage(fileName)
150
            self.setImage(image)
151

    
152
        return fileName
153

    
154
    def updateViewer(self):
155
        """ Show current zoom (if showing entire image, apply current aspect ratio mode).
156
        """
157
        if not self.hasImage():
158
            return
159
        if len(self.zoomStack) and self.sceneRect().contains(self.zoomStack[-1]):
160
            self.fitInView(self.zoomStack[-1], Qt.KeepAspectRatioByExpanding)  # Show zoomed rect (ignore aspect ratio).
161
        else:
162
            self.zoomStack = []  # Clear the zoom stack (in case we got here because of an invalid zoom).
163
            self.fitInView(self.sceneRect(), self.aspectRatioMode)  # Show entire image (use current aspect ratio mode).
164

    
165
    def zoomImageInit(self):
166
        if self.hasImage():
167
            self.zoomStack = []
168
            self.updateViewer()
169
            self.setCursor(QCursor(Qt.ArrowCursor))
170

    
171
    def zoomImage(self, isZoomIn, event):
172
        """ Zoom in & out
173
        """
174
        clickPos = event.pos()
175
        scenePos1 = self.mapToScene(clickPos.x() - 300, clickPos.y() - 300)
176
        scenePos2 = self.mapToScene(clickPos.x() + 300, clickPos.y() + 300)
177
        if isZoomIn:
178
            print("zoom in")
179
            zoomArea = QRectF(QPointF(scenePos1.x(), scenePos1.y()), QPointF(scenePos2.x(), scenePos2.y()))
180
            viewBBox = self.zoomStack[-1] if len(self.zoomStack) else self.sceneRect()
181
            selectionBBox = zoomArea.intersected(viewBBox)
182
            self.scene.setSelectionArea(QPainterPath())  # Clear current selection area.
183
            if selectionBBox.isValid() and (selectionBBox != viewBBox):
184
                self.zoomStack.append(selectionBBox)
185
                self.updateViewer()
186
        else:
187
            print("zoom out")
188
            self.scene.setSelectionArea(QPainterPath())  # Clear current selection area.
189
            if len(self.zoomStack):
190
                self.zoomStack.pop()
191
            self.updateViewer()
192

    
193
    def resizeEvent(self, event):
194
        """ Maintain current zoom on resize.
195
        """
196
        self.updateViewer()
197

    
198
    #'''
199
    #    @brief draw something event
200
    #'''
201
    #def paintEvent(self, event):
202
    #    if self.command is not None:
203
    #        scenePos = self.mapToScene(event.pos())
204
    #        self.command.execute(['paintEvent', event, scenePos])
205
    #        if self.command.isTreated == True: return
206

    
207
    '''
208
        @brief  mouse move event
209
    '''
210
    def mouseMoveEvent(self, event):
211
        if self.command is not None:
212
            scenePos = self.mapToScene(event.pos())
213
            self.command.execute(['mouseMoveEvent', event, scenePos])
214
            if self.command.isTreated == True: return
215

    
216
        QGraphicsView.mouseMoveEvent(self, event)
217

    
218
    def mousePressEvent(self, event):
219
        """ Start mouse pan or zoom mode.
220
        """
221
        
222
        if self.command is not None:
223
            scenePos = self.mapToScene(event.pos())
224
            self.command.execute(['mousePressEvent', event, scenePos])
225
            if self.command.isTreated == True: return
226

    
227
        QGraphicsView.mousePressEvent(self, event)
228

    
229
    def mouseReleaseEvent(self, event):
230
        """ Stop mouse pan or zoom mode (apply zoom if valid).
231
        """
232

    
233
        if self.command is not None:
234
            scenePos = self.mapToScene(event.pos())
235
            instance = self.command.execute(['mouseReleaseEvent', event, scenePos])
236
            if instance is not None:
237
                self.scene.addItem(instance)
238
                if self.command.isTreated == True: return
239
        
240
        QGraphicsView.mouseReleaseEvent(self, event)
241

    
242
    def mouseDoubleClickEvent(self, event):
243
        """ Show entire image.
244
        """
245
        scenePos = self.mapToScene(event.pos())
246
        if event.button() == Qt.LeftButton:
247
            self.leftMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
248
        elif event.button() == Qt.RightButton:
249
            if self.canZoom:
250
                self.zoomStack = []  # Clear zoom stack.
251
                self.updateViewer()
252
            self.rightMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
253
        QGraphicsView.mouseDoubleClickEvent(self, event)
254

    
255
    def keyPressEvent(self, event):
256
        if event.key() == Qt.Key_Control:
257
            self.isPressCtrl = True
258
            print("Pressed Ctrl")
259

    
260
    def keyReleaseEvent(self, event):
261
        if event.key() == Qt.Key_Control:
262
            self.isPressCtrl = False
263
            print("Released Ctrl")
264

    
265
    def wheelEvent(self, event):
266
        if self.isPressCtrl == True:
267
            print("Pressed Ctrl and Mouse Wheel")
268
            if self.canZoom and self.hasImage():
269
                numDegrees = event.angleDelta() / 8
270
                if numDegrees is not None:
271
                    if numDegrees.y() > 0:
272
                        self.zoomImage(True, event)
273
                    elif numDegrees.y() < 0:
274
                        self.zoomImage(False, event)
275
                #print("Zoomable")
276
                #numDegrees = event.angleDelta().y() // 8
277
                #numSteps = numDegrees // 15
278
                #self.numScheduledScalings = self.numScheduledScalings + numSteps
279
                #if self.numScheduledScalings * numSteps < 0:
280
                #    self.numScheduledScalings = numSteps
281
                #self.scaleFactor = 1.0 + (self.numScheduledScalings / 300.0)
282
                #print("scaleFactor : " + str(self.scaleFactor))
283
                #self.scale(self.scaleFactor, self.scaleFactor)
284
        else:
285
            super().wheelEvent(event)
286

    
287
    def drawForeground(self, painter, rect):
288
        image = self.image()
289
        if image is not None:
290
            width = image.width()
291
            height = image.height()
292
            if self.crosshairPos is not None:
293
                pen = QPen()
294
                pen.setColor(QColor(180, 180, 180))
295
                pen.setStyle(Qt.DashLine)
296
                pen.setWidthF(0.3)
297
                painter.setPen(pen)
298
                painter.drawLine(self.crosshairPos.x(), 0, self.crosshairPos.x(), height)#Vertical
299
                painter.drawLine(0, self.crosshairPos.y(), width, self.crosshairPos.y())#Horizontal
300
            #else:
301
            #    painter.eraseRect(QRectF(0, 0, width, height))
302
            
303
    '''
304
        @brief  override paint event
305
    '''
306
    def paintEvent(self, event):
307
        try:
308
            super(QtImageViewer, self).paintEvent(event)
309

    
310
            #if (self.command is not None) and ('CreateCommand' == self.command.__class__.__name__):
311
            #    self.command.template.paint(QPainter(self))
312
        except Exception as ex:
313
            print('Error occurs', ex)
314

    
315
    GUIDELINE_ITEMS = []
316
    def showGuideline(self, isShow):
317
        image = self.image()
318
        width = image.width()
319
        height = image.height()
320
        pen = QPen()
321
        pen.setColor(QColor(180, 180, 180))
322
        pen.setStyle(Qt.DashLine)
323
        pen.setWidthF(0.5)
324
        if isShow:
325
            verticalLine = self.scene.addLine(width/2, 0, width/2, height, pen)
326
            horizontalLine = self.scene.addLine(0, height/2, width, height/2, pen)
327
            self.GUIDELINE_ITEMS.append(verticalLine)
328
            self.GUIDELINE_ITEMS.append(horizontalLine)
329
            self.scene.addItem(verticalLine)
330
            self.scene.addItem(horizontalLine)
331
        else:
332
            for item in self.GUIDELINE_ITEMS:
333
                self.scene.removeItem(item)
334

    
335

    
336
if __name__ == '__main__':
337
    import sys
338
    try:
339
        from PyQt5.QtWidgets import QApplication
340
    except ImportError:
341
        try:
342
            from PyQt4.QtGui import QApplication
343
        except ImportError:
344
            raise ImportError("ImageViewerQt: Requires PyQt5 or PyQt4.")
345
    print('Using Qt ' + QT_VERSION_STR)
346

    
347
    def handleLeftClick(x, y):
348
        row = int(y)
349
        column = int(x)
350
        print("Clicked on image pixel (row="+str(row)+", column="+str(column)+")")
351

    
352
    # Create the application.
353
    app = QApplication(sys.argv)
354

    
355
    # Create image viewer and load an image file to display.
356
    viewer = QtImageViewer()
357
    viewer.loadImageFromFile()  # Pops up file dialog.
358

    
359
    # Handle left mouse clicks with custom slot.
360
    viewer.leftMouseButtonPressed.connect(handleLeftClick)
361

    
362
    # Show viewer and run application.
363
    viewer.show()
364
    sys.exit(app.exec_())