대화형 푸리에 파형 시각화 생성

Beginner

This tutorial is from open-source community. Access the source code

소개

이 랩에서는 Python 의 Matplotlib 을 사용하여 간단한 GUI 를 만드는 방법을 배웁니다. 특히, 주파수 및 시간 도메인에서 두 개의 파형을 표시하는 푸리에 데모 (Fourier Demo) 를 만들 것입니다. 플롯을 클릭하고 드래그하여 파형의 주파수와 진폭을 조정할 수 있습니다.

VM 팁

VM 시작이 완료되면 왼쪽 상단을 클릭하여 Notebook 탭으로 전환하여 실습을 위해 Jupyter Notebook에 액세스하십시오.

때로는 Jupyter Notebook 이 로딩을 완료하는 데 몇 초 정도 기다려야 할 수 있습니다. Jupyter Notebook 의 제한 사항으로 인해 작업의 유효성 검사는 자동화할 수 없습니다.

학습 중에 문제가 발생하면 언제든지 Labby 에게 문의하십시오. 세션 후 피드백을 제공해주시면 즉시 문제를 해결해 드리겠습니다.

라이브러리 임포트

첫 번째 단계는 필요한 라이브러리를 임포트하는 것입니다. Matplotlib, wxPython 및 NumPy 를 사용할 것입니다. Matplotlib 은 Python 용 플로팅 라이브러리이고, wxPython 은 Python 용 GUI 툴킷이며, NumPy 는 Python 을 사용한 수치 계산을 위한 라이브러리입니다.

import wx
import numpy as np
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.figure import Figure

Knob 및 Param 클래스 정의

다음 단계는 Knob 및 Param 클래스를 정의하는 것입니다. 이 클래스는 GUI 에서 파형의 주파수와 진폭을 제어하는 데 사용됩니다.

class Knob:
    """
    Knob - "setKnob" 메서드를 가진 간단한 클래스.
    Knob 인스턴스는 Param 인스턴스에 연결됩니다 (예: param.attach(knob)).
    기본 클래스는 문서화 목적으로 사용됩니다.
    """

    def setKnob(self, value):
        pass


class Param:
    """
    "Param" 클래스의 아이디어는 GUI 의 일부 매개변수가
    여러 개의 노브를 가지고 있어 이를 제어하고 매개변수의 상태를 반영할 수 있다는 것입니다.
    예를 들어, 슬라이더, 텍스트 및 드래깅은 모두 이 예제의 파형에서 주파수 값을 변경할 수 있습니다.
    이 클래스는 하나가 변경될 때 다른 노브에 대한 "피드백"을 업데이트하는 더 깔끔한 방법을 제공합니다.
    또한, 이 클래스는 모든 노브에 대한 최소/최대 제약 조건을 처리합니다.
    아이디어 - 노브 목록 - "set" 메서드에서 노브 객체도 전달됩니다.
      - 노브 목록의 다른 노브에는 다른 노브에 대해 호출되는 "set" 메서드가 있습니다.
    """

    def __init__(self, initialValue=None, minimum=0., maximum=1.):
        self.minimum = minimum
        self.maximum = maximum
        if initialValue != self.constrain(initialValue):
            raise ValueError('illegal initial value')
        self.value = initialValue
        self.knobs = []

    def attach(self, knob):
        self.knobs += [knob]

    def set(self, value, knob=None):
        self.value = value
        self.value = self.constrain(value)
        for feedbackKnob in self.knobs:
            if feedbackKnob != knob:
                feedbackKnob.setKnob(self.value)
        return self.value

    def constrain(self, value):
        if value <= self.minimum:
            value = self.minimum
        if value >= self.maximum:
            value = self.maximum
        return value

SliderGroup 클래스 정의

SliderGroup 클래스는 파형의 주파수와 진폭을 조정하기 위해 GUI 에 슬라이더와 텍스트 필드를 생성합니다.

class SliderGroup(Knob):
    def __init__(self, parent, label, param):
        self.sliderLabel = wx.StaticText(parent, label=label)
        self.sliderText = wx.TextCtrl(parent, -1, style=wx.TE_PROCESS_ENTER)
        self.slider = wx.Slider(parent, -1)
        self.slider.SetRange(0, int(param.maximum * 1000))
        self.setKnob(param.value)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.sliderLabel, 0,
                  wx.EXPAND | wx.ALL,
                  border=2)
        sizer.Add(self.sliderText, 0,
                  wx.EXPAND | wx.ALL,
                  border=2)
        sizer.Add(self.slider, 1, wx.EXPAND)
        self.sizer = sizer

        self.slider.Bind(wx.EVT_SLIDER, self.sliderHandler)
        self.sliderText.Bind(wx.EVT_TEXT_ENTER, self.sliderTextHandler)

        self.param = param
        self.param.attach(self)

    def sliderHandler(self, event):
        value = event.GetInt() / 1000.
        self.param.set(value)

    def sliderTextHandler(self, event):
        value = float(self.sliderText.GetValue())
        self.param.set(value)

    def setKnob(self, value):
        self.sliderText.SetValue(f'{value:g}')
        self.slider.SetValue(int(value * 1000))

FourierDemoFrame 클래스 정의

FourierDemoFrame 클래스는 wxPython 과 Matplotlib 을 사용하여 GUI 를 생성합니다.

class FourierDemoFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        panel = wx.Panel(self)

        ## create the GUI elements
        self.createCanvas(panel)
        self.createSliders(panel)

        ## place them in a sizer for the Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.canvas, 1, wx.EXPAND)
        sizer.Add(self.frequencySliderGroup.sizer, 0,
                  wx.EXPAND | wx.ALL, border=5)
        sizer.Add(self.amplitudeSliderGroup.sizer, 0,
                  wx.EXPAND | wx.ALL, border=5)
        panel.SetSizer(sizer)

    def createCanvas(self, parent):
        self.lines = []
        self.figure = Figure()
        self.canvas = FigureCanvas(parent, -1, self.figure)
        self.canvas.callbacks.connect('button_press_event', self.mouseDown)
        self.canvas.callbacks.connect('motion_notify_event', self.mouseMotion)
        self.canvas.callbacks.connect('button_release_event', self.mouseUp)
        self.state = ''
        self.mouseInfo = (None, None, None, None)
        self.f0 = Param(2., minimum=0., maximum=6.)
        self.A = Param(1., minimum=0.01, maximum=2.)
        self.createPlots()

        self.f0.attach(self)
        self.A.attach(self)

    def createSliders(self, panel):
        self.frequencySliderGroup = SliderGroup(
            panel,
            label='Frequency f0:',
            param=self.f0)
        self.amplitudeSliderGroup = SliderGroup(panel, label=' Amplitude a:',
                                                param=self.A)

    def mouseDown(self, event):
        if self.lines[0].contains(event)[0]:
            self.state = 'frequency'
        elif self.lines[1].contains(event)[0]:
            self.state = 'time'
        else:
            self.state = ''
        self.mouseInfo = (event.xdata, event.ydata,
                          max(self.f0.value, .1),
                          self.A.value)

    def mouseMotion(self, event):
        if self.state == '':
            return
        x, y = event.xdata, event.ydata
        if x is None:  ## outside the axes
            return
        x0, y0, f0Init, AInit = self.mouseInfo
        self.A.set(AInit + (AInit * (y - y0) / y0), self)
        if self.state == 'frequency':
            self.f0.set(f0Init + (f0Init * (x - x0) / x0))
        elif self.state == 'time':
            if (x - x0) / x0 != -1.:
                self.f0.set(1. / (1. / f0Init + (1. / f0Init * (x - x0) / x0)))

    def mouseUp(self, event):
        self.state = ''

    def createPlots(self):
        self.subplot1, self.subplot2 = self.figure.subplots(2)
        x1, y1, x2, y2 = self.compute(self.f0.value, self.A.value)
        color = (1., 0., 0.)
        self.lines += self.subplot1.plot(x1, y1, color=color, linewidth=2)
        self.lines += self.subplot2.plot(x2, y2, color=color, linewidth=2)
        self.subplot1.set_title(
            "Click and drag waveforms to change frequency and amplitude",
            fontsize=12)
        self.subplot1.set_ylabel("Frequency Domain Waveform X(f)", fontsize=8)
        self.subplot1.set_xlabel("frequency f", fontsize=8)
        self.subplot2.set_ylabel("Time Domain Waveform x(t)", fontsize=8)
        self.subplot2.set_xlabel("time t", fontsize=8)
        self.subplot1.set_xlim([-6, 6])
        self.subplot1.set_ylim([0, 1])
        self.subplot2.set_xlim([-2, 2])
        self.subplot2.set_ylim([-2, 2])
        self.subplot1.text(0.05, .95,
                           r'$X(f) = \mathcal{F}\{x(t)\}$',
                           verticalalignment='top',
                           transform=self.subplot1.transAxes)
        self.subplot2.text(0.05, .95,
                           r'$x(t) = a \cdot \cos(2\pi f_0 t) e^{-\pi t^2}$',
                           verticalalignment='top',
                           transform=self.subplot2.transAxes)

    def compute(self, f0, A):
        f = np.arange(-6., 6., 0.02)
        t = np.arange(-2., 2., 0.01)
        x = A * np.cos(2 * np.pi * f0 * t) * np.exp(-np.pi * t ** 2)
        X = A / 2 * \
            (np.exp(-np.pi * (f - f0) ** 2) + np.exp(-np.pi * (f + f0) ** 2))
        return f, X, t, x

    def setKnob(self, value):
        x1, y1, x2, y2 = self.compute(self.f0.value, self.A.value)
        self.lines[0].set(xdata=x1, ydata=y1)
        self.lines[1].set(xdata=x2, ydata=y2)
        self.canvas.draw()

App 클래스 정의

App 클래스는 애플리케이션을 생성하고 GUI 를 표시합니다.

class App(wx.App):
    def OnInit(self):
        self.frame1 = FourierDemoFrame(parent=None, title="Fourier Demo",
                                       size=(640, 480))
        self.frame1.Show()
        return True

애플리케이션 실행

마지막 단계는 애플리케이션을 실행하는 것입니다.

if __name__ == "__main__":
    app = App()
    app.MainLoop()

요약

이 랩에서는 Python 에서 Matplotlib 을 사용하여 간단한 GUI 를 만드는 방법을 배웠습니다. 주파수 및 시간 도메인에서 두 개의 파형을 표시하는 Fourier Demo 를 만들었습니다. 플롯을 클릭하고 드래그하여 파형의 주파수와 진폭을 조정할 수 있었습니다.