Spaces:
Sleeping
Sleeping
| <!-- indexPhoto_v3.html --> | |
| <!-- Третья рабочая версия --> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>GrechnikNet — Snapshot Detection</title> | |
| <style> | |
| body { background:#111; color:#eee; font-family:sans-serif; margin:0; text-align:center; } | |
| header { padding:16px; } | |
| video, img { width: min(100vw, 720px); height:auto; border:1px solid #333; border-radius:8px; margin:10px 0; } | |
| .controls { display:flex; gap:16px; flex-wrap:wrap; justify-content:center; margin:16px 0; } | |
| .control { background:#1b1b1b; padding:10px 12px; border-radius:8px; } | |
| label { display:block; font-size:14px; margin-bottom:6px; } | |
| input[type="range"] { width:200px; } | |
| button { padding:10px 16px; border:none; border-radius:6px; background:#3a6df0; color:#fff; cursor:pointer; } | |
| button:disabled { background:#555; cursor:not-allowed; } | |
| #status { margin-left:12px; font-size:14px; color:#aaa; } | |
| pre { text-align:left; background:#0e0e0e; padding:12px; border-radius:8px; width:min(100vw,720px); margin:10px auto; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h2>GrechnikNet — Snapshot Detection</h2> | |
| <p>Сверху поток с камеры, снизу результат последнего снимка</p> | |
| </header> | |
| <!-- Панель управления --> | |
| <div class="controls"> | |
| <div class="control"> | |
| <label>Камера:</label> | |
| <select id="cameraSelect"></select> | |
| </div> | |
| <div class="control"> | |
| <label>Степень уверенности (conf): <span id="confVal">0.25</span></label> | |
| <input type="range" id="confSlider" min="0" max="1" step="0.05" value="0.25"> | |
| </div> | |
| <div class="control"> | |
| <label>Степень пересечения (IoU): <span id="iouVal">0.45</span></label> | |
| <input type="range" id="iouSlider" min="0" max="1" step="0.05" value="0.45"> | |
| </div> | |
| <div class="control"> | |
| <label>Режим ответа:</label> | |
| <select id="modeSelect"> | |
| <option value="image">Аннотированное изображение</option> | |
| <option value="json">Только боксы (JSON)</option> | |
| </select> | |
| </div> | |
| <div class="control"> | |
| <button id="startBtn">Старт</button> | |
| <button id="stopBtn" disabled>Стоп</button> | |
| </div> | |
| </div> | |
| <!-- Верхнее окно: поток --> | |
| <video id="video" autoplay playsinline muted></video> | |
| <!-- Кнопка + индикатор --> | |
| <div style="margin:10px;"> | |
| <button id="snapBtn">Сделать фото</button> | |
| <span id="status"></span> | |
| </div> | |
| <!-- Нижнее окно: результат --> | |
| <img id="resultImg" alt="Результат появится здесь"> | |
| <pre id="jsonOut" style="display:none;"></pre> | |
| <script> | |
| const video = document.getElementById('video'); | |
| const resultImg = document.getElementById('resultImg'); | |
| const jsonOut = document.getElementById('jsonOut'); | |
| const snapBtn = document.getElementById('snapBtn'); | |
| const cameraSelect = document.getElementById('cameraSelect'); | |
| const confSlider = document.getElementById('confSlider'); | |
| const iouSlider = document.getElementById('iouSlider'); | |
| const confVal = document.getElementById('confVal'); | |
| const iouVal = document.getElementById('iouVal'); | |
| const modeSelect = document.getElementById('modeSelect'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const statusEl = document.getElementById('status'); | |
| let stream = null; | |
| let currentDeviceId = null; | |
| confSlider.oninput = () => confVal.textContent = confSlider.value; | |
| iouSlider.oninput = () => iouVal.textContent = iouSlider.value; | |
| async function enumerateCameras() { | |
| const devices = await navigator.mediaDevices.enumerateDevices(); | |
| cameraSelect.innerHTML = ''; | |
| const cams = devices.filter(d => d.kind === 'videoinput'); | |
| cams.forEach((cam, i) => { | |
| const opt = document.createElement('option'); | |
| opt.value = cam.deviceId || i; | |
| opt.textContent = cam.label || `Камера ${i+1}`; | |
| cameraSelect.appendChild(opt); | |
| }); | |
| if (cams.length > 0) currentDeviceId = cams[0].deviceId; | |
| } | |
| async function startCamera(deviceId) { | |
| if (stream) stopCamera(); | |
| const constraints = { | |
| video: { | |
| deviceId: deviceId ? { exact: deviceId } : undefined, | |
| facingMode: 'environment', | |
| width: { ideal: 720 }, | |
| height: { ideal: 720 } | |
| }, | |
| audio: false | |
| }; | |
| stream = await navigator.mediaDevices.getUserMedia(constraints); | |
| video.srcObject = stream; | |
| await video.play(); | |
| } | |
| function stopCamera() { | |
| if (stream) { | |
| stream.getTracks().forEach(t => t.stop()); | |
| stream = null; | |
| } | |
| } | |
| cameraSelect.onchange = async () => { | |
| currentDeviceId = cameraSelect.value; | |
| await startCamera(currentDeviceId); | |
| }; | |
| async function takePhoto() { | |
| if (!video.srcObject) return; | |
| statusEl.textContent = "Обработка…"; | |
| const canvas = document.createElement('canvas'); | |
| const w = Math.min(720, video.videoWidth || 640); | |
| const h = Math.floor(w * (video.videoHeight / video.videoWidth)); | |
| canvas.width = w; | |
| canvas.height = h; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(video, 0, 0, w, h); | |
| const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9)); | |
| const formData = new FormData(); | |
| formData.append('file', blob, 'frame.jpg'); | |
| formData.append('conf', confSlider.value); | |
| formData.append('iou', iouSlider.value); | |
| const returnImage = (modeSelect.value === 'image') ? 1 : 0; | |
| formData.append('return_image', returnImage.toString()); | |
| const resp = await fetch('/predict', { method: 'POST', body: formData }); | |
| if (returnImage === 1) { | |
| const arrBuf = await resp.arrayBuffer(); | |
| const blobRes = new Blob([arrBuf], { type: 'image/jpeg' }); | |
| resultImg.src = URL.createObjectURL(blobRes); | |
| resultImg.style.display = 'block'; | |
| jsonOut.style.display = 'none'; | |
| statusEl.textContent = "Готово"; | |
| } else { | |
| const data = await resp.json(); | |
| jsonOut.textContent = JSON.stringify(data, null, 2); | |
| jsonOut.style.display = 'block'; | |
| resultImg.style.display = 'none'; | |
| // считаем количество боксов | |
| // const count = data.boxes ? data.boxes.length : (Array.isArray(data) ? data.length : 0); | |
| // statusEl.textContent = `Обнаружено: ${count} примесей`; | |
| statusEl.textContent = "Готово"; | |
| } | |
| } | |
| snapBtn.onclick = takePhoto; | |
| startBtn.onclick = async () => { | |
| await enumerateCameras(); | |
| await startCamera(currentDeviceId); | |
| startBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| }; | |
| stopBtn.onclick = () => { | |
| stopCamera(); | |
| startBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| }; | |
| </script> | |
| </body> | |
| </html> |