2
0

More than 3 years have passed since last update.

Pythonでオリジナルのドット絵エディタを作る(3) - FFT機能の実装

Last updated at Posted at 2021-08-10

はじめに

前回の記事で作成したドット絵エディタにお遊び機能として高速フーリエ変換(FFT)機能を追加します。
搭載する機能の仕様は以下の通りです。

  • FFTボタンをクリックするとペイントツールで描いた原画像をFFTしてそのパワースペクトルと位相像を表示する。
  • IFFTボタンをクリックするとパワースペクトルと位相像から原画像に逆変換する。
  • ライブFFTボタンを有効にするとペイントツールや編集ボタンで描画したしたときに自動で上記の相互変換を行う。
  • パワースペクトルおよび位相像のキャンバスにも原画像のキャンバスと同じようにペイントツールでドット絵が描ける。

環境

前回から使用している環境に加えてmathライブラリを使用します。
mathは標準ライブラリのためインストールは不要です。
円周率(math.pi)を使用するだけなので、直接数値を入れるなどして代用する場合は不要です。

解説

FFT機能の実装方法を解説します。

キャンバスの準備

パワースペクトルと位相像のためのキャンバスを用意します。これは最初に実装したキャンバス(image1)と同じようにQImageを用意してQPaint.drawImage関数で描画すればOKです。便宜上image1を原画像と呼ぶことにし、パワースペクトルをimage2、位相像をimage3に対応させます。

フーリエ変換

NumPyのfftクラスによる画像のFFTはOpenCV-Pythonチュートリアルのフーリエ変換の項が参考になります。画像でFFTを行うにはnumpy.fft.fft2関数を使用します。その後のnumpy.fft.fftshift関数は低周波成分が中心になるように画像を4分割してずらす操作を行います。この時点で変換された配列fshiftは複素数の配列になっています。この複素数の絶対値をとる(numpy.abs)と振幅成分が取り出せ、さらに対数をとる(numpy.log)とパワースペクトルになります。複素数が0をとった場合でもエラーにならないように+1してから対数をとる補正を行っています。
また、複素数から偏角をとる(numpy.angle)と位相成分が取り出せます。このとき位相は-π〜πの範囲をとるので、これを8-bitの範囲に変換するための補正を入れて位相像として表しました。

    def drawFFT(self):
        arrImg = qimage_to_cv(self.image1)
        f = np.fft.fft2(arrImg).copy()
        fshift = np.fft.fftshift(f)
        mag = np.uint8(20*np.log(np.abs(fshift)+1))
        power = cv_to_pixmap(mag)
        ang = np.angle(fshift)
        z = np.uint8((ang/math.pi + 1) * (255/2))
        phase = cv_to_pixmap(z)
        self.back2.append(self.resizeImage(self.image2, self.image2.size()))
        self.image2 = power
        self.back3.append(self.resizeImage(self.image3, self.image3.size()))
        self.image3 = phase

フーリエ逆変換

逆変換ではまず、パワースペクトルと位相像のそれぞれに対して順変換で最後に行った補正を戻し、合成して複素数にします。複素数への合成は要素ごとの積(numpy.multiply:アダマール積)によって行います。

    def drawIFFT(self):
        arrImg = qimage_to_cv(self.image2)
        amplitude = (np.exp((arrImg / 20)-1))
        phaseImg = qimage_to_cv(self.image3)
        phase = np.float32((phaseImg / 127.5 - 1)*math.pi)
        combined = np.multiply(amplitude, np.exp(1j*phase))
        ifshift = np.fft.ifftshift(combined)
        ifimg = np.fft.ifft2(ifshift)
        absifimg = np.abs(ifimg)
        y = np.uint8(absifimg * 255 / np.amax(absifimg)).copy()
        ifftqImg = cv_to_pixmap(y)
        self.back1.append(self.resizeImage(self.image1, self.image1.size()))
        self.image1 = ifftqImg

ライブ変換

ライブFFTによるリアルタイム変換は、mouseReleaseEventでの描画後、および、消去ボタン(AllClear)の実行後に上記の変換を行う判定を追加するだけで実装できます。

全体のソースコード

全体のソースコードを示します。

FFT-paint.py
import sys
import copy
import math
import cv2
import numpy as np
from collections import deque
from PySide6.QtCore import *
from PySide6.QtWidgets import *
from PySide6.QtGui import *

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.canvas = Canvas()
        self.setCentralWidget(self.canvas)
        self.setFocus()
        self.setFocusPolicy(Qt.ClickFocus)
        #ウィンドウ設定
        self.initUI()

    def initUI(self):
        self.setGeometry(100, 200, 1800, 650)
        self.setWindowTitle("PAINTapl")
        #メニューバーの設定
        menubar = self.menuBar()
        openAct = QAction("&画像開く", self)
        openPowerImgAct = QAction("&パワースペクトルを読み込む", self)
        openPhaseImgAct = QAction("&位相像を読み込む", self)
        saveAct = QAction("&保存", self)
        savePowerImgAct = QAction("&パワースペクトルを保存", self)
        savePhaseImgAct = QAction("&位相像を保存", self)
        exitAct = QAction("&終了", self)
        openAct.setShortcut("Ctrl+O")
        openAct.triggered.connect(self.openFile)
        openPowerImgAct.triggered.connect(self.openPowerImg)
        openPhaseImgAct.triggered.connect(self.openPhaseImg)
        saveAct.setShortcut("Ctrl+S")
        saveAct.triggered.connect(self.saveFile)
        savePowerImgAct.triggered.connect(self.savePowerImg)
        savePhaseImgAct.triggered.connect(self.savePhaseImg)
        exitAct.triggered.connect(QCoreApplication.instance().quit)
        fileMenu = menubar.addMenu("&ファイル")
        fileMenu.addAction(openAct)
        fileMenu.addAction(openPowerImgAct)
        fileMenu.addAction(openPhaseImgAct)
        fileMenu.addAction(saveAct)
        fileMenu.addAction(savePowerImgAct)
        fileMenu.addAction(savePhaseImgAct)
        fileMenu.addAction(exitAct)

        #画像ラベル
        font = QFont()
        font.setPointSize(20)
        self.ImgLabel = QLabel(self)
        self.ImgLabel.move(378, 570)
        self.ImgLabel.resize(200, 50)
        self.ImgLabel.setText("原画像")
        self.ImgLabel.setFont(font)
        self.PowerImgLabel = QLabel(self)
        self.PowerImgLabel.move(885, 570)
        self.PowerImgLabel.resize(200, 50)
        self.PowerImgLabel.setText("パワースペクトル")
        self.PowerImgLabel.setFont(font)
        self.PhaseImgLabel = QLabel(self)
        self.PhaseImgLabel.move(1450, 570)
        self.PhaseImgLabel.resize(200, 50)
        self.PhaseImgLabel.setText("位相像")
        self.PhaseImgLabel.setFont(font)
        #FFTボタン設定
        self.FFTButton = QPushButton(self)
        self.FFTButton.move(594, 570)
        self.FFTButton.resize(70, 30)
        self.FFTButton.setText("FFT")
        self.FFTButton.clicked.connect(self.performeFFT)
        #IFFTボタン設定
        self.IFFTButton = QPushButton(self)
        self.IFFTButton.move(700, 570)
        self.IFFTButton.resize(70, 30)
        self.IFFTButton.setText("IFFT")
        self.IFFTButton.clicked.connect(self.performeIFFT)
        #ライブFFTボタン設定
        self.liveFFTButton = QPushButton(self)
        self.liveFFTButton.move(650, 600)
        self.liveFFTButton.resize(70, 30)
        self.liveFFTButton.setText("ライブFFT")
        self.liveFFTButton.setCheckable(True)
        self.liveFFTButton.clicked.connect(self.liveFFT)
        #原画像のグリッド表示切り替え
        self.cb_image1 = QCheckBox(self)
        self.cb_image1.move(300, 20)
        self.cb_image1.resize(200, 30)
        self.cb_image1.setText("グリッドを表示")
        self.cb_image1.setCheckState(Qt.Checked)
        self.cb_image1.stateChanged.connect(self.gridControl1)
        #パワースペクトルのグリッド表示切り替え
        self.cb_image2 = QCheckBox(self)
        self.cb_image2.move(850, 20)
        self.cb_image2.resize(200, 30)
        self.cb_image2.setText("グリッドを表示")
        self.cb_image2.setCheckState(Qt.Checked)
        self.cb_image2.stateChanged.connect(self.gridControl2)
        #位相像のグリッド表示切り替え
        self.cb_image3 = QCheckBox(self)
        self.cb_image3.move(1370, 20)
        self.cb_image3.resize(200, 30)
        self.cb_image3.setText("グリッドを表示")
        self.cb_image3.setCheckState(Qt.Checked)
        self.cb_image3.stateChanged.connect(self.gridControl3)

        #幅入力のスピンボックス
        self.sb_width = QSpinBox(self)
        self.sb_width.setFocusPolicy(Qt.NoFocus)
        self.sb_width.move(70, 350)
        self.sb_width.resize(40,18)
        self.sb_width.setMinimum(1)
        self.sb_width.valueChanged.connect(self.valueToCanvas)
        self.sb_width_label = QLabel(self)
        self.sb_width_label.move(10, 350)
        self.sb_width_label.resize(50,18)
        self.sb_width_label.setText("横:")
        self.sb_width_label.setAlignment(Qt.AlignRight)
        #スペース入力のスピンボックス
        self.sb_height = QSpinBox(self)
        self.sb_height.setFocusPolicy(Qt.NoFocus)
        self.sb_height.move(70, 370)
        self.sb_height.resize(40,18)
        self.sb_height.setMinimum(1)
        self.sb_height.valueChanged.connect(self.valueToCanvas)
        self.sb_height_label = QLabel(self)
        self.sb_height_label.move(10, 370)
        self.sb_height_label.resize(50,18)
        self.sb_height_label.setText("縦:")
        self.sb_height_label.setAlignment(Qt.AlignRight)
        #スペース入力のスピンボックス
        self.sb_space = QSpinBox(self)
        self.sb_space.setFocusPolicy(Qt.NoFocus)
        self.sb_space.move(70, 390)
        self.sb_space.resize(40,18)
        self.sb_space.setMinimum(1)
        self.sb_space.valueChanged.connect(self.valueToCanvas)
        self.sb_space_label = QLabel(self)
        self.sb_space_label.move(10, 390)
        self.sb_space_label.resize(50,18)
        self.sb_space_label.setText("スペース:")
        self.sb_space_label.setAlignment(Qt.AlignRight)

        #ボタングループの設定
        self.bg_paint = QButtonGroup()
        self.bg_image1 = QButtonGroup()
        #ペイントツール用のトグルボタン
        self.dt_paint = [
            (0, 0, 0, "Pen", "Icon_Pen.png"),
            (0, 1, 1, "Line", "Icon_Line.png"),
            (0, 2, 2, "RectLine", "Icon_RectLine.png"),
            (0, 3, 3, "RectFill", "Icon_RectFill.png"),
            (0, 4, 4, "Dropper", "Icon_Dropper.png"),
            (0, 5, 5, "PaintFill", "Icon_PaintFill.png"),
            (0, 6, 6, "Lattice", "Icon_Lattice.png"),
        ]
        for x, y, id, _, iconImg in self.dt_paint:
            qpix = QPixmap.fromImage(iconImg)
            icon = QIcon(qpix)
            btn = QPushButton(icon, "", self)
            btn.move(40*x + 60, 40*y + 60)
            btn.resize(40, 40)
            btn.setIconSize(qpix.size())
            btn.setCheckable(True)
            if id == 0:
                btn.setChecked(True)
            self.bg_paint.addButton(btn, id)
        self.bg_paint.setExclusive(True)  # 排他的なボタン処理にする
        self.bg_paint.buttonClicked.connect(self.changedButton)
        #編集ボタン
        self.dt_image1 = [
            (0, 0, 0, "ClearAll", "Icon_Clear.png"),
            (1, 0, 1, "Previous", "Icon_Previous.png"),
            (2, 0, 2, "Next", "Icon_Next.png"),
        ]
        for x, y, id, _, iconImg in self.dt_image1:
            qpix = QPixmap.fromImage(iconImg)
            icon = QIcon(qpix)
            btn = QPushButton(icon, "", self)
            btn.move(40*x + self.canvas.offsetx_image1, 40*y + 10)
            btn.resize(40, 40)
            btn.setIconSize(qpix.size())
            self.bg_image1.addButton(btn, id)
        self.bg_image1.buttonClicked.connect(self.pushedButton_image1)

    def changedButton(self):
        id = self.bg_paint.checkedId()
        self.canvas.drawMode = self.dt_paint[id][3]

    def pushedButton_image1(self, obj):
        id = self.bg_image1.id(obj)
        processMode = self.dt_image1[id][3]
        self.pushedButton(processMode, self.canvas.image1, self.canvas.back1, self.canvas.next1)

    def pushed_button_image2(self, obj):
        id = self.bg_image2.id(obj)
        processMode = self.dt_image2[id][3]
        self.pushed_button(processMode, self.canvas.image2, self.canvas.back2, self.canvas.next2)

    def pushed_button_image3(self, obj):
        id = self.bg_image3.id(obj)
        processMode = self.dt_image3[id][3]
        self.pushed_button(processMode, self.canvas.image3, self.canvas.back3, self.canvas.next3)

    def pushed_button(self, processMode, image, queBack, queNext):
        if processMode == "EraseAll":
            queBack.append(self.canvas.resizeImage(image, image.size()))
            if id(image) == id(self.canvas.image1):
                image.fill(255)
            elif id(image) == id(self.canvas.image2):
                image.fill(0)
                image.setPixelColor(self.canvas.imgSize/2, self.canvas.imgSize/2, QColor(255, 255, 255))
            elif id(image) == id(self.canvas.image3):
                image.fill(127)
        elif processMode == "Previous":
            if queBack:
                back_ = queBack.pop()
                queNext.append(self.canvas.resizeImage(image, image.size()))
                image.swap(back_)
                self.update()
        elif processMode == "Next":
            if queNext:
                next_ = queNext.pop()
                queBack.append(self.canvas.resizeImage(image, image.size()))
                image.swap(next_)
                self.update()
        if self.canvas.check_liveFFT:
            if id(image) == id(self.canvas.image1):
                self.canvas.drawFFT()
            elif id(image) == id(self.canvas.image2) or id(image) == id(self.canvas.image3):
                self.canvas.drawIFFT()

    def gridControl1(self):
        if self.cb_image1.checkState() == Qt.Checked:
            self.canvas.gridOn(self.canvas.image1_grid)
        elif self.cb_image1.checkState() == Qt.Unchecked:
            self.canvas.gridOff(self.canvas.image1_grid)
        self.canvas.update()

    def gridControl2(self):
        if self.cb_image2.checkState() == Qt.Checked:
            self.canvas.gridOn(self.canvas.image2_grid)
        elif self.cb_image2.checkState() == Qt.Unchecked:
            self.canvas.gridOff(self.canvas.image2_grid)
        self.canvas.update()

    def gridControl3(self):
        if self.cb_image3.checkState() == Qt.Checked:
            self.canvas.gridOn(self.canvas.image3_grid)
        elif self.cb_image3.checkState() == Qt.Unchecked:
            self.canvas.gridOff(self.canvas.image3_grid)
        self.canvas.update()

    def openFile(self):
        path = QDir.currentPath()
        fileName, _ = QFileDialog.getOpenFileName(self, "Open File", path)
        if fileName:
            self.canvas.openImage(fileName, self.canvas.image1)

    def openPowerImg(self):
        path = QDir.currentPath()
        fileName, _ = QFileDialog.getOpenFileName(self, "Open File", path)
        if fileName:
            self.canvas.openImage(fileName, self.canvas.image2)

    def openPhaseImg(self):
        path = QDir.currentPath()
        fileName, _ = QFileDialog.getOpenFileName(self, "Open File", path)
        if fileName:
            self.canvas.openImage(fileName, self.canvas.image3)

    def saveFile(self):
        path = QDir.currentPath()
        fileName, _ = QFileDialog.getSaveFileName(self, "Save as", path)
        if fileName:
            if not fileName.endswith(".png"):
                fileName = fileName + ".png"
            return self.canvas.saveImage(fileName, self.canvas.image1)
        return False

    def savePowerImg(self):
        path = QDir.currentPath()
        fileName, _ = QFileDialog.getSaveFileName(self, "Save as", path)
        if fileName:
            if not fileName[-4:] == ".png":
                fileName = fileName + ".png"
            return self.canvas.saveImage(fileName, self.canvas.image2)
        return False

    def savePhaseImg(self):
        path = QDir.currentPath()
        fileName, _ = QFileDialog.getSaveFileName(self, "Save as", path)
        if fileName:
            if not fileName[-4:] == ".png":
                fileName = fileName + ".png"
            return self.canvas.saveImage(fileName, self.canvas.image3)
        return False

    def performeFFT(self):
        self.canvas.drawFFT()

    def performeIFFT(self):
        self.canvas.drawIFFT()

    def liveFFT(self):
        if self.liveFFTButton.isChecked():
            self.canvas.check_liveFFT = True
        else:
            self.canvas.check_liveFFT = False

    def DrawLineMode(self):
        self.canvas.drawMode = "Line"

    def valueToCanvas(self):
        self.canvas.LatticeWidth = self.sb_width.value()
        self.canvas.LatticeHeight = self.sb_height.value()
        self.canvas.LatticeSpace = self.sb_space.value()

    def keyPressEvent(self, event):
        if event.isAutoRepeat():
            return
        pressed = event.key()
        if pressed == Qt.Key_Shift:
            self.canvas.shiftKey = True

    def keyReleaseEvent(self, event):
        pressed = event.key()
        if pressed == Qt.Key_Shift:
            self.canvas.shiftKey = False

class Canvas(QWidget):
    def __init__(self, parent=None):
        super(Canvas, self).__init__(parent)

        self.imgMag = 16                                    #画像の拡大倍率
        self.canvasSize = 512                               #画像領域の大きさ
        self.imgSize = int(self.canvasSize/self.imgMag)     #画像の実際の画像サイズ
        self.divColor = 17                                  #色の分割数
        self.paletteMag = 16                                #パレットの拡大倍率

        self.myPenWidth = 1
        self.myPenColor = QColor(0,0,0)
        self.image1 = QImage()
        self.image2 = QImage()
        self.image3 = QImage()
        self.guide_image1 = QImage()
        self.guide_image2 = QImage()
        self.guide_image3 = QImage()
        self.image1_grid = QImage(self.canvasSize, self.canvasSize, QImage.Format_ARGB32)
        self.image2_grid = QImage(self.canvasSize, self.canvasSize, QImage.Format_ARGB32)
        self.image3_grid = QImage(self.canvasSize, self.canvasSize, QImage.Format_ARGB32)
        self.palette = QImage(1, self.divColor, QImage.Format_Grayscale8)
        self.Frame = QImage()
        self.shiftKey = False
        self.pix = QPoint(0,0)

        self.check1 = False
        self.check2 = False
        self.check3 = False
        self.check_palette = False
        self.check_liveFFT = False

        self.back1 = deque(maxlen = 10)
        self.next1 = deque(maxlen = 10)
        self.back2 = deque(maxlen = 10)
        self.next2 = deque(maxlen = 10)
        self.back3 = deque(maxlen = 10)
        self.next3 = deque(maxlen = 10)

        #ウィンドウ左上からの原画像キャンバスの位置
        self.offsetx_image1 = 150
        self.offsety_image1 = 60
        #ウィンドウ左上からのパワースペクトルキャンバスの位置
        self.offsetx_image2 = 700
        self.offsety_image2 = 60
        #ウィンドウ左上からの位相像キャンバスの位置
        self.offsetx_image3 = 1220
        self.offsety_image3 = 60
        #ウィンドウ左上からのパレットの位置
        self.offsetx_palette = 120
        self.offsety_palette = 300

        #各キャンバスの領域
        self.rect1_place = QRect(self.offsetx_image1, self.offsety_image1, self.canvasSize, self.canvasSize)
        self.rect2_place = QRect(self.offsetx_image2, self.offsety_image2, self.canvasSize, self.canvasSize)
        self.rect3_place = QRect(self.offsetx_image3, self.offsety_image3, self.canvasSize, self.canvasSize)
        #パレットの領域
        self.rect_palette_place = QRect(self.offsetx_palette, self.offsety_palette, self.paletteMag, self.paletteMag*(self.divColor))

        self.setGrid(self.image1_grid)
        self.setGrid(self.image2_grid)
        self.setGrid(self.image3_grid)
        self.setPalette(self.palette)

        self.drawMode = "Pen"
        self.LatticeWidth = 1
        self.LatticeHeight = 1
        self.LatticeSpace = 1

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.lastPos = event.position().toPoint()
            if self.rect1_place.contains(self.lastPos.x(), self.lastPos.y()):
                if self.drawMode == "Pen" or self.drawMode == "Line" or self.drawMode == "RectLine" or self.drawMode == "RectFill":
                    self.back1.append(self.resizeImage(self.image1, self.image1.size()))
                self.check1 = True
            elif self.rect2_place.contains(self.lastPos.x(), self.lastPos.y()):
                if self.drawMode == "Pen" or self.drawMode == "Line" or self.drawMode == "RectLine" or self.drawMode == "RectFill":
                    self.back2.append(self.resizeImage(self.image2, self.image2.size()))
                self.check2 = True
            elif self.rect3_place.contains(self.lastPos.x(), self.lastPos.y()):
                if self.drawMode == "Pen" or self.drawMode == "Line" or self.drawMode == "RectLine" or self.drawMode == "RectFill":
                    self.back3.append(self.resizeImage(self.image3, self.image3.size()))
                self.check3 = True
            elif self.rect_palette_place.contains(self.lastPos.x(), self.lastPos.y()):
                self.check_palette = True

    def mouseMoveEvent(self, event):
        if event.buttons() and Qt.LeftButton:
            if self.check1 or self.check2 or self.check3:
                if self.check1:
                    image = self.image1
                    guide_image = self.guide_image1
                    rect = self.rect1_place
                elif self.check2:
                    image = self.image2
                    guide_image = self.guide_image2
                    rect = self.rect2_place
                elif self.check3:
                    image = self.image3
                    guide_image = self.guide_image3
                    rect = self.rect3_place
                guide_image.fill(qRgba(255, 255, 255,0))
                if self.drawMode == "Pen":
                    self.drawLine(event.position().toPoint(), image, rect)
                elif self.drawMode == "Line":
                    self.drawLine(event.position().toPoint(), guide_image, rect)
                elif self.drawMode == "RectLine":
                    self.drawRect(event.position().toPoint(), guide_image, rect, False)
                elif self.drawMode == "RectFill":
                    self.drawRect(event.position().toPoint(), guide_image, rect, True)
                elif self.drawMode == "Lattice":
                    self.drawLattice(event.position().toPoint(), guide_image, rect)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            if self.check_palette:
                if self.rect_palette_place.contains(event.position().toPoint().x(), event.position().toPoint().y()):
                    self.pix = (event.position().toPoint() - QPoint(self.rect_palette_place.topLeft()) - QPoint(self.paletteMag, self.paletteMag)/2)/self.paletteMag
                    self.myPenColor = self.palette.pixelColor(self.pix)
                    self.drawFrame(self.pix, self.Frame)
            elif self.check1 or self.check2 or self.check3: 
                if self.check1:
                    image = self.image1
                    rect = self.rect1_place
                    self.guide_image1.fill(qRgba(255, 255, 255,0))
                elif self.check2:
                    image = self.image2
                    rect = self.rect2_place
                    self.guide_image2.fill(qRgba(255, 255, 255,0))
                elif self.check3:
                    image = self.image3
                    rect = self.rect3_place
                    self.guide_image3.fill(qRgba(255, 255, 255,0))
                if self.drawMode == "Pen" or self.drawMode == "Line":
                    self.drawLine(event.position().toPoint(), image, rect)
                elif self.drawMode == "RectLine":
                    self.drawRect(event.position().toPoint(), image, rect, False)
                elif self.drawMode == "RectFill":
                    self.drawRect(event.position().toPoint(), image, rect, True)
                elif self.drawMode == "Dropper":
                    self.pickColor(event.position().toPoint(), image, rect)
                elif self.drawMode == "PaintFill":
                    self.paintFill(event.position().toPoint(), image, rect)
                elif self.drawMode == "Lattice":
                    self.drawLattice(event.position().toPoint(), image, rect)
            if self.check_liveFFT:
                if self.check1:
                    self.drawFFT()
                elif self.check2 or self.check3:
                    self.drawIFFT()
            self.check1 = False
            self.check2 = False
            self.check3 = False
            self.check_palette = False

    #ペンツール、線ツール
    def drawLine(self, endPos, image, rect):
        painter = QPainter(image)
        if id(image) == id(self.guide_image1) or id(image) == id(self.guide_image2) or id(image) == id(self.guide_image3):
            painter.setPen(
                QPen(QColor(0,0,255,191), self.myPenWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
            )
        else:
            painter.setPen(
                QPen(self.myPenColor, self.myPenWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
            )
        v1 = (self.lastPos-rect.topLeft()-QPoint(self.imgMag, self.imgMag)/2)/self.imgMag
        v2 = (endPos-rect.topLeft()-QPoint(self.imgMag, self.imgMag)/2)/self.imgMag
        #Shiftキーを押している時は線を水平・垂直・45°方向に限定する
        if self.shiftKey:
            if abs((v2 - v1).x()) > abs((v2 - v1).y()) * 1.5:
                v2.setY(v1.y())
            elif abs((v2 - v1).y()) > abs((v2 - v1).x()) * 1.5:
                v2.setX(v1.x())
            else:
                if v2.x() > v1.x():
                    if v2.y() > v1.y():                     #右下方向
                        v2.setY(v1.y()+(v2.x()-v1.x()))
                    else:                                   #右上方向
                        v2.setY(v1.y()-(v2.x()-v1.x()))
                else:
                    if v2.y() > v1.y():                     #左下方向
                        v2.setY(v1.y()-(v2.x()-v1.x()))
                    else:                                   #右下方向
                        v2.setY(v1.y()+(v2.x()-v1.x()))
        painter.drawLine(v1, v2 )
        self.update()
        if self.drawMode == "Pen":
            self.lastPos = QPoint(endPos)

    #四角形ツール(fillがTrueで塗りつぶし有り、Falseで塗りつぶし無し)
    def drawRect(self, endPos, image, rect, fill):
        painter = QPainter(image)
        if id(image) == id(self.guide_image1) or id(image) == id(self.guide_image2) or id(image) == id(self.guide_image3):
            penColor = QColor(0,0,255,191)
        else:
            penColor = self.myPenColor
        painter.setPen(
            QPen(penColor, self.myPenWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
        )
        v1 = (self.lastPos-rect.topLeft()-QPoint(self.imgMag, self.imgMag)/2)/self.imgMag
        v2 = (endPos-rect.topLeft()-QPoint(self.imgMag, self.imgMag)/2)/self.imgMag
        #Shiftキーを押している時は正方形にする
        if self.shiftKey:
            if v2.x() > v1.x():
                if v2.y() > v1.y():                     #右下方向
                    v2.setY(v1.y()+(v2.x()-v1.x()))
                else:                                   #右上方向
                    v2.setY(v1.y()-(v2.x()-v1.x()))
            else:
                if v2.y() > v1.y():                     #左下方向
                    v2.setY(v1.y()-(v2.x()-v1.x()))
                else:                                   #右舌方向
                    v2.setY(v1.y()+(v2.x()-v1.x()))
        rect = QRect(v1, v2)
        if fill:
            painter.fillRect(rect, penColor)
        else:
            painter.drawRect(v1.x(), v1.y(),rect.width()-1, rect.height()-1)
        self.update()
        if self.drawMode == "Pen":
            self.lastPos = QPoint(endPos)

    #スポイトツール
    def pickColor(self, endPos, image, rect):
        v = (endPos-rect.topLeft()-QPoint(self.imgMag, self.imgMag)/2)/self.imgMag
        self.myPenColor = image.pixelColor(v)
        n = image.pixelColor(v).red() / (self.divColor - 1)
        self.pix = QPoint(0, n)
        self.drawFrame(self.pix, self.Frame)

    #塗りつぶしツール
    def paintFill(self, endPos, image, rect):
        v = (endPos-rect.topLeft()-QPoint(self.imgMag, self.imgMag)/2)/self.imgMag
        fillColor = self.myPenColor.getRgb()
        cvImage = qimage_to_cv(image)
        _, fillImage, _, _ = cv2.floodFill(cvImage, None, (v.x(), v.y()), fillColor[0:3], flags = 4)
        image.swap(cv_to_pixmap(fillImage))

    #格子ツール
    def drawLattice(self, endPos, image, rect):
        painter = QPainter(image)
        if id(image) == id(self.guide_image1) or id(image) == id(self.guide_image2) or id(image) == id(self.guide_image3):
            penColor = QColor(0,0,255,191)
        else:
            penColor = self.myPenColor
        painter.setPen(
            QPen(penColor, self.myPenWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
        )
        v1 = (self.lastPos-rect.topLeft()-QPoint(self.imgMag, self.imgMag)/2)/self.imgMag
        v2 = (endPos-rect.topLeft()-QPoint(self.imgMag, self.imgMag)/2)/self.imgMag
        width = self.LatticeWidth
        height = self.LatticeHeight
        space = self.LatticeSpace
        y = abs((v2-v1).y()) + 1
        x = abs((v2-v1).x()) + 1
        for j in range(y):
            if (v2 - v1).y() >= 0:
                dj = j
            else:
                dj = -j
            if j % (height + space) < height:
                for i in range(x):
                    if (v2 - v1).x() >= 0:
                        di = i
                    else:
                        di = -i
                    if i % (width + space) < width:
                        painter.drawLine(v1.x() + di, v1.y() + dj, v1.x() + di, v1.y() + dj)
        self.update()

    #グリッドの描画
    def setGrid(self, image):
        image.fill(qRgba(255, 255, 255,0))
        for i in range(0, self.canvasSize + 1, self.imgMag):
            for j in range(0, self.canvasSize):
                if not i == self.canvasSize:
                    image.setPixelColor(i, j, QColor(193,193,225,255))
                    image.setPixelColor(j, i, QColor(193,193,225,255))
                if not i == 0:
                    image.setPixelColor(i-1, j, QColor(193,193,225,255))
                    image.setPixelColor(j, i-1, QColor(193,193,225,255))
        #中心の線を赤色にする
        for j in range(0, self.canvasSize):
            image.setPixelColor(self.canvasSize/2, j, QColor(255,127,127,255))
            image.setPixelColor(j, self.canvasSize/2, QColor(255,127,127,255))
            image.setPixelColor(self.canvasSize/2-1, j, QColor(255,127,127,255))
            image.setPixelColor(j, self.canvasSize/2-1, QColor(255,127,127,255))

    #パレットの描画
    def setPalette(self, image):
        for i in range(self.divColor-1):
            gray = i*256/(self.divColor - 1)
            image.setPixelColor(QPoint(0,i), QColor(gray,gray,gray))
        else:
            i += 1
            image.setPixelColor(QPoint(0,i), QColor(255,255,255))

    #色選択枠の描画
    def drawFrame(self, pix, image):
        painter = QPainter(image)
        image.fill(qRgba(255, 255, 255,0))
        painter.setPen(
            QPen(QColor(255,0,0,255), 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
        )
        topleft = pix * self.paletteMag
        rect = QRect(topleft, topleft + QPoint(self.paletteMag-1, self.paletteMag-1))
        painter.drawRect(rect)
        self.update()

    def drawFFT(self):
        arrImg = qimage_to_cv(self.image1)
        f = np.fft.fft2(arrImg).copy()
        fshift = np.fft.fftshift(f)
        mag = np.uint8(20*np.log(np.abs(fshift)+1))
        power = cv_to_pixmap(mag)
        ang = np.angle(fshift)
        z = np.uint8((ang/math.pi + 1) * (255/2))
        phase = cv_to_pixmap(z)
        self.back2.append(self.resizeImage(self.image2, self.image2.size()))
        self.image2 = power
        self.back3.append(self.resizeImage(self.image3, self.image3.size()))
        self.image3 = phase

    def drawIFFT(self):
        arrImg = qimage_to_cv(self.image2)
        P_norm = np.float32(arrImg / 20)-1
        amplitude = (np.exp(P_norm))
        phaseImg = qimage_to_cv(self.image3)
        phase = np.float32((phaseImg / 127.5 - 1)*math.pi)
        combined = np.multiply(amplitude, np.exp(1j*phase))
        ifshift = np.fft.ifftshift(combined)
        ifimg = np.fft.ifft2(ifshift)
        absifimg = np.abs(ifimg)
        y = np.uint8(absifimg * 255 / np.amax(absifimg)).copy()
        ifftqImg = cv_to_pixmap(y)
        self.back1.append(self.resizeImage(self.image1, self.image1.size()))
        self.image1 = ifftqImg

    def paintEvent(self, event):
        #原画像キャンバスの描画
        painter1 = QPainter(self)
        rect1_size = QRect(0, 0, self.imgSize, self.imgSize)
        painter1.drawImage(self.rect1_place, self.image1, rect1_size)
        painter1.drawImage(self.rect1_place, self.guide_image1, rect1_size)
        #パワースペクトルキャンバスの描画
        painter2 = QPainter(self)
        painter2.drawImage(self.rect2_place, self.image2, rect1_size)
        painter2.drawImage(self.rect2_place, self.guide_image2, rect1_size)
        #位相像キャンバスの描画
        painter3 = QPainter(self)
        painter3.drawImage(self.rect3_place, self.image3, rect1_size)
        painter3.drawImage(self.rect3_place, self.guide_image3, rect1_size)
        #原画像グリッドの描画
        rect_grid = QRect(0, 0, self.canvasSize, self.canvasSize)
        painter1_grid = QPainter(self)
        painter1_grid.drawImage(self.rect1_place, self.image1_grid, rect_grid)
        #パワースペクトルグリッドの描画
        painter2_grid = QPainter(self)
        painter2_grid.drawImage(self.rect2_place, self.image2_grid, rect_grid)
        #位相像グリッドの描画
        painter3_grid = QPainter(self)
        painter3_grid.drawImage(self.rect3_place, self.image3_grid, rect_grid)
        #パレットの描画
        painter_palette = QPainter(self)
        rect_palette_size = QRect(0, 0, 1, self.divColor)
        painter_palette.drawImage(self.rect_palette_place, self.palette, rect_palette_size)
        #パレットの選択枠の描画
        painter_Frame = QPainter(self)
        rect_Frame = QRect(0, 0, self.paletteMag, self.paletteMag*(self.divColor))
        self.drawFrame(self.pix, self.Frame)
        painter_Frame.drawImage(self.rect_palette_place, self.Frame, rect_Frame)

    #ウィンドウサイズを固定している場合、ウィンドを開いたときだけ動作する処理
    def resizeEvent(self, event):
        if self.image1.width() < self.width() or self.image1.height() < self.height():
            changeWidth = max(self.width(), self.image1.width())
            changeHeight = max(self.height(), self.image1.height())
            self.image1 = self.resizeImage(self.image1, QSize(changeWidth, changeHeight))
            self.guide_image1 = self.resizeimage_grid(self.guide_image1, QSize(changeWidth, changeHeight))
            self.image1_grid = self.resizeimage_grid(self.image1_grid, QSize(changeWidth, changeHeight))
            self.update()
        if self.image2.width() < self.width() or self.image2.height() < self.height():
            changeWidth = max(self.width(), self.image2.width())
            changeHeight = max(self.height(), self.image2.height())
            self.image2 = self.resizeImage(self.image2, QSize(changeWidth, changeHeight))
            self.guide_image2 = self.resizeimage_grid(self.guide_image2, QSize(changeWidth, changeHeight))
            self.update()
        if self.image3.width() < self.width() or self.image3.height() < self.height():
            changeWidth = max(self.width(), self.image3.width())
            changeHeight = max(self.height(), self.image3.height())
            self.image3 = self.resizeImage(self.image3, QSize(changeWidth, changeHeight))
            self.guide_image3 = self.resizeimage_grid(self.guide_image3, QSize(changeWidth, changeHeight))
            self.update()
        if self.palette.width() < self.width() or self.palette.height() < self.height():
            changeWidth = max(self.width(), self.palette.width())
            changeHeight = max(self.height(), self.palette.height())
            self.palette = self.resizeImage(self.palette, QSize(changeWidth, changeHeight))
            self.update()
        if self.Frame.width() < self.width() or self.Frame.height() < self.height():
            changeWidth = max(self.width(), self.Frame.width())
            changeHeight = max(self.height(), self.Frame.height())
            self.Frame = self.resizeimage_grid(self.Frame, QSize(changeWidth, changeHeight))
            self.update()

    def resizeImage(self, image, newSize):
        changeImage = QImage(QSize(self.imgSize, self.imgSize), QImage.Format_Grayscale8)
        changeImage.fill(255)
        painter = QPainter(changeImage)
        painter.drawImage(QPoint(0, 0), image)
        return changeImage

    def resizeimage_grid(self, image, newSize):
        changeImage = QImage(newSize, QImage.Format_ARGB32)
        changeImage.fill(qRgba(255, 255, 255,0))
        painter = QPainter(changeImage)
        painter.drawImage(QPoint(0, 0), image)
        return changeImage

    def gridOn(self, image):
        self.setGrid(image)

    def gridOff(self, image):
        image.fill(qRgba(255, 255, 255,0))

    def openImage(self, filename, image):
        if not image.load(filename):
            return False
        self.update()
        return True

    def saveImage(self, filename, image):
        if image.save(filename, "PNG"):
            return True
        else:
            return False

#QImageをndarrayに変換
def qimage_to_cv(qimage):
    w, h, d = qimage.size().width(), qimage.size().height(), qimage.depth()
    arr = np.array(qimage.bits()).reshape((h, w, d // 8))
    h, w = arr.shape[:2]
    arrImg = cv2.resize(arr, (w, h), interpolation=cv2.INTER_NEAREST)   #正常な画像の配列にするために必要な変換
    return arrImg

#ndarrayをQImageに変換
def cv_to_pixmap(cv_image):
    height, width = cv_image.shape[:2]
    bytesPerLine = cv_image.strides[0]
    image = QImage(cv_image.data, width, height, bytesPerLine, QImage.Format_Grayscale8).copy()
    return image

def main():
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

実行結果

このコードを実行すると、下の画像のウィンドウが開きます。
スクリーンショット 2021-08-10 23.03.01.png

フーリエ変換、フーリエ逆変換によりどのような絵が作られるかは色々な絵を描いて変換を試してみたほうが分かりやすいと思います。ぜひ遊んでみてください。

次回予告

次回はこのエディタにベクター画像出力する機能を追加します。

2
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0