38 properties (Constant, Access =
private)
39 %> names of the variables in the waypoints table
40 tabVarNames = ["t_start", "t_stay","pos","ori"];
42 %> types of the variables in the waypoints table
43 tabVarTypes = ["duration", "duration","cell","quaternion"];
45 %> available interpolation methods
for position
46 interpMethodsPos = ["minJerk","minSnap","linear","spline","pchip","makima"];
48 %> available interpolation methods
for orientation
49 interpMethodsOri = ["linear","spline","pchip","makima"];
51 %> names of the variables in the trajectory table
52 trajVarNames = ["pos","vel","acc","jerk","ori","gyr"];
54 %> types of the variables in the trajectory table
55 trajVarTypes = ["cell","cell","cell","cell","quaternion", "cell"];
57 %> units of the variables in the trajectory table
58 trajVarUnits = ["m","m/s","m/s^2","m/s^3","quaternion", "deg/s"];
61 properties (Access =
private)
62 %> sampling frequency in Hz
71 %> interpolation method of the position
74 %> interpolation method of the orientation
77 %> flag
if the trajectory is generated and up to date
81 methods (Access =
public)
82 % ==================================================================
85 %> @param options (optional) - Structure specifying custom settings for the
Trajectory object.
86 %> - options.fs (optional) - Sampling frequency in Hz (default: 200)
88 %> @retval obj - The created Trajectory object.
89 % ==================================================================
90 function obj = Trajectory(options)
93 options.fs (1,1) double = 200
97 obj.
tab = table(
'Size',[0,numel(obj.tabVarNames)],
'VariableNames', obj.tabVarNames,
'VariableTypes', obj.tabVarTypes); % waypoints table
99 obj.interpMethodPos = obj.interpMethodsPos(1);
100 obj.interpMethodOri = obj.interpMethodsOri(1);
103 % ==================================================================
108 %> @retval obj - Copy of the
Trajectory object
109 %> ==================================================================
110 function obj = copy(obj)
115 obj.interpMethodPos = obj.interpMethodPos;
116 obj.interpMethodOri = obj.interpMethodOri;
117 obj.isGenerated = obj.isGenerated;
120 % ==================================================================
121 %> @brief Add waypoints to the trajectory
123 %> addWaypoints(obj,t_start,t_stay,pos,ori) adds waypoints to the
124 %> trajectory from the inputs. The waypoints are defined by their
125 %> start time, stay time, position, and orientation.
127 %> @param t_start - Start time of each waypoint (duration array)
128 %> @param t_stay - Duration of each waypoint (duration array)
129 %> @param pos - Position of each waypoint (Nx3
double array)
130 %> @param ori - Orientation of each waypoint (quaternion array)
134 %> @note The inputs must have the same height.
135 % ==================================================================
136 function addWaypoints(obj,t_start,t_stay,pos,ori)
140 t_start (:,1) duration
141 t_stay (:,1) duration
146 % make sure the height of the inputs is the same
148 assert(all([height(t_stay),height(pos),height(ori)] == n),
"The inputs must have the same height");
150 %
try to add the waypoints to the table
153 obj.tab = table(t_start, t_stay, pos, ori, 'VariableNames', obj.tabVarNames);
155 obj.tab = [obj.tab; table(t_start, t_stay, pos, ori, 'VariableNames', obj.tabVarNames)];
158 error("The inputs are not compatible with the waypoints table");
161 % flag the trajectory as not generated
162 obj.isGenerated = false;
165 % ==================================================================
166 %> @brief set the sampling frequency
168 %> @param fs - Sampling frequency in Hz (positive integer)
172 %> @note The Sampling frequency is used to generate the trajectory
173 % ==================================================================
174 function setSamplingFrequency(obj,fs)
178 fs (1,1)
double {mustBePositive,mustBeInteger}
181 % update the sampling frequency
184 % flag the trajectory as not generated
185 obj.isGenerated =
false;
188 % ==================================================================
189 %> @brief get the sampling frequency
193 %> @retval fs - Sampling frequency in Hz
194 % ==================================================================
195 function fs = getSamplingFrequency(obj)
199 % ==================================================================
200 %> @brief plot the waypoints
202 %> plotWaypoints(obj,ax,options) plots the waypoints on the
203 %> specified axes
using the custom settings specified by the options
204 %> structure. The resulting plot is a 3D plot with the waypoints
205 %> represented by a model of the JUMP sensor and a line connecting the
208 %> @param ax (optional) - Axes to plot the waypoints on
209 %> @param options (optional) - Structure specifying custom settings for the plot
210 %> - options.scale (optional) - Scale factor for the waypoints (default: 10)
211 %> - options.numbers (optional) - Flag to display the waypoint numbers (default: true)
212 %> - options.line (optional) - Flag to display a line between the waypoints (default: true)
214 %> @retval fig - Figure handle of the plot
215 % ==================================================================
216 function fig = plotWaypoints(obj, ax, options)
221 options.scale (1,1) double = 10
222 options.numbers (1,1) logical = true
223 options.line (1,1) logical = true
227 for i = 1:height(obj.tab)
228 poseplot(ax, obj.tab.ori(i), obj.tab.pos(i,:), "NED",...
229 MeshFileName = "jumpSimplified.stl", PatchFaceAlpha = 0.8, ...
230 PatchFaceColor = [0,0.57,0.82], ScaleFactor = options.scale);
233 % plot the waypoint numbers above the waypoints
235 offset = 0.02*options.scale; % make sure the numbers are not hidden by the mesh
236 text(obj.tab.pos(i,1)-offset,obj.tab.pos(i,2)+offset,obj.tab.pos(i,3)-offset,...
237 num2str(i),
'FontSize',12,
'Color',
'white',
'FontWeight',
'bold',
'BackgroundColor',
'black');
244 % display a line between the waypoints with changing color
246 colors = hsv(height(obj.tab)-1); % generate a colormap based on the number of waypoints
247 for i = 1:height(obj.tab)-1
248 plot3(ax, obj.tab.pos(i:i+1,1), obj.tab.pos(i:i+1,2), obj.tab.pos(i:i+1,3), 'Color', colors(i,:), 'LineWidth', 2);
252 % add a colorbar to the figure
254 clim([1,height(obj.tab)]);
255 c = colorbar(
"Direction",
"reverse");
256 c.Label.String =
'Waypoint number';
257 c.Label.FontSize = 14;
258 c.Ticks = 1:height(obj.tab);
259 c.Limits = [1,height(obj.tab)];
267 % ==================================================================
268 %> @brief
return the waypoints table in a reduced format
272 %> @retval tab - Waypoints table in a reduced format
275 % ==================================================================
276 function tab = reduce(obj)
278 if height(obj.tab) == 0
279 tab = table('Size',[0,9],'VariableNames',["t_start","t_stay","x","y","z","q","i","j","k"],'VariableTypes',["string","string","double","double","double","double","double","double","double"]);
283 t_start = string(obj.tab.t_start,
"mm:ss.SSS");
284 dur = string(obj.tab.t_stay,
"mm:ss.SSS");
286 x = obj.tab.pos(:,1);
287 y = obj.tab.pos(:,2);
288 z = obj.tab.pos(:,3);
289 quat = obj.tab.ori.compact;
295 tab = table(t_start,dur,x,y,z,q,i,j,k,
'VariableNames',[
"t_start",
"t_stay",
"x",
"y",
"z",
"q",
"i",
"j",
"k"]);
298 % ==================================================================
299 %> @brief
import a waypoints table
301 %> @param tab - Waypoints table
305 %> @note this is used in the trajectoryGenerationApp. The table must
306 %> have the same format as the one returned by the reduce method.
307 % ==================================================================
308 function importTab(obj,tab)
310 t_start = duration(tab.t_start,"InputFormat",
"mm:ss.SSS");
311 t_stay = duration(tab.t_stay,
"InputFormat",
"mm:ss.SSS");
313 pos = [
tab.x,tab.y,tab.z];
314 ori = quaternion(tab.q,tab.i,tab.j,tab.k);
316 obj.tab = table(t_start, t_stay, pos, ori,
'VariableNames', obj.tabVarNames);
318 % flag the trajectory as not generated
319 obj.isGenerated =
false;
322 % ==================================================================
323 %> @brief
export the waypoints table
327 %> @retval tab - Waypoints table
328 % ==================================================================
329 function tab = getTab(obj)
333 % ==================================================================
334 %> @brief set the waypoints table
336 %> @param tab - Waypoints table
339 % ==================================================================
340 function setTab(obj,tab)
343 % flag the trajectory as not generated
344 obj.isGenerated =
false;
347 % ==================================================================
348 %> @brief sort the waypoints table in ascending order of start time
353 % ==================================================================
354 function sortTab(obj)
355 % SORTTAB Sort the waypoints table by start time
357 obj.tab = sortrows(obj.tab,1);
360 % ==================================================================
361 %> @brief clean the waypoints table
363 %> cleanTab(obj) removes duplicate waypoints, rounds the start and
364 %> stay times to the nearest multiple of the sampling time, and checks
365 %>
for time overlapping. The resulting table is sorted by start time.
370 % ==================================================================
371 function cleanTab(obj)
374 tab_backup = obj.tab;
376 % make sure the table is sorted
379 %
if the smallest t_start is not 0, add a waypoint at the origin
380 if obj.tab.t_start(1) > seconds(0)
381 obj.addWaypoints(seconds(0),seconds(0),[0,0,0],quaternion(1,0,0,0));
382 obj.sortTab(); % make sure the new waypoint is at the top
385 % remove duplicate waypoints (same t_start)
386 [~,idx] = unique(obj.tab.t_start);
387 if isempty(idx) && height(obj.tab) > 0
390 obj.tab = obj.tab(idx,:);
392 % round the start time to the nearest multiple of the sampling time
393 obj.tab.t_start = round(obj.tab.t_start/seconds(1/obj.fs))*seconds(1/obj.fs);
395 % round the stay time to the nearest multiple of the sampling time
396 obj.tab.t_stay = round(obj.tab.t_stay/seconds(1/obj.fs))*seconds(1/obj.fs);
398 % check for time overlapping (t_start + t_stay must be < t_start of the next waypoint)
399 for i = 1:height(obj.tab)-1
400 if obj.tab.t_start(i) + obj.tab.t_stay(i) > obj.tab.t_start(i+1) - seconds(2/obj.fs)
401 % reduce the t_stay of the current waypoint
402 obj.tab.t_stay(i) = obj.tab.t_start(i+1) - obj.tab.t_start(i) - seconds(2/obj.fs);
406 % flag the trajectory as not generated if the table has changed
407 if ~isequal(tab_backup,obj.tab)
408 obj.isGenerated = false;
412 % ==================================================================
413 %> @brief get the interpolation methods
417 %> @retval methods - Structure containing the available interpolation
418 %> methods for position and orientation, as well as the currently
420 % ==================================================================
421 function methods = getInterpMethods(obj)
423 methods.pos = obj.interpMethodsPos;
424 methods.ori = obj.interpMethodsOri;
425 methods.posCurrent = obj.interpMethodPos;
426 methods.oriCurrent = obj.interpMethodOri;
429 % ==================================================================
430 %> @brief set the interpolation methods
432 %> setInterpMethods(obj,methods) sets the interpolation methods for
433 %> position and orientation.
435 %> @param methods.pos (optional) - Interpolation method for position
436 %> @param methods.ori (optional) - Interpolation method for orientation
440 %> @note The interpolation methods must be part of the available
441 %> interpolation methods. The available interpolation methods can be
442 %> obtained by calling the getInterpMethods() method.
443 % ==================================================================
444 function setInterpMethods(obj,methods)
446 if isfield(methods,"pos")
447 if ismember(methods.pos,obj.interpMethodsPos)
448 obj.interpMethodPos = methods.pos;
451 if isfield(methods,"ori")
452 if ismember(methods.ori,obj.interpMethodsOri)
453 obj.interpMethodOri = methods.ori;
457 % flag the trajectory as not generated
458 obj.isGenerated = false;
461 % ==================================================================
462 %> @brief Generate the trajectory from the waypoints table
464 %> generateTrajectory(obj) generates the trajectory from the waypoints
465 %> table using the default settings. For the generation of the
466 %> trajectory, the position is interpolated to get a trajectory with
467 %> the selected sampling frequency matching the waypoints.
469 %> generateTrajectory(obj, options) generates the trajectory from the
470 %> waypoints table using custom settings specified by the options
473 %> @param options (optional) - Custom settings for the trajectory
475 %> @param options.pos (optional) - Interpolation method for
477 %> @param options.ori (optional) - Interpolation method for
479 %> @param options.fs (optional) - Sampling frequency in Hz
480 % ==================================================================
481 function generateTrajectory(obj, options)
485 options.pos (1,1)
string = obj.interpMethodPos
486 options.ori (1,1)
string = obj.interpMethodOri
487 options.fs (1,1)
double = obj.fs
490 % update the settings of the obj from the options
491 obj.setSamplingFrequency(options.fs);
492 obj.setInterpMethods(options);
494 % make sure the table is clean
497 % get the time limits of the trajectory
498 traj_t_start = obj.tab.t_start(1);
499 traj_t_end = obj.tab.t_start(end) + obj.tab.t_stay(end);
501 % generate the time vector
502 t = (traj_t_start:seconds(1/obj.fs):traj_t_end)';
504 % generate the trajectory table
505 obj.traj = timetable(...
507 nan(height(t),3),... % position
508 nan(height(t),3),... % velocity
509 nan(height(t),3),... % acceleration
510 nan(height(t),3),... % jerk
511 quaternion.zeros(height(t),1),... % orientation
512 nan(height(t),3),... % angular velocity
513 'VariableNames',obj.trajVarNames);
515 obj.traj.Properties.VariableUnits = obj.trajVarUnits;
517 % fill the known values and save the start and end indices of each waypoint
518 idx_start = zeros(height(obj.tab),1);
519 idx_end = zeros(height(obj.tab),1);
521 for i = 1:height(obj.tab)
522 % find the indices of the current waypoint in the trajectory table
523 idx_start(i) = find(t >= obj.tab.t_start(i),1);
524 idx_end(i) = find(t >= obj.tab.t_start(i) + obj.tab.t_stay(i),1);
527 obj.traj.pos(idx_start(i):idx_end(i),:) = repmat(obj.tab.pos(i,:),idx_end(i)-idx_start(i)+1,1);
529 % fill the orientation
530 obj.traj.ori(idx_start(i):idx_end(i)) = repmat(obj.tab.ori(i),idx_end(i)-idx_start(i)+1,1);
533 % interpolate the position, velocity, acceleration and jerk
534 switch obj.interpMethodPos
535 case {
"minJerk",
"minSnap"}
536 assert(numel(idx_start) == numel(idx_end),
"The number of start and end indices must be the same");
539 % explanation of the usage of a
while loop and the iExtra variable:
540 %
if the waypoint has no stay time, the position should not be
541 % slowed down to 0 at the end of the waypoint. Therefore, we
542 % interpolate trough the waypoints with no stay time and
543 % increment the index by the number of waypoints with no stay time
545 while (i < numel(idx_start))
547 %
handle waypoints with no stay time
549 while i+iExtra < numel(idx_start)-1
550 if idx_start(i+iExtra+1) == idx_end(i+iExtra+1)
556 idx = zeros(1,iExtra+2);
559 idx(j+1) = idx_start(j+i);
562 switch obj.interpMethodPos
564 [pos, vel, acc, jerk, ~, ~, t] = minjerkpolytraj(obj.traj.pos(idx,:)
', seconds(obj.traj.t(idx))', idx(end)-idx(1));
567 [pos, vel, acc, jerk, ~, ~, ~, t] = minsnappolytraj(obj.traj.pos(idx,:)
', seconds(obj.traj.t(idx))', idx(end)-idx(1));
571 % since the functions
do not sample at the exact wanted time, we need to interpolate the results
572 tt = timetable(seconds(t
'),pos',vel
',acc',jerk
','VariableNames
',["pos","vel","acc","jerk"]);
573 tt = retime(tt, obj.traj.t(idx(1):idx(end)), 'linear
');
575 % fill the trajectory table
576 assert(all(tt.Time == obj.traj.t(idx(1):idx(end))), "The time vectors do not match");
577 obj.traj.pos(idx(1):idx(end),:) = tt.pos;
578 obj.traj.vel(idx(1):idx(end),:) = tt.vel;
579 obj.traj.acc(idx(1):idx(end),:) = tt.acc;
580 obj.traj.jerk(idx(1):idx(end),:) = tt.jerk;
582 % increment the index
587 case {"linear","spline","pchip","makima"}
588 % interpolate the actual position
589 obj.traj.pos = fillmissing(obj.traj.pos,obj.interpMethodPos,1);
591 % find the velocity, acceleration, and jerk from the position
592 obj.traj.vel = gradient(obj.traj.pos',1/obj.fs)
';
593 obj.traj.acc = gradient(obj.traj.vel',1/obj.fs)
';
594 obj.traj.jerk = gradient(obj.traj.acc',1/obj.fs)
';
596 % Suppress extremely small values
598 obj.traj.vel(abs(obj.traj.vel) < threshold) = 0;
599 obj.traj.acc(abs(obj.traj.acc) < threshold) = 0;
600 obj.traj.jerk(abs(obj.traj.jerk) < threshold) = 0;
605 % interpolate the orientation
606 f = nan(height(obj.traj),1);
607 f(idx_end) = 0:numel(idx_end)-1; %end of the waypoint = start of the orientation change
608 f(idx_start) = 0:numel(idx_end)-1; %start of the waypoint = end of the orientation change
610 % fill the missing values during a stay time
611 for i=1:numel(idx_start)
612 f(idx_start(i):idx_end(i)) = fillmissing(f(idx_start(i):idx_end(i)),"nearest");
615 % add values to start and end to smooth beginning and stop
616 f = [zeros(10,1); f; ones(10,1)*max(f)];
618 % fill the missing values with the interpolation method
619 f = fillmissing(f,obj.interpMethodOri,1);
621 % remove the added ones and zeros
624 % restrict to numbers greater than one
625 f(f > 1) = f(f > 1) - floor(f(f > 1));
627 for i = 1: numel(idx_start)-1
628 idx = [idx_end(i), idx_start(i+1)];
630 % define the normalized amount of the transformation
631 this_f = f(idx(1):idx(2));
633 % make sure, beginning and end ar correct (problem occurs
634 % with zero stay waypoints)
639 ori = nan(numel(this_f),4);
640 % NOTE : can also be a parfor loop
641 for j = 1:numel(this_f)
642 ori(j,:) = quatinterp(obj.traj.ori(idx(1)).normalize.compact,obj.traj.ori(idx(2)).normalize.compact,this_f(j),"slerp");
645 % fill the trajectory table
646 obj.traj.ori(idx(1):idx(2)) = quaternion(ori);
650 % fill the missing values
651 obj.traj.ori = obj.quaternionFillMissing(obj.traj.ori);
652 obj.traj.gyr = angvel(obj.traj.ori,1/obj.fs,"frame") * 180/pi;
653 obj.traj(:,1:end-2) = fillmissing(obj.traj(:,1:end-2),"nearest");
655 % flag the trajectory as generated
656 obj.isGenerated = true;
660 % ==================================================================
661 %> @brief get the trajectory timetable
665 %> @retval traj - Trajectory timetable
666 % ==================================================================
667 function traj = getTrajectory(obj)
669 obj.generateTrajectory();
674 function isGenerated = getIsGenerated(obj)
675 isGenerated = obj.isGenerated;
679 methods (Access = private, Static)
681 % ==================================================================
682 %> @brief fill missing values in a quaternion array
684 %> @param q - Quaternion array
686 %> @retval qFilled - Quaternion array with filled missing values
687 % ==================================================================
688 function qFilled = quaternionFillMissing(q)
694 qFilled = q; % Initialize filled quaternion vector with original values
696 % Identify zero quaternions
697 isZero = arrayfun(@(x) isequal(x, quaternion.zeros(1)), q);
698 isZeroIdx = find(isZero);
700 for i = 1:numel(isZeroIdx) % Iterate over indices of zero quaternions
703 % check previous values
704 prevIdx = isZeroIdx(i) - j;
707 qFilled(isZeroIdx(i)) = q(prevIdx);
713 nextIdx = isZeroIdx(i) + j;
716 qFilled(isZeroIdx(i)) = q(nextIdx);