Created
April 20, 2012 22:38
-
-
Save KL-7/2432386 to your computer and use it in GitHub Desktop.
B-splines editor.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import division | |
import math | |
def bspline(points, m): | |
''' | |
points - control points | |
m - degree of B-splines | |
''' | |
M = len(points) - 1 | |
n = M - m + 1 | |
knots = [0] * m + [k / n for k in xrange(n + 1)] + [1] * m | |
# uncomment to make two central knots equal | |
# if n > 1: | |
# knots = [0] * m + [k / (n - 1) for k in xrange(n)] + [1] * m | |
# knots.insert(len(knots) // 2, knots[len(knots) // 2]) | |
# if n > 2: | |
# knots = [0] * m + [k / (n - 2) for k in xrange(n - 1)] + [1] * m | |
# knots.insert(len(knots) // 2, knots[len(knots) // 2]) | |
# knots.insert(len(knots) // 2, knots[len(knots) // 2]) | |
def curve(t): | |
if t == 1: | |
return points[-1] | |
k = m | |
while k + 1 < len(knots) and t > knots[k + 1]: k += 1 | |
# values of N[m, k-m],...,N[m, k] at t | |
Nk = [1] + [0] * m | |
V = lambda m, i, t: (t - knots[i]) / (knots[i + m] - knots[i]) \ | |
if knots[i] != knots[i + m] \ | |
else 0 | |
for i in xrange(1, m + 1): | |
for j in xrange(i, -1, -1): | |
# count N[i, k-j] | |
if j: | |
Nk[j] = Nk[j] * V(i, k - j, t) + Nk[j - 1] * (1 - V(i, k - j + 1, t)) | |
else: | |
Nk[j] = Nk[j] * V(i, k - j, t) | |
Nk.reverse() | |
return [sum(p[j] * N for p, N in zip(points[k-m:k+1], Nk)) for j in (0, 1)] | |
return curve | |
if __name__ == '__main__': | |
import matplotlib.pyplot as plt | |
# control_points = ((2, 3), (1, 1), (0, 0), (2, 1), (3, 0), (3, 3), (4, 1), (6, 3)) | |
control_points = ((0, 1), (1, 3), (3, 1), (1, 0)) | |
degree = 2 | |
curve = bspline(control_points, degree) | |
N = 50 | |
points = map(curve, (i / N for i in xrange(N + 1))) | |
x, y = ([p[j] for p in points] for j in (0, 1)) | |
plt.plot(x, y) | |
x, y = ([p[j] for p in control_points] for j in (0, 1)) | |
plt.plot(x, y) | |
plt.grid(True) | |
plt.show() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import math | |
import bspline | |
from PySide.QtCore import * | |
from PySide.QtGui import * | |
class ControlPoint(QGraphicsEllipseItem): | |
PointRadius = 5 | |
BackgroundColor = QColor('#BCF1F7') | |
def __init__(self, number, x, y, spline, *args, **kwargs): | |
super(ControlPoint, self).__init__(*args, **kwargs) | |
self.setFlags(QGraphicsItem.ItemIsMovable | | |
QGraphicsItem.ItemSendsGeometryChanges) | |
self.segments = [] | |
self.spline = spline | |
self.number = number | |
self.label = QGraphicsTextItem(str(number), self) | |
self.setBrush(self.BackgroundColor) | |
self.setPos(x, y) | |
self.setRect(-self.PointRadius, -self.PointRadius, | |
2 * self.PointRadius, 2 * self.PointRadius) | |
def addSegment(self, segment): | |
self.segments.append(segment) | |
def itemChange(self, change, value): | |
if change == QGraphicsItem.ItemPositionHasChanged: | |
for segment in self.segments: | |
segment.trackPoints() | |
self.spline.updateSpline() | |
return super(ControlPoint, self).itemChange(change, value) | |
class Segment(QGraphicsLineItem): | |
def __init__(self, startPoint, endPoint, *args, **kwargs): | |
super(Segment, self).__init__(*args, **kwargs) | |
self.setZValue(-1) | |
self.startPoint = startPoint | |
self.startPoint.addSegment(self) | |
self.endPoint = endPoint | |
self.endPoint.addSegment(self) | |
self.trackPoints() | |
def trackPoints(self): | |
self.setLine(QLineF(self.startPoint.pos(), self.endPoint.pos())) | |
class BSpline(QGraphicsPathItem): | |
def __init__(self, degree, controlPointsNumber, width, height, renderingStepsNumber=200, *args, **kwargs): | |
super(BSpline, self).__init__(*args, **kwargs) | |
self.width = width | |
self.height = height | |
self.controlPoints = [] | |
self.segments = [] | |
self.renderingStepsNumber = renderingStepsNumber | |
self.controlPointsNumber = controlPointsNumber | |
self.degree = min(degree, self.maxDegree()) | |
self.generateConrolPoints() | |
self.updateSpline() | |
def setRenderingStepsNumber(self, renderingStepsNumber): | |
self.renderingStepsNumber = renderingStepsNumber | |
self.updateSpline() | |
def setDegree(self, degree): | |
if degree == self.degree: | |
return | |
self.degree = degree | |
self.updateSpline() | |
def setControlPointsNumber(self, controlPointsNumber): | |
if controlPointsNumber == self.controlPointsNumber or \ | |
controlPointsNumber < 2: | |
return | |
self.controlPointsNumber = controlPointsNumber | |
self.generateConrolPoints() | |
if self.degree > self.maxDegree(): | |
self.degree = self.maxDegree() | |
self.updateSpline() | |
def maxDegree(self): | |
return self.controlPointsNumber - 1 | |
def generateConrolPoints(self): | |
self.circleControlPoints() | |
def circleControlPoints(self): | |
if self.scene(): | |
for controlPoint in self.controlPoints: | |
self.scene().removeItem(controlPoint) | |
for segment in self.segments: | |
self.scene().removeItem(segment) | |
self.controlPoints = [] | |
self.segments = [] | |
r = min(self.width, self.height) / 3.0 | |
step = 2 * math.pi / self.controlPointsNumber | |
for i in xrange(self.controlPointsNumber): | |
alpha = i * step | |
x = self.width / 2 + r * math.cos(alpha) | |
y = self.height / 2 + r * math.sin(alpha) | |
point = ControlPoint(i + 1, x, y, self, parent=self) | |
self.controlPoints.append(point) | |
if len(self.controlPoints) > 1: | |
segment = Segment(self.controlPoints[-2], | |
self.controlPoints[-1], | |
parent=self) | |
self.segments.append(segment) | |
def updateSpline(self): | |
if self.controlPointsNumber != len(self.controlPoints): | |
return | |
cp2xy = lambda p: (p.scenePos().x(), p.scenePos().y()) | |
curve = bspline.bspline(map(cp2xy, self.controlPoints), self.degree) | |
points = map(curve, (i / float(self.renderingStepsNumber) | |
for i in xrange(self.renderingStepsNumber + 1))) | |
path = QPainterPath() | |
path.addPolygon(QPolygonF([QPointF(x, y) for x, y in points])) | |
self.setPath(path) | |
def __str__(self): | |
info = [] | |
info.append('B-Spline') | |
info.append('Degree: %d' % self.degree) | |
info.append('Control points number: %d' % self.controlPointsNumber) | |
cp2xy = lambda p: '(%2.3f, %2.3f)' % (p.scenePos().x(), self.height - p.scenePos().y()) | |
info.append('Control points: [%s]' % ' '.join(map(cp2xy, self.controlPoints))) | |
return '\n'.join(info) | |
class SplineScene(QGraphicsScene): | |
BackgroundColor = QColor('#DFFCDE') | |
GridColor = QColor('#99C997') | |
AxisColor = QColor('#000000') | |
AxisArrowDX = 5 | |
AxisArrowDY = 4 | |
SceneRect = QRect(0, 0, 980, 700) | |
GridXStep = 70 | |
GridYStep = 70 | |
def __init__(self, splineDegree, controlPointsNumber, SplineType, *args, **kwargs): | |
super(SplineScene, self).__init__(*args, **kwargs) | |
self.setSceneRect(self.SceneRect) | |
self.setBackgroundBrush(self.BackgroundColor) | |
self.addCoordinateSystem() | |
self.spline = SplineType(splineDegree, controlPointsNumber, | |
self.width(), self.height()) | |
self.addItem(self.spline) | |
def addCoordinateSystem(self): | |
self.addGrid() | |
self.addAxis() | |
def addGrid(self): | |
pen = QPen(self.GridColor) | |
x = 0 | |
while x <= self.width(): | |
self.addLine(x, 0, x, self.height(), pen) | |
if x: | |
label = self.addText(str(x)) | |
label.setPos(x - label.boundingRect().width() / 2, | |
self.height()) | |
x += self.GridXStep | |
y = 0 | |
while y <= self.height(): | |
self.addLine(0, y, self.width(), y, pen) | |
if self.height() - y: | |
label = self.addText(str((self.height() - y))) | |
label.setPos(-label.boundingRect().width() - 4, | |
y - label.boundingRect().height() / 2) | |
y += self.GridYStep | |
label = self.addText('0') | |
label.setPos(-label.boundingRect().width() - 4, | |
self.height()) | |
def addAxis(self): | |
pen = QPen(self.AxisColor, 2) | |
leftEnd = QPointF(-self.GridXStep, self.height()) | |
rightEnd = QPointF(self.width() + self.GridXStep, self.height()) | |
self.addLine(QLineF(leftEnd, rightEnd), pen) | |
self.addLine(QLineF(rightEnd, rightEnd + | |
QPointF(-self.AxisArrowDX, -self.AxisArrowDY)), pen) | |
self.addLine(QLineF(rightEnd, rightEnd + | |
QPointF(-self.AxisArrowDX, self.AxisArrowDY)), pen) | |
topEnd = QPointF(0, -self.GridYStep) | |
bottomEnd = QPointF(0, self.height() + self.GridYStep) | |
self.addLine(QLineF(bottomEnd, topEnd), pen) | |
self.addLine(QLineF(topEnd, topEnd + | |
QPointF(-self.AxisArrowDY, self.AxisArrowDX)), pen) | |
self.addLine(QLineF(topEnd, topEnd + | |
QPointF(self.AxisArrowDY, self.AxisArrowDX)), pen) | |
def saveImage(self, fileName): | |
width = self.width() + 3 * self.GridXStep | |
height = self.height() + 3 * self.GridYStep | |
img = QImage(width, height, QImage.Format_ARGB32_Premultiplied) | |
painter = QPainter() | |
painter.begin(img) | |
painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing) | |
self.render(painter, source=QRectF(-1.5 * self.GridXStep, -1.5 * self.GridYStep, width, height)) | |
painter.end() | |
img.save(fileName) | |
def saveData(self, fileName): | |
open(fileName, 'w').write(str(self.spline)) | |
class WheelZoomGraphicsView(QGraphicsView): | |
ZoomFactor = 1.2 | |
def __init__(self, *args, **kwargs): | |
super(WheelZoomGraphicsView, self).__init__(*args, **kwargs) | |
self.setRenderHints(QPainter.Antialiasing | | |
QPainter.TextAntialiasing) | |
self.zoom = 1 | |
def wheelEvent(self, event): | |
if event.modifiers() & Qt.ControlModifier: | |
numDegrees = event.delta() / 8.0 | |
numSteps = numDegrees / 15.0 | |
factor = 1.125 ** numSteps | |
self.scale(factor, factor) | |
self.zoom * factor | |
else: | |
super(WheelZoomGraphicsView, self).wheelEvent(event) | |
def zoomIn(): | |
self.scale(self.ZoomFactor, self.ZoomFactor) | |
self.zoom *= self.ZoomFactor | |
def zoomOut(): | |
self.scale(1 / self.ZoomFactor, 1 / self.ZoomFactor) | |
self.zoom /= self.ZoomFactor | |
def zoomNormal(): | |
self.scale(1 / self.zoom, 1 / self.zoom) | |
self.zoom = 1 | |
class SplineEditorWidget(QWidget): | |
DefaultSplineDegree = 4 | |
DefaultControlPointsNumber = 10 | |
DefaultRenderingStepsNumber = 200 | |
def __init__(self, SplineType, *args, **kwargs): | |
super(SplineEditorWidget, self).__init__(*args, **kwargs) | |
self.scene = SplineScene(self.DefaultSplineDegree, self.DefaultControlPointsNumber, | |
SplineType) | |
self.view = WheelZoomGraphicsView(self.scene) | |
self.splineDegreeSpinBox = QSpinBox() | |
self.splineDegreeSpinBox.setValue(self.scene.spline.degree) | |
self.splineDegreeSpinBox.valueChanged.connect( | |
self.changeSplineDegree) | |
self.controlPointsNumberSpinBox = QSpinBox() | |
self.controlPointsNumberSpinBox.setRange(1, 100) | |
self.controlPointsNumberSpinBox.valueChanged.connect( | |
self.changeControlPointsNumber) | |
self.controlPointsNumberSpinBox.setValue(self.scene.spline.controlPointsNumber) | |
self.renderingStepsNumberSpinBox = QSpinBox() | |
self.renderingStepsNumberSpinBox.setRange(20, 1000) | |
self.renderingStepsNumberSpinBox.setSingleStep(20) | |
self.renderingStepsNumberSpinBox.valueChanged.connect( | |
self.changeRenderingStepsNumber) | |
self.renderingStepsNumberSpinBox.setValue(self.scene.spline.renderingStepsNumber) | |
self.saveImageButton = QPushButton('Save image...') | |
self.saveImageButton.clicked.connect(self.saveImage) | |
self.saveDataButton = QPushButton('Save data...') | |
self.saveDataButton.clicked.connect(self.saveData) | |
controlsLayout = QHBoxLayout() | |
controlsLayout.addWidget(QLabel('Spline degree:')) | |
controlsLayout.addWidget(self.splineDegreeSpinBox) | |
controlsLayout.addWidget(QLabel('Control points number:')) | |
controlsLayout.addWidget(self.controlPointsNumberSpinBox) | |
controlsLayout.addWidget(QLabel('Rendering steps number:')) | |
controlsLayout.addWidget(self.renderingStepsNumberSpinBox) | |
controlsLayout.addStretch(1) | |
controlsLayout.addWidget(self.saveImageButton) | |
controlsLayout.addWidget(self.saveDataButton) | |
mainLayout = QVBoxLayout() | |
mainLayout.addWidget(self.view) | |
mainLayout.addLayout(controlsLayout) | |
self.setLayout(mainLayout) | |
def changeControlPointsNumber(self, controlPointsNumber): | |
controlPointsNumber = int(controlPointsNumber) | |
self.scene.spline.setControlPointsNumber(controlPointsNumber) | |
self.splineDegreeSpinBox.setRange(1, self.scene.spline.maxDegree()) | |
if self.scene.spline.degree != self.splineDegreeSpinBox: | |
self.splineDegreeSpinBox.setValue(self.scene.spline.degree) | |
def changeSplineDegree(self, splineDegree): | |
self.scene.spline.setDegree(int(splineDegree)) | |
def changeRenderingStepsNumber(self, renderingStepsNumber): | |
self.scene.spline.setRenderingStepsNumber(int(renderingStepsNumber)) | |
def saveImage(self): | |
fileName = QFileDialog.getSaveFileName(self, 'Save Image', '.', | |
'PNG Files (*.png);;All Files(*.*)')[0] | |
if fileName: | |
if not fileName.lower().endswith('.png'): | |
fileName += '.png' | |
self.scene.saveImage(fileName) | |
def saveData(self): | |
fileName = QFileDialog.getSaveFileName(self, 'Save Image', '.', 'All Files(*.*)')[0] | |
if fileName: | |
self.scene.saveData(fileName) | |
if __name__ == '__main__': | |
import sys | |
app = QApplication(sys.argv) | |
w = SplineEditorWidget(BSpline) | |
w.show() | |
sys.exit(app.exec_()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment