본문 바로가기
Python/PySide PyQt

[PySide] Ex: Matplotlib 에서 상호작용 기능 구현

by ds31x 2024. 5. 19.

PySide6와 Matplotlib을 사용한 이미지 마킹 애플리케이션 개발

PySide6와 Matplotlib 라이브러리를 활용하여 이미지 위에 사용자 상호작용을 기반으로

  • 사각형 선택 (Drag) 및
  • 위치 마킹 기능(right double click)을 구현하는 방법을 소개함.

기본 설정 및 필수 컴포넌트

우선, PySide6와 Matplotlib을 사용하여 Event 처리 기능을 테스트하기 위한 기본 GUI 애플리케이션을 구현함.

 

다음 코드로 필요한 모듈들을 import 함.

import sys
import numpy as np

import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.image import imread

from PySide6.QtWidgets import (
        QApplication, QMainWindow, QVBoxLayout, 
        QWidget, QPushButton, QFileDialog, QMessageBox
        )

 

기본적인 윈도우에는

  • 이미지를 불러와 표시할 FigureCanvas객체와
  • 이미지를 로드할 QPushButton 객체가 포함됨.
  • 또한 마우스를 이용한 드래그 처리 등을 위한 variable을 선언.
class InteractivePlot(QMainWindow):
    def __init__(self):
        super().__init__()

        # 창의 기본 설정: 그래프 영역, 캔버스, 그리기 도구 및 버튼 초기화
        self.figure = Figure()
        self.canvas = FigureCanvas(self.figure)
        self.ax = self.figure.add_subplot(111)
        self.ax.axis('off')  # 축을 초기에 비활성화하여 이미지만 표시

        # 이미지 로드 버튼 설정 및 클릭 이벤트에 대한 연결 설정
        self.load_button = QPushButton("Load Image")
        self.load_button.clicked.connect(self.load_image)

        # 메인 위젯 및 레이아웃 설정
        layout = QVBoxLayout()
        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)
        layout.addWidget(self.canvas)
        layout.addWidget(self.load_button)

        # 마우스 드래그 상태 및 사각형 선택을 위한 변수 초기화
        self.dragging = False
        self.rect = None
        self.start_point = (0, 0)
        self.click_count = 0  # 클릭 횟수 카운트를 위한 변수
        
        # 마우스 클릭 및 더블클릭 이벤트 연결
        ...

마우스 클릭 및 더블클릭 이벤트 연결

아래 코드는 MainWindow의 생성자에서

matplotlibFigureCanvas를 사용하여 여러 마우스 이벤트를 처리하는 연결을 설정하고 있음.

각각의 mpl_connect 메서드 호출은 특정 이벤트 유형에 대한 콜백 함수를 등록함.

이를 통해 사용자의 마우스 동작에 반응하여 특정 작업을 수행할 수 있음.

        # 마우스 클릭 및 더블클릭 이벤트 연결
        self.canvas.mpl_connect('button_press_event', self.on_click)
        self.canvas.mpl_connect('motion_notify_event', self.on_drag)
        self.canvas.mpl_connect('button_release_event', self.on_release)

아래에 각 이벤트 핸들러의 목적과 기능을 설명합니다:

  1. 'button_press_event' 연결:
    • 이 이벤트는 사용자가 마우스 버튼을 누를 때 발생함.
      self.on_click 메서드를 이 이벤트의 콜백으로 등록하여, 사용자가 이미지 위에서 클릭한 위치에 대한 정보를 처리함.
    • 버튼 종류나 더블클릭 구분은 이벤트의 콜백에서 구분.
    • 더블 클릭 처리를 위해, on_click 메서드 내에서 event.dblclick 속성을 확인하여 더블클릭을 감지하고 처리하도록 구현.
  2. 'motion_notify_event' 연결:
    • 'motion_notify_event'는 사용자가 마우스 버튼을 누른 상태로 마우스를 움직일 때 발생.
    • 해당 이벤트는 self.on_drag 메서드와 연결되어 있으며,
    • 사용자가 이미지 위에서 드래그하는 동안 사각형의 크기를 동적으로 변경함.
  3. 'button_release_event' 연결:
    • 이 이벤트는 사용자가 마우스 버튼을 놓을 때 발생.
    • self.on_release 메서드를 이 이벤트의 콜백으로 사용하여,
    • 드래그 작업의 종료와 함께 사각형을 유지할지 여부를 묻는 다이얼로그를 표시함.

이미지 로딩 기능

사용자가 버튼을 클릭하면 QFileDialog를 통해 이미지를 선택하고, 해당 image를 FigureCanvas 객체에 로드하는 기능을 담당하는 callback 구현임.

    def load_image(self):
        # 사용자가 선택한 이미지 파일을 불러오고 캔버스에 표시
        file_name, _ = QFileDialog.getOpenFileName(self, 
                                                   "Open Image", 
                                                   "", 
                                                   "Image Files (*.png *.jpg *.bmp)")
        if file_name:
            self.image = imread(file_name)
            print(type(self.image))
            self.ax.clear()
            self.ax.imshow(self.image)
            self.ax.axis('on')
            self.canvas.draw()

 

load_image 메서드는 사용자가 파일 대화 상자를 통해 이미지 파일을 선택하고, 선택된 이미지를 프로그램의 캔버스에 로드 및 표시하는 기능을 수행함:

  1. 파일 선택 대화 상자:
    • file_name, _ = QFileDialog.getOpenFileName(self, "Open Image", "", "Image Files (*.png *.jpg *.bmp)"):
      • QFileDialog.getOpenFileName() 함수는 파일 선택 대화 상자를 열어 사용자가 파일 시스템에서 파일을 선택할 수 있게 해줌.
      • 첫 번째 인자 self는 현재 인스턴스를 나타내며, "Open Image"는 대화 상자의 제목임.
      • 두 번째 인자는 대화 상자가 처음 열릴 때 표시할 기본 디렉토리를 지정하는데, 여기서는 빈 문자열 ""이 사용되어 기본 디렉토리가 설정되지 않음.
      • 세 번째 인자 "Image Files (*.png *.jpg *.bmp)"는 대화 상자에서 선택 가능한 파일 유형을 필터링함.
        • 여기서는 PNG, JPG, BMP 형식의 이미지 파일만 선택할 수 있도록 설정됨.
  2. 이미지 파일 로드 및 표시:
    • if file_name::
      • 사용자가 파일 선택 대화 상자에서 파일을 선택하고 "열기" 버튼을 클릭하면,
        선택된 파일의 경로가 file_name 변수에 저장됨.
      • 이 조건문은 사용자가 실제로 파일을 선택했는지 확인.
    • self.image = imread(file_name):
      • imread 함수를 사용하여 선택된 이미지 파일을 읽고,
      • 읽어들인 이미지 데이터를 self.image 변수에 저장함.
    • self.ax.clear():
      • clear() 메서드를 사용하여 이전에 FigureCanvas에 그려진 모든 내용을 지움.
      • 이는 새 이미지를 로드할 때 이전 이미지 또는 그래픽 요소들이 겹치지 않도록 하기 위함임.
    • self.ax.imshow(self.image):
      • imshow() 함수를 사용하여 self.image에 저장된 이미지 데이터를 캔버스(FigureCanvas객체)에 표시.
      • 이 함수는 이미지 데이터를 받아 그래픽 형태로 시각화하는 역할을 수행.
    • self.ax.axis('on'):
      • axis() 메서드를 사용하여 축을 활성화함.
      • 일반적으로 이미지를 보다 깔끔하게 표시하기 위해 축을 끄기도 함 ('off' 로 설정)
      • 위 구현에선 축이 활성화 상태로 설정되어 있어 축의 눈금과 경계가 보여지며 이를 바탕으로 마우스 위치를 확인 가능.
    • self.canvas.draw():
      • 마지막으로 draw() 메서드를 호출하여 캔버스(FigureCanvas객체)를 갱신.
      • 이 과정에서 새로 로드된 이미지가 사용자에게 시각적으로 표시됨.
      • 이 메서드는 캔버스의 모든 변경사항을 적용하고 최신 상태로 렌더링함.
      • 때문에 이미지를 로드하거나 그래픽 요소를 업데이트할 때 반드시 이 메서드가 호출되어야 함.

마우스 클릭 이벤트를 이용한 사각형 그리기 시작 및 더블클릭과 연결

다음은 마우스 이벤트를 통해 사각형을 그리는 기능을 위한 callback 구현임.

    def on_click(self, event):
        if self.image is None:
            return
        # 마우스 클릭 이벤트 핸들러: 더블클릭 검출을 위해 클릭 횟수 계산
        if event.button == 3:
            self.on_right_click(event)
        else:
            if event.inaxes != self.ax:
                return
            self.dragging = True
            self.start_point = (event.xdata, event.ydata)
            self.rect = self.ax.add_patch(
                plt.Rectangle(self.
                              start_point, 
                              0, 0, 
                              fill=False, color='red')
                )
            self.canvas.draw()

 

on_click 메서드는 사용자가 이미지를 보여주는 캔버스 위에서 마우스 클릭 이벤트가 발생할 때 호출됨.

이 메서드는 단일 클릭과 더블 클릭을 감지하고, 각각에 대해 적절한 처리를 수행함:

  1. 더블클릭 검출:
    • if event.button == 3:
      • 이 조건문은 이벤트가 우클릭인지 확인.
      • event.button 속성은 마우스 버튼의 종류를 나타냄: 1(좌), 2(휠), 3(우)
    • self.on_right_click(event):
      • 만약 우클릭 이벤트가 감지되면, on_right_click 메서드를 호출하여 우클릭에 대한 처리를 수행.
      • 이 메서드는 우클릭된 위치에 원을 그리는 등의 특정 동작을 수행할 수 있음.
  2. 단일 좌클릭 처리:
    • else::
      • 만약 우클릭이 아니라 좌클릭인 경우, 이 코드 블록이 실행됨.
    • if event.inaxes != self.ax::
      • 이 조건문은 이벤트가 현재 활성화된 축(즉, 이미지가 표시되는 영역) 내에서 발생했는지 확인.
      • 그렇지 않은 경우, method 내에서 더 이상 진행하지 않고 반환 처리함.
      • 이는 사용자가 이미지 외부를 클릭하는 것을 무시하도록 해 줌.
  3. 드래그 상태 초기화:
    • self.dragging = True:
      • 사용자가 이미지 위에서 드래그를 시작했다는 것을 나타내는 플래그를 True로 설정.
    • self.start_point = (event.xdata, event.ydata):
      • 사용자가 클릭한 위치의 x, y 좌표를 start_point 변수에 저장함.
      • 이 좌표는 나중에 사각형을 그릴 때 시작점으로 사용됨.
  4. 사각형 그리기 시작:
    • self.rect = self.ax.add_patch(plt.Rectangle(self.start_point, 0, 0, fill=False, color='red')):
      • matplotlibRectangle 객체를 생성하여 캔버스(FigureCanvas객체)에 추가함
      • 초기 너비와 높이는 0으로 설정하여 사각형이 보이지 않게 시작함
      • 사용자가 드래그하는 동안 너비와 높이가 조정될 예정임.
      • 사각형은 채우지 않고 빨간색 경계선만 표시됨.
  5. 캔버스(FigureCanvas객체) 갱신:
    • self.canvas.draw():
      • 캔버스(FigureCanvas객체)에 그려진 내용을 갱신하여 사각형이 사용자의 클릭 위치에 따라 그려지도록 함.
      • 이 호출을 통해 캔버스(FigureCanvas객체)에 사각형의 초기 상태가 그려짐.

Right Double Click으로 클릭된 위치에 Circle 표시하기

다음 코드는 사용자가 image 위에서 우클릭할 때 해당 위치에 circle을 그리는 기능을 수행하는 callback 함수 구현임.

 

이는 사용자가 특정 지점을 표시할 때 유용.

    def on_right_click(self, event):
        if self.image is None:
            return
        #  우클릭 이벤트 핸들러: 클릭된 위치에 원을 그림
        if event.dblclick:
            self.ax.add_patch(
                plt.Circle(
                    (event.xdata, event.ydata), 
                    10, 
                    color='blue', fill=True)
                )
            self.canvas.draw()

 

on_right_click 메서드는 사용자가 캔버스(FigureCanvas객체) 위에서 마우스를 우클릭할 때 호출됨.

이 메서드는 우 더블클릭된 위치에 원을 그려서 특정 지점을 표시하는 기능을 수행함:

  1. 우더블클릭 이벤트 확인:
    • if event.dblclick::
      • 이 조건문은 이벤트가 더블클릭인지 여부를 확인함.
      • event.dblclick 속성은 이벤트가 더블클릭이면 True를 반환.
      • 이 속성을 사용함으로써, 함수는 우더블클릭 이벤트에만 반응하도록 할 수 있음.
  2. 원 그리기:
    • self.ax.add_patch(plt.Circle((event.xdata, event.ydata), 10, color='blue', fill=True)):
      • plt.Circle: matplotlib.pyplot 모듈의 Circle 함수를 사용하여 새로운 원 객체를 생성함.
        • 이 함수는 원의 중심 좌표, 반지름, 색상, 채움 유무 등 원을 그릴 때 필요한 매개변수들을 지정할 수 있음.
      • (event.xdata, event.ydata): 이 튜플은 더블클릭된 위치의 x, y 좌표를 나타냄.
        • event.xdataevent.ydata는 마우스 클릭 위치의 축에 따른 데이터 좌표를 제공함.
      • 10: 원의 반지름을 지정함.
      • color='blue': 원의 색상을 파란색으로 설정함.
      • fill=True: 원 내부를 색으로 채움. False로 설정할 경우, 원의 경계선만 표시됨.
  3. 캔버스 갱신:
    • self.canvas.draw(): 캔버스(FigureCanvas객체)에 그려진 내용을 갱신함.
      • 원을 추가한 후 이 메서드를 호출하여 그래픽 변경사항을 적용하고 그려짐.
      • 캔버스(FigureCanvas객체)를 다시 그림으로써, 새롭게 추가된 원이 보여지게 됨.

드래그에 의한 사각형 그리기(폭과 높이 설정)

아래 코드는 사용자가 이미지 위에서 마우스를 드래그할 때 실행되는 on_drag 메서드를 구현.

이 메서드는 GUI 기반 이미지 편집 도구에서 사용자가 선택 영역을 시각적으로 표시하고 조정할 수 있도록 해줌.

    def on_drag(self, event):
        if self.image is None:
            return
        # 마우스 드래그 이벤트 핸들러: 사각형의 위치와 크기를 실시간으로 조정
        if not self.dragging or not event.inaxes:
            return
        if event.dblclick:
            return 
        x0, y0 = self.start_point
        x1, y1 = event.xdata, event.ydata
        self.rect.set_width(x1 - x0)
        self.rect.set_height(y1 - y0)
        self.rect.set_xy((min(x0, x1), min(y0, y1)))
        self.canvas.draw()
  1. 드래그 상태 확인: 메서드의 시작에서 if not self.dragging or not event.inaxes 조건을 통해 다음 두 가지를 확인:
    • self.draggingTrue인지:
      • 이 변수는 사용자가 드래그를 시작했는지 여부를 나타냄.
      • 사용자가 마우스를 클릭했을 때 True로 설정되며, 마우스를 놓았을 때 False로 재설정됨.
      • 이를 통해 메서드는 실제로 드래그 이벤트 중에만 동작하도록 제한됨.
    • event.inaxesTrue인지:
      • 이 조건은 이벤트가 발생한 위치가 현재 활성화된 축(이 경우 이미지가 표시된 축) 내부인지 확인.
      • 그래프 밖에서 발생한 이벤트를 무시하도록 처리하기 위해 확인.
  2. 사각형의 시작점과 현재점 계산:
    • x0, y0은 사각형을 그리기 시작한 초기 클릭 위치를 나타냄.
    • x1, y1은 현재 마우스 위치를 나타내며, 사용자가 마우스를 움직일 때마다 업데이트됨.
  3. 사각형 크기 조정:
    • self.rect.set_width(x1 - x0)self.rect.set_height(y1 - y0)를 호출함으로서
      • 사각형의 너비와 높이를 실시간으로 조정.
      • 여기서 너비와 높이는 초기 클릭 지점과 현재 마우스 위치 사이의 차이를 기반으로 계산됨.
    • self.rect.set_xy((min(x0, x1), min(y0, y1)))사각형의 왼쪽 상단 모서리를 설정.
      • min 함수를 사용하는 이유는 사용자가 왼쪽에서 오른쪽, 혹은 오른쪽에서 왼쪽으로 드래그할 수 있기 때문임.
      • 이 방식으로 사각형의 위치를 유동적으로 조정할 수 있음.
  4. 캔버스(FigureCanvas객체)갱신:
  • self.canvas.draw()를 호출하여 캔버스에 변경사항을 적용하고, 사용자에게 새로 그려진 사각형을 보여줌.
  • 이는 사용자가 마우스를 드래그하는 동안 실시간으로 사각형이 확장되거나 축소되는 것을 볼 수 있도록 함.

사각형 남길지 결정하는 다이얼로그

사용자가 마우스를 release 할 때 사각형을 남길지 결정하는 다이얼로그가 표시되도록 해주는 callback 함수 구현임.

이를 통해 사용자는 자신이 그린 사각형을 image 위에 남길지 선택할 수 있음.

    def on_release(self, event):
        if self.image is None:
            return
        # 마우스 버튼 해제 이벤트 핸들러: 사용자가 사각형을 그린 후 마우스 버튼을 놓으면 호출됨.
        if event.button == 3: # 우클릭인 경우는 무시.
            return
        if self.dragging:
            self.dragging = False
            response = QMessageBox.question(self, 
                                            "Confirm", 
                                            "Keep the rectangle?", 
                                            QMessageBox.Yes | QMessageBox.No)
            if response == QMessageBox.No:
                self.rect.remove()  # 사용자가 'No'를 선택했을 때 사각형 삭제
            self.canvas.draw()

결론

위의 코드는 이미지 분석이나 표시 작업에서 사용자가 보다 정밀하게 상호작용할 수 있게 해주며, 필요에 따라 쉽게 수정하거나 확장할 수 있는 기본 구현방법을 보여줌.


전체코드

다음은 전체 코드임.

import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.image import imread

from PySide6.QtWidgets import (
        QApplication, QMainWindow, QVBoxLayout, 
        QWidget, QPushButton, QFileDialog, QMessageBox
        )

class InteractivePlot(QMainWindow):
    def __init__(self):
        super().__init__()

        # 창의 기본 설정: 그래프 영역, 캔버스, 그리기 도구 및 버튼 초기화
        self.figure = Figure()
        self.canvas = FigureCanvas(self.figure)
        self.ax = self.figure.add_subplot(111)
        self.ax.axis('off')  # 축을 초기에 비활성화하여 이미지만 표시

        # 이미지 로드 버튼 설정 및 클릭 이벤트에 대한 연결 설정
        self.load_button = QPushButton("Load Image")
        self.load_button.clicked.connect(self.load_image)

        # 메인 위젯 및 레이아웃 설정
        layout = QVBoxLayout()
        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)
        layout.addWidget(self.canvas)
        layout.addWidget(self.load_button)

        # 마우스 드래그 상태 및 사각형 선택을 위한 변수 초기화
        self.dragging = False
        self.rect = None
        self.start_point = (0, 0)
        self.click_count = 0  # 클릭 횟수 카운트를 위한 변수
        self.image = None


        # 마우스 클릭 및 더블클릭 이벤트 연결
        self.canvas.mpl_connect('button_press_event', self.on_click)
        self.canvas.mpl_connect('motion_notify_event', self.on_drag)
        self.canvas.mpl_connect('button_release_event', self.on_release)


    def load_image(self):
        # 사용자가 선택한 이미지 파일을 불러오고 캔버스에 표시
        file_name, _ = QFileDialog.getOpenFileName(self, 
                                                   "Open Image", 
                                                   "", 
                                                   "Image Files (*.png *.jpg *.bmp)")
        if file_name:
            self.image = imread(file_name)
            print(type(self.image))
            self.ax.clear()
            self.ax.imshow(self.image)
            self.ax.axis('on')
            self.canvas.draw()

    def on_click(self, event):
        if self.image is None:
            return
        # 마우스 클릭 이벤트 핸들러: 더블클릭 검출을 위해 클릭 횟수 계산
        if event.button == 3:
            self.on_right_click(event)
        else:
            if event.inaxes != self.ax:
                return
            self.dragging = True
            self.start_point = (event.xdata, event.ydata)
            self.rect = self.ax.add_patch(
                plt.Rectangle(self.
                              start_point, 
                              0, 0, 
                              fill=False, color='red')
                )
            self.canvas.draw()

    def on_right_click(self, event):
        if self.image is None:
            return
        #  우클릭 이벤트 핸들러: 클릭된 위치에 원을 그림
        if event.dblclick:
            self.ax.add_patch(
                plt.Circle(
                    (event.xdata, event.ydata), 
                    10, 
                    color='blue', fill=True)
                )
            self.canvas.draw()

    def on_drag(self, event):
        if self.image is None:
            return
        # 마우스 드래그 이벤트 핸들러: 사각형의 위치와 크기를 실시간으로 조정
        if not self.dragging or not event.inaxes:
            return
        if event.dblclick:
            return 
        x0, y0 = self.start_point
        x1, y1 = event.xdata, event.ydata
        self.rect.set_width(x1 - x0)
        self.rect.set_height(y1 - y0)
        self.rect.set_xy((min(x0, x1), min(y0, y1)))
        self.canvas.draw()

    def on_release(self, event):
        if self.image is None:
            return
        # 마우스 버튼 해제 이벤트 핸들러: 사용자가 사각형을 그린 후 마우스 버튼을 놓으면 호출됨.
        if event.button == 3: # 우클릭인 경우는 무시.
            return
        if self.dragging:
            self.dragging = False
            response = QMessageBox.question(self, 
                                            "Confirm", 
                                            "Keep the rectangle?", 
                                            QMessageBox.Yes | QMessageBox.No)
            if response == QMessageBox.No:
                self.rect.remove()  # 사용자가 'No'를 선택했을 때 사각형 삭제
            self.canvas.draw()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mwd = InteractivePlot()
    mwd.show()
    sys.exit(app.exec())

같이 읽어보면 좋은 자료들

2024.05.19 - [Python/PySide PyQt] - [PySide] FigureCanvas.mpl_connect

 

[PySide] FigureCanvas.mpl_connect

FigureCanvas 클래스는Matplotlib 그래픽 요소와의 상호작용을 위한 event handling을 구현하기 위해,mpl_connect 메소드를 제공함.이 메소드 mpl_connect를 사용하여특정 event에 대응하는callback function을 연결할

ds31x.tistory.com

2024.03.18 - [Python/matplotlib] - [matplotlib] patches: 도형 그리기.

 

[matplotlib] patches: 도형 그리기.

patches 는 모듈은 Artist 의 subclass인 Patch 클래스들을 제공하여, 다음의 다양한 2D 도형을 쉽게 그릴 수 있게 해줌. Arc (호), Circle (원), CirclePolygon (원의 근사 다각형), Ellipse (타원), Arrow (화살표), FancyA

ds31x.tistory.com

2024.04.29 - [Python/PySide PyQt] - [PySide6] matplotlib 이용하기

 

[PySide6] matplotlib 이용하기

matplotlib 이용하기PyQt, PySide에서는 PyQtGraph를 통해서도 graph등을 그릴 수 있으나,대중적으로 사용되는 matplotlib를 이용할 수도 있다.PyQtGraph는 Qt vector 기반의 QGraphicsScene를 통해 상호작용이 가능한

ds31x.tistory.com