Hysteresis thresholding in visual basic - vb.net

I'm trying to make a hysteresis thresholding in visual basic for canny edge detection. As I'm new into this topic and new to vb (I mainly use php), many of the references I read,point out that I need to do this. Since the image is already on black and white, I only get 1 color for the intensities. I already did the gaussian blur, grayscale, sobel mask, and non maxima supression in only seconds. but on hysteresis, the time to execute the function is taking too long. I don't know where I did wrong. The image is on 640 x 480 if that helps. I try to change the resolution to smaller one, it's indeed faster but I want to keep the resolution to 640 x 480. I already change the code, and this is my final approach
Dim bmp_thres As New Bitmap(pic_nonmaxima)
Dim visited_maps As New List(Of String)
Dim threshold_H As Integer = 100
Dim threshold_L As Integer = 50
Dim Ycount As Integer
For Ycount = 1 To bmp_thres.Height - 2
Dim Xcount As Integer
For Xcount = 1 To bmp_thres.Width - 2
'check current pointer
Dim currPointer As String = Xcount & "," & Ycount
'find if coordinate visited already
Dim find_array As String
If visited_maps IsNot Nothing Then
find_array = visited_maps.Contains(currPointer)
Else
find_array = "False"
End If
If find_array Then
'if existed, do nothing
Else
'if not, do something
Dim currThreshold As Integer
Dim currColor As Color
currColor = bmp_thres.GetPixel(Xcount, Ycount)
currThreshold = currColor.R
'add coordinate into visited maps
Dim visited As String = Xcount & "" & Ycount
visited_maps.Add(visited)
If currThreshold > threshold_H Then
bmp_thres.SetPixel(Xcount, Ycount, Color.FromArgb(255, 255, 255))
Else
bmp_thres.SetPixel(Xcount, Ycount, Color.FromArgb(0, 0, 0))
'check connectedness
Dim coord_N As String = Xcount & "," & Ycount + 1
Dim coord_E As String = Xcount + 1 & "," & Ycount
Dim coord_S As String = Xcount & "," & Ycount - 1
Dim coord_W As String = Xcount - 1 & "," & Ycount
Dim coord_NE As String = Xcount + 1 & "," & Ycount + 1
Dim coord_SE As String = Xcount + 1 & "," & Ycount - 1
Dim coord_SW As String = Xcount - 1 & "," & Ycount - 1
Dim coord_NW As String = Xcount - 1 & "," & Ycount + 1
Dim myCoord As New List(Of String)
myCoord.Add(coord_N)
myCoord.Add(coord_E)
myCoord.Add(coord_S)
myCoord.Add(coord_W)
myCoord.Add(coord_NE)
myCoord.Add(coord_SE)
myCoord.Add(coord_SW)
myCoord.Add(coord_NW)
For Each coord In myCoord
If Not visited_maps.Contains(coord) Then
'Split by ,
Dim split_Coord() As String = Split(coord, ",")
'check thres on coord
Dim coordColor As Color = bmp_thres.GetPixel(split_Coord(0), split_Coord(1))
Dim coordThres As Integer = coordColor.R
If coordThres > threshold_H Then
bmp_thres.SetPixel(split_Coord(0), split_Coord(1), Color.FromArgb(255, 255, 255))
Else
bmp_thres.SetPixel(split_Coord(0), split_Coord(1), Color.FromArgb(0, 0, 0))
End If
End If
visited_maps.Add(coord)
Next 'end if foreach
End If ' end if checking current threshold
End If 'end if find coord in visited maps
Next 'end for xcount
Next 'end for ycount
Return bmp_thres
Or if you spot some wrong codes I did, please point out to me.
If I get it right, when we do hysteresis thresholding, we first check the coordinate if it's visited already, if it's visited we check the next coordinate. if it's not, we add the coordinate into the visited maps and if the current coordinate is larger than the threshold high, we change the pixel value into white else black. then we check the connectedness, if they pass the threshold low, we change the pixel value into white else black. then we add all the connectedness into visited maps. repeat.
What can I do to reduce the time ? or please point out my mistake. any help will be appreciated. sorry for the english if you didnt understand. this will help my final year project T_T

I think this could be on topic at Code Review as it is working (albeit slowly). But just in case it isn't, I'm leaving this here as a partial answer.
You have a slight bug in your coordinate search. Regardless of whether it is already in visited_maps or not you still add it in, which will be a lot of extra results in the list.
If Not visited_maps.Contains(coord) Then
' YOUR CODE
End If
visited_maps.Add(coord)
This line: visited_maps.Add(coord) needs to be inside the If so you don't have repeat values expanding your list further than it needs to. A 640 * 480 px image will create over 300 000 entries into your list, and with this coordinate bug it will have even more.
List is probably also not the most appropriate type, something like a HashSet is better because you don't need to access by index. Have a look at What is the difference between HashSet and List?
When you call Color.FromArgb(255, 255, 255) you are creating a new Color object every time. That's going to be at least 300 000 objects again, when you could declare one instance for black and another for white at the top and then use those as needed.
I'm not sure what the performance difference of using the Point structure over a comma-separated string would be, but it would save a lot of splitting/concatenation and be much nicer to read.
Dim currPointer As String = Xcount & "," & Ycount
Dim coord_N As String = Xcount & "," & Ycount + 1
Would become
Dim currPointer As Point = New Point(Xcount, Ycount)
Dim coord_N as Point = New Point(currPointer.X, currPointer.Y + 1)
There are still more things wrong but they are fairly minor so I'll leave them off for now

Related

Application.Match not exact value

Have a piece of code that looks for matches between 2 sheets (sheet1 is customer list and rData is copied pdf with invoices). It usually is exact match but in some cases I'm looking for 6 first characters that matches rData
Dim rData As Variant
Dim r As Variant
Dim r20 As Variant
Dim result As Variant
Dim i As Long
rData = ActiveWorkbook.Sheets(2).Range("A1:A60000")
r20 = ActiveWorkbook.Sheets(1).Range("C2:C33")
For Each r In r20
result = Application.Match(r, rData, 0)
If Not IsError(result) Then
For i = 1 To 5
If (result - i) > 0 Then
If (Left(Trim(rData(result - i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result - i, 1)
End If
End If
Next
For i = 1 To 15
If (result + i) > 0 Then
If (Left(Trim(rData(result + i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result + i, 1)
End If
End If
Next
End If
Next r
End Sub
Only part of this that is giving me a headache is this part result = Application.Match(r, rData, 0). How do it get match for not exact match?
Sample of Sheet1
This is what more or less looks like. Matching after CustomerNumber# is easy because they are the same every invoice. BUT sometimes invoice does not have it so I'm searching after CustomerName and sometimes they have uppercase letters, sometimes there is extra stuff behind it and therefore it cannot find exact match.
Hope it makes sense.
To match the customer name from your customer list to the customer name in the invoice even if it has extra characters appended, you can use the wildcard * in Match().
You also have a typo in the Match() function. r20 should be rData.
This is your code with the fixes applied:
Sub Test()
'v4
Dim rData As Variant
Dim r As Variant
Dim r20 As Variant
Dim result As Variant
Dim i As Long
rData = ActiveWorkbook.Sheets(2).Range("A1:A60000")
r20 = ActiveWorkbook.Sheets(1).Range("C2:C33")
For Each r In r20
result = Application.Match(r & "*", rData, 0) ' <~ Fixed here
If Not IsError(result) Then
For i = 1 To 5
If (result - i) > 0 Then
If (Left(Trim(rData(result - i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result - i, 1)
End If
End If
Next
For i = 1 To 15
If (result + i) > 0 Then
If (Left(Trim(rData(result + i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result + i, 1)
End If
End If
Next
End If
Next r
End Sub
Notes:
Match() is case insensitive, so it works with different capitalisations.
The data in Sheets(2) must all be text for Match() to work correctly with wildcards.
EDIT1: New better version
EDIT2: Refactored constants and made data ranges dynamic
EDIT3: Allows for any prefix to an invoice number of a fixed length
The following is a better, rewritten version of your code:
Sub MuchBetter()
'v3
Const s_InvoiceDataWorksheet As String = "Sheet2"
Const s_InvoiceDataColumn As String = "A:A"
Const s_CustomerWorksheet As String = "Sheet1"
Const s_CustomerStartCell As String = "C2"
Const s_InvoiceNumPrefix As String = "418"
Const n_InvoiceNumLength As Long = 8
Const n_InvScanStartOffset As Long = -5
Const n_InvScanEndOffset As Long = 15
Dim ƒ As Excel.WorksheetFunction: Set ƒ = Excel.WorksheetFunction ' Shortcut
With Worksheets(s_InvoiceDataWorksheet).Range(s_InvoiceDataColumn)
With .Parent.Range(.Cells(1), .Cells(Cells.Rows.Count).End(xlUp))
Dim varInvoiceDataArray As Variant
varInvoiceDataArray = ƒ.Transpose(.Cells.Value2)
End With
End With
With Worksheets(s_CustomerWorksheet).Range(s_CustomerStartCell)
With .Parent.Range(.Cells(1), .EntireColumn.Cells(Cells.Rows.Count).End(xlUp))
Dim varCustomerArray As Variant
varCustomerArray = ƒ.Transpose(.Cells.Value2)
End With
End With
Dim varCustomer As Variant
For Each varCustomer In varCustomerArray
Dim dblCustomerIndex As Double
dblCustomerIndex = Application.Match(varCustomer & "*", varInvoiceDataArray, 0)
If Not IsError(dblCustomerIndex) _
And varCustomer <> vbNullString _
Then
Dim i As Long
For i = ƒ.Max(dblCustomerIndex + n_InvScanStartOffset, 1) _
To ƒ.Min(dblCustomerIndex + n_InvScanEndOffset, UBound(varInvoiceDataArray))
Dim strInvoiceNum As String
strInvoiceNum = Right$(Trim$(varInvoiceDataArray(i)), n_InvoiceNumLength)
If (Left$(strInvoiceNum, Len(s_InvoiceNumPrefix)) = s_InvoiceNumPrefix) Then
MsgBox "customer: " & varCustomer & ". invoice: " & strInvoiceNum
End If
Next
End If
Next varCustomer
End Sub
Notes:
It is a good idea to use constants so all literal values are typed once only and kept grouped together.
Using the RVBA naming convention greatly increases the readability of the code, and reduces the likelihood of bugs.
Using long, appropriately named variables makes the code essentially self-documenting.
Using .Value2 whenever reading cell values is highly recommended (it avoids implicit casting, making it slightly faster as well as eliminating certain issues caused by the casting ).
Surprisingly, in VBA there are good reasons to put a variable declaration as close as possible to the first use of the variable. Two such reasons are 1) it improves readability, and 2) it simplifies future refactoring. Just remember that the variable is not reinitialised every time the Dim is encountered. Initialisation only occurs the first time.
The twin loops have been rolled into one according to the DRY principle.
Whilst the check for an empty customer name/number is not strictly necessary if you can guarantee it will never be so, it is good defensive programming as an empty value will cause erroneous results.
The negative index check inside the loop has been removed and replaced with the one-time use of the Max() worksheet function in the For statement.
The Min() worksheet function is also used in the For statement to avoid trying to read past the end of the array.
Always use worksheet functions on the WorksheetFunction object unless you are explicitly checking for errors, in which case use the Application object.

Excel VBA-Getting a blank message box with data derrived from an array

I am getting a blank message box for an array which should automatically have and display the following values:
00:00:00
01:00:00
02:00:00
and so on...
Here is my code
Dim i As Integer
i = 0
Dim sampleArr(0 To 24) As Variant
Dim a As Integer
a = 0
Do Until i > 23
sampleArr(a) = i & ":00:00"
a = a + 1
i = i + 1
MsgBox sampleArr(a)
Loop
Please tell me what's wrong with this code
You update the value of sampleArr(a), then increment a. So to get the just-updated value you need to use the pre-incremented value: a-1.
MsgBox sampleArr(a-1)
Put the Msgbox first before you increment a and i.
MsgBox sampleArr(a)
a = a + 1
i = i + 1
It's not entirely clear what you're trying to achieve here, (especially with a and i being identical. Presumably the msgbox is only actually in there to prove you've created the array correctly and will be removed later?
That said, as everyone is pointing out, you're incrementing your pointer before displaying the entry. The simplest way to fix that is to put the display line in immediately after creating the element.
I've also formatted i in order to produce the exact output you've requested.
Also, I suspect your array only needs to go 0 To 23 if this is some kind of time selector?
So, fixing your issue looks like:
Dim i As Integer
i = 0
Dim sampleArr(0 To 23) As Variant
Dim a As Integer
a = 0
Do Until i > 23
sampleArr(a) = Format(i, "00") & ":00:00"
MsgBox sampleArr(a)
a = a + 1
i = i + 1
Loop
However, you could just do the following:
Dim i As Integer
Dim sampleArr(0 To 23) As Variant
For i = 0 To 23
sampleArr(a) = Format(i, "00") & ":00:00"
MsgBox sampleArr(a)
Next
Beyond this, if you want to store the values in the array as TIME rather than a text representation of the time (useful for calculations etc.) then replace the sampleArr line with
sampleArr(a) = TimeSerial(i, 0, 0)

VBA - check for duplicates while filling cells through a loop

I am writing a VBA code that goes through a defined matrix size and filling cells randomly within its limits.
I got the code here from a user on stackoverflow, but after testing it I realized that it does not fit for avoiding duplicate filling, and for instance when filling 5 cells, I could only see 4 cells filled, meaning that the random filling worked on a previously filled cell.
This is the code I'm working with:
Dim lRandom As Long
Dim sCells As String
Dim sRandom As String
Dim rMolecules As Range
Dim i As Integer, j As Integer
Dim lArea As Long
lArea = 400 '20x20
'Populate string of cells that make up the container so they can be chosen at random
For i = 1 To 20
For j = 1 To 20
sCells = sCells & "|" & Cells(i, j).Address
Next j
Next i
sCells = sCells & "|"
'Color the molecules at random
For i = 1 To WorksheetFunction.Min(5, lArea)
Randomize
lRandom = Int(Rnd() * 400) + 1
sRandom = Split(sCells, "|")(lRandom)
Select Case (i = 1)
Case True: Set rMolecules = Range(sRandom)
Case Else: Set rMolecules = Union(rMolecules, Range(Split(sCells, "|")(lRandom)))
End Select
sCells = Replace(sCells, "|" & sRandom & "|", "|")
lArea = lArea - 1
Next i
rMolecules.Interior.ColorIndex = 5
Using this same exact code which works perfectly, WHAT can I insert and WHERE do I do that so that the code would check if a cell is previously already filled with a string or a color?
I feel as though this code I'm looking for should be right before
rMolecules.Interior.ColorIndex = 5
But I'm not sure what to type.
EDIT
From the comments I realized that I should be more specific.
I am trying to randomly fill cells with the blue color (.ColorIndex = 5), but what I need to check first is if the randomizing hadn't marked a cell twice, so that for instance in this case, if I want to mark 5 different cells, it marks only 4 of them because of a duplicate and thus fills only 4 cells with the blue color. I need to avoid that and make it choose another cell to mark/fill.
I'd appreciate your help.
Keep the cells you use in a Collection and remove them as you fill the random cells:
Sub FillRandomCells(targetRange As Range, numberOfCells As Long)
' populate collection of unique cells
Dim c As Range
Dim targetCells As New Collection
' make sure arguments make sense
If numberOfCells > targetRange.Cells.Count Then
Err.Raise vbObjectError, "FillRandomCells()", _
"Number of cells to be changed can not exceed number of cells in range"
End If
For Each c In targetRange.Cells
targetCells.Add c
Next
' now pick random 5
Dim i As Long, randomIndex As Long
Dim upperbound As Long
Dim lowerbound As Long
For i = 1 To numberOfCells
lowerbound = 1 ' collections start with 1
upperbound = targetCells.Count ' changes as we are removing cells we used
randomIndex = Int((upperbound - lowerbound + 1) * Rnd + lowerbound)
Set c = targetCells(randomIndex)
targetCells.Remove randomIndex ' remove so we don't use it again!
c.Interior.Color = 5 ' do what you need to do here
Next
End Sub
Sub testFillRandomCells()
FillRandomCells ActiveSheet.[a1:t20], 5
FillRandomCells ActiveSheet.[b25:f30], 3
End Sub
EDIT: Changed to make the target range and number of changed cells configurable as arguments to a function. Also added error checking (always do that!).
Why not build a list of random numbers and place in a Scripting.Dictionary, one can use the Dictionary's Exist method to detect duplicates, loop through until you have enough then you can enter your colouring code confident that you have a unique list.

Progress bar in Status bar, a blank and a filled in char are not equal width

I am playing around creating code for a progress bar that runs in the Excel Status Bar. I want to replace my old dated userform with the 2 rectangles (which worked but I would sooner a less obtrusive method now).
Problem: The width of the chars I am using to signify "Filled In" and "Not Filled in" are slightly different, when using 100 of them you can see the percentage at the end appears to shift right as the progress increases.
Here is some working sample code to show you exactly what I mean:
Sub TestNewProgBar()
Dim X As Long
For X = 1 To 100000
Call NewProgressBar("Testing", X, 100000)
Next
End Sub
Sub NewProgressBar(MyMessage As String, CurrentVal As Long, MaxVal As Long)
Dim FilledIn As Long, NotFilledIn As Long
If CurrentVal >= MaxVal Then
Application.StatusBar = MyMessage & ": Complete"
Else
FilledIn = Round((CurrentVal / MaxVal) * 100, 0)
NotFilledIn = (100 - FilledIn)
Application.StatusBar = MyMessage & ": " & Application.WorksheetFunction.Rept(ChrW(9608), FilledIn) & Application.WorksheetFunction.Rept(ChrW(9620), NotFilledIn) & "| " & FilledIn & "%"
End If
End Sub
Run TestNewProgBar and look at the status bar.
Is this going to be a simple case of choosing a different Unicode symbol or are there forces beyond my control at work here?
There's a Unicode block from U+25A0 to U+25FF called Geometric Shapes. There are some matching pairs of black/ white shapes in there that will successfully work for your progress bar implementation.
In the test code below, some pairs work and some do not!. Personally I like the last example (pairing U+25AE and U+25AD).
Option Explicit
Sub TestNewProgBar()
Dim lngCounter As Long
Dim lngMax As Long
Dim strFilledChar As String
Dim strNotFilledChar As String
'iterations
lngMax = 100000
'small squares - works
strFilledChar = ChrW(&H25AA)
strNotFilledChar = ChrW(&H25AB)
For lngCounter = 1 To lngMax
Call NewProgressBar("Small squares", lngCounter, lngMax, strFilledChar, strNotFilledChar)
Next
'large squares - doesn't work
strFilledChar = ChrW(&H25A0)
strNotFilledChar = ChrW(&H25A1)
For lngCounter = 1 To lngMax
Call NewProgressBar("Large squares", lngCounter, lngMax, strFilledChar, strNotFilledChar)
Next
'large squares 2 - doesn't work (but opposite effect)
strFilledChar = ChrW(&H25A3)
strNotFilledChar = ChrW(&H25A1)
For lngCounter = 1 To lngMax
Call NewProgressBar("Large squares 2", lngCounter, lngMax, strFilledChar, strNotFilledChar)
Next
'mixed vertical/ horizontal rectangles - works!
strFilledChar = ChrW(&H25AE)
strNotFilledChar = ChrW(&H25AD)
For lngCounter = 1 To lngMax
Call NewProgressBar("Mixed rectangles", lngCounter, lngMax, strFilledChar, strNotFilledChar)
Next
End Sub
Sub NewProgressBar(strMyMessage As String, lngCurrentVal As Long, lngMaxVal As Long, strFilledChar As String, strNotFilledChar As String)
Dim lngFilledIn As Long
Dim lngNotFilledIn As Long
Dim strStatus As String
If lngCurrentVal >= lngMaxVal Then
Application.StatusBar = strMyMessage & ": Complete"
Else
lngFilledIn = Round((lngCurrentVal / lngMaxVal) * 100, 0)
lngNotFilledIn = (100 - lngFilledIn)
strStatus = strMyMessage & ": " & _
String(lngFilledIn, strFilledChar) & _
String(lngNotFilledIn, strNotFilledChar) & _
"| " & lngFilledIn & "%"
Application.StatusBar = strStatus
End If
End Sub
Edit:
To follow up on my 'aside' below, I did some experimenting, and Comintern was onto something when s/he provided a link to this issue. The problem described above is to do with ScreenUpdating. If Screenupdating is set to false when the status bar is changed, the character widths of ChrW(9608) and ChrW(9620) are the same.
I've no idea why, but it does the trick. So you'll want to do the following:
Application.Screenupdating = False
'code which changes the status bar
Application.Screenupdating = True
(my previous comment continues below)
I prefer this pairing:
strFilledChar = ChrW(&H2588) 'a black rectangle, "Full Block"
strUnfilledChar = ChrW(&H2584) 'an array of sparse dots, "Light Shade"
Or this one:
strFilledChar = ChrW(&H2588) 'a black rectangle, "Full Block"
strUnfilledChar = ChrW(&H2500) 'a horizontal line, "Block Drawings Light Horizontal"
(as an aside, I encountered the same problem as described in the question whereby ChrW(9608) and ChrW(9620) have different widths - but only in one of my workbooks. In another workbook, they have the same widths and so the progress bar displays properly. I have no idea why.)

for loop : string & number without keep adding &

I'm learning for loop and I cannot get this problem fixed.
The problems are in the following codes.
dim rt as integer = 2
dim i As Integer = 0
dim currentpg as string = "http://homepg.com/"
For i = 0 To rt
currentpg = currentpg & "?pg=" & i
messagebox.show(currentpg)
next
'I hoped to get the following results
http://homepg.com/?pg=0
http://homepg.com/?pg=1
http://homepg.com/?pg=2
'but instead I'm getting this
http://homepg.com/?pg=0
http://homepg.com/?pg=0?pg=0
http://homepg.com/?pg=0?pg=0?pg=0
Please help me
Thank you.
You probably need something like this:
Dim basepg as string = "http://homepg.com/"
For i = 0 To rt
Dim currentpg As String = basepg & "?pg=" & i
messagebox.show(currentpg)
Next
Although a proper approach would be to accumulate results into a List(Of String), and then display in a messagebox once (or a textbox/file, if too many results). You don't want to bug user for every URL (what if there are 100 of them?). They would get tired of clicking OK.
First of all, you went wrong while copying the output of the buggy code. Here is the real one.
http://homepg.com/?pg=0
http://homepg.com/?pg=0?pg=1
http://homepg.com/?pg=0?pg=1?pg=2
It does not work because currentpg should be a constant but it is changed on each iteration.
Do not set, just get.
MessageBox.Show(currentpg & "?pg=" & i)
Or you can use another variable to make it more readable.
Dim newpg As String = currentpg & "?pg=" & i
MessageBox.Show(newpg)
Also, your code is inefficient. I suggest you to change it like this.
Dim iterations As Integer = 2
Dim prefix As String = "http://homepg.com/?pg="
For index As Integer = 0 To iterations
MessageBox.Show(prefix & index)
Next