|
| 1 | +import argparse |
| 2 | +import cv2 |
| 3 | +import glob |
| 4 | +import numpy as np |
| 5 | +import os |
| 6 | +import time |
| 7 | + |
| 8 | + |
| 9 | +# This tool is intended for evaluation of different background subtraction algorithms presented in OpenCV. |
| 10 | +# Several presets with different settings are available. You can see them below. |
| 11 | +# This tool measures quality metrics as well as speed. |
| 12 | + |
| 13 | + |
| 14 | +ALGORITHMS_TO_EVALUATE = [ |
| 15 | + (cv2.bgsegm.createBackgroundSubtractorMOG, 'MOG', {}), |
| 16 | + (cv2.bgsegm.createBackgroundSubtractorGMG, 'GMG', {}), |
| 17 | + (cv2.bgsegm.createBackgroundSubtractorCNT, 'CNT', {}), |
| 18 | + (cv2.bgsegm.createBackgroundSubtractorLSBP, 'LSBP-vanilla', {'nSamples': 20, 'LSBPRadius': 4, 'Tlower': 2.0, 'Tupper': 200.0, 'Tinc': 1.0, 'Tdec': 0.05, 'Rscale': 5.0, 'Rincdec': 0.05, 'LSBPthreshold': 8}), |
| 19 | + (cv2.bgsegm.createBackgroundSubtractorLSBP, 'LSBP-speed', {'nSamples': 10, 'LSBPRadius': 16, 'Tlower': 2.0, 'Tupper': 32.0, 'Tinc': 1.0, 'Tdec': 0.05, 'Rscale': 10.0, 'Rincdec': 0.005, 'LSBPthreshold': 8}), |
| 20 | + (cv2.bgsegm.createBackgroundSubtractorLSBP, 'LSBP-quality', {'nSamples': 20, 'LSBPRadius': 16, 'Tlower': 2.0, 'Tupper': 32.0, 'Tinc': 1.0, 'Tdec': 0.05, 'Rscale': 10.0, 'Rincdec': 0.005, 'LSBPthreshold': 8}), |
| 21 | + (cv2.bgsegm.createBackgroundSubtractorLSBP, 'LSBP-camera-motion-compensation', {'mc': 1}), |
| 22 | + (cv2.bgsegm.createBackgroundSubtractorGSOC, 'GSOC', {}), |
| 23 | + (cv2.bgsegm.createBackgroundSubtractorGSOC, 'GSOC-camera-motion-compensation', {'mc': 1}) |
| 24 | +] |
| 25 | + |
| 26 | + |
| 27 | +def contains_relevant_files(root): |
| 28 | + return os.path.isdir(os.path.join(root, 'groundtruth')) and os.path.isdir(os.path.join(root, 'input')) |
| 29 | + |
| 30 | + |
| 31 | +def find_relevant_dirs(root): |
| 32 | + relevant_dirs = [] |
| 33 | + for d in sorted(os.listdir(root)): |
| 34 | + d = os.path.join(root, d) |
| 35 | + if os.path.isdir(d): |
| 36 | + if contains_relevant_files(d): |
| 37 | + relevant_dirs += [d] |
| 38 | + else: |
| 39 | + relevant_dirs += find_relevant_dirs(d) |
| 40 | + return relevant_dirs |
| 41 | + |
| 42 | + |
| 43 | +def load_sequence(root): |
| 44 | + gt_dir, frames_dir = os.path.join(root, 'groundtruth'), os.path.join(root, 'input') |
| 45 | + gt = sorted(glob.glob(os.path.join(gt_dir, '*.png'))) |
| 46 | + f = sorted(glob.glob(os.path.join(frames_dir, '*.jpg'))) |
| 47 | + assert(len(gt) == len(f)) |
| 48 | + return gt, f |
| 49 | + |
| 50 | + |
| 51 | +def evaluate_algorithm(gt, frames, algo, algo_arguments): |
| 52 | + bgs = algo(**algo_arguments) |
| 53 | + mask = [] |
| 54 | + t_start = time.time() |
| 55 | + |
| 56 | + for i in range(len(gt)): |
| 57 | + frame = np.uint8(cv2.imread(frames[i], cv2.IMREAD_COLOR)) |
| 58 | + mask.append(bgs.apply(frame)) |
| 59 | + |
| 60 | + average_duration = (time.time() - t_start) / len(gt) |
| 61 | + average_precision, average_recall, average_f1, average_accuracy = [], [], [], [] |
| 62 | + |
| 63 | + for i in range(len(gt)): |
| 64 | + gt_mask = np.uint8(cv2.imread(gt[i], cv2.IMREAD_GRAYSCALE)) |
| 65 | + roi = ((gt_mask == 255) | (gt_mask == 0)) |
| 66 | + if roi.sum() > 0: |
| 67 | + gt_answer, answer = gt_mask[roi], mask[i][roi] |
| 68 | + |
| 69 | + tp = ((answer == 255) & (gt_answer == 255)).sum() |
| 70 | + tn = ((answer == 0) & (gt_answer == 0)).sum() |
| 71 | + fp = ((answer == 255) & (gt_answer == 0)).sum() |
| 72 | + fn = ((answer == 0) & (gt_answer == 255)).sum() |
| 73 | + |
| 74 | + if tp + fp > 0: |
| 75 | + average_precision.append(float(tp) / (tp + fp)) |
| 76 | + if tp + fn > 0: |
| 77 | + average_recall.append(float(tp) / (tp + fn)) |
| 78 | + if tp + fn + fp > 0: |
| 79 | + average_f1.append(2.0 * tp / (2.0 * tp + fn + fp)) |
| 80 | + average_accuracy.append(float(tp + tn) / (tp + tn + fp + fn)) |
| 81 | + |
| 82 | + return average_duration, np.mean(average_precision), np.mean(average_recall), np.mean(average_f1), np.mean(average_accuracy) |
| 83 | + |
| 84 | + |
| 85 | +def evaluate_on_sequence(seq, summary): |
| 86 | + gt, frames = load_sequence(seq) |
| 87 | + category, video_name = os.path.basename(os.path.dirname(seq)), os.path.basename(seq) |
| 88 | + print('=== %s:%s ===' % (category, video_name)) |
| 89 | + |
| 90 | + for algo, algo_name, algo_arguments in ALGORITHMS_TO_EVALUATE: |
| 91 | + print('Algorithm name: %s' % algo_name) |
| 92 | + sec_per_step, precision, recall, f1, accuracy = evaluate_algorithm(gt, frames, algo, algo_arguments) |
| 93 | + print('Average accuracy: %.3f' % accuracy) |
| 94 | + print('Average precision: %.3f' % precision) |
| 95 | + print('Average recall: %.3f' % recall) |
| 96 | + print('Average F1: %.3f' % f1) |
| 97 | + print('Average sec. per step: %.4f' % sec_per_step) |
| 98 | + print('') |
| 99 | + |
| 100 | + if category not in summary: |
| 101 | + summary[category] = {} |
| 102 | + if algo_name not in summary[category]: |
| 103 | + summary[category][algo_name] = [] |
| 104 | + summary[category][algo_name].append((precision, recall, f1, accuracy)) |
| 105 | + |
| 106 | + |
| 107 | +def main(): |
| 108 | + parser = argparse.ArgumentParser(description='Evaluate all background subtractors using Change Detection 2014 dataset') |
| 109 | + parser.add_argument('--dataset_path', help='Path to the directory with dataset. It may contain multiple inner directories. It will be scanned recursively.', required=True) |
| 110 | + parser.add_argument('--algorithm', help='Test particular algorithm instead of all.') |
| 111 | + |
| 112 | + args = parser.parse_args() |
| 113 | + dataset_dirs = find_relevant_dirs(args.dataset_path) |
| 114 | + assert len(dataset_dirs) > 0, ("Passed directory must contain at least one sequence from the Change Detection dataset. There is no relevant directories in %s. Check that this directory is correct." % (args.dataset_path)) |
| 115 | + if args.algorithm is not None: |
| 116 | + global ALGORITHMS_TO_EVALUATE |
| 117 | + ALGORITHMS_TO_EVALUATE = filter(lambda a: a[1].lower() == args.algorithm.lower(), ALGORITHMS_TO_EVALUATE) |
| 118 | + summary = {} |
| 119 | + |
| 120 | + for seq in dataset_dirs: |
| 121 | + evaluate_on_sequence(seq, summary) |
| 122 | + |
| 123 | + for category in summary: |
| 124 | + for algo_name in summary[category]: |
| 125 | + summary[category][algo_name] = np.mean(summary[category][algo_name], axis=0) |
| 126 | + |
| 127 | + for category in summary: |
| 128 | + print('=== SUMMARY for %s (Precision, Recall, F1, Accuracy) ===' % category) |
| 129 | + for algo_name in summary[category]: |
| 130 | + print('%05s: %.3f %.3f %.3f %.3f' % ((algo_name,) + tuple(summary[category][algo_name]))) |
| 131 | + |
| 132 | + |
| 133 | +if __name__ == '__main__': |
| 134 | + main() |
0 commit comments