| | <!DOCTYPE html> |
| | <html lang="en"> |
| |
|
| | <head> |
| | <meta charset="UTF-8" /> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| | <title>DeOldify Artistic (Browser)</title> |
| | <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script> |
| | <style> |
| | body { |
| | font-family: sans-serif; |
| | max-width: 800px; |
| | margin: 0 auto; |
| | padding: 20px; |
| | } |
| | |
| | h1 { |
| | text-align: center; |
| | } |
| | |
| | .container { |
| | display: flex; |
| | flex-direction: column; |
| | align-items: center; |
| | gap: 20px; |
| | } |
| | |
| | canvas { |
| | border: 1px solid #ccc; |
| | max-width: 100%; |
| | } |
| | |
| | .controls { |
| | margin-bottom: 20px; |
| | } |
| | |
| | #status { |
| | font-weight: bold; |
| | margin-top: 10px; |
| | } |
| | </style> |
| | </head> |
| |
|
| | <body> |
| | <h1>DeOldify Artistic Model</h1> |
| | <div class="container"> |
| | <div class="controls"> |
| | <input type="file" id="imageInput" accept="image/*" /> |
| | </div> |
| | <div id="status">Select an image to start...</div> |
| | <canvas id="outputCanvas"></canvas> |
| | </div> |
| |
|
| | <script> |
| | const MODEL_URL = "https://huggingface.co/thookham/DeOldify-on-Browser/resolve/main/deoldify-art.onnx"; |
| | let session = null; |
| | |
| | const preprocess = (input_imageData, width, height) => { |
| | const floatArr = new Float32Array(width * height * 3); |
| | let j = 0; |
| | for (let i = 0; i < input_imageData.data.length; i += 4) { |
| | |
| | floatArr[j] = input_imageData.data[i] / 255.0; |
| | floatArr[j + 1] = input_imageData.data[i + 1] / 255.0; |
| | floatArr[j + 2] = input_imageData.data[i + 2] / 255.0; |
| | j += 3; |
| | } |
| | return floatArr; |
| | }; |
| | |
| | const postprocess = (tensor) => { |
| | const channels = tensor.dims[1]; |
| | const height = tensor.dims[2]; |
| | const width = tensor.dims[3]; |
| | const imageData = new ImageData(width, height); |
| | const data = imageData.data; |
| | const tensorData = new Float32Array(tensor.data); |
| | |
| | for (let h = 0; h < height; h++) { |
| | for (let w = 0; w < width; w++) { |
| | let rgb = []; |
| | for (let c = 0; c < channels; c++) { |
| | const tensorIndex = (c * height + h) * width + w; |
| | const value = tensorData[tensorIndex]; |
| | |
| | let val = value * 255.0; |
| | if (val < 0) val = 0; |
| | if (val > 255) val = 255; |
| | rgb.push(Math.round(val)); |
| | } |
| | data[(h * width + w) * 4] = rgb[0]; |
| | data[(h * width + w) * 4 + 1] = rgb[1]; |
| | data[(h * width + w) * 4 + 2] = rgb[2]; |
| | data[(h * width + w) * 4 + 3] = 255; |
| | } |
| | } |
| | return imageData; |
| | }; |
| | |
| | async function init() { |
| | const status = document.getElementById('status'); |
| | status.innerText = "Checking cache..."; |
| | try { |
| | let buffer; |
| | const cacheName = 'deoldify-models-v1'; |
| | |
| | |
| | try { |
| | const cache = await caches.open(cacheName); |
| | const cachedResponse = await cache.match(MODEL_URL); |
| | |
| | if (cachedResponse) { |
| | status.innerText = "Loading model from cache..."; |
| | const blob = await cachedResponse.blob(); |
| | buffer = await blob.arrayBuffer(); |
| | } |
| | } catch (e) { |
| | console.warn("Cache API not supported or failed:", e); |
| | } |
| | |
| | |
| | if (!buffer) { |
| | status.innerText = "Downloading model from Hugging Face... 0%"; |
| | const response = await fetch(MODEL_URL); |
| | if (!response.ok) throw new Error(`Failed to fetch model: ${response.statusText}`); |
| | |
| | const contentLength = response.headers.get('content-length'); |
| | const total = contentLength ? parseInt(contentLength, 10) : 0; |
| | let loaded = 0; |
| | |
| | const reader = response.body.getReader(); |
| | const chunks = []; |
| | |
| | while (true) { |
| | const { done, value } = await reader.read(); |
| | if (done) break; |
| | chunks.push(value); |
| | loaded += value.length; |
| | if (total) { |
| | const progress = Math.round((loaded / total) * 100); |
| | status.innerText = `Downloading model from Hugging Face... ${progress}%`; |
| | } else { |
| | status.innerText = `Downloading model from Hugging Face... ${(loaded / 1024 / 1024).toFixed(1)} MB`; |
| | } |
| | } |
| | |
| | const blob = new Blob(chunks); |
| | buffer = await blob.arrayBuffer(); |
| | |
| | |
| | try { |
| | const cache = await caches.open(cacheName); |
| | await cache.put(MODEL_URL, new Response(blob)); |
| | console.log("Model saved to cache"); |
| | } catch (e) { |
| | console.warn("Failed to save to cache:", e); |
| | } |
| | } |
| | |
| | status.innerText = "Initializing session..."; |
| | session = await ort.InferenceSession.create(buffer); |
| | |
| | status.innerText = "Model loaded! Select an image."; |
| | console.log("Session created:", session); |
| | } catch (e) { |
| | status.innerText = "Error loading model: " + e.message; |
| | console.error(e); |
| | if (e.message.includes("Failed to fetch")) { |
| | status.innerHTML += "<br><br>⚠️ <b>CORS Error Detected</b>: If you are running this file directly (file://), you must use a local server.<br>Run <code>python -m http.server 8000</code> in the terminal and visit <code>http://localhost:8000/artistic.html</code>"; |
| | } |
| | } |
| | } |
| | |
| | document.getElementById('imageInput').addEventListener('change', async function (e) { |
| | if (!session) { |
| | await init(); |
| | } |
| | |
| | const file = e.target.files[0]; |
| | if (!file) return; |
| | |
| | |
| | if (!file.type.startsWith('image/')) { |
| | alert('Please select a valid image file.'); |
| | return; |
| | } |
| | |
| | const image = new Image(); |
| | const objectUrl = URL.createObjectURL(file); |
| | image.src = objectUrl; |
| | |
| | image.onload = async function () { |
| | document.getElementById('status').innerText = "Processing..."; |
| | |
| | |
| | let canvas = document.createElement("canvas"); |
| | const size = 256; |
| | canvas.width = size; |
| | canvas.height = size; |
| | let ctx = canvas.getContext("2d"); |
| | ctx.drawImage(image, 0, 0, size, size); |
| | |
| | const input_img = ctx.getImageData(0, 0, size, size); |
| | const test = preprocess(input_img, size, size); |
| | const input = new ort.Tensor(new Float32Array(test), [1, 3, size, size]); |
| | |
| | try { |
| | const result = await session.run({ "input": input }); |
| | |
| | const output = result["output"] || result["out"] || Object.values(result)[0]; |
| | |
| | if (!output) throw new Error("No output tensor found in model result"); |
| | |
| | const imgdata = postprocess(output); |
| | |
| | |
| | const outCanvas = document.getElementById('outputCanvas'); |
| | outCanvas.width = image.width; |
| | outCanvas.height = image.height; |
| | const outCtx = outCanvas.getContext('2d'); |
| | |
| | |
| | const tempCanvas = document.createElement('canvas'); |
| | tempCanvas.width = size; |
| | tempCanvas.height = size; |
| | tempCanvas.getContext('2d').putImageData(imgdata, 0, 0); |
| | |
| | |
| | outCtx.drawImage(tempCanvas, 0, 0, image.width, image.height); |
| | |
| | document.getElementById('status').innerText = "Done!"; |
| | } catch (err) { |
| | document.getElementById('status').innerText = "Error processing: " + err.message; |
| | console.error(err); |
| | } finally { |
| | |
| | URL.revokeObjectURL(objectUrl); |
| | } |
| | }; |
| | }); |
| | |
| | |
| | init(); |
| | </script> |
| | </body> |
| |
|
| | </html> |