LoginSignup
3
1

More than 3 years have passed since last update.

カメラモジュールOV7725の使い方(ESP32-WROVER-B)

Last updated at Posted at 2019-07-27

概要

以前OV2640を使って画像を取得したので、今回はOV7725を使って画像を取得してみました。OV7725はjpegではなく、bmp形式で画像が出力されるので、少し制御方法が異なります。

今回はブラウザ上でOV7725のレジスタを変更できるようにしました!結論から申し上げると、30万画素ではあまりきれいな写真は撮れませんでした。初期の携帯に搭載されていたカメラと同性能らしいです…
きれいな写真を撮りたい場合はラズベリーパイを使って、OV5640で撮影したりした方がいいかと思います。

te.jpg

DSC_0418.JPG

OV2640ではESP32-WROOM-32を使用しましたが、OV7725の場合はbmpで、メモリ使用量が増えるため、PSRAM搭載のESP32-WROVER-Bを使用しました。

公式
https://github.com/espressif/esp32-camera

たぶん公式に採用される前
https://github.com/igrr/esp32-cam-demo

ピン配置
https://www.instructables.com/id/The-Incredible-ESP32-Wrover-From-Espressif/

OV7725 データシート
https://cdn.sparkfun.com/datasheets/Sensors/LightImaging/OV7725.pdf

設定

esp-idfのフォルダ内でsubmoduleをクローンします。

git submodule add https://github.com/espressif/esp32-camera.git components/esp32-camera
mv esp-idf/components/esp32-camera/driver/private_include/sccb.h esp-idf/components/esp32-camera/driver/include/sccb.h
mv esp-idf/components/esp32-camera/driver/private_include/sensor.h esp-idf/components/esp32-camera/driver/include/sensor.h

注意点

  • driver公式のページ通りにbuildすると、camera_init()が重複していてエラーになったので、中身をapp_main関数に移動しました。
  • ピン配置は公式のものではなく、https://github.com/igrr/esp32-cam-demo を参考にしました。
  • XCLK:20MHz→6MHzへ変更

コード

esp-idfのexampleにあるsimple serverを改良し、ブラウザで「http://<ローカルアドレス>/」へアクセスすると撮影画像が表示されます。ajaxで画像データ(base64で変換したデータ)を取得し、ブラウザに表示しています。

全コードは下記に載せています。
https://github.com/koki-iwaizumi/esp32-ov7725

サイズはVGA、出力形式はYUV422にしています。wifi周りの設定は変更してください。

app_main.c

#include <string.h>
#include <esp_wifi.h>
#include <esp_event_loop.h>
#include <esp_log.h>
#include <esp_system.h>
#include <nvs_flash.h>
#include <sys/param.h>
#include <esp_http_server.h>
#include <esp_camera.h>

#include "mbedtls/base64.h"
#include "cJSON.h"

#include "sensor.h"
#include "sccb.h"

/********* io ***********/
#define CAM_PIN_PWDN    2
#define CAM_PIN_RESET   15
#define CAM_PIN_XCLK    27
#define CAM_PIN_SIOD    25
#define CAM_PIN_SIOC    23
#define CAM_PIN_VSYNC   22
#define CAM_PIN_HREF    26
#define CAM_PIN_PCLK    21
#define CAM_PIN_D9      19
#define CAM_PIN_D8      36
#define CAM_PIN_D7      18
#define CAM_PIN_D6      39
#define CAM_PIN_D5      5
#define CAM_PIN_D4      34
#define CAM_PIN_D3      32
#define CAM_PIN_D2      35

/********* wifi ***********/
#define EXAMPLE_WIFI_SSID "*"
#define EXAMPLE_WIFI_PASS "*"

static const char *TAG = "APP";
sensor_t* sensor = NULL;

static camera_config_t camera_config = {
    .pin_pwdn  = CAM_PIN_PWDN,
    .pin_reset = CAM_PIN_RESET,
    .pin_xclk = CAM_PIN_XCLK,
    .pin_sscb_sda = CAM_PIN_SIOD,
    .pin_sscb_scl = CAM_PIN_SIOC,

    .pin_d7 = CAM_PIN_D9,
    .pin_d6 = CAM_PIN_D8,
    .pin_d5 = CAM_PIN_D7,
    .pin_d4 = CAM_PIN_D6,
    .pin_d3 = CAM_PIN_D5,
    .pin_d2 = CAM_PIN_D4,
    .pin_d1 = CAM_PIN_D3,
    .pin_d0 = CAM_PIN_D2,
    .pin_vsync = CAM_PIN_VSYNC,
    .pin_href = CAM_PIN_HREF,
    .pin_pclk = CAM_PIN_PCLK,

    .xclk_freq_hz = 6000000,
    .ledc_timer = LEDC_TIMER_0,
    .ledc_channel = LEDC_CHANNEL_0,

    .pixel_format = PIXFORMAT_YUV422,
    .frame_size = FRAMESIZE_VGA,

    .jpeg_quality = 12,
    .fb_count = 1
};

esp_err_t index_get_handler(httpd_req_t *req)
{
    extern const unsigned char index_start[] asm("_binary_index_html_start");
    extern const unsigned char index_end[]   asm("_binary_index_html_end");
    const size_t index_size = (index_end - index_start);

    httpd_resp_send_chunk(req, (const char *)index_start, index_size);
    httpd_resp_sendstr_chunk(req, NULL);

    return ESP_OK;
}

esp_err_t image_post_handler(httpd_req_t *req)
{
    int content_buf_size = 10 * 1000;
    char *content_buf = calloc(content_buf_size, sizeof(char));
    if(content_buf == NULL){
        ESP_LOGE(TAG, "Failed to allocate frame buffer - content_buf");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    if(httpd_req_recv(req, content_buf, req->content_len) <= 0){
        ESP_LOGE(TAG, "httpd_req_recv failed");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    ESP_LOGE(TAG, "httpd_req_recv content=%s", content_buf);

    cJSON *content_json = cJSON_Parse(content_buf);

    for(int i = 0; i <= 172; i++){
        char key[4];
        itoa(i,key,10);
        int value = strtol(cJSON_GetObjectItem(content_json, key)->valuestring, NULL, 16);

        SCCB_Write(sensor->slv_addr, i, value);
    }

    cJSON_Delete(content_json);
    free(content_buf);

    camera_fb_t *fb = NULL;
    esp_err_t res = ESP_OK;
    int64_t fr_start = esp_timer_get_time();

    fb = esp_camera_fb_get();
    if( ! fb){
        ESP_LOGE(TAG, "Camera capture failed");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    uint8_t *buf = NULL;
    size_t buf_len = 0;
    bool converted = frame2bmp(fb, &buf, &buf_len);
    esp_camera_fb_return(fb);
    if( ! converted){
        ESP_LOGE(TAG, "BMP conversion failed");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    ESP_LOGI(TAG, "esp_get_free_heap_size (%d bytes)", esp_get_free_heap_size());
    ESP_LOGI(TAG, "esp_get_minimum_free_heap_size (%d bytes)", esp_get_minimum_free_heap_size());
    ESP_LOGI(TAG, "heap_caps_get_free_size(MALLOC_CAP_8BIT) (%d bytes)", heap_caps_get_free_size(MALLOC_CAP_8BIT));
    ESP_LOGI(TAG, "heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT) (%d bytes)", heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT));
    ESP_LOGI(TAG, "heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) (%d bytes)", heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));

    int image_buf_size = 1300 * 1000;
    uint8_t *image_buf = calloc(image_buf_size, sizeof(char));
    if(image_buf == NULL){
        free(buf);
        ESP_LOGE(TAG, "Failed to allocate frame buffer - image_buf");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    size_t olen = 0;
    int base64_err = mbedtls_base64_encode(image_buf, image_buf_size, &olen, buf, buf_len);
    free(buf);
    if (base64_err != 0) {
        ESP_LOGE(TAG, "error base64 encoding, error %d, buff size: %d", base64_err, olen);
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    ESP_LOGI(TAG, "esp_get_free_heap_size (%d bytes)", esp_get_free_heap_size());
    ESP_LOGI(TAG, "esp_get_minimum_free_heap_size (%d bytes)", esp_get_minimum_free_heap_size());
    ESP_LOGI(TAG, "heap_caps_get_free_size(MALLOC_CAP_8BIT) (%d bytes)", heap_caps_get_free_size(MALLOC_CAP_8BIT));
    ESP_LOGI(TAG, "heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT) (%d bytes)", heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT));
    ESP_LOGI(TAG, "heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) (%d bytes)", heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));

    uint8_t *image_json = calloc(image_buf_size, sizeof(char));
    if(image_json == NULL){
        free(image_buf);
        ESP_LOGE(TAG, "Failed to allocate frame buffer - image_json");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    sprintf((char *)image_json, "{\"image\":\"%s\"}", (const char *)image_buf);
    free(image_buf);
    ESP_LOGI(TAG, "image_json length=%d", strlen((const char*)image_json));

    res = httpd_resp_set_type(req, "application/json") || httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*") || httpd_resp_send(req, (const char *)image_json, strlen((const char*)image_json));
    free(image_json);

    int64_t fr_end = esp_timer_get_time();
    ESP_LOGI(TAG, "BMP: %uKB %ums", (uint32_t)(buf_len/1024), (uint32_t)((fr_end - fr_start)/1000));
    return res;
}

esp_err_t config_get_handler(httpd_req_t *req)
{
    int data_json_size = 10 * 1000;
    char *data_json = calloc(data_json_size, sizeof(char));
    if(data_json == NULL){
        ESP_LOGE(TAG, "Failed to allocate frame buffer - data_json");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    strcat(data_json, "{");
    for(int i = 0; i <= 172; i++){
        char data_json_buf[255] = {'\0'};
        sprintf(data_json_buf, "\"%d\":\"%d\"", i, SCCB_Read(sensor->slv_addr, i));
        strcat(data_json, data_json_buf);
        if(i != 172) strcat(data_json, ",");
    }
    strcat(data_json, "}");

    ESP_LOGI(TAG, "data_json=%s", data_json);
    ESP_LOGI(TAG, "data_json length=%d", strlen((const char*)data_json));

    esp_err_t res = httpd_resp_set_type(req, "application/json") || httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*") || httpd_resp_send(req, (const char *)data_json, strlen((const char*)data_json));
    free(data_json);

    return res;
}

httpd_uri_t index_uri = {
    .uri = "/",
    .method = HTTP_GET,
    .handler = index_get_handler,
};

httpd_uri_t image_uri = {
    .uri = "/image",
    .method = HTTP_POST,
    .handler = image_post_handler,
};

httpd_uri_t config_uri = {
    .uri = "/config",
    .method = HTTP_GET,
    .handler = config_get_handler,
};

httpd_handle_t start_webserver(void)
{
    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();

    ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
    if(httpd_start(&server, &config) == ESP_OK){
        ESP_LOGI(TAG, "Registering URI handlers");
        httpd_register_uri_handler(server, &index_uri);
        httpd_register_uri_handler(server, &image_uri);
        httpd_register_uri_handler(server, &config_uri);
        return server;
    }

    ESP_LOGI(TAG, "Error starting server!");
    return NULL;
}

void stop_webserver(httpd_handle_t server)
{
    httpd_stop(server);
}

static esp_err_t event_handler(void *ctx, system_event_t *event)
{
    httpd_handle_t *server = (httpd_handle_t *) ctx;

    switch(event->event_id){
        case SYSTEM_EVENT_STA_START:
            ESP_LOGI(TAG, "SYSTEM_EVENT_STA_START");
            ESP_ERROR_CHECK(esp_wifi_connect());
            break;
        case SYSTEM_EVENT_STA_GOT_IP:
            ESP_LOGI(TAG, "SYSTEM_EVENT_STA_GOT_IP");
            ESP_LOGI(TAG, "Got IP: '%s'", ip4addr_ntoa(&event->event_info.got_ip.ip_info.ip));

            if(*server == NULL){
                *server = start_webserver();
            }
            break;
        case SYSTEM_EVENT_STA_DISCONNECTED:
            ESP_LOGI(TAG, "SYSTEM_EVENT_STA_DISCONNECTED");
            ESP_ERROR_CHECK(esp_wifi_connect());

            if(*server){
                stop_webserver(*server);
                *server = NULL;
            }
            break;
        default:
            break;
    }
    return ESP_OK;
}

static void initialise_wifi(void *arg)
{
    tcpip_adapter_init();
    ESP_ERROR_CHECK(esp_event_loop_init(event_handler, arg));
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = EXAMPLE_WIFI_SSID,
            .password = EXAMPLE_WIFI_PASS,
        },
    };
    ESP_LOGI(TAG, "Setting WiFi configuration SSID %s...", wifi_config.sta.ssid);
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());
}

void app_main()
{
    static httpd_handle_t server = NULL;
    ESP_ERROR_CHECK(nvs_flash_init());

    esp_err_t err = esp_camera_init(&camera_config);
    if(err != ESP_OK){
        ESP_LOGE(TAG, "Camera Init Failed");
        return;
    }

    sensor = esp_camera_sensor_get();

    initialise_wifi(&server);
}

HTML周り

index.html

<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.4/css/all.css">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <title>OV7725</title>
        <style>
            .alert{
                display:none;
            }
            #overlay{   
                position: fixed;
                top: 0;
                z-index: 100;
                width: 100%;
                height:100%;
                display: none;
                background: rgba(0,0,0,0.6);
            }
            .cv-spinner{
                height: 100%;
                display: flex;
                justify-content: center;
                align-items: center;  
            }
            .spinner{
                width: 40px;
                height: 40px;
                border: 4px #ddd solid;
                border-top: 4px #2e93e6 solid;
                border-radius: 50%;
                animation: sp-anime 0.8s infinite linear;
            }
            @keyframes sp-anime{
                0% { 
                    transform: rotate(0deg); 
                }
                100% { 
                    transform: rotate(359deg); 
                }
            }
            .is-hide{
                display:none;
            }
            .ov_a{
                height:600px;
                overflow-y:auto;
            }
        </style>
    </head>
    <body>

        <div id="overlay">
            <div class="cv-spinner">
                <span class="spinner"></span>
            </div>
        </div>

        <div class="text-center mt-3 mb-3">
            <h1>OV7725</h1>
        </div>

        <div class="container-fluid">
            <div class="row">
                <div class="col-lg-6 text-center mb-4">
                    <div class="row">
                        <div class="col-md-12">
                            <div class="alert alert-success" role="alert">success</div>
                            <div class="alert alert-danger" role="alert">failed</div>
                        </div>
                        <div class="col-md-12">
                            <img id="img" class="mw-100">
                        </div>
                        <div class="col-md-12 mt-4">
                            <button id="image_button" type="button" class="btn btn-success w-100">Take a Picture</button>
                        </div>
                    </div>
                </div>

                <div class="col-lg-6">
                    <div class="ov_a">
                        <form id="form">
                            <table id="table_regs" class="table table-striped">
                                <tr>
                                    <th>Address(Hex)</th>
                                    <th>Value(Hex)</th>
                                </tr>
                            </table>
                        </form>
                    </div>
                </div>
            </div>
        </div>

        <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>

        <script type="text/javascript">
            $(function(){

                var parseJson = function(data) {
                    var returnJson = {};
                    for(idx = 0; idx < data.length; idx++){
                    returnJson[data[idx].name] = data[idx].value
                    }
                    return returnJson;
                };

                $(document).ready(function(){
                    read_config();
                    get_image();
                });

                $("#image_button").on("click", get_image);

                function get_image(){
                    $.ajax({
                        url: "./image",
                        type: "POST",
                        data: JSON.stringify(parseJson($("#form").serializeArray())),
                        dataType: "json",
                        beforeSend: function() {
                            $(".alert").css("display", "none");
                            $("#overlay").fadeIn(300);
                        }
                    })
                    .done((data) => {
                        $(".alert").css("display", "none");
                        $(".alert-success").css("display", "block");
                        $("#img").attr("src", "data:image/jpeg;base64," + data["image"]);
                    })
                    .fail((data) => {
                        $(".alert").css("display", "none");
                        $(".alert-danger").css("display", "block");
                        console.log(data);
                    })
                    .always((data) => {
                        setTimeout(function(){
                            $("#overlay").fadeOut(300);
                        },500);
                    });
                }

                function read_config(){
                    $.ajax({
                        url: "./config",
                        type: "GET",
                        dataType: "json",
                        async: false,
                    })
                    .done((data) => {
                        $.each(data, function(i, item){
                            $("#table_regs").append('<tr><td>0x' + Number(i).toString(16) + '</td><td><div class="input-group"><div class="input-group-prepend"><span class="input-group-text" id="text1a">0x</span></div><input name="' + i + '" value="' + Number(item).toString(16) + '" type="text" class="form-control"></div></td></tr>');
                        });
                    })
                }
            });
        </script>
    </body>
</html>

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1