こんにちは、タナカです。
今回は、PyQt5で画像に四角形を動的に描画する方法を紹介します。
ここでの動的とは、自分がマウスで選択した位置から好きなサイズで四角形を作れることを言います。
具体的には下図のようにクリックした位置から、クリックを離した範囲で四角形を描画することができます。
この記事の内容
- PyQt5で画像に四角形を描画する方法がわかる
1. ウィンドウに画像を表示する方法
ウィンドウに画像を表示させる方法はpaintEventを使って画像をウィンドウ上に表示させる方法で紹介していますので、詳しい実装方法など参考にしてください。
そちらの記事で作成したコードに追記する形で実装していきます。
2. Shapeクラスの実装
新たに四角形を描画するためのShapeクラスを実装します。Shapeクラスはクリックした位置と離した位置をもとに四角形を描画します。
Shapeクラスのコードはこのようになります。コードの内容はコメントを参照してください。
# 四角を描画するためのShapeクラス
class Shape(object):
def __init__(self):
# 初期値
self.scale = 1.0
self.point_size = 8
self.points = []
self.rectangle_color = QtGui.QColor(0, 255, 0, 128)
# Shapeクラスのインスタンスからリストのように要素を指定して、自分の欲しい情報を取得できる
def __getitem__(self, key):
return self.points[key]
# Canvasで実装したQPainterを受け取って四角を描画する関数
def paint(self, painter):
# pointsがTrueの場合
if self.points:
# paintEventで使用するpenを設定
pen = QtGui.QPen(self.rectangle_color)
pen.setWidth(max(1, int(round(2.0 / self.scale))))
painter.setPen(pen)
# ウィンドウに可視化するためのパス(描画するための道のりやアウトラインのイメージ)
line_path = QtGui.QPainterPath()
vrtx_path = QtGui.QPainterPath()
if len(self.points) == 2:
# 四角形の情報(x, y, w, h)
rectangle = self.getRectFromLine(*self.points)
line_path.addRect(rectangle)
# クリックした位置と離した位置を丸で表現
for i in range(len(self.points)):
self.drawVrtx(vrtx_path, i)
# ウィンドウ上に描画
painter.drawPath(line_path)
painter.drawPath(vrtx_path)
# クリックした位置と離した位置をアウトラインとして提供する関数
def drawVrtx(self, path, i):
d = self.point_size / self.scale
point = self.points[i]
path.addEllipse(point, d / 2.0, d / 2.0)
# クリックした位置を保持する関数
def addPoint(self, point):
self.points.append(point)
# クリックした位置と離した位置から四角形の情報を取得する関数
def getRectFromLine(self, point1, point2):
x1, y1 = point1.x(), point1.y()
x2, y2 = point2.x(), point2.y()
w = x2 - x1
h = y2 - y1
return QtCore.QRectF(x1, y1, w, h)
3. Canvasクラスの追記
前回作成したCanvasクラスに先ほど作ったShapeクラスを使って追記していきます。追記した個所はコメントで分かるようにしています。
class Canvas(QWidget):
def __init__(self):
super(Canvas, self).__init__()
# 初期値
self.painter = QtGui.QPainter()
self.pixmap = QtGui.QPixmap()
self.scale = 1.0
## 追記 ##
self.shapes = []
self.rectangle = Shape()
self.current = None
# canvasのレイアウト
self.canvas_layout = QGridLayout()
# canvas_layoutをQWidget(self)にセット
self.setLayout(self.canvas_layout)
def openImage(self, filepath):
img = QtGui.QImage()
# 画像ファイルの読み込み
if not img.load(filepath):
return False
# QImage -> QPixmap
self.pixmap = QtGui.QPixmap.fromImage(img)
def paintEvent(self, event):
if not self.pixmap:
return super(Canvas, self).paintEvent(event)
# paintオブジェクトの生成(描画するためのもの)
p = self.painter
# paintができる状態にする
p.begin(self)
# 画像のスケール情報
p.scale(self.scale, self.scale)
# 原点の設定
p.translate(self.offsetToCenter())
# 画像を描画する
p.drawPixmap(0, 0, self.pixmap)
## 追記 ##
Shape.scale = self.scale
# 四角を描画
if self.current:
self.current.paint(p)
self.rectangle.paint(p)
# 描画したものを表示
for shape in self.shapes:
shape.paint(p)
# paintの終了
p.end()
# 原点補正
def offsetToCenter(self):
scale = self.scale
area = super(Canvas, self).size()
w, h = self.pixmap.width() * scale, self.pixmap.height() * scale
aw, ah = area.width(), area.height()
x = (aw - w) / (2 * scale) if aw > w else 0
y = (ah - h) / (2 * scale) if ah > h else 0
return QtCore.QPoint(int(x), int(y))
## ここより下 追記 ##
# press_event(マウスをクリックしたときに発生)
def mousePressEvent(self, event):
# クリックした場所の位置(x, y)を取得
pos = self.transformPos(event.localPos())
# 左クリックが押された場合
if event.button() == QtCore.Qt.LeftButton:
self.current = Shape()
self.current.addPoint(pos)
# move_event(クリックした状態でマウスを移動したときに発生)
def mouseMoveEvent(self, event):
pos = self.transformPos(event.localPos())
if not self.current:
return
# 四角を描画するための位置情報
self.rectangle.points = [self.current[0], pos]
# 四角を可視化するために必要
self.repaint()
# release_event(クリックを離したときに発生)
def mouseReleaseEvent(self, event):
if self.current:
self.current.points = self.rectangle.points
self.initialize()
# 初期化関数
def initialize(self):
self.shapes = [self.current]
self.current = None
self.update()
# 画像左上を(0, 0)に補正
def transformPos(self, point):
return point / self.scale - self.offsetToCenter()
マウス操作をした際のevent関連を追加しています。
特にマウスでクリックしたタイミング、マウスをクリックした状態で移動させたタイミング、クリックを離したタイミングでeventが発生します。
4. 最終的なコード
完成したコードを示します。
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QAction, QFileDialog, QGridLayout, QSpinBox
from PyQt5 import QtGui
from PyQt5 import QtCore
import sys
import os
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
# 初期値
self.setGeometry(0, 0, 500, 500)
self.image = QtGui.QImage()
# canvas
self.canvas = Canvas()
self.setCentralWidget(self.canvas)
# menubarを作成
self.createMenubar()
# zoomwidge
self.zoomWidget = ZoomWidget()
self.zoomWidget.valueChanged.connect(self.paintCanvas)
def createMenubar(self):
# menubar
self.menubar = self.menuBar()
# menubarにメニューを追加
self.filemenu = self.menubar.addMenu('File')
# アクションの追加
self.openAction()
def openAction(self):
# アクションの作成
self.open_act = QAction('開く')
self.open_act.setShortcut('Ctrl+O') # shortcut
self.open_act.triggered.connect(self.openFile) # open_actとメソッドを紐づける
# メニューにアクションを割り当てる
self.filemenu.addAction(self.open_act)
def openFile(self):
self.filepath = QFileDialog.getOpenFileName(self, 'open file', '', 'Images (*.jpeg *.jpg *.png *.bmp)')[0]
if self.filepath:
self.canvas.openImage(self.filepath)
self.paintCanvas()
def paintCanvas(self):
# canvasのスケールを更新
self.canvas.scale = self.scaleFitWindow()
self.canvas.update()
def scaleFitWindow(self):
# MainWindowのウィンドウサイズ
e = 2.0 # 余白
w1 = self.centralWidget().width() - e
h1 = self.centralWidget().height() - e
a1 = w1 / h1
# pixmapのサイズ
w2 = self.canvas.pixmap.width()
h2 = self.canvas.pixmap.height()
a2 = w2 / h2
# a1が大きい -> 高さに合わせる
# a2が大きい -> 幅に合わせる
return w1 / w2 if a2 >= a1 else h1 / h2
def adjustScale(self):
value = self.scaleFitWindow()
value = int(100 * value)
self.zoomWidget.setValue(value)
def resizeEvent(self, event):
# canvasがTrue かつ pixmapがnullじゃない場合
if self.canvas and not self.canvas.pixmap.isNull():
self.adjustScale()
super(MainWindow, self).resizeEvent(event)
class Canvas(QWidget):
def __init__(self):
super(Canvas, self).__init__()
# 初期値
self.painter = QtGui.QPainter()
self.pixmap = QtGui.QPixmap()
self.scale = 1.0
## 追記 ##
self.shapes = []
self.rectangle = Shape()
self.current = None
# canvasのレイアウト
self.canvas_layout = QGridLayout()
# canvas_layoutをQWidget(self)にセット
self.setLayout(self.canvas_layout)
def openImage(self, filepath):
img = QtGui.QImage()
# 画像ファイルの読み込み
if not img.load(filepath):
return False
# QImage -> QPixmap
self.pixmap = QtGui.QPixmap.fromImage(img)
def paintEvent(self, event):
if not self.pixmap:
return super(Canvas, self).paintEvent(event)
# paintオブジェクトの生成(描画するためのもの)
p = self.painter
# paintができる状態にする
p.begin(self)
# 画像のスケール情報
p.scale(self.scale, self.scale)
# 原点の設定
p.translate(self.offsetToCenter())
# 画像を描画する
p.drawPixmap(0, 0, self.pixmap)
## 追記 ##
Shape.scale = self.scale
# 四角を描画
if self.current:
self.current.paint(p)
self.rectangle.paint(p)
# 描画したものを表示
for shape in self.shapes:
shape.paint(p)
# paintの終了
p.end()
# 原点補正
def offsetToCenter(self):
scale = self.scale
area = super(Canvas, self).size()
w, h = self.pixmap.width() * scale, self.pixmap.height() * scale
aw, ah = area.width(), area.height()
x = (aw - w) / (2 * scale) if aw > w else 0
y = (ah - h) / (2 * scale) if ah > h else 0
return QtCore.QPoint(int(x), int(y))
## ここより下 追記 ##
# press_event(マウスをクリックしたときに発生)
def mousePressEvent(self, event):
# クリックした場所の位置(x, y)を取得
pos = self.transformPos(event.localPos())
# 左クリックが押された場合
if event.button() == QtCore.Qt.LeftButton:
self.current = Shape()
self.current.addPoint(pos)
# move_event(クリックした状態でマウスを移動したときに発生)
def mouseMoveEvent(self, event):
pos = self.transformPos(event.localPos())
if not self.current:
return
# 四角を描画するための位置情報
self.rectangle.points = [self.current[0], pos]
# 四角を可視化するために必要
self.repaint()
# release_event(クリックを離したときに発生)
def mouseReleaseEvent(self, event):
if self.current:
self.current.points = self.rectangle.points
self.initialize()
# 初期化関数
def initialize(self):
self.shapes = [self.current]
self.current = None
self.update()
# 画像左上を(0, 0)に補正
def transformPos(self, point):
return point / self.scale - self.offsetToCenter()
class ZoomWidget(QSpinBox):
def __init__(self, value=100):
super(ZoomWidget, self).__init__()
# 四角を描画するためのShapeクラス
class Shape(object):
def __init__(self):
# 初期値
self.scale = 1.0
self.point_size = 8
self.points = []
self.rectangle_color = QtGui.QColor(0, 255, 0, 128)
# リストのようにShapeクラスのインスタンスから取得する情報
def __getitem__(self, key):
return self.points[key]
# Canvasで実装したQPainterを受け取って四角を描画する関数
def paint(self, painter):
# pointsがTrueの場合
if self.points:
# paintEventで使用するpenを設定
pen = QtGui.QPen(self.rectangle_color)
pen.setWidth(max(1, int(round(2.0 / self.scale))))
painter.setPen(pen)
# ウィンドウに可視化するための位置情報
line_path = QtGui.QPainterPath()
vrtx_path = QtGui.QPainterPath()
if len(self.points) == 2:
# 四角形の情報(x, y, w, h)
rectangle = self.getRectFromLine(*self.points)
line_path.addRect(rectangle)
# クリックした位置と離した位置を丸で表現
for i in range(len(self.points)):
self.drawVrtx(vrtx_path, i)
# ウィンドウ上に描画
painter.drawPath(line_path)
painter.drawPath(vrtx_path)
# クリックした位置と離した位置をアウトラインとして提供する関数
def drawVrtx(self, path, i):
d = self.point_size / self.scale
point = self.points[i]
path.addEllipse(point, d / 2.0, d / 2.0)
# クリックした位置を保持する関数
def addPoint(self, point):
self.points.append(point)
# クリックした位置と離した位置から四角形の情報を取得する関数
def getRectFromLine(self, point1, point2):
x1, y1 = point1.x(), point1.y()
x2, y2 = point2.x(), point2.y()
w = x2 - x1
h = y2 - y1
return QtCore.QRectF(x1, y1, w, h)
def main():
app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec_())
main()
まとめ
pyqt5を使って画像に四角形を描画する方法を紹介しました。
マウスを使って直感的に操作しようとするとevent関連の関数を使うことになります。
ここまで来るとソースコードもかなり長くなってしまいました。
また、今の状態ですとウィンドウ内のどこでも四角を描画することができるため、次回の記事で画像内のみ描画できるようにします。