Spaces:
Running
Running
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>画像処理アプリ</title> | |
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"> | |
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet"> | |
<style> | |
/* 既存のスタイル */ | |
body { | |
background-color: #f0f0f5; | |
color: #333; | |
padding: 40px 20px; | |
} | |
/* ... 他の既存のスタイル ... */ | |
/* ヘルプボタンのスタイル */ | |
.help-button { | |
position: fixed; | |
bottom: 20px; | |
right: 20px; | |
width: 50px; | |
height: 50px; | |
border-radius: 50%; | |
background-color: #007bff; | |
color: white; | |
border: none; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.2); | |
z-index: 1000; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
font-size: 1.5rem; | |
} | |
.help-button:hover { | |
background-color: #0056b3; | |
} | |
/* モーダル内のスタイル */ | |
.help-section { | |
margin-bottom: 30px; | |
padding: 15px; | |
border-radius: 8px; | |
background-color: #f8f9fa; | |
} | |
.help-section h4 { | |
color: #007bff; | |
margin-bottom: 15px; | |
} | |
.step-list { | |
padding-left: 20px; | |
} | |
.step-list li { | |
margin-bottom: 10px; | |
} | |
.modal-body img { | |
max-width: 100%; | |
border-radius: 5px; | |
margin: 10px 0; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
} | |
</style> | |
<style> | |
body { | |
background-color: #f0f0f5; | |
color: #333; | |
padding: 40px 20px; | |
} | |
h1 { | |
color: #555; | |
margin-bottom: 30px; | |
font-weight: bold; | |
text-align: center; | |
} | |
.image-preview, .processed-preview { | |
max-width: 100%; | |
height: auto; | |
border-radius: 10px; | |
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); | |
margin-top: 20px; | |
} | |
#result { | |
margin-top: 40px; | |
display: none; | |
} | |
.slider-container { | |
text-align: left; | |
margin-top: 20px; | |
} | |
.slider-label { | |
font-size: 1.2rem; | |
color: #333; | |
} | |
.btn-primary { | |
background-color: #007bff; | |
border-color: #007bff; | |
font-size: 1.2rem; | |
padding: 10px 20px; | |
border-radius: 50px; | |
} | |
.btn-primary:hover { | |
background-color: #0056b3; | |
border-color: #004085; | |
} | |
.form-control, .custom-select { | |
border-radius: 20px; | |
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); | |
} | |
.nav-tabs { | |
margin-bottom: 30px; | |
border-bottom: 2px solid #007bff; | |
} | |
.nav-tabs .nav-link { | |
border: none; | |
color: #555; | |
font-size: 1.1rem; | |
padding: 12px 25px; | |
border-radius: 10px 10px 0 0; | |
} | |
.nav-tabs .nav-link.active { | |
background-color: #007bff; | |
color: white; | |
} | |
.tab-content { | |
padding: 20px; | |
background: white; | |
border-radius: 0 0 10px 10px; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
} | |
#loadingSpinner { | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background: rgba(255,255,255,0.9); | |
padding: 20px; | |
border-radius: 10px; | |
box-shadow: 0 0 15px rgba(0,0,0,0.2); | |
z-index: 9999; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="loadingSpinner" style="display: none;"> | |
<i class="fas fa-spinner fa-spin fa-3x"></i> | |
<p>画像を処理中です。少々お待ちください...</p> | |
</div> | |
<div class="container"> | |
<h1><i class="fas fa-image"></i> AIデンティファイ</h1> | |
<ul class="nav nav-tabs" role="tablist"> | |
<li class="nav-item"> | |
<a class="nav-link active" id="general-tab" data-toggle="tab" href="#general" role="tab"> | |
<i class="fas fa-magic"></i> 個人情報保護モード | |
</a> | |
</li> | |
<li class="nav-item"> | |
<a class="nav-link" id="face-tab" data-toggle="tab" href="#face" role="tab"> | |
<i class="fas fa-user-circle"></i> 顔保護モード | |
</a> | |
</li> | |
</ul> | |
<div class="tab-content"> | |
<!-- 一般処理モード --> | |
<div class="tab-pane fade show active" id="general" role="tabpanel"> | |
<div class="form-group"> | |
<label for="uploadImage1">画像をアップロード:</label> | |
<input type="file" id="uploadImage1" class="form-control-file" accept="image/*" onchange="previewAndResizeImage('uploadImage1', 'uploadedImage1')"> | |
</div> | |
<img id="uploadedImage1" class="image-preview" src="#" alt="アップロードされた画像" style="display: none;"> | |
<div class="form-group mt-4"> | |
<label for="processingType">処理方法を選択:</label> | |
<select id="processingType" class="custom-select"> | |
<option value="opencv">OpenCVインペイント</option> | |
<option value="simple_lama">Simple Lamaインペイント</option> | |
<option value="stamp">stampインペイント</option> | |
<option value="mosaic">mosaicインペイント</option> | |
<option value="llm-auto">llm-autoインペイント</option> | |
<option value="llm">llmインペイント</option> | |
</select> | |
</div> | |
<div class="slider-container"> | |
<label for="riskLevel1" class="slider-label">リスクレベル (0-100): <span id="riskLevelLabel1">50</span></label> | |
<div id="slider1"></div> | |
</div> | |
<button class="btn btn-primary mt-4" onclick="processGeneralImage()">処理開始</button> | |
</div> | |
<!-- 顔照合モード --> | |
<div class="tab-pane fade" id="face" role="tabpanel"> | |
<div class="form-group"> | |
<label for="uploadImage2">処理する画像をアップロード:</label> | |
<input type="file" id="uploadImage2" class="form-control-file" accept="image/*" onchange="previewAndResizeImage('uploadImage2', 'uploadedImage2')"> | |
</div> | |
<img id="uploadedImage2" class="image-preview" src="#" alt="アップロードされた画像" style="display: none;"> | |
<div class="form-group mt-4"> | |
<label for="faceOption">自分の顔の入力方法を選択:</label> | |
<select id="faceOption" class="custom-select" onchange="toggleFaceInput()"> | |
<option value="upload">ファイルからアップロード</option> | |
<option value="camera">カメラで撮影</option> | |
</select> | |
</div> | |
<div class="form-group" id="uploadFaceGroup"> | |
<label for="uploadFace">顔画像をアップロード:</label> | |
<input type="file" id="uploadFace" class="form-control-file" accept="image/*" onchange="previewFaceImage()"> | |
<img id="facePreview" class="image-preview" src="#" alt="顔画像のプレビュー" style="display: none;"> | |
</div> | |
<div class="form-group" id="cameraFaceGroup" style="display: none;"> | |
<video id="cameraStream" width="100%" autoplay></video> | |
<button class="btn btn-secondary mt-2" onclick="captureFaceImage()">顔をキャプチャ</button> | |
<canvas id="cameraCanvas" style="display: none;"></canvas> | |
</div> | |
<div class="slider-container"> | |
<label for="riskLevel2" class="slider-label">リスクレベル (0-100): <span id="riskLevelLabel2">50</span></label> | |
<div id="slider2"></div> | |
</div> | |
<button class="btn btn-primary mt-4" onclick="processFaceImage()">処理開始</button> | |
</div> | |
<!-- 処理結果(共通) --> | |
<div id="result" class="mt-5"> | |
<h2>処理結果:</h2> | |
<img id="processedImage" class="processed-preview" src="" alt=""> | |
<a id="downloadLink" class="btn btn-success mt-3" href="#" download="processed_image.jpg">処理された画像をダウンロード</a> | |
</div> | |
</div> | |
</div> | |
<!-- ヘルプボタン --> | |
<button class="help-button" data-toggle="modal" data-target="#helpModal"> | |
<i class="fas fa-question"></i> | |
</button> | |
<!-- ヘルプモーダル --> | |
<div class="modal fade" id="helpModal" tabindex="-1" role="dialog" aria-labelledby="helpModalLabel" aria-hidden="true"> | |
<div class="modal-dialog modal-lg" role="document"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title" id="helpModalLabel"> | |
<i class="fas fa-question-circle"></i> AIデンティファイの使い方 | |
</h5> | |
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> | |
<span aria-hidden="true">×</span> | |
</button> | |
</div> | |
<div class="modal-body"> | |
<!-- 一般処理モードの説明 --> | |
<div class="help-section"> | |
<h4><i class="fas fa-magic"></i> 個人情報保護モードの使い方</h4> | |
<ol class="step-list"> | |
<li>このモードではあなたの個人情報を簡単に保護することができます</li> | |
<li>「画像をアップロード」ボタンをクリックして、処理したい画像を選択します。</li> | |
<li>処理方法を以下から選択します: | |
<ul> | |
<li><strong>OpenCVインペイント:</strong> 一般的な画像補正</li> | |
<li><strong>Simple Lamaインペイント:</strong> 高度な画像補正</li> | |
<li><strong>stampインペイント:</strong> スタンプによる画像加工</li> | |
<li><strong>mosaicインペイント:</strong> モザイクによる加工</li> | |
</ul> | |
</li> | |
<li>スライダーでリスクレベルを調整します(0-100): | |
<ul> | |
<li>値が高いほど、より強い保護が適用されます</li> | |
<li>推奨値は30前後です</li> | |
</ul> | |
</li> | |
<li>「処理開始」ボタンをクリックして処理を実行します。</li> | |
</ol> | |
</div> | |
<!-- 顔照合モードの説明 --> | |
<div class="help-section"> | |
<h4><i class="fas fa-user-circle"></i> 顔保護モードの使い方</h4> | |
<ol class="step-list"> | |
<li>このモードではあなたの顔以外を隠すことができます。</li> | |
<li>「処理する画像をアップロード」から、処理したい画像を選択します。</li> | |
<li>自分の顔の入力方法を選択します: | |
<ul> | |
<li><strong>ファイルからアップロード:</strong> 既存の顔写真を使用</li> | |
<li><strong>カメラで撮影:</strong> Webカメラを使用して顔写真を撮影</li> | |
</ul> | |
</li> | |
<li>選択した方法で顔画像を入力します。</li> | |
<li>スライダーでリスクレベルを調整します。</li> | |
<li>「処理開始」ボタンをクリックして処理を実行します。</li> | |
</ol> | |
</div> | |
<!-- 共通の注意事項 --> | |
<div class="help-section"> | |
<h4><i class="fas fa-exclamation-triangle"></i> 注意事項</h4> | |
<ul> | |
<li>アップロードする画像は1200×1200ピクセル以下に自動でリサイズされます。</li> | |
<li>処理には数秒から30秒ほどかかる場合があります。</li> | |
<li>処理が完了すると、処理結果が表示され、画像のダウンロードが可能になります。</li> | |
<li>ブラウザのプライバシー設定によっては、カメラの使用許可が必要な場合があります。</li> | |
</ul> | |
</div> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- 既存のスクリプト --> | |
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> | |
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> | |
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script> | |
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> | |
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> | |
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script> | |
<script> | |
// スライダーの初期化 | |
$(function() { | |
$("#slider1, #slider2").slider({ | |
range: "min", | |
value: 50, | |
min: 0, | |
max: 100, | |
slide: function(event, ui) { | |
const labelId = $(this).attr('id') === 'slider1' ? 'riskLevelLabel1' : 'riskLevelLabel2'; | |
$("#" + labelId).text(ui.value); | |
} | |
}); | |
}); | |
let resizedImageBlob1 = null; | |
let resizedImageBlob2 = null; | |
let faceImageBlob = null; | |
function previewAndResizeImage(inputId, imageId) { | |
const fileInput = document.getElementById(inputId); | |
const uploadedImage = document.getElementById(imageId); | |
if (fileInput.files && fileInput.files[0]) { | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
const img = new Image(); | |
img.onload = function() { | |
const maxWidth = 1200; | |
const maxHeight = 1200; | |
let width = img.width; | |
let height = img.height; | |
if (width > maxWidth || height > maxHeight) { | |
const ratio = Math.min(maxWidth / width, maxHeight / height); | |
width *= ratio; | |
height *= ratio; | |
} | |
const canvas = document.createElement('canvas'); | |
canvas.width = width; | |
canvas.height = height; | |
const ctx = canvas.getContext('2d'); | |
ctx.drawImage(img, 0, 0, width, height); | |
uploadedImage.src = canvas.toDataURL('image/jpeg'); | |
uploadedImage.style.display = 'block'; | |
canvas.toBlob((blob) => { | |
if (inputId === 'uploadImage1') { | |
resizedImageBlob1 = blob; | |
} else { | |
resizedImageBlob2 = blob; | |
} | |
}, 'image/jpeg'); | |
}; | |
img.src = e.target.result; | |
}; | |
reader.readAsDataURL(fileInput.files[0]); | |
} | |
} | |
function previewFaceImage() { | |
const fileInput = document.getElementById('uploadFace'); | |
if (fileInput.files && fileInput.files[0]) { | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
document.getElementById('facePreview').src = e.target.result; | |
document.getElementById('facePreview').style.display = 'block'; | |
}; | |
reader.readAsDataURL(fileInput.files[0]); | |
faceImageBlob = fileInput.files[0]; | |
} | |
} | |
function toggleFaceInput() { | |
const faceOption = document.getElementById('faceOption').value; | |
document.getElementById('uploadFaceGroup').style.display = faceOption === 'upload' ? 'block' : 'none'; | |
document.getElementById('cameraFaceGroup').style.display = faceOption === 'camera' ? 'block' : 'none'; | |
if (faceOption === 'camera') { | |
startCamera(); | |
} else { | |
stopCamera(); | |
} | |
} | |
function startCamera() { | |
const video = document.getElementById('cameraStream'); | |
navigator.mediaDevices.getUserMedia({ video: true }) | |
.then(stream => video.srcObject = stream) | |
.catch(error => console.error('カメラの起動に失敗しました:', error)); | |
} | |
function stopCamera() { | |
const video = document.getElementById('cameraStream'); | |
const stream = video.srcObject; | |
if (stream) { | |
stream.getTracks().forEach(track => track.stop()); | |
video.srcObject = null; | |
} | |
} | |
function captureFaceImage() { | |
const video = document.getElementById('cameraStream'); | |
const canvas = document.getElementById('cameraCanvas'); | |
const context = canvas.getContext('2d'); | |
canvas.width = video.videoWidth; | |
canvas.height = video.videoHeight; | |
context.drawImage(video, 0, 0, canvas.width, canvas.height); | |
canvas.toBlob(blob => faceImageBlob = blob, 'image/jpeg'); | |
stopCamera(); | |
} | |
function processGeneralImage() { | |
if (!resizedImageBlob1) { | |
alert("画像を選択してください。"); | |
return; | |
} | |
const processingType = document.getElementById('processingType').value; | |
const riskLevel = $("#slider1").slider("value"); | |
showLoadingSpinner(); | |
const formData = new FormData(); | |
formData.append('image', resizedImageBlob1, 'resized_image.jpg'); | |
formData.append('risk_level', riskLevel); | |
let apiEndpoint; | |
if (processingType === "opencv") { | |
apiEndpoint = "/create-mask-and-inpaint-opencv"; | |
} else if (processingType === "simple_lama") { | |
apiEndpoint = "/create-mask-and-inpaint-simple-lama"; | |
} else if (processingType === "stamp") { | |
apiEndpoint = "/create-mask-and-inpaint-stamp"; | |
} else if (processingType === "mosaic") { | |
apiEndpoint = "/create-mask-and-inpaint-mosaic"; | |
} else if (processingType === "llm-auto") { | |
apiEndpoint = "/create-mask-and-inpaint-sum-llm-auto"; | |
formData.append('x1', 0.001); // FastAPIで使うデフォルト値と同じ値を設定 | |
formData.append('y1', 0.001); // FastAPIで使うデフォルト値と同じ値を設定 | |
formData.append('x2', 0.001); // FastAPIで使うデフォルト値と同じ値を設定 | |
formData.append('y2', 0.001); // FastAPIで使うデフォルト値と同じ値を設定 | |
} else if (processingType === "llm") { | |
apiEndpoint = "/create-mask-and-inpaint-sum-llm-simple"; | |
formData.append('x1', 0.001); // FastAPIで使うデフォルト値と同じ値を設定 | |
formData.append('y1', 0.001); // FastAPIで使うデフォルト値と同じ値を設定 | |
formData.append('x2', 0.001); // FastAPIで使うデフォルト値と同じ値を設定 | |
formData.append('y2', 0.001); // FastAPIで使うデフォルト値と同じ値を設定 | |
} else { | |
alert("無効な処理方法が選択されました。"); | |
hideLoadingSpinner(); | |
return; | |
} | |
processImageRequest(formData, "https://rein0421-aidentify.hf.space" + apiEndpoint); | |
} | |
function processFaceImage() { | |
if (!resizedImageBlob2 || !faceImageBlob) { | |
alert("処理する画像と顔画像の両方を設定してください。"); | |
return; | |
} | |
const riskLevel = $("#slider2").slider("value"); | |
showLoadingSpinner(); | |
const formData = new FormData(); | |
formData.append('reference_image', faceImageBlob, 'reference_image.jpg'); | |
formData.append('test_image', resizedImageBlob2, 'test_image.jpg'); | |
formData.append('risk_level', riskLevel); | |
processImageRequest(formData, "https://rein0421-aidentify.hf.space/mosaic_faces"); | |
} | |
function processImageRequest(formData, url) { | |
fetch(url, { | |
method: 'POST', | |
body: formData | |
}) | |
.then(response => { | |
if (!response.ok) throw new Error("Network response was not ok"); | |
return response.blob(); | |
}) | |
.then(blob => { | |
const objectURL = URL.createObjectURL(blob); | |
document.getElementById('processedImage').src = objectURL; | |
document.getElementById('downloadLink').href = objectURL; | |
document.getElementById('result').style.display = "block"; | |
}) | |
.catch(error => { | |
console.error("画像処理に失敗しました。", error); | |
alert("画像処理に失敗しました。"); | |
}) | |
.finally(() => { | |
hideLoadingSpinner(); | |
}); | |
} | |
function showLoadingSpinner() { | |
document.getElementById('loadingSpinner').style.display = 'block'; | |
} | |
function hideLoadingSpinner() { | |
document.getElementById('loadingSpinner').style.display = 'none'; | |
} | |
// タブ切り替え時の処理 | |
$('.nav-tabs a').on('shown.bs.tab', function (e) { | |
// 結果表示をリセット | |
document.getElementById('result').style.display = 'none'; | |
document.getElementById('processedImage').src = ''; | |
document.getElementById('downloadLink').href = '#'; | |
}); | |
</script> | |
<script> | |
</script> | |
</body> | |
</html> |