@@ -144,8 +144,9 @@ Attribute VB_Exposed = False
144144'Monochrome Conversion Form
145145'Copyright 2002-2021 by Tanner Helland
146146'Created: some time 2002
147- 'Last updated: 02/April/18
148- 'Last update: add "single neighbor" as a dithering option
147+ 'Last updated: 07/September/21
148+ 'Last update: change "auto threshold" calculation to use Otsu's method (https://en.wikipedia.org/wiki/Otsu%27s_method);
149+ ' this provides better results with no meaningful change to calculation time or complexity
149150'
150151'The meat of this form is in the module with the same name...look there for
151152' real algorithm info.
@@ -157,23 +158,31 @@ Attribute VB_Exposed = False
157158
158159Option Explicit
159160
161+ 'Used to avoid recursive changes
162+ Dim m_AutoActive As Boolean
163+
160164Private Sub btsTransparency_Click (ByVal buttonIndex As Long )
161165 UpdatePreview
162166End Sub
163167
164168Private Sub cboDither_Click ()
165- If chkAutoThreshold.Value Then sltThreshold.Value = CalculateOptimalThreshold()
166169 sldDitheringAmount.Visible = (cboDither.ListIndex <> 0 )
167170 UpdatePreview
168171End Sub
169172
170- 'When the auto threshold button is clicked, disable the scroll bar and text box and calculate the optimal value immediately
173+ 'When the auto threshold button is clicked, calculate the optimal value immediately and set the
174+ ' threshold slider to match whatever value we calculate
171175Private Sub chkAutoThreshold_Click ()
176+
177+ m_AutoActive = True
172178 cmdBar.SetPreviewStatus False
179+
173180 If chkAutoThreshold.Value Then sltThreshold.Value = CalculateOptimalThreshold()
174- sltThreshold.Enabled = Not chkAutoThreshold.Value
181+
182+ m_AutoActive = False
175183 cmdBar.SetPreviewStatus True
176184 UpdatePreview
185+
177186End Sub
178187
179188'OK button
@@ -202,8 +211,10 @@ Private Sub cmdBar_ResetClick()
202211End Sub
203212
204213Private Function GetFunctionParamString () As String
214+
205215 Dim cParams As pdSerialize
206216 Set cParams = New pdSerialize
217+
207218 With cParams
208219 .AddParam "threshold" , sltThreshold.Value
209220 .AddParam "dither" , cboDither.ListIndex
@@ -212,7 +223,9 @@ Private Function GetFunctionParamString() As String
212223 .AddParam "color2" , csMono(1 ).Color
213224 .AddParam "removetransparency" , (btsTransparency.ListIndex = 1 )
214225 End With
226+
215227 GetFunctionParamString = cParams.GetParamString
228+
216229End Function
217230
218231Private Sub csMono_ColorChanged (Index As Integer )
@@ -242,7 +255,7 @@ Private Sub Form_Unload(Cancel As Integer)
242255 ReleaseFormTheming Me
243256End Sub
244257
245- 'Calculate the optimal threshold for the current image
258+ 'Calculate the optimal threshold for the current image (using Otsu's method - https://en.wikipedia.org/wiki/Otsu%27s_method)
246259Private Function CalculateOptimalThreshold () As Long
247260
248261 'Create a local array and point it at the pixel data of the image
@@ -285,25 +298,59 @@ Private Function CalculateOptimalThreshold() As Long
285298 'Safely deallocate imageData() and free the target DIB (as it's no longer needed)
286299 workingDIB.UnwrapArrayFromDIB imageData
287300 Set workingDIB = Nothing
288-
289- 'We want to find the midpoint of the current histogram; divide the number of pixels by two
290- numOfPixels = numOfPixels \ 2
291301
292- Dim pixelCount As Long
293- pixelCount = 0
294- x = 0
295-
296- 'Loop through the histogram table until we have moved past half the pixels in the image
297- Do
298- pixelCount = pixelCount + lLookup(x)
299- x = x + 1
300- Loop While pixelCount < numOfPixels
302+ 'Next, use Otsu's method for finding the ideal threshold value.
303+ ' Thank you to https://en.wikipedia.org/wiki/Otsu%27s_method
304+ ' and http://www.labbookpages.co.uk/software/imgProc/otsuThreshold.html
305+ ' for a nice breakdown of how Otsu thresholding can be efficiently implemented.
306+ Dim hSum As Double
307+ For x = 0 To 255
308+ hSum = hSum + CDbl(x) * CDbl(lLookup(x))
309+ Next x
310+
311+ Dim sumB As Double
312+ Dim wB As Long , wF As Long '"Background" and "Foreground"
301313
302- 'Make sure our suggestion doesn't go too high; 220 is an arbitrarily selected value that's
303- ' near-white but not actually white
304- If (x > 254 ) Then x = 220
314+ Dim varMax As Double
315+ CalculateOptimalThreshold = 0
305316
306- CalculateOptimalThreshold = x
317+ For x = 0 To 255
318+
319+ 'Update background tracker
320+ wB = wB + lLookup(x)
321+ If (wB > 0 ) Then
322+
323+ 'Update foreground tracker
324+ wF = numOfPixels - wB
325+ If (wF > 0 ) Then 'Required to avoid DBZ
326+
327+ 'Sum of histogram to this point
328+ sumB = sumB + CDbl(x) * CDbl(lLookup(x))
329+
330+ 'Calculate background and foreground variance
331+ Dim mB As Double , mF As Double
332+ mB = sumB / wB
333+ mF = (hSum - sumB) / wF
334+
335+ 'Calculate between-class variance; this can be proven (algebraically) to correlate
336+ ' with within-class variance; see the above links for details
337+ Dim varBetween As Double
338+ varBetween = CDbl(wB) * CDbl(wF) * (mB - mF) * (mB - mF)
339+
340+ 'Look for new max
341+ If (varBetween > varMax) Then
342+ varMax = varBetween
343+ CalculateOptimalThreshold = x
344+ Debug.Print varMax, x
345+ End If
346+
347+ 'wF = 0
348+ End If
349+
350+ 'wB = 0
351+ End If
352+
353+ Next x
307354
308355End Function
309356
@@ -336,7 +383,7 @@ Public Sub MasterBlackWhiteConversion(ByVal monochromeParams As String, Optional
336383
337384 'If the user wants transparency removed from the image, apply that change prior to monochrome conversion
338385 Dim alphaAlreadyPremultiplied As Boolean : alphaAlreadyPremultiplied = False
339- If (removeTransparency And (curDIBValues.BytesPerPixel = 4 )) Then
386+ If (removeTransparency And (curDIBValues.bytesPerPixel = 4 )) Then
340387 EffectPrep.PrepImageData tmpSA, toPreview, dstPic, , , True
341388 workingDIB.CompositeBackgroundColor 255 , 255 , 255
342389 alphaAlreadyPremultiplied = True
@@ -652,7 +699,10 @@ Private Sub sldDitheringAmount_Change()
652699End Sub
653700
654701Private Sub sltThreshold_Change ()
655- UpdatePreview
702+ If (Not m_AutoActive) Then
703+ chkAutoThreshold.Value = False
704+ UpdatePreview
705+ End If
656706End Sub
657707
658708Private Sub UpdatePreview ()
0 commit comments