@tensorflow-models/blazeface
Advanced tools
Comparing version 0.0.3 to 0.0.4
@@ -17,2 +17,2 @@ /** | ||
*/ | ||
import{slice,add,div,sub,mul,concat2d,Tensor,concat,tensor1d,tensor2d,tidy,sigmoid,image,browser}from"@tensorflow/tfjs-core";import{loadGraphModel}from"@tensorflow/tfjs-converter";const disposeBox=t=>{t.startEndTensor.dispose(),t.startPoint.dispose(),t.endPoint.dispose()},createBox=t=>({startEndTensor:t,startPoint:slice(t,[0,0],[-1,2]),endPoint:slice(t,[0,2],[-1,2])}),scaleBox=(t,s)=>{const e=mul(t.startPoint,s),o=mul(t.endPoint,s),i=concat2d([e,o],1);return createBox(i)},ANCHORS_CONFIG={strides:[8,16],anchors:[2,6]},NUM_LANDMARKS=6;function generateAnchors(t,s,e){const o=[];for(let i=0;i<e.strides.length;i++){const a=e.strides[i],n=Math.floor((s+a-1)/a),r=Math.floor((t+a-1)/a),c=e.anchors[i];for(let t=0;t<n;t++){const s=a*(t+.5);for(let t=0;t<r;t++){const e=a*(t+.5);for(let t=0;t<c;t++)o.push([e,s])}}}return o}function decodeBounds(t,s,e){const o=slice(t,[0,1],[-1,2]),i=add(o,s),a=slice(t,[0,3],[-1,2]),n=div(a,e),r=div(i,e),c=div(n,2),l=sub(r,c),d=add(r,c),h=mul(l,e),p=mul(d,e);return concat2d([h,p],1)}function getInputTensorDimensions(t){return t instanceof Tensor?[t.shape[0],t.shape[1]]:[t.height,t.width]}function flipFaceHorizontal(t,s){return t.topLeft instanceof Tensor?{topLeft:concat([sub(s-1,t.topLeft.slice(0,1)),t.topLeft.slice(1,1)]),bottomRight:concat([sub(s-1,t.bottomRight.slice(0,1)),t.bottomRight.slice(1,1)]),landmarks:sub(tensor1d([s-1,0]),t.landmarks).mul(tensor1d([1,-1])),probability:t.probability}:{topLeft:[s-1-t.topLeft[0],t.topLeft[1]],bottomRight:[s-1-t.bottomRight[0],t.bottomRight[1]],landmarks:t.landmarks.map(t=>[s-1-t[0],t[1]]),probability:t.probability}}class BlazeFaceModel{constructor(t,s,e,o,i,a){this.blazeFaceModel=t,this.width=s,this.height=e,this.maxFaces=o,this.anchorsData=generateAnchors(s,e,ANCHORS_CONFIG),this.anchors=tensor2d(this.anchorsData),this.inputSizeData=[s,e],this.inputSize=tensor1d([s,e]),this.iouThreshold=i,this.scoreThreshold=a}async getBoundingBoxes(t,s){const[e,o,i]=tidy(()=>{const s=t.resizeBilinear([this.width,this.height]),e=mul(sub(s.div(255),.5),2),o=this.blazeFaceModel.predict(e).squeeze(),i=decodeBounds(o,this.anchors,this.inputSize),a=slice(o,[0,0],[-1,1]);return[o,i,sigmoid(a).squeeze()]}),a=console.warn;console.warn=(()=>{});const n=image.nonMaxSuppression(o,i,this.maxFaces,this.iouThreshold,this.scoreThreshold);console.warn=a;const r=await n.array();n.dispose();let c=r.map(t=>slice(o,[t,0],[1,-1]));s||(c=await Promise.all(c.map(async t=>{const s=await t.array();return t.dispose(),s})));const l=t.shape[1],d=t.shape[2];let h;h=s?div([d,l],this.inputSize):[d/this.inputSizeData[0],l/this.inputSizeData[1]];const p=c.map((t,o)=>tidy(()=>{const a=r[o];let n;return n=s?this.anchors.slice([a,0],[1,2]):this.anchorsData[a],{box:createBox(t instanceof Tensor?t:tensor2d(t)),landmarks:slice(e,[a,NUM_LANDMARKS-1],[1,-1]).squeeze().reshape([NUM_LANDMARKS,-1]),probability:slice(i,[a],[1]),anchor:n}}));return o.dispose(),i.dispose(),e.dispose(),[p,h]}async estimateFaces(t,s=!1,e=!1){const[,o]=getInputTensorDimensions(t),i=tidy(()=>(t instanceof Tensor||(t=browser.fromPixels(t)),t.toFloat().expandDims(0))),[a,n]=await this.getBoundingBoxes(i,s);return i.dispose(),s?a.map(t=>{const s=scaleBox(t.box,n).startEndTensor.squeeze();let i={topLeft:s.slice([0],[2]),bottomRight:s.slice([2],[2]),landmarks:t.landmarks.add(t.anchor).mul(n),probability:t.probability};return e&&(i=flipFaceHorizontal(i,o)),i}):Promise.all(a.map(async t=>{const s=tidy(()=>scaleBox(t.box,n).startEndTensor.squeeze()),[i,a,r]=await Promise.all([t.landmarks,s,t.probability].map(async t=>t.array())),c=t.anchor,l=i.map(t=>[(t[0]+c[0])*n[0],(t[1]+c[1])*n[1]]);s.dispose(),disposeBox(t.box),t.landmarks.dispose(),t.probability.dispose();let d={topLeft:a.slice(0,2),bottomRight:a.slice(2),landmarks:l,probability:r};return e&&(d=flipFaceHorizontal(d,o)),d}))}}const BLAZEFACE_MODEL_URL="https://tfhub.dev/tensorflow/tfjs-model/blazeface/1/default/1";async function load({maxFaces:t=10,inputWidth:s=128,inputHeight:e=128,iouThreshold:o=.3,scoreThreshold:i=.75}={}){const a=await loadGraphModel(BLAZEFACE_MODEL_URL,{fromTFHub:!0});return new BlazeFaceModel(a,s,e,t,o,i)}export{load,BlazeFaceModel}; | ||
import{slice,add,div,sub,mul,concat2d,Tensor,tidy,concat,tensor1d,tensor2d,sigmoid,image,browser}from"@tensorflow/tfjs-core";import{loadGraphModel}from"@tensorflow/tfjs-converter";const disposeBox=t=>{t.startEndTensor.dispose(),t.startPoint.dispose(),t.endPoint.dispose()},createBox=t=>({startEndTensor:t,startPoint:slice(t,[0,0],[-1,2]),endPoint:slice(t,[0,2],[-1,2])}),scaleBox=(t,o)=>{const s=mul(t.startPoint,o),e=mul(t.endPoint,o),i=concat2d([s,e],1);return createBox(i)},ANCHORS_CONFIG={strides:[8,16],anchors:[2,6]},NUM_LANDMARKS=6;function generateAnchors(t,o,s){const e=[];for(let i=0;i<s.strides.length;i++){const n=s.strides[i],a=Math.floor((o+n-1)/n),r=Math.floor((t+n-1)/n),c=s.anchors[i];for(let t=0;t<a;t++){const o=n*(t+.5);for(let t=0;t<r;t++){const s=n*(t+.5);for(let t=0;t<c;t++)e.push([s,o])}}}return e}function decodeBounds(t,o,s){const e=slice(t,[0,1],[-1,2]),i=add(e,o),n=slice(t,[0,3],[-1,2]),a=div(n,s),r=div(i,s),c=div(a,2),l=sub(r,c),d=add(r,c),h=mul(l,s),p=mul(d,s);return concat2d([h,p],1)}function getInputTensorDimensions(t){return t instanceof Tensor?[t.shape[0],t.shape[1]]:[t.height,t.width]}function flipFaceHorizontal(t,o){let s;if(null!=t.probability&&(s.probability=t.probability instanceof Tensor?t.probability.clone():t.probability),t.topLeft instanceof Tensor&&t.bottomRight instanceof Tensor){const[e,i]=tidy(()=>[concat([sub(o-1,t.topLeft.slice(0,1)),t.topLeft.slice(1,1)]),concat([sub(o-1,t.bottomRight.slice(0,1)),t.bottomRight.slice(1,1)])]);if(s={topLeft:e,bottomRight:i},null!=t.landmarks){const e=tidy(()=>sub(tensor1d([o-1,0]),t.landmarks).mul(tensor1d([1,-1])));s.landmarks=e}}else{const[e,i]=t.topLeft,[n,a]=t.bottomRight;s={topLeft:[o-1-e,i],bottomRight:[o-1-n,a]},null!=t.landmarks&&(s.landmarks=t.landmarks.map(t=>[o-1-t[0],t[1]]))}return s}function scaleBoxFromPrediction(t,o){return tidy(()=>{let s;return s=t.hasOwnProperty("box")?t.box:t,scaleBox(s,o).startEndTensor.squeeze()})}class BlazeFaceModel{constructor(t,o,s,e,i,n){this.blazeFaceModel=t,this.width=o,this.height=s,this.maxFaces=e,this.anchorsData=generateAnchors(o,s,ANCHORS_CONFIG),this.anchors=tensor2d(this.anchorsData),this.inputSizeData=[o,s],this.inputSize=tensor1d([o,s]),this.iouThreshold=i,this.scoreThreshold=n}async getBoundingBoxes(t,o,s=!0){const[e,i,n]=tidy(()=>{const o=t.resizeBilinear([this.width,this.height]),s=mul(sub(o.div(255),.5),2),e=this.blazeFaceModel.predict(s).squeeze(),i=decodeBounds(e,this.anchors,this.inputSize),n=slice(e,[0,0],[-1,1]);return[e,i,sigmoid(n).squeeze()]}),a=console.warn;console.warn=(()=>{});const r=image.nonMaxSuppression(i,n,this.maxFaces,this.iouThreshold,this.scoreThreshold);console.warn=a;const c=await r.array();r.dispose();let l=c.map(t=>slice(i,[t,0],[1,-1]));o||(l=await Promise.all(l.map(async t=>{const o=await t.array();return t.dispose(),o})));const d=t.shape[1],h=t.shape[2];let p;p=o?div([h,d],this.inputSize):[h/this.inputSizeData[0],d/this.inputSizeData[1]];const u=[];for(let t=0;t<l.length;t++){const i=l[t],a=tidy(()=>{const a=createBox(i instanceof Tensor?i:tensor2d(i));if(!s)return a;const r=c[t];let l;return l=o?this.anchors.slice([r,0],[1,2]):this.anchorsData[r],{box:a,landmarks:slice(e,[r,NUM_LANDMARKS-1],[1,-1]).squeeze().reshape([NUM_LANDMARKS,-1]),probability:slice(n,[r],[1]),anchor:l}});u.push(a)}return i.dispose(),n.dispose(),e.dispose(),{boxes:u,scaleFactor:p}}async estimateFaces(t,o=!1,s=!1,e=!0){const[,i]=getInputTensorDimensions(t),n=tidy(()=>(t instanceof Tensor||(t=browser.fromPixels(t)),t.toFloat().expandDims(0))),{boxes:a,scaleFactor:r}=await this.getBoundingBoxes(n,o,e);return n.dispose(),o?a.map(t=>{const o=scaleBoxFromPrediction(t,r);let n={topLeft:o.slice([0],[2]),bottomRight:o.slice([2],[2])};if(e){const{landmarks:o,probability:s,anchor:e}=t,i=o.add(e).mul(r);n.landmarks=i,n.probability=s}return s&&(n=flipFaceHorizontal(n,i)),n}):Promise.all(a.map(async t=>{const o=scaleBoxFromPrediction(t,r);let n;if(e){const[s,e,i]=await Promise.all([t.landmarks,o,t.probability].map(async t=>t.array())),a=t.anchor,[c,l]=r,d=s.map(t=>[(t[0]+a[0])*c,(t[1]+a[1])*l]);n={topLeft:e.slice(0,2),bottomRight:e.slice(2),landmarks:d,probability:i},disposeBox(t.box),t.landmarks.dispose(),t.probability.dispose()}else{const t=await o.array();n={topLeft:t.slice(0,2),bottomRight:t.slice(2)}}return o.dispose(),s&&(n=flipFaceHorizontal(n,i)),n}))}}const BLAZEFACE_MODEL_URL="https://tfhub.dev/tensorflow/tfjs-model/blazeface/1/default/1";async function load({maxFaces:t=10,inputWidth:o=128,inputHeight:s=128,iouThreshold:e=.3,scoreThreshold:i=.75}={}){const n=await loadGraphModel(BLAZEFACE_MODEL_URL,{fromTFHub:!0});return new BlazeFaceModel(n,o,s,t,e,i)}export{load,BlazeFaceModel}; |
@@ -83,29 +83,58 @@ /** | ||
function flipFaceHorizontal(face, imageWidth) { | ||
if (face.topLeft instanceof tf.Tensor) { | ||
return { | ||
topLeft: tf.concat([ | ||
tf.sub(imageWidth - 1, face.topLeft.slice(0, 1)), | ||
face.topLeft.slice(1, 1) | ||
]), | ||
bottomRight: tf.concat([ | ||
tf.sub(imageWidth - 1, face.bottomRight.slice(0, 1)), | ||
face.bottomRight.slice(1, 1) | ||
]), | ||
landmarks: tf.sub(tf.tensor1d([imageWidth - 1, 0]), face.landmarks) | ||
.mul(tf.tensor1d([1, -1])), | ||
probability: face.probability | ||
let flipped; | ||
if (face.probability != null) { | ||
flipped.probability = face.probability instanceof tf.Tensor ? | ||
face.probability.clone() : | ||
face.probability; | ||
} | ||
if (face.topLeft instanceof tf.Tensor && | ||
face.bottomRight instanceof tf.Tensor) { | ||
const [topLeft, bottomRight] = tf.tidy(() => { | ||
return [ | ||
tf.concat([ | ||
tf.sub(imageWidth - 1, face.topLeft.slice(0, 1)), | ||
face.topLeft.slice(1, 1) | ||
]), | ||
tf.concat([ | ||
tf.sub(imageWidth - 1, face.bottomRight.slice(0, 1)), | ||
face.bottomRight.slice(1, 1) | ||
]) | ||
]; | ||
}); | ||
flipped = { topLeft, bottomRight }; | ||
if (face.landmarks != null) { | ||
const flippedLandmarks = tf.tidy(() => tf.sub(tf.tensor1d([imageWidth - 1, 0]), face.landmarks) | ||
.mul(tf.tensor1d([1, -1]))); | ||
flipped.landmarks = flippedLandmarks; | ||
} | ||
} | ||
else { | ||
const [topLeftX, topLeftY] = face.topLeft; | ||
const [bottomRightX, bottomRightY] = face.bottomRight; | ||
flipped = { | ||
topLeft: [imageWidth - 1 - topLeftX, topLeftY], | ||
bottomRight: [imageWidth - 1 - bottomRightX, bottomRightY] | ||
}; | ||
if (face.landmarks != null) { | ||
flipped.landmarks = | ||
face.landmarks.map((coord) => ([ | ||
imageWidth - 1 - coord[0], | ||
coord[1] | ||
])); | ||
} | ||
} | ||
return { | ||
topLeft: [imageWidth - 1 - face.topLeft[0], face.topLeft[1]], | ||
bottomRight: [ | ||
imageWidth - 1 - face.bottomRight[0], | ||
face.bottomRight[1] | ||
], | ||
landmarks: face.landmarks.map((coord) => ([ | ||
imageWidth - 1 - coord[0], coord[1] | ||
])), | ||
probability: face.probability | ||
}; | ||
return flipped; | ||
} | ||
function scaleBoxFromPrediction(face, scaleFactor) { | ||
return tf.tidy(() => { | ||
let box; | ||
if (face.hasOwnProperty('box')) { | ||
box = face.box; | ||
} | ||
else { | ||
box = face; | ||
} | ||
return scaleBox(box, scaleFactor).startEndTensor.squeeze(); | ||
}); | ||
} | ||
class BlazeFaceModel { | ||
@@ -124,3 +153,3 @@ constructor(model, width, height, maxFaces, iouThreshold, scoreThreshold) { | ||
} | ||
async getBoundingBoxes(inputImage, returnTensors) { | ||
async getBoundingBoxes(inputImage, returnTensors, annotateBoxes = true) { | ||
const [detectedOutputs, boxes, scores] = tf.tidy(() => { | ||
@@ -133,3 +162,4 @@ const resizedImage = inputImage.resizeBilinear([this.width, this.height]); | ||
const logits = tf.slice(prediction, [0, 0], [-1, 1]); | ||
return [prediction, decodedBounds, tf.sigmoid(logits).squeeze()]; | ||
const scores = tf.sigmoid(logits).squeeze(); | ||
return [prediction, decodedBounds, scores]; | ||
}); | ||
@@ -162,27 +192,37 @@ const savedConsoleWarnFn = console.warn; | ||
} | ||
const annotatedBoxes = boundingBoxes | ||
.map((boundingBox, i) => tf.tidy(() => { | ||
const boxIndex = boxIndices[i]; | ||
let anchor; | ||
if (returnTensors) { | ||
anchor = this.anchors.slice([boxIndex, 0], [1, 2]); | ||
} | ||
else { | ||
anchor = this.anchorsData[boxIndex]; | ||
} | ||
const box = boundingBox instanceof tf.Tensor ? | ||
createBox(boundingBox) : | ||
createBox(tf.tensor2d(boundingBox)); | ||
const landmarks = tf.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1]) | ||
.squeeze() | ||
.reshape([NUM_LANDMARKS, -1]); | ||
const probability = tf.slice(scores, [boxIndex], [1]); | ||
return { box, landmarks, probability, anchor }; | ||
})); | ||
const annotatedBoxes = []; | ||
for (let i = 0; i < boundingBoxes.length; i++) { | ||
const boundingBox = boundingBoxes[i]; | ||
const annotatedBox = tf.tidy(() => { | ||
const box = boundingBox instanceof tf.Tensor ? | ||
createBox(boundingBox) : | ||
createBox(tf.tensor2d(boundingBox)); | ||
if (!annotateBoxes) { | ||
return box; | ||
} | ||
const boxIndex = boxIndices[i]; | ||
let anchor; | ||
if (returnTensors) { | ||
anchor = this.anchors.slice([boxIndex, 0], [1, 2]); | ||
} | ||
else { | ||
anchor = this.anchorsData[boxIndex]; | ||
} | ||
const landmarks = tf.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1]) | ||
.squeeze() | ||
.reshape([NUM_LANDMARKS, -1]); | ||
const probability = tf.slice(scores, [boxIndex], [1]); | ||
return { box, landmarks, probability, anchor }; | ||
}); | ||
annotatedBoxes.push(annotatedBox); | ||
} | ||
boxes.dispose(); | ||
scores.dispose(); | ||
detectedOutputs.dispose(); | ||
return [annotatedBoxes, scaleFactor]; | ||
return { | ||
boxes: annotatedBoxes, | ||
scaleFactor | ||
}; | ||
} | ||
async estimateFaces(input, returnTensors = false, flipHorizontal = false) { | ||
async estimateFaces(input, returnTensors = false, flipHorizontal = false, annotateBoxes = true) { | ||
const [, width] = getInputTensorDimensions(input); | ||
@@ -195,17 +235,19 @@ const image = tf.tidy(() => { | ||
}); | ||
const [prediction, scaleFactor] = await this.getBoundingBoxes(image, returnTensors); | ||
const { boxes, scaleFactor } = await this.getBoundingBoxes(image, returnTensors, annotateBoxes); | ||
image.dispose(); | ||
if (returnTensors) { | ||
return prediction.map((face) => { | ||
const scaledBox = scaleBox(face.box, scaleFactor) | ||
.startEndTensor.squeeze(); | ||
return boxes.map((face) => { | ||
const scaledBox = scaleBoxFromPrediction(face, scaleFactor); | ||
let normalizedFace = { | ||
topLeft: scaledBox.slice([0], [2]), | ||
bottomRight: scaledBox.slice([2], [2]), | ||
landmarks: face.landmarks.add(face.anchor).mul(scaleFactor), | ||
probability: face.probability | ||
bottomRight: scaledBox.slice([2], [2]) | ||
}; | ||
if (annotateBoxes) { | ||
const { landmarks, probability, anchor } = face; | ||
const normalizedLandmarks = landmarks.add(anchor).mul(scaleFactor); | ||
normalizedFace.landmarks = normalizedLandmarks; | ||
normalizedFace.probability = probability; | ||
} | ||
if (flipHorizontal) { | ||
normalizedFace = | ||
flipFaceHorizontal(normalizedFace, width); | ||
normalizedFace = flipFaceHorizontal(normalizedFace, width); | ||
} | ||
@@ -215,26 +257,32 @@ return normalizedFace; | ||
} | ||
return Promise.all(prediction.map(async (face) => { | ||
const scaledBox = tf.tidy(() => { | ||
return scaleBox(face.box, scaleFactor) | ||
.startEndTensor.squeeze(); | ||
}); | ||
const [landmarkData, boxData, probabilityData] = await Promise.all([face.landmarks, scaledBox, face.probability].map(async (d) => d.array())); | ||
const anchor = face.anchor; | ||
const scaledLandmarks = landmarkData | ||
.map((landmark) => ([ | ||
(landmark[0] + anchor[0]) * | ||
scaleFactor[0], | ||
(landmark[1] + anchor[1]) * | ||
scaleFactor[1] | ||
])); | ||
return Promise.all(boxes.map(async (face) => { | ||
const scaledBox = scaleBoxFromPrediction(face, scaleFactor); | ||
let normalizedFace; | ||
if (!annotateBoxes) { | ||
const boxData = await scaledBox.array(); | ||
normalizedFace = { | ||
topLeft: boxData.slice(0, 2), | ||
bottomRight: boxData.slice(2) | ||
}; | ||
} | ||
else { | ||
const [landmarkData, boxData, probabilityData] = await Promise.all([face.landmarks, scaledBox, face.probability].map(async (d) => d.array())); | ||
const anchor = face.anchor; | ||
const [scaleFactorX, scaleFactorY] = scaleFactor; | ||
const scaledLandmarks = landmarkData | ||
.map(landmark => ([ | ||
(landmark[0] + anchor[0]) * scaleFactorX, | ||
(landmark[1] + anchor[1]) * scaleFactorY | ||
])); | ||
normalizedFace = { | ||
topLeft: boxData.slice(0, 2), | ||
bottomRight: boxData.slice(2), | ||
landmarks: scaledLandmarks, | ||
probability: probabilityData | ||
}; | ||
disposeBox(face.box); | ||
face.landmarks.dispose(); | ||
face.probability.dispose(); | ||
} | ||
scaledBox.dispose(); | ||
disposeBox(face.box); | ||
face.landmarks.dispose(); | ||
face.probability.dispose(); | ||
let normalizedFace = { | ||
topLeft: boxData.slice(0, 2), | ||
bottomRight: boxData.slice(2), | ||
landmarks: scaledLandmarks, | ||
probability: probabilityData | ||
}; | ||
if (flipHorizontal) { | ||
@@ -241,0 +289,0 @@ normalizedFace = flipFaceHorizontal(normalizedFace, width); |
@@ -17,2 +17,2 @@ /** | ||
*/ | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("@tensorflow/tfjs-core"),require("@tensorflow/tfjs-converter")):"function"==typeof define&&define.amd?define(["exports","@tensorflow/tfjs-core","@tensorflow/tfjs-converter"],e):e(t.blazeface={},t.tf,t.tf)}(this,function(t,e,s){"use strict";const o=t=>{t.startEndTensor.dispose(),t.startPoint.dispose(),t.endPoint.dispose()},i=t=>({startEndTensor:t,startPoint:e.slice(t,[0,0],[-1,2]),endPoint:e.slice(t,[0,2],[-1,2])}),n=(t,s)=>{const o=e.mul(t.startPoint,s),n=e.mul(t.endPoint,s),a=e.concat2d([o,n],1);return i(a)},a={strides:[8,16],anchors:[2,6]},r=6;function c(t,s){return t.topLeft instanceof e.Tensor?{topLeft:e.concat([e.sub(s-1,t.topLeft.slice(0,1)),t.topLeft.slice(1,1)]),bottomRight:e.concat([e.sub(s-1,t.bottomRight.slice(0,1)),t.bottomRight.slice(1,1)]),landmarks:e.sub(e.tensor1d([s-1,0]),t.landmarks).mul(e.tensor1d([1,-1])),probability:t.probability}:{topLeft:[s-1-t.topLeft[0],t.topLeft[1]],bottomRight:[s-1-t.bottomRight[0],t.bottomRight[1]],landmarks:t.landmarks.map(t=>[s-1-t[0],t[1]]),probability:t.probability}}class l{constructor(t,s,o,i,n,r){this.blazeFaceModel=t,this.width=s,this.height=o,this.maxFaces=i,this.anchorsData=function(t,e,s){const o=[];for(let i=0;i<s.strides.length;i++){const n=s.strides[i],a=Math.floor((e+n-1)/n),r=Math.floor((t+n-1)/n),c=s.anchors[i];for(let t=0;t<a;t++){const e=n*(t+.5);for(let t=0;t<r;t++){const s=n*(t+.5);for(let t=0;t<c;t++)o.push([s,e])}}}return o}(s,o,a),this.anchors=e.tensor2d(this.anchorsData),this.inputSizeData=[s,o],this.inputSize=e.tensor1d([s,o]),this.iouThreshold=n,this.scoreThreshold=r}async getBoundingBoxes(t,s){const[o,n,a]=e.tidy(()=>{const s=t.resizeBilinear([this.width,this.height]),o=e.mul(e.sub(s.div(255),.5),2),i=this.blazeFaceModel.predict(o).squeeze(),n=function(t,s,o){const i=e.slice(t,[0,1],[-1,2]),n=e.add(i,s),a=e.slice(t,[0,3],[-1,2]),r=e.div(a,o),c=e.div(n,o),l=e.div(r,2),d=e.sub(c,l),h=e.add(c,l),p=e.mul(d,o),u=e.mul(h,o);return e.concat2d([p,u],1)}(i,this.anchors,this.inputSize),a=e.slice(i,[0,0],[-1,1]);return[i,n,e.sigmoid(a).squeeze()]}),c=console.warn;console.warn=(()=>{});const l=e.image.nonMaxSuppression(n,a,this.maxFaces,this.iouThreshold,this.scoreThreshold);console.warn=c;const d=await l.array();l.dispose();let h=d.map(t=>e.slice(n,[t,0],[1,-1]));s||(h=await Promise.all(h.map(async t=>{const e=await t.array();return t.dispose(),e})));const p=t.shape[1],u=t.shape[2];let f;f=s?e.div([u,p],this.inputSize):[u/this.inputSizeData[0],p/this.inputSizeData[1]];const m=h.map((t,n)=>e.tidy(()=>{const c=d[n];let l;return l=s?this.anchors.slice([c,0],[1,2]):this.anchorsData[c],{box:t instanceof e.Tensor?i(t):i(e.tensor2d(t)),landmarks:e.slice(o,[c,r-1],[1,-1]).squeeze().reshape([r,-1]),probability:e.slice(a,[c],[1]),anchor:l}}));return n.dispose(),a.dispose(),o.dispose(),[m,f]}async estimateFaces(t,s=!1,i=!1){const[,a]=function(t){return t instanceof e.Tensor?[t.shape[0],t.shape[1]]:[t.height,t.width]}(t),r=e.tidy(()=>(t instanceof e.Tensor||(t=e.browser.fromPixels(t)),t.toFloat().expandDims(0))),[l,d]=await this.getBoundingBoxes(r,s);return r.dispose(),s?l.map(t=>{const e=n(t.box,d).startEndTensor.squeeze();let s={topLeft:e.slice([0],[2]),bottomRight:e.slice([2],[2]),landmarks:t.landmarks.add(t.anchor).mul(d),probability:t.probability};return i&&(s=c(s,a)),s}):Promise.all(l.map(async t=>{const s=e.tidy(()=>n(t.box,d).startEndTensor.squeeze()),[r,l,h]=await Promise.all([t.landmarks,s,t.probability].map(async t=>t.array())),p=t.anchor,u=r.map(t=>[(t[0]+p[0])*d[0],(t[1]+p[1])*d[1]]);s.dispose(),o(t.box),t.landmarks.dispose(),t.probability.dispose();let f={topLeft:l.slice(0,2),bottomRight:l.slice(2),landmarks:u,probability:h};return i&&(f=c(f,a)),f}))}}const d="https://tfhub.dev/tensorflow/tfjs-model/blazeface/1/default/1";t.load=async function({maxFaces:t=10,inputWidth:e=128,inputHeight:o=128,iouThreshold:i=.3,scoreThreshold:n=.75}={}){const a=await s.loadGraphModel(d,{fromTFHub:!0});return new l(a,e,o,t,i,n)},t.BlazeFaceModel=l,Object.defineProperty(t,"__esModule",{value:!0})}); | ||
!function(t,s){"object"==typeof exports&&"undefined"!=typeof module?s(exports,require("@tensorflow/tfjs-core"),require("@tensorflow/tfjs-converter")):"function"==typeof define&&define.amd?define(["exports","@tensorflow/tfjs-core","@tensorflow/tfjs-converter"],s):s(t.blazeface={},t.tf,t.tf)}(this,function(t,s,e){"use strict";const o=t=>{t.startEndTensor.dispose(),t.startPoint.dispose(),t.endPoint.dispose()},i=t=>({startEndTensor:t,startPoint:s.slice(t,[0,0],[-1,2]),endPoint:s.slice(t,[0,2],[-1,2])}),n=(t,e)=>{const o=s.mul(t.startPoint,e),n=s.mul(t.endPoint,e),a=s.concat2d([o,n],1);return i(a)},a={strides:[8,16],anchors:[2,6]},r=6;function c(t,e){let o;if(null!=t.probability&&(o.probability=t.probability instanceof s.Tensor?t.probability.clone():t.probability),t.topLeft instanceof s.Tensor&&t.bottomRight instanceof s.Tensor){const[i,n]=s.tidy(()=>[s.concat([s.sub(e-1,t.topLeft.slice(0,1)),t.topLeft.slice(1,1)]),s.concat([s.sub(e-1,t.bottomRight.slice(0,1)),t.bottomRight.slice(1,1)])]);if(o={topLeft:i,bottomRight:n},null!=t.landmarks){const i=s.tidy(()=>s.sub(s.tensor1d([e-1,0]),t.landmarks).mul(s.tensor1d([1,-1])));o.landmarks=i}}else{const[s,i]=t.topLeft,[n,a]=t.bottomRight;o={topLeft:[e-1-s,i],bottomRight:[e-1-n,a]},null!=t.landmarks&&(o.landmarks=t.landmarks.map(t=>[e-1-t[0],t[1]]))}return o}function l(t,e){return s.tidy(()=>{let s;return s=t.hasOwnProperty("box")?t.box:t,n(s,e).startEndTensor.squeeze()})}class d{constructor(t,e,o,i,n,r){this.blazeFaceModel=t,this.width=e,this.height=o,this.maxFaces=i,this.anchorsData=function(t,s,e){const o=[];for(let i=0;i<e.strides.length;i++){const n=e.strides[i],a=Math.floor((s+n-1)/n),r=Math.floor((t+n-1)/n),c=e.anchors[i];for(let t=0;t<a;t++){const s=n*(t+.5);for(let t=0;t<r;t++){const e=n*(t+.5);for(let t=0;t<c;t++)o.push([e,s])}}}return o}(e,o,a),this.anchors=s.tensor2d(this.anchorsData),this.inputSizeData=[e,o],this.inputSize=s.tensor1d([e,o]),this.iouThreshold=n,this.scoreThreshold=r}async getBoundingBoxes(t,e,o=!0){const[n,a,c]=s.tidy(()=>{const e=t.resizeBilinear([this.width,this.height]),o=s.mul(s.sub(e.div(255),.5),2),i=this.blazeFaceModel.predict(o).squeeze(),n=function(t,e,o){const i=s.slice(t,[0,1],[-1,2]),n=s.add(i,e),a=s.slice(t,[0,3],[-1,2]),r=s.div(a,o),c=s.div(n,o),l=s.div(r,2),d=s.sub(c,l),h=s.add(c,l),u=s.mul(d,o),p=s.mul(h,o);return s.concat2d([u,p],1)}(i,this.anchors,this.inputSize),a=s.slice(i,[0,0],[-1,1]);return[i,n,s.sigmoid(a).squeeze()]}),l=console.warn;console.warn=(()=>{});const d=s.image.nonMaxSuppression(a,c,this.maxFaces,this.iouThreshold,this.scoreThreshold);console.warn=l;const h=await d.array();d.dispose();let u=h.map(t=>s.slice(a,[t,0],[1,-1]));e||(u=await Promise.all(u.map(async t=>{const s=await t.array();return t.dispose(),s})));const p=t.shape[1],f=t.shape[2];let b;b=e?s.div([f,p],this.inputSize):[f/this.inputSizeData[0],p/this.inputSizeData[1]];const m=[];for(let t=0;t<u.length;t++){const a=u[t],l=s.tidy(()=>{const l=a instanceof s.Tensor?i(a):i(s.tensor2d(a));if(!o)return l;const d=h[t];let u;return u=e?this.anchors.slice([d,0],[1,2]):this.anchorsData[d],{box:l,landmarks:s.slice(n,[d,r-1],[1,-1]).squeeze().reshape([r,-1]),probability:s.slice(c,[d],[1]),anchor:u}});m.push(l)}return a.dispose(),c.dispose(),n.dispose(),{boxes:m,scaleFactor:b}}async estimateFaces(t,e=!1,i=!1,n=!0){const[,a]=function(t){return t instanceof s.Tensor?[t.shape[0],t.shape[1]]:[t.height,t.width]}(t),r=s.tidy(()=>(t instanceof s.Tensor||(t=s.browser.fromPixels(t)),t.toFloat().expandDims(0))),{boxes:d,scaleFactor:h}=await this.getBoundingBoxes(r,e,n);return r.dispose(),e?d.map(t=>{const s=l(t,h);let e={topLeft:s.slice([0],[2]),bottomRight:s.slice([2],[2])};if(n){const{landmarks:s,probability:o,anchor:i}=t,n=s.add(i).mul(h);e.landmarks=n,e.probability=o}return i&&(e=c(e,a)),e}):Promise.all(d.map(async t=>{const s=l(t,h);let e;if(n){const[i,n,a]=await Promise.all([t.landmarks,s,t.probability].map(async t=>t.array())),r=t.anchor,[c,l]=h,d=i.map(t=>[(t[0]+r[0])*c,(t[1]+r[1])*l]);e={topLeft:n.slice(0,2),bottomRight:n.slice(2),landmarks:d,probability:a},o(t.box),t.landmarks.dispose(),t.probability.dispose()}else{const t=await s.array();e={topLeft:t.slice(0,2),bottomRight:t.slice(2)}}return s.dispose(),i&&(e=c(e,a)),e}))}}const h="https://tfhub.dev/tensorflow/tfjs-model/blazeface/1/default/1";t.load=async function({maxFaces:t=10,inputWidth:s=128,inputHeight:o=128,iouThreshold:i=.3,scoreThreshold:n=.75}={}){const a=await e.loadGraphModel(h,{fromTFHub:!0});return new d(a,s,o,t,i,n)},t.BlazeFaceModel=d,Object.defineProperty(t,"__esModule",{value:!0})}); |
@@ -7,6 +7,6 @@ import * as tfconv from '@tensorflow/tfjs-converter'; | ||
bottomRight: [number, number] | tf.Tensor1D; | ||
landmarks: number[][] | tf.Tensor2D; | ||
probability: number | tf.Tensor1D; | ||
landmarks?: number[][] | tf.Tensor2D; | ||
probability?: number | tf.Tensor1D; | ||
} | ||
declare type BlazeFacePrediction = { | ||
export declare type BlazeFacePrediction = { | ||
box: Box; | ||
@@ -29,5 +29,7 @@ landmarks: tf.Tensor2D; | ||
constructor(model: tfconv.GraphModel, width: number, height: number, maxFaces: number, iouThreshold: number, scoreThreshold: number); | ||
getBoundingBoxes(inputImage: tf.Tensor4D, returnTensors: boolean): Promise<[BlazeFacePrediction[], tf.Tensor | [number, number]]>; | ||
estimateFaces(input: tf.Tensor3D | ImageData | HTMLVideoElement | HTMLImageElement | HTMLCanvasElement, returnTensors?: boolean, flipHorizontal?: boolean): Promise<NormalizedFace[]>; | ||
getBoundingBoxes(inputImage: tf.Tensor4D, returnTensors: boolean, annotateBoxes?: boolean): Promise<{ | ||
boxes: Array<BlazeFacePrediction | Box>; | ||
scaleFactor: tf.Tensor | [number, number]; | ||
}>; | ||
estimateFaces(input: tf.Tensor3D | ImageData | HTMLVideoElement | HTMLImageElement | HTMLCanvasElement, returnTensors?: boolean, flipHorizontal?: boolean, annotateBoxes?: boolean): Promise<NormalizedFace[]>; | ||
} | ||
export {}; |
206
dist/face.js
@@ -48,29 +48,58 @@ "use strict"; | ||
function flipFaceHorizontal(face, imageWidth) { | ||
if (face.topLeft instanceof tf.Tensor) { | ||
return { | ||
topLeft: tf.concat([ | ||
tf.sub(imageWidth - 1, face.topLeft.slice(0, 1)), | ||
face.topLeft.slice(1, 1) | ||
]), | ||
bottomRight: tf.concat([ | ||
tf.sub(imageWidth - 1, face.bottomRight.slice(0, 1)), | ||
face.bottomRight.slice(1, 1) | ||
]), | ||
landmarks: tf.sub(tf.tensor1d([imageWidth - 1, 0]), face.landmarks) | ||
.mul(tf.tensor1d([1, -1])), | ||
probability: face.probability | ||
let flipped; | ||
if (face.probability != null) { | ||
flipped.probability = face.probability instanceof tf.Tensor ? | ||
face.probability.clone() : | ||
face.probability; | ||
} | ||
if (face.topLeft instanceof tf.Tensor && | ||
face.bottomRight instanceof tf.Tensor) { | ||
const [topLeft, bottomRight] = tf.tidy(() => { | ||
return [ | ||
tf.concat([ | ||
tf.sub(imageWidth - 1, face.topLeft.slice(0, 1)), | ||
face.topLeft.slice(1, 1) | ||
]), | ||
tf.concat([ | ||
tf.sub(imageWidth - 1, face.bottomRight.slice(0, 1)), | ||
face.bottomRight.slice(1, 1) | ||
]) | ||
]; | ||
}); | ||
flipped = { topLeft, bottomRight }; | ||
if (face.landmarks != null) { | ||
const flippedLandmarks = tf.tidy(() => tf.sub(tf.tensor1d([imageWidth - 1, 0]), face.landmarks) | ||
.mul(tf.tensor1d([1, -1]))); | ||
flipped.landmarks = flippedLandmarks; | ||
} | ||
} | ||
else { | ||
const [topLeftX, topLeftY] = face.topLeft; | ||
const [bottomRightX, bottomRightY] = face.bottomRight; | ||
flipped = { | ||
topLeft: [imageWidth - 1 - topLeftX, topLeftY], | ||
bottomRight: [imageWidth - 1 - bottomRightX, bottomRightY] | ||
}; | ||
if (face.landmarks != null) { | ||
flipped.landmarks = | ||
face.landmarks.map((coord) => ([ | ||
imageWidth - 1 - coord[0], | ||
coord[1] | ||
])); | ||
} | ||
} | ||
return { | ||
topLeft: [imageWidth - 1 - face.topLeft[0], face.topLeft[1]], | ||
bottomRight: [ | ||
imageWidth - 1 - face.bottomRight[0], | ||
face.bottomRight[1] | ||
], | ||
landmarks: face.landmarks.map((coord) => ([ | ||
imageWidth - 1 - coord[0], coord[1] | ||
])), | ||
probability: face.probability | ||
}; | ||
return flipped; | ||
} | ||
function scaleBoxFromPrediction(face, scaleFactor) { | ||
return tf.tidy(() => { | ||
let box; | ||
if (face.hasOwnProperty('box')) { | ||
box = face.box; | ||
} | ||
else { | ||
box = face; | ||
} | ||
return box_1.scaleBox(box, scaleFactor).startEndTensor.squeeze(); | ||
}); | ||
} | ||
class BlazeFaceModel { | ||
@@ -89,3 +118,3 @@ constructor(model, width, height, maxFaces, iouThreshold, scoreThreshold) { | ||
} | ||
async getBoundingBoxes(inputImage, returnTensors) { | ||
async getBoundingBoxes(inputImage, returnTensors, annotateBoxes = true) { | ||
const [detectedOutputs, boxes, scores] = tf.tidy(() => { | ||
@@ -98,3 +127,4 @@ const resizedImage = inputImage.resizeBilinear([this.width, this.height]); | ||
const logits = tf.slice(prediction, [0, 0], [-1, 1]); | ||
return [prediction, decodedBounds, tf.sigmoid(logits).squeeze()]; | ||
const scores = tf.sigmoid(logits).squeeze(); | ||
return [prediction, decodedBounds, scores]; | ||
}); | ||
@@ -127,27 +157,37 @@ const savedConsoleWarnFn = console.warn; | ||
} | ||
const annotatedBoxes = boundingBoxes | ||
.map((boundingBox, i) => tf.tidy(() => { | ||
const boxIndex = boxIndices[i]; | ||
let anchor; | ||
if (returnTensors) { | ||
anchor = this.anchors.slice([boxIndex, 0], [1, 2]); | ||
} | ||
else { | ||
anchor = this.anchorsData[boxIndex]; | ||
} | ||
const box = boundingBox instanceof tf.Tensor ? | ||
box_1.createBox(boundingBox) : | ||
box_1.createBox(tf.tensor2d(boundingBox)); | ||
const landmarks = tf.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1]) | ||
.squeeze() | ||
.reshape([NUM_LANDMARKS, -1]); | ||
const probability = tf.slice(scores, [boxIndex], [1]); | ||
return { box, landmarks, probability, anchor }; | ||
})); | ||
const annotatedBoxes = []; | ||
for (let i = 0; i < boundingBoxes.length; i++) { | ||
const boundingBox = boundingBoxes[i]; | ||
const annotatedBox = tf.tidy(() => { | ||
const box = boundingBox instanceof tf.Tensor ? | ||
box_1.createBox(boundingBox) : | ||
box_1.createBox(tf.tensor2d(boundingBox)); | ||
if (!annotateBoxes) { | ||
return box; | ||
} | ||
const boxIndex = boxIndices[i]; | ||
let anchor; | ||
if (returnTensors) { | ||
anchor = this.anchors.slice([boxIndex, 0], [1, 2]); | ||
} | ||
else { | ||
anchor = this.anchorsData[boxIndex]; | ||
} | ||
const landmarks = tf.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1]) | ||
.squeeze() | ||
.reshape([NUM_LANDMARKS, -1]); | ||
const probability = tf.slice(scores, [boxIndex], [1]); | ||
return { box, landmarks, probability, anchor }; | ||
}); | ||
annotatedBoxes.push(annotatedBox); | ||
} | ||
boxes.dispose(); | ||
scores.dispose(); | ||
detectedOutputs.dispose(); | ||
return [annotatedBoxes, scaleFactor]; | ||
return { | ||
boxes: annotatedBoxes, | ||
scaleFactor | ||
}; | ||
} | ||
async estimateFaces(input, returnTensors = false, flipHorizontal = false) { | ||
async estimateFaces(input, returnTensors = false, flipHorizontal = false, annotateBoxes = true) { | ||
const [, width] = getInputTensorDimensions(input); | ||
@@ -160,17 +200,19 @@ const image = tf.tidy(() => { | ||
}); | ||
const [prediction, scaleFactor] = await this.getBoundingBoxes(image, returnTensors); | ||
const { boxes, scaleFactor } = await this.getBoundingBoxes(image, returnTensors, annotateBoxes); | ||
image.dispose(); | ||
if (returnTensors) { | ||
return prediction.map((face) => { | ||
const scaledBox = box_1.scaleBox(face.box, scaleFactor) | ||
.startEndTensor.squeeze(); | ||
return boxes.map((face) => { | ||
const scaledBox = scaleBoxFromPrediction(face, scaleFactor); | ||
let normalizedFace = { | ||
topLeft: scaledBox.slice([0], [2]), | ||
bottomRight: scaledBox.slice([2], [2]), | ||
landmarks: face.landmarks.add(face.anchor).mul(scaleFactor), | ||
probability: face.probability | ||
bottomRight: scaledBox.slice([2], [2]) | ||
}; | ||
if (annotateBoxes) { | ||
const { landmarks, probability, anchor } = face; | ||
const normalizedLandmarks = landmarks.add(anchor).mul(scaleFactor); | ||
normalizedFace.landmarks = normalizedLandmarks; | ||
normalizedFace.probability = probability; | ||
} | ||
if (flipHorizontal) { | ||
normalizedFace = | ||
flipFaceHorizontal(normalizedFace, width); | ||
normalizedFace = flipFaceHorizontal(normalizedFace, width); | ||
} | ||
@@ -180,26 +222,32 @@ return normalizedFace; | ||
} | ||
return Promise.all(prediction.map(async (face) => { | ||
const scaledBox = tf.tidy(() => { | ||
return box_1.scaleBox(face.box, scaleFactor) | ||
.startEndTensor.squeeze(); | ||
}); | ||
const [landmarkData, boxData, probabilityData] = await Promise.all([face.landmarks, scaledBox, face.probability].map(async (d) => d.array())); | ||
const anchor = face.anchor; | ||
const scaledLandmarks = landmarkData | ||
.map((landmark) => ([ | ||
(landmark[0] + anchor[0]) * | ||
scaleFactor[0], | ||
(landmark[1] + anchor[1]) * | ||
scaleFactor[1] | ||
])); | ||
return Promise.all(boxes.map(async (face) => { | ||
const scaledBox = scaleBoxFromPrediction(face, scaleFactor); | ||
let normalizedFace; | ||
if (!annotateBoxes) { | ||
const boxData = await scaledBox.array(); | ||
normalizedFace = { | ||
topLeft: boxData.slice(0, 2), | ||
bottomRight: boxData.slice(2) | ||
}; | ||
} | ||
else { | ||
const [landmarkData, boxData, probabilityData] = await Promise.all([face.landmarks, scaledBox, face.probability].map(async (d) => d.array())); | ||
const anchor = face.anchor; | ||
const [scaleFactorX, scaleFactorY] = scaleFactor; | ||
const scaledLandmarks = landmarkData | ||
.map(landmark => ([ | ||
(landmark[0] + anchor[0]) * scaleFactorX, | ||
(landmark[1] + anchor[1]) * scaleFactorY | ||
])); | ||
normalizedFace = { | ||
topLeft: boxData.slice(0, 2), | ||
bottomRight: boxData.slice(2), | ||
landmarks: scaledLandmarks, | ||
probability: probabilityData | ||
}; | ||
box_1.disposeBox(face.box); | ||
face.landmarks.dispose(); | ||
face.probability.dispose(); | ||
} | ||
scaledBox.dispose(); | ||
box_1.disposeBox(face.box); | ||
face.landmarks.dispose(); | ||
face.probability.dispose(); | ||
let normalizedFace = { | ||
topLeft: boxData.slice(0, 2), | ||
bottomRight: boxData.slice(2), | ||
landmarks: scaledLandmarks, | ||
probability: probabilityData | ||
}; | ||
if (flipHorizontal) { | ||
@@ -206,0 +254,0 @@ normalizedFace = flipFaceHorizontal(normalizedFace, width); |
@@ -9,2 +9,2 @@ import { BlazeFaceModel } from './face'; | ||
}): Promise<BlazeFaceModel>; | ||
export { NormalizedFace, BlazeFaceModel } from './face'; | ||
export { NormalizedFace, BlazeFaceModel, BlazeFacePrediction } from './face'; |
@@ -1,2 +0,2 @@ | ||
declare const version = "0.0.3"; | ||
declare const version = "0.0.4"; | ||
export { version }; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const version = '0.0.3'; | ||
const version = '0.0.4'; | ||
exports.version = version; | ||
//# sourceMappingURL=version.js.map |
{ | ||
"name": "@tensorflow-models/blazeface", | ||
"version": "0.0.3", | ||
"version": "0.0.4", | ||
"description": "Pretrained face detection model in TensorFlow.js", | ||
@@ -16,8 +16,8 @@ "main": "dist/index.js", | ||
"peerDependencies": { | ||
"@tensorflow/tfjs-core": "^1.3.2", | ||
"@tensorflow/tfjs-converter": "^1.3.2" | ||
"@tensorflow/tfjs-core": "^1.5.2", | ||
"@tensorflow/tfjs-converter": "^1.5.2" | ||
}, | ||
"devDependencies": { | ||
"@tensorflow/tfjs-core": "^1.3.2", | ||
"@tensorflow/tfjs-converter": "^1.3.2", | ||
"@tensorflow/tfjs-core": "^1.5.2", | ||
"@tensorflow/tfjs-converter": "^1.5.2", | ||
"@types/jasmine": "~2.5.53", | ||
@@ -24,0 +24,0 @@ "jasmine": "~3.2.0", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
780078
1551