edge-impulse-linux 1.17.1 → 1.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/build/cli/linux/runner-downloader.js +7 -2
  2. package/build/cli/linux/runner-downloader.js.map +1 -1
  3. package/build/cli/linux/runner-utils.d.ts +9 -2
  4. package/build/cli/linux/runner-utils.js +143 -52
  5. package/build/cli/linux/runner-utils.js.map +1 -1
  6. package/build/cli/linux/runner.js +77 -7
  7. package/build/cli/linux/runner.js.map +1 -1
  8. package/build/cli/linux/webserver/public/webserver.js +249 -25
  9. package/build/cli/linux/webserver/public/webserver.js.map +1 -1
  10. package/build/cli/linux/webserver/views/index.d.ts +2 -0
  11. package/build/cli/linux/webserver/views/index.js +33 -7
  12. package/build/cli/linux/webserver/views/index.js.map +1 -1
  13. package/build/library/classifier/image-classifier.d.ts +4 -1
  14. package/build/library/classifier/image-classifier.js +4 -4
  15. package/build/library/classifier/image-classifier.js.map +1 -1
  16. package/build/library/classifier/linux-impulse-runner-types.d.ts +1 -0
  17. package/build/sdk/studio/sdk/api/classifyApi.d.ts +0 -32
  18. package/build/sdk/studio/sdk/api/classifyApi.js +0 -64
  19. package/build/sdk/studio/sdk/api/classifyApi.js.map +1 -1
  20. package/build/sdk/studio/sdk/api/rawDataApi.d.ts +55 -1
  21. package/build/sdk/studio/sdk/api/rawDataApi.js +166 -1
  22. package/build/sdk/studio/sdk/api/rawDataApi.js.map +1 -1
  23. package/build/sdk/studio/sdk/model/batchEditBoundingBoxesRequest.d.ts +32 -0
  24. package/build/sdk/studio/sdk/model/batchEditBoundingBoxesRequest.js +34 -0
  25. package/build/sdk/studio/sdk/model/batchEditBoundingBoxesRequest.js.map +1 -0
  26. package/build/sdk/studio/sdk/model/classifyJobResponsePage.d.ts +4 -0
  27. package/build/sdk/studio/sdk/model/classifyJobResponsePage.js +5 -0
  28. package/build/sdk/studio/sdk/model/classifyJobResponsePage.js.map +1 -1
  29. package/build/sdk/studio/sdk/model/classifyJobResponsePageAllOf.d.ts +4 -0
  30. package/build/sdk/studio/sdk/model/classifyJobResponsePageAllOf.js +5 -0
  31. package/build/sdk/studio/sdk/model/classifyJobResponsePageAllOf.js.map +1 -1
  32. package/build/sdk/studio/sdk/model/modelPrediction.d.ts +5 -0
  33. package/build/sdk/studio/sdk/model/modelPrediction.js +5 -0
  34. package/build/sdk/studio/sdk/model/modelPrediction.js.map +1 -1
  35. package/build/sdk/studio/sdk/model/models.d.ts +1 -0
  36. package/build/sdk/studio/sdk/model/models.js +3 -0
  37. package/build/sdk/studio/sdk/model/models.js.map +1 -1
  38. package/build/sdk/studio/sdk/model/permission.d.ts +1 -1
  39. package/build/sdk/studio/sdk/model/permission.js +1 -1
  40. package/build/sdk/studio/sdk/model/permission.js.map +1 -1
  41. package/cli/linux/webserver/public/assets/mobileclient.css +36 -3
  42. package/cli/linux/webserver/public/webserver.js +280 -25
  43. package/package.json +1 -1
@@ -1,7 +1,11 @@
1
- window.WebServer = async () => {
1
+ window.WebServer = async (vmStr) => {
2
+
3
+ const vm = JSON.parse(decodeURIComponent(vmStr));
4
+ console.log('vm', vm);
2
5
 
3
6
  const els = {
4
7
  title: document.querySelector('#header-row h1'),
8
+ cameraOuterContainer: document.querySelector('#capture-camera .capture-camera-outer'),
5
9
  cameraContainer: document.querySelector('#capture-camera .capture-camera-inner'),
6
10
  cameraImg: document.querySelector('#capture-camera img'),
7
11
  timePerInference: document.querySelector('#time-per-inference'),
@@ -29,6 +33,9 @@ window.WebServer = async () => {
29
33
  ];
30
34
  let colorIx = 0;
31
35
  const labelToColor = { };
36
+ let isFirstClassification = true;
37
+ let inferenceIx = 0;
38
+ let lastClassification;
32
39
 
33
40
  function switchView(el) {
34
41
  for (let k of Object.keys(els.views)) {
@@ -150,6 +157,7 @@ window.WebServer = async () => {
150
157
  inputEl.oninput = () => {
151
158
  if (typeof threshold[k] === 'number') {
152
159
  if (!inputEl.value || isNaN(Number(inputEl.value))) return;
160
+ if (k === 'min_score' && Number(inputEl.value) === 0) return;
153
161
 
154
162
  threshold[k] = Number(inputEl.value);
155
163
  }
@@ -194,20 +202,75 @@ window.WebServer = async () => {
194
202
  bindThresholdSettings(opts.thresholds);
195
203
  });
196
204
 
205
+ const onWindowResize = () => {
206
+ if (els.cameraContainer.naturalWidth === 0) {
207
+ return;
208
+ }
209
+
210
+ let oldStyleWidth = els.cameraImg.style.width;
211
+ const containerWidth = els.cameraOuterContainer.getBoundingClientRect().width;
212
+
213
+ // height >480
214
+ if (els.cameraImg.naturalHeight > 480) {
215
+ // and fits within the container? just display as is
216
+ if (els.cameraImg.naturalWidth < containerWidth) {
217
+ els.cameraImg.style.width = els.cameraImg.naturalWidth + 'px';
218
+ }
219
+ else {
220
+ // does not fit within container? just use 100%
221
+ els.cameraImg.style.width = containerWidth + 'px';
222
+ }
223
+ }
224
+ else {
225
+ // what if we resize to 480 high?
226
+ const factor = els.cameraImg.naturalWidth / els.cameraImg.naturalHeight;
227
+ let newHeight = 480;
228
+ let newWidth = newHeight * factor;
229
+
230
+ // fits within the container? just display as is
231
+ if (newWidth < containerWidth) {
232
+ els.cameraImg.style.width = newWidth + 'px';
233
+ }
234
+ else {
235
+ // does not fit within container? just use 100%
236
+ els.cameraImg.style.width = containerWidth + 'px';
237
+ }
238
+ }
239
+
240
+ if (oldStyleWidth !== els.cameraImg.style.width && lastClassification) {
241
+ onClassification({
242
+ dontUpdateTable: true,
243
+ ...lastClassification,
244
+ });
245
+ }
246
+ };
247
+
248
+ let isFirstImage = true;
197
249
  socket.on('image', (opts) => {
250
+ if (isFirstImage) {
251
+ els.cameraImg.onload = () => {
252
+ onWindowResize();
253
+ els.cameraImg.onload = null;
254
+ };
255
+ isFirstImage = false;
256
+ }
198
257
  els.cameraImg.src = opts.img;
199
258
  });
259
+ window.addEventListener('resize', onWindowResize);
200
260
 
201
- let isFirstClassification = true;
202
- let inferenceIx = 0;
261
+ let lastFivePerfCalPredictions = [];
262
+
263
+ function onClassification(opts) {
264
+ lastClassification = opts;
203
265
 
204
- socket.on('classification', (opts) => {
205
266
  let result = opts.result;
206
267
  let modelType = opts.modelType;
207
268
 
208
269
  els.timePerInference.textContent = opts.timeMs;
209
270
  els.additionalInfo.textContent = opts.additionalInfo;
210
- els.timePerInferenceContainer.style.display = '';
271
+ if (!vm.isEmbedView) {
272
+ els.timePerInferenceContainer.style.display = '';
273
+ }
211
274
  els.additionalInfoContainer.style.display = '';
212
275
 
213
276
  console.log('classification', opts.result, opts.timeMs);
@@ -218,16 +281,39 @@ window.WebServer = async () => {
218
281
 
219
282
  els.imageClassify.row.style.display = 'none';
220
283
 
221
- if (result.classification) {
284
+ if (result.classification && !opts.dontUpdateTable) {
285
+ const showOnlyTopResults = Object.keys(result.classification).length > 10 && !result.visual_anomaly_grid;
286
+
222
287
  if (isFirstClassification) {
223
- for (let ix = 0; ix < Object.keys(result.classification).length; ix++) {
224
- const key = Object.keys(result.classification)[ix];
288
+ if (showOnlyTopResults) {
289
+ // only 1 results th
290
+ {
291
+ let th = document.createElement('th');
292
+ th.scope = 'col';
293
+ th.textContent = th.title = 'Top 5 results';
294
+ th.classList.add('px-0', 'text-center');
295
+ els.resultsThead.appendChild(th);
296
+ }
225
297
 
226
- let th = document.createElement('th');
227
- th.scope = 'col';
228
- th.classList.add('px-0', 'text-center');
229
- th.textContent = th.title = key;
230
- els.resultsThead.appendChild(th);
298
+ // unless also have anomaly...
299
+ if (Object.keys(result.classification).indexOf('anomaly') > -1) {
300
+ let th = document.createElement('th');
301
+ th.scope = 'col';
302
+ th.textContent = th.title = 'anomaly';
303
+ th.classList.add('px-0', 'text-center');
304
+ els.resultsThead.appendChild(th);
305
+ }
306
+ }
307
+ else {
308
+ for (let ix = 0; ix < Object.keys(result.classification).length; ix++) {
309
+ const key = Object.keys(result.classification)[ix];
310
+
311
+ let th = document.createElement('th');
312
+ th.scope = 'col';
313
+ th.classList.add('px-0', 'text-center');
314
+ th.textContent = th.title = key;
315
+ els.resultsThead.appendChild(th);
316
+ }
231
317
  }
232
318
 
233
319
  if (result.visual_anomaly_grid) {
@@ -244,12 +330,31 @@ window.WebServer = async () => {
244
330
 
245
331
  els.imageClassify.row.style.display = '';
246
332
 
247
- let conclusion = 'uncertain';
333
+ let conclusion = vm.hasPerformanceCalibration ? '...' : 'uncertain';
248
334
  let highest = Math.max(...Object.values(result.classification));
249
335
 
250
336
  for (let k of Object.keys(result.classification)) {
251
337
  if (result.classification[k] >= 0.55) {
252
- conclusion = k + ' (' + result.classification[k].toFixed(2) + ')';
338
+ if (vm.hasPerformanceCalibration) {
339
+ conclusion = k;
340
+ }
341
+ else {
342
+ conclusion = k + ' (' + result.classification[k].toFixed(2) + ')';
343
+ }
344
+ }
345
+ }
346
+
347
+ // for perfcal models, if one of the last 5 predictions was the keyword => select that
348
+ if (vm.hasPerformanceCalibration) {
349
+ lastFivePerfCalPredictions.push(conclusion);
350
+ if (lastFivePerfCalPredictions.length > 5) {
351
+ lastFivePerfCalPredictions = lastFivePerfCalPredictions.slice(
352
+ lastFivePerfCalPredictions.length - 5);
353
+ }
354
+
355
+ const dedup = Array.from(new Set(lastFivePerfCalPredictions));
356
+ if (dedup.length === 2 && dedup.find(x => x === '...')) {
357
+ conclusion = dedup.find(x => x !== '...');
253
358
  }
254
359
  }
255
360
 
@@ -264,15 +369,53 @@ window.WebServer = async () => {
264
369
  let td1 = document.createElement('td');
265
370
  td1.textContent = (++inferenceIx).toString();
266
371
  tr.appendChild(td1);
267
- for (let k of Object.keys(result.classification)) {
372
+ if (showOnlyTopResults) {
373
+ // only print top 5
268
374
  let td = document.createElement('td');
269
- td.classList.add('text-center');
270
- td.textContent = result.classification[k].toFixed(2);
271
- if (result.classification[k] === highest && !isVisualAnomaly) {
272
- td.style.fontWeight = 600;
375
+
376
+ let results = [];
377
+ for (const key of Object.keys(result.classification)) {
378
+ if (key === 'anomaly') continue;
379
+
380
+ results.push({
381
+ label: key,
382
+ value: result.classification[key],
383
+ });
384
+ }
385
+
386
+ const top = results.sort((a, b) => b.value - a.value).slice(0, 5);
387
+ for (let ix = 0; ix < top.length; ix++) {
388
+ let span = ix === 0 ? document.createElement('strong') : document.createElement('span');
389
+ span.textContent = `${top[ix].label}: ${top[ix].value.toFixed(2)}`;
390
+ td.appendChild(span);
391
+
392
+ if (ix !== top.length - 1) {
393
+ let commaSpan = document.createElement('span');
394
+ commaSpan.textContent = ', ';
395
+ td.appendChild(commaSpan);
396
+ }
273
397
  }
274
398
  tr.appendChild(td);
399
+
400
+ if (Object.keys(result.classification).indexOf('anomaly') > -1) {
401
+ let anomalyTd = document.createElement('td');
402
+ anomalyTd.classList.add('text-center');
403
+ anomalyTd.textContent = result.classification.anomaly.toFixed(2);
404
+ tr.appendChild(anomalyTd);
405
+ }
406
+ }
407
+ else {
408
+ for (let k of Object.keys(result.classification)) {
409
+ let td = document.createElement('td');
410
+ td.classList.add('text-center');
411
+ td.textContent = result.classification[k].toFixed(2);
412
+ if (result.classification[k] === highest && !isVisualAnomaly) {
413
+ td.style.fontWeight = 600;
414
+ }
415
+ tr.appendChild(td);
416
+ }
275
417
  }
418
+
276
419
  if (result.visual_anomaly_grid) {
277
420
  let td = document.createElement('td');
278
421
  td.classList.add('text-center');
@@ -300,18 +443,61 @@ window.WebServer = async () => {
300
443
  }
301
444
 
302
445
  els.imageClassify.text.textContent = conclusion;
446
+
447
+ // for image classification models, draw overlay on top of the image with top 3 conclusions in embed mode
448
+ if (vm.sensorType === 'camera' && vm.isEmbedView) {
449
+ let results = [];
450
+ for (const key of Object.keys(result.classification)) {
451
+ if (key === 'anomaly') continue;
452
+
453
+ results.push({
454
+ label: key,
455
+ value: result.classification[key],
456
+ });
457
+ }
458
+
459
+ // don't change order if we only have 3
460
+ const top = results.length > 3 ?
461
+ results.sort((a, b) => b.value - a.value).slice(0, 3) :
462
+ results;
463
+ if (result.visual_anomaly_grid) {
464
+ // also visual AD? add to top results
465
+ top.push({
466
+ label: 'anomaly',
467
+ value: result.visual_anomaly_max,
468
+ });
469
+ }
470
+
471
+ let classificationOverlayEl = els.cameraContainer.querySelector('.classification-top-overlay');
472
+ if (!classificationOverlayEl) {
473
+ classificationOverlayEl = document.createElement('div');
474
+ classificationOverlayEl.classList.add('classification-top-overlay');
475
+ els.cameraContainer.appendChild(classificationOverlayEl);
476
+ }
477
+ classificationOverlayEl.textContent = '';
478
+
479
+ for (const topRes of top) {
480
+ const topDiv = document.createElement('div');
481
+ topDiv.textContent = `${topRes.label} (${topRes.value.toFixed(2)})`;
482
+ classificationOverlayEl.appendChild(topDiv);
483
+ }
484
+
485
+ els.imageClassify.row.style.display = 'none';
486
+ }
303
487
  }
304
488
  if (result.bounding_boxes) {
305
489
  let factor = els.cameraImg.naturalHeight / els.cameraImg.clientHeight;
306
490
 
307
- for (let b of result.bounding_boxes) {
491
+ for (let b of result.object_tracking || result.bounding_boxes) {
308
492
  let bb = {
309
493
  x: b.x / factor,
310
494
  y: b.y / factor,
311
495
  width: b.width / factor,
312
496
  height: b.height / factor,
313
- label: b.label,
314
- value: b.value
497
+ label: 'object_id' in b ?
498
+ `${b.label} (ID ${b.object_id})` :
499
+ b.label,
500
+ value: 'value' in b ? b.value : undefined,
315
501
  };
316
502
 
317
503
  if (!labelToColor[bb.label]) {
@@ -345,7 +531,10 @@ window.WebServer = async () => {
345
531
  let label = document.createElement('div');
346
532
  label.classList.add('bounding-box-label');
347
533
  label.style.background = color;
348
- label.textContent = bb.label + ' (' + bb.value.toFixed(2) + ')';
534
+ label.textContent = bb.label;
535
+ if (typeof bb.value === 'number') {
536
+ label.textContent += ' (' + bb.value.toFixed(2) + ')';
537
+ }
349
538
  if (modelType === 'constrained_object_detection') {
350
539
  el.style.whiteSpace = 'nowrap';
351
540
  }
@@ -406,9 +595,75 @@ window.WebServer = async () => {
406
595
  els.cameraContainer.appendChild(el);
407
596
  }
408
597
  }
409
- });
598
+ }
599
+
600
+ socket.on('classification', onClassification);
410
601
 
411
602
  if (els.websocketAddress) {
412
603
  els.websocketAddress.textContent = `ws://${location.host}`;
413
604
  }
605
+
606
+ // Here's a helper function that'll loop every second, checks if "classification-top-overlay" is present
607
+ // and then switches between white/black text automatically. If this element is not created, it just sits idle
608
+ (async () => {
609
+ function getAvgBrightness(img, x, y, w, h) {
610
+ const canvas = document.createElement('canvas');
611
+ const ctx = canvas.getContext('2d');
612
+ canvas.width = img.naturalWidth;
613
+ canvas.height = img.naturalHeight;
614
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
615
+
616
+ // Scale from CSS coords (relative to image on page) to natural pixel coords
617
+ const scaleX = img.naturalWidth / img.clientWidth;
618
+ const scaleY = img.naturalHeight / img.clientHeight;
619
+
620
+ const sx = (x - img.getBoundingClientRect().x) * scaleX;
621
+ const sy = (y - img.getBoundingClientRect().y) * scaleY;
622
+ const sw = w * scaleX;
623
+ const sh = h * scaleY;
624
+
625
+ const data = ctx.getImageData(sx, sy, sw, sh).data;
626
+
627
+ let total = 0;
628
+ for (let i = 0; i < data.length; i += 4) {
629
+ const r = data[i];
630
+ const g = data[i + 1];
631
+ const b = data[i + 2];
632
+ total += (r * 299 + g * 587 + b * 114) / 1000;
633
+ }
634
+
635
+ // debug info
636
+ // const cropCanvas = document.createElement('canvas');
637
+ // cropCanvas.width = sw;
638
+ // cropCanvas.height = sh;
639
+ // const cctx = cropCanvas.getContext('2d');
640
+ // cctx.drawImage(img, sx, sy, sw, sh, 0, 0, sw, sh);
641
+ // const cropEl = document.querySelector('#crop') || document.createElement('img');
642
+ // cropEl.id = 'crop';
643
+ // cropEl.src = cropCanvas.toDataURL('image/png');
644
+ // document.body.appendChild(cropEl);
645
+
646
+ return total / (data.length / 4);
647
+ }
648
+
649
+ if (!vm.isEmbedView || vm.sensorType !== 'camera') return;
650
+
651
+ while (1) {
652
+ const labelEl = document.querySelector('.classification-top-overlay');
653
+ if (!labelEl) {
654
+ await new Promise(resolve => setTimeout(resolve, 1000));
655
+ continue;
656
+ }
657
+
658
+ const rect = labelEl.getBoundingClientRect();
659
+ const brightness = getAvgBrightness(els.cameraImg, rect.x, rect.y, rect.width, rect.height);
660
+ if (brightness > 180) {
661
+ labelEl.style.color = 'black';
662
+ }
663
+ else {
664
+ labelEl.style.color = 'white';
665
+ }
666
+ await new Promise(resolve => setTimeout(resolve, 1000));
667
+ }
668
+ })();
414
669
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edge-impulse-linux",
3
- "version": "1.17.1",
3
+ "version": "1.17.3",
4
4
  "description": "Node.js SDK and tools for Edge Impulse for Linux",
5
5
  "directories": {
6
6
  "example": "examples"