This commit is contained in:
Linrador 2026-07-01 09:36:52 +02:00
parent 93b51978c5
commit 4484fb978d
4 changed files with 142 additions and 68 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4523,10 +4523,14 @@ export default function TrainingTab(props: {
const poseFaceImagePanCenterRef = useRef<{ x: number; y: number } | null>(null) const poseFaceImagePanCenterRef = useRef<{ x: number; y: number } | null>(null)
const poseFaceImagePanGestureRef = useRef<PoseFaceImagePanGesture | null>(null) const poseFaceImagePanGestureRef = useRef<PoseFaceImagePanGesture | null>(null)
const poseFaceImageDisplayCameraRef = useRef<PoseFaceImageCamera | null>(null) const poseFaceImageDisplayCameraRef = useRef<PoseFaceImageCamera | null>(null)
const poseFaceImageCameraRef = useRef<PoseFaceImageCamera | null>(null)
const poseFaceImageCameraAnimationFrameRef = useRef<number | null>(null) const poseFaceImageCameraAnimationFrameRef = useRef<number | null>(null)
const poseFaceImageZoomAnimationResolveRef = useRef<(() => void) | null>(null)
const poseFaceImageZoomAppliedRef = useRef(false)
const poseFaceImageZoomOutActiveRef = useRef(false) const poseFaceImageZoomOutActiveRef = useRef(false)
const correctionRef = useRef<CorrectionState>(correction) const correctionRef = useRef<CorrectionState>(correction)
const hasManualCorrectionRef = useRef(hasManualCorrection) const hasManualCorrectionRef = useRef(hasManualCorrection)
const saveFeedbackInFlightRef = useRef(false)
const latestGestureBoxRef = useRef<TrainingBox | null>(null) const latestGestureBoxRef = useRef<TrainingBox | null>(null)
const pendingPointerMoveRef = useRef<{ clientX: number; clientY: number } | null>(null) const pendingPointerMoveRef = useRef<{ clientX: number; clientY: number } | null>(null)
const pointerMoveRafRef = useRef<number | null>(null) const pointerMoveRafRef = useRef<number | null>(null)
@ -4634,8 +4638,14 @@ export default function TrainingTab(props: {
cancelAnimationFrame(poseFaceImageCameraAnimationFrameRef.current) cancelAnimationFrame(poseFaceImageCameraAnimationFrameRef.current)
poseFaceImageCameraAnimationFrameRef.current = null poseFaceImageCameraAnimationFrameRef.current = null
} }
if (poseFaceImageZoomAnimationResolveRef.current !== null) {
poseFaceImageZoomAnimationResolveRef.current()
poseFaceImageZoomAnimationResolveRef.current = null
}
poseFaceImageCameraRef.current = null
poseFaceImageDisplayCameraRef.current = null poseFaceImageDisplayCameraRef.current = null
poseFaceImageZoomAppliedRef.current = false
poseFaceImageZoomOutActiveRef.current = false poseFaceImageZoomOutActiveRef.current = false
setPoseFaceImageDisplayCamera(null) setPoseFaceImageDisplayCamera(null)
setPoseFaceImageCameraAnimating(false) setPoseFaceImageCameraAnimating(false)
@ -4653,6 +4663,87 @@ export default function TrainingTab(props: {
setPoseKeypointMapAnimating(false) setPoseKeypointMapAnimating(false)
}, []) }, [])
const zoomOutPoseFaceImageToFullImage = useCallback(() => {
if (poseFaceImageZoomAnimationResolveRef.current !== null) {
poseFaceImageZoomAnimationResolveRef.current()
poseFaceImageZoomAnimationResolveRef.current = null
}
if (poseFaceImageCameraAnimationFrameRef.current !== null) {
cancelAnimationFrame(poseFaceImageCameraAnimationFrameRef.current)
poseFaceImageCameraAnimationFrameRef.current = null
}
const fromCamera =
poseFaceImageDisplayCameraRef.current ??
poseFaceImageCameraRef.current ??
poseFaceImageFullCamera()
const toCamera = poseFaceImageFullCamera()
const shouldAnimate =
poseFaceImageZoomAppliedRef.current ||
poseFaceImageZoomOutActiveRef.current ||
Math.abs(fromCamera.scale - toCamera.scale) > 0.001 ||
Math.abs(fromCamera.translateX - toCamera.translateX) > 0.5 ||
Math.abs(fromCamera.translateY - toCamera.translateY) > 0.5
switchPoseKeypointMapView('body')
if (!shouldAnimate) {
return Promise.resolve()
}
return new Promise<void>((resolve) => {
let settled = false
const settle = () => {
if (settled) return
settled = true
if (poseFaceImageZoomAnimationResolveRef.current === settle) {
poseFaceImageZoomAnimationResolveRef.current = null
}
resolve()
}
const finish = () => {
poseFaceImageCameraAnimationFrameRef.current = null
poseFaceImageDisplayCameraRef.current = null
poseFaceImageZoomOutActiveRef.current = false
poseFaceImageZoomAppliedRef.current = false
setPoseFaceImageDisplayCamera(null)
setPoseFaceImageCameraAnimating(false)
setPoseFaceImageZoomOutActive(false)
settle()
}
poseFaceImagePanGestureRef.current = null
poseFaceImagePanCenterRef.current = null
setPoseFaceImagePanCenter(null)
setPoseFaceImagePanning(false)
poseFaceImageZoomAnimationResolveRef.current = settle
poseFaceImageZoomOutActiveRef.current = true
setPoseFaceImageZoomOutActive(true)
setPoseFaceImageCameraAnimating(true)
poseFaceImageDisplayCameraRef.current = fromCamera
setPoseFaceImageDisplayCamera(fromCamera)
const startedAt = performance.now()
const animateZoomOut = (now: number) => {
const progress = Math.min(1, (now - startedAt) / POSE_FACE_IMAGE_ZOOM_DURATION_MS)
const nextCamera = interpolatePoseFaceImageCamera(fromCamera, toCamera, progress)
poseFaceImageDisplayCameraRef.current = nextCamera
setPoseFaceImageDisplayCamera(nextCamera)
if (progress < 1) {
poseFaceImageCameraAnimationFrameRef.current = requestAnimationFrame(animateZoomOut)
return
}
finish()
}
poseFaceImageCameraAnimationFrameRef.current = requestAnimationFrame(animateZoomOut)
})
}, [switchPoseKeypointMapView])
const labelsRef = useRef<TrainingLabels>(emptyLabels) const labelsRef = useRef<TrainingLabels>(emptyLabels)
useEffect(() => { useEffect(() => {
@ -4790,13 +4881,14 @@ export default function TrainingTab(props: {
) )
const activePosePerson = const activePosePerson =
activePosePersonIndex !== null ? posePersons[activePosePersonIndex] : undefined activePosePersonIndex !== null ? posePersons[activePosePersonIndex] : undefined
const activePosePersonVisible = Boolean( const activePosePersonSelectable = Boolean(
activePosePerson && activePosePersonIndex !== null &&
(hasVisiblePoseBox(activePosePerson) || activePosePersonIndex >= 0 &&
activePosePerson.keypoints?.some(isPoseKeypointVisible)) activePosePersonIndex < posePersons.length &&
activePosePerson
) )
const poseCorrectionPersonIndex = const poseCorrectionPersonIndex =
activePosePersonIndex !== null && activePosePersonVisible activePosePersonIndex !== null && activePosePersonSelectable
? activePosePersonIndex ? activePosePersonIndex
: visiblePosePersonCount === 1 : visiblePosePersonCount === 1
? firstVisiblePosePersonIndex ? firstVisiblePosePersonIndex
@ -7111,15 +7203,18 @@ export default function TrainingTab(props: {
negative?: boolean negative?: boolean
} }
) => { ) => {
if (!sample || trainingRunning) return if (!sample || trainingRunning || saveFeedbackInFlightRef.current) return
resetPoseUiToBoxes() saveFeedbackInFlightRef.current = true
setSavingOverlayText(editingFeedback ? 'Feedback wird aktualisiert…' : 'Feedback wird gespeichert…')
setSaving(true)
setError(null)
setMessage(null)
try { try {
await zoomOutPoseFaceImageToFullImage()
resetPoseUiToBoxes()
setSavingOverlayText(editingFeedback ? 'Feedback wird aktualisiert…' : 'Feedback wird gespeichert…')
setSaving(true)
setError(null)
setMessage(null)
const normalizedBoxes = (correction.boxes ?? []) const normalizedBoxes = (correction.boxes ?? [])
.map(normalizeBox) .map(normalizeBox)
.filter((box) => box.label && box.w > 0 && box.h > 0) .filter((box) => box.label && box.w > 0 && box.h > 0)
@ -7262,6 +7357,7 @@ export default function TrainingTab(props: {
setError(`Feedback konnte nicht gespeichert werden. ${short}`) setError(`Feedback konnte nicht gespeichert werden. ${short}`)
} finally { } finally {
saveFeedbackInFlightRef.current = false
setSaving(false) setSaving(false)
setSavingOverlayText('') setSavingOverlayText('')
} }
@ -7276,6 +7372,7 @@ export default function TrainingTab(props: {
loadNextImportedQueuedSample, loadNextImportedQueuedSample,
resetPoseUiToBoxes, resetPoseUiToBoxes,
trainingRunning, trainingRunning,
zoomOutPoseFaceImageToFullImage,
] ]
) )
@ -8676,6 +8773,8 @@ export default function TrainingTab(props: {
}, },
} }
}, [imageBoxSize.height, imageBoxSize.width, imageLayerStyle, poseFaceImageEffectiveZoomRegion, poseFaceImageZoomActive]) }, [imageBoxSize.height, imageBoxSize.width, imageLayerStyle, poseFaceImageEffectiveZoomRegion, poseFaceImageZoomActive])
poseFaceImageCameraRef.current = poseFaceImageCamera
useEffect(() => { useEffect(() => {
if (poseFaceImageZoomOutActiveRef.current) { if (poseFaceImageZoomOutActiveRef.current) {
return return
@ -8753,6 +8852,8 @@ export default function TrainingTab(props: {
} }
}, [poseFaceImageRenderedCamera]) }, [poseFaceImageRenderedCamera])
const poseFaceImageZoomApplied = Boolean(poseFaceImageZoomStyle) const poseFaceImageZoomApplied = Boolean(poseFaceImageZoomStyle)
poseFaceImageZoomAppliedRef.current = poseFaceImageZoomApplied
const setClampedPoseFaceImagePanCenter = useCallback(( const setClampedPoseFaceImagePanCenter = useCallback((
centerX: number, centerX: number,
centerY: number, centerY: number,
@ -8907,54 +9008,8 @@ export default function TrainingTab(props: {
}, [setClampedPoseFaceImagePanCenter]) }, [setClampedPoseFaceImagePanCenter])
const zoomOutPoseFaceImageToBody = useCallback(() => { const zoomOutPoseFaceImageToBody = useCallback(() => {
if (poseFaceImageCameraAnimationFrameRef.current !== null) { void zoomOutPoseFaceImageToFullImage()
cancelAnimationFrame(poseFaceImageCameraAnimationFrameRef.current) }, [zoomOutPoseFaceImageToFullImage])
poseFaceImageCameraAnimationFrameRef.current = null
}
const fromCamera =
poseFaceImageDisplayCameraRef.current ??
poseFaceImageCamera ??
poseFaceImageFullCamera()
const toCamera = poseFaceImageFullCamera()
const startedAt = performance.now()
poseFaceImagePanGestureRef.current = null
poseFaceImagePanCenterRef.current = null
setPoseFaceImagePanCenter(null)
setPoseFaceImagePanning(false)
poseFaceImageZoomOutActiveRef.current = true
setPoseFaceImageZoomOutActive(true)
setPoseFaceImageCameraAnimating(true)
poseFaceImageDisplayCameraRef.current = fromCamera
setPoseFaceImageDisplayCamera(fromCamera)
// Die rechte Map darf parallel zurueckfahren; die Bildkamera bleibt durch
// den Override sichtbar, bis der Zoom-Out fertig ist.
switchPoseKeypointMapView('body')
const animateZoomOut = (now: number) => {
const progress = Math.min(1, (now - startedAt) / POSE_FACE_IMAGE_ZOOM_DURATION_MS)
const nextCamera = interpolatePoseFaceImageCamera(fromCamera, toCamera, progress)
poseFaceImageDisplayCameraRef.current = nextCamera
setPoseFaceImageDisplayCamera(nextCamera)
if (progress < 1) {
poseFaceImageCameraAnimationFrameRef.current = requestAnimationFrame(animateZoomOut)
return
}
poseFaceImageCameraAnimationFrameRef.current = null
poseFaceImageDisplayCameraRef.current = null
poseFaceImageZoomOutActiveRef.current = false
setPoseFaceImageDisplayCamera(null)
setPoseFaceImageCameraAnimating(false)
setPoseFaceImageZoomOutActive(false)
}
poseFaceImageCameraAnimationFrameRef.current = requestAnimationFrame(animateZoomOut)
}, [poseFaceImageCamera, switchPoseKeypointMapView])
const deletePosePerson = useCallback((personIndex: number) => { const deletePosePerson = useCallback((personIndex: number) => {
const shouldNavigateAfterDelete = const shouldNavigateAfterDelete =
@ -10426,7 +10481,7 @@ export default function TrainingTab(props: {
.filter(isPoseKeypointVisible) .filter(isPoseKeypointVisible)
.map((point) => poseKeypointId(point.name)) .map((point) => poseKeypointId(point.name))
) )
const disabled = uiLocked || frameBusy || trainingRunning || saving || !targetPerson const disabled = uiLocked || frameBusy || trainingRunning || saving || !poseTrainingAllowed
const faceVisibleCount = POSE_FACE_KEYPOINT_SHAPES.filter((shape) => const faceVisibleCount = POSE_FACE_KEYPOINT_SHAPES.filter((shape) =>
visibleKeypointIds.has(shape.keypointName) visibleKeypointIds.has(shape.keypointName)
).length ).length
@ -10459,10 +10514,29 @@ export default function TrainingTab(props: {
const showMapParts = !poseKeypointMapAnimating const showMapParts = !poseKeypointMapAnimating
const selectPoseKeypoint = (keypointName: string) => { const selectPoseKeypoint = (keypointName: string) => {
if (disabled || targetIndex === null) return if (disabled) return
let selectionPersonIndex = targetIndex
if (selectionPersonIndex === null || !targetPerson) {
const persons = clonePosePersons(correctionRef.current.posePersons)
selectionPersonIndex = persons.length
const next: CorrectionState = {
...correctionRef.current,
posePersons: [...persons, createEmptyPosePerson()],
}
markManualCorrection()
correctionRef.current = next
setCorrection(next)
resetBoxDrawingMode()
setShowPoseSkeleton(true)
setActivePosePersonIndex(selectionPersonIndex)
setMobilePanel('labels')
}
const pending = const pending =
pendingPoseKeypoint?.personIndex === targetIndex && pendingPoseKeypoint?.personIndex === selectionPersonIndex &&
pendingPoseKeypoint.keypointName === keypointName pendingPoseKeypoint.keypointName === keypointName
if (pending) { if (pending) {
@ -10472,10 +10546,10 @@ export default function TrainingTab(props: {
} }
setShowPoseSkeleton(true) setShowPoseSkeleton(true)
setActivePosePersonIndex(targetIndex) setActivePosePersonIndex(selectionPersonIndex)
setSelectedPoseKeypointAction(null) setSelectedPoseKeypointAction(null)
setPendingPoseKeypoint({ setPendingPoseKeypoint({
personIndex: targetIndex, personIndex: selectionPersonIndex,
keypointName, keypointName,
}) })
} }
@ -10492,9 +10566,9 @@ export default function TrainingTab(props: {
<div className="mt-0.5 text-[10px] text-gray-500 dark:text-gray-400"> <div className="mt-0.5 text-[10px] text-gray-500 dark:text-gray-400">
{targetPerson {targetPerson
? `${targetPoseLabel}: Punkt wählen und im Bild setzen.` ? `${targetPoseLabel}: Punkt wählen und im Bild setzen.`
: visiblePosePersonCount > 1 : poseTrainingAllowed
? 'Links erst eine Pose auswählen.' ? 'Körperteil wählen, neue Pose wird angelegt.'
: 'Keine Pose vorhanden.'} : 'Für Unknown wird keine Pose trainiert.'}
</div> </div>
</div> </div>
@ -10530,7 +10604,7 @@ export default function TrainingTab(props: {
height="1386" height="1386"
preserveAspectRatio="xMidYMid meet" preserveAspectRatio="xMidYMid meet"
pointerEvents="none" pointerEvents="none"
className={targetPerson ? 'opacity-90' : 'opacity-35'} className={disabled ? 'opacity-35' : 'opacity-90'}
/> />
{showMapParts && poseKeypointMapView === 'head' ? ( {showMapParts && poseKeypointMapView === 'head' ? (