将您的 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 等现代硬件平台来创造引人入胜的互动体验,从而弥合传统摄影和人工智能驱动的创造力之间的差距。
该项目的模块化设计使得尝试不同的人工智能模型、添加新功能或使其适应其他创意应用程序变得容易。无论您是对计算机视觉、物联网开发感兴趣,还是只是想创造一些独特的东西,这个项目都为进一步探索提供了坚实的基础。







项目代码
#include "unihiker_k10.h"
#include <HTTPClient.h>
#include <WiFi.h>
#include <WebSocketsClient.h>
#include <ArduinoJson.h>
#include <esp_camera.h>
// 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));
}
【Arduino 动手做】使用 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

评论