Spaces:
Running
Running
<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> |