Created
February 20, 2023 12:26
-
-
Save motsu0/b86634b111f71f00bb1434341db2b6c2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.area-file{ | |
margin: 12px 0; | |
text-align: center; | |
} | |
#message{ | |
margin: 12px 0; | |
text-align: center; | |
color: #e00000; | |
} | |
/* */ | |
.area-canvas{ | |
text-align: center; | |
} | |
#canvas-outer{ | |
display: inline-flex; | |
position: relative; | |
} | |
#canvas{ | |
width: 100%; | |
} | |
.face-box{ | |
box-sizing: border-box; | |
position: absolute; | |
border: 2px solid #e00000; | |
cursor: pointer; | |
} | |
.face-box:hover{ | |
background-color: rgba(255,255,255,.3); | |
} | |
.face-box.s-disable{ | |
border-color: rgba(0,0,255,.7); | |
} | |
/* */ | |
.area-tag-kind{ | |
display: flex; | |
justify-content: center; | |
column-gap: 8px; | |
} | |
.tag-kind{ | |
padding: 0 4px; | |
} | |
.tag-kind--enable{ | |
border: 1px solid #e00000; | |
color: #e00000; | |
} | |
.tag-kind--disable{ | |
border: 1px solid rgba(0,0,255,.7); | |
color: rgba(0,0,255,.7); | |
} | |
.area-time{ | |
text-align: right; | |
font-size: .9rem; | |
} | |
/* */ | |
.control{ | |
margin: 12px 0; | |
} | |
.control__row{ | |
padding: 12px; | |
border: 1px dashed #777; | |
} | |
.control__row:nth-of-type(n+2){ | |
border-top: none; | |
} | |
.control__label{ | |
padding-left: 8px; | |
margin-bottom: 8px; | |
font-weight: bold; | |
} | |
.area-bt{ | |
display: flex; | |
flex-wrap: wrap; | |
gap: 12px; | |
} | |
.label-cb-png{ | |
display: inline-block; | |
margin-top: 8px; | |
cursor: pointer; | |
} | |
#cb-png{ | |
transform: scale(1.2) translateY(-.07em); | |
margin-left: 4px; | |
margin-right: 8px; | |
vertical-align: middle; | |
cursor: pointer; | |
} | |
/* */ | |
.bt-normal{ | |
padding: 4px 8px; | |
cursor: pointer; | |
} | |
/* */ | |
.s-hide{ | |
display: none; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="area-file"> | |
<input type="file" id="input-file"> | |
</div> | |
<img src="" alt="" id="img-main" class="s-hide"> | |
<div id="message" class="s-hide"></div> | |
<div class="area-canvas"> | |
<div id="canvas-outer"> | |
<canvas id="canvas" width="0" height="0"></canvas> | |
</div> | |
<div class="area-tag-kind"> | |
<div class="tag-kind tag-kind--enable">対象の顔</div> | |
<div class="tag-kind tag-kind--disable">対象外の顔</div> | |
</div> | |
</div> | |
<div class="area-time"> | |
検出時間:<span id="time-detect">0</span>ms | |
</div> | |
<div class="control"> | |
<div class="control__row"> | |
<div class="control__label">効果</div> | |
<div class="area-bt"> | |
<button id="bt-mosaic" class="bt-normal">モザイク</button> | |
<button id="bt-blur-simple" class="bt-normal">簡易ぼかし</button> | |
<button id="bt-blur" class="bt-normal">ぼかし</button> | |
<button id="bt-eyeline" class="bt-normal">目線</button> | |
</div> | |
</div> | |
<div class="control__row"> | |
<div class="control__label">リセット</div> | |
<div class="area-bt"> | |
<button id="bt-reset" class="bt-normal">加工をリセット</button> | |
<button id="bt-detect" class="bt-normal">顔を再検出</button> | |
</div> | |
</div> | |
<div class="control__row"> | |
<div class="control__label">保存</div> | |
<div class="area-bt"> | |
<button id="bt-download" class="bt-normal">画像を保存</button> | |
</div> | |
<label class="label-cb-png"> | |
<input type="checkbox" id="cb-png">高画質で保存 | |
</label> | |
</div> | |
<div class="control__row"> | |
<div class="control__label">テスト用</div> | |
<div class="area-bt"> | |
<button id="bt-sample" class="bt-normal">サンプル画像を読み込む</button> | |
</div> | |
</div> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const nowloading = new nowLoading(); | |
nowloading.start(); | |
const src_model = 'pathto/face-api/models/'; | |
const src_sample = 'pathto/sample08.jpg'; | |
const el_input_file = document.getElementById('input-file'); | |
const el_message = document.getElementById('message'); | |
const el_img_main = document.getElementById('img-main'); | |
const el_canvas_outer = document.getElementById('canvas-outer'); | |
const el_canvas = document.getElementById('canvas'); | |
const ctx = el_canvas.getContext('2d',{willReadFrequently: true}); | |
const els_face_box = document.getElementsByClassName('face-box'); | |
const el_time_detect = document.getElementById('time-detect'); | |
const bt_blur_simple = document.getElementById('bt-blur-simple'); | |
const bt_blur = document.getElementById('bt-blur'); | |
const bt_mosaic = document.getElementById('bt-mosaic'); | |
const bt_eyeline = document.getElementById('bt-eyeline'); | |
const bt_reset = document.getElementById('bt-reset'); | |
const bt_detect = document.getElementById('bt-detect'); | |
const bt_download = document.getElementById('bt-download'); | |
const el_cb_png = document.getElementById('cb-png'); | |
const bt_sample = document.getElementById('bt-sample'); | |
let array_output_box = []; | |
let array_output_landmarks = []; | |
let array_is_facebox; | |
init(); | |
function init(){ | |
el_input_file.addEventListener('change',fileCheck); | |
// | |
bt_blur_simple.addEventListener('click',()=>{ | |
nowloading.start(); | |
setTimeout(()=>{ | |
effectBlurSimple(); | |
nowloading.stop(); | |
},1); | |
}); | |
bt_blur.addEventListener('click',()=>{ | |
nowloading.start(); | |
setTimeout(()=>{ | |
effectBlur(); | |
nowloading.stop(); | |
},1); | |
}); | |
bt_mosaic.addEventListener('click',()=>{ | |
nowloading.start(); | |
setTimeout(()=>{ | |
effectMosaic(); | |
nowloading.stop(); | |
},1); | |
}); | |
bt_eyeline.addEventListener('click',()=>{ | |
nowloading.start(); | |
setTimeout(()=>{ | |
effectEyeline(); | |
nowloading.stop(); | |
},1); | |
}); | |
// | |
bt_reset.addEventListener('click',()=>{ | |
ctx.clearRect(0,0,el_canvas.width,el_canvas.height); | |
ctx.drawImage(el_img_main, 0, 0, el_canvas.width, el_canvas.height); | |
}); | |
bt_detect.addEventListener('click',()=>{ | |
nowloading.start(); | |
setTimeout(()=>{ | |
faceDetect().then(()=>{ | |
nowloading.stop(); | |
}) | |
},1); | |
}); | |
bt_download.addEventListener('click',canvasDownload); | |
bt_sample.addEventListener('click',()=>{ | |
nowloading.start(); | |
bt_download.disabled = true; | |
el_img_main.src = src_sample; | |
}); | |
// | |
el_img_main.addEventListener('load',()=>{ | |
canvasInit(); | |
faceDetect().then(()=>{ | |
nowloading.stop(); | |
}) | |
}); | |
Promise.all([ | |
faceapi.loadSsdMobilenetv1Model(src_model), | |
faceapi.loadFaceLandmarkModel(src_model) | |
]).then(()=>{ | |
console.log('model loaded'); | |
nowloading.stop(); | |
}); | |
} | |
function fileCheck(e){ | |
const files = e.target.files; | |
if(files.length===0) return; | |
nowloading.start(); | |
const file = files[0]; | |
e.target.value = ''; | |
el_message.classList.add('s-hide'); | |
if(!file.type.includes('image')){ | |
el_message.textContent = '画像を選択して下さい。' | |
el_message.classList.remove('s-hide'); | |
nowloading.stop(); | |
return; | |
} | |
bt_download.disabled = false; | |
const reader = new FileReader(); | |
reader.onload = ev=>{ | |
el_img_main.src = ev.target.result; | |
} | |
reader.readAsDataURL(file); | |
} | |
function canvasInit(){ | |
//draw | |
const rate_canvas = (()=>{ | |
const r_w = 16000/el_img_main.naturalWidth; | |
const r_h = 16000/el_img_main.naturalHeight; | |
return Math.min(r_w,r_h,1); | |
})(); | |
el_canvas.width = el_img_main.naturalWidth * rate_canvas; | |
el_canvas.height = el_img_main.naturalHeight * rate_canvas; | |
ctx.clearRect(0,0,el_canvas.width,el_canvas.height); | |
ctx.drawImage(el_img_main, 0, 0, el_canvas.width, el_canvas.height); | |
} | |
async function faceDetect(){ | |
if(el_canvas.width===0||el_canvas.height===0) return; | |
//初期化 | |
array_output_box = []; | |
array_output_landmarks = []; | |
array_is_facebox = []; | |
[...els_face_box].forEach(el=>{ | |
el.remove(); | |
}); | |
el_message.classList.add('s-hide'); | |
//顔検出 | |
const time_start = performance.now(); | |
const results = await faceapi.detectAllFaces(el_canvas).withFaceLandmarks(); | |
const time_end = performance.now(); | |
const rate = el_canvas.clientWidth / el_canvas.width; | |
results.forEach((result,i)=>{ | |
//box | |
const box = result.detection._box; | |
const obj_box = { | |
x: box._x, | |
y: box._y, | |
width: box._width, | |
height: box._height | |
}; | |
array_output_box.push(obj_box); | |
//preview box | |
const el_face_box = document.createElement('box'); | |
el_face_box.classList.add('face-box'); | |
el_face_box.style.top = (box._y * rate) + 'px'; | |
el_face_box.style.left = (box._x * rate) + 'px'; | |
el_face_box.style.width = (box._width * rate) + 'px'; | |
el_face_box.style.height = (box._height * rate) + 'px'; | |
el_face_box.addEventListener('click',()=>{ | |
el_face_box.classList.toggle('s-disable'); | |
array_is_facebox[i] = !array_is_facebox[i]; | |
}); | |
el_canvas_outer.appendChild(el_face_box); | |
//ランドマーク | |
const position = result.landmarks._positions; | |
array_output_landmarks.push(position); | |
}); | |
//後処理 | |
array_is_facebox = (new Array(results.length)).fill(true); | |
//検出時間 | |
el_time_detect.textContent = Math.round(time_end-time_start); | |
//対象0 | |
if(results.length===0){ | |
el_message.textContent = '顔が検出できませんでした。' | |
el_message.classList.remove('s-hide'); | |
} | |
} | |
function effectBlurSimple(){ | |
if(array_output_box.length===0) return; | |
ctx.clearRect(0,0,el_canvas.width,el_canvas.height); | |
ctx.drawImage(el_img_main, 0, 0, el_canvas.width, el_canvas.height); | |
array_output_box.forEach((box,i)=>{ | |
if(!array_is_facebox[i]) return; | |
const canvas_mini = document.createElement('canvas'); | |
canvas_mini.width = 8; | |
const rate_mini = box.width / 8; | |
canvas_mini.height = box.height / rate_mini; | |
const ctx_mini = canvas_mini.getContext('2d'); | |
ctx_mini.drawImage(el_canvas, box.x, box.y, box.width, box.height, 0, 0, canvas_mini.width, canvas_mini.height); | |
ctx.drawImage(canvas_mini, box.x, box.y, box.width, box.height); | |
}); | |
} | |
function effectBlur(){ //平均化フィルタ | |
if(array_output_box.length===0) return; | |
ctx.clearRect(0,0,el_canvas.width,el_canvas.height); | |
ctx.drawImage(el_img_main, 0, 0, el_canvas.width, el_canvas.height); | |
array_output_box.forEach((box,i)=>{ | |
if(!array_is_facebox[i]) return; | |
const box_width = Math.round(box.width); | |
const box_height = Math.round(box.height); | |
const r_blur = Math.round(box.width/8); | |
const image_data = ctx.getImageData(box.x, box.y, box_width, box_height); | |
const array_pixel_old = (new Array(box_height)).fill().map(()=>new Array(box_width)); | |
const array_pixel_sum = (new Array(box_height)).fill().map(()=>(new Array(box_width)).fill().map(()=>( | |
{ | |
num: 0, | |
sum_r: 0, | |
sum_g: 0, | |
sum_b: 0, | |
sum_a: 0 | |
} | |
))); | |
for(let r=0;r<box_height;r++){ | |
for(let c=0;c<box_width;c++){ | |
array_pixel_old[r][c] = { | |
r: image_data.data[(r*box_width+c)*4+0], | |
g: image_data.data[(r*box_width+c)*4+1], | |
b: image_data.data[(r*box_width+c)*4+2], | |
a: image_data.data[(r*box_width+c)*4+3] | |
}; | |
} | |
} | |
//左上の1個目計算 | |
for(let y=0;y<=r_blur;y++){ | |
for(let x=0;x<=r_blur;x++){ | |
const pixel = array_pixel_old[y][x]; | |
array_pixel_sum[0][0].num++; | |
array_pixel_sum[0][0].sum_r += pixel.r; | |
array_pixel_sum[0][0].sum_g += pixel.g; | |
array_pixel_sum[0][0].sum_b += pixel.b; | |
array_pixel_sum[0][0].sum_a += pixel.a; | |
} | |
} | |
//1行目2個目以降計算 | |
for(let c=1;c<box_width;c++){ | |
const base = {}; | |
Object.assign(base, array_pixel_sum[0][c-1]); | |
for(let y=0;y<=r_blur;y++){ | |
//プラス列 | |
if(array_pixel_old[0+y]!==undefined){ | |
const pixel_plus = array_pixel_old[0+y][c+r_blur]; | |
if(pixel_plus!==undefined){ | |
base.num++; | |
base.sum_r += pixel_plus.r; | |
base.sum_g += pixel_plus.g; | |
base.sum_b += pixel_plus.b; | |
base.sum_a += pixel_plus.a; | |
} | |
} | |
//マイナス列 | |
if(array_pixel_old[0+y]!==undefined){ | |
const pixel_minus = array_pixel_old[0+y][c-1-r_blur]; | |
if(pixel_minus!==undefined){ | |
base.num--; | |
base.sum_r -= pixel_minus.r; | |
base.sum_g -= pixel_minus.g; | |
base.sum_b -= pixel_minus.b; | |
base.sum_a -= pixel_minus.a; | |
} | |
} | |
} | |
array_pixel_sum[0][c] = base; | |
} | |
//2行目以降 | |
for(let r=1;r<box_height;r++){ | |
for(let c=0;c<box_width;c++){ | |
const base = {}; | |
Object.assign(base, array_pixel_sum[r-1][c]); | |
//プラス行 | |
for(let x=-r_blur;x<=r_blur;x++){ | |
//プラス行 | |
if(array_pixel_old[r+r_blur]!==undefined){ | |
const pixel_plus = array_pixel_old[r+r_blur][c+x]; | |
if(pixel_plus!==undefined){ | |
base.num++; | |
base.sum_r += pixel_plus.r; | |
base.sum_g += pixel_plus.g; | |
base.sum_b += pixel_plus.b; | |
base.sum_a += pixel_plus.a; | |
} | |
} | |
//マイナス行 | |
if(array_pixel_old[r-1-r_blur]!==undefined){ | |
const pixel_minus = array_pixel_old[r-1-r_blur][c+x]; | |
if(pixel_minus!==undefined){ | |
base.num--; | |
base.sum_r -= pixel_minus.r; | |
base.sum_g -= pixel_minus.g; | |
base.sum_b -= pixel_minus.b; | |
base.sum_a -= pixel_minus.a; | |
} | |
} | |
} | |
array_pixel_sum[r][c] = base; | |
} | |
} | |
//書き換え | |
for(let r=0;r<box_height;r++){ | |
for(let c=0;c<box_width;c++){ | |
const data = array_pixel_sum[r][c]; | |
const r_avg = data.sum_r / data.num; | |
const g_avg = data.sum_g / data.num; | |
const b_avg = data.sum_b / data.num; | |
const a_avg = data.sum_a / data.num; | |
image_data.data[(r*box_width+c)*4+0] = r_avg; | |
image_data.data[(r*box_width+c)*4+1] = g_avg; | |
image_data.data[(r*box_width+c)*4+2] = b_avg; | |
image_data.data[(r*box_width+c)*4+3] = a_avg; | |
} | |
} | |
ctx.putImageData(image_data,box.x,box.y); | |
}); | |
} | |
function effectMosaic(){ | |
if(array_output_box.length===0) return; | |
ctx.clearRect(0,0,el_canvas.width,el_canvas.height); | |
ctx.drawImage(el_img_main, 0, 0, el_canvas.width, el_canvas.height); | |
array_output_box.forEach((box,i)=>{ | |
if(!array_is_facebox[i]) return; | |
const box_width = Math.round(box.width); | |
const box_height = Math.round(box.height); | |
const el_canvas_mini = document.createElement('canvas'); | |
el_canvas_mini.width = box_width; | |
el_canvas_mini.height = box_height; | |
const ctx_mini = el_canvas_mini.getContext('2d',{willReadFrequently: true}); | |
ctx_mini.drawImage(el_canvas, box.x, box.y, box_width, box_height, 0, 0, box_width, box_height); | |
const step_x = 8; | |
const length_unit = Math.ceil(box_width/step_x); | |
const image_data = ctx_mini.getImageData(0, 0, box_width, box_height); | |
const array_pixel = (new Array(box_height)).fill(0).map(v=>new Array(box_width)); | |
for(let r=0;r<box_height;r++){ | |
for(let c=0;c<box_width;c++){ | |
array_pixel[r][c] = { | |
r: image_data.data[(r*box_width+c)*4+0], | |
g: image_data.data[(r*box_width+c)*4+1], | |
b: image_data.data[(r*box_width+c)*4+2], | |
a: image_data.data[(r*box_width+c)*4+3] | |
}; | |
} | |
} | |
for(let r=0;r<box_height;r+=length_unit){ | |
for(let c=0;c<box_width;c+=length_unit){ | |
const array_r = []; | |
const array_g = []; | |
const array_b = []; | |
const array_a = []; | |
for(let y=0;y<length_unit;y++){ | |
for(let x=0;x<length_unit;x++){ | |
if(array_pixel[r+y]===undefined) continue; | |
const pixel = array_pixel[r+y][c+x]; | |
if(pixel===undefined) continue; | |
array_r.push(pixel.r); | |
array_g.push(pixel.g); | |
array_b.push(pixel.b); | |
array_a.push(pixel.a); | |
} | |
} | |
const num_pixel = array_r.length; | |
const r_avg = array_r.reduce((p,c)=>p+c) / num_pixel; | |
const g_avg = array_g.reduce((p,c)=>p+c) / num_pixel; | |
const b_avg = array_b.reduce((p,c)=>p+c) / num_pixel; | |
const a_avg = array_a.reduce((p,c)=>p+c) / num_pixel; | |
ctx_mini.fillStyle = `rgba(${r_avg},${g_avg},${b_avg},${a_avg})`; | |
ctx_mini.fillRect(c, r, length_unit, length_unit); | |
} | |
} | |
ctx.drawImage(el_canvas_mini, box.x, box.y, box_width, box_height); | |
}); | |
} | |
function effectEyeline(){ | |
if(array_output_landmarks.length===0) return; | |
ctx.clearRect(0,0,el_canvas.width,el_canvas.height); | |
ctx.drawImage(el_img_main, 0, 0, el_canvas.width, el_canvas.height); | |
const array_index = [37,38,39,40,41,42,43,44,46,47]; | |
array_output_landmarks.forEach((position,i_p)=>{ | |
if(!array_is_facebox[i_p]) return; | |
const x1 = position[36]._x; | |
const y1 = position[36]._y; | |
const x2 = position[45]._x; | |
const y2 = position[45]._y; | |
const px_extension = Math.sqrt(((x2-x1)**2) + ((y2-y1)**2)) * .2; | |
//通常処理 | |
if(x1!==x2){ | |
const l_a = (y2-y1)/(x2-x1); | |
const l_b = -1; | |
const l_c = (-(y2-y1)/(x2-x1))*x1 + y1; | |
const radian = Math.atan(l_a); | |
const x_start = x1 - (px_extension * Math.cos(radian)); | |
const y_start = y1 - (px_extension * Math.sin(radian)); | |
const x_end = x2 + (px_extension * Math.cos(radian)); | |
const y_end = y2 + (px_extension * Math.sin(radian)); | |
let max_dist = 0; | |
array_index.forEach(i=>{ | |
const x3 = position[i]._x; | |
const y3 = position[i]._y; | |
const dist = Math.abs(l_a*x3+l_b*y3+l_c) / Math.sqrt((l_a**2) + (l_b**2)); | |
max_dist = Math.max(max_dist,dist); | |
}); | |
ctx.lineWidth = max_dist*4; | |
ctx.strokeStyle = 'rgb(0,0,0)'; | |
ctx.beginPath(); | |
ctx.moveTo(x_start,y_start); | |
ctx.lineTo(x_end,y_end); | |
ctx.stroke(); | |
} | |
//x1===x2の場合 | |
else{ | |
const x_start = x1; | |
const y_start = Math.min(y1,y2) - px_extension; | |
const x_end = x2; | |
const y_end = Math.max(y1,y2) + px_extension; | |
let max_dist = 0; | |
array_index.forEach(i=>{ | |
const x3 = position[i]._x; | |
const dist = Math.abs(x1-x3); | |
max_dist = Math.max(max_dist,dist); | |
}); | |
ctx.lineWidth = max_dist*4; | |
ctx.strokeStyle = 'rgb(0,0,0)'; | |
ctx.beginPath(); | |
ctx.moveTo(x_start,y_start); | |
ctx.lineTo(x_end,y_end); | |
ctx.stroke(); | |
} | |
}); | |
} | |
function canvasDownload(){ | |
if(el_canvas.width===0||el_canvas.height===0) return; | |
nowloading.start(); | |
if(el_cb_png.checked){ | |
el_canvas.toBlob(blob=>{ | |
const el_a = document.createElement('a'); | |
el_a.download = 'output.png'; | |
el_a.href = URL.createObjectURL(blob); | |
el_a.click(); | |
URL.revokeObjectURL(blob); | |
nowloading.stop(); | |
}, 'image/png'); | |
}else{ | |
el_canvas.toBlob(blob=>{ | |
const el_a = document.createElement('a'); | |
el_a.download = 'output.jpg'; | |
el_a.href = URL.createObjectURL(blob); | |
el_a.click(); | |
URL.revokeObjectURL(blob); | |
nowloading.stop(); | |
}, 'image/jpg', .9); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment