Implementação
Supondo que você já tenha uma rotina de desenho de linha, basta suplementar isso com algum tipo de interpolação. As curvas são criadas desenhando linhas curtas interpoladas suficientes para tornar o resultado mais suave. Um bom ponto de partida seria usar uma rotina de interpolação existente, como as fornecidas por Paul Bourke aqui .
Ilustrarei isso usando as rotinas cúbicas que ele fornece, pois essas são algumas das mais simples que ainda fornecerão resultados razoáveis. Aqui está o primeiro (traduzido para python) para referência:
def cubic(mu,y0,y1,y2,y3):
mu2 = mu*mu
a0 = y3 - y2 - y0 + y1
a1 = y0 - y1 - a0
a2 = y2 - y0
a3 = y1
return a0*mu*mu2 + a1*mu2 + a2*mu + a3
Cada rotina possui um parâmetro mu
que representa a parte fracionária do índice que você deseja interpolar. Dependendo da rotina, os outros parâmetros serão um número de amostras em torno do índice em questão. No caso cúbico, você precisará de quatro amostras. Por exemplo, se seus dados estão y[n]
, e você quer o valor em 10.3
, mu
seria .3
, e você passar em y[9]
, y[10]
, y[11]
, e y[12]
.
Em vez de desenhar uma única linha com pontos finais, digamos, ( 10 ,y10) → ( 11 ,y11), você desenharia vários mais curtos usando os valores interpolados (por exemplo, (10,y10)→(10.1,cubic(.1,y9,y10,y11,y12))…) Obviamente, esses pontos precisariam ser escalados para ox e y dimensões da imagem a ser renderizada.
Teoria
Agora, como a página / rotina que referi não cita nenhuma fonte, vale a pena explicar de onde vêm essas rotinas cúbicas (e como elas funcionam). Tanto o que reproduzi acima, quanto o spline Catmull-Rom muito semelhante que ele menciona logo abaixo, são dois casos específicos de uso do seguinte núcleo de convolução cúbica:
ψ(x)=⎧⎩⎨⎪⎪(α+2)|x|3−(α+3)|x|2+1,α|x|3−5α|x|2+8α|x|−4α,0, if 0≤|x|<1 if 1≤|x|<2 if 2≤|x|
A rotina listada acima corresponde a um valor de α=−1e o spline Catmull-Rom corresponde a α=−1/2. Não entrarei em muitos detalhes sobre como a forma geral do kernel é derivada, mas envolve várias restrições, como garantir queψ(x) é um em zero e zero em todos os outros números inteiros.
Isto é o que parece:
As duas opções para o valor de αprovêm de tentativas de combinar vários aspectos da função sinc , o núcleo de reconstrução ideal. Configuraçãoα=−1 faz a derivada de ψ coincidir com a derivada da função sinc em x=1e tornando-o igual a −1/2fornece a melhor aproximação de baixa frequência. Em todas as contas, um valor deα=−1/2possui propriedades muito melhores no geral, portanto é provavelmente o melhor valor para usar na prática. Uma discussão muito mais extensa pode ser encontrada no documento a seguir, começando na página 328:
Meijering, Erik. "Uma cronologia da interpolação: da astronomia antiga ao processamento moderno de sinais e imagens". Anais do IEEE. vol. 90, n. 3, pp. 319-42. Março de 2002.
Discernimento
Agora, apenas olhando para o kernel em relação à implementação real do código de interpolação, pode não estar claro como os dois estão relacionados. Basicamente, o processo de interpolação pode ser considerado como a adição de cópias deslocadas do kernel, que são dimensionadas pelas amostras dos dados, da seguinte forma:
De fato, se você tiver uma implementação do kernel, poderá usá-lo diretamente para fazer a interpolação, da seguinte maneira:
def kernel(x, a=-1.0):
x = abs(x)
if x >= 0.0 and x < 1.0:
return (a + 2.0)*x**3.0 - (a + 3.0)*x**2.0 + 1
elif x >= 1.0 and x < 2.0:
return a*x**3.0 - 5.0*a*x**2.0 + 8.0*a*x - 4.0*a
else:
return 0.0
def cubic(mu,y0,y1,y2,y3):
a = -1.0
result = y0 * kernel(mu + 1, a)
result += y1 * kernel(mu, a)
result += y2 * kernel(mu - 1, a)
result += y3 * kernel(mu - 2, a)
return result
No entanto, é muito menos eficiente computacionalmente fazê-lo dessa maneira. Como uma ponte da abordagem direta do kernel para a mais simplificada acima, considere que, com um pouco de manipulação algébrica, a primeira implementação pode ser colocada da seguinte forma:
def cubic(mu,y0,y1,y2,y3):
mu2 = mu*mu
mu3 = mu*mu2
c0 = -mu3 + 2*mu2 - mu
c1 = mu3 - 2*mu2 + 1
c2 = -mu3 + mu2 + mu
c3 = mu3 - mu2
return c0*y0 + c1*y1 + c2*y2 + c3*y3
Nesta formulação, os c0...c3
valores podem ser considerados os coeficientes de um filtro FIR que é aplicado aos valores da amostra. Agora é muito mais fácil ver como derivar a rotina do kernel. Considere o kernel comα=−1, igual a:
ψ(x)=⎧⎩⎨⎪⎪|x|3−2|x|2+1,−|x|3+5|x|2−8|x|+4,0, if 0≤|x|<1 if 1≤|x|<2 if 2≤|x|
Agora avalie esse kernel simbolicamente em várias compensações deslocadas, tendo em mente que mu
(μ) varia de 0
a 1
:
ψ(μ+1)ψ(μ)ψ(μ−1)ψ(μ−2)====−(μ+1)3+5(μ+1)2−8(μ+1)+4μ3−2μ2+1(1−μ)3−2(1−μ)2+1−(2−μ)3+5(2−μ)2−8(2−μ)+4====−μ3+2μ2−μμ3−2μ2+1−μ3+μ2+μμ3−μ2(c0)(c1)(c2)(c3)
Observe que μ−1,μ−2 seja "virado" para 1−μ,2−μ respectivamente, devido ao valor absoluto no xna definição do kernel. Agora, temos os polinômios exatos que são usados na "versão FIR" da rotina de interpolação. A avaliação desses polinômios pode então ser mais eficiente através de técnicas padrão (por exemplo , método de Horner ). Coisas semelhantes podem ser feitas com outros kernels e também existem outras maneiras de construir implementações eficientes ( consulte a Home Page da Digital Audio Resampling Home ).