--- a +++ b/Cobiveco/@cobiveco/computeApicobasal.m @@ -0,0 +1,210 @@ +function computeApicobasal(o) + +if ~o.available.rotational + o.computeRotational; +end +o.printStatus('Computing apicobasal coordinate...'); +t = toc; + +% parameters +numTmVal = 10; % number of contour lines in transmural direction +numRtVal = 48; % number of contour lines in rotational direction, ideally a multiple of 6 +numClPoints = 100; % number of points per contour line after resampling using cubic splines + +% compute contour surfaces of tm (cs) +tmp = vtkDeleteDataArrays(o.m1.vol); +tmp.pointData.tm = o.m1.tm; +tmVal = linspace(0.5/numTmVal, 1-0.5/numTmVal, numTmVal); +cs = vtkContourFilter(tmp, 'points', 'tm', tmVal); + +% assign regions to cs +cs = vtkConnectivityFilter(cs); +cs.pointData.csRegion = cs.pointData.RegionId; +cs = vtkDeleteDataArrays(cs, {'pointData.csRegion'}); + +% interpolate abLaplace from o.m2.vol to cs +Mcs = baryInterpMat(o.m2.vol.points, o.m2.vol.cells, cs.points); +cs.pointData.abLaplace = Mcs * o.m2.abLaplace; + +if o.cfg.exportLevel > 2 + vtkWrite(cs, sprintf('%sabContourSurfaces.vtp', o.cfg.outPrefix)); +end + +% find apex point for each csRegion +csRegions = unique(cs.pointData.csRegion); +apexPoints = NaN(numel(csRegions),3); +for i = 1:numel(csRegions) + pointIds = find(cs.pointData.csRegion==csRegions(i)); + [~,minId] = min(cs.pointData.abLaplace(pointIds)); + apexPoints(i,:) = cs.points(pointIds(minId),:); +end + +% compute contour lines of rtSin (rtSinCl) +tmp = cs; +tmp.pointData.rtSin = Mcs * o.m2.rtSin; +rtSinVal = sin(pi*linspace(-0.25, 0.25, round(numRtVal/2)+1)); +rtSinCl = vtkContourFilter(tmp, 'points', 'rtSin', rtSinVal); +rtSinCl.pointData = rmfield(rtSinCl.pointData, 'rtSin'); + +% compute contour lines of rtCos (rtCosCl) +tmp = cs; +tmp.pointData.rtCos = Mcs * o.m2.rtCos; +rtCosVal = rtSinVal(2:end-1); +rtCosCl = vtkContourFilter(tmp, 'points', 'rtCos', rtCosVal); +rtCosCl.pointData = rmfield(rtCosCl.pointData, 'rtCos'); + +% combine rtSin and rtCos contour lines (cl) +cl = vtkAppendPolyData({rtSinCl, rtCosCl}); + +% exclude points close to the apex to split the contour lines into two parts +% apexRadius needs to be large enough for each contour line to be splitted +apexRadius = 3*o.m1.meanEdgLen; +cl.pointData.clRegion = zeros(size(cl.points,1),1); +for i = 1:numel(csRegions) + pointIds = find(cl.pointData.csRegion==csRegions(i)); + ids = rangesearch(cl.points(pointIds,:), apexPoints(i,:), apexRadius); + cl.pointData.clRegion(pointIds(ids{1})) = -1; +end +cl = vtkThreshold(cl, 'points', 'clRegion', [0 inf]); + +% identify separate contour lines --> clRegion +cl = vtkConnectivityFilter(cl); +cl.pointData.clRegion = cl.pointData.RegionId; +cl.pointData = rmfield(cl.pointData, 'RegionId'); +cl = rmfield(cl, 'cellData'); + +% exclude too short contour lines based on values of abLaplace +clRegions = unique(cl.pointData.clRegion); +abLaplaceMin = NaN(numel(clRegions),1); +abLaplaceMax = NaN(numel(clRegions),1); +pointIds = cell(numel(clRegions),1); +for i = 1:numel(clRegions) + pointIds{i} = find(cl.pointData.clRegion==clRegions(i)); + abLaplaceVal = cl.pointData.abLaplace(pointIds{i}); + abLaplaceMin(i) = min(abLaplaceVal); + abLaplaceMax(i) = max(abLaplaceVal); +end +abLaplaceMinThresh = min(abLaplaceMin) + 2*(median(abLaplaceMin)-min(abLaplaceMin)); +abLaplaceMaxThresh = 0.99; +for i = 1:numel(clRegions) + if abLaplaceMin(i) > abLaplaceMinThresh || abLaplaceMax(i) < abLaplaceMaxThresh + cl.pointData.clRegion(pointIds{i}) = -1; + end +end +cl = vtkThreshold(cl, 'points', 'clRegion', [0 inf]); + +% reassign clRegion +cl = vtkConnectivityFilter(cl); +cl.pointData.clRegion = cl.pointData.RegionId; +cl.pointData = rmfield(cl.pointData, 'RegionId'); +cl = rmfield(cl, 'cellData'); + +if o.cfg.exportLevel > 2 + vtkWrite(cl, sprintf('%sabContourLines.vtp', o.cfg.outPrefix)); +end + +% use cubic smoothing spline to smooth and resample the contour lines +% and to compute the normalized distance along the contour lines (abSmoothCl) +clRegions = unique(cl.pointData.clRegion); +splineMisfit = 5e-4 * norm(mean(double(o.m1.sur.points(o.m1.sur.pointData.class==1,:)),1)-mean(apexPoints,1)); +smoothClPoints = NaN(numel(clRegions)*numClPoints, 3); +abSmoothCl = zeros(numel(clRegions)*numClPoints, 1); +abLength = abSmoothCl; +for i = 1:numel(clRegions) + pointIds = find(cl.pointData.clRegion==clRegions(i)); + [~,sortInd] = sort(cl.pointData.abLaplace(pointIds)); + apexPoint = apexPoints(csRegions==cl.pointData.csRegion(pointIds(sortInd(1))),:); + P = [apexPoint; double(cl.points(pointIds(sortInd),:))]; + d = sqrt(sum(diff(P,1,1).^2,2)); + ind = d < 1e-4*o.m1.meanEdgLen; + P(ind,:) = []; + d(ind) = []; + d = [0; cumsum(d)]; + w = [100; ones(size(P,1)-1,1)]; + tol = numel(d)*splineMisfit^2; + sp = spaps(d, P', tol, w); + P = fnval(sp, linspace(min(d), max(d), numClPoints))'; + d = [0; cumsum(sqrt(sum(diff(P,1,1).^2,2)))]; + ind = (i-1)*numClPoints+1:i*numClPoints; + smoothClPoints(ind,:) = P; + abSmoothCl(ind) = d/max(d); + abLength(ind) = max(d); +end + +if o.cfg.exportLevel > 2 + clSmooth.points = single(smoothClPoints); + clSmooth.pointData.ab = single(abSmoothCl); + clSmooth.cells = int32(repmat(reshape(repmat(0:numClPoints:numel(clRegions)*numClPoints-1,numClPoints-1,1),[],1),1,2) + repmat([(1:numClPoints-1)' (2:numClPoints)'],numel(clRegions),1)); + clSmooth.cellTypes = repmat(uint8(3), size(clSmooth.cells,1), 1); + vtkWrite(clSmooth, sprintf('%sabContourLinesSmooth.vtp', o.cfg.outPrefix)); +end + +% BEGIN: Laplacian extrapolation of abSmoothCl to o.m1.vol + +% ||M ab - abSmoothCl|| forces extrapolated values to fit to contour values +M = baryInterpMat(o.m1.vol.points, o.m1.vol.cells, smoothClPoints); + +% ||E ab - 1|| forces extrapolated values to 1 at the base +baseIds = double(o.m1.surToVol(o.m1.sur.pointData.class==1)); +baseVal = ones(numel(baseIds),1); +E = sparse(1:numel(baseIds), baseIds, ones(size(baseIds)), numel(baseIds), size(o.m1.vol.points,1)); +eta = (numel(abSmoothCl)/(numel(baseIds)))^2; + +% ||L ab|| forces extrapolated values to be smooth +L = o.m1.massMat \ o.m1.L; + +% Initial guess for ab using nearest-neighbor interpolation +ab = abSmoothCl(knnsearch(smoothClPoints, o.m1.vol.points, 'NSMethod','kdtree')); + +% Initial guess for lambda based on the relation between lambda and the +% half-width at half-maximum of the point spread function of the operator +% inv(speye(size(L))+lambda*(L'*L)) +hwhm = mean(abLength)/100; +lambda = 1.58*hwhm^3.57; +lambda = numel(abSmoothCl)/numel(ab)*lambda; +fprintf('\n0\t%.3e\n', lambda); + +b = M'*abSmoothCl + eta*E'*baseVal; +MM = M'*M + eta*(E'*E); +LL = L'*L; +extrapMisfit = o.cfg.abExtrapSmooth/100; + +try + [~,flag,iter] = secant(@objFun, 1e-1*lambda, lambda, 1e-2*extrapMisfit); + if flag + warning('Secant stopped at iteration %i without converging.', iter); + end +catch err + disp(getReport(err, 'extended', 'hyperlinks', 'off')); + warning('Optimization of lambda failed. Using default value instead.'); + objFun(lambda); +end + +function objVal = objFun(lambda) + A = MM + lambda*LL; + icMat = ichol_autocomp(A, struct('michol','on')); + [ab, flag, relres, iter] = pcg(A, b, o.cfg.tol, o.cfg.maxit, icMat, icMat', ab); + if flag + error('pcg failed at iteration %i with flag %i and relative residual %.1e.', iter, flag, relres); + end + objVal = rms(M*ab-abSmoothCl)-extrapMisfit; +end + +% END: Laplacian extrapolation + +o.m1.ab = min(max(ab,0),1); +o.m0.ab = min(max(o.m1.M*o.m1.ab,0),1); +o.result.pointData.ab = single(o.m0.ab); + +if o.cfg.exportLevel > 1 + o.m1.debug.pointData.ab = single(o.m1.ab); + vtkWrite(o.m1.debug, sprintf('%sdebug1.vtu', o.cfg.outPrefix)); + + o.m0.debug.pointData.ab = single(o.m0.ab); + vtkWrite(o.m0.debug, sprintf('%sdebug0.vtu', o.cfg.outPrefix)); +end + +o.printStatus(sprintf('%.1f seconds\n', toc-t), true); +o.available.apicobasal = true; + +end