闲来无事-esp32cam实现延时摄影
扯淡时间
在上一篇文章中我提了一嘴,打算使用esp32cam实现一个延迟摄影,奈何存在各种硬件问题,商家发了好几个地板都不好使(就是那个拼多多商家的问题,还说我供电不稳,我特意买了独立供电的hub),后来逛淘宝的时候又给我推送了esp32的板子,我不信邪的买了一个~他妈的上来就好使,所以才有了这篇文章,嗨嗨嗨
最后我只想对拼多多商家说:
既然能用了,那就开始搞
思路
- esp32-cam定时拍摄照片(拍摄速度还是需要跟网络传输速度来定,esp32的网卡太辣鸡),发送到树莓派上存储(可以在树莓派上去合成视频)
- 树莓派接受到指令以后,将图片拼接成视频,通过邮件发送给我
- 写个页面,能看到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时间吧
- esp32拍摄照片+将图片使用http请求发送到服务端
- 树莓派4b开启服务器,接收图片并存储到本地,执行合并视频的指令
- 给个前端页面展示一下图片(为了调整视角)
项目地址: 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->set_vflip(s, 1);//flip it back
// s->set_brightness(s, 1);//up the blightness just a bit
// s->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); //这一步在发送完毕后要执行,具体作用还未可知。
}
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 }
还存在的问题
- 因为白天太亮了,天空毛线都看不到,本来就是打算来拍云彩的,貌似得整个nd滤镜啥的,需要手动去调整镜头的参数了
- 合并视频的时候必须是1,2,3依次的文件名称,我这里虽然做了处理,但是开始不一定是1,我是读取了最后一个然后加一
- 没有清除图片的逻辑
为啥不解决呢?因为我又不经常用,就为了玩玩,能达到百分之80的功能就行,有些小bug,没有心劲去解决了