#はじめに
前回の記事で作成したドット絵エディタにお遊び機能として高速フーリエ変換(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
)の実行後に上記の変換を行う判定を追加するだけで実装できます。
#全体のソースコード
全体のソースコードを示します。
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()
###実行結果
このコードを実行すると、下の画像のウィンドウが開きます。
フーリエ変換、フーリエ逆変換によりどのような絵が作られるかは色々な絵を描いて変換を試してみたほうが分かりやすいと思います。ぜひ遊んでみてください。
#次回予告
次回はこのエディタにベクター画像出力する機能を追加します。