From 14b17db2313aa591872d05ca844c5ecbc64518e0 Mon Sep 17 00:00:00 2001 From: liubenyuan Date: Thu, 14 Apr 2022 15:44:10 +0800 Subject: [PATCH] Optimize .et3 and .mes loading routine. Generate optimized thorax shape using distmesh and shapely (#38) Optimize .et3 and .mes loading routine. Generate optimized thorax shape using distmesh and shapely. --- examples/mesh_distmesh2d.py | 7 +- pyeit/feature_extraction/__init__.py | 4 +- .../feature_extraction/transfer_impedance.py | 17 ++- pyeit/io/et3.py | 33 +++-- pyeit/io/mes.py | 80 ++++++------ pyeit/mesh/distmesh.py | 53 ++------ pyeit/mesh/shape.py | 122 +++++++++--------- 7 files changed, 146 insertions(+), 170 deletions(-) diff --git a/examples/mesh_distmesh2d.py b/examples/mesh_distmesh2d.py index 169b942..3596ee9 100644 --- a/examples/mesh_distmesh2d.py +++ b/examples/mesh_distmesh2d.py @@ -164,7 +164,7 @@ def example6(): (-0.1653, 0.6819), ] # build triangles - p, t = distmesh.build(thorax, fh=area_uniform, pfix=p_fix, h0=0.05) + p, t = distmesh.build(thorax, fh=area_uniform, pfix=p_fix, h0=0.1) # plot fig, ax = plt.subplots() ax.triplot(p[:, 0], p[:, 1], t) @@ -176,6 +176,11 @@ def example6(): plt.show() +def example_head_symm(): + """head phantom (symmetric)""" + pass + + def example_voronoi_plot(): """draw voronoi plots for triangle elements""" diff --git a/pyeit/feature_extraction/__init__.py b/pyeit/feature_extraction/__init__.py index e9ff7ad..c5c7e86 100644 --- a/pyeit/feature_extraction/__init__.py +++ b/pyeit/feature_extraction/__init__.py @@ -4,7 +4,7 @@ EIT dynamic images and static properties. """ -from .transfer_impedance import ati, fmmu_index, ati_lr, ati_df +from .transfer_impedance import ati, fmmu_index, ati_roi, ati_df from .mesh_geometry import SimpleMeshGeometry -__all__ = ["ati", "fmmu_index", "ati_lr", "ati_df", "SimpleMeshGeometry"] +__all__ = ["ati", "fmmu_index", "ati_roi", "ati_df", "SimpleMeshGeometry"] diff --git a/pyeit/feature_extraction/transfer_impedance.py b/pyeit/feature_extraction/transfer_impedance.py index 3c9b960..7b091d0 100644 --- a/pyeit/feature_extraction/transfer_impedance.py +++ b/pyeit/feature_extraction/transfer_impedance.py @@ -6,6 +6,11 @@ import numpy as np +def nansum(x): + """implement numpy-1.8 behavior of nansum""" + return np.NAN if np.isnan(x).any() else np.nansum(np.abs(x)) + + def ati(x): """ averaged total impedance, unit (mV), @@ -16,13 +21,7 @@ def ati(x): ----- if I=1mA, then ati returns Ohms """ - # implement old behavior of numpy.nansum - if np.isnan(x).any(): - v = np.nan - else: - v = np.sum(np.abs(x)) / 192.0 - - return v + return nansum(x) / 192.0 def ati_df(x): @@ -66,8 +65,8 @@ def fmmu_index(n_el=16, dist=8, step=1): return left_sel, right_sel -def ati_lr(x, sel): - """extract ATI left, right""" +def ati_roi(x, sel): + """extract ATI from ROI region""" x_sel = np.nanmean(np.abs(x[sel])) return x_sel diff --git a/pyeit/io/et3.py b/pyeit/io/et3.py index 854fcb6..e081c7c 100644 --- a/pyeit/io/et3.py +++ b/pyeit/io/et3.py @@ -156,7 +156,7 @@ def load(self): d = fh.read(self.frame_size) # extract time ticks time_array[i] = unpack("d", d[8:16])[0] - # extract ADC samples + # extract ADC samples (double precision) dp = d[960 : self.header_size] adc_array[i] = np.array(unpack("8d", dp)) # extract demodulated I,Q data @@ -206,24 +206,31 @@ def to_df(self): return df def to_dp(self, adc_filter=False): - """convert raw ADC data to DataFrame""" - # left ear, right ear, Nasopharyngeal, rectal - columns = ["tle", "tre", "tn", "tr", "c4", "c5", "c6", "c7"] + """ + in new ET3 data, the left ear, right ear, Nasopharyngeal, rectal + temperature are recorded in the headers of .et3 file. + This script convert raw ADC data to DataFrame. + """ + # + columns = [ + "t_left_ear", + "t_right_ear", + "t_naso", + "t_renal", + "aux_c4", + "aux_c5", + "aux_c6", + "aux_c7", + ] dp = pd.DataFrame(self.adc_array, index=self.ts, columns=columns) dp = dp[~dp.index.duplicated()] if adc_filter: # correct temperature (temperature can accidently be 0) - dp.loc[dp["tle"] == 0, "tle"] = np.nan - dp.loc[dp["tre"] == 0, "tre"] = np.nan - dp.loc[dp["tn"] == 0, "tn"] = np.nan - dp.loc[dp["tr"] == 0, "tr"] = np.nan - # filter auxillary sampled data - dp.tle = med_outlier(dp.tle) - dp.tre = med_outlier(dp.tre) - dp.tn = med_outlier(dp.tn) - dp.tr = med_outlier(dp.tr) + for c in columns: + dp.loc[dp[c] == 0, c] = np.NAN + dp[c] = med_outlier(dp[c]) return dp diff --git a/pyeit/io/mes.py b/pyeit/io/mes.py index 0bf6b61..ae61238 100644 --- a/pyeit/io/mes.py +++ b/pyeit/io/mes.py @@ -10,6 +10,7 @@ """ # Copyright (c) Benyuan Liu. All Rights Reserved. # Distributed under the (new) BSD License. See LICENSE.txt for more info. +import os import ctypes import struct import numpy as np @@ -158,57 +159,58 @@ def extract_el(fh): return el_pos -if __name__ == "__main__": - # How to load and use a .mes file (github.com/liubenyuan/eitmesh) - mstr = resource_filename("eitmesh", "data/I0007.mes") - mesh_obj, el_pos = load(fstr=mstr) - - # print the size - e, pts = mesh_obj["element"], mesh_obj["node"] - mesh_center = np.array([np.median(pts[:, 0]), np.median(pts[:, 1])]) - # print('tri size = (%d, %d)' % e.shape) - # print('pts size = (%d, %d)' % pts.shape) - - # show mesh - fig, ax = plt.subplots(1, figsize=(6, 6)) - ax.triplot(pts[:, 0], pts[:, 1], e) - ax.plot(pts[el_pos, 0], pts[el_pos, 1], "ro") +def mesh_plot(ax, mesh_obj, el_pos, imstr="", title=None): + """plot and annotate mesh""" + p, e, perm = mesh_obj["node"], mesh_obj["element"], mesh_obj["perm"] + annotate_color = "k" + if os.path.exists(imstr): + im = plt.imread(imstr) + annotate_color = "w" + ax.imshow(im, origin="lower") + ax.tripcolor(p[:, 0], p[:, 1], e, facecolors=perm, edgecolors="k", alpha=0.4) + ax.triplot(p[:, 0], p[:, 1], e, lw=1) + ax.plot(p[el_pos, 0], p[el_pos, 1], "ro") for i, el in enumerate(el_pos): - xy = np.array([pts[el, 0], pts[el, 1]]) + xy = np.array([p[el, 0], p[el, 1]]) text_offset = (xy - mesh_center) * [1, -1] * 0.05 ax.annotate( str(i + 1), xy=xy, xytext=text_offset, textcoords="offset points", - color="k", + color=annotate_color, ha="center", va="center", ) ax.set_aspect("equal") + ax.set_title(title) ax.invert_yaxis() - # bmp and mesh overlay - fig, ax = plt.subplots(figsize=(6, 6)) + return ax + + +if __name__ == "__main__": + # How to load and use a .mes file (github.com/liubenyuan/eitmesh) + mstr = resource_filename("eitmesh", "data/IM470.mes") imstr = mstr.replace(".mes", ".bmp") - im = plt.imread(imstr) - ax.imshow(im) - ax.set_aspect("equal") + mesh_obj, el_pos = load(fstr=mstr) - # the plot will automatically align with an overlay image - ax.triplot(pts[:, 0], pts[:, 1], e) - ax.plot(pts[el_pos, 0], pts[el_pos, 1], "ro") - for i, el in enumerate(el_pos): - xy = np.array([pts[el, 0], pts[el, 1]]) - text_offset = (xy - mesh_center) * [1, -1] * 0.05 - ax.annotate( - str(i + 1), - xy=xy, - xytext=text_offset, - textcoords="offset points", - color="w", - ha="center", - va="center", - ) - ax.axis("off") - plt.show() + # print the size + e, pts, perm = mesh_obj["element"], mesh_obj["node"], mesh_obj["perm"] + mesh_center = np.array([np.median(pts[:, 0]), np.median(pts[:, 1])]) + # print('tri size = (%d, %d)' % e.shape) + # print('pts size = (%d, %d)' % pts.shape) + fig, ax = plt.subplots(1, figsize=(6, 6)) + mesh_plot(ax, mesh_obj, el_pos, imstr=imstr) + # fig.savefig("IM470.png", dpi=100) + + # compare two mesh + mstr = resource_filename("eitmesh", "data/DLS2.mes") + mesh_obj2, el_pos2 = load(fstr=mstr) + mesh_array = [[mesh_obj, el_pos, "IM470"], [mesh_obj2, el_pos2, "DLS2"]] + + fig, axs = plt.subplots(1, 2, figsize=(12, 6)) + for i, ax in enumerate(axs): + mesh, elp, title = mesh_array[i] + mesh_plot(ax, mesh, elp, title=title) + # fig.savefig("mesh_plot.png", dpi=100) diff --git a/pyeit/mesh/distmesh.py b/pyeit/mesh/distmesh.py index 7d922a5..81f7c89 100644 --- a/pyeit/mesh/distmesh.py +++ b/pyeit/mesh/distmesh.py @@ -12,11 +12,8 @@ from numpy import sqrt from scipy.spatial import Delaunay from scipy.sparse import csr_matrix - from .utils import dist, edge_project -from .shape import thorax - class DISTMESH: """class for distmesh""" @@ -101,15 +98,8 @@ def __init__( self.num_density = 0 self.num_move = 0 - """ - keep points that are inside the thorax shape using a function that returns a matrix containing - True if the corresponing point is inside the shape, False if not. - """ - if fd == thorax: - p = p[fd(p)] - else: - # keep points inside (minus distance) with a small gap (geps) - p = p[fd(p) < self.geps] # pylint: disable=E1136 + # keep points inside (minus distance) with a small gap (geps) + p = p[fd(p) < self.geps] # pylint: disable=E1136 # rejection points by sampling on fh r0 = 1.0 / fh(p) ** self.n_dim @@ -122,21 +112,12 @@ def __init__( self.pfix = p_fix self.nfix = len(p_fix) - # convert boolean array to 2D to be compatible with Delaunay pts paramater (must be 2D) - if fd == thorax: - p = np.reshape(p, (-1, 2)) - # remove duplicated points of p and p_fix # avoid overlapping of mesh points if self.nfix > 0: p = remove_duplicate_nodes(p, p_fix, self.geps) p = np.vstack([p_fix, p]) - if fd == thorax: - p = np.reshape( - p, (-1, 2) - ) # convert boolean array to 2D to be compatible with Delaunay pts paramater (must be 2D) - # store p and N self.N = p.shape[0] self.p = p @@ -163,23 +144,12 @@ def triangulate(self): self.pold = self.p.copy() # triangles where the points are arranged counterclockwise - if self.fd != thorax: - tri = Delaunay(self.p).simplices - else: - tri = Delaunay( - self.p, qhull_options="QJ" - ).simplices # QJ parameter so tuples don't exceed boundary - + # QJ parameter so tuples don't exceed boundary + tri = Delaunay(self.p, qhull_options="QJ").simplices pmid = np.mean(self.p[tri], axis=1) - if self.fd != thorax: - # keeps only interior points - t = tri[self.fd(pmid) < -self.geps] - else: - # adapting returned triangles matrix with the thorax integrated fd - tri_pmid = [p[0] for p in self.fd(pmid)] - tri_pmid = np.array(tri_pmid) - t = tri[tri_pmid] + # keeps only interior points + t = tri[self.fd(pmid) < -self.geps] # extract edges (bars) bars = t[:, self.edge_combinations].reshape((-1, 2)) # sort and remove duplicated edges, eg (1,2) and (2,1) @@ -459,13 +429,10 @@ def build( # calculate bar forces Ftot = dm.bar_force(L, L0, barvec) - if fd != thorax: - # update p - converge = dm.move_p(Ftot) - # the stopping ctriterion (movements interior are small) - if converge: - break - else: # Thorax mesh is created so far without iteration process (to be updated) + # update p + converge = dm.move_p(Ftot) + # the stopping ctriterion (movements interior are small) + if converge: break # at the end of iteration, (p - pold) is small, so we recreate delaunay diff --git a/pyeit/mesh/shape.py b/pyeit/mesh/shape.py index 1e9bcd8..8e50905 100644 --- a/pyeit/mesh/shape.py +++ b/pyeit/mesh/shape.py @@ -353,69 +353,65 @@ def thorax(pts): """ # Thorax contour points coordinates are taken from a thorax simulation based on EIDORS - thrx = Polygon( - [ - (0.0487, 0.6543), - (0.1564, 0.6571), - (0.2636, 0.6697), - (0.3714, 0.6755), - (0.479, 0.6686), - (0.5814, 0.6353), - (0.6757, 0.5831), - (0.7582, 0.5137), - (0.8298, 0.433), - (0.8894, 0.3431), - (0.9347, 0.2452), - (0.9698, 0.1431), - (0.9938, 0.0379), - (1.0028, -0.0696), - (0.9914, -0.1767), - (0.9637, -0.281), - (0.9156, -0.3771), - (0.8359, -0.449), - (0.7402, -0.499), - (0.6432, -0.5463), - (0.5419, -0.5833), - (0.4371, -0.6094), - (0.3308, -0.6279), - (0.2243, -0.6456), - (0.1168, -0.6508), - (0.0096, -0.6387), - (-0.098, -0.6463), - (-0.2058, -0.6433), - (-0.313, -0.6312), - (-0.4181, -0.6074), - (-0.5164, -0.5629), - (-0.6166, -0.5232), - (-0.7207, -0.4946), - (-0.813, -0.4398), - (-0.8869, -0.3614), - (-0.933, -0.2647), - (-0.9451, -0.1576), - (-0.9425, -0.0498), - (-0.9147, 0.0543), - (-0.8863, 0.1585), - (-0.8517, 0.2606), - (-0.8022, 0.3565), - (-0.7413, 0.4455), - (-0.6664, 0.5231), - (-0.5791, 0.5864), - (-0.4838, 0.6369), - (-0.3804, 0.667), - (-0.2732, 0.6799), - (-0.1653, 0.6819), - (-0.0581, 0.6699), - ] - ) - - pts_ = [Point(p[0], p[1]) for p in pts] - # ba : boolean "array" , will be then converted to an array - # initialized as list because it's smoother in initialization - # compared to an array (in first append : axis=1, >=second :axis=0) - ba = [[thrx.contains(pt), thrx.contains(pt)] for pt in pts_] - ba = np.array(ba) - - return ba + thrx_pts = [ + (0.0487, 0.6543), + (0.1564, 0.6571), + (0.2636, 0.6697), + (0.3714, 0.6755), + (0.479, 0.6686), + (0.5814, 0.6353), + (0.6757, 0.5831), + (0.7582, 0.5137), + (0.8298, 0.433), + (0.8894, 0.3431), + (0.9347, 0.2452), + (0.9698, 0.1431), + (0.9938, 0.0379), + (1.0028, -0.0696), + (0.9914, -0.1767), + (0.9637, -0.281), + (0.9156, -0.3771), + (0.8359, -0.449), + (0.7402, -0.499), + (0.6432, -0.5463), + (0.5419, -0.5833), + (0.4371, -0.6094), + (0.3308, -0.6279), + (0.2243, -0.6456), + (0.1168, -0.6508), + (0.0096, -0.6387), + (-0.098, -0.6463), + (-0.2058, -0.6433), + (-0.313, -0.6312), + (-0.4181, -0.6074), + (-0.5164, -0.5629), + (-0.6166, -0.5232), + (-0.7207, -0.4946), + (-0.813, -0.4398), + (-0.8869, -0.3614), + (-0.933, -0.2647), + (-0.9451, -0.1576), + (-0.9425, -0.0498), + (-0.9147, 0.0543), + (-0.8863, 0.1585), + (-0.8517, 0.2606), + (-0.8022, 0.3565), + (-0.7413, 0.4455), + (-0.6664, 0.5231), + (-0.5791, 0.5864), + (-0.4838, 0.6369), + (-0.3804, 0.667), + (-0.2732, 0.6799), + (-0.1653, 0.6819), + (-0.0581, 0.6699), + ] + thrx = Polygon(thrx_pts) + pts_ = [Point(p) for p in pts] + # calculate signed distance + dist = [thrx.exterior.distance(p) for p in pts_] + sign = np.sign([-int(thrx.contains(p)) + 0.5 for p in pts_]) + + return sign * dist # L_shaped mesh (for testing)