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