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 poseFaceImagePanGestureRef = useRef<PoseFaceImagePanGesture | null>(null)
const poseFaceImageDisplayCameraRef = useRef<PoseFaceImageCamera | null>(null)
const poseFaceImageCameraRef = useRef<PoseFaceImageCamera | null>(null)
const poseFaceImageCameraAnimationFrameRef = useRef<number | null>(null)
const poseFaceImageZoomAnimationResolveRef = useRef<(() => void) | null>(null)
const poseFaceImageZoomAppliedRef = useRef(false)
const poseFaceImageZoomOutActiveRef = useRef(false)
const correctionRef = useRef<CorrectionState>(correction)
const hasManualCorrectionRef = useRef(hasManualCorrection)
const saveFeedbackInFlightRef = useRef(false)
const latestGestureBoxRef = useRef<TrainingBox | null>(null)
const pendingPointerMoveRef = useRef<{ clientX: number; clientY: number } | null>(null)
const pointerMoveRafRef = useRef<number | null>(null)
@ -4634,8 +4638,14 @@ export default function TrainingTab(props: {
cancelAnimationFrame(poseFaceImageCameraAnimationFrameRef.current)
poseFaceImageCameraAnimationFrameRef.current = null
}
if (poseFaceImageZoomAnimationResolveRef.current !== null) {
poseFaceImageZoomAnimationResolveRef.current()
poseFaceImageZoomAnimationResolveRef.current = null
}
poseFaceImageCameraRef.current = null
poseFaceImageDisplayCameraRef.current = null
poseFaceImageZoomAppliedRef.current = false
poseFaceImageZoomOutActiveRef.current = false
setPoseFaceImageDisplayCamera(null)
setPoseFaceImageCameraAnimating(false)
@ -4653,6 +4663,87 @@ export default function TrainingTab(props: {
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)
useEffect(() => {
@ -4790,13 +4881,14 @@ export default function TrainingTab(props: {
)
const activePosePerson =
activePosePersonIndex !== null ? posePersons[activePosePersonIndex] : undefined
const activePosePersonVisible = Boolean(
activePosePerson &&
(hasVisiblePoseBox(activePosePerson) ||
activePosePerson.keypoints?.some(isPoseKeypointVisible))
const activePosePersonSelectable = Boolean(
activePosePersonIndex !== null &&
activePosePersonIndex >= 0 &&
activePosePersonIndex < posePersons.length &&
activePosePerson
)
const poseCorrectionPersonIndex =
activePosePersonIndex !== null && activePosePersonVisible
activePosePersonIndex !== null && activePosePersonSelectable
? activePosePersonIndex
: visiblePosePersonCount === 1
? firstVisiblePosePersonIndex
@ -7111,15 +7203,18 @@ export default function TrainingTab(props: {
negative?: boolean
}
) => {
if (!sample || trainingRunning) return
if (!sample || trainingRunning || saveFeedbackInFlightRef.current) return
saveFeedbackInFlightRef.current = true
try {
await zoomOutPoseFaceImageToFullImage()
resetPoseUiToBoxes()
setSavingOverlayText(editingFeedback ? 'Feedback wird aktualisiert…' : 'Feedback wird gespeichert…')
setSaving(true)
setError(null)
setMessage(null)
try {
const normalizedBoxes = (correction.boxes ?? [])
.map(normalizeBox)
.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}`)
} finally {
saveFeedbackInFlightRef.current = false
setSaving(false)
setSavingOverlayText('')
}
@ -7276,6 +7372,7 @@ export default function TrainingTab(props: {
loadNextImportedQueuedSample,
resetPoseUiToBoxes,
trainingRunning,
zoomOutPoseFaceImageToFullImage,
]
)
@ -8676,6 +8773,8 @@ export default function TrainingTab(props: {
},
}
}, [imageBoxSize.height, imageBoxSize.width, imageLayerStyle, poseFaceImageEffectiveZoomRegion, poseFaceImageZoomActive])
poseFaceImageCameraRef.current = poseFaceImageCamera
useEffect(() => {
if (poseFaceImageZoomOutActiveRef.current) {
return
@ -8753,6 +8852,8 @@ export default function TrainingTab(props: {
}
}, [poseFaceImageRenderedCamera])
const poseFaceImageZoomApplied = Boolean(poseFaceImageZoomStyle)
poseFaceImageZoomAppliedRef.current = poseFaceImageZoomApplied
const setClampedPoseFaceImagePanCenter = useCallback((
centerX: number,
centerY: number,
@ -8907,54 +9008,8 @@ export default function TrainingTab(props: {
}, [setClampedPoseFaceImagePanCenter])
const zoomOutPoseFaceImageToBody = useCallback(() => {
if (poseFaceImageCameraAnimationFrameRef.current !== null) {
cancelAnimationFrame(poseFaceImageCameraAnimationFrameRef.current)
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])
void zoomOutPoseFaceImageToFullImage()
}, [zoomOutPoseFaceImageToFullImage])
const deletePosePerson = useCallback((personIndex: number) => {
const shouldNavigateAfterDelete =
@ -10426,7 +10481,7 @@ export default function TrainingTab(props: {
.filter(isPoseKeypointVisible)
.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) =>
visibleKeypointIds.has(shape.keypointName)
).length
@ -10459,10 +10514,29 @@ export default function TrainingTab(props: {
const showMapParts = !poseKeypointMapAnimating
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 =
pendingPoseKeypoint?.personIndex === targetIndex &&
pendingPoseKeypoint?.personIndex === selectionPersonIndex &&
pendingPoseKeypoint.keypointName === keypointName
if (pending) {
@ -10472,10 +10546,10 @@ export default function TrainingTab(props: {
}
setShowPoseSkeleton(true)
setActivePosePersonIndex(targetIndex)
setActivePosePersonIndex(selectionPersonIndex)
setSelectedPoseKeypointAction(null)
setPendingPoseKeypoint({
personIndex: targetIndex,
personIndex: selectionPersonIndex,
keypointName,
})
}
@ -10492,9 +10566,9 @@ export default function TrainingTab(props: {
<div className="mt-0.5 text-[10px] text-gray-500 dark:text-gray-400">
{targetPerson
? `${targetPoseLabel}: Punkt wählen und im Bild setzen.`
: visiblePosePersonCount > 1
? 'Links erst eine Pose auswählen.'
: 'Keine Pose vorhanden.'}
: poseTrainingAllowed
? 'Körperteil wählen, neue Pose wird angelegt.'
: 'Für Unknown wird keine Pose trainiert.'}
</div>
</div>
@ -10530,7 +10604,7 @@ export default function TrainingTab(props: {
height="1386"
preserveAspectRatio="xMidYMid meet"
pointerEvents="none"
className={targetPerson ? 'opacity-90' : 'opacity-35'}
className={disabled ? 'opacity-35' : 'opacity-90'}
/>
{showMapParts && poseKeypointMapView === 'head' ? (