الكاميرا ونماذج اللغة المحلية
كل حسّاس قرأناه حتى الآن أعطانا عددا واحدا في كل مرة. الكاميرا تعطي مئات آلاف الأعداد في الصورة الواحدة، ونموذج اللغة يعطي جملا تبدو واثقة حتى حين تكون خاطئة. في هذا الفصل الأخير نشغّل الأداتين، ونتعلّم متى لا نصدّقهما، وماذا ندين به لمن يظهر في الصورة.
- نحسب حجم الإطار الخام، ونفهم لماذا ترسل اللوحة صور JPEG.
- نبني خادم بثّ MJPEG، ونصل به كاميرا على ESP32 ببرنامج ثابت خاص.
- نلتقط صورة على Raspberry Pi 4/5 بمكتبة Picamera2، ونقرأ بثّا بعيدا بمكتبة OpenCV.
- نسأل نموذج لغة محليا عبر Ollama من اللوحة، ونفحص جوابه قبل أن ينفّذ البرنامج شيئا.
- نوثّق استعمال الذكاء الاصطناعي، ونحترم خصوصية المصوَّرين وموافقتهم.
24.1 الصورة أعداد كثيرة – התמונה היא הרבה מספרים (An Image is Many Numbers)
الصورة شبكة من نقاط نسمّيها البكسلات (פיקסלים)، والبكسل الملوّن ثلاثة أعداد: أحمر وأخضر وأزرق، لكل منها بايت واحد عادة. والصورة الواحدة من سلسلة صور متتابعة نسمّيها الإطار (פריים).
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| W, H | عرض الإطار وارتفاعه بالبكسلات | — |
| c | عدد البايتات لكل بكسل: 3 للملوّن، و1 للرمادي | — |
| B | حجم الإطار بالبايتات، قبل أي ضغط | — |
هذا العدد أكبر من كل الذاكرة الداخلية في شريحة ESP32 الأصلية، وهي نحو 520 kB حسب الشركة المصنّعة. لذلك لا تحمل اللوحة الإطار الخام: تطلب من الكاميرا صورة مضغوطة بصيغة JPEG، وتستعين بذاكرة خارجية PSRAM إن وُجدت. أما Raspberry Pi 4/5 فذاكرته تتّسع للإطار الخام، ولذلك نحلّل الصور عليه.
24.2 كاميرا على ESP32 وبثّ MJPEG – מצלמה על ESP32 ושידור MJPEG (ESP32 Camera and MJPEG Streaming)
البرنامج الثابت (קושחה) الرسمي لمايكروبايثون لا يضمّ وحدة باسم camera، وهذا سبب أن هذا القسم يخص ESP32 مع كاميرا فقط. نحتاج إلى برنامج ثابت خاص يُبنى مع مشغّل الكاميرا. نتبع هنا توثيق مشروع مفتوح اسمه micropython-camera-driver، وهو يدعم الحسّاس OV2640 ولوحات مثل ESP32-CAM.
البرنامج التالي مكتوب حسب ذلك التوثيق، ولم يُشغَّل في بناء الكتاب، لأن اللوحة المُحاكاة لا تملك كاميرا:
import camera
camera.init(0, format=camera.JPEG, fb_location=camera.PSRAM)
jpg = camera.capture()
print("bytes:", len(jpg))
with open("photo.jpg", "wb") as f:
f.write(jpg)camera ليست جزءا من مايكروبايثون الرسمية، فقد تختلف الأسماء بين برنامج ثابت وآخر، وأرقام أطراف الكاميرا تختلف بين اللوحات وتُمرَّر إلى init. نراجع توثيق البرنامج الثابت الذي ثبّتناه فعلا، ونتأكد من أنه يناسب لوحتنا ومن حاجته إلى PSRAM. والبرنامج الثابت الخاص قد يتأخر عن آخر إصدار رسمي، وقد تنقصه ميزات: فهذا البرنامج مثلا مبني بلا BLE حسب توثيقها.كيف تصل الصور إلى المتصفّح؟ أبسط طريقة هي MJPEG: الخادم لا ينهي الجواب، بل يرسل صور JPEG واحدة بعد الأخرى داخل جواب HTTP واحد، ويفصل بينها بفاصل نختاره. والمتصفّح يستبدل كل صورة بالتي بعدها. وهذا هو البثّ (שידור זרם). يمكننا تجربة هيكل الخادم كله من غير كاميرا، بأن نضع نصوصا قصيرة مكان الصور:
import socket
import wifi
HEAD = (b"HTTP/1.0 200 OK\r\n"
b"Content-Type: multipart/x-mixed-replace;"
b" boundary=frame\r\n\r\n")
FRAMES = (b"<jpeg-1>", b"<jpeg-2>")
def send_frame(conn, jpg):
conn.sendall(b"--frame\r\nContent-Type: image/jpeg\r\n")
conn.sendall(b"Content-Length: %d\r\n\r\n" % len(jpg))
conn.sendall(jpg)
conn.sendall(b"\r\n")
wlan = wifi.connect()
print("open http://%s/" % wlan.ifconfig()[0])
addr = socket.getaddrinfo("0.0.0.0", 80)[0][-1]
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(addr)
server.listen(1)
while True:
conn, peer = server.accept()
conn.recv(1024)
conn.sendall(HEAD)
for jpg in FRAMES:
send_frame(conn, jpg)
conn.close()open http://192.168.1.50/ [http_sent] HTTP/1.0 200 OK Content-Type: multipart/x-mixed-replace; boundary=frame --frame Content-Type: image/jpeg Content-Length: 8 <jpeg-1> --frame Content-Type: image/jpeg Content-Length: 8 <jpeg-2> ...
وحين يتوفر البرنامج الثابت الخاص، نستبدل بالحلقة for دالة تلتقط بلا توقف حتى يغلق المتصفّح الاتصال. ولعرض البثّ في صفحة ويب يكفي الوسم <img src="/">.
for في الخادم السابق. لم يُشغَّل في بناء الكتاب، لأنه يحتاج إلى الكاميرا.def stream(conn):
conn.sendall(HEAD)
try:
while True:
send_frame(conn, camera.capture())
except OSError:
conn.close()24.3 الكاميرا على راسبيري باي: Picamera2 – מצלמה ב-Raspberry Pi: Picamera2 (Picamera2)
على Raspberry Pi 4/5 لا نحتاج إلى برنامج ثابت خاص. Picamera2 هي المكتبة الرسمية بلغة بايثون، وتعمل فوق طبقة libcamera في النظام. نوصل وحدة الكاميرا بشريطها إلى منفذ الكاميرا والتغذية مفصولة، مع الانتباه إلى أن منفذ Pi 5 أصغر ويحتاج إلى شريط مناسب له. والمكتبة مثبّتة مسبقا في نسخ النظام ذات سطح المكتب، وليست مثبّتة في نسخ Lite:
sudo apt install python3-picamera2 --no-install-recommends rpicam-hello --list-cameras
import time
from picamera2 import Picamera2
picam2 = Picamera2()
config = picam2.create_still_configuration(
main={"size": (1280, 720), "format": "RGB888"})
picam2.configure(config)
picam2.start()
time.sleep(1)
picam2.capture_file("photo.jpg")
array = picam2.capture_array("main")
print(array.shape)
picam2.stop()| الأمر | عمله |
|---|---|
| create_still_configuration(main=...) | إعداد التصوير الثابت: الأبعاد وترتيب البايتات. |
| picam2.start() | تشغيل الكاميرا. وننتظر قليلا حتى يستقر ضبط الإضاءة. |
| capture_file("photo.jpg") | حفظ الصورة في ملف، والامتداد يحدّد الصيغة. |
| capture_array("main") | الصورة مصفوفة NumPy للتحليل. |
RGB888 ترتّب البايتات أزرق ثم أخضر ثم أحمر، وهذا الترتيب هو ما تتوقعه OpenCV. ولا نثبّت المكتبة بالأمر pip؛ والدليل يوصي بـapt حتى تتوافق مع libcamera. ومن يعمل في بيئة افتراضية ينشئها مع --system-site-packages.وللبثّ من الباي نفسه لا نكتب الخادم من الصفر: في أمثلة المكتبة الرسمية ملف mjpeg_server.py يستعمل المرمّز JpegEncoder لبناء البثّ نفسه.
24.4 تحليل بثّ بعيد: الفرق بين إطارين – ניתוח שידור מרוחק (Analysing a Remote Stream)
حين تعجز اللوحة عن التحليل، يحلّل الباي بدلا منها: يقرأ بثّها، ويعمل على كل إطار. أبسط تحليل مفيد هو اكتشاف الحركة، ولا يحتاج إلى أي تعلّم آلي: نقارن كل بكسل بنظيره في الإطار السابق.
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| S | مجموع الفروق المطلقة بين كل بكسلين متقابلين | — |
| N | عدد البكسلات | — |
| D | الفرق المتوسّط لكل بكسل | — |
ولأن الكاميرا لها ضجيج، لا يبقى أي إطار مطابقا للسابق تماما. لذلك نقارن D بعتبة، كما فعلنا مع مقياس الجهد. نجرّب ذلك الآن على إطارين صغيرين مكتوبين في البرنامج، فيعمل على اللوحتين دون أي كاميرا:
THRESHOLD = 5.0
REF = ((10, 10, 10), (10, 10, 10), (10, 10, 10))
NOISY = ((11, 10, 9), (10, 12, 10), (10, 9, 11))
MOVED = ((10, 10, 10), (10, 200, 10), (10, 10, 10))
def score(a, b):
total = 0
count = 0
for row_a, row_b in zip(a, b):
for p, q in zip(row_a, row_b):
total += abs(p - q)
count += 1
return total / count
for name, frame in (("noisy", NOISY), ("moved", MOVED)):
d = score(REF, frame)
print("%s: D=%.2f motion=%s" % (name, d, d > THRESHOLD))noisy: D=0.67 motion=False moved: D=21.11 motion=True
البرنامج التالي يفعل الشيء نفسه بمكتبة OpenCV على إطارات حقيقية، ويقرأ بثّ اللوحة من القسم 24.2. نثبّتها بالأمر sudo apt install python3-opencv، والقراءة من عنوان HTTP تتطلب نسخة مبنية بدعم تشغيل البثّ. لم يُشغَّل في بناء الكتاب، لأنه يحتاج إلى بثّ حقيقي:
import cv2
URL = "http://192.168.1.50/"
THRESHOLD = 5.0
cap = cv2.VideoCapture(URL)
previous = None
while True:
ok, frame = cap.read()
if not ok:
print("no frame")
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if previous is not None:
d = float(cv2.absdiff(gray, previous).mean())
if d > THRESHOLD:
print("motion: D=%.2f" % d)
previous = gray
cap.release()24.5 التعرّف على الصور وTinyML – זיהוי תמונה ו-TinyML (Image Recognition and TinyML)
الفرق بين إطارين يقول إن شيئا تغيّر، ولا يقول ما هو. أما التعرّف على الصور (זיהוי תמונה) فيحتاج إلى نموذج (מודל): ملف من أعداد كثيرة ضُبطت بالتدريب على أمثلة، وهذا هو التعلّم الآلي (למידת מכונה). وتشغيل النموذج المدرَّب على مُدخَل جديد نسمّيه الاستدلال (הסקה).
الفكرة أن التدريب يبقى على الحاسوب، والاستدلال وحده ينزل إلى الشريحة. ولا نبرمج ذلك في هذا الكتاب، لأن مايكروبايثون الرسمية لا تضمّ وحدة جاهزة له، وتفاصيله أوسع من فصل. والجدول يلخّص الاختيار وصفيا. ولا نضع فيه أرقام دقة أو زمن، لأنها تتعلق بالنموذج وبالعتاد، وما يصح اليوم يتغيّر غدا، فنقيسها بأنفسنا في مشروعنا.
| أين يعمل النموذج | ما يناسبه | الثمن |
|---|---|---|
| على المتحكّم (TinyML) | سؤال ضيق جدا وجواب فوري دون شبكة | نموذج صغير ومحدود |
| على الباي | تحليل صور ونموذج لغة صغير داخل البيت | أبطأ من حاسوب قوي، ويستهلك ذاكرة كبيرة |
| على حاسوب في الشبكة | نماذج أكبر، دون إخراج البيانات من الشبكة | جهاز إضافي يعمل طوال الوقت |
| خدمة سحابية | أكبر النماذج دون عتاد | البيانات تغادر البيت، وتحتاج إلى إنترنت |
24.6 نموذج لغة محلي عبر Ollama – מודל שפה מקומי דרך Ollama (A Local Language Model with Ollama)
Ollama برنامج يشغّل نماذج لغة على جهازنا، ويفتح لها واجهة HTTP. وهذا يعني أن لوحتنا تحادثه بالأوامر التي نعرفها من فصل الطلبات، دون أي مكتبة جديدة. النموذج نفسه لا يعمل على اللوحة أبدا، بل على Raspberry Pi 4/5 أو على حاسوب. نثبّت Ollama حسب التعليمات الرسمية في موقعه، ثم ننزّل نموذجا:
model-name نموذجا صغيرا من مكتبة الموقع. الأسماء تتغيّر كثيرا، فلا نحدّد واحدا. لم يُشغَّل في بناء الكتاب.ollama pull model-name ollama list
- حسب التوثيق وقت كتابة الفصل: الخادم يستمع على
127.0.0.1:11434، أي على الجهاز نفسه فقط. - حتى تصل إليه اللوحة، نضبط متغيّر البيئة
OLLAMA_HOST=0.0.0.0:11434. وعندئذ يستطيع كل من في الشبكة استعماله، فنفعل ذلك في شبكة نثق بها فقط. - نطلب الجواب من
POST /api/generate، ونجده في الحقلresponse. - البثّ مفعّل افتراضيا، فنرسل
"stream": Falseحتى يصل الجواب قطعة واحدة بصيغة JSON.
SIMULATED. الذي جرى فعلا هو البرنامج نفسه: الطلب الذي أرسله، وقراءته للجواب. أما جواب النموذج الحقيقي فسيختلف.import requests
import wifi
URL = "http://192.168.1.20:11434/api/generate"
MODEL = "model-name"
body = {"model": MODEL,
"prompt": "Explain PWM in one short sentence.",
"stream": False}
wifi.connect()
r = requests.post(URL, json=body, timeout=60)
try:
if r.status_code == 200:
print(r.json()["response"])
else:
print("server said", r.status_code)
finally:
r.close()SIMULATED ANSWER: written by the book, no model ran.
وماذا لو أردنا أن يقرّر النموذج شيئا في الدائرة؟ هنا القاعدة الأهم في الفصل: جواب النموذج مُدخَل غير موثوق، مثل أي مُدخَل من خارج البرنامج. لا يصل إلى مشغّل قبل الفحص. نطلب منه صيغة JSON بالحقل format، ونقبل قيما من قائمة ضيقة فقط، وكل ما عداها يُرفض ويبقى الحال كما هو:
note فيه يقول ذلك. لم يعمل أي نموذج هنا.import json
import requests
import wifi
URL = "http://192.168.1.20:11434/api/generate"
MODEL = "model-name"
ALLOWED = ("on", "off")
PROMPT = ('Temperature is %.1f C. Reply only with JSON: '
'{"fan":"on"} or {"fan":"off"}.')
def ask(prompt):
body = {"model": MODEL, "prompt": prompt,
"format": "json", "stream": False}
r = requests.post(URL, json=body, timeout=60)
try:
if r.status_code != 200:
return None
return r.json()["response"]
finally:
r.close()
def fan_command(text):
try:
cmd = json.loads(text)["fan"]
except (ValueError, KeyError, TypeError):
return None
return cmd if cmd in ALLOWED else None
wifi.connect()
answer = ask(PROMPT % 31.5)
print("model said:", answer)
cmd = fan_command(answer) if answer else None
if cmd is None:
print("rejected: fan unchanged")
else:
print("fan ->", cmd)model said: {"fan":"on","note":"simulated"}
fan -> onنبدّل الآن النص الذي يعيده الخادم المُحاكى، ونترك البرنامج كما هو. كل الأجوبة في هذا الجدول كتبها الكتاب لفحص البرنامج:
| الجواب المُحاكى | السطر الأخير من المُخرَج |
|---|---|
{"fan":"on","note":"simulated"} | fan -> on |
{"fan":"max","note":"simulated"} | rejected: fan unchanged |
SIMULATED: I think the fan should run. | rejected: fan unchanged |
OLLAMA_NO_CLOUD=1.24.7 التفكير النقدي وتوثيق استعمال الذكاء الاصطناعي – חשיבה ביקורתית ותיעוד שימוש ב-AI (Critical Thinking and Documenting AI Use)
نموذج اللغة لا يبحث عن الحقيقة، بل يولّد نصا محتملا. لذلك قد يخترع بثقة تامة دالة غير موجودة، أو رقم طرف، أو مرجعا لا وجود له. ونسمّي ذلك الهلوسة (הזיה). وهذا الخطر أشد في الكهرباء، لأن وصلة خاطئة قد تتلف اللوحة أو تؤذي من يلمسها.
- الأمر والمكتبة: نبحث عنهما في التوثيق الرسمي للوحتنا ولإصدارنا.
- الشيفرة: نقرؤها سطرا سطرا، ونشغّلها، ونشرحها بكلماتنا. وما لا نستطيع شرحه لا نسلّمه.
- الرقم: نعيد حسابه بأنفسنا، أو نقيسه.
- التوصيل والجهد: لا نوصل شيئا لأن نموذجا قال ذلك. نراجع ورقة بيانات المكوّن.
- التكرار: السؤال نفسه قد يعطي جوابين مختلفين، فاتفاق جوابين ليس برهانا.
والتوثيق جزء من الأمانة ومن الهندسة معا: من يقرأ مشروعنا بعد سنة يحتاج إلى معرفة ما كتبناه نحن وما جاء من أداة. نحتفظ بسجلّ قصير في ملف داخل المشروع:
| البند في السجلّ | ماذا نكتب |
|---|---|
| الأداة والنموذج | الاسم والإصدار كما يظهران عند التشغيل، والتاريخ. |
| نصّ الطلب | ما سألناه بالضبط، أو ملخّص أمين له. |
| ما أخذناه | الملفات والأسطر التي جاءت من الجواب، وما عدّلناه فيها. |
| كيف تحققنا | التوثيق الذي راجعناه، والتجربة التي أجريناها ونتيجتها. |
| ما رفضناه | الخطأ الذي وجدناه في الجواب. وهذا أثمن بند. |
وقبل استعمال أي أداة في عمل يُقيَّم، نقرأ قواعد مدرستنا أو الجهة التي نسلّم لها، ونلتزم بها.
machine لم نرها من قبل. اذكروا طريقتين للتحقق منها قبل استعمالها.24.8 الكاميرا والخصوصية – מצלמה ופרטיות (Cameras and Privacy)
الدائرة الوحيدة في هذا الكتاب التي قد تؤذي من لا علاقة له بالمشروع هي الكاميرا. الصورة تكشف من كان في المكان، ومتى، ومع من. والخصوصية (פרטיות) قيد على التصميم مثل حد الجهد على الطرف، نفكر فيه قبل كتابة البرنامج.
- لا نصوّر حيث يتوقع الناس الخصوصية، ولا نوجّه الكاميرا إلى بيت الجار أو إلى الطريق العام.
- نعلن عنها: نترك الكاميرا ظاهرة، ونضع لافتة، ونُشعل ثنائيا باعثا للضوء ما دام البثّ يعمل.
- أقل ما يكفي: أصغر دقة تؤدي المهمة، ولا حفظ دون حاجة، ومدة محدّدة ثم حذف. وإن كفى البرنامج أن يقول «هناك حركة»، فلا يرسل الصورة.
- التحليل داخل البيت: التحليل على الباي يُبقي الصور في الشبكة المحلية. لكن «محلي» لا يعني «آمن»: السجلّات والملفات قد تبقى على القرص.
- الوصول: خادمنا بلا كلمة مرور ولا تشفير، فيبقى في شبكة موثوقة، ولا نوجّه إليه منفذا من الموجّه.
- الوجوه: التعرّف على الأشخاص بأعيانهم أخطر بكثير من اكتشاف الحركة. ولا نبنيه في مشروع تعليمي.
أهمّ أفكار الفصل:
- الإطار الخام كبير جدا على المتحكّم، ولذلك تبثّ اللوحة صور JPEG، ويحلّل الباي.
- وحدة
cameraليست في البرنامج الثابت الرسمي، وأوامرها تختلف من برنامج ثابت إلى آخر. - MJPEG هو صور متتابعة داخل جواب HTTP واحد، ودرجة الفرق مع عتبة تكشف التغيّر دون أي تعلّم آلي.
- جواب النموذج مُدخَل غير موثوق: يمر بالفحص قبل أن يصل إلى المشغّل.
- نتحقق من كل ما يولّده النموذج، ونوثّق ما أخذناه منه.
- كل من يظهر في الصورة يعرف ويوافق، ونجمع أقل ما يكفي.
ملخّص قوانين الفصل:
| القانون | الصيغة |
|---|---|
| حجم الإطار الخام | B = W · H · c |
| درجة الفرق | D = SN |
أوامر الفصل:
| الأمر | عمله |
|---|---|
| camera.init(0, format=camera.JPEG) | تهيئة الكاميرا في البرنامج الثابت الخاص. |
| camera.capture() | صورة JPEG على شكل بايتات، في البرنامج الثابت الخاص. |
| multipart/x-mixed-replace | نوع المحتوى في بثّ MJPEG. |
| picam2.capture_file(name) | التقاط صورة إلى ملف على الباي. |
| picam2.capture_array("main") | التقاط صورة إلى مصفوفة على الباي. |
| cv2.VideoCapture(url) | فتح بثّ بعيد بمكتبة OpenCV. |
| cv2.absdiff(a, b) | الفرق المطلق بين إطارين. |
| requests.post(url, json=body) | طلب إلى خادم Ollama. |
| r.json()["response"] | نصّ الجواب من Ollama. |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية | English |
|---|---|---|
| البكسل | פיקסל | Pixel |
| الإطار | פריים | Frame |
| البرنامج الثابت | קושחה | Firmware |
| البثّ | שידור זרם | Streaming |
| التعرّف على الصور | זיהוי תמונה | Image Recognition |
| التعلّم الآلي | למידת מכונה | Machine Learning |
| النموذج | מודל | Model |
| الاستدلال | הסקה | Inference |
| نموذج اللغة | מודל שפה | Language Model |
| نصّ الطلب | פרומפט | Prompt |
| الهلوسة | הזיה | Hallucination |
| الخصوصية | פרטיות | Privacy |
| الموافقة | הסכמה | Consent |
"max". والبرنامج وحده يعرف ما هو آمن للدائرة.dir(machine). ووجودها لا يكفي: نقرأ أيضا ماذا تفعل.أسئلة للتمرين:
cam_esp32.py على البرنامج الثابت الرسمي لمايكروبايثون؟camera، فيتوقف عند سطر الاستيراد."stream": False؟response. ودون ذلك يصل الجواب قطعا متتابعة، كل قطعة JSON مستقل، فلا تصلح قراءة الرد كله دفعة واحدة.frame_diff.py: ما أصغر قيمة للبكسل الأوسط في الإطار MOVED تجعل D أكبر من 5؟ والقيم الأخرى لا تتغيّر، والبكسل عدد صحيح.ask تنتهي بخطأ OSError إذا كان الخادم مطفأ. اكتبوا دالة تحيط بها، وتعيد None في هذه الحالة.def safe_ask(prompt):
try:
return ask(prompt)
except OSError:
print("server unreachable")
return Noneexec() على اللوحة، «حتى يكتب النموذج البرنامج بنفسه». اشرحوا الخطر، واقترحوا بديلا.fan_command.(أ) ما الذي يعمل على اللوحة وما الذي يعمل على الباي؟
(ب) أين يتدخل واجب الموافقة؟
(ج) ما الذي نقيسه بأنفسنا قبل أن نعد بأي زمن استجابة؟
من أول ثنائي باعث للضوء إلى هنا
بدأنا هذا الكتاب بثنائي باعث للضوء يومض، وبسطر واحد يكتب 1 على طرف. ثم قرأنا زرا وجهدا، وتعلّمنا أن الزمن والمقاطعات جزء من البرنامج، وتحدّثنا مع الرقائق عبر النواقل، وكتبنا مكتبات وملفات، ثم خرجنا إلى الشبكة، وبنينا نظام تحكّم ومشروعا كاملا، وجعلنا الباي خادما له، وانتهينا بصورة ونموذج لغة. الأدوات كبرت كثيرا، أما العادة فبقيت واحدة منذ السطر الأول: نعرف من أين جاء كل رقم، ولا نصدّق ما لم نتحقق منه. هذه العادة هي ما نأخذه معنا إلى كل لوحة وكل أداة ستأتي بعدها.