Skip to content

Instantly share code, notes, and snippets.

@motsu0
Created February 20, 2023 12:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save motsu0/b86634b111f71f00bb1434341db2b6c2 to your computer and use it in GitHub Desktop.
Save motsu0/b86634b111f71f00bb1434341db2b6c2 to your computer and use it in GitHub Desktop.
.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;
}
<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>
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