|
<!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"); |
|
|
|
|
|
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; |
|
|
|
let mimeType = "image/png"; |
|
if(n === "jpeg") mimeType = "image/jpeg"; |
|
if(n === "webp") mimeType = "image/webp"; |
|
|
|
|
|
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); |
|
|
|
|
|
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; |
|
|
|
|
|
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"); |
|
})); |
|
|
|
i.addEventListener("click",(function(){ |
|
U&&M(U,"semi-transparent-image.png", "png"); |
|
})); |
|
})); |
|
</script> |
|
</body> |
|
</html> |