将2D曲线转换为点以进行数据存储


12

我创建了一种算法,可以将任何曲线(即路径)转换为最小点数,以便将其保存到文件或数据库中。

该方法很简单:它以相等的步长移动三个点,并测量这些点形成的线之间的角度。如果角度大于公差,则会在该点处创建一条新的三次曲线。然后它将线向前移动并再次测量角度…

对于那些知道Android Path类的人-请注意dstPath是一个自定义类,它将点记录到Array中,以便稍后保存点,而srcPath是Regions联合的结果,因此对我没有关键点保存。

问题在于,如下面的代码所示,该圆形看起来并不平滑,该图像是由下面的代码生成的,其中的源路径由一个完美的圆形和矩形组成。我试图更改公差角度和步长,但没有任何帮助。我想知道您是否可以建议对此算法进行任何改进或采用其他方法。

编辑:我现在为使用Android java的用户发布了完整的代码,因此他们可以轻松地尝试和试验。

在此处输入图片说明

public class CurveSavePointsActivity extends Activity{

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(new CurveView(this));
    }

    class CurveView extends View{

        Path srcPath, dstPath;
        Paint srcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        Paint dstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        public CurveView(Context context) {
            super(context);

            srcPaint.setColor(Color.BLACK);
            srcPaint.setStyle(Style.STROKE);
            srcPaint.setStrokeWidth(2);
            srcPaint.setTextSize(20);

            dstPaint.setColor(Color.BLUE);
            dstPaint.setStyle(Style.STROKE);
            dstPaint.setStrokeWidth(2);
            dstPaint.setTextSize(20);

            srcPath = new Path();
            dstPath = new Path();

        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);

            //make a circle path
            srcPath.addCircle(w/4, h/2, w/6 - 30, Direction.CW);

            //make a rectangle path
            Path rectPath = new Path();
            rectPath.addRect(new RectF(w/4, h/2 - w/16, w*0.5f, h/2 + w/16), Direction.CW);


            //create a path union of circle and rectangle paths
            RectF bounds = new RectF();
            srcPath.computeBounds(bounds, true);
            Region destReg = new Region();
            Region clip = new Region();
            clip.set(new Rect(0,0, w, h));
            destReg.setPath(srcPath, clip);
            Region srcReg = new Region();
            srcReg.setPath(rectPath, clip); 
            Region resultReg = new Region();
            resultReg.op(destReg, srcReg, Region.Op.UNION);
            if(!resultReg.isEmpty()){
                srcPath.reset();
                srcPath.addPath(resultReg.getBoundaryPath());
            }

            //extract a new path from the region boundary path
            extractOutlinePath();

            //shift the resulting path bottom left, so they can be compared
            Matrix matrix = new Matrix();
            matrix.postTranslate(10, 30);
            dstPath.transform(matrix);

        }

         @Override 
            public void onDraw(Canvas canvas) { 
                super.onDraw(canvas);    
                canvas.drawColor(Color.WHITE);
                canvas.drawPath(srcPath, srcPaint);
                canvas.drawPath(dstPath, dstPaint);

                canvas.drawText("Source path", 40, 50, srcPaint);
                canvas.drawText("Destination path", 40, 100, dstPaint);
         }


         public void extractOutlinePath() {

             PathMeasure pm = new PathMeasure(srcPath, false); //get access to curve points

             float p0[] = {0f, 0f}; //current position of the new polygon
             float p1[] = {0f, 0f}; //beginning of the first line
             float p2[] = {0f, 0f}; //end of the first & the beginning of the second line
             float p3[] = {0f, 0f}; //end of the second line

             float pxStep = 5; //sampling step for extracting points
             float pxPlace  = 0; //current place on the curve for taking x,y coordinates
             float angleT = 5; //angle of tolerance

             double a1 = 0; //angle of the first line
             double a2 = 0; //angle of the second line

             pm.getPosTan(0, p0, null); //get the beginning x,y of the original curve into p0
             dstPath.moveTo(p0[0], p0[1]); //start new path from the beginning of the curve
             p1 = p0.clone(); //set start of the first line

             pm.getPosTan(pxStep, p2, null); //set end of the first line & the beginning of the second

             pxPlace = pxStep * 2;
             pm.getPosTan(pxPlace, p3, null); //set end of the second line


             while(pxPlace < pm.getLength()){
             a1 = 180 - Math.toDegrees(Math.atan2(p1[1] - p2[1], p1[0] - p2[0])); //angle of the first line
             a2 = 180 - Math.toDegrees(Math.atan2(p2[1] - p3[1], p2[0] - p3[0])); //angle of the second line

             //check the angle between the lines
             if (Math.abs(a1-a2) > angleT){

               //draw a straight line to the first point if the current p0 is not already there
               if(p0[0] != p1[0] && p0[1] != p1[1]) dstPath.quadTo((p0[0] + p1[0])/2, (p0[1] + p1[1])/2, p1[0], p1[1]);

               dstPath.quadTo(p2[0] , p2[1], p3[0], p3[1]); //create a curve to the third point through the second

               //shift the three points by two steps forward
               p0 = p3.clone();
               p1 = p3.clone();
               pxPlace += pxStep;
               pm.getPosTan(pxPlace, p2, null); 
               pxPlace += pxStep;
               pm.getPosTan(pxPlace, p3, null);
               if (pxPlace > pm.getLength()) break;
             }else{
               //shift three points by one step towards the end of the curve
               p1 = p2.clone(); 
               p2 = p3.clone();
               pxPlace += pxStep;
               pm.getPosTan(pxPlace, p3, null); 
             }
             }
             dstPath.close();
         }
    }

}

这是原始算法与我的算法产生的结果之间的比较:

路径之间的比较; 导数上明显更平滑的角


为什么不使用b样条曲线?
GriffinHeart

4
如果您知道东西是一个圆形和一个矩形,为什么不存储一个圆形和一个矩形呢?并采用一般形式-生成的任何输入都可能是存储它的一种合理格式。如果您正在寻找一个看起来像是另一个问题的压缩方案(或者至少我们需要有关源数据的更多信息)会有帮助)。
杰夫·盖茨

就像我在第一句话中所说的那样,它可以是任何无法改变的形状-这里的圆和矩形仅仅是一个测试示例。
Lumis

@Lumis,您真的应该研究b样条曲线,它们的用途是什么。有什么理由尝试实施自己的解决方案?
GriffinHeart

1
路径类将使用样条曲线构造这些曲线,因此您已经在使用它。我还有另一个建议,即不以数学为导向:不要保存点,而是保存用户输入(命令模式)并重播以构建相同的“图像”。
GriffinHeart

Answers:


6

我认为您有两个问题:

非对称控制点

最初,您从p0到p1和p1到p2之间​​的距离相等。如果线段之间的公差角不满足,则将p1和p2向前移动,但保持p0不变。这增加了p0到p1之间的距离,同时保持p1到p2之间​​的距离相同。当使用p1作为控制点创建曲线时,根据自上一条曲线以来经过了多少次迭代,它可能会严重偏向p2。如果将p2移动的距离是p1的两倍,则两点之间的距离将相等。

二次曲线

就像其他答案中提到的那样,二次曲线对于这种情况也不是很好。您创建的相邻曲线应共享一个控制点和一个切线。当您的输入数据仅是点时,Catmull-Rom样条曲线是实现此目的的不错选择。这是三次Hermite曲线,其中控制点的切线是根据上一个和下一个点计算的。

Android中的Path API支持Bézier曲线,在参数方面与Hermite曲线略有不同。幸运的是,可以将Hermite曲线转换为Bézier曲线。是我在谷歌搜索时发现的第一个示例代码。这个Stackoverflow的答案似乎也给出了公式。

您还提到了尖锐边缘的问题。有了输入数据,就不可能检测出是否存在实际的尖角或仅是非常陡峭的曲线。如果这成为问题,则可以通过根据需要即时增加/减少步长来使迭代更具适应性。

编辑: 经过进一步思考,毕竟可以使用二次曲线。与其使用p1作为控制点绘制从p0到p2的二次曲线,不如使用新点p0_1作为控制点从p0绘制到p1的二次曲线。参见下图。 新的控制点

如果p0_1在p0和p1中的切线的交点处,则结果应平滑。更好的是,由于PathMeasure.getPosTan()返回值也将切线作为第三个参数,因此您可以使用实际的准确切线,而不是从相邻点进行近似。使用这种方法,您需要对现有解决方案进行的更改更少。

基于此答案,可以使用以下公式计算交点:

getPosTan(pxPlace0, p0, t0); // Also get the tangent
getPosTan(pxPlace1, p1, t1);
t1 = -t1; // Reverse direction of second tangent
vec2 d = p1 - p0;
float det = t1.x * t0.y - t1.y * t0.x;
float u = (d.y * t1.x - d.x * t1.y) / det;
float v = (d.y * t0.x - d.x * t0.y) / det; // Not needed ... yet
p0_1 = p0 + u * t0;

但是,仅当u和v均为非负数时,此解决方案才有效。参见第二张图片: 光线不相交

尽管光线会相交,但此处光线不会相交,因为u为负。在这种情况下,不可能绘制能够平滑地连接到前一条曲线的二次曲线。在这里,您需要贝塞尔曲线。您可以使用此答案前面给出的方法为其计算控制点,也可以直接从切线中得出它们。将p0投影到切线射线p0 + u * t0上,反之亦然,将另一条射线投影到切线射线p0和c1上。您也可以通过使用p0和c0之间的任何点而不是c0来调整曲线,只要它位于切线上即可。

Edit2: 如果绘图位置在p1中,则可以使用以下伪代码将bezier控制点计算为p2:

vec2 p0, p1, p2, p3; // These are calculated with PathMeasure
vec2 cp1 = p1 + (p2 - p0) / 6;
vec2 cp2 = p2 - (p3 - p1) / 6;

有了这些,您可以追加一个从p1到p2的路径:

path.cubicTo(cp1.x, cp1.y, cp2.x, cp2.y, p2.x, p2.y);

将向量操作替换为float [ 2 ]数组上的每个组件操作,以匹配您的代码。从初始化开始,p1 = start;接下来是p2和p3。p0最初是不确定的。对于尚没有p0的第一个细分,可以使用以cp2为控制点的从p1到p2的二次曲线。对于没有p3的路径的末尾,同样可以使用cp1作为控制点绘制从p1到p2的二次曲线。或者,您可以为第一个段初始化p0 = p1,为最后一个段初始化p3 = p2。在每个段之后,您p0 = p1; p1 = p2; and p2 = p3;在向前移动时会移动值。

保存路径时,只需保存所有点p0 ... pN。无需保存控制点cp1和cp2,因为可以根据需要进行计算。

Edit3: 由于似乎很难为曲线生成获得良好的输入值,因此我提出了另一种方法:使用序列化。Android Path似乎不支持它,但是幸运的是Region类支持。有关代码,请参见此答案。这应该给您确切的结果。如果未优化,则序列化形式可能会占用一些空间,但在这种情况下,压缩效果应该很好。使用GZIPOutputStream在Android Java中进行压缩很容易。


听起来很有希望。但是,使用的不是p0而是p1,p2,p3,p0仅用于在计算新的确定点时存储它们,并且为了直线起见,因此不会在每个步骤中对它们进行采样。您能帮我如何为新的控制点计算x,y吗?
Lumis

我可以稍后再做,但是与此同时请检查stackoverflow.com/questions/2931573/…。使用u和v可以得到交点。
msell 2013年

谢谢您的帮助,我想尝试一下,但是它需要用Java for Android编写。没有vector2,并且t1和p1等都是浮点数组,因此我无法对它们进行任何直接操作,例如t1 = -t1或u * t0。我假设t1 = -t1意味着t1.x = -t1x; t1.y = -t1.y等,对吧?
Lumis

是的,那只是使它更紧凑和更易读的伪代码。
msell,2013年

好吧,情节正在扩大。由于Android中两条路径的区域交点返回的路径不是抗锯齿的,因此切线在该位置上。因此,正确的解决方案是先驱动一条平滑的曲线通过给定的点,然后再对其进行采样。您的代码在抗锯齿的路径上工作得很好,它会产生适当的控制点。
Lumis

13

W3C会做什么?

互联网有这个问题。在万维网联盟的注意。自1999年以来,它具有推荐的标准解决方案可缩放矢量图形(SVG)。这是一种基于XML的文件格式,专门用于存储2D形状。

可扩展的-什么?

可扩展的矢量图形

  • 可扩展:可以平滑扩展到任何大小。
  • 向量:它基于向量的数学概念。
  • 图形。它是用来制作图片的。

是SVG 1.1版的技术规范。
(不要怕这个名字;读起来实际上很愉快。)

他们准确地写下了如何存储基本形状(圆形矩形)。例如,矩形具有以下属性:xywidthheightrxry。(rxry可用于圆角。)

这是他们在SVG中使用的矩形示例:(嗯,实际上是两个-一个用于画布轮廓。)

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 
  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="12cm" height="4cm" viewBox="0 0 1200 400"
     xmlns="http://www.w3.org/2000/svg" version="1.1">
  <desc>Example rect01 - rectangle with sharp corners</desc>

  <!-- Show outline of canvas using 'rect' element -->
  <rect x="1" y="1" width="1198" height="398"
        fill="none" stroke="blue" stroke-width="2"/>

  <rect x="400" y="100" width="400" height="200"
        fill="yellow" stroke="navy" stroke-width="10"  />
</svg>

它代表的是:

具有蓝色轮廓的黄色矩形

如规范所述,如果不需要它们,您可以自由地省略一些属性。(例如,rxry属性未在这里使用。)是的,有大约在顶部一吨克鲁夫特的DOCTYPE,你不会只需要为你的游戏。它们也是可选的。

路径

SVG 路径在某种意义上是“路径”,即如果您将铅笔放在纸上,四处移动并最终再次抬起它,那么您就有了一条路径。他们不关闭,但他们可能是。

每个路径都有一个d属性(我喜欢认为它代表“绘制”),其中包含路径数据,一系列命令序列,这些命令基本上只不过是将笔放在纸上并在周围移动

他们以三角形为例:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 
  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="4cm" height="4cm" viewBox="0 0 400 400"
     xmlns="http://www.w3.org/2000/svg" version="1.1">
  <title>Example triangle01- simple example of a 'path'</title>
  <desc>A path that draws a triangle</desc>
  <rect x="1" y="1" width="398" height="398"
        fill="none" stroke="blue" />
  <path d="M 100 100 L 300 100 L 200 300 z"
        fill="red" stroke="blue" stroke-width="3" />
</svg>

一个红色的三角形

请参阅中的d属性path

d="M 100 100 L 300 100 L 200 300 z"

M是一个命令用于移动到(接着坐标),则Ls为用于线到(具有坐标)和z为命令关闭路径(即画一条线回到第一位置;不需要坐标)。

直线很无聊吗?使用三次二次 Bézier命令!

一些立方贝塞尔

贝塞尔曲线背后的理论在其他地方也有很多报道(例如Wikipedia上),但是这里是执行摘要:贝塞尔曲线有一个起点和终点,可能有许多控制点会影响曲线之间的走向。

追踪二次贝塞尔曲线

该规范还提供了将大多数基本形状转换为路径(如果需要)的说明。

为什么以及何时使用SVG

仔细决定是否要沿这条路走(双关语意),因为在文本中表示任意2D形状真的很复杂!如果您将自己限制为仅由(可能很多)直线构成的路径,则可以使您的生活变得更加轻松。

但是,如果您确定要使用任意形状,那么SVG是必经之路:它具有强大的工具支持:您可以在底层找到许多用于XML解析的库,而在高层可以找到SVG编辑器工具

无论如何,SVG标准树立了一个很好的榜样。


问题是关于将曲线转换为点而不是将其保存。但是,感谢您的参考,很高兴了解SVG标准。
Lumis

@Lumis标题和内容可能会建议其他方式。考虑改写这个问题。(或者,既然这个已经很确定了,再问另一个。)
Anko

4

您的代码包含一个误导性注释:

dstPath.quadTo(p2[0] , p2[1], p3[0], p3[1]); //create a curve to the third point through the second

二次贝塞尔曲线并没有通过第二点。如果要通过第二点,则需要另一种类型的曲线,例如Hermite curve。您可以将Hermite曲线转换为贝塞尔曲线,以便可以使用Path类。

另一个建议是,不要采样点,而应使用跳过的点的平均值。

另一个建议是,不要使用角度作为阈值,而应使用实际曲线和近似曲线之间的差。角度不是真正的问题。真正的问题是当点集不适合贝塞尔曲线时。

另一个建议是使用三次方贝塞尔曲线,其中一个的切线与下一个的切线匹配。否则(使用二次方),我认为您的曲线将无法平滑匹配。


没错,第二个点仅向其“拉”曲线。cubeTo需要两个控制点,而不是quadTo。问题当然是如何获得正确的控制点。请注意,我不想丢失锐角,因为源路径可以是直线或圆形的任意形状的组合-基本上,我正在制作一个图像选择工具,可以在其中保存选定的路径。
Lumis

4

要使两条路径的交点更平滑,可以在交点之前放大它们,在交点之后缩小它们。

我不知道这是否是一个好的解决方案,但对我来说效果很好。它也很快。在我的示例中,我将圆角路径与我创建的图案(条纹)相交。即使缩放也看起来不错。

这是我的代码:

    Path mypath=new Path(<desiredpath to fill with a pattern>);
    String sPatternType=cpath.getsPattern();

    Path pathtempforbounds=new Path(cpath.getPath());
    RectF rectF = new RectF();
     if (sPatternType.equals("1")){
         turnPath(pathtempforbounds, -45);
     }
     pathtempforbounds.computeBounds(rectF, true);

     float ftop=rectF.top;
     float fbottom=rectF.bottom;
     float fleft=rectF.left;
     float fright=rectF.right;
     float xlength=fright-fleft;

     Path pathpattern=new Path();

     float ypos=ftop;
     float xpos=fleft;

     float fStreifenbreite=4f;

     while(ypos<fbottom){
         pathpattern.moveTo(xpos,ypos);
         xpos=xpos+xlength;
         pathpattern.lineTo(xpos, ypos);
         ypos=ypos+fStreifenbreite;
         pathpattern.lineTo(xpos, ypos);
         xpos=xpos-xlength;
         pathpattern.lineTo(xpos, ypos);
         ypos=ypos-fStreifenbreite;
         pathpattern.lineTo(xpos, ypos);
         pathpattern.close();
         ypos=ypos+2*fStreifenbreite;

     }

     // Original vergrössern

     scalepath(pathpattern,10);
     scalepath(mypath,10);

     if (sPatternType.equals("1")){
         Matrix mdrehen=new Matrix();
         RectF bounds=new RectF();
         pathpattern.computeBounds(bounds, true);
         mdrehen.postRotate(45, (bounds.right + bounds.left)/2,(bounds.bottom + bounds.top)/2);
         pathpattern.transform(mdrehen);
     }

     RectF rectF2 = new RectF();
     mypath.computeBounds(rectF2, true);

     Region clip = new Region();
     clip.set((int)(rectF2.left-100f),(int)(rectF2.top -100f), (int)(rectF2.right+100f),(int)( rectF2.bottom+100f));
     Region region1 = new Region();
     region1.setPath(pathpattern, clip);

     Region region2 = new Region();
     region2.setPath(mypath, clip);

     region1.op(region2, Region.Op.INTERSECT);


     Path pnew=region1.getBoundaryPath();


     scalepath(pnew, 0.1f);
     cpath.setPathpattern(pnew);




public void turnPath(Path p,int idegree){
     Matrix mdrehen=new Matrix();
     RectF bounds=new RectF();
     p.computeBounds(bounds, true);
     mdrehen.postRotate(idegree, (bounds.right + bounds.left)/2,(bounds.bottom + bounds.top)/2);
     p.transform(mdrehen);
}

public void scalepath(Path p,float fscale){
     Matrix mverkleinern=new Matrix();
     mverkleinern.preScale(fscale,fscale);
     p.transform(mverkleinern);
}

在此处输入图片说明

使用canvas.scale()缩放时,看起来仍然很平滑: 在此处输入图片说明


感谢谁花了我10点声誉来添加图像:-)
user1344545 2013年

1
令人惊讶的是,这个简单的技巧解决了两个问题:首先,它使生成的相交或联合的路径变得平滑,其次,当我对相同的按比例放大的路径进行采样时,问题中的代码产生了完美的平滑结果。多么意外和简单的解决方案,谢谢!
Lumis

@user编辑是免费的。对于<2k-rep用户,实际上是+2。
Anko 2013年

@Lumis我有些困惑-我以为您问过如何存储路径?
Anko 2013年

1
不幸的是,在进行了更多测试之后,我发现由于Region使用的像素在绘制时会占据路径,因此如果Path的缩放比例很大且反复进行,则应用很容易用完内存。因此,此解决方案是有限且冒险的,但请牢记。
Lumis

3

查看多边形插值(http://en.wikipedia.org/wiki/Polynomial_interpolation

基本上,您需要n个等距节点(最优插值不是等距的,但对于您的情况,它应该足够好并且易于实现)

你有n阶的多边形从而降低你的曲线之间的误差结束,如果(< -如果大)的线顺利不够。

在您的情况下,您正在执行线性(1阶)插值。

另一种情况(如GriffinHeart建议的)是使用样条线(http://en.wikipedia.org/wiki/Spline_interpolation

无论哪种情况,都会为您的曲线提供某种形式的多项式拟合


2

如果转换点仅用于存储,并且将其重新显示在屏幕上时需要平滑,则可以得到的最高保真度存储,同时仍使保持给定曲线所需的总存储量最小化。实际存储圆(或弧)的属性,并根据需要重新绘制。

起源。半径。绘制圆弧的起始/终止角度。

如果您仍然需要将圆/弧转换为点以进行渲染,则可以从存储中加载圆/弧,同时始终仅存储属性。


源路径/曲线可以是任何形状,包括自由线的图形。我一直在考虑该解决方案,该解决方案必须分别保存每个组件,然后在加载时将它们组合在一起,但它需要大量的工作,并且会减慢对如此复杂的对象的处理,因为每次转换都必须应用于每个其组件,以便能够再次保存它。
Lumis

2

是否有理由选择曲线而不是直线?直线更易于使用,并且可以在硬件中有效地渲染。

值得考虑的另一种方法是每个像素存储几个位,说明它是在形状的内部,外部还是在轮廓上。这应该很好地压缩,并且对于复杂选择而言可能比行更有效。

您可能还会发现这些文章有趣/有用:


1

看一下曲线插值-您可以实现几种不同的类型,这将有助于平滑曲线。您可以在该圈子中获得的积分越多越好。存储非常便宜-因此,如果提取360个关闭的节点足够便宜(即使位置为8字节,存储360个节点也几乎不会很昂贵)。

您可以在此处仅用四个点放置一些插值样本。并且结果是相当不错的(我最喜欢的是这种情况下的贝塞尔曲线,尽管其他人可能会对其他有效的解决方案产生兴趣)。

您也可以在这里玩。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.