闲来无事-esp32cam实现延时摄影

扯淡时间

上一篇文章中我提了一嘴,打算使用esp32cam实现一个延迟摄影,奈何存在各种硬件问题,商家发了好几个地板都不好使(就是那个拼多多商家的问题,还说我供电不稳,我特意买了独立供电的hub),后来逛淘宝的时候又给我推送了esp32的板子,我不信邪的买了一个~他妈的上来就好使,所以才有了这篇文章,嗨嗨嗨
最后我只想对拼多多商家说:
既然能用了,那就开始搞

思路

  1. esp32-cam定时拍摄照片(拍摄速度还是需要跟网络传输速度来定,esp32的网卡太辣鸡),发送到树莓派上存储(可以在树莓派上去合成视频)
  2. 树莓派接受到指令以后,将图片拼接成视频,通过邮件发送给我
  3. 写个页面,能看到esp32拍到的照片,也支持下发指令,生成视频(理想很丰满,现实就是能用,凑活着用,都是bug)
graph TB subgraph 服务器 id1[esp32拍摄照片]-.http发送到树莓派.->id2[接受] id2-.存储数据.->id4[图片文件夹] end subgraph 页面端 id3[显示最新的条图片]-->id2 end

先看个效果-我买了好几个摄像头,这里发一下视频,可以让大家参考一下再购买

200w摄像头 https://player.bilibili.com/player.html?bvid=BV1CC4y1y7aX&page=1

300w带夜视广角摄像头 https://player.bilibili.com/player.html?bvid=BV11w411b75z&page=1

500w摄像头 https://player.bilibili.com/player.html?bvid=BV1hC4y1w7SR&page=1

好了我们开始无聊的code时间吧

  1. esp32拍摄照片+将图片使用http请求发送到服务端
  2. 树莓派4b开启服务器,接收图片并存储到本地,执行合并视频的指令
  3. 给个前端页面展示一下图片(为了调整视角)

项目地址: https://github.com/dadademo/time-lapse

esp32-cam拍摄+发送图片

#include <Arduino.h>
#include <WiFi.h>
#include "esp_camera.h"
#include <vector>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <FS.h>
#include <sstream>

const char *ssid = "nxd";
const char *password = "niexianda123";  //192.168.100.16 laptop

// 定义远程服务器的 URL
const char *server_url = "http://example.com/image_upload";



//CAMERA_MODEL_AI_THINKER类型摄像头的引脚定义
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27

#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22

static camera_config_t camera_config = {
  .pin_pwdn = PWDN_GPIO_NUM,
  .pin_reset = RESET_GPIO_NUM,
  .pin_xclk = XCLK_GPIO_NUM,
  .pin_sscb_sda = SIOD_GPIO_NUM,
  .pin_sscb_scl = SIOC_GPIO_NUM,

  .pin_d7 = Y9_GPIO_NUM,
  .pin_d6 = Y8_GPIO_NUM,
  .pin_d5 = Y7_GPIO_NUM,
  .pin_d4 = Y6_GPIO_NUM,
  .pin_d3 = Y5_GPIO_NUM,
  .pin_d2 = Y4_GPIO_NUM,
  .pin_d1 = Y3_GPIO_NUM,
  .pin_d0 = Y2_GPIO_NUM,
  .pin_vsync = VSYNC_GPIO_NUM,
  .pin_href = HREF_GPIO_NUM,
  .pin_pclk = PCLK_GPIO_NUM,

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

  .pixel_format = PIXFORMAT_JPEG,
  .frame_size = FRAMESIZE_QSXGA,
  .jpeg_quality = 10,
  .fb_count = 1,
};

// 开启wifi
void wifi_init() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi Connected!");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());
}

// 关闭 WiFi
void closeWiFi() {
  WiFi.disconnect();
  WiFi.mode(WIFI_OFF);
}

// 初始化摄像头
esp_err_t camera_init() {
  //initialize the camera
  esp_err_t err = esp_camera_init(&camera_config);
  if (err != ESP_OK) {
    Serial.println("Camera Init Failed");
    return err;
  }
  sensor_t *s = esp_camera_sensor_get();
  //initial sensors are flipped vertically and colors are a bit saturated
  if (s->id.PID == OV5640_PID) {
    //        s-&gt;set_vflip(s, 1);//flip it back
    //        s-&gt;set_brightness(s, 1);//up the blightness just a bit
    //        s-&gt;set_contrast(s, 1);
  }
  Serial.println("Camera Init OK!");
  return ESP_OK;
}

// 发送图片信息
void sendImg() {
  // 获取图片信息
  camera_fb_t *fb = esp_camera_fb_get();
  if (fb == NULL) {
    printf("Failed to get camera frame buffer\n");
    return;
  }


  char *beginText = "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file\"; filename=\"img.jpeg\"\r\nContent-Type: image/jpeg\r\n\r\n";
  size_t beginSize = strlen(beginText);

  // 数据的长度
  uint8_t *imgData = fb->buf;
  size_t fb_len = fb->len;

  char *endText = "\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n";
  size_t endSize = strlen(endText);

  // 计算总大小,包括空终止符
  size_t sizeAll = beginSize + fb_len + endSize + 1;

  // 分配内存并复制数据
  uint8_t *data = (uint8_t *)malloc(sizeAll);
  memcpy(data, beginText, beginSize);
  memcpy(data + beginSize, imgData, fb_len);
  memcpy(data + beginSize + fb_len, endText, endSize + 1);  // +1 for null terminator

  printf("Size: %zu\n", sizeAll);
  char Length[64];  // 用于存储转换后的字符串
  sprintf(Length, "%zu", sizeAll);

  HTTPClient http;
  http.begin("http://192.168.0.104:4396/upload");  // 指定URL
  http.addHeader("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW");
  http.addHeader("Content-Length", Length);

  int http_response_code = http.POST(data, sizeAll);

  if (http_response_code > 0)  // 如果状态码大于0说明请求过程无异常
  {
    if (http_response_code == HTTP_CODE_OK)  // 请求被服务器正常响应,等同于httpCode == 200
    {
      Serial.print("Content-Type = ");
      Serial.println(http.header("Content-Type"));

      Serial.print("Content-Length = ");
      Serial.println(http.header("Content-Length"));
    }
  } else {
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(http_response_code).c_str());
  }

  http.end();  // 结束当前连接
  free(data);                                                                 
  esp_camera_fb_return(fb);  //这一步在发送完毕后要执行&#xff0c;具体作用还未可知。
}

void setup() {
  Serial.begin(115200);
  wifi_init();
  camera_init();
}

void loop() {
  delay(60*1000);
  Serial.print("获取手机号码");
  sendImg();
}

服务端生成视频

const { exec } = require('child_process');
const path = require('path');
function initVideo() {
    return new Promise((resolve, reject) => {
        const videoName = `${new Date().getTime()}.mp4`
        // 假设您的图片序列文件在 uploads 文件夹下,命名为 00000001.jpeg、00000002.jpeg 等
        const inputPattern = path.join(__dirname, 'uploads', '%08d.jpg');
        const outputVideo = path.join(__dirname, 'video', videoName);

        // 构建 ffmpeg 命令
        const ffmpegCommand = `ffmpeg -framerate 30 -i ${inputPattern} -c:v libx264 -r 30 -pix_fmt yuv420p ${outputVideo}`;

        // 执行命令
        exec(ffmpegCommand, (error, stdout, stderr) => {
            if (error) {
                console.error(`Error: ${error.message}`);
                return;
            }
            console.log('成功')
            resolve(outputVideo)
        });
    })
}

module.exports = {
    initVideo
}

接收图片,对外提供接口

const express = require('express');
const multer = require('multer');
const app = express();
const path = require('path');
const fs = require('fs');
var objFunc = require('./merege')   // 目录名/文件名
const email = require('./email');

let counter = 1;
const currentPath = path.join(__dirname, 'uploads');
const files = fs.readdirSync(currentPath);
if (files && files.length) {
  if (files[files.length - 1] && files[files.length - 1].split) {
    counter = Number(files[files.length - 1].split('.')[0])
    counter += 1
  }
}


const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadPath = path.join('uploads/');
    fs.mkdirSync(uploadPath, { recursive: true });
    cb(null, uploadPath);
  },
  filename: (req, file, cb) => {
    const filename = `${String(counter).padStart(8, '0')}.jpg`;
    counter++;
    cb(null, filename);
  },
});

const upload = multer({ storage: storage });

// 静态文件服务,用于访问uploads目录下的图片
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

// 新增的路由,用于获取最后一条图片的地址
app.get('/latest-image', (req, res) => {
  const uploadPath = path.join(__dirname, 'uploads');
  const files = fs.readdirSync(uploadPath);

  if (files.length === 0) {
    return res.status(404).send('No images found.');
  }

  const latestFile = files[files.length - 1];
  const imageUrl = `http://${req.hostname}:${req.socket.localPort}/uploads/${latestFile}`;
  res.status(200).json({ imageUrl });
});

app.post('/upload', upload.single('file'), (req, res) => {
  const file = req.file;
  console.log(file);

  if (!file) {
    return res.status(400).send('No file uploaded.');
  }

  console.log(`File saved as ${file.path}`);
  res.status(200).send('File uploaded!');
});

// 新增的路由,用于初始化视频
app.get('/initVideo', (req, res) => {
  objFunc.initVideo().then(videoName => {
    email.sendMail(videoName)
    res.status(200).json({ videoName });
  })
});

app.listen(4396, () => {
  console.log('Server is running on port 4396');
});

邮件发送视频

const nodemailer = require('nodemailer');

/**
 * 发送邮件
 * @param {*} html  发送的信息支持html
 * @param {*} toEmail  1298175585@qq.com
 * @param {*} title  恭喜发财
 */
function sendMail(path) {
    console.log('准备发送今日好心情qwq')
    let transporter = nodemailer.createTransport({
        host: 'smtp.qq.com',
        port: 465,
        secure: true,
        auth: {
            user: '你的邮箱@qq.com',
            pass: '你的邮箱key'
        }
    });

    let mailOptions = {
        from: '你的邮箱@qq.com',
        to: '你的邮箱@qq.com',
        subject: '视频信息已经到位了',
        html: '<h2>今天的视频信息</h2>',
        attachments: [
            {
                filename: '视频.mp4',
                path
            }
        ]
    };
    transporter.sendMail(mailOptions, (error, info) => {
        if (error) {
            return console.log(error);
        }
        console.log('信息已送达!')
    });
}
module.exports = { sendMail }




还存在的问题

  1. 因为白天太亮了,天空毛线都看不到,本来就是打算来拍云彩的,貌似得整个nd滤镜啥的,需要手动去调整镜头的参数了
  2. 合并视频的时候必须是1,2,3依次的文件名称,我这里虽然做了处理,但是开始不一定是1,我是读取了最后一个然后加一
  3. 没有清除图片的逻辑

为啥不解决呢?因为我又不经常用,就为了玩玩,能达到百分之80的功能就行,有些小bug,没有心劲去解决了

posted @ 2024-04-03 16:47  聂显达  阅读(145)  评论(4编辑  收藏  举报