프로젝트

일반

사용자정보

통계
| 개정판:

hytos / DTI_PID / DTI_PID / QtImageViewer.py @ 0bcf00da

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

1
# coding: utf-8
2
import sys
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
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '\\Commands')
16
import DefaultCommand
17

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

    
21

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

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

    
46
    def __init__(self):
47
        QGraphicsView.__init__(self)
48

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

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

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

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

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

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

    
82
        self.setRenderHint(QPainter.Antialiasing)
83

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

    
90
    '''
91
        @brief      Use Default ImageViewer Command
92
        @author     Jeongwoo
93
        @date       18.04.10
94
        @history    .
95
    '''
96
    def useDefaultCommand(self):
97
        """ Use Default Command
98
        """
99
        self.command = DefaultCommand.DefaultCommand(self)
100

    
101
    def hasImage(self):
102
        """ Returns whether or not the scene contains an image pixmap.
103
        """
104
        return self._pixmapHandle is not None
105

    
106
    def clearImage(self):
107
        """ Removes the current image pixmap from the scene if it exists.
108
        """
109
        if self.hasImage():
110
            self.scene.removeItem(self._pixmapHandle)
111
            self._pixmapHandle = None
112

    
113
    def pixmap(self):
114
        """ Returns the scene's current image pixmap as a QPixmap, or else None if no image exists.
115
        :rtype: QPixmap | None
116
        """
117
        if self.hasImage():
118
            return self._pixmapHandle.pixmap()
119
        return None
120

    
121
    def image(self):
122
        """ Returns the scene's current image pixmap as a QImage, or else None if no image exists.
123
        :rtype: QImage | None
124
        """
125
        if self.hasImage():
126
            return self._pixmapHandle.pixmap().toImage()
127
        return None
128

    
129
    def setImage(self, image):
130
        """ Set the scene's current image pixmap to the input QImage or QPixmap.
131
        Raises a RuntimeError if the input image has type other than QImage or QPixmap.
132
        :type image: QImage | QPixmap
133
        """
134
        if type(image) is QPixmap:
135
            pixmap = image
136
        elif type(image) is QImage:
137
            pixmap = QPixmap.fromImage(image)
138
        else:
139
            raise RuntimeError("ImageViewer.setImage: Argument must be a QImage or QPixmap.")
140
        if self.hasImage():
141
            self._pixmapHandle.setPixmap(pixmap)
142
        else:
143
            self._pixmapHandle = self.scene.addPixmap(pixmap)
144

    
145
        self.setSceneRect(QRectF(pixmap.rect()))  # Set scene size to image size.
146
        self.updateViewer()
147

    
148
    def loadImageFromFile(self, fileName=""):
149
        """ Load an image from file.
150
        Without any arguments, loadImageFromFile() will popup a file dialog to choose the image file.
151
        With a fileName argument, loadImageFromFile(fileName) will attempt to load the specified image file directly.
152
        """
153

    
154
        if len(fileName) == 0:
155
            options = QFileDialog.Options()
156
            options |= QFileDialog.DontUseNativeDialog
157
            if QT_VERSION_STR[0] == '4':
158
                fileName = QFileDialog.getOpenFileName(self, "Open image file", os.getcwd(), "Image files(*.png *.jpg)", options=options)
159
            elif QT_VERSION_STR[0] == '5':
160
                fileName, dummy = QFileDialog.getOpenFileName(self, "Open image file", os.getcwd(), "Image files(*.png *.jpg)", options=options)
161
        if len(fileName) and os.path.isfile(fileName):
162
            image = QImage(fileName)
163
            self.setImage(image)
164

    
165
        return fileName
166

    
167
    def updateViewer(self):
168
        """ Show current zoom (if showing entire image, apply current aspect ratio mode).
169
        """
170
        if not self.hasImage():
171
            return
172
        if len(self.zoomStack) and self.sceneRect().contains(self.zoomStack[-1]):
173
            self.fitInView(self.zoomStack[-1], Qt.KeepAspectRatioByExpanding)  # Show zoomed rect (ignore aspect ratio).
174
        else:
175
            self.zoomStack = []  # Clear the zoom stack (in case we got here because of an invalid zoom).
176
            self.fitInView(self.sceneRect(), self.aspectRatioMode)  # Show entire image (use current aspect ratio mode).
177

    
178
    def zoomImageInit(self):
179
        if self.hasImage():
180
            self.zoomStack = []
181
            self.updateViewer()
182
            self.setCursor(QCursor(Qt.ArrowCursor))
183

    
184
    '''
185
        @brief      Zoom in & out image
186
        @author     Jeongwoo
187
        @date       -
188
        @history    18.04.11    Jeongwoo    add parameter 'adjust' (@ref QResultTreeWidget.itemClickEvent(self, item, columnNo))
189
    '''
190
    def zoomImage(self, isZoomIn, event, adjust = 1):
191
        """ Zoom in & out
192
        """
193
        HALF_SIZE = 300
194
        clickPos = event.pos()
195
        scenePos1 = self.mapToScene(clickPos.x() - HALF_SIZE//adjust, clickPos.y() - HALF_SIZE//adjust)
196
        scenePos2 = self.mapToScene(clickPos.x() + HALF_SIZE//adjust, clickPos.y() + HALF_SIZE//adjust)
197
        if isZoomIn:
198
            print("zoom in")
199
            zoomArea = QRectF(QPointF(scenePos1.x() if scenePos1.x() > 0 else 0, scenePos1.y() if scenePos1.y() > 0 else 0), QPointF(scenePos2.x(), scenePos2.y()))
200
            #self.fitInView(zoomArea, Qt.KeepAspectRatioByExpanding)
201
            viewBBox = self.zoomStack[-1] if len(self.zoomStack) else self.sceneRect()
202
            selectionBBox = zoomArea.intersected(viewBBox)
203
            self.scene.setSelectionArea(QPainterPath())  # Clear current selection area.
204
            if selectionBBox.width() > HALF_SIZE*2 and selectionBBox.height() > HALF_SIZE*2:
205
                if selectionBBox.isValid() and (selectionBBox != viewBBox):
206
                    self.zoomStack.append(selectionBBox)
207
                    self.updateViewer()
208
        else:
209
            print("zoom out")
210
            self.scene.setSelectionArea(QPainterPath())  # Clear current selection area.
211
            if len(self.zoomStack):
212
                self.zoomStack.pop()
213
            self.updateViewer()
214

    
215
    def resizeEvent(self, event):
216
        """ Maintain current zoom on resize.
217
        """
218
        self.updateViewer()
219

    
220
    #'''
221
    #    @brief draw something event
222
    #'''
223
    #def paintEvent(self, event):
224
    #    if self.command is not None:
225
    #        scenePos = self.mapToScene(event.pos())
226
    #        self.command.execute(['paintEvent', event, scenePos])
227
    #        if self.command.isTreated == True: return
228

    
229
    '''
230
        @brief  mouse move event
231
    '''
232
    def mouseMoveEvent(self, event):
233
        if self.command is not None:
234
            scenePos = self.mapToScene(event.pos())
235
            self.command.execute(['mouseMoveEvent', event, scenePos])
236
            if self.command.isTreated == True: return
237

    
238
        QGraphicsView.mouseMoveEvent(self, event)
239

    
240
    def mousePressEvent(self, event):
241
        """ Start mouse pan or zoom mode.
242
        """
243
        
244
        if self.command is not None:
245
            scenePos = self.mapToScene(event.pos())
246
            self.command.execute(['mousePressEvent', event, scenePos])
247
            if self.command.isTreated == True: return
248

    
249
        QGraphicsView.mousePressEvent(self, event)
250

    
251
    def mouseReleaseEvent(self, event):
252
        """ Stop mouse pan or zoom mode (apply zoom if valid).
253
        """
254

    
255
        if self.command is not None:
256
            scenePos = self.mapToScene(event.pos())
257
            instance = self.command.execute(['mouseReleaseEvent', event, scenePos])
258
            if instance is not None:
259
                self.scene.addItem(instance)
260
                if self.command.isTreated == True: return
261
        
262
        QGraphicsView.mouseReleaseEvent(self, event)
263

    
264
    def mouseDoubleClickEvent(self, event):
265
        """ Show entire image.
266
        """
267
        scenePos = self.mapToScene(event.pos())
268
        if event.button() == Qt.LeftButton:
269
            self.leftMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
270
        elif event.button() == Qt.RightButton:
271
            if self.canZoom:
272
                self.zoomStack = []  # Clear zoom stack.
273
                self.updateViewer()
274
            self.rightMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
275
        QGraphicsView.mouseDoubleClickEvent(self, event)
276

    
277
    def keyPressEvent(self, event):
278
        if event.key() == Qt.Key_Control:
279
            self.isPressCtrl = True
280
            print("Pressed Ctrl")
281

    
282
    def keyReleaseEvent(self, event):
283
        if event.key() == Qt.Key_Control:
284
            self.isPressCtrl = False
285
            print("Released Ctrl")
286

    
287
    def wheelEvent(self, event):
288
        if self.isPressCtrl == True:
289
            print("Pressed Ctrl and Mouse Wheel")
290
            if self.canZoom and self.hasImage():
291
                numDegrees = event.angleDelta() / 8
292
                if numDegrees is not None:
293
                    if numDegrees.y() > 0:
294
                        self.zoomImage(True, event)
295
                    elif numDegrees.y() < 0:
296
                        self.zoomImage(False, event)
297
                #print("Zoomable")
298
                #numDegrees = event.angleDelta().y() // 8
299
                #numSteps = numDegrees // 15
300
                #self.numScheduledScalings = self.numScheduledScalings + numSteps
301
                #if self.numScheduledScalings * numSteps < 0:
302
                #    self.numScheduledScalings = numSteps
303
                #self.scaleFactor = 1.0 + (self.numScheduledScalings / 300.0)
304
                #print("scaleFactor : " + str(self.scaleFactor))
305
                #self.scale(self.scaleFactor, self.scaleFactor)
306
        else:
307
            super().wheelEvent(event)
308

    
309
    def drawForeground(self, painter, rect):
310
        image = self.image()
311
        if image is not None:
312
            width = image.width()
313
            height = image.height()
314
            if self.crosshairPos is not None:
315
                pen = QPen()
316
                pen.setColor(QColor(180, 180, 180))
317
                pen.setStyle(Qt.DashLine)
318
                pen.setWidthF(0.3)
319
                painter.setPen(pen)
320
                painter.drawLine(self.crosshairPos.x(), 0, self.crosshairPos.x(), height)#Vertical
321
                painter.drawLine(0, self.crosshairPos.y(), width, self.crosshairPos.y())#Horizontal
322
            #else:
323
            #    painter.eraseRect(QRectF(0, 0, width, height))
324
                        
325
    #'''
326
    #    @brief  override paint event
327
    #'''
328
    #def paintEvent(self, event):
329
    #    try:
330
    #        super(QtImageViewer, self).paintEvent(event)
331

    
332
    #        #if (self.command is not None) and ('CreateCommand' == self.command.__class__.__name__):
333
    #        #    self.command.template.paint(QPainter(self))
334
    #    except Exception as ex:
335
    #        print('Error occurs', ex)
336

    
337
    GUIDELINE_ITEMS = []
338
    def showGuideline(self, isShow):
339
        image = self.image()
340
        width = image.width()
341
        height = image.height()
342
        pen = QPen()
343
        pen.setColor(QColor(180, 180, 180))
344
        pen.setStyle(Qt.DashLine)
345
        pen.setWidthF(0.5)
346
        if isShow:
347
            verticalLine = self.scene.addLine(width/2, 0, width/2, height, pen)
348
            horizontalLine = self.scene.addLine(0, height/2, width, height/2, pen)
349
            self.GUIDELINE_ITEMS.append(verticalLine)
350
            self.GUIDELINE_ITEMS.append(horizontalLine)
351
            self.scene.addItem(verticalLine)
352
            self.scene.addItem(horizontalLine)
353
        else:
354
            for item in self.GUIDELINE_ITEMS:
355
                self.scene.removeItem(item)
356

    
357

    
358
if __name__ == '__main__':
359
    import sys
360
    try:
361
        from PyQt5.QtWidgets import QApplication
362
    except ImportError:
363
        try:
364
            from PyQt4.QtGui import QApplication
365
        except ImportError:
366
            raise ImportError("ImageViewerQt: Requires PyQt5 or PyQt4.")
367
    print('Using Qt ' + QT_VERSION_STR)
368

    
369
    def handleLeftClick(x, y):
370
        row = int(y)
371
        column = int(x)
372
        print("Clicked on image pixel (row="+str(row)+", column="+str(column)+")")
373

    
374
    # Create the application.
375
    app = QApplication(sys.argv)
376

    
377
    # Create image viewer and load an image file to display.
378
    viewer = QtImageViewer()
379
    viewer.loadImageFromFile()  # Pops up file dialog.
380

    
381
    # Handle left mouse clicks with custom slot.
382
    viewer.leftMouseButtonPressed.connect(handleLeftClick)
383

    
384
    # Show viewer and run application.
385
    viewer.show()
386
    sys.exit(app.exec_())