├── .gitignore ├── README.md ├── example.py ├── images ├── automatic_threshold.png ├── different_levels.png ├── soil.jpg └── threshold_values_histogram.png ├── otsu.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Improved two-stage multithreshold Otsu method. 2 | 3 | This is an implementation of the method described in: 4 | 5 | ``` 6 | Huang, D. Y., Lin, T. W., & Hu, W. C. (2011). 7 | Automatic multilevel thresholding based on two-stage Otsu’s method with cluster determination by valley estimation. 8 | International Journal of Innovative Computing, Information and Control, 7(10), 56315644. 9 | ``` 10 | 11 | ## Results 12 | ### Select number of thresholds automatically 13 | ![Automatic Thresholding](images/automatic_threshold.png) 14 | ### Selecting top X threshold values from output (1, 3, auto) 15 | ![Different Levels](images/different_levels.png) 16 | ### Threshold values displayed on histogram, black line is value from vanilla Otsu's method 17 | ![Thresh Values on Histogram](images/threshold_values_histogram.png) 18 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import otsu 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | 6 | 7 | def show_thresholds(src_img, dst_img, thresholds): 8 | """Visualise thresholds.""" 9 | colors = [(255, 0, 0), (255, 128, 0), (255, 255, 0), (0, 128, 0), (0, 204, 102), 10 | (51, 255, 255), (0, 128, 255), (0, 0, 255), (128, 0, 255), (255, 0, 255), (255, 0, 127)] 11 | masks = otsu.multithreshold(src_img, thresholds) 12 | for i, mask in enumerate(masks): 13 | # for debugging masks 14 | # background = np.zeros((dst_img.shape[0], dst_img.shape[1], 3)) 15 | # background[mask] = (255, 255, 255) 16 | # plt.figure() 17 | # plt.imshow(background) 18 | dst_img[mask] = colors[i] 19 | return dst_img 20 | 21 | 22 | def full_method(img, L=256, M=32): 23 | """Obtain thresholds and image masks directly from image. 24 | 25 | Abstracts away the need to handle histogram generation. 26 | """ 27 | # Calculate histogram 28 | hist = cv2.calcHist( 29 | [img], 30 | channels=[0], 31 | mask=None, 32 | histSize=[L], 33 | ranges=[0, L] 34 | ) 35 | 36 | thresholds = otsu.modified_TSMO(hist, M=M, L=L) 37 | masks = otsu.multithreshold(img, thresholds) 38 | return thresholds, masks 39 | 40 | 41 | def main(path='images/soil.jpg'): 42 | L = 256 # number of levels 43 | M = 32 # number of bins for bin-grouping normalisation 44 | 45 | N = L // M 46 | # Load original image 47 | img = cv2.imread(path, 0) # read image in as grayscale 48 | 49 | # Blur image to denoise 50 | img = cv2.GaussianBlur(img, (5, 5), 0) 51 | 52 | # Calculate histogram 53 | hist = cv2.calcHist( 54 | [img], 55 | channels=[0], 56 | mask=None, 57 | histSize=[L], 58 | ranges=[0, L] 59 | ) 60 | 61 | ### Do modified TSMO step by step 62 | # Normalise bin histogram 63 | norm_hist = otsu.normalised_histogram_binning(hist, M=M, L=L) 64 | 65 | # Estimate valley regions 66 | valleys = otsu.find_valleys(norm_hist) 67 | # plot valleys 68 | plt.figure() 69 | plt.bar(range(0, norm_hist.shape[0]), norm_hist.ravel()) 70 | for v in valleys: 71 | plt.axvline(x=v, color='red') 72 | thresholds = otsu.threshold_valley_regions(hist, valleys, N) 73 | ### 74 | 75 | # modified_TSMO does all the steps above 76 | thresholds2 = otsu.modified_TSMO(hist, M=M, L=L) 77 | 78 | # Threshold obtained through default otsu method. 79 | otsu_threshold, _ = otsu.otsu_method(hist) 80 | 81 | print('Otsu threshold: {}\nStep-by-step MTSMO: {}\nMTSMO: {}'.format( 82 | otsu_threshold, thresholds, thresholds2)) 83 | 84 | # Show histogram with thresholds 85 | plt.figure() 86 | plt.bar(range(0, hist.shape[0]), hist.ravel()) 87 | plt.axvline(x=otsu_threshold, color='k') 88 | for t in thresholds: 89 | plt.axvline(x=t, color='red') 90 | 91 | # Illustrate thresholds 92 | img_1 = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) 93 | img_3 = img_1.copy() 94 | img_auto = img_1.copy() 95 | 96 | show_thresholds(img, img_1, [thresholds[0]]) 97 | show_thresholds(img, img_3, thresholds[0:3]) 98 | show_thresholds(img, img_auto, thresholds) 99 | 100 | plt.figure() 101 | ax = plt.subplot(2, 2, 1) 102 | ax.set_title('Original image') 103 | plt.imshow(img, cmap='gray') 104 | ax = plt.subplot(2, 2, 2) 105 | ax.set_title('{} levels (Automatic)'.format(len(thresholds))) 106 | plt.imshow(img_auto) 107 | ax = plt.subplot(2, 2, 3) 108 | ax.set_title('3 levels') 109 | plt.imshow(img_3) 110 | ax = plt.subplot(2, 2, 4) 111 | ax.set_title('1 level') 112 | plt.imshow(img_1) 113 | plt.show() 114 | 115 | if __name__ == '__main__': 116 | main() 117 | -------------------------------------------------------------------------------- /images/automatic_threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ps-george/multithreshold/4324dda00afa7c35904386554cfef96b249e8d46/images/automatic_threshold.png -------------------------------------------------------------------------------- /images/different_levels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ps-george/multithreshold/4324dda00afa7c35904386554cfef96b249e8d46/images/different_levels.png -------------------------------------------------------------------------------- /images/soil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ps-george/multithreshold/4324dda00afa7c35904386554cfef96b249e8d46/images/soil.jpg -------------------------------------------------------------------------------- /images/threshold_values_histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ps-george/multithreshold/4324dda00afa7c35904386554cfef96b249e8d46/images/threshold_values_histogram.png -------------------------------------------------------------------------------- /otsu.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def multithreshold(img, thresholds): 5 | """Multithreshold a single-channel image using provided thresholds. 6 | 7 | Returns boolean mask indices. 8 | """ 9 | masks = np.zeros((len(thresholds) + 1, img.shape[0], img.shape[1]), bool) 10 | for i, t in enumerate(sorted(thresholds)): 11 | masks[i+1] = (img > t) 12 | masks[0] = ~masks[1] 13 | for i in range(1, len(masks) - 1): 14 | masks[i] = masks[i] ^ masks[i+1] 15 | return masks 16 | 17 | 18 | def otsu_method(hist): 19 | """ 20 | Optimized implementation of Otsu's Method algorithm. 21 | 22 | Adapted from https://github.com/subokita/Sandbox/blob/master/otsu.py and the Matlab implementation on Wikipedia. 23 | """ 24 | num_bins = hist.shape[0] 25 | total = hist.sum() 26 | sum_total = np.dot(range(0, num_bins), hist) 27 | 28 | weight_background = 0.0 29 | sum_background = 0.0 30 | 31 | optimum_value = 0 32 | maximum = -np.inf 33 | 34 | for t in range(0, num_bins): 35 | # background weight will be incremented, while foreground's will be reduced 36 | weight_background += hist.item(t) 37 | if weight_background == 0: 38 | continue 39 | weight_foreground = total - weight_background 40 | if weight_foreground == 0: 41 | break 42 | 43 | sum_background += t * hist.item(t) 44 | mean_foreground = (sum_total - sum_background) / weight_foreground 45 | mean_background = sum_background / weight_background 46 | 47 | inter_class_variance = weight_background * weight_foreground * \ 48 | (mean_background - mean_foreground) ** 2 49 | 50 | # find the threshold with maximum variances between classes 51 | if inter_class_variance > maximum: 52 | optimum_value = t 53 | maximum = inter_class_variance 54 | return optimum_value, maximum 55 | 56 | 57 | def otsu(hist): 58 | pos, val = otsu_method(hist) 59 | return pos 60 | 61 | 62 | def normalised_histogram_binning(hist, M=32, L=256): 63 | """Normalised histogram binning""" 64 | norm_hist = np.zeros((M, 1), dtype=np.float32) 65 | N = L // M 66 | counters = [range(x, x+N) for x in range(0, L, N)] 67 | for i, C in enumerate(counters): 68 | norm_hist[i] = 0 69 | for j in C: 70 | norm_hist[i] += hist[j] 71 | norm_hist = (norm_hist / norm_hist.max()) * 100 72 | return norm_hist 73 | 74 | 75 | def find_valleys(H): 76 | """Valley estimation on *H*, H should be normalised-binned-grouped histogram.""" 77 | hsize = H.shape[0] 78 | probs = np.zeros((hsize, 1), dtype=int) 79 | costs = np.zeros((hsize, 1), dtype=float) 80 | for i in range(1, hsize-1): 81 | if H[i] > H[i-1] or H[i] > H[i+1]: 82 | probs[i] = 0 83 | elif H[i] < H[i-1] and H[i] == H[i+1]: 84 | probs[i] = 1 85 | costs[i] = H[i-1] - H[i] 86 | elif H[i] == H[i-1] and H[i] < H[i+1]: 87 | probs[i] = 3 88 | costs[i] = H[i+1] - H[i] 89 | elif H[i] < H[i-1] and H[i] < H[i+1]: 90 | probs[i] = 4 91 | costs[i] = (H[i-1] + H[i+1]) - 2*H[i] 92 | elif H[i] == H[i-1] and H[i] == H[i+1]: 93 | probs[i] = probs[i-1] 94 | costs[i] = probs[i-1] 95 | for i in range(1, hsize-1): 96 | if probs[i] != 0: 97 | # floor devision. if > 4 = 1, else 0 98 | probs[i] = (probs[i-1] + probs[i] + probs[i+1]) // 4 99 | valleys = [i for i, x in enumerate(probs) if x > 0] 100 | # if maximum is not None and maximum < len(valleys): 101 | # valleys = sorted(valleys, key=lambda x: costs[x])[0:maximum] 102 | return valleys 103 | 104 | 105 | def valley_estimation(hist, M=32, L=256): 106 | """Valley estimation for histogram. L should be divisible by M.""" 107 | # Normalised histogram binning 108 | norm_hist = normalised_histogram_binning(hist, M, L) 109 | valleys = find_valleys(norm_hist) 110 | return valleys 111 | 112 | 113 | def threshold_valley_regions(hist, valleys, N): 114 | """Perform Otsu's method over estimated valley regions. 115 | 116 | Returns: 117 | list: thresholds ordered by greatest intra-class variance. 118 | """ 119 | thresholds = [] 120 | for valley in valleys: 121 | start_pos = (valley * N) - N 122 | end_pos = (valley + 2) * N 123 | h = hist[start_pos:end_pos] 124 | sub_threshold, val = otsu_method(h) 125 | thresholds.append((start_pos + sub_threshold, val)) 126 | thresholds.sort(key=lambda x: x[1], reverse=True) 127 | thresholds, values = [list(t) for t in zip(*thresholds)] 128 | return thresholds 129 | 130 | 131 | def modified_TSMO(hist, M=32, L=256): 132 | """Modified Two-Stage Multithreshold Otsu Method. 133 | 134 | Implemented based on description in: 135 | Huang, D. Y., Lin, T. W., & Hu, W. C. (2011). 136 | Automatic multilevel thresholding based on two-stage Otsu’s method with cluster determination by valley estimation. 137 | International Journal of Innovative Computing, Information and Control, 7(10), 56315644. 138 | 139 | Args: 140 | hist: Histogram of grayscale image. 141 | 142 | Returns: 143 | list: List of detected thresholds ordered by greatest intra-class variance. 144 | """ 145 | 146 | N = L // M 147 | valleys = valley_estimation(hist, M, L) 148 | thresholds = threshold_valley_regions(hist, valleys, N) 149 | return thresholds 150 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | --------------------------------------------------------------------------------