import numpy as np
import json
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from scipy import ndimage, optimize, signal
from skimage.feature import peak_local_max
from skimage.filters import window
import warnings
warnings.filterwarnings('ignore')

# 1. Load and normalize image
path = '/Users/maxim.ziatdinov/Code/SciLink/analysis_session_20260331_155336/results/analysis_YBCO_tem_ImageAnalysis_20260331_155505_001/image_0000/temp_image_0.npy'
img_raw = np.load(path)
print(f"Image shape: {img_raw.shape}, dtype: {img_raw.dtype}")
print(f"Intensity range: [{img_raw.min()}, {img_raw.max()}]")

# Normalize to float [0,1]
img = (img_raw.astype(np.float64) - img_raw.min()) / (img_raw.max() - img_raw.min())
H, W = img.shape

# 2. Compute FFT and identify dominant lattice peaks
# Apply window to reduce spectral leakage
win = window('hann', (H, W))
img_windowed = (img - img.mean()) * win
fft2 = np.fft.fft2(img_windowed)
fft_shift = np.fft.fftshift(fft2)
magnitude = np.abs(fft_shift)
log_magnitude = np.log1p(magnitude)

# Create frequency grids
fy = np.fft.fftshift(np.fft.fftfreq(H))  # cycles/pixel
fx = np.fft.fftshift(np.fft.fftfreq(W))
FX, FY = np.meshgrid(fx, fy)
freq_radius = np.sqrt(FX**2 + FY**2)

# Find peaks in FFT magnitude (exclude DC and very low frequencies)
# Mask out DC region
mag_for_peaks = log_magnitude.copy()
center_y, center_x = H // 2, W // 2
mask_dc = freq_radius < 0.01  # exclude very low frequencies
mag_for_peaks[mask_dc] = 0

# Also exclude very high frequencies (likely noise)
mask_high = freq_radius > 0.15
mag_for_peaks[mask_high] = 0

# Find peaks in FFT
fft_peaks = peak_local_max(mag_for_peaks, min_distance=5, num_peaks=20, threshold_rel=0.3)

# Get frequency coordinates and magnitudes for each peak
peak_info = []
for py_idx, px_idx in fft_peaks:
    freq_y = FY[py_idx, px_idx]
    freq_x = FX[py_idx, px_idx]
    freq_r = freq_radius[py_idx, px_idx]
    mag_val = magnitude[py_idx, px_idx]
    peak_info.append({
        'pixel': (py_idx, px_idx),
        'freq': (freq_x, freq_y),
        'freq_radius': freq_r,
        'magnitude': mag_val,
        'period_pixels': 1.0 / freq_r if freq_r > 0 else np.inf
    })

# Sort by magnitude
peak_info.sort(key=lambda x: x['magnitude'], reverse=True)

print(f"\nTop FFT peaks:")
for i, p in enumerate(peak_info[:10]):
    print(f"  Peak {i}: freq=({p['freq'][0]:.4f}, {p['freq'][1]:.4f}), "
          f"|f|={p['freq_radius']:.4f} cy/px, period={p['period_pixels']:.1f} px, mag={p['magnitude']:.0f}")

# Identify the two fundamental lattice vectors from FFT peaks
# Group peaks by Friedel pairs and find fundamental vectors
# Take the strongest peaks that are not collinear
def angle_between(v1, v2):
    cos_a = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-12)
    return np.degrees(np.arccos(np.clip(cos_a, -1, 1)))

# Filter to only one hemisphere (avoid Friedel pairs)
half_peaks = []
for p in peak_info:
    fx_val, fy_val = p['freq']
    # Keep peaks in upper half-plane, or on positive x-axis
    if fy_val > 0.001 or (abs(fy_val) < 0.001 and fx_val > 0.001):
        half_peaks.append(p)

print(f"\nHalf-plane peaks: {len(half_peaks)}")
for i, p in enumerate(half_peaks[:6]):
    print(f"  Peak {i}: freq=({p['freq'][0]:.4f}, {p['freq'][1]:.4f}), "
          f"period={p['period_pixels']:.1f} px, mag={p['magnitude']:.0f}")

# Select two strongest non-collinear peaks as lattice vectors
lattice_vectors_freq = []
if len(half_peaks) >= 1:
    lattice_vectors_freq.append(half_peaks[0])
    for p in half_peaks[1:]:
        if len(lattice_vectors_freq) >= 2:
            break
        v1 = np.array(lattice_vectors_freq[0]['freq'])
        v2 = np.array(p['freq'])
        ang = angle_between(v1, v2)
        if ang > 15 and ang < 165:  # not collinear
            lattice_vectors_freq.append(p)
            break

lattice_params = {}
if len(lattice_vectors_freq) >= 2:
    v1_freq = np.array(lattice_vectors_freq[0]['freq'])
    v2_freq = np.array(lattice_vectors_freq[1]['freq'])
    
    # Lattice vectors in real space (pixels)
    # Real space lattice vector magnitude = 1/|freq|
    a1_pixels = lattice_vectors_freq[0]['period_pixels']
    a2_pixels = lattice_vectors_freq[1]['period_pixels']
    
    # Angles of lattice vectors
    angle1 = np.degrees(np.arctan2(v1_freq[1], v1_freq[0]))
    angle2 = np.degrees(np.arctan2(v2_freq[1], v2_freq[0]))
    lattice_angle = abs(angle2 - angle1)
    if lattice_angle > 180:
        lattice_angle = 360 - lattice_angle
    
    # Assuming pixel size ~ 0.035 nm/pixel for YBCO (approximate, typical for STEM)
    # YBCO a~3.82A, b~3.89A, c~11.68A
    # We'll report in pixels and estimate nm
    pixel_size_nm = 0.035  # approximate, will note in summary
    a1_nm = a1_pixels * pixel_size_nm
    a2_nm = a2_pixels * pixel_size_nm
    
    lattice_params = {
        'a1_pixels': float(a1_pixels),
        'a2_pixels': float(a2_pixels),
        'a1_nm_approx': float(a1_nm),
        'a2_nm_approx': float(a2_nm),
        'lattice_angle_deg': float(lattice_angle),
        'a1_freq_direction_deg': float(angle1),
        'a2_freq_direction_deg': float(angle2),
    }
    print(f"\nLattice parameters:")
    print(f"  a1 = {a1_pixels:.1f} px ({a1_nm:.2f} nm approx)")
    print(f"  a2 = {a2_pixels:.1f} px ({a2_nm:.2f} nm approx)")
    print(f"  angle = {lattice_angle:.1f} deg")
    
    expected_freq = 1.0 / ((a1_pixels + a2_pixels) / 2.0)
else:
    # Fallback
    expected_freq = 0.04
    lattice_params = {'error': 'Could not determine two lattice vectors'}
    a1_pixels = 25
    a2_pixels = 25

# 3. Bandpass filter in Fourier space
# Pass frequencies between 0.5x and 1.5x expected lattice frequency
min_lattice_period = min(a1_pixels, a2_pixels) if len(lattice_vectors_freq) >= 2 else 20
max_lattice_period = max(a1_pixels, a2_pixels) if len(lattice_vectors_freq) >= 2 else 30

# Frequency bounds for bandpass
f_low = 0.5 / max_lattice_period   # lower cutoff (pass above this)
f_high = 1.5 / min_lattice_period  # upper cutoff (pass below this)

print(f"\nBandpass filter: f_low={f_low:.4f}, f_high={f_high:.4f} cycles/pixel")
print(f"  Corresponding to periods: {1/f_high:.1f} - {1/f_low:.1f} pixels")

# Create bandpass filter with smooth edges (Butterworth-like)
def butterworth_bandpass(freq_r, f_low, f_high, order=4):
    # High-pass component
    hp = 1.0 / (1.0 + (f_low / (freq_r + 1e-10))**(2*order))
    # Low-pass component  
    lp = 1.0 / (1.0 + (freq_r / f_high)**(2*order))
    return hp * lp

bp_filter = butterworth_bandpass(freq_radius, f_low, f_high, order=4)

# Apply bandpass filter
fft_filtered = fft_shift * bp_filter
img_filtered = np.real(np.fft.ifft2(np.fft.ifftshift(fft_filtered)))

# Normalize filtered image
img_filtered_norm = (img_filtered - img_filtered.min()) / (img_filtered.max() - img_filtered.min() + 1e-10)

print(f"Filtered image range: [{img_filtered.min():.4f}, {img_filtered.max():.4f}]")

# 4. Detect atomic column positions using peak_local_max
# min_distance should be safely below nearest-neighbor spacing
min_dist = max(int(min_lattice_period * 0.4), 5)  # ~40% of smallest lattice parameter
print(f"\nPeak detection min_distance: {min_dist} pixels")

# Use bandpass filtered image for detection
coords = peak_local_max(
    img_filtered_norm,
    min_distance=min_dist,
    threshold_abs=0.15,  # adjusted threshold
    exclude_border=5
)

print(f"Initial peak detection: {len(coords)} columns found")

# If too few or too many, adjust threshold
expected_count_per_axis = H / ((a1_pixels + a2_pixels) / 2.0)
expected_count = expected_count_per_axis ** 2
print(f"Expected column count (rough): {expected_count:.0f}")

if len(coords) < expected_count * 0.3:
    print("Too few detections, lowering threshold...")
    coords = peak_local_max(
        img_filtered_norm,
        min_distance=min_dist,
        threshold_abs=0.05,
        exclude_border=5
    )
    print(f"After adjustment: {len(coords)} columns")
elif len(coords) > expected_count * 3:
    print("Too many detections, raising threshold...")
    coords = peak_local_max(
        img_filtered_norm,
        min_distance=min_dist,
        threshold_abs=0.3,
        exclude_border=5
    )
    print(f"After adjustment: {len(coords)} columns")

# 5. Sub-pixel refinement via 2D Gaussian fitting
def gaussian_2d(xy, amplitude, x0, y0, sigma_x, sigma_y, offset):
    x, y = xy
    g = offset + amplitude * np.exp(
        -((x - x0)**2 / (2 * sigma_x**2) + (y - y0)**2 / (2 * sigma_y**2))
    )
    return g.ravel()

refined_positions = []
fit_qualities = []
window_size = 7  # 7x7 window for Gaussian fit
half_w = window_size // 2

for i, (cy, cx) in enumerate(coords):
    # Extract window from original normalized image
    y_min = max(0, cy - half_w)
    y_max = min(H, cy + half_w + 1)
    x_min = max(0, cx - half_w)
    x_max = min(W, cx + half_w + 1)
    
    patch = img[y_min:y_max, x_min:x_max]
    
    if patch.shape[0] < 5 or patch.shape[1] < 5:
        refined_positions.append([float(cy), float(cx)])
        fit_qualities.append(0.0)
        continue
    
    yy, xx = np.meshgrid(
        np.arange(patch.shape[1]),
        np.arange(patch.shape[0])
    )
    
    # Initial guess
    amp_guess = patch.max() - patch.min()
    x0_guess = cx - x_min
    y0_guess = cy - y_min
    sig_guess = 2.0
    off_guess = patch.min()
    
    try:
        popt, pcov = optimize.curve_fit(
            gaussian_2d,
            (xx.ravel(), yy.ravel()),
            patch.ravel(),
            p0=[amp_guess, x0_guess, y0_guess, sig_guess, sig_guess, off_guess],
            bounds=(
                [0, -1, -1, 0.5, 0.5, 0],
                [2*amp_guess+0.01, patch.shape[0]+1, patch.shape[1]+1, half_w, half_w, 1]
            ),
            maxfev=500
        )
        
        refined_y = y_min + popt[1]
        refined_x = x_min + popt[2]
        
        # Check if refined position is reasonable
        if abs(refined_y - cy) < half_w and abs(refined_x - cx) < half_w:
            refined_positions.append([float(refined_y), float(refined_x)])
            # R-squared as quality metric
            fitted = gaussian_2d((xx.ravel(), yy.ravel()), *popt).reshape(patch.shape)
            ss_res = np.sum((patch - fitted)**2)
            ss_tot = np.sum((patch - patch.mean())**2) + 1e-10
            r_squared = 1 - ss_res / ss_tot
            fit_qualities.append(float(r_squared))
        else:
            refined_positions.append([float(cy), float(cx)])
            fit_qualities.append(0.0)
    except (RuntimeError, ValueError):
        refined_positions.append([float(cy), float(cx)])
        fit_qualities.append(0.0)

refined_positions = np.array(refined_positions)
fit_qualities = np.array(fit_qualities)

print(f"\nRefined {len(refined_positions)} atomic column positions")
print(f"Mean Gaussian fit R²: {fit_qualities.mean():.3f} (median: {np.median(fit_qualities):.3f})")

# 6. Compute nearest-neighbor distances
from scipy.spatial import KDTree

if len(refined_positions) > 1:
    tree = KDTree(refined_positions)
    # Query k=7 nearest neighbors (skip self)
    k_neighbors = min(7, len(refined_positions))
    distances, indices = tree.query(refined_positions, k=k_neighbors)
    
    # Nearest neighbor distance (first non-zero)
    nn_distances = distances[:, 1]  # distance to closest neighbor
    
    nn_mean = float(np.mean(nn_distances))
    nn_std = float(np.std(nn_distances))
    nn_median = float(np.median(nn_distances))
    
    print(f"\nNearest-neighbor distances:")
    print(f"  Mean: {nn_mean:.2f} px")
    print(f"  Std: {nn_std:.2f} px")
    print(f"  Median: {nn_median:.2f} px")
    
    # All distances to k nearest neighbors for histogram
    all_nn = distances[:, 1:].ravel()
else:
    nn_mean = 0.0
    nn_std = 0.0
    nn_median = 0.0
    nn_distances = np.array([])
    all_nn = np.array([])

# 7. Compare detected count to expected
image_area_px2 = H * W
if len(lattice_vectors_freq) >= 2:
    unit_cell_area = a1_pixels * a2_pixels * np.sin(np.radians(lattice_params.get('lattice_angle_deg', 90)))
    # YBCO has multiple atoms per unit cell, but for column count estimation
    # assume ~2-4 columns per unit cell in projection
    atoms_per_cell_est = 2  # rough estimate for YBCO projected
    expected_from_area = (image_area_px2 / unit_cell_area) * atoms_per_cell_est
else:
    unit_cell_area = 0
    expected_from_area = 0

print(f"\nColumn count: {len(refined_positions)}")
print(f"Expected from area: {expected_from_area:.0f} (rough, assumes {atoms_per_cell_est} cols/cell)")

# 8. Extract intensity at each column position from original image
column_intensities_raw = []
for (cy, cx) in refined_positions:
    iy, ix = int(round(cy)), int(round(cx))
    iy = np.clip(iy, 0, H-1)
    ix = np.clip(ix, 0, W-1)
    # Average over small neighborhood (3x3)
    y_lo = max(0, iy-1)
    y_hi = min(H, iy+2)
    x_lo = max(0, ix-1)
    x_hi = min(W, ix+2)
    column_intensities_raw.append(float(np.mean(img_raw[y_lo:y_hi, x_lo:x_hi])))

column_intensities_raw = np.array(column_intensities_raw)
column_intensities_norm = (column_intensities_raw - column_intensities_raw.min()) / \
                          (column_intensities_raw.max() - column_intensities_raw.min() + 1e-10)

print(f"\nColumn intensities (raw):")
print(f"  Mean: {column_intensities_raw.mean():.0f}")
print(f"  Std: {column_intensities_raw.std():.0f}")
print(f"  Min: {column_intensities_raw.min():.0f}, Max: {column_intensities_raw.max():.0f}")

# Intensity-based classification using K-means clustering
from sklearn.cluster import KMeans

if len(column_intensities_raw) > 10:
    # Try 2 and 3 clusters, pick best by silhouette score
    from sklearn.metrics import silhouette_score
    
    best_n = 2
    best_score = -1
    intensities_for_clustering = column_intensities_raw.reshape(-1, 1)
    
    for n_clusters in [2, 3, 4]:
        if n_clusters >= len(column_intensities_raw):
            continue
        km = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
        labels = km.fit_predict(intensities_for_clustering)
        if len(np.unique(labels)) > 1:
            score = silhouette_score(intensities_for_clustering, labels)
            print(f"  K={n_clusters}: silhouette={score:.3f}")
            if score > best_score:
                best_score = score
                best_n = n_clusters
    
    km_final = KMeans(n_clusters=best_n, random_state=42, n_init=10)
    column_labels = km_final.fit_predict(intensities_for_clustering)
    cluster_centers = km_final.cluster_centers_.ravel()
    
    # Sort clusters by intensity
    sorted_idx = np.argsort(cluster_centers)
    label_map_km = np.zeros_like(column_labels)
    for new_label, old_label in enumerate(sorted_idx):
        label_map_km[column_labels == old_label] = new_label
    column_labels = label_map_km
    cluster_centers = cluster_centers[sorted_idx]
    
    classification_info = {
        'n_classes': int(best_n),
        'silhouette_score': float(best_score),
        'cluster_centers_raw_intensity': [float(c) for c in cluster_centers],
        'counts_per_class': [int(np.sum(column_labels == i)) for i in range(best_n)]
    }
    print(f"\nIntensity classification: {best_n} classes")
    for i in range(best_n):
        n_in_class = np.sum(column_labels == i)
        print(f"  Class {i}: center={cluster_centers[i]:.0f}, count={n_in_class}")
else:
    column_labels = np.zeros(len(refined_positions), dtype=int)
    classification_info = {'n_classes': 1, 'note': 'Too few columns for clustering'}

# 9. Generate visualizations
fig, axes = plt.subplots(2, 3, figsize=(18, 12), dpi=100)

# 9a. Original image
ax = axes[0, 0]
ax.imshow(img, cmap='gray')
ax.set_title('Original Image (normalized)', fontsize=11)
ax.axis('off')

# 9b. FFT with peak annotations
ax = axes[0, 1]
vmax = np.percentile(log_magnitude, 99.5)
ax.imshow(log_magnitude, cmap='hot', vmin=0, vmax=vmax)
if len(fft_peaks) > 0:
    ax.plot(fft_peaks[:, 1], fft_peaks[:, 0], 'c+', markersize=8, markeredgewidth=1.5)
# Annotate lattice vectors
for j, lv in enumerate(lattice_vectors_freq[:2]):
    py, px = lv['pixel']
    ax.plot(px, py, 'go', markersize=10, markeredgewidth=2, fillstyle='none')
    ax.annotate(f'a{j+1}={lv["period_pixels"]:.1f}px', (px+3, py-3), color='lime', fontsize=8)
ax.set_title('FFT (log magnitude) + Peaks', fontsize=11)
ax.axis('off')

# 9c. Detected columns overlaid on image
ax = axes[0, 2]
ax.imshow(img, cmap='gray')
if len(refined_positions) > 0:
    colors_for_labels = plt.cm.tab10(column_labels / max(column_labels.max(), 1))
    ax.scatter(refined_positions[:, 1], refined_positions[:, 0],
              c=column_labels, cmap='tab10', s=15, alpha=0.8, edgecolors='white', linewidths=0.3)
ax.set_title(f'Detected Columns (n={len(refined_positions)})', fontsize=11)
ax.axis('off')

# 9d. Bandpass filtered image
ax = axes[1, 0]
ax.imshow(img_filtered_norm, cmap='gray')
ax.set_title('Bandpass Filtered Image', fontsize=11)
ax.axis('off')

# 9e. Nearest-neighbor distance histogram
ax = axes[1, 1]
if len(all_nn) > 0:
    ax.hist(all_nn, bins=50, color='steelblue', edgecolor='black', alpha=0.7)
    ax.axvline(nn_mean, color='red', linestyle='--', label=f'NN mean={nn_mean:.1f} px')
    ax.axvline(nn_median, color='orange', linestyle='--', label=f'NN median={nn_median:.1f} px')
    ax.set_xlabel('Distance (pixels)', fontsize=10)
    ax.set_ylabel('Count', fontsize=10)
    ax.legend(fontsize=9)
ax.set_title('Neighbor Distances (k=6 NN)', fontsize=11)

# 9f. Intensity histogram of columns
ax = axes[1, 2]
if len(column_intensities_raw) > 0:
    ax.hist(column_intensities_raw, bins=40, color='coral', edgecolor='black', alpha=0.7)
    if 'cluster_centers_raw_intensity' in classification_info:
        for ci, center in enumerate(classification_info['cluster_centers_raw_intensity']):
            ax.axvline(center, color=plt.cm.tab10(ci/10), linestyle='--', linewidth=2,
                      label=f'Class {ci}: {center:.0f}')
    ax.set_xlabel('Raw Intensity', fontsize=10)
    ax.set_ylabel('Count', fontsize=10)
    ax.legend(fontsize=9)
ax.set_title('Column Intensity Distribution', fontsize=11)

plt.tight_layout()
plt.savefig('analysis_visualization.png', dpi=100, bbox_inches='tight')
plt.close()
print("Saved analysis_visualization.png")

# Save output arrays
# Positions array: [N, 4] -> (y, x, intensity, label)
if len(refined_positions) > 0:
    output_array = np.column_stack([
        refined_positions,  # y, x
        column_intensities_raw,  # raw intensity
        column_labels  # class label
    ])
else:
    output_array = np.empty((0, 4))

np.save('atomic_column_positions.npy', output_array)
print(f"Saved atomic_column_positions.npy: shape={output_array.shape}")

# Save bandpass filtered image
np.save('bandpass_filtered_image.npy', img_filtered_norm.astype(np.float32))
print(f"Saved bandpass_filtered_image.npy")

# Compute lattice vector angles for feature extraction
lattice_vector_angles = {}
if len(lattice_vectors_freq) >= 2:
    lattice_vector_angles = {
        'a1_angle_deg': float(lattice_params.get('a1_freq_direction_deg', 0)),
        'a2_angle_deg': float(lattice_params.get('a2_freq_direction_deg', 0)),
        'inter_vector_angle_deg': float(lattice_params.get('lattice_angle_deg', 0))
    }

# Build results JSON
results = {
    "analysis_type": "STEM atomic column detection and lattice analysis via FFT-based lattice determination, bandpass filtering, peak finding, and 2D Gaussian sub-pixel refinement",
    "extracted_features": {
        "lattice_parameters_from_FFT": lattice_params,
        "atomic_column_positions": {
            "count": int(len(refined_positions)),
            "first_5_yx": refined_positions[:5].tolist() if len(refined_positions) > 0 else []
        },
        "column_count": int(len(refined_positions)),
        "nearest_neighbor_distances_mean_and_std": {
            "mean_px": float(nn_mean),
            "std_px": float(nn_std),
            "median_px": float(nn_median)
        },
        "column_intensity_distribution": {
            "mean_raw": float(column_intensities_raw.mean()) if len(column_intensities_raw) > 0 else 0,
            "std_raw": float(column_intensities_raw.std()) if len(column_intensities_raw) > 0 else 0,
            "min_raw": float(column_intensities_raw.min()) if len(column_intensities_raw) > 0 else 0,
            "max_raw": float(column_intensities_raw.max()) if len(column_intensities_raw) > 0 else 0
        },
        "intensity_based_column_classification": classification_info,
        "lattice_vector_angles": lattice_vector_angles
    },
    "quality_metrics": {
        "gaussian_fit_r_squared_mean": float(fit_qualities.mean()) if len(fit_qualities) > 0 else 0,
        "gaussian_fit_r_squared_median": float(np.median(fit_qualities)) if len(fit_qualities) > 0 else 0,
        "fraction_good_fits": float(np.mean(fit_qualities > 0.5)) if len(fit_qualities) > 0 else 0,
        "nn_distance_cv": float(nn_std / nn_mean) if nn_mean > 0 else 0,
        "expected_vs_detected_ratio": float(len(refined_positions) / expected_from_area) if expected_from_area > 0 else 0,
        "bandpass_freq_range_cycles_per_pixel": [float(f_low), float(f_high)],
        "peak_detection_min_distance_px": int(min_dist)
    },
    "summary": (f"Detected {len(refined_positions)} atomic columns in YBCO STEM image. "
                f"FFT analysis reveals lattice periods of {lattice_params.get('a1_pixels', 'N/A'):.1f} and "
                f"{lattice_params.get('a2_pixels', 'N/A'):.1f} pixels with inter-vector angle "
                f"{lattice_params.get('lattice_angle_deg', 'N/A'):.1f} deg. "
                f"Mean nearest-neighbor distance: {nn_mean:.2f} +/- {nn_std:.2f} px. "
                f"Intensity clustering identified {classification_info.get('n_classes', '?')} column classes "
                f"(silhouette={classification_info.get('silhouette_score', 'N/A')}). "
                f"Pixel size assumed ~0.035 nm/px for approximate nm conversions. "
                f"Bandpass filter range: {f_low:.4f}-{f_high:.4f} cy/px. "
                f"Gaussian fit window: 7x7, min_distance for peak detection: {min_dist} px."),
    "saved_arrays": {
        "atomic_column_positions.npy": {
            "description": f"Atomic column positions array with columns [y_px, x_px, raw_intensity, class_label], {len(refined_positions)} columns detected",
            "shape": list(output_array.shape),
            "dtype": str(output_array.dtype)
        },
        "bandpass_filtered_image.npy": {
            "description": "Bandpass filtered image (normalized float32) used for peak detection",
            "shape": [H, W],
            "dtype": "float32"
        }
    }
}

print(f"IMAGE_ANALYSIS_RESULTS_JSON:{json.dumps(results)}")
