I want to make a image classifier, but I don't know python.
Tensorflow.js works with javascript, which I am familiar with. Can models be trained with it and what would be the steps to do so?
Frankly I have no clue where to start.
The only thing I figured out is how to load "mobilenet", which apparently is a set of pre-trained models, and classify images with it:
const tf = require('#tensorflow/tfjs'),
mobilenet = require('#tensorflow-models/mobilenet'),
tfnode = require('#tensorflow/tfjs-node'),
fs = require('fs-extra');
const imageBuffer = await fs.readFile(......),
tfimage = tfnode.node.decodeImage(imageBuffer),
mobilenetModel = await mobilenet.load();
const results = await mobilenetModel.classify(tfimage);
which works, but it's no use to me because I want to train my own model using my images with labels that I create.
=======================
Say I have a bunch of images and labels. How do I use them to train a model?
const myData = JSON.parse(await fs.readFile('files.json'));
for(const data of myData){
const image = await fs.readFile(data.imagePath),
labels = data.labels;
// how to train, where to pass image and labels ?
}
First of all, the images needs to be converted to tensors. The first approach would be to create a tensor containing all the features (respectively a tensor containing all the labels). This should the way to go only if the dataset contains few images.
const imageBuffer = await fs.readFile(feature_file);
tensorFeature = tfnode.node.decodeImage(imageBuffer) // create a tensor for the image
// create an array of all the features
// by iterating over all the images
tensorFeatures = tf.stack([tensorFeature, tensorFeature2, tensorFeature3])
The labels would be an array indicating the type of each image
labelArray = [0, 1, 2] // maybe 0 for dog, 1 for cat and 2 for birds
One needs now to create a hot encoding of the labels
tensorLabels = tf.oneHot(tf.tensor1d(labelArray, 'int32'), 3);
Once there is the tensors, one would need to create the model for training. Here is a simple model.
const model = tf.sequential();
model.add(tf.layers.conv2d({
inputShape: [height, width, numberOfChannels], // numberOfChannels = 3 for colorful images and one otherwise
filters: 32,
kernelSize: 3,
activation: 'relu',
}));
model.add(tf.layers.flatten());
model.add(tf.layers.dense({units: 3, activation: 'softmax'}));
Then the model can be trained
model.fit(tensorFeatures, tensorLabels)
If the dataset contains a lot of images, one would need to create a tfDataset instead. This answer discusses why.
const genFeatureTensor = image => {
const imageBuffer = await fs.readFile(feature_file);
return tfnode.node.decodeImage(imageBuffer)
}
const labelArray = indice => Array.from({length: numberOfClasses}, (_, k) => k === indice ? 1 : 0)
function* dataGenerator() {
const numElements = numberOfImages;
let index = 0;
while (index < numFeatures) {
const feature = genFeatureTensor(imagePath);
const label = tf.tensor1d(labelArray(classImageIndex))
index++;
yield {xs: feature, ys: label};
}
}
const ds = tf.data.generator(dataGenerator).batch(1) // specify an appropriate batchsize;
And use model.fitDataset(ds) to train the model
The above is for training in nodejs. To do such a processing in the browser, genFeatureTensor can be written as follow:
function loadImage(url){
return new Promise((resolve, reject) => {
const im = new Image()
im.crossOrigin = 'anonymous'
im.src = 'url'
im.onload = () => {
resolve(im)
}
})
}
genFeatureTensor = image => {
const img = await loadImage(image);
return tf.browser.fromPixels(image);
}
One word of caution is that doing heavy processing might block the main thread in the browser. This is where web workers come into play.
Consider the exemple https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/#0
What they do is:
take a BIG png image (a vertical concatenation of images)
take some labels
build the dataset (data.js)
then train
The building of the dataset is as follows:
images
The big image is divided into n vertical chunks.
(n being chunkSize)
Consider a chunkSize of size 2.
Given the pixel matrix of image 1:
1 2 3
4 5 6
Given the pixel matrix of image 2 is
7 8 9
1 2 3
The resulting array would be
1 2 3 4 5 6 7 8 9 1 2 3 (the 1D concatenation somehow)
So basically at the end of the processing, you have a big buffer representing
[...Buffer(image1), ...Buffer(image2), ...Buffer(image3)]
labels
That kind of formatting is done a lot for classification problems. Instead of classifying with a number, they take a boolean array.
To predict 7 out of 10 classes we would consider
[0,0,0,0,0,0,0,1,0,0] // 1 in 7e position, array 0-indexed
What you can do to get started
Take your image (and its associated label)
Load your image to the canvas
Extract its associated buffer
Concatenate all your image's buffer as a big buffer. That's it for xs.
Take all your associated labels, map them as a boolean array, and concatenate them.
Below, I subclass MNistData::load (the rest can be let as is (except in script.js where you need to instantiate your own class instead)
I still generate 28x28 images, write a digit on it, and get a perfect accuracy since I don't include noise or voluntarily wrong labelings.
import {MnistData} from './data.js'
const IMAGE_SIZE = 784;// actually 28*28...
const NUM_CLASSES = 10;
const NUM_DATASET_ELEMENTS = 5000;
const NUM_TRAIN_ELEMENTS = 4000;
const NUM_TEST_ELEMENTS = NUM_DATASET_ELEMENTS - NUM_TRAIN_ELEMENTS;
function makeImage (label, ctx) {
ctx.fillStyle = 'black'
ctx.fillRect(0, 0, 28, 28) // hardcoded, brrr
ctx.fillStyle = 'white'
ctx.fillText(label, 10, 20) // print a digit on the canvas
}
export class MyMnistData extends MnistData{
async load() {
const canvas = document.createElement('canvas')
canvas.width = 28
canvas.height = 28
let ctx = canvas.getContext('2d')
ctx.font = ctx.font.replace(/\d+px/, '18px')
let labels = new Uint8Array(NUM_DATASET_ELEMENTS*NUM_CLASSES)
// in data.js, they use a batch of images (aka chunksize)
// let's even remove it for simplification purpose
const datasetBytesBuffer = new ArrayBuffer(NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
for (let i = 0; i < NUM_DATASET_ELEMENTS; i++) {
const datasetBytesView = new Float32Array(
datasetBytesBuffer, i * IMAGE_SIZE * 4,
IMAGE_SIZE);
// BEGIN our handmade label + its associated image
// notice that you could loadImage( images[i], datasetBytesView )
// so you do them by bulk and synchronize after your promises after "forloop"
const label = Math.floor(Math.random()*10)
labels[i*NUM_CLASSES + label] = 1
makeImage(label, ctx)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// END you should be able to load an image to canvas :)
for (let j = 0; j < imageData.data.length / 4; j++) {
// NOTE: you are storing a FLOAT of 4 bytes, in [0;1] even though you don't need it
// We could make it with a uint8Array (assuming gray scale like we are) without scaling to 1/255
// they probably did it so you can copy paste like me for color image afterwards...
datasetBytesView[j] = imageData.data[j * 4] / 255;
}
}
this.datasetImages = new Float32Array(datasetBytesBuffer);
this.datasetLabels = labels
//below is copy pasted
this.trainIndices = tf.util.createShuffledIndices(NUM_TRAIN_ELEMENTS);
this.testIndices = tf.util.createShuffledIndices(NUM_TEST_ELEMENTS);
this.trainImages = this.datasetImages.slice(0, IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
this.testImages = this.datasetImages.slice(IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
this.trainLabels =
this.datasetLabels.slice(0, NUM_CLASSES * NUM_TRAIN_ELEMENTS);// notice, each element is an array of size NUM_CLASSES
this.testLabels =
this.datasetLabels.slice(NUM_CLASSES * NUM_TRAIN_ELEMENTS);
}
}
I found a tutorial [1] how to use existing model to train new classes. Main code parts here:
index.html head:
<script src="https://unpkg.com/#tensorflow-models/knn-classifier"></script>
index.html body:
<button id="class-a">Add A</button>
<button id="class-b">Add B</button>
<button id="class-c">Add C</button>
index.js:
const classifier = knnClassifier.create();
....
// Reads an image from the webcam and associates it with a specific class
// index.
const addExample = async classId => {
// Capture an image from the web camera.
const img = await webcam.capture();
// Get the intermediate activation of MobileNet 'conv_preds' and pass that
// to the KNN classifier.
const activation = net.infer(img, 'conv_preds');
// Pass the intermediate activation to the classifier.
classifier.addExample(activation, classId);
// Dispose the tensor to release the memory.
img.dispose();
};
// When clicking a button, add an example for that class.
document.getElementById('class-a').addEventListener('click', () => addExample(0));
document.getElementById('class-b').addEventListener('click', () => addExample(1));
document.getElementById('class-c').addEventListener('click', () => addExample(2));
....
Main idea is to use existing network to make its prediction and then substitute the found label with your own one.
Complete code is in the tutorial. Another promising, more advanced one in [2]. It needs strict pre processing, so I leave it only here, I mean it is so much more advanced one.
Sources:
[1] https://codelabs.developers.google.com/codelabs/tensorflowjs-teachablemachine-codelab/index.html#6
[2] https://towardsdatascience.com/training-custom-image-classification-model-on-the-browser-with-tensorflow-js-and-angular-f1796ed24934
TL;DR
MNIST is the image recognition Hello World. After learning it by heart, these questions in your mind are easy to solve.
Question setting:
Your main question written is
// how to train, where to pass image and labels ?
inside your code block. For those I found perfect answer from examples of Tensorflow.js examples section: MNIST example. My below links have pure javascript and node.js versions of it and Wikipedia explanation. I will go them through on the level necessary to answer the main question in your mind and I will add also perspectives how your own images and labels have anything to do with MNIST image set and the examples using it.
First things first:
Code snippets.
where to pass images (Node.js sample)
async function loadImages(filename) {
const buffer = await fetchOnceAndSaveToDiskWithBuffer(filename);
const headerBytes = IMAGE_HEADER_BYTES;
const recordBytes = IMAGE_HEIGHT * IMAGE_WIDTH;
const headerValues = loadHeaderValues(buffer, headerBytes);
assert.equal(headerValues[0], IMAGE_HEADER_MAGIC_NUM);
assert.equal(headerValues[2], IMAGE_HEIGHT);
assert.equal(headerValues[3], IMAGE_WIDTH);
const images = [];
let index = headerBytes;
while (index < buffer.byteLength) {
const array = new Float32Array(recordBytes);
for (let i = 0; i < recordBytes; i++) {
// Normalize the pixel values into the 0-1 interval, from
// the original 0-255 interval.
array[i] = buffer.readUInt8(index++) / 255;
}
images.push(array);
}
assert.equal(images.length, headerValues[1]);
return images;
}
Notes:
MNIST dataset is a huge image, where in one file there are several images like tiles in puzzle, each and every with same size, side by side, like boxes in x and y coordination table. Each box has one sample and corresponding x and y in the labels array has the label. From this example, it is not a big deal to turn it to several files format, so that actually only one pic at a time is given to the while loop to handle.
Labels:
async function loadLabels(filename) {
const buffer = await fetchOnceAndSaveToDiskWithBuffer(filename);
const headerBytes = LABEL_HEADER_BYTES;
const recordBytes = LABEL_RECORD_BYTE;
const headerValues = loadHeaderValues(buffer, headerBytes);
assert.equal(headerValues[0], LABEL_HEADER_MAGIC_NUM);
const labels = [];
let index = headerBytes;
while (index < buffer.byteLength) {
const array = new Int32Array(recordBytes);
for (let i = 0; i < recordBytes; i++) {
array[i] = buffer.readUInt8(index++);
}
labels.push(array);
}
assert.equal(labels.length, headerValues[1]);
return labels;
}
Notes:
Here, labels are also byte data in a file. In Javascript world, and with the approach you have in your starting point, labels could also be a json array.
train the model:
await data.loadData();
const {images: trainImages, labels: trainLabels} = data.getTrainData();
model.summary();
let epochBeginTime;
let millisPerStep;
const validationSplit = 0.15;
const numTrainExamplesPerEpoch =
trainImages.shape[0] * (1 - validationSplit);
const numTrainBatchesPerEpoch =
Math.ceil(numTrainExamplesPerEpoch / batchSize);
await model.fit(trainImages, trainLabels, {
epochs,
batchSize,
validationSplit
});
Notes:
Here model.fit is the actual line of code that does the thing: trains the model.
Results of the whole thing:
const {images: testImages, labels: testLabels} = data.getTestData();
const evalOutput = model.evaluate(testImages, testLabels);
console.log(
`\nEvaluation result:\n` +
` Loss = ${evalOutput[0].dataSync()[0].toFixed(3)}; `+
`Accuracy = ${evalOutput[1].dataSync()[0].toFixed(3)}`);
Note:
In Data Science, also this time here, the most faschinating part is to know how well the model survives the test of new data and no labels, can it label them or not? For that is the evaluation part that now prints us some numbers.
Loss and accuracy: [4]
The lower the loss, the better a model (unless the model has over-fitted to the training data). The loss is calculated on training and validation and its interperation is how well the model is doing for these two sets. Unlike accuracy, loss is not a percentage. It is a summation of the errors made for each example in training or validation sets.
..
The accuracy of a model is usually determined after the model parameters are learned and fixed and no learning is taking place. Then the test samples are fed to the model and the number of mistakes (zero-one loss) the model makes are recorded, after comparison to the true targets.
More information:
In the github pages, in README.md file, there is a link to tutorial, where all in the github example is explained in greater detail.
[1] https://github.com/tensorflow/tfjs-examples/tree/master/mnist
[2] https://github.com/tensorflow/tfjs-examples/tree/master/mnist-node
[3] https://en.wikipedia.org/wiki/MNIST_database
[4] How to interpret "loss" and "accuracy" for a machine learning model
Related
I am new to TensorflowJS and I try to code something but I am stuck...
I have two input layers like that:
const input1 = tf.input({ shape: [64, 64, 3] });
const input2 = tf.input({ shape: [1536] });
The first one is for an image of 64 by 64 and the 3 is for RGB.
The second one is for an array that contains 1536 numbers (floats).
I tried to concatenate them with .concatenate().apply(input1, input2) but got the following error:
ValueError: A `Concatenate` layer requires inputs with matching shapes except for the concat axis. Got input shapes: [[null,64,64,3],[null,1536]]
I also tried to add { axis: -1 } or { axis: 1 } (found that on stack overflow but that doesnt work too).
I also tried that (answer by chat gpt) :
const flatten1 = tf.layers.flatten().apply(input1);
const flatten2 = tf.layers.flatten().apply(input2);
const concat = tf.layers.concatenate({ axis: -1 }).apply([flatten1, flatten2]);
but same error...
Can someone help me? I just want to add this to my tf.sequential() as an input...
PS: This is the module I use:
const tf = require('#tensorflow/tfjs-node');
This just shows you what concatenation is, and how can be done for those specific inputs.
This is a textual description of the code at the bottom of the post:
create an image-like shape (width, height, colorChannels)
create a random one dimensional array (aka vector) of 1536 values
1536 elements can be reshaped into a little "image" of 8 x 64 and 3 channels
Result is 72 x 64 x 8 so this should hint you what the code did (extend the axis with different number of values, that is it.
const original = tf.ones([64, 64, 3]);
const random = tf.randomNormal([1536]);
const reshaped = tf.reshape(random, [8, 64, 3]);
const axis = 0;
const concat = tf.concat([original, reshaped], axis);
console.log(concat);
<script src="https://cdn.jsdelivr.net/npm/#tensorflow/tfjs#2.0.0/dist/tf.min.js"></script>
Another possibility is flat, extend and then back re-shape but I find this simple enough.
It has two inputs and one output.
Input: [Temperature, Humidity]
Output: [wattage]
I learned as follows
Even after 5 million rotations, it does not work properly.
Did I choose the wrong option?
var input_data = [
[-2.4,2.7,9,14.2,17.1,22.8,281,25.9,22.6,15.6,8.2,0.6],
[58,56,63,54,68,73,71,74,71,70,68,62]
];
var power_data = [239,224,189,189,179,192,243,317,224,190,189,202];
var reason_data = tf.tensor2d(input_data);
var result_data = tf.tensor(power_data);
var X = tf.input({ shape: [2] });
var Y = tf.layers.dense({ units: 1 }).apply(X);
var model = tf.model({ inputs: X, outputs: Y });
var compileParam = { optimizer: tf.train.adam(), loss: tf.losses.meanSquaredError }
model.compile(compileParam);
var fitParam = {
epochs: 500000,
callbacks: {
onEpochEnd: function (epoch, logs) {
console.log('epoch', epoch, logs, "RMSE --> ", Math.sqrt(logs.loss));
}
}
}
model.fit(reason_data, result_data, fitParam).then(function (result) {
var final_result = model.predict(reason_data);
final_result.print();
model.save('file:///path/');
});
The following is the result for 5 million times.
It should be the same as power_data , but it failed.
What should I fix?
While there is rarely one simple reason to point to when a model doesn't perform the way you would expect, here are some options to consider:
You don't have enough data points. Twelve is not nearly sufficient to get an accurate result.
You need to normalize the data of the input tensors. Given that your two features [temperature and humidity] have different ranges, they need to be normalized to give them equal opportunity to influence the output. The following is a normalization function you could start with:
function normalize(tensor, min, max) {
const result = tf.tidy(function() {
// Find the minimum value contained in the Tensor.
const MIN_VALUES = min || tf.min(tensor, 0);
// Find the maximum value contained in the Tensor.
const MAX_VALUES = max || tf.max(tensor, 0);
// Now calculate subtract the MIN_VALUE from every value in the Tensor
// And store the results in a new Tensor.
const TENSOR_SUBTRACT_MIN_VALUE = tf.sub(tensor, MIN_VALUES);
// Calculate the range size of possible values.
const RANGE_SIZE = tf.sub(MAX_VALUES, MIN_VALUES);
// Calculate the adjusted values divided by the range size as a new Tensor.
const NORMALIZED_VALUES = tf.div(TENSOR_SUBTRACT_MIN_VALUE, RANGE_SIZE);
// Return the important tensors.
return {NORMALIZED_VALUES, MIN_VALUES, MAX_VALUES};
});
return result;
}
You should try a different optimizer. Adam might be the best choice, but for a linear regression problem such as this, you should also consider Stochastic Gradient Descent (SGD).
Check out this sample code for an example that uses normalization and sgd. I ran your data points through the code (after making the changes to the tensors so they fit your data), and I was able to reduce the loss to less than 40. There is room for improvement, but that's where adding more data points comes in.
Okay so I've been trying to map some heatmaps to a Revit room using the DataViz api. I was able to get X Y Z from Revit for the sensor inside the rooms, i've substracted the viewer.model.getGlobalOffset() and managed to show some sprites on these points. I know for a fact that those sprites / points are inside Rooms, but whenever I try to use the same points to load a heatmap I get the Some devices did not map to a room: warning and no heatmap is displayed.
Following the API documentation this warning appears when there is no room information in the point. Did I miss anything? This is "my" code:
async function loadHeatmaps(model){
const dataVizExtn = await viewer.loadExtension("Autodesk.DataVisualization");
// Given a model loaded from Forge
const structureInfo = new Autodesk.DataVisualization.Core.ModelStructureInfo(model);
const devices = [
{
id: "Oficina 6", // An ID to identify this device
name:"Oficina-",
position: { x: 22.475382737884104, y: 7.4884431474006163, z: 3.0 }, // World coordinates of this device
sensorTypes: ["temperature", "humidity"], // The types/properties this device exposes
}
];
var offset = viewer.model.getGlobalOffset();
removeOffset(devices[0],offset)
// Generates `SurfaceShadingData` after assigning each device to a room.
const shadingData = await structureInfo.generateSurfaceShadingData(devices);
console.log(shadingData)
// Use the resulting shading data to generate heatmap from.
await dataVizExtn.setupSurfaceShading(model, shadingData);
// Register color stops for the heatmap. Along with the normalized sensor value
// in the range of [0.0, 1.0], `renderSurfaceShading` will interpolate the final
// heatmap color based on these specified colors.
const sensorColors = [0x0000ff, 0x00ff00, 0xffff00, 0xff0000];
// Set heatmap colors for temperature
const sensorType = "temperature";
dataVizExtn.registerSurfaceShadingColors(sensorType, sensorColors);
// Function that provides sensor value in the range of [0.0, 1.0]
function getSensorValue(surfaceShadingPoint, sensorType) {
// The `SurfaceShadingPoint.id` property matches one of the identifiers passed
// to `generateSurfaceShadingData` function. In our case above, this will either
// be "cafeteria-entrace-01" or "cafeteria-exit-01".
const deviceId = surfaceShadingPoint.id;
// Read the sensor data, along with its possible value range
let sensorValue = readSensorValue(deviceId, sensorType);
const maxSensorValue = getMaxSensorValue(sensorType);
const minSensorValue = getMinSensorValue(sensorType);
// Normalize sensor value to [0, 1.0]
sensorValue = (sensorValue - minSensorValue) / (maxSensorValue - minSensorValue);
return clamp(sensorValue, 0.0, 1.0);
}
// This value can also be a room instead of a floor
const floorName = "01 - Entry Level";
dataVizExtn.renderSurfaceShading(floorName, sensorType, getSensorValue);
}
function removeOffset(pos, offset) {
pos.position.x = pos.position.x - offset.x;
pos.position.y = pos.position.y - offset.y;
pos.position.z = pos.position.z - offset.z;
}
And I'm calling the loadHeatmaps() function inside onDocumentLoadSuccess callback.
EDIT: It looks like in this particular case it was a problem with floorName not being set to the right value. Note that this value (first parameter to dataVizExtn.renderSurfaceShading) should be set either to the name of the room, or to the name of the floor you want to update.
The offsets are a bit tricky so I'd suggest debugging that area, for example:
What coordinate system are the sensors defined in? If they are in the same coordinate system as the building model itself, you shouldn't subtract or add any offset to them. Whenever there's a model with a "global offset" in its metadata, it basically means that the Model Derivative service moved the model to origin to avoid floating point precision issues, and the viewer will then add the global offset back to each geometry when its loaded.
Try using the viewer APIs to get the bounding box of one of the rooms that the devices should map to, and see if the bounding box actually contains the XYZ point of the device you're trying to pass into the DataViz extension. The bounds of any object can be found like so:
function getObjectBounds(model, dbid) {
const tree = model.getInstanceTree();
const frags = model.getFragmentList();
let bounds = new THREE.Box3();
tree.enumNodeFragments(dbid, function (fragid) {
let _bounds = new THREE.Box3();
frags.getWorldBounds(fragid, _bounds);
bounds.union(_bounds);
}, true);
return bounds;
}
I have the same issue and my revit model was built by revit 2020. When I update model to 2022, heatmap can show on the room correctly.
I'm trying to train a neural network with brain.js - but am wondering if my data is already too big?
Code:
await getDataSet(type);
const listings = await getFilteredListings(type);
const normalizer = new Normalizer(listings);
normalizer.setOutputProperties(['runningTime']);
normalizer.normalize();
const inputs = normalizer.getBinaryInputDataset();
const outputs = normalizer.getBinaryOutputDataset();
const metadata = normalizer.getDatasetMetaData();
fs.writeFileSync(METADATA_FILE[type],JSON.stringify(metadata));
const trainingData=[];
for(let i=0;i<inputs.length;i++){
trainingData.push({input:inputs[i],output:outputs[i]});
}
const options={
hiddenLayers:[180,60],
};
const net = new NeuralNetworkGPU(
options
);
net.train(trainingData,{
iterations: 60000, // the maximum times to iterate the training data --> number greater than 0
errorThresh: 0.001, // the acceptable error percentage from training data --> number between 0 and 1
log:true,
});
Tried with amazon GPU instance - but it is suuuper slow. Would take 100h to train.
Features:
export const featureColumns = [
'location.lat', 'location.lon', 'locationFactor.score', 'constructionYear', 'squareMeter', 'grossReturn','floor',
'pricePerSqm', 'privateOffer', 'foreClosure', 'rented', 'apartmentType', 'condition'];
As you can see I've 13 Features - and roughly 50k training data.
Why is it taking so long? Decreasing units in hidden nets speeds up the training but then the net can not make sense of the data.
Im trying to create a convex hull with opencv.js based on an array with points, does anyone know a way to do this correctly and efficient? An array would look like this:
[
[5,5],
[10,10],
[15,15]
...
]
-> where the first value would be the x and the second the y value, but it wouldn't be a problem to change this format to something more suitable.
Thnx for the help :)
As far I could experiment OpenCV stores contour/hull data in Mat format with type CV_32SC2: essentially a flat list of 32bit short integers in [x1,y1,x2,y2,x3,y3,...] order.
Note the two channels/planes part of 32SC2: one channel for all the x values and another for all the y values
You can manually create such a Mat, access it's data32S property and fill in each value:
let testHull = cv.Mat.ones(4, 1, cv.CV_32SC2);
testHull.data32S[0] = 100;
testHull.data32S[1] = 100;
testHull.data32S[2] = 200;
testHull.data32S[3] = 100;
testHull.data32S[4] = 200;
testHull.data32S[5] = 200;
testHull.data32S[6] = 100;
testHull.data32S[7] = 200;
However OpenCV.js comes with a handy method to convert a flat array of values to such a Mat:
let testHull = cv.matFromArray(4, 1, cv.CV_32SC2, [100,100,200,100,200,200,100,200])
If your array is nested, you can simply use JS Array's flat() method to flatten it from a 2D array([[x1,y1]...]) to a 1D array ([x1,y1,...]).
So you don't have to worry about the Mat type and all that you can wrap it all into a nice function, for example:
function nestedPointsArrayToMat(points){
return cv.matFromArray(points.length, 1, cv.CV_32SC2, points.flat());
}
Here's a quick demo:
function onOpenCvReady(){
cv.then(test);
}
function nestedPointsArrayToMat(points){
return cv.matFromArray(points.length, 1, cv.CV_32SC2, points.flat());
}
function test(cv){
console.log("cv loaded");
// make a Mat to draw into
let mainMat = cv.Mat.zeros(30, 30, cv.CV_8UC3);
// make a fake hull
let points = [
[ 5, 5],
[25, 5],
[25,25],
[ 5,25]
]
let hull = nestedPointsArrayToMat(points);
console.log("hull data", hull.data32S);
// make a fake hulls vector
let hulls = new cv.MatVector();
// add the recently created hull
hulls.push_back(hull);
// test drawing it
cv.drawContours(mainMat, hulls, 0, [192,64,0,0], -1, 8);
// output to canvas
cv.imshow('canvasOutput', mainMat);
}
<script async src="https://docs.opencv.org/4.4.0/opencv.js" onload="onOpenCvReady();" type="text/javascript"></script>
<canvas id="canvasOutput" width="30" height="30"></canvas>
Note that the above is a rough example, there's no data validation or any other fancier checks, but hopefully it illustrates the idea so it can be extended robustly as required.
Lets say that your points represent a contour:
var contours = new cv.MatVector();
for (var i = 0; i < points.size(); ++i) {
contours.push_back(new cv.Mat(points[i][0], points[i][1])
}
Now following this tutorial from opencv website:
// approximates each contour to convex hull
for (var i = 0; i < contours.size(); ++i) {
var tmp = new cv.Mat();
var cnt = contours.get(i);
// You can try more different parameters
cv.convexHull(cnt, tmp, false, true);
hull.push_back(tmp);
cnt.delete(); tmp.delete();
}