-
-
Notifications
You must be signed in to change notification settings - Fork 92
Implement preprocessing pipeline with addBorder preprocessor #626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
c95103f
19c1514
95f544c
2a4c333
a547dbd
d88f28a
439c2d5
d9e3e28
e5161d4
d286161
87e5855
c346a17
6b73def
01fd518
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export type { PreprocessorFunction } from './preprocessor'; | ||
|
||
| export { | ||
| addBorder, | ||
| applyPreprocessors, | ||
| Preprocessors, | ||
| } from './preprocessor'; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| import ImageWrapper from '../common/image_wrapper'; | ||
|
|
||
| /** | ||
| * A preprocessor function that transforms image data. | ||
| * Preprocessors are applied to the image data after frame grabbing | ||
| * but before barcode localization and decoding. | ||
| * | ||
| * IMPORTANT: Preprocessors should maintain the same image dimensions. | ||
| * The returned ImageWrapper should have the same size as the input. | ||
| * | ||
| * @param imageWrapper The image wrapper to process | ||
| * @returns The processed image wrapper (same size as input) | ||
| */ | ||
| export type PreprocessorFunction = (imageWrapper: ImageWrapper) => ImageWrapper; | ||
|
||
|
|
||
| /** | ||
| * Built-in preprocessor: Adds a white border around the image. | ||
| * This is useful for barcodes that lack sufficient quiet zone (whitespace) | ||
| * around them. When a barcode is generated or cropped without proper margins, | ||
| * the decoder may fail to detect it. Adding a border simulates the whitespace | ||
| * that would naturally exist when displaying the barcode on paper or screen. | ||
| * | ||
| * The image is shrunk slightly and centered, with white border pixels added | ||
| * around it. The output size remains the same as the input size. | ||
| * | ||
| * @param borderSize Number of pixels of white border to add on each side | ||
| * @returns A preprocessor function that adds the border | ||
| * | ||
| * @example | ||
| * // Add 10 pixels of white border around all images | ||
| * config.preprocessing = [Quagga.Preprocessors.addBorder(10)]; | ||
| */ | ||
| export function addBorder(borderSize: number): PreprocessorFunction { | ||
| return (imageWrapper: ImageWrapper): ImageWrapper => { | ||
| if (borderSize <= 0) { | ||
| return imageWrapper; | ||
| } | ||
|
|
||
| const width = imageWrapper.size.x; | ||
| const height = imageWrapper.size.y; | ||
|
|
||
| // Calculate the inner image area (shrunk to make room for border) | ||
| const innerWidth = width - (borderSize * 2); | ||
| const innerHeight = height - (borderSize * 2); | ||
|
|
||
| // If border is too large for the image, just fill with white | ||
| if (innerWidth <= 0 || innerHeight <= 0) { | ||
| for (let i = 0; i < imageWrapper.data.length; i++) { | ||
| imageWrapper.data[i] = 255; | ||
| } | ||
| return imageWrapper; | ||
| } | ||
|
|
||
| // Calculate scale factors for shrinking | ||
| const scaleX = innerWidth / width; | ||
| const scaleY = innerHeight / height; | ||
|
|
||
| // Create a temporary copy of the original data | ||
| // Note: We need to handle both TypedArray and Array<number> types | ||
| const originalData = imageWrapper.data instanceof Uint8Array | ||
| ? new Uint8Array(imageWrapper.data) | ||
| : new Uint8Array(imageWrapper.data); | ||
ericblade marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Fill the entire image with white first | ||
| if (imageWrapper.data instanceof Uint8Array) { | ||
| imageWrapper.data.fill(255); | ||
| } else { | ||
| for (let i = 0; i < imageWrapper.data.length; i++) { | ||
| imageWrapper.data[i] = 255; | ||
| } | ||
| } | ||
|
|
||
| // Copy the shrunk image into the center using bilinear interpolation | ||
| for (let y = 0; y < innerHeight; y++) { | ||
| for (let x = 0; x < innerWidth; x++) { | ||
| // Map destination coordinates to source coordinates | ||
| const srcX = x / scaleX; | ||
| const srcY = y / scaleY; | ||
|
|
||
| // Bilinear interpolation | ||
| const x0 = Math.floor(srcX); | ||
| const y0 = Math.floor(srcY); | ||
| const x1 = Math.min(x0 + 1, width - 1); | ||
| const y1 = Math.min(y0 + 1, height - 1); | ||
|
|
||
| const fx = srcX - x0; | ||
| const fy = srcY - y0; | ||
|
|
||
| const v00 = originalData[y0 * width + x0]; | ||
| const v10 = originalData[y0 * width + x1]; | ||
| const v01 = originalData[y1 * width + x0]; | ||
| const v11 = originalData[y1 * width + x1]; | ||
|
|
||
| const v0 = v00 * (1 - fx) + v10 * fx; | ||
| const v1 = v01 * (1 - fx) + v11 * fx; | ||
| const value = Math.round(v0 * (1 - fy) + v1 * fy); | ||
|
|
||
| // Write to destination with border offset | ||
| const destIdx = (y + borderSize) * width + (x + borderSize); | ||
| imageWrapper.data[destIdx] = value; | ||
| } | ||
| } | ||
|
|
||
| return imageWrapper; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Applies a chain of preprocessor functions to an image wrapper. | ||
| * @param imageWrapper The image wrapper to process | ||
| * @param preprocessors Array of preprocessor functions to apply in order | ||
| * @returns The processed image wrapper (same instance, potentially modified) | ||
| */ | ||
| export function applyPreprocessors( | ||
| imageWrapper: ImageWrapper, | ||
| preprocessors: PreprocessorFunction[], | ||
| ): ImageWrapper { | ||
| let result = imageWrapper; | ||
| for (const preprocessor of preprocessors) { | ||
| result = preprocessor(result); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Collection of built-in preprocessor factories. | ||
| * Users can use these or provide their own PreprocessorFunction implementations. | ||
| */ | ||
| export const Preprocessors = { | ||
| addBorder, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import { expect } from 'chai'; | ||
| import ImageWrapper from '../../common/image_wrapper'; | ||
| import { addBorder, applyPreprocessors, Preprocessors } from '../preprocessor'; | ||
|
|
||
| describe('Preprocessors', () => { | ||
| describe('addBorder', () => { | ||
| it('should return the same image when borderSize is 0', () => { | ||
| const size = { x: 10, y: 10, type: 'XYSize' as const }; | ||
| const data = new Uint8Array(100); | ||
| data.fill(128); | ||
| const wrapper = new ImageWrapper(size, data); | ||
|
|
||
| const preprocessor = addBorder(0); | ||
| const result = preprocessor(wrapper); | ||
|
|
||
| expect(result).to.equal(wrapper); | ||
| expect(result.data).to.deep.equal(data); | ||
| }); | ||
|
|
||
| it('should return the same image when borderSize is negative', () => { | ||
| const size = { x: 10, y: 10, type: 'XYSize' as const }; | ||
| const data = new Uint8Array(100); | ||
| data.fill(128); | ||
| const wrapper = new ImageWrapper(size, data); | ||
|
|
||
| const preprocessor = addBorder(-5); | ||
| const result = preprocessor(wrapper); | ||
|
|
||
| expect(result).to.equal(wrapper); | ||
| }); | ||
|
|
||
| it('should add white border and shrink image', () => { | ||
| const size = { x: 10, y: 10, type: 'XYSize' as const }; | ||
| const data = new Uint8Array(100); | ||
| data.fill(0); // Black image | ||
| const wrapper = new ImageWrapper(size, data); | ||
|
|
||
| const borderSize = 2; | ||
| const preprocessor = addBorder(borderSize); | ||
| const result = preprocessor(wrapper); | ||
|
|
||
| // Size should remain the same | ||
| expect(result.size.x).to.equal(10); | ||
| expect(result.size.y).to.equal(10); | ||
|
|
||
| // Border pixels should be white (255) | ||
| // Top border | ||
| for (let y = 0; y < borderSize; y++) { | ||
| for (let x = 0; x < size.x; x++) { | ||
| expect(result.data[y * size.x + x]).to.equal(255, `Top border at (${x}, ${y})`); | ||
| } | ||
| } | ||
| // Bottom border | ||
| for (let y = size.y - borderSize; y < size.y; y++) { | ||
| for (let x = 0; x < size.x; x++) { | ||
| expect(result.data[y * size.x + x]).to.equal(255, `Bottom border at (${x}, ${y})`); | ||
| } | ||
| } | ||
| // Left border | ||
| for (let y = 0; y < size.y; y++) { | ||
| for (let x = 0; x < borderSize; x++) { | ||
| expect(result.data[y * size.x + x]).to.equal(255, `Left border at (${x}, ${y})`); | ||
| } | ||
| } | ||
| // Right border | ||
| for (let y = 0; y < size.y; y++) { | ||
| for (let x = size.x - borderSize; x < size.x; x++) { | ||
| expect(result.data[y * size.x + x]).to.equal(255, `Right border at (${x}, ${y})`); | ||
| } | ||
| } | ||
|
|
||
| // Center should contain shrunk black image (approximately 0) | ||
| const centerX = Math.floor(size.x / 2); | ||
| const centerY = Math.floor(size.y / 2); | ||
| expect(result.data[centerY * size.x + centerX]).to.be.lessThan(50, 'Center should be dark'); | ||
| }); | ||
|
|
||
| it('should fill with white when border is too large', () => { | ||
| const size = { x: 10, y: 10, type: 'XYSize' as const }; | ||
| const data = new Uint8Array(100); | ||
| data.fill(0); | ||
| const wrapper = new ImageWrapper(size, data); | ||
|
|
||
| // Border of 5 on each side = 10 total, which equals the image size | ||
| const preprocessor = addBorder(5); | ||
| const result = preprocessor(wrapper); | ||
|
|
||
| // All pixels should be white | ||
| for (let i = 0; i < result.data.length; i++) { | ||
| expect(result.data[i]).to.equal(255); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| describe('applyPreprocessors', () => { | ||
| it('should apply preprocessors in order', () => { | ||
| const size = { x: 10, y: 10, type: 'XYSize' as const }; | ||
| const data = new Uint8Array(100); | ||
| data.fill(100); | ||
| const wrapper = new ImageWrapper(size, data); | ||
|
|
||
| const calls: string[] = []; | ||
|
|
||
| const preprocessor1 = (img: ImageWrapper) => { | ||
| calls.push('first'); | ||
| return img; | ||
| }; | ||
|
|
||
| const preprocessor2 = (img: ImageWrapper) => { | ||
| calls.push('second'); | ||
| return img; | ||
| }; | ||
|
|
||
| applyPreprocessors(wrapper, [preprocessor1, preprocessor2]); | ||
|
|
||
| expect(calls).to.deep.equal(['first', 'second']); | ||
| }); | ||
|
|
||
| it('should return original wrapper when no preprocessors', () => { | ||
| const size = { x: 10, y: 10, type: 'XYSize' as const }; | ||
| const data = new Uint8Array(100); | ||
| const wrapper = new ImageWrapper(size, data); | ||
|
|
||
| const result = applyPreprocessors(wrapper, []); | ||
|
|
||
| expect(result).to.equal(wrapper); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Preprocessors object', () => { | ||
| it('should export addBorder function', () => { | ||
| expect(Preprocessors.addBorder).to.be.a('function'); | ||
| }); | ||
|
|
||
| it('should create valid preprocessor from addBorder', () => { | ||
| const preprocessor = Preprocessors.addBorder(5); | ||
| expect(preprocessor).to.be.a('function'); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is sharp necessary? doesn't sharp require native code? or is there a browser version of it now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
sharpfrom package.json in commit d88f28a. It was installed during initial development for test image creation but is not used by the library code.