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_()) |