Skip to content

Commit 4286395

Browse files
committed
Adjustments > Monochrome: switch to Otsu's method for calculating "ideal" threshold
https://en.wikipedia.org/wiki/Otsu%27s_method With extra thanks to http://www.labbookpages.co.uk/software/imgProc/otsuThreshold.html for an optimized implementation!
1 parent c5e0485 commit 4286395

2 files changed

Lines changed: 75 additions & 25 deletions

File tree

Forms/Adjustments_BlackAndWhite.frm

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

158159
Option Explicit
159160

161+
'Used to avoid recursive changes
162+
Dim m_AutoActive As Boolean
163+
160164
Private Sub btsTransparency_Click(ByVal buttonIndex As Long)
161165
UpdatePreview
162166
End Sub
163167

164168
Private Sub cboDither_Click()
165-
If chkAutoThreshold.Value Then sltThreshold.Value = CalculateOptimalThreshold()
166169
sldDitheringAmount.Visible = (cboDither.ListIndex <> 0)
167170
UpdatePreview
168171
End 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
171175
Private 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+
177186
End Sub
178187

179188
'OK button
@@ -202,8 +211,10 @@ Private Sub cmdBar_ResetClick()
202211
End Sub
203212

204213
Private 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+
216229
End Function
217230

218231
Private Sub csMono_ColorChanged(Index As Integer)
@@ -242,7 +255,7 @@ Private Sub Form_Unload(Cancel As Integer)
242255
ReleaseFormTheming Me
243256
End 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)
246259
Private 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

308355
End 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()
652699
End Sub
653700

654701
Private Sub sltThreshold_Change()
655-
UpdatePreview
702+
If (Not m_AutoActive) Then
703+
chkAutoThreshold.Value = False
704+
UpdatePreview
705+
End If
656706
End Sub
657707

658708
Private Sub UpdatePreview()

PhotoDemon.vbp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ Description="PhotoDemon Photo Editor"
478478
CompatibleMode="0"
479479
MajorVer=8
480480
MinorVer=9
481-
RevisionVer=686
481+
RevisionVer=687
482482
AutoIncrementVer=1
483483
ServerSupportFiles=0
484484
VersionComments="Copyright 2000-2021 Tanner Helland - photodemon.org"

0 commit comments

Comments
 (0)