Я пытаюсь проследить квадратичные кривые Безье, расставляя «маркеры» на заданной длине шага distance. Пытался сделать это наивным способом:
const p = toPoint(map, points[section + 1]);
const p2 = toPoint(map, points[section]);
const {x: cx, y: cy} = toPoint(map, cp);
const ll1 = toLatLng(map, p),
ll2 = toLatLng(map, p2),
llc = toLatLng(map, { x: cx, y: cy });
const lineLength = quadraticBezierLength(
ll1.lat,
ll1.lng,
llc.lat,
llc.lng,
ll2.lat,
ll2.lng
);
for (let index = 0; index < Math.floor(lineLength / distance); index++) {
const t = distance / lineLength;
const markerPoint = getQuadraticPoint(
t * index,
p.x,
p.y,
cx,
cy,
p2.x,
p2.y
);
const markerLatLng = toLatLng(map, markerPoint);
markers.push(markerLatLng);
}
Этот подход не работает, поскольку корреляция квадратичной кривой между t и L не является линейной. Я не мог найти формулу, которая давала бы мне хорошее приближение, поэтому рассматриваю решение этой задачи с помощью численных методов [Ньютон]. Я рассматриваю один простой вариант — разбить кривую на x [например, в 10] раз больше частей, чем нужно. После этого с помощью той же функции quadraticBezierLength() рассчитайте расстояние до каждой из этих точек. После этого выберите точку так, чтобы длина была ближе всего к distance * index.
Однако это было бы огромным перебором с точки зрения сложности алгоритма. Я, вероятно, мог бы начать сравнивать точки для index + 1 с подмножества после/без точки, которую я уже выбрал, тем самым пропустив начало набора. Это немного снизит сложность, но все равно будет очень неэффективно.
Есть идеи и/или предложения?
В идеале мне нужна функция, которая принимала бы d — расстояние вдоль кривой, p0, cp, p1 — три точки, определяющие квадратичную кривую Безье, и возвращала бы массив координат, реализованный с наименьшей возможной сложностью.





ОК, я нашел аналитическую формулу для двумерной квадратичной кривой Безье здесь:
Итак, идея заключается в простом бинарном поиске параметра t до тех пор, пока аналитически полученная длина дуги не совпадет с желаемой длиной...
Код С++:
//---------------------------------------------------------------------------
float x0,x1,x2,y0,y1,y2; // control points
float ax[3],ay[3]; // coefficients
//---------------------------------------------------------------------------
void get_xy(float &x,float &y,float t) // get point on curve from parameter t=<0,1>
{
float tt=t*t;
x=ax[0]+(ax[1]*t)+(ax[2]*tt);
y=ay[0]+(ay[1]*t)+(ay[2]*tt);
}
//---------------------------------------------------------------------------
float get_l_naive(float t) // get arclength from parameter t=<0,1>
{
// naive iteration
float x0,x1,y0,y1,dx,dy,l=0.0,dt=0.001;
get_xy(x1,y1,t);
for (int e=1;e;)
{
t-=dt; if (t<0.0){ e=0; t=0.0; }
x0=x1; y0=y1; get_xy(x1,y1,t);
dx=x1-x0; dy=y1-y0;
l+=sqrt((dx*dx)+(dy*dy));
}
return l;
}
//---------------------------------------------------------------------------
float get_l(float t) // get arclength from parameter t=<0,1>
{
// analytic fomula from: https://stackoverflow.com/a/11857788/2521214
float ax,ay,bx,by,A,B,C,b,c,u,k,cu,cb;
ax=x0-x1-x1+x2;
ay=y0-y1-y1+y2;
bx=x1+x1-x0-x0;
by=y1+y1-y0-y0;
A=4.0*((ax*ax)+(ay*ay));
B=4.0*((ax*bx)+(ay*by));
C= (bx*bx)+(by*by);
b=B/(2.0*A);
c=C/A;
u=t+b;
k=c-(b*b);
cu=sqrt((u*u)+k);
cb=sqrt((b*b)+k);
return 0.5*sqrt(A)*((u*cu)-(b*cb)+(k*log(fabs((u+cu))/(b+cb))));
}
//---------------------------------------------------------------------------
float get_t(float l0) // get parameter t=<0,1> from arclength
{
float t0,t,dt,l;
for (t=0.0,dt=0.5;dt>1e-10;dt*=0.5)
{
t0=t; t+=dt;
l=get_l(t);
if (l>l0) t=t0;
}
return t;
}
//---------------------------------------------------------------------------
void set_coef() // compute coefficients from control points
{
ax[0]= ( x0);
ax[1]= +(2.0*x1)-(2.0*x0);
ax[2]=( x2)-(2.0*x1)+( x0);
ay[0]= ( y0);
ay[1]= +(2.0*y1)-(2.0*y0);
ay[2]=( y2)-(2.0*y1)+( y0);
}
//---------------------------------------------------------------------------
Применение:
x0,y0,...t=get_t(wanted_arclength)Если вы хотите использовать get_t_naive и или get_xy, вы должны сначала вызвать set_coef
Если вы хотите настроить скорость / точность, вы можете играть с целевой точностью binsearch, установленной в настоящее время на1e-10
Здесь оптимизированная (объединенная get_l,get_t функция) версия:
//---------------------------------------------------------------------------
float get_t(float l0) // get parameter t=<0,1> from arclength
{
float t0,t,dt,l;
float ax,ay,bx,by,A,B,C,b,c,u,k,cu,cb,cA;
// precompute get_l(t) constants
ax=x0-x1-x1+x2;
ay=y0-y1-y1+y2;
bx=x1+x1-x0-x0;
by=y1+y1-y0-y0;
A=4.0*((ax*ax)+(ay*ay));
B=4.0*((ax*bx)+(ay*by));
C= (bx*bx)+(by*by);
b=B/(2.0*A);
c=C/A;
k=c-(b*b);
cb=sqrt((b*b)+k);
cA=0.5*sqrt(A);
// bin search t so get_l == l0
for (t=0.0,dt=0.5;dt>1e-10;dt*=0.5)
{
t0=t; t+=dt;
// l=get_l(t);
u=t+b; cu=sqrt((u*u)+k);
l=cA*((u*cu)-(b*cb)+(k*log(fabs((u+cu))/(b+cb))));
if (l>l0) t=t0;
}
return t;
}
//---------------------------------------------------------------------------
оценить попытку. тем не менее, хотя вы пытались ответить на другой вопрос, я считаю, что решение, которое вы предложили для проблемы, которую вы пытались решить, далеко от оптимального. В любом случае, это не имеет отношения к моей задаче, к сожалению.
@IgorShmukler Я нашел правильную формулу с помощью t ... Я полностью переработал свой ответ, снова удалил все устаревшие вещи и оставил только решение, основанное на бинарном поиске ... оно работает точно на кривых, которые я пробовал ...
На данный момент я придумал следующее:
for (let index = 0; index < Math.floor(numFloat * times); index++) {
const t = distance / lineLength / times;
const l1 = toLatLng(map, p), lcp = toLatLng(map, new L.Point(cx, cy));
const lutPoint = getQuadraticPoint(
t * index,
p.x,
p.y,
cx,
cy,
p2.x,
p2.y
);
const lutLatLng = toLatLng(map, lutPoint);
const length = quadraticBezierLength(l1.lat, l1.lng, lcp.lat, lcp.lng, lutLatLng.lat, lutLatLng.lng);
lut.push({t: t * index, length});
}
const lut1 = lut.filter(({length}) => !isNaN(length));
console.info('lookup table:', lut1);
for (let index = 0; index < Math.floor(numFloat); index++) {
const t = distance / lineLength;
// find t closest to distance * index
const markerT = lut1.reduce((a, b) => {
return a.t && Math.abs(b.length - distance * index) < Math.abs(a.length - distance * index) ? b.t : a.t || 0;
});
const markerPoint = getQuadraticPoint(
markerT,
p.x,
p.y,
cx,
cy,
p2.x,
p2.y
);
const markerLatLng = toLatLng(map, markerPoint);
}
Я думаю только о том, что моя длина кривой Безье работает не так, как я ожидал.
function quadraticBezierLength(x1, y1, x2, y2, x3, y3) {
let a, b, c, d, e, u, a1, e1, c1, d1, u1, v1x, v1y;
v1x = x2 * 2;
v1y = y2 * 2;
d = x1 - v1x + x3;
d1 = y1 - v1y + y3;
e = v1x - 2 * x1;
e1 = v1y - 2 * y1;
c1 = a = 4 * (d * d + d1 * d1);
c1 += b = 4 * (d * e + d1 * e1);
c1 += c = e * e + e1 * e1;
c1 = 2 * Math.sqrt(c1);
a1 = 2 * a * (u = Math.sqrt(a));
u1 = b / u;
a = 4 * c * a - b * b;
c = 2 * Math.sqrt(c);
return (
(a1 * c1 + u * b * (c1 - c) + a * Math.log((2 * u + u1 + c1) / (u1 + c))) /
(4 * a1)
);
}
Я считаю, что полная длина кривой верна, но частичная длина, вычисляемая для таблицы поиска, неверна.
Расчет длины всей кривой работает нормально. Проблема в том, что я использую его для расчета не той вещи. Я нахожу точку на кривой, а затем предполагаю, что новая кривая, состоящая из начала, контроля и этой вновь найденной точки, будет составлять более короткую часть той же кривой. Это неправильно, я понимаю.
Вы действительно тестировали свой код? Я создал аналогичную функцию, и я не получаю правильных результатов.
Я получаю значения, которые даже не близки. Длина всей кривой для моего случая составляет около 218, а результат для t = 1 — более 50 000.
В JavaScript все является числом с плавающей запятой, поэтому нет необходимости преобразовывать целые числа в числа с плавающей запятой. Это проблема для большинства вычислений, но здесь работает нормально. Кстати, зачем вам fabs в вашем коде?
это то, что я тестирую i.stack.imgur.com/ObbQi.png точки масштабируются с размером окна, поэтому я могу быстро протестировать, изменяя размер окна. Как вы можете видеть длины для совпадений t=0.75 ... также get_l(get_t(200))=200.000005, что довольно хорошо, учитывая, что я использую только поплавки
fabs здесь, потому что это в формуле, которую я связал ... вы знаете, что || рядом с k*log|...| означает абсолютное значение в математическом синтаксисе
Я сделал два расчета, полная длина дуги с: x1: 286, y1: 389, x2: 324.98205596852745, y2: 246.72650759509187, x3: 393.5, y3: 265.5 и результат - длина линии: 181.71253724569695; затем quadraticBezierArcLength(1, 286, 389, 324,98205596852745, 246,72650759509187, 393,5, 265,5) и результат 41919.46098139183. Интересно, как это возможно, что вы получаете правильные результаты.
Я жестко закодировал ваши точки, использовал t=1.0 и l=100 ... i.stack.imgur.com/eHO0k.png ... хех, печать get_l(200) должна быть 100 ее жестко запрограммированной строки :) ... попробуйте скопировать и вставить мой get_t и функции get_l ... просто добавьте x0,y0,x1,y1,x2,y2 к параметрам вызова ... также у вашего quadraticBezierArcLength есть 6 параметров, как вы можете вызывать его с 7? это синтаксическая ошибка ... или у вас другой код, чем здесь
@Spektre Журнал был в порядке, но где-то была ошибка. Попробовал ваш вариант, очень похож на мой. Тем не менее, ваша версия работает правильно, а моя нет. Большое спасибо. Постараюсь выяснить, что не так.
Кстати, вы можете оптимизировать дальнейшую оптимизацию, объединив get_l с get_t, поскольку get_l имеют много констант (не зависящих от t), которые можно вычислить только один раз ... добавят код в мой ответ через минуту
да, хороший момент.
хорошо, я сделал небольшое изменение ... там все еще есть несколько постоянных терминов, но это был бы беспорядок с константами ... я думаю, что с этого момента я оставлю код как есть ...
Если я прав, вам нужны точки в равноотстоящих точках с точки зрения криволинейной абсциссы (а не с точки зрения постоянного евклидова расстояния, что было бы совсем другой проблемой).
Вычисление криволинейной абсциссы s как функции параметра кривой t действительно вариант, но это приводит к решению уравнения s(t) = Sk/n для целого числа k, где S — общая длина (или s(t) = kd, если наложен шаг). Это неудобно, потому что s(t) недоступна как простая функция и является трансцендентной.
Лучшим методом является решение дифференциального уравнения
dt/ds = 1/(ds/dt) = 1/√(dx/dt)²+(dy/dt)²
используя предпочитаемый вами решатель ODE (RK4). Это позволяет вам наложить фиксированный шаг на s и эффективно с точки зрения вычислений.
Не до конца понял ваш пост. Следовательно, я собираюсь предположить, что вы, возможно, имели в виду. s(t) легко вычислить. Я искал решение в закрытой форме для C(s) [в ваших терминах]. Хотя я считаю, что эта проблема теоретически решаема, считаю, что решения, кроме как с использованием численных методов, пока нет.
t(s) зависит от обратной эллиптической функции, которую вы все равно должны вычислить численно. Этого легко достичь с помощью ОДУ.
Я не знаю, что такое ODE. Не думайте, что вы ДОЛЖНЫ решить это численно. На самом деле я использовал числовое решение. Тем не менее, я считаю, что решение в закрытой форме возможно. Мы получили бы уравнения 4-й степени, и они разрешимы. Я лично не понял решения. Я верю, что однажды кто-нибудь придет с умной заменой и опубликует решение. Время от времени я буду просматривать темы SIGGRAPH.
@IgorShmukler: извините, ошибся. Для квадратичного Безье длина дуги имеет аналитическое выражение, включающее гиперболическую функцию. Это выражение не может быть инвертировано аналитически и никогда не будет. Точно так же, как многочлены пятой степени никогда не будут решаться радикалами.
Меня не волнует ни куб, ни расстояние по прямым линиям. Меня интересуют квадратичные кривые.