Save cropped figure in Matlab - matlab

I have opened a highly complex .fig figure from file in matlab with openfig.
After this I have cropped the figure with
axis([X1, X2, Y1, Y2])
I now want save the cropped figure so that it is of a reduced size when saved. The problem is that savefig will save the complete image zoomed to the axis specified. How can I save the figure, permanently cropped to the axis I have specified? e.g. it should not be possible to zoom out when opening the new image.

I don't have MATLAB in front of me, and I don't have your data to test this with, so this is an approximate answer, but as #natan mentions in the comments above, you can harvest the data from the figure, which is stored in 'XData' and 'YData'. You can then crop the data to just the parts you want and then replace the existing data with the cropped data. It will end up looking something like this:
kids = findobj(gca,'Type','line');
K = length(kids);
Xrange = get(gca,'XLim');
Yrange = get(gca,'YLim');
for k = 1:K
X = get(kids(k),'XData');
Y = get(kids(k),'YData');
idx = X >= min(Xrange) & X <= max(Xrange) & Y >= min(Yrange) & Y <= max(Yrange);
set(kids(k),'XDATA',X(idx),'YDATA',Y(idx));
end
You'll have to modify this if your plots have patch objects, bar objects, etc., but the idea is there.
Edge cases:
There are a few edge cases to consider as #Jan has rightly pointed out. The first is that the approach above assumes a rather high density of points even in the expanded scales and will leave a small gap between the end of the line and the axes. For low-point-density lines, you need to expand the idx variable to capture the next point outside the axes in either direction:
idx_center = X >= min(Xrange) & X <= max(Xrange) & Y >= min(Yrange) & Y <= max(Yrange);
idx_plus = [false idx(1:end-1)];
idx_minus = [idx(2:end) false];
idx = idx_center | idx_plus | idx_minus;
That will only expand the number of points if there is at least one point inside the window. The other edge case is where the line passes through the window but does not contain any points inside the window, which would be the case if idx is all false. In that case, you need the largest point outside the left axis and the smallest point outside the right axis. If either of these searches come up empty, then the line doesn't pass through the window and can be discarded.
if ~any(idx)
idx_low = find(X < min(Xrange),1,'last');
idx_high = find(X > max(Xrange),1,'first');
if isempty(idx_low) | isempty(idx_high)
% if there aren't points outside either side of the axes then the line doesn't pass through
idx = false(size(X));
else
idx = idx_low:idx_high;
end
end
This doesn't test whether the line is inside the y-bounds, so it may be possible that the line will pass above or below the window without intersecting. If that is important you will need to test the line connecting the found points. An extra 2 points in memory should not be a big deal. If it is an issue, then I leave it as an exercise to the student to work out the test of whether a line passes through the axes.
Putting it all together:
kids = findobj(gca,'Type','line'); % find children of the axes that are lines
K = length(kids); % count them
Xrange = get(gca,'XLim'); % get the axis limits
Yrange = get(gca,'YLim');
for k = 1:K
X = get(kids(k),'XData'); % pull the x-y data for the line
Y = get(kids(k),'YData');
% find the points inside the window and then dilate the window by one in
% each direction
idx_center = X >= min(Xrange) & X <= max(Xrange) & Y >= min(Yrange) & Y <= max(Yrange);
idx_plus = [false idx(1:end-1)];
idx_minus = [idx(2:end) false];
idx = idx_center | idx_plus | idx_minus;
% handle the edge case where there are no points in the window but the line still passes through
if ~any(idx)
idx_low = find(X < min(Xrange),1,'last');
idx_high = find(X > max(Xrange),1,'first');
if isempty(idx_low) | isempty(idx_high)
% if there aren't points outside either side of the axes then the line doesn't pass
% through
idx = false(size(X));
else
% numerical indexing instead of logical, but it all works the same
idx = idx_low:idx_high;
end
end
if any(idx)
% reset the line with just the desired data
set(kids(k),'XDATA',X(idx),'YDATA',Y(idx));
else
% if there still aren't points in the window, remove the object from the figure
delete(kids(k));
end
end

Related

How to divide the area outside a polygon between left and right with respect to a certain given value in MATLAB?

I have another blocking problem, I extracted the x and y coordinates outside the polygon, when plotting them the result is correct, however, I want to extract x and y coordinates on the right side outside the polygon(considered for x < 1078), and the x and y coordinates on the right side outside the polygon otherwise, however, when running this code (where I think it is a more logical one), it's dividing the whole set of coordinates (in and out of the polygon) between left and right.
This's my code, and the resulted images, the first one is the resulted plot of my code, and the second one is the initial polygon with the inside and outside areas, and if any one has a better idea I will appreciate their help, thanks a lot.
%% Check inpolygon function
nb_lignes = size(image_in_ref,1);
nb_colonnes = size(image_in_ref,2);
[Xq,Yq] = meshgrid(1:nb_colonnes,1:nb_lignes);%grille du cube
[in,on] = inpolygon(Xq,Yq,X_ref,Y_ref);
out = ~inpolygon(Xq,Yq,X_ref,Y_ref);
f = numel(Xq(in));
p = numel(Xq(on));
X_out = Xq(~in);
Y_out = Yq(~in);
figure
plot(X_ref,Y_ref) % polygon
axis equal
hold on
plot(Xq(in),Yq(in),'r-') % points inside
plot(X_out,Y_out,'b:') % points outside
plot(out,'y')
hold off
%% Cocatenate X_out and Y_out into one table
My_Matrix_out=horzcat(X_out(:),Y_out(:));
colNames = {'x_out','y_out'};
sTable_out = array2table(My_Matrix_out,'VariableNames',colNames);
%% Divide between left and right side limits
LeftTable_out = sTable_out(sTable_out.x_out< 1078, :);
RightTable_out = sTable_out(sTable_out.x_out >= 1078 , :);
%% Extract X and Y coordinates from each table separately
x_Left_out = LeftTable_out{:,'x_out'}; % or LeftTable{:,1}
y_Left_out = LeftTable_out{:,'y_out'}; % or LeftTable{:,2}
x_Right_out = RightTable_out{:,'x_out'};
y_Right_out = RightTable_out{:,'y_out'};
%% Plot Each Part Separately
figure
Left_Side_out =plot(x_Left_out,y_Left_out, 'r-');
hold on;
Right_Side_out=plot(x_Right_out, y_Right_out , 'g:');
The resulted plot of the posted code
The polygon with the inside and outside areas

How do I animate a single propagating pulse without showing its entire range and only the pulse itself as its x-axis changes dynamically?

The code found here implements a MATLAB script that solves and draws the 1D wave equation with boundaries at x=0 and x=90. When you run the script in MATLAB it shows a single pulse propagate from x=0 to x=90 and bounce back and forth.
This script shows the entire range of x (i.e. from x=0 to 90) even though the pulse itself is only about 20 units long in the x-direction. See image below:
My question: How do I construct an imaginary green box shown in the image above that only shows the pulse as it moves along instead of the entire range? The imaginary green box moves with the pulse. So when you play the movie, all you see is the pulse and the changing x-axis limits (like x=0 to 20, then x=20 to 40, then x=40 to 60, and so forth).
I think the answer to this question has something to do with the camera position or view, but I am not sure how.
Here is the code:
% 1D wave equation using boundary condition
% Mohammad Ismail Hossain
% Jacobs University Bremen
clc
clear all
c = 1;
l = 4.5;
dt = 0.06;
dx = 0.05;
ldx = 4.5/dx;
x = 1:ldx;
tm = 300;
N = 0.4/dt;
n = 1:8*N;
n0 = 4*N;
En = zeros(tm,ldx);
En0 = zeros(tm,1);
En0(1:8*N) = exp(((-(n-n0).^2)/(2*N^2)));
Ent(1:tm) = 0;
En(1,1) = En0(1);
for p = 2:tm-1
En(p,1) = En0(p);
for q = 2:ldx-1
if (q == (ldx/9)*4)||(q == (ldx/9)*5)
c = sqrt(0.4);
end
if ((q > (ldx/9)*4) && (q < (ldx/9)*5))
c = 0.5;
end
if ((q < (ldx/9)*4) || (q > (ldx/9)*5))
C = 1;
end
En(p+1,q) = ((c*dx)/dt)^2*(En(p,q+1)-2*En(p,q)+En(p,q-1)) + 2*En(p,q) - En(p-1,q);
end
plot(En(p,:))
title(['Time = ',num2str(p+1)]);
YLIM([-1.5 1.5])
pause(0.03);
end
You only need to change xlim dynamically. Ideally it should be synchronized with the speed and direction of the wave, but it's hard to tell what the code is doing (no comments, not very informative variable names).
A more "automatic" approach is to find the position of the absolute maximum of the wave and define xlim around that position. To do this, just add the following lines at the end of the outer loop. Also change YLIM to ylim to avoid an error:
···
title(['Time = ',num2str(p+1)]);
ylim([-1.5 1.5]) % changed line
[~, xm] = max(abs(En(p,:))); % new line: find position of maximum
xlim(xm + [-20 20]) % new line: set xlim around that value
pause(0.03);
···

Adding colors to lines of a stairstep plot

I am trying to plot a hypnogram (graph that shows sleep cycles) and am currently using stairstep function to plot it. Below is a sample data since the one I am working with is huge:
X = linspace(0,4*pi,10);
Y = sin(X);
stairs(X,Y)
How do I make the lines of every ticks/score on the y-axis have a unique color? Which looks something like this:
One way to do it would be to segregate your data into as many dataset as your have flat levels, then plot all these data sets with the required properties.
There is however a way to keep the original dataset into one piece. If we consider your initial example data:
X = linspace(0,4*pi,10);
Y = sin(X);
step 1: recreate a "stair" like data set
Then by recombining the elements of X and Y we can obtain the exact same output than with the stairs function:
x = reshape( [X;X], 1,[] ); x(1) = [] ; % duplicate each element, remove the first one
y = reshape( [Y;Y], 1,[] ); y(end) = [] ; % duplicate each element, remove the lastone
hp = plot(x,y) ;
step 2: Use patch to be able to specify level colors
The patch object has many option for colouring faces, vertex and edges. The default patch object will try to close any profile given in coordinate by joining the first and last point. To override this behaviour, you just need to add a NaN element to the end of the coordinate set and patch will produce a simple line (but all the colouring options remain !).
To determine how many levels and how many colors we will need, we use the function unique. This will tell us how many unique levels exist in the data, and also we can associate each level with an index which will point to the color map.
%% Basic level colored line patch
% create profile for patch object
x = reshape([X;X],1,[]); x(1) = [] ; % same as above to get a "stairs" shape
y = reshape([Y;Y],1,[]); y(end) = [] ; % idem
xp = [x,NaN] ; % add NaN in last position so the patch does not close the profile
yp = [y,NaN]; % idem
% prepare colour informations
[uy,~,colidx] = unique(Y) ;
ncolor = length(uy) ; % Number of unique level
colormap(hsv(ncolor)) % assign a colormap with this number of color
% create the color matrix wich will be sent to the patch object
% same method of interleaving than for the X and Y coordinates
cd = reshape([colidx.';colidx.'],1,[]);
hp = patch(xp,yp,cd,'EdgeColor','interp','LineWidth',2) ;
colorbar
Yes! ... now our flat levels have a colour corresponding to them ... but wait, those pesky vertical lines are still there and polluting the graph. Could we colour them in a different way? Unfortunately no. No worries however, there is still a way to make them completely disappear ...
step 3: Use NaN to disable some segments
Those NaN will come to the rescue again. Any segment defined with a NaN will not be plotted by graphic functions (be it plot, patch, surf or any other ...). So what we can do is again interleave some NaN in the original coordinate set so only the horizontal lines will be rendered. Once the patch is created, we can build a second, "opposite", coordinate set where only the vertical lines are visible. For this second set, since we do not need fancy colouring, we can simply render them with plot (but you could also build a specific patch for that too if you wanted to colour them differently).
%% invisible vertical line patch + dashed vertical lines
% prepare profile points, interleaving NaN between each pair
vnan = NaN(size(X)) ;
xp = reshape([X;vnan;X],1,[]); xp([1:2 end]) = [] ;
yp = reshape([Y;Y;vnan],1,[]); yp(end-2:end) = [] ;
% prepare the vertical lines, same method but we interleave the NaN at one
% element offset
xv = reshape([X;X;vnan],1,[]); xv([1:3 end]) = [] ;
yv = reshape([Y;vnan;Y],1,[]); yv([1:2 end-1:end]) = [] ;
% prepare colormap and color matrix (same method than above)
[uy,~,colidx] = unique(Y) ;
ncolor = length(uy) ; % Number of unique level
colormap(hsv(ncolor)) % assign a colormap with this number of color
% create the color matrix wich will be sent to the patch object
% same method of interleaving than for the X and Y coordinates
cd = reshape([colidx.';colidx.';vnan],1,[]); cd(end-2:end) = [] ;
% draw the patch (without vertical lines)
hp = patch(xp,yp,cd,'EdgeColor','flat','LineWidth',2) ;
% add the vertical dotted lines
hold on
hv = plot(xv,yv,':k') ;
% add a label centered colorbar
colorbar('Ticks',((1:ncolor)+.5)*ncolor/(ncolor+1),'TickLabels',sprintf('level %02d\n',1:ncolor))
I have used the hsv colormap in the last example because your example seems to indicate that you do not need gradually progressing colors. You could also define a custom colormap with the exact color you want for each level (but that would be another topic, already covered many time if you search for it on Stack Overflow).
Happy R.E.M. sleeping !
Below code is not that efficient, but works well.
Basically, it draws line by line from left to right.
Firstly, generate sample data
num_stage = 6;
% generate sample point
x = linspace(0,1,1000)';
% generate its stage
y = round((sin(pi*x)+1)*(num_stage-1)/2)/(num_stage-1);
stage = unique(y); % find value of each stage
color_sample = rand(num_stage,3); % set color sample
Then we can draw like this
idx = find([1;diff(y)]); % find stage change
idx(end+1) = length(x)+1; % add last point
% display routine
figure;
% left end stage
k = 1;
% find current stage level
c = find(stage == y(idx(k)));
% plot bold line
plot(x([idx(k),idx(k+1)-1]),y(idx(k))*ones(2,1),'color',color_sample(c,:),'linewidth',5);
hold on;
for k = 2 : length(idx)-1
% find current stage level
c = find(stage == y(idx(k)));
% plot dashed line from left stage to current stage
plot(x([idx(k)-1,idx(k)]),[y(idx(k-1));y(idx(k))],'--','color',[0.7,0.7,0.7]);
% plot bold line for current stage with specified color
plot(x([idx(k),idx(k+1)-1]),y(idx(k))*ones(2,1),'color',color_sample(c,:),'linewidth',5);
end
% set x-axis
set(gca,'xlim',[x(1),x(end)]);
Following is result
Use if statement and divide it blocks. Check the criteria of Y-axis to be in a certain range and if it falls in that range, plot it there using the colors you want. For example if (y>1) plot(x,y,'r') else if (y some range) plot(x,y,'b'). Hope it helps

Separating axes from plot area in MATLAB

I find that data points that lie on or near the axes are difficult to see. The obvious fix, of course, is to simply change the plot area using axis([xmin xmax ymin ymax]), but this is not preferable in all cases; for example, if the x axis is time, then moving the minimum x value to -1 to show activity at 0 does not make sense.
Instead, I was hoping to simply move the x and y axes away from the plot area, like I have done here:
left: MATLAB generated, right: desired (image editing software)
Is there a way to automatically do this in MATLAB? I thought there might be a way to do it by using the outerposition axes property (i.e., set it to [0 0 0.9 0.9] and drawing new axes where they originally were?), but I didn't get anywhere with that strategy.
The answers here already show you most of the way - here is the last step to separate the x and y axle as per the example you put together.
f = figure ( 'color', 'white' );
% create the axes and set some properties
ax = axes ( 'parent', f, 'box', 'off', 'nextplot', 'add', 'XMinorTick', 'on', 'YMinorTick', 'on' );
% plot some data
plot ( ax, 0:10, [0:10].^2, 'rx-' )
% modify the x and y limits to below the data (by a small amount)
ax.XLim(1) = ax.XLim(1)-(ax.XTick(2)-ax.XTick(1))/4;
ax.YLim(1) = ax.YLim(1)-(ax.YTick(2)-ax.YTick(1))/4;
% Set the tick direction
ax.TickDir = 'out';
% draw the plot to generate the undocumented vertex data var
drawnow()
%% R2015a
% X, Y and Z row of the start and end of the individual axle.
ax.XRuler.Axle.VertexData(1,1) = 0;
ax.YRuler.Axle.VertexData(2,1) = 0;
%% R2015b
% extract the x axis vertext data
% X, Y and Z row of the start and end of the individual axle.
vd = get(ax.XAxis.Axle,'VertexData');
% reset the zero value
vd(1,1) = 0;
% Update the vertex data
set(ax.XAxis.Axle,'VertexData',vd);
% repeat for Y (set 2nd row)
vd = get(ax.YAxis.Axle,'VertexData');
vd(2,1) = 0;
set(ax.YAxis.Axle,'VertexData',vd);
Edit: The vertex is something that Matlab recreates whenever the axes/figure changes size or if you zoom or pan for example.
You can try to counteract this (remember you are using undocumented features here) by adding a listener to attempt to capture this. We can use the MarkedClean event which is called quite a lot of times.
addlistener ( ax, 'MarkedClean', #(obj,event)resetVertex(ax) );
Where you resetVertex function is something like: (R2015b shown only)
Edit 2 added the code to turn off the minor ticks below 0.
function resetVertex ( ax )
% extract the x axis vertext data
% X, Y and Z row of the start and end of the individual axle.
ax.XAxis.Axle.VertexData(1,1) = 0;
% repeat for Y (set 2nd row)
ax.YAxis.Axle.VertexData(2,1) = 0;
% You can modify the minor Tick values by modifying the vertex data
% for them, e.g. remove any minor ticks below 0
ax.XAxis.MinorTickChild.VertexData(:,ax.XAxis.MinorTickChild.VertexData(1,:)<0) = [];
ax.YAxis.MinorTickChild.VertexData(:,ax.YAxis.MinorTickChild.VertexData(2,:)<0) = [];
end
Note: this uses undocumented features -> so may only work in certain versions of Matlab (I have added the code for r2015a & r2015b) and Matlab may recreate the vertex data depending on what you do with the plots..
Here is a simple way for achieving that:
% some data:
x = 1:100;
f=#(x) 5.*x;
y=f(x)+rand(1,length(x))*50;
close all
% plotting:
f1 = figure('Color','white');
ax = axes;
plot(ax,x,y,'o');
% 'clean' the data area a little bit:
box off
ax.TickDir = 'out';
% pushing the axis a bit forward:
lims = axis;
pos = ax.Position;
axis([lims(1)-ax.XTick(2)/5 lims(2)+0.1 lims(3)-ax.YTick(2)/5 lims(4)+0.1])
% Create lines
firstXtick = 0.013; %this value need to be adjusted only once per figure
firstYtick = 0.023; %this value need to be adjusted only once per figure
lx = annotation(f1,'line',[pos(1) pos(1)+firstXtick],...
[pos(2) pos(2)],'Color',[1 1 1],'LineWidth',1);
ly = annotation(f1,'line',[pos(1) pos(1)],...
[pos(2) pos(2)+firstYtick],'Color',[1 1 1],'LineWidth',1);
Which yields this figure:
The only thing to adjust here, once per type of figure, is firstXtick and firstYtick values, that have to be fine tuned to the specific axis. After setting them to the correct value the figure can be resized with no problem. Zoom and pan require a little fixes.
You can start your axes from less than zero and then remove the less than zero ticks from your plot. e.g.
plot(0:3:30,0:3:30); %Some random data for plotting
h = gca;
axis([-1 30 -1 30]); %Setting the axis from less than zero
box off; %Removing box
h.TickDir = 'out'; %Setting Direction of ticks to outwards
h.XTickLabel(1)= {' '}; %Removing the first tick of X-axis
h.YTickLabel(1)= {' '}; %Removing the first tick of Y-axis
With this code, you'll get this result:
This may have a drawback, sometimes, that zero ticks may also get removed (as you can see in above figure). This is because the plot had set the first ticks of axes equal to zero. This can be avoided using if condition. So, the code can be modified as below:
plot(0:3:30,0:3:30);
h = gca;
axis([-1 30 -1 30]);
box off;
h.TickDir = 'out';
if str2num(cell2mat(h.XTickLabel(1))) <0
h.XTickLabel(1)= {' '};
end
if str2num(cell2mat(h.YTickLabel(1))) <0
h.YTickLabel(1)= {' '};
end
The above code will yield the following result:-
Also note that, for your case, since your axes ticks are very less, -1 may not be much suitable for the starting value of axes and you may need to use -0.1 instead i.e. axis([-0.1 30 -0.1 30]);
With a slight modification of #matlabgui's answer you can track the (major) tick limits:
ax = gca();
% Set the tick direction
ax.TickDir = 'out';
% Make sure this stays when saving, zooming, etc
addlistener ( ax, 'MarkedClean', #(obj,event) change_ticks(ax) );
% Draw the plot to generate the undocumented vertex data variable
% and call callback for the first time
drawnow();
The callback
function change_ticks( ax )
% Modify ruler
ax.XRuler.Axle.VertexData(1,1) = ax.XTick(1);
ax.XRuler.Axle.VertexData(1,2) = ax.XTick(end);
ax.YRuler.Axle.VertexData(2,1) = ax.YTick(1);
ax.YRuler.Axle.VertexData(2,2) = ax.YTick(end);
end
I haven't test extensively but seems to work for custom ticks too. This nicely cuts the rulers not only on zero but beyond the fist and last tick. This was tested in Matlab 2019a on Windows and ax.XRuler.Axle.VertexData works just fine. Note this is only for major ticks!

Matlab - Find Coordinates between a straight line and a perimeter

I segmented a mouse and get its image-properties using bwlabel. Thereby I have access to the position of the centroid and the orientation of the mouse. I also get the perimeter of the mouse using bwperim.
I want to find the two points of the straight line passing through the centroid and having the same direction than the orientation of the mouse cutting the perimeter.
I find the equation of the straight line using that code :
% E is a 2*2 matrix containing the coordinates of the centroid and the
% coordinates of the point which belong to the straight line and making
% the right angle given by the orientation
coeffs = polyfit(E(:,1),E(:,2),1);
% Create the equation of the straight line
x = 1:width;
yfit = coeffs(1)*x+coeffs(2);
% Make sure there are only int values.
yfit = uint16(yfit);
I convert my values to uint16 because i want to fill a new matrix that I will compare with the matrix containing the perimeter. Here is what I do then :
% Create a matrix of zeros and set to 1 all the pixels which belong to the
% straight line
k = 1;
temp = false;
m = false(size(iPerim));
while temp~=true
temp = false;
if yfit(k) > 0
m(yfit(k),k)=1;
temp = true;
end
k = k+1;
end
[t,p] = ind2sub(size(m), find(m==1));
minM = [min(p),min(t)];
% complete the straight line to don't have little holes
x = linspace(minM(1),D(1),width);
y = coeffs(1)*x+coeffs(2);
idx = sub2ind(size(m),round(y),round(x));
m(idx) = 1;
Then I compare m with iPerim which is the matrix containing my perimeter:
% Compare the matrix of the perimeter and the matrix of the straight line
% and find the two points in common. It is the points where the straight
% line cut the perimeter
p = m & iPerim;
% Extract thoses coordinates
[coordsY,coordsX] = ind2sub(size(p), find(p==1));
Well I am a new user of Matlab so I think this is not a elegant solution but there is the result:
Matrix m
Perimeter in which I plot yfit
As you can see the algorithm detects only one point and not the second one (the yellow spot)... I figure why but I can't find the solution. It is because the line straight is cutting the perimeter through a diagonal but there are not coordinates in common...
Somebody has a solution to my problem ? And of course I am taking any advises conerning my code :)
Thank you very much !
Edit: If there is a easier solution I take it obviously
When the coordinate of the point where the mouse-perimeter and the line cross are E(2,:), then the position of this point in the line is where the distance is minimal. E.g. like:
[xLine, yLine] = find(m); % x,y positions of the line
dX = abs(xline-E(2,1)) % x-distance to x-coordinate of direction-point
dY = abs(yLine-E(2,2)) % y-distance to y-coordinate of direction-point
distP = sqrt(dX.^2+dY.^2) % distance of line-points to directon-point
[~,indMin] = min(distP); % index of line-point which has the minimum distance
xPoint = xLine(indMin(1));
yPoint = yLine(indMin(1));
The abs and sqrtfunctions are not necessary here for finding the right point, only for the correct intermediate values...
From the Matlab Documentation about ind2sub:
For matrices, [I,J] = ind2sub(size(A),find(A>5)) returns the same values as [I,J] = find(A>5).