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
~/facesdä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
/unlockom matchningen är tillräckligt bra. - Lock skickar
/locktill servo-Pi.
Raspberry Pi-kameran skickar MJPEG-bilder.
Senaste bilden sparas i LAST_FRAME.
Ansiktet görs om till en encoding.
face_distance avgör om personen är godkänd.
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.
Bilder från bygget
Bilderna visar gränssnittet, servolåset och den fysiska konstruktionen av kassaskåpet.