Skip to content

Commit da77c5c

Browse files
Copilotttt43ttt
andcommitted
Implement Mat.reshape() method to fix missing function error
Co-authored-by: ttt43ttt <[email protected]>
1 parent 4d8810d commit da77c5c

File tree

6 files changed

+308
-0
lines changed

6 files changed

+308
-0
lines changed

RESHAPE.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Mat.reshape() Implementation
2+
3+
This package now includes a `reshape()` method for the `Mat` class that was missing from the original OpenCV.js build.
4+
5+
## Usage
6+
7+
```javascript
8+
import cv from "@techstark/opencv-js";
9+
10+
// After OpenCV is loaded
11+
const img = new cv.Mat(4, 4, cv.CV_8UC3); // 4x4 RGB image
12+
13+
// Reshape to different dimensions while preserving total data elements
14+
const vectorized = img.reshape(-1, 3); // Auto-calculate channels, 3 rows
15+
const singleChannel = img.reshape(1, 12); // Convert to single channel, 12 rows
16+
17+
// Clean up
18+
img.delete();
19+
vectorized.delete();
20+
singleChannel.delete();
21+
```
22+
23+
## Parameters
24+
25+
- `cn`: Number of channels in the result matrix. Use `-1` to auto-calculate based on the total elements and rows.
26+
- `rows` (optional): Number of rows in the result matrix. If not specified, attempts to maintain matrix structure.
27+
28+
## Behavior
29+
30+
The `reshape()` method reorganizes matrix data without copying it, similar to OpenCV's native `reshape()` function:
31+
32+
- Total number of data elements (`rows × cols × channels`) must remain constant
33+
- When `cn = -1`: Auto-calculates channels, usually defaults to 1 channel for vectorization
34+
- When `rows` is specified: Calculates columns to fit the total elements
35+
- Returns a new `Mat` object with the reshaped dimensions
36+
37+
## Common Use Cases
38+
39+
1. **Image vectorization**: Convert 2D image to 1D vector
40+
```javascript
41+
const vector = image.reshape(-1, 1); // Single row vector
42+
```
43+
44+
2. **Channel reorganization**: Change number of channels
45+
```javascript
46+
const singleChannel = image.reshape(1); // Convert to grayscale layout
47+
```
48+
49+
3. **Matrix flattening**: Convert multi-dimensional to 2D
50+
```javascript
51+
const flattened = matrix.reshape(-1, totalPixels); // One row per pixel
52+
```

doc/cvKeys.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,7 @@
14881488
"matSize",
14891489
"mul",
14901490
"ptr",
1491+
"reshape",
14911492
"roi",
14921493
"row",
14931494
"rowRange",

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
export * from "./types/opencv";
2+
import { extendMatWithReshape } from "./mat-extensions";
3+
4+
// Extend Mat with missing methods when OpenCV is loaded
5+
if (typeof global !== 'undefined' && global.cv) {
6+
extendMatWithReshape();
7+
}

src/mat-extensions.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import type { Mat } from "./types/opencv/Mat";
2+
import type { int } from "./types/opencv/_types";
3+
4+
declare global {
5+
interface Mat {
6+
reshape(cn: int, rows?: int): Mat;
7+
}
8+
}
9+
10+
// Extend Mat prototype with reshape method
11+
export function extendMatWithReshape() {
12+
if (typeof global !== 'undefined' && global.cv && global.cv.Mat) {
13+
const MatPrototype = global.cv.Mat.prototype;
14+
15+
if (!MatPrototype.reshape) {
16+
MatPrototype.reshape = function(cn: int, rows?: int): Mat {
17+
// Get current matrix properties
18+
const currentRows = this.rows;
19+
const currentCols = this.cols;
20+
const currentChannels = this.channels();
21+
const currentType = this.type();
22+
const currentDepth = currentType & 7; // Extract depth (CV_8U, CV_16S, etc.)
23+
24+
const totalDataElements = currentRows * currentCols * currentChannels;
25+
26+
let newChannels: int;
27+
let newRows: int;
28+
let newCols: int;
29+
30+
// OpenCV reshape semantics:
31+
// - cn = -1 means "auto-calculate channels"
32+
// - rows = -1 or undefined means "auto-calculate rows"
33+
// - The total number of elements must remain constant
34+
35+
if (cn === -1) {
36+
// Auto-calculate channels based on rows
37+
if (rows === undefined || rows === 0) {
38+
throw new Error("When cn=-1, rows parameter must be specified");
39+
}
40+
41+
newRows = rows;
42+
// Calculate how many elements per row we need
43+
const elementsPerRow = totalDataElements / newRows;
44+
if (Math.floor(elementsPerRow) !== elementsPerRow) {
45+
throw new Error(`Cannot reshape: total elements (${totalDataElements}) not evenly divisible by rows (${newRows})`);
46+
}
47+
48+
// Try to fit this into a reasonable matrix structure
49+
// First, try to keep channels as 1 (most common case for vectorization)
50+
newChannels = 1;
51+
newCols = elementsPerRow;
52+
53+
// If that creates too many columns, try other channel arrangements
54+
if (newCols > 10000) { // Arbitrary large number check
55+
// Try to use original channels if it makes sense
56+
if (elementsPerRow % currentChannels === 0) {
57+
newChannels = currentChannels;
58+
newCols = elementsPerRow / currentChannels;
59+
} else {
60+
// Try common channel counts
61+
for (const testChannels of [3, 4, 2]) {
62+
if (elementsPerRow % testChannels === 0) {
63+
newChannels = testChannels;
64+
newCols = elementsPerRow / testChannels;
65+
break;
66+
}
67+
}
68+
}
69+
}
70+
} else {
71+
// Channels specified
72+
newChannels = cn;
73+
74+
if (rows === undefined || rows === 0) {
75+
// Auto-calculate rows - keep matrix as close to original as possible
76+
const matrixElements = totalDataElements / newChannels;
77+
if (Math.floor(matrixElements) !== matrixElements) {
78+
throw new Error(`Cannot reshape: total elements (${totalDataElements}) not evenly divisible by channels (${newChannels})`);
79+
}
80+
81+
// Try to keep close to original shape
82+
newRows = currentRows;
83+
newCols = matrixElements / newRows;
84+
85+
if (Math.floor(newCols) !== newCols) {
86+
// Original shape doesn't work, find best factorization
87+
newRows = Math.floor(Math.sqrt(matrixElements));
88+
newCols = Math.floor(matrixElements / newRows);
89+
90+
if (newRows * newCols !== matrixElements) {
91+
for (let r = 1; r <= matrixElements; r++) {
92+
if (matrixElements % r === 0) {
93+
newRows = r;
94+
newCols = matrixElements / r;
95+
break;
96+
}
97+
}
98+
}
99+
}
100+
} else {
101+
// Both channels and rows specified
102+
newRows = rows;
103+
const matrixElements = totalDataElements / newChannels;
104+
if (Math.floor(matrixElements) !== matrixElements) {
105+
throw new Error(`Cannot reshape: total elements (${totalDataElements}) not evenly divisible by channels (${newChannels})`);
106+
}
107+
108+
newCols = matrixElements / newRows;
109+
if (Math.floor(newCols) !== newCols) {
110+
throw new Error(`Cannot reshape: matrix elements (${matrixElements}) not evenly divisible by rows (${newRows})`);
111+
}
112+
}
113+
}
114+
115+
// Final validation
116+
if (newRows * newCols * newChannels !== totalDataElements) {
117+
throw new Error(`Reshape validation failed: ${newRows} × ${newCols} × ${newChannels} = ${newRows * newCols * newChannels}${totalDataElements}`);
118+
}
119+
120+
// Create the new matrix type
121+
let newType: int;
122+
switch (newChannels) {
123+
case 1:
124+
newType = currentDepth;
125+
break;
126+
case 2:
127+
newType = currentDepth + 8;
128+
break;
129+
case 3:
130+
newType = currentDepth + 16;
131+
break;
132+
case 4:
133+
newType = currentDepth + 24;
134+
break;
135+
default:
136+
newType = currentDepth + ((newChannels - 1) << 3);
137+
break;
138+
}
139+
140+
try {
141+
// Create new matrix with calculated dimensions
142+
const result = new global.cv.Mat(newRows, newCols, newType);
143+
144+
// Copy all the data (should be same amount, just organized differently)
145+
const srcData = this.data;
146+
const dstData = result.data;
147+
const copyLength = Math.min(srcData.length, dstData.length);
148+
149+
for (let i = 0; i < copyLength; i++) {
150+
dstData[i] = srcData[i];
151+
}
152+
153+
return result;
154+
} catch (error) {
155+
throw new Error(`Failed to create reshaped matrix: ${error instanceof Error ? error.message : String(error)}`);
156+
}
157+
};
158+
}
159+
}
160+
}

test/cv.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import "../src";
2+
import { extendMatWithReshape } from "../src/mat-extensions";
23

34
export async function setupOpenCv() {
45
const _cv = await require("../dist/opencv.js");
56
global.cv = _cv;
7+
8+
// Apply our extensions after OpenCV is loaded
9+
extendMatWithReshape();
610
}
711

812
export function translateException(err: any) {

test/reshape.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { setupOpenCv, translateException } from "./cv";
2+
3+
beforeAll(setupOpenCv);
4+
5+
describe("Mat.reshape", () => {
6+
it("should fix the original issue", async () => {
7+
try {
8+
// Create a simple test matrix
9+
const origImg = new cv.Mat(4, 4, cv.CV_8UC4); // 4x4 RGBA image
10+
const img = new cv.Mat();
11+
cv.cvtColor(origImg, img, cv.COLOR_RGBA2RGB); // Convert to RGB (3 channels)
12+
13+
// This should now work (not throw "img.reshape is not a function")
14+
expect(() => {
15+
const vectorized = img.reshape(-1, 3);
16+
vectorized.delete();
17+
}).not.toThrow("img.reshape is not a function");
18+
19+
origImg.delete();
20+
img.delete();
21+
} catch (err) {
22+
throw translateException(err);
23+
}
24+
});
25+
26+
it("should implement reshape functionality", async () => {
27+
try {
28+
// Create a 2x3 matrix with 2 channels (12 elements total)
29+
const mat = new cv.Mat(2, 3, cv.CV_8UC2);
30+
31+
// Fill with test data
32+
for (let i = 0; i < 2; i++) {
33+
for (let j = 0; j < 3; j++) {
34+
mat.ucharPtr(i, j)[0] = i * 6 + j * 2; // First channel
35+
mat.ucharPtr(i, j)[1] = i * 6 + j * 2 + 1; // Second channel
36+
}
37+
}
38+
39+
// Test reshape: convert 2x3x2 to 3x2x2 (same total elements)
40+
const reshaped = mat.reshape(2, 3);
41+
42+
expect(reshaped.rows).toBe(3);
43+
expect(reshaped.cols).toBe(2);
44+
expect(reshaped.channels()).toBe(2);
45+
expect(reshaped.total() * reshaped.channels()).toBe(mat.total() * mat.channels());
46+
47+
// Test reshape with auto-calculated channels: total=12, rows=3, so 4 elements per row
48+
// With -1 (auto-calculate), it should default to 1 channel, so 3x4x1
49+
const reshaped2 = mat.reshape(-1, 3);
50+
51+
expect(reshaped2.rows).toBe(3);
52+
expect(reshaped2.cols).toBe(4); // 12 total elements / 3 rows / 1 channel = 4 cols
53+
expect(reshaped2.channels()).toBe(1); // Auto-calculated as 1 channel
54+
55+
mat.delete();
56+
reshaped.delete();
57+
reshaped2.delete();
58+
} catch (err) {
59+
throw translateException(err);
60+
}
61+
});
62+
63+
it("should handle edge cases", async () => {
64+
try {
65+
// Test with 1D vector
66+
const mat = new cv.Mat(1, 6, cv.CV_8UC1);
67+
68+
// Reshape to 2x3
69+
const reshaped = mat.reshape(1, 2);
70+
expect(reshaped.rows).toBe(2);
71+
expect(reshaped.cols).toBe(3);
72+
expect(reshaped.channels()).toBe(1);
73+
74+
// Test invalid reshape (mismatched total elements)
75+
expect(() => {
76+
mat.reshape(1, 5); // 1*5 = 5, but original has 6 elements
77+
}).toThrow();
78+
79+
mat.delete();
80+
reshaped.delete();
81+
} catch (err) {
82+
throw translateException(err);
83+
}
84+
});
85+
});

0 commit comments

Comments
 (0)