Python Curve Editor
2021. 8. 6. 17:13ㆍIT
포토샵의 커브와 같은 기능을 하는 유저인터페이스를 만들기 위해서 웹서핑을 하다가 결국 찾아서 약간 수정을 해 봤다. 최신 파이썬이 3.9x인데 이 예제에서는 PyQt4를 써야해서 부득불 파이썬 3.7버전을 다운로드 받아서 약간의 수정만 거쳐서 돌려봤다.
main.py
import PyQt4.QtCore as QtCore
import PyQt4.QtGui as QtGui
import sys
from CurveWidget import CurveWidget
class Editor(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.resize(580, 480)
widget = CurveWidget(self)
widget.setGeometry(40, 40, 500, 400)
# Start application
app = QtGui.QApplication(sys.argv)
editor = Editor()
editor.show()
app.exec_()
CurveWidget.py
from __future__ import print_function
import math
import sys
from random import random
import PyQt4.QtCore as QtCore
import PyQt4.QtGui as QtGui
from panda3d.core import NurbsCurve, Vec3, Notify, HermiteCurve, CurveFitter
class Curve:
""" Interface to the NURBS curve which also manages connecting the end of the
curve with the beginning """
def __init__(self):
self._curve = None
# Append some points to the border, to make sure the curve matches at
# the edges
self._border_points = 2
# Curve color, used for displaying the curve
self._color = (0, 0, 0)
# Control points, those are some demo values
self._cv_points = [
[0,0],
[0.1, 0.5 + 0.1 * random()],
[0.15, 0.2 + 0.1 * random()],
[0.4, 0.3 + 0.1 * random()],
[0.5, 0.4 + 0.1 * random()],
[0.75, 0 + 0.1 * random()],
[0.99, 0.99]
]
# Build the curve
self.build_curve()
def get_cv_points(self):
""" Returns a list of all controll points """
return self._cv_points
def set_color(self, r, g, b):
""" Sets the display color of the curve """
self._color = (r, g, b)
def get_color(self):
""" Returns the display color of the curve """
return self._color
def _get_cv_tangent(self, index):
""" Returns the tangent of the nth point """
pb = self._cv_points[(index - 1) % len(self._cv_points)]
pm = self._cv_points[index]
pa = self._cv_points[(index + 1) % len(self._cv_points)]
get_diff = lambda p1, p2: Vec3(p1[0] - p2[0], p1[1] - p2[1], 0)
tangent_left = get_diff(pm, pb)
tangent_right = get_diff(pa, pm)
tangent_avg = (tangent_left + tangent_right) * 0.5
return tangent_avg
def build_curve(self):
""" Rebuilds the curve based on the controll point values """
sorted_points = sorted(self._cv_points, key=lambda v: v[0])
first_point = sorted_points[0]
fitter = CurveFitter()
# Duplicate curve at the beginning
for i in range(self._border_points):
end_point = self._cv_points[(-i + self._border_points - 1) % len(self._cv_points)]
end_point = first_point
fitter.add_xyz(0.0, Vec3(0, end_point[1], 0))
# Append the actual points
for point in self._cv_points:
# Clamp point x position to avoid artifacts at the beginning
point_t = max(0.01, point[0])
fitter.add_xyz(point_t, Vec3(point_t, point[1], 0))
# Duplicate curve at the end
for i in range(self._border_points):
start_point = self._cv_points[i % len(self._cv_points)]
start_point = first_point
fitter.add_xyz(1.0, Vec3(1, start_point[1], 0))
fitter.sort_points()
fitter.compute_tangents(1.0)
self._curve = fitter.make_hermite()
def set_cv_value(self, index, x_value, y_value):
""" Updates the cv point at the given index """
self._cv_points[index] = [x_value, y_value]
def get_curve_scale(self):
""" Returns the scale of the curve """
return self._curve.get_max_t()
def get_value(self, offset):
""" Returns the value on the curve ranging whereas the offset should be
from 0 to 1 (0 denotes the start of the curve). The returned value will
be a value from 0 to 1 as well. """
point = Vec3(0)
self._curve.evaluate_xyz(offset, point)
return point.y
class CurveWidget(QtGui.QWidget):
""" This is a resizeable Widget which shows an editable curve which can
be modified. """
def __init__(self, parent):
""" Constructs the CurveWidget, we start with an initial curve """
QtGui.QWidget.__init__(self, parent)
self.curves = []
# Append initial curve
curve = Curve()
curve.set_color(255, 0, 0)
self.curves.append(curve)
""" 샘플은 세 개이나 난 1개만 그린다.
curve = Curve()
curve.set_color(0, 255, 0)
self.curves.append(curve)
curve = Curve()
curve.set_color(0, 0, 255)
self.curves.append(curve)
"""
# Widget render constants
self._cv_point_size = 3
self._legend_border = 35
self._bar_h = 30
# Currently dragged control point, format is:
# (CurveIndex, PointIndex, Drag-Offset (x,y))
self._drag_point = None
# Currently selected control point, format is:
# (CurveIndex, PointIndex)
self._selected_point = None
def paintEvent(self, e):
""" Internal QT paint event, draws the entire widget """
qp = QtGui.QPainter()
qp.begin(self)
self._draw(qp)
qp.end()
def mousePressEvent(self, QMouseEvent):
""" Internal mouse-press handler """
self._drag_point = None
self._selected_point = None
mouse_pos = QMouseEvent.pos()
mouse_x = mouse_pos.x() - self._legend_border
mouse_y = mouse_pos.y()
for index, curve in enumerate(self.curves):
for cv_index, (x, y) in enumerate(curve.get_cv_points()):
point_x = self._get_x_value_for(x)
point_y = self._get_y_value_for(y)
if abs(point_x - mouse_x) < self._cv_point_size + 4:
if (abs(point_y - mouse_y)) < self._cv_point_size + 4:
drag_x_offset = point_x - mouse_x
drag_y_offset = point_y - mouse_y
self._drag_point = (index, cv_index, (drag_x_offset, drag_y_offset))
self._selected_point = (index, cv_index)
self.update()
def mouseReleaseEvent(self, QMouseEvent):
""" Internal mouse-release handler """
self._drag_point = None
def mouseMoveEvent(self, QMouseEvent):
""" Internal mouse-move handler """
# print("mouse moved:", QMouseEvent.pos())
if self._drag_point is not None:
mouse_x = QMouseEvent.pos().x() - self._drag_point[2][0] - self._legend_border
mouse_y = QMouseEvent.pos().y() - self._drag_point[2][1] - self._bar_h
# Convert to local coordinate
local_x = max(0, min(1, mouse_x / float(self.width() - self._legend_border)))
local_y = 1 - max(0, min(1, mouse_y / float(self.height() - self._legend_border - self._bar_h)))
# Set new point data
self.curves[self._drag_point[0]].set_cv_value(self._drag_point[1], local_x, local_y)
#print(local_x, local_y)
# Redraw curve
self.curves[self._drag_point[0]].build_curve()
self.update()
def _get_y_value_for(self, local_value):
""" Converts a value from 0 to 1 to a value from 0 .. canvas height """
local_value = max(0, min(1.0, 1.0 - local_value))
local_value *= self.height() - self._legend_border - self._bar_h
local_value += self._bar_h
return local_value
def _get_x_value_for(self, local_value):
""" Converts a value from 0 to 1 to a value from 0 .. canvas width """
local_value = max(0, min(1.0, local_value))
local_value *= self.width() - self._legend_border
return local_value
def _draw(self, painter):
""" Internal method to draw the widget """
canvas_width = self.width() - self._legend_border
canvas_height = self.height() - self._legend_border - self._bar_h
# Draw field background
painter.setPen(QtGui.QColor(170, 170, 170))
painter.setBrush(QtGui.QColor(230, 230, 230))
painter.drawRect(0, 0, self.width() - 1, self.height() - 1)
# Draw legend
# Compute amount of horizontal / vertical lines
# line_spacing = 20
num_vert_lines = 6 # 24 / 6 = 4, one entry per 4 hours
line_spacing_x = (self.width() - self._legend_border) / 6.0
line_spacing_y = (self.height() - self._legend_border - self._bar_h) / 20.0
num_horiz_lines = int(math.ceil(canvas_height / float(line_spacing_y)) + 1)
# Draw vertical lines
painter.setPen(QtGui.QColor(200, 200, 200))
for i in range(num_vert_lines):
line_pos = i * line_spacing_x + self._legend_border
painter.drawLine(line_pos, self._bar_h, line_pos, canvas_height + self._bar_h)
# Draw horizontal lines
painter.setPen(QtGui.QColor(200, 200, 200))
for i in range(num_horiz_lines):
line_pos = canvas_height - i * line_spacing_y + self._bar_h
painter.drawLine(self._legend_border, line_pos, self.width(), line_pos)
# Draw vetical legend labels
painter.setPen(QtGui.QColor(120, 120, 120))
for i in range(num_horiz_lines):
line_pos = canvas_height - i * line_spacing_y + self._bar_h
painter.drawText(6, line_pos + 3, str(round(float(i) / (num_horiz_lines - 1), 2)))
# Draw horizontal legend labels
for i in range(num_vert_lines + 1):
line_pos = i * line_spacing_x + self._legend_border
offpos_x = -14
if i == 0:
offpos_x = -2
elif i == num_vert_lines:
offpos_x = -33
time_string = str(int(float(i) / num_vert_lines * 24)) + ":00"
painter.drawText(line_pos + offpos_x, canvas_height + self._bar_h + 18, time_string)
# Draw curve
for index, curve in enumerate(self.curves):
painter.setPen(QtGui.QColor(*curve.get_color()))
last_value = 0
for i in range(canvas_width):
rel_offset = i / (canvas_width - 1.0)
curve_height = self._get_y_value_for(curve.get_value(rel_offset))
if i == 0:
last_value = curve_height
painter.drawLine(self._legend_border + i - 1, last_value, self._legend_border + i, curve_height)
print(rel_offset, ",", abs(curve_height/365-1))
last_value = curve_height
# Draw the CV points of the curve
painter.setBrush(QtGui.QColor(240, 240, 240))
for cv_index, (x, y) in enumerate(curve.get_cv_points()):
offs_x = x * canvas_width + self._legend_border
offs_y = (1 - y) * canvas_height + self._bar_h
if self._selected_point and self._selected_point[0] == index and self._selected_point[1] == cv_index:
painter.setPen(QtGui.QColor(255, 0, 0))
else:
painter.setPen(QtGui.QColor(100, 100, 100))
painter.drawRect(offs_x - self._cv_point_size, offs_y - self._cv_point_size,
2 * self._cv_point_size, 2 * self._cv_point_size)
# Draw bar
if len(self.curves) == 1:
bar_curve = self.curves[0]
else:
bar_curve = self.curves[0:3]
#for i in range(canvas_width - 1):
#xpos = self._legend_border + i
#relv = float(i) / float(canvas_width)
""" 지우면 에러 없음... ㅜㅜ
if len(bar_curve) == 1:
val = max(0, min(255, int(bar_curve.get_value(relv) * 255.0)))
painter.setPen(QtGui.QColor(val, val, val))
painter.drawLine(xpos, 6, xpos, self._bar_h - 6)
else:
r = max(0, min(255, int(bar_curve[0].get_value(relv) * 255.0)))
g = max(0, min(255, int(bar_curve[1].get_value(relv) * 255.0)))
b = max(0, min(255, int(bar_curve[2].get_value(relv) * 255.0)))
painter.setPen(QtGui.QColor(r, g, b))
"""
#painter.drawLine(xpos, 6, xpos, self._bar_h - 6)
반응형
'IT' 카테고리의 다른 글
파이썬 - 투명한 배경을 가진 이미지를 PDF로 저장하기 (0) | 2021.12.15 |
---|---|
Python / Image to PDF (0) | 2021.10.29 |
Python Image Crop Code (0) | 2021.08.06 |
Python Image + Masking Image 합성 (0) | 2021.08.06 |
Python Making Image 만들기 (0) | 2021.08.06 |