برمجة المتحكّمات بلغة مايكروبايثون · الفصل 24 — الكاميرا ونماذج اللغة المحليةמצלמה ומודלי שפה מקומיים · תכנות מיקרו-בקרים בשפת MicroPython⌂📚🏠
الفصل 24

الكاميرا ونماذج اللغة المحلية

מצלמה ומודלי שפה מקומיים – Camera and Local Language Models
3 ساعات تعليمية

كل حسّاس قرأناه حتى الآن أعطانا عددا واحدا في كل مرة. الكاميرا تعطي مئات آلاف الأعداد في الصورة الواحدة، ونموذج اللغة يعطي جملا تبدو واثقة حتى حين تكون خاطئة. في هذا الفصل الأخير نشغّل الأداتين، ونتعلّم متى لا نصدّقهما، وماذا ندين به لمن يظهر في الصورة.

في هذا الفصل:
  • نحسب حجم الإطار الخام، ونفهم لماذا ترسل اللوحة صور JPEG.
  • نبني خادم بثّ MJPEG، ونصل به كاميرا على ESP32 ببرنامج ثابت خاص.
  • نلتقط صورة على Raspberry Pi 4/5 بمكتبة Picamera2، ونقرأ بثّا بعيدا بمكتبة OpenCV.
  • نسأل نموذج لغة محليا عبر Ollama من اللوحة، ونفحص جوابه قبل أن ينفّذ البرنامج شيئا.
  • نوثّق استعمال الذكاء الاصطناعي، ونحترم خصوصية المصوَّرين وموافقتهم.

24.1 الصورة أعداد كثيرة – התמונה היא הרבה מספרים (An Image is Many Numbers)

الصورة شبكة من نقاط نسمّيها البكسلات (פיקסלים)، والبكسل الملوّن ثلاثة أعداد: أحمر وأخضر وأزرق، لكل منها بايت واحد عادة. والصورة الواحدة من سلسلة صور متتابعة نسمّيها الإطار (פריים).

حجم الإطار الخام
B = W · H · c
الرمزالمعنىوحدة القياس
W, Hعرض الإطار وارتفاعه بالبكسلات—
cعدد البايتات لكل بكسل: 3 للملوّن، و1 للرمادي—
Bحجم الإطار بالبايتات، قبل أي ضغط—
مثال 24.1
ما حجم إطار ملوّن خام أبعاده 640 × 480؟
الحل:
B = W · H · c
B = 640 · 480 · 3
B = 921600 byte
B = 921600 byte

هذا العدد أكبر من كل الذاكرة الداخلية في شريحة ESP32 الأصلية، وهي نحو 520 kB حسب الشركة المصنّعة. لذلك لا تحمل اللوحة الإطار الخام: تطلب من الكاميرا صورة مضغوطة بصيغة JPEG، وتستعين بذاكرة خارجية PSRAM إن وُجدت. أما Raspberry Pi 4/5 فذاكرته تتّسع للإطار الخام، ولذلك نحلّل الصور عليه.

لوحة بكاميراMJPEGالشبكة المحليةراسبيري بايOpenCV, Ollamaالمتصفّح
الخريطة التي نبنيها: اللوحة تصوّر وتبثّ، والباي يحلّل ويشغّل نموذج اللغة، والمتصفّح يعرض.
سؤال 24.1: ما حجم إطار رمادي خام أبعاده 320 × 240؟

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()
ESP32 + Pico W · MicroPython 1.29 · simulated board
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.2: نفترض، للحساب فقط وليس قياسا، أن حجم الإطار 20 kB وأننا نرسل 5 إطارات في الثانية. كم كيلوبايت يمر في الثانية؟ وكم كيلوبت؟

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)

حين تعجز اللوحة عن التحليل، يحلّل الباي بدلا منها: يقرأ بثّها، ويعمل على كل إطار. أبسط تحليل مفيد هو اكتشاف الحركة، ولا يحتاج إلى أي تعلّم آلي: نقارن كل بكسل بنظيره في الإطار السابق.

درجة الفرق بين إطارين
D = SN
الرمزالمعنىوحدة القياس
Sمجموع الفروق المطلقة بين كل بكسلين متقابلين—
Nعدد البكسلات—
Dالفرق المتوسّط لكل بكسل—
مثال 24.2
إطار مرجعي من 9 بكسلات، قيمة كل منها 10. وفي الإطار الجديد صار البكسل الأوسط 200، وبقيت البكسلات الأخرى كما هي. احسبوا D.
الحل:
D = SN
D = 1909
D = 21.11
D = 21.11

ولأن الكاميرا لها ضجيج، لا يبقى أي إطار مطابقا للسابق تماما. لذلك نقارن D بعتبة، كما فعلنا مع مقياس الجهد. نجرّب ذلك الآن على إطارين صغيرين مكتوبين في البرنامج، فيعمل على اللوحتين دون أي كاميرا:

الإطار الأول فيه ضجيج فقط، والثاني فيه تغيّر واضح. والعتبة 5 مثال لهذه الأعداد الصغيرة.
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))
ESP32 + Pico W · MicroPython 1.29 · simulated board
noisy: D=0.67 motion=False
moved: D=21.11 motion=True

البرنامج التالي يفعل الشيء نفسه بمكتبة OpenCV على إطارات حقيقية، ويقرأ بثّ اللوحة من القسم 24.2. نثبّتها بالأمر sudo apt install python3-opencv، والقراءة من عنوان HTTP تتطلب نسخة مبنية بدعم تشغيل البثّ. لم يُشغَّل في بناء الكتاب، لأنه يحتاج إلى بثّ حقيقي:

اكتشاف الحركة في بثّ بعيد. العتبة تُضبط بالتجربة: نطبع D في مشهد ساكن أولا، ثم نختار قيمة أعلى منه.
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.3: لماذا قد يبلّغ البرنامج عن حركة حين ينطفئ مصباح الغرفة، ولا أحد يتحرك؟

24.5 التعرّف على الصور وTinyML – זיהוי תמונה ו-TinyML (Image Recognition and TinyML)

الفرق بين إطارين يقول إن شيئا تغيّر، ولا يقول ما هو. أما التعرّف على الصور (זיהוי תמונה) فيحتاج إلى نموذج (מודל): ملف من أعداد كثيرة ضُبطت بالتدريب على أمثلة، وهذا هو التعلّم الآلي (למידת מכונה). وتشغيل النموذج المدرَّب على مُدخَل جديد نسمّيه الاستدلال (הסקה).

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
انتبهوا: جواب مُحاكى: الجواب على هذه الشاشة مُحاكى، ولم يكتبه أي نموذج. الكتاب يُبنى بلا Ollama، فكتبنا نصّ الجواب بأنفسنا داخل الخادم المُحاكى، وبدأناه بالكلمة 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()
ESP32 + Pico W · MicroPython 1.29 · simulated board
SIMULATED ANSWER: written by the book, no model ran.
تحذير: اللوحة تحمل الجواب كاملا في ذاكرتها. فإذا طال الجواب انتهى البرنامج بخطأ ذاكرة، فنطلب جوابا قصيرا. والمهلة 60 ثانية مثال فقط: النموذج الكبير على جهاز صغير قد يتأخر كثيرا، فنضبطها بالتجربة.

وماذا لو أردنا أن يقرّر النموذج شيئا في الدائرة؟ هنا القاعدة الأهم في الفصل: جواب النموذج مُدخَل غير موثوق، مثل أي مُدخَل من خارج البرنامج. لا يصل إلى مشغّل قبل الفحص. نطلب منه صيغة JSON بالحقل format، ونقبل قيما من قائمة ضيقة فقط، وكل ما عداها يُرفض ويبقى الحال كما هو:

الحسّاسنصّ الطلبالنموذجOllamaفحص الجوابالمشغّلرفض
النموذج يقترح، والبرنامج يقرّر: لا طريق إلى المشغّل إلا عبر الفحص.
جواب مُحاكى أيضا، والحقل 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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
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، النماذج التي تعمل محليا لا ترسل نصوصنا إلى الشركة، لكن تنزيل النماذج يمر بالإنترنت، ويتيح البرنامج وقت كتابة الفصل نماذج سحابية أيضا. ويمكن تعطيلها بالمتغيّر OLLAMA_NO_CLOUD=1.
سؤال 24.4: لماذا نفحص الجواب في البرنامج، مع أننا طلبنا من النموذج صيغة محدّدة؟

24.7 التفكير النقدي وتوثيق استعمال الذكاء الاصطناعي – חשיבה ביקורתית ותיעוד שימוש ב-AI (Critical Thinking and Documenting AI Use)

نموذج اللغة لا يبحث عن الحقيقة، بل يولّد نصا محتملا. لذلك قد يخترع بثقة تامة دالة غير موجودة، أو رقم طرف، أو مرجعا لا وجود له. ونسمّي ذلك الهلوسة (הזיה). وهذا الخطر أشد في الكهرباء، لأن وصلة خاطئة قد تتلف اللوحة أو تؤذي من يلمسها.

والتوثيق جزء من الأمانة ومن الهندسة معا: من يقرأ مشروعنا بعد سنة يحتاج إلى معرفة ما كتبناه نحن وما جاء من أداة. نحتفظ بسجلّ قصير في ملف داخل المشروع:

البند في السجلّماذا نكتب
الأداة والنموذجالاسم والإصدار كما يظهران عند التشغيل، والتاريخ.
نصّ الطلبما سألناه بالضبط، أو ملخّص أمين له.
ما أخذناهالملفات والأسطر التي جاءت من الجواب، وما عدّلناه فيها.
كيف تحققناالتوثيق الذي راجعناه، والتجربة التي أجريناها ونتيجتها.
ما رفضناهالخطأ الذي وجدناه في الجواب. وهذا أثمن بند.

وقبل استعمال أي أداة في عمل يُقيَّم، نقرأ قواعد مدرستنا أو الجهة التي نسلّم لها، ونلتزم بها.

سؤال 24.5: اقترح علينا نموذج دالة في machine لم نرها من قبل. اذكروا طريقتين للتحقق منها قبل استعمالها.

24.8 الكاميرا والخصوصية – מצלמה ופרטיות (Cameras and Privacy)

الدائرة الوحيدة في هذا الكتاب التي قد تؤذي من لا علاقة له بالمشروع هي الكاميرا. الصورة تكشف من كان في المكان، ومتى، ومع من. والخصوصية (פרטיות) قيد على التصميم مثل حد الجهد على الطرف، نفكر فيه قبل كتابة البرنامج.

قاعدة ملزمة: الموافقة (הסכמה) واجبة: كل من قد يظهر في الصورة يجب أن يعرف أن هناك كاميرا، ولماذا، وأن يوافق قبل التصوير. ويشمل ذلك الزملاء والأهل والزوّار. والتصوير دون موافقة قد يكون مخالفا للقانون أيضا، فنسأل عن القواعد السارية حيث نعمل. وفي المدرسة نحتاج قبل ذلك إلى إذن من المسؤولين، خصوصا حين يكون المصوَّرون قاصرين.
نشاط: خذوا مشروعكم، أو تخيّلوا جرس باب بكاميرا، واكتبوا صفحة واحدة: من قد يظهر في الصورة؟ كيف يعرف ذلك ويوافق؟ ما الذي يُحفظ وأين ومتى يُحذف؟ من يستطيع الوصول إليه؟ ثم غيّروا التصميم حتى يقل ما يُجمع.
سؤال 24.6: نظام يكتشف الحركة في مخزن المدرسة، ويحتاج فقط إلى إنذار. اقترحوا تصميما يحترم الخصوصية.

أهمّ أفكار الفصل:

ملخّص قوانين الفصل:

القانونالصيغة
حجم الإطار الخام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
الإجابات النهائية لأسئلة الفصل
24.1 B = 320 · 240 · 1 = 76800 byte.
24.2 20 × 5 = 100 kB/s، أي 800 kbit/s. والأرقام الحقيقية تتعلق بالمشهد وبالضبط وبالشبكة، فنقيسها.
24.3 لأن D يقيس تغيّر قيم البكسلات، وليس الحركة نفسها. انطفاء المصباح يغيّر كل البكسلات، فتتجاوز D العتبة.
24.4 لأن الطلب لا يضمن المعنى: قد يعيد قيمة مكتوبة بصيغة سليمة لكنها غير مسموحة، مثل "max". والبرنامج وحده يعرف ما هو آمن للدائرة.
24.5 نبحث عنها في docs.micropython.org لإصدار البرنامج الثابت عندنا، ونفحصها على اللوحة في REPL بالأمر dir(machine). ووجودها لا يكفي: نقرأ أيضا ماذا تفعل.
24.6 يعمل التحليل على الجهاز المحلي، ولا تُحفظ الصور ولا تُبث، ويُرسل فقط «هناك حركة» والوقت، بدقة منخفضة، مع إذن إدارة المدرسة، ولافتة ظاهرة للداخلين.

أسئلة للتمرين:

المستوى الأول — أساسي
1.
ما حجم إطار ملوّن خام أبعاده 1280 × 720؟
الإجابات: B = 1280 · 720 · 3 = 2764800 byte.
2.
لماذا لا يعمل البرنامج cam_esp32.py على البرنامج الثابت الرسمي لمايكروبايثون؟
الإجابات: لأنه لا يضمّ الوحدة camera، فيتوقف عند سطر الاستيراد.
3.
في أي حقل نجد نصّ الجواب في رد Ollama؟ وماذا يحدث إن لم نرسل "stream": False؟
الإجابات: في response. ودون ذلك يصل الجواب قطعا متتابعة، كل قطعة JSON مستقل، فلا تصلح قراءة الرد كله دفعة واحدة.
المستوى الثاني — تطبيق
4.
في برنامج frame_diff.py: ما أصغر قيمة للبكسل الأوسط في الإطار MOVED تجعل D أكبر من 5؟ والقيم الأخرى لا تتغيّر، والبكسل عدد صحيح.
الإجابات: نحتاج إلى S > 45، أي فرق أكبر من 45: القيمة 56.
5.
الدالة ask تنتهي بخطأ OSError إذا كان الخادم مطفأ. اكتبوا دالة تحيط بها، وتعيد None في هذه الحالة.
الإجابات:
def safe_ask(prompt):
    try:
        return ask(prompt)
    except OSError:
        print("server unreachable")
        return None
6.
ضعوا سجلّ توثيق لاستعمال حقيقي واحد لأداة ذكاء اصطناعي في مشروعكم، بالبنود الخمسة من القسم 24.7.
الإجابات: الجواب فردي. ونفحص: هل يستطيع قارئ آخر أن يعرف أي أسطر جاءت من الأداة، وكيف تحققتم منها؟
المستوى الثالث — تعمّق
7.
يريد زميل أن يضع جواب النموذج مباشرة في exec() على اللوحة، «حتى يكتب النموذج البرنامج بنفسه». اشرحوا الخطر، واقترحوا بديلا.
الإجابات: أي نص مولَّد أو مخترَق يصبح شيفرة تعمل على دائرة حقيقية، وقد يشغّل مشغّلا أو يمحو ملفا. البديل قائمة أوامر مسموحة وقيم محدّدة، وفحص كما في fan_command.
8.
صمّموا جرس باب: لوحة بكاميرا تُرسل صورة عند الضغط على زر.
(أ) ما الذي يعمل على اللوحة وما الذي يعمل على الباي؟
(ب) أين يتدخل واجب الموافقة؟
(ج) ما الذي نقيسه بأنفسنا قبل أن نعد بأي زمن استجابة؟
الإجابات: (أ) اللوحة تقرأ الزر وتلتقط JPEG وترسلها؛ والباي يستقبل ويخزّن لمدة محدّدة. (ب) يعرف الزائر بلافتة ظاهرة، ولا يتجاوز مجال الكاميرا عتبة الباب إلى الطريق أو الجيران. (ج) زمن الالتقاط والإرسال والتخزين على شبكتنا، فلا نأخذ رقما من الإنترنت.

من أول ثنائي باعث للضوء إلى هنا

بدأنا هذا الكتاب بثنائي باعث للضوء يومض، وبسطر واحد يكتب 1 على طرف. ثم قرأنا زرا وجهدا، وتعلّمنا أن الزمن والمقاطعات جزء من البرنامج، وتحدّثنا مع الرقائق عبر النواقل، وكتبنا مكتبات وملفات، ثم خرجنا إلى الشبكة، وبنينا نظام تحكّم ومشروعا كاملا، وجعلنا الباي خادما له، وانتهينا بصورة ونموذج لغة. الأدوات كبرت كثيرا، أما العادة فبقيت واحدة منذ السطر الأول: نعرف من أين جاء كل رقم، ولا نصدّق ما لم نتحقق منه. هذه العادة هي ما نأخذه معنا إلى كل لوحة وكل أداة ستأتي بعدها.

لوحة القوانين: كل قوانين هذا الفصل مجموعة مع رسومها وجداول رموزها في الفصل 25 — لوحة القوانين الشاملة. (נוסחאון הספר)