Spaces:
Running
Running
| <template> | |
| <div class="zoom-view"> | |
| <div v-if="hasAnnotations" class="zoom-image-container"> | |
| <canvas | |
| ref="zoomCanvas" | |
| class="zoom-canvas" | |
| :width="zoomWidth" | |
| :height="zoomHeight" | |
| ></canvas> | |
| <div class="zoom-info"> | |
| <span>Frame {{ currentFrameNumber }}</span> | |
| <span v-if="zoomRegion">{{ Math.round(zoomRegion.width) }}x{{ Math.round(zoomRegion.height) }}px</span> | |
| <span v-if="zoomRegion?.type === 'points' && selectedAnnotations.length > 1"> | |
| {{ selectedAnnotations.length }} points | |
| </span> | |
| </div> | |
| </div> | |
| <div v-else class="no-annotations"> | |
| <p>Aucune annotation sur cette frame</p> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| import { useAnnotationStore } from '@/stores/annotationStore' | |
| import { useVideoStore } from '@/stores/videoStore' | |
| import { computed, ref, watch, nextTick, onMounted } from 'vue' | |
| export default { | |
| name: 'ZoomView', | |
| mounted() { | |
| // Forcer le rafraîchissement quand le composant est monté | |
| this.$nextTick(() => { | |
| setTimeout(() => { | |
| this.updateZoomImage() | |
| }, 100) // Petit délai pour s'assurer que la vidéo est prête | |
| }) | |
| }, | |
| setup() { | |
| const annotationStore = useAnnotationStore() | |
| const videoStore = useVideoStore() | |
| const zoomCanvas = ref(null) | |
| // Dimensions du canvas de zoom | |
| const zoomWidth = 200 | |
| const zoomHeight = 300 | |
| const getCurrentFrameNumber = () => { | |
| const frameRate = annotationStore.currentSession?.frameRate || 30 | |
| return Math.round(videoStore.currentTime * frameRate) | |
| } | |
| const currentFrameNumber = computed(() => getCurrentFrameNumber()) | |
| const selectedAnnotations = computed(() => { | |
| const currentFrame = getCurrentFrameNumber() | |
| const frameAnnotations = annotationStore.getAnnotationsForFrame(currentFrame) || [] | |
| return frameAnnotations.filter( | |
| annotation => annotation && annotation.objectId === annotationStore.selectedObjectId | |
| ) | |
| }) | |
| const hasAnnotations = computed(() => { | |
| return selectedAnnotations.value.length > 0 | |
| }) | |
| const zoomRegion = computed(() => { | |
| const annotations = selectedAnnotations.value | |
| if (!annotations.length) return null | |
| // Séparer rectangles et points | |
| const rectangles = annotations.filter(a => a.type === 'rectangle') | |
| const points = annotations.filter(a => a.type === 'point') | |
| if (rectangles.length > 0) { | |
| // Utiliser le premier rectangle trouvé | |
| const rect = rectangles[0] | |
| return { | |
| x: rect.x, | |
| y: rect.y, | |
| width: rect.width, | |
| height: rect.height, | |
| type: 'rectangle' | |
| } | |
| } else if (points.length > 0) { | |
| // Calculer le centre moyen des points | |
| const avgX = points.reduce((sum, p) => sum + p.x, 0) / points.length | |
| const avgY = points.reduce((sum, p) => sum + p.y, 0) / points.length | |
| if (points.length === 1) { | |
| // Pour un seul point, utiliser une taille fixe raisonnable | |
| const fixedSize = 120 | |
| return { | |
| x: avgX - fixedSize, | |
| y: avgY - fixedSize, | |
| width: fixedSize * 2, | |
| height: fixedSize * 2, | |
| type: 'points', | |
| centerX: avgX, | |
| centerY: avgY | |
| } | |
| } else { | |
| // Pour plusieurs points, calculer la bounding box englobante | |
| const minX = Math.min(...points.map(p => p.x)) | |
| const maxX = Math.max(...points.map(p => p.x)) | |
| const minY = Math.min(...points.map(p => p.y)) | |
| const maxY = Math.max(...points.map(p => p.y)) | |
| // Calculer les dimensions nécessaires | |
| const pointsWidth = maxX - minX | |
| const pointsHeight = maxY - minY | |
| // Ajouter une marge (minimum 60px de chaque côté) | |
| const marginX = Math.max(60, pointsWidth * 0.3) | |
| const marginY = Math.max(60, pointsHeight * 0.3) | |
| // Calculer les dimensions finales | |
| const finalWidth = pointsWidth + marginX * 2 | |
| const finalHeight = pointsHeight + marginY * 2 | |
| return { | |
| x: minX - marginX, | |
| y: minY - marginY, | |
| width: finalWidth, | |
| height: finalHeight, | |
| type: 'points', | |
| centerX: avgX, | |
| centerY: avgY, | |
| pointsBounds: { | |
| minX, maxX, minY, maxY, | |
| pointsWidth, pointsHeight | |
| } | |
| } | |
| } | |
| } | |
| return null | |
| }) | |
| const drawAnnotationsOnZoom = (ctx, sourceX, sourceY, sourceWidth, sourceHeight) => { | |
| const annotations = selectedAnnotations.value | |
| if (!annotations.length) return | |
| // Calculer le facteur d'échelle entre la source et le canvas | |
| const scaleX = zoomWidth / sourceWidth | |
| const scaleY = zoomHeight / sourceHeight | |
| annotations.forEach(annotation => { | |
| if (annotation.type === 'rectangle') { | |
| // Calculer la position du rectangle dans le canvas zoomé | |
| const rectX = (annotation.x - sourceX) * scaleX | |
| const rectY = (annotation.y - sourceY) * scaleY | |
| const rectWidth = annotation.width * scaleX | |
| const rectHeight = annotation.height * scaleY | |
| // Ne dessiner que si le rectangle est visible dans la zone | |
| if (rectX < zoomWidth && rectY < zoomHeight && | |
| rectX + rectWidth > 0 && rectY + rectHeight > 0) { | |
| // Dessiner le rectangle | |
| ctx.strokeStyle = '#00ff00' // Vert pour les rectangles | |
| ctx.lineWidth = 2 | |
| ctx.setLineDash([5, 3]) // Trait pointillé | |
| ctx.strokeRect(rectX, rectY, rectWidth, rectHeight) | |
| ctx.setLineDash([]) // Remettre trait plein | |
| } | |
| } else if (annotation.type === 'point') { | |
| // Calculer la position du point dans le canvas zoomé | |
| const pointX = (annotation.x - sourceX) * scaleX | |
| const pointY = (annotation.y - sourceY) * scaleY | |
| // Ne dessiner que si le point est visible dans la zone | |
| if (pointX >= 0 && pointX <= zoomWidth && pointY >= 0 && pointY <= zoomHeight) { | |
| // Couleur selon le type de point | |
| const pointColor = annotation.pointType === 'positive' ? '#00ff00' : '#ff0000' | |
| // Dessiner le cercle du point | |
| ctx.fillStyle = pointColor | |
| ctx.strokeStyle = '#ffffff' | |
| ctx.lineWidth = 2 | |
| ctx.beginPath() | |
| ctx.arc(pointX, pointY, 6, 0, 2 * Math.PI) | |
| ctx.fill() | |
| ctx.stroke() | |
| // Dessiner le symbole + ou - | |
| ctx.strokeStyle = '#ffffff' | |
| ctx.lineWidth = 2 | |
| ctx.beginPath() | |
| if (annotation.pointType === 'positive') { | |
| // Dessiner + | |
| ctx.moveTo(pointX - 3, pointY) | |
| ctx.lineTo(pointX + 3, pointY) | |
| ctx.moveTo(pointX, pointY - 3) | |
| ctx.lineTo(pointX, pointY + 3) | |
| } else { | |
| // Dessiner - | |
| ctx.moveTo(pointX - 3, pointY) | |
| ctx.lineTo(pointX + 3, pointY) | |
| } | |
| ctx.stroke() | |
| } | |
| } | |
| }) | |
| // Si c'est une vue centrée sur des points, dessiner une croix de repère au centre | |
| if (zoomRegion.value?.type === 'points') { | |
| ctx.strokeStyle = '#ffff00' // Jaune pour le centre | |
| ctx.lineWidth = 1 | |
| ctx.setLineDash([3, 3]) | |
| ctx.beginPath() | |
| const centerX = zoomWidth / 2 | |
| const centerY = zoomHeight / 2 | |
| ctx.moveTo(centerX - 15, centerY) | |
| ctx.lineTo(centerX + 15, centerY) | |
| ctx.moveTo(centerX, centerY - 15) | |
| ctx.lineTo(centerX, centerY + 15) | |
| ctx.stroke() | |
| ctx.setLineDash([]) | |
| } | |
| } | |
| const updateZoomImage = async () => { | |
| if (!zoomCanvas.value || !zoomRegion.value) return | |
| // Trouver l'élément vidéo | |
| const videoElement = document.querySelector('video') | |
| if (!videoElement) return | |
| const canvas = zoomCanvas.value | |
| const ctx = canvas.getContext('2d') | |
| // Effacer le canvas | |
| ctx.clearRect(0, 0, zoomWidth, zoomHeight) | |
| try { | |
| // Calculer les coordonnées source dans la vidéo | |
| const sourceX = Math.max(0, zoomRegion.value.x) | |
| const sourceY = Math.max(0, zoomRegion.value.y) | |
| const sourceWidth = Math.min(zoomRegion.value.width, videoElement.videoWidth - sourceX) | |
| const sourceHeight = Math.min(zoomRegion.value.height, videoElement.videoHeight - sourceY) | |
| // S'assurer que les dimensions sont valides | |
| if (sourceWidth <= 0 || sourceHeight <= 0) return | |
| // Dessiner la région zoomée sur le canvas | |
| ctx.drawImage( | |
| videoElement, | |
| sourceX, sourceY, sourceWidth, sourceHeight, // Source (région de la vidéo) | |
| 0, 0, zoomWidth, zoomHeight // Destination (canvas) | |
| ) | |
| // Dessiner les annotations sur l'image zoomée | |
| drawAnnotationsOnZoom(ctx, sourceX, sourceY, sourceWidth, sourceHeight) | |
| } catch (error) { | |
| console.error('Erreur lors de la capture du zoom:', error) | |
| // Afficher un message d'erreur sur le canvas | |
| ctx.fillStyle = '#666' | |
| ctx.fillRect(0, 0, zoomWidth, zoomHeight) | |
| ctx.fillStyle = '#fff' | |
| ctx.font = '14px Arial' | |
| ctx.textAlign = 'center' | |
| ctx.fillText('Erreur de capture', zoomWidth / 2, zoomHeight / 2) | |
| } | |
| } | |
| // Watcher pour mettre à jour l'image quand les annotations changent | |
| watch([selectedAnnotations, currentFrameNumber], () => { | |
| nextTick(() => { | |
| updateZoomImage() | |
| }) | |
| }, { deep: true }) | |
| // Watcher pour mettre à jour quand le temps de la vidéo change | |
| watch(() => videoStore.currentTime, () => { | |
| nextTick(() => { | |
| updateZoomImage() | |
| }) | |
| }) | |
| // Hook onMounted pour forcer le rafraîchissement au montage | |
| onMounted(() => { | |
| nextTick(() => { | |
| setTimeout(() => { | |
| updateZoomImage() | |
| }, 200) // Délai plus long pour s'assurer que tout est prêt | |
| }) | |
| }) | |
| return { | |
| selectedAnnotations, | |
| hasAnnotations, | |
| zoomRegion, | |
| currentFrameNumber, | |
| zoomCanvas, | |
| zoomWidth, | |
| zoomHeight, | |
| updateZoomImage | |
| } | |
| } | |
| } | |
| </script> | |
| <style scoped> | |
| .zoom-view { | |
| height: 100%; | |
| padding: 10px; | |
| color: white; | |
| overflow: auto; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .zoom-image-container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .zoom-canvas { | |
| border: 1px solid #555; | |
| border-radius: 4px; | |
| background: #000; | |
| } | |
| .zoom-info { | |
| display: flex; | |
| gap: 12px; | |
| font-size: 0.8rem; | |
| color: #ccc; | |
| } | |
| .no-annotations { | |
| text-align: center; | |
| color: #888; | |
| font-style: italic; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| } | |
| .no-annotations p { | |
| margin: 0; | |
| } | |
| </style> |