Å få kameraet til å sende meg e-post når noen dukker opp i bildet

28. juni 2026 code learningpython

Så kamera-greia funker endelig, etter en første versjon som teknisk sett også "funket" hvis definisjonen din av å funke inkluderer å sende meg e-post førtitre ganger før lunsj.

Jeg har et gammelt webkamera klipset fast på en hylle rettet mot inngangsdøren, og et script som kjører på en ledig maskin og overvåker feeden. I det øyeblikket noe beveger seg gjennom bildet tar det et bilde og sender det til meg på e-post. Ikke et vaktselskap, ikke et abonnement, bare Python som gjør nøyaktig den ene tingen jeg ba det om, til slutt.

hvorfor jeg i det hele tatt ville ha dette

Jeg jobber i andre etasje de fleste dagene, og døren er i første, langt nok unna at et bud som banker på eller noen som faktisk går opp ikke nødvendigvis når meg. Pakker ble stående ute lenger enn de burde et par ganger, en gang i vær som ikke var særlig snilt mot esken. Løsningen trengte ikke være smart. Den trengte bare å fortelle meg i det øyeblikket noen sto der.

forsøk én, som ikke var det

Den første versjonen sammenlignet to bilder direkte, i farger, piksel for piksel, og summerte hvor forskjellige de var.

import cv2

cap = cv2.VideoCapture(0)
_, prev = cap.read()

while True:
    _, curr = cap.read()
    diff = cv2.absdiff(prev, curr)
    total_diff = diff.sum()
    if total_diff > 500000:
        send_alert(curr)
    prev = curr

Dette gikk fint i omtrent seks minutter med testing i dagslys, og falt så sammen i det øyeblikket sola beveget seg bak en sky. Hele bildets lysstyrke skiftet litt, hver piksel endret seg med en liten mengde, og summert over et helt fargebilde er det mer enn nok til å sprenge enhver terskel jeg satte. Jeg fikk en e-post. Så en til fire sekunder senere fordi skyen fortsatte å bevege seg. Så en fordi lyktene på en bil sveipte over verandaen den kvelden. Ingen av disse var en person. Alle sammen var "hele bildet ble litt lysere eller mørkere."

det jeg faktisk googlet

"opencv motion detection false positive lighting change" tok meg til en pyimagesearch-guide som tydeligvis er tingen halve internett lærer grunnleggende bevegelsesdeteksjon fra, og å lese den fikk meg til å føle meg litt dum på en nyttig måte. To ting jeg hadde gått helt glipp av:

  • Konverter til gråtoner først og gjør det uskarpt før du sammenligner, så enkeltpiksel-støy og små lysendringer blir jevnet ut i stedet for talt med
  • I stedet for å summere rå pikselforskjeller, terskelsett forskjellen til en svart-hvitt-maske, se deretter på sammenhengende regioner (konturer) og bry deg bare om de som er større enn et minimumsareal

Den andre delen er den som faktisk betydde noe. En sky som passerer endrer lysstyrken litt overalt, noe som stort sett forsvinner etter terskelsetting siden en tilstrekkelig liten endring aldri krysser terskelen i utgangspunktet. En person som går gjennom bildet endrer et stort, sammenhengende område med piksler mye. En helt annen form når du ser på det som regioner i stedet for ett summert tall.

versjonen som faktisk fungerer

Bakgrunnshåndtering først. I stedet for å sammenligne bilde mot bilde, tar jeg ett bilde som utgangspunkt og gjør det uskarpt:

import cv2

cap = cv2.VideoCapture(0)
_, first_frame = cap.read()
baseline = cv2.GaussianBlur(cv2.cvtColor(first_frame, cv2.COLOR_BGR2GRAY), (21, 21), 0)

Så, per bilde, gjøres det uskarpt og sammenlignes mot dette utgangspunktet i stedet for forrige bilde:

def detect_regions(frame, baseline, min_area=2000):
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (21, 21), 0)
    diff = cv2.absdiff(baseline, gray)
    thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)[1]
    thresh = cv2.dilate(thresh, None, iterations=2)
    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    return [c for c in contours if cv2.contourArea(c) > min_area]

Og hovedløkken, som også sakte oppdaterer utgangspunktet slik at langsiktig lysdrift (morgen til ettermiddag, ikke bare en forbipasserende sky) ikke til slutt får alt til å se ut som bevegelse for alltid:

while True:
    _, frame = cap.read()
    regions = detect_regions(frame, baseline)
    if regions:
        send_alert(frame)
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (21, 21), 0)
    baseline = cv2.addWeighted(gray.astype("float"), 0.05, baseline.astype("float"), 0.95, 0).astype("uint8")

Den addWeighted-linjen gjør et sakte glidende gjennomsnitt, dytter utgangspunktet 5 % mot gjeldende bilde hver runde i stedet for å erstatte det helt. Sakte nok til at en faktisk person som går forbi ikke blir absorbert inn i utgangspunktet før den utløser noe, gradvis nok til at sola som krysser himmelen over timevis ikke til slutt leses som én kjempestor permanent bevegelsesklump.

en nedkjølingsperiode, fordi e-postleverandører har meninger

Den andre biten som tok en kveld: selv med riktig deteksjon kunne én person som gikk forbi døren i tre sekunder utløse fem separate e-poster idet de krysset flere bilder. Et enkelt nedkjølingstidsstempel fikset det.

import time

last_alert = 0
COOLDOWN_SECONDS = 60

def maybe_alert(frame):
    global last_alert
    if time.time() - last_alert < COOLDOWN_SECONDS:
        return
    send_alert(frame)
    last_alert = time.time()

e-post-delen

smtplib og email.message er ikke spennende biblioteker (litt klønete ærlig talt, API-et føles som det ikke har endret seg siden 2003, noe det for det meste ikke har), men de får en melding med et vedlagt JPEG-bilde inn i innboksen min på rundt fire linjer når boilerplaten er på plass.

import smtplib
from email.message import EmailMessage

def send_alert(image_bytes):
    msg = EmailMessage()
    msg["Subject"] = "motion detected"
    msg["From"] = "camera@example.com"
    msg["To"] = "bjorn@example.com"
    msg.set_content("Something moved in frame.")
    msg.add_attachment(image_bytes, maintype="image", subtype="jpeg", filename="frame.jpg")

    with smtplib.SMTP_SSL("smtp.example.com", 465) as server:
        server.login("camera@example.com", "app-password-not-real")
        server.send_message(msg)

Bevegelse oppdaget, bilde sendt på e-post

De første ekte varslene havnet rett i søppelpost, noe jeg ikke hadde tenkt på i det hele tatt. Det viser seg at en e-postkonto som bare noensinne sender, aldri mottar, uten en ordentlig SPF-post satt opp, ser nøyaktig ut som noe et spamfilter bør være mistenksomt til. Det tok en SPF- og DKIM-post på det sendende domenet før Gmail sluttet å stille begrave mine egne varsler fra meg selv.

utplasseringstabben som gjorde meg mest flau

Den første uken "kjørte" dette ubemannet inne i et terminalvindu på laptopen min, i en vanlig innloggingsøkt. Noe som betydde at den ene gangen det faktisk hadde noe å si, en levering på et tidspunkt jeg tilfeldigvis hadde laptopen igjen, hadde hele greia gått i dvale sammen med lokket og aldri sett noe. Ubemannet gjorde mye jobb i den setningen som oppsettet faktisk ikke støttet opp om. Det er en systemd-tjeneste på den ledige maskinen nå, restarter ved krasj, overlever at jeg lukker hva som helst.

hvorfor denne føltes annerledes

Det meste av det jeg hadde bygget før dette var selvstendig: en gjøremålsapp, et PDF-verktøy, en scraper som kjører én gang og skriver ut noe. Denne kjører kontinuerlig, for alltid, ubemannet (skikkelig, nå), og må faktisk være riktig, ellers går den enten glipp av det jeg bryr meg om eller spammer meg med falske alarmer. Første uke gjorde den begge deler, noe som var irriterende på to helt forskjellige måter.

Det finnes også en versjon av denne ideen jeg ikke har bygget ennå, hvor den skiller en person-formet klump fra alt annet, i stedet for enhver tilstrekkelig stor bevegelig region. Konturareal tar meg mesteparten av veien, men en stor hund eller en busk som svaier etter en storm klarer begge det samme størrelsesterskelet en person gjør. Det er et genuint vanskeligere problem, nærmere ekte objektdeteksjon enn bildesammenligning, og det ligger på listen under det å faktisk lese CS50Ps fil-I/O-forelesning ordentlig i stedet for å skumlese den.

hva som er neste

En nedkjølingsperiode som tilpasser seg i stedet for et flatt seksti sekunder, slik at en person som står ved døren og prater ikke utløser på nytt i det øyeblikket den utløper. Og etter hvert person-versus-møll-versus-busk-problemet, som jeg mistenker ender med at jeg endelig lærer meg en ordentlig objektdeteksjonsmodell i stedet for å lene meg på bildesammenligning for alltid.

0 kommentarer

Logg inn for å kommentere.

Logg inn

Ingen konto?