2010-07-08 8 views
8

Tengo una curva Bezier B con los puntos S, C1, C2, E, y un número positivo w que representa el ancho. ¿Hay alguna forma de calcular rápidamente los puntos de control de dos curvas de bezier B1, B2 de manera que lo que está entre B1 y B2 sea la ruta ensanchada representada por B?bezier path widening

Más formalmente: calcular los puntos de control de buenas aproximaciones de Bezier a B1, B2, donde B1 = {(x, y) + N (x, y) (w/2) | (x, y) en C}
B2 = {(x, y) - N (x, y)
(w/2) | (x, y) en C},
donde N (x, y) es el normal de C en (x, y).

Digo buenas aproximaciones porque B1, B2 podrían no ser curvas polinomiales (no estoy seguro si lo son).

+0

Tiene razón en que B1 y B2 en realidad no son curvas polinomiales, y desafortunadamente no se pueden expresar como curvas de bezier. Encontré valioso el siguiente recurso: http://pomax.github.io/bezierinfo/#offsetting –

+1

Esta pregunta parece relacionada: http://stackoverflow.com/questions/4148831/how-to-offset-a-cubic-bezier -curve –

Respuesta

18

El paralelo exacto de una curva bezier es bastante feo desde el punto de vista matemático (requiere polinomios de 10º grado).

Lo que es fácil de hacer es calcular una ampliación a partir de una aproximación poligonal del bezier (es decir, calcular segmentos de línea del bezier y luego mover los puntos a lo largo de las normales en los dos lados de la curva).

Esto da buenos resultados si su grosor no es demasiado grande en comparación con la curvatura ... un "paralelo lejano" en su lugar es un monstruo en sí mismo (y ni siquiera es fácil encontrar una definición de lo que es un paralelo de una curva abierta que haría felices a todos).

Una vez que tiene dos polilíneas para los dos lados, lo que puede hacer es encontrar un bezier que se aproxime mejor para esas rutas si necesita esa representación. Una vez más, creo que para "casos normales" (es decir, líneas finas), incluso un solo arco bezier para cada uno de los dos lados debería ser bastante preciso (el error debería ser mucho menor que el grosor de la línea).

EDITAR: De hecho el uso de un solo arco de Bezier se ve mucho peor de lo que hubiera esperado incluso para casos razonablemente normales. Intenté también usar dos arcos bezier para cada lado y el resultado es mejor, pero aún no perfecto. El error es, por supuesto, mucho más pequeño que el grosor de la línea, por lo que, a menos que las líneas sean muy gruesas, podría ser una opción razonable. En la siguiente imagen se muestra un bezier engrosado (con engrosamiento por punto), una aproximación usando un arco bezier para cada lado y una aproximación usando dos arcos bezier para cada lado.

enter image description here

EDIT 2 Tal y como exigía agrego el código que utiliza para obtener las imágenes; está en python y solo requiere Qt. Este código no debe ser leído por otros, así que usé algunos trucos que probablemente no utilizaría en el código de producción real. El algoritmo también es muy ineficiente, pero no me importó la velocidad (se suponía que era un programa de una sola pasada para ver si la idea funciona).

# 
# This code has been written during an ego-pumping session on 
# www.stackoverflow.com, while trying to reply to an interesting 
# question. Do whatever you want with it but don't blame me if 
# doesn't do what *you* think it should do or even if doesn't do 
# what *I* say it should do. 
# 
# Comments of course are welcome... 
# 
# Andrea "6502" Griffini 
# 
# Requirements: Qt and PyQt 
# 
import sys 
from PyQt4.Qt import * 

QW = QWidget 

bezlevels = 5 

def avg(a, b): 
    """Average of two (x, y) points""" 
    xa, ya = a 
    xb, yb = b 
    return ((xa + xb)*0.5, (ya + yb)*0.5) 

def bez3split(p0, p1, p2,p3): 
    """ 
    Given the control points of a bezier cubic arc computes the 
    control points of first and second half 
    """ 
    p01 = avg(p0, p1) 
    p12 = avg(p1, p2) 
    p23 = avg(p2, p3) 
    p012 = avg(p01, p12) 
    p123 = avg(p12, p23) 
    p= avg(p012, p123) 
    return [(p0, p01, p012, p0123), 
      (p0123, p123, p23, p3)] 

def bez3(p0, p1, p2, p3, levels=bezlevels): 
    """ 
    Builds a bezier cubic arc approximation using a fixed 
    number of half subdivisions. 
    """ 
    if levels <= 0: 
     return [p0, p3] 
    else: 
     (a0, a1, a2, a3), (b0, b1, b2, b3) = bez3split(p0, p1, p2, p3) 
     return (bez3(a0, a1, a2, a3, levels-1) + 
       bez3(b0, b1, b2, b3, levels-1)[1:]) 

def thickPath(pts, d): 
    """ 
    Given a polyline and a distance computes an approximation 
    of the two one-sided offset curves and returns it as two 
    polylines with the same number of vertices as input. 

    NOTE: Quick and dirty approach, just uses a "normal" for every 
      vertex computed as the perpendicular to the segment joining 
      the previous and next vertex. 
      No checks for self-intersections (those happens when the 
      distance is too big for the local curvature), and no check 
      for degenerate input (e.g. multiple points). 
    """ 
    l1 = [] 
    l2 = [] 
    for i in xrange(len(pts)): 
     i0 = max(0, i - 1)    # previous index 
     i1 = min(len(pts) - 1, i + 1) # next index 
     x, y = pts[i] 
     x0, y0 = pts[i0] 
     x1, y1 = pts[i1] 
     dx = x1 - x0 
     dy = y1 - y0 
     L = (dx**2 + dy**2) ** 0.5 
     nx = - d*dy/L 
     ny = d*dx/L 
     l1.append((x - nx, y - ny)) 
     l2.append((x + nx, y + ny)) 
    return l1, l2 

def dist2(x0, y0, x1, y1): 
    "Squared distance between two points" 
    return (x1 - x0)**2 + (y1 - y0)**2 

def dist(x0, y0, x1, y1): 
    "Distance between two points" 
    return ((x1 - x0)**2 + (y1 - y0)**2) ** 0.5 

def ibez(pts, levels=bezlevels): 
    """ 
    Inverse-bezier computation. 
    Given a list of points computes the control points of a 
    cubic bezier arc that approximates them. 
    """ 
    # 
    # NOTE: 
    # 
    # This is a very specific routine that only works 
    # if the input has been obtained from the computation 
    # of a bezier arc with "levels" levels of subdivisions 
    # because computes the distance as the maximum of the 
    # distances of *corresponding points*. 
    # Note that for "big" changes in the input from the 
    # original bezier I dont't think is even true that the 
    # best parameters for a curve-curve match would also 
    # minimize the maximum distance between corresponding 
    # points. For a more general input a more general 
    # path-path error estimation is needed. 
    # 
    # The minimizing algorithm is a step descent on the two 
    # middle control points starting with a step of about 
    # 1/10 of the lenght of the input to about 1/1000. 
    # It's slow and ugly but required no dependencies and 
    # is just a bunch of lines of code, so I used that. 
    # 
    # Note that there is a closed form solution for finding 
    # the best bezier approximation given starting and 
    # ending points and a list of intermediate parameter 
    # values and points, and this formula also could be 
    # used to implement a much faster and accurate 
    # inverse-bezier in the general case. 
    # If you care about the problem of inverse-bezier then 
    # I'm pretty sure there are way smarter methods around. 
    # 
    # The minimization used here is very specific, slow 
    # and not so accurate. It's not production-quality code. 
    # You have been warned. 
    # 

    # Start with a straight line bezier arc (surely not 
    # the best choice but this is just a toy). 
    x0, y0 = pts[0] 
    x3, y3 = pts[-1] 
    x1, y1 = (x0*3 + x3)/4.0, (y0*3 + y3)/4.0 
    x2, y2 = (x0 + x3*3)/4.0, (y0 + y3*3)/4.0 
    L = sum(dist(*(pts[i] + pts[i-1])) for i in xrange(len(pts) - 1)) 
    step = L/10 
    limit = step/100 

    # Function to minimize = max((a[i] - b[i])**2) 
    def err(x0, y0, x1, y1, x2, y2, x3, y3): 
     return max(dist2(*(x+p)) for x, p in zip(pts, bez3((x0, y0), (x1, y1), 
                  (x2, y2), (x3, y3), 
                  levels))) 
    while step > limit: 
     best = None 
     for dx1 in (-step, 0, step): 
      for dy1 in (-step, 0, step): 
       for dx2 in (-step, 0, step): 
        for dy2 in (-step, 0, step): 
         e = err(x0, y0, 
           x1+dx1, y1+dy1, 
           x2+dx2, y2+dy2, 
           x3, y3) 
         if best is None or e < best[0] * 0.9999: 
          best = e, dx1, dy1, dx2, dy2 
     e, dx1, dy1, dx2, dy2 = best 
     if (dx1, dy1, dx2, dy2) == (0, 0, 0, 0): 
      # We got to a minimum for this step => refine 
      step *= 0.5 
     else: 
      # We're still moving 
      x1 += dx1 
      y1 += dy1 
      x2 += dx2 
      y2 += dy2 

    return [(x0, y0), (x1, y1), (x2, y2), (x3, y3)] 

def poly(pts): 
    "Converts a list of (x, y) points to a QPolygonF)" 
    return QPolygonF(map(lambda p: QPointF(*p), pts)) 

class Viewer(QW): 
    def __init__(self, parent): 
     QW.__init__(self, parent) 
     self.pts = [(100, 100), (200, 100), (200, 200), (100, 200)] 
     self.tracking = None # Mouse dragging callback 
     self.ibez = 0   # Thickening algorithm selector 

    def sizeHint(self): 
     return QSize(900, 700) 

    def wheelEvent(self, e): 
     # Moving the wheel changes between 
     # - original polygonal thickening 
     # - single-arc thickening 
     # - double-arc thickening 
     self.ibez = (self.ibez + 1) % 3 
     self.update() 

    def paintEvent(self, e): 
     dc = QPainter(self) 
     dc.setRenderHints(QPainter.Antialiasing) 

     # First build the curve and the polygonal thickening 
     pts = bez3(*self.pts) 
     l1, l2 = thickPath(pts, 15) 

     # Apply inverse bezier computation if requested 
     if self.ibez == 1: 
      # Single arc 
      l1 = bez3(*ibez(l1)) 
      l2 = bez3(*ibez(l2)) 
     elif self.ibez == 2: 
      # Double arc 
      l1 = (bez3(*ibez(l1[:len(l1)/2+1], bezlevels-1)) + 
        bez3(*ibez(l1[len(l1)/2:], bezlevels-1))[1:]) 
      l2 = (bez3(*ibez(l2[:len(l2)/2+1], bezlevels-1)) + 
        bez3(*ibez(l2[len(l2)/2:], bezlevels-1))[1:]) 

     # Draw results 
     dc.setBrush(QBrush(QColor(0, 255, 0))) 
     dc.drawPolygon(poly(l1 + l2[::-1])) 
     dc.drawPolyline(poly(pts)) 
     dc.drawPolyline(poly(self.pts)) 

     # Draw control points 
     dc.setBrush(QBrush(QColor(255, 0, 0))) 
     dc.setPen(QPen(Qt.NoPen)) 
     for x, y in self.pts: 
      dc.drawEllipse(QRectF(x-3, y-3, 6, 6)) 

     # Display the algorithm that has been used 
     dc.setPen(QPen(QColor(0, 0, 0))) 
     dc.drawText(20, 20, 
        ["Polygonal", "Single-arc", "Double-arc"][self.ibez]) 

    def mousePressEvent(self, e): 
     # Find closest control point 
     i = min(range(len(self.pts)), 
       key=lambda i: (e.x() - self.pts[i][0])**2 + 
           (e.y() - self.pts[i][1])**2) 

     # Setup a callback for mouse dragging 
     self.tracking = lambda p: self.pts.__setitem__(i, p) 

    def mouseMoveEvent(self, e): 
     if self.tracking: 
      self.tracking((e.x(), e.y())) 
      self.update() 

    def mouseReleaseEvent(self, e): 
     self.tracking = None 

# Qt boilerplate 
class MyDialog(QDialog): 
    def __init__(self, parent): 
     QDialog.__init__(self, parent) 
     self.ws = Viewer(self) 
     L = QVBoxLayout(self) 
     L.addWidget(self.ws) 
     self.setModal(True) 
     self.show() 

app = QApplication([]) 
aa = MyDialog(None) 
aa.exec_() 
aa = None 
+0

¿Hay alguna posibilidad de que compartas el código para esto? Parece que el zip contiene solo capturas de pantalla. – Quasimondo

+0

@Quasimondo De nada (pero ten cuidado con el código ... fue un hack de una sola vez para comprobar que la idea no era una tontería). – 6502

+0

¡Ah genial! ¡Gracias! – Quasimondo