|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>AI Image Enhancer</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script> |
|
|
|
|
|
<style> |
|
|
|
|
|
body { |
|
|
font-family: sans-serif; |
|
|
} |
|
|
canvas { |
|
|
max-width: 100%; |
|
|
height: auto; |
|
|
border: 1px solid #ccc; |
|
|
} |
|
|
.loader { |
|
|
border: 5px solid #f3f3f3; |
|
|
border-top: 5px solid #3498db; |
|
|
border-radius: 50%; |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
animation: spin 1s linear infinite; |
|
|
margin: 20px auto; |
|
|
} |
|
|
@keyframes spin { |
|
|
0% { transform: rotate(0deg); } |
|
|
100% { transform: rotate(360deg); } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-100 p-8"> |
|
|
|
|
|
<div class="container mx-auto max-w-4xl bg-white p-6 rounded-lg shadow-lg"> |
|
|
|
|
|
<h1 class="text-3xl font-bold mb-6 text-center text-blue-600">AI Image Enhancer</h1> |
|
|
<p class="text-center text-gray-600 mb-6">Increase resolution, retouch, denoise, and more using TensorFlow.js.</p> |
|
|
|
|
|
<div class="mb-6 p-4 border rounded-md bg-gray-50"> |
|
|
<label for="imageUpload" class="block text-lg font-medium text-gray-700 mb-2">1. Upload Image:</label> |
|
|
<input type="file" id="imageUpload" accept="image/*" class="block w-full text-sm text-gray-500 |
|
|
file:mr-4 file:py-2 file:px-4 |
|
|
file:rounded-full file:border-0 |
|
|
file:text-sm file:font-semibold |
|
|
file:bg-blue-50 file:text-blue-700 |
|
|
hover:file:bg-blue-100 |
|
|
"/> |
|
|
<p id="uploadError" class="text-red-500 text-sm mt-2"></p> |
|
|
</div> |
|
|
|
|
|
<div class="mb-6 p-4 border rounded-md bg-gray-50"> |
|
|
<label for="enhancementType" class="block text-lg font-medium text-gray-700 mb-2">2. Select Enhancement:</label> |
|
|
<select id="enhancementType" class="block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"> |
|
|
<option value="upscale">Increase Resolution (Upscale 2x)</option> |
|
|
<option value="denoise">Denoise (Basic)</option> |
|
|
<option value="retouch">Retouch (Simple Filter)</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
<div class="text-center mb-6"> |
|
|
<button id="enhanceButton" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-full text-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
|
|
Enhance Image |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div id="status" class="text-center text-gray-600 mb-4 h-10"></div> |
|
|
<div id="loader" class="loader hidden"></div> |
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|
|
<div> |
|
|
<h2 class="text-xl font-semibold mb-2 text-center">Original Image</h2> |
|
|
<canvas id="originalCanvas"></canvas> |
|
|
</div> |
|
|
<div> |
|
|
<h2 class="text-xl font-semibold mb-2 text-center">Enhanced Image</h2> |
|
|
<canvas id="enhancedCanvas"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="mt-8 text-center text-xs text-gray-500"> |
|
|
<p>Powered by TensorFlow.js</p> |
|
|
<p>MIT License - [Your Name/Org] 2025</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
const imageUpload = document.getElementById('imageUpload'); |
|
|
const enhanceButton = document.getElementById('enhanceButton'); |
|
|
const enhancementType = document.getElementById('enhancementType'); |
|
|
const originalCanvas = document.getElementById('originalCanvas'); |
|
|
const enhancedCanvas = document.getElementById('enhancedCanvas'); |
|
|
const statusDiv = document.getElementById('status'); |
|
|
const loader = document.getElementById('loader'); |
|
|
const uploadError = document.getElementById('uploadError'); |
|
|
|
|
|
const originalCtx = originalCanvas.getContext('2d'); |
|
|
const enhancedCtx = enhancedCanvas.getContext('2d'); |
|
|
|
|
|
let originalImage = null; |
|
|
let model = null; |
|
|
|
|
|
|
|
|
imageUpload.addEventListener('change', handleImageUpload); |
|
|
enhanceButton.addEventListener('click', handleEnhancement); |
|
|
|
|
|
|
|
|
|
|
|
async function handleImageUpload(event) { |
|
|
const file = event.target.files[0]; |
|
|
uploadError.textContent = ''; |
|
|
enhanceButton.disabled = true; |
|
|
originalImage = null; |
|
|
originalCtx.clearRect(0, 0, originalCanvas.width, originalCanvas.height); |
|
|
enhancedCtx.clearRect(0, 0, enhancedCanvas.width, enhancedCanvas.height); |
|
|
|
|
|
|
|
|
if (!file || !file.type.startsWith('image/')) { |
|
|
uploadError.textContent = 'Please select a valid image file.'; |
|
|
return; |
|
|
} |
|
|
|
|
|
statusDiv.textContent = 'Loading image...'; |
|
|
try { |
|
|
originalImage = await loadImageFromFile(file); |
|
|
displayImageOnCanvas(originalImage, originalCanvas, originalCtx); |
|
|
statusDiv.textContent = 'Image loaded. Ready to enhance.'; |
|
|
enhanceButton.disabled = false; |
|
|
} catch (error) { |
|
|
console.error("Error loading image:", error); |
|
|
uploadError.textContent = 'Could not load the image.'; |
|
|
statusDiv.textContent = ''; |
|
|
} |
|
|
} |
|
|
|
|
|
function loadImageFromFile(file) { |
|
|
return new Promise((resolve, reject) => { |
|
|
const reader = new FileReader(); |
|
|
reader.onload = (e) => { |
|
|
const img = new Image(); |
|
|
img.onload = () => resolve(img); |
|
|
img.onerror = reject; |
|
|
img.src = e.target.result; |
|
|
}; |
|
|
reader.onerror = reject; |
|
|
reader.readAsDataURL(file); |
|
|
}); |
|
|
} |
|
|
|
|
|
function displayImageOnCanvas(img, canvas, ctx) { |
|
|
|
|
|
canvas.width = img.naturalWidth; |
|
|
canvas.height = img.naturalHeight; |
|
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height); |
|
|
console.log(`Displayed original image (${canvas.width}x${canvas.height})`); |
|
|
} |
|
|
|
|
|
async function handleEnhancement() { |
|
|
if (!originalImage) { |
|
|
statusDiv.textContent = 'Please upload an image first.'; |
|
|
return; |
|
|
} |
|
|
|
|
|
const selectedTask = enhancementType.value; |
|
|
statusDiv.textContent = `Starting ${selectedTask}...`; |
|
|
loader.classList.remove('hidden'); |
|
|
enhanceButton.disabled = true; |
|
|
enhancedCtx.clearRect(0, 0, enhancedCanvas.width, enhancedCanvas.height); |
|
|
|
|
|
try { |
|
|
|
|
|
|
|
|
await runAIEnhancement(selectedTask, originalCanvas, enhancedCanvas, enhancedCtx); |
|
|
statusDiv.textContent = 'Enhancement complete!'; |
|
|
console.log("Enhancement successful."); |
|
|
|
|
|
} catch (error) { |
|
|
console.error(`Error during ${selectedTask}:`, error); |
|
|
statusDiv.textContent = `Error during enhancement: ${error.message || error}`; |
|
|
} finally { |
|
|
loader.classList.add('hidden'); |
|
|
enhanceButton.disabled = false; |
|
|
} |
|
|
} |
|
|
|
|
|
async function runAIEnhancement(task, sourceCanvas, targetCanvas, targetCtx) { |
|
|
console.log(`Running task: ${task}`); |
|
|
|
|
|
|
|
|
await tf.ready(); |
|
|
console.log(`Using TFJS backend: ${tf.getBackend()}`); |
|
|
|
|
|
|
|
|
|
|
|
const inputTensor = tf.browser.fromPixels(sourceCanvas); |
|
|
console.log("Input tensor shape:", inputTensor.shape); |
|
|
|
|
|
let outputTensor; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
statusDiv.textContent = 'Loading AI model... (Placeholder)'; |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500)); |
|
|
|
|
|
statusDiv.textContent = 'Processing image... (Placeholder)'; |
|
|
|
|
|
if (task === 'upscale') { |
|
|
|
|
|
|
|
|
|
|
|
console.log("Simulating upscale..."); |
|
|
const scale = 1.5; |
|
|
targetCanvas.width = Math.round(sourceCanvas.width * scale); |
|
|
targetCanvas.height = Math.round(sourceCanvas.height * scale); |
|
|
targetCtx.drawImage(sourceCanvas, 0, 0, targetCanvas.width, targetCanvas.height); |
|
|
|
|
|
|
|
|
} else if (task === 'denoise') { |
|
|
|
|
|
|
|
|
|
|
|
console.log("Simulating denoise..."); |
|
|
targetCanvas.width = sourceCanvas.width; |
|
|
targetCanvas.height = sourceCanvas.height; |
|
|
targetCtx.filter = 'blur(1px)'; |
|
|
targetCtx.drawImage(sourceCanvas, 0, 0); |
|
|
targetCtx.filter = 'none'; |
|
|
|
|
|
|
|
|
} else if (task === 'retouch') { |
|
|
|
|
|
|
|
|
console.log("Simulating retouch (sepia filter)..."); |
|
|
targetCanvas.width = sourceCanvas.width; |
|
|
targetCanvas.height = sourceCanvas.height; |
|
|
targetCtx.filter = 'sepia(60%)'; |
|
|
targetCtx.drawImage(sourceCanvas, 0, 0); |
|
|
targetCtx.filter = 'none'; |
|
|
|
|
|
|
|
|
} else { |
|
|
console.warn("Unknown enhancement task:", task); |
|
|
|
|
|
targetCanvas.width = sourceCanvas.width; |
|
|
targetCanvas.height = sourceCanvas.height; |
|
|
targetCtx.drawImage(sourceCanvas, 0, 0); |
|
|
outputTensor = inputTensor.clone(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (inputTensor && !inputTensor.isDisposed) { |
|
|
inputTensor.dispose(); |
|
|
console.log("Disposed input tensor."); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
statusDiv.textContent = 'Ready. Please upload an image.'; |
|
|
|
|
|
</script> |
|
|
|
|
|
</body> |
|
|
</html> |
|
|
|