Інтерполяція: малюємо плавні графіки за допомогою кривих Безьє

Інтерполяція: малюємо плавні графіки за допомогою кривих Безьє

Доброго часу доби, харбачитник.

У цій статті мені хотілося б розповісти про один придуманий колись алгоритм (або швидше за все - переобретенный велосипед) побудови плавного графіка за заданими точками, використовуючи криві Безьє. Стаття була написана під впливом ось цієї статті і дуже корисного коментаря товариша lany, за що їм окреме спасибі.

Показ завдання

Є масив Y-ків точок, розташованих рівномірно по осі X. Потрібно отримати плавний графік, який проходить через всі задані точки. Приклад на малюнку нижче:

Всіх, кому цікаво, прошу під кат.

Існує ряд стандартних рішень для проведення плавної кривої через точки (з цього приводу багато цікавого написано в вже згаданій статті) таких, як наприклад, інтерполяції сплайнами. Коли на третьому курсі був придуманий цей алгоритм, слово «інтерполяція» вселяло в мене жах, а гуглення за запитом «згладжування графіків» не давало посильних розумінню результатів. Але якось я дійшов до кривих Безьє і вже дуже вони мені сподобалися. Малює швидко, алгоритм інтуїтивно зрозумілий... Що ще треба для щастя. Ну і якось понеслося.

Основна ідея

Розіб'ю ідею на три підпункти, щоб було зрозуміліше і читабельніше.

  1. Про кривих Безьє добре написано на віки і на javascript.ru. Якщо уважно читати, можна звернути увагу, що крива Безьє виходить з першої точки відносно прямої початкова _ точка-перша _ опорна _ точка. Аналогічно і в кінці - крива заходить відносно прямої остання _ опорна точка-кінцева _ точка. Таким чином виходить, що якщо у нас одна крива закінчується в точці А і зайшла відносно прямої, а інша крива виходить з цієї точки А щодо тієї-ж прямої а, то цей перехід між двома кривими Безьє у нас вийде плавним.
  2. Виходячи з першого пункту виходить, що у нас опорні точки ліворуч і праворуч відносно точки А повинні лежати на одній прямій. Поміркувавши трохи, було вирішено, що ця пряма повинна бути такою, щоб ∠BAB1=∠CAC1 (малюнок нижче), де точки B1 і C1 - опорні.
  3. Відстань від точки А до точок B1 і C1 було вирішено взяти рівною половині кроку по X між точками B і A, A і C, і т. д. Мені складно якось обґрунтувати такий вибір, але важливо, щоб ця відстань була менше, ніж крок по X між точками А і B, інакше може вийде щось таке, як на малюнку нижче. Важливо розуміти, що чим більше буде ця відстань, тим більш звивистою буде крива і навпаки. Відстань у половину кроку по X мені здається оптимальною, але тут вже можливі варіанти.

Таким чином виходить, що завдання зводиться до пошуку прямої (B1C1) і, власне, опорних точок B1 і C1, за якими ми потім будемо будувати криві Безьє.

Пошук прямої

Як відомо, пряма на площині виражається формулою y = kx + b, де k - тангенс кута нахилу прямої до осі Х, а b - «висота» перетину прямої і осі Y.

Пошук коефіцієнта k

Скажу наперед, що k = tg ( ) = tg ((в-у-у )/2) = (Sqrt (((YA-YB) 2 + В X2) * ((YA-YC) 2 + В X2)  (YA-YB) * (YA-YC)) де (В X Точ( Y* (Y-yb розташовані, X) Y) Y) YA- Y- YC- Y- Y- YC- Y- Y- Y- Y- Y- Y- Y- Y- Нижче представлено математичний доказ правильності формули, але якщо ви не в настрої, то можете його просто пропустити.

Математичне виведення коефіцієнта k

  1. Нехай кут ∠O1BA=α, а кут ∠O2CA=β.

Тоді ∠BAO1=90o - ∠CAO2=90o, так як △ABO1 і △ACO2 - прямокутні.

  1. (1) ∠B11=∠B1AB+∠BAO1+∠O1AС+∠CAС1=180o

(2) ∠B1AB=∠C1AC - за умовою

(3) ∠BAO1=90o

(4) ∠O1AС=∠CAO2=90o

З (1), (2), (3) і (4) виходить, що:

2*∠C1AС+(90o-α)+(90o-β)=180o

2*∠C1AС+180o-α-β=180o

(5) ∠C1AС=(α+β)/2

  1. (6) ∠C1AС=∠C1AD+∠DAC=φ+∠DAC

(7) ∠DAC=∠O2CA=β - як внутрішні різносторонні кути утворені двома паралельними прямими (AD) і (O2C) і секущею (AC)

З (5), (6) і (7) виходить, що:

∠C1AС=φ+∠DAC

(α+β)/2=φ+β

φ+β=(α+β)/2

(8) φ=(α-β)/2

  1. k=tg(φ)=tg((α-β)/2)

З △ABO1:

sin(α)=[AO1]/[AB]

cos(α)=[BO1]/[AB]

З △ACO2:

sin(β)=[AO2]/[AC]

cos(β)=[CO2]/[AC]

Під квадратними дужками мається на увазі довгий відрізок (не хотів використовувати вертикальні прямі - сподіваюся, що читач мене пробачить)

  1. З попереднього підпункту випливає:
  2. Знаючи, що:

[BO1]=[CO2]=ΔX

[AO1]=YA-YB

[AO2]=YA-YC

[AB]=Sqrt([AO1]2+[BO1]2)=Sqrt((YA-YB)2+ΔX2)

[AC]=Sqrt([AO2]2+[CO2]2)=Sqrt((YA-YC)2+ΔX2)

Отримуємо, що:

І так, k ми знайшли. Забігаючи наперед скажу, що b нам при розрахунках не стане в нагоді. Приступимо до пошуку опорних точок.

Шукаємо опорні точки

Відразу скажу, що: ^ X'= ^ X/2 * Sqrt (1/( 1 + k2)), координати опорної точки праворуч: XC1=XA+ΔX'; YC1 = YA + k * ^ X', і зліва:XB1=XA-ΔX'; YB1=YA-k*ΔX'.

Трохи математики, яка це доводить

З тригонометрії ми пам'ятаємо, що:

З △AC1O:

ΔX'=[AO]=[AC1]*cos(φ).

[C1O]=[AO]*tg(φ)=k*ΔX'

Якщо ми приймаємо [AC1] рівним половині кроку по X між основними точками графіка (точками B і A, A і C, т. д.), то:

І ось, власне:

XC1=XA+ΔX'

XB1=XA-ΔX'

YC1=YA+k*ΔX'

YB1=YA-k*ΔX'

До приємного!

  1. Від товариша lany (величезне йому за це спасибі і плюс йому до карми) я дізнався, що html5 за допомогою функцій quadraticCurveTo () і bezierCurveTo () вміє малювати по canvas-у криві Безьє сам. Відповідно, алгоритм можна застосувати з JavaScript-ом.
  2. Приємною особливістю алгоритму є те, що графік передбачувано «випирає» за межі простору точок, через які ми власне проводимо графік. Якщо брати відстань до опорних точок рівними половині кроку по X (відрізок [AC1] на останньому малюнку), то зазору в чверть кроку по X зверху і знизу від меж canvas-а буде достатньо.

Приклад реалізації на JSFiddle

UPDATE:

  1. Спроби зробити так, щоб опорні точки потрібно було рахувати один раз, а потім, при масштабуванні графіка, просто використовувати їх координати, зазнали невдачі. Все впирається в те, що в розрахунок коефіцієнта k входить і поточний масштаб по X, і поточний масштаб по Y. І витягнути їх з формули не виходить. Товариш quverty мене про це попереджав і виявився правий.
  2. Хотів би звернути увагу, що кривизну побудованого графіка можна регулювати. Для цього слід змінювати відстань до опорних точок - змінювати коефіцієнт ^ X'= ^ X/2 * Sqrt (1/( 1 + k2)), а саме - знаменник ось цього його шматочка: ΔX/2. Знаменник менше 1 брати не слід (читай третій підпункт пункту «Основна ідея»). Поекспериментувати можна на JSFiddle (129 рядок JavaScript-а).
  3. Зверніть увагу на реалізацію сплайну Катмулла-Рома товариша ^ vana. І спасибі йому за цей коментар.
Image