├── .gitignore ├── LICENSE ├── README.md ├── analyze.m ├── analyze_environ.m ├── build_scene.m ├── colorspaces.txt ├── diff_span.m ├── dwest.m ├── extract_masked_regions.m ├── im_rgb2lab.m ├── im_rgb2upvpl.m ├── im_rgb2uvl.m ├── im_rgb2xyy.m ├── im_rgb2xyz.m ├── knna.m ├── knnpdf.m ├── mwnswtd.m ├── mwpcag.m ├── mwpcag_block.m ├── normalize_image.m ├── nswtd.m ├── opd.m ├── osp.m ├── pad_image_symmetric.m ├── pcad.m ├── pcad_block.m ├── pcag.m ├── pcag_block.m ├── performance.m ├── roc_anomaly.m ├── run.m ├── run_script.m ├── rxg.m ├── rxl.m ├── rxl_block.m ├── select_channel.m ├── sq_mask.m └── transform_channel.m /.gitignore: -------------------------------------------------------------------------------- 1 | *.tiff 2 | *.mat 3 | roc 4 | scenes 5 | anomalies 6 | *.jpg 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 L. Nathan Perkins 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Color Anomaly Detector 2 | ====================== 3 | 4 | Written as part of a class project on color anomaly detection for search and rescue purposes, this repository contains a number of Matlab implementations of common anomaly detection algorithm from literature on hyperspectral techniques, as well as our own algorithm (PCAG) selected to balance performance and computation time. 5 | 6 | The full technical report on the project, including citations and evaluations of the different anomaly detection algorithms is available online at: 7 | 8 | [Color Outlier Detection for Search and Rescue](https://www.nathanntg.com/writing/color-anomaly-detection.pdf) 9 | 10 | 11 | Implemented hyperspectral algorithms 12 | ------------------------------------ 13 | 14 | The following hyperspectral algorithms are implemented in this repository: 15 | 16 | * Global RX (`rxg`) 17 | * Local RX (`rxl`) 18 | * Dual Window-based Eigen Separation Transform (`dwest`) 19 | * Nested Spatial Window-based Target Detection (`nswtd`) 20 | * Multiple Window Nested Spatial Window-based Target Detection (`mwnswtd`) 21 | 22 | Each function above takes a single argument representing the image as a Matlab double matrix with spectral intensities between 0 and 1. 23 | 24 | Novel anomaly detection algorithms 25 | ---------------------------------- 26 | 27 | The following new algorithms are implemented in this repository: 28 | 29 | * Principal Component Analysis Gaps (`pcag`) 30 | * Multiple Window Principal Component Analysis Gaps (`mwpcag`) 31 | 32 | Utility functions 33 | ----------------- 34 | 35 | A few useful utility functions are implemented in this repository: 36 | 37 | * `im_norm = normalize_image(im)` normalizes an image to be stored in a Matlab double matrix and discarding any transparency channel data. 38 | * `[scene, target] = build_scene(file, num_anomalies, blended)` superimposes random anomalies (from an "anomalies" directory) onto a scene (from a "scenes" directory) in a random position and rotation, and with luminance matching to the surrounding pixels. Returns both the new scene (scene) and a target image (target), representing anomaly positions. 39 | * `[tpr, fpr, th, auc] = roc_anomaly(target, out)` calculates the ROC curve for a particular anomaly detector output (out) based on the target image (target). Shows a plot and returns the true-positive rate (tpr), false-positive rate (fpr), thresholds (th) and area under the curve (auc). 40 | 41 | Analysis scripts 42 | ---------------- 43 | 44 | * `run` evaluates the above algorithms over a number of scenes and color spaces in parallel (using `run_script`) 45 | * `analyze` evaluates the output of the run function above across algorithms and colorspaces 46 | * `analyze_environ` evaluates the output of the run function above across scene types (assuming consistently prefixed scene names) 47 | * `performance` evaluates the execution time of the various algorithms 48 | 49 | ### Authors 50 | 51 | **L. Nathan Perkins** 52 | 53 | - 54 | - 55 | 56 | **Travis Marshall** 57 | -------------------------------------------------------------------------------- /analyze.m: -------------------------------------------------------------------------------- 1 | % algorithms to compare 2 | algos = {'rx', 'rxl', 'dwest', 'nswtd', 'mwnswtd', 'pcag', 'mwpcag', 'pcad', 'knna'}; 3 | algos_nice = {'Global RX', 'Local RX', 'DWEST', 'NSWTD', 'MW-NSWTD', 'PCAG', 'MW-PCAG', 'PCAD', 'KNN'}; 4 | 5 | % scenes to compare 6 | scene_files = dir('scenes/*.jpg'); 7 | scene_files = {scene_files.name}; 8 | % scene_files = {'beach.jpg', 'desert.jpg', 'island.jpg'}; 9 | 10 | % color spaces to compare 11 | color_spaces = {'RGB', 'L*a*b', 'u''v''L', 'uvL', 'xyY', 'XYZ', '*a*b', 'u''v''', 'uv', 'xy', 'XZ', 'log(L)*a*b', 'YCbCr', 'CbCr'}; 12 | 13 | % make comprehensive target 14 | target = []; 15 | for i = 1:length(scene_files) 16 | % load scene 17 | scene = scene_files{i}; 18 | S = load(sprintf('output/%s.mat', scene)); 19 | 20 | % append to target 21 | target = [target; S.target(:)]; 22 | end 23 | 24 | % TABLE 25 | % COLUMNS: algorithms 26 | % ROWS: color spaces 27 | 28 | % for a color space 29 | tbl = zeros(length(color_spaces), length(algos)); 30 | for i = 1:length(color_spaces) 31 | for j = 1:length(algos) 32 | algo = algos{j}; 33 | 34 | % make comprehensive output 35 | out = []; 36 | for k = 1:length(scene_files) 37 | scene = scene_files{k}; 38 | fname = sprintf('output/%s-%d-%s.mat', scene, i, algo); 39 | 40 | % load 41 | S = load(fname); 42 | f = fieldnames(S); 43 | 44 | % out 45 | cur_out = S.(f{1}); 46 | 47 | out = [out; cur_out(:)]; 48 | end 49 | 50 | % calculate AUC 51 | [~, ~, ~, auc] = roc_anomaly(target, out); 52 | title(sprintf('%s with %s color space', algos_nice{j}, color_spaces{i})); 53 | print(gcf, sprintf('roc/%d-%s.png', i, algo), '-dpng', '-r300'); 54 | 55 | % close figure window 56 | close; 57 | 58 | % store AUC 59 | tbl(i, j) = auc; 60 | end 61 | end 62 | 63 | % bar plot 64 | b = bar(tbl); 65 | ylim([0.6 1.0]); 66 | ylabel('AUC'); 67 | xlabel('Color space'); 68 | legend(b, algos, 'Location', 'EastOutside'); 69 | set(gca, 'XTickLabel', color_spaces); 70 | title('Comparative Performance'); 71 | print(gcf, 'bar.png', '-dpng', '-r300'); -------------------------------------------------------------------------------- /analyze_environ.m: -------------------------------------------------------------------------------- 1 | % algorithms to compare 2 | algos = {'rx', 'nswtd', 'mwpcag'}; 3 | algos_nice = {'Global RX', 'NSWTD', 'MW-PCAG'}; 4 | 5 | % scenes to compare 6 | scene_files = {{'beach.jpg', 'beach2.jpg', 'beach3.jpg'}, {'desert.jpg', 'desert2.jpg'}, {'forest.jpg', 'forest2.jpg'}, {'island.jpg', 'island2.jpg'}, {'mountain.jpg', 'mountain2.jpg'}, {'ocean.jpg'}}; 7 | scene_nice = {'Beach', 'Desert', 'Forest', 'Arid', 'Mountain', 'Ocean'}; 8 | 9 | % color spaces to compare 10 | color_space = 'L*a*b'; 11 | color_space_idx = 2; 12 | 13 | % TABLE 14 | % COLUMNS: environment 15 | % ROWS: color spaces 16 | 17 | % for a color space 18 | tbl = zeros(length(scene_files), length(algos)); 19 | for i = 1:length(scene_files) 20 | % current scene files 21 | cur_scene_files = scene_files{i}; 22 | 23 | % make comprehensive target 24 | target = []; 25 | for j = 1:length(cur_scene_files) 26 | % load scene 27 | scene = cur_scene_files{j}; 28 | fname = sprintf('output/%s.mat', scene); 29 | S = load(fname); 30 | 31 | % append to target 32 | target = [target; S.target(:)]; 33 | end 34 | 35 | % for each algorithm 36 | for j = 1:length(algos) 37 | algo = algos{j}; 38 | 39 | % make comprehensive output 40 | out = []; 41 | for k = 1:length(cur_scene_files) 42 | scene = cur_scene_files{k}; 43 | fname = sprintf('output/%s-%d-%s.mat', scene, color_space_idx, algo); 44 | 45 | % load 46 | S = load(fname); 47 | f = fieldnames(S); 48 | 49 | % out 50 | cur_out = S.(f{1}); 51 | 52 | out = [out; cur_out(:)]; 53 | end 54 | 55 | % calculate AUC 56 | [~, ~, ~, auc] = roc_anomaly(target, out); 57 | title(sprintf('%s for %s scenes', algos_nice{j}, scene_nice{i})); 58 | print(gcf, sprintf('roc/%s-%s.png', scene_nice{i}, algo), '-dpng', '-r300'); 59 | 60 | % close figure window 61 | close; 62 | 63 | % store AUC 64 | tbl(i, j) = auc; 65 | end 66 | end 67 | 68 | % bar plot 69 | b = bar(tbl); 70 | ylim([0.6 1.0]); 71 | ylabel('AUC'); 72 | xlabel('Color space'); 73 | legend(b, algos, 'Location', 'EastOutside'); 74 | set(gca, 'XTickLabel', color_spaces); 75 | title('Comparative Performance'); 76 | print(gcf, 'bar-environ.png', '-dpng', '-r300'); -------------------------------------------------------------------------------- /build_scene.m: -------------------------------------------------------------------------------- 1 | function [scene, target] = build_scene(file, num_anomalies, blended) 2 | %BUILD_SCENE Reads image file and adds anomalies to it. 3 | % Returns image and target map showing anomalies. Anomalies are luminance 4 | % adjusted to match the surroundings. Defaults to 3 anomalies. 5 | 6 | if nargin < 2 7 | num_anomalies = 3; 8 | blended = true; 9 | elseif nargin < 3 10 | blended = true; 11 | end 12 | 13 | % settings 14 | desired_width = 1536; 15 | anomaly_height = 10; % all anomalies are normalized to 100 high, so use height as normalizing factor 16 | 17 | % read a scene 18 | scene = imread(['scenes/' file]); 19 | scene = normalize_image(imresize(scene, [nan desired_width])); 20 | 21 | % make target 22 | target = false(size(scene, 1), size(scene, 2)); 23 | 24 | % anomaly files 25 | anomaly_files = dir('anomalies/*.png'); 26 | anomaly_files = {anomaly_files.name}; 27 | perm = randperm(length(anomaly_files)); 28 | anomaly_files = anomaly_files(perm); 29 | 30 | % scene luminance information 31 | scene_luminance = rgb2lab(scene); 32 | scene_luminance = scene_luminance(:, :, 1); 33 | 34 | for j = 1:num_anomalies 35 | % read an anomaly 36 | [anom, ~, anom_mask] = imread(['anomalies/' anomaly_files{j}]); 37 | 38 | % resize 39 | anom = imresize(anom, [anomaly_height nan]); 40 | anom_mask = imresize(anom_mask, [anomaly_height nan]); 41 | 42 | % rotate 43 | ang = rand * 365; 44 | anom = normalize_image(imrotate(anom, ang, 'bicubic')); 45 | anom_mask = imrotate(anom_mask, ang, 'bilinear'); 46 | 47 | % figure out threshold for alpha (want enough of the anomaly, but not grain edges) 48 | th = 255; 49 | while sum(sum(anom_mask >= th)) < (0.5 * anomaly_height * anomaly_height) 50 | th = th - 5; 51 | end 52 | 53 | % position 54 | pos = 1 + rand(1, 3) .* (size(scene) - size(anom)); 55 | pos = round(pos(1:2)); 56 | 57 | % average luminance 58 | med_y_scene = median(median(scene_luminance(pos(1):pos(1) + size(anom, 1) - 1, pos(2):pos(2) + size(anom, 2) - 1))); 59 | 60 | % adjust luminance 61 | anom = rgb2lab(anom); 62 | anom_y = anom(:, :, 1); 63 | med_y_anom = median(median(anom_y(anom_mask >= th))); 64 | anom(:, :, 1) = anom_y * (med_y_scene / med_y_anom); 65 | anom = lab2rgb(anom); 66 | 67 | % build mask for all channels 68 | single_mask = (anom_mask >= th); 69 | mask = single_mask; 70 | while size(mask, 3) < size(scene, 3) 71 | mask = cat(3, mask, single_mask); 72 | end 73 | 74 | % local scene 75 | scene_local = scene(pos(1):pos(1) + size(anom, 1) - 1, pos(2):pos(2) + size(anom, 2) - 1, :); 76 | % replace channels 77 | if blended 78 | w = repmat(double(anom_mask), 1, 1, 3) / 255; 79 | w(~mask) = 0; 80 | scene_local = (1 - w) .* scene_local + w .* anom; 81 | else 82 | scene_local(mask) = anom(mask); 83 | end 84 | scene(pos(1):pos(1) + size(anom, 1) - 1, pos(2):pos(2) + size(anom, 2) - 1, :) = scene_local; 85 | 86 | % update target 87 | target(pos(1):pos(1) + size(anom, 1) - 1, pos(2):pos(2) + size(anom, 2) - 1) = target(pos(1):pos(1) + size(anom, 1) - 1, pos(2):pos(2) + size(anom, 2) - 1) | single_mask; 88 | end 89 | 90 | end 91 | 92 | -------------------------------------------------------------------------------- /colorspaces.txt: -------------------------------------------------------------------------------- 1 | Color space transformations: 2 | 3 | function img_ss = select_channel(img, channels) 4 | img_ss = img(:, :, channels) 5 | end 6 | 7 | function img_tc = transform_channel(img, channel, cb) 8 | img_tc = img; 9 | img_tc(:, :, channel) = cb(img_tc(:, :, channel); 10 | end 11 | 12 | rgb2ycbcr 13 | @(img) select_channel(rgb2ycbcr(img), 2:3); 14 | @(img) transform_channel(rgb2ycbcr(img), 1, log) 15 | im_rgb2lab 16 | -------------------------------------------------------------------------------- /diff_span.m: -------------------------------------------------------------------------------- 1 | function d = diff_span(v, n) 2 | %DIFF_SPAN Differences in vector with a span of n. 3 | d = v(1 + n:end) - v(1:end - n); 4 | end -------------------------------------------------------------------------------- /dwest.m: -------------------------------------------------------------------------------- 1 | function im_result = dwest(img) 2 | %DWEST Perform the DWEST algorithm on img. 3 | 4 | % largest window size (must be odd) 5 | w_max_size = 11; 6 | 7 | % image information 8 | [height, width, channels] = size(img); 9 | 10 | % return image 11 | im_result = zeros(height, width); 12 | 13 | % prebuild masks 14 | masks = {}; 15 | masks_inv = {}; 16 | for w_size = 1:2:(w_max_size - 2) 17 | masks{end + 1} = reshape(sq_mask(w_max_size, 1, w_size), [], 1); 18 | masks_inv{end + 1} = ~masks{end}; 19 | end 20 | num_masks = length(masks); 21 | 22 | % pad image 23 | border = (w_max_size - 1) / 2; 24 | pad_img = pad_image_symmetric(img, border); 25 | for i = 1:width 26 | for j = 1:height 27 | d = nan; 28 | 29 | % reshape window 30 | win = reshape(pad_img(j:j + w_max_size - 1, i:i + w_max_size - 1, :), [], channels); 31 | 32 | % for each mask 33 | for k = 1:num_masks 34 | iw = win(masks{k}, :); 35 | ow = win(masks_inv{k}, :); 36 | 37 | % inner window 38 | if 1 < k 39 | iw_mean = mean(iw, 1); 40 | iw_centered = bsxfun(@minus, iw, iw_mean); 41 | else 42 | iw_mean = iw; 43 | iw_centered = zeros(size(iw)); 44 | end 45 | 46 | % outer window 47 | ow_mean = mean(ow, 1); 48 | ow_centered = bsxfun(@minus, ow, ow_mean); 49 | 50 | % difference in covariance 51 | diff_cov = ((iw_centered' * iw_centered) - (ow_centered' * ow_centered)) / (size(ow_centered, 1) - 1); 52 | 53 | % eigenvalues difference in covariance 54 | [e_vec, e_val] = eig(diff_cov); 55 | 56 | % eigen vectors associated with positive value times mean 57 | d = max(d, abs(sum((ow_mean - iw_mean) * e_vec(:, diag(e_val) > 0)))); 58 | end 59 | 60 | im_result(j, i) = d; 61 | end 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /extract_masked_regions.m: -------------------------------------------------------------------------------- 1 | function img_regions = extract_masked_regions(img, mask, threshold) 2 | %EXTRACT_MASKED_REGIONS Extract channels from image where mask >= threhsold 3 | 4 | l = mask >= threshold; 5 | 6 | % duplicate mask for all channels 7 | fl = l; 8 | while size(fl, 3) < size(img, 3) 9 | fl = cat(3, fl, l); 10 | end 11 | 12 | img_regions = zeros(size(img), 'like', img); 13 | img_regions(fl) = img(fl); 14 | 15 | end 16 | -------------------------------------------------------------------------------- /im_rgb2lab.m: -------------------------------------------------------------------------------- 1 | function im_lab = im_rgb2lab(im_rgb ) 2 | %IM_RGB2LAB Convert RGB to L*a*b colorspace 3 | cf = makecform('srgb2lab'); 4 | im_lab = applycform(im_rgb, cf); 5 | end 6 | 7 | -------------------------------------------------------------------------------- /im_rgb2upvpl.m: -------------------------------------------------------------------------------- 1 | function im_upvpl = im_rgb2upvpl(im_rgb ) 2 | %IM_RGB2UPVPL Convert RGB to u'v'L colorspace 3 | cf = makecform('srgb2xyz'); 4 | cf2 = makecform('xyz2upvpl'); 5 | im_upvpl = applycform(applycform(im_rgb, cf), cf2); 6 | end 7 | 8 | -------------------------------------------------------------------------------- /im_rgb2uvl.m: -------------------------------------------------------------------------------- 1 | function im_uvl = im_rgb2uvl(im_rgb ) 2 | %IM_RGB2LAB Convert RGB to uvL colorspace 3 | cf = makecform('srgb2xyz'); 4 | cf2 = makecform('xyz2uvl'); 5 | im_uvl = applycform(applycform(im_rgb, cf), cf2); 6 | end 7 | 8 | -------------------------------------------------------------------------------- /im_rgb2xyy.m: -------------------------------------------------------------------------------- 1 | function im_xyy = im_rgb2xyy(im_rgb ) 2 | %IM_RGB2XYY Convert RGB to xyY colorspace 3 | cf = makecform('srgb2xyz'); 4 | cf2 = makecform('xyz2xyl'); 5 | im_xyy = applycform(applycform(im_rgb, cf), cf2); 6 | end 7 | 8 | -------------------------------------------------------------------------------- /im_rgb2xyz.m: -------------------------------------------------------------------------------- 1 | function im_xyz = im_rgb2xyz(im_rgb) 2 | %IM_RGB2XYZ Convert RGB to XYZ colorspace 3 | cf = makecform('srgb2xyz'); 4 | im_xyz = applycform(im_rgb, cf); 5 | end 6 | 7 | -------------------------------------------------------------------------------- /knna.m: -------------------------------------------------------------------------------- 1 | function im_result = knna(img) 2 | %KNN Perform the PCAG algorithm on img. 3 | 4 | b = blockproc(img, [50 50], @knnpdf, 'PadPartialBlocks', 1, 'PadMethod', 'symmetric'); 5 | size_im = size(img); 6 | b = b(1:size_im(1), 1:size_im(2)); 7 | 8 | im_result = 1 - b; 9 | 10 | return; 11 | 12 | %thresholds and parameters 13 | thr_nomrf = 1e-4; 14 | thr_mrf = 1e-4; 15 | g_temp = 1; 16 | 17 | % labels 18 | label = cell(1, 7); 19 | 20 | label{1} = b < thr_nomrf; 21 | 22 | % MRF model 23 | n_mask = ones(3); 24 | n_mask(2, 2) = 0; 25 | for i = 1:6 26 | anom_normal_cnt = -8 + 2 .* conv2(double(label{i}), n_mask, 'same'); 27 | label{i + 1} = b < thr_mrf .* exp(1 / g_temp .* anom_normal_cnt); 28 | end 29 | 30 | im_result = double(label{7}); 31 | 32 | end 33 | 34 | -------------------------------------------------------------------------------- /knnpdf.m: -------------------------------------------------------------------------------- 1 | function P = knnpdf(block) 2 | 3 | sz = block.blockSize; 4 | channels = size(block.data, 3); 5 | 6 | % variables 7 | kn = 100; 8 | n = sz(1) * sz(2); 9 | features = zeros(n, channels); 10 | 11 | for i = 1:channels 12 | temp = block.data(:, :, i); 13 | features(:, i) = double(temp(:)); 14 | end 15 | 16 | X = features; 17 | Y = features; 18 | 19 | [~, D] = knnsearch(X, Y, 'K', kn); 20 | P = kn ./ (n .* (pi .* (D(:, kn) + 1) .^ 2)); 21 | P = reshape(P, sz(1), sz(2)); 22 | 23 | end -------------------------------------------------------------------------------- /mwnswtd.m: -------------------------------------------------------------------------------- 1 | function im_result = mwnswtd(img) 2 | %MWNSWTD Perform the MW-NSWTD algorithm on img. 3 | 4 | % largest window size (must be odd) 5 | w_max_size = 11; 6 | 7 | % image information 8 | [height, width, channels] = size(img); 9 | 10 | % return image 11 | im_result = zeros(height, width); 12 | 13 | % create inner window (size 1) 14 | iw_size = 1; 15 | mask_iw = reshape(sq_mask(w_max_size, 1, iw_size), [], 1); 16 | 17 | % prebuild other masks 18 | masks = {}; 19 | for w_size = (iw_size + 2):2:w_max_size 20 | masks{end + 1} = reshape(sq_mask(w_max_size, 1, w_size, w_size - 2), [], 1); 21 | end 22 | num_masks = length(masks); 23 | 24 | % pad image 25 | border = (w_max_size - 1) / 2; 26 | pad_img = pad_image_symmetric(img, border); 27 | for i = 1:width 28 | for j = 1:height 29 | d = nan; 30 | 31 | % reshape window 32 | win = reshape(pad_img(j:j + w_max_size - 1, i:i + w_max_size - 1, :), [], channels); 33 | 34 | % get inner window mean 35 | iw_mean = mean(win(mask_iw, :), 1); 36 | 37 | % last outer window to pass into loop 38 | ow_mean = mean(win(masks{1}, :), 1); 39 | 40 | % for each mask 41 | for k = 2:num_masks 42 | % last outer window is new middle window 43 | mw_mean = ow_mean; 44 | 45 | % new outer window 46 | ow_mean = mean(win(masks{k}, :), 1); 47 | 48 | % use OSP based on outer window to project both inner and middle window 49 | p_outer = osp(ow_mean); 50 | cd = (iw_mean * p_outer * iw_mean') + (mw_mean * p_outer * mw_mean'); 51 | if 0 <= cd 52 | d = max(d, sqrt(cd)); 53 | end 54 | end 55 | 56 | im_result(j, i) = d; 57 | end 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /mwpcag.m: -------------------------------------------------------------------------------- 1 | function im_result = mwpcag(img) 2 | %MWPCAG Perform the MW-PCAG algorithm on img. 3 | 4 | height = size(img, 1); 5 | width = size(img, 2); 6 | 7 | im_result = zeros(height, width); 8 | for sz = 9:2:15 9 | % run MW-PCAG 10 | cb = @(block) mwpcag_block(block, floor((sz-2)^2*0.1), floor(sz*sz*0.45)); 11 | r = blockproc(img, [sz sz], cb, 'PadPartialBlocks', true, 'PadMethod', 'symmetric'); 12 | 13 | % MAX 14 | im_result = max(im_result, r(1:height, 1:width)); 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /mwpcag_block.m: -------------------------------------------------------------------------------- 1 | function px = mwpcag_block(block_struct, min_size, max_size) 2 | %MWPCAG_BLOCK Perform the MW-PCAG algorithm on img 3 | 4 | % gap between pixels 5 | diff_num = 2; 6 | 7 | % square odd dimension 8 | block_size = size(block_struct.data); 9 | channels = size(block_struct.data, 3); 10 | 11 | vals = reshape(block_struct.data, [], channels); 12 | [~, p_sc, p_la] = pca(vals, 'Algorithm', 'svd', 'Centered', false); 13 | 14 | ret = zeros([size(vals, 1) 1]); 15 | 16 | p_nm = size(p_sc, 2); 17 | for c = 1:p_nm 18 | [s, s_i] = sort(p_sc(:, c)); 19 | 20 | % forward 21 | [fm, fm_i] = max(diff_span(s(min_size:max_size), diff_num)); 22 | 23 | % backward 24 | [bm, bm_i] = max(diff_span(s(end - max_size:end - min_size), diff_num)); 25 | 26 | % largest 27 | if fm > bm 28 | fm_i = min_size - 1 + fm_i; 29 | ret(s_i(1:fm_i)) = ret(s_i(1:fm_i)) + fm; 30 | else 31 | bm_i = numel(s) - max_size - 1 + bm_i + diff_num; 32 | ret(s_i(bm_i:end)) = ret(s_i(bm_i:end)) + bm; 33 | end 34 | end 35 | 36 | % normalize 37 | % restore shape 38 | px = reshape(ret, block_size(1), block_size(2)); 39 | 40 | end 41 | -------------------------------------------------------------------------------- /normalize_image.m: -------------------------------------------------------------------------------- 1 | function im_norm = normalize_image(im) 2 | %NORMALIZE_IMAGE Return image in double format with no alpha channel. 3 | 4 | % normalize type 5 | if isa(im, 'uint8') 6 | im_norm = double(im) / (256 - 1); 7 | elseif isa(im, 'uint16') 8 | im_norm = double(im) / (256 * 256 - 1); 9 | else 10 | im_norm = im; 11 | end 12 | 13 | % remove alpha channel 14 | if 3 == ndims(im_norm) && 4 == size(im_norm, 3) 15 | im_norm = im_norm(:, :, 1:3); 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /nswtd.m: -------------------------------------------------------------------------------- 1 | function im_result = nswtd(img) 2 | %NSWTD Perform the NSWTD algorithm on img. 3 | 4 | % largest window size (must be odd) 5 | w_max_size = 11; 6 | 7 | % image information 8 | [height, width, channels] = size(img); 9 | 10 | % return image 11 | im_result = zeros(height, width); 12 | 13 | % prebuild masks 14 | masks = {}; 15 | masks_inv = {}; 16 | for w_size = 1:2:(w_max_size - 2) 17 | masks{end + 1} = reshape(sq_mask(w_max_size, 1, w_size), [], 1); 18 | masks_inv{end + 1} = ~masks{end}; 19 | end 20 | num_masks = length(masks); 21 | 22 | % pad image 23 | border = (w_max_size - 1) / 2; 24 | pad_img = pad_image_symmetric(img, border); 25 | for i = 1:width 26 | for j = 1:height 27 | d = nan; 28 | 29 | % reshape window 30 | win = reshape(pad_img(j:j + w_max_size - 1, i:i + w_max_size - 1, :), [], channels); 31 | 32 | % for each mask 33 | for k = 1:num_masks 34 | % inner and outer window mean 35 | if 1 < k 36 | iw_mean = mean(win(masks{k}, :), 1); 37 | else 38 | iw_mean = win(masks{k}, :); 39 | end 40 | ow_mean = mean(win(masks_inv{k}, :), 1); 41 | 42 | % OPD 43 | d = max(d, opd(iw_mean, ow_mean)); 44 | end 45 | 46 | im_result(j, i) = d; 47 | end 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /opd.m: -------------------------------------------------------------------------------- 1 | function d = opd(vec_a, vec_b) 2 | %OPD Orthogonal projection divergence 3 | 4 | d = (vec_a * osp(vec_b) * vec_a') + (vec_b * osp(vec_a) * vec_b'); 5 | if 0 <= d 6 | d = sqrt(d); 7 | else 8 | d = nan; 9 | end 10 | 11 | end 12 | 13 | -------------------------------------------------------------------------------- /osp.m: -------------------------------------------------------------------------------- 1 | function P = osp(vec) 2 | %OSP Orthoogonal Subspace Projection 3 | 4 | % http://www.umbc.edu/rssipl/pdf/TGRS/tgrs_OSP_3_05.pdf 5 | 6 | P = eye(size(vec, 2)) - vec' / (vec * vec') * vec; 7 | 8 | end 9 | -------------------------------------------------------------------------------- /pad_image_symmetric.m: -------------------------------------------------------------------------------- 1 | function img_padded = pad_image_symmetric(img, padding) 2 | %PAD_IMAGE_SYMMETRIC Pad img by padding semmetrically filling in new pixels 3 | 4 | % constant padding 5 | if all(size(padding) == 1) 6 | padding = [padding padding padding padding]; 7 | end 8 | 9 | % expand padding 10 | pad_top = padding(1); 11 | pad_right = padding(1); 12 | pad_bottom = padding(3); 13 | pad_left = padding(4); 14 | 15 | % get current dimensions 16 | [height, width, channels] = size(img); 17 | 18 | img_padded = zeros(height + pad_top + pad_bottom, width + pad_left + pad_right, channels); 19 | img_padded(pad_top + 1:pad_top + height, pad_left + 1:pad_left + width, :) = img; 20 | 21 | % fill in sides 22 | img_padded(1:pad_top, pad_left + 1:pad_left + width, :) = img(pad_top:-1:1, :, :); 23 | img_padded(pad_top + height:end, pad_left + 1:pad_left + width, :) = img(end:-1:end - pad_bottom, :, :); 24 | img_padded(pad_top + 1:pad_top + height, 1:pad_left, :) = img(:, pad_left:-1:1, :); 25 | img_padded(pad_top + 1:pad_top + height, pad_left + width:end, :) = img(:, end:-1:end - pad_right, :); 26 | 27 | % fill in corners 28 | img_padded(1:pad_top, 1:pad_left, :) = img(pad_top:-1:1, pad_left:-1:1, :); 29 | img_padded(1:pad_top, pad_left + width:end, :) = img(pad_top:-1:1, end:-1:end - pad_right, :); 30 | img_padded(pad_top + height:end, 1:pad_left, :) = img(end:-1:end - pad_bottom, pad_left:-1:1, :); 31 | img_padded(pad_top + height:end, pad_left + width:end, :) = img(end:-1:end - pad_bottom, end:-1:end - pad_right, :); 32 | 33 | end 34 | -------------------------------------------------------------------------------- /pcad.m: -------------------------------------------------------------------------------- 1 | function im_result = pcad(img) 2 | %PCAD Perform the PCAD algorithm on img. 3 | 4 | height = size(img, 1); 5 | width = size(img, 2); 6 | 7 | cb = @(block) pcad_block(block, 50); 8 | r = blockproc(img, [15 15], cb, 'PadPartialBlocks', true, 'PadMethod', 'symmetric'); 9 | 10 | % MAX 11 | im_result = r(1:height, 1:width); 12 | 13 | end 14 | -------------------------------------------------------------------------------- /pcad_block.m: -------------------------------------------------------------------------------- 1 | function px = pcad_block(block_struct, d) 2 | %PCAD_BLOCK Perform the PCAD algorithm on img 3 | 4 | % square dimension 5 | block_size = size(block_struct.data); 6 | channels = size(block_struct.data, 3); 7 | 8 | % PCA 9 | vals = reshape(block_struct.data, [], channels); 10 | [~, p_sc, p_la] = pca(vals, 'Algorithm', 'svd', 'Centered', false); 11 | 12 | % resuable values 13 | pad = nan(d, 1); 14 | p_nm = size(p_sc, 2); 15 | ret = zeros(size(p_sc)); 16 | 17 | for c = 1:p_nm 18 | [s, s_i] = sort(p_sc(:, c)); 19 | 20 | ret(s_i, c) = max(s - [pad; s(1:end-d)], [s(1 + d:end); pad] - s); 21 | end 22 | 23 | % Euclidian distance 24 | % restore shape 25 | px = reshape(sqrt(sum(ret .^ 2, 2)), block_size(1), block_size(2)); 26 | 27 | end 28 | -------------------------------------------------------------------------------- /pcag.m: -------------------------------------------------------------------------------- 1 | function im_result = pcag(img) 2 | %PCAG Perform the PCAG algorithm on img. 3 | 4 | im_result = blockproc(img, [15 15], @pcag_block); 5 | 6 | end 7 | 8 | -------------------------------------------------------------------------------- /pcag_block.m: -------------------------------------------------------------------------------- 1 | function px = pcag_block(block_struct) 2 | %PCAG Perform the PCAG algorithm on img. 3 | 4 | % size of anomaly 5 | min_size = 4; 6 | max_size = min(100, floor(0.5 * prod(block_struct.blockSize))); 7 | 8 | % gap between pixels 9 | diff_num = 3; 10 | 11 | % square odd dimension 12 | block_size = size(block_struct.data); 13 | channels = size(block_struct.data, 3); 14 | 15 | vals = reshape(block_struct.data, [], channels); 16 | [~, p_sc, p_la] = pca(vals, 'Algorithm', 'svd', 'Centered', false); 17 | 18 | ret = zeros([size(vals, 1) 1]); 19 | 20 | p_nm = size(p_sc, 2); 21 | for c = 1:p_nm 22 | [s, s_i] = sort(p_sc(:, c)); 23 | 24 | % forward 25 | [fm, fm_i] = max(diff_span(s(min_size:max_size), diff_num)); 26 | 27 | % backward 28 | [bm, bm_i] = max(diff_span(s(end - max_size:end - min_size), diff_num)); 29 | 30 | % largest 31 | if fm > bm 32 | fm_i = min_size - 1 + fm_i; 33 | ret(s_i(1:fm_i)) = ret(s_i(1:fm_i)) + fm; 34 | else 35 | bm_i = numel(s) - max_size - 1 + bm_i + diff_num; 36 | ret(s_i(bm_i:end)) = ret(s_i(bm_i:end)) + bm; 37 | end 38 | end 39 | 40 | % normalize 41 | % restore shape 42 | px = reshape(ret, block_size(1), block_size(2)); 43 | 44 | end 45 | -------------------------------------------------------------------------------- /performance.m: -------------------------------------------------------------------------------- 1 | % algorithms to performance test 2 | algos = {@rxg, @rxl, @dwest, @nswtd, @mwnswtd, @pcag, @mwpcag, @pcad, @knna}; 3 | algos_nice = {'Global RX', 'Local RX', 'DWEST', 'NSWTD', 'MW-NSWTD', 'PCAG', 'MW-PCAG', 'PCAD', 'KNN'}; 4 | 5 | % scenes to compare 6 | scene_files = {'beach.jpg', 'desert.jpg', 'island.jpg'}; 7 | 8 | % results 9 | tbl = zeros(numel(algos), numel(scene_files)); 10 | 11 | for i = 1:numel(algos) 12 | cb = algos{i}; 13 | 14 | for j = 1:numel(scene_files) 15 | % load scene 16 | scene = scene_files{j}; 17 | s = load(sprintf('output/%s.mat', scene), 'scene'); 18 | img = s.scene; 19 | 20 | % profile 21 | t = cputime; 22 | a = cb(img); 23 | e = cputime - t; 24 | 25 | % store result 26 | tbl(i, j) = e; 27 | end 28 | end 29 | 30 | % bar plot 31 | b = bar(tbl); 32 | ylabel('Average time (s)'); 33 | xlabel('Algorithm'); 34 | set(gca, 'XTickLabel', algos_nice); 35 | title('Execution Time'); 36 | print(gcf, 'exec.png', '-dpng', '-r300'); -------------------------------------------------------------------------------- /roc_anomaly.m: -------------------------------------------------------------------------------- 1 | function [tpr, fpr, th, auc] = roc_anomaly(im_target, im_out) 2 | %ROC_ANOMALY Plot receiver operating characteristic and return values. 3 | % Takes a target image and the output of an anomaly detector. 4 | % Shows a plot of the ROC curve. 5 | 6 | % remove color component 7 | if 3 == ndims(im_target) 8 | im_target = (mean(im_target, 3) >= 0.5); 9 | end 10 | 11 | % reshape 12 | v_target = im_target(:); 13 | v_out = im_out(:); 14 | 15 | % sort target accordingly 16 | [th, ind] = sort(v_out, 'descend'); 17 | s_target = v_target(ind); 18 | 19 | num = numel(s_target); 20 | tp = 0; fp = 0; 21 | fn = sum(s_target); tn = num - fn; 22 | fpr = zeros(1 + num, 1); tpr = zeros(1 + num, 1); 23 | for v = 1:num 24 | if s_target(v) 25 | tp = tp + 1; 26 | fn = fn - 1; 27 | else 28 | fp = fp + 1; 29 | tn = tn - 1; 30 | end 31 | tpr(1 + v) = tp / (tp + fn); 32 | fpr(1 + v) = fp / (tn + fp); 33 | end 34 | 35 | % make plot 36 | figure; 37 | plot(fpr, tpr, 'b', 'LineWidth', 2.); 38 | auc = trapz(fpr, tpr); 39 | legend(sprintf('ROC (AUC: %.3f)', auc), 'Location', 'SouthEast'); 40 | hold on; plot([0; 1], [0; 1], '-', 'Color', [.8 .8 .8]); hold off; 41 | xlim([0 1]); ylim([0 1]); 42 | xlabel('FPR'); ylabel('TPR'); 43 | 44 | end 45 | 46 | -------------------------------------------------------------------------------- /run.m: -------------------------------------------------------------------------------- 1 | % each scene 2 | scene_files = dir('scenes/*.jpg'); 3 | scene_files = {scene_files.name}; 4 | 5 | % tuned for my laptop (3 workers) 6 | parpool('local', 3); 7 | 8 | % build 9 | parfor i = 1:length(scene_files) 10 | run_script(scene_files{i}); 11 | end 12 | -------------------------------------------------------------------------------- /run_script.m: -------------------------------------------------------------------------------- 1 | function run_script(scene_file) 2 | %RUN_SCRIPT 3 | 4 | % color spaces 5 | color_spaces = {}; 6 | color_spaces{end + 1} = @(img) img; 7 | color_spaces{end + 1} = @im_rgb2lab; 8 | color_spaces{end + 1} = @im_rgb2upvpl; 9 | color_spaces{end + 1} = @im_rgb2uvl; 10 | color_spaces{end + 1} = @im_rgb2xyy; 11 | color_spaces{end + 1} = @im_rgb2xyz; 12 | color_spaces{end + 1} = @(img) select_channel(im_rgb2lab(img), 2:3); 13 | color_spaces{end + 1} = @(img) select_channel(im_rgb2upvpl(img), 1:2); 14 | color_spaces{end + 1} = @(img) select_channel(im_rgb2uvl(img), 1:2); 15 | color_spaces{end + 1} = @(img) select_channel(im_rgb2xyz(img), [1 3]); 16 | color_spaces{end + 1} = @(img) select_channel(im_rgb2xyy(img), [1 3]); 17 | color_spaces{end + 1} = @(img) transform_channel(im_rgb2lab(img), 1, @(x) log(1+x)); 18 | color_spaces{end + 1} = @rgb2ycbcr; 19 | color_spaces{end + 1} = @(img) select_channel(rgb2ycbcr(img), 2:3); 20 | 21 | 22 | % load file 23 | fname = sprintf('output/%s.mat', scene_file); 24 | if exist(fname, 'file') 25 | load(fname); 26 | else 27 | % build scene and target 28 | if strcmp(scene_file(1:7), 'control') 29 | [scene, target] = build_scene(scene_file, 0); 30 | else 31 | [scene, target] = build_scene(scene_file); 32 | end 33 | 34 | % save 35 | save(['output/' scene_file '.mat'], 'scene', 'target'); 36 | end 37 | 38 | fprintf('Scene %s...\n', scene_file); 39 | 40 | % for each color space 41 | for j = 1:length(color_spaces) 42 | % get callback 43 | color_cb = color_spaces{j}; 44 | 45 | % transform image 46 | img = color_cb(scene); 47 | 48 | fprintf('Color %d...\n', j); 49 | 50 | % run four algorithms 51 | % RX GLOBAL 52 | fname = sprintf('output/%s-%d-rx.mat', scene_file, j); 53 | if ~exist(fname, 'file') 54 | img_rx = rxg(img); 55 | save(fname, 'img_rx'); 56 | end 57 | 58 | % RX LOCAL 59 | fname = sprintf('output/%s-%d-rxl.mat', scene_file, j); 60 | if ~exist(fname, 'file') 61 | img_rxl = rxl(img); 62 | save(fname, 'img_rxl'); 63 | end 64 | 65 | % DWEST 66 | fname = sprintf('output/%s-%d-dwest.mat', scene_file, j); 67 | if ~exist(fname, 'file') 68 | img_dwest = dwest(img); 69 | save(fname, 'img_dwest'); 70 | end 71 | 72 | % NSWTD 73 | fname = sprintf('output/%s-%d-nswtd.mat', scene_file, j); 74 | if ~exist(fname, 'file') 75 | img_nswtd = nswtd(img); 76 | save(fname, 'img_nswtd'); 77 | end 78 | 79 | % MW-NSWTD 80 | fname = sprintf('output/%s-%d-mwnswtd.mat', scene_file, j); 81 | if ~exist(fname, 'file') 82 | img_mwnswtd = mwnswtd(img); 83 | save(fname, 'img_mwnswtd'); 84 | end 85 | 86 | % PCAG 87 | fname = sprintf('output/%s-%d-pcag.mat', scene_file, j); 88 | if ~exist(fname, 'file') 89 | img_pcag = pcag(img); 90 | save(fname, 'img_pcag'); 91 | end 92 | 93 | % MW-PCAG 94 | fname = sprintf('output/%s-%d-mwpcag.mat', scene_file, j); 95 | if ~exist(fname, 'file') 96 | img_mwpcag = mwpcag(img); 97 | save(fname, 'img_mwpcag'); 98 | end 99 | 100 | % PCAD 101 | fname = sprintf('output/%s-%d-pcad.mat', scene_file, j); 102 | if ~exist(fname, 'file') 103 | img_pcad = pcad(img); 104 | save(fname, 'img_pcad'); 105 | end 106 | 107 | % KNNA 108 | fname = sprintf('output/%s-%d-knna.mat', scene_file, j); 109 | if ~exist(fname, 'file') 110 | img_knna = knna(img); 111 | save(fname, 'img_knna'); 112 | end 113 | end 114 | 115 | end 116 | 117 | -------------------------------------------------------------------------------- /rxg.m: -------------------------------------------------------------------------------- 1 | function im_result = rxg(img) 2 | % Perform RX algorithm on global of image 3 | 4 | % Dimensions of block 5 | [height, width, channels] = size(img); 6 | 7 | % Separate block into color channels to calculate K matrix 8 | cc = reshape(img, [], channels); 9 | k_inv = inv(cov(cc)); 10 | 11 | % Create mean color column vector 12 | mu = mean(cc); 13 | 14 | % zero-mean 15 | cc_rows = size(cc, 1); 16 | cc = cc - (ones(cc_rows, 1) * mu); 17 | 18 | % Preallocate 19 | im_result = zeros(cc_rows, 1); 20 | 21 | for i = 1:cc_rows 22 | % Locate center pixel and convert to column vector 23 | r = cc(i, :); 24 | 25 | % Run RX detector on center pixel 26 | im_result(i) = r * k_inv * r'; 27 | end 28 | 29 | im_result = reshape(im_result, height, width); 30 | -------------------------------------------------------------------------------- /rxl.m: -------------------------------------------------------------------------------- 1 | function im_result = rxl(img) 2 | %PCAG Perform the PCAG algorithm on img. 3 | 4 | im_result = blockproc(img, [1 1], @rxl_block, 'BorderSize', [25 25], 'TrimBorder', false, 'PadPartialBlocks', true, 'PadMethod', 'symmetric'); 5 | 6 | end 7 | 8 | -------------------------------------------------------------------------------- /rxl_block.m: -------------------------------------------------------------------------------- 1 | function img_out = rxl_block(block_struct) 2 | %RXL_BLOCK Perform the local RX algorithm on a single image block. 3 | 4 | % image size 5 | %height = size(block_struct.data, 1); 6 | width = size(block_struct.data, 2); 7 | channels = size(block_struct.data, 3); 8 | 9 | % get inner and out 10 | guard = 7; 11 | %mask_inner = sq_mask(width, channels, 1); 12 | mask_outer = sq_mask(width, channels, width, guard); 13 | 14 | % get reshaped data 15 | %inner = reshape(block_struct.data(mask_inner), [], channels); 16 | inner = reshape(block_struct.data(1 + block_struct.border(1), 1 + block_struct.border(2), :), [], channels); 17 | outer = reshape(block_struct.data(mask_outer), [], channels); 18 | 19 | % Separate block into color channels to calculate K matrix 20 | K = cov(outer); 21 | 22 | % cneter inner pixel 23 | inner = inner - mean(outer); 24 | 25 | img_out = (inner / inv(K)) * inner'; 26 | 27 | end 28 | -------------------------------------------------------------------------------- /select_channel.m: -------------------------------------------------------------------------------- 1 | function img_ss = select_channel(img, channels) 2 | %SELECT_CHANNEL Select specific channel(s). 3 | img_ss = img(:, :, channels); 4 | end 5 | 6 | -------------------------------------------------------------------------------- /sq_mask.m: -------------------------------------------------------------------------------- 1 | function mask = sq_mask(sz, channels, m_size, h_size) 2 | %SQ_MASK Generate a square mask. 3 | % sz must be an odd number represnting the mask size 4 | mid_point = 1 + (sz - 1) / 2; 5 | if m_size == sz 6 | mask = true([sz sz channels]); 7 | else 8 | mask = false([sz sz channels]); 9 | d = (m_size - 1) / 2; 10 | mask(mid_point - d:mid_point + d, mid_point - d:mid_point + d, :) = true; 11 | end 12 | % hollow 13 | if nargin == 4 14 | d = (h_size - 1) / 2; 15 | mask(mid_point - d:mid_point + d, mid_point - d:mid_point + d, :) = false; 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /transform_channel.m: -------------------------------------------------------------------------------- 1 | function img_tc = transform_channel(img, channel, cb) 2 | %TRANSFORM_CHANNEL Apply callback to channel(s). 3 | img_tc = img; 4 | img_tc(:, :, channel) = cb(img_tc(:, :, channel)); 5 | end 6 | 7 | --------------------------------------------------------------------------------