人工智能观鸟器


宇宙的尽头
转载
发布时间: 2025-11-27 14:20:27 | 阅读数 0收藏数 0评论数 0
封面
人工智能观鸟器能够检测用户感兴趣的区域内是否有鸟类出现,并利用人工智能技术,特别是卷积神经网络(CNN)来识别鸟类的存在。一旦检测到鸟类,它会实时向用户的手机发送Gmail通知,提醒用户慢慢靠近鸟类!这样,观鸟爱好者就可以将大部分时间用于其他活动,而当鸟类出现时,他们则可以兴致勃勃地观察并记录下来!

准备工作:

材料:

树莓派 5 8GB AI 基础套件 - 英式插头 (26 TOPS),包含从树莓派到 AI 模块以及合适的电源适配器等所有组件。

树莓派摄像头模块 3 宽

有线USB鼠标

有线USB键盘

显示器(带电源线),或者小型触摸屏(3.5英寸或7英寸都很好用)

Micro-HDMI 转 HDMI 线缆

3D打印机,或者可以使用3D打印机。我使用的是FlashForge Adventurer 5M Pro。

UHU 粘合剂

电脑

SG90 位置伺服电机


1

将 Raspberry Pi 操作系统刷入 MicroSD

  1. 要安装 Raspberry Pi OS,请将 microSD 卡插入 USB 适配器并将其连接到计算机。
  2. 从树莓派官方网站下载并安装树莓派图像处理软件。
  3. 打开 Imager,并按照图片中提供的设置进行配置。
  4. 写入操作系统文件。这大约需要30分钟。
  5. 过程完成后,取出 microSD 卡,将其插入 Raspberry Pi 的 microSD 卡槽,然后开机——Raspberry Pi 将启动进入操作系统。


2

树莓派设置

树莓派本质上是一台配置较低的电脑,但它的功能与电脑相同。它仍然需要显示器/屏幕和输入命令(键盘和鼠标)。

要正确配置,请将键盘和鼠标通过 USB 3.0 接口(蓝色端口)连接到树莓派。然后,将 Micro-HDMI 线缆的一端插入树莓派,另一端插入显示器。启动树莓派,等待几秒钟,即可进入设置页面。

在此页面上,系统会提示您输入管理员信息,例如您的 Raspberry Pi 用户名和密码、您的 Wi-Fi SSID 和密码,以及位置和语言偏好 - 请填写这些信息!


3

连接摄像头并安装AI到 Raspberry PI 5上

要将树莓派 5 与摄像头和 AI 扩展板连接起来,首先要连接摄像头。轻轻抬起摄像头接口上的黑色塑料盖,然后将摄像头线缆较短的一端(带亮光触点的一端)插入接口,注意要确保触点方向正确。最后将黑色盖向下按压以将其固定到位。

最好先连接摄像头,这样可以腾出更多空间,也更方便操作。之后,将AI扩展板放在树莓派顶部,确保接口对齐。

使用树莓派底部的短螺丝和顶部的长螺丝固定AI扩展板。AI扩展板安装后,如果不先取下扩展板,就很难拆卸或调整摄像头。

请务必将伺服电机通过 GPIO 引脚连接到树莓派。请按照以下接线图进行操作:

  1. 棕色线 → 接地
  2. 红线 → 5V
  3. 黄线 → GPIO 17


4

外壳的 3D 渲染图

我使用Autodesk Fusion 360创建了这个 CAD 模型。第一张图是渲染图,第二张图可以帮助您了解我使用的具体工具。我还使用 Autodesk 的软件来了解此模型的优势,以确保生产能够满足要求。

要查看外壳的 3D 渲染图,请查看上方的外壳图片。这也有助于您更好地了解项目可能的改进方案。


STL
Smart+Line+Bird,+Version+1+v12.stl
15.67MB
5

3D打印安全

3D打印完成后,从打印床上取下打印件时,请务必等待5分钟,让打印床完全冷却。这可以避免烫伤等潜在危险。

6

3D打印外壳

请下载我在“支持文件”部分添加的所有 .STL 文件,并将它们导入到您的切片软件中,例如 Orca 或 FlashPrint。我推荐使用 Orca,因为它允许您自定义文件的填充率,例如您可以将螺丝周围区域的填充率设置为 100%,而在其他区域降低填充率。我不建议将所有填充率都设置为 100%,因为这会使模块非常重。

请查看我在 Orca 中配置此设置的图片。

打印完成后,请记得移除所有支撑结构。


7

粘合

使用UHU胶水将各部件粘合在一起。首先,将顶盖粘合到机箱底部。如果您有夹子,现在就可以用上了;如果没有,用卡扣也很好用。

接下来,用同样的胶水将外壳粘到支架上。我没有用夹子固定,因为表面积很大,胶水可以牢固地粘合。UHU胶水30分钟后就开始凝固,但我让它静置了一夜,以确保粘合牢固。


8

安装

该模块的安装极其简单;只需将组件放置在视野范围内的室外环境中即可!

9

设置 VNC 并确保摄像头正常工作

目前,连接树莓派时需要连接显示器、键盘和鼠标,这非常麻烦。不过,也有更简单的替代方案,例如使用名为 VNC 的工具。有了 VNC,您无需显示器、键盘或鼠标即可访问树莓派——只需电源即可。

要进行此设置,请点击树莓派图标,进入树莓派配置,找到接口,然后启用 VNC 选项卡。完成此操作后,请在您的计算机上安装 RealVNC。

启用 VNC 后,您需要找到树莓派的 IP 地址。您可以通过树莓派的用户界面 (UI) 查看,也可以使用 IP 地址扫描器。要使用 UI 查看,请将光标悬停在 Wi-Fi 图标上,即可显示 IP 地址。只需将这些地址输入到计算机上的 RealVNC 中,然后输入您的用户名和密码即可。这样,您就可以通过无头模式完全控制树莓派。如果您想使用 IP 地址扫描器,请搜索 IP 地址并将其输入到 RealVNC 中。


10

使用代码测试数据

现在到了有趣的部分——代码。首先,我们需要收集要分析的视频。我建议准备大约 5 小时的视频素材。您可以随意使用下面的代码,它可以帮助您收集数据并将其存储在名为“videos”的本地文件夹中。

此步骤中您需要拍摄的视频就是您感兴趣的室外区域。

from picamera2 import Picamera2
from picamera2.encoders import H264Encoder
from picamera2.outputs import FfmpegOutput
from datetime import datetime, timedelta, time as dtime
import os
import time
import sys

def next_window(now):
today = now.date()
start_today = datetime.combine(today, dtime(12, 20, 0))
end_today = datetime.combine(today, dtime(14, 0, 0))

if now < start_today:
return start_today, end_today
elif now < end_today:
return now, end_today
else:
tomorrow = today + timedelta(days=1)
start_tomorrow = datetime.combine(tomorrow, dtime(12, 20, 0))
end_tomorrow = datetime.combine(tomorrow, dtime(14, 0, 0))
return start_tomorrow, end_tomorrow

def main():
out_dir = os.path.join(os.getcwd(), "videos")
os.makedirs(out_dir, exist_ok=True)

now = datetime.now()
start_dt, end_dt = next_window(now)

stamp = start_dt.strftime("%d%m%Y")
filename = f"{stamp}_video.mp4"
out_path = os.path.join(out_dir, filename)

wait_seconds = (start_dt - datetime.now()).total_seconds()
if wait_seconds > 0:
print(f"waiting until {start_dt.strftime('%Y-%m-%d %H:%M:%S')}...")
try:
time.sleep(wait_seconds)
except KeyboardInterrupt:
print("\nInterrupted while waiting. Exiting.")
sys.exit(0)

duration = max(0, (end_dt - datetime.now()).total_seconds())
if duration == 0:
print("[Scheduler] No time left in the window. Exiting.")
return

print(f"Starting capture at {datetime.now().strftime('%H:%M:%S')} "
f"for {int(duration)} seconds (until {end_dt.strftime('%H:%M:%S')}).")
print(f"[Recorder] Output: {out_path}")

picam2 = Picamera2()
video_config = picam2.create_video_configuration(main={"size": (1920, 1080)})
picam2.configure(video_config)

encoder = H264Encoder(bitrate=10_000_000)
output = FfmpegOutput(out_path)

try:
picam2.start_recording(encoder, output)
time.sleep(duration)
except KeyboardInterrupt:
print("\n[Recorder] Interrupted. Stopping recording...")
finally:
try:
picam2.stop_recording()
except Exception:
pass
try:
picam2.close()
except Exception:
pass

print(f"Finished at {datetime.now().strftime('%H:%M:%S')}. Saved: {out_path}")

if __name__ == "__main__":
main()

如果您想找到在检测到鸟类时向您的邮箱发送通知的代码,请使用以下代码。请注意,您需要将变量替换为相关的电子邮件地址和密码

import argparse
import os
import smtplib
import ssl
import time
from datetime import datetime
from email.message import EmailMessage
from email.utils import formatdate

import cv2
import numpy as np
import pandas as pd
import torch

SMTP_SERVER = "smtp.gmail.com"
SMTP_PORT = 587
GMAIL_SENDER = "youremail@gmail.com"
GMAIL_APP_PASSWORD = "xxxx xxxx xxxx xxxx"
GMAIL_RECIPIENT = "recipient@example.com"

CONFIDENCE_THRESHOLD = 0.35
NMS_IOU_THRESHOLD = 0.45
SNAPSHOT_DIR = "snapshots"
PREVIEW_WINDOW_NAME = "Bird Watch"

def send_email_with_image(subject, body, image_path):
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = GMAIL_SENDER
msg["To"] = GMAIL_RECIPIENT
msg["Date"] = formatdate(localtime=True)
msg.set_content(body)
if image_path and os.path.exists(image_path):
with open(image_path, "rb") as f:
data = f.read()
ext = os.path.splitext(image_path)[1].lower()
maintype, subtype = ("image", "jpeg") if ext in [".jpg", ".jpeg"] else ("image", "png")
msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=os.path.basename(image_path))
context = ssl.create_default_context()
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
server.ehlo()
server.starttls(context=context)
server.ehlo()
server.login(GMAIL_SENDER, GMAIL_APP_PASSWORD.replace(" ", ""))
server.send_message(msg)

def draw_boxes(frame, detections_df, color=(0, 255, 0)):
for _, row in detections_df.iterrows():
if row["name"] != "bird":
continue
x1, y1, x2, y2 = int(row["xmin"]), int(row["ymin"]), int(row["xmax"]), int(row["ymax"])
conf = float(row["confidence"])
label = f"bird {conf:.2f}"
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
cv2.putText(frame, label, (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2, cv2.LINE_AA)
return frame

def save_snapshot(frame, folder):
os.makedirs(folder, exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
path = os.path.join(folder, f"bird_{ts}.jpg")
cv2.imwrite(path, frame)
return path

def run(video_path, cooldown_s):
model = torch.hub.load("ultralytics/yolov5", "yolov5s", pretrained=True)
model.conf = CONFIDENCE_THRESHOLD
model.iou = NMS_IOU_THRESHOLD
model.classes = None
model.max_det = 300

cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise RuntimeError(f"Could not open video file: {video_path}")

last_email_time = 0.0

try:
while True:
ok, frame = cap.read()
if not ok:
break
results = model(frame, size=640)
df = results.pandas().xyxy[0]
bird_df = df[(df["name"] == "bird") & (df["confidence"] >= CONFIDENCE_THRESHOLD)]
display_frame = frame.copy()
if not bird_df.empty:
display_frame = draw_boxes(display_frame, bird_df)
cv2.imshow(PREVIEW_WINDOW_NAME, display_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
now = time.time()
if not bird_df.empty and (now - last_email_time >= cooldown_s):
snapshot_path = save_snapshot(display_frame, SNAPSHOT_DIR)
count = len(bird_df)
top_conf = float(bird_df["confidence"].max())
subject = f"[Bird Alert] {count} bird(s) detected - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
body = f"Detected {count} bird(s) with top confidence {top_conf:.2f}.\nSource: {video_path}\nSnapshot: {os.path.basename(snapshot_path)}\nCooldown: {cooldown_s} seconds\n"
try:
send_email_with_image(subject, body, snapshot_path)
last_email_time = now
except Exception as e:
print(f"Email send failed: {e}")
finally:
cap.release()
cv2.destroyAllWindows()

def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--video", type=str, default="input.mp4")
parser.add_argument("--cooldown", type=int, default=180)
args = parser.parse_args()
return args.video, args.cooldown

if __name__ == "__main__":
vid, cd = parse_args()
run(vid, cd)

如果您想使用能够生成特定时间段内鸟类数量图表的代码,请使用以下代码:

import argparse
import os
import csv
from datetime import datetime

import cv2
import pandas as pd
import torch
import matplotlib.pyplot as plt

CONFIDENCE_THRESHOLD = 0.35
NMS_IOU_THRESHOLD = 0.45
CSV_FILE = "bird_counts.csv"

def run(video_path):
model = torch.hub.load("ultralytics/yolov5", "yolov5s", pretrained=True)
model.conf = CONFIDENCE_THRESHOLD
model.iou = NMS_IOU_THRESHOLD

cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise RuntimeError(f"Could not open video file: {video_path}")

fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = 0

with open(CSV_FILE, mode="w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["time_seconds", "bird_count"])

while True:
ok, frame = cap.read()
if not ok:
break

results = model(frame, size=640)
df = results.pandas().xyxy[0]
bird_df = df[(df["name"] == "bird") & (df["confidence"] >= CONFIDENCE_THRESHOLD)]
bird_count = len(bird_df)

time_sec = frame_count / fps
writer.writerow([round(time_sec, 2), bird_count])

display_frame = frame.copy()
for _, row in bird_df.iterrows():
x1, y1, x2, y2 = int(row["xmin"]), int(row["ymin"]), int(row["xmax"]), int(row["ymax"])
conf = float(row["confidence"])
label = f"bird {conf:.2f}"
cv2.rectangle(display_frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(display_frame, label, (x1, max(0, y1 - 6)),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA)

cv2.imshow("Bird Tracking", display_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break

frame_count += 1

cap.release()
cv2.destroyAllWindows()

data = pd.read_csv(CSV_FILE)
plt.figure(figsize=(10, 5))
plt.plot(data["time_seconds"], data["bird_count"], marker="o")
plt.xlabel("Time (seconds)")
plt.ylabel("Number of birds")
plt.title("Birds detected over time")
plt.grid(True)
plt.savefig("bird_graph.png")
plt.show()

def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--video", type=str, default="input.mp4")
return parser.parse_args().video

if __name__ == "__main__":
video_file = parse_args()
run(video_file)


11

运行程序

要运行这段代码,只需输入“python3 文件名”,其中文件名是你的文件名。在我的例子中,文件名是 main.py。你看到的分析结果应该与附图类似。

需要注意的主要方面:

  1. 0.XY - 该值(其中 XY 为整数)表示模型对检测人员的置信度百分比。

请注意,此脚本还会将数据保存到 CSV 文件中,我们将在数据收集后的步骤中使用它。

视频地址:https://youtu.be/KmH63ENa5fA

12

数据收集后分析

如果你使用了最后一个示例代码,你会得到一个图表,显示一天中不同时间检测到的鸟类数量。这对观鸟者来说尤其有价值,因为许多鸟类都遵循可预测的群飞模式。通过研究这个图表,观鸟者可以在第二天类似的时间返回同一区域,很有可能再次看到相同的鸟类。

目前,我的项目可以检测到鸟类的出现,但我计划提高其识别鸟类种类的精度。例如,如果用户只想在鸽子出现时收到通知,我的模型就应该忽略其他类型的鸟类。

阅读记录0
点赞0
收藏0
禁止 本文未经作者允许授权,禁止转载
猜你喜欢
评论/提问(已发布 0 条)
评论 评论
收藏 收藏
分享 分享
pdf下载 下载
pdf下载 举报