1 # Copyright (c) 2018 David Aguilar <davvid@gmail.com>
3 # Git Cola is GPL licensed, but this file has a more permissive license.
4 # This file is dual-licensed Git Cola GPL + pyqimageview MIT.
5 # imageview.py was originally based on the pyqimageview:
6 # https://github.com/nevion/pyqimageview/
8 # The MIT License (MIT)
10 # Copyright (c) 2014 Jason Newton <nevion@gmail.com>
12 # Permission is hereby granted, free of charge, to any person obtaining a copy
13 # of this software and associated documentation files (the "Software"), to deal
14 # in the Software without restriction, including without limitation the rights
15 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 # copies of the Software, and to permit persons to whom the Software is
17 # furnished to do so, subject to the following conditions:
19 # The above copyright notice and this permission notice shall be included in
20 # all copies or substantial portions of the Software.
22 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29 from __future__
import absolute_import
, division
, print_function
, unicode_literals
34 from qtpy
import QtCore
35 from qtpy
import QtGui
36 from qtpy
import QtWidgets
37 from qtpy
.QtCore
import Qt
38 from qtpy
.QtCore
import Signal
47 from .. import qtcompat
53 return max(min(x
, hi
), lo
)
56 class ImageView(QtWidgets
.QGraphicsView
):
57 image_changed
= Signal()
59 def __init__(self
, parent
=None):
60 super(ImageView
, self
).__init
__(parent
)
62 scene
= QtWidgets
.QGraphicsScene(self
)
63 self
.graphics_pixmap
= QtWidgets
.QGraphicsPixmapItem()
64 scene
.addItem(self
.graphics_pixmap
)
67 self
.zoom_factor
= 1.125
68 self
.rubberband
= None
70 self
.first_show_occured
= False
71 self
.last_scene_roi
= None
72 self
.start_drag
= QtCore
.QPoint()
73 self
.checkerboard
= None
78 check_pattern
= QtGui
.QImage(
79 CHECK_MEDIUM
* 2, CHECK_MEDIUM
* 2, QtGui
.QImage
.Format_RGB888
81 color_gray
= QtGui
.QColor(CHECK_GRAY
, CHECK_GRAY
, CHECK_GRAY
)
82 color_light
= QtGui
.QColor(CHECK_LIGHT
, CHECK_LIGHT
, CHECK_LIGHT
)
83 painter
= QtGui
.QPainter(check_pattern
)
84 painter
.fillRect(0, 0, CHECK_MEDIUM
, CHECK_MEDIUM
, color_gray
)
86 CHECK_MEDIUM
, CHECK_MEDIUM
, CHECK_MEDIUM
, CHECK_MEDIUM
, color_gray
88 painter
.fillRect(0, CHECK_MEDIUM
, CHECK_MEDIUM
, CHECK_MEDIUM
, color_light
)
89 painter
.fillRect(CHECK_MEDIUM
, 0, CHECK_MEDIUM
, CHECK_MEDIUM
, color_light
)
90 self
.check_pattern
= check_pattern
91 self
.check_brush
= QtGui
.QBrush(check_pattern
)
93 def load(self
, filename
):
94 image
= QtGui
.QImage()
96 ok
= not image
.isNull()
103 return self
.graphics_pixmap
.pixmap()
106 def pixmap(self
, image
, image_format
=None):
108 if have_numpy
and isinstance(image
, np
.ndarray
):
110 if image
.shape
[2] == 3:
111 if image_format
is None:
112 image_format
= QtGui
.QImage
.Format_RGB888
113 q_image
= QtGui
.QImage(
114 image
.data
, image
.shape
[1], image
.shape
[0], image_format
116 pixmap
= QtGui
.QPixmap
.fromImage(q_image
)
117 elif image
.shape
[2] == 4:
118 if image_format
is None:
119 image_format
= QtGui
.QImage
.Format_RGB32
120 q_image
= QtGui
.QImage(
121 image
.data
, image
.shape
[1], image
.shape
[0], image_format
123 pixmap
= QtGui
.QPixmap
.fromImage(q_image
)
125 raise TypeError(image
)
126 elif image
.ndim
== 2:
127 if image_format
is None:
128 image_format
= QtGui
.QImage
.Format_RGB888
129 q_image
= QtGui
.QImage(
130 image
.data
, image
.shape
[1], image
.shape
[0], image_format
132 pixmap
= QtGui
.QPixmap
.fromImage(q_image
)
134 raise ValueError(image
)
136 elif isinstance(image
, QtGui
.QImage
):
137 pixmap
= QtGui
.QPixmap
.fromImage(image
)
138 elif isinstance(image
, QtGui
.QPixmap
):
141 raise TypeError(image
)
143 if pixmap
.hasAlpha():
144 checkerboard
= QtGui
.QImage(
145 pixmap
.width(), pixmap
.height(), QtGui
.QImage
.Format_ARGB32
147 self
.checkerboard
= checkerboard
148 painter
= QtGui
.QPainter(checkerboard
)
149 painter
.fillRect(checkerboard
.rect(), self
.check_brush
)
150 painter
.drawPixmap(0, 0, pixmap
)
151 pixmap
= QtGui
.QPixmap
.fromImage(checkerboard
)
153 self
.graphics_pixmap
.setPixmap(pixmap
)
154 self
.update_scene_rect()
155 self
.fitInView(self
.image_scene_rect
, flags
=Qt
.KeepAspectRatio
)
156 self
.graphics_pixmap
.update()
157 self
.image_changed
.emit()
159 # image property alias
165 def image(self
, image
):
168 def update_scene_rect(self
):
172 QtCore
.QPointF(0, 0), QtCore
.QPointF(pixmap
.width(), pixmap
.height())
177 def image_scene_rect(self
):
178 return QtCore
.QRectF(
179 self
.graphics_pixmap
.pos(), QtCore
.QSizeF(self
.pixmap
.size())
182 def resizeEvent(self
, event
):
183 super(ImageView
, self
).resizeEvent(event
)
184 self
.update_scene_rect()
186 self
.fitInView(self
.last_scene_roi
, Qt
.KeepAspectRatio
)
189 def zoomROICentered(self
, p
, zoom_level_delta
):
190 roi
= self
.current_scene_ROI
193 roi_dims
= QtCore
.QPointF(roi
.width(), roi
.height())
196 if zoom_level_delta
> 0:
197 roi_scalef
= 1.0 / self
.zoom_factor
198 elif zoom_level_delta
< 0:
199 roi_scalef
= self
.zoom_factor
201 nroi_dims
= roi_dims
* roi_scalef
202 nroi_dims
.setX(max(nroi_dims
.x(), 1))
203 nroi_dims
.setY(max(nroi_dims
.y(), 1))
206 nroi_dimsh
= nroi_dims
/ 2.0
207 nroi_topleft
= nroi_center
- nroi_dimsh
208 nroi
= QtCore
.QRectF(
209 nroi_topleft
.x(), nroi_topleft
.y(), nroi_dims
.x(), nroi_dims
.y()
211 self
.fitInView(nroi
, Qt
.KeepAspectRatio
)
214 def zoomROITo(self
, p
, zoom_level_delta
):
215 roi
= self
.current_scene_ROI
218 roi_dims
= QtCore
.QPointF(roi
.width(), roi
.height())
219 roi_topleft
= roi
.topLeft()
222 if zoom_level_delta
> 0:
223 roi_scalef
= 1.0 / self
.zoom_factor
224 elif zoom_level_delta
< 0:
225 roi_scalef
= self
.zoom_factor
227 nroi_dims
= roi_dims
* roi_scalef
228 nroi_dims
.setX(max(nroi_dims
.x(), 1.0))
229 nroi_dims
.setY(max(nroi_dims
.y(), 1.0))
231 prel_scaled_x
= (p
.x() - roi_topleft
.x()) / roi_dims
.x()
232 prel_scaled_y
= (p
.y() - roi_topleft
.y()) / roi_dims
.y()
233 nroi_topleft_x
= p
.x() - prel_scaled_x
* nroi_dims
.x()
234 nroi_topleft_y
= p
.y() - prel_scaled_y
* nroi_dims
.y()
236 nroi
= QtCore
.QRectF(
237 nroi_topleft_x
, nroi_topleft_y
, nroi_dims
.x(), nroi_dims
.y()
239 self
.fitInView(nroi
, Qt
.KeepAspectRatio
)
242 def _scene_ROI(self
, geometry
):
243 return QtCore
.QRectF(
244 self
.mapToScene(geometry
.topLeft()), self
.mapToScene(geometry
.bottomRight())
248 def current_scene_ROI(self
):
249 return self
.last_scene_roi
251 def mousePressEvent(self
, event
):
252 super(ImageView
, self
).mousePressEvent(event
)
253 button
= event
.button()
254 modifier
= event
.modifiers()
257 if modifier
== Qt
.ControlModifier
and button
== Qt
.LeftButton
:
258 self
.start_drag
= event
.pos()
261 # initiate/show ROI selection
262 if modifier
== Qt
.ShiftModifier
and button
== Qt
.LeftButton
:
263 self
.start_drag
= event
.pos()
264 if self
.rubberband
is None:
265 self
.rubberband
= QtWidgets
.QRubberBand(
266 QtWidgets
.QRubberBand
.Rectangle
, self
.viewport()
268 self
.rubberband
.setGeometry(QtCore
.QRect(self
.start_drag
, QtCore
.QSize()))
269 self
.rubberband
.show()
271 def mouseMoveEvent(self
, event
):
272 super(ImageView
, self
).mouseMoveEvent(event
)
273 # update selection display
274 if self
.rubberband
is not None:
275 self
.rubberband
.setGeometry(
276 QtCore
.QRect(self
.start_drag
, event
.pos()).normalized()
280 end_drag
= event
.pos()
281 pan_vector
= end_drag
- self
.start_drag
282 scene2view
= self
.transform()
284 sx
= scene2view
.m11()
285 sy
= scene2view
.m22()
286 scene_pan_x
= pan_vector
.x() / sx
287 scene_pan_y
= pan_vector
.y() / sy
288 scene_pan_vector
= QtCore
.QPointF(scene_pan_x
, scene_pan_y
)
289 roi
= self
.current_scene_ROI
290 top_left
= roi
.topLeft()
291 new_top_left
= top_left
- scene_pan_vector
292 scene_rect
= self
.sceneRect()
294 clamp(new_top_left
.x(), scene_rect
.left(), scene_rect
.right())
297 clamp(new_top_left
.y(), scene_rect
.top(), scene_rect
.bottom())
299 nroi
= QtCore
.QRectF(new_top_left
, roi
.size())
300 self
.fitInView(nroi
, Qt
.KeepAspectRatio
)
301 self
.start_drag
= end_drag
304 def mouseReleaseEvent(self
, event
):
305 super(ImageView
, self
).mouseReleaseEvent(event
)
306 # consume rubber band selection
307 if self
.rubberband
is not None:
308 self
.rubberband
.hide()
311 rect
= self
.rubberband
.geometry().normalized()
313 if rect
.width() > 5 and rect
.height() > 5:
315 self
.mapToScene(rect
.topLeft()), self
.mapToScene(rect
.bottomRight())
317 self
.fitInView(roi
, Qt
.KeepAspectRatio
)
319 self
.rubberband
= None
325 def wheelEvent(self
, event
):
326 delta
= qtcompat
.wheel_delta(event
)
329 scene_pos
= self
.mapToScene(event
.pos())
334 self
.zoomROITo(scene_pos
, sign
)
337 self
.update_scene_rect()
338 self
.fitInView(self
.image_scene_rect
, flags
=Qt
.KeepAspectRatio
)
341 # override arbitrary and unwanted margins:
342 # https://bugreports.qt.io/browse/QTBUG-42331 - based on QT sources
343 def fitInView(self
, rect
, flags
=Qt
.IgnoreAspectRatio
):
344 if self
.scene() is None or not rect
or rect
.isNull():
346 self
.last_scene_roi
= rect
347 unity
= self
.transform().mapRect(QtCore
.QRectF(0.0, 0.0, 1.0, 1.0))
348 self
.scale(1.0 / unity
.width(), 1.0 / unity
.height())
349 viewrect
= self
.viewport().rect()
350 scene_rect
= self
.transform().mapRect(rect
)
351 xratio
= viewrect
.width() / scene_rect
.width()
352 yratio
= viewrect
.height() / scene_rect
.height()
353 if flags
== Qt
.KeepAspectRatio
:
354 xratio
= yratio
= min(xratio
, yratio
)
355 elif flags
== Qt
.KeepAspectRatioByExpanding
:
356 xratio
= yratio
= max(xratio
, yratio
)
357 self
.scale(xratio
, yratio
)
358 self
.centerOn(rect
.center())
361 class AppImageView(ImageView
):
362 def __init__(self
, parent
=None):
363 ImageView
.__init
__(self
, parent
=parent
)
364 self
.main_widget
= None
366 def mousePressEvent(self
, event
):
367 ImageView
.mousePressEvent(self
, event
)
369 def mouseMoveEvent(self
, event
):
370 ImageView
.mouseMoveEvent(self
, event
)
372 scene_pos
= self
.mapToScene(pos
)
373 msg
= 'ui: %d, %d image: %d, %d' % (
379 self
.main_widget
.statusBar().showMessage(msg
)
382 class ImageViewerWindow(QtWidgets
.QMainWindow
):
383 def __init__(self
, image
, input_path
):
384 QtWidgets
.QMainWindow
.__init
__(self
)
386 self
.input_path
= input_path
387 self
.image_view
= AppImageView(parent
=self
)
388 self
.image_view
.main_widget
= self
389 self
.statusBar().showMessage('')
391 padding
= self
.frameGeometry().size() - self
.geometry().size()
392 self
.resize(image
.size() + padding
)
394 central
= QtWidgets
.QWidget(self
)
395 self
.vbox
= QtWidgets
.QVBoxLayout(central
)
396 self
.vbox
.setContentsMargins(0, 0, 0, 0)
397 self
.setCentralWidget(central
)
398 self
.layout().setContentsMargins(0, 0, 0, 0)
400 Expanding
= QtWidgets
.QSizePolicy
.Expanding
401 height_for_width
= self
.image_view
.sizePolicy().hasHeightForWidth()
402 policy
= QtWidgets
.QSizePolicy(Expanding
, Expanding
)
403 policy
.setHorizontalStretch(1)
404 policy
.setVerticalStretch(1)
405 policy
.setHeightForWidth(height_for_width
)
406 self
.image_view
.setSizePolicy(policy
)
408 self
.image_view
.setMouseTracking(True)
409 self
.image_view
.setFocusPolicy(Qt
.NoFocus
)
410 self
.image_view
.setVerticalScrollBarPolicy(Qt
.ScrollBarAlwaysOff
)
411 self
.image_view
.setHorizontalScrollBarPolicy(Qt
.ScrollBarAlwaysOff
)
412 self
.vbox
.addWidget(self
.image_view
)
414 screen
= QtWidgets
.QDesktopWidget().screenGeometry(self
)
415 size
= self
.geometry()
417 (screen
.width() - size
.width()) // 4, (screen
.height() - size
.height()) // 4
421 self
.image_view
.reset()
423 def hideEvent(self
, _event
):
424 QtWidgets
.QMainWindow
.hide(self
)
426 def update_view(self
):
427 self
.image_view
.image
= self
.image
428 self
.setWindowTitle(self
.make_window_title())
430 def make_window_title(self
):
431 return os
.path
.basename(self
.input_path
)
433 def keyPressEvent(self
, event
):
435 global main_loop_type
# pylint: disable=global-statement
436 if key
== Qt
.Key_Escape
:
437 if main_loop_type
== 'qt':
438 QtWidgets
.QApplication
.quit()
439 elif main_loop_type
== 'ipython':
442 # IPython.get_ipython().ask_exit()
445 # pylint: disable=unused-argument
446 def sigint_handler(*args
):
447 """Handler for the SIGINT signal."""
448 sys
.stderr
.write('\r')
449 QtWidgets
.QApplication
.quit()
453 parser
= argparse
.ArgumentParser(description
='image viewer')
454 parser
.add_argument('image', help='path to the image')
455 opts
= parser
.parse_args()
457 input_image
= opts
.image
458 image
= QtGui
.QImage()
459 image
.load(input_image
)
461 app
= QtWidgets
.QApplication(sys
.argv
)
463 import signal
# pylint: disable=all
465 signal
.signal(signal
.SIGINT
, sigint_handler
)
468 window
= ImageViewerWindow(image
, input_image
)
473 if __name__
== '__main__':