Step 3: Quasi-equiareal spherical parameterization of a genus-0 surface, \(S_{\text{ref}}(x,y,z)\)#

Any genus-0 surface can be mapped to the unit sphere by minimizing conformal error. This necessarily means shrinking surface protrusions to point-like singularities. This detrimentally affects the representation of high curvature surface protrusions when mapped to the sphere. For cells whose reference shape is of branched morphology, this necessarily means complete under-representation of the branches and almost surely any surface protrusions displayed on the branches.

Consequently, it is imperative to recover area-distortion, if the objective is to analyze surface protrusions. This is the objective of Step 3.

The u-Unwrap3D area-distortion relaxation scheme adapts and improves the approach of Lee et al., to relax distortion directly on the sphere iteratively to achieve near-ideal area-distortion minimization. Theoretically any genus-0 surface should be relaxable. Practically, we are beholden to the limitations of computer precision, such that optimal area-distortion is achievable if users construct or modify \(S_{\text{ref}}(x,y,z)\) to satisfy the following: \(S_{\text{ref}}(x,y,z)\) should not have i) sharp features, ii) be isotropically meshed, iii) not have thin necks, and iv) not be too densely meshed. All 4 of these criteria is to allow for the area distortion to be ‘diffused’. i), iii) are ensured by dilating the shape and remeshing, as described in Step 1. ii), iv) is achieved by isotropic remeshing to achieve a desired average edge length.

Note on the tradeoff of conformal and equiareal distortion: For a closed 3D surface, it is not possible to jointly minimize conformal and equiareal distortion like a seesaw - improving one necessarily increases the other error. Conformal maps preserve local angles - this has many nice mathematical properties such as being able to apply calculus. Meanwhile, equiareal maps are required to properly represent surface protrusions but comes at the cost of potentially greatly distorting local orientation. Consequently u-Unwrap3D has designed an iterative relaxation procedure so that users can adjust the degree of relaxation based on their desired objective e.g. minimize up to a certain target error, achieve an isometric map that balances conformal and area distortion etc.

1. Load \(S_{\text{ref}}(x,y,z)\) and \(S_{\text{ref}}^{\mathcal{Q}}(x,y,z)\) mesh#

We assume the user has worked through step 2 which generates and saves the conformal spherical parameterization, \(S_{\text{ref}}^{\mathcal{Q}}(x,y,z)\) of the genus-0 reference surface, \(S_{\text{ref}}(x,y,z)\) of an input cell surface mesh to the folder example_results/bleb_example/step2_conformal_sphere_param. We also need the reference surface mesh, \(S_{\text{ref}}(x,y,z)\) which was computed in step 1 and was saved to the folder example_results/bleb_example/step1_cMCF_reference. Moreover these surface has been colored by mean curvature of \(S(x,y,z)\).

import unwrap3D.Utility_Functions.file_io as fio
import unwrap3D.Mesh.meshtools as meshtools
import numpy as np
import os 
import scipy.io as spio

# example cell used
imgfolder = '../../data/img'
imgfile = os.path.join(imgfolder, 'bleb_example.tif')
basefname = os.path.split(imgfile)[-1].split('.tif')[0] # get the filename with extension

# create the analysis save folder for this step
savefolder = os.path.join('example_results', 
                         basefname,
                         'step3_equiareal_sphere_param')
fio.mkdir(savefolder) # auto generates the specified folder structure if doesn't currently exist.


# load the pre-computed genus-0 reference, S_ref(x,y,z)
S_ref_folder = 'example_results/%s/step1_cMCF_reference' %(basefname)
S_ref_file = os.path.join(S_ref_folder, 
                            'unwrap_cMCF_Sref_mesh_H_color.obj') 
S_ref_mesh = meshtools.read_mesh(S_ref_file)

# load the pre-computed genus-0 spherical parameterization of S_ref(x,y,z)
conformal_folder = 'example_results/%s/step2_conformal_sphere_param' %(basefname)
conformal_file = os.path.join(conformal_folder, 
                               'iterative-conformal_sphere_param_Sref_%s.obj' %(basefname))
conformal_sphere_mesh = meshtools.read_mesh(conformal_file)

2. Equiareal spherical parameterization, \(S_{\text{ref}}^{\Omega}(x,y,z)\) by diffusing area distortion#

To relax area distortion, we can compute the area fraction of the current spherical parameterization and \(S_{ref}\). We can think of this as a sort of energy and advect the vertices on the sphere to maximally dissipate this energy over the surface. As this dissipation necessarily requires changing the triangle edge lengths, minimizing area distortion is in direct opposition to minimizing conformal error.

Depending on the intended application, the user does not need to fully relax the area. For this reason we chose to implement an iterative relaxation scheme.

"""
Area distortion relaxation from conformal to equiareal.
"""
# For a single cell I like to set debugviz=True to check the flow is working, if the function is terminating much earlier than expected. The flow will not work if the input mesh doesn't support flow either because triangles are insufficiently large or if they are not near-equilateral
# debugviz=True will plot a histogram of the log(area distortion). If it is working the histogram should steadily converge to a symmetric distribution around 0. 
# If the plots don't converge see troubleshooting tips below.
relax_v, relax_f, area_distortion_iter= meshtools.area_distortion_flow_relax_sphere(conformal_sphere_mesh, 
                                                                                    S_ref_mesh, 
                                                                                    max_iter=100, # this needs to be increased the more S_ref deviates from spherical. 
                                                                                    delta=0.1, # controls the stiffness, if too high - there is no flow, if too low - bijectivity is lost and flow may breakdown 
                                                                                    stepsize=1, 
                                                                                    debugviz=False) 

# compute the evolution of the mean area distortion. We use this to determine the first iteration when area distortion is minimized. 
area_distortion_iter_curve = np.hstack([np.mean(mm) for mm in area_distortion_iter])

# to determine when area distortion is minimised we look for the timepoint when the sign first changes. This is because once the minimum is reached, depending on stepsize, the mesh will start to oscilate. The larger the stepsize, the faster the convergence but the larger the oscillatory instability. 
min_area_distort = (np.arange(len(area_distortion_iter_curve))[:-1])[np.sign(np.diff(area_distortion_iter_curve))>0][0] # falls below 0. 

# check: print the area distortion value at min_area_distort. This should be close to 1 (the ideal minimum) for optimal relaxation.
print('distortion at found iteration number:', area_distortion_iter_curve[min_area_distort])


# the area_distortion_iter_curve is another great check of things are working, we plot the np.log(curve) to visualize a log scale.
# the curve should go down smoothly, if the decay is bumpy etc, it means the mesh is not great, and can be improved 
import pylab as plt 

fig, ax = plt.subplots(figsize=(5,5))
plt.plot(np.arange(0,len(area_distortion_iter_curve)), np.log(area_distortion_iter_curve), '.-', color='k')
plt.vlines(min_area_distort, 
            np.nanmin(np.log(area_distortion_iter_curve)), 
            np.nanmax(np.log(area_distortion_iter_curve)), 'k', linestyles='dashed', lw=3)
plt.tick_params(length=5, right=True)
plt.xticks(fontsize=16, fontname='Arial')
plt.yticks(fontsize=16, fontname='Arial')
plt.xlim([0,len(area_distortion_iter_curve)])
plt.xlabel('Iteration Number', fontsize=18, fontname='Arial')
plt.ylabel('Area distortion', fontsize=18, fontname='Arial')
plt.savefig(os.path.join(savefolder, 
                          'area-distortion_relax_iterations_'+basefname.replace('.tif', '.svg')), bbox_inches='tight')
plt.show()

# create the equiareal mesh and save it out
equiareal_sphere_mesh = conformal_sphere_mesh.copy()
equiareal_sphere_mesh.vertices = relax_v[min_area_distort].copy()

tmp = equiareal_sphere_mesh.export(os.path.join(savefolder,
                                               'equiareal_sphere_param_Sref_%s.obj' %(basefname)))
100%|██████████| 100/100 [00:27<00:00,  3.69it/s]
distortion at found iteration number: 0.9995375983497246
../_images/502ae4ed87c93acd41a6454b330524fd42902aece62bfe687187c29bfbb3cae4.png

3. Quantifying and visualizing the conformal and equiareal distortion error during relaxation#

Area distortion is computed as part of the relaxation process. Below, we compute the conformal error per iteration.

import unwrap3D.Visualisation.colors as vol_colors
import unwrap3D.Visualisation.plotting as plotting
from matplotlib import cm 
import pylab as plt 

"""
Compute the conformal error for each iteration of the relaxation and also color the resulting mesh. 
"""

save_area_relaxation_folder = os.path.join(savefolder, 'area_relaxation_iterations'); 
fio.mkdir(save_area_relaxation_folder)


# this will save both the conformal + equiareal error.
conformal_error_flow = []

for iter_ii in np.arange(len(area_distortion_iter)):

    # color by curvature
    relax_mesh_H = conformal_sphere_mesh.copy()
    relax_mesh_H.vertices = relax_v[iter_ii].copy()
    tmp = relax_mesh_H.export(os.path.join(save_area_relaxation_folder,
                                           'sphere_curvature_%s.obj' %(str(iter_ii).zfill(3))))

    # color by area distortion error
    area_distortion_error_colors = vol_colors.get_colors(np.log(area_distortion_iter[iter_ii]), 
                                                         colormap=cm.coolwarm, 
                                                         vmin=-3, vmax=3)
    relax_mesh_area_distort = meshtools.create_mesh(relax_v[iter_ii],
                                                    relax_f[iter_ii],
                                                    face_colors=np.uint8(255*area_distortion_error_colors[...,:3]))
    relax_mesh_area_distort.export(os.path.join(save_area_relaxation_folder,
                                                'sphere_area_distortion_%s.obj' %(str(iter_ii).zfill(3))))

    # measure and color by conformal error relative to the sphere at the start
    conformal_errors = meshtools.quasi_conformal_error(relax_v[0], 
                                                      relax_v[iter_ii], 
                                                      relax_f[iter_ii])
    conformal_error_flow.append(conformal_errors)

    conformal_error_color = vol_colors.get_colors(conformal_errors[1], # per face conformal error. 
                                                  colormap=cm.jet, 
                                                  vmin=1., vmax=1.5)
    relax_mesh_conformal_error = relax_mesh_area_distort.copy()
    relax_mesh_conformal_error.visual.face_colors = np.uint8(255*conformal_error_color)
    relax_mesh_conformal_error.export(os.path.join(save_area_relaxation_folder,
                                                   'sphere_conformal_error_%s.obj' %(str(iter_ii).zfill(3))))
    
    
    # matplotlib visualization of curvature, conformal error and area-distortion error.
    # you should see that as the area-distortion dissipates, conformal error increase
    sampling = 1 # plot every just so its not fully dense in the plot so we don't see anything!

    fig = plt.figure(figsize=(3*3,3))
    ax = fig.add_subplot(1,3,1, projection='3d')
    plt.title(r'$H(S(x,y,z)$) '+str(iter_ii).zfill(3))
    ax.scatter(relax_mesh_H.vertices[::sampling,2], 
               relax_mesh_H.vertices[::sampling,1],
               relax_mesh_H.vertices[::sampling,0], 
               s=1, 
               c=relax_mesh_H.visual.vertex_colors[::sampling,:3]/255.)
    plotting.set_axes_equal(ax)

    ax = fig.add_subplot(1,3,2, projection='3d')
    plt.title('conformal error '+str(iter_ii).zfill(3))
    ax.scatter(relax_mesh_conformal_error.vertices[::sampling,2], 
               relax_mesh_conformal_error.vertices[::sampling,1],
               relax_mesh_conformal_error.vertices[::sampling,0], 
               s=1, 
               c=relax_mesh_conformal_error.visual.vertex_colors[::sampling,:3]/255.)
    plotting.set_axes_equal(ax)

    ax = fig.add_subplot(1,3,3, projection='3d')
    plt.title('area-distortion error '+str(iter_ii).zfill(3))
    ax.scatter(relax_mesh_area_distort.vertices[::sampling,2], 
               relax_mesh_area_distort.vertices[::sampling,1],
               relax_mesh_area_distort.vertices[::sampling,0], 
               s=1, 
               c=relax_mesh_area_distort.visual.vertex_colors[::sampling,:3]/255.)
    plotting.set_axes_equal(ax)

    plt.show()
    
# reorganising the conformal error_flow from the relaxation process. 
conformal_error_flow_arrays = [ np.array([cc[jj] for cc in conformal_error_flow]) for jj in np.arange(len(conformal_error_flow[0]))]
../_images/7ab8f468b6664e7de165b8dc96de5ffb29ed3d810db267d4d6251adbecbc643b.png ../_images/52936785c3bce856f939aa1577c3d458eaeabf6234f427f4a4d84176ae40aab0.png ../_images/4a5161236af6124a4c3311f7ab84d8057b4bfd5c3876856163f19f8a6d15d807.png ../_images/cc853323e84d9f3cfbc28a6fff9b70f46a9eb18b1ed6872cc473698dafb94853.png ../_images/1a6d19bd9d2bb95862142701bfe0a2ffcaa1aee8faeaed09e1e828a5b99ccdbf.png ../_images/348b3e6167d7a27f7e02f40964a90d83306ba80494e798e3300c961168a2dc1a.png ../_images/449c319a8b00e4acb749ec74379f318e5b14986e717385e804926e58cbe7c9d4.png ../_images/52db486d6bb9bee4b283c081bdd10486ad3661d0df76f33fd79c9052d1dec793.png ../_images/0e2387b3087e75625c2deb975af928947d17af13641384f9742bee523d591162.png ../_images/506b901db940e45ce0a06f3a731902f5e52330912ddd64c01cf11fcd6b6f1140.png ../_images/5715f1f600b7125ee802b42c8b48891226d8ad1120dd8111e9bab30535a2c2b4.png ../_images/036c8593442b9d7e27995a92015e0e0a2602b6099cb91b60f169bc9edc1c11dc.png ../_images/f5b4e4761f38e9d016d1288696fb36793dc4da6435ff78fc99409a74d3c5cf12.png ../_images/ffc2d42af1fe7722b7484e3d35444905c715baebea7d8708afdb3db021d26695.png ../_images/b2f1ef0aef7f719e5e9eeebdfc7a61985e97c965b64e87d5943892d2ae98b040.png ../_images/ecd8c7aaadb1c57892fc068c5e06f5f3a6e7d46e4efb3d7eb102d9174d482cae.png ../_images/42e9b21af5fce6aeefe1567c6184188ccde31e31329147a3a09e4ff98519a06d.png ../_images/a270dff1af8703cc0929d515a2f2a20d425f2fe2c49b3006669d9a1994392072.png ../_images/4601ae538fdd61ce941ddda9bcab3656cb512b6f25c6e0f342a798c0c588ebf1.png ../_images/a2516161aac297270e29df43cbb596a86a9cd8d5f31936c4b426ca35c64c29c7.png ../_images/795e80f52caa9537b08974b636fbd99e5a040af36686a390b81b80fb3fd86e0f.png ../_images/e35517218c8fc3a5ead0a29a9908fe765926abc57d687c837ce27f20b82bba41.png ../_images/32e96975bbb64268c2fe655082d5cf26c80f0ee5c55dc3ea6d8a1e7bcc5982c0.png ../_images/b2bbd0a4ca7e714927367c1a6df895529d55ca879eaa7772f41f4121f56c7c51.png ../_images/348c661d779e338da6187a6ba46f95c631e6ba162df4495168ba505be16af0cb.png ../_images/5f0f7c124d4863cfdbd1c539cba22f620666e30232aca14c2aaeb4fc59d0e0dc.png ../_images/35d5c6274068d772630ee3eaa19e00b98281459cb76e2c8ba9b2cf61cbaded3c.png ../_images/a61e4890a4b6065691541da83009fb1080e10f25e99e1e2b22cc6768bc7275b5.png ../_images/3d562c8b1c397d1413c3c1cd3f58a2957e6213a9f4412f66e8d68a875a04d77b.png ../_images/b55f6d8b773fc5cc140cd037e46f3c550a47376abf6006d5f1087abdc5bb22f1.png ../_images/89505c0532acd9c45363121493e5e41ec1bccd20e6ade8730b2ec63d618fe62f.png ../_images/29ff1402476aca388c038db390521a851d601d739b67cfbcef091aa83735dcce.png ../_images/3f0a82d51c3e4f3298c0c936c6cd8a4e8366f467e40efdadd5eb7463f27e6aec.png ../_images/e53fdc704c123ca415d0ccef45b3968f2a6c0847d775a0f76f660999ab217800.png ../_images/4df0bee01759672cc8e4b9dc3c49fab17cd314b2a414f071838daa4197c00aa6.png ../_images/4bfcc13cd0b0df98b2c5d21a9fb23f988087d6cbae74fe6f27031dea75c5ae05.png ../_images/9a9037ffa3e988cb8f68ed3a66788047b1d5563c5251941f908233f91cf18349.png ../_images/45f5103b7a3e51c4f6f3374cb8d59e459befdd04e84b2ea17a302f187318fc00.png ../_images/14b880f3254765a0c78c9d659548942eeeb427f0c60329769e26fdc3ae46b212.png ../_images/13c5f6638a1dfbc02b4337dd6eb82268fee875770e8223c97dafdb30f4881b64.png ../_images/68b97ea360b3c2f2b66eb8ef85abbf7d71d04caedfc44016f17538b1df75c404.png ../_images/3dac7751f74cb5b2b5ffccdcd9cf50e3da4e31851c64bba884efd39a183abb89.png ../_images/162a7aa0d28fb16ef3e488b51ea463eae7e2013290a29aa5d4998500d8c5f52a.png ../_images/39b8b63435743e6d3c2372b74386aa6f1fae1caa803b2ae14313a29535652b3f.png ../_images/79644f990bc8135bcf6d265e060b4f393d691944dd759378711703eab168485d.png ../_images/2881645aebce0218c3de81a07d499f363b7709e4507f5f87d73dc9653de0d1bd.png ../_images/948725fc5dd4a9370b4372b1dcfabee5ad56fb43f6b3aef924bc2fe6deeac459.png ../_images/0a2ced1141f825b2b07811e038e87ee4b3a2a164980636eefdfd51fd05d4eb51.png ../_images/94e8021235d55e5e440c86952bae54a3fbb5c0c141f9f6c1dec8857bad0bd265.png ../_images/795121178c3665c250ae3fe96c51b2c34b218e4e641cb9dec188286e5b79311d.png ../_images/b26c4818fd9d2baebc5cd91670a8854ef80b7961332d9f355a7c0207af7cb6f3.png ../_images/4c41de3de4765a3570a61059566874e1b78631177cb9128d49bfd8ea6ce91bde.png ../_images/d7dfbfac6ad8cd9bd4dd453b66cfcfdaf57706fa1ba4e6b514411b86386ee746.png ../_images/f78e0e96c1c37a03a3fe7c4803ebec1e20c388e14860d680ff7572a6a0f83567.png ../_images/b02bdd190def209ff9e6e03483db4ed8b47b797cfc4777bcc66b0e45b47e1638.png ../_images/8e22ea8b1cc73664617ee50fbe7522b3ed4c94be8b851bcf7e811dcfd1343c8c.png ../_images/6236338ff2a5897788ff5620308167c7f49a4476ed68194d630276db4e41562e.png ../_images/8f44a12f7bbf36c435ef9a1d227d4f5aad1cd4b22654eac7b60ecc197876f946.png ../_images/50784cdfe474ade8680051df2e36502cf55f5cf882154a79f346c0d3e698a7be.png ../_images/a61717d0d2576d7dc1e7e568049f0beccf7473b6dd8968b640a00176da760210.png ../_images/a0613d06b242c2ff2b601954ec5ec21f4e07d1030b436899fbea87a2a8157e23.png ../_images/611f9c3859fc512071b7d4d9f43d2dc36425493f2e59b1b7a8ee5e02c67074b5.png ../_images/e9c758833726be7653adcfd5ec617b5f4ac43e39e7b9953bf1c51a6e7be0057f.png ../_images/7e33a5c089b17330476f871df42b1b2c6869cf05067daeb9c41b07d25b7d5bdf.png ../_images/174ec5df4b8238c07bc00fd55a8e87ec9f0ea3856988b17af7cf3e671db2331b.png ../_images/bed273e18e4fd4e9be2b5280960fa3d11c1bbd7b09af194d36cf9bb00164ab57.png ../_images/85f9ca9d96dc8858d8f2ba24a0de81193f862bc2497bf8ab6d718c25790e8878.png ../_images/e6666c1c21c434bf60c421a88cc95f836c6f9b04da6afca11ef221e30ccccdf4.png ../_images/115cea0b9795945c76c36c458a032b67ac51f61782efaed06fd8ec297bb13613.png ../_images/677ef6d526550ade0157c1e6b5844cecdd8851b4e0876d710e831d2c153b01d6.png ../_images/c407f6ac604a93dbae43e4b186ced101c8b6636113430c58799d1e917e951b5f.png ../_images/389010dc42a3186d6bdb1fd16e3a44f50f683ce062d52e807141a9884ba27844.png ../_images/eb9a4c43f7732b65697cfe12ee2725aa1804680df744d673c0a86cc680c048a2.png ../_images/b8bc0b017364ea3c10568d9a889f1ca31359578dbe8ec104ed0a03c3e0cfb0cd.png ../_images/57ca70e2807fa3b2033a3ac64292170b6be17bed4d226102713025909b406db6.png ../_images/fc52639402cb1dc44e197f13bc5e2fa742b77a2663a7a48fbb8907f1bf7d3569.png ../_images/2ea0de0b37ce8c2b20f4da4959c19abb58aad147a8212169f58825ada638e5e8.png ../_images/616058ac679f391500ca0bd58f9af473644007465a4097472cedebc99ae9dca8.png ../_images/9f740f88905b08daad385f69ac989624fdb759d3d96d7f8136ad224aff21d3b5.png ../_images/c87f07a45d6ff2a48673601787142f53f48f7b02b684d0aafee23a6dca95c0b2.png ../_images/3ac183d931692aac44e70eb5a01a8b952b3ff7027dbbf327281974c60a8ae570.png ../_images/0ed1976b4fa757ea56be5ea9439a5d5b4d778e4e0b0c4f9d152aa968b457e306.png ../_images/9e3ff13168952fde2a75d4121b19b85e640a21905979f27065dcfaf062d69d4d.png ../_images/079255c3de752eb910a0867dc2c86f012a26c484f771dcacd5c08c4b8372da7f.png ../_images/6c51397cdec44a84a6af1e1972530e58b13a97c602ab0d87219e455f69f67a1f.png ../_images/0656c2db38d125be3ed83ed0032d4f85fad45a1e4fe47c53c712f30d68313591.png ../_images/d4ca738f8339bbc5e152e69b9acfec2ecf147944c4e2dcd6a47f10d32ce51616.png ../_images/6c3e6630cfc6c1b88c8c8406fefc5f5f8ce6f6359a0e00fbe11f7e1a5bc745c2.png ../_images/e21bf3771313fabd4c81e20411d2ab7913b45b973ecce640eeabfbb104753713.png ../_images/519cbaafb1ed860cfc9a406c605b5a1cabab2958d8e42eb05c626c40f3ed7d48.png ../_images/bcfd575d85df083b7a6c22e7733b51b5cfe8bd31dfe185a83dafc1669ca68317.png ../_images/714f9a94da2febbe8a996871883cc6a7b2bdd5144686f03a3b063764b2ed8378.png ../_images/fa835f3174a802cac73a828bfde64d3317733bcbbf32183b96bf9886aab73a4e.png ../_images/d4bfd3d36b589ca76037ddd7b680c5b13e810dcc535abf6699d8db0a10577e42.png ../_images/a6418fa8cbc05fef64fb6b9efc876948b37e3eb5179a36ab2cc015b9a6d923c1.png ../_images/cc6023af8e4069c82371f509b7be09a5395aac2a474ee6ae83255b955916f3e4.png ../_images/bcd7c5dea7a528537aa819103fdcb8396a401caaac9b914dc38cb287f808f34a.png ../_images/9eff7a564f1ead0c3f696fab87d28e25910eab90881aeef5f899a6d1ef1346ec.png ../_images/e78ce3534339b3f62cae95ff031a3da68d0aa6b6a8089794ee1f3b6ea415173a.png ../_images/f705f117fec4db66cbfe4e471415c4f235a37176d0906449e7fb56f48a54ac06.png

4. Plotting the mean conformal and equiareal error curve during relaxation#

mean_conformal_curve = conformal_error_flow_arrays[2]
mean_area_distort_curve = np.array(area_distortion_iter).mean(axis=1)


import pylab as plt 

fig, ax1 = plt.subplots(figsize=(5,5))
ax1.set_xlabel('Iteration Number', fontsize=18, fontname='Arial')
ax1.set_ylabel('Conformal error', fontsize=18, fontname='Arial', color='k')
ax1.plot(np.arange(len(mean_conformal_curve)), mean_conformal_curve, 'ko-')
ax1.vlines(min_area_distort,1, np.nanmax(mean_conformal_curve), linestyles='dashed', color='k', lw=3)
ax1.tick_params(axis='y', labelcolor='k')
plt.xticks(fontname='Arial', fontsize=14)
plt.yticks(fontname='Arial', fontsize=14)

ax2 = ax1.twinx()  # instantiate a second axes that shares the same x-axis
ax2.set_ylabel('Final area / initial area', fontsize=18, color='r', fontname='Arial')  # we already handled the x-label with ax1
ax2.plot(np.arange(len(mean_area_distort_curve)), 1./mean_area_distort_curve, 'ro-')
ax2.tick_params(axis='y', labelcolor='r')
plt.xticks(fontname='Arial', fontsize=14)
plt.yticks(fontname='Arial', fontsize=14)
plt.tick_params(length=5)
plt.savefig(os.path.join(savefolder,  
                        'mean_area-relax_distortion_errors_iterations_'+basefname+'.png'), 
            bbox_inches='tight', dpi=300)
plt.show()
    
    
# print the mean errors
print('mean conformal distortion at selected iteration number: ', mean_conformal_curve[min_area_distort])
print('mean area distortion at selected iteration number ', np.nanmean(area_distortion_iter, axis=-1)[min_area_distort])
../_images/bc06f2c49915cbfd19b9aa169686af2d4770f95fb54d8358b0ac5718ceb002b5.png
mean conformal distortion at selected iteration number:  1.5382336929755507
mean area distortion at selected iteration number  0.9995375983497246

5. The tradeoff between conformal and equiareal#

You will notice a tradeoff between conformal and area distortion. Conceptually, this is because conformal is akin to ‘rigid’ transformation, triangles are not allowed to change shape, only rotate and rescale, area distortion is akin to ‘nonrigid’ transformation whereby inevitably the edge lengths and shapes of triangles are allowed to deform.

To make this explicit, we can plot the combined ‘isometric’ error proposed in the u-Unwrap3D paper, conformal + log(area distortion). It is then apparent that there exists a global minimum where both types of errors are minimized. (green dashed line).

min_combined = np.argmin(mean_conformal_curve + np.log(mean_area_distort_curve))

# plotting combined conformal + log(area distortion) error
fig, ax = plt.subplots(figsize=(5,5))
ax.set_xlabel('Iteration Number', fontsize=14, fontname='Arial')
ax.set_ylabel('Conformal + log(area distortion) error', fontsize=14, fontname='Arial', color='k')
ax.plot(np.arange(len(mean_conformal_curve)), mean_conformal_curve + np.log(mean_area_distort_curve), 'go-')
ax.vlines(min_area_distort,1,np.nanmax(mean_conformal_curve + np.log(mean_area_distort_curve)), linestyles='dashed', color='k', lw=3) # this is the iteration number for equiareal
ax.vlines(min_combined,1,np.nanmax(mean_conformal_curve + np.log(mean_area_distort_curve)), linestyles='dashed', color='g', lw=3) # this is the iteration number for global minimum of conformal + equiareal
ax.tick_params(axis='y', labelcolor='k')
plt.xticks(fontname='Arial', fontsize=10)
plt.yticks(fontname='Arial', fontsize=10)
plt.show()
../_images/21d0c1ee3606453f9084f08f290186fb41ae0899291147863e7b9b7ff83fb347.png

Generally, however for surface protrusions, at the iteration number that minimizes conformal and area distortion equally (dashed green), the area distortion relaxation is insufficient. Consequently we recommend to parameterize the isometric error with a weighting \(\lambda\): $\( \text{Isometric error}(\lambda) = (1-\lambda)\cdot \text{conformal} + \lambda \cdot \log \text{area distortion}\)\( We plot the family of curves according to this hybrid error, showing how increasing \)\lambda\( favors more equiareal, and decreasing \)\lambda$ favors the initial conformal parameterization.

from matplotlib import cm
isometric_errors_curves = []

lams = np.linspace(0,1,10+1)
lams_min_iter = []
for lam in lams:
    isometric_errors_curve = (1.-lam)*mean_conformal_curve + lam*np.log(mean_area_distort_curve)
    isometric_errors_curves.append(isometric_errors_curve)
    lams_min_iter.append(np.argmin(isometric_errors_curve))

# isometric_errors_curves_colors = sns.color_palette('RdYlBu_r', len(isometric_errors_curves))
# isometric_errors_curves_colors = np.vstack(isometric_errors_curves_colors)
isometric_errors_curves_colors = vol_colors.get_colors(lams, 
                                                       colormap=cm.RdYlBu_r)

# plotting the curves 
fig, ax = plt.subplots(figsize=(5,5))
ax.set_xlabel('Iteration Number', fontsize=14, fontname='Arial')
ax.set_ylabel('Error', fontsize=14, fontname='Arial', color='k')
for lam_ii, lam_err_curve in enumerate(isometric_errors_curves):
    # plot curve
    ax.plot(np.arange(len(lam_err_curve)), lam_err_curve, '-', 
            color=isometric_errors_curves_colors[lam_ii], label=r'$\lambda=%.2f$' %(lams[lam_ii]))
    # plot minimum iteration number on curve
    ax.plot(lams_min_iter[lam_ii], 
            lam_err_curve[lams_min_iter[lam_ii]], 'o', 
            color=isometric_errors_curves_colors[lam_ii], mec='k')
ax.tick_params(axis='y', labelcolor='k')
plt.xticks(fontname='Arial', fontsize=10)
plt.yticks(fontname='Arial', fontsize=10)
plt.legend()
plt.show()
../_images/dc801efbfaa262c1190234238ba19640a19807e9953a49c1def5b66b3279b933.png
"""
Save all the computed measurements out 
"""
save_sphere_parametrization_mat = os.path.join(savefolder, 
                                                'Sref_area_relax_sphere.mat')
spio.savemat(save_sphere_parametrization_mat,
              {'relax_v':np.array(relax_v), 
              'relax_f': np.array(relax_f), 
              'area_distortion_iter': np.array(area_distortion_iter), # we have this saved.... (we want also the conformal area?)
              'min_area_distort_id':min_area_distort,
              'sphere_param': conformal_sphere_mesh.vertices, 
              'conformal_Jac_eigvals': conformal_error_flow_arrays[0], 
              'conformal_stretch_factors': conformal_error_flow_arrays[1], 
              'mean_conformal_stretch_factors': conformal_error_flow_arrays[2], 
              'conformal_tri_area_pairs': conformal_error_flow_arrays[3]})

Troubleshooting tips for area distortion relaxation#

We have noticed several common occurrences of how the area distortion relaxation appears to fail, and how to address them here.

1. The function doesn’t complete 1 iteration of relaxation and terminates early.#

Reason: the Lapacian matrix is likely singular or close to singular, and therefore cannot support matrix inversion for backwards Euler. This is likely one of or all of the following:

  • at least one triangle face of your conformal spherical parameterization has interior angle close to \(0^0\). Check by running unwrap3D.Mesh.meshtools.measure_triangle_props. The minimum angle of your mesh should be at least > \(10^0\) (poor triangle quality)

  • at least one triangle face of your conformal spherical parameterization has too small a surface area relative to floating point precision. Check by computing face area using igl, using the code igl.doublearea(v,f)/2. where v and f is the vertices and faces of the mesh respectively.

  • \(S_{\text{ref}}(x,y,z)\) was not genus-0 or if it is, you have a thin neck or a ‘plate-like’ infill of a handle (if using shrinkwrapping)

Fix: recompute genus-0 \(S_{\text{ref}}(x,y,z)\) from Step 1 and perform the conformal spherical map in Step 2 using the iterative method

  • use remesh_method='CGAL' for isotropic remeshing to improve triangle quality in unwrap3D.Mesh.meshtools.marching_cubes_mesh_binary and that you have remesh active, i.e. remesh=True

  • use lower remesh_samples after voxelization with unwrap3D.Mesh.meshtools.marching_cubes_mesh_binary to generate a coarser mesh

  • use a dilated voxelization i.e. set ksize_dilate at least greater than ksize_erode by 2.

2. Relaxation runs for XXX iterations, doesn’t finish and area distortion at min_area_distort is much greater than 1.#

Reason: There’s two potential reasons:

  1. the shape or mesh of \(S_{\text{ref}}(x,y,z)\) is preventing diffusion. \(S_{\text{ref}}(x,y,z)\) likely has a diffusion-limiting geometry such as a thin neck or a ‘plate-like’ infill of a handle, or remesh_samples is too high, leading to too small a triangle area in the conformal spherical parameterization.

  2. delta is too high or too low. delta governs ‘stiffness’ between edges. If you have a densely sampled mesh, i.e. lots of vertices delta=0.1 may be too high, and there is barely any relaxation. The triangle area also internally limits the diffusion step size. Check by setting debugviz=True. The histogram bars should noticeably shift lower every iteration. If not, its a sign of too high a delta. Similarly, if delta is too low, its hard for neighboring vertices to maintain connectivity (move together). This often leads to visible ‘wiggles’ when plotting the area_distortion_iter_curve curve.

Fix:

  1. recompute genus-0 \(S_{\text{ref}}(x,y,z)\) from Step 1 and perform the conformal spherical map in Step 2 using the iterative method as for troubleshooting 1 above i.e. use remesh_method='CGAL', use lower remesh_samples (drop min_mesh_size too accordingly), set ksize_dilate>ksize_erode to widen geometry features. Generally, you will spot problematic geometry because the area_distortion_iter_curve will not go down smoothly - it should show monotoniic linear decrease then plateau as it hits the minimum, as for the example here. Critically the first part whilst its minimizing is smooth.

  2. start with delta=0.1 go down as delta=0.05, 0.01, 0.005, 0.001 etc.. If its not improving, try setting lloyd_relax_bool=True - this performs lloyd relaxation of the mesh every lloyd_every_iter iterations to help improve triangle quality during the flow. This is most helpful for densely sampled meshes.

3. Cannot find automatically min_area_distort#

Reason: min_area_distort is computed by observing the sign change when the area_distortion_iter_curve first changes sign from negative to positive. This assumes you run iterations after full convergence. There is two reasons if the other two troubleshooting do not apply:

  1. \(S_{\text{ref}}(x,y,z)\) is essentially spherical or close to. In this case conformal = equiareal parameterization. Therefore there is no need to relax.

  2. the relaxation has yet to converge. If stepsize=1 already, then increase max_iter. The larger the deviation of \(S_{\text{ref}}(x,y,z)\) is from a sphere, the greater the max_iter required. Particulary for elongated structures, its not unusual to set max_iter=1000.