diff --git a/backend/dist/nsfwapp-linux-amd64 b/backend/dist/nsfwapp-linux-amd64 index 9cc9ccf..02fc904 100644 Binary files a/backend/dist/nsfwapp-linux-amd64 and b/backend/dist/nsfwapp-linux-amd64 differ diff --git a/backend/dist/nsfwapp.exe b/backend/dist/nsfwapp.exe index 13b6629..b748ca3 100644 Binary files a/backend/dist/nsfwapp.exe and b/backend/dist/nsfwapp.exe differ diff --git a/backend/dist/nsfwapp_amd64.deb b/backend/dist/nsfwapp_amd64.deb index 28407f6..e197049 100644 Binary files a/backend/dist/nsfwapp_amd64.deb and b/backend/dist/nsfwapp_amd64.deb differ diff --git a/frontend/src/components/ui/TrainingTab.tsx b/frontend/src/components/ui/TrainingTab.tsx index 351fa39..6c34e7b 100644 --- a/frontend/src/components/ui/TrainingTab.tsx +++ b/frontend/src/components/ui/TrainingTab.tsx @@ -4523,10 +4523,14 @@ export default function TrainingTab(props: { const poseFaceImagePanCenterRef = useRef<{ x: number; y: number } | null>(null) const poseFaceImagePanGestureRef = useRef(null) const poseFaceImageDisplayCameraRef = useRef(null) + const poseFaceImageCameraRef = useRef(null) const poseFaceImageCameraAnimationFrameRef = useRef(null) + const poseFaceImageZoomAnimationResolveRef = useRef<(() => void) | null>(null) + const poseFaceImageZoomAppliedRef = useRef(false) const poseFaceImageZoomOutActiveRef = useRef(false) const correctionRef = useRef(correction) const hasManualCorrectionRef = useRef(hasManualCorrection) + const saveFeedbackInFlightRef = useRef(false) const latestGestureBoxRef = useRef(null) const pendingPointerMoveRef = useRef<{ clientX: number; clientY: number } | null>(null) const pointerMoveRafRef = useRef(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((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(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 - resetPoseUiToBoxes() - setSavingOverlayText(editingFeedback ? 'Feedback wird aktualisiert…' : 'Feedback wird gespeichert…') - setSaving(true) - setError(null) - setMessage(null) + saveFeedbackInFlightRef.current = true try { + await zoomOutPoseFaceImageToFullImage() + resetPoseUiToBoxes() + setSavingOverlayText(editingFeedback ? 'Feedback wird aktualisiert…' : 'Feedback wird gespeichert…') + setSaving(true) + setError(null) + setMessage(null) + 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: {
{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.'}
@@ -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' ? (