-
-
Notifications
You must be signed in to change notification settings - Fork 203
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #822 from sebromero/sebromero/web-camera
Add WebSerial Camera Preview
- Loading branch information
Showing
10 changed files
with
1,434 additions
and
0 deletions.
There are no files selected for viewing
160 changes: 160 additions & 0 deletions
160
libraries/Camera/examples/CameraCaptureWebSerial/CameraCaptureWebSerial.ino
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
}; |
Oops, something went wrong.