├── Shen2008 └── Shen2008.m ├── paper ├── figures │ ├── drm.jpg │ ├── example.png │ └── example.m ├── paper.md └── paper.bib ├── utils ├── my_clip.m ├── my_normc.m ├── automated_testing.m └── bilateralFilter.m ├── Tan2005 ├── zResetLabels.m ├── zSpecular2Diffuse.m ├── zHighlightRemoval.m ├── zSpecularFreeImage.m ├── zRemoveHighlights.m ├── zInit.m ├── Tan2005.m ├── z.m └── zIteration.m ├── images └── README.md ├── LICENSE ├── Yang2010 └── Yang2010.m ├── Shen2009 └── Shen2009.m ├── Akashi2016 └── Akashi2016.m ├── SIHR.m ├── Yoon2006 └── Yoon2006.m ├── Yamamoto2019 └── Yamamoto2019.m ├── Shen2013 └── Shen2013.m ├── CONTRIBUTING.md └── README.md /Shen2008/Shen2008.m: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitorsr/SIHR/HEAD/Shen2008/Shen2008.m -------------------------------------------------------------------------------- /paper/figures/drm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitorsr/SIHR/HEAD/paper/figures/drm.jpg -------------------------------------------------------------------------------- /utils/my_clip.m: -------------------------------------------------------------------------------- 1 | function Y = my_clip(X, lb, ub) 2 | Y = min(ub, max(lb, X)); 3 | end 4 | -------------------------------------------------------------------------------- /paper/figures/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitorsr/SIHR/HEAD/paper/figures/example.png -------------------------------------------------------------------------------- /Tan2005/zResetLabels.m: -------------------------------------------------------------------------------- 1 | function src = zResetLabels(src) 2 | src.i(src.i~=z.CAMERA_DARK) = 0; 3 | end 4 | 5 | -------------------------------------------------------------------------------- /utils/my_normc.m: -------------------------------------------------------------------------------- 1 | function Y = my_normc(X) 2 | den = sqrt(sum(X.^2, 1)); 3 | Y = zeros(size(X), class(X)); 4 | Y(:, den ~= 0) = X(:, den ~= 0) ./ den(den ~= 0); 5 | end 6 | -------------------------------------------------------------------------------- /Tan2005/zSpecular2Diffuse.m: -------------------------------------------------------------------------------- 1 | function [orgb,oi] = zSpecular2Diffuse(irgb,ii,maxChroma) 2 | c = z.MaxChroma(irgb); 3 | dI = (z.Max(irgb).*(3*c-1))./(c*(3*maxChroma-1)); 4 | sI = (z.Total(irgb)-dI)./3; 5 | nrgb = irgb-sI; 6 | if nrgb(:,:,1) <= 0 || nrgb(:,:,2) <= 0 || nrgb(:,:,3) <= 0 7 | orgb = irgb; 8 | oi = z.NOISE; 9 | return 10 | end 11 | orgb = nrgb; 12 | oi = ii; 13 | end 14 | 15 | -------------------------------------------------------------------------------- /Tan2005/zHighlightRemoval.m: -------------------------------------------------------------------------------- 1 | function [img,sfi,diff] = zHighlightRemoval(input) 2 | img.rgb = 255 * input; % img.rgb = double(imread(fname)); 3 | img.rgb(end+1,1:end,:) = img.rgb(end,1:end,:); % pad post 4 | img.rgb(1:end,end+1,:) = img.rgb(1:end,end,:); 5 | img.i = zeros([size(img.rgb,1) size(img.rgb,2)],'uint8'); 6 | [sfi,diff] = zRemoveHighlights(img); 7 | img = img.rgb(1:end-1,1:end-1,:); 8 | end 9 | 10 | -------------------------------------------------------------------------------- /Tan2005/zSpecularFreeImage.m: -------------------------------------------------------------------------------- 1 | function [src,sfi] = zSpecularFreeImage(src) 2 | Lambda = 0.6; 3 | camDark = 20; 4 | r = src.rgb(:,:,1); g = src.rgb(:,:,2); b = src.rgb(:,:,3); 5 | src.i(intersect(intersect(find(r= 0 10 | src = zIteration(src,sfi,epsilon); 11 | % 12 | waitbar(1-2*epsilon,f) % disp(['epsilon = ' num2str(epsilon)]) 13 | % 14 | epsilon = epsilon - step; 15 | end 16 | % 17 | close(f) 18 | % 19 | sfi = sfi.rgb(1:end-1,1:end-1,:); 20 | diff = src.rgb(1:end-1,1:end-1,:); 21 | end 22 | 23 | -------------------------------------------------------------------------------- /paper/figures/example.m: -------------------------------------------------------------------------------- 1 | I = im2double(imread('fruit.bmp')); 2 | I_d = Yang2010(I); 3 | figure(1) 4 | subplot(1, 3, 1), imshow(I), xlabel('(a)', 'FontName', 'Helvetica', 'FontSize', 8, 'VerticalAlignment', 'bottom') 5 | subplot(1, 3, 2), imshow(I_d), xlabel('(b)', 'FontName', 'Helvetica', 'FontSize', 8, 'VerticalAlignment', 'bottom') 6 | subplot(1, 3, 3), imshow(I-I_d), xlabel('(c)', 'FontName', 'Helvetica', 'FontSize', 8, 'VerticalAlignment', 'bottom') 7 | set(gcf, 'PaperUnits', 'inches') 8 | set(gcf, 'PaperPosition', [0, 0, 5.35, 3]) 9 | print('example.png', '-dpng', '-r300') 10 | % convert figure1.png -trim figure1.png 11 | -------------------------------------------------------------------------------- /images/README.md: -------------------------------------------------------------------------------- 1 | # `images` 2 | 3 | Download the zipped files from the archival links below which contain the codes for the methods of, respectively, Tan and Ikeuchi1, Shen *et al.*2, and Shen and Zheng3. Then, place the therein contained images inside this folder. See structure below. 4 | 5 | Note: the links were archived directly from the respective authors' pages (see link suffix) where their codes along with test images are freely distributed for research purposes. 6 | 7 | 1 http://web.archive.org/web/20190828202158/https://tanrobby.github.io/code/highlight.zip 8 | 2 http://web.archive.org/web/20190828202109/http://ivlab.org/publications/PR2008_code.zip 9 | 3 http://web.archive.org/web/20190828202133/http://ivlab.org/publications/AO2013_code.zip 10 | 11 | The `images` folder should have the following structure. 12 | 13 | ``` 14 | README.md 15 | animals.bmp 16 | animals_gt.bmp 17 | cups.bmp 18 | cups_gt.bmp 19 | fish.ppm 20 | fruit.bmp 21 | fruit_gt.bmp 22 | head.ppm 23 | lady.bmp 24 | masks.bmp 25 | masks_gt.bmp 26 | rabbit.bmp 27 | toys.ppm 28 | train.bmp 29 | watermelon.bmp 30 | wood.bmp 31 | ``` 32 | -------------------------------------------------------------------------------- /utils/automated_testing.m: -------------------------------------------------------------------------------- 1 | is_octave = exist('OCTAVE_VERSION', 'builtin'); % https://wiki.octave.org/Compatibility 2 | 3 | method = { ... 4 | @Tan2005, @Yoon2006, @Shen2008, @Shen2009, ... 5 | @Yang2010, @Shen2013, @Akashi2016}; 6 | 7 | name = {'animals', 'cups', 'fruit', 'masks'}; 8 | gt = '_gt'; 9 | ext = '.bmp'; 10 | 11 | image = cell(4, 1); 12 | truth = cell(4, 1); 13 | 14 | for i = 1:length(name) 15 | image{i} = im2double(imread([name{i}, ext])); 16 | truth{i} = im2double(imread([name{i}, gt, ext])); 17 | end 18 | 19 | my_toc = zeros(length(method), length(name)); 20 | my_psnr = zeros(length(method), length(name)); 21 | if ~is_octave 22 | my_ssim = zeros(length(method), length(name)); 23 | end 24 | 25 | for m = 1:length(method) 26 | disp(['Method ', func2str(method{m})]) 27 | for i = 1:length(name) 28 | disp([' Image ', name{i}]) 29 | tic 30 | I_d = feval(method{m}, image{i}); 31 | my_toc(m, i) = toc; 32 | disp([' Elapsed time ', num2str(my_toc(m, i)), ' s']) 33 | my_psnr(m, i) = psnr(I_d, truth{i}); 34 | if ~is_octave 35 | my_ssim(m, i) = ssim(I_d, truth{i}); 36 | end 37 | end 38 | end 39 | 40 | disp('my_psnr =') 41 | disp(round(10*my_psnr)/10) 42 | 43 | if ~is_octave 44 | disp('my_ssim =') 45 | disp(round(1000*my_ssim)/1000) 46 | end 47 | -------------------------------------------------------------------------------- /Tan2005/zInit.m: -------------------------------------------------------------------------------- 1 | function [src,count] = zInit(src,sfi,epsilon) 2 | count = 0; 3 | nY = size(src.rgb,1); 4 | nX = size(src.rgb,2); 5 | dlog_src_x = log(abs(z.Total(src.rgb(1:nY-1,2:nX ,:))-... 6 | z.Total(src.rgb(1:nY-1,1:nX-1,:)))); 7 | dlog_src_y = log(abs(z.Total(src.rgb(2:nY ,1:nX-1,:))-... 8 | z.Total(src.rgb(1:nY-1,1:nX-1,:)))); 9 | dlog_sfi_x = log(abs(z.Total(sfi.rgb(1:nY-1,2:nX ,:))-... 10 | z.Total(sfi.rgb(1:nY-1,1:nX-1,:)))); 11 | dlog_sfi_y = log(abs(z.Total(sfi.rgb(2:nY ,1:nX-1,:))-... 12 | z.Total(sfi.rgb(1:nY-1,1:nX-1,:)))); 13 | dlogx = abs(dlog_src_x-dlog_sfi_x); 14 | dlogy = abs(dlog_src_y-dlog_sfi_y); 15 | for iY = 1:nY-1 16 | for iX = 1:nX-1 17 | switch src.i(iY,iX) 18 | case z.BOUNDARY 19 | continue 20 | case z.NOISE 21 | continue 22 | case z.CAMERA_DARK 23 | continue 24 | end 25 | if dlogx(iY,iX) > epsilon 26 | src.i(iY,iX) = z.SPECULARX; 27 | count = count + 1; 28 | continue 29 | end 30 | if dlogy(iY,iX) > epsilon 31 | src.i(iY,iX) = z.SPECULARY; 32 | count = count + 1; 33 | continue 34 | end 35 | src.i(iY,iX) = z.DIFFUSE; 36 | end 37 | end 38 | end 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, Vítor Saraiva Ramos 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Tan2005/Tan2005.m: -------------------------------------------------------------------------------- 1 | function I_d = Tan2005(I) 2 | %Tan2005 I_d = Tan2005(I) 3 | % This Frankenstein of a code translation is the attempt of a 4 | % transliteration of the original C++ code. It is pending refactoring. 5 | % 6 | % Normally it should produce similar results, give or take some numerical 7 | % errors. Full bitmatching has not been confirmed however, because 8 | % constants utilized are sensitive to variation. 9 | % 10 | % Namely, you can try changing the chroma threshold that identifies color 11 | % discontinuities: 12 | % edit zIteration.m 13 | % Search for 'thR' and 'thG'. 14 | % 15 | % You can also change the threshold values that define camera dark pixels 16 | % and the maximum chromaticity that generates the pseudo specular-free 17 | % representation used to guide the method: 18 | % edit zRemoveHighlights.m 19 | % Search for 'Lambda' and 'camDark'. 20 | % 21 | % Contact me should you find any mistakes. 22 | % 23 | % See also SIHR, Yoon2006, Yang2010. 24 | 25 | warning(['This method is slow and noise-sensitive!', sprintf('\n'), ... 26 | ' It takes ~2 min on a small image (60+ on Octave!).']) 27 | 28 | assert(isa(I, 'float'), 'SIHR:I:notTypeSingleNorDouble', ... 29 | 'Input I is not type single nor double.') 30 | assert(min(I(:)) >= 0 && max(I(:)) <= 1, 'SIHR:I:notWithinRange', ... 31 | 'Input I is not within [0, 1] range.') 32 | [n_row, n_col, n_ch] = size(I); 33 | assert(n_row > 1 && n_col > 1, 'SIHR:I:singletonDimension', ... 34 | 'Input I has a singleton dimension.') 35 | assert(n_ch == 3, 'SIHR:I:notRGB', ... 36 | 'Input I is not a RGB image.') 37 | 38 | [~, ~, I_d] = z.main(I); 39 | 40 | end 41 | -------------------------------------------------------------------------------- /Yang2010/Yang2010.m: -------------------------------------------------------------------------------- 1 | function I_d = Yang2010(I) 2 | %Yang2010 I_d = Yang2010(I) 3 | % This method uses a fast bilateralFilter implementation by Jiawen Chen: 4 | % http://people.csail.mit.edu/jiawen/software/bilateralFilter.m 5 | % 6 | % This method should have equivalent functionality as 7 | % `qx_highlight_removal_bf.cpp` formerly distributed by the author. 8 | % 9 | % See also SIHR, Tan2005. 10 | 11 | assert(isa(I, 'double'), 'SIHR:I:notTypeDouble', ... 12 | 'Input I is not type double.') 13 | assert(min(I(:)) >= 0 && max(I(:)) <= 1, 'SIHR:I:notWithinRange', ... 14 | 'Input I is not within [0, 1] range.') 15 | [n_row, n_col, n_ch] = size(I); 16 | assert(n_row > 1 && n_col > 1, 'SIHR:I:singletonDimension', ... 17 | 'Input I has a singleton dimension.') 18 | assert(n_ch == 3, 'SIHR:I:notRGB', ... 19 | 'Input I is not a RGB image.') 20 | 21 | total = sum(I, 3); 22 | 23 | sigma = I ./ total; 24 | sigma(isnan(sigma)) = 0; 25 | 26 | sigmaMin = min(sigma, [], 3); 27 | sigmaMax = max(sigma, [], 3); 28 | 29 | lambda = ones(size(I)) / 3; 30 | lambda = (sigma - sigmaMin) ./ ... 31 | (3 * (lambda - sigmaMin)); 32 | lambda(isnan(lambda)) = 1 / 3; 33 | 34 | lambdaMax = max(lambda, [], 3); 35 | 36 | SIGMAS = 0.25 * min(size(I, 1), size(I, 2)); 37 | % sqrt(size(I, 1)*size(I, 2)); 38 | SIGMAR = 0.04; 39 | THR = 0.03; 40 | 41 | while true 42 | sigmaMaxF = bilateralFilter(sigmaMax, lambdaMax, 0, 1, SIGMAS, SIGMAR); 43 | if nnz(sigmaMaxF-sigmaMax > THR) == 0 44 | break 45 | end 46 | sigmaMax = max(sigmaMax, sigmaMaxF); 47 | end 48 | 49 | Imax = max(I, [], 3); 50 | 51 | den = (1 - 3 * sigmaMax); 52 | I_s = (Imax - sigmaMax .* total) ./ den; 53 | I_s(den == 0) = max(I_s(den ~= 0)); 54 | 55 | I_d = min(1, max(0, I-I_s)); 56 | 57 | end 58 | -------------------------------------------------------------------------------- /Tan2005/z.m: -------------------------------------------------------------------------------- 1 | classdef z 2 | properties (Constant) 3 | SPECULARX = uint8(10) 4 | SPECULARY = uint8(11) 5 | DIFFUSE = uint8(12) 6 | BOUNDARY = uint8(13) 7 | NOISE = uint8(14) 8 | CAMERA_DARK = uint8(15) 9 | end 10 | methods(Static) 11 | function [src,sfi,diff] = main(input) 12 | [src,sfi,diff] = zHighlightRemoval(input); 13 | src = src/255; 14 | sfi = sfi/255; 15 | diff = diff/255; 16 | end 17 | function c = Chroma_r(rgb) 18 | r = rgb(:,:,1); 19 | t = z.Total(rgb); 20 | c = zeros(size(r)); 21 | if any(t(:)) 22 | c(t~=0) = r(t~=0)./t(t~=0); 23 | end 24 | c(t==0) = 0; 25 | end 26 | function c = Chroma_g(rgb) 27 | g = rgb(:,:,2); 28 | t = z.Total(rgb); 29 | c = zeros(size(g)); 30 | if any(t(:)) 31 | c(t~=0) = g(t~=0)./t(t~=0); 32 | end 33 | c(t==0) = 0; 34 | end 35 | function c = Chroma_b(rgb) 36 | b = rgb(:,:,3); 37 | t = z.Total(rgb); 38 | c = zeros(size(b)); 39 | if any(t(:)) 40 | c(t~=0) = b(t~=0)./t(t~=0); 41 | end 42 | c(t==0) = 0; 43 | end 44 | function m = Max(rgb) 45 | m = max(rgb,[],3); 46 | end 47 | function c = MaxChroma(rgb) 48 | m = z.Max(rgb); 49 | t = z.Total(rgb); 50 | c = zeros(size(m)); 51 | if any(t(:)) 52 | c(t~=0) = m(t~=0)./t(t~=0); 53 | end 54 | c(t==0) = 0; 55 | end 56 | function m = Min(rgb) 57 | m = min(rgb,[],3); 58 | end 59 | function t = Total(rgb) 60 | t = sum(rgb,3); 61 | end 62 | end 63 | end 64 | 65 | -------------------------------------------------------------------------------- /Shen2009/Shen2009.m: -------------------------------------------------------------------------------- 1 | function I_d = Shen2009(I) 2 | %Shen2009 I_d = Shen2009(I) 3 | % This method works by finding the largest highlight area, dilating it to 4 | % find the surrounding region, and then finding a coefficient that scales 5 | % how much the pseudo specular component will be subtracted from the 6 | % original image to find I_d. 7 | % 8 | % The nomenclature is in accordance with the corresponding paper with 9 | % exception of using I* instead of V* to denote an image. 10 | % 11 | % See also SIHR, Shen2008, Shen2013. 12 | 13 | assert(isa(I, 'float'), 'SIHR:I:notTypeSingleNorDouble', ... 14 | 'Input I is not type single nor double.') 15 | assert(min(I(:)) >= 0 && max(I(:)) <= 1, 'SIHR:I:notWithinRange', ... 16 | 'Input I is not within [0, 1] range.') 17 | [n_row, n_col, n_ch] = size(I); 18 | assert(n_row > 1 && n_col > 1, 'SIHR:I:singletonDimension', ... 19 | 'Input I has a singleton dimension.') 20 | assert(n_ch == 3, 'SIHR:I:notRGB', ... 21 | 'Input I is not a RGB image.') 22 | 23 | nu = 0.5; 24 | 25 | I = reshape(I, n_row*n_col, 3); 26 | 27 | % Calculate specular-free image 28 | I_min = min(I, [], 2); 29 | T_v = mean(I_min) + nu * std(I_min); 30 | % I_MSF = I - repmat(I_min, 1, 3) .* (I_min > T_v) + T_v * (I_min > T_v); 31 | 32 | % Calculate specular component 33 | beta_s = (I_min - T_v) .* (I_min > T_v) + 0; 34 | 35 | % Estimate largest region of highlight 36 | IHighlight = reshape(beta_s, n_row, n_col, 1); 37 | IHighlight = mat2gray(IHighlight); 38 | IHighlight = im2bw(IHighlight, 0.1); %#ok 39 | IDominantRegion = bwareafilt(IHighlight, 1, 'largest'); 40 | 41 | % Dilate largest region by 5 pixels to obtain its surrounding region 42 | se = strel('square', 5); 43 | ISurroundingRegion = imdilate(IDominantRegion, se); 44 | ISurroundingRegion = logical(imabsdiff(ISurroundingRegion, IDominantRegion)); 45 | 46 | % Solve least squares problem 47 | I_dom = mean(I(IDominantRegion, :)); 48 | I_sur = mean(I(ISurroundingRegion, :)); 49 | beta_dom = mean(beta_s(IDominantRegion, :)); 50 | beta_sur = mean(beta_s(ISurroundingRegion, :)); 51 | k = (I_dom - I_sur) / (beta_dom - beta_sur); 52 | 53 | % Estimate diffuse and specular components 54 | I_d = reshape(I-min(k)*beta_s, n_row, n_col, n_ch); 55 | 56 | end 57 | -------------------------------------------------------------------------------- /Akashi2016/Akashi2016.m: -------------------------------------------------------------------------------- 1 | function I_d = Akashi2016(I) 2 | %Akashi2016 I_d = Akashi2016(I) 3 | % The core mechanism is herein implemented fully in accordance with their 4 | % paper. I have not implemented the repeated score trials. It is pending. 5 | % This is because this method is somewhat slow. 6 | % 7 | % Convergence criterion is based on Yamamoto and Nakazawa (exp(-15)), you 8 | % can change it: 9 | % edit Akashi2016.m 10 | % Search 'eps' and change it. 11 | % 12 | % Also, because of random initialization, I believe averaging the 13 | % estimated diffuse component over many applications of the method will 14 | % greatly improve the quality. 15 | % 16 | % See also SIHR. 17 | 18 | assert(isa(I, 'float'), 'SIHR:I:notTypeSingleNorDouble', ... 19 | 'Input I is not type single nor double.') 20 | assert(min(I(:)) >= 0 && max(I(:)) <= 1, 'SIHR:I:notWithinRange', ... 21 | 'Input I is not within [0, 1] range.') 22 | [n_row, n_col, n_ch] = size(I); 23 | assert(n_row > 1 && n_col > 1, 'SIHR:I:singletonDimension', ... 24 | 'Input I has a singleton dimension.') 25 | assert(n_ch == 3, 'SIHR:I:notRGB', ... 26 | 'Input I is not a RGB image.') 27 | 28 | I = 255 * I; 29 | M = 3; % = number of color channels 30 | N = n_row * n_col; % = number of pixels 31 | R = 7; % = number of color clusters - 1 32 | I = reshape(I, [N, M])'; 33 | i_s = ones(3, 1, class(I)) / sqrt(3); 34 | H = 254 * rand(R, N, class(I)) + 1; 35 | W_d = 254 * rand(3, R-1, class(I)) + 1; 36 | W_d = my_normc(W_d); % W_d ./ vecnorm(W_d, 2, 1); % normalize(W_d,1,'norm'); 37 | W = [i_s, W_d]; 38 | A = ones(M, class(I)); 39 | lambda = 3; 40 | eps = exp(-15); 41 | F_t_1 = Inf; 42 | iter = uint16(0); 43 | max_iter = uint16(10e3); % change for later convergence (takes way longer) 44 | % tic 45 | while true 46 | W_bar = my_normc(W); 47 | H = H .* ((W_bar') * I) ./ ... 48 | ((W_bar') * W_bar * H + lambda); 49 | H_d = H(2:end, :); 50 | Vl = max(0, I-i_s*H(1, :)); 51 | % W_d = W(:,2:end); % not sure 52 | W_d_bar = W_bar(:, 2:end); 53 | W_d = W_d_bar .* (Vl * (H_d') + W_d_bar .* (A * W_d_bar * H_d * (H_d'))) ./ ... 54 | (W_d_bar * H_d * (H_d') + W_d_bar .* (A * Vl * (H_d'))); 55 | W = [i_s, W_d]; 56 | F_t = 0.5 * norm((I - W * H), 'fro') + lambda * sum(H(:)); 57 | if abs(F_t-F_t_1) < eps * abs(F_t) || iter >= max_iter 58 | break 59 | end 60 | F_t_1 = F_t; 61 | iter = iter + 1; 62 | end 63 | % toc 64 | W_d = W(:, 2:end); 65 | % h_s = H(1, :); 66 | H_d = H(2:end, :); 67 | % I_s = i_s * h_s; 68 | I_d = W_d * H_d; 69 | % I_s = reshape(full(I_s)', [n_row, n_col, n_ch]) / 255; 70 | I_d = reshape(full(I_d)', [n_row, n_col, n_ch]) / 255; 71 | 72 | % figure(1), imshow(I_s) 73 | % figure(2), imshow(I_d) 74 | 75 | end 76 | -------------------------------------------------------------------------------- /Tan2005/zIteration.m: -------------------------------------------------------------------------------- 1 | function src = zIteration(src,sfi,epsilon) 2 | [src,count] = zInit(src,sfi,epsilon); 3 | thR = 0.1; thG = 0.1; 4 | nY = size(src.rgb,1); 5 | nX = size(src.rgb,2); 6 | while true 7 | cr = z.Chroma_r(src.rgb(1:nY-1,1:nX-1,:)); 8 | cg = z.Chroma_g(src.rgb(1:nY-1,1:nX-1,:)); 9 | cr_next_x = z.Chroma_r(src.rgb(1:nY-1,2:nX,:)); 10 | cg_next_x = z.Chroma_g(src.rgb(1:nY-1,2:nX,:)); 11 | cr_next_y = z.Chroma_r(src.rgb(2:nY,1:nX-1,:)); 12 | cg_next_y = z.Chroma_g(src.rgb(2:nY,1:nX-1,:)); 13 | drx = cr_next_x - cr; 14 | dgx = cg_next_x - cg; 15 | dry = cr_next_y - cr; 16 | dgy = cg_next_y - cg; 17 | iMaxChroma = z.MaxChroma(src.rgb); 18 | for iY = 1:nY-1 19 | for iX = 1:nX-1 20 | if src.i(iY,iX) == z.CAMERA_DARK 21 | continue 22 | end 23 | if src.i(iY,iX) == z.SPECULARX 24 | if abs(drx(iY,iX)) > thR && abs(dgx(iY,iX)) > thG 25 | src.i(iY,iX) = z.BOUNDARY; 26 | continue 27 | end 28 | if abs(iMaxChroma(iY,iX) - iMaxChroma(iY,iX+1)) < 0.01 29 | src.i(iY,iX) = z.NOISE; 30 | continue 31 | end 32 | if iMaxChroma(iY,iX) < iMaxChroma(iY,iX+1) 33 | [src.rgb(iY,iX,:),src.i(iY,iX)] = zSpecular2Diffuse(src.rgb(iY,iX,:),src.i(iY,iX),iMaxChroma(iY,iX+1)); 34 | src.i(iY,iX) = z.DIFFUSE; 35 | src.i(iY,iX+1) = z.DIFFUSE; 36 | else 37 | [src.rgb(iY,iX+1,:),src.i(iY,iX+1)] = zSpecular2Diffuse(src.rgb(iY,iX+1,:),src.i(iY,iX+1),iMaxChroma(iY,iX)); 38 | src.i(iY,iX) = z.DIFFUSE; 39 | src.i(iY,iX+1) = z.DIFFUSE; 40 | end 41 | end 42 | % 43 | if src.i(iY,iX) == z.SPECULARY 44 | if abs(dry(iY,iX)) > thR && abs(dgy(iY,iX)) > thG 45 | src.i(iY,iX) = z.BOUNDARY; 46 | continue 47 | end 48 | if abs(iMaxChroma(iY,iX) - iMaxChroma(iY+1,iX)) < 0.01 49 | src.i(iY,iX) = z.NOISE; 50 | continue 51 | end 52 | if iMaxChroma(iY,iX) < iMaxChroma(iY+1,iX) 53 | [src.rgb(iY,iX,:),src.i(iY,iX)] = zSpecular2Diffuse(src.rgb(iY,iX,:),src.i(iY,iX),iMaxChroma(iY+1,iX)); 54 | src.i(iY,iX) = z.DIFFUSE; 55 | src.i(iY+1,iX) = z.DIFFUSE; 56 | else 57 | [src.rgb(iY+1,iX,:),src.i(iY+1,iX)] = zSpecular2Diffuse(src.rgb(iY+1,iX,:),src.i(iY+1,iX),iMaxChroma(iY,iX)); 58 | src.i(iY,iX) = z.DIFFUSE; 59 | src.i(iY+1,iX) = z.DIFFUSE; 60 | end 61 | end 62 | end 63 | end 64 | pcount = count; 65 | [src,count] = zInit(src,sfi,epsilon); 66 | if count < 0 67 | break 68 | end 69 | if pcount <= count 70 | break 71 | end 72 | end 73 | src = zResetLabels(src); 74 | end 75 | 76 | -------------------------------------------------------------------------------- /SIHR.m: -------------------------------------------------------------------------------- 1 | function SIHR 2 | %SIHR Session path setup 3 | % SIHR is a repository for the development and implementation of single 4 | % image highlight removal methods. 5 | % 6 | % These methods are not the official implementations!!! This project has 7 | % the objective of reproducing these methods for facilitating research in 8 | % this specific subject. If you authored a method and would like to officially 9 | % make it available here instead of my implementation, please contact me, 10 | % I am happy to do so. 11 | % 12 | % Project page: 13 | % https://github.com/vitorsr/SIHR 14 | % 15 | % Usage: 16 | % SIHR % run it for a one-time session path setup 17 | % 18 | % API: 19 | % J = im2double(imread('toys.ppm')); % input image 20 | % J_d = Yang2010(J); % call AuthorYEAR method 21 | % % e.g. Yang2010 22 | % imshow([J, J_d, J - J_d]) % display result 23 | % 24 | % See below for available methods. 25 | % 26 | % See also Tan2005 [1], Yoon2006 [2], Shen2008 [3], Shen2009 [4], 27 | % Yang2010 [5], Shen2013 [6], Akashi2016 [7], Yamamoto2019 [8]. 28 | % 29 | %[1] R. T. Tan and K. Ikeuchi, "Separating reflection components of textured surfaces using a single image," IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 27, no. 2, pp. 178-193, Feb. 2005. 30 | %[2] K. Yoon, Y. Choi, and I. S. Kweon, "Fast Separation of Reflection Components using a Specularity-Invariant Image Representation," in 2006 International Conference on Image Processing, 2006. 31 | %[3] H.-L. Shen, H.-G. Zhang, S.-J. Shao, and J. H. Xin, "Chromaticity-based separation of reflection components in a single image," Pattern Recognition, vol. 41, no. 8, pp. 2461-2469, Aug. 2008. 32 | %[4] H.-L. Shen and Q.-Y. Cai, "Simple and efficient method for specularity removal in an image," Applied Optics, vol. 48, no. 14, p. 2711, May 2009. 33 | %[5] Q. Yang, S. Wang, and N. Ahuja, "Real-Time Specular Highlight Removal Using Bilateral Filtering," in Computer Vision - ECCV 2010, Springer Berlin Heidelberg, 2010, pp. 87-100. 34 | %[6] H.-L. Shen and Z.-H. Zheng, "Real-time highlight removal using intensity ratio," Applied Optics, vol. 52, no. 19, p. 4483, Jun. 2013. 35 | %[7] Y. Akashi and T. Okatani, "Separation of reflection components by sparse non-negative matrix factorization," Computer Vision and Image Understanding, vol. 146, pp. 77-85, May 2016. 36 | %[8] T. Yamamoto and A. Nakazawa, "General Improvement Method of Specular Component Separation Using High-Emphasis Filter and Similarity Function," ITE Transactions on Media Technology and Applications, vol. 7, no. 2, pp. 92-102, 2019. 37 | 38 | my_foldername = fileparts(which('SIHR.m')); % this file 39 | my_path = strsplit(genpath(my_foldername), pathsep); 40 | N = numel(my_foldername) + numel([filesep, '.git']); % delete .\.git\* 41 | 42 | for i = 1:length(my_path) 43 | if strncmp(my_path{i}, [my_foldername, filesep, '.git'], N) 44 | my_path{i} = []; 45 | end 46 | end 47 | 48 | my_path = my_path(~cellfun('isempty', my_path)); 49 | my_path = strjoin(my_path, pathsep); 50 | 51 | addpath(my_path, '-end') 52 | 53 | if (is_octave) % adds octave image path 54 | my_val = IMAGE_PATH; 55 | IMAGE_PATH([my_val, pathsep, my_foldername, filesep, 'images']) 56 | end 57 | 58 | if (is_octave) 59 | assert(isempty(pkg('list', 'image')) == 0) % && ... 60 | % isempty(pkg('list', 'statistics')) == 0) 61 | pkg unload image % statistics 62 | pkg load image % statistics 63 | else 64 | assert(isequal(license('test', 'image_toolbox'), 1)) % && ... 65 | % isequal(license('test', 'statistics_toolbox'), 1)) 66 | end 67 | 68 | end 69 | 70 | function r = is_octave() % https://wiki.octave.org/Compatibility 71 | persistent x; 72 | if (isempty(x)) 73 | x = exist('OCTAVE_VERSION', 'builtin'); 74 | end 75 | r = x; 76 | end 77 | -------------------------------------------------------------------------------- /Yoon2006/Yoon2006.m: -------------------------------------------------------------------------------- 1 | function I_d = Yoon2006(I) 2 | %Yoon2006 I_d = Yoon2006(I) 3 | % This method operates by iteratively reducing or increasing a 4 | % neighboring pixel intensity. 5 | % 6 | % It is ambiguous how they define the neighborhood in which they operate, so I 7 | % have assumed they mean the next pixel when the image is represented in a 8 | % lexicographic order. 9 | % 10 | % This method is specially slow in Octave because of the repeated iterated 11 | % accesses. Should take 1 s, but takes 10+ on Octave/Linux and 30+ on 12 | % Octave/Windows. Ideally it should be made into a vectorized 13 | % implementation. 14 | % 15 | % If you spot any obvious mistakes, please inform me of them, as I have 16 | % made some special considerations in the implementation in order to avoid 17 | % propagating unreliable pixels. 18 | % 19 | % See also SIHR, Tan2005. 20 | 21 | assert(isa(I, 'float'), 'SIHR:I:notTypeSingleNorDouble', ... 22 | 'Input I is not type single nor double.') 23 | assert(min(I(:)) >= 0 && max(I(:)) <= 1, 'SIHR:I:notWithinRange', ... 24 | 'Input I is not within [0, 1] range.') 25 | [n_row, n_col, n_ch] = size(I); 26 | assert(n_row > 1 && n_col > 1, 'SIHR:I:singletonDimension', ... 27 | 'Input I has a singleton dimension.') 28 | assert(n_ch == 3, 'SIHR:I:notRGB', ... 29 | 'Input I is not a RGB image.') 30 | 31 | I = reshape(I, [n_row * n_col, n_ch]); 32 | I_min = min(I, [], 2); 33 | I_sf = I - I_min; 34 | 35 | sum_I_sf_ = sum(I_sf, 2); 36 | 37 | cr = I_sf(:, 1) ./ sum_I_sf_; 38 | cg = I_sf(:, 2) ./ sum_I_sf_; 39 | 40 | cr(isnan(cr)) = 0; 41 | cg(isnan(cg)) = 0; 42 | 43 | skip = false([n_row * n_col - 1, 1]); 44 | 45 | % chroma threshold values (color discontinuity) 46 | th_r = 0.05; 47 | th_g = 0.05; 48 | 49 | % iterate until only diffuse pixels are left 50 | count = uint32(0); 51 | iter = uint16(0); 52 | 53 | idx = (1:(n_row * n_col - 1))'; 54 | skip(sum_I_sf_(idx) == 0 ... % check if sum along rows ~= 0 55 | | sum_I_sf_(idx+1) == 0) = true; 56 | skip(abs(cr(idx)-cr(idx+1)) > th_r ... % check discontinuities 57 | | abs(cg(idx)-cg(idx+1)) > th_g) = true; 58 | skip((1:n_row-1)*n_col) = true; 59 | skip(I_min(idx) < 12/255) = true; 60 | 61 | rd = ones([n_row * n_col - 1, 1]); 62 | rd(idx(~skip)) = sum(I_sf(idx(~skip), :), 2) ... 63 | ./ sum(I_sf(idx(~skip)+1, :), 2); 64 | 65 | rds = ones([n_row * n_col - 1, 1]); 66 | 67 | while true 68 | rds(idx(~skip)) = sum(I((idx(~skip)), :), 2) ... 69 | ./ sum(I((idx(~skip))+1, :), 2); 70 | for x1 = 1:n_row * n_col - 1 71 | x2 = x1 + 1; 72 | if skip(x1) 73 | continue 74 | elseif sum(I(x1,:), 2) == 0 || ... 75 | sum(I(x2,:), 2) == 0 % || ... 76 | % (abs(cr_col(x1)-cr_col(x2)) > th_r && ... 77 | % abs(cg_col(x1)-cg_col(x2)) > th_g) 78 | skip(x1) = true; 79 | continue 80 | end 81 | % compare ratios and decrease intensity 82 | if rds(x1) > rd(x1) % && rds ~= 1 83 | m = sum(I(x1, :), 2) - rd(x1) * sum(I(x2, :), 2); 84 | if m < 1e-3 85 | continue 86 | end 87 | I(x1, :) = I(x1, :)-m/3; 88 | %skip(x1) = true; 89 | count = count + 1; 90 | elseif rds(x1) < rd(x1) % && rd(x1) ~= 1 91 | m = sum(I(x2, :), 2) - sum(I(x1, :), 2) / rd(x1); 92 | if m < 1e-3 93 | continue 94 | end 95 | I(x2, :) = I(x2, :)-m/3; 96 | %skip(x2) = true; 97 | count = count + 1; 98 | end 99 | end 100 | if count == 0 || iter == 1000 101 | break 102 | end 103 | count = 0; 104 | iter = iter + 1; 105 | end 106 | 107 | I_d = reshape(I, [n_row, n_col, n_ch]); 108 | 109 | end 110 | -------------------------------------------------------------------------------- /paper/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'SIHR: a MATLAB/GNU Octave toolbox for single image highlight removal' 3 | tags: 4 | - blind source separation 5 | - feature extraction 6 | - gnu octave 7 | - image color analysis 8 | - image enhancement 9 | - image processing 10 | - image texture analysis 11 | - matlab 12 | - octave 13 | authors: 14 | - name: Vítor S. Ramos 15 | orcid: 0000-0002-7583-5577 16 | affiliation: 1 17 | affiliations: 18 | - name: Federal University of Rio Grande do Norte, Natal, Brazil 19 | index: 1 20 | date: 23 November 2019 21 | bibliography: paper.bib 22 | --- 23 | 24 | # Summary 25 | 26 | Single image highlight removal (SIHR) refers to an open problem in computer vision concerning the separation of diffuse and specular reflection components from a single image [@Tan2014]. Briefly, the diffuse component refers to a generally low-parametric reflection that takes the color of the body, while the specular component refers to a higher-frequency reflection that takes the color of the illuminant. Under the dichromatic reflection model [@Shafer1985], they are linearly additive. It is an intrinsic image decomposition, so it has several applications. Recently, there was an interest renewal in this problem for the objective of image enhancement in visual display systems such as TVs [@Yamamoto2017]. 27 | 28 | ![Example decomposition. (a\) Input, (b\) diffuse, and (c\) specular reflection components](figures/example.png) 29 | 30 | The primary objective of this toolbox is to serve as an aid for ongoing research and development of SIHR methods. Being written in such a high-level language that is MATLAB/GNU Octave allows an easier understanding of the inner workings of these methods. To the best of our knowledge, the resources available to further the understanding of this specific problem are relatively scarce. 31 | 32 | Hence, we have started ``SIHR`` to implement and gather several different methods from technical literature--starting with the most computationally efficient ones, since the abovementioned systems operate on a limited computing budget and need timely processing. Other methods of interest can be found in recent surveys [@Artusi2011; @Khan2017]. 33 | 34 | Usage is rather straightforward as the focus of these methods is to work with only a single linear RGB image, i.e. an *m*×*n*×3 matrix. For uniformity, we ask the image to be double-valued. In ``SIHR``, calls are simply ``I_d = AuthorYEAR(I);``, in which ``I`` is the original image and ``I_d`` is the diffuse component estimate calculated by the ``AuthorYEAR`` method. The specular component is ``I_s = I - I_d;``. 35 | 36 | At the time of writing, the methods listed in Table 1 are available. We refer to the ``SIHR`` documentation for the latest list of methods available. 37 | 38 | | Function | Method | PSNR (dB) | SSIM | Runtime (s) | 39 | |:-------------:|:-------------:|:---------:|:-----:|:-----------:| 40 | | `Tan2005` | @Tan2005 | 29.5 | 0.888 | 160 | 41 | | `Yoon2006` | @Yoon2006 | 34.2 | 0.964 | 2.6 | 42 | | `Shen2008` | @Shen2008 | 35.5 | 0.960 | 4.7 | 43 | | `Shen2009` | @Shen2009 | 35.8 | 0.970 | 0.26 | 44 | | `Yang2010` | @Yang2010 | 35.9 | 0.925 | 0.16 | 45 | | `Shen2013` | @Shen2013 | 36.7 | 0.960 | 0.063 | 46 | | `Akashi2016` | @Akashi2016 | 34.3 | 0.856 | 180 | 47 | 48 | Table: List of methods in `SIHR` 49 | 50 | | Function | Method | 51 | |:--------------:|:-------------:| 52 | | `Yamamoto2019` | @Yamamoto2019 | 53 | 54 | Table: List of improvements in `SIHR` 55 | 56 | Figure 1 presents an actual result of a method from technical literature which was implemented in ``SIHR``. In Table 1, the reproduced metrics are from the @Shen2013 dataset, and were averaged--for each metric--for all four images. 57 | 58 | ``SIHR`` aims to be a continuous project and welcomes community contributions. 59 | 60 | # References 61 | -------------------------------------------------------------------------------- /Yamamoto2019/Yamamoto2019.m: -------------------------------------------------------------------------------- 1 | function I_d = Yamamoto2019(I, AuthorYEAR) 2 | %Yamamoto2019 I_d = Yamamoto2019(I, AuthorYEAR) 3 | % This function enhances the diff. comp. I_d obtained by AuthorYEAR method. 4 | % 5 | % AuthorYEAR is optional (defaults to Shen2013), but can also be specified 6 | % as a valid function handle, e.g. @Yang2010, @Shen2008. 7 | % 8 | % Example: 9 | % J = im2double(imread('toys.ppm')); 10 | % J_d = Yamamoto2019(J, @Yoon2006); 11 | % imshow(J_d) 12 | % 13 | % There were no major ambiguities in my interpretation of the corresponding 14 | % paper. 15 | % 16 | % There's only a mention of applying SVD to obtain the specular component 17 | % after calculating a diffuse component by applying an existing method. 18 | % However, all operations regard $\beta S$, i.e. I_s which is I - I_d under 19 | % DRM assumption (that they're linearly additive). Hence, we skip this 20 | % step (Part 2). 21 | % 22 | % See also SIHR, Yang2010, Shen2013. 23 | 24 | if nargin == 2 25 | if isempty(AuthorYEAR) 26 | AuthorYEAR = @Shen2013; 27 | else 28 | assert(isa(AuthorYEAR, 'function_handle'), 'SIHR:I:notTypeFunctionHandle', ... 29 | 'Input AuthorYEAR is not of type function_handle.') 30 | my_f = functions(AuthorYEAR); 31 | if isempty(my_f.file) 32 | warning(['Undefined function ''', func2str(AuthorYEAR), '''.', sprintf('\n'), ... 33 | ' Defaulting to ''Shen2013''.']) 34 | AuthorYEAR = @Shen2013; 35 | end 36 | end 37 | elseif nargin == 1 38 | AuthorYEAR = @Shen2013; 39 | end 40 | 41 | [n_row, n_col, ~] = size(I); 42 | 43 | % DRM: I = I_d + I_s 44 | I_d = feval(AuthorYEAR, I); 45 | I_s = my_clip(I-I_d, 0, 1); 46 | 47 | I_d_m_1 = I_d; 48 | % I_d_init = I_d; % %DEBUGVAR% 49 | 50 | % Table 1 51 | omega = 0.3; 52 | k = 10; 53 | epsilon = 0.2; % RMSE convergence criteria 54 | iter_count = uint8(0); 55 | max_iter_count = uint8(5); 56 | 57 | H_low = fspecial('average', 3); 58 | H_h_emph = -k * H_low; 59 | H_h_emph(2, 2) = 1 + k - k * H_low(2, 2); 60 | Theta = my_clip(imfilter(I, H_h_emph, 'symmetric'), 0, 1); 61 | 62 | while true 63 | Upsilon_d = my_clip(imfilter(I_d, H_h_emph, 'symmetric'), 0, 1); 64 | Upsilon_s = my_clip(imfilter(I_s, H_h_emph, 'symmetric'), 0, 1); 65 | Upsilon = my_clip(Upsilon_d+Upsilon_s, 0, 1); 66 | 67 | err_diff = sum(double(Upsilon_d > Theta), 3) >= 3; % 1 68 | 69 | counts = imhist(I_s(:, :, 1)); 70 | I_s_bin = im2bw(I_s, otsuthresh(counts)); %#ok 71 | N_s = nnz(I_s_bin); 72 | if N_s == 0 73 | break 74 | end 75 | N_s = 2 * ceil(sqrt(N_s)/2) + 1; 76 | % N_s = 3; 77 | center = floor(([N_s, N_s] + 1)/2); 78 | 79 | [row, col] = ind2sub(size(err_diff), find(err_diff)); 80 | 81 | offset_r = center(1) - 1; 82 | offset_c = center(2) - 1; 83 | 84 | for ind = 1:nnz(err_diff) 85 | nh_r = max(1, row(ind)-offset_r):min(row(ind)+offset_r, n_row); 86 | nh_c = max(1, col(ind)-offset_c):min(col(ind)+offset_c, n_col); 87 | 88 | nh_I = reshape(I(nh_r, nh_c, :), [], 3); 89 | nh_Theta = reshape(Theta(nh_r, nh_c, :), [], 3); 90 | nh_Upsilon = reshape(Upsilon(nh_r, nh_c, :), [], 3); 91 | 92 | center_p = reshape(I(row(ind), col(ind), :), [], 3); 93 | 94 | Phi_I = sum((center_p - nh_I).^2, 2); 95 | Phi_Th_Up = sum((nh_Theta - nh_Upsilon).^2, 2); 96 | 97 | Phi = omega * Phi_I + (1 - omega) * Phi_Th_Up; 98 | 99 | [~, plausible] = min(Phi(:)); 100 | [p_row, p_col] = ind2sub([length(nh_r), length(nh_c)], plausible); 101 | 102 | I_d(row(ind), col(ind), :) = ... 103 | I_d_m_1(nh_r(p_row), ... 104 | nh_c(p_col), :); 105 | end 106 | 107 | iter_count = iter_count + 1; 108 | 109 | I_s = my_clip(I-I_d, 0, 1); 110 | 111 | if sqrt(immse(I_d, I_d_m_1)) < epsilon || ... 112 | iter_count >= max_iter_count 113 | break 114 | end 115 | 116 | I_d_m_1 = I_d; 117 | end 118 | 119 | % figure(1), imshow([I_d_init, I_d]) 120 | % figure(2), imshow(10*abs(I_d-I_d_init)) 121 | 122 | end 123 | -------------------------------------------------------------------------------- /Shen2013/Shen2013.m: -------------------------------------------------------------------------------- 1 | function I_d = Shen2013(I) 2 | %Shen2013 I_d = Shen2013(I) 3 | % You can optionally edit the code to use kmeans instead of the clustering 4 | % function proposed by the author. 5 | % 6 | % This method should have equivalent functionality as 7 | % `sp_removal.cpp` distributed by the author. 8 | % 9 | % See also SIHR, Shen2008, Shen2009. 10 | 11 | assert(isa(I, 'float'), 'SIHR:I:notTypeSingleNorDouble', ... 12 | 'Input I is not type single nor double.') 13 | assert(min(I(:)) >= 0 && max(I(:)) <= 1, 'SIHR:I:notWithinRange', ... 14 | 'Input I is not within [0, 1] range.') 15 | [n_row, n_col, n_ch] = size(I); 16 | assert(n_row > 1 && n_col > 1, 'SIHR:I:singletonDimension', ... 17 | 'Input I has a singleton dimension.') 18 | assert(n_ch == 3, 'SIHR:I:notRGB', ... 19 | 'Input I is not a RGB image.') 20 | 21 | height = size(I, 1); 22 | width = size(I, 2); 23 | I = reshape(I, [height * width, 3]); 24 | 25 | Imin = min(I, [], 2); 26 | Imax = max(I, [], 2); 27 | Iran = Imax - Imin; 28 | 29 | umin_val = mean2(Imin); 30 | 31 | Imask = Imin > umin_val; 32 | 33 | Ich_pseudo = zeros([height * width, 2]); 34 | frgb = zeros([height * width, 3]); 35 | crgb = frgb; 36 | srgb = zeros([height * width, 1]); 37 | 38 | frgb(Imask, :) = I(Imask, :) - Imin(Imask) + umin_val; 39 | srgb(Imask) = sum(frgb(Imask, :), 2); 40 | crgb(Imask, :) = frgb(Imask, :) ./ srgb(Imask); 41 | 42 | Ich_pseudo(Imask, 1) = min(min(crgb(Imask, 1), crgb(Imask, 2)), crgb(Imask, 3)); 43 | Ich_pseudo(Imask, 2) = max(max(crgb(Imask, 1), crgb(Imask, 2)), crgb(Imask, 3)); 44 | 45 | % num_clust = 3; 46 | % Iclust = zeros([height * width, 1]); 47 | % Iclust(Imask) = kmeans([Ich_pseudo(Imask, 1), Ich_pseudo(Imask, 2)], num_clust, 'Distance', 'cityblock', 'Replicates', ceil(sqrt(num_clust))); 48 | th_chroma = 0.3; 49 | [Iclust, num_clust] = pixel_clustering(Ich_pseudo, Imask, width, height, th_chroma); 50 | 51 | ratio = zeros([height * width, 1]); 52 | Iratio = zeros([height * width, 1]); 53 | 54 | N = width * height; 55 | EPS = 1e-10; 56 | th_percent = 0.5; 57 | 58 | for k = 1:num_clust 59 | num = 0; 60 | for i = 1:N 61 | if (Iclust(i) == k && Iran(i) > umin_val) 62 | ratio(num+1) = Imax(i) / (Iran(i) + EPS); 63 | num = num + 1; 64 | end 65 | end 66 | 67 | if num == 0 68 | continue 69 | end 70 | 71 | tmp = sort(ratio(1:num)); 72 | ratio_est = tmp(round(num*th_percent)+1); 73 | 74 | for i = 1:N 75 | if (Iclust(i) == k) 76 | Iratio(i) = ratio_est; 77 | end 78 | end 79 | end 80 | 81 | I_s = zeros([height * width, 1]); 82 | I_d = I; 83 | 84 | for i = 1:N 85 | if (Imask(i) == 1) 86 | uvalue = (Imax(i) - Iratio(i) * Iran(i)); % round( . ) 87 | I_s(i) = max(uvalue, 0); 88 | fvalue = I(i, 1) - I_s(i); 89 | I_d(i, 1) = (clip(fvalue, 0, 1)); % round 90 | fvalue = I(i, 2) - I_s(i); 91 | I_d(i, 2) = (clip(fvalue, 0, 1)); % round 92 | fvalue = I(i, 3) - I_s(i); 93 | I_d(i, 3) = (clip(fvalue, 0, 1)); % round 94 | end 95 | end 96 | 97 | % I_s = reshape(I_s, [height, width]); 98 | I_d = reshape(I_d, [height, width, 3]); 99 | 100 | end 101 | 102 | 103 | function [Iclust, num_clust] = pixel_clustering(Ich_pseudo, Imask, width, height, th_chroma) 104 | MAX_NUM_CLUST = 100; 105 | 106 | label = 0; 107 | c = zeros([2, 1]); 108 | 109 | clust_mean = zeros([MAX_NUM_CLUST, 2]); 110 | num_pixel = zeros([MAX_NUM_CLUST, 1]); 111 | 112 | N = width * height; 113 | 114 | Idone = zeros([height * width, 1], 'logical'); 115 | Iclust = zeros([height * width, 1], 'uint8'); 116 | 117 | for i = 1:N 118 | if (Idone(i) == 0 && Imask(i) == 1) 119 | c(1) = Ich_pseudo(i, 1); 120 | c(2) = Ich_pseudo(i, 2); 121 | label = label + 1; 122 | for j = i:N 123 | if (Idone(j) == 0 && Imask(j) == 1) 124 | dist = abs(c(1)-Ich_pseudo(j, 1)) + abs(c(2)-Ich_pseudo(j, 2)); 125 | if (dist < th_chroma) 126 | Idone(j) = 1; 127 | Iclust(j) = label; 128 | end 129 | end 130 | end 131 | end 132 | end 133 | 134 | num_clust = label; 135 | 136 | if num_clust > MAX_NUM_CLUST 137 | return 138 | end 139 | 140 | for i = 1:N 141 | k = Iclust(i); 142 | if (k >= 1 && k <= num_clust) 143 | num_pixel(k) = num_pixel(k) + 1; 144 | clust_mean(k, 1) = clust_mean(k, 1) + Ich_pseudo(i, 1); 145 | clust_mean(k, 2) = clust_mean(k, 2) + Ich_pseudo(i, 2); 146 | end 147 | end 148 | 149 | for k = 1:num_clust 150 | clust_mean(k, 1) = clust_mean(k, 1) / num_pixel(k); 151 | clust_mean(k, 2) = clust_mean(k, 2) / num_pixel(k); 152 | end 153 | 154 | for i = 1:N 155 | if Imask(i) == 1 156 | c(1) = Ich_pseudo(i, 1); 157 | c(2) = Ich_pseudo(i, 2); 158 | dist_min = abs(c(1)-clust_mean(2, 1)) + abs(c(2)-clust_mean(2, 2)); 159 | label = 1; 160 | for k = 2:num_clust 161 | dist = abs(c(1)-clust_mean(k, 1)) + abs(c(2)-clust_mean(k, 2)); 162 | if (dist < dist_min) 163 | dist_min = dist; 164 | label = k; 165 | end 166 | end 167 | Iclust(i) = label; 168 | end 169 | end 170 | 171 | end 172 | 173 | function y = clip(x, lb, ub) 174 | y = min(ub, max(lb, x)); 175 | end 176 | -------------------------------------------------------------------------------- /paper/paper.bib: -------------------------------------------------------------------------------- 1 | @article{Shafer1985, 2 | doi = {10.1002/col.5080100409}, 3 | url = {https://doi.org/10.1002/col.5080100409}, 4 | year = {1985}, 5 | publisher = {Wiley}, 6 | volume = {10}, 7 | number = {4}, 8 | pages = {210--218}, 9 | author = {Steven A. Shafer}, 10 | title = {Using color to separate reflection components}, 11 | journal = {Color Research {\&} Application} 12 | } 13 | 14 | @incollection{Tan2014, 15 | doi = {10.1007/978-0-387-31439-6_538}, 16 | url = {https://doi.org/10.1007/978-0-387-31439-6_538}, 17 | year = {2014}, 18 | publisher = {Springer {US}}, 19 | pages = {750--752}, 20 | author = {Robby T. Tan}, 21 | title = {Specularity, Specular Reflectance}, 22 | booktitle = {Computer Vision} 23 | } 24 | 25 | @inproceedings{Yamamoto2017, 26 | doi = {10.1109/icip.2017.8297078}, 27 | url = {https://doi.org/10.1109/icip.2017.8297078}, 28 | year = {2017}, 29 | month = sep, 30 | publisher = {{IEEE}}, 31 | author = {Takahisa Yamamoto and Toshihiro Kitajima and Ryota Kawauchi}, 32 | title = {Efficient improvement method for separation of reflection components based on an energy function}, 33 | booktitle = {2017 {IEEE} International Conference on Image Processing ({ICIP})} 34 | } 35 | 36 | @article{Artusi2011, 37 | doi = {10.1111/j.1467-8659.2011.01971.x}, 38 | url = {https://doi.org/10.1111/j.1467-8659.2011.01971.x}, 39 | year = {2011}, 40 | month = aug, 41 | publisher = {Wiley}, 42 | volume = {30}, 43 | number = {8}, 44 | pages = {2208--2230}, 45 | author = {Alessandro Artusi and Francesco Banterle and Dmitry Chetverikov}, 46 | title = {A Survey of Specularity Removal Methods}, 47 | journal = {Computer Graphics Forum} 48 | } 49 | 50 | @incollection{Khan2017, 51 | doi = {10.1007/978-3-319-56010-6_17}, 52 | url = {https://doi.org/10.1007/978-3-319-56010-6_17}, 53 | year = {2017}, 54 | publisher = {Springer International Publishing}, 55 | pages = {197--208}, 56 | author = {Haris Ahmad Khan and Jean-Baptiste Thomas and Jon Yngve Hardeberg}, 57 | title = {Analytical Survey of Highlight Detection in Color and Spectral Images}, 58 | booktitle = {Lecture Notes in Computer Science} 59 | } 60 | 61 | @article{Tan2005, 62 | doi = {10.1109/tpami.2005.36}, 63 | url = {https://doi.org/10.1109/tpami.2005.36}, 64 | year = {2005}, 65 | month = feb, 66 | publisher = {Institute of Electrical and Electronics Engineers ({IEEE})}, 67 | volume = {27}, 68 | number = {2}, 69 | pages = {178--193}, 70 | author = {R.T. Tan and K. Ikeuchi}, 71 | title = {Separating reflection components of textured surfaces using a single image}, 72 | journal = {{IEEE} Transactions on Pattern Analysis and Machine Intelligence} 73 | } 74 | 75 | @inproceedings{Yoon2006, 76 | doi = {10.1109/icip.2006.312650}, 77 | url = {https://doi.org/10.1109/icip.2006.312650}, 78 | year = {2006}, 79 | month = oct, 80 | publisher = {{IEEE}}, 81 | author = {Kuk-jin Yoon and Yoojin Choi and In So Kweon}, 82 | title = {Fast Separation of Reflection Components using a Specularity-Invariant Image Representation}, 83 | booktitle = {2006 International Conference on Image Processing} 84 | } 85 | 86 | @article{Shen2008, 87 | doi = {10.1016/j.patcog.2008.01.026}, 88 | url = {https://doi.org/10.1016/j.patcog.2008.01.026}, 89 | year = {2008}, 90 | month = aug, 91 | publisher = {Elsevier {BV}}, 92 | volume = {41}, 93 | number = {8}, 94 | pages = {2461--2469}, 95 | author = {Hui-Liang Shen and Hong-Gang Zhang and Si-Jie Shao and John H. Xin}, 96 | title = {Chromaticity-based separation of reflection components in a single image}, 97 | journal = {Pattern Recognition} 98 | } 99 | 100 | @article{Shen2009, 101 | doi = {10.1364/ao.48.002711}, 102 | url = {https://doi.org/10.1364/ao.48.002711}, 103 | year = {2009}, 104 | month = may, 105 | publisher = {The Optical Society}, 106 | volume = {48}, 107 | number = {14}, 108 | pages = {2711}, 109 | author = {Hui-Liang Shen and Qing-Yuan Cai}, 110 | title = {Simple and efficient method for specularity removal in an image}, 111 | journal = {Applied Optics} 112 | } 113 | 114 | @incollection{Yang2010, 115 | doi = {10.1007/978-3-642-15561-1_7}, 116 | url = {https://doi.org/10.1007/978-3-642-15561-1_7}, 117 | year = {2010}, 118 | publisher = {Springer Berlin Heidelberg}, 119 | pages = {87--100}, 120 | author = {Qingxiong Yang and Shengnan Wang and Narendra Ahuja}, 121 | title = {Real-Time Specular Highlight Removal Using Bilateral Filtering}, 122 | booktitle = {Computer Vision {\textendash} {ECCV} 2010} 123 | } 124 | 125 | @article{Shen2013, 126 | doi = {10.1364/ao.52.004483}, 127 | url = {https://doi.org/10.1364/ao.52.004483}, 128 | year = {2013}, 129 | month = jun, 130 | publisher = {The Optical Society}, 131 | volume = {52}, 132 | number = {19}, 133 | pages = {4483}, 134 | author = {Hui-Liang Shen and Zhi-Huan Zheng}, 135 | title = {Real-time highlight removal using intensity ratio}, 136 | journal = {Applied Optics} 137 | } 138 | 139 | @article{Akashi2016, 140 | doi = {10.1016/j.cviu.2015.09.001}, 141 | url = {https://doi.org/10.1016/j.cviu.2015.09.001}, 142 | year = {2016}, 143 | month = may, 144 | publisher = {Elsevier {BV}}, 145 | volume = {146}, 146 | pages = {77--85}, 147 | author = {Yasushi Akashi and Takayuki Okatani}, 148 | title = {Separation of reflection components by sparse non-negative matrix factorization}, 149 | journal = {Computer Vision and Image Understanding} 150 | } 151 | 152 | @article{Yamamoto2019, 153 | doi = {10.3169/mta.7.92}, 154 | url = {https://doi.org/10.3169/mta.7.92}, 155 | year = {2019}, 156 | publisher = {Institute of Image Information and Television Engineers}, 157 | volume = {7}, 158 | number = {2}, 159 | pages = {92--102}, 160 | author = {Takahisa Yamamoto and Atsushi Nakazawa}, 161 | title = {General Improvement Method of Specular Component Separation Using High-Emphasis Filter and Similarity Function}, 162 | journal = {{ITE} Transactions on Media Technology and Applications} 163 | } 164 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | [![Maintenance](https://img.shields.io/maintenance/yes/2019)](https://github.com/vitorsr/SIHR/graphs/contributors) 4 | [![GitHub last commit](https://img.shields.io/github/last-commit/vitorsr/SIHR)](https://github.com/vitorsr/SIHR/commits/master) 5 | 6 | **TL;DR: send contributions through PRs. Please try to use matrix operations in your implementations. Create an issue for problems and support.** 7 | 8 | This document has the purpose of informing how to effectively contribute to this repository. 9 | 10 | This repository (`SIHR`) is intented for reproduction and development of single image highlight removal methods, so the biggest challenge isn't even coding, rather, reading the related research papers and *deciphering* how to implement them. 11 | 12 | So, please don't feel overwhelmed by the amount of minute detail on contributing, it's mostly to give an overview should you need additional information on general MATLAB/GNU Octave development. 13 | 14 | There is a number of ways you can help, and it's not restricted to code contributions. 15 | 16 | In the first section (below) I'll put down a few words on non-code contributions. 17 | 18 | *Note: since deep learning models typically require a whole project for a complete "reproducibility suite", these will not be **initially** accounted for in this repository. In addition, they are outside the **current** domain of application for these methods (image enhancement for visual display systems). On a future version, perhaps.* 19 | 20 | ## Call for collaboration 21 | 22 | If you'd like to collaborate on a survey on this specific subject, please contact me. You can find contact information [here](#contact). I have set up a SIHR interest group on [Mendeley ![Mendeley](https://cdn1.iconfinder.com/data/icons/simple-icons/16/mendeley-16-black.png)](https://www.mendeley.com/community/sihr/) for this purpose. 23 | 24 | 25 | 26 | ## Issues 27 | 28 | [![GitHub issues](https://img.shields.io/github/issues/vitorsr/SIHR)](https://github.com/vitorsr/SIHR/issues) 29 | 30 | Since we're a small community and this is a rather small repository, feel free to create an issue with any comments on bugs, improvements, compatibility problems, etc. and I will get to them eventually (or hopefully someone else will first). 31 | 32 | ## Testing 33 | 34 | As the methods are single-input, single-output, general testing and functionality verification is done by simply invoking the methods' functions with a valid input. 35 | 36 | Additionally on a general note, I expect that new methods contributed fulfill the `I_d = AuthorYEAR(I);` command, `I` being any input image of numeric floating-point class `single | double` representing linear RGB values in `[0, 1]` and size *m*×*n*×3, *m* and *n* non-zero, non-singleton, positive integers, and `I_d` the corresponding diffuse image of same class, domain and dimension. 37 | 38 | In short: double RGB image in, double RGB image out. 39 | 40 | Note: there is a [`utils/automated_testing.m`](https://github.com/vitorsr/SIHR/blob/master/utils/my_clip.m) script to reproduce reported quality results. Be warned that `Tan2005`, `Yoon2006` and `Akashi2016` run very slowly on Octave (lots of iterations). 41 | 42 | ## Submitting changes 43 | 44 | Please send a [pull request](https://github.com/vitorsr/SIHR/pull/new/master) to the main repository. Please follow the coding conventions (below) to some extent and make sure all of your commits are restricted to either one method or one general modification per commit. Try to write a concise log message for the commits. 45 | 46 | ## Coding conventions 47 | 48 | The beauty in MATLAB/GNU Octave syntax is that there's nothing exquisite about it, it just stays out of your way in favor of mathematics-oriented programming. Just try to keep it tidy. And **always** favor matrix operations. 49 | 50 | Some loose tips on formatting: 51 | 52 | 1. On MATLAB: 53 | * Please use **default** [smart identing](https://www.mathworks.com/help/matlab/matlab_prog/improve-code-readability.html) settings. Apply in editor via Ctrl + I or ⌘ Cmd + I 54 | * Exception can be made for aligned comments, assignments and general data 55 | * Optionally, use [MBeautifier](https://github.com/davidvarga/MBeautifier) 56 | * I recommend adding the provided [shortcuts](https://github.com/davidvarga/MBeautifier#shortcuts) 57 | * To disable (for, e.g. same case as above), see the [directives](https://github.com/davidvarga/MBeautifier#directives) 58 | 2. On Octave: 59 | * Follow the [Octave style guide](https://wiki.octave.org/Octave_style_guide) to some extent 60 | 61 | Some tips on matrix-vector coding: 62 | 63 | * **Always** prefer operations in the following order: matrix ≻ array ≻ scalar 64 | * From the MATLAB documentation: [Techniques to Improve Performance](https://www.mathworks.com/help/matlab/matlab_prog/techniques-for-improving-performance.html) 65 | * From the Octave documentation: [Basic Vectorization](https://octave.org/doc/interpreter/Vectorization-and-Faster-Code-Execution.html) 66 | * [`bsxfun`](https://www.mathworks.com/help/matlab/ref/bsxfun.html) is needed for MATLAB versions earlier than 2016b for implicit expansion 67 | * Octave also supports implicit expansion. It goes by the name of [broadcasting](https://octave.org/doc/interpreter/Broadcasting.html) 68 | * Short "books" 69 | * [Good MATLAB Coding Practices](https://blogs.mathworks.com/pick/2011/01/14/good-matlab-coding-practices/) 70 | * [Writing Fast MATLAB Code](https://www.mathworks.com/matlabcentral/fileexchange/5685-writing-fast-matlab-code) 71 | * [Guidelines for writing clean and fast code in MATLAB](https://www.mathworks.com/matlabcentral/fileexchange/22943-guidelines-for-writing-clean-and-fast-code-in-matlab) 72 | 73 | ### A word on `help` text 74 | 75 | Please follow the MATLAB basic structure for `help` text: [Add Help for Your Program](https://www.mathworks.com/help/matlab/matlab_prog/add-help-for-your-program.html). 76 | 77 | Octave provides additional stylistic guidelines: [Help text](https://wiki.octave.org/Help_text#Guidelines). 78 | 79 | ### A word on MATLAB ↔ Octave compatibility 80 | 81 | Octave is "less compatible" with MATLAB than the inverse relation. Reason being that Octave is not merely an open-source copy of MATLAB and actually has its own language extensions. 82 | 83 | See this wikibooks entry: [MATLAB Programming/Differences between Octave and MATLAB](https://en.wikibooks.org/wiki/MATLAB_Programming/Differences_between_Octave_and_MATLAB). 84 | 85 | My personal recommendation is to code in MATLAB-style syntax and then check Octave for *functionality* compatibility. In case of incompatibility, either change algorithmic/coding approach or implement the desired functionality from scratch. 86 | 87 | You can use the [`utils`](https://github.com/vitorsr/SIHR/tree/master/utils) folder for such eventual functions. 88 | 89 | ## Code of conduct 90 | 91 | Please be polite. 92 | 93 | ## Contact 94 | 95 | Vítor Ramos [![ORCID](https://orcid.org/sites/default/files/images/orcid_16x16.png)](https://orcid.org/0000-0002-7583-5577) 96 | E-mail: [`vitorsr+SIHR@ufrn.edu.br`](mailto:vitorsr+SIHR@ufrn.edu.br)1 97 | 98 | 1 `+SIHR` [works](https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html), please append it so I know where you're coming from 99 | -------------------------------------------------------------------------------- /utils/bilateralFilter.m: -------------------------------------------------------------------------------- 1 | % 2 | % output = bilateralFilter( data, edge, ... 3 | % edgeMin, edgeMax, ... 4 | % sigmaSpatial, sigmaRange, ... 5 | % samplingSpatial, samplingRange ) 6 | % 7 | % Bilateral and Cross-Bilateral Filter using the Bilateral Grid. 8 | % 9 | % Bilaterally filters the image 'data' using the edges in the image 'edge'. 10 | % If 'data' == 'edge', then it the standard bilateral filter. 11 | % Otherwise, it is the 'cross' or 'joint' bilateral filter. 12 | % For convenience, you can also pass in [] for 'edge' for the normal 13 | % bilateral filter. 14 | % 15 | % Note that for the cross bilateral filter, data does not need to be 16 | % defined everywhere. Undefined values can be set to 'NaN'. However, edge 17 | % *does* need to be defined everywhere. 18 | % 19 | % data and edge should be of the greyscale, double-precision floating point 20 | % matrices of the same size (i.e. they should be [ height x width ]) 21 | % 22 | % data is the only required argument 23 | % 24 | % edgeMin and edgeMax specifies the min and max values of 'edge' (or 'data' 25 | % for the normal bilateral filter) and is useful when the input is in a 26 | % range that's not between 0 and 1. For instance, if you are filtering the 27 | % L channel of an image that ranges between 0 and 100, set edgeMin to 0 and 28 | % edgeMax to 100. 29 | % 30 | % edgeMin defaults to min( edge( : ) ) and edgeMax defaults to max( edge( : ) ). 31 | % This is probably *not* what you want, since the input may not span the 32 | % entire range. 33 | % 34 | % sigmaSpatial and sigmaRange specifies the standard deviation of the space 35 | % and range gaussians, respectively. 36 | % sigmaSpatial defaults to min( width, height ) / 16 37 | % sigmaRange defaults to ( edgeMax - edgeMin ) / 10. 38 | % 39 | % samplingSpatial and samplingRange specifies the amount of downsampling 40 | % used for the approximation. Higher values use less memory but are also 41 | % less accurate. The default and recommended values are: 42 | % 43 | % samplingSpatial = sigmaSpatial 44 | % samplingRange = sigmaRange 45 | % 46 | 47 | % Copyright (c) 2007 Jiawen Chen, Sylvain Paris, and Fredo Durand 48 | % 49 | % Permission is hereby granted, free of charge, to any person obtaining a copy 50 | % of this software and associated documentation files (the "Software"), to deal 51 | % in the Software without restriction, including without limitation the rights 52 | % to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 53 | % copies of the Software, and to permit persons to whom the Software is 54 | % furnished to do so, subject to the following conditions: 55 | % 56 | % The above copyright notice and this permission notice shall be included in 57 | % all copies or substantial portions of the Software. 58 | % 59 | % THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 60 | % IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 61 | % FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 62 | % AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 63 | % LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 64 | % OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 65 | % THE SOFTWARE. 66 | 67 | function output = bilateralFilter( data, edge, edgeMin, edgeMax, sigmaSpatial, sigmaRange, ... 68 | samplingSpatial, samplingRange ) 69 | 70 | if( ndims( data ) > 2 ), 71 | error( 'data must be a greyscale image with size [ height, width ]' ); 72 | end 73 | 74 | if( ~isa( data, 'double' ) ), 75 | error( 'data must be of class "double"' ); 76 | end 77 | 78 | if ~exist( 'edge', 'var' ), 79 | edge = data; 80 | elseif isempty( edge ), 81 | edge = data; 82 | end 83 | 84 | if( ndims( edge ) > 2 ), 85 | error( 'edge must be a greyscale image with size [ height, width ]' ); 86 | end 87 | 88 | if( ~isa( edge, 'double' ) ), 89 | error( 'edge must be of class "double"' ); 90 | end 91 | 92 | inputHeight = size( data, 1 ); 93 | inputWidth = size( data, 2 ); 94 | 95 | if ~exist( 'edgeMin', 'var' ), 96 | edgeMin = min( edge( : ) ); 97 | warning( 'edgeMin not set! Defaulting to: %f\n', edgeMin ); 98 | end 99 | 100 | if ~exist( 'edgeMax', 'var' ), 101 | edgeMax = max( edge( : ) ); 102 | warning( 'edgeMax not set! Defaulting to: %f\n', edgeMax ); 103 | end 104 | 105 | edgeDelta = edgeMax - edgeMin; 106 | 107 | if ~exist( 'sigmaSpatial', 'var' ), 108 | sigmaSpatial = min( inputWidth, inputHeight ) / 16; 109 | fprintf( 'Using default sigmaSpatial of: %f\n', sigmaSpatial ); 110 | end 111 | 112 | if ~exist( 'sigmaRange', 'var' ), 113 | sigmaRange = 0.1 * edgeDelta; 114 | fprintf( 'Using default sigmaRange of: %f\n', sigmaRange ); 115 | end 116 | 117 | if ~exist( 'samplingSpatial', 'var' ), 118 | samplingSpatial = sigmaSpatial; 119 | end 120 | 121 | if ~exist( 'samplingRange', 'var' ), 122 | samplingRange = sigmaRange; 123 | end 124 | 125 | if size( data ) ~= size( edge ), 126 | error( 'data and edge must be of the same size' ); 127 | end 128 | 129 | % parameters 130 | derivedSigmaSpatial = sigmaSpatial / samplingSpatial; 131 | derivedSigmaRange = sigmaRange / samplingRange; 132 | 133 | paddingXY = floor( 2 * derivedSigmaSpatial ) + 1; 134 | paddingZ = floor( 2 * derivedSigmaRange ) + 1; 135 | 136 | % allocate 3D grid 137 | downsampledWidth = floor( ( inputWidth - 1 ) / samplingSpatial ) + 1 + 2 * paddingXY; 138 | downsampledHeight = floor( ( inputHeight - 1 ) / samplingSpatial ) + 1 + 2 * paddingXY; 139 | downsampledDepth = floor( edgeDelta / samplingRange ) + 1 + 2 * paddingZ; 140 | 141 | gridData = zeros( downsampledHeight, downsampledWidth, downsampledDepth ); 142 | gridWeights = zeros( downsampledHeight, downsampledWidth, downsampledDepth ); 143 | 144 | % compute downsampled indices 145 | [ jj, ii ] = meshgrid( 0 : inputWidth - 1, 0 : inputHeight - 1 ); 146 | 147 | % ii = 148 | % 0 0 0 0 0 149 | % 1 1 1 1 1 150 | % 2 2 2 2 2 151 | 152 | % jj = 153 | % 0 1 2 3 4 154 | % 0 1 2 3 4 155 | % 0 1 2 3 4 156 | 157 | % so when iterating over ii( k ), jj( k ) 158 | % get: ( 0, 0 ), ( 1, 0 ), ( 2, 0 ), ... (down columns first) 159 | 160 | di = round( ii / samplingSpatial ) + paddingXY + 1; 161 | dj = round( jj / samplingSpatial ) + paddingXY + 1; 162 | dz = round( ( edge - edgeMin ) / samplingRange ) + paddingZ + 1; 163 | 164 | % perform scatter (there's probably a faster way than this) 165 | % normally would do downsampledWeights( di, dj, dk ) = 1, but we have to 166 | % perform a summation to do box downsampling 167 | for k = 1 : numel( dz ), 168 | 169 | dataZ = data( k ); % traverses the image column wise, same as di( k ) 170 | if ~isnan( dataZ ), 171 | 172 | dik = di( k ); 173 | djk = dj( k ); 174 | dzk = dz( k ); 175 | 176 | gridData( dik, djk, dzk ) = gridData( dik, djk, dzk ) + dataZ; 177 | gridWeights( dik, djk, dzk ) = gridWeights( dik, djk, dzk ) + 1; 178 | 179 | end 180 | end 181 | 182 | % make gaussian kernel 183 | kernelWidth = 2 * derivedSigmaSpatial + 1; 184 | kernelHeight = kernelWidth; 185 | kernelDepth = 2 * derivedSigmaRange + 1; 186 | 187 | halfKernelWidth = floor( kernelWidth / 2 ); 188 | halfKernelHeight = floor( kernelHeight / 2 ); 189 | halfKernelDepth = floor( kernelDepth / 2 ); 190 | 191 | [gridX, gridY, gridZ] = meshgrid( 0 : kernelWidth - 1, 0 : kernelHeight - 1, 0 : kernelDepth - 1 ); 192 | gridX = gridX - halfKernelWidth; 193 | gridY = gridY - halfKernelHeight; 194 | gridZ = gridZ - halfKernelDepth; 195 | gridRSquared = ( gridX .* gridX + gridY .* gridY ) / ( derivedSigmaSpatial * derivedSigmaSpatial ) + ( gridZ .* gridZ ) / ( derivedSigmaRange * derivedSigmaRange ); 196 | kernel = exp( -0.5 * gridRSquared ); 197 | 198 | % convolve 199 | blurredGridData = convn( gridData, kernel, 'same' ); 200 | blurredGridWeights = convn( gridWeights, kernel, 'same' ); 201 | 202 | % divide 203 | blurredGridWeights( blurredGridWeights == 0 ) = -2; % avoid divide by 0, won't read there anyway 204 | normalizedBlurredGrid = blurredGridData ./ blurredGridWeights; 205 | normalizedBlurredGrid( blurredGridWeights < -1 ) = 0; % put 0s where it's undefined 206 | 207 | % for debugging 208 | % blurredGridWeights( blurredGridWeights < -1 ) = 0; % put zeros back 209 | 210 | % upsample 211 | [ jj, ii ] = meshgrid( 0 : inputWidth - 1, 0 : inputHeight - 1 ); % meshgrid does x, then y, so output arguments need to be reversed 212 | % no rounding 213 | di = ( ii / samplingSpatial ) + paddingXY + 1; 214 | dj = ( jj / samplingSpatial ) + paddingXY + 1; 215 | dz = ( edge - edgeMin ) / samplingRange + paddingZ + 1; 216 | 217 | % interpn takes rows, then cols, etc 218 | % i.e. size(v,1), then size(v,2), ... 219 | output = interpn( normalizedBlurredGrid, di, dj, dz ); 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SIHR: a MATLAB/GNU Octave toolbox for single image highlight removal 2 | 3 | [![DOI](https://joss.theoj.org/papers/10.21105/joss.01822/status.svg)](https://doi.org/10.21105/joss.01822) 4 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3381328.svg)](https://doi.org/10.5281/zenodo.3381328) 5 | [![license](https://img.shields.io/github/license/vitorsr/SIHR)](https://github.com/vitorsr/SIHR/blob/master/LICENSE) 6 | 7 | ## Citation 8 | 9 | ```BibTeX 10 | @article{Ramos2020, 11 | doi = {10.21105/joss.01822}, 12 | url = {https://doi.org/10.21105/joss.01822}, 13 | year = {2020}, 14 | month = jan, 15 | publisher = {The Open Journal}, 16 | volume = {5}, 17 | number = {45}, 18 | pages = {1822}, 19 | author = {V{\'{\i}}tor Ramos}, 20 | title = {{SIHR}: a {MATLAB}/{GNU} {Octave} toolbox for single image highlight removal}, 21 | journal = {Journal of Open Source Software} 22 | } 23 | ``` 24 | 25 | ## Summary 26 | 27 | An ongoing effort of developing new and implementing established single image highlight removal (SIHR) methods on MATLAB/GNU Octave. 28 | 29 | Highlight, specularity, or specular reflection removal (see 1 for a proper Web of Science expression, see [1] for a reference work entry, see [2], [3] for survey on this problem) concerns the following decomposition. 30 | 31 | [![Dichromatic Reflection Model](paper/figures/drm.jpg)](https://link.springer.com/referenceworkentry/10.1007%2F978-0-387-31439-6_532) 32 | 33 | I welcome and encourage contributions to this project upon review. Please check [`CONTRIBUTING.md`](CONTRIBUTING.md) for more details. 34 | 35 | *Disclaimer 1: this repository is intended for research purposes only.* 36 | *Disclaimer 2: ~~some~~ most of these methods are based on chromaticity analysis, they fail **miserably** for grayscale images.* 37 | 38 | 1 `((remov* NEAR/1 (highlight* OR specular*)) OR (separat* NEAR/1 (reflect* OR specular*)))` 39 | 40 | ### *Raison d'être* 41 | 42 | I started out this repository by implementing, translating and collecting code snippets from the *rare* available2,3,4,5 codes. Oftentimes papers are cryptical, codes are in C/C++ (requires compilation and major source code modification for general testing), or are just unavailable. See, e.g. this CSDN post6 that has no valid links at all. 43 | 44 | In this context, this repository aims to be a continous algorithmic aid for ongoing research and development of SIHR methods. 45 | 46 | 2 Tan and Ikeuchi. [Online]. Available: 47 | 3 Shen *et al.* [Online]. Available: 48 | 4 ~~Yang *et al.* [Online]. Available: ~~ 49 | 5 Shen and Zheng. [Online]. Available: 50 | 6 ~~https://blog.csdn.net/nvidiacuda/article/details/8078167~~ 51 | 52 | ## Usage (API) 53 | 54 | Calling this toolbox's functions is **very** straightforward: 55 | 56 | ```MATLAB 57 | I_d = AuthorYEAR(I); % I is a double-valued input image of dimension 58 | % m×n×3 containing linear RGB values and 59 | % I_d is the calculated diffuse reflection 60 | % using AuthorYEAR method. 61 | % The specular component is simply 62 | % I_s = I - I_d; 63 | ``` 64 | 65 | ### Methods 66 | 67 | The following methods are available. 68 | 69 | |Year| Method | Function | 70 | |:--:|----------------------------|----------------| 71 | |2005| Tan and Ikeuchi [4] | `Tan2005` | 72 | |2006| Yoon *et al.* [5] | `Yoon2006` | 73 | |2008| Shen *et al.* [6] | `Shen2008` | 74 | |2009| Shen and Cai [7] | `Shen2009` | 75 | |2010| Yang *et al.* [8] | `Yang2010` | 76 | |2013| Shen and Zheng [9] | `Shen2013` | 77 | |2016| Akashi and Okatani [10] | `Akashi2016` | 78 | 79 | The following improvement is available. 80 | 81 | |Year| Method | Function | 82 | |:--:|----------------------------|----------------| 83 | |2019| Yamamoto and Nakazawa [11] | `Yamamoto2019` | 84 | 85 | ### Environment 86 | 87 | The environment this repository is being developed is: 88 | 89 | * [MATLAB](https://www.mathworks.com/products/matlab.html) 90 | * [Image Processing Toolbox](https://www.mathworks.com/products/image.html) 91 | * [GNU Octave](https://www.gnu.org/software/octave/) 92 | * [Image package](https://octave.sourceforge.io/image/index.html) 93 | * `pkg install -forge image` 94 | 95 | #### Tested environments 96 | 97 | Octave 4.2 Ubuntu 18.04 98 | Octave 5.1 (latest) Windows 10 1903 99 | MATLAB 9.1 (R2016b) Windows 10 1903 100 | MATLAB 9.6 (R2019a) Windows 10 1903 Ubuntu 16.04 (MATLAB Online) 101 | 102 | ### Installation 103 | 104 | 1. `git clone https://github.com/vitorsr/SIHR.git` or [download](https://github.com/vitorsr/SIHR/archive/master.zip) a copy of the repository. 105 | 1. Start Octave or MATLAB. 106 | 1. `cd('path/to/SIHR')`, i.e. change current folder to `SIHR` root (where `SIHR.m` is located). 107 | 1. `run SIHR.m` for session path setup. 108 | 1. `help SIHR` or `doc SIHR` provides a summary of the methods available. 109 | 110 | #### Additional Debian/Ubuntu installation 111 | 112 | To install the [`image`](https://octave.sourceforge.io/image/index.html) package from Octave Forge, `build-essential` and `liboctave-dev` need to be present. Install them via `apt`, then proceed with package installation. 113 | 114 | ```bash 115 | sudo apt-get install -qq -y build-essential liboctave-dev 116 | octave --eval "pkg install -forge image" 117 | ``` 118 | 119 | ## Performance 120 | 121 | This section aims to clarify how well (or not) the methods reproduced in this project were at reproducing results in literature. 122 | 123 | Note: Akashi and Okatani's [10] method has highly fluctuating results because of random initialization. 124 | 125 | ### Dataset 126 | 127 | In technical literature, there exist two ground truth datasets commonly used right now. One by Shen and Zheng [9] which is distributed alongside their code, and one by Grosse *et al.* [12] in a dedicated page7. 128 | 129 | Other test images are included alongside the code for Shen *et al.* [6] and Yang *et al.* [8]. 130 | 131 | Follow the instructions in [`images`](https://github.com/vitorsr/SIHR/tree/master/images) in order to download a local copy of these images from the respective authors' pages. 132 | 133 | 7 Grosse *et al.* [Online]. Available: 134 | 135 | ### Quality 136 | 137 | Quantitative results reported are usually regarding the quality of the recovered diffuse component with respect to the ground truth available in the Shen and Zheng [9] test image set. 138 | 139 | #### Automated testing 140 | 141 | Reproduced results below are available in the [`utils/automated_testing.m`](https://github.com/vitorsr/SIHR/blob/master/utils/automated_testing.m) script. 142 | 143 | Note: `ssim` is not available in Octave Forge `image`. 144 | 145 | #### Highest (self and peer-reported | reproduced) PSNR results (in dB) 146 | 147 | |Year| Method | *animals* | *cups* | *fruit* | *masks* | Reproduced | *animals* | *cups* | *fruit* | *masks* | 148 | |:--:|--------------------|:---------:|:---------:|:---------:|:---------:|--------------|:---------:|:------:|:-------:|:-------:| 149 | |2005| Tan and Ikeuchi | 30.2 | 30.1 | 29.6 | 25.6 | `Tan2005` | 30.4 | 31.6 | 30.4 | 25.8 | 150 | |2006| Yoon *et al.* | - | - | - | - | `Yoon2006` | 32.9 | 33.3 | 36.6 | 34.1 | 151 | |2008| Shen *et al.* | 34.6 | 37.7 | 37.6 | 31.7 | `Shen2008` | 34.2 | 37.5 | 38.0 | 32.1 | 152 | |2009| Shen and Cai | 34.8 | 37.6 | 36.9 | 34.0 | `Shen2009` | 34.9 | 37.6 | 36.7 | 34.0 | 153 | |2010| Yang *et al.* | *37.2* | 38.0 | 37.6 | 32.2 | `Yang2010` | 36.5 | 37.5 | 36.2 | 33.5 | 154 | |2013| Shen and Zheng | **37.3** | **39.3** | *38.9* | 34.1 | `Shen2013` | 37.5 | 38.3 | 38.2 | 32.7 | 155 | |2015| Liu *et al.* | 33.4 | 37.6 | 35.1 | **34.5** | - | - | - | - | - | 156 | |2016| Akashi and Okatani | 26.8 | 35.7 | 30.8 | 32.3 | `Akashi2016` | 32.7 | 35.9 | 34.8 | 34.0 | 157 | |2016| Suo *et al.* | - | - | **40.4** | 34.2 | - | - | - | - | - | 158 | |2017| Ren *et al.* | - | 38.0 | 37.7 | **34.5** | - | - | - | - | - | 159 | |2018| Guo *et al.* | 35.7 | *39.1* | 36.4 | *34.4* | - | - | - | - | - | 160 | 161 | #### Highest (self and peer-reported | reproduced) SSIM results 162 | 163 | |Year| Method | *animals* | *cups* | *fruit* | *masks* | Reproduced | *animals* | *cups* | *fruit* | *masks* | 164 | |:--:|--------------------|:---------:|:---------:|:---------:|:---------:|--------------|:---------:|:------:|:-------:|:-------:| 165 | |2005| Tan and Ikeuchi | 0.929 | 0.767 | 0.912 | 0.789 | `Tan2005` | 0.928 | 0.895 | 0.907 | 0.821 | 166 | |2006| Yoon *et al.* | - | - | - | - | `Yoon2006` | 0.980 | 0.961 | 0.961 | 0.953 | 167 | |2008| Shen *et al.* | *0.974* | 0.962 | **0.961** | *0.943* | `Shen2008` | 0.975 | 0.962 | 0.961 | 0.943 | 168 | |2009| Shen and Cai | - | - | - | - | `Shen2009` | 0.985 | 0.970 | 0.962 | 0.961 | 169 | |2010| Yang *et al.* | 0.970 | 0.941 | 0.939 | 0.899 | `Yang2010` | 0.952 | 0.937 | 0.916 | 0.896 | 170 | |2013| Shen and Zheng | 0.971 | **0.966** | *0.960* | 0.941 | `Shen2013` | 0.985 | 0.964 | 0.958 | 0.935 | 171 | |2015| Liu *et al.* | - | - | - | - | - | - | - | - | - | 172 | |2016| Akashi and Okatani | 0.802 | 0.937 | 0.765 | 0.657 | `Akashi2016` | 0.7340 | 0.9190 | 0.9010 | 0.8710 | 173 | |2016| Suo *et al.* | - | - | - | - | - | - | - | - | - | 174 | |2017| Ren *et al.* | 0.896 | 0.957 | 0.952 | 0.913 | - | - | - | - | - | 175 | |2018| Guo *et al.* | **0.975** | *0.963* | 0.930 | **0.955** | - | - | - | - | - | 176 | 177 | #### Expected running time (in seconds) 178 | 179 | Note: results for MATLAB R2019b, Intel i5-8250U CPU, and 24 GB DDR4 2400 MHz RAM. 180 | 181 | |Year| Reproduced | *animals* | *cups* | *fruit* | *masks* | 182 | |:--:|--------------|:---------:|:--------:|:---------:|:---------:| 183 | |2005| `Tan2005` | 67.0 | 170.0 | 210.0 | 190.0 | 184 | |2006| `Yoon2006` | 2.8 | 1.6 | 2.6 | 3.4 | 185 | |2008| `Shen2008` | 1.9 | 7.8 | 4.3 | 4.9 | 186 | |2009| `Shen2009` | 0.9 | **0.05** | **0.041** | **0.029** | 187 | |2010| `Yang2010` | *0.31* | 0.13 | 0.11 | 0.081 | 188 | |2013| `Shen2013` | **0.043** | *0.071* | *0.083* | *0.056* | 189 | |2016| `Akashi2016` | 140.0 | 170.0 | 230.0 | 200.0 | 190 | 191 | ## References 192 | 193 | 194 | 195 | 1. R. T. Tan, “Specularity, Specular Reflectance,” in Computer Vision, Springer US, 2014, pp. 750–752 [Online]. Available: http://dx.doi.org/10.1007/978-0-387-31439-6_538 196 | 1. A. Artusi, F. Banterle, and D. Chetverikov, “A Survey of Specularity Removal Methods,” Computer Graphics Forum, vol. 30, no. 8, pp. 2208–2230, Aug. 2011 [Online]. Available: http://dx.doi.org/10.1111/J.1467-8659.2011.01971.X 197 | 1. H. A. Khan, J.-B. Thomas, and J. Y. Hardeberg, “Analytical Survey of Highlight Detection in Color and Spectral Images,” in Lecture Notes in Computer Science, Springer International Publishing, 2017, pp. 197–208 [Online]. Available: http://dx.doi.org/10.1007/978-3-319-56010-6_17 198 | 1. R. T. Tan and K. Ikeuchi, “Separating reflection components of textured surfaces using a single image,” IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 27, no. 2, pp. 178–193, Feb. 2005 [Online]. Available: http://dx.doi.org/10.1109/TPAMI.2005.36 199 | 1. K. Yoon, Y. Choi, and I. S. Kweon, “Fast Separation of Reflection Components using a Specularity-Invariant Image Representation,” in 2006 International Conference on Image Processing, 2006 [Online]. Available: http://dx.doi.org/10.1109/ICIP.2006.312650 200 | 1. H.-L. Shen, H.-G. Zhang, S.-J. Shao, and J. H. Xin, “Chromaticity-based separation of reflection components in a single image,” Pattern Recognition, vol. 41, no. 8, pp. 2461–2469, Aug. 2008 [Online]. Available: http://dx.doi.org/10.1016/J.PATCOG.2008.01.026 201 | 1. H.-L. Shen and Q.-Y. Cai, “Simple and efficient method for specularity removal in an image,” Applied Optics, vol. 48, no. 14, p. 2711, May 2009 [Online]. Available: http://dx.doi.org/10.1364/AO.48.002711 202 | 1. Q. Yang, S. Wang, and N. Ahuja, “Real-Time Specular Highlight Removal Using Bilateral Filtering,” in Computer Vision – ECCV 2010, Springer Berlin Heidelberg, 2010, pp. 87–100 [Online]. Available: http://dx.doi.org/10.1007/978-3-642-15561-1_7 203 | 1. H.-L. Shen and Z.-H. Zheng, “Real-time highlight removal using intensity ratio,” Applied Optics, vol. 52, no. 19, p. 4483, Jun. 2013 [Online]. Available: http://dx.doi.org/10.1364/AO.52.004483 204 | 1. Y. Akashi and T. Okatani, “Separation of reflection components by sparse non-negative matrix factorization,” Computer Vision and Image Understanding, vol. 146, pp. 77–85, May 2016 [Online]. Available: http://dx.doi.org/10.1016/j.cviu.2015.09.001 205 | 1. T. Yamamoto and A. Nakazawa, “General Improvement Method of Specular Component Separation Using High-Emphasis Filter and Similarity Function,” ITE Transactions on Media Technology and Applications, vol. 7, no. 2, pp. 92–102, 2019 [Online]. Available: http://dx.doi.org/10.3169/mta.7.92 206 | 1. R. Grosse, M. K. Johnson, E. H. Adelson, and W. T. Freeman, “Ground truth dataset and baseline evaluations for intrinsic image algorithms,” in 2009 IEEE 12th International Conference on Computer Vision, 2009 [Online]. Available: http://dx.doi.org/10.1109/ICCV.2009.5459428 207 | 208 | 209 | --------------------------------------------------------------------------------