|
|
!function(){"use strict";class e{constructor(){this.currentStream=null,this.availableCameras=[],this.selectedCameraId=null,this.videoElement=null,this.preferredConstraints={width:{ideal:480,min:240},height:{ideal:640,min:320}},this.fallbackConstraints={width:{ideal:640,min:320},height:{ideal:480,min:240}}}async initialize(){try{await this.requestCameraPermission(),await this.enumerateCameras(),console.log(`Found ${this.availableCameras.length} camera(s)`)}catch(e){throw console.error("Failed to initialize camera manager:",e),new Error(`Camera initialization failed: ${e.message}`)}}async requestCameraPermission(){try{(await navigator.mediaDevices.getUserMedia({video:!0})).getTracks().forEach(e=>e.stop()),console.log("Camera permission granted")}catch(e){throw new Error("Camera permission denied or not available")}}async enumerateCameras(){try{const e=await navigator.mediaDevices.enumerateDevices();if(this.availableCameras=e.filter(e=>"videoinput"===e.kind).map((e,t)=>({id:e.deviceId,label:e.label||`Camera ${t+1}`,groupId:e.groupId})),0===this.availableCameras.length)throw new Error("No cameras found");this.selectedCameraId=this.availableCameras[0].id}catch(e){throw new Error(`Failed to enumerate cameras: ${e.message}`)}}getAvailableCameras(){return[...this.availableCameras]}async startCamera(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;try{this.currentStream&&this.stopCamera();const a=t||this.selectedCameraId;this.selectedCameraId=a,this.videoElement=e;let n=null,s=null;try{const e={video:{deviceId:{exact:a},...this.preferredConstraints}};console.log("Trying preferred resolution (480x640)..."),n=await navigator.mediaDevices.getUserMedia(e),s=this.preferredConstraints}catch(e){console.log("Preferred resolution failed, trying fallback (640x480)...");const t={video:{deviceId:{exact:a},...this.fallbackConstraints}};n=await navigator.mediaDevices.getUserMedia(t),s=this.fallbackConstraints}e.srcObject=n,this.currentStream=n,await new Promise((t,a)=>{e.addEventListener("loadedmetadata",t,{once:!0}),e.addEventListener("error",a,{once:!0}),setTimeout(()=>a(new Error("Video load timeout")),1e4)});const i=e.videoWidth,o=e.videoHeight;console.log(`Video stream started: ${i}x${o}`);const r=!1,l=o>i,c=e.closest(".video-wrapper");return r||e.classList.remove("rotate-90ccw"),c&&(l?(c.classList.add("portrait"),console.log("Applied portrait aspect ratio (3:4)")):c.classList.remove("portrait")),{width:i,height:o,rotated:r,constraints:s,cameraId:a,cameraLabel:this.getCameraLabel(a)}}catch(e){throw console.error("Failed to start camera:",e),new Error(`Camera start failed: ${e.message}`)}}stopCamera(){if(this.currentStream&&(this.currentStream.getTracks().forEach(e=>{e.stop(),console.log(`Stopped ${e.kind} track`)}),this.currentStream=null),this.videoElement){this.videoElement.srcObject=null,this.videoElement.classList.remove("rotate-90ccw");const e=this.videoElement.closest(".video-wrapper");e&&e.classList.remove("portrait")}console.log("Camera stopped")}async switchCamera(e){if(!this.videoElement)throw new Error("No video element available");return await this.startCamera(this.videoElement,e)}getCameraLabel(e){const t=this.availableCameras.find(t=>t.id===e);return t?t.label:"Unknown Camera"}getCurrentCameraInfo(){return this.selectedCameraId&&this.videoElement?{id:this.selectedCameraId,label:this.getCameraLabel(this.selectedCameraId),width:this.videoElement.videoWidth,height:this.videoElement.videoHeight,isRotated:this.videoElement.classList.contains("rotate-90ccw")}:null}isActive(){return null!==this.currentStream&&this.currentStream.getTracks().some(e=>"live"===e.readyState)}getVideoElement(){return this.videoElement}populateCameraSelect(e){if(e.innerHTML="",0===this.availableCameras.length){const t=document.createElement("option");return t.value="",t.textContent="No cameras available",t.disabled=!0,void e.appendChild(t)}this.availableCameras.forEach(t=>{const a=document.createElement("option");a.value=t.id,a.textContent=t.label,e.appendChild(a)}),this.selectedCameraId&&(e.value=this.selectedCameraId)}onDeviceChange(e){navigator.mediaDevices.addEventListener("devicechange",async()=>{console.log("Camera devices changed");try{await this.enumerateCameras(),e(this.availableCameras)}catch(t){console.error("Error handling device change:",t),e([])}})}dispose(){this.stopCamera(),this.availableCameras=[],this.selectedCameraId=null,this.videoElement=null,console.log("Camera manager disposed")}}class t{static createCanvas(e,t){const a=document.createElement("canvas");return a.width=e,a.height=t,a}static rotateImage90CCW(e){const{width:t,height:a,data:n}=e,s=this.createCanvas(a,t).getContext("2d"),i=this.createCanvas(t,a);return i.getContext("2d").putImageData(e,0,0),s.translate(a,0),s.rotate(Math.PI/2),s.drawImage(i,0,0),s.getImageData(0,0,a,t)}static resizeImage(e,t,a){const{width:n,height:s}=e,i=this.createCanvas(n,s);i.getContext("2d").putImageData(e,0,0);const o=this.createCanvas(t,a).getContext("2d");return o.drawImage(i,0,0,n,s,0,0,t,a),o.getImageData(0,0,t,a)}static getVideoFrame(e){const t=this.createCanvas(e.videoWidth,e.videoHeight).getContext("2d");return t.drawImage(e,0,0),t.getImageData(0,0,e.videoWidth,e.videoHeight)}static normalizeImageData(e){const{width:t,height:a,data:n}=e,s=new Float32Array(3*t*a),i=[.485,.456,.406],o=[.229,.224,.225];let r=0;for(let e=0;e<n.length;e+=4){const l=n[e]/255,c=n[e+1]/255,d=n[e+2]/255;s[r]=(l-i[0])/o[0],s[r+t*a]=(c-i[1])/o[1],s[r+2*t*a]=(d-i[2])/o[2],r++}return s}static preprocessVideoFrame(e,t,a){try{let n=this.getVideoFrame(e);return n=this.cropCenter3to4(n),n.width===t&&n.height===a||(n=this.resizeImage(n,t,a)),this.normalizeImageData(n)}catch(e){throw console.error("Error preprocessing video frame:",e),e}}static createInputTensor(e,t,a){return{data:e,dims:[1,3,t,a],type:"float32"}}static processModelOutput(e,t,a){const n=new Uint8Array(t*a);for(let s=0;s<t*a;s++){const i=e[s],o=e[s+t*a];n[s]=o>i?255:0}return n}static drawSegmentationOverlay(e,t,a,n){let s=arguments.length>4&&void 0!==arguments[4]&&arguments[4];const i=n.getContext("2d"),o=this.createCanvas(t,a),r=o.getContext("2d"),l=r.createImageData(t,a);for(let t=0;t<e.length;t++){const a=4*t;e[t]>0?(l.data[a]=0,l.data[a+1]=255,l.data[a+2]=255,l.data[a+3]=128):l.data[a+3]=0}r.putImageData(l,0,0),i.clearRect(0,0,n.width,n.height),s=!1,i.drawImage(o,0,0,t,a,0,0,n.width,n.height)}static shouldRotateCamera(e,t){return e>t}static shouldRotateForModel(e,t){return 640===e&&480===t}static cropCenter3to4(e){const{width:t,height:a}=e;let n,s;t/a>3/4?(s=a,n=Math.floor(3*a/4)):(n=t,s=Math.floor(4*t/3));const i=Math.floor((t-n)/2),o=Math.floor((a-s)/2),r=this.createCanvas(t,a);r.getContext("2d").putImageData(e,0,0);const l=this.createCanvas(n,s).getContext("2d");return l.drawImage(r,i,o,n,s,0,0,n,s),l.getImageData(0,0,n,s)}}class a{constructor(){this.session=null,this.isModelLoaded=!1,this.isInferring=!1,this.modelPath="models/card_segmentation_fp16.onnx",this.inputHeight=320,this.inputWidth=240,this.numClasses=2,this.accelerationInfo={activeProvider:"unknown",availableProviders:[],gpuInfo:null,supportsWebGL:!1,supportsWebGPU:!1},this.inferenceStats={totalInferences:0,totalTime:0,averageTime:0,lastInferenceTime:0}}async initialize(){try{console.log("Initializing ONNX Runtime with GPU acceleration..."),ort.env.wasm.wasmPaths="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.22.0/dist/",ort.env.wasm.numThreads=1,await this.detectGPUCapabilities();const e=this.getOptimalExecutionProviders();return console.log("Attempting to load model with providers:",e),console.log(`Loading model from: ${this.modelPath}`),this.session=await this.loadModelWithFallback(e),console.log(`Model loaded successfully with provider: ${this.accelerationInfo.activeProvider}`),this.isModelLoaded=!0,this.logModelInfo(),this.logAccelerationInfo(),!0}catch(e){throw console.error("Failed to initialize model:",e),this.isModelLoaded=!1,new Error(`Model initialization failed: ${e.message}`)}}async detectGPUCapabilities(){console.log("Detecting GPU capabilities..."),this.accelerationInfo.supportsWebGL=this.checkWebGLSupport(),this.accelerationInfo.supportsWebGPU=await this.checkWebGPUSupport(),this.accelerationInfo.supportsWebGL&&(this.accelerationInfo.gpuInfo=this.getWebGLInfo()),console.log("GPU Capabilities:",{WebGL:this.accelerationInfo.supportsWebGL,WebGPU:this.accelerationInfo.supportsWebGPU,GPU:this.accelerationInfo.gpuInfo})}checkWebGLSupport(){try{const e=document.createElement("canvas");return!!(e.getContext("webgl")||e.getContext("experimental-webgl"))}catch(e){return console.warn("WebGL check failed:",e),!1}}async checkWebGPUSupport(){try{if(!navigator.gpu)return!1;return!!await navigator.gpu.requestAdapter()}catch(e){return console.warn("WebGPU check failed:",e),!1}}getWebGLInfo(){try{const e=document.createElement("canvas"),t=e.getContext("webgl")||e.getContext("experimental-webgl");if(!t)return null;const a=t.getExtension("WEBGL_debug_renderer_info");return{vendor:a?t.getParameter(a.UNMASKED_VENDOR_WEBGL):t.getParameter(t.VENDOR),renderer:a?t.getParameter(a.UNMASKED_RENDERER_WEBGL):t.getParameter(t.RENDERER),version:t.getParameter(t.VERSION),shadingLanguageVersion:t.getParameter(t.SHADING_LANGUAGE_VERSION)}}catch(e){return console.warn("Failed to get WebGL info:",e),null}}getOptimalExecutionProviders(){const e=[];return this.accelerationInfo.supportsWebGPU&&(e.push("webgpu"),this.accelerationInfo.availableProviders.push("webgpu")),this.accelerationInfo.supportsWebGL&&(e.push("webgl"),this.accelerationInfo.availableProviders.push("webgl")),e.push("wasm"),this.accelerationInfo.availableProviders.push("wasm"),e}async loadModelWithFallback(e){const t={enableMemPattern:!1,enableCpuMemArena:!1,graphOptimizationLevel:"all"};for(const n of e)try{console.log(`Attempting to load model with ${n} provider...`);const e={...t,executionProviders:[n]},a=await ort.InferenceSession.create(this.modelPath,e);return this.accelerationInfo.activeProvider=n,console.log(`✓ Successfully loaded with ${n} provider`),a}catch(e){const t=e.message;if(t.includes("cannot resolve operator")){var a;const e=(null===(a=t.match(/operator '(\w+)'/))||void 0===a?void 0:a[1])||"unknown";console.warn(`✗ ${n.toUpperCase()} provider doesn't support operator '${e}' - falling back to next provider`)}else console.warn(`✗ Failed to load with ${n} provider:`,t);this.accelerationInfo[`${n}Error`]=t;continue}throw new Error("Failed to load model with any execution provider")}logAccelerationInfo(){console.log("=== Acceleration Information ==="),console.log(`Active Provider: ${this.accelerationInfo.activeProvider}`),console.log(`Available Providers: ${this.accelerationInfo.availableProviders.join(", ")}`),console.log(`WebGL Support: ${this.accelerationInfo.supportsWebGL}`),console.log(`WebGPU Support: ${this.accelerationInfo.supportsWebGPU}`),this.accelerationInfo.gpuInfo&&(console.log("GPU Information:"),console.log(` Vendor: ${this.accelerationInfo.gpuInfo.vendor}`),console.log(` Renderer: ${this.accelerationInfo.gpuInfo.renderer}`),console.log(` Version: ${this.accelerationInfo.gpuInfo.version}`)),console.log("===============================")}getAccelerationInfo(){return{...this.accelerationInfo}}logModelInfo(){this.session&&(console.log("=== Model Information ==="),console.log("Input tensors:"),this.session.inputNames.forEach((e,t)=>{try{if(this.session.inputs&&this.session.inputs[t]){const a=this.session.inputs[t];console.log(` ${e}: ${a.dims} (${a.type})`)}else console.log(` ${e}: metadata not available`)}catch(t){console.log(` ${e}: metadata not available`)}}),console.log("Output tensors:"),this.session.outputNames.forEach((e,t)=>{try{if(this.session.outputs&&this.session.outputs[t]){const a=this.session.outputs[t];console.log(` ${e}: ${a.dims} (${a.type})`)}else console.log(` ${e}: metadata not available`)}catch(t){console.log(` ${e}: metadata not available`)}}),console.log("========================"))}async runInference(e){if(!this.isModelLoaded||!this.session)throw new Error("Model not loaded. Call initialize() first.");if(this.isInferring)return console.warn("Inference already in progress, skipping..."),null;try{this.isInferring=!0;const t=performance.now(),a=new ort.Tensor("float32",e,[1,3,this.inputHeight,this.inputWidth]),n=this.session.inputNames[0],s=await this.session.run({[n]:a}),i=s[this.session.outputNames[0]],o=performance.now()-t;return this.updateInferenceStats(o),console.log(`Inference completed in ${o.toFixed(2)}ms`),i.data}catch(e){throw console.error("Inference failed:",e),new Error(`Inference failed: ${e.message}`)}finally{this.isInferring=!1}}async processVideoFrame(e){try{if(!this.isModelLoaded)throw new Error("Model not initialized");const a=e.videoWidth,n=e.videoHeight;if(0===a||0===n)throw new Error("Video not ready");const s=t.shouldRotateForModel(a,n),i=t.preprocessVideoFrame(e,this.inputWidth,this.inputHeight,s),o=await this.runInference(i);if(!o)return null;return{mask:t.processModelOutput(o,this.inputHeight,this.inputWidth),shouldRotateBack:s,stats:this.getInferenceStats()}}catch(e){throw console.error("Error processing video frame:",e),e}}updateInferenceStats(e){this.inferenceStats.totalInferences++,this.inferenceStats.totalTime+=e,this.inferenceStats.averageTime=this.inferenceStats.totalTime/this.inferenceStats.totalInferences,this.inferenceStats.lastInferenceTime=e}getInferenceStats(){return{...this.inferenceStats,fps:this.inferenceStats.lastInferenceTime>0?1e3/this.inferenceStats.lastInferenceTime:0}}resetStats(){this.inferenceStats={totalInferences:0,totalTime:0,averageTime:0,lastInferenceTime:0}}isReady(){return this.isModelLoaded&&!this.isInferring}getModelSpecs(){return{inputHeight:this.inputHeight,inputWidth:this.inputWidth,numClasses:this.numClasses,isLoaded:this.isModelLoaded}}dispose(){this.session&&(this.session.release(),this.session=null),this.isModelLoaded=!1,this.isInferring=!1,console.log("Model resources disposed")}}class n{constructor(){this.cameraManager=new e,this.modelInference=new a,this.elements={},this.state={isInitialized:!1,isModelLoaded:!1,isCameraActive:!1,isInferenceRunning:!1,currentError:null},this.inferenceLoopId=null,this.performanceStats={frameCount:0,lastFpsUpdate:0,fps:0}}async init(){try{console.log("Initializing Card Segmentation App..."),this.setupDOMElements(),this.setupEventListeners(),this.showLoading("Initializing cameras..."),await this.cameraManager.initialize(),this.populateCameraDropdown(),this.showLoading("Loading AI model..."),await this.modelInference.initialize(),this.updateAccelerationInfo(),this.hideLoading(),this.showCameraSelection(),this.state.isInitialized=!0,this.state.isModelLoaded=!0,console.log("Application initialized successfully")}catch(e){console.error("Application initialization failed:",e),this.showError("Failed to initialize application",e.message)}}setupDOMElements(){this.elements={cameraSelection:document.getElementById("cameraSelection"),videoContainer:document.getElementById("videoContainer"),loadingIndicator:document.getElementById("loadingIndicator"),errorDisplay:document.getElementById("errorDisplay"),cameraSelect:document.getElementById("cameraSelect"),startCamera:document.getElementById("startCamera"),videoElement:document.getElementById("videoElement"),overlayCanvas:document.getElementById("overlayCanvas"),toggleInference:document.getElementById("toggleInference"),switchCamera:document.getElementById("switchCamera"),resolutionInfo:document.getElementById("resolutionInfo"),fpsInfo:document.getElementById("fpsInfo"),inferenceStatus:document.getElementById("inferenceStatus"),accelerationInfo:document.getElementById("accelerationInfo"),loadingText:document.getElementById("loadingText"),errorMessage:document.getElementById("errorMessage"),retryButton:document.getElementById("retryButton")}}setupEventListeners(){this.elements.cameraSelect.addEventListener("change",e=>{this.elements.startCamera.disabled=!e.target.value}),this.elements.startCamera.addEventListener("click",()=>{this.startCamera()}),this.elements.toggleInference.addEventListener("click",()=>{this.toggleInference()}),this.elements.switchCamera.addEventListener("click",()=>{this.showCameraSelection()}),this.elements.retryButton.addEventListener("click",()=>{this.hideError(),this.init()}),this.elements.videoElement.addEventListener("loadedmetadata",()=>{this.setupOverlayCanvas()}),this.cameraManager.onDeviceChange(()=>{this.populateCameraDropdown()}),window.addEventListener("beforeunload",()=>{this.cleanup()})}populateCameraDropdown(){this.cameraManager.populateCameraSelect(this.elements.cameraSelect),this.elements.startCamera.disabled=""===this.elements.cameraSelect.value}async startCamera(){try{const e=this.elements.cameraSelect.value;if(!e)throw new Error("No camera selected");this.showLoading("Starting camera...");const t=await this.cameraManager.startCamera(this.elements.videoElement,e);this.updateResolutionInfo(t),this.setupOverlayCanvas(),this.hideLoading(),this.hideCameraSelection(),this.showVideoContainer(),this.state.isCameraActive=!0,console.log("Camera started successfully:",t)}catch(e){console.error("Failed to start camera:",e),this.showError("Failed to start camera",e.message)}}setupOverlayCanvas(){const e=this.elements.videoElement,t=this.elements.overlayCanvas;e.videoWidth&&e.videoHeight&&(t.width=e.videoWidth,t.height=e.videoHeight,e.classList.contains("rotate-90ccw")?t.classList.add("rotate-90ccw"):t.classList.remove("rotate-90ccw"))}toggleInference(){this.state.isInferenceRunning?this.stopInference():this.startInference()}startInference(){this.state.isModelLoaded&&this.state.isCameraActive?(this.state.isInferenceRunning=!0,this.elements.toggleInference.textContent="Stop Detection",this.elements.toggleInference.className="btn btn-danger",this.updateInferenceStatus("Detection: Running"),this.performanceStats.frameCount=0,this.performanceStats.lastFpsUpdate=performance.now(),this.runInferenceLoop(),console.log("Inference started")):this.showError("Cannot start inference","Camera or model not ready")}stopInference(){this.state.isInferenceRunning=!1,this.elements.toggleInference.textContent="Start Detection",this.elements.toggleInference.className="btn btn-success",this.updateInferenceStatus("Detection: Off"),this.inferenceLoopId&&(cancelAnimationFrame(this.inferenceLoopId),this.inferenceLoopId=null);this.elements.overlayCanvas.getContext("2d").clearRect(0,0,this.elements.overlayCanvas.width,this.elements.overlayCanvas.height),console.log("Inference stopped")}async runInferenceLoop(){if(this.state.isInferenceRunning){try{const e=await this.modelInference.processVideoFrame(this.elements.videoElement);e&&e.mask&&(t.drawSegmentationOverlay(e.mask,this.modelInference.inputWidth,this.modelInference.inputHeight,this.elements.overlayCanvas,e.shouldRotateBack),this.updatePerformanceStats(e.stats))}catch(e){console.error("Inference loop error:",e)}this.state.isInferenceRunning&&(this.inferenceLoopId=requestAnimationFrame(()=>this.runInferenceLoop()))}}updatePerformanceStats(e){this.performanceStats.frameCount++;const t=performance.now();t-this.performanceStats.lastFpsUpdate>=1e3&&(this.performanceStats.fps=this.performanceStats.frameCount,this.performanceStats.frameCount=0,this.performanceStats.lastFpsUpdate=t,this.elements.fpsInfo.textContent=`FPS: ${this.performanceStats.fps}`)}updateResolutionInfo(e){const t=`Resolution: ${e.width}x${e.height}${e.rotated?" (rotated)":""}`;this.elements.resolutionInfo.textContent=t}updateInferenceStatus(e){this.elements.inferenceStatus.textContent=e,this.elements.inferenceStatus.className="status-inactive",e.includes("Running")?this.elements.inferenceStatus.className="status-active":e.includes("Processing")&&(this.elements.inferenceStatus.className="status-processing")}updateAccelerationInfo(){if(!this.elements.accelerationInfo)return;const e=this.modelInference.getAccelerationInfo();let t=`Acceleration: ${e.activeProvider.toUpperCase()}`;if(e.gpuInfo&&"wasm"!==e.activeProvider){const a=e.gpuInfo;t+=` (${a.vendor.includes("Google")?a.renderer.split(" ")[0]:a.vendor})`}"wasm"===e.activeProvider&&(e.webglError&&e.webglError.includes("cannot resolve operator")?t+=" (GPU unsupported operators)":e.supportsWebGL||e.supportsWebGPU?t+=" (GPU provider failed)":t+=" (No GPU support)"),this.elements.accelerationInfo.textContent=t,this.elements.accelerationInfo.className="acceleration-info","webgpu"===e.activeProvider?this.elements.accelerationInfo.classList.add("acceleration-webgpu"):"webgl"===e.activeProvider?this.elements.accelerationInfo.classList.add("acceleration-webgl"):this.elements.accelerationInfo.classList.add("acceleration-wasm")}showLoading(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"Loading...";this.elements.loadingText.textContent=e,this.elements.loadingIndicator.style.display="block",this.elements.cameraSelection.style.display="none",this.elements.videoContainer.style.display="none",this.elements.errorDisplay.style.display="none"}hideLoading(){this.elements.loadingIndicator.style.display="none"}showCameraSelection(){this.stopInference(),this.cameraManager.stopCamera(),this.state.isCameraActive=!1,this.elements.cameraSelection.style.display="block",this.elements.videoContainer.style.display="none",this.elements.errorDisplay.style.display="none"}hideCameraSelection(){this.elements.cameraSelection.style.display="none"}showVideoContainer(){this.elements.videoContainer.style.display="block",this.elements.errorDisplay.style.display="none"}showError(e,t){this.elements.errorMessage.innerHTML=`<strong>${e}</strong><br>${t}`,this.elements.errorDisplay.style.display="block",this.elements.loadingIndicator.style.display="none",this.elements.cameraSelection.style.display="none",this.elements.videoContainer.style.display="none",this.state.currentError={title:e,message:t}}hideError(){this.elements.errorDisplay.style.display="none",this.state.currentError=null}cleanup(){console.log("Cleaning up application..."),this.stopInference(),this.cameraManager.dispose(),this.modelInference.dispose(),this.state={isInitialized:!1,isModelLoaded:!1,isCameraActive:!1,isInferenceRunning:!1,currentError:null}}}document.addEventListener("DOMContentLoaded",()=>{const e=new n;e.init(),window.cardSegmentationApp=e})}(); |