Team Ai
Apppublic

icekun/tinymodel

sourceHugging Faceupdated 23d agoView on Hugging Face
0likes
index.html510 linesDownload Raw Back to root
1<!DOCTYPE html>2<html lang="ja">3<head>4    <meta charset="UTF-8">5    <meta name="viewport" content="width=device-width, initial-scale=1.0">6    <title>その画像、必要?</title>7    <!-- ONNX Runtime Web -->8    <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>9    <style>10        :root {11            --orange-primary: #ff7c00;12            --orange-hover: #e56f00;13            --bg-main: #f9fafb;14            --block-bg: #ffffff;15            --border-color: #e5e7eb;16            --text-title: #111827;17            --text-body: #374151;18            --text-muted: #6b7280;19            --radius-block: 10px;20            --radius-input: 6px;21        }22 23        * {24            box-sizing: border-box;25            margin: 0;26            padding: 0;27        }28 29        body {30            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;31            background-color: var(--bg-main);32            color: var(--text-body);33            padding: 2rem 1rem;34            display: flex;35            justify-content: center;36        }37 38        .gradio-container {39            width: 100%;40            max-width: 960px;41            background: var(--bg-main);42        }43 44        .gradio-header {45            margin-bottom: 1.5rem;46            text-align: left;47        }48 49        .gradio-header h1 {50            font-size: 1.75rem;51            font-weight: 700;52            color: var(--text-title);53            margin-bottom: 0.5rem;54        }55 56        .gradio-header p {57            color: var(--text-muted);58            font-size: 0.95rem;59        }60 61        .gradio-layout {62            display: grid;63            grid-template-columns: 1fr;64            gap: 1.5rem;65        }66 67        @media (min-width: 768px) {68            .gradio-layout {69                grid-template-columns: 1fr 1fr;70            }71        }72 73        .gradio-column {74            display: flex;75            flex-direction: column;76            gap: 1.25rem;77        }78 79        .gr-block {80            background: var(--block-bg);81            border: 1px solid var(--border-color);82            border-radius: var(--radius-block);83            padding: 1rem;84            box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);85        }86 87        .gr-label {88            font-size: 0.85rem;89            font-weight: 600;90            color: var(--text-muted);91            margin-bottom: 0.5rem;92            display: block;93            text-transform: uppercase;94            letter-spacing: 0.05em;95        }96 97        .gr-select {98            width: 100%;99            padding: 0.6rem 0.75rem;100            border: 1px solid var(--border-color);101            border-radius: var(--radius-input);102            background-color: #fff;103            font-size: 0.95rem;104            color: var(--text-title);105            outline: none;106            cursor: pointer;107        }108 109        .gr-select:focus {110            border-color: var(--orange-primary);111        }112 113        /* Gradio Image Upload Component */114        .gr-image-upload {115            border: 2px dashed var(--border-color);116            border-radius: var(--radius-input);117            padding: 2rem 1rem;118            text-align: center;119            background-color: #f9fafb;120            cursor: pointer;121            position: relative;122            transition: all 0.2s;123            min-height: 220px;124            display: flex;125            flex-direction: column;126            align-items: center;127            justify-content: center;128        }129 130        .gr-image-upload:hover {131            border-color: var(--orange-primary);132            background-color: #fff7ed;133        }134 135        .gr-image-upload input[type="file"] {136            position: absolute;137            width: 100%;138            height: 100%;139            top: 0;140            left: 0;141            opacity: 0;142            cursor: pointer;143        }144 145        .upload-icon {146            font-size: 2.5rem;147            color: var(--text-muted);148            margin-bottom: 0.5rem;149        }150 151        .upload-text {152            font-size: 0.9rem;153            color: var(--text-muted);154        }155 156        .preview-img {157            max-width: 100%;158            max-height: 260px;159            border-radius: var(--radius-input);160            display: none;161            object-fit: contain;162        }163 164        /* Gradio Button */165        .gr-button-primary {166            width: 100%;167            padding: 0.75rem;168            background-color: var(--orange-primary);169            color: #ffffff;170            border: none;171            border-radius: var(--radius-input);172            font-size: 1rem;173            font-weight: 600;174            cursor: pointer;175            transition: background-color 0.15s;176            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);177        }178 179        .gr-button-primary:hover:not(:disabled) {180            background-color: var(--orange-hover);181        }182 183        .gr-button-primary:disabled {184            background-color: #d1d5db;185            cursor: not-allowed;186            box-shadow: none;187        }188 189        /* Status Banner */190        .status-banner {191            font-size: 0.85rem;192            padding: 0.5rem 0.75rem;193            border-radius: var(--radius-input);194            background-color: #f3f4f6;195            color: var(--text-muted);196            display: flex;197            align-items: center;198            gap: 0.5rem;199        }200 201        /* Gradio Label / Output Result Component */202        .gr-label-output {203            display: flex;204            flex-direction: column;205            gap: 0.75rem;206        }207 208        .top-class-box {209            text-align: center;210            padding: 1rem;211            background-color: #fff7ed;212            border: 1px solid #ffedd5;213            border-radius: var(--radius-input);214            margin-bottom: 0.5rem;215        }216 217        .top-class-name {218            font-size: 1.5rem;219            font-weight: 700;220            color: var(--orange-primary);221        }222 223        .top-class-conf {224            font-size: 0.9rem;225            color: var(--text-muted);226        }227 228        .prob-row {229            display: flex;230            flex-direction: column;231            gap: 0.25rem;232        }233 234        .prob-header {235            display: flex;236            justify-content: space-between;237            font-size: 0.85rem;238            font-weight: 600;239        }240 241        .prob-bar-bg {242            height: 20px;243            background-color: #f3f4f6;244            border-radius: 4px;245            overflow: hidden;246        }247 248        .prob-bar-fill {249            height: 100%;250            background-color: var(--orange-primary);251            width: 0%;252            transition: width 0.4s ease;253        }254 255        .prob-bar-fill.secondary {256            background-color: #9ca3af;257        }258 259        .empty-output {260            text-align: center;261            color: var(--text-muted);262            padding: 3rem 1rem;263            font-size: 0.9rem;264        }265    </style>266</head>267<body>268 269<div class="gradio-container">270    <div class="gradio-header">271        <h1>必要な画像かどうかを判別</h1>272        <p>画像をアップロードし、ドロップダウンで選択した .onnx モデルの判定結果(need / trash)を表示します。</p>273    </div>274 275    <div class="gradio-layout">276        <!-- 左カラム:入力 -->277        <div class="gradio-column">278            <div class="gr-block">279                <span class="gr-label">使用するモデル重み (.onnx)</span>280                <select id="modelSelect" class="gr-select"></select>281            </div>282 283            <div class="gr-block">284                <span class="gr-label">入力画像</span>285                <div class="gr-image-upload" id="dropzone">286                    <input type="file" id="fileInput" accept="image/*">287                    <div id="uploadPrompt">288                        <div class="upload-icon">📁</div>289                        <div class="upload-text">クリックまたはドラッグ&ドロップで画像をアップロード</div>290                    </div>291                    <img id="previewImg" class="preview-img" alt="Preview">292                </div>293            </div>294 295            <button id="submitBtn" class="gr-button-primary" disabled>Submit</button>296 297            <div id="statusBanner" class="status-banner">298                <span id="statusText">モデルをロード中...</span>299            </div>300        </div>301 302        <!-- 右カラム:出力 -->303        <div class="gradio-column">304            <div class="gr-block" style="min-height: 100%;">305                <span class="gr-label">判定結果 (確率)</span>306                307                <div id="emptyOutput" class="empty-output">308                    画像を送信すると判定結果がここに表示されます。309                </div>310 311                <div id="resultOutput" class="gr-label-output" style="display: none;">312                    <div class="top-class-box">313                        <div id="topClassName" class="top-class-name">need</div>314                        <div id="topClassConf" class="top-class-conf">信頼度: 100%</div>315                    </div>316 317                    <div class="prob-row">318                        <div class="prob-header">319                            <span>need</span>320                            <span id="probNeedText">0%</span>321                        </div>322                        <div class="prob-bar-bg">323                            <div id="probNeedBar" class="prob-bar-fill"></div>324                        </div>325                    </div>326 327                    <div class="prob-row">328                        <div class="prob-header">329                            <span>trash</span>330                            <span id="probTrashText">0%</span>331                        </div>332                        <div class="prob-bar-bg">333                            <div id="probTrashBar" class="prob-bar-fill secondary"></div>334                        </div>335                    </div>336                </div>337            </div>338        </div>339    </div>340</div>341<script>342    // モデル定義(JSONの閾値 threshold: 0.19671 を追加)343    const models = [344        { file: "tinymodelV9s.onnx", type: "hsc", size: 256, threshold: 0.3 }, // 最適化閾値を適用345        { file: "tinymodelV3.onnx",  type: "hsc", size: 224, threshold: 0.5 },346        { file: "tinymodelV2.onnx",  type: "softmax", size: 224, threshold: 0.5 }347    ];348 349    let session = null;350    let isReady = false;351    let currentModelConfig = null;352 353    const modelSelect = document.getElementById('modelSelect');354    const fileInput = document.getElementById('fileInput');355    const previewImg = document.getElementById('previewImg');356    const uploadPrompt = document.getElementById('uploadPrompt');357    const submitBtn = document.getElementById('submitBtn');358    const statusText = document.getElementById('statusText');359    const emptyOutput = document.getElementById('emptyOutput');360    const resultOutput = document.getElementById('resultOutput');361 362    function initSelect() {363        modelSelect.innerHTML = '';364        models.forEach((m, index) => {365            const opt = document.createElement('option');366            opt.value = index;367            opt.textContent = m.file;368            modelSelect.appendChild(opt);369        });370        modelSelect.addEventListener('change', (e) => loadModel(models[e.target.value]));371    }372 373    async function loadModel(modelConfig) {374        isReady = false;375        submitBtn.disabled = true;376        currentModelConfig = modelConfig;377        statusText.innerText = "⏳ " + modelConfig.file + " をロード中...";378        379        try {380            if (session) {381                await session.release();382                session = null;383            }384            session = await ort.InferenceSession.create('./' + modelConfig.file);385            isReady = true;386            statusText.innerText = "✅ " + modelConfig.file + " の準備完了 (閾値: " + (modelConfig.threshold * 100).toFixed(1) + "%)";387            388            if (previewImg.src && previewImg.style.display !== 'none') {389                submitBtn.disabled = false;390            }391        } catch (e) {392            statusText.innerText = "❌ モデル読込エラー: " + e.message;393        }394    }395 396    function handleFile(file) {397        if (!file || !file.type.startsWith('image/')) return;398        const reader = new FileReader();399        reader.onload = (e) => {400            previewImg.src = e.target.result;401            previewImg.style.display = 'block';402            uploadPrompt.style.display = 'none';403            resultOutput.style.display = 'none';404            emptyOutput.style.display = 'block';405            if (isReady) submitBtn.disabled = false;406        };407        reader.readAsDataURL(file);408    }409 410    fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));411 412    async function preprocess(imgElement, targetSize) {413        const canvas = document.createElement('canvas');414        canvas.width = targetSize;415        canvas.height = targetSize;416        const ctx = canvas.getContext('2d');417        ctx.drawImage(imgElement, 0, 0, targetSize, targetSize);418        const data = ctx.getImageData(0, 0, targetSize, targetSize).data;419        420        const mean = [0.485, 0.456, 0.406];421        const std = [0.229, 0.224, 0.225];422        423        const floatData = new Float32Array(3 * targetSize * targetSize);424        for (let i = 0; i < targetSize * targetSize; i++) {425            const r = data[i * 4] / 255.0;426            const g = data[i * 4 + 1] / 255.0;427            const b = data[i * 4 + 2] / 255.0;428            floatData[i] = (r - mean[0]) / std[0];429            floatData[targetSize * targetSize + i] = (g - mean[1]) / std[1];430            floatData[2 * targetSize * targetSize + i] = (b - mean[2]) / std[2];431        }432        return new ort.Tensor('float32', floatData, [1, 3, targetSize, targetSize]);433    }434 435    submitBtn.addEventListener('click', async () => {436        if (!isReady || !session || !previewImg.src || !currentModelConfig) return;437        438        submitBtn.disabled = true;439        statusText.innerText = "⚡ 推論中...";440        try {441            const targetSize = currentModelConfig.size || 224;442            const origW = previewImg.naturalWidth;443            const origH = previewImg.naturalHeight;444            445            let probNeed, probTrash, rawTrashScore;446 447            // 256px 以下の低解像度画像の除外判定 (inf.py 仕様)448            if (currentModelConfig.file === "tinymodelV9s.onnx" && (origW <= 256 || origH <= 256)) {449                rawTrashScore = 1.0;450                probTrash = 100.0;451                probNeed = 0.0;452                statusText.innerText = "⚠️ 低解像度のため判定をスキップ (Trash)";453            } else {454                const inputTensor = await preprocess(previewImg, targetSize);455                const feeds = {};456                feeds[session.inputNames[0]] = inputTensor;457                458                const start = performance.now();459                const results = await session.run(feeds);460                const duration = (performance.now() - start).toFixed(1);461                462                const output = results[session.outputNames[0]].data;463 464                if (currentModelConfig.type === "hsc") {465                    rawTrashScore = output[0];466                    probTrash = rawTrashScore * 100;467                    probNeed = (1.0 - rawTrashScore) * 100;468                } else if (currentModelConfig.type === "softmax") {469                    const exp0 = Math.exp(output[0]);470                    const exp1 = Math.exp(output[1]);471                    const sum = exp0 + exp1;472                    probNeed = (exp0 / sum) * 100;473                    probTrash = (exp1 / sum) * 100;474                    rawTrashScore = probTrash / 100.0;475                }476                statusText.innerText = "✅ 完了 (" + duration + "ms)";477            }478 479            // 判定閾値 (threshold) を使用した判定ロジック480            const threshold = currentModelConfig.threshold ?? 0.5;481            const topClass = rawTrashScore > threshold ? "trash" : "need";482            const topConf = (topClass === "trash" ? probTrash : probNeed).toFixed(1);483 484            // UI描画485            emptyOutput.style.display = 'none';486            resultOutput.style.display = 'flex';487            488            document.getElementById('topClassName').innerText = topClass;489            document.getElementById('topClassConf').innerText = "信頼度: " + topConf + "%";490            491            document.getElementById('probNeedText').innerText = probNeed.toFixed(1) + "%";492            document.getElementById('probNeedBar').style.width = probNeed + "%";493            document.getElementById('probTrashText').innerText = probTrash.toFixed(1) + "%";494            document.getElementById('probTrashBar').style.width = probTrash + "%";495            496        } catch (e) {497            statusText.innerText = "❌ エラー: " + e.message;498        } finally {499            submitBtn.disabled = false;500        }501    });502 503    window.onload = () => {504        initSelect();505        if (models.length > 0) loadModel(models[0]);506    };507</script>508 509</body>510</html>