回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

【Arduino 动手做】使用 Unihiker K10 构建人工智能驱动的宝丽来相机 简单

头像 驴友花雕 2025.07.26 4 0

将您的 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 等现代硬件平台来创造引人入胜的互动体验,从而弥合传统摄影和人工智能驱动的创造力之间的差距。

该项目的模块化设计使得尝试不同的人工智能模型、添加新功能或使其适应其他创意应用程序变得容易。无论您是对计算机视觉、物联网开发感兴趣,还是只是想创造一些独特的东西,这个项目都为进一步探索提供了坚实的基础。

 

3D.png
Arduino.png
Film.png
Locket.png
Memento.jpg
Polaroid.png
Unihiker.png

项目代码

 

 

代码
#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
 

00189--.gif

评论

user-avatar
icon 他的勋章
    展开更多