Fast "for each pixel in bitmap" loop - vb.net

I'm writing a Visual Basic application that takes a screenshot of the desktop and crops it down to a 200px by 200px image around the center of the screen. One part of the application would iterate through each pixel and check if the RGB of that pixel is a certain color (this is meant to take under a second for it to be efficient), and unfortunately Bitmap.Getpixel is not doing me any good whether or not It's being loaded into the memory via Bitmap.Lock or not.
Is there a faster (almost instantaneous) way of doing so? Thanks.

Sure there is. Typically what you do is :
for each pixel
Get device contex
Read Pixel
Release device contex (unless you want memory leak)
For this to work you need few external windows library calls, ex :
[DllImport("user32.dll")]
static extern IntPtr GetDC(IntPtr hwnd);
[DllImport("user32.dll")]
static extern Int32 ReleaseDC(IntPtr hwnd, IntPtr hdc);
[DllImport("gdi32.dll")]
static extern uint GetPixel(IntPtr hdc, int nXPos, int nYPos);
static public System.Drawing.Color getPixelColor(int x, int y) {
IntPtr hdc = GetDC(IntPtr.Zero);
uint pixel = GetPixel(hdc, x, y);
ReleaseDC(IntPtr.Zero, hdc);
Color color = Color.FromArgb((int)(pixel & 0x000000FF),
(int)(pixel & 0x0000FF00) >> 8,
(int)(pixel & 0x00FF0000) >> 16);
return color;
}
It would be much better to
GetDC
for each pixel
read pixel and store value
ReleaseDC
However I have found that get pixel method itself is slow. Therefore to get better performance just grab the entire screen into a bitmap and get the pixels from there.
Here is some sample code in c#, you can convert it in VB.net if you want using online converters:
var maxX=200;
var maxY=200;
var screensize = Screen.PrimaryScreen.Bounds;
var xCenterSub100 = (screensize.X-maxX)/2;
var yCenterSub100 = (screensize.Y-maxY)/2;
Bitmap hc = new Bitmap(maxX, maxY);
using (Graphics gf = Graphics.FromImage(hc)){
gf.CopyFromScreen(xCenterSub100, yCenterSub100, 0, 0, new Size(maxX, maxY), CopyPixelOperation.SourceCopy);
//...
for (int x = 0; x < maxX; x++){
for (int y = 0; y < maxY; y++){
var pColor = hc.GetPixel(x, y);
//do something with the color...
}
}
}
In Vb.net (using http://converter.telerik.com/) :
Dim maxX = 200
Dim maxY = 200
Dim screensize = Screen.PrimaryScreen.Bounds
Dim xCenterSub100 = (screensize.X - maxX) / 2
Dim yCenterSub100 = (screensize.Y - maxY) / 2
Dim hc As New Bitmap(maxX, maxY)
Using gf As Graphics = Graphics.FromImage(hc)
gf.CopyFromScreen(xCenterSub100, yCenterSub100, 0, 0, New Size(maxX, maxY), CopyPixelOperation.SourceCopy)
'...
For x As Integer = 0 To maxX - 1
For y As Integer = 0 To maxY - 1
Dim pColor = hc.GetPixel(x, y)
'do something with the color...
Next
Next
End Using
With c# on my old computer i got around 30 fps, run time is about 35ms. There are faster ways, but they start to abuse several things to get that speed. Note that you do not use the getPixelColor, it is here just for reference. You instead use the screen scraped image method.

If you don't wish to resort to p/invoke, you can use the LockBits method. This code sets each component of a 200 x 200 area at the center of a bitmap in a PictureBox to a random value. It runs in about 100 milliseconds (not counting the refresh of the PictureBox).
EDIT: I realized you were trying to read pixels, so I added a line to show how to do that.
Private Sub DoGraphics()
Dim x As Integer
Dim y As Integer
'PixelSize is 4 bytes for a 32bpp Argb image.
'Change this value appropriately
Dim PixelSize As Integer = 4
Dim rnd As New Random()
'This code uses a bitmap that is loaded in a picture box.
'Any bitmap should work.
Dim bm As Bitmap = Me.PictureBox1.Image
'lock an area of the bitmap for editing that is 200 x 200 pixels in the center.
Dim bmd As BitmapData = bm.LockBits(New Rectangle((bm.Width - 200) / 2, (bm.Height - 200) / 2, 200, 200), System.Drawing.Imaging.ImageLockMode.ReadOnly, bm.PixelFormat)
'loop through the locked area of the bitmap.
For y = 0 To bmd.Height - 1
For x = 0 To bmd.Width - 1
'Get the various pixel locations This calculation is for a 32bpp Argb bitmap
Dim blue As Integer = (bmd.Stride * y) + (PixelSize * x)
Dim green As Integer = blue + 1
Dim red As Integer = green + 1
Dim alpha As Integer = red + 1
'Set each component of the pixel to a random rgb value.
'There are 4 bytes that make up each pixel (32bpp Argb)
Marshal.WriteByte(bmd.Scan0, red, CByte(rnd.Next(0, 256)))
Marshal.WriteByte(bmd.Scan0, blue, CByte(rnd.Next(0, 256)))
Marshal.WriteByte(bmd.Scan0, green, CByte(rnd.Next(0, 256)))
Marshal.WriteByte(bmd.Scan0, alpha, 255)
'Use the ReadInt32() method to read back the entire pixel
Dim intColor As Integer = Marshal.ReadInt32(bmd.Scan0)
If intColor = Color.Blue.ToArgb() Then
'The pixel is blue
Else
'The pixel is not blue
End If
Next
Next
'Important!
bm.UnlockBits(bmd)
Me.PictureBox1.Refresh()
End Sub

Related

VB.net crop an image (jpg) like ms paint with no quality loss

Given an jpg image slightly larger than 19" x 23" I need to crop it to exactly 19" x 23" and preserve the original quality using VB.NET.
I can do this in MS paint, If I open a 2851 x 4651 200 DPI jpg and use the Image Properties dialog I can change the width and Height to 3800 x 4600 (exactly 19" x 23" # 200 DPI).
The resultant image is identical to the original in quality and compression but is cropped on the right and bottom by the 51 pixels. The file size is slightly smaller as expected.
When I use the many techniques I have found on SO to crop/resize an image when I save the image it always saves as 96 DPI. I can adjust the width and height to accommodate the 96 DPI so the end result is exactly 19" x 23", however the resulting pixilation is higher than the original, and the files size is considerably smaller, so obvious quality loss.
What I want is to do is (a simple?) crop like MS paint does. Just take a little off the side and bottom, but I cannot seem to save an image with anything other than 96 DPI.
If I can figure out how to the save the cropped file at 200 DPI (or whatever the original image was) I think what I have will work fine.
I am willing to use an external library if that is what it takes.
Here is one example that works in the sense that the resulting image is 19" x 23" and the image is actually scaled preserving the aspect ratio, however the quality is less than the original.
This code is from another SO answer with some minor modifications.
Public Shared Function ResizeImage(SourceImage As Drawing.Image, TargetWidthIn As Decimal, TargetHeightIn As Decimal) As Drawing.Bitmap
'Dim TargetWidth As Integer = TargetWidthIn * SourceImage.HorizontalResolution
'Dim TargetHeight As Integer = TargetHeightIn * SourceImage.VerticalResolution
Dim TargetWidth As Integer = TargetWidthIn * 96
Dim TargetHeight As Integer = TargetHeightIn * 96
Dim bmSource = New Drawing.Bitmap(SourceImage)
Dim bmDest As New Drawing.Bitmap(TargetWidth, TargetHeight, Drawing.Imaging.PixelFormat.Format32bppArgb)
Dim nSourceAspectRatio = bmSource.Width / bmSource.Height
Dim nDestAspectRatio = bmDest.Width / bmDest.Height
Dim NewX = 0
Dim NewY = 0
Dim NewWidth = bmDest.Width
Dim NewHeight = bmDest.Height
If nDestAspectRatio = nSourceAspectRatio Then
'same ratio
ElseIf nDestAspectRatio > nSourceAspectRatio Then
'Source is taller
NewWidth = Convert.ToInt32(Math.Floor(nSourceAspectRatio * NewHeight))
NewX = Convert.ToInt32(Math.Floor((bmDest.Width - NewWidth) / 2))
Else
'Source is wider
NewHeight = Convert.ToInt32(Math.Floor((1 / nSourceAspectRatio) * NewWidth))
NewY = Convert.ToInt32(Math.Floor((bmDest.Height - NewHeight) / 2))
End If
Using grDest = Drawing.Graphics.FromImage(bmDest)
With grDest
.CompositingQuality = Drawing.Drawing2D.CompositingQuality.HighQuality
'.InterpolationMode = Drawing.Drawing2D.InterpolationMode.HighQualityBicubic
.InterpolationMode = Drawing.Drawing2D.InterpolationMode.NearestNeighbor
.PixelOffsetMode = Drawing.Drawing2D.PixelOffsetMode.HighQuality
.CompositingMode = Drawing.Drawing2D.CompositingMode.SourceCopy
'.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias
'.CompositingMode = Drawing.Drawing2D.CompositingMode.SourceOver
.DrawImage(bmSource, NewX, NewY, NewWidth, NewHeight)
End With
End Using
Return bmDest
End Function
I found a solution on SO here
I modifyed my original code above to keep the DPI of the original image:
Dim TargetWidth As Integer = TargetWidthIn * SourceImage.HorizontalResolution
Dim TargetHeight As Integer = TargetHeightIn * SourceImage.VerticalResolution
Then after the call to ResizeImage:
Select Case imageType.ToLower
Case "jpg"
Dim jpgEncoder As ImageCodecInfo = GetEncoder(ImageFormat.Jpeg)
Dim myEncoder As System.Drawing.Imaging.Encoder = System.Drawing.Imaging.Encoder.Quality
Dim myEncoderParams As New EncoderParameters(1)
Dim myEncoderQuality As New EncoderParameter(myEncoder, CType(98L, Int32)) '98%
myEncoderParams.Param(0) = myEncoderQuality
bm.SetResolution(img.HorizontalResolution, img.VerticalResolution)
bm.Save(tempfile, jpgEncoder, myEncoderParams)
Case "png", "gif"
bm.Save(tempfile, System.Drawing.Imaging.ImageFormat.Png)
Case "tiff", "tif"
bm.Save(tempfile, System.Drawing.Imaging.ImageFormat.Tiff)
Case Else
bm.Save(tempfile, System.Drawing.Imaging.ImageFormat.Png)
End Select
bm.Dispose()
I only use jpg now so I don't know if the tiff and png parts work, but it seems using the jpeg encoder allowed me to save the file with 200 DPI and maintain the original quality.
Here is the GetEncoder part that is missing from the other post:
Private Shared Function GetEncoder(f As Drawing.Imaging.ImageFormat) As ImageCodecInfo
Dim myEncoders() As ImageCodecInfo
myEncoders = ImageCodecInfo.GetImageEncoders()
Dim numEncoders As Integer = myEncoders.GetLength(0)
Dim strNumEncoders As String = numEncoders.ToString()
' Get the info. for all encoders in the array.
If numEncoders > 0 Then
Dim myEncoderInfo(numEncoders * 10) As String
For i As Integer = 0 To numEncoders - 1
If myEncoders(i).FilenameExtension.Contains(f.ToString.ToUpper) Then
Return myEncoders(i)
End If
Next
End If
Return Nothing

How to get rid of white space between picture boxes in VB.NET?

For fun I'm trying to recreate the first level of one my favorite games, Fire Emblem 7. I got a picture of the map online. I've broken down the image into "squares" with each square assigned a picture box to display the image. This is because each square needs to have certain properties such as terrain values, units inside them, etc.
The actual image is quite small (240 x 160), so I want to be able to scale it to any user defined value. The size of each square should be 16c x 16c with a scaler of c (all map dimensions are divisible by 16). For some reason, when c > 1, white lines appear between the squares. I've check the code and it looks like the squares should be adjacent with no empty spaces regardless of c.
I have provided a piece of my code and links to the images of different values of c below. Thank you for you help.
'This Sub Creates The Map From Initial Image And Assigns Part Of Image to Each Square
Public Sub New(Name As String, Image As Image)
Dim cropRect As Rectangle
Dim cropImage As Bitmap
Me.Name = Name
Me.Image = Image
Height = Me.Image.Height / 16
Width = Me.Image.Width / 16
ReDim Squares(Height - 1, Width - 1)
For i = 0 To Height - 1
For j = 0 To Width - 1
cropRect = New Rectangle(16 * j, 16 * i, 16, 16)
cropImage = New Bitmap(16, 16)
Graphics.FromImage(cropImage).DrawImage(Me.Image, 0, 0, cropRect, GraphicsUnit.Pixel)
Squares(i, j) = New Square(cropImage)
Next
Next
End Sub
'This Sub Sizes Each Square With User Defined Scale Value
Public Sub Draw(Scale As Double)
For i = 0 To Height - 1
For j = 0 To Width - 1
With Squares(i, j).Box
.Size = New Size(16 * Scale, 16 * Scale)
.Location = New Point(16 * j * Scale, 16 * i * Scale)
.SizeMode = PictureBoxSizeMode.StretchImage
End With
Next
Next
End Sub

VB Downscaling coordinates

I have the need to know when a gdi+ drawn line is clicked on by the mouse.
I have fashioned this function which is used in a loop on all the existing lines and what the function does is:
It makes a buffer of the line's container's size
It makes the whole thing black
It draws the line in green
It gets the pixel at the mouse location
If the pixel is different from black a.k.a green, the line has successfully been clicked and the function should then return true.
This works great, there's no misinterpretations, but I'm afraid that there's a tiny delay (not really noticeable) when my form is in full screen (due to the large buffer).
I'm looking for a way to optimize this, and my first thought is to downscale everything. So what I mean by that is make the buffer like 20x20 and then draw the line in a scaled down version using math. Problem is, I suck at math, so I'm basically asking you how to do this and preferably with an explanation for dummies.
This is the function:
Public Function Contains(ByVal e As Point) As Boolean
Dim Width As Integer = Container.Size.Width
Dim Height As Integer = Container.Size.Height
Dim Buffer As Bitmap = New Bitmap(Width, Height)
Using G As Graphics = Graphics.FromImage(Buffer)
G.Clear(Color.Black)
Dim Start As Point = New Point(ParentNode.Location.X + ParentNode.Size.Width / 2, ParentNode.Location.Y + ParentNode.Size.Height / 2)
Dim [End] As Point = New Point(ChildNode.Location.X + ChildNode.Size.Width / 2, ChildNode.Location.Y + ChildNode.Size.Height / 2)
Dim Control1 As Point
Dim Control2 As Point
Control1.X = Start.X + GetAngle(ChildNode.Location, ParentNode.Location, ChildNode.Location.X - ParentNode.Location.X, ChildNode.Location.Y - ParentNode.Location.Y)
Control1.Y = Start.Y
Control2.X = [End].X
Control2.Y = Start.Y
G.DrawBezier(New Pen(Color.Green, 4), Start, Control1, Control2, [End])
End Using
If Buffer.GetPixel(e.X, e.Y).ToArgb() <> Color.Black.ToArgb() Then
Return True
End If
Return False
End Function
This is one of my attempts to make the function use the idea above:
Public Function Contains(ByVal e As Point) As Boolean
Dim Width As Integer = 20
Dim Height As Integer = 20
Dim Buffer As Bitmap = New Bitmap(Width, Height)
Using G As Graphics = Graphics.FromImage(Buffer)
G.Clear(Color.Black)
Dim Start As Point = New Point(ParentNode.Location.X + ParentNode.Size.Width / 2, ParentNode.Location.Y + ParentNode.Size.Height / 2)
Dim [End] As Point = New Point(ChildNode.Location.X + ChildNode.Size.Width / 2, ChildNode.Location.Y + ChildNode.Size.Height / 2)
Dim Control1 As Point
Dim Control2 As Point
Control1.X = Start.X + GetAngle(ChildNode.Location, ParentNode.Location, ChildNode.Location.X - ParentNode.Location.X, ChildNode.Location.Y - ParentNode.Location.Y)
Control1.Y = Start.Y
Control2.X = [End].X
Control2.Y = Start.Y
G.DrawBezier(New Pen(Color.Green, 4), New Point(Start.X / Width, Start.Y / Height), New Point(Control1.X / Width, Control1.Height / Height), New Point(Control2.X / Width, Control2.Y / Height), New Point([End].X / Width, [End].Y / Height))
End Using
If Buffer.GetPixel(Width, Height).ToArgb() <> Color.Black.ToArgb() Then
Return True
End If
Return False
End Function
Try using a GraphicsPath for drawing and testing with the built-in IsOutlineVisible function:
Public Function Contains(ByVal e As Point) As Boolean
Dim result as Boolean = False
Using gp As New GraphicsPath
gp.AddBezier(your four points)
Using p As New Pen(Color.Empty, 4)
result = gp.IsOutlineVisible(e, p)
End Using
End Using
Return result
End Function
Side note: Bitmaps and Graphic objects need to be disposed when you create them.

Reading pixel value of an image

Q> If showing all the RGB pixel value of a 60*66 PNG image takes 10-34 seconds then how Image Viewer shows image instantly ?
Dim clr As Integer ' or string
Dim xmax As Integer
Dim ymax As Integer
Dim x As Integer
Dim y As Integer
Dim bm As New Bitmap(dlgOpen.FileName)
xmax = bm.Width - 1
ymax = bm.Height - 1
For y = 0 To ymax
For x = 0 To xmax
With bm.GetPixel(x, y)
clr = .R & .G & .B
txtValue.AppendText(clr)
End With
Next x
Next y
Edit
Dim bmp As New Bitmap(dlgOpen.FileName)
Dim rect As New Rectangle(0, 0, bmp.Width, bmp.Height)
Dim bmpData As System.Drawing.Imaging.BitmapData = bmp.LockBits(rect,Drawing.Imaging.ImageLockMode.ReadWrite, bmp.PixelFormat)
Dim ptr As IntPtr = bmpData.Scan0
Dim bytes As Integer = Math.Abs(bmpData.Stride) * bmp.Height
Dim rgbValues(bytes - 1) As Byte
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes)
For counter As Integer = 0 To rgbValues.Length - 1
txtValue.AppendText(rgbValues(counter))
Next
System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes)
bmp.UnlockBits(bmpData)
The first code takes 10 seconds and the 2nd one around 34 seconds for showing all the value in a textbox for a 59*66 PNG image on AMD A6 3500 with 4 GB RAM !
The problem exist when reading from file and writing to a textbox takes place in same time !
The problem is that the feature you're using, GetPixel, is very slow if you need to access a lot of pixels. Try using LockBits. You can use that to gather image data nearly instantly.
Using the LockBits method to access image data.

Visual Basic memory usage keeps expanding

Having a problem with the amount of memory being used going up and up, and expanding until there is no memory left. I'm using the GHeat.Net plugin to build images. Here is the full code:
Dim pm As New gheat.PointManager()
Dim g As Graphics
Dim startZoom As Integer = 2
Dim maxZoom As Integer = 17
gheat.Settings.BaseDirectory = "C:\\gheatWeb\\__\\etc\\"
pm.LoadPointsFromFile("C:\\points.txt")
For zoom As Integer = startZoom To maxZoom
Dim startX As Integer = 0
Dim startY As Integer = 0
Dim maxX As Integer = 2 ^ zoom
Dim maxY As Integer = 2 ^ zoom
For x As Integer = startX To maxX
For y As Integer = startY To maxY
Dim filename As String = "C:\\images\\" + zoom.ToString + "\\x" + x.ToString + "y" + y.ToString + "zoom" + zoom.ToString + ".gif"
gheat.GHeat.GetTile(pm, "classic", zoom, x, y).Save(filename, System.Drawing.Imaging.ImageFormat.Gif)
Next
Next
Next
For some reason, when I hit the for loops, the amount of memory used just goes up and up and up, until it hits a ceiling. Even then, the program keeps running, but the amount of memory doesn't go up. The program generates fine at 20Mb, so I can't figure out why it just keeps going up.
I've also tried GC.Collect at the end of the innermost loop, to no avail. Any ideas?
GHeat.GetTile returns a Bitmap which must be disposed.
Also, there's no need to escape paths like that in VB.
For x As Integer = startX To maxX
For y As Integer = startY To maxY
Dim filename As String = String.Format("C:\images\{0}\x{1}y{2}zoom{3}.gif", zoom, x, y, zoom)
Using img = gheat.GHeat.GetTile(pm, "classic", zoom, x, y)
img.Save(filename, System.Drawing.Imaging.ImageFormat.Gif)
End Using
Next
Next