Fast update of bitmap from polar coordinate data
Hi,
I am producing a marine radar display in VB.Net and struggling to get the radar data to display quickly enough.
The data is coming into my application from a callback from the radar maker's dll. Each dataset consists of an integer between 0 and 8191 denoting the angle from the datum (ship's head) that the dataset represents and a byte array with 480 elements containing data from ship's position (byte 0) to maximum range (byte 480). Each of these locations is a single byte, I want to set both red and green components of the relevant pixel in a 1000 x 1000 bitmap to this byte value. The dataset number increments round from 0 to 8191 as the radar scanner rotates.
My first approach was to use math.sin and math.cos to convert polar coordinates to cartesian coordinates, and then use setPixel to set the colour of the relevant pixels but this is far too slow.
I'm now trying to use Boop Boops 'FastPix' code:
http://www.vbforums.com/showthread.php?t=586709
Which looks like it should be much better, but I am wondering if there is a more efficient approach, for example cutting out the polar to cartesian conversion, or putting the data direct into a byte array?
I am also running the radar picture processing code on a thread separate to the UI thread to keep the UI responsive - what is the best way to display the resultant bitmap onto the UI? I am currently using a delegate and putting the bitmap into a picturebox, and I am sure there must be a better way?
Many Thanks
Chris
Re: Fast update of bitmap from polar coordinate data
The suggestion I can make is that you show us a bit of your drawing code so we can see how it looks and offer suggestions for improvements there. FastPix is a good idea as well since it moves pretty fast.
But, as another suggestion: I would handle all drawing code inside the PictureBox Paint method. You have direct access to the Graphics object which allows you to paint directly on the PictureBox. I would think you'd see a speed improvement there, and you wouldn't need to utilize FastPix or SetPixel as you would literally be drawing right to the PictureBox.
Re: Fast update of bitmap from polar coordinate data
Thanks for the suggestions. Using fastpix is definitely better, but my code is still too slow:
Code:
Try
Dim echo(scale) As Byte 'Create byte array to hold echo data
Dim bm As New Bitmap(frmRadarInstance.PictureBox1.Image)
FastPix.ConvertFormat(bm) 'convert bitmap to 32bppPArgb
System.Runtime.InteropServices.Marshal.Copy(echoPtr, echo, 0, scale) 'Get the echo data
dblAngleRad = angle / 8192 * (Math.PI * 2) 'Convert the scan number to an angle in radians
dblAngleCos = Math.Cos(dblAngleRad)
dblAngleSin = Math.Sin(dblAngleRad)
Using fp As New FastPix(bm)
For radius As Integer = 0 To scale
Dim dimX As Integer = 500 + Convert.ToInt32(radius * dblAngleCos) 'Calculate cartesian coordinates with origin offset to 500,500
Dim dimY As Integer = 500 + Convert.ToInt32(radius * dblAngleSin)
fp.SetPixel(dimX, dimY, Color.FromArgb(echo(radius), echo(radius), 0))
Next
End Using
frmRadarInstance.BeginInvoke(New UpdateBitmap( _
AddressOf frmRadarInstance.updateBitmap), New Object() _
{bm})
Catch ex As Exception
Debug.WriteLine(ex.ToString)
End Try
I will try using the PictureBox paint method. I am also inclined to try a lookup table for the conversion to cartesian coordinates?
Thanks
Chris
Re: Fast update of bitmap from polar coordinate data
Hi chriscross,
It's nice to see you are using FastPix. Here's a suggestion for what I think will be a more efficient approach. Instead of plotting the points directly to the picturebox Image, plot them to a single horizontal line:
Code:
Dim radialLine As New Bitmap(scale, 1)
Using fp As New FastPix(radialLine)
Dim Pixels() As Integer = fp.PixelArray
For radius As Integer = 0 To Scale - 1
Pixels(radius) = &HFF000000 + echo(radius) * 257 * 256 'I may have to explain this!
Next
End Using
I've used my favourite FastPix version, the Integer array, because it's far away the most efficient. Still, I'm not certain that the above will actually be much faster than old-fashioned Bitmap.SetPixel, because the number of pixels involved is probably small and Lockbits/FastPix has an overhead.
Now you have a horizontal line plotted in the correct colours. Next draw it at the required angle on your target bitmap. I'll assume you want to draw the full sweep circle, not just the single line. Otherwise use g.Clear to clear the graphics.
Code:
'At form level:
Dim bm As New Bitmap(PictureBox1.Width, PictureBox1.Height)
'In your sub:
Using g As Graphics = Graphics.FromImage(bm)
Using mtx As New Drawing2d.Matrix
mtx.RotateAt(angle, New Point(500, 500)) 'no need to convert to radians
g.Transform = mtx 'rotate the Graphics
End Using
g.DrawImageUnscaled(radialLine, 500, 500)
End Using
PictureBox1.Image = bm
This is just a sketch and I haven't tried it out (yet) but I hope the idea is clear enough. You might gain some speed by doing your own maths and using a lookup table, but my impression is that GDI+ is pretty efficient with things like rotating the graphics. And I hope you'll agree this is much simpler.
BB
Re: Fast update of bitmap from polar coordinate data
I can't see a need, here, to draw to a bitmap. Draw directly to the drawing surface in a Paint or OnPaint method.
Re: Fast update of bitmap from polar coordinate data
Quote:
Originally Posted by
boops boops
It's nice to see you are using FastPix. Here's a suggestion for what I think will be a more efficient approach. Instead of plotting the points directly to the picturebox Image, plot them to a single horizontal line:
Hi BB,
Thank you for the suggestion, that certainly appears to be a neater and simpler implementation, however I am having a few issues with it. In the line:
Code:
Pixels(i) = echo(radius) * 257 * 256 'I may have to explain this!
Should pixels(i) be pixels(radius)?
In the second section, should it be
Code:
Using g As Graphics = Graphics.FromImage(bm)
Using mtx As New Drawing2d.Matrix
mtx.RotateAt(angle, new PointF (500, 500)) 'no need to convert to radians
g.Transform = mtx 'rotate the Graphics
End Using
g.Graphics.DrawImageUnscaled(radialLine, 500, 500)
End Using
PictureBox1.Image = bm
I am not getting any exceptions, but it is not drawing anything to the picture box? The pixels array appears to have valid data in it, but I am not sure about either radialLine or bm.
Many Thanks
Chris
Re: Fast update of bitmap from polar coordinate data
Quote:
Originally Posted by
SJWhiteley
I can't see a need, here, to draw to a bitmap. Draw directly to the drawing surface in a Paint or OnPaint method.
I did try this, but it made the UI very unresponsive. I assume I need to iterate through the whole data array within the paint event in order to display the full circle of data?
Thanks
Chris
Re: Fast update of bitmap from polar coordinate data
Quote:
Originally Posted by
chriscross123
Hi BB,
Should pixels(i) be pixels(radius)?
Yes, of course. I was in a bit of a hurry and didn't even check the syntax!
Quote:
Code:
Using g As Graphics = Graphics.FromImage(bm)
mtx.RotateAt(angle, new PointF (500, 500)) 'no need to convert to radians
You're right again. Same excuse...:blush:
EDIT: I've spotted the problem (plus another syntax correction). I forgot to set the Alpha byte:sick:! See the corrections in red in Post #4. I think you will find it's quick enough. I just did a test with "scale" set to 500 and 360 lines drawn at 1 degree intervals. On my rather average PC it took 41 milliseconds to complete -- and that included generating the random data and updating the PictureBox.Image.
@SJWhiteley. The radar detects echoes in a single direction, but the aerial rotates to produce a usable picture. So the lines of data have to be accumulated somewhere. You could do that on a bitmap or in a collection. But to plot a point in the Paint event you have to draw a 1*1 pixel rectangle. Assuming a data line of 500 pixels and a rotary resolution of 1 degree, that would mean drawing 180,000 tiny rectangles in the Paint event. I haven't tried it but it doesn't sound very efficient to me. Anyway, what's wrong with using a bitmap?
BB
1 Attachment(s)
Re: Fast update of bitmap from polar coordinate data
Hi BB,
Thanks again for your help.
Radius is up to 496, and the picture box is 1000 x 1000.
I have done a quick check with
commented out and still got no line.
I have attached a small sample of one line of data with the first 100 echo() values and equivalent pixel() values.
Am I right in thinking that
Code:
Pixels(radius) = echo(radius) * 257 * 256 'I may have to explain this!
gives the RGB value for the pixel?
From the sample data Pixels(0) = 16579584 = 0xFCFC00 = R:252 G:252 B:0
Does there need to be an alpha value included in this?
Many Thanks
Chris
Re: Fast update of bitmap from polar coordinate data
See the edit to my last post, which crossed with your reply. It was indeed the alpha! I put some corrections in the code in post #4.
BB
Re: Fast update of bitmap from polar coordinate data
That is great, thank you.
I have only had a chance to do a quick check, but it is now drawing the line and is much, much quicker than what I had previously.
I will test it properly tomorrow.
Many Thanks
Chris
Re: Fast update of bitmap from polar coordinate data
Quote:
Originally Posted by
boops boops
...
@SJWhiteley. The radar detects echoes in a single direction, but the aerial rotates to produce a usable picture. So the lines of data have to be accumulated somewhere. You could do that on a bitmap or in a collection. But to plot a point in the Paint event you have to draw a 1*1 pixel rectangle. Assuming a data line of 500 pixels and a rotary resolution of 1 degree, that would mean drawing 180,000 tiny rectangles in the Paint event. I haven't tried it but it doesn't sound very efficient to me. Anyway, what's wrong with using a bitmap?
BB
Using a bitmap like this, I think, is heavy handed (and a lot of people seem to want to do it this way). While I think you are probably correct that drawing those rectangles will be very time consuming, there are alternatives rather than using a bitmap.
If things start to get sluggish in a paint event, the next option is to create a Buffer (BufferedGraphics Object), perform your drawing on that, and Render the buffer in the paint event. It's faster than blitting a bitmap using DrawImage.
edit: yes, drawing that many rectangles was pretty chronic: here's an example (without the angular math, as that's not really the point) that draws 2 million rectangles randomly in a 1000x1000 buffer with a random color. The resultant buffer is rendered in the OnPaint event.
Code:
Public Class Form1
Private Structure MyPoint
Public Location As PointF
Public Color As Color
End Structure
Private Rnd As New Random
Private MyDataSet As List(Of List(Of MyPoint))
Private context As BufferedGraphicsContext
Private grafx As BufferedGraphics
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Dim styles As Windows.Forms.ControlStyles = ControlStyles.AllPaintingInWmPaint Or ControlStyles.OptimizedDoubleBuffer Or ControlStyles.ResizeRedraw Or ControlStyles.UserPaint
Me.SetStyle(styles, True)
MyDataSet = New List(Of List(Of MyPoint))(8192)
'
For i As Integer = 0 To MyDataSet.Capacity - 1
Dim l As New List(Of MyPoint)
For j As Integer = 0 To 255
Dim x As Single = CSng(Rnd.NextDouble * 1000.0)
Dim y As Single = CSng(Rnd.NextDouble * 1000.0)
Dim c As Integer = Rnd.Next(0, Integer.MaxValue)
Dim mp As New MyPoint
mp.Location = New PointF(x, y)
mp.Color = Color.FromArgb(c)
l.Add(mp)
Next
MyDataSet.Add(l)
Next
'
End Sub
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
context = BufferedGraphicsManager.Current
context.MaximumBuffer = New Size(Me.Width + 1, Me.Height + 1)
grafx = context.Allocate(Me.CreateGraphics(), New Rectangle(0, 0, 1000, 1000))
Call RenderRectangles(grafx.Graphics)
End Sub
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
grafx.Render(e.Graphics)
MyBase.OnPaint(e)
End Sub
Private Sub RenderRectangles(ByVal g As Graphics)
g.Clear(Color.White)
For Each pds As List(Of MyPoint) In MyDataSet
If pds Is Nothing Then Continue For
For Each p As MyPoint In pds
Dim pn As Pen = New Drawing.Pen(p.Color)
g.DrawRectangle(pn, p.Location.X, p.Location.Y, 1, 1)
Next
Next
End Sub
End Class
Re: Fast update of bitmap from polar coordinate data
Hi SJW,
I'm curious about the BufferedGraphics Class. Msdn says it provides custom double buffering, which I understood to be its main purpose. If that's correct then I don't see what good it can do when used on a PictureBox, which is double buffered by default anyway.
Still, I wonder whether it offers any performance advantages? Have you done (or seen) a performance comparison with a using bitmap used as a buffer? If not, it would be interesting to time them head to head.
BB
Re: Fast update of bitmap from polar coordinate data
Quote:
Originally Posted by
boops boops
Hi SJW,
I'm curious about the BufferedGraphics Class. Msdn says it provides custom double buffering, which I understood to be its main purpose. If that's correct then I don't see what good it can do when used on a PictureBox, which is double buffered by default anyway.
Still, I wonder whether it offers any performance advantages? Have you done (or seen) a performance comparison with a using bitmap used as a buffer? If not, it would be interesting to time them head to head.
BB
The bufferedGraphics, as MSDN says, is for double buffering. Double buffering is more than just for pushing graphics to the screen. I've used it to create textured backgrounds on chart controls, so they don't need to be recreated constantly when drawing.
When you have just a few images to draw/render, the performance isn't too different, but when you have 30 or so (chart controls in my case) to render with a fast update rate, there is a difference.
However, it doesn't solve all [speed/usability] problems: the GraphicsBuffer is simply a buffer with a fixed graphics object pointer, and renders the buffer using BitBlt. I've used a combination of BitBlt directly - nothing beats BitBlt for versatility - image/bitmaps with DrawImage, and drawing directly in paint events, as well as custom C DLLs passing in handles; using whatever is appropriate.
I think a 'classic' example would be rendering an oscilloscope background: the scale is quite complex and time consuming to render dotted/dashed lines, perhaps a texture, and then draw the actual signal(s) on top of that. Precreating the scale in a buffer, rendering the buffer, then drawing the signal and other components on top of that is significantly faster. True, a Bitmap can be used for this, and would give similar performance, but I don't feel (through experimentation) that it is as scalable as using a BufferedGraphics object.
YMMV, as it really does depend on the task at hand. It's just another - quite useful - tool. It isn't an image manipulation tool - not like your 'FastPix' object - but is designed for high speed rendering.
Re: Fast update of bitmap from polar coordinate data
I agree that if the performance still isn't good enough, the next thing to look at would be rendering speed. A simple way to improve that would be to draw the bitmap to an XNA viewport instead of a PictureBox (I have some code for that somewhere, originally posted by Jenner). Or use WPF. BB
Re: Fast update of bitmap from polar coordinate data
Thank you both for your suggestions.
BB's solution in post #4 is working much better than my previous approach, but is still rather borderline for performance (in particular at short ranges where the radar spins faster and generates more data) , so I think I need to do some more work.
I will try the bufferedGraphics approach - could you give me a pointer towards the XNA approach please BB?
Many Thanks
Chris
Re: Fast update of bitmap from polar coordinate data
Here is an advanced search of "Post by Jenner" with Key word XNA.
This should help.
Good luck, this has been an interesting thread to follow!
Re: Fast update of bitmap from polar coordinate data
Hi chriscross,
Unfortunately I was mistaken about the XNA example. I found the demo that Jenner made (it was from this thread, post#10) but unfortunately the link is now dead. I dug out the code but I think it will take a lot of work to convert it to something you can use; there is too much I don't know about XNA.
Still, I made my own attempt at a "radar" screen which you might find interesting for comparison. With an angular resolution of 1 degree it takes me about 3-4 seconds to do a full 360 degree sweep. It uses random data for each echo line.
It seems that most of the time goes into updating the bitmap with the new line (not the rendering after all). I almost doubled the performance up by drawing 2 lines in each pass, and the speed could be further improved by increasing that to 3 or more.
I widened the line bitmap to 3 pixels to provide a kind of anti-aliasing. (The middle line has the data at full opacity, and the first and third lines the same data partly transparent). It seems this costs little extra time and the resulting scan is much smoother.
If you want to try it you can just copy the code below into a default form. There's no need to do anything in the designer. You can resize the form to enlarge the scan. The timing results are shown in the Output window.
Code:
Public Class Form1
Private WithEvents tim As New Timer With {.Interval = 15}
Private WithEvents pictureBox1 As New PictureBox
Private sw As Stopwatch
Private echo1() As Integer 'echo data array 1
Private echo2() As Integer 'echo data array 2
Private echoResolution As Integer 'was "scale"
Private angleResolution As Single = 1
Private rnd As New Random
Private scanBitmap As Bitmap 'was "bm"
Private echoLine1 As Bitmap
Private echoLine2 As Bitmap
Private angle As Single
Private refreshInterval As Integer = 1
Private fadeInterval As Integer = 4
Private refreshCount As Integer
Private fadeCount As Integer
'setup in the load event:
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
Me.ClientSize = New Size(600, 600)
PictureBox1.BackColor = Color.FromArgb(0, 50, 20)
pictureBox1.Dock = DockStyle.Fill
Me.Controls.Add(pictureBox1)
tim.Start()
End Sub
'some setup in the SizeChanged event, to allow user resizing
Private Sub PictureBox1_SizeChanged(sender As Object, e As System.EventArgs) Handles pictureBox1.SizeChanged
scanBitmap = New Bitmap(pictureBox1.Width, pictureBox1.Height)
echoResolution = pictureBox1.Width \ 2 - 5
ReDim echo1(echoResolution - 1)
ReDim echo2(echoResolution - 1)
echoLine1 = New Bitmap(echoResolution, 3)
echoLine2 = New Bitmap(echoResolution, 3)
End Sub
Private Sub PictureBox1_Paint(sender As Object, e As System.Windows.Forms.PaintEventArgs) Handles pictureBox1.Paint
'draw a sweep indicator line
e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
Dim centre As New Point(pictureBox1.Width \ 2, pictureBox1.Height \ 2)
Using mtx As New Drawing2D.Matrix
mtx.RotateAt(angle + 0.2F, centre)
e.Graphics.Transform = mtx
Using pn As New Pen(Color.LightGreen, 1)
e.Graphics.DrawLine(pn, centre, New Point(pictureBox1.Width - 5, centre.Y))
End Using
End Using
End Sub
Private Sub Timer1_Tick(sender As Object, e As System.EventArgs) Handles tim.Tick
'update the bitmap and depending on the refreshinterval, put it in the picture box
UpdateScanBitmap()
refreshCount = (refreshCount + 1) Mod refreshInterval
If refreshCount = 0 Then pictureBox1.Image = scanBitmap
fadeCount = (fadeCount + 1) Mod fadeInterval
If fadeCount = 0 Then scanBitmap = FadeImage(scanBitmap, 98)
End Sub
'Update the scan:
Private Sub UpdateScanBitmap()
If sw Is Nothing Then sw = Stopwatch.StartNew
'create a set of simulated data for the echo line:
For d As Integer = 0 To echo1.Count - 1
echo1(d) = rnd.Next(0, 255)
echo2(d) = rnd.Next(0, 255)
Next
'conver the data to a line bitmap:
echoLine1 = GetEchoBitmap(echo1, echoLine1)
echoLine2 = GetEchoBitmap(echo2, echoLine2)
'draw the echo lines on the scan
Using g As Graphics = Graphics.FromImage(scanBitmap)
Dim centre As New Point(pictureBox1.Width \ 2, pictureBox1.Height \ 2)
Using mtx As New Drawing2D.Matrix
mtx.RotateAt(angle, centre) '
g.Transform = mtx 'rotate the Graphics
g.DrawImageUnscaled(echoLine1, centre.X, centre.Y - 1)
mtx.RotateAt(angleResolution, centre)
g.Transform = mtx
g.DrawImageUnscaled(echoLine2, centre.X, centre.Y - 1)
End Using
End Using
'update the angle
angle = (angle + 2 * angleResolution) Mod 360.0F
'provide feedback in the Output window:
If angle < angleResolution Then
angle = angle Mod 360.0F
Console.WriteLine("360 deg. sweep time = " & ((sw.ElapsedMilliseconds / 1000).ToString("N4")))
sw = Stopwatch.StartNew
End If
End Sub
Private Function GetEchoBitmap(echo As Integer(), echoLine As Bitmap) As Bitmap
Using fp As New FastPix(echoLine)
Dim pixels() As Integer = fp.PixelArray
For radius As Integer = 0 To echoResolution - 1
Dim data As Integer = echo(radius) * 256
pixels(radius) = &HAF000000 + data
pixels(radius + echoResolution) = &HFF000000 + data
pixels(radius + 2 * echoResolution) = &HAF000000 + data
Next
End Using
Return echoLine
End Function
Private Function FadeImage(bmp As Bitmap, opacityPercent As Double) As Bitmap
'set the opacity of an image:
If opacityPercent >= 0 AndAlso opacityPercent <= 100 Then
Dim alphaRatio = opacityPercent / 100
Using fp As New FastPix(bmp)
Dim pixels() = fp.PixelArray
For i As Integer = 0 To pixels.Count - 1
Dim pixel = pixels(i)
Dim alpha As Integer = (pixel >> 24) And &HFF
alpha = CInt(alphaRatio * alpha) << 24
pixels(i) = pixel And &HFFFFFF Or alpha
Next
End Using
Return bmp
Else
Console.WriteLine("FadeImage: opacityPercent must be in the range 0-100.")
Return Nothing
End If
End Function
End Class
BB
Re: Fast update of bitmap from polar coordinate data
Hi,
Apologies for not updating to this thread sooner - thanks to everyone for their input, particularly BB for taking the time to produce an example radar.
I now have a system that works well on my development machine, I now need to optimise it for slower processors (the target is an Intel Atom based single board computer)
I am currently working through the best way to implement BB's multiple lines in each pass with the real data I am processing - I will post back with the results and my code when I have got a bit further with it.
Thanks
Chris