Mercurial > code > home > repos > homeauto
changeset 1740:c77b5ab7b99d
camera work
author | drewp@bigasterisk.com |
---|---|
date | Fri, 01 Sep 2023 17:13:51 -0700 |
parents | 28a3e35bc23f |
children | 097bfd91187d |
files | espNode/cam.yaml espNode/component/cam.h espNode/readcam.py espNode/signalgen/main.py |
diffstat | 4 files changed, 201 insertions(+), 53 deletions(-) [+] |
line wrap: on
line diff
--- a/espNode/cam.yaml Fri Sep 01 17:12:06 2023 -0700 +++ b/espNode/cam.yaml Fri Sep 01 17:13:51 2023 -0700 @@ -53,3 +53,62 @@ // App.register_component(camc); return {camc}; + +# ffplay -i http://10.2.0.22:81/stream -vf hqdn3d=luma_spatial=0:chroma_spatial=50:luma_tmp=10:chroma_tmp=10 + +# maybe this first: +# vaguedenoiser=threshold=5 + + + + + +# # https://randomnerdtutorials.com/esp32-cam-ai-thinker-pinout/ +# esp32_camera: +# external_clock: +# pin: GPIO0 +# frequency: 20MHz +# i2c_pins: +# sda: GPIO26 +# scl: GPIO27 +# data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35] +# vsync_pin: GPIO25 +# href_pin: GPIO23 +# pixel_clock_pin: GPIO22 +# power_down_pin: GPIO32 + +# name: camera + +# # https://github.com/esphome/esphome/blob/dev/esphome/components/esp32_camera/esp32_camera.cpp#L265 says a 'stream' is 5 sec long + +# # setting to 5 causes 'Setup Failed: ERROR' +# # max_framerate: 4 fps +# # https://github.com/raphaelbs/esp32-cam-ai-thinker#capabilities says camera +# # is likely ov2640 with these native resolutions: +# # uxga=1600x1200 svga=800x600 cif=400x296 + +# # 160x120 (QQVGA) 'Got invalid frame', then no more +# # 128x160 (QQVGA2) +# # 176x144 (QCIF) fps: 25 jpg: 20 img: 2KB burst of frames then stopped. +# # fps: 20 jpg: 20 no frames +# # 240x176 (HQVGA) +# # 320x240 (QVGA) fps: 10 jpg: 20 some frames, 4.5KB +# # 400x296 (CIF) +# # 640x480 (VGA) fps: 4 jpg: 10 works, 20KB +# # 800x600 (SVGA) +# # 1024x768 (XGA) +# # 1280x1024 (SXGA) fps: 1 works +# # 1600x1200 (UXGA) + +# resolution: 320x240 + +# # 10 to 63. default=10. higher is +# # worse. https://github.com/esphome/esphome/blob/6682c43dfaeb1c006943ae546145e5f22262cadb/esphome/components/esp32_camera/__init__.py#L84 +# # sets the lower limit to 10, but +# # https://github.com/raphaelbs/esp32-cam-ai-thinker/blob/master/components/ov2640/sensors/ov2640.c#L345 +# # suggests that it might be 0 (for an ov2640, anyway). +# # jpeg_quality: 10 + +# esp32_camera_web_server: +# - {port: 8000, mode: stream} +# - {port: 8001, mode: snapshot}
--- a/espNode/component/cam.h Fri Sep 01 17:12:06 2023 -0700 +++ b/espNode/component/cam.h Fri Sep 01 17:13:51 2023 -0700 @@ -33,6 +33,17 @@ static const char *TAG = "cam"; +#define PART_BOUNDARY "123456789000000000000987654321" +static const char *_STREAM_CONTENT_TYPE = + "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; +static const char *_STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; +static const char *_STREAM_PART = + "Content-Type: image/jpeg\r\nContent-Length: %u\r\nX-Timestamp: " + "%d.%06d\r\n\r\n"; + +httpd_handle_t camera_httpd = NULL; +httpd_handle_t stream_httpd = NULL; + typedef struct { httpd_req_t *req; size_t len; @@ -66,7 +77,7 @@ config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; - config.frame_size = FRAMESIZE_QVGA; + config.frame_size = FRAMESIZE_SVGA; config.pixel_format = PIXFORMAT_JPEG; // for streaming // config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition // config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; @@ -84,6 +95,7 @@ esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x", err); + mark_failed(); return; } @@ -143,10 +155,90 @@ return res; } + static esp_err_t stream_handler(httpd_req_t *req) { + camera_fb_t *fb = NULL; + struct timeval _timestamp; + esp_err_t res = ESP_OK; + size_t _jpg_buf_len = 0; + uint8_t *_jpg_buf = NULL; + char *part_buf[128]; + + static int64_t last_frame = 0; + if (!last_frame) { + last_frame = esp_timer_get_time(); + } + + res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE); + if (res != ESP_OK) { + return res; + } + + while (true) { + fb = esp_camera_fb_get(); + if (!fb) { + ESP_LOGE(TAG, "Camera capture failed"); + res = ESP_FAIL; + } else { + _timestamp.tv_sec = fb->timestamp.tv_sec; + _timestamp.tv_usec = fb->timestamp.tv_usec; + if (fb->format != PIXFORMAT_JPEG) { + ESP_LOGI(TAG, "format was %d; sw convert to jpeg", fb->format); + bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len); + esp_camera_fb_return(fb); + fb = NULL; + if (!jpeg_converted) { + ESP_LOGE(TAG, "JPEG compression failed"); + res = ESP_FAIL; + } + } else { + _jpg_buf_len = fb->len; + _jpg_buf = fb->buf; + } + } + if (res == ESP_OK) { + res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, + strlen(_STREAM_BOUNDARY)); + } + if (res == ESP_OK) { + size_t hlen = + snprintf((char *)part_buf, 128, _STREAM_PART, _jpg_buf_len, + _timestamp.tv_sec, _timestamp.tv_usec); + res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen); + } + if (res == ESP_OK) { + res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len); + } + if (fb) { + esp_camera_fb_return(fb); + fb = NULL; + _jpg_buf = NULL; + } else if (_jpg_buf) { + free(_jpg_buf); + _jpg_buf = NULL; + } + if (res != ESP_OK) { + ESP_LOGE(TAG, "send frame failed failed"); + break; + } + int64_t fr_end = esp_timer_get_time(); + + int64_t frame_time = fr_end - last_frame; + frame_time /= 1000; + uint32_t avg_frame_time = -1; + // ESP_LOGI(TAG, "MJPG: %uB %ums (%.1ffps), AVG: %ums (%.1ffps)" + // , + // (uint32_t)(_jpg_buf_len), (uint32_t)frame_time, + // 1000.0 / (uint32_t)frame_time, avg_frame_time, + // 1000.0 / avg_frame_time); + } + + return res; + } + void startCameraServer() { ESP_LOGD(TAG, "startCameraServer"); httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - config.server_port = 8000; + config.server_port = 80; config.max_uri_handlers = 16; httpd_uri_t capture_uri = {.uri = "/capture", @@ -154,18 +246,16 @@ .handler = capture_handler, .user_ctx = NULL}; - // httpd_uri_t stream_uri = { - // .uri = "/stream", - // .method = HTTP_GET, - // .handler = stream_handler, - // .user_ctx = NULL - // }; + httpd_uri_t stream_uri = {.uri = "/stream", + .method = HTTP_GET, + .handler = stream_handler, + .user_ctx = NULL}; // ra_filter_init(&ra_filter, 20); ESP_LOGCONFIG(TAG, "startCameraServer2"); ESP_LOGI(TAG, "Starting web server on port: '%d'", config.server_port); - httpd_handle_t camera_httpd = NULL; + if (httpd_start(&camera_httpd, &config) == ESP_OK) { // httpd_register_uri_handler(camera_httpd, &index_uri); // httpd_register_uri_handler(camera_httpd, &cmd_uri); @@ -180,13 +270,12 @@ // httpd_register_uri_handler(camera_httpd, &win_uri); } - // config.server_port += 1; - // config.ctrl_port += 1; - // ESP_LOGI(TAG, "Starting stream server on port: '%d'", - // config.server_port); if (httpd_start(&stream_httpd, &config) == ESP_OK) - // { - // httpd_register_uri_handler(stream_httpd, &stream_uri); - // } + config.server_port += 1; + config.ctrl_port += 1; + ESP_LOGI(TAG, "Starting stream server on port: '%d'", config.server_port); + if (httpd_start(&stream_httpd, &config) == ESP_OK) { + httpd_register_uri_handler(stream_httpd, &stream_uri); + } } void loop() override {}
--- a/espNode/readcam.py Fri Sep 01 17:12:06 2023 -0700 +++ b/espNode/readcam.py Fri Sep 01 17:13:51 2023 -0700 @@ -1,59 +1,56 @@ #!camtest/bin/python3 +import asyncio import binascii +import io +import json import logging -import time -import io import os -import json -from docopt import docopt -from standardservice.logsetup import log, verboseLogging +import time -logging.basicConfig(level=logging.INFO) +import apriltag +import cv2 +import numpy +from aioesphomeapi.client import APIClient +from aioesphomeapi.model import CameraState from aiohttp import web from aiohttp.web import Response from aiohttp_sse import sse_response - -import asyncio +from docopt import docopt -from aioesphomeapi import APIClient -from aioesphomeapi.model import CameraState -import apriltag -import cv2 -import numpy +logging.basicConfig(level=logging.INFO) +log = logging.getLogger(__name__) + class CameraReceiver: - def __init__(self, loop, host): + + def __init__(self, host): self.lastFrameTime = None - self.loop = loop self.host = host self.lastFrame = b"", '' self.recent = [] async def start(self): try: - self.c = c = APIClient(self.loop, - self.host, - 6053, 'MyPassword') + self.c = c = APIClient(self.host, 6053, 'MyPassword') await c.connect(login=True) await c.subscribe_states(on_state=self.on_state) except OSError: - loop.stop() return - self.loop.create_task(self.start_requesting_image_stream_forever()) + self.t = asyncio.create_task( + self.start_requesting_image_stream_forever()) async def start_requesting_image_stream_forever(self): while True: try: await self.c.request_image_stream() except AttributeError: - self.loop.stop() return # https://github.com/esphome/esphome/blob/dev/esphome/components/esp32_camera/esp32_camera.cpp#L265 says a 'stream' is 5 sec long await asyncio.sleep(4) def on_state(self, s): if isinstance(s, CameraState): - jpg = s.image + jpg = s.data if len(self.recent) > 10: self.recent = self.recent[-10:] @@ -64,8 +61,7 @@ print('other on_state', s) def analyze(self, jpg): - img = cv2.imdecode(numpy.asarray(bytearray(jpg)), - cv2.IMREAD_GRAYSCALE) + img = cv2.imdecode(numpy.asarray(bytearray(jpg)), cv2.IMREAD_GRAYSCALE) result = detector.detect(img) msg = {} if result: @@ -92,6 +88,7 @@ def imageUri(jpg): return 'data:image/jpeg;base64,' + binascii.b2a_base64(jpg).decode('ascii') + async def stream(request): async with sse_response(request) as resp: await resp.send(imageUri(recv.lastFrame[0])) @@ -101,6 +98,7 @@ await resp.send(imageUri(frame)) return resp + async def index(request): d = r""" <html> @@ -146,31 +144,26 @@ this Usage: - this [-v] [--cam host] [--port to_serve] +this [-v] [--cam host] [--port to_serve] Options: - -v --verbose more log - --port n http server [default: 10020] - --cam host hostname of esphome server +--port n http server [default: 10020] +--cam host hostname of esphome server ''') -verboseLogging(arguments['--verbose']) logging.getLogger('aioesphomeapi.connection').setLevel(logging.INFO) -loop = asyncio.get_event_loop() +recv = CameraReceiver(arguments['--cam']) -recv = CameraReceiver(loop, arguments['--cam']) detector = apriltag.Detector() f = recv.start() -loop.create_task(f) +async def starter(app): + asyncio.create_task(f) start_time = time.time() app = web.Application() +app.on_startup.append(starter) app.router.add_route('GET', '/stream', stream) app.router.add_route('GET', '/', index) -try: - web.run_app(app, host='0.0.0.0', port=int(arguments['--port'])) -except RuntimeError as e: - log.error(e) -log.info(f'run_app stopped after {time.time() - start_time} sec') \ No newline at end of file +web.run_app(app, port=int(arguments['--port'])) \ No newline at end of file