@@ -7821,7 +8996,7 @@ export default function TrainingTab(props: {
aria-disabled={uiLocked}
onClick={() => {
if (uiLocked) return
- setActiveBoxIndex(index)
+ setActiveBoxIndex((current) => current === index ? null : index)
}}
className={[
'group relative overflow-hidden rounded-2xl border transition-all duration-200',
@@ -7861,7 +9036,7 @@ export default function TrainingTab(props: {
onClick={(e) => {
e.stopPropagation()
if (uiLocked) return
- setActiveBoxIndex(index)
+ setActiveBoxIndex((current) => current === index ? null : index)
}}
className={[
'flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ring-1 transition',
@@ -7978,8 +9153,278 @@ export default function TrainingTab(props: {
)
}
+ const poseKeypointCorrectionPanel = () => {
+ const targetIndex = poseCorrectionPersonIndex
+ const targetPerson =
+ targetIndex !== null && targetIndex >= 0 ? posePersons[targetIndex] : undefined
+ const targetPoseLabel =
+ targetIndex !== null && targetIndex >= 0 ? `Pose #${targetIndex + 1}` : 'Keine Pose ausgewählt'
+ const visibleKeypointIds = new Set(
+ (targetPerson?.keypoints ?? [])
+ .filter(isPoseKeypointVisible)
+ .map((point) => poseKeypointId(point.name))
+ )
+ const disabled = uiLocked || frameBusy || trainingRunning || saving || !targetPerson
+ const faceVisibleCount = POSE_FACE_KEYPOINT_SHAPES.filter((shape) =>
+ visibleKeypointIds.has(shape.keypointName)
+ ).length
+ const mapShapes =
+ poseKeypointMapView === 'head' ? POSE_FACE_KEYPOINT_SHAPES : POSE_BODY_KEYPOINT_SHAPES
+ const mapAspectRatio =
+ poseKeypointMapView === 'head' ? POSE_HEAD_MAP_ASPECT_RATIO : POSE_BODY_MAP_ASPECT_RATIO
+ const mapViewBox = poseKeypointMapViewBoxValue
+ const mapMarkerRadius = poseKeypointMapView === 'head' ? 6 : 7
+ const mapWidthLimitPx =
+ poseMapSlotHeight > 0
+ ? Math.floor(poseMapSlotHeight * POSE_BODY_MAP_ASPECT_RATIO_VALUE)
+ : 0
+ const mapContainerStyle = {
+ aspectRatio: mapAspectRatio,
+ ...(mapWidthLimitPx > 0
+ ? {
+ maxHeight: `${poseMapSlotHeight}px`,
+ width: `min(14.5rem, 86%, ${mapWidthLimitPx}px)`,
+ }
+ : {}),
+ } as CSSProperties
+ const headPending =
+ pendingPoseKeypoint?.personIndex === targetIndex &&
+ POSE_FACE_KEYPOINT_SHAPES.some(
+ (shape) => shape.keypointName === pendingPoseKeypoint.keypointName
+ )
+ const headPresent = faceVisibleCount > 0
+ const headActive = headPending || headPresent
+ const showMapParts = !poseKeypointMapAnimating
+
+ const selectPoseKeypoint = (keypointName: string) => {
+ if (disabled || targetIndex === null) return
+
+ const pending =
+ pendingPoseKeypoint?.personIndex === targetIndex &&
+ pendingPoseKeypoint.keypointName === keypointName
+
+ if (pending) {
+ setPendingPoseKeypoint(null)
+ return
+ }
+
+ setShowPoseSkeleton(true)
+ setActivePosePersonIndex(targetIndex)
+ setPendingPoseKeypoint({
+ personIndex: targetIndex,
+ keypointName,
+ })
+ }
+
+ return (
+
+
+
+
+
+ Pose-Körperteile
+
+
+
+ {targetPerson
+ ? `${targetPoseLabel}: Punkt wählen und im Bild setzen.`
+ : visiblePosePersonCount > 1
+ ? 'Links erst eine Pose auswählen.'
+ : 'Keine Pose vorhanden.'}
+
+
+
+ {targetPerson ? (
+
+ {visibleKeypointIds.size}/{POSE_KEYPOINT_OPTIONS.length}
+
+ ) : null}
+
+
+
+
+
+
+
+
+ {showMapParts && poseKeypointMapView === 'head' ? (
+ {
+ event.preventDefault()
+ event.stopPropagation()
+ switchPoseKeypointMapView('body')
+ }}
+ onClick={(event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ switchPoseKeypointMapView('body')
+ }}
+ onKeyDown={(event) => {
+ if (event.key !== 'Enter' && event.key !== ' ') return
+ event.preventDefault()
+ switchPoseKeypointMapView('body')
+ }}
+ >
+ Koerperansicht oeffnen
+
+ ) : null}
+
+ {showMapParts && poseKeypointMapView === 'body' ? (
+ {
+ event.preventDefault()
+ event.stopPropagation()
+ if (disabled) return
+ switchPoseKeypointMapView('head')
+ }}
+ onClick={(event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ if (disabled) return
+ switchPoseKeypointMapView('head')
+ }}
+ onKeyDown={(event) => {
+ if (event.key !== 'Enter' && event.key !== ' ') return
+ event.preventDefault()
+ if (disabled) return
+ switchPoseKeypointMapView('head')
+ }}
+ >
+ Kopf vergrößern
+
+ ) : null}
+
+ {showMapParts && poseKeypointMapView === 'body' ? (
+
+ ) : null}
+
+ {showMapParts ? mapShapes.map((shape) => {
+ const present = visibleKeypointIds.has(shape.keypointName)
+ const pending =
+ pendingPoseKeypoint?.personIndex === targetIndex &&
+ pendingPoseKeypoint.keypointName === shape.keypointName
+
+ return (
+
+ )
+ }) : null}
+
+ {showMapParts ? mapShapes.map((shape) => {
+ const keypointName = shape.keypointName
+ const label = poseKeypointLabel(keypointName)
+ const present = visibleKeypointIds.has(keypointName)
+ const pending =
+ pendingPoseKeypoint?.personIndex === targetIndex &&
+ pendingPoseKeypoint.keypointName === keypointName
+ const active = present || pending
+
+ return (
+ selectPoseKeypoint(keypointName)}
+ onKeyDown={(event) => {
+ if (event.key !== 'Enter' && event.key !== ' ') return
+ event.preventDefault()
+ selectPoseKeypoint(keypointName)
+ }}
+ >
+ {label}
+
+ )
+ }) : null}
+
+
+
+
+
+
+ )
+ }
+
const imageTouchClass =
- boxLabel || drawingBox || boxInteraction
+ boxLabel || drawingBox || boxInteraction || poseInteraction || pendingPoseKeypoint
? 'touch-none'
: 'touch-pan-y'
@@ -8259,10 +9704,6 @@ export default function TrainingTab(props: {
-
- Bestätige oder korrigiere die Analyse. Jede Antwort wird als Trainingsdatenpunkt gespeichert.
-
-
{detectorBoxesPanel({
@@ -8297,18 +9738,6 @@ export default function TrainingTab(props: {
].join(' ')}
style={imageStageStyle}
>
- {hasPosePersons ? (
-
-
-
- ) : null}
-
{imageSrc ? (
- {showPoseSkeleton && imageLayerStyle && hasPosePersons ? (() => {
+ {poseSkeletonVisible && imageLayerStyle ? (() => {
const layerWidth = Number(imageLayerStyle.width)
const layerHeight = Number(imageLayerStyle.height)
@@ -8379,12 +9808,17 @@ export default function TrainingTab(props: {
return (
{posePersons.map((person, personIndex) => {
+ if (!hasVisiblePoseBox(person) && !person.keypoints?.some(isPoseKeypointVisible)) {
+ return null
+ }
+
const reliable = isPosePersonReliable(person)
+ const activePose = activePosePersonIndex === personIndex
const color = reliable
? POSE_PERSON_COLORS[personIndex % POSE_PERSON_COLORS.length]
: POSE_UNRELIABLE_COLOR
@@ -8396,23 +9830,88 @@ export default function TrainingTab(props: {
{
+ e.preventDefault()
+ e.stopPropagation()
+ setActivePosePersonIndex(personIndex)
}}
>
-
- {posePersons.length > 1 ? `Pose ${personIndex + 1}` : 'Pose'} {score}%
-
+
+ {
+ e.preventDefault()
+ e.stopPropagation()
+ setActivePosePersonIndex(personIndex)
+ }}
+ >
+
+ {visiblePosePersonCount > 1 ? `#${personIndex + 1}` : 'Pose'}
+
+
+
+ {score}%
+
+
+ {
+ e.preventDefault()
+ e.stopPropagation()
+ removePosePerson(personIndex)
+ }}
+ title="Diese Pose aus dem Training entfernen"
+ aria-label="Pose entfernen"
+ >
+
+
+
+
)
})}
@@ -8472,8 +9971,9 @@ export default function TrainingTab(props: {
: POSE_UNRELIABLE_COLOR
return person.keypoints
- .filter(isPoseKeypointVisible)
.map((point, pointIndex) => {
+ if (!isPoseKeypointVisible(point)) return null
+
const left = poseCoordPx(point.x, layerWidth)
const top = poseCoordPx(point.y, layerHeight)
const labelToLeft = left > layerWidth * 0.72
@@ -8487,23 +9987,24 @@ export default function TrainingTab(props: {
/>
)
+ const labelToneClass = reliable
+ ? 'bg-white/95 text-gray-950 ring-black/10 hover:bg-gray-50 dark:bg-white/95 dark:text-gray-950 dark:ring-black/10 dark:hover:bg-gray-50'
+ : 'bg-white/90 text-slate-700 ring-slate-400/30 hover:bg-gray-50 dark:bg-white/90 dark:text-slate-700 dark:ring-black/10'
+
const text = (
-
+
{label}
)
return (
- {labelToLeft ? (
- <>
- {text}
- {dot}
- >
- ) : (
- <>
- {dot}
- {text}
- >
- )}
+
+ {labelToLeft ? (
+ <>
+ {
+ e.stopPropagation()
+ }}
+ onClick={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ removePoseKeypoint(personIndex, pointIndex)
+ }}
+ >
+
+
+ startPoseKeypointDrag(e, personIndex, pointIndex)}
+ aria-label={`${label} verschieben`}
+ >
+ {text}
+ {dot}
+
+ >
+ ) : (
+ <>
+ startPoseKeypointDrag(e, personIndex, pointIndex)}
+ aria-label={`${label} verschieben`}
+ >
+ {dot}
+ {text}
+
+ {
+ e.stopPropagation()
+ }}
+ onClick={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ removePoseKeypoint(personIndex, pointIndex)
+ }}
+ >
+
+
+ >
+ )}
+
)
})
@@ -8640,9 +10218,11 @@ export default function TrainingTab(props: {
activePointerIdRef.current = e.pointerId
try {
+ activePointerCaptureElementRef.current = imageBoxRef.current
imageBoxRef.current?.setPointerCapture(e.pointerId)
} catch {
activePointerIdRef.current = null
+ activePointerCaptureElementRef.current = null
}
const nextMagnifier: MagnifierState = {
@@ -8776,9 +10356,11 @@ export default function TrainingTab(props: {
activePointerIdRef.current = e.pointerId
try {
+ activePointerCaptureElementRef.current = imageBoxRef.current
imageBoxRef.current?.setPointerCapture(e.pointerId)
} catch {
activePointerIdRef.current = null
+ activePointerCaptureElementRef.current = null
}
const nextMagnifier: MagnifierState = {
@@ -8821,7 +10403,7 @@ export default function TrainingTab(props: {
) : null}
- {touchMagnifier?.visible && imageSrc && showImageBoxes ? (() => {
+ {touchMagnifier?.visible && imageSrc && showAnnotationMagnifier ? (() => {
const rect = activeImageContentRectRef.current ?? getImageContentRect()
if (!rect || rect.width <= 0 || rect.height <= 0) return null
@@ -8956,6 +10538,8 @@ export default function TrainingTab(props: {
const boxTop = hasUsableBox ? imageTop + activeBox.y * imageHeight : 0
const boxWidth = hasUsableBox ? activeBox.w * imageWidth : 0
const boxHeight = hasUsableBox ? activeBox.h * imageHeight : 0
+ const poseMagnifierInteraction = poseInteraction
+ const showPoseMagnifierSkeleton = poseSkeletonVisible && Boolean(poseMagnifierInteraction)
return typeof document !== 'undefined'
? createPortal(
@@ -8981,6 +10565,87 @@ export default function TrainingTab(props: {
}}
/>
+ {showPoseMagnifierSkeleton ? (
+
+ {posePersons.map((person, personIndex) => {
+ if (!person.keypoints?.some(isPoseKeypointVisible)) {
+ return null
+ }
+
+ const reliable = isPosePersonReliable(person)
+ const isActivePose = poseMagnifierInteraction?.personIndex === personIndex
+ const color = reliable
+ ? POSE_PERSON_COLORS[personIndex % POSE_PERSON_COLORS.length]
+ : POSE_UNRELIABLE_COLOR
+ const keypointsByName = new Map(
+ person.keypoints.map((point) => [poseKeypointId(point.name), point])
+ )
+
+ return (
+
+ {POSE_SKELETON_EDGES.map(([from, to]) => {
+ const fromPoint = keypointsByName.get(from)
+ const toPoint = keypointsByName.get(to)
+
+ if (
+ !isPoseKeypointVisible(fromPoint) ||
+ !isPoseKeypointVisible(toPoint)
+ ) {
+ return null
+ }
+
+ return (
+
+ )
+ })}
+
+ {person.keypoints.map((point, pointIndex) => {
+ if (!isPoseKeypointVisible(point)) return null
+
+ const isActivePoint =
+ poseMagnifierInteraction?.personIndex === personIndex &&
+ poseMagnifierInteraction.keypointIndex === pointIndex
+
+ return (
+
+ )
+ })}
+
+ )
+ })}
+
+ ) : null}
+
{hasUsableBox ? (
- mAP50
+ {trainingMetricLabel}
- {formatMapPercent(trainingStatus?.training?.map50) || '—'}
+ {trainingMetricText}
@@ -9310,154 +10975,160 @@ export default function TrainingTab(props: {
-
{
- mobileSectionRefs.current.sexPosition = el
- }}
- >
-
- toggleMobileCorrectionSection('sexPosition', expanded)
- }
- onChange={(value) =>
- setCorrection((p) => {
- if (p.sexPosition !== value) {
- setHasManualCorrection(true)
+ {poseModeActive ? (
+ poseKeypointCorrectionPanel()
+ ) : (
+ <>
+ {
+ mobileSectionRefs.current.sexPosition = el
+ }}
+ >
+
+ toggleMobileCorrectionSection('sexPosition', expanded)
}
+ onChange={(value) =>
+ setCorrection((p) => {
+ if (p.sexPosition !== value) {
+ setHasManualCorrection(true)
+ }
- return {
- ...p,
- sexPosition: value,
+ return {
+ ...p,
+ sexPosition: value,
+ }
+ })
}
- })
- }
- disabled={uiLocked}
- gridClassName="grid grid-cols-3 gap-2"
- />
-
+ disabled={uiLocked}
+ gridClassName="grid grid-cols-3 gap-2"
+ />
+
-
{
- mobileSectionRefs.current.people = el
- }}
- >
-
- toggleMobileCorrectionSection('people', expanded)
- }
- onToggle={() => {}}
- drawLabel={drawLabelForSection(labels.people)}
- onDrawLabelChange={setBoxLabel}
- disabled={uiLocked}
- singleDrawMode
- gridClassName="grid grid-cols-2 gap-2"
- />
-
-
-
{
- mobileSectionRefs.current.bodyParts = el
- }}
- >
-
- toggleMobileCorrectionSection('bodyParts', expanded)
- }
- onToggle={(value) =>
- setCorrection((p) => {
- setHasManualCorrection(true)
-
- return {
- ...p,
- bodyPartsPresent: toggleArrayValue(p.bodyPartsPresent, value),
+ {
+ mobileSectionRefs.current.people = el
+ }}
+ >
+
+ toggleMobileCorrectionSection('people', expanded)
}
- })
- }
- drawLabel={drawLabelForSection(labels.bodyParts)}
- onDrawLabelChange={setBoxLabel}
- disabled={uiLocked}
- />
-
+ onToggle={() => {}}
+ drawLabel={drawLabelForSection(labels.people)}
+ onDrawLabelChange={setBoxLabel}
+ disabled={uiLocked}
+ singleDrawMode
+ gridClassName="grid grid-cols-2 gap-2"
+ />
+
-
{
- mobileSectionRefs.current.objects = el
- }}
- >
-
- toggleMobileCorrectionSection('objects', expanded)
- }
- onToggle={(value) =>
- setCorrection((p) => {
- setHasManualCorrection(true)
-
- return {
- ...p,
- objectsPresent: toggleArrayValue(p.objectsPresent, value),
+ {
+ mobileSectionRefs.current.bodyParts = el
+ }}
+ >
+
+ toggleMobileCorrectionSection('bodyParts', expanded)
}
- })
- }
- drawLabel={drawLabelForSection(labels.objects)}
- onDrawLabelChange={setBoxLabel}
- disabled={uiLocked}
- />
-
+ onToggle={(value) =>
+ setCorrection((p) => {
+ setHasManualCorrection(true)
- {
- mobileSectionRefs.current.clothing = el
- }}
- >
-
- toggleMobileCorrectionSection('clothing', expanded)
- }
- onToggle={(value) =>
- setCorrection((p) => {
- setHasManualCorrection(true)
-
- return {
- ...p,
- clothingPresent: toggleArrayValue(p.clothingPresent, value),
+ return {
+ ...p,
+ bodyPartsPresent: toggleArrayValue(p.bodyPartsPresent, value),
+ }
+ })
}
- })
- }
- drawLabel={drawLabelForSection(labels.clothing)}
- onDrawLabelChange={setBoxLabel}
- disabled={uiLocked}
- />
-
+ drawLabel={drawLabelForSection(labels.bodyParts)}
+ onDrawLabelChange={setBoxLabel}
+ disabled={uiLocked}
+ />
+
+
+
{
+ mobileSectionRefs.current.objects = el
+ }}
+ >
+
+ toggleMobileCorrectionSection('objects', expanded)
+ }
+ onToggle={(value) =>
+ setCorrection((p) => {
+ setHasManualCorrection(true)
+
+ return {
+ ...p,
+ objectsPresent: toggleArrayValue(p.objectsPresent, value),
+ }
+ })
+ }
+ drawLabel={drawLabelForSection(labels.objects)}
+ onDrawLabelChange={setBoxLabel}
+ disabled={uiLocked}
+ />
+
+
+
{
+ mobileSectionRefs.current.clothing = el
+ }}
+ >
+
+ toggleMobileCorrectionSection('clothing', expanded)
+ }
+ onToggle={(value) =>
+ setCorrection((p) => {
+ setHasManualCorrection(true)
+
+ return {
+ ...p,
+ clothingPresent: toggleArrayValue(p.clothingPresent, value),
+ }
+ })
+ }
+ drawLabel={drawLabelForSection(labels.clothing)}
+ onDrawLabelChange={setBoxLabel}
+ disabled={uiLocked}
+ />
+
+ >
+ )}
) : null}
@@ -9480,20 +11151,19 @@ export default function TrainingTab(props: {
{/* Rechte Details/Korrektur */}