Custom loss function for discontinuous angle calculation

username: spacemeerkat
cooked: <p>I’m trying to input images of objects that are rotated in the xy plane and use a CNN to learn the angle of rotation in the range 0-360 degrees.</p>
<p>However as mentioned in many posts (eg. <a href="https://datascience.stackexchange.com/questions/24986/parameterization-regression-of-rotation-angle" rel="noopener nofollow ugc">Data_Science</a>) , there’s  a discontinuity between 0 degrees and 360 degrees where predictions of 359.9 degrees will be penalised heavily when the true value is in fact 0 degrees and so the prediction is very close to the target but the discontinuous nature of the numerical range makes the error huge.</p>
<p>This results in striping at 0 and 360 degrees (and also 180 degrees) - see image below:</p>
<p><div class="lightbox-wrapper"><a class="lightbox" href="https://discuss.pytorch.org/uploads/default/original/3X/c/e/cef25628340c7e009af517ecac7670c4167d7981.png" data-download-href="https://discuss.pytorch.org/uploads/default/cef25628340c7e009af517ecac7670c4167d7981" title="only_pos1"><img src="https://discuss.pytorch.org/uploads/default/optimized/3X/c/e/cef25628340c7e009af517ecac7670c4167d7981_2_517x369.png" alt="only_pos1" data-base62-sha1="twJySybcmclKz3Tlp3yafOt3Ra9" width="517" height="369" srcset="https://discuss.pytorch.org/uploads/default/optimized/3X/c/e/cef25628340c7e009af517ecac7670c4167d7981_2_517x369.png, https://discuss.pytorch.org/uploads/default/original/3X/c/e/cef25628340c7e009af517ecac7670c4167d7981.png 1.5x, https://discuss.pytorch.org/uploads/default/original/3X/c/e/cef25628340c7e009af517ecac7670c4167d7981.png 2x" data-dominant-color="FBFAFA"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use href="#far-image"></use></svg><span class="filename">only_pos1</span><span class="informations">700×500 15.3 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use href="#discourse-expand"></use></svg></div></a></div></p>
<p>I want to write a custom loss function that instead looks to minimise the smallest angle between predicted and true rotation angle given that there are 4 ways in which you can measure the angle between them.</p>
<p><div class="lightbox-wrapper"><a class="lightbox" href="https://discuss.pytorch.org/uploads/default/original/3X/0/a/0a8a0c2c4f972a0f52c3eaa6fc8337382408c8af.png" data-download-href="https://discuss.pytorch.org/uploads/default/0a8a0c2c4f972a0f52c3eaa6fc8337382408c8af" title="image"><img src="https://discuss.pytorch.org/uploads/default/optimized/3X/0/a/0a8a0c2c4f972a0f52c3eaa6fc8337382408c8af_2_475x500.png" alt="image" data-base62-sha1="1vexuBfuWcfyM2A8mwJQ7mZU7Lx" width="475" height="500" srcset="https://discuss.pytorch.org/uploads/default/optimized/3X/0/a/0a8a0c2c4f972a0f52c3eaa6fc8337382408c8af_2_475x500.png, https://discuss.pytorch.org/uploads/default/original/3X/0/a/0a8a0c2c4f972a0f52c3eaa6fc8337382408c8af.png 1.5x, https://discuss.pytorch.org/uploads/default/original/3X/0/a/0a8a0c2c4f972a0f52c3eaa6fc8337382408c8af.png 2x" data-dominant-color="EFF3F8"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use href="#far-image"></use></svg><span class="filename">image</span><span class="informations">523×550 39 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use href="#discourse-expand"></use></svg></div></a></div></p>
<p>Taking the absolute of these (or the squared angular difference) makes two of these angular measurements equivalent and therefore you only have 2 ways to measure the angular difference between true and predicted angle.</p>
<p>Therefore my custom loss function looks as follows:</p>
<pre><code class="lang-auto">def pos_loss(output, target):
   """ LOSS FUNCTION FOR POSITION ANGLE BASED ON MINIMUM ARC SEPARATION """
   loss = torch.mean(torch.stack([ (output-target)**2,
                                  (1-torch.abs(output-target))**2] ).min(dim=0)[0])
   return loss
</code></pre>
<p>Note: the 1 in <code>1-torch.abs(ou...</code> comes from my target being in the range 0-1 as  I have normalised the range 0-360 to mean that I can use a sigmoid activation for the output layer and simply get back to the true angle by multiplying up by 360, post training.</p>
<p>And I use the loss function during a CNN training run simply by calling:</p>
<pre><code class="lang-auto">loss = pos_loss(prediction,targets)
optim.zero_grad(); loss.backward(); optim.step()
</code></pre>
<p>Am I missing a huge step in implementing a custom loss function here that means the way I’m using mine means no appropriate gradients can be calculated?</p>
<p>Using <code>torch.nn.MSELoss()</code> instead trains well but gives striping, but using my custom loss function gives really unstable training that often defaults to predicting close to 0 for all input examples.</p>
<p>Many thanks in advance and please ask as many questions as you’d like.</p>
<hr>
<p>Extra information:</p>
<p>I am using:<br>
Python 3.6.8<br>
PyTorch 1.0.0</p>

username: Oli
cooked: <p>Hi <img src="https://discuss.pytorch.org/images/emoji/apple/weight_lifting_man.png?v=9" title=":weight_lifting_man:" class="emoji" alt=":weight_lifting_man:"></p>
<p>Your gradients should be fine since you are using pytorch functions. I don’t know about that logic though, looks a bit weird. Try if it gives the same outputs as this function I found on SO.</p>
<pre><code class="lang-auto">def angle(angle1, angle2):
    a = angle1 - angle2
    a = (a + 180) % 360 - 180
    print(abs(a))


angle(359, 1)  # -&gt; 2
angle(10, 100)  # -&gt; 90
angle(10, 150)  # -&gt; 140
angle(10, 200)  # -&gt; 170
</code></pre>

username: spacemeerkat
cooked: <p>Apologies for the late reply on this!</p>
<p>I found that your method worked but then your use of quadrants got me thinking about Euler rotation matrices. So I tried instead learning the diagonal components of the rotation matrix and then running an atan2 method of retrieving theta. This worked like a charm in removing the discontinuity at 0/360 degrees and accounts for the quadrants too.</p>

username: pvardanis
cooked: <p>Could you share the loss function? I’m on a same problem here.</p>

username: spacemeerkat
cooked: <p>I don’t know how in depth you want me to go but here’s some useful info:</p>
<p>I was trying to learn the angle as an output of a network so instead of learning one value I instead learned 2 values corresponding to two components of the rotation matrix about z (R_z(φ) in <a href="https://www.gregslabaugh.net/publications/euler.pdf" rel="nofollow noopener">this helpful guide</a>).</p>
<p>I then recovered the angle using equation 3 in the same link above which corresponds to atan(sin(φ),cos(φ)).</p>
<p>So I learned sin(φ) and cos(φ) (which lay in the range (-1,1) using a hardtanh activation function,<br>
(<code>torch.nn.Hardtanh(min_val=-1,max_val=1</code>),</p>
<p>and then recovered the angle using,<br>
<code>torch.atan2(sin(φ),cos(φ))</code></p>
<p>This gave the resulting angle back in the range (-180,180) degrees so you have to be careful and make sure your sin(φ) and cos(φ) which come out at  the end of the network are in the range (-1,1).</p>
<p>I hope that helps! As for a loss function I simply used mean squared error loss and it works beautifully.</p>

username: pvardanis
cooked: <p>Sounds like an interesting idea. However, I’m trying to predict 512 phases (not 1 as in your case). Do you have any idea on how to do this? I could just increase my vector output to 1024, so it’s like (sin(φ1), cos(φ1), sin(φ2), cos(φ2), … , sin(φn), cos(φn)) for n = 1 to 512. Another think that is important is, that on my problem the output phases I get after I generate data have values of 128 levels from 0 to 2pi (reason doesn’t really matter). So, it’s not actually continuous values in a sense that they could be any value in the range of 0 to 2*pi.</p>
<p>EDIT: The second idea I have, is doing multi-output classification. So, instead of having only one full connected fork at the end, I can have 512. And each fork would have 128 classes with a sigmoid as an activation function.</p>

username: KFrank
cooked: <p>Hello pvardanis (and Mr. Meerkat)!</p>
<p>May I suggest <code>loss = 1.0 - cos (theta_pred - theta_targ)</code>?  This<br>
loss is zero when <code>theta_pred = theta_targ</code> (or they differ by a multiple<br>
of 360 degrees).  It is the analog on the circle of the squared error on the<br>
real line.</p>
<p>Best.</p>
<p>K. Frank</p>

username: pvardanis
cooked: <p>Hi KFrank,</p>
<p>My true labels are int numbers from 0 to 127 representing values from 0 to 2pi in discrete levels, due to hardware limitations. I think I need to approach this problem as a classification task, because right now it doesnt seem to converge, plus I think it’s a little bottleneck to try to predict continuous values when my true labels are not continuous.Thus, a CrossEntropy loss might be better.</p>

username: KFrank
cooked: <p>Hello pvardanis!</p>
<aside class="quote no-group" data-username="pvardanis" data-post="8" data-topic="58579" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/pvardanis/48/21308_2.png" class="avatar"> pvardanis:</div>
<blockquote>
<p>My true labels are int numbers from 0 to 127 representing values from 0 to 2pi in discrete levels, due to hardware limitations.</p>
</blockquote>
</aside>
<p>I understand this as saying that, in some sense, your “real” problem<br>
involves an angular variable that ranges from 0 to 2pi (and where<br>
0 = 2pi, etc.), and that, in practice, you use 128 discrete values for<br>
“technical” reasons.</p>
<aside class="quote no-group">
<blockquote>
<p>I think I need to approach this problem as a classification task …  Thus, a CrossEntropy loss might be better.</p>
</blockquote>
</aside>
<p>I think that this is actually backwards.  If you treat this as a classification<br>
problem, then you (and the <code>CrossEntropyLoss</code> you suggest using) are<br>
saying that if your label for a sample is 0, then, while a prediction of 0 is<br>
right, a prediction of 1 is just as wrong as a prediction of, say, 64.  In my<br>
understanding of your problem, a prediction of 1 (and also 127) is quite<br>
good, although not the very best, and a prediction of 64 is very bad.</p>
<p>Do you really want to penalize a prediction of 1 anywhere near as much<br>
as a prediction of 64?</p>
<aside class="quote no-group">
<blockquote>
<p>because right now it doesnt seem to converge,</p>
</blockquote>
</aside>
<p>Well, that’s a problem, but I really don’t think working with a seven-bit<br>
discretization of an angular variable is the cause.</p>
<aside class="quote no-group">
<blockquote>
<p>plus I think it’s a little bottleneck to try to predict continuous values when my true labels are not continuous.</p>
</blockquote>
</aside>
<p>I don’t see this as a problem.  128 values (seven bits) is a bit coarse,<br>
but still a fair approximation to a continuous variable.</p>
<p>Best.</p>
<p>K. Frank</p>

username: pvardanis
cooked: <p>Hi K. Frank,</p>
<p>Thanks for your suggestions. What I mean by “bottleneck” is, that regression with 512 units as outputs is a more difficult task than classification, especially since you are not supressing your output in any case (at least that’s what I think, correct me if I’m wrong). I tried creating a custom loss as <code>torch.mean(torch.fmod(output - target, 2 * np.pi) ** 2)</code>, but it didn’t seem to converge to a loss lower than pi / 2. And considering that due to <code>torch.fmod()</code> the loss is always going to be less than 2 * pi, then a loss of pi / 2 is still huge. Still, I totally get the thing you said on penalizing the model on neighboring predictions and you’re right. Maybe I could try what has been used for ordinal regression? Never read anything about this, but I know that it exists. You can check the thread I opened here, and maybe you can understand my problem better: <a href="https://discuss.pytorch.org/t/transfer-learning-using-vgg-16-or-19-for-regression/72513/10" class="inline-onebox">Transfer learning using VGG-16 (or 19) for regression</a></p>
<p>Thanks</p>

username: KFrank
cooked: <p>Hi pvardanis!</p>
<aside class="quote no-group" data-username="pvardanis" data-post="10" data-topic="58579" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/pvardanis/48/21308_2.png" class="avatar"> pvardanis:</div>
<blockquote>
<p>What I mean by “bottleneck” is, that regression with 512 units as outputs is a more difficult task than classification,</p>
</blockquote>
</aside>
<p>If I understand your problem, the correct comparison is 512<br>
“regressions” vs. 512 classifiers, each with 128 classes.  Not<br>
only do I think that the classifier is not really the problem you<br>
want to solve, I also don’t see that it would be easier.</p>
<aside class="quote no-group">
<blockquote>
<p>especially since you are not supressing your output in any case</p>
</blockquote>
</aside>
<p>I’m not sure what you mean by this.  If you mean that for a label<br>
of 0, a prediction of 0 and of 2*pi and of 1000*pi are all equally<br>
good, so that your predictions could drift off and become very<br>
large, I think you’re right that this could be a problem.</p>
<aside class="quote no-group">
<blockquote>
<p>Still, I totally get the thing you said on penalizing the model on neighboring predictions and you’re right.</p>
</blockquote>
</aside>
<p>I do think that your real problem is to predict an angular variable (or<br>
more precisely, a set of 512 angular variables), and the fact that your<br>
targets are angular variables discretized to 128 different values is just<br>
a technical detail.</p>
<p>There are probably standard approaches for predicting angular<br>
variables with neural networks, but I don’t know what they are.</p>
<p>Having said that, here’s what I would suggest:</p>
<p>Represent each of your 512 angular outputs with two real numbers.<br>
If those two number were forced to lie on the unit circle, they would<br>
have only one degree of freedom, and that degree of freedom would<br>
be an angle.</p>
<p>We will “encourage” (but not fully constrain) each pair of such numbers<br>
to lie on the unit circle.  (Call them <code>x</code> and <code>y</code>.)</p>
<p>So build a network that outputs 1024 numbers, understood to be<br>
512 pairs, <code>(x_i, y_i)</code>.  Let your loss function have two terms: first,<br>
something like</p>
<pre><code class="lang-plaintext">   ( (x_i**2 + y_i**2) - 1 )**2,
</code></pre>
<p>to penalize the pair <code>(x_i, y_i)</code> from moving off the unit circle; and</p>
<pre><code class="lang-plaintext">   1 - cos (theta_i - theta_pred_i),
</code></pre>
<p>where <code>theta_i = atan2 (y_i, x_i)</code> is the angle predicted by your<br>
network, as represented by the pair <code>(x_i, y_i)</code>.</p>
<p>(Again, <code>1 - cos (delta_theta)</code> is the angular analog of the<br>
conventional squared error, <code>delta_x**2</code>, that you would use for<br>
a linear regression.)</p>
<p>I think that this is very much along the lines of what you and Mr. Meerkat<br>
were discussing, except it deals more cleanly with the constraint implicit<br>
in representing an angle with a pair of numbers, and uses the natural<br>
angular version of squared error for the loss.</p>
<p>To be clear, in this scheme I imagine the last layer of your network to be<br>
<code>nn.Linear (n, 1024)</code>, fed into the loss function outlined above.</p>
<p>Good luck.</p>
<p>K. Frank</p>

username: pvardanis
cooked: <p>Very interesting approach. So, the loss function is the sum of these two terms? And the</p>
<pre><code class="lang-auto">   ( (x_i**2 + y_i**2) - 1 )**2,
</code></pre>
<p>term refers only to the prediction?</p>
<p>EDIT: Supposing I have a vector of 1024 outputs with (x_i, y_i) pairs I think what you describe should look like this:</p>
<pre><code class="lang-auto">def Cosine(output, target):

    '''

    Cosine loss function. Penalizes the area out of the unit circle and wraps the output around 2pi.

    '''    

    loss_1 = (np.add.reduceat(output ** 2, np.arange(0, len(output), 2)) - 1) ** 2 # penalize if output is outside the unit circle

    loss_1 = torch.from_numpy(loss_1)

    loss_2 = 1. - torch.cos(output - target) # wrap loss around 2pi

    return torch.mean(loss_1) + torch.mean(loss_2)
</code></pre>
<p>loss_1 calculation just takes the squares of all elements of the output array and sums them up in pairs pair two, then subtract by 1 and take the square.</p>

username: KFrank
cooked: <p>Hello pvardanis!</p>
<aside class="quote no-group" data-username="pvardanis" data-post="12" data-topic="58579" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/pvardanis/48/21308_2.png" class="avatar"> pvardanis:</div>
<blockquote>
<pre><code class="lang-auto">   ( (x_i**2 + y_i**2) - 1 )**2,
</code></pre>
<p>term refers only to the prediction?</p>
</blockquote>
</aside>
<p>Yes.  The “real” loss – the part that also depends on the target – only<br>
cares about the angle of <code>(x, y)</code> – its direction in the x-y plane.  This<br>
term is just to <em>regularize</em> <code>(x, y)</code> by penalizing it if it drifts off the unit<br>
circle.  (There will be no term in the gradient pushing it off the unit circle).</p>
<aside class="quote no-group">
<blockquote>
<p>EDIT: Supposing I have a vector of 1024 outputs with (x_i, y_i) pairs I think what you describe should look like this:</p>
<pre><code class="lang-auto">    loss_1 = (np.add.reduceat(output ** 2, np.arange(0, len(output), 2)) - 1) ** 2 # penalize if output is outside the unit circle

    loss_1 = torch.from_numpy(loss_1)
</code></pre>
</blockquote>
</aside>
<p>This part won’t work as-is.  When you do part of your computation with<br>
numpy, you no longer get the gradients (autograd) “for free.”  If you go<br>
the numpy route, you will have to write your own explicit <code>backward()</code><br>
function that will let autograd backpropagate the gradients through this<br>
part of your loss.</p>
<p>I don’t know off-hand how to do it, but the cleanest approach would be<br>
to write this entirely with pytorch tensor operations (somehow slicing,<br>
indexing, and/or reshaping to get the <code>(x, y)</code> pairs).  If you do this just<br>
with pytorch tensor functions you will get autograd for free, and you<br>
won’t have to write a <code>backward()</code> function (and it will probably run<br>
faster).</p>
<aside class="quote no-group">
<blockquote>
<pre><code class="lang-auto">    loss_2 = 1. - torch.cos(output - target) # wrap loss around 2pi
</code></pre>
</blockquote>
</aside>
<p>This isn’t right.  First, <code>target</code> is of shape <code>(nBatch, 512)</code>, while <code>output</code><br>
is of shape <code>(nBatch, 1024)</code>.  Second, <code>target</code> is a set of angles, while<br>
<code>output</code> is a set of these <code>(x, y)</code> pairs that <em>encode</em> angles, but aren’t<br>
actually angles themselves.</p>
<p>You need to convert your <code>(x, y)</code> pairs to angles with<br>
<code>theta = atan2 (x, y)</code> and package them as a batch, <code>thetas</code>,<br>
of shape <code>(nBatch, 512)</code> with which you will then calculate your</p>
<pre data-code-wrap="python"><code class="lang-python">   loss_2 = 1. - torch.cos (thetas - target) 
</code></pre>
<p>Again, you should try to do this just with pytorch tensor operations so<br>
that autograd can give you the gradients without your writing an explicit<br>
<code>backward()</code> function.</p>
<p>I imagine that people have worked this all out somewhere already, but<br>
I couldn’t find anything off-hand, and I’m not aware of it being built into<br>
pytorch.</p>
<p>Good luck.</p>
<p>K. Frank</p>

username: pvardanis
cooked: <p>Now it’s correct:</p>
<pre><code class="lang-auto">def Cosine(output, target):
    '''
    Cosine loss function. Penalizes the area out of the unit circle and wraps the output around 2pi.
    ''' 

    # Penalize if output is out of the unit circle   
    squares = output ** 2 # (x ^ 2, y ^ 2)
    loss_1 = ((squares[:, ::2] + squares[:, 1::2]) - 1) ** 2 # (x ^ 2 + y ^ 2 - 1) ** 2

    # Compute the second loss, 1 - cos
    loss_2 =  1. - torch.cos(torch.atan2(output[:, 1::2], output[:, ::2]) - target)  
    
    return torch.mean(loss_1 + loss_2)
</code></pre>

username: KFrank
cooked: <p>Hi pvardanis!</p>
<aside class="quote no-group" data-username="pvardanis" data-post="14" data-topic="58579" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/pvardanis/48/21308_2.png" class="avatar"> pvardanis:</div>
<blockquote>
<p>Now it’s correct:</p>
</blockquote>
</aside>
<p>Yes, this does look right to me.  (No promises, though!)  This is indeed<br>
the loss function I was suggesting.</p>
<p>Does it work in practice for your model?</p>
<p>Good luck!</p>
<p>K. Frank</p>

username: pvardanis
cooked: <p>It doesn’t seem to converge. Indeed, starting with a value around 1.5, then when it’s approaching 1 it just remains in the same value range, and <code>optim.lr_scheduler.ReduceLROnPlateau</code> doesn’t seem to help. It’s still a long way to go of course, as this could be due to several reasons, and not the loss function. To sum up what I use as of now:</p>
<ul>
<li>VGG-16 with <code>batch_norm</code> pre-trained weights. I only train the classifier part.</li>
<li>Adam and SGD optimizers with several learning rates, to name a few: 0.0001, 0.0003, 0.001, 0.003.</li>
<li>Loss function is the one you suggested.</li>
<li>Dataset consists of 12k images, splitted randomly with a percent of .9 for training examples.</li>
</ul>
<p>I’m wondering if after all the problem may be the sparsity of the artificial images I’m creating, or the architecture itself. My images have dimensions (3, 224, 224) and number of points on those images ranges from 1 to 8 for now. Each image represents an area of measurements (in reality each combination of particles exist in a 12x12cm area, thus do the math and you have the resolution you can get with 224x224 pixels). That means that in the image <strong>5x5 area of pixels * number of points</strong> is colored (what each color represent doesn’t matter for now) and the rest is black. I.e. if I have 10 particles then 10 * (5 ** 2) pixels are different than black, the rest is black.</p>

username: KFrank
cooked: <p>Hello pvardanis!</p>
<aside class="quote no-group" data-username="pvardanis" data-post="16" data-topic="58579" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/pvardanis/48/21308_2.png" class="avatar"> pvardanis:</div>
<blockquote>
<p>It doesn’t seem to converge.</p>
</blockquote>
</aside>
<p>It might make sense to start a new thread to get more general insights<br>
about your use case and model from forum participants.</p>
<p>If you do, it would probably be helpful to give a short synopsis of the<br>
problem you are working on, and post a few sample images.</p>
<p>Some specific questions I would have:  What, conceptually, do your<br>
angles mean?  Is each image annotated with 512 angles?  How easy<br>
would it be for a person looking at one of your images to predict the<br>
angle(s)?  What does the post-VGG-16 part of your network look like?<br>
What do your loss curves (loss versus batch or epoch training iteration)<br>
look like?  (Maybe post some.)</p>
<p>Best.</p>
<p>K. Frank</p>

username: pvardanis
cooked: <p>There’s a thread already indeed here: <a href="https://discuss.pytorch.org/t/transfer-learning-using-vgg-16-or-19-for-regression/72513/8" class="inline-onebox">Transfer learning using VGG-16 (or 19) for regression</a>. I’m gonna update it with a new info.</p>

username: shbmgpta
cooked: <p>Hi,<br>
Wanted to check what about discontinuity at -180 and 180? Isn’t that same as (0,360).<br>
I am little confused on this loss function calculation. What is it that you are passing in MSE loss? Is it that sin and cos values or is it the angle retrieved using atan2?<br>
I think it makes more sense to pass the sin and cos values rather than the angle as it might have discontinuity.</p>