insituTEM 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
insituTEM/__ init __.py CHANGED
@@ -0,0 +1,14 @@
1
+ # __init__.py
2
+ __version__ = '1.0.0'
3
+ __author__ = 'Meng Li'
4
+
5
+ import insitu_alignment as AL
6
+ import insitu_DP as DP
7
+ import insitu_IO as IO
8
+ import insitu_Preprocess as PP
9
+ import insitu_Diff as Diff
10
+ import insitu_EMraw as EM
11
+ import insitu_DENS as DENS
12
+
13
+
14
+ __all__ = ['function1', 'Class2']
@@ -0,0 +1,318 @@
1
+ """
2
+ in-situ TEM Toolbox - DENS holder data process
3
+
4
+ Assembles of functions related to process raw data by DENS holders
5
+
6
+ Example:
7
+
8
+ from insitu_TEM import insitu_DENS as DENS
9
+ DENS.XXX
10
+
11
+ Created on Tue May 13 2025
12
+ @author: Meng Li mli4@bnl.gov
13
+
14
+ """
15
+
16
+ """
17
+ Codes for Stream data process
18
+ """
19
+ import pandas as pd
20
+
21
+ def read_pstrace_csv(filepath):
22
+ # Try several encodings until one works
23
+ for encoding in ['utf-16', 'utf-8', 'ISO-8859-1']:
24
+ try:
25
+ with open(filepath, 'r', encoding=encoding) as f:
26
+ lines = [line.strip() for line in f if line.strip()]
27
+ break
28
+ except UnicodeDecodeError:
29
+ continue
30
+ else:
31
+ raise UnicodeDecodeError("Failed to decode file using common encodings.")
32
+
33
+ # Detect measurement type (e.g., CV I vs E Scan 1)
34
+ measurement_type = None
35
+ for line in lines:
36
+ if "vs" in line and not line.lower().startswith("date"):
37
+ measurement_type = line
38
+ break
39
+
40
+ # Detect actual column header (like "V,µA" or "s,V")
41
+ header_index = None
42
+ for i, line in enumerate(lines):
43
+ if ',' in line and all(any(c.isalpha() for c in token) for token in line.split(',')):
44
+ header_index = i
45
+ break
46
+
47
+ if header_index is None:
48
+ raise ValueError("Could not find a valid data header (e.g., 'V,µA' or 's,V')")
49
+
50
+ headers = [h.strip() for h in lines[header_index].split(',')]
51
+ data_lines = lines[header_index + 1:]
52
+
53
+ # Read and convert numeric data only
54
+ data = []
55
+ for line in data_lines:
56
+ try:
57
+ row = [float(val) for val in line.split(',')]
58
+ data.append(row)
59
+ except ValueError:
60
+ continue # skip non-numeric lines
61
+
62
+ df = pd.DataFrame(data, columns=headers)
63
+ return measurement_type, df
64
+
65
+
66
+ import matplotlib.pyplot as plt
67
+
68
+ def plot_multiple_xy(datasets, labels=None, xlabel=None, ylabel=None, title=None):
69
+ """
70
+ Plot multiple datasets (each as a 2-column DataFrame) on one plot.
71
+
72
+ Args:
73
+ datasets: list of DataFrames with 2 columns
74
+ labels: list of labels for each line
75
+ xlabel, ylabel: optional axis labels
76
+ title: optional plot title
77
+ """
78
+ fig, ax = plt.subplots(figsize=(6, 4), dpi=300)
79
+
80
+ colors = ['blue', 'green', 'red', 'orange', 'purple']
81
+
82
+ for i, df in enumerate(datasets):
83
+ x = df.iloc[:, 0]
84
+ y = df.iloc[:, 1]
85
+ label = labels[i] if labels else None
86
+ color = colors[i % len(colors)]
87
+ ax.plot(x, y, label=label, color=color, linewidth=1)
88
+
89
+ # X and Y = 0 guide lines
90
+ ax.axhline(0, color='gray', linestyle='-', linewidth=0.5)
91
+ ax.axvline(0, color='gray', linestyle='-', linewidth=0.5)
92
+
93
+ # Axis style: boxed
94
+ for side in ['top', 'bottom', 'left', 'right']:
95
+ ax.spines[side].set_linewidth(1)
96
+ ax.spines[side].set_color('black')
97
+
98
+ # Axis labels
99
+ ax.set_xlabel(xlabel if xlabel else datasets[0].columns[0], fontsize=12, fontweight='bold', labelpad=10)
100
+ ax.set_ylabel(ylabel if ylabel else datasets[0].columns[1], fontsize=12, fontweight='bold', labelpad=10)
101
+
102
+ # Axis limits
103
+ # Extract x/y and lengths
104
+ x_list = [df.iloc[:, 0].values for df in datasets]
105
+ y_list = [df.iloc[:, 1].values for df in datasets]
106
+ all_x = np.concatenate(x_list)
107
+ all_y = np.concatenate(y_list)
108
+ # ax.set_xlim(all_x.min()*1.1, all_x.max()*1.1)
109
+ # ax.set_ylim(all_y.min()*1.2, all_y.max()*1.1)
110
+ # Add 10% margin to x and y limits
111
+ x_margin = (all_x.max() - all_x.min()) * 0.05
112
+ y_margin = (all_y.max() - all_y.min()) * 0.05
113
+
114
+ ax.set_xlim(all_x.min() - x_margin, all_x.max() + x_margin)
115
+ ax.set_ylim(all_y.min() - y_margin, all_y.max() + y_margin)
116
+
117
+
118
+ # Ticks and layout
119
+ ax.tick_params(direction='in', top=True, right=True)
120
+ ax.grid(False)
121
+
122
+ # Title and legend
123
+ if title:
124
+ ax.set_title(title, fontsize=12,fontweight='bold', pad=10)
125
+ if labels:
126
+ ax.legend()
127
+
128
+ plt.tight_layout()
129
+ plt.show()
130
+
131
+
132
+ mport matplotlib.pyplot as plt
133
+
134
+ def plot_xy(df, measurement_type=None):
135
+ """
136
+ Plot single xy curve
137
+ """
138
+ x_col = df.columns[0]
139
+ y_col = df.columns[1]
140
+ x = df[x_col]
141
+ y = df[y_col]
142
+
143
+ fig, ax = plt.subplots(figsize=(6,4),dpi=300) # square aspect ratio
144
+
145
+ # Plot data
146
+ ax.plot(x, y, linewidth=1, color='blue')
147
+
148
+ # Keep left and bottom axis
149
+ ax.spines['left'].set_visible(True)
150
+ ax.spines['bottom'].set_visible(True)
151
+
152
+ # Show top and right spine to make a box
153
+ ax.spines['top'].set_visible(True)
154
+ ax.spines['right'].set_visible(True)
155
+
156
+ # Draw all 4 spines in black (like a square)
157
+ for side in ['top', 'bottom', 'left', 'right']:
158
+ ax.spines[side].set_linewidth(1)
159
+ ax.spines[side].set_color('black')
160
+
161
+ # Add x=0 and y=0 guide lines
162
+ ax.axhline(0, color='orange', linestyle='-', linewidth=0.5)
163
+ ax.axvline(0, color='orange', linestyle='-', linewidth=0.5)
164
+
165
+ # Axis label styling
166
+ ax.set_xlabel('Voltage (V)', fontsize=12, fontweight='bold', labelpad=10)
167
+ ax.set_ylabel('Current (uA)' , fontsize=12, fontweight='bold', labelpad=10)
168
+
169
+ # Title
170
+ if measurement_type:
171
+ ax.set_title(measurement_type, fontsize=12, fontweight='bold', pad=10)
172
+
173
+ # Tick styling
174
+ ax.tick_params(direction='in', top=True, right=True)
175
+
176
+ # Turn off grid
177
+ ax.grid(False)
178
+
179
+ plt.tight_layout()
180
+ plt.show()
181
+
182
+
183
+ # Convert the plot to movie
184
+
185
+ import matplotlib.pyplot as plt
186
+ import matplotlib.animation as animation
187
+ import numpy as np
188
+ import cv2
189
+ import tifffile
190
+ from matplotlib.backends.backend_agg import FigureCanvasAgg
191
+ import tqdm
192
+
193
+ def animate_CVplots(datasets, labels=["Scan 1", "Scan 2", "Scan 3"], xlabel="Voltage (V)", ylabel="Current (µA)", title="CV Scan",
194
+ fps=60, save_basename="cv_animation"):
195
+ """
196
+ Function to animate the plot
197
+ datasets, # List of DataFrames, each with 2 columns (x, y) for a scan
198
+ labels=None, # Optional list of labels for each scan (used in legend)
199
+ xlabel=None, # Optional label for x-axis (default: first column name)
200
+ ylabel=None, # Optional label for y-axis (default: second column name)
201
+ title=None, # Optional plot title
202
+ fps=50, # fps of the input data. Original speed: 4 fps
203
+ save_basename="cv_animation" # Prefix for saved output files (AVI + TIFF stack)
204
+
205
+ """
206
+ # Setup plot and canvas
207
+ fig, ax = plt.subplots(figsize=(6, 4), dpi=150)
208
+ fig.subplots_adjust(left=0.15, right=0.95, top=0.9, bottom=0.15)
209
+ canvas = FigureCanvasAgg(fig)
210
+
211
+ # Extract x/y and lengths
212
+ x_list = [df.iloc[:, 0].values for df in datasets]
213
+ y_list = [df.iloc[:, 1].values for df in datasets]
214
+ lengths = [len(x) for x in x_list]
215
+ cum_lengths = np.cumsum(lengths)
216
+ total_frames = cum_lengths[-1]
217
+
218
+ # Axis formatting
219
+ for side in ['top', 'bottom', 'left', 'right']:
220
+ ax.spines[side].set_linewidth(1)
221
+ ax.spines[side].set_color('black')
222
+
223
+ ax.axhline(0, color='gray', linewidth=0.5)
224
+ ax.axvline(0, color='gray', linewidth=0.5)
225
+ ax.tick_params(direction='in', top=True, right=True)
226
+ ax.set_xlabel(xlabel or datasets[0].columns[0], fontsize=12, fontweight='bold', labelpad=10)
227
+ ax.set_ylabel(ylabel or datasets[0].columns[1], fontsize=12, fontweight='bold', labelpad=10)
228
+ if title:
229
+ ax.set_title(title, fontsize=10)
230
+
231
+ # Prepare plot elements
232
+ colors = ['blue', 'green', 'red', 'purple']
233
+ lines, tips = [], []
234
+ for i in range(len(datasets)):
235
+ line, = ax.plot([], [], color=colors[i % len(colors)], label=(labels[i] if labels else None), linewidth=1)
236
+ tip, = ax.plot([], [], color=colors[i % len(colors)], marker='o', markersize=4)
237
+ lines.append(line)
238
+ tips.append(tip)
239
+
240
+ # Add live current value in top-left
241
+ value_text = ax.text(0.02, 0.95, '', transform=ax.transAxes,
242
+ fontsize=10, color='black', ha='left', va='top')
243
+
244
+
245
+ if labels:
246
+ ax.legend()
247
+
248
+ # Axis limits
249
+ all_x = np.concatenate(x_list)
250
+ all_y = np.concatenate(y_list)
251
+
252
+ # Add 10% margin to x and y limits
253
+ x_margin = (all_x.max() - all_x.min()) * 0.05
254
+ y_margin = (all_y.max() - all_y.min()) * 0.05
255
+
256
+ ax.set_xlim(all_x.min() - x_margin, all_x.max() + x_margin)
257
+ ax.set_ylim(all_y.min() - y_margin, all_y.max() + y_margin)
258
+ # ax.set_xlim(all_x.min()*1.1, all_x.max()*1.1)
259
+ # ax.set_ylim(all_y.min()*1.2, all_y.max()*1.1)
260
+
261
+ # Video writer setup
262
+ canvas.draw()
263
+ width, height = canvas.get_width_height()
264
+ # fps=int(1000/interval)
265
+ aviout=save_basename+"_"+str(fps)+".avi"
266
+ out = cv2.VideoWriter(aviout, cv2.VideoWriter_fourcc(*'MJPG'), fps, (width, height))
267
+ # tiff_frames = []
268
+
269
+ # Setup TIFF writer with BigTIFF support
270
+ tif_writer = tifffile.TiffWriter(f"{save_basename}.tif", bigtiff=True)
271
+
272
+ # Render frames
273
+ print("Plotting the curves into movies~")
274
+ for frame in tqdm.tqdm(range(total_frames)):
275
+ ax.set_title(title, fontsize=12,fontweight='bold') # reset title in case of matplotlib bugs
276
+
277
+ for i, (x, y, line, tip) in enumerate(zip(x_list, y_list, lines, tips)):
278
+ start = cum_lengths[i - 1] if i > 0 else 0
279
+ end = cum_lengths[i]
280
+ if frame < start:
281
+ # Not started yet
282
+ line.set_data([], [])
283
+ tip.set_data([], [])
284
+ elif frame >= end:
285
+ # Completed
286
+ line.set_data(x, y)
287
+ tip.set_data([], [])
288
+ else:
289
+ # Active scan
290
+ idx = frame - start + 1
291
+ line.set_data(x[:idx], y[:idx])
292
+ tip.set_data([x[idx - 1]], [y[idx - 1]])
293
+ value_text.set_text(
294
+ f"{labels[i] if labels else f'Scan {i+1}'}\nV = {x[idx - 1]:.3f} V\nI = {y[idx - 1]:.3f} µA"
295
+ )
296
+
297
+ # Render and convert to image
298
+ canvas.draw()
299
+ buf = np.frombuffer(canvas.tostring_rgb(), dtype=np.uint8)
300
+ image = buf.reshape((height, width, 3))
301
+ image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
302
+ out.write(image_bgr)
303
+ # Save TIFF frame (with compression)
304
+ tif_writer.write(image, photometric='rgb', compression='deflate')
305
+
306
+ out.release()
307
+ tif_writer.close()
308
+
309
+ # tifffile.imwrite(f"{save_basename}.tif", tiff_frames, photometric='rgb',compression='deflate')
310
+ print("Conversion done!")
311
+ print(f"Saved AVI: {save_basename}.avi")
312
+ print(f"Saved TIFF stack: {save_basename}.tif")
313
+
314
+ from tkinter import Tk, filedialog
315
+ def select_multiple_files(filetypes=(("csv files", "*.csv"), ("All files", "*.*"))):
316
+ Tk().withdraw() # Hide the root window
317
+ filepaths = filedialog.askopenfilenames(title="Select PSTrace .csv files", filetypes=filetypes)
318
+ return list(filepaths) # Convert from tuple to list
insituTEM/insitu_DP.py CHANGED
@@ -10,7 +10,7 @@ Example:
10
10
  IO.f2tif(path,1)
11
11
 
12
12
  Created on Tue Jun 11 10:25:25 2019
13
- @author: Mona
13
+ @author: Meng Li
14
14
  """
15
15
  import numpy as np
16
16
 
@@ -57,12 +57,12 @@ def readcsv(path,col_list):
57
57
  return D
58
58
 
59
59
 
60
- def writeCSV(pathout,data):
61
- # import csv
62
- import numpy as np
63
- # (length,width)=np.shape(data)
64
- # np.savetxt(pathout, data, fmt='%1.4d', delimiter=",")
65
- np.savetxt(pathout, data, fmt='%.4f', delimiter=",")
60
+ # def writeCSV(pathout,data):
61
+ # # import csv
62
+ # import numpy as np
63
+ # # (length,width)=np.shape(data)
64
+ # # np.savetxt(pathout, data, fmt='%1.4d', delimiter=",")
65
+ # np.savetxt(pathout, data, fmt='%.4f', delimiter=",")
66
66
 
67
67
  def plot2ani(x,y,outpath,label,w=800,h=600,fps=10,dpi=72,tl_size=20,c='r'):
68
68
  """
@@ -158,4 +158,224 @@ def AutoROIbg(img):
158
158
  else:
159
159
  ROI = 0
160
160
  return ROI
161
-
161
+
162
+ def readscale(path):
163
+ """
164
+ Function to measure scalebar from movie first frame
165
+ Input: path - movie path
166
+ scalebarcolr- 0 for black, 255 for white
167
+ """
168
+ import os
169
+ os.environ["IMAGEIO_FFMPEG_EXE"] = "/opt/homebrew/bin/ffmpeg"
170
+ import moviepy.editor as mp
171
+ import cv2
172
+
173
+ video = mp.VideoFileClip(path)
174
+ fr = video.get_frame(mp.cvsecs(0))
175
+ fr = cv2.cvtColor(fr, cv2.COLOR_BGR2GRAY)
176
+
177
+ # Select ROI using cv2.selectROI()
178
+ roi = cv2.selectROI('Select scalebar ROI, quit with SPACE', fr)
179
+ cv2.waitKey(10)
180
+ # cv2.destroyAllWindows()
181
+ cv2.destroyWindow('Select scalebar ROI, quit with SPACE')
182
+
183
+ # print(roi)
184
+ # Crop the image based on the selected ROI
185
+ cropped_image = fr[int(roi[1]):int(roi[1] + roi[3]), int(roi[0]):int(roi[0] + roi[2])]
186
+
187
+ # # # Display the cropped image
188
+ # from PIL import Image
189
+ # from IPython.display import display
190
+ # pil_image= Image.fromarray(cropped_image)
191
+ # display(pil_image)
192
+
193
+ # plt.imshow(cropped_image)
194
+
195
+ IO.showFr(cropped_image)
196
+ #calculate the number of pixels in scalebar
197
+ # print(cropped_image[-5,:])
198
+ import numpy as np
199
+
200
+ scalebar_color = int(input("Input the scalebar color, 0-black, 255-white: "))
201
+
202
+
203
+ scalebar_px = np.count_nonzero(cropped_image[-10,:]==scalebar_color)
204
+
205
+ print("scalebar_px=", scalebar_px)
206
+
207
+ scale_nm = int(input("Input the scalebar in nm: "))
208
+ scale =scalebar_px/scale_nm
209
+ print("scale=",scale,"px/nm")
210
+ return scale
211
+
212
+ def next_power_of_2(x):
213
+ return 1 if x == 0 else 2**(x - 1).bit_length()
214
+
215
+ def pad_to_nearest_power_of_2(image):
216
+ import cv2
217
+ # Get original dimensions
218
+ h, w = image.shape
219
+ size=max(h,w)
220
+
221
+ # Calculate the nearest power of 2 dimensions
222
+ new_size = next_power_of_2(size)
223
+
224
+ # Calculate padding amounts
225
+ top_pad = (new_size - h) // 2
226
+ bottom_pad = new_size - h - top_pad
227
+ left_pad = (new_size - w) // 2
228
+ right_pad = new_size - w - left_pad
229
+
230
+ # Apply padding
231
+ padded_image = cv2.copyMakeBorder(
232
+ image,
233
+ top=top_pad, bottom=bottom_pad,
234
+ left=left_pad, right=right_pad,
235
+ borderType=cv2.BORDER_CONSTANT,
236
+ value=0 # Black color
237
+ )
238
+
239
+ return padded_image
240
+
241
+ def getFFT(im):
242
+ """
243
+ Function to get FFT from grayscale image, auto pad the image to keep the image size 2^n
244
+ Input: grayscale image (np array)
245
+ Output: grayscale FFT with 2^n size, 32 bit
246
+ """
247
+
248
+ pad_im=pad_to_nearest_power_of_2(im)
249
+
250
+
251
+ fft_image = np.fft.fft2(pad_im)
252
+ fft_shifted = np.fft.fftshift(fft_image)
253
+ fft = 20 * np.log(np.abs(fft_shifted))
254
+ return fft
255
+
256
+ def FFTstack(path):
257
+ """
258
+ Function to get FFT of the stack
259
+ input: path of the image stack
260
+ output: FFT stack with 2^n size 8 bit images
261
+ """
262
+ import tifffile
263
+ import tqdm
264
+ import cv2
265
+
266
+ stack = tifffile.imread(path)
267
+ nFrames,h,w = stack.shape #make sure the order of h and w!
268
+ pathout=path[:-4]+'_FFT.tif'
269
+
270
+ print("------------------------------------------")
271
+ print('Calculating FFT of each frame')
272
+ with tifffile.TiffWriter(pathout,bigtiff=True) as tif:
273
+ for i in tqdm.tqdm(range(nFrames)):
274
+ fr=stack[i]
275
+ fft=getFFT(fr)
276
+ #convert the 32 bit image to 8 bit
277
+ normalized_image = cv2.normalize(fft, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX)
278
+ # Convert the normalized image to 8-bit
279
+ fft_8bit = np.uint8(normalized_image)
280
+ tif.write(fft_8bit, contiguous=True)
281
+
282
+ def FFT2Polar(fft):
283
+ """
284
+ Function to convert FFT to polar image, y= 180˚, x= Rho
285
+ Input: grayscale fft image (np array)
286
+ """
287
+ import cv2
288
+ #function to convert fft image to polar coordinates from the center from 0-180 deg
289
+ center = (fft.shape[1] // 2, fft.shape[0] // 2)
290
+ polar_fft = cv2.linearPolar(fft, center, center[0], cv2.WARP_FILL_OUTLIERS)
291
+ # polar_fft= polar_fft[:,:center[1]]
292
+ #resize the image to realistic size
293
+ resized_polar_fft = cv2.resize(polar_fft, (center[0], 360))
294
+ # plt.imshow(resized_polar_fft)
295
+
296
+ p_fft1=resized_polar_fft[:180,:]
297
+ p_fft2=resized_polar_fft[180:,:]
298
+ p_fft=p_fft1+p_fft2
299
+ return p_fft
300
+
301
+ def FFTidentify(fft,pathout,scale=1,sigmaMax=2.5,sigmaMin=2.4,thrs=0.96,sigma_ratio=10):
302
+ """
303
+ Function to identify DPs from FFT
304
+ input:
305
+ fft: image
306
+ scale: scale of the original image : 1 px = XXX nm
307
+ parameters for blobs detection: sigmaMax, sigmaMin, thrs, sigma_ratio
308
+ pathout: path for output image (.tif) and csv
309
+
310
+ output:
311
+ idnetified image
312
+ indexed csv
313
+
314
+
315
+
316
+ """
317
+ import math
318
+ import matplotlib.pyplot as plt
319
+ from scipy import ndimage
320
+ from skimage.feature import blob_dog, blob_log, blob_doh
321
+ from math import sqrt
322
+ from insituTEM import insitu_IO as IO
323
+
324
+ h,w = fft.shape
325
+ x_c=int(w/2)
326
+ y_c=int(h/2)
327
+ r=int(x_c/2)
328
+ if w>320:
329
+ fft_crop=fft[x_c-r:x_c+r,y_c-r:y_c+r]
330
+ image= ndimage.maximum_filter(fft_crop, size=2, mode='wrap')
331
+ else:
332
+ fft_crop = fft
333
+ image= ndimage.maximum_filter(fft_crop, size=3, mode='wrap')
334
+
335
+
336
+
337
+
338
+ blobs_dog = blob_dog(image, max_sigma=sigmaMax,min_sigma=sigmaMin, threshold=thrs,sigma_ratio=sigma_ratio,exclude_border=True)
339
+ blobs_dog[:, 2] = blobs_dog[:, 2] * sqrt(2)
340
+
341
+ title = 'DP detection'
342
+
343
+ fig, axes = plt.subplots(1, 2, figsize=(10, 5), sharex=True, sharey=True)
344
+ ax = axes.ravel()
345
+
346
+ ax[0].set_title('FFT_')
347
+ ax[0].imshow(fft_crop)
348
+ # ax[0].set_axis_off()
349
+ ax[1].set_title(title)
350
+ ax[1].imshow(image)
351
+
352
+
353
+ plt.tight_layout()
354
+
355
+
356
+ data=np.zeros((len(blobs_dog),6))
357
+ data[:,0]=blobs_dog[:,1]
358
+ data[:,1]=blobs_dog[:,0]
359
+
360
+ for j in range(len(blobs_dog)):
361
+ y=data[j,1]-r
362
+ x=data[j,0]-r
363
+
364
+ data[j,2]=math.sqrt(x**2+y**2)
365
+ # if data[j,2] < 10:
366
+ # data[j,2]=0
367
+ # theta = 0
368
+ # else:
369
+ # theta=math.atan(y/x)
370
+ theta=math.atan(y/x)
371
+ data[j,3]=math.degrees(theta)
372
+
373
+ data[:,4]=data[:,2]*scale
374
+ data[:,5]=1/data[:,4]
375
+
376
+ plt.scatter(data[:,0],data[:,1],marker='+',color='red')
377
+ for i, (x_i, y_i) in enumerate(zip(data[:,0],data[:,1])):
378
+ plt.annotate(str(i+1), (x_i, y_i), textcoords="offset points", xytext=(0,10), ha='center',color='white')
379
+ pathoutcsv=pathout[:-4]+'.csv'
380
+ IO.writeCSV(pathoutcsv,data,fmt='%1.3f')
381
+ plt.savefig(pathout)