0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ESP32でお気に入りの写真チェンジャーを作る

Last updated at Posted at 2021-02-12

ちょっとだけ脱線して、ESP32でお気に入りの写真チェンジャーを作ってみます。
よく、Googleで単語での検索に加えて、画像やイラストを検索していますよね。
そこで、お気に入りの有名人の写真画像を検索して、それをESP32のLCDに表示し、しかもそれを定期的に別の画像に切り替えます。

image.png

もろもろのソースコードをGitHubに置いておきました。

poruruba/FavoriteGallery

Google Custom Searchの取得

まずは、カスタム検索エンジンを作成します。

Google Programmable Search
https://cse.google.com/cse/all

image.png

追加ボタンを押下。
とりあえず、検索するサイトに例えば「www.google.com」と入力。言語を日本語。検索エンジン名を例えば「Google画像検索」と入力し作成ボタンを押下します。

image.png

「コントロールパネル」ボタンを押下

image.png

検索エンジンIDをメモる。
画像検索をOn

image.png

検索するサイトを削除して空にする。
ウェブ全体を検索をOn

image.png

 Custom Search JSON API
 1 日あたり 10,000 クエリを上限とします。

の「使ってみる」ボタンを押下。

image.png

Get a Keyボタンを押下。
自身が持つ、いずれかのプロジェクトを選択して、NEXTを押下。

image.png

生成されたYOUR API KEYをメモります。

image.png

※場合によっては、作成されたAPIキーの利用アプリケーションの種類が制限されているかもしれません。その場合は以下から適切なものに変更してください。

#画像検索サーバ

以下のnpmモジュールを使います。

googleapis/google-api-nodejs-client
 https://github.com/googleapis/google-api-nodejs-client

Googleカスタム検索エンジンを呼ぶ際に使います。

node-fetch/node-fetch
 https://github.com/node-fetch/node-fetch

HTTP Getで画像ファイルをダウンロードする際に使います。

lovell/sharp
 https://github.com/lovell/sharp
 https://sharp.pixelplumbing.com/

取得した画像ファイルをLCDのサイズに合わせて縮小し、JPEGファイルを生成します。

server/api/controller/search-image/index.js
'use strict';

const SEARCH_API_KEY = process.env.SEARCH_API_KEY || '【APIキー】';
const SEARCH_CSE_ID = process.env.SEARCH_CSE_ID || '【検索エンジンID】';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const BinResponse = require(HELPER_BASE + 'binresponse');

const sharp = require('sharp')
const fetch = require('node-fetch');
const { google } = require('googleapis');
const customSearch = google.customsearch('v1');

exports.handler = async (event, context, callback) => {
	if( event.path == '/search-image'){
		console.log(event.queryStringParameters);

		var keyword = event.queryStringParameters.keyword || 'ミニオン';
		var num = event.queryStringParameters.num || 10;
		var width = event.queryStringParameters.width || 320;
		var height = event.queryStringParameters.height || 240;

		var link = await search_image(keyword, num);
		console.log(link);

		var buffer = await download_image(link, width, height);

		return new BinResponse('image/jpeg', buffer);
	}
};

async function search_image(keyword, num = 10){
	var index = Math.floor(Math.random() * num);
	const result = await customSearch.cse.list({
		cx: SEARCH_CSE_ID,
		q: keyword,
		auth: SEARCH_API_KEY,
		searchType: 'image',
		safe: 'high',
		num: 1, // max:10
		start: index + 1,
	});

	return result.data.items[0].link;
}

async function download_image(url, width, height){
  const blob = await fetch(url)
  .then(response =>{
    if( !response.ok )
      throw 'status is not 200';
    return response.blob();
  });
	
  const buffer = await blob.arrayBuffer();

return sharp(new Uint8Array(buffer))
  .resize({ width: width, height: height })
  .toFormat('jpeg')
  .toBuffer();
}

以下は先ほど取得したものです。
・【APIキー】
・【検索エンジンID】

「/search-image」というエンドポイントを立ち上げています。

以下、大したことはやっていないのですが、、、

・async function search_image(keyword, num = 10)

Googleカスタム検索エンジンを使って、キーワードに関する画像を検索します。numという入力がありますが、例えば、num個の検索結果からランダムに選ぶようにしています。これにより、同じキーワードでも呼び出すたびに違う画像が取得されるようになります。

・async function download_image(url, width, height)

node-fetchを使ってHTTP Getで画像をダウンロードし、sharpを使ってLCDサイズに合わせて縮小し、JPEG画像を出力しています。

#Arduino

大したことはやっていないのですが、20分ごとに、HTTP Getで先ほど立ち上げたサーバにキーワードを引数にして要求し、取得したJPEGファイルをLCDに表示させています。

以下のライブラリを使っています。(いつもありがとうございます)

lovyan03/LovyanGFX
 https://github.com/lovyan03/LovyanGFX

FavoriteGallery/src/main.c
#include <Arduino.h>
#include <LovyanGFX.hpp>
#include "LGFX_Config_TTGO_TMusic.hpp"
#include <WiFi.h>
#include <HTTPClient.h>

const char* keyword = "【キーワード】";
const char* base_url = "【サーバのURL】/search-image";
const char* wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char* wifi_password = "【WiFiアクセスポイントのパスワード】";
#define UPDATE_INTERVAL   (60 * 20)
#define NUM_OF_SEARCH     20

static LGFX lcd;

#define BACKGROUND_BUFFER_SIZE  70000
unsigned long background_buffer_length;
unsigned char background_buffer[BACKGROUND_BUFFER_SIZE];

void wifi_connect(const char *ssid, const char *password);
long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len);
String urlencode(String str);

void setup() {
  lcd.init();
  lcd.setRotation(1);
  lcd.setBrightness(128);
  lcd.setColorDepth(24);
  Serial.begin(9600);
  
  wifi_connect(wifi_ssid, wifi_password);
}

void loop() {
  String url = base_url;
  url += "?keyword=";
  url += urlencode(keyword);
  url += "&num=" + String(NUM_OF_SEARCH);

  Serial.println(url);

  background_buffer_length = sizeof(background_buffer);
  long ret = doHttpGet(url, background_buffer, &background_buffer_length);
  if( ret != 0 ){
    Serial.println("doHttpGet Error");
    delay(1000);
    return;
  }

  lcd.drawJpg(background_buffer, background_buffer_length, 0, 0);

  delay(1000UL * UPDATE_INTERVAL);
}

void wifi_connect(const char *ssid, const char *password){
  Serial.println("");
  Serial.print("WiFi Connenting");

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(1000);
  }
  Serial.println("");
  Serial.print("Connected : ");
  Serial.println(WiFi.localIP());
}

long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len){
  HTTPClient http;

  Serial.print("[HTTP] GET begin...\n");
  // configure traged server and url
  http.begin(url);

  Serial.print("[HTTP] GET...\n");
  // start connection and send HTTP header
  int httpCode = http.GET();
  unsigned long index = 0;

  // httpCode will be negative on error
  if(httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      Serial.printf("[HTTP] GET... code: %d\n", httpCode);

      // file found at server
      if(httpCode == HTTP_CODE_OK) {
        // get tcp stream
        WiFiClient * stream = http.getStreamPtr();

        // get lenght of document (is -1 when Server sends no Content-Length header)
        int len = http.getSize();
        Serial.printf("[HTTP] Content-Length=%d\n", len);
        if( len != -1 && len > *p_len ){
          Serial.printf("[HTTP] buffer size over\n");
          http.end();
          return -1;
        }

        // read all data from server
        while(http.connected() && (len > 0 || len == -1)) {
            // get available data size
            size_t size = stream->available();

            if(size > 0) {
                // read up to 128 byte
                if( (index + size ) > *p_len){
                  Serial.printf("[HTTP] buffer size over\n");
                  http.end();
                  return -1;
                }
                int c = stream->readBytes(&p_buffer[index], size);

                index += c;
                if(len > 0) {
                    len -= c;
                }
            }
            delay(1);
        }
      }else{
        http.end();
        return -1;
      }
  } else {
    http.end();
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    return -1;
  }

  http.end();
  *p_len = index;

  return 0;
}

String urlencode(String str){
    String encodedString = "";
    char c;
    char code0;
    char code1;
//    char code2;
    for (int i = 0 ; i < str.length() ; i++){
      c = str.charAt(i);
      if (c == ' '){
        encodedString += '+';
      } else if (isalnum(c)){
        encodedString += c;
      } else{
        code1 = (c & 0xf) + '0';
        if ((c & 0xf) > 9){
            code1 = (c & 0xf) - 10 + 'A';
        }
        c = (c >> 4) & 0xf;
        code0 = c + '0';
        if (c > 9){
            code0 = c - 10 + 'A';
        }
//        code2 = '\0';
        encodedString += '%';
        encodedString += code0;
        encodedString += code1;
        //encodedString+=code2;
      }
      yield();
    }
    return encodedString;
}

以下の部分を環境に合わせてください。

・【キーワード】
・【サーバのURL】/search-image
・【WiFiアクセスポイントのSSID】
・【WiFiアクセスポイントのパスワード】

【キーワード】が、表示させたい画像の検索キーワードです。

先ほど見ていただいた通り、呼び出し回数は1日1万回です。
それに合わせて、以下の呼び出し頻度を決めてください。秒です

 > #define UPDATE_INTERVAL (60 * 20)

#おわりに

以下を参考にしています。

 ESP32で作るBeebotteダッシュボード
 M5Core2のLCDにWebページのスクリーンショットを表示する
 ESP32でバイナリファイルのダウンロード・アップロード

以上

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?