מודול 8: ניתוח קווי מתאר — ניתוח פורייה אליפטי
שבוע 8 — קווי מתאר ותקופת הברונזה
כשציוני דרך אינם מספיקים
ניתוח המטבעות שלנו השתמש בציוני דרך בדידים — נקודות ניתנות לזיהוי על הדיוקן. אך לחפצים ארכאולוגיים רבים אין נקודות ציון ברורות. חשבו על גרזן ברונזה עם כנפיים: קו מתארו הוא עקומה חלקה ללא נקודות “הומולוגיות” ביולוגית או ארכאולוגית לאורך הקצה.
עבור חפצים כאלה, אנחנו משתמשים בניתוח קווי מתאר — והשיטה הנפוצה ביותר היא ניתוח פורייה אליפטי (EFA).
הרעיון המרכזי: פירוק צורה
EFA מתייחס לקו מתאר כפונקציה מתמטית ומפרק אותו לסכום של רכיבים פשוטים יותר — הרמוניות אליפטיות — בדיוק כשם שאקורד מוזיקלי מורכב ניתן לפירוק לגלי סינוס בודדים (ניתוח פורייה).
האנלוגיה המוזיקלית
- תו מוזיקלי הוא גל מורכב
- ניתוח פורייה מפרק אותו לגלי סינוס טהורים בתדרים שונים
- תדרים נמוכים = האופי הכולל של הצליל
- תדרים גבוהים = פרטים עדינים ומרקם
לקווי מתאר: - הרמוניה 1 = הצורה האליפטית הכללית (מאורך לעומת עגול) - הרמוניות 2–4 = תכונות פרופורציה עיקריות (היכן נקודת הרוחב הגדולה ביותר? כמה אסימטרית הבסיס?) - הרמוניות גבוהות יותר = פרטים עדינים (שיניים בקצה, בליטות קטנות)
כיצד EFA עובד
קו מתאר מיוצג כרצף סגור של קואורדינטות \((x_i, y_i)\). EFA מפרק את הקואורדינטות \(x\) ו-\(y\) בנפרד כסדרות פורייה, ואז משלב אותם מחדש כאליפסות.
כל הרמוניה \(n\) מייצרת 4 מקדמים: \((a_n, b_n, c_n, d_n)\).
עבור \(N\) הרמוניות, מקבלים \(4N\) מקדמים בסך הכל. מקדמים אלה הם הצורה — ניתן להשתמש בהם ישירות ב-PCA בדיוק כמו קואורדינטות ציוני דרך.
נרמול
מקדמי EFA חייבים להיות מנורמלים להסרת אפקטי גודל, מיקום, סיבוב ונקודת התחלה. לאחר נרמול, נותר רק מידע צורה.
בחירת N (מספר הרמוניות)
יותר הרמוניות = יותר פרטים נלכדים, אבל גם יותר רעש. הקריטריון הסטנדרטי:
בחרו את ה-\(N\) המינימלי שמסביר ≥ 99% מהשונות בצורת קו המתאר במדגם שלכם.
עבור גרזנים כשלנו, 8–12 הרמוניות בדרך כלל מספיקות.
שלב אחר שלב: EFA ב-Colab
שלב 1: טעינת ועיבוד מקדם של תמונת גרזן אחת
!pip install pyefd scikit-image python-bidi -q
import numpy as np
import matplotlib.pyplot as plt
from bidi.algorithm import get_display
rtl = get_display # תיקון כיוון טקסט עברי בגרפים של matplotlib
from skimage import io, color, filters, measure
import pyefd
def extract_outline(image_path, threshold_method='otsu'):
"""
טוען תמונת גרזן וחולץ את קו המתאר הגדול ביותר.
מחזיר: מערך של קואורדינטות (x, y)
"""
img = io.imread(image_path, as_gray=True)
# סף לבינארי
thresh = filters.threshold_otsu(img)
binary = img > thresh # רקע = True, חפץ = False
# מציאת קווי מתאר של החפץ
contours = measure.find_contours(~binary, 0.5)
# שמירת הקו הגדול ביותר (= הגרזן)
largest = max(contours, key=len)
return largest # צורה: (n_נקודות, 2)
# בדיקה על גרזן אחד
outline = extract_outline("data/flanged_axes/G3/ALT.tif")
print(f"לקו המתאר {len(outline)} נקודות")
# ציור
plt.figure(figsize=(6, 8))
plt.plot(outline[:, 1], -outline[:, 0], 'b-', linewidth=1)
plt.fill(outline[:, 1], -outline[:, 0], alpha=0.2)
plt.title(rtl("קו מתאר גרזן ALT"))
plt.axis('equal')
plt.show()שלב 2: התאמת EFA
# התאמת EFA עם 12 הרמוניות, מנורמל
coeffs = pyefd.elliptic_fourier_descriptors(
outline, order=12, normalize=True
)
print(f"צורת מקדמי EFA: {coeffs.shape}") # (12, 4)
# כל שורה = הרמוניה אחת: (a_n, b_n, c_n, d_n)שלב 3: שחזור הצורה מ-EFA (בדיקת שפיות)
# שחזור קו המתאר ממקדמי EFA (אמור להתאים למקור)
locus = pyefd.reconstruct_contour(coeffs, num_points=300)
plt.figure(figsize=(6, 8))
plt.plot(outline[:, 1], -outline[:, 0], 'gray', linewidth=2, alpha=0.5, label=rtl('מקורי'))
plt.plot(locus[:, 0], -locus[:, 1], 'r-', linewidth=2, label=rtl('שחזור EFA'))
plt.legend()
plt.axis('equal')
plt.title(rtl("שחזור EFA (12 הרמוניות)"))
plt.show()שלב 4: עיבוד כל הגרזנים ויצירת מטריצה
import os
def process_all_axes(folder_g3, folder_g4):
"""טוען את כל תמונות הגרזן, חולץ קווי מתאר ומתאים EFA."""
all_coeffs = []
all_labels = []
all_names = []
for folder, group in [(folder_g3, 'G3'), (folder_g4, 'G4')]:
for fname in sorted(os.listdir(folder)):
if fname.endswith('.tif'):
path = os.path.join(folder, fname)
try:
outline = extract_outline(path)
coeffs = pyefd.elliptic_fourier_descriptors(
outline, order=12, normalize=True
)
# שיטוח: 12 הרמוניות × 4 מקדמים = 48 ערכים
all_coeffs.append(coeffs.flatten())
all_labels.append(group)
all_names.append(fname.replace('.tif', ''))
except Exception as e:
print(f"דולג {fname}: {e}")
return np.array(all_coeffs), np.array(all_labels), np.array(all_names)
coeff_matrix, labels, names = process_all_axes(
"data/flanged_axes/G3/",
"data/flanged_axes/G4/"
)
print(f"צורת המטריצה: {coeff_matrix.shape}")
# צפוי: (n_גרזנים, 48)
print(f"קבוצות: {dict(zip(*np.unique(labels, return_counts=True)))}")שלב 5: PCA על מקדמי EFA
from sklearn.decomposition import PCA
import pandas as pd
pca = PCA()
scores = pca.fit_transform(coeff_matrix)
var_exp = pca.explained_variance_ratio_ * 100
df = pd.DataFrame({
'PC1': scores[:, 0],
'PC2': scores[:, 1],
'קבוצה': labels,
'שם': names
})
# ציור
fig, ax = plt.subplots(figsize=(9, 7))
colors = {'G3': '#1976D2', 'G4': '#C62828'}
for group, grp_df in df.groupby('קבוצה'):
ax.scatter(grp_df['PC1'], grp_df['PC2'],
color=colors[group], label=group,
s=60, alpha=0.85, edgecolor='white')
ax.set_xlabel(f"PC1 ({var_exp[0]:.1f}%)")
ax.set_ylabel(f"PC2 ({var_exp[1]:.1f}%)")
ax.set_title(rtl("מרחב הצורות EFA: גרזני ברונזה עם כנפיים"))
ax.legend(title=rtl("קבוצה טיפולוגית"))
plt.tight_layout()
plt.show()קריאת גרף PCA של EFA
בניגוד ל-PCA של ציוני דרך, צירי PCA של EFA אינם ממפים ישירות לתזוזות בציוני דרך ספציפיים. במקום זאת, אנחנו מדמיינים את קווי המתאר המשוחזרים בקצוות של כל רכיב עיקרי:
# שחזור צורה ממוצעת וצורות ±2SD לאורך PC1
mean_coeffs = pca.mean_.reshape(12, 4)
pc1_vector = pca.components_[0].reshape(12, 4)
std1 = np.sqrt(pca.explained_variance_[0])
fig, axes = plt.subplots(1, 3, figsize=(15, 7))
for ax, scale, title in zip(axes, [-2, 0, 2], ['PC1 מינימום (−2SD)', 'צורה ממוצעת', 'PC1 מקסימום (+2SD)']):
shape_coeffs = mean_coeffs + scale * std1 * pc1_vector
locus = pyefd.reconstruct_contour(shape_coeffs, num_points=400)
ax.fill(locus[:, 0], locus[:, 1], alpha=0.3, color='steelblue')
ax.plot(locus[:, 0], locus[:, 1], 'steelblue', linewidth=2)
ax.set_title(title)
ax.set_aspect('equal')
ax.axis('off')
plt.suptitle("שונות צורה לאורך PC1 — גרזני ברונזה", fontsize=13)
plt.tight_layout()
plt.show()מה לחפש: האם הצורות המשוחזרות בקצות PC1 מציגות את אותן תכונות מורפולוגיות שמבדילות בין G3 ל-G4 בטיפולוגיה המסורתית?
מטלה 4 מחולקת השבוע
EFA + PCA מלא של גרזנים G3 לעומת G4, עם בדיקה סטטיסטית ומפה גיאוגרפית.