img-edge / index.html
soiz1's picture
Update index.html
c88e5fd verified
<!DOCTYPE html>
<html lang=ja>
<head>
<meta charset=UTF-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>画像変換ツール</title>
<style>
body{font-family:Arial,sans-serif;margin:0;padding:20px;background-color:#f5f5f5}
.container{max-width:900px;margin:0 auto;background-color:#fff;padding:20px;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.1)}
h1,h2{color:#333}
.mode-selector{display:flex;margin-bottom:20px;flex-wrap:wrap}
.mode-selector button{flex:1;min-width:120px;padding:10px;border:none;background-color:#eee;cursor:pointer;margin:2px}
.mode-selector button.active{background-color:#4caf50;color:#fff}
.file-input{margin-bottom:20px}
.file-input input[type=file]{display:none}
.file-input label{display:inline-block;padding:10px 15px;background-color:#4caf50;color:#fff;border-radius:4px;cursor:pointer}
.settings{margin-bottom:20px;border:1px solid #ddd;padding:15px;border-radius:4px}
.settings-panel{display:none}
.settings-panel.active{display:block}
.setting-item{margin-bottom:15px}
.setting-item label{display:block;margin-bottom:5px;font-weight:700}
button{padding:10px 15px;background-color:#4caf50;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:16px}
button:disabled{background-color:#ccc;cursor:not-allowed}
.result-container{margin-top:30px}
.image-preview{display:flex;justify-content:space-between;margin:20px 0;flex-wrap:wrap}
.canvas-container{width:48%;margin-bottom:10px;position:relative}
canvas{max-width:100%;border:1px solid #ddd;box-shadow:0 2px 5px rgba(0,0,0,.1);background-color:#fff}
.image-info{font-size:14px;color:#666;margin-top:5px}
#downloadBtn{margin-top:10px}
.loading{display:none;margin:20px 0;padding:10px;background-color:#f8f8f8;border:1px solid #ddd;border-radius:4px;text-align:center}
.result-options{margin-top:10px}
.result-options button{margin-right:10px;background-color:#2196f3}
.result-options button.download-depth{background-color:#9c27b0}
.result-options button.download-semi{background-color:#ff9800}
.format-selector{margin-top:15px}
.depth-image-container{margin-top:20px;border-top:1px dashed #ddd;padding-top:20px}
</style>
</head>
<body>
<div class=container>
<h1>画像変換ツール</h1>
<div class=mode-selector>
<button id=lineDrawingMode class=active>線画モード</button>
<button id=blackWhiteMode>白黒画像モード</button>
<button id=bgRemovalMode>背景削除</button>
<button id=upscaleMode>高画質化</button>
</div>
<div class=file-input>
<input type=file id=imageUpload accept=image/*>
<label for=imageUpload>画像を選択</label>
</div>
<div class=settings>
<div id=lineDrawingSettings class="settings-panel active">
<h2>線画設定</h2>
<div class=setting-item>
<label for=lineThickness>線の太さ</label>
<input type=range id=lineThickness min=1 max=20 value=2>
<span id=lineThicknessValue>2</span>
</div>
<div class=setting-item>
<label for=lineColor>線の色</label>
<input type=color id=lineColor value=#000000>
</div>
<div class=setting-item>
<label for=bgColor>背景色</label>
<input type=color id=bgColor value=#ffffff>
</div>
<div class=setting-item>
<label for=bgOpacity>背景の透明度</label>
<input type=range id=bgOpacity min=0 max=100 value=100>
<span id=bgOpacityValue>100%</span>
</div>
<div class=setting-item>
<label for=sensitivity>感度</label>
<input type=range id=sensitivity min=1 max=100 value=50>
<span id=sensitivityValue>50</span>
</div>
</div>
<div id=blackWhiteSettings class=settings-panel>
<h2>白黒画像設定</h2>
<div class=setting-item>
<label for=bwBgColor>背景色</label>
<input type=color id=bwBgColor value=#ffffff>
</div>
<div class=setting-item>
<label for=bwFgColor>前景色</label>
<input type=color id=bwFgColor value=#000000>
</div>
<div class=setting-item>
<label for=threshold>明るさの閾値</label>
<input type=range id=threshold min=0 max=100 value=50>
<span id=thresholdValue>50%</span>
</div>
</div>
<div id=bgRemovalSettings class=settings-panel>
<h2>背景削除設定</h2>
<div class=setting-item>
<p>背景を削除した画像が生成されます</p>
</div>
</div>
<div id=upscaleSettings class=settings-panel>
<h2>高画質化設定</h2>
<div class=setting-item>
<label for=version>拡大モデル</label>
<select id=version name=version>
<option value=v1.2>GFPGANv1.2(顔の拡大に特化)</option>
<option value=v1.3>GFPGANv1.3(顔の拡大に特化)</option>
<option value=v1.4>GFPGANv1.4(顔の拡大に特化)</option>
<option value=RestoreFormer selected>RestoreFormer</option>
<option value=CodeFormer>CodeFormer</option>
<option value=RealESR-General-x4v3>RealESR-General-x4v3</option>
</select>
</div>
<div class=setting-item>
<label for=scale>拡大率</label>
<input type=range id=scale min=1 max=4 value=2 step=1>
<span id=scaleValue>2x</span>
</div>
</div>
</div>
<div class=loading id=loadingIndicator>
<p>処理中です...しばらくお待ちください</p>
</div>
<div class=format-selector>
<label for=outputFormat>出力フォーマット: </label>
<select id=outputFormat>
<option value=png selected>PNG</option>
<option value=jpeg>JPEG</option>
<option value=webp>WebP</option>
</select>
<label for=jpegQuality style="display:none" id=jpegQualityLabel>品質: </label>
<input type=range id=jpegQuality min=1 max=100 value=90 style="display:none;width:100px">
<span id=jpegQualityValue style="display:none">90%</span>
</div>
<button id=processBtn disabled>変換実行</button>
<div class=result-container>
<h2>結果</h2>
<div class=image-preview>
<div class=canvas-container>
<h3>元画像</h3>
<canvas id=originalCanvas></canvas>
<div class=image-info id=originalInfo></div>
</div>
<div class=canvas-container>
<h3>結果画像</h3>
<canvas id=resultCanvas></canvas>
<div class=image-info id=resultInfo></div>
</div>
</div>
<div class=depth-image-container id=depthImageContainer style=display:none>
<h3>深度画像</h3>
<canvas id=depthCanvas></canvas>
<div class=image-info id=depthInfo></div>
</div>
<div class=result-options id=resultOptions style=display:none>
<button id=downloadBtn>結果をダウンロード</button>
<button id=downloadDepthBtn class=download-depth>深度画像をダウンロード</button>
<button id=downloadSemiBtn class=download-semi>半透明画像をダウンロード</button>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded",(function(){
const e=document.getElementById("imageUpload"),
t=document.getElementById("processBtn"),
n=document.getElementById("downloadBtn"),
a=document.getElementById("downloadDepthBtn"),
i=document.getElementById("downloadSemiBtn"),
o=document.getElementById("originalCanvas"),
d=document.getElementById("resultCanvas"),
depthCanvas=document.getElementById("depthCanvas"),
l=document.getElementById("loadingIndicator"),
c=document.getElementById("resultOptions"),
depthContainer=document.getElementById("depthImageContainer"),
s=document.getElementById("lineDrawingMode"),
g=document.getElementById("blackWhiteMode"),
r=document.getElementById("bgRemovalMode"),
m=document.getElementById("upscaleMode"),
h=document.getElementById("lineDrawingSettings"),
u=document.getElementById("blackWhiteSettings"),
f=document.getElementById("bgRemovalSettings"),
y=document.getElementById("upscaleSettings"),
p=document.getElementById("lineThickness"),
I=document.getElementById("lineThicknessValue"),
outputFormat=document.getElementById("outputFormat"),
jpegQuality=document.getElementById("jpegQuality"),
jpegQualityLabel=document.getElementById("jpegQualityLabel"),
jpegQualityValue=document.getElementById("jpegQualityValue"),
originalInfo=document.getElementById("originalInfo"),
resultInfo=document.getElementById("resultInfo"),
depthInfo=document.getElementById("depthInfo");
// JPEG品質設定の表示/非表示
outputFormat.addEventListener("change",function(){
if(outputFormat.value === "jpeg"){
jpegQuality.style.display = "inline-block";
jpegQualityLabel.style.display = "inline-block";
jpegQualityValue.style.display = "inline-block";
}else{
jpegQuality.style.display = "none";
jpegQualityLabel.style.display = "none";
jpegQualityValue.style.display = "none";
}
});
jpegQuality.addEventListener("input",function(){
jpegQualityValue.textContent = jpegQuality.value + "%";
});
p.addEventListener("input",(()=>{I.textContent=p.value}));
const w=document.getElementById("bgOpacity"),E=document.getElementById("bgOpacityValue");
w.addEventListener("input",(()=>{E.textContent=`${w.value}%`}));
const v=document.getElementById("sensitivity"),B=document.getElementById("sensitivityValue");
v.addEventListener("input",(()=>{B.textContent=v.value}));
const b=document.getElementById("threshold"),L=document.getElementById("thresholdValue");
b.addEventListener("input",(()=>{L.textContent=`${b.value}%`}));
const C=document.getElementById("scale"),k=document.getElementById("scaleValue");
function R(e,t){
[s,g,r,m].forEach((e=>{e.classList.remove("active")})),
[h,u,f,y].forEach((e=>{e.classList.remove("active")})),
e.classList.add("active"),
t.classList.add("active")
}
C.addEventListener("input",(()=>{k.textContent=`${C.value}x`})),
s.addEventListener("click",(()=>{R(s,h)})),
g.addEventListener("click",(()=>{R(g,u)})),
r.addEventListener("click",(()=>{R(r,f)})),
m.addEventListener("click",(()=>{R(m,y)}));
let D=null,S=null,x=null,U=null;
function $(e,t){return(e[t]+e[t+1]+e[t+2])/3}
function O(e){return{r:parseInt(e.slice(1,3),16),g:parseInt(e.slice(3,5),16),b:parseInt(e.slice(5,7),16)}}
function M(e,t,n){
const a=document.createElement("a");
a.download=t;
// フォーマットに応じたMIMEタイプを設定
let mimeType = "image/png";
if(n === "jpeg") mimeType = "image/jpeg";
if(n === "webp") mimeType = "image/webp";
// データURLを作成
const img = new Image();
img.onload = function(){
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// JPEGの場合は品質を設定
let dataUrl;
if(n === "jpeg"){
dataUrl = canvas.toDataURL(mimeType, jpegQuality.value/100);
}else{
dataUrl = canvas.toDataURL(mimeType);
}
a.href = dataUrl;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
img.src = e;
}
function updateImageInfo(canvas, infoElement, isOriginal=false){
const format = outputFormat.value;
let mimeType = "image/png";
if(format === "jpeg") mimeType = "image/jpeg";
if(format === "webp") mimeType = "image/webp";
let dataUrl;
if(format === "jpeg"){
dataUrl = canvas.toDataURL(mimeType, jpegQuality.value/100);
}else{
dataUrl = canvas.toDataURL(mimeType);
}
const size = Math.round((dataUrl.length - 'data:image/png;base64,'.length) * 3 / 4 / 1024);
infoElement.textContent = `${canvas.width}×${canvas.height}px | ${size}KB | ${format.toUpperCase()}`;
if(isOriginal){
const ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
let colorCount = 0;
const colors = {};
for(let i=0; i<imageData.length; i+=4){
const r = imageData[i];
const g = imageData[i+1];
const b = imageData[i+2];
const a = imageData[i+3];
const color = `${r},${g},${b},${a}`;
if(!colors[color]){
colors[color] = true;
colorCount++;
}
}
infoElement.textContent += ` | ${colorCount}色`;
}
}
e.addEventListener("change",(function(e){
if(e.target.files&&e.target.files[0]){
const n=new FileReader;
n.onload=function(e){
D=new Image;
D.onload=function(){
const e=o.getContext("2d");
o.width=D.width;
o.height=D.height;
e.fillStyle="#ffffff";
e.fillRect(0,0,o.width,o.height);
e.drawImage(D,0,0);
d.width=D.width;
d.height=D.height;
const n=d.getContext("2d");
n.fillStyle="#ffffff";
n.fillRect(0,0,d.width,d.height);
t.disabled=!1;
c.style.display="none";
depthContainer.style.display="none";
updateImageInfo(o, originalInfo, true);
};
D.src=e.target.result;
};
n.readAsDataURL(e.target.files[0]);
}
}));
t.addEventListener("click",(async function(){
if(D){
l.style.display="block";
t.disabled=!0;
try{
const e=s.classList.contains("active"),
t=g.classList.contains("active"),
a=r.classList.contains("active"),
i=m.classList.contains("active"),
l=d.getContext("2d");
l.fillStyle="#ffffff";
l.fillRect(0,0,d.width,d.height);
if(e){
// 線画モード
const ctx=d.getContext("2d");
d.width=o.width;
d.height=o.height;
// 背景を描画
const bgColor=O(document.getElementById("bgColor").value);
const bgOpacity=parseInt(document.getElementById("bgOpacity").value)/100;
ctx.fillStyle=`rgba(${bgColor.r}, ${bgColor.g}, ${bgColor.b}, ${bgOpacity})`;
ctx.fillRect(0,0,d.width,d.height);
// 元画像を一時的に描画
ctx.drawImage(D,0,0);
// 画像データを取得
const imageData=ctx.getImageData(0,0,d.width,d.height);
const data=imageData.data;
const lineColor=O(document.getElementById("lineColor").value);
const lineThickness=parseInt(document.getElementById("lineThickness").value);
const sensitivity=parseInt(document.getElementById("sensitivity").value);
// 背景色で一旦クリア
ctx.fillStyle=`rgba(${bgColor.r}, ${bgColor.g}, ${bgColor.b}, ${bgOpacity})`;
ctx.fillRect(0,0,d.width,d.height);
// エッジ検出して線を描画
for(let y=0;y<d.height;y++){
for(let x=0;x<d.width;x++){
const idx=(y*d.width+x)*4;
if(x>0 && y>0 && x<d.width-1 && y<d.height-1){
const brightness=$(data,idx);
const right=$(data,idx+4);
const bottom=$(data,idx+4*d.width);
const diffX=Math.abs(brightness-right);
const diffY=Math.abs(brightness-bottom);
if(diffX>sensitivity/10 || diffY>sensitivity/10){
ctx.fillStyle=`rgb(${lineColor.r}, ${lineColor.g}, ${lineColor.b})`;
ctx.fillRect(x-lineThickness/2, y-lineThickness/2, lineThickness, lineThickness);
}
}
}
}
S=d.toDataURL("image/png");
updateImageInfo(d, resultInfo);
}else if(t){
// 白黒画像モード
const ctx=d.getContext("2d");
d.width=o.width;
d.height=o.height;
const bgColor=O(document.getElementById("bwBgColor").value);
const fgColor=O(document.getElementById("bwFgColor").value);
const threshold=parseInt(document.getElementById("threshold").value)/100*255;
// 背景色で塗りつぶし
ctx.fillStyle=`rgb(${bgColor.r}, ${bgColor.g}, ${bgColor.b})`;
ctx.fillRect(0,0,d.width,d.height);
// 元画像を描画
ctx.drawImage(D,0,0);
// 画像データを取得
const imageData=ctx.getImageData(0,0,d.width,d.height);
const data=imageData.data;
// 完全な2色化処理
for(let i=0;i<data.length;i+=4){
const brightness=$(data,i);
if(brightness<=threshold){
data[i]=fgColor.r;
data[i+1]=fgColor.g;
data[i+2]=fgColor.b;
data[i+3]=255;
}else{
data[i]=bgColor.r;
data[i+1]=bgColor.g;
data[i+2]=bgColor.b;
data[i+3]=255;
}
}
ctx.putImageData(imageData,0,0);
S=d.toDataURL("image/png");
updateImageInfo(d, resultInfo);
}else if(a){
// 背景削除モード
try{
const e=o.toDataURL("image/png");
const t=await fetch("https://soiz1-dis-background-removal-api-proxy.hf.space/remove_background",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({image_url:e})
});
if(!t.ok) throw new Error(`APIエラー: ${t.status}`);
const n=await t.json();
const resultImg=new Image;
resultImg.onload=function(){
d.width=resultImg.width;
d.height=resultImg.height;
const e=d.getContext("2d");
e.fillStyle="#ffffff";
e.fillRect(0,0,d.width,d.height);
e.drawImage(resultImg,0,0);
S=d.toDataURL("image/png");
updateImageInfo(d, resultInfo);
// 深度画像がある場合
if(n.depth_image){
const depthImg=new Image;
depthImg.onload=function(){
depthCanvas.width=depthImg.width;
depthCanvas.height=depthImg.height;
const ctx=depthCanvas.getContext("2d");
ctx.fillStyle="#ffffff";
ctx.fillRect(0,0,depthCanvas.width,depthCanvas.height);
ctx.drawImage(depthImg,0,0);
x=depthCanvas.toDataURL("image/png");
updateImageInfo(depthCanvas, depthInfo);
depthContainer.style.display="block";
};
depthImg.src=n.depth_image;
}else{
depthContainer.style.display="none";
}
if(n.semi_transparent_image){
U=n.semi_transparent_image;
}
};
resultImg.src=n.semi_transparent_image;
}catch(e){
console.error("背景削除処理中にエラーが発生しました:",e);
alert("背景削除処理中にエラーが発生しました: "+e.message);
}
}else if(i){
// 高画質化モード
try{
const e=document.getElementById("version").value,
t=parseInt(document.getElementById("scale").value),
n=new FormData;
n.append("file",function(e){
const t=e.split(","),
n=t[0].match(/:(.*?);/)[1],
a=atob(t[1]);
let i=a.length;
const o=new Uint8Array(i);
for(;i--;)o[i]=a.charCodeAt(i);
return new Blob([o],{type:n})
}(o.toDataURL("image/png")));
n.append("version",e);
n.append("scale",t);
const a=await fetch("https://soiz1-image-face-upscale-restoration-gfpgan-api.hf.space/api/restore",{
method:"POST",
body:n
});
if(!a.ok) throw new Error(`APIエラー: ${a.status}`);
const i=await a.blob(),
l=URL.createObjectURL(i),
c=new Image;
c.onload=function(){
d.width=c.width;
d.height=c.height;
const e=d.getContext("2d");
e.fillStyle="#ffffff";
e.fillRect(0,0,d.width,d.height);
e.drawImage(c,0,0);
S=d.toDataURL("image/png");
updateImageInfo(d, resultInfo);
URL.revokeObjectURL(l);
};
c.src=l;
}catch(e){
console.error("高画質化処理中にエラーが発生しました:",e);
alert("高画質化処理中にエラーが発生しました: "+e.message);
}
}
t.disabled=!1;
c.style.display=a?"block":"none";
}catch(e){
console.error("処理中にエラーが発生しました:",e);
alert("処理中にエラーが発生しました: "+e.message);
}finally{
l.style.display="none";
t.disabled=!1;
}
}
}));
n.addEventListener("click",(function(){
S&&M(S,"converted-image."+outputFormat.value, outputFormat.value);
}));
a.addEventListener("click",(function(){
x&&M(x,"depth-image.png", "png"); // 深度画像は常にPNG
}));
i.addEventListener("click",(function(){
U&&M(U,"semi-transparent-image.png", "png"); // 半透明画像は常にPNG
}));
}));
</script>
</body>
</html>