Skip to content

Commit b6d20c4

Browse files
committed
Add filters
1 parent 28d4e37 commit b6d20c4

File tree

4 files changed

+226
-2
lines changed

4 files changed

+226
-2
lines changed

libraries/Camera/extras/WebSerialCamera/app.js

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ const canvas = document.getElementById('bitmapCanvas');
2525
const ctx = canvas.getContext('2d');
2626

2727
const imageDataTransfomer = new ImageDataTransformer(ctx);
28+
// 🐣 Uncomment one of the following lines to apply a filter to the image data
29+
// imageDataTransfomer.filter = new GrayScaleFilter();
30+
// imageDataTransfomer.filter = new BlackAndWhiteFilter();
31+
// imageDataTransfomer.filter = new SepiaColorFilter();
32+
// imageDataTransfomer.filter = new PixelateFilter(8);
33+
// imageDataTransfomer.filter = new BlurFilter(8);
2834
const connectionHandler = new SerialConnectionHandler();
2935

3036

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/**
2+
* @fileoverview This file contains the filters that can be applied to an image.
3+
* @author Sebastian Romero
4+
*/
5+
6+
/**
7+
* Represents an image filter interface. This class is meant to be extended by subclasses.
8+
*/
9+
class ImageFilter {
10+
/**
11+
* Applies a filter to the given pixel data.
12+
* @param {Uint8Array} pixelData - The pixel data to apply the filter to. The pixel data gets modified in place.
13+
* @param {number} [width=null] - The width of the image. Defaults to null.
14+
* @param {number} [height=null] - The height of the image. Defaults to null.
15+
* @throws {Error} - Throws an error if the applyFilter method is not implemented.
16+
*/
17+
applyFilter(pixelData, width = null, height = null) {
18+
throw new Error('applyFilter not implemented');
19+
}
20+
}
21+
22+
/**
23+
* Represents a grayscale filter that converts an image to grayscale.
24+
* @extends ImageFilter
25+
*/
26+
class GrayScaleFilter extends ImageFilter {
27+
/**
28+
* Applies the grayscale filter to the given pixel data.
29+
* @param {Uint8ClampedArray} pixelData - The pixel data to apply the filter to.
30+
* @param {number} [width=null] - The width of the image.
31+
* @param {number} [height=null] - The height of the image.
32+
*/
33+
applyFilter(pixelData, width = null, height = null) {
34+
for (let i = 0; i < pixelData.length; i += 4) {
35+
const r = pixelData[i];
36+
const g = pixelData[i + 1];
37+
const b = pixelData[i + 2];
38+
const gray = (r + g + b) / 3;
39+
pixelData[i] = gray;
40+
pixelData[i + 1] = gray;
41+
pixelData[i + 2] = gray;
42+
}
43+
}
44+
}
45+
46+
/**
47+
* A class representing a black and white image filter.
48+
* @extends ImageFilter
49+
*/
50+
class BlackAndWhiteFilter extends ImageFilter {
51+
applyFilter(pixelData, width = null, height = null) {
52+
for (let i = 0; i < pixelData.length; i += 4) {
53+
const r = pixelData[i];
54+
const g = pixelData[i + 1];
55+
const b = pixelData[i + 2];
56+
const gray = (r + g + b) / 3;
57+
const bw = gray > 127 ? 255 : 0;
58+
pixelData[i] = bw;
59+
pixelData[i + 1] = bw;
60+
pixelData[i + 2] = bw;
61+
}
62+
}
63+
}
64+
65+
/**
66+
* Represents a color filter that applies a sepia tone effect to an image.
67+
* @extends ImageFilter
68+
*/
69+
class SepiaColorFilter extends ImageFilter {
70+
applyFilter(pixelData, width = null, height = null) {
71+
for (let i = 0; i < pixelData.length; i += 4) {
72+
const r = pixelData[i];
73+
const g = pixelData[i + 1];
74+
const b = pixelData[i + 2];
75+
const gray = (r + g + b) / 3;
76+
pixelData[i] = gray + 100;
77+
pixelData[i + 1] = gray + 50;
78+
pixelData[i + 2] = gray;
79+
}
80+
}
81+
}
82+
83+
/**
84+
* Represents a filter that applies a pixelation effect to an image.
85+
* @extends ImageFilter
86+
*/
87+
class PixelateFilter extends ImageFilter {
88+
89+
constructor(blockSize = 8){
90+
super();
91+
this.blockSize = blockSize;
92+
}
93+
94+
applyFilter(pixelData, width, height) {
95+
for (let y = 0; y < height; y += this.blockSize) {
96+
for (let x = 0; x < width; x += this.blockSize) {
97+
const blockAverage = this.getBlockAverage(x, y, width, height, pixelData, this.blockSize);
98+
99+
// Set all pixels in the block to the calculated average color
100+
for (let blockY = 0; blockY < this.blockSize && y + blockY < height; blockY++) {
101+
for (let blockX = 0; blockX < this.blockSize && x + blockX < width; blockX++) {
102+
const pixelIndex = ((y + blockY) * width + (x + blockX)) * 4;
103+
pixelData[pixelIndex] = blockAverage.red;
104+
pixelData[pixelIndex + 1] = blockAverage.green;
105+
pixelData[pixelIndex + 2] = blockAverage.blue;
106+
}
107+
}
108+
}
109+
}
110+
}
111+
112+
/**
113+
* Calculates the average RGB values of a block of pixels.
114+
*
115+
* @param {number} x - The x-coordinate of the top-left corner of the block.
116+
* @param {number} y - The y-coordinate of the top-left corner of the block.
117+
* @param {number} width - The width of the image.
118+
* @param {number} height - The height of the image.
119+
* @param {Uint8ClampedArray} pixels - The array of pixel data.
120+
* @returns {Object} - An object containing the average red, green, and blue values.
121+
*/
122+
getBlockAverage(x, y, width, height, pixels) {
123+
let totalRed = 0;
124+
let totalGreen = 0;
125+
let totalBlue = 0;
126+
const blockSizeSquared = this.blockSize * this.blockSize;
127+
128+
for (let blockY = 0; blockY < this.blockSize && y + blockY < height; blockY++) {
129+
for (let blockX = 0; blockX < this.blockSize && x + blockX < width; blockX++) {
130+
const pixelIndex = ((y + blockY) * width + (x + blockX)) * 4;
131+
totalRed += pixels[pixelIndex];
132+
totalGreen += pixels[pixelIndex + 1];
133+
totalBlue += pixels[pixelIndex + 2];
134+
}
135+
}
136+
137+
return {
138+
red: totalRed / blockSizeSquared,
139+
green: totalGreen / blockSizeSquared,
140+
blue: totalBlue / blockSizeSquared,
141+
};
142+
}
143+
144+
}
145+
146+
/**
147+
* Represents a filter that applies a blur effect to an image.
148+
* @extends ImageFilter
149+
*/
150+
class BlurFilter extends ImageFilter {
151+
constructor(radius = 8) {
152+
super();
153+
this.radius = radius;
154+
}
155+
156+
applyFilter(pixelData, width, height) {
157+
for (let y = 0; y < height; y++) {
158+
for (let x = 0; x < width; x++) {
159+
const pixelIndex = (y * width + x) * 4;
160+
161+
const averageColor = this.getAverageColor(x, y, width, height, pixelData, this.radius);
162+
pixelData[pixelIndex] = averageColor.red;
163+
pixelData[pixelIndex + 1] = averageColor.green;
164+
pixelData[pixelIndex + 2] = averageColor.blue;
165+
}
166+
}
167+
}
168+
169+
/**
170+
* Calculates the average color of a rectangular region in an image.
171+
*
172+
* @param {number} x - The x-coordinate of the top-left corner of the region.
173+
* @param {number} y - The y-coordinate of the top-left corner of the region.
174+
* @param {number} width - The width of the region.
175+
* @param {number} height - The height of the region.
176+
* @param {Uint8ClampedArray} pixels - The pixel data of the image.
177+
* @param {number} radius - The radius of the neighborhood to consider for each pixel.
178+
* @returns {object} - An object representing the average color of the region, with red, green, and blue components.
179+
*/
180+
getAverageColor(x, y, width, height, pixels, radius) {
181+
let totalRed = 0;
182+
let totalGreen = 0;
183+
let totalBlue = 0;
184+
let pixelCount = 0;
185+
186+
for (let offsetY = -radius; offsetY <= radius; offsetY++) {
187+
for (let offsetX = -radius; offsetX <= radius; offsetX++) {
188+
const neighborX = x + offsetX;
189+
const neighborY = y + offsetY;
190+
191+
if (neighborX >= 0 && neighborX < width && neighborY >= 0 && neighborY < height) {
192+
const pixelIndex = (neighborY * width + neighborX) * 4;
193+
totalRed += pixels[pixelIndex];
194+
totalGreen += pixels[pixelIndex + 1];
195+
totalBlue += pixels[pixelIndex + 2];
196+
pixelCount++;
197+
}
198+
}
199+
}
200+
201+
return {
202+
red: totalRed / pixelCount,
203+
green: totalGreen / pixelCount,
204+
blue: totalBlue / pixelCount,
205+
};
206+
}
207+
}

libraries/Camera/extras/WebSerialCamera/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<button id="start">Start</button>
1717
</div>
1818
</div>
19+
<script src="filters.js"></script>
1920
<script src="transformers.js"></script>
2021
<script src="imageDataProcessor.js"></script>
2122
<script src="serialConnectionHandler.js"></script>

libraries/Camera/extras/WebSerialCamera/transformers.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* @fileoverview This file contains classes that transform incoming data into higher-level data types.
3+
* @author Sebastian Romero
4+
*/
5+
6+
17
/**
28
* A transformer class that waits for a specific number of bytes before processing them.
39
*/
@@ -68,7 +74,6 @@ class BytesWaitTransformer {
6874
}
6975
}
7076

71-
7277
/**
7378
* Represents an Image Data Transformer that converts bytes into image data.
7479
* See other example for PNGs here: https://github.com/mdn/dom-examples/blob/main/streams/png-transform-stream/png-transform-stream.js
@@ -149,7 +154,12 @@ class ImageDataTransformer extends BytesWaitTransformer {
149154
* @returns {ImageData} The converted ImageData object.
150155
*/
151156
convertBytes(bytes) {
152-
const pixelData = this.imageDataProcessor.convertToPixelData(bytes);
157+
let pixelData = this.imageDataProcessor.convertToPixelData(bytes);
158+
159+
if(this.filter){
160+
this.filter.applyFilter(pixelData, imageDataTransfomer.width, imageDataTransfomer.height);
161+
}
162+
153163
const imageData = this.context.createImageData(imageDataTransfomer.width, imageDataTransfomer.height);
154164
imageData.data.set(pixelData);
155165
return imageData;

0 commit comments

Comments
 (0)