Back to portfolio
AI Safe dokumentation

Ansiktsstyrt kassaskåp med kamera, servo och Wi-Fi

Koden är ett ansiktsstyrt kassaskåp, “AI Safe”, med ett Tkinter-gränssnitt, Raspberry Pi-kamera, ansiktsigenkänning och Wi-Fi-kommandon till en servo-Pi som låser och låser upp.

0.48 tolerance för ansiktsmatchning
80 ms uppdatering av live preview
Wi-Fi HTTP-kommandon till servo-Pi
AI Safe kassaskåp med display monterad på framsidan
AI Safe gränssnitt med livekamera och status Insidan av kassaskåpet med Raspberry Pi och servo

Vad den gör

När programmet startar skapas ett system där kameran, ansiktsigenkänningen, gränssnittet och servolåset arbetar tillsammans. Användaren ser ett helskärmsfönster med livekamera, status, kontroller och aktivitetslogg.

Vid start

  • Skapar en mapp ~/faces där godkända ansikten sparas.
  • Startar en livekameraström med rpicam-vid.
  • Läser in alla sparade ansiktsbilder från ~/faces.
  • Visar ett helskärmsfönster med livekamera, status, knappar, antal godkända ansikten och aktivitetslogg.

Huvudflöde

  • Add Face sparar ett nytt godkänt ansikte.
  • Unlock tar aktuell kamerabild, jämför ansiktet med sparade ansikten och skickar /unlock om matchningen är tillräckligt bra.
  • Lock skickar /lock till servo-Pi.
01 Kamera

Raspberry Pi-kameran skickar MJPEG-bilder.

02 Frame

Senaste bilden sparas i LAST_FRAME.

03 Face

Ansiktet görs om till en encoding.

04 Match

face_distance avgör om personen är godkänd.

05 Servo

HTTP-kommandot låser eller låser upp.

Viktiga konstanter

BASE_DIR = os.path.expanduser("~/faces")
SERVO_PI_IP = "10.71.203.135"
TOLERANCE = 0.48

BASE_DIR

Är mappen där godkända ansikten lagras.

SERVO_PI_IP

Är IP-adressen till en annan Raspberry Pi eller server som styr servot.

TOLERANCE = 0.48 avgör hur strikt ansiktsmatchningen är. Lägre värde betyder striktare matchning. Om avståndet mellan två ansikten är mindre än eller lika med 0.48 räknas det som samma person.

Kameradelen

Funktionen _build_preview_command() bygger kommandot rpicam-vid --codec mjpeg .... Det betyder att kameran skickar en kontinuerlig MJPEG-ström, alltså många JPEG-bilder efter varandra.

Sedan startas processen i init_camera_stream(). Den kör rpicam-vid med subprocess.Popen, fångar bildströmmen via stdout och startar en separat tråd:

STREAM_THREAD = threading.Thread(target=_stream_reader_loop, daemon=True)

Det är viktigt eftersom kameran hela tiden måste läsas utan att låsa gränssnittet.

Varför kameran fungerar

I _stream_reader_loop() läser programmet råa bytes från kameran. JPEG-bilder börjar alltid med byte-sekvensen b"\xff\xd8" och slutar med b"\xff\xd9".

Koden letar efter dessa start- och slutmarkörer, plockar ut en hel JPEG-bild, öppnar den med PIL och gör om den till en NumPy-array:

image = Image.open(io.BytesIO(jpeg_bytes)).convert("RGB")
frame = np.array(image)

Sedan sparas senaste bilden i LAST_FRAME med ett lås: FRAME_LOCK. Det gör att olika trådar inte läser och skriver bilden samtidigt.

Live preview

Funktionen update_preview() hämtar senaste kamerabilden, gör om den till en Tkinter-bild och visar den i camera_label.

root.after(80, update_preview)

Den kör sig själv igen var 80:e millisekund. Det är därför kamerabilden uppdateras kontinuerligt utan en vanlig while-loop som hade fryst gränssnittet.

Ansiktsinläsning

När programmet startar körs load_faces(). Den går igenom alla .jpg, .jpeg och .png i ~/faces.

img = face_recognition.load_image_file(path)
encodings = face_recognition.face_encodings(img)

Biblioteket face_recognition gör om ansiktet till en matematisk representation, en så kallad encoding. Det är ungefär ett numeriskt fingeravtryck för ansiktet.

Bara bilder med exakt ett ansikte accepteras:

if len(encodings) == 1:
    KNOWN_ENCODINGS.append(encodings[0])

Det förhindrar att programmet råkar spara fel person eller flera personer i samma bild.

Add Face, Unlock och Lock

Add Face

När användaren trycker på Add Face körs add_face(). Den fungerar bara om len(KNOWN_ENCODINGS) == 0 or SAFE_UNLOCKED.

Man får alltså lägga till första ansiktet när systemet är tomt, men efter det måste kassaskåpet redan vara upplåst. Det är en säkerhetsregel.

Funktionen tar aktuell kamerabild, kräver exakt ett ansikte, sparar bilden i ~/faces och lägger till ansiktets encoding i minnet.

Lock

När användaren trycker på Lock körs lock_flow(). Den skickar http://10.71.203.135:5000/lock och sätter statusen till locked.

Unlock

När användaren trycker på Unlock körs unlock_flow(). Först kontrollerar den att kassaskåpet inte redan är upplåst och att det finns minst ett godkänt ansikte.

Sedan tas en aktuell kamerabild:

frame = capture_face_frame()

Programmet hittar ansikten och skapar encodings för dem:

locations = face_recognition.face_locations(frame)
encodings = face_recognition.face_encodings(frame, locations)

Sedan jämförs varje ansikte med alla godkända ansikten:

distances = face_recognition.face_distance(KNOWN_ENCODINGS, encoding)

Om det minsta avståndet är tillräckligt lågt, alltså float(np.min(distances)) <= TOLERANCE, skickas ett HTTP-anrop:

http://10.71.203.135:5000/unlock

Då förväntas servo-Pi:n låsa upp det fysiska låset.

Gränssnittet

Tkinter används för att bygga hela fönstret. Det finns separata cards för kamera, knappar, systemöversikt och logg.

Knapparna aktiveras och inaktiveras av update_controls().

unlock_btn.config(state=tk.NORMAL if (has_faces and not SAFE_UNLOCKED) else tk.DISABLED)

Det betyder att Unlock-knappen bara går att trycka på om det finns godkända ansikten och kassaskåpet är låst.