【Arduino 动手做】使用K10构建人工智能驱动的宝丽来相机
2025-07-261722
精华项目
AI 快速预览详细
该项目介绍如何使用Unihiker K10开发板构建一款人工智能驱动的宝丽来相机。通过Google Gemini API和ImageRouter API,用户可以实时拍摄照片并应用AI生成的艺术效果。项目包括硬件组装、软件设置及AI后端配置。最终,用户将获得一款具有直观界面和实时处理功能的AI宝丽来相机,适合青少年创客教育。
|
将您的 Unihiker K10 变****工智能驱动的宝丽来相机,可以实时拍摄照片并应用令人惊叹的人工智能生成效果!该项目结合了 Google 的 Gemini AI 和 ImageRouter 的强大功能,创造了一种神奇的摄影体验,让每张照片都成为一件艺术品。 该设备具有直观的界面,带有物理按钮、RGB 灯光效果和摇动捕捉机制,使摄影感觉自然且引人入胜。当您拍摄照片时,它会通过人工智能自动处理,以创造独特的艺术诠释。 您将构建的内容 人工智能相机:拍照并应用人工智能生成的艺术效果 交互界面:物理按钮和抖动检测,实现自然交互 实时处理:连接WiFi的后端处理AI处理 视觉反馈:RGB 灯光和屏幕动画指导用户体验 3D 打印外壳:定制外壳,感觉就像真正的宝丽来相机 硬件要求 Unihiker K10 - 主开发板(在这里购买) 3D 打印机 - 用于打印定制外壳 SD 卡 - 最小 512MB(FAT32 格式) 计算机 - 用于 Arduino IDE 和服务器设置 USB 电缆 - 用于编程和电源 关于 Unihiker K10 Unihiker K10 是一款集成了以下功能的人工智能学习设备: 2.8英寸彩色触摸屏 内置摄像头 WiFi 和蓝牙连接 RGB LED 灯 加速度计(用于抖动检测) 扬声器和麦克风 温度、湿度和光传感器 用于附加传感器的边缘连接器 软件要求 Arduino IDE - 用于固件开发 带有 uv 包管理器的 Python - 用于 AI 后端服务器 Google Gemini API 密钥 - 用于 AI 图像处理 ImageRouter API 密钥 - 用于其他图像效果 开始之前 您可以在此存储库中找到项目文件和 3D 打印:https://github.com/pham-tuan-binh/memento 该存储库还包含一系列不同项目的代码和说明。 第 1 步:硬件组装 该项目包括三个主要的 3D 打印组件: 1. 车身 - Unihiker K10 的主外壳 2. 背板 - 覆盖背面(选择带螺丝或不带螺丝) 3. Button - 交互式按钮组件 打印设置: 材质:推荐 PLA 或 PETG 填充物:所有零件 15% 支持:为悬垂启用 层高:0.2mm,质量好 方向:打印体,大平面朝下 组装步骤: 1. 先将按钮插入侧面的孔中 2. 将 Unihiker K10 放入机身,将 Type-C 端口与其孔对齐 3. 将背板安装到设备背面 4.插入SD卡(必须在开机前完成) 第 2 步:软件设置 Arduino IDE 配置 1. 安装 Arduino IDE(如果尚未安装) 2. 设置 Unihiker K10 板支持: 遵循 Unihiker 官方文档 注意:如果遇到文档错误,请检查更新的板包 3. 打开宝丽来项目: 打开 Arduino IDE 转到文件→ 打开 导航到项目/宝丽来/ 选择 polaroid.ino WiFi 配置 1. 在您的项目中打开 wifi_helper.ino 2. 更新 WiFi 凭据: const char *WIFI_SSID = "YOUR_WIFI_NETWORK_NAME"; const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; 3.保存文件 服务器 IP 配置 1. 打开 gen_ai_helper.ino 2. 使用您计算机的 IP 地址更新服务器 URL: const char *SERVER_URL = "http://YOUR_COMPUTER_IP:8000/upload_adv"; 要查找您计算机的 IP: Windows: 在命令提示符下运行 ipconfig Mac/Linux: 在终端中运行 ifconfig 或 ip addr 查找您的本地网络 IP(通常以“192.168”或“10”开头)。 第 3 步:后端服务器设置 安装uv包管理器 # Windows: powershell -c "irm https://astral.sh/uv/install.ps1 | iex # macOS/Linux curl -LsSf https://astral.sh/uv/install.sh | sh 设置 API 密钥 1. 导航到服务器目录: cd project/polaroid/server 2. 复制环境模板: cp .example.env .env 3. 使用您的 API 密钥编辑 .env 文件: GEMINI_API_KEY=your_actual_gemini_api_key_here IMAGEROUTER_API_KEY=your_actual_imagerouter_api_key_here 获取 API 密钥 谷歌双子座 API: 1. 前往 Google AI Studio 2. 使用您的 Google 帐户登录 3. 单击“获取 API 密钥”或导航至 API 密钥部分 4. 创建新的 API 密钥 5. 将其复制并粘贴到您的“.env”文件中 ImageRouter API: 1. 转到 ImageRouter 2. 注册一个帐户 3. 导航到您的 API 密钥部分 4. 生成新的 API 密钥 5. 将其复制并粘贴到您的“.env”文件中 启动服务器 1. 运行服务器: uv run main.py 2. 通过打开浏览器来验证它是否正常工作:http://localhost:8000/docs 这显示了 FastAPI 文档界面,您可以在其中测试端点。 第 4 步:SD 卡设置 准备SD卡 1. 将 SD 卡格式化为 FAT32 2. 复制存储文件: 将“storage/”目录的全部内容复制到 SD 卡的根目录 确保保留文件夹结构 3. 开机前将 SD 卡插入 Unihiker K10 宝丽来项目所需文件: 存储/宝丽来/ - UI 图像(begin.jpg、loading.jpg、shake.jpg) 存储/shutter.wav - 相机快门声音 存储/loading.wav - 处理声音 第 5 步:上传固件 编译和上传 1. 通过 USB 将 Unihiker K10 连接到计算机 2. 选择正确的板:Unihiker K10 3.选择正确的端口 4. 上传代码 5. 等待完成 - 设备将自动重启 第 6 步:测试您的 AI 宝丽来相机 开机和测试 1. 打开设备电源 - 它应该显示“开始”屏幕 2. 验证 WiFi 连接 - 设备应连接到您的网络 3. 测试相机 - 按按钮 A 启动相机模式 使用相机 1. 开始:按按钮A进入相机模式 2. 拍摄:再次按按钮 A 拍照 3. 处理:在 AI 处理图像时观看加载动画 4. 摇动显示:摇动设备即可查看 AI 生成的结果 5. 比较:按按钮 B 在原始图像和 AI 处理后的图像之间切换 6. 新照片:按按钮 A 拍摄另一张照片 视觉反馈: RGB 灯:加载时紫/蓝波,摇晃时白闪 屏幕状态:不同的图像引导您完成每个步骤 音效:拍摄时有快门声,处理时加载声音 结论 祝贺!您已经成功打造了一款人工智能驱动的宝丽来相机,将即时摄影的怀旧之情与尖端的人工智能技术相结合。该项目展示了如何使用 Unihiker K10 等现代硬件平台来创造引人入胜的互动体验,从而弥合传统摄影和人工智能驱动的创造力之间的差距。 该项目的模块化设计使得尝试不同的人工智能模型、添加新功能或使其适应其他创意应用程序变得容易。无论您是对计算机视觉、物联网开发感兴趣,还是只是想创造一些独特的东西,这个项目都为进一步探索提供了坚实的基础。 ![]() ![]() ![]() ![]() ![]() ![]() ![]() |











项目链接:https://www.hackster.io/phamtuanbinh1504/build-an-ai-powered-polaroid-camera-with-unihiker-k10-1f1585
项目作者:Pham Binh
项目视频 :https://www.youtube.com/watch?v=p7J4b8OQXAY
项目代码:https://github.com/pham-tuan-binh/memento
3D 文件:https://github.com/pham-tuan-binh/memento/tree/main/models
Unihiker K10 文档:https://www.unihiker.com/wiki/K10/
Google Gemini API 文档:https://ai.google.dev/
ImageRouter API 文档:https://imagerouter.io/
FastAPI 文档:https://fastapi.tiangolo.com/
项目仓库:https://github.com/pham-tuan-binh/memento
快乐的建造和快乐的快照!
https://github.com/pham-tuan-binh/memento
[code]#include "unihiker_k10.h"
#include
#include
#include
#include
#include
// Server configuration
const char* SERVER_HOST = "10.8.162.58";
const int SERVER_PORT = 7860;
String USER_ID = "550e8400-e29b-41d4-a716-446655440000";
// Wifi Configuration
const char* ssid = "PUT YOUR WIFI SSID HERE";
const char* password = "PUT YOUR WIFI PASSWORD HERE";
// Create UNIHIKER K10 instance
UNIHIKER_K10 board;
// WebSocket and HTTP clients
WebSocketsClient webSocket;
HTTPClient httpClient;
// Connection state
bool serverConnected = false;
bool waitingForFrameRequest = false;
// Task handles
TaskHandle_t websocketTaskHandle = NULL;
TaskHandle_t streamingTaskHandle = NULL;
TaskHandle_t cameraTaskHandle = NULL;
// LVGL
extern SemaphoreHandle_t xLvglMutex;
// Display object
lv_obj_t *diffusedImageObject = NULL;
// Camera queue and global frame storage
QueueHandle_t xQueueCamera = NULL;
camera_fb_t *globalFrame = NULL;
SemaphoreHandle_t frameMutex = NULL;
bool frameReady = false;
// WebSocket event handler
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
switch(type) {
case WStype_DISCONNECTED:
Serial.println("WebSocket Disconnected - attempting reconnect...");
serverConnected = false;
break;
case WStype_CONNECTED:
Serial.printf("WebSocket Connected to: %s\n", payload);
serverConnected = true;
break;
case WStype_TEXT: {
Serial.printf("Received: %s\n", payload);
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
String status = doc["status"];
if (status == "send_frame") {
waitingForFrameRequest = true;
}
break;
}
case WStype_ERROR:
Serial.printf("WebSocket Error: %s\n", payload);
break;
case WStype_FRAGMENT_TEXT_START:
case WStype_FRAGMENT_BIN_START:
case WStype_FRAGMENT:
case WStype_FRAGMENT_FIN:
Serial.println("WebSocket Fragment received");
break;
default:
Serial.printf("WebSocket event type: %d\n", type);
break;
}
}
// Continuous camera task - always consuming frames
void cameraTask(void* parameter) {
camera_fb_t *frame = NULL;
while (true) {
// Aggressively consume ALL frames from queue to prevent overflow
while (xQueueReceive(xQueueCamera, &frame, pdMS_TO_TICKS(1))) {
// Take mutex and update global frame
if (xSemaphoreTake(frameMutex, pdMS_TO_TICKS(1)) == pdTRUE) {
// Release previous global frame if exists
if (globalFrame != NULL) {
esp_camera_fb_return(globalFrame);
}
// Store new frame globally
globalFrame = frame;
frameReady = true;
xSemaphoreGive(frameMutex);
// Only print frame info occasionally
static uint32_t frameCount = 0;
if (frameCount % 100 == 0) {
Serial.printf("Frame #%d: %dx%d, %d bytes\n", frameCount, frame->width, frame->height, frame->len);
}
frameCount++;
} else {
// If can't get mutex, just return the frame to prevent memory leak
esp_camera_fb_return(frame);
}
}
vTaskDelay(pdMS_TO_TICKS(10)); // Fast consumption rate
}
}
// WebSocket task
void websocketTask(void* parameter) {
unsigned long lastSendTime = 0;
unsigned long lastDebugTime = 0;
const unsigned long sendInterval = 250; // Send frame every 1000ms (1 second)
const unsigned long debugInterval = 5000; // Debug print every 5 seconds
while (true) {
if (WiFi.status() == WL_CONNECTED) {
webSocket.loop();
// Always try to send frames when connected and frame is ready
if (serverConnected && frameReady && waitingForFrameRequest) {
Serial.println("Sending frame to server...");
sendCameraFrame();
waitingForFrameRequest = false;
lastSendTime = millis();
} else if (!serverConnected && (millis() - lastDebugTime > debugInterval)) {
Serial.println("DEBUG: Server not connected");
lastDebugTime = millis();
}
} else {
Serial.println("WiFi disconnected, attempting reconnect...");
connectToWiFi();
}
vTaskDelay(pdMS_TO_TICKS(50)); // Check more frequently
}
}
// Send camera frame to server
void sendCameraFrame() {
if (xSemaphoreTake(frameMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
if (globalFrame != NULL && frameReady) {
// Send next_frame status
DynamicJsonDocument statusDoc(256);
statusDoc["status"] = "next_frame";
String statusJson;
serializeJson(statusDoc, statusJson);
webSocket.sendTXT(statusJson);
// Send parameters for diffusion
DynamicJsonDocument paramsDoc(512);
paramsDoc["strength"] = 0.8;
paramsDoc["guidance_scale"] = 7.5;
paramsDoc["prompt"] = "Portrait of The Joker halloween costume, face painting, with , glare pose, detailed, intricate, full of colour, cinematic lighting, trending on artstation, 8k, hyperrealistic, focused, extreme details, unreal engine 5 cinematic, masterpiece";
String paramsJson;
serializeJson(paramsDoc, paramsJson);
webSocket.sendTXT(paramsJson);
// Convert RGB565 to JPEG if needed
uint8_t *jpeg_buf = NULL;
size_t jpeg_len = 0;
bool conversion_success = false;
if (globalFrame->format == PIXFORMAT_RGB565) {
// Convert RGB565 to JPEG
conversion_success = fmt2jpg(globalFrame->buf, globalFrame->len, globalFrame->width, globalFrame->height,
PIXFORMAT_RGB565, 80, &jpeg_buf, &jpeg_len);
if (conversion_success) {
webSocket.sendBIN(jpeg_buf, jpeg_len);
free(jpeg_buf); // Free the allocated JPEG buffer
}
} else {
// Send raw data if already JPEG
webSocket.sendBIN(globalFrame->buf, globalFrame->len);
conversion_success = true;
}
if (conversion_success) {
Serial.printf("Frame sent successfully (%d bytes)\n", jpeg_len > 0 ? jpeg_len : globalFrame->len);
} else {
Serial.println("ERROR: Failed to convert frame to JPEG");
}
}
xSemaphoreGive(frameMutex);
}
}
// Streaming task to receive diffused images
void streamingTask(void* parameter) {
while (true) {
if (WiFi.status() == WL_CONNECTED && serverConnected) {
receiveDiffusedImage();
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
// Receive diffused image from server
void receiveDiffusedImage() {
String streamUrl = "http://" + String(SERVER_HOST) + ":" + String(SERVER_PORT) + "/api/stream/" + USER_ID;
httpClient.begin(streamUrl);
httpClient.setTimeout(2000);
int httpCode = httpClient.GET();
if (httpCode == HTTP_CODE_OK) {
WiFiClient* stream = httpClient.getStreamPtr();
String boundary = "--frame";
String line;
while (httpClient.connected()) {
if (stream->available()) {
line = stream->readStringUntil('\n');
if (line.indexOf(boundary) >= 0) {
// Skip headers
while (stream->available()) {
line = stream->readStringUntil('\n');
if (line.length() <= 2) break;
}
// Read diffused image data
if (stream->available()) {
processDiffusedImage(stream);
}
}
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
httpClient.end();
}
// Process diffused image
void processDiffusedImage(WiFiClient* stream) {
static uint8_t imageBuffer[50000];
size_t bytesRead = 0;
while (stream->available() && bytesRead < sizeof(imageBuffer)) {
int byte = stream->read();
if (byte == -1) break;
imageBuffer[bytesRead++] = byte;
}
if (bytesRead > 0) {
displayDiffusedImage(imageBuffer, bytesRead);
}
}
// Display diffused image on screen
void displayDiffusedImage(uint8_t* imageData, size_t dataSize) {
xSemaphoreTake(xLvglMutex, portMAX_DELAY);
static lv_img_dsc_t diffused_img;
diffused_img.header.cf = LV_IMG_CF_TRUE_COLOR;
diffused_img.header.always_zero = 0;
diffused_img.header.w = 240;
diffused_img.header.h = 320;
diffused_img.data_size = dataSize;
diffused_img.data = imageData;
lv_img_set_src(diffusedImageObject, &diffused_img);
xSemaphoreGive(xLvglMutex);
}
// WiFi connection function
void connectToWiFi() {
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Connected! IP: ");
Serial.println(WiFi.localIP());
}
// Camera initialization function
void initCamera() {
// Create frame mutex
frameMutex = xSemaphoreCreateMutex();
// Initialize camera using UNIHIKER K10 system with RGB565 format
if (!xQueueCamera) {
xQueueCamera = xQueueCreate(5, sizeof(camera_fb_t *)); // Larger queue to prevent overflow
register_camera(PIXFORMAT_RGB565, FRAMESIZE_QVGA, 2, xQueueCamera);
Serial.println("Camera initialized successfully");
}
}
void setup() {
Serial.begin(115200);
// Initialize board and screen
board.begin();
board.initScreen();
// Black background
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_black(), LV_PART_MAIN);
// Create image display object
diffusedImageObject = lv_img_create(lv_scr_act());
lv_obj_set_pos(diffusedImageObject, 0, 0);
lv_obj_set_size(diffusedImageObject, 240, 320);
// Initialize camera
initCamera();
// Connect to WiFi
connectToWiFi();
// Wait a bit for WiFi to stabilize
delay(2000);
// Test server connectivity first
Serial.printf("Testing server connectivity to %s:%d\n", SERVER_HOST, SERVER_PORT);
HTTPClient testClient;
testClient.begin("http://" + String(SERVER_HOST) + ":" + String(SERVER_PORT) + "/api/queue");
int httpCode = testClient.GET();
if (httpCode > 0) {
String response = testClient.getString();
Serial.printf("Server test response: %d - %s\n", httpCode, response.c_str());
} else {
Serial.printf("Server test failed: %d\n", httpCode);
}
testClient.end();
Serial.printf("Connecting to WebSocket: ws://%s:%d/api/ws/%s\n", SERVER_HOST, SERVER_PORT, USER_ID.c_str());
// Initialize WebSocket connection
webSocket.begin(SERVER_HOST, SERVER_PORT, "/api/ws/" + USER_ID);
webSocket.onEvent(webSocketEvent);
webSocket.setReconnectInterval(5000);
webSocket.enableHeartbeat(15000, 3000, 2); // Enable heartbeat to keep connection alive
// Start tasks
xTaskCreatePinnedToCore(cameraTask, "Camera", 6144, NULL, 4, &cameraTaskHandle, 1); // Even higher priority camera task
xTaskCreatePinnedToCore(websocketTask, "WebSocket", 8192, NULL, 2, &websocketTaskHandle, 0);
xTaskCreatePinnedToCore(streamingTask, "Streaming", 10240, NULL, 1, &streamingTaskHandle, 1); // Lower priority for streaming
Serial.println("Setup complete!");
}
void loop() {
// Handle LVGL
xSemaphoreTake(xLvglMutex, portMAX_DELAY);
lv_task_handler();
xSemaphoreGive(xLvglMutex);
vTaskDelay(pdMS_TO_TICKS(5));
}[/code]