Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="calibration"> | |
| <!-- Bouton retour home --> | |
| <button @click="goBack" class="btn-home" title="Retour à l'accueil"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> | |
| <polyline points="9,22 9,12 15,12 15,22"/> | |
| </svg> | |
| </button> | |
| <div class="main-content"> | |
| <div class="content-area"> | |
| <div class="video-display"> | |
| <div class="calibration-container"> | |
| <CalibrationArea | |
| ref="calibrationArea" | |
| :thumbnail="thumbnail" | |
| :calibrationPoints="calibrationPoints" | |
| :calibrationLines="calibrationLines" | |
| :selectedFieldPoint="selectedFieldPoint" | |
| :selectedFieldLine="selectedFieldLine" | |
| @update:thumbnail="updateThumbnail" | |
| @update:calibrationPoints="updateCalibrationPoints" | |
| @update:calibrationLines="updateCalibrationLines" | |
| @update:selectedFieldPoint="updateSelectedFieldPoint" | |
| @update:selectedFieldLine="updateSelectedFieldLine" | |
| @clear-calibration="clearCalibration" | |
| @process-calibration="processCalibration" | |
| /> | |
| </div> | |
| <div class="field-container"> | |
| <FootballField | |
| ref="footballField" | |
| @point-selected="handleFieldPointSelected" | |
| @line-selected="handleFieldLineSelected" | |
| :positionedPoints="calibrationPoints" | |
| :positionedLines="calibrationLines" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| import CalibrationArea from '@/components/CalibrationArea.vue' | |
| import FootballField from '@/components/FootballField.vue' | |
| import { useUploadStore } from '@/stores/upload' | |
| import { useCalibrationStore } from '@/stores/calibration' | |
| import api from '@/services/api' | |
| export default { | |
| name: 'ManualView', | |
| components: { | |
| CalibrationArea, | |
| FootballField | |
| }, | |
| setup() { | |
| const uploadStore = useUploadStore() | |
| const calibrationStore = useCalibrationStore() | |
| return { | |
| uploadStore, | |
| calibrationStore | |
| } | |
| }, | |
| data() { | |
| return { | |
| thumbnail: null, | |
| calibrationPoints: {}, | |
| selectedFieldPoint: null, | |
| calibrationLines: {}, | |
| selectedFieldLine: null | |
| } | |
| }, | |
| computed: { | |
| canProcess() { | |
| return Object.keys(this.calibrationLines).length > 0 || Object.keys(this.calibrationPoints).length > 0 | |
| } | |
| }, | |
| async created() { | |
| // Vérifier qu'un fichier est sélectionné | |
| if (!this.uploadStore.selectedFile) { | |
| this.$router.push('/') | |
| return | |
| } | |
| // Appliquer les styles fullscreen | |
| this.applyFullscreenStyles() | |
| // Charger l'image/vidéo | |
| await this.loadThumbnail() | |
| }, | |
| beforeUnmount() { | |
| // Restaurer les styles originaux | |
| this.removeFullscreenStyles() | |
| }, | |
| methods: { | |
| goBack() { | |
| this.removeFullscreenStyles() | |
| this.$router.push('/') | |
| }, | |
| applyFullscreenStyles() { | |
| const app = document.getElementById('app') | |
| if (app) { | |
| app.style.maxWidth = 'none' | |
| app.style.margin = '0' | |
| app.style.padding = '0' | |
| } | |
| }, | |
| removeFullscreenStyles() { | |
| const app = document.getElementById('app') | |
| if (app) { | |
| app.style.maxWidth = '1280px' | |
| app.style.margin = '0 auto' | |
| app.style.padding = '1rem' | |
| } | |
| }, | |
| async loadThumbnail() { | |
| try { | |
| this.thumbnail = null | |
| if (this.uploadStore.isImage) { | |
| // Pour les images, utiliser directement la preview | |
| this.thumbnail = this.uploadStore.filePreview | |
| } else if (this.uploadStore.isStaticVideo && this.uploadStore.extractedFrame) { | |
| // Pour les vidéos statiques, utiliser la frame déjà extraite | |
| console.log('Using extracted frame from static video:', this.uploadStore.selectedFile.name) | |
| this.thumbnail = URL.createObjectURL(this.uploadStore.extractedFrame) | |
| } else if (this.uploadStore.isVideo) { | |
| // Pour les autres vidéos, extraire la première frame | |
| console.log('Extracting first frame from video:', this.uploadStore.selectedFile.name) | |
| // Créer une URL pour le fichier vidéo | |
| const videoUrl = URL.createObjectURL(this.uploadStore.selectedFile) | |
| // Extraire la première frame avec un canvas | |
| const video = document.createElement('video') | |
| video.src = videoUrl | |
| video.muted = true // Important pour éviter les problèmes d'autoplay | |
| await new Promise((resolve, reject) => { | |
| video.addEventListener('loadedmetadata', () => { | |
| video.currentTime = 0 // Aller à la première frame | |
| }) | |
| video.addEventListener('seeked', () => { | |
| try { | |
| const canvas = document.createElement('canvas') | |
| canvas.width = video.videoWidth | |
| canvas.height = video.videoHeight | |
| const ctx = canvas.getContext('2d') | |
| ctx.drawImage(video, 0, 0) | |
| this.thumbnail = canvas.toDataURL('image/jpeg', 0.9) | |
| URL.revokeObjectURL(videoUrl) | |
| resolve() | |
| } catch (err) { | |
| URL.revokeObjectURL(videoUrl) | |
| reject(err) | |
| } | |
| }) | |
| video.addEventListener('error', (err) => { | |
| URL.revokeObjectURL(videoUrl) | |
| reject(err) | |
| }) | |
| }) | |
| } | |
| } catch (error) { | |
| console.error('Erreur lors du chargement du thumbnail:', error) | |
| this.thumbnail = null | |
| } | |
| }, | |
| handleFieldPointSelected(pointData) { | |
| this.selectedFieldLine = null | |
| this.selectedFieldPoint = pointData | |
| }, | |
| handleFieldLineSelected(lineData) { | |
| this.selectedFieldPoint = null | |
| this.selectedFieldLine = lineData | |
| }, | |
| updateThumbnail(newThumbnail) { | |
| this.thumbnail = newThumbnail | |
| }, | |
| updateCalibrationPoints(newPoints) { | |
| this.calibrationPoints = { ...newPoints } | |
| }, | |
| updateCalibrationLines(newLines) { | |
| this.calibrationLines = { ...newLines } | |
| }, | |
| updateSelectedFieldPoint(newPoint) { | |
| this.selectedFieldPoint = newPoint | |
| }, | |
| updateSelectedFieldLine(newLine) { | |
| this.selectedFieldLine = newLine | |
| if (this.$refs.footballField) { | |
| this.$refs.footballField.selectedLine = newLine ? newLine.id : null | |
| } | |
| }, | |
| clearCalibration() { | |
| this.calibrationPoints = {} | |
| this.calibrationLines = {} | |
| this.selectedFieldPoint = null | |
| this.selectedFieldLine = null | |
| }, | |
| async processCalibration() { | |
| if (!this.uploadStore.selectedFile || Object.keys(this.calibrationLines).length === 0) { | |
| alert('Veuillez créer au moins une ligne de calibration') | |
| return | |
| } | |
| try { | |
| this.calibrationStore.setProcessing(true, 'Traitement de la calibration manuelle...') | |
| if (!this.$refs.calibrationArea) { | |
| console.error('CalibrationArea component is not mounted.') | |
| return | |
| } | |
| const imageContainer = document.querySelector('.video-frame') | |
| const imageSize = this.$refs.calibrationArea.imageSize | |
| if (!imageSize) { | |
| console.error('Image size is not available.') | |
| return | |
| } | |
| const containerWidth = imageContainer.clientWidth | |
| const containerHeight = imageContainer.clientHeight | |
| // Préparer les données des lignes pour l'API /calibrate | |
| const linesData = {} | |
| // Traitement des lignes - conversion des coordonnées container vers image | |
| for (const [lineName, line] of Object.entries(this.calibrationLines)) { | |
| linesData[lineName] = line.points.map(point => { | |
| return { | |
| x: point.x / containerWidth * imageSize.width, | |
| y: point.y / containerHeight * imageSize.height | |
| } | |
| }) | |
| } | |
| console.log('🔥 Données de lignes pour calibration:', linesData) | |
| // Appeler l'API /calibrate avec l'image et les lignes | |
| const result = await api.calibrateCamera(this.uploadStore.selectedFile, linesData) | |
| console.log('🔥 Réponse API calibration:', result) | |
| if (result.status === 'success') { | |
| this.calibrationStore.setResults(result) | |
| this.$router.push('/') | |
| } else if (result.status === 'failed') { | |
| throw new Error(result.message || "Échec du traitement de la calibration") | |
| } else { | |
| throw new Error(result.error || 'Erreur de traitement') | |
| } | |
| } catch (error) { | |
| console.error('❌ Erreur lors du traitement manuel:', error) | |
| this.calibrationStore.setError(error.message) | |
| this.$router.push('/') | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style scoped> | |
| .calibration { | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| color: white; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| } | |
| .btn-home { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| background: rgba(255, 255, 255, 0.1); | |
| color: #888; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 8px; | |
| padding: 10px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| z-index: 1000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| backdrop-filter: blur(10px); | |
| } | |
| .btn-home:hover { | |
| background: rgba(255, 255, 255, 0.15); | |
| color: var(--color-primary); | |
| border-color: var(--color-primary); | |
| transform: scale(1.05); | |
| } | |
| .btn-home svg { | |
| transition: all 0.3s ease; | |
| } | |
| .main-content { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| .content-area { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| padding: 0; | |
| } | |
| .video-display { | |
| flex: 1; | |
| display: flex; | |
| gap: 15px; | |
| padding: 15px; | |
| overflow: hidden; | |
| height: 100%; | |
| } | |
| .calibration-container { | |
| flex: 1; | |
| min-width: 0; | |
| display: flex; | |
| flex-direction: column; | |
| height: 100%; | |
| } | |
| .field-container { | |
| flex: 1; | |
| min-width: 0; | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| } | |
| .field-container :deep(svg) { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| margin-top: -10px; | |
| } | |
| .calibration-container :deep(.video-frame-container) { | |
| height: 100%; | |
| } | |
| .calibration-container :deep(.video-frame) { | |
| height: calc(100% - 80px); | |
| } | |
| .actions { | |
| display: flex; | |
| justify-content: center; | |
| background-color: #2a2a2a; | |
| border-top: 1px solid #333; | |
| } | |
| .btn-process { | |
| background: var(--color-primary); | |
| color: var(--color-secondary); | |
| border: none; | |
| padding: 1rem 2rem; | |
| border-radius: 8px; | |
| font-weight: 700; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .btn-process:hover:not(:disabled) { | |
| background: var(--color-primary-soft); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(217, 255, 4, 0.4); | |
| } | |
| .btn-process:disabled { | |
| background: #555; | |
| color: #888; | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .video-display { | |
| flex-direction: column; | |
| } | |
| .header-actions { | |
| padding: 1rem; | |
| } | |
| .header-actions h1 { | |
| font-size: 1.2rem; | |
| } | |
| .file-info { | |
| display: none; | |
| } | |
| } | |
| </style> |