Skip to content

Commit

Permalink
Merge pull request #822 from sebromero/sebromero/web-camera
Browse files Browse the repository at this point in the history
Add WebSerial Camera Preview
  • Loading branch information
sebromero authored Jan 18, 2024
2 parents 3b56169 + b696b59 commit f2af60a
Show file tree
Hide file tree
Showing 10 changed files with 1,434 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* This example shows how to capture images from the camera and send them over Web Serial.
*
* There is a companion web app that receives the images and displays them in a canvas.
* It can be found in the "extras" folder of this library.
* The on-board LED lights up while the image is being sent over serial.
*
* Instructions:
* 1. Make sure the correct camera is selected in the #include section below by uncommenting the correct line.
* 2. Upload this sketch to your camera-equipped board.
* 3. Open the web app in a browser (Chrome or Edge) by opening the index.html file
* in the "WebSerialCamera" folder which is located in the "extras" folder.
*
* Initial author: Sebastian Romero @sebromero
*/

#include "camera.h"

#ifdef ARDUINO_NICLA_VISION
#include "gc2145.h"
GC2145 galaxyCore;
Camera cam(galaxyCore);
#define IMAGE_MODE CAMERA_RGB565
#elif defined(ARDUINO_PORTENTA_H7_M7)
// uncomment the correct camera in use
#include "hm0360.h"
HM0360 himax;
// #include "himax.h";
// HM01B0 himax;
Camera cam(himax);
#define IMAGE_MODE CAMERA_GRAYSCALE
#elif defined(ARDUINO_GIGA)
#include "ov767x.h"
// uncomment the correct camera in use
OV7670 ov767x;
// OV7675 ov767x;
Camera cam(ov767x);
#define IMAGE_MODE CAMERA_RGB565
#else
#error "This board is unsupported."
#endif

/*
Other buffer instantiation options:
FrameBuffer fb(0x30000000);
FrameBuffer fb(320,240,2);
If resolution higher than 320x240 is required, please use external RAM via
#include "SDRAM.h"
FrameBuffer fb(SDRAM_START_ADDRESS);
...
// and adding in setup()
SDRAM.begin();
*/
constexpr uint16_t CHUNK_SIZE = 512; // Size of chunks in bytes
constexpr uint8_t RESOLUTION = CAMERA_R320x240; // CAMERA_R160x120
constexpr uint8_t CONFIG_SEND_REQUEST = 2;
constexpr uint8_t IMAGE_SEND_REQUEST = 1;

uint8_t START_SEQUENCE[4] = { 0xfa, 0xce, 0xfe, 0xed };
uint8_t STOP_SEQUENCE[4] = { 0xda, 0xbb, 0xad, 0x00 };
FrameBuffer fb;

/**
* Blinks the LED a specified number of times.
* @param ledPin The pin number of the LED.
* @param count The number of times to blink the LED. Default is 0xFFFFFFFF.
*/
void blinkLED(int ledPin, uint32_t count = 0xFFFFFFFF) {
while (count--) {
digitalWrite(ledPin, LOW); // turn the LED on (HIGH is the voltage level)
delay(50); // wait for a second
digitalWrite(ledPin, HIGH); // turn the LED off by making the voltage LOW
delay(50); // wait for a second
}
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(LEDR, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
digitalWrite(LEDR, HIGH);

// Init the cam QVGA, 30FPS
if (!cam.begin(RESOLUTION, IMAGE_MODE, 30)) {
blinkLED(LEDR);
}

blinkLED(LED_BUILTIN, 5);
}

/**
* Sends a chunk of data over a serial connection.
*
* @param buffer The buffer containing the data to be sent.
* @param bufferSize The size of the buffer.
*/
void sendChunk(uint8_t* buffer, size_t bufferSize){
Serial.write(buffer, bufferSize);
Serial.flush();
delay(1); // Optional: Add a small delay to allow the receiver to process the chunk
}

/**
* Sends a frame of camera image data over a serial connection.
*/
void sendFrame(){
// Grab frame and write to serial
if (cam.grabFrame(fb, 3000) == 0) {
byte* buffer = fb.getBuffer();
size_t bufferSize = cam.frameSize();
digitalWrite(LED_BUILTIN, LOW);

sendChunk(START_SEQUENCE, sizeof(START_SEQUENCE));

// Split buffer into chunks
for(size_t i = 0; i < bufferSize; i += CHUNK_SIZE) {
size_t chunkSize = min(bufferSize - i, CHUNK_SIZE);
sendChunk(buffer + i, chunkSize);
}

sendChunk(STOP_SEQUENCE, sizeof(STOP_SEQUENCE));

digitalWrite(LED_BUILTIN, HIGH);
} else {
blinkLED(20);
}
}

/**
* Sends the camera configuration over a serial connection.
* This is used to configure the web app to display the image correctly.
*/
void sendCameraConfig(){
Serial.write(IMAGE_MODE);
Serial.write(RESOLUTION);
Serial.flush();
delay(1);
}

void loop() {
if(!Serial) {
Serial.begin(115200);
while(!Serial);
}

if(!Serial.available()) return;

byte request = Serial.read();

switch(request){
case IMAGE_SEND_REQUEST:
sendFrame();
break;
case CONFIG_SEND_REQUEST:
sendCameraConfig();
break;
}

}
12 changes: 12 additions & 0 deletions libraries/Camera/extras/WebSerialCamera/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 📹 WebSerial Camera Stream

This folder contains a web application that provides a camera stream over WebSerial.
This is an experimental feature not supported in all browsers. It's recommended to use Google Chrome.
See [Browser Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility)

## Instructions

1. Upload the companion [Arduino sketch](../../examples/CameraCaptureWebSerial/CameraCaptureWebSerial.ino) to your board.
2. Open the web app either by directly opening the index.html file or serving it via a webserver and opening the URL provided by the webserver.
3. Click "Connect". Your board's serial port should show up in the popup. Select it. Click once again "Connect". The camera feed should start. If the board has been previously connected to the browser, it will connect automatically.
4. (Optional) click "Save Image" if you want to save individual camera frames to your computer.
135 changes: 135 additions & 0 deletions libraries/Camera/extras/WebSerialCamera/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* @fileoverview This file contains the main application logic.
*
* The application uses the Web Serial API to connect to the serial port.
* Check the following links for more information on the Web Serial API:
* https://developer.chrome.com/articles/serial/
* https://wicg.github.io/serial/
*
* The flow of the application is as follows:
* 1. The user clicks the "Connect" button or the browser automatically connects
* to the serial port if it has been previously connected.
* 2. The application requests the camera configuration (mode and resolution) from the board.
* 3. The application starts reading the image data stream from the serial port.
* It waits until the expected amount of bytes have been read and then processes the data.
* 4. The processed image data is rendered on the canvas.
*
* @author Sebastian Romero
*/

const connectButton = document.getElementById('connect');
const refreshButton = document.getElementById('refresh');
const startButton = document.getElementById('start');
const saveImageButton = document.getElementById('save-image');
const canvas = document.getElementById('bitmapCanvas');
const ctx = canvas.getContext('2d');

const imageDataTransfomer = new ImageDataTransformer(ctx);
imageDataTransfomer.setStartSequence([0xfa, 0xce, 0xfe, 0xed]);
imageDataTransfomer.setStopSequence([0xda, 0xbb, 0xad, 0x00]);

// 🐣 Uncomment one of the following lines to apply a filter to the image data
// imageDataTransfomer.filter = new GrayScaleFilter();
// imageDataTransfomer.filter = new BlackAndWhiteFilter();
// imageDataTransfomer.filter = new SepiaColorFilter();
// imageDataTransfomer.filter = new PixelateFilter(8);
// imageDataTransfomer.filter = new BlurFilter(8);
const connectionHandler = new SerialConnectionHandler();


// Connection handler event listeners

connectionHandler.onConnect = async () => {
connectButton.textContent = 'Disconnect';
cameraConfig = await connectionHandler.getConfig();
if(!cameraConfig){
console.error('🚫 Could not read camera configuration. Aborting...');
return;
}
const imageMode = CAMERA_MODES[cameraConfig[0]];
const imageResolution = CAMERA_RESOLUTIONS[cameraConfig[1]];
if(!imageMode || !imageResolution){
console.error(`🚫 Invalid camera configuration: ${cameraConfig[0]}, ${cameraConfig[1]}. Aborting...`);
return;
}
imageDataTransfomer.setImageMode(imageMode);
imageDataTransfomer.setResolution(imageResolution.width, imageResolution.height);
renderStream();
};

connectionHandler.onDisconnect = () => {
connectButton.textContent = 'Connect';
imageDataTransfomer.reset();
};


// Rendering logic

async function renderStream(){
while(connectionHandler.isConnected()){
if(imageDataTransfomer.isConfigured()) await renderFrame();
}
}

/**
* Renders the image data for one frame from the board and renders it.
* @returns {Promise<boolean>} True if a frame was rendered, false otherwise.
*/
async function renderFrame(){
if(!connectionHandler.isConnected()) return;
const imageData = await connectionHandler.getFrame(imageDataTransfomer);
if(!imageData) return false; // Nothing to render
if(!(imageData instanceof ImageData)) throw new Error('🚫 Image data is not of type ImageData');
renderBitmap(ctx, imageData);
return true;
}

/**
* Renders the image data on the canvas.
* @param {CanvasRenderingContext2D} context The canvas context to render on.
* @param {ImageData} imageData The image data to render.
*/
function renderBitmap(context, imageData) {
context.canvas.width = imageData.width;
context.canvas.height = imageData.height;
context.clearRect(0, 0, canvas.width, canvas.height);
context.putImageData(imageData, 0, 0);
}


// UI Event listeners

startButton.addEventListener('click', renderStream);

connectButton.addEventListener('click', async () => {
if(connectionHandler.isConnected()){
connectionHandler.disconnectSerial();
} else {
await connectionHandler.requestSerialPort();
await connectionHandler.connectSerial();
}
});

refreshButton.addEventListener('click', () => {
if(imageDataTransfomer.isConfigured()) renderFrame();
});

saveImageButton.addEventListener('click', () => {
const link = document.createElement('a');
link.download = 'image.png';
link.href = canvas.toDataURL();
link.click();
link.remove();
});

// On page load event, try to connect to the serial port
window.addEventListener('load', async () => {
console.log('🚀 Page loaded. Trying to connect to serial port...');
setTimeout(() => {
connectionHandler.autoConnect();
}, 1000);
});

if (!("serial" in navigator)) {
alert("The Web Serial API is not supported in your browser.");
}
51 changes: 51 additions & 0 deletions libraries/Camera/extras/WebSerialCamera/cameraConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @fileoverview This file contains the configuration for the camera.
* @author Sebastian Romero
*/

/**
* The available camera (color) modes.
* The Arduino sketch uses the same values to communicate which mode should be used.
**/
const CAMERA_MODES = {
0: "GRAYSCALE",
1: "BAYER",
2: "RGB565"
};

/**
* The available camera resolutions.
* The Arduino sketch uses the same values to communicate which resolution should be used.
*/
const CAMERA_RESOLUTIONS = {
0: {
"name": "QQVGA",
"width": 160,
"height": 120
},
1: {
"name": "QVGA",
"width": 320,
"height": 240
},
2: {
"name": "320x320",
"width": 320,
"height": 320
},
3: {
"name": "VGA",
"width": 640,
"height": 480
},
5: {
"name": "SVGA",
"width": 800,
"height": 600
},
6: {
"name": "UXGA",
"width": 1600,
"height": 1200
}
};
Loading

0 comments on commit f2af60a

Please sign in to comment.