mirror of
https://github.com/c-sooyoung/lemon-sandbox.git
synced 2026-09-17 11:59:07 +09:00
2.8 MiB
2.8 MiB
In [1]:
import os
import numpy as np
# import abtem
# from ase.build import bulk, surface
# from scipy.special import erf
from scipy.ndimage import gaussian_filter
# import skimage
# from tifffile import imread
import kemstem
import tifffile
from scipy.spatial import cKDTree
from matplotlib import pyplot as plt, colors, colormaps, cm
from mpl_toolkits.axes_grid1.anchored_artists import AnchoredSizeBar
# from sklearn.cluster import KMeans
# from scipy.optimize import linear_sum_assigAent
# from scipy.spatial.distance import cdist
# from scipy.interpolate import griddata
# import imageio.v3 as iio
# %matplotlib inline
plt.rcParams['font.family'] = 'monospace'In [5]:
basedir = "/home/swim/Si_project/260707_BCAT_tif_file"
os.listdir(basedir)Out [5]:
['f_410_1_obj_phase_roi_Niter100.tiff', 'f_410_2_obj_phase_roi_Niter100.tiff', 'f_410_3_obj_phase_roi_Niter100.tiff', 'f_410_4_obj_phase_roi_Niter100.tiff', 'f_420_1_obj_phase_roi_Niter120.tiff', 'f_420_2_obj_phase_roi_Niter120.tiff', 'f_420_3_obj_phase_roi_Niter120.tiff', 'f_420_4_obj_phase_roi_Niter120.tiff', 'f_430_1_obj_phase_roi_Niter80.tiff', 'f_430_2_obj_phase_roi_Niter80.tiff', 'f_430_3_obj_phase_roi_Niter80.tiff', 'f_430_4_obj_phase_roi_Niter80.tiff', 'f_450_1_obj_phase_roi_Niter90.tiff', 'f_450_2_obj_phase_roi_Niter90.tiff', 'f_450_3_obj_phase_roi_Niter90.tiff', 'f_450_4_obj_phase_roi_Niter90.tiff', 'f_471_1_obj_phase_roi_Niter50.tiff', 'f_471_2_obj_phase_roi_Niter50.tiff', 'f_471_3_obj_phase_roi_Niter50.tiff', 'f_471_4_obj_phase_roi_Niter50.tiff', 'f_471_5_obj_phase_roi_Niter230.tiff', 'f_471_6_obj_phase_roi_Niter230.tiff', 'f_471_7_obj_phase_roi_Niter230.tiff', 'f_472_1_obj_phase_roi_Niter50.tiff', 'f_472_2_obj_phase_roi_Niter50.tiff', 'f_472_3_obj_phase_roi_Niter50.tiff', 'f_472_4_obj_phase_roi_Niter50.tiff']
In [6]:
tiff = 'f_410_3_obj_phase_roi_Niter100.tiff'
image_file = os.path.join(basedir, tiff)
layer = 16
image = tifffile.imread(image_file)[layer]
roi_A = 92
A_per_px = roi_A / image.shape[0] # A/pixel
Si_distance_px = 1.358 / A_per_px
print(f"Shape : {image.shape}")
print(f"Dtype : {image.dtype}")
print(f"H × W : {image.shape[0]} × {image.shape[1]}")
print(f"Pixel Size : {A_per_px:.2f} A/px")
fig, ax = plt.subplots(figsize=(7, 7))
ax.imshow(image, cmap='gray')
# ax.set_title(f"Raw Image ({image.shape[0]} × {image.shape[1]} px)")
# ax.axis('on') # 픽셀 좌표 눈금 표시
plt.tight_layout()
plt.show()Shape : (379, 379) Dtype : uint8 H × W : 379 × 379 Pixel Size : 0.24 A/px
In [7]:
# gaussian_filter는 find_columns 보조용으로만, refine은 image로
blurred_image = gaussian_filter(image, 1)
distance = 2 # 원자 간격 픽셀의 절반 정도로 조정
threshold = 0.5
c0 = kemstem.find_columns(blurred_image, distance=distance, threshold=threshold)
fig, ax = plt.subplots(1, 1, constrained_layout=True, figsize=(7, 7))
ax.imshow(image, cmap='gray') # matshow → imshow 통일
ax.axis('off')
ax.plot(c0[:, 1], c0[:, 0], 'r.', markersize=5)
print(f'{c0.shape[0]} columns found')1036 columns found
In [8]:
# # 1. 반드시 widget 모드로 설정 (VS Code/Jupyter Lab 권장)
# %matplotlib widget
# import matplotlib.pyplot as plt
# import numpy as np
# # 만약 widget 모드가 지원되지 않는 환경이라면 에러가 날 수 있습니다.
# # 그럴 경우 다시 알려주세요.
# class AtomEditor:
# def __init__(self, img, coords):
# self.img = img
# self.coords = list(coords)
# self.fig, self.ax = plt.subplots(figsize=(7, 7))
# self.ax.imshow(self.img, cmap='gray')
# # 초기 원자 표시 (데이터가 없을 경우를 대비해 빈 리스트 처리)
# x_init = [c[1] for c in self.coords] if self.coords else []
# y_init = [c[0] for c in self.coords] if self.coords else []
# self.points, = self.ax.plot(x_init, y_init, 'r.', markersize=3)
# self.ax.set_title("Left Click: Add (+) | Right Click: Remove (-)")
# # 클릭 이벤트 연결
# self.fig.canvas.mpl_connect('button_press_event', self.on_click)
# def on_click(self, event):
# # 마우스가 이미지 안에 있지 않으면 무시
# if event.inaxes != self.ax: return
# if event.button == 1: # 왼쪽 클릭: 추가
# self.coords.append([event.ydata, event.xdata])
# elif event.button == 3: # 오른쪽 클릭: 삭제
# if len(self.coords) == 0: return
# # 클릭한 지점에서 가장 가까운 원자 찾기
# dists = np.sqrt([(c[0]-event.ydata)**2 + (c[1]-event.xdata)**2 for c in self.coords])
# closest_idx = np.argmin(dists)
# if dists[closest_idx] < 15: # 15픽셀 이내일 때만 삭제 (너무 멀면 실수 방지)
# self.coords.pop(closest_idx)
# self.update_plot()
# def update_plot(self):
# # 좌표 업데이트 후 즉시 다시 그리기
# if len(self.coords) > 0:
# y, x = zip(*self.coords)
# self.points.set_data(x, y)
# else:
# self.points.set_data([], [])
# # 화면 강제 갱신
# self.fig.canvas.draw_idle()
# # 실행 (기존에 찾은 원자 좌표 c0 전달)
# editor = AtomEditor(blurred_image, c0)In [9]:
# # 에디터 객체(editor) 내부에 저장된 리스트를 numpy 배열로 변환하여 c0에 덮어쓰기
# c0 = np.array(editor.coords)
# print(f"✅ 수동 수정 후 c0 개수: {len(c0)}개")
# %matplotlib inline
# plt.close()
# # 여기서 개수가 늘어난 것을 확인했다면 성공입니다.In [10]:
test_it = 100
window_dim = 5
cf,errs,opts,data_fits = kemstem.refine_columns(blurred_image,c0[test_it,:],window_dim)
visualization_window = 5*window_dim
fig,ax = plt.subplots(2,2,constrained_layout=True)
ax[0,0].matshow(data_fits[:,:,0,0],cmap='gray')
ax[0,1].matshow(data_fits[:,:,0,1],cmap='gray')
ax[1,0].matshow(blurred_image,cmap='gray')
ax[1,0].plot(c0[test_it,1],c0[test_it,0],'b.') # original unfit position shown in blue
ax[1,0].plot(cf[0,1],cf[0,0],'r.') # fit position shown in red
ax[1,0].set_xlim([c0[test_it,1]-visualization_window,c0[test_it,1]+visualization_window])
ax[1,0].set_ylim([c0[test_it,0]+visualization_window,c0[test_it,0]-visualization_window])
ax[1,1].matshow(data_fits[:,:,0,0]-data_fits[:,:,0,1],cmap='gray',vmin=-.1,vmax=.1) # fit residual
_ = [tax.axis('off') for tax in ax.ravel()]100%|██████████| 1/1 [00:00<00:00, 104.77it/s]
In [11]:
cf, errs, opts, data_fits = kemstem.refine_columns(image, c0, window_dim)100%|██████████| 1036/1036 [00:03<00:00, 302.79it/s]
In [12]:
fig, ax = plt.subplots(1, 1, constrained_layout=True, figsize=(7, 7))
ax.imshow(image, cmap='gray') # matshow → imshow
ax.plot(cf[:, 1], cf[:, 0], 'r.', markersize=3)
ax.axis('off')
plt.show()In [13]:
atoms_xy = cf[:,::-1]
tree = cKDTree(atoms_xy)
dists, inds = tree.query(atoms_xy, k=7)
nearest_distances = dists[:, 1:].ravel()
plt.figure(figsize=(6, 4))
plt.hist(nearest_distances[nearest_distances < 12], bins=80)
plt.xlim(0, 12)
plt.xlabel("peak-to-peak distance [pixels]")
plt.ylabel("count")
plt.title("Neighbour distance histogram")
plt.show()In [14]:
# Cell 7
# Choose a dumbbell-pairing distance window in pixels.
# For a real image, inspect the histogram above and set these manually.
d_min = 4
d_max = 6
# Cell 8
# Make all candidate close pairs within the dumbbell distance window.
pairs = []
for i, p in enumerate(atoms_xy):
neighbours = tree.query_ball_point(p, r=d_max)
for j in neighbours:
if j <= i:
continue
q = atoms_xy[j]
d = np.linalg.norm(q - p)
if d_min <= d <= d_max:
pairs.append((i, j, d))
pairs = np.array(pairs, dtype=float)
len(pairs)Out [14]:
499
In [15]:
# Cell 9
# Greedy pairing:
# sort candidate pairs by distance and assign each atom to at most one dumbbell.
pairs_sorted = pairs[np.argsort(pairs[:, 2])]
used = set()
dumbbells = []
for i, j, d in pairs_sorted:
i = int(i)
j = int(j)
if i in used or j in used:
continue
dumbbells.append((i, j, d))
used.add(i)
used.add(j)
dumbbells = np.array(dumbbells, dtype=float)
len(dumbbells)Out [15]:
499
In [16]:
# Cell 10
# Extract dumbbell geometry:
# center, vector, length, and angle.
dumbbell_centers = []
dumbbell_vectors = []
dumbbell_lengths = []
dumbbell_angles = []
for i, j, d in dumbbells:
i = int(i)
j = int(j)
p = cf[i]
q = cf[j]
center = 0.5 * (p + q)
vector = q - p
length = np.linalg.norm(vector)
angle = np.degrees(np.arctan2(vector[1], vector[0]))
dumbbell_centers.append(center)
dumbbell_vectors.append(vector)
dumbbell_lengths.append(length)
dumbbell_angles.append(angle)
dumbbell_centers = np.array(dumbbell_centers)
dumbbell_vectors = np.array(dumbbell_vectors)
dumbbell_lengths = np.array(dumbbell_lengths)
dumbbell_angles = np.array(dumbbell_angles)In [17]:
# Cell 11
plt.figure(figsize=(7, 7))
plt.imshow(image, cmap="gray")
# plt.scatter(
# atoms_xy[:, 0],
# atoms_xy[:, 1],
# s=20,
# facecolors="none",
# edgecolors="r"
# )
for i, j, d in dumbbells:
i = int(i)
j = int(j)
p = atoms_xy[i]
q = atoms_xy[j]
plt.plot(
[p[0], q[0]],
[p[1], q[1]],
'ro-',
linewidth=1,
markersize=2
)
plt.axis("off")
plt.title("Paired Si [110] dumbbells")
plt.show()In [18]:
# Cell 11
# Histogram of dumbbell distances.
# Inspect this plot, then choose vmin and vmax manually in the next cell.
dumbbell_distances = dumbbells[:, 2]
plt.figure(figsize=(6, 4), dpi=100)
plt.hist(dumbbell_distances, bins=30, color='#aaa')
# xticks, xticklabels = plt.xticks()
# plt.xticks(xticks, np.round(xticks * A_per_px, 2))
plt.xlabel("dumbbell distance [px]")
plt.ylabel("count")
# plt.title("Histogram of Si [110] dumbbell distances")
vmin = 5.2
vmax = 5.8
plt.axvline(vmin, color='k', linestyle='--', label=f'vmin = {vmin:.2f}')
plt.axvline(vmax, color='k', linestyle='--', label=f'vmax = {vmax:.2f}')
plt.axvline(Si_distance_px, color='red', linestyle='-', label=f'vmax = {vmax:.2f}')
plt.show()In [19]:
# plt.figure(figsize=(4, 4), dpi=100)
# plt.hist(dumbbell_distances, bins=30, color='#aaa')
# xticks, xticklabels = plt.xticks()
# plt.xticks(xticks, np.round(xticks * A_per_px, 2))
# plt.xlabel("dumbbell distance [Å]")
# plt.ylabel("count")
# plt.axvline(vmin, color=plt.cm.viridis.colors[0], linestyle='--', label=f'vmin = {vmin:.2f}')
# plt.axvline(vmax, color=plt.cm.viridis.colors[-1], linestyle='--', label=f'vmax = {vmax:.2f}')
# plt.axvline(Si_distance_px, color='red', linestyle='-', label=f'vmax = {vmax:.2f}')
# plt.show()In [27]:
# 1. Create the figure and axes
fig, axs = plt.subplots(2, 1, dpi=300)
ax1, ax2 = axs
norm = colors.Normalize(vmin=vmin, vmax=vmax, clip=True)
cmap = colormaps['PiYG']
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
# Keep the original 1:1 aspect ratio for the image
ax1.imshow(image, cmap="gray", alpha=.7)
ax1.axis('off')
for i, j, d in dumbbells:
i, j = int(i), int(j)
p, q = atoms_xy[i], atoms_xy[j]
ax1.plot([p[0], q[0]], [p[1], q[1]], 'o-', linewidth=.5, markersize=1, color=cmap(norm(d)))
# Plot the histogram
ax2.hist(dumbbell_distances, bins=50, color='#bbb')
ax2.axvline(Si_distance_px, color='k', linewidth=1)
ax2.set_xlim(vmin, vmax)
# Turn off the histogram's x-axis ticks/labels since the colorbar replaces it
ax2.tick_params(axis='x', which='both', bottom=False, labelbottom=False)
# 2. Force layout geometry matching
plt.tight_layout() # Initial pass to establish image bounds based on 1:1 aspect
fig.canvas.draw() # Evaluate positions
# Get the physical layout position of the image channel
pos1 = ax1.get_position()
pos2 = ax2.get_position()
# Adjust the histogram height here (e.g., matching 60% of its original height)
new_hist_height = pos2.height * 0.5
# Update ax2 to match ax1's horizontal span exactly, with your custom height
ax2.set_position([pos1.x0, pos2.y0 + (pos2.height - new_hist_height), pos1.width, new_hist_height])
# 3. Add the colorbar exactly flush beneath the modified histogram
# [left, bottom, width, height] relative to ax2
cax = ax2.inset_axes([0, -0.1, 1, 0.1], transform=ax2.transAxes)
cbar = fig.colorbar(sm, cax=cax, orientation='horizontal')
cbar.set_ticks(cbar.get_ticks()) # type: ignore
cbar.set_ticklabels(np.round(A_per_px * cbar.get_ticks(), 2))
cbar.ax.axvline(Si_distance_px, color='k', linewidth=1)
cbar.ax.set_xlabel('Dumbbell distance [Å]')
cbar.ax.set_xlim(vmin, vmax)
sbar_length_A = 10
sbar_length_px = sbar_length_A // A_per_px
scalebar = AnchoredSizeBar(
ax1.transData,
sbar_length_px, # Size of the scale bar in data units (pixels)
f'{sbar_length_A} Å', # Text label
loc='lower right', # Position (e.g., 'lower right', 'lower left')
pad=1, # Padding inside the box
color='white', # Color of the bar and text
frameon=False, # Remove background box boundary
size_vertical=sbar_length_px * 0.1, # Thickness of the scale bar line
label_top=True,
fontproperties={'weight': 'normal', 'size': 8} # Optional: fine-tune text style/size
)
ax1.add_artist(scalebar)
# scale_bar_A = 10
# scale_bar_px = scale_bar_A // A_per_px
# scale_bar_start_px = 30
# scale_bar_x = [scale_bar_start_px, scale_bar_start_px+scale_bar_px]
# scale_bar_y = [350] * 2
# sbar = ax1.plot(scale_bar_x, scale_bar_y, color='black', linewidth=4)
# sbar.text(scale_bar_start_px, 340, "10 Å", size=10)
plt.show()In [ ]: