※当サイトではアフィリエイト広告を利用しています。

GUI開発

【PyQt5】画像の範囲外にマウスが来た時の挙動を考える

outofpixmap-cursor

こんにちは、タナカです。

以前の記事で【PyQt5】画像に四角形を動的に描画する方法で画像に四角形を描画する方法を解説しました。

しかし、前回のコードではウィンドウ全体が描画可能領域となっており、画像外にも四角形を描画することができてしまう状態でした。

今回は、画像内のみを描画可能領域とするようコードを追記していきたいと思います。

この記事の内容
  • 画像内のみを描画可能領域にする

1. アウトプットの確認

今回は右図のように画像外を描画できないようにしていきたいと思います。

output-image

2. 画像外とは

画像外とは言葉通り、画像の外のことを言います。具体的には下記の番号を振った部分にあたります。

この画像外を「クリックする」もしくは、画像外に「カーソルを持っていく」ときの挙動を考えることが必要になります。

3. 画像外をクリックする

初めに画像外をクリックする場合を考えます。

先ほどの図でいう①~⑧の部分をクリックしても、四角が描画されないようにする必要があります。

①~⑧までの条件を考えてコードを書いてもいいのですが、それだとかなり効率が悪いので、画像の範囲外という条件で考えます。

画像の範囲外 = 画像の範囲の逆になりますよね。つまり、画像の範囲を考えて、その逆を取ったものが画像の範囲外ということです。

画像の範囲は下記のようになります。

$$0 \leq x < w かつ 0 \leq y < h$$

この範囲の逆を取るようにコードを書くとこのようになります。

    # 画像の範囲外
    def outOfPixmap(self, point):
        w = self.pixmap.width()
        h = self.pixmap.height()
        return not (0 <= point.x() < w and 0 <= point.y() < h)

 

上記の条件に当てはまるもの、つまりTrueになる条件が画像の範囲外ということです。

この条件を使って、mousePressEventを書くとこのようになります。

    # press_event(マウスをクリックしたときに発生)
    def mousePressEvent(self, event):
        # クリックした場所の位置(x, y)を取得
        pos = self.transformPos(event.localPos())

        # 左クリックが押された場合
        if event.button() == QtCore.Qt.LeftButton:
            ## 追記 ##
            if not self.outOfPixmap(pos):
                self.current = Shape()
                self.current.addPoint(pos)

 

ここでは、not self.outOfPixmap(pos)がTrueになった場合のみ、描画することができるようにしています。

self.outOfPixmap(pos)は画像の範囲外を意味していますので、その逆(not)は画像の範囲内ということになります。

この条件を入れることで、画像の範囲外をクリックしても四角が描画されないようになりました。

4. 画像の範囲外にカーソルを持っていく

次は、画像の範囲外にカーソルを持っていくことを考えます。四角形を描画する際は、画像の範囲内を一度クリックし、クリックした状態で好きな位置にカーソルを移動させます。

この好きな位置が画像の範囲外になった場合の挙動を考えていきます。

画像の範囲外にカーソルを持っていくパターンとしては下記の4つが考えられます。

  • 上から抜ける
  • 右から抜ける
  • 下から抜ける
  • 左から抜ける
outofpixmap-cursor

例えば、p1の位置でクリックして、マウスをp2_1の位置に持って行った場合を考えます。このときにp1とp2_1を結んだ線と(0, 0)と(w, 0)を結んだ線に交点ができます。

この交点を四角を描画するときの最大位置とすることで画像外に描画しないようにすることができます。

そのほかの場合も同様でp2_2を考えた場合は、p1とp2_2を結んだ線と(w, 0)と(w, h)を結んだ線の交点が最大位置となります。

交点を求めるコードはこのようになります。

    # 交点の座標を求める
    def intersectingEdges(self, point1, point2, points):
        # クリックした位置
        (x1, y1) = point1

        # マウスを移動させた位置
        (x2, y2) = point2
        
        for i in range(4):
            # points: [(0, 0), (w, 0), (w. h), (0, h)]
            # p1-p2を結んだ線と交差
            x3, y3 = points[i]
            x4, y4 = points[(i + 1) % 4]

            # 交差する点を求めるときに使う式
            denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
            nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)
            nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)

            # 交差しない場合
            if denom == 0:
                continue

            # 交差する点を求める
            ua, ub = nua / denom, nub / denom
            if 0 <= ua <= 1 and 0 <= ub <= 1:
                x = x1 + ua * (x2 - x1)
                y = y1 + ua * (y2 - y1)
    
        return x, y

 

交点を求める方法はopencvで交点を求める方法で詳しく解説しています。

p1とp2を結んだ線と交差する線かどうかをfor文を使って確認しています。交差しない場合は、continueでスキップし、交差する場合のみ交点を求めるようになっています。

for文の中に4で割ったときの余りを使って、x4、y4を求めているのですが、この書き方を見てlabelmeすごいってなりました。

交点を求める関数(self.intersection())を使って、交点として取得する関数がこのようになります。

    # 交点
    def intersectionPoint(self, point1, point2):
        size = self.pixmap.size()
        points = [
            (0, 0),
            (size.width(), 0),
            (size.width(), size.height()),
            (0, size.height()),
        ]

        x1, y1 = point1.x(), point1.y()
        x2, y2 = point2.x(), point2.y()
        x, y = self.intersectingEdges((x1, y1), (x2, y2), points)

        return QtCore.QPoint(x, y)

 

最後にカーソルを移動させた際に発生するイベント関数が、mouseMoveEvent関数になりますので、こちらを修正するとこのようになります。

    # move_event(クリックした状態でマウスを移動したときに発生)
    def mouseMoveEvent(self, event):
        pos = self.transformPos(event.localPos())
        if not self.current:
            return

        ## 追記 ##
        if self.outOfPixmap(pos):
            # 四角を描画するための位置情報
            pos = self.intersectionPoint(self.current[0], pos)
        self.rectangle.points = [self.current[0], pos]
        
        # 四角を可視化するために必要
        self.repaint()

 

self.outOfPixmap()がTrueの場合、つまり画像の範囲外の場合に先ほどのself.intersectionPoint()が実行されます。これによって交点を取得し、この交点が四角形の最大位置となります。

以上で、画像の範囲内のみを描画可能領域とすることができました。

まとめ

画像内のみを描画可能領域とする方法を解説しました。

画像の大きさ(w, hの値)を利用して、交点の座標を求めることができましたね。

考えられパターンを想定して、それらの条件をすべて設定することがポイントになります。

ぜひ参考にしてみてください。