From d3ff8b5eeda9530dcf7d361c76d9b21fe3294cbb Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Thu, 5 Apr 2018 14:30:50 -0400 Subject: [PATCH] Test VideoMotion and VideoSensing blocks - Add pngjs dev dependency to read test images --- package.json | 1 + .../scratch3_video_sensing/index.js | 43 +- test/unit/extension_video_sensing.js | 409 ++++++++++++++++++ test/unit/extension_video_sensing_center.png | Bin 0 -> 10289 bytes test/unit/extension_video_sensing_down-10.png | Bin 0 -> 10273 bytes test/unit/extension_video_sensing_left-10.png | Bin 0 -> 10008 bytes test/unit/extension_video_sensing_left-5.png | Bin 0 -> 10022 bytes 7 files changed, 441 insertions(+), 12 deletions(-) create mode 100644 test/unit/extension_video_sensing.js create mode 100644 test/unit/extension_video_sensing_center.png create mode 100644 test/unit/extension_video_sensing_down-10.png create mode 100644 test/unit/extension_video_sensing_left-10.png create mode 100644 test/unit/extension_video_sensing_left-5.png diff --git a/package.json b/package.json index cc35511c6..b362696e3 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "lodash.defaultsdeep": "4.6.0", "minilog": "3.1.0", "nets": "3.2.0", + "pngjs": "^3.3.2", "promise": "8.0.1", "scratch-audio": "latest", "scratch-blocks": "latest", diff --git a/src/extensions/scratch3_video_sensing/index.js b/src/extensions/scratch3_video_sensing/index.js index a59daf489..941ba9904 100644 --- a/src/extensions/scratch3_video_sensing/index.js +++ b/src/extensions/scratch3_video_sensing/index.js @@ -81,15 +81,18 @@ class Scratch3VideoSensingBlocks { */ this._sampleContext = null; - // Clear target motion state values when the project starts. - this.runtime.on(Runtime.PROJECT_RUN_START, this.reset.bind(this)); + if (this.runtime.ioDevices) { + // Clear target motion state values when the project starts. + this.runtime.on(Runtime.PROJECT_RUN_START, this.reset.bind(this)); - // Boot up the video, canvas to down/up sample the video stream, the - // preview skin and drawable, and kick off looping the analysis logic. - this._setupVideo(); - this._setupSampleCanvas(); - this._setupPreview(); - this._loop(); + // Boot up the video, canvas to down/up sample the video stream, the + // preview skin and drawable, and kick off looping the analysis + // logic. + this._setupVideo(); + this._setupSampleCanvas(); + this._setupPreview(); + this._loop(); + } } /** @@ -322,6 +325,14 @@ class Scratch3VideoSensingBlocks { ]; } + static get MOTION () { + return 1; + } + + static get DIRECTION () { + return 2; + } + /** * An array of info about each drum. * @type {object[]} an array of objects. @@ -341,6 +352,14 @@ class Scratch3VideoSensingBlocks { ]; } + static get STAGE () { + return 1; + } + + static get SPRITE () { + return 2; + } + /** * @returns {object} metadata for this extension and its blocks. */ @@ -359,12 +378,12 @@ class Scratch3VideoSensingBlocks { MOTION_DIRECTION: { type: ArgumentType.NUMBER, menu: 'MOTION_DIRECTION', - defaultValue: 1 + defaultValue: Scratch3VideoSensingBlocks.MOTION }, STAGE_SPRITE: { type: ArgumentType.NUMBER, menu: 'STAGE_SPRITE', - defaultValue: 1 + defaultValue: Scratch3VideoSensingBlocks.STAGE } } }, @@ -413,11 +432,11 @@ class Scratch3VideoSensingBlocks { this.detect.analyzeFrame(); let state = this.detect; - if (Number(args.STAGE_SPRITE) === 2) { + if (Number(args.STAGE_SPRITE) === Scratch3VideoSensingBlocks.SPRITE) { state = this._analyzeLocalMotion(util.target); } - if (Number(args.MOTION_DIRECTION) === 1) { + if (Number(args.MOTION_DIRECTION) === Scratch3VideoSensingBlocks.MOTION) { return state.motionAmount; } return state.motionDirection; diff --git a/test/unit/extension_video_sensing.js b/test/unit/extension_video_sensing.js new file mode 100644 index 000000000..35f0381fa --- /dev/null +++ b/test/unit/extension_video_sensing.js @@ -0,0 +1,409 @@ +const {createReadStream} = require('fs'); +const {join} = require('path'); + +const {PNG} = require('pngjs'); +const {test} = require('tap'); + +const {wrapClamp} = require('../../src/util/math-util'); + +const VideoSensing = require('../../src/extensions/scratch3_video_sensing/index.js'); +const VideoMotion = require('../../src/extensions/scratch3_video_sensing/library.js'); + +/** + * Prefix to the mock frame images used to test the video sensing extension. + * @type {string} + */ +const pngPrefix = 'extension_video_sensing_'; + +/** + * Map of frame keys to the image filenames appended to the pngPrefix. + * @type {object} + */ +const framesMap = { + center: 'center', + left: 'left-5', + left2: 'left-10', + down: 'down-10' +}; + +/** + * Asynchronously read a png file and copy its pixel data into a typed array + * VideoMotion will accept. + * @param {string} name - partial filename to read + * @returns {Promise.} pixel data of the image + */ +const readPNG = name => ( + new Promise((resolve, reject) => { + const png = new PNG(); + createReadStream(join(__dirname, `${pngPrefix}${name}.png`)) + .pipe(png) + .on('parsed', () => { + // Copy the RGBA pixel values into a separate typed array and + // cast the array to Uint32, the array format VideoMotion takes. + resolve(new Uint32Array(new Uint8ClampedArray(png.data).buffer)); + }) + .on('error', reject); + }) +); + +/** + * Read all the frames for testing asynchrnously and produce an object with + * keys following the keys in framesMap. + * @returns {object} mapping of keys in framesMap to image data read from disk + */ +const readFrames = (() => { + // Use this immediately invoking function expression (IIFE) to delay reading + // once to the first test that calls readFrames. + let _promise = null; + + return () => { + if (_promise === null) { + _promise = Promise.all(Object.keys(framesMap).map(key => readPNG(framesMap[key]))) + .then(pngs => ( + Object.keys(framesMap).reduce((frames, key, i) => { + frames[key] = pngs[i]; + return frames; + }, {}) + )); + } + return _promise; + }; +})(); + +/** + * Match if actual is within optMargin to expect. If actual is under -180, + * match if actual + 360 is near expect. If actual is over 180, match if actual + * - 360 is near expect. + * @param {number} actual - actual angle in degrees + * @param {number} expect - expected angle in degrees + * @param {number} optMargin - allowed margin between actual and expect in degrees + * @returns {boolean} true if actual is close to expect + */ +const isNearAngle = (actual, expect, optMargin = 10) => ( + (wrapClamp(actual - expect, 0, 359) < optMargin) || + (wrapClamp(actual - expect, 0, 359) > 360 - optMargin) +); + +// A fake scratch-render drawable that will be used by VideoMotion to restrain +// the area considered for motion detection in VideoMotion.getLocalMotion +const fakeDrawable = { + getFastBounds () { + return { + left: -120, + top: 60, + right: 0, + bottom: -60 + }; + }, + + isTouching () { + return true; + } +}; + +// A fake MotionState used to test the stored values in +// VideoMotion.getLocalMotion, VideoSensing.videoOn and +// VideoSensing.whenMotionGreaterThan. +const fakeMotionState = { + motionFrameNumber: -1, + motionAmount: -1, + motionDirection: -Infinity +}; + +// A fake target referring to the fake drawable and MotionState. +const fakeTarget = { + drawableID: 0, + + getCustomState () { + return fakeMotionState; + }, + setCustomState () {} +}; + +const fakeRuntime = { + targets: [fakeTarget], + + // Without defined devices, VideoSensing will not try to start sampling from + // a video source. + ioDevices: null, + + renderer: { + _allDrawables: [ + fakeDrawable + ] + } +}; + +const fakeBlockUtility = { + target: fakeTarget +}; + +test('detect motionAmount between frames', t => { + t.plan(6); + + return readFrames() + .then(frames => { + const detect = new VideoMotion(); + + // Each of these pairs should have enough motion for the detector. + const framePairs = [ + [frames.center, frames.left], + [frames.center, frames.left2], + [frames.left, frames.left2], + [frames.left, frames.center], + [frames.center, frames.down], + [frames.down, frames.center] + ]; + + // Add both frames of a pair and test for motion. + let index = 0; + for (const [frame1, frame2] of framePairs) { + detect.addFrame(frame1); + detect.addFrame(frame2); + + detect.analyzeFrame(); + t.ok( + detect.motionAmount > 10, + `frame pair ${index + 1} has motion ${detect.motionAmount} over threshold (10)` + ); + index += 1; + } + + t.end(); + }); +}); + +test('detect local motionAmount between frames', t => { + t.plan(6); + + return readFrames() + .then(frames => { + const detect = new VideoMotion(); + + // Each of these pairs should have enough motion for the detector. + const framePairs = [ + [frames.center, frames.left], + [frames.center, frames.left2], + [frames.left, frames.left2], + [frames.left, frames.center], + [frames.center, frames.down], + [frames.down, frames.center] + ]; + + // Add both frames of a pair and test for local motion. + let index = 0; + for (const [frame1, frame2] of framePairs) { + detect.addFrame(frame1); + detect.addFrame(frame2); + + detect.analyzeFrame(); + detect.getLocalMotion(fakeDrawable, fakeMotionState); + t.ok( + fakeMotionState.motionAmount > 10, + `frame pair ${index + 1} has motion ${fakeMotionState.motionAmount} over threshold (10)` + ); + index += 1; + } + + t.end(); + }); +}); + +test('detect motionDirection between frames', t => { + t.plan(6); + + return readFrames() + .then(frames => { + const detect = new VideoMotion(); + + // Each of these pairs is moving in the given direction. Does the detector + // guess a value to that? + const directionMargin = 10; + const framePairs = [ + { + frames: [frames.center, frames.left], + direction: -90 + }, + { + frames: [frames.center, frames.left2], + direction: -90 + }, + { + frames: [frames.left, frames.left2], + direction: -90 + }, + { + frames: [frames.left, frames.center], + direction: 90 + }, + { + frames: [frames.center, frames.down], + direction: 180 + }, + { + frames: [frames.down, frames.center], + direction: 0 + } + ]; + + // Add both frames of a pair and check if the motionDirection is near the + // expected angle. + let index = 0; + for (const {frames: [frame1, frame2], direction} of framePairs) { + detect.addFrame(frame1); + detect.addFrame(frame2); + + detect.analyzeFrame(); + t.ok( + isNearAngle(detect.motionDirection, direction, directionMargin), + `frame pair ${index + 1} is ${detect.motionDirection.toFixed(0)} ` + + `degrees and close to ${direction} degrees` + ); + index += 1; + } + + t.end(); + }); +}); + +test('detect local motionDirection between frames', t => { + t.plan(6); + + return readFrames() + .then(frames => { + const detect = new VideoMotion(); + + // Each of these pairs is moving in the given direction. Does the detector + // guess a value to that? + const directionMargin = 10; + const framePairs = [ + { + frames: [frames.center, frames.left], + direction: -90 + }, + { + frames: [frames.center, frames.left2], + direction: -90 + }, + { + frames: [frames.left, frames.left2], + direction: -90 + }, + { + frames: [frames.left, frames.center], + direction: 90 + }, + { + frames: [frames.center, frames.down], + direction: 180 + }, + { + frames: [frames.down, frames.center], + direction: 0 + } + ]; + + // Add both frames of a pair and check if the local motionDirection is near + // the expected angle. + let index = 0; + for (const {frames: [frame1, frame2], direction} of framePairs) { + detect.addFrame(frame1); + detect.addFrame(frame2); + + detect.analyzeFrame(); + detect.getLocalMotion(fakeDrawable, fakeMotionState); + const motionDirection = fakeMotionState.motionDirection; + t.ok( + isNearAngle(motionDirection, direction, directionMargin), + `frame pair ${index + 1} is ${motionDirection.toFixed(0)} degrees and close to ${direction} degrees` + ); + index += 1; + } + + t.end(); + }); +}); + +test('videoOn returns value dependent on arguments', t => { + t.plan(4); + + return readFrames() + .then(frames => { + const sensing = new VideoSensing(fakeRuntime); + + // With these two frame test if we get expected values depending on the + // arguments to videoOn. + sensing.detect.addFrame(frames.center); + sensing.detect.addFrame(frames.left); + + const motionAmount = sensing.videoOn({ + MOTION_DIRECTION: VideoSensing.MOTION, + STAGE_SPRITE: VideoSensing.STAGE + }, fakeBlockUtility); + t.ok( + motionAmount > 10, + `stage motionAmount ${motionAmount} is over the threshold (10)` + ); + + const localMotionAmount = sensing.videoOn({ + MOTION_DIRECTION: VideoSensing.MOTION, + STAGE_SPRITE: VideoSensing.SPRITE + }, fakeBlockUtility); + t.ok( + localMotionAmount > 10, + `sprite motionAmount ${localMotionAmount} is over the threshold (10)` + ); + + const motionDirection = sensing.videoOn({ + MOTION_DIRECTION: VideoSensing.DIRECTION, + STAGE_SPRITE: VideoSensing.STAGE + }, fakeBlockUtility); + t.ok( + isNearAngle(motionDirection, -90), + `stage motionDirection ${motionDirection.toFixed(0)} degrees is close to ${90} degrees` + ); + + const localMotionDirection = sensing.videoOn({ + MOTION_DIRECTION: VideoSensing.DIRECTION, + STAGE_SPRITE: VideoSensing.SPRITE + }, fakeBlockUtility); + t.ok( + isNearAngle(localMotionDirection, -90), + `sprite motionDirection ${localMotionDirection.toFixed(0)} degrees is close to ${90} degrees` + ); + + t.end(); + }); +}); + +test('whenMotionGreaterThan returns true if local motion meets target', t => { + t.plan(2); + + return readFrames() + .then(frames => { + const sensing = new VideoSensing(fakeRuntime); + + // With these two frame test if we get expected values depending on the + // arguments to whenMotionGreaterThan. + sensing.detect.addFrame(frames.center); + sensing.detect.addFrame(frames.left); + + const over20 = sensing.whenMotionGreaterThan({ + REFERENCE: 20 + }, fakeBlockUtility); + t.ok( + over20, + `enough motion in drawable bounds to reach reference of 20` + ); + + const over80 = sensing.whenMotionGreaterThan({ + REFERENCE: 80 + }, fakeBlockUtility); + t.notOk( + over80, + `not enough motion in drawable bounds to reach reference of 80` + ); + + t.end(); + }); +}); diff --git a/test/unit/extension_video_sensing_center.png b/test/unit/extension_video_sensing_center.png new file mode 100644 index 0000000000000000000000000000000000000000..eed3cb73ef8ace3eab758be452d9ca927fc0a370 GIT binary patch literal 10289 zcmeHtbzBrm+cvSJf`oL7h~$z2OD$3o3KG&K5=$+xyHb)Oh?F3pl8bbMl%xovN0FsN zQbf8$O8TAkoacRf&iA~3eE%PREI)Q;?umQux#qg=F-rf2CKV+sB>@2emDV*i0|ElV zFW}=$P6SE_j9>eJ--IXw&8r0EIQAv*g~Ici8H#}53_bosNRXP&Oh7l%EIM&R4_(#rtO`|LRB00cG!v@I)h!9^CkT?QSD6XvGT`@B{tt zzrW_`?)kq1d7%DU6<8nC*Ul3vE-D87Z$@Z@Ymt9m=wp^@H3 zNTi#RKEl_*%^m-hTT)c!Ps`uye>26e0;cDUZ~*SbJ5fno0s8-x{k^>^(%sYB0foZ1 zm;S5$?`3~)Z{UDJx?%9$8+#zoN)msW{H^%!R%ZXMSCaXc&F_VOH!%Hg2EP~nW`N%Y z*e^essv+DRJbtZ3N>)q(`d@4QZmEiNLwXx}+Sxnc9rnxQx1ztd{xy!-zsHfh@*m^; zR`Zvo0u&$1zs2^S2j^EQI5|p`U{C*b3Y938RBH+e2rgdGQd2SVC0xsJjWHf#cuzv; zr$tL6l6Rh&Pb2pV?Ikt(i!No^*BQcS^>d8Jp;zIwgQ-$=MT9D}#6kV6R506%+}CSb zb1t$H{d{8(^M)FlEJ&g@6{dYE;qdIa{9e_@nA3D1ZoiwXWiP#U#CoNE_qckaVPSQ0 z=I7>Y!6q3YIh=rKlXj8W=9(Jh*B50PIF^NsCWHWTfiDEEY0lgpMgZXsflHEd6A+TC zX%mFT)rf_Y;M;~eo(0x450eOpI1qN^XNZ6mQ8zO%<)#rJguwdSX&52E3X&=gOyS%H zMC9n)2Ry>~G0COufoTXl7ecdm?To4^en1-Mcfgb&^cn?sZvtaPC4NBe-k4w0N}z*d zc}33JsQ@c5LE?WV_>TwwiNXKRL()qDp?R3z7JPa*jsIk&{~T|`tehMlo@1Bo-L`3Q zJ}{(Iv^dJ}n}6a$Z8hhKg;S?CoAj9odXxtnL|DmDVrx<8+W zSs5;~ZaJ_yJ$Nc**5B;2VGZ5fUZ1p(5NT*#d=dRFhDFk4uuu{w+Y#-Q4(H|p7;x3r zYX$zLH(S+pxYTcN(kHLW-|O2Sw2|+IQtz&fVYV0h43@i~uPvdCptmyqF%@nBU}bP; z(;}tKDJyWd{S36!wCbwiy#4#Q&-F4LUaS*3BM(C z0C#1;-uhhEQ>ay=(bn8(c_n7CR|U2F;R(N9x@;A4x|Q=~Ckf;%Xicn;+DIdN@8|cA z$wF+btk%KDJ7c(~LQ&?Z<&uh4|Bp!mv+v_KhRZDYu2+VJ_z;oT_~gpE>DcyUDn>Iw zt!te7%B`BlGNVrdUk4rMUr)P&SsqjhJW%5jCUlG?Aj+tHq>Jc2vM+j-;qp~(pwH?U6K2V?+yfb7{f=G|L zGGdA9o{d*lavQDGy`xG;OKU6L^zczaqyF}@4;|-}OKm>=D8&`0_A^1UAFef|4aQz3 z@=y(7L9=l;mnF5~GUSo1`;Lkj1H^sy(WTERd^#!H8`H{U4T-nOi9TUf^>Nl*pv8PA zlT8e)zNj>WJo-zG9L|E=NRuG6JkBD>VySgr`D8ObRWN9?lW9LsUjb!D%g*z$pXy8J zz?|+&$g8&r!qYK+({=7?DFwc#$9s|~CUi@d{!4l0G>-!Ep4g^wWs#$UT->|2@1Ij* zmhngzRkYxk%Wo0Yc>Uv}&{IoqEf0xHH+w)wGc3-xCYKtQN2}~_Iu#sHKE--zjZv_^ z69G4v2;zHVAkjhb$%@UXdDCfaaB5B`1$k;Og{>Do4vxJ{DC%`yy(8)p(HSPu=EL&7?dJhgx*X;FW&_>H1aCiN(u=6ujaKNaet#5v$t5rUlIj5(%W28Qu8B_E9u_(Ev&m<;s zKI>$=AhWp+v$a%^rJZjBy!F&3NbVD9Z8zrET&+g%E29{z~~I-(PXAJx6Fv^7OKZl=6s7dV9{s z%3L}w`9i{EiS0~6HZJ9hS~W6(?EvKef!;hCUgt3tG&M9Imj*vV@LF zTbi^5?|0c;8X?J)1pEgFr)|l@I+5(ys(bIrXQ6O%jj<|BQeTtiQ`q?i8W*$AY}y3v zJ9N!*+N@l8<1^(Kxo}qTLU*PR1QsA&Cc#@)ebdG!tG7CA%mYA0(K)4`U*dzda|CBg z)T=9ApO}k7t(#@DljG5Ifs>7x(vh3dWBECH5EutW8>UV1c->9Mnc<>e(AOybh;v+- zXhw?t=~rH}0I!SWw-UcsNGE?YI}PERVb4PFx8+PBDzI5UPqzz{`Lc11!Rr;l$1CH= z*BRCrMjMif^GOf;H}5w+V6tO6VkGmW2sMN}Q^~rWFm7Vrt*OHt@Y-EZG?~c3sQ}!X97cv9ZBe{Z1$J5>%JBG_Z@h6JJ-aNy3xHOQYrDO{L z;&!<8?x2~&Ed%ZA6bB*=+SseUr*yM2*P0*jhrlD9IB&*p&3A7#RbYcw{pX)Mp?%EN zv@X#+qBBYj3jS=!V-yDo>vVWT;Tgr-*(n*(iWtbJ<<(f+l*`GQpPy{<2?h{RyQ007 z;P$%LLtw-v_&8fN@?Kt1q@ax3s2KRMq27FQ7vRo-^_%6{ETzCkP0adueMJiU$9PzD zQ=;)l?dAj48jm9H!pmjxo1!DYYYR0;_}g`-vbN$IpMkl{KDe*8=fu>5lnxeo%S>AW z_B?FFS5&s9Pj>5Dn`ij(Sa0;S;^UWE@|WvtL#(^Tb;wCwlPgjQ?27*Al@2-oddF^7 zk*CWm=8<>#dvnCiS>-h$TD9bO1R;E~)L~xP{S>wmDPiSe@@+{c+;$$Wk%JlI!3n=QvvT6haLvJG_fzx)oh@lbMgNLudDiLCg0=T>b(sI1 zj_49$ITk<-Di+S3?)L;QEBzt-hKI=nxXvmnZ+vPbhvs0njK5)^`@-|Z2c08#>sucs z>%Q=%QUTCU{xM-YyQBw$!R+S6))uDS`r}i-;I*JLxzfos?Ztsi=EuvUqoaQ$JHH;% zdMe}CyHxyCU<^Rt=#Uxh5g1G(L&xZq{?5_4% zu6}uQe=Qv)m)nrds5wmeTr}3(=@8s}HeQ_!yV55C+l$#7ih&2)A2?L#lvG0E&8$ho zcHLi=RJWpCk>>{+Rml+IUCfkTTyN znN*xu{xShhVUiqcA!RsoZ<6tIyAX6hg&_2XW=71-<)BwqO?O!`ob^1`urBDtlKFN% z=XXiQvK~%S61qqf1lCfEdU$WCKaYA?U3USOkyftQ5dDpIz0#8+wM7{^R~;D|`5p2z zK}=z*S8;hkWvvjsIx4R7o>w!@vgxjSBsGWd4e!xgX}Nf4H*oMq96u-9-7giH*Kgkcfgyx?%-VaXJQ!jlf-rcVY=8M`9jJC3H zHE}c@)Q%SNvs7qwmSIukK@&Oa;;hx|38X400M=g`?Z6xWeuxkVv=6R!?7n?qDXp5_ z1devG$JC&{`q9x=PwWlnOc=OfmbK>@zn_6N+fE4iw}lYOOroDyRG8LZ!g4e{sM-;) z_YwU1(xGc&%r+wkv_5b(mAKC!bSp{0=Hwfoku}c|e)7VP=xN}Gj`w#B7|%VHOrZNB z8h7o?ET6D@`(Tp~vI`n+&V&GnbbPqCN2iyWu{zOU_{6!3QoHe~{l-+wn5}RR^Hz$y zNWx$--pqX>c5a;r$!$a~MTXV~zB*DT?Vs}HpByL!JU?-}B4omv|xD$CICZg3w zJz(9NNa!trUkI9Sllk%H36;CBqFp;Ae)HQzvMuZ0@)bs@g`Qhivqa>Indj@w!cI+;L#j%-rv48xtvt2 z?Gz(4(!N4st{LkqdfH4VI=$2Yw~e`a+E!IC2dDg9w`v({UmollbxhEzgET38>oJJO z$2IqNYrSSQbb?hQ&a%jQV!=|*P&g}_g^DOx@6+0w32l&fF#|;{Zp4$othE1)KHBEc zvZp^FVx092YgejW<#S!CP~07};AN@rfN{RED+MBti!ZPIP;n<9XyceN$ zGrfjRDf+Rn+e@%<_gGuwTZi+qpD(57tS) zCba0L3gwX(mVuDh+TP3JL3`ROK*zjB3dRgqp+om%>Ggs5<**1^@?Q1liM0>QgdyUT3zsIkPa#*%(M}wYJu;3 z3oa`uccJ&WfJ2E|%#+eg#eh2R_iPoA7~j~=om5I(A|U4cUG=x7VzcaW!UhFbYPRuH z_AtLpTL3oVGdtHw13-US#*yr@q_e zkG&|}ggrjpd00jT&K=d`m}cpqh*-)*+J{kjcKBXzBW6aG2_a+31I!An${iXcQK9fp z;`k-9NwVB_RV4`ZAil_p{56+5klVKr_^LEhbJ<-zg3#R_Dbo0Nnmnd!Jbf2f@fFT4 zvU@t9qDzty`A%yX=pOqI7o)v?8L7W`-c1pTb40{G5x&KK18w@N^gk?i`eP{R&O9)k z75gg9D055j?GQDXHpgWa**lhAqlMfo1kao)@r%Bc%8EM%>7C;rF`a3fk`tq&zDGYd zO%p%dVkZ)6PDzjHtVLfRZvzqD)<%p0ENa2b9|ly~e0Q1%r?W*&NE;=xsydln6DyRR zM8yVyv0>cOIsqhs`+cKM(`u^1x@D}BeO#M@`Au=@@xe|>jpVAq4G=Sagwc`6bTu|i zuCJw^vOUCA*mY{DKqMFfb7v@#wVq{~0c5)VYq=Pj+ZI%osA8XK0NNLaCI1la*{9BA z#twAZ+<=~PtuNM{hmeP1w0$<2OFX6aM_gr0D9DPbGSEfY0fMga`W626vXArte|fx5?SI?xs)-^w4ng3f_~fpfd2`5Ayx z?NTzvp2kSxm;oi|8(>KH-Jg3J+jU^ns}-jUF}x;1QG&Hh(j>*GJwt)e#Y-Ib^9a^pzboOf_=M z*12JDvdqQn>KVmnegKv3Up@o$&5*Q730P_1pWs?CMog7P4$u_^aC|y&&uRz`^H~S# ze`RGPj(GbHi)DD04qyicbc60a#A1I?Q?Ey#|ozrxU{ z$?2E7Bfj7x_(FSWB8F4|&`2;mL%6cvk8hNv+RyJDZN4wLRVL{0rr|3j{02dER>fjW z%R>gt{hN0nucCQhhFzq=fkg%%&2m|+&*|&`dGnTSx@Zx7=v+4#a8f+14?6bO!8QiZ zDxdD_0of*U13x7|B2A_1pY6P9Jy&6)N!{B~g#;*qnQ6 z1JuKP>4!?SRzQZbbePN-Ddo_o7J%ezMt^(5gUK?Bdcb}gY7ssDn_oNGWIWt9&b}X` zqq#ie5-@n&LRs*mp*}~Qk^DQa9b>Vn7B%2aEK<(BZ|;)`iHq9h%yCZz9j?W3>=(2^ zatB&ZPY#93U$wU$pyD|d>yXn^1$tQq8D64Ike|;S>g&oR2O^Jl$C1du(o`S13^~-i z(SU@Wd-xyaIHLT-Y@8pftUJnqbTZ{y#I&pEEfbTywfbpR)F7p{K~|S&)2BuLlJ4mM zK*@T<%A0v)Ghi4OH@CsP%+jz66)ypyeX5?sK^!>9e?9e1k7j?)!X@BSp{9~mKN`k2 zRVCGb6q**Ib~PRvYT!ljAR9=PjO$R39c|Z*du)9bw7(JFIpWv^7^rxAgEmT;xS=yMnTPgp~%R+q4m_a`TZ8x{Eu z0q*OsX5&n({qkBQ_hFaFug(ZLF_2?+NhF7Yo~aZx-ZLhxDAR#9e{UDDs5AQ9ijj2R z+8nk(viQ&-*CZS^hD*&*bIB$y_r;-J=QvA9%{l7GsGotfa(s`tfi zmc6>|+iBBB^EvTKg0fGdiM1`44h@`mEGlgYm(!!O4OJ4!(Ik&wI(x5yrvX&AbgRU` zqkQw5*7RC7)B|5N1%$lvHiT>$)blpysFZ<(a{pkZVo%m*y+5xjf{G3NqzImcq;V5E_ENORPSl2ffd=nKzvt#NPCcHx z^VvDC=#FA>HD$Ip7RO0;htyL1J`mI;dMby3i}E`w6|b#ZqIb<*_$Z+(-1Q6ckL(+# zKhI`}T^B$YpP1MLCM&wWxa=vV!rj}K^g<&PVA0msXy^&}9PI{AVn!4}_Q5FwlVVdZ zy_e75uv3byD7cwAAh$PxQa<|L0kWPlTr(sPwGbV4KQ2wiOM|>!_xZ$U*}m_YOpmiK=Yp{SbsjP&wqnHvk{?O9mP1L@pa^in??vRNEKIFwib9NW_^Go3#1nymcnwtn7v zzVEjL&6>diR}21tz);_l{e{}iM}|T%sB3`C+Vyo6G<(imwF(eHI@K|Y*|q_SkM85m z?wV#GElhiZQBgJ97_G`_zr-k6W|UWohjn@Y>o+LZ((^@MD}gPvy8))?$b6pf`{DHINl`(j zpg$~>-rUj9RV0>6+4{9H;D=(NoI##N0nafe5u${ywY(y+8T!z*#-SG{_!L)Dfh>9G zRWJ=6`ndE9G~S$n#Uu0;fNsBFE3AE1a8eV|GWYJBmELr--_|mHaH6A)JVOn|t5u76 zoCB?0Zv+9+Bq=#^oUS+dbJIqv{L=K<@!he5mfgjYk`npTZ__}g_9$!?$PcBS+$nD& zPCMDn`jw;S=Raj-V+&k+GYTu$o?fzH66xr{ENJ`L`6Ln$1tvmHp*P?rh#h&5CVT%V z%`^Wp;bi`cGE9S0&SmZ!=fNGvh-z=;Lr@v>l>R1%}Be&8y z9?XoBA+A;v-ovuYq%fH^yXrX9Qi|QjirD3k){@c98c}JpD{T2SVfF-VZ8jJ@LP#X4qJQ!*OCfMF(i=Vn0*3yh~J1@YqjS(?X6$mT)l5115azHYV)w<>l;;GA+p(l{@)>!b; z)PykX0FbKlbY*45O#ST{)AWmY{T?Thc}&yW!o}|se0*oT&i!zA&2@@>_jLSi=IFzf zZzolXwHa;$D?phauR1BqHw>)qH*$68!F@yFmx1Rm_iqz}_ravjHv5zBX_U|J1DsvhciOpLoE_bvVc_M_#zDjDM+S2Ld#%O)L`sGX4nY zf=5!vz>voCVhCLRpU(er%RkZb&msC9bAH2?qjRq3sVC2%-aW+sKRzw>8*1fOZNvTt Dnp0Ek literal 0 HcmV?d00001 diff --git a/test/unit/extension_video_sensing_down-10.png b/test/unit/extension_video_sensing_down-10.png new file mode 100644 index 0000000000000000000000000000000000000000..109d6b8cafcb5a448ad28361ec3b1bf66d9f3aca GIT binary patch literal 10273 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 literal 0 HcmV?d00001 diff --git a/test/unit/extension_video_sensing_left-10.png b/test/unit/extension_video_sensing_left-10.png new file mode 100644 index 0000000000000000000000000000000000000000..8a42a119b9d59baac78b9d79737d31158d8565fc GIT binary patch literal 10008 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 KzCzX_?Ee9sOyHIP literal 0 HcmV?d00001 diff --git a/test/unit/extension_video_sensing_left-5.png b/test/unit/extension_video_sensing_left-5.png new file mode 100644 index 0000000000000000000000000000000000000000..5e84c3edffb093f62f32ce35c21a94ccd9989b30 GIT binary patch literal 10022 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 PkJl6)D$7<#8wdO!u@el( literal 0 HcmV?d00001