├── squats.gif ├── chrome_amazon_squats.zip ├── tiny_face_detector_model-shard1 ├── README.md ├── styles.css ├── js ├── imageSelectionControls.js ├── bbt.js ├── faceDetectionControls.js └── commons.js ├── sample-form.html ├── tiny_face_detector_model-weights_manifest.json ├── index.html └── LICENSE /squats.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivirenremoto/squat_captcha/HEAD/squats.gif -------------------------------------------------------------------------------- /chrome_amazon_squats.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivirenremoto/squat_captcha/HEAD/chrome_amazon_squats.zip -------------------------------------------------------------------------------- /tiny_face_detector_model-shard1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivirenremoto/squat_captcha/HEAD/tiny_face_detector_model-shard1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # squat_captcha 2 | 3 | This captcha forces you to do 10 squats to continue. 4 | 5 | This demo works with chrome/firefox, desktop environment and also a webcam. 6 | 7 | ## Demo 8 | 9 | [https://vivirenremoto.github.io/squat_captcha/sample-form.html](https://vivirenremoto.github.io/squat_captcha/sample-form.html) 10 | 11 | ## Chrome extension 12 | 13 | [https://chrome.google.com/webstore/detail/squat-captcha/imflbohnilnolfogbakfmahmjmbejinm](https://chrome.google.com/webstore/detail/squat-captcha/imflbohnilnolfogbakfmahmjmbejinm) 14 | 15 | ## Information 16 | 17 | [https://dev.to/vivirenremoto/the-hatest-captcha-ever-squat-captcha-4bei](https://dev.to/vivirenremoto/the-hatest-captcha-ever-squat-captcha-4bei) 18 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .page-container { 2 | left: 0; 3 | right: 0; 4 | margin: auto; 5 | margin-top: 20px; 6 | padding-left: 280px; 7 | display: inline-flex !important; 8 | } 9 | 10 | @media only screen and (max-width : 992px) { 11 | .page-container { 12 | padding-left: 0; 13 | display: flex !important; 14 | } 15 | } 16 | 17 | #navbar { 18 | position: absolute; 19 | top: 20px; 20 | left: 20px; 21 | } 22 | 23 | .center-content { 24 | display: flex; 25 | flex-direction: column; 26 | justify-content: center; 27 | align-items: center; 28 | flex-wrap: wrap; 29 | } 30 | 31 | .side-by-side { 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | } 36 | .side-by-side >* { 37 | margin: 0 5px; 38 | } 39 | 40 | .bold { 41 | font-weight: bold; 42 | } 43 | 44 | .margin-sm { 45 | margin: 5px; 46 | } 47 | 48 | .margin { 49 | margin: 20px; 50 | } 51 | 52 | .button-sm { 53 | padding: 0 10px !important; 54 | } 55 | 56 | .pad-sides-sm { 57 | padding: 0 8px !important; 58 | } 59 | 60 | #github-link { 61 | display: flex !important; 62 | justify-content: center; 63 | align-items: center; 64 | border-bottom: 1px solid; 65 | margin-bottom: 10px; 66 | } 67 | 68 | #overlay, .overlay { 69 | position: absolute; 70 | top: 0; 71 | left: 0; 72 | } 73 | 74 | #facesContainer canvas { 75 | margin: 10px; 76 | } 77 | 78 | input[type="file"]::-webkit-file-upload-button { 79 | background: #26a69a; 80 | border: 1px solid gray; 81 | cursor: pointer; 82 | color: #fff; 83 | border-radius: .2em; 84 | } -------------------------------------------------------------------------------- /js/imageSelectionControls.js: -------------------------------------------------------------------------------- 1 | async function onSelectedImageChanged(uri) { 2 | const img = await faceapi.fetchImage(uri) 3 | $(`#inputImg`).get(0).src = img.src 4 | updateResults() 5 | } 6 | 7 | async function loadImageFromUrl(url) { 8 | const img = await requestExternalImage($('#imgUrlInput').val()) 9 | $('#inputImg').get(0).src = img.src 10 | updateResults() 11 | } 12 | 13 | async function loadImageFromUpload() { 14 | const imgFile = $('#queryImgUploadInput').get(0).files[0] 15 | const img = await faceapi.bufferToImage(imgFile) 16 | $('#inputImg').get(0).src = img.src 17 | updateResults() 18 | } 19 | 20 | function renderImageSelectList(selectListId, onChange, initialValue, withFaceExpressionImages) { 21 | let images = [1, 2, 3, 4, 5].map(idx => `bbt${idx}.jpg`) 22 | 23 | if (withFaceExpressionImages) { 24 | images = [ 25 | 'happy.jpg', 26 | 'sad.jpg', 27 | 'angry.jpg', 28 | 'disgusted.jpg', 29 | 'surprised.jpg', 30 | 'fearful.jpg', 31 | 'neutral.jpg' 32 | ].concat(images) 33 | } 34 | 35 | function renderChildren(select) { 36 | images.forEach(imageName => 37 | renderOption( 38 | select, 39 | imageName, 40 | imageName 41 | ) 42 | ) 43 | } 44 | 45 | renderSelectList( 46 | selectListId, 47 | onChange, 48 | initialValue, 49 | renderChildren 50 | ) 51 | } 52 | 53 | function initImageSelectionControls(initialValue = 'bbt1.jpg', withFaceExpressionImages = false) { 54 | renderImageSelectList( 55 | '#selectList', 56 | async (uri) => { 57 | await onSelectedImageChanged(uri) 58 | }, 59 | initialValue, 60 | withFaceExpressionImages 61 | ) 62 | onSelectedImageChanged($('#selectList select').val()) 63 | } -------------------------------------------------------------------------------- /js/bbt.js: -------------------------------------------------------------------------------- 1 | const classes = ['amy', 'bernadette', 'howard', 'leonard', 'penny', 'raj', 'sheldon', 'stuart'] 2 | 3 | function getFaceImageUri(className, idx) { 4 | return `${className}/${className}${idx}.png` 5 | } 6 | 7 | function renderFaceImageSelectList(selectListId, onChange, initialValue) { 8 | const indices = [1, 2, 3, 4, 5] 9 | function renderChildren(select) { 10 | classes.forEach(className => { 11 | const optgroup = document.createElement('optgroup') 12 | optgroup.label = className 13 | select.appendChild(optgroup) 14 | indices.forEach(imageIdx => 15 | renderOption( 16 | optgroup, 17 | `${className} ${imageIdx}`, 18 | getFaceImageUri(className, imageIdx) 19 | ) 20 | ) 21 | }) 22 | } 23 | 24 | renderSelectList( 25 | selectListId, 26 | onChange, 27 | getFaceImageUri(initialValue.className, initialValue.imageIdx), 28 | renderChildren 29 | ) 30 | } 31 | 32 | // fetch first image of each class and compute their descriptors 33 | async function createBbtFaceMatcher(numImagesForTraining = 1) { 34 | const maxAvailableImagesPerClass = 5 35 | numImagesForTraining = Math.min(numImagesForTraining, maxAvailableImagesPerClass) 36 | 37 | const labeledFaceDescriptors = await Promise.all(classes.map( 38 | async className => { 39 | const descriptors = [] 40 | for (let i = 1; i < (numImagesForTraining + 1); i++) { 41 | const img = await faceapi.fetchImage(getFaceImageUri(className, i)) 42 | descriptors.push(await faceapi.computeFaceDescriptor(img)) 43 | } 44 | 45 | return new faceapi.LabeledFaceDescriptors( 46 | className, 47 | descriptors 48 | ) 49 | } 50 | )) 51 | 52 | return new faceapi.FaceMatcher(labeledFaceDescriptors) 53 | } -------------------------------------------------------------------------------- /sample-form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |This demo works with chrome/firefox, desktop environment and also a webcam.
22 | 23 |Click the button to try this infamous captcha.
24 | 25 | 36 | 37 | 38 | 39 | 40 | 41 | 71 | 72 | 73 | 74 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /tiny_face_detector_model-weights_manifest.json: -------------------------------------------------------------------------------- 1 | [{"weights":[{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}],"paths":["tiny_face_detector_model-shard1"]}] -------------------------------------------------------------------------------- /js/faceDetectionControls.js: -------------------------------------------------------------------------------- 1 | const SSD_MOBILENETV1 = 'ssd_mobilenetv1' 2 | const TINY_FACE_DETECTOR = 'tiny_face_detector' 3 | 4 | 5 | let selectedFaceDetector = SSD_MOBILENETV1 6 | 7 | // ssd_mobilenetv1 options 8 | let minConfidence = 0.5 9 | 10 | // tiny_face_detector options 11 | let inputSize = 512 12 | let scoreThreshold = 0.5 13 | 14 | function getFaceDetectorOptions() { 15 | return selectedFaceDetector === SSD_MOBILENETV1 16 | ? new faceapi.SsdMobilenetv1Options({ minConfidence }) 17 | : new faceapi.TinyFaceDetectorOptions({ inputSize, scoreThreshold }) 18 | } 19 | 20 | function onIncreaseMinConfidence() { 21 | minConfidence = Math.min(faceapi.utils.round(minConfidence + 0.1), 1.0) 22 | $('#minConfidence').val(minConfidence) 23 | updateResults() 24 | } 25 | 26 | function onDecreaseMinConfidence() { 27 | minConfidence = Math.max(faceapi.utils.round(minConfidence - 0.1), 0.1) 28 | $('#minConfidence').val(minConfidence) 29 | updateResults() 30 | } 31 | 32 | function onInputSizeChanged(e) { 33 | changeInputSize(e.target.value) 34 | updateResults() 35 | } 36 | 37 | function changeInputSize(size) { 38 | inputSize = parseInt(size) 39 | 40 | const inputSizeSelect = $('#inputSize') 41 | inputSizeSelect.val(inputSize) 42 | inputSizeSelect.material_select() 43 | } 44 | 45 | function onIncreaseScoreThreshold() { 46 | scoreThreshold = Math.min(faceapi.utils.round(scoreThreshold + 0.1), 1.0) 47 | $('#scoreThreshold').val(scoreThreshold) 48 | updateResults() 49 | } 50 | 51 | function onDecreaseScoreThreshold() { 52 | scoreThreshold = Math.max(faceapi.utils.round(scoreThreshold - 0.1), 0.1) 53 | $('#scoreThreshold').val(scoreThreshold) 54 | updateResults() 55 | } 56 | 57 | function onIncreaseMinFaceSize() { 58 | minFaceSize = Math.min(faceapi.utils.round(minFaceSize + 20), 300) 59 | $('#minFaceSize').val(minFaceSize) 60 | } 61 | 62 | function onDecreaseMinFaceSize() { 63 | minFaceSize = Math.max(faceapi.utils.round(minFaceSize - 20), 50) 64 | $('#minFaceSize').val(minFaceSize) 65 | } 66 | 67 | function getCurrentFaceDetectionNet() { 68 | if (selectedFaceDetector === SSD_MOBILENETV1) { 69 | return faceapi.nets.ssdMobilenetv1 70 | } 71 | if (selectedFaceDetector === TINY_FACE_DETECTOR) { 72 | return faceapi.nets.tinyFaceDetector 73 | } 74 | } 75 | 76 | function isFaceDetectionModelLoaded() { 77 | return !!getCurrentFaceDetectionNet().params 78 | } 79 | 80 | async function changeFaceDetector(detector) { 81 | ['#ssd_mobilenetv1_controls', '#tiny_face_detector_controls'] 82 | .forEach(id => $(id).hide()) 83 | 84 | selectedFaceDetector = detector 85 | const faceDetectorSelect = $('#selectFaceDetector') 86 | faceDetectorSelect.val(detector) 87 | faceDetectorSelect.material_select() 88 | 89 | $('#loader').show() 90 | if (!isFaceDetectionModelLoaded()) { 91 | await getCurrentFaceDetectionNet().load('/') 92 | } 93 | 94 | $(`#${detector}_controls`).show() 95 | $('#loader').hide() 96 | 97 | } 98 | 99 | async function onSelectedFaceDetectorChanged(e) { 100 | selectedFaceDetector = e.target.value 101 | 102 | await changeFaceDetector(e.target.value) 103 | updateResults() 104 | } 105 | 106 | function initFaceDetectionControls() { 107 | const faceDetectorSelect = $('#selectFaceDetector') 108 | faceDetectorSelect.val(selectedFaceDetector) 109 | faceDetectorSelect.on('change', onSelectedFaceDetectorChanged) 110 | faceDetectorSelect.material_select() 111 | 112 | const inputSizeSelect = $('#inputSize') 113 | inputSizeSelect.val(inputSize) 114 | inputSizeSelect.on('change', onInputSizeChanged) 115 | inputSizeSelect.material_select() 116 | } -------------------------------------------------------------------------------- /js/commons.js: -------------------------------------------------------------------------------- 1 | async function requestExternalImage(imageUrl) { 2 | const res = await fetch('fetch_external_image', { 3 | method: 'post', 4 | headers: { 5 | 'content-type': 'application/json' 6 | }, 7 | body: JSON.stringify({ imageUrl }) 8 | }) 9 | if (!(res.status < 400)) { 10 | console.error(res.status + ' : ' + await res.text()) 11 | throw new Error('failed to fetch image from url: ' + imageUrl) 12 | } 13 | 14 | let blob 15 | try { 16 | blob = await res.blob() 17 | return await faceapi.bufferToImage(blob) 18 | } catch (e) { 19 | console.error('received blob:', blob) 20 | console.error('error:', e) 21 | throw new Error('failed to load image from url: ' + imageUrl) 22 | } 23 | } 24 | 25 | function renderNavBar(navbarId, exampleUri) { 26 | const examples = [ 27 | { 28 | uri: 'face_detection', 29 | name: 'Face Detection' 30 | }, 31 | { 32 | uri: 'face_landmark_detection', 33 | name: 'Face Landmark Detection' 34 | }, 35 | { 36 | uri: 'face_expression_recognition', 37 | name: 'Face Expression Recognition' 38 | }, 39 | { 40 | uri: 'age_and_gender_recognition', 41 | name: 'Age and Gender Recognition' 42 | }, 43 | { 44 | uri: 'face_recognition', 45 | name: 'Face Recognition' 46 | }, 47 | { 48 | uri: 'face_extraction', 49 | name: 'Face Extraction' 50 | }, 51 | { 52 | uri: 'video_face_tracking', 53 | name: 'Video Face Tracking' 54 | }, 55 | { 56 | uri: 'webcam_face_detection', 57 | name: 'Webcam Face Detection' 58 | }, 59 | { 60 | uri: 'webcam_face_landmark_detection', 61 | name: 'Webcam Face Landmark Detection' 62 | }, 63 | { 64 | uri: 'webcam_face_expression_recognition', 65 | name: 'Webcam Face Expression Recognition' 66 | }, 67 | { 68 | uri: 'webcam_age_and_gender_recognition', 69 | name: 'Webcam Age and Gender Recognition' 70 | }, 71 | { 72 | uri: 'bbt_face_landmark_detection', 73 | name: 'BBT Face Landmark Detection' 74 | }, 75 | { 76 | uri: 'bbt_face_similarity', 77 | name: 'BBT Face Similarity' 78 | }, 79 | { 80 | uri: 'bbt_face_matching', 81 | name: 'BBT Face Matching' 82 | }, 83 | { 84 | uri: 'bbt_face_recognition', 85 | name: 'BBT Face Recognition' 86 | }, 87 | { 88 | uri: 'batch_face_landmarks', 89 | name: 'Batch Face Landmark Detection' 90 | }, 91 | { 92 | uri: 'batch_face_recognition', 93 | name: 'Batch Face Recognition' 94 | } 95 | ] 96 | 97 | const navbar = $(navbarId).get(0) 98 | const pageContainer = $('.page-container').get(0) 99 | 100 | const header = document.createElement('h3') 101 | header.innerHTML = examples.find(ex => ex.uri === exampleUri).name 102 | pageContainer.insertBefore(header, pageContainer.children[0]) 103 | 104 | const menuContent = document.createElement('ul') 105 | menuContent.id = 'slide-out' 106 | menuContent.classList.add('side-nav', 'fixed') 107 | navbar.appendChild(menuContent) 108 | 109 | const menuButton = document.createElement('a') 110 | menuButton.href='#' 111 | menuButton.classList.add('button-collapse', 'show-on-large') 112 | menuButton.setAttribute('data-activates', 'slide-out') 113 | const menuButtonIcon = document.createElement('img') 114 | menuButtonIcon.src = 'menu_icon.png' 115 | menuButton.appendChild(menuButtonIcon) 116 | navbar.appendChild(menuButton) 117 | 118 | const li = document.createElement('li') 119 | const githubLink = document.createElement('a') 120 | githubLink.classList.add('waves-effect', 'waves-light', 'side-by-side') 121 | githubLink.id = 'github-link' 122 | githubLink.href = 'https://github.com/justadudewhohacks/face-api.js' 123 | const h5 = document.createElement('h5') 124 | h5.innerHTML = 'face-api.js' 125 | githubLink.appendChild(h5) 126 | const githubLinkIcon = document.createElement('img') 127 | githubLinkIcon.src = 'github_link_icon.png' 128 | githubLink.appendChild(githubLinkIcon) 129 | li.appendChild(githubLink) 130 | menuContent.appendChild(li) 131 | 132 | examples 133 | .forEach(ex => { 134 | const li = document.createElement('li') 135 | if (ex.uri === exampleUri) { 136 | li.style.background='#b0b0b0' 137 | } 138 | const a = document.createElement('a') 139 | a.classList.add('waves-effect', 'waves-light', 'pad-sides-sm') 140 | a.href = ex.uri 141 | const span = document.createElement('span') 142 | span.innerHTML = ex.name 143 | span.style.whiteSpace = 'nowrap' 144 | a.appendChild(span) 145 | li.appendChild(a) 146 | menuContent.appendChild(li) 147 | }) 148 | 149 | $('.button-collapse').sideNav({ 150 | menuWidth: 260 151 | }) 152 | } 153 | 154 | function renderSelectList(selectListId, onChange, initialValue, renderChildren) { 155 | const select = document.createElement('select') 156 | $(selectListId).get(0).appendChild(select) 157 | renderChildren(select) 158 | $(select).val(initialValue) 159 | $(select).on('change', (e) => onChange(e.target.value)) 160 | $(select).material_select() 161 | } 162 | 163 | function renderOption(parent, text, value) { 164 | const option = document.createElement('option') 165 | option.innerHTML = text 166 | option.value = value 167 | parent.appendChild(option) 168 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
110 |