Skip to content

Commit 11fb738

Browse files
authored
Implement EIT system performance measures. (#94)
* .ply reading * new figures of merit example * improvements to figures of merit * add convenience function for rendering * bugfix mesh plot * add tests for figures of merit * update render to keep image orientation * update mesh plot to allow mesh created with create function * figures of merit example for range of targets * EIT system analysis * adding drift calculation * add open EIT format * add option to calculate snr and detectability in decibels * add EIT system performance example * add colorbar function that matches height with corresponding image * make parse oeit return numpy array * read oeit reject non uniform lines * add allantools to environment.yml
1 parent a3b631c commit 11fb738

9 files changed

Lines changed: 858 additions & 15 deletions

File tree

environment.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ dependencies:
1212
- vispy
1313
- shapely
1414
- trimesh
15-
- "imageio>=2.19"
15+
- "imageio>=2.19"
16+
- allantools
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# coding: utf-8
2+
from __future__ import absolute_import, division, print_function
3+
4+
import matplotlib.pyplot as plt
5+
import numpy as np
6+
import pyeit.eit.jac as jac
7+
import pyeit.mesh as mesh
8+
from pyeit.eit.fem import EITForward
9+
import pyeit.eit.protocol as protocol
10+
from pyeit.mesh.wrapper import PyEITAnomaly_Circle
11+
from pyeit.mesh.external import place_electrodes_equal_spacing
12+
from numpy.random import default_rng
13+
from pyeit.quality.eit_system import (
14+
calc_signal_to_noise_ratio,
15+
calc_accuracy,
16+
calc_drift,
17+
calc_detectability,
18+
)
19+
from pyeit.eit.render import render_2d_mesh
20+
from pyeit.visual.plot import colorbar
21+
22+
23+
def main():
24+
# Configuration
25+
# ------------------------------------------------------------------------------------------------------------------
26+
n_el = 16
27+
render_resolution = (64, 64)
28+
background_value = 1
29+
anomaly_value = 2
30+
noise_magnitude = 2e-4
31+
drift_rate = 2e-7 # Per frame
32+
33+
n_background_measurements = 10
34+
n_drift_measurements = 1800
35+
measurement_frequency = 1
36+
37+
detectability_r = 0.5
38+
distinguishability_r = 0.35
39+
40+
# Initialization
41+
# ------------------------------------------------------------------------------------------------------------------
42+
drift_period_hours = n_drift_measurements / (60 * 60 * measurement_frequency)
43+
conductive_target = True if anomaly_value - background_value > 0 else False
44+
det_center_range = np.arange(0, 0.667, 0.067)
45+
dist_distance_range = np.arange(0.35, 1.2, 0.09)
46+
rng = default_rng(0)
47+
48+
# Problem setup
49+
# ------------------------------------------------------------------------------------------------------------------
50+
sim_mesh = mesh.create(n_el, h0=0.05)
51+
electrode_nodes = place_electrodes_equal_spacing(sim_mesh, n_electrodes=n_el)
52+
sim_mesh.el_pos = np.array(electrode_nodes)
53+
protocol_obj = protocol.create(n_el, dist_exc=1, step_meas=1, parser_meas="std")
54+
fwd = EITForward(sim_mesh, protocol_obj)
55+
56+
recon_mesh = mesh.create(n_el, h0=0.1)
57+
electrode_nodes = place_electrodes_equal_spacing(recon_mesh, n_electrodes=n_el)
58+
recon_mesh.el_pos = np.array(electrode_nodes)
59+
eit = jac.JAC(recon_mesh, protocol_obj)
60+
eit.setup(
61+
p=0.5, lamb=0.03, method="kotre", perm=background_value, jac_normalized=True
62+
)
63+
64+
# Simulate background
65+
# ------------------------------------------------------------------------------------------------------------------
66+
v0 = fwd.solve_eit(perm=background_value)
67+
d1 = np.array(range(n_drift_measurements)) * drift_rate # Create drift
68+
n1 = noise_magnitude * rng.standard_normal(
69+
(n_drift_measurements, len(v0))
70+
) # Create noise
71+
72+
v0_dn = np.tile(v0, (n_drift_measurements, 1)) + np.tile(d1, (len(v0), 1)).T + n1
73+
74+
# Calculate background performance measures
75+
# ------------------------------------------------------------------------------------------------------------------
76+
snr = calc_signal_to_noise_ratio(v0_dn[:n_background_measurements], method="db")
77+
accuracy = calc_accuracy(v0_dn[:n_background_measurements], v0, method="EIDORS")
78+
t2, adevs = calc_drift(v0_dn, method="Allan")
79+
80+
drifts_delta = calc_drift(v0_dn, sample_period=10, method="Delta")
81+
start = np.average(v0_dn[0:10], axis=0)
82+
drifts_percent = 100 * drifts_delta / start
83+
84+
# Simulate detectability test
85+
# ------------------------------------------------------------------------------------------------------------------
86+
detectabilities = []
87+
detectability_renders = []
88+
for c in det_center_range:
89+
anomaly = PyEITAnomaly_Circle(
90+
center=[c, 0], r=detectability_r, perm=anomaly_value
91+
)
92+
sim_mesh_new = mesh.set_perm(
93+
sim_mesh, anomaly=anomaly, background=background_value
94+
)
95+
v1 = fwd.solve_eit(perm=sim_mesh_new.perm)
96+
n = noise_magnitude * rng.standard_normal(len(v1))
97+
v1_n = v1 + n
98+
ds = eit.solve(v1_n, v0_dn[0], normalize=True)
99+
solution = np.real(ds)
100+
image = render_2d_mesh(recon_mesh, solution, resolution=render_resolution)
101+
detectability = calc_detectability(
102+
image, conductive_target=conductive_target, method="db"
103+
)
104+
detectabilities.append(detectability)
105+
detectability_renders.append(image)
106+
107+
# Simulate distinguishability test
108+
# ------------------------------------------------------------------------------------------------------------------
109+
anomaly = PyEITAnomaly_Circle(center=[0, 0], r=detectability_r, perm=anomaly_value)
110+
sim_mesh_new = mesh.set_perm(sim_mesh, anomaly=anomaly, background=background_value)
111+
dist_v0 = fwd.solve_eit(perm=sim_mesh_new.perm)
112+
dist_v0n = dist_v0 + noise_magnitude * rng.standard_normal(len(dist_v0))
113+
114+
distinguishabilities = []
115+
distinguishabilitiy_renders = []
116+
for d in dist_distance_range:
117+
a1 = PyEITAnomaly_Circle(
118+
center=[d / 2, 0], r=distinguishability_r, perm=anomaly_value
119+
)
120+
a2 = PyEITAnomaly_Circle(
121+
center=[-d / 2, 0], r=distinguishability_r, perm=anomaly_value
122+
)
123+
sim_mesh_new = mesh.set_perm(
124+
sim_mesh, anomaly=[a1, a2], background=background_value
125+
)
126+
v1 = fwd.solve_eit(perm=sim_mesh_new.perm)
127+
v1_n = v1 + noise_magnitude * rng.standard_normal(len(v1))
128+
ds = eit.solve(v1_n, dist_v0n, normalize=True)
129+
solution = np.real(ds)
130+
image = render_2d_mesh(recon_mesh, solution, resolution=render_resolution)
131+
# Distinguishability is detectability but with a target as the background.
132+
distinguishability = calc_detectability(
133+
image, conductive_target=conductive_target, method="db"
134+
)
135+
distinguishabilities.append(distinguishability)
136+
distinguishabilitiy_renders.append(image)
137+
138+
# Plot results
139+
# ------------------------------------------------------------------------------------------------------------------
140+
fig, axs = plt.subplots(2, 2)
141+
142+
axs[0, 0].plot(snr)
143+
axs[0, 0].set_xlabel("Channel Number")
144+
axs[0, 0].set_ylabel("Signal to Noise Ratio\n(dB)")
145+
axs[0, 0].title.set_text(f"Signal to Noise Ratio for {len(snr)} channels")
146+
147+
axs[0, 1].plot(accuracy)
148+
axs[0, 1].set_xlabel("Channel Number")
149+
axs[0, 1].set_ylabel("Accuracy")
150+
axs[0, 1].title.set_text(f"Accuracy for {len(snr)} channels")
151+
152+
axs[1, 0].set_xlabel("Averaging Window (s)")
153+
axs[1, 0].set_ylabel("Allan Deviation")
154+
axs[1, 0].title.set_text(f"Allan Deviation for {len(snr)} channels")
155+
for adev in adevs:
156+
axs[1, 0].plot(t2, adev)
157+
158+
axs[1, 1].plot(drifts_percent)
159+
axs[1, 1].title.set_text(
160+
f"Drift percentage on all channels.\nDrift period (hours): {drift_period_hours}"
161+
)
162+
axs[1, 1].set_xlabel("Channel number")
163+
axs[1, 1].set_ylabel("Drift (% of starting value)")
164+
165+
fig.tight_layout()
166+
fig.set_size_inches((10, 6))
167+
168+
fig, axs = plt.subplots(1, 2)
169+
axs[0].plot(det_center_range, detectabilities, ".-", label="-x axis")
170+
axs[0].legend()
171+
axs[0].set_xlabel("Target position (radius fraction)")
172+
axs[0].set_ylabel("Detectability (dB)")
173+
axs[0].title.set_text("Detectability vs radial position")
174+
175+
axs[1].plot(dist_distance_range, distinguishabilities)
176+
axs[1].set_xlabel("Separation distance (radius fraction)")
177+
axs[1].set_ylabel("Distinguishability (dB)")
178+
axs[1].title.set_text("Distinguishability vs separation distance")
179+
180+
fig.set_size_inches((10, 4))
181+
fig.tight_layout()
182+
183+
fig, axs = plt.subplots(1, len(det_center_range))
184+
for i, c in enumerate(det_center_range):
185+
axs[i].imshow(detectability_renders[i])
186+
axs[i].xaxis.set_ticks([])
187+
axs[i].yaxis.set_ticks([])
188+
189+
fig.set_size_inches((14, 2))
190+
fig.suptitle("Detectability Renders")
191+
fig.tight_layout()
192+
193+
fig, axs = plt.subplots(1, len(dist_distance_range))
194+
for i, d in enumerate(dist_distance_range):
195+
img = axs[i].imshow(distinguishabilitiy_renders[i])
196+
axs[i].xaxis.set_ticks([])
197+
axs[i].yaxis.set_ticks([])
198+
colorbar(img)
199+
200+
fig.set_size_inches((18, 2))
201+
fig.suptitle("Distinguishability Renders")
202+
fig.tight_layout()
203+
plt.show()
204+
205+
206+
if __name__ == "__main__":
207+
main()

examples/figures_of_merit_range.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,14 @@ def main():
8888
axs[i],
8989
solution,
9090
recon_mesh,
91-
ax_kwargs={"title": f"Target pos: {plot_c[i]:.2f}/r"},
91+
ax_kwargs={"title": f"Target Pos: {plot_c[i]:.2f}/r"},
9292
)
9393

94-
fig.set_size_inches(15, 2)
94+
fig.set_size_inches(12, 2)
9595
fig.tight_layout()
9696

9797
figs_list = np.array(figs_list)
98-
fig, axs = plt.subplots(5, 1, sharex=True)
99-
axs[4].set_xlabel("Target pos/r")
98+
fig, axs = plt.subplots(1, 5)
10099
titles = [
101100
"Average Amplitude",
102101
"Position Error",
@@ -106,9 +105,12 @@ def main():
106105
]
107106
for i in range(5):
108107
axs[i].plot(c_range, figs_list[:, i])
109-
axs[i].set_title(titles[i], size="small")
108+
axs[i].set_title(f"{titles[i]}\nvs Target Pos")
109+
axs[i].set_xlabel("Target Pos/r")
110+
axs[i].set_ylabel(titles[i])
110111

111-
plt.tight_layout()
112+
fig.set_size_inches(15, 3)
113+
fig.tight_layout()
112114
plt.show()
113115

114116

examples/figures_of_merit_single.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,20 @@ def main():
6969
# Print figures of merit
7070
print("")
7171
print(f"Amplitude: Average pixel value in reconstruction image is {figs[0]:.4f}")
72-
print(f"Position Error: {100*figs[1]:.2f}% of widest axis")
72+
print(f"Position Error: {100 * figs[1]:.2f}% of widest axis")
7373
print(
74-
f"Resolution: Reconstructed point radius {100*figs[2]:.2f}% of image equivalent radius"
74+
f"Resolution: Reconstructed point radius {100 * figs[2]:.2f}% of image equivalent radius"
7575
)
7676
print(
77-
f"Shape Deformation: {100*figs[3]:.2f}% of pixels in the thresholded image are outside the equivalent circle"
77+
f"Shape Deformation: {100 * figs[3]:.2f}% of pixels in the thresholded image are outside the equivalent circle"
7878
)
7979
print(
80-
f"Ringing: Ringing pixel amplitude is {100*figs[4]:.2f}% of image amplitude in thresholded region"
80+
f"Ringing: Ringing pixel amplitude is {100 * figs[4]:.2f}% of image amplitude in thresholded region"
8181
)
8282

8383
# Create mesh plots
8484
fig, axs = plt.subplots(1, 2)
85-
create_mesh_plot(axs[0], sim_mesh, ax_kwargs={"title": "Sim mesh"})
85+
create_mesh_plot(axs[0], sim_mesh_new, ax_kwargs={"title": "Sim mesh"})
8686
create_mesh_plot(axs[1], recon_mesh, ax_kwargs={"title": "Recon mesh"})
8787
fig.set_size_inches(10, 4)
8888

pyeit/io/oeit.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import numpy as np
2+
from scipy import stats
3+
4+
5+
def load_oeit_data(file_name):
6+
with open(file_name, "r") as f:
7+
lines = f.readlines()
8+
9+
data = []
10+
for line in lines:
11+
eit = parse_oeit_line(line)
12+
if eit is not None:
13+
data.append(eit)
14+
15+
mode_len = stats.mode([len(item) for item in data], keepdims=False)
16+
data = [item for item in data if len(item) == mode_len.mode]
17+
18+
return np.array(data)
19+
20+
21+
def parse_oeit_line(line):
22+
try:
23+
_, data = line.split(":", 1)
24+
except (ValueError, AttributeError):
25+
return None
26+
items = []
27+
for item in data.split(","):
28+
item = item.strip()
29+
if not item:
30+
continue
31+
try:
32+
items.append(float(item))
33+
except ValueError:
34+
return None
35+
return np.array(items)

0 commit comments

Comments
 (0)