function hAxes = digitalVectorscope( inRGB, varargin )
%
%% digitalVectorscope
% The function presents a Vector-scope plot, with multiple options.
%
%% Syntax:
% hAxes = digitalVectorscope( inRGB )
% hAxes = digitalVectorscope( inRGB, hAxes )
% hAxes = digitalVectorscope( inRGB, hAxes, isPixelClr )
% hAxes = digitalVectorscope( inRGB, hAxes, isPixelClr, plotParamPairs )
% hAxes = digitalVectorscope( inRGB, hAxes, isPixelClr, is3D, plotParamPairs )
%
%% Description:
% When performing color analysis, Electrical Engineers are frequently using a Vector-scope -
% usually an analogue device that measures the image color (see Wiki for some details:
% http://en.wikipedia.org/wiki/Vectorscope ). This function allows the user to present a
% 2D or a 3D plot of the La*b image data.
%
%% Input arguments (defaults exist):
% inRGB- a 3D matrix with pixels data in RGB. If no data is specified, user will be
% prompted for an image file via file browser.
%
% The remaining inputs are specified via "Name- Value" pairs, while default values exist
% hAxes- a handle to the axis where the Vector-scope plot will be placed. If no axis
% handle is supplied, one will be created.
% isPixelClr- a logical variable. When enabled, the marker representing each inRGB pixel,
% will have the pixels color. Note that since it slows down the program, it is
% disabled by default. It is also disabled in case isBckndImg is enabled, otherwise
% the line data will not be seen.
% is3D- a logical variable. When enabled a 3D Vector-scope plot will be presented (a*, b ,
% chanIntensity). By default it is disabled, as Vector-scope plot is usually 2D.
% isBckndImg- a logical variable. When enabled, for 2D case a background image will be
% presented beneath the point location emphasis the appropriate coordinate color.
% The flag is automatically disabled in case is3D is enabled.
% colorSpace a string described the color space used in analysis. Currently the following
% color spaces are supported:{['LAB'], 'YCBCR', 'XYZ', 'XYL'}.
% borderSize- the size of the rectangul;ar around target points.
% varargin- a cell array with Lineseries Properties as described in Matlab help (usually
% pairs of property name and property value). clrChanA default value of a marker
% line style and color exist.
%
%% Output arguments:
% hAxes- a handle to the axis where the Vector-scope plot was placed.
%
%% Issues & Comments
% - isPixelClr- when enabled- code runs slowly...
% - isBckndImg- complicates the code, and turns axis and points value to be irrelevant
% - is3D- the plot is not 3D, and seems to be incorrect in case of isBckndImg.
%
%% Example
% (see demo file for more options)
% close all;
%
% inRGB=imread('peppers.png');
% imgFigH=figure;
% imshow(inRGB)
%
% %% Yellow Pepepr
% iRows=185:215;
% iCols=235:265;
%
% figure(imgFigH);
% minCol=min(iCols);
% maxCol=max(iCols);
% minRow=min(iRows);
% maxRow=max(iRows);
% rectangle('Position', [minCol, minRow, maxCol-minCol, maxRow-minRow], 'EdgeColor', 'w',...
% 'LineWidth', 4);
% rectangle('Position', [minCol, minRow, maxCol-minCol, maxRow-minRow], 'EdgeColor', 'b',...
% 'LineWidth', 2);
%
% scopeCLrSpace='LAB';
%
% digitalVectorscope( inRGB(iRows, iCols, :), 'colorSpace', scopeCLrSpace,...
% 'isPixelClr', false, 'is3D', false, 'isBckndImg', true, 'MarkerEdgeColor', 'b');
% title('Background enabled.', 'FontSize', 18);
% legend('Ancor colors', 'Scope axis origin', 'Yellow pepper');
%
% hAxes=digitalVectorscope( inRGB(iRows, iCols, :), 'colorSpace', scopeCLrSpace,...
% 'isPixelClr', false, 'is3D', false, 'isBckndImg', false, 'MarkerEdgeColor', 'y');
%
%% See also:
% - applycform
% - makecform
%
%% Revision history:
% First version: Nikolay S. 2013-01-03.
% Last update: Nikolay S. 2013-02-05.
%
%% *List of Changes*:
% - 2013-02-05- isBckndImg added. Parameter Name- Parameter Value input style adopted.
% - 2013-01-03- first version- issues may arise...
%% Default params- before reading user inputs.
isBckndImg=[];
colorSpace=[];
hAxes=[];
isPixelClr=[];
is3D=[];
borderSize=[];
%% Load uses params, overifding default ones
specificParamsList={'colorSpace', 'hAxes', 'isPixelClr', 'is3D', 'isBckndImg'};
if nargin>1
nVarargin=length(varargin);
isSpecificParam=false(1, nVarargin);
iArg=1;
while iArg <= (nVarargin-1)
% automatically get all RELEVANT input pairs and store in local vairables
if any( strcmpi(varargin{iArg}, specificParamsList) )
assignin_value(varargin{iArg}, varargin{iArg+1});
isSpecificParam( [iArg, iArg+1] )=true; % parameters come in Name- value pairs
iArg=iArg+1;
end
iArg=iArg+1;
end % while iArg < (nVarargin-1)
varargin(isSpecificParam)=[];
end
%% Default params- after reading user inputs.
if isempty(inRGB) || nargin==0
% force the user to choose input file if inRGB is missing
imageFormats=imformats;
imageExtList=cat(2, imageFormats.ext); % image files extentions
inRGB=filesFullName([], imageExtList, 'Choose input image file', true);
end
if isempty(colorSpace)
colorSpace='LAB'; % {['LAB'], 'YCBCR', 'XYZ', 'XYL'}
end
if isempty(isPixelClr)
isPixelClr=false; % by default do not set each pixels marker color- it is slow
end
if isempty(is3D)
is3D=false; % by default use 2D plot- this is the common case
end
if isempty(isBckndImg) % default enabled or disabled plot bacground
isBckndImg=true;
end
if isBckndImg
if isPixelClr
% when isPixelClr enabled, one will not see the points on top of the bacground, so
% this is a bad combination
warning( 'digitalVectorscope:paramAmbiguous', ...
strcat('\nContradictive parameters: isBckndImg & isPixelClr enabled',...
' is bad practise.\n Thus isBckndImg is disabled.') );
isBckndImg=false;
end
if is3D
% when is3D enabled, 3D plot is badly combined with image located in XY plane
warning( 'digitalVectorscope:paramAmbiguous', ...
strcat('\nContradictive parameters: isBckndImg & is3D enabled',...
' is bad practise.\n Thus isBckndImg is disabled.') );
isBckndImg=false;
end
end % if isBckndImg
if isempty(hAxes)
% Open a figure if no hAxes was specified (plot- like behaviour)
figure; % figureFullScreen(figure, true); %
hAxes=axes;
% if is3D
% set( hAxes, 'CameraPosition', [-5 -5 5]);
% end
end
if isempty(borderSize)
borderSize=10;
end
%% !!!! Background image is currenlty disabled !!!
% isBckndImg=false;
%% Default marker and line parameters
if isempty(varargin) | ~strcmpi(varargin, 'Marker')
varargin=cat(2, varargin, {'Marker', '.'});
if ~isPixelClr
varargin=cat( 2, varargin, {'Color', 'r'} );
end
end
if isempty(varargin) | ~strcmpi(varargin, 'LineStyle')
varargin=cat( 2, varargin, {'LineStyle', 'None'} );
end
if isPixelClr
if any( cat(2, strcmpi(varargin, 'Color'), strcmpi(varargin, 'MarkerEdgeColor') ) )
isPixelClr=false;
warning('digitalVectorscope:paramAmbiguous', ...
'Contradictive parameters: isPixelClr enabled, while line color specified');
end
end
%% Program start
% ############################################################## %
% ############################################################## %
holdState=ishold; % the hold {no, off} state at program start
if ischar(inRGB) && exist(inRGB, 'file')==2
inRGB=imread(inRGB);
end
nClrDims=size(inRGB, 3);
colsInRGB=size(inRGB, 2);
% in case of non 3D matrix (for example is indexing via logicals was used) reshape to
% proper dimentions
if nClrDims==1
inRGB=reshape(inRGB, [], colsInRGB, 3);
end
inRGB=reshape(inRGB, [], 3); % convert to 2D matrix
% Convert to chanIntensity*clrChanA*clrChanB* and divide to color channels
% For each input class, set maximal and minimal values
switch( upper(class(inRGB)) )
case('UINT8')
maxRGB=2^8-1;
case('UINT16')
maxRGB=2^16-1;
case('DOUBLE')
maxRGB=1;
end
switch(upper(colorSpace))
case('LAB')
cform2CLrSpace = makecform('srgb2lab');
hFunc2ClrSpace=@(x) applycform(x, cform2CLrSpace);
cform2RGB = makecform('lab2srgb');
hFunc2RGB=@(x) applycform(x, cform2RGB);
minValClr=-100;
maxValClr=100;
minValInt=0;
maxValInt=100;
xString='A*';
yString='B*';
zString='L*';
iInt=1;
iClrA=2;
iClrB=3;
case('XYZ')
cform2CLrSpace = makecform('srgb2xyz');
hFunc2ClrSpace=@(x) applycform(x, cform2CLrSpace);
cform2RGB = makecform('xyz2srgb');
hFunc2RGB=@(x) applycform(x, cform2RGB);
minValClr=0;
maxValClr=1;
minValInt=0;
maxValInt=1;
xString='X';
yString='Y';
zString='Z';
iInt=1;
iClrA=2;
iClrB=3;
case('XYL')
cform2CLrSpace1 = makecform('srgb2xyz');
cform2CLrSpace2 = makecform('xyz2xyl');
hFunc2ClrSpace=@(x) applycform(applycform(x, cform2CLrSpace1) ,cform2CLrSpace2);
cform2RGB1 = makecform('xyl2xyz');
cform2RGB2 = makecform('xyz2srgb');
hFunc2RGB=@(x) applycform(applycform(x, cform2RGB1) ,cform2RGB2);
minValClr=0;
maxValClr=1;
minValInt=0;
maxValInt=1;
xString='X';
yString='Y';
zString='L';
iInt=3;
iClrA=1;
iClrB=2;
case('YCBCR')
hFunc2ClrSpace=@(x) rgb2ycbcr(x);
hFunc2RGB=@(x) ycbcr2rgb(x);
minValClr=0;
maxValClr=1;
minValInt=0;
maxValInt=1;
xString='Cb';
yString='Cr';
zString='Y';
iInt=1;
iClrA=2;
iClrB=3;
end
hChildren=get(hAxes, 'Children');
if strcmpi(get(hChildren, 'Type'), 'image')
hFuncConv2Img=@conv2ImgSpace;
isBckndImgFound=true;
else
hFuncConv2Img=@(x) reshape(x, [], 3);
isBckndImgFound=false;
end
hLines=hChildren( strcmpi(get(hChildren, 'Type'), 'Line') );
isCenterVectorscope=any( strcmpi(get(hLines, 'Tag'), 'centerVectorscope') );
if ~isCenterVectorscope
if isBckndImg
%% Generate background image
nA=200;
nB=200;
bcndClrSpace=zeros( [nB, nA] );
[ bcndClrSpace(:, :, 2), bcndClrSpace(:, :, 3) ]=...
meshgrid( linspace(minValClr, maxValClr, nA), linspace(minValClr, maxValClr, nB) );
bcndClrSpace(:, :, 1)=maxValInt; % mean( [minValInt, maxValInt] );
bcndRGB=hFunc2RGB(bcndClrSpace);
imshow(bcndRGB);
isBckndImgFound=true;
%% Order the resulting axes
%% X axis
% set(hAxes, 'XTick', xTickVals);
% set(hAxes, 'XAxisLocation', 'top'); % [ top | {bottom} ]
nXTick=length( get(hAxes, 'XTick') )+1;
xTickLabelVals=linspace(minValClr, maxValClr, nXTick);
set( hAxes, 'XTickLabel', xTickLabelVals(2:end) );
% set( hAxes, 'XTickLabelMode', 'manual' );
set( hAxes, 'XTickMode', 'manual' );
% set(hAxes, 'XTick', linspace(minValClr, maxValClr, nXTickLabel) );
%% Y axis
set(hAxes, 'YDir', 'normal');
% set(hAxes, 'YAxisLocation', 'right'); % [ {left} | right ]
% nYTickLabelElems=length( get(hAxes, 'YTickLabel') );
nYTick=length( get(hAxes, 'YTick') )+1;
yTickLabelVals=linspace(minValClr, maxValClr, nYTick);
set(hAxes, 'YTickLabel', yTickLabelVals(2:end) );
set(hAxes, 'YTickLabelMode', 'manual');
set(hAxes, 'YTickMode', 'manual');
hFuncConv2Img=@conv2ImgSpace;
else
hFuncConv2Img=@(x) reshape(x, [], 3);
end
%% Title, lables, grids and center point
title('Digital Vectorscope', 'FontSize', 20);
grid on;
hold on;
xlabel(xString, 'FontSize', 18);
ylabel(yString, 'FontSize', 18);
axis on;
axis equal;
baseColorsText={'M_G', 'B', 'C_Y', 'G', 'Y_L', 'R'};
dTextDist=[(maxValClr-minValClr)/25, (maxValClr-minValClr)/50, (maxValClr-minValClr)/50];
nColors=length(baseColorsText);
baseColorsRGB=[...
168 61 134;...
31 46 131;...
0 119 151;...
64 133 52;...
232 188 0;...
154 31 43 ]/255;
baseColorsSpaceData=hFunc2ClrSpace( baseColorsRGB );
%% Convert data to imageClrSpace
baseColorsImgSpaceData=hFuncConv2Img(baseColorsSpaceData);
if is3D
[sphereX, sphereY, sphereZ] = sphere(11);
sphereX=borderSize*sphereX;
sphereY=borderSize*sphereY;
sphereZ=borderSize*sphereZ;
zlabel(zString, 'FontSize', 18);
centerPoint=[maxValClr+minValClr, maxValClr+minValClr, maxValInt+minValInt]/2;
centerPoint=hFuncConv2Img(centerPoint);
[cylinderX, cylinderY, cylinderZ] = cylinder(maxValClr-minValClr);
cylinderX=cylinderX + centerPoint(1);
cylinderY=cylinderY + centerPoint(2);
cylinderZ=cylinderZ*(maxValInt- minValInt);
cylinderZ=cylinderZ + minValInt;
meshH=mesh(cylinderX, cylinderY, cylinderZ);
set( meshH, 'FaceAlpha', 0); % make pathces fully transparent
axis tight;
hCenterLine=plot3(centerPoint(1), centerPoint(2),centerPoint(3),...
'Marker', 'o', 'MarkerEdgeColor', 'k', 'MarkerFaceColor', 'k',...
'LineStyle', 'None');
% text(centerPoint(1), centerPoint(2)-5, centerPoint(3)-5, 'Origin', 'Color', 'r');
pointX=baseColorsImgSpaceData(:, iClrA);
pointY=baseColorsImgSpaceData(:, iClrB);
pointZ=baseColorsImgSpaceData(:, iInt);
plot3( pointZ, pointX, pointY, 'Marker', 's',...
'MarkerEdgeColor', 'k', 'LineWidth', 2, 'LineStyle', 'None');
for iBaseColors=1:nColors
meshH=mesh(sphereZ+pointZ(iBaseColors), sphereX+pointX(iBaseColors),...
sphereY+pointY(iBaseColors), 'EdgeColor', 'k', 'LineStyle', ':');
set( meshH, 'FaceAlpha', 0); % make pathces fully transparent
text(pointZ(iBaseColors)+dTextDist(1), pointX(iBaseColors)+dTextDist(2),...
pointY(iBaseColors)+dTextDist(3),...
baseColorsText{iBaseColors}, 'Color', 'k', 'FontSize', 16);
end
else
if ~isBckndImgFound
axis( [minValClr, maxValClr, minValClr, maxValClr] );
end
axis tight;
plot( baseColorsImgSpaceData(:, iClrA),...
baseColorsImgSpaceData(:, iClrB),...
'Marker', 's', 'MarkerEdgeColor', 'k', 'LineWidth', 2,...
'LineStyle', 'None');
for iBaseColors=1:nColors
pointX=baseColorsImgSpaceData(iBaseColors, iClrA);
pointY=baseColorsImgSpaceData(iBaseColors, iClrB);
rectangle('Position', [pointX-borderSize/2, pointY-borderSize/2,...
borderSize, borderSize],...
'Curvature', [0.8, 0.4], 'EdgeColor', 'k',...
'LineWidth', 2, 'LineStyle','--');
text(pointX+dTextDist(2), pointY+dTextDist(3), baseColorsText{iBaseColors},...
'Color', 'k', 'FontSize', 16);
end
rectOffset=maxValClr/(2*255);
if isBckndImg
rectData=[rectOffset, rectOffset, nA, nB];
centerPoint=[nA, nB]/2;
else
rectData=[minValClr+rectOffset, minValClr+rectOffset,...
maxValClr-minValClr-2*rectOffset, maxValClr-minValClr-2*rectOffset];
centerPoint=[maxValClr+minValClr, maxValClr+minValClr]/2;
end
rectangle('Position', rectData, 'Curvature',[1,1],...
'LineWidth', 2, 'EdgeColor', 'k');
hCenterLine=plot(centerPoint(1), centerPoint(2),...
'Marker', 'o', 'MarkerEdgeColor', 'k', 'MarkerFaceColor', 'k',...
'LineStyle', 'None');
% text(centerPoint(1), centerPoint(2)-5, 'Origin', 'Color', 'r');
end % if is3D
% hold off;
set(hCenterLine, 'Tag', 'centerVectorscope');
end % if isempty( get(hAxes, 'UserData') ) || ~get(hAxes, 'UserData')
normInRGB=double(inRGB)/maxRGB;
clrSpaceData=hFunc2ClrSpace( normInRGB );
clrImgSpaceData=hFuncConv2Img(clrSpaceData);
chanIntensity=clrImgSpaceData(:, iInt);
clrChanA=clrImgSpaceData(:, iClrA);
clrChanB=clrImgSpaceData(:, iClrB);
axes(hAxes); % make user selected axes current
nPixels=numel(chanIntensity);
if isPixelClr % each pixel color will be used for it's marker- it is slow...
for iPixel=1:nPixels
if is3D % 3D case
hLine=plot3( clrChanA(iPixel), clrChanB(iPixel), chanIntensity(iPixel),...
'Color', normInRGB(iPixel, :),...
'MarkerFaceColor', normInRGB(iPixel, :), varargin{:} );
else % 2D case
hLine=plot( clrChanA(iPixel), clrChanB(iPixel),...
'Color', normInRGB(iPixel, :),...
'MarkerFaceColor', normInRGB(iPixel, :), varargin{:} );
end
if iPixel==1
hold on; % Enable "hold" starting from the first pixel
elseif iPixel==nPixels
hold off; % Disable "hold" when finished
end
end
else % all markers color are the same
if is3D
hLine=plot3( clrChanA(:), clrChanB(:), chanIntensity(:), varargin{:} );
else
hLine=plot( clrChanA(:), clrChanB(:), varargin{:} );
end
end
%% Return to initial hold state at program start
if xor(holdState, ishold) % if current hold state different from the initial one- change it
hold(hAxes);
end
%% make hLine be the last (first) child, so it will be affected by further operations
% This si good for elgending the plot later on...
hChildren=get(hAxes, 'Children');
iLine=(hChildren==hLine);
% hChildren(iLine)=hChildren(end);
% hChildren(end)=hLine;
% This works fine
hChildren=cat( 1, hLine, hChildren(~iLine) ); % make hLine first
% This brings center and text lable above the plot data
% hChildren=cat( 1, hChildren(~iLine), hLine ); % make hLine last
set(hAxes, 'Children', hChildren);
%% Internal nested function
function imgSpaceData=conv2ImgSpace(clrSpaceData)
is2D=false;
if size(clrSpaceData, 3)==2
clrSpaceData=cat( 3, clrSpaceData, clrSpaceData(:, :, 2) );
is2D=true;
elseif size(clrSpaceData, 2)==2
clrSpaceData=cat( 2, clrSpaceData, clrSpaceData(:, 2) );
is2D=true;
end
imgSpaceData=reshape(clrSpaceData, [], 3);
imgSpaceData(:, iInt)=( imgSpaceData(:, iInt)-minValInt)/(maxValInt-minValInt );
imgSpaceData(:, iClrA)=nA*( imgSpaceData(:, iClrA)-minValClr )/(maxValClr-minValClr);
imgSpaceData(:, iClrB)=nB*( imgSpaceData(:, iClrB)-minValClr )/(maxValClr-minValClr);
if is2D
imgSpaceData(:, end)=[]; % delete last columnn
end
end
end