This commit is contained in:
Linrador 2026-06-29 21:24:42 +02:00
parent e571dd4d86
commit 5c9552447a
5 changed files with 674 additions and 112 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -14,6 +14,7 @@ import {
PhotoIcon, PhotoIcon,
RectangleGroupIcon, RectangleGroupIcon,
SparklesIcon, SparklesIcon,
UserGroupIcon,
} from '@heroicons/react/20/solid' } from '@heroicons/react/20/solid'
import Button from './Button' import Button from './Button'
import Modal from './Modal' import Modal from './Modal'
@ -29,6 +30,23 @@ export type TrainingFeedbackBox = {
h: number h: number
} }
export type TrainingFeedbackKeypoint = {
name: string
x: number
y: number
conf: number
}
export type TrainingFeedbackPosePerson = {
label?: string
score: number
box: TrainingFeedbackBox
keypoints: TrainingFeedbackKeypoint[]
quality?: number
visibleKeypoints?: number
reliable?: boolean
}
export type TrainingFeedbackScoredLabel = { export type TrainingFeedbackScoredLabel = {
label: string label: string
score: number score: number
@ -45,6 +63,7 @@ export type TrainingFeedbackPrediction = {
objectsPresent: TrainingFeedbackScoredLabel[] objectsPresent: TrainingFeedbackScoredLabel[]
clothingPresent: TrainingFeedbackScoredLabel[] clothingPresent: TrainingFeedbackScoredLabel[]
boxes?: TrainingFeedbackBox[] boxes?: TrainingFeedbackBox[]
persons?: TrainingFeedbackPosePerson[]
} }
export type TrainingFeedbackCorrection = { export type TrainingFeedbackCorrection = {
@ -54,6 +73,7 @@ export type TrainingFeedbackCorrection = {
objectsPresent: string[] objectsPresent: string[]
clothingPresent: string[] clothingPresent: string[]
boxes: TrainingFeedbackBox[] boxes: TrainingFeedbackBox[]
posePersons?: TrainingFeedbackPosePerson[]
} }
export type TrainingFeedbackAnnotation = { export type TrainingFeedbackAnnotation = {
@ -112,6 +132,125 @@ function normalizeBox(box: TrainingFeedbackBox): TrainingFeedbackBox {
} }
} }
const POSE_KEYPOINT_MIN_CONFIDENCE = 0.15
const POSE_PERSON_MIN_SCORE = 0.30
const POSE_UNRELIABLE_COLOR = '#94a3b8'
const POSE_PERSON_COLORS = ['#38bdf8', '#a78bfa', '#34d399', '#f59e0b', '#fb7185']
const POSE_SKELETON_EDGES: Array<readonly [string, string]> = [
['left_ear', 'left_eye'],
['left_eye', 'nose'],
['nose', 'right_eye'],
['right_eye', 'right_ear'],
['left_shoulder', 'right_shoulder'],
['left_shoulder', 'left_elbow'],
['left_elbow', 'left_wrist'],
['right_shoulder', 'right_elbow'],
['right_elbow', 'right_wrist'],
['left_shoulder', 'left_hip'],
['right_shoulder', 'right_hip'],
['left_hip', 'right_hip'],
['left_hip', 'left_knee'],
['left_knee', 'left_ankle'],
['right_hip', 'right_knee'],
['right_knee', 'right_ankle'],
]
const POSE_KEYPOINT_LABELS: Record<string, string> = {
nose: 'Nase',
left_eye: 'L Auge',
right_eye: 'R Auge',
left_ear: 'L Ohr',
right_ear: 'R Ohr',
left_shoulder: 'L Schulter',
right_shoulder: 'R Schulter',
left_elbow: 'L Ellbogen',
right_elbow: 'R Ellbogen',
left_wrist: 'L Hand',
right_wrist: 'R Hand',
left_hip: 'L Huefte',
right_hip: 'R Huefte',
left_knee: 'L Knie',
right_knee: 'R Knie',
left_ankle: 'L Fuss',
right_ankle: 'R Fuss',
}
function poseKeypointId(value?: string | null) {
return String(value || '').trim().toLowerCase().replace(/[\s-]+/g, '_')
}
function poseKeypointLabel(value?: string | null) {
const id = poseKeypointId(value)
return POSE_KEYPOINT_LABELS[id] || String(value || '').trim() || 'Punkt'
}
function isPoseKeypointVisible(point?: TrainingFeedbackKeypoint | null): point is TrainingFeedbackKeypoint {
if (!point) return false
return (
Number.isFinite(Number(point.x)) &&
Number.isFinite(Number(point.y)) &&
clamp01(Number(point.conf ?? 1)) >= POSE_KEYPOINT_MIN_CONFIDENCE
)
}
function hasVisiblePoseBox(person: TrainingFeedbackPosePerson) {
const box = person.box
const w = Number(box?.w)
const h = Number(box?.h)
return (
clamp01(Number(person.score)) >= POSE_PERSON_MIN_SCORE &&
Number.isFinite(w) &&
Number.isFinite(h) &&
w > 0 &&
h > 0
)
}
function normalizePosePerson(person: TrainingFeedbackPosePerson): TrainingFeedbackPosePerson | null {
if (!person?.box) return null
const box = normalizeBox(person.box)
if (!box) return null
return {
label: String(person.label || '').trim() || 'person',
score: clamp01(Number(person.score ?? 1)),
box,
keypoints: (person.keypoints ?? [])
.map((point) => ({
name: poseKeypointId(point.name),
x: clamp01(Number(point.x)),
y: clamp01(Number(point.y)),
conf: clamp01(Number(point.conf ?? 1)),
}))
.filter((point) => point.name),
quality: typeof person.quality === 'number' ? clamp01(Number(person.quality)) : undefined,
visibleKeypoints:
typeof person.visibleKeypoints === 'number'
? Math.max(0, Math.round(Number(person.visibleKeypoints)))
: undefined,
reliable: typeof person.reliable === 'boolean' ? person.reliable : undefined,
}
}
function normalizePosePersons(persons?: TrainingFeedbackPosePerson[]) {
return (persons ?? [])
.map(normalizePosePerson)
.filter((person): person is TrainingFeedbackPosePerson => Boolean(person))
}
function visiblePosePersons(persons?: TrainingFeedbackPosePerson[]) {
return normalizePosePersons(persons).filter(
(person) => hasVisiblePoseBox(person) || person.keypoints.some(isPoseKeypointVisible)
)
}
function poseCoordPx(value: number, size: number) {
return clamp01(Number(value)) * Math.max(0, Number(size) || 0)
}
function annotationEffectiveCorrection( function annotationEffectiveCorrection(
annotation: TrainingFeedbackAnnotation annotation: TrainingFeedbackAnnotation
): TrainingFeedbackCorrection { ): TrainingFeedbackCorrection {
@ -123,6 +262,7 @@ function annotationEffectiveCorrection(
objectsPresent: [], objectsPresent: [],
clothingPresent: [], clothingPresent: [],
boxes: [], boxes: [],
posePersons: [],
} }
} }
@ -130,6 +270,9 @@ function annotationEffectiveCorrection(
return { return {
...annotation.correction, ...annotation.correction,
sexPosition: normalizeSexPositionValue(annotation.correction.sexPosition), sexPosition: normalizeSexPositionValue(annotation.correction.sexPosition),
posePersons: normalizePosePersons(
annotation.correction.posePersons ?? annotation.prediction.persons
),
} }
} }
@ -140,6 +283,7 @@ function annotationEffectiveCorrection(
objectsPresent: (annotation.prediction.objectsPresent ?? []).map((item) => item.label), objectsPresent: (annotation.prediction.objectsPresent ?? []).map((item) => item.label),
clothingPresent: (annotation.prediction.clothingPresent ?? []).map((item) => item.label), clothingPresent: (annotation.prediction.clothingPresent ?? []).map((item) => item.label),
boxes: annotation.prediction.boxes ?? [], boxes: annotation.prediction.boxes ?? [],
posePersons: normalizePosePersons(annotation.prediction.persons),
} }
} }
@ -370,6 +514,7 @@ function FilterButton(props: {
function ReadonlyTrainingFrameViewer(props: { function ReadonlyTrainingFrameViewer(props: {
imageSrc: string imageSrc: string
boxes: TrainingFeedbackBox[] boxes: TrainingFeedbackBox[]
posePersons?: TrainingFeedbackPosePerson[]
sourceFile?: string sourceFile?: string
className?: string className?: string
highlightedBoxIndex?: number | null highlightedBoxIndex?: number | null
@ -384,6 +529,10 @@ function ReadonlyTrainingFrameViewer(props: {
.map(normalizeBox) .map(normalizeBox)
.filter((box) => String(box.label || '').trim() && box.w > 0 && box.h > 0) .filter((box) => String(box.label || '').trim() && box.w > 0 && box.h > 0)
}, [props.boxes]) }, [props.boxes])
const posePersons = useMemo(
() => visiblePosePersons(props.posePersons),
[props.posePersons]
)
type ImageContentRect = { type ImageContentRect = {
left: number left: number
@ -612,6 +761,92 @@ function ReadonlyTrainingFrameViewer(props: {
})} })}
</div> </div>
) : null} ) : null}
{loaded && posePersons.length > 0 && imageLayerStyle ? (() => {
const layerWidth = Number(imageLayerStyle.width)
const layerHeight = Number(imageLayerStyle.height)
if (
!Number.isFinite(layerWidth) ||
!Number.isFinite(layerHeight) ||
layerWidth <= 0 ||
layerHeight <= 0
) {
return null
}
return (
<div
className="pointer-events-none absolute z-[320] overflow-visible"
style={imageLayerStyle}
>
<svg
className="absolute inset-0 h-full w-full overflow-visible"
viewBox={`0 0 ${layerWidth} ${layerHeight}`}
preserveAspectRatio="none"
>
{posePersons.map((person, personIndex) => {
const color = person.reliable === false
? POSE_UNRELIABLE_COLOR
: POSE_PERSON_COLORS[personIndex % POSE_PERSON_COLORS.length]
const keypointsByName = new Map(
person.keypoints.map((point) => [poseKeypointId(point.name), point])
)
return (
<g key={`feedback-pose-${personIndex}`}>
{POSE_SKELETON_EDGES.map(([from, to]) => {
const fromPoint = keypointsByName.get(from)
const toPoint = keypointsByName.get(to)
if (
!isPoseKeypointVisible(fromPoint) ||
!isPoseKeypointVisible(toPoint)
) {
return null
}
return (
<line
key={`${from}-${to}`}
x1={poseCoordPx(fromPoint.x, layerWidth)}
y1={poseCoordPx(fromPoint.y, layerHeight)}
x2={poseCoordPx(toPoint.x, layerWidth)}
y2={poseCoordPx(toPoint.y, layerHeight)}
stroke={color}
strokeOpacity={0.86}
strokeWidth={2.4}
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
)
})}
{person.keypoints.map((point, pointIndex) => {
if (!isPoseKeypointVisible(point)) return null
return (
<circle
key={`feedback-pose-point-${personIndex}-${poseKeypointId(point.name)}-${pointIndex}`}
cx={poseCoordPx(point.x, layerWidth)}
cy={poseCoordPx(point.y, layerHeight)}
r={3.5}
fill={color}
fillOpacity={0.95}
stroke="#fff"
strokeOpacity={0.95}
strokeWidth={1.25}
vectorEffect="non-scaling-stroke"
/>
)
})}
</g>
)
})}
</svg>
</div>
)
})() : null}
</div> </div>
</div> </div>
) : ( ) : (
@ -631,6 +866,7 @@ function FeedbackListItem(props: {
}) { }) {
const effective = annotationEffectiveCorrection(props.item) const effective = annotationEffectiveCorrection(props.item)
const boxCount = effective.boxes?.length ?? 0 const boxCount = effective.boxes?.length ?? 0
const poseCount = visiblePosePersons(effective.posePersons).length
const imageSrc = frameUrlForHistory(props.item) const imageSrc = frameUrlForHistory(props.item)
return ( return (
@ -700,6 +936,10 @@ function FeedbackListItem(props: {
{boxCount} Boxen {boxCount} Boxen
</span> </span>
<span className="rounded-full bg-gray-50 px-2 py-0.5 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
{poseCount} Posen
</span>
<span className="max-w-full truncate rounded-full bg-gray-50 px-2 py-0.5 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10"> <span className="max-w-full truncate rounded-full bg-gray-50 px-2 py-0.5 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
{normalizeSexPositionValue(effective.sexPosition)} {normalizeSexPositionValue(effective.sexPosition)}
</span> </span>
@ -808,6 +1048,94 @@ function BoxDetailsList(props: {
) )
} }
function PoseDetailsList(props: {
persons?: TrainingFeedbackPosePerson[]
}) {
const persons = visiblePosePersons(props.persons)
return (
<section className="shrink-0 overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-900/75">
<div className="flex shrink-0 items-start justify-between gap-3 border-b border-gray-100 px-3.5 py-2.5 dark:border-white/10">
<div className="min-w-0">
<div className="text-sm font-semibold text-gray-950 dark:text-white">
Pose
</div>
<div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
Gespeicherte Skelette
</div>
</div>
<span className="rounded-full bg-cyan-50 px-2.5 py-1 text-xs font-medium text-cyan-700 ring-1 ring-cyan-200 dark:bg-cyan-500/15 dark:text-cyan-100 dark:ring-cyan-400/30">
{persons.length}
</span>
</div>
<div className="space-y-2 p-2.5">
{persons.length === 0 ? (
<div className="flex min-h-20 items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50 px-3 py-5 text-center text-xs text-gray-500 dark:border-white/10 dark:bg-white/[0.03] dark:text-gray-400">
Keine Pose gespeichert.
</div>
) : (
persons.map((person, index) => {
const color = person.reliable === false
? POSE_UNRELIABLE_COLOR
: POSE_PERSON_COLORS[index % POSE_PERSON_COLORS.length]
const keypoints = (person.keypoints ?? []).filter(isPoseKeypointVisible)
return (
<div
key={`feedback-pose-details-${index}`}
className="rounded-xl bg-gray-50 p-2.5 ring-1 ring-black/5 dark:bg-white/[0.04] dark:ring-white/10"
>
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<div
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-white ring-1 ring-black/10"
style={{ backgroundColor: color }}
>
<UserGroupIcon className="h-4 w-4" aria-hidden="true" />
</div>
<div className="min-w-0">
<div className="truncate text-xs font-semibold text-gray-950 dark:text-white">
Person #{index + 1}
</div>
<div className="truncate text-[10px] text-gray-500 dark:text-gray-400">
{keypoints.length} Punkte - Confidence {percent(person.score)}
</div>
</div>
</div>
</div>
{keypoints.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-1">
{keypoints.slice(0, 8).map((point) => (
<span
key={`${index}-${poseKeypointId(point.name)}`}
className="rounded-full bg-white px-1.5 py-0.5 text-[10px] font-medium text-gray-700 ring-1 ring-black/5 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10"
>
{poseKeypointLabel(point.name)}
</span>
))}
{keypoints.length > 8 ? (
<span className="rounded-full bg-white px-1.5 py-0.5 text-[10px] font-medium text-gray-500 ring-1 ring-black/5 dark:bg-white/10 dark:text-gray-300 dark:ring-white/10">
+{keypoints.length - 8}
</span>
) : null}
</div>
) : null}
</div>
)
})
)}
</div>
</section>
)
}
function fileNameFromPath(path?: string) { function fileNameFromPath(path?: string) {
const clean = String(path || '').trim() const clean = String(path || '').trim()
if (!clean) return '' if (!clean) return ''
@ -818,6 +1146,7 @@ function fileNameFromPath(path?: string) {
function CompactSourceCard(props: { function CompactSourceCard(props: {
item: TrainingFeedbackAnnotation item: TrainingFeedbackAnnotation
boxCount: number boxCount: number
poseCount: number
}) { }) {
const size = fileSizeLabel(props.item.sourceSizeBytes) const size = fileSizeLabel(props.item.sourceSizeBytes)
@ -853,6 +1182,10 @@ function CompactSourceCard(props: {
{props.boxCount} Boxen {props.boxCount} Boxen
</MetaPill> </MetaPill>
<MetaPill icon={<UserGroupIcon className="h-3.5 w-3.5" aria-hidden="true" />}>
{props.poseCount} Posen
</MetaPill>
{size ? <MetaPill>{size}</MetaPill> : null} {size ? <MetaPill>{size}</MetaPill> : null}
</div> </div>
</div> </div>
@ -984,6 +1317,7 @@ export default function TrainingFeedbackHistoryModal(props: {
const negativeCount = props.items.filter((item) => item.negative).length const negativeCount = props.items.filter((item) => item.negative).length
const shownCount = props.items.length const shownCount = props.items.length
const selectedBoxCount = effective?.boxes?.length ?? 0 const selectedBoxCount = effective?.boxes?.length ?? 0
const selectedPoseCount = visiblePosePersons(effective?.posePersons).length
const filteredItems = useMemo(() => { const filteredItems = useMemo(() => {
if (props.onSearchChange) { if (props.onSearchChange) {
@ -1015,6 +1349,9 @@ export default function TrainingFeedbackHistoryModal(props: {
...effectiveItem.objectsPresent, ...effectiveItem.objectsPresent,
...effectiveItem.clothingPresent, ...effectiveItem.clothingPresent,
...(effectiveItem.boxes ?? []).map((box) => box.label), ...(effectiveItem.boxes ?? []).map((box) => box.label),
...visiblePosePersons(effectiveItem.posePersons).flatMap((person) =>
(person.keypoints ?? []).map((point) => poseKeypointLabel(point.name))
),
] ]
.filter(Boolean) .filter(Boolean)
.join(' ') .join(' ')
@ -1319,6 +1656,7 @@ export default function TrainingFeedbackHistoryModal(props: {
<ReadonlyTrainingFrameViewer <ReadonlyTrainingFrameViewer
imageSrc={selectedImageSrc} imageSrc={selectedImageSrc}
boxes={effective.boxes ?? []} boxes={effective.boxes ?? []}
posePersons={effective.posePersons}
sourceFile={selected.sourceFile} sourceFile={selected.sourceFile}
className="shrink-0 mt-4" className="shrink-0 mt-4"
highlightedBoxIndex={hoveredBoxIndex} highlightedBoxIndex={hoveredBoxIndex}
@ -1327,10 +1665,11 @@ export default function TrainingFeedbackHistoryModal(props: {
<CompactSourceCard <CompactSourceCard
item={selected} item={selected}
boxCount={selectedBoxCount} boxCount={selectedBoxCount}
poseCount={selectedPoseCount}
/> />
</section> </section>
<aside className="grid min-h-0 gap-3 overflow-visible lg:grid-rows-[auto_minmax(0,1fr)] lg:overflow-hidden"> <aside className="grid min-h-0 gap-3 overflow-visible lg:grid-rows-[auto_minmax(0,1fr)_auto] lg:overflow-hidden">
<ResultLabelsCard <ResultLabelsCard
selected={selected} selected={selected}
effective={effective} effective={effective}
@ -1341,6 +1680,8 @@ export default function TrainingFeedbackHistoryModal(props: {
hoveredBoxIndex={hoveredBoxIndex} hoveredBoxIndex={hoveredBoxIndex}
onHoveredBoxIndexChange={setHoveredBoxIndex} onHoveredBoxIndexChange={setHoveredBoxIndex}
/> />
<PoseDetailsList persons={effective.posePersons} />
</aside> </aside>
</div> </div>
)} )}

View File

@ -16,6 +16,7 @@ import {
PauseIcon, PauseIcon,
RectangleGroupIcon, RectangleGroupIcon,
UserGroupIcon, UserGroupIcon,
UserPlusIcon,
TrashIcon, TrashIcon,
VideoCameraIcon, VideoCameraIcon,
XCircleIcon, XCircleIcon,
@ -435,6 +436,43 @@ const POSE_FACE_KEYPOINT_NAME_SET = new Set([
'right_ear', 'right_ear',
]) ])
function poseFaceKeypointLabelPlacement(keypointName: string) {
switch (poseKeypointId(keypointName)) {
case 'right_ear':
return {
labelToLeft: true,
transform: 'translate(calc(-100% - 10px), -50%)',
zIndex: 390,
}
case 'right_eye':
return {
labelToLeft: true,
transform: 'translate(calc(-100% - 8px), -135%)',
zIndex: 392,
}
case 'left_eye':
return {
labelToLeft: false,
transform: 'translate(8px, -135%)',
zIndex: 392,
}
case 'left_ear':
return {
labelToLeft: false,
transform: 'translate(10px, -50%)',
zIndex: 390,
}
case 'nose':
return {
labelToLeft: false,
transform: 'translate(-50%, 10px)',
zIndex: 394,
}
default:
return null
}
}
type PoseKeypointShape = { type PoseKeypointShape = {
keypointName: keyof typeof POSE_KEYPOINT_LABELS keypointName: keyof typeof POSE_KEYPOINT_LABELS
d: string d: string
@ -1873,6 +1911,22 @@ function clonePosePersons(persons: TrainingPosePerson[] | undefined): TrainingPo
})) }))
} }
function createEmptyPosePerson(): TrainingPosePerson {
return updatePosePersonQuality({
label: 'person',
score: 1,
box: {
label: 'person',
score: 1,
x: 0.32,
y: 0.06,
w: 0.36,
h: 0.88,
},
keypoints: [],
})
}
function poseBoxFromKeypoints(person: TrainingPosePerson): TrainingBox { function poseBoxFromKeypoints(person: TrainingPosePerson): TrainingBox {
const points = (person.keypoints ?? []).filter((point) => const points = (person.keypoints ?? []).filter((point) =>
Number.isFinite(point.x) && Number.isFinite(point.x) &&
@ -4013,7 +4067,7 @@ export default function TrainingTab(props: {
return false return false
} }
}) })
const [showPoseSkeleton, setShowPoseSkeleton] = useState(true) const [showPoseSkeleton, setShowPoseSkeleton] = useState(false)
const [frameNaturalSize, setFrameNaturalSize] = useState<{ const [frameNaturalSize, setFrameNaturalSize] = useState<{
width: number width: number
@ -4193,6 +4247,7 @@ export default function TrainingTab(props: {
setTouchMagnifier(null) setTouchMagnifier(null)
setBoxLabel('') setBoxLabel('')
setActiveBoxIndex(null) setActiveBoxIndex(null)
setShowPoseSkeleton(false)
setActivePosePersonIndex(null) setActivePosePersonIndex(null)
setPendingPoseKeypoint(null) setPendingPoseKeypoint(null)
if (poseKeypointMapAnimationFrameRef.current !== null) { if (poseKeypointMapAnimationFrameRef.current !== null) {
@ -4508,6 +4563,39 @@ export default function TrainingTab(props: {
poseKeypointMapAnimationFrameRef.current = requestAnimationFrame(animateZoom) poseKeypointMapAnimationFrameRef.current = requestAnimationFrame(animateZoom)
}, []) }, [])
const resetPoseUiToBoxes = useCallback(() => {
setShowPoseSkeleton(false)
setActivePosePersonIndex(null)
setPendingPoseKeypoint(null)
poseFaceImagePanGestureRef.current = null
poseFaceImagePanCenterRef.current = null
setPoseFaceImagePanCenter(null)
setPoseFaceImagePanning(false)
if (poseFaceImageCameraAnimationFrameRef.current !== null) {
cancelAnimationFrame(poseFaceImageCameraAnimationFrameRef.current)
poseFaceImageCameraAnimationFrameRef.current = null
}
poseFaceImageDisplayCameraRef.current = null
poseFaceImageZoomOutActiveRef.current = false
setPoseFaceImageDisplayCamera(null)
setPoseFaceImageCameraAnimating(false)
setPoseFaceImageZoomOutActive(false)
if (poseKeypointMapAnimationFrameRef.current !== null) {
cancelAnimationFrame(poseKeypointMapAnimationFrameRef.current)
poseKeypointMapAnimationFrameRef.current = null
}
poseKeypointMapViewRef.current = 'body'
poseKeypointMapViewBoxValueRef.current = POSE_BODY_MAP_VIEW_BOX
setPoseKeypointMapView('body')
setPoseKeypointMapViewBoxValue(POSE_BODY_MAP_VIEW_BOX)
setPoseKeypointMapAnimating(false)
}, [])
const labelsRef = useRef<TrainingLabels>(emptyLabels) const labelsRef = useRef<TrainingLabels>(emptyLabels)
useEffect(() => { useEffect(() => {
@ -5448,17 +5536,7 @@ export default function TrainingTab(props: {
setTouchMagnifier(null) setTouchMagnifier(null)
setBoxLabel('') setBoxLabel('')
setActiveBoxIndex(null) setActiveBoxIndex(null)
setActivePosePersonIndex(null) resetPoseUiToBoxes()
setPendingPoseKeypoint(null)
if (poseKeypointMapAnimationFrameRef.current !== null) {
cancelAnimationFrame(poseKeypointMapAnimationFrameRef.current)
poseKeypointMapAnimationFrameRef.current = null
}
poseKeypointMapViewRef.current = 'body'
poseKeypointMapViewBoxValueRef.current = POSE_BODY_MAP_VIEW_BOX
setPoseKeypointMapView('body')
setPoseKeypointMapViewBoxValue(POSE_BODY_MAP_VIEW_BOX)
setPoseKeypointMapAnimating(false)
setMobilePanel(trainingRunningRef.current ? 'training' : 'labels') setMobilePanel(trainingRunningRef.current ? 'training' : 'labels')
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@ -5499,7 +5577,7 @@ export default function TrainingTab(props: {
clothing: false, clothing: false,
} }
) )
}, []) }, [resetPoseUiToBoxes])
const setQueuedTrainingSamples = useCallback((nextQueue: QueuedTrainingSample[]) => { const setQueuedTrainingSamples = useCallback((nextQueue: QueuedTrainingSample[]) => {
importedSampleQueueRef.current = nextQueue importedSampleQueueRef.current = nextQueue
@ -5660,6 +5738,7 @@ export default function TrainingTab(props: {
setLoadingPreviewCandidate(previewUrl) setLoadingPreviewCandidate(previewUrl)
loadingRef.current = true loadingRef.current = true
setLoading(true) setLoading(true)
resetPoseUiToBoxes()
setAnalysisSourceFile('') setAnalysisSourceFile('')
setAnalysisProgress(8) setAnalysisProgress(8)
setAnalysisStep( setAnalysisStep(
@ -5798,6 +5877,7 @@ export default function TrainingTab(props: {
}, [ }, [
applyTrainingAnalysisEvent, applyTrainingAnalysisEvent,
completeNextFromData, completeNextFromData,
resetPoseUiToBoxes,
setLoadingPreviewCandidate, setLoadingPreviewCandidate,
waitForNextResult, waitForNextResult,
]) ])
@ -7295,31 +7375,11 @@ export default function TrainingTab(props: {
? getPointerPosInImage(snapshot.clientX, snapshot.clientY, { clamp: false }) ?? clampedPos ? getPointerPosInImage(snapshot.clientX, snapshot.clientY, { clamp: false }) ?? clampedPos
: clampedPos : clampedPos
const nextMagnifier: MagnifierState = {
visible: true,
clientX: snapshot.clientX,
clientY: snapshot.clientY,
imageX: clampedPos.x,
imageY: clampedPos.y,
}
setTouchMagnifier((prev) => {
if (
prev?.visible &&
prev.clientX === nextMagnifier.clientX &&
prev.clientY === nextMagnifier.clientY &&
Math.abs(prev.imageX - nextMagnifier.imageX) <= 0.0005 &&
Math.abs(prev.imageY - nextMagnifier.imageY) <= 0.0005
) {
return prev
}
return nextMagnifier
})
if (poseInteraction) { if (poseInteraction) {
markManualCorrection() markManualCorrection()
let nextPoseMagnifier: MagnifierState | null = null
setCorrection((prev) => { setCorrection((prev) => {
const persons = clonePosePersons(prev.posePersons) const persons = clonePosePersons(prev.posePersons)
const person = persons[poseInteraction.personIndex] const person = persons[poseInteraction.personIndex]
@ -7336,6 +7396,14 @@ export default function TrainingTab(props: {
conf: Math.max(clamp01(Number(point.conf ?? 1)), POSE_RELIABLE_KEYPOINT_MIN_CONFIDENCE), conf: Math.max(clamp01(Number(point.conf ?? 1)), POSE_RELIABLE_KEYPOINT_MIN_CONFIDENCE),
} }
nextPoseMagnifier = {
visible: true,
clientX: snapshot.clientX,
clientY: snapshot.clientY,
imageX: nextPoint.x,
imageY: nextPoint.y,
}
const nextPerson = updatePosePersonQuality({ const nextPerson = updatePosePersonQuality({
...person, ...person,
box: poseBoxFromKeypoints({ box: poseBoxFromKeypoints({
@ -7360,9 +7428,35 @@ export default function TrainingTab(props: {
return next return next
}) })
if (nextPoseMagnifier) {
setTouchMagnifier(nextPoseMagnifier)
}
return return
} }
const nextMagnifier: MagnifierState = {
visible: true,
clientX: snapshot.clientX,
clientY: snapshot.clientY,
imageX: clampedPos.x,
imageY: clampedPos.y,
}
setTouchMagnifier((prev) => {
if (
prev?.visible &&
prev.clientX === nextMagnifier.clientX &&
prev.clientY === nextMagnifier.clientY &&
Math.abs(prev.imageX - nextMagnifier.imageX) <= 0.0005 &&
Math.abs(prev.imageY - nextMagnifier.imageY) <= 0.0005
) {
return prev
}
return nextMagnifier
})
if (interaction) { if (interaction) {
const dx = pos.x - interaction.startX const dx = pos.x - interaction.startX
const dy = pos.y - interaction.startY const dy = pos.y - interaction.startY
@ -7922,6 +8016,31 @@ export default function TrainingTab(props: {
}) })
}, [markManualCorrection]) }, [markManualCorrection])
const addPosePerson = useCallback(() => {
if (uiLocked) return
markManualCorrection()
const nextPersonIndex = clonePosePersons(correctionRef.current.posePersons).length
setCorrection((prev) => {
const persons = clonePosePersons(prev.posePersons)
const next: CorrectionState = {
...prev,
posePersons: [...persons, createEmptyPosePerson()],
}
correctionRef.current = next
return next
})
setShowPoseSkeleton(true)
setActiveBoxIndex(null)
setActivePosePersonIndex(nextPersonIndex)
setPendingPoseKeypoint(null)
switchPoseKeypointMapView('body')
}, [markManualCorrection, switchPoseKeypointMapView, uiLocked])
const removePoseKeypoint = useCallback((personIndex: number, keypointIndex: number) => { const removePoseKeypoint = useCallback((personIndex: number, keypointIndex: number) => {
markManualCorrection() markManualCorrection()
@ -7964,58 +8083,6 @@ export default function TrainingTab(props: {
}) })
}, [markManualCorrection]) }, [markManualCorrection])
const removePoseFaceKeypoints = useCallback((personIndex: number) => {
markManualCorrection()
setCorrection((prev) => {
const persons = clonePosePersons(prev.posePersons)
const person = persons[personIndex]
if (!person) return prev
const keypoints = person.keypoints.map((point) =>
POSE_FACE_KEYPOINT_NAME_SET.has(poseKeypointId(point.name))
? {
...point,
x: 0,
y: 0,
conf: 0,
}
: point
)
const nextPerson = updatePosePersonQuality({
...person,
keypoints,
box: poseBoxFromKeypoints({
...person,
keypoints,
}),
})
persons[personIndex] = nextPerson
const next: CorrectionState = {
...prev,
posePersons: persons,
}
correctionRef.current = next
return next
})
setPendingPoseKeypoint((current) => {
if (
current?.personIndex === personIndex &&
POSE_FACE_KEYPOINT_NAME_SET.has(poseKeypointId(current.keypointName))
) {
return null
}
return current
})
}, [markManualCorrection])
const resetPosePersons = useCallback(() => { const resetPosePersons = useCallback(() => {
markManualCorrection() markManualCorrection()
const persons = clonePosePersons(sampleRef.current?.prediction.persons) const persons = clonePosePersons(sampleRef.current?.prediction.persons)
@ -8075,6 +8142,12 @@ export default function TrainingTab(props: {
if (!contentRect) return if (!contentRect) return
const pos = getPointerPosFromRect(contentRect, e.clientX, e.clientY) const pos = getPointerPosFromRect(contentRect, e.clientX, e.clientY)
const point = correctionRef.current.posePersons?.[personIndex]?.keypoints?.[keypointIndex]
const pointX = Number(point?.x)
const pointY = Number(point?.y)
const hasPointPosition =
Number.isFinite(pointX) &&
Number.isFinite(pointY)
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
@ -8105,8 +8178,8 @@ export default function TrainingTab(props: {
visible: true, visible: true,
clientX: e.clientX, clientX: e.clientX,
clientY: e.clientY, clientY: e.clientY,
imageX: pos.x, imageX: hasPointPosition ? clamp01(pointX) : pos.x,
imageY: pos.y, imageY: hasPointPosition ? clamp01(pointY) : pos.y,
}) })
}, [getImageContentRect, getPointerPosFromRect, uiLocked]) }, [getImageContentRect, getPointerPosFromRect, uiLocked])
@ -8163,12 +8236,17 @@ export default function TrainingTab(props: {
setLoadingPreviewFailed(false) setLoadingPreviewFailed(false)
}, [loading, frameBusy, loadingPreviewFallbackUrl, loadingPreviewUrl]) }, [loading, frameBusy, loadingPreviewFallbackUrl, loadingPreviewUrl])
const poseModeAvailable = hasPosePersons || originalPosePersonCount > 0 const poseModeAvailable = Boolean(sample) || hasPosePersons || originalPosePersonCount > 0
const poseModeActive = showPoseSkeleton && poseModeAvailable const poseModeActive = showPoseSkeleton && poseModeAvailable
const annotationLayerHidden = frameBusy || trainingRunning || saving const annotationLayerHidden = frameBusy || trainingRunning || saving
const poseSkeletonVisible = poseModeActive && hasPosePersons && !annotationLayerHidden const poseSkeletonVisible = poseModeActive && hasPosePersons && !annotationLayerHidden
const showImageBoxes = !annotationLayerHidden && !poseModeActive const showImageBoxes = !annotationLayerHidden && !poseModeActive
const showAnnotationMagnifier = showImageBoxes || poseSkeletonVisible const showAnnotationMagnifier = showImageBoxes || poseSkeletonVisible
const focusedPoseOverlayPersonIndex = poseCorrectionPersonIndex
const shouldRenderPoseOverlayPerson = useCallback((personIndex: number) =>
focusedPoseOverlayPersonIndex === null || personIndex === focusedPoseOverlayPersonIndex,
[focusedPoseOverlayPersonIndex]
)
const poseFaceImageZoomActive = const poseFaceImageZoomActive =
poseModeActive && poseModeActive &&
poseKeypointMapView === 'head' && poseKeypointMapView === 'head' &&
@ -8536,6 +8614,45 @@ export default function TrainingTab(props: {
poseFaceImageCameraAnimationFrameRef.current = requestAnimationFrame(animateZoomOut) poseFaceImageCameraAnimationFrameRef.current = requestAnimationFrame(animateZoomOut)
}, [poseFaceImageCamera, switchPoseKeypointMapView]) }, [poseFaceImageCamera, switchPoseKeypointMapView])
const deletePosePerson = useCallback((personIndex: number) => {
const shouldNavigateAfterDelete =
poseFaceImageZoomApplied &&
poseCorrectionPersonIndex === personIndex
if (!shouldNavigateAfterDelete) {
removePosePerson(personIndex)
return
}
const remainingVisibleIndexes = visiblePosePersonIndexes.filter((index) => index !== personIndex)
if (remainingVisibleIndexes.length === 0) {
removePosePerson(personIndex)
zoomOutPoseFaceImageToBody()
return
}
const currentPosition = visiblePosePersonIndexes.indexOf(personIndex)
const nextOldIndex =
remainingVisibleIndexes.find((index) =>
visiblePosePersonIndexes.indexOf(index) > currentPosition
) ?? remainingVisibleIndexes[0]
const nextPersonIndex = nextOldIndex > personIndex ? nextOldIndex - 1 : nextOldIndex
removePosePerson(personIndex)
setShowPoseSkeleton(true)
setActivePosePersonIndex(nextPersonIndex)
setPendingPoseKeypoint(null)
switchPoseKeypointMapView('head')
}, [
poseCorrectionPersonIndex,
poseFaceImageZoomApplied,
removePosePerson,
switchPoseKeypointMapView,
visiblePosePersonIndexes,
zoomOutPoseFaceImageToBody,
])
const finishPoseFaceImagePan = useCallback((event: React.PointerEvent<HTMLElement>) => { const finishPoseFaceImagePan = useCallback((event: React.PointerEvent<HTMLElement>) => {
const gesture = poseFaceImagePanGestureRef.current const gesture = poseFaceImagePanGestureRef.current
if (!gesture || gesture.pointerId !== event.pointerId) return if (!gesture || gesture.pointerId !== event.pointerId) return
@ -9501,7 +9618,8 @@ export default function TrainingTab(props: {
].filter(Boolean).join(' ')} ].filter(Boolean).join(' ')}
> >
{poseModeActive ? ( {poseModeActive ? (
!hasPoseItems ? ( <>
{!hasPoseItems ? (
<div className="flex min-h-32 items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50 px-3 py-6 text-center dark:border-white/10 dark:bg-white/[0.03]"> <div className="flex min-h-32 items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50 px-3 py-6 text-center dark:border-white/10 dark:bg-white/[0.03]">
<div> <div>
<div className="text-xs font-semibold text-gray-700 dark:text-gray-200"> <div className="text-xs font-semibold text-gray-700 dark:text-gray-200">
@ -9509,7 +9627,9 @@ export default function TrainingTab(props: {
</div> </div>
<div className="mt-1 max-w-[220px] text-[11px] leading-snug text-gray-500 dark:text-gray-400"> <div className="mt-1 max-w-[220px] text-[11px] leading-snug text-gray-500 dark:text-gray-400">
Reset stellt die zuletzt erkannte Pose wieder her. {originalPosePersonCount > 0
? 'Reset stellt die zuletzt erkannte Pose wieder her.'
: 'Lege eine Person an und setze die Pose-Punkte im Bild.'}
</div> </div>
</div> </div>
</div> </div>
@ -9636,7 +9756,7 @@ export default function TrainingTab(props: {
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (uiLocked) return if (uiLocked) return
removePosePerson(personIndex) deletePosePerson(personIndex)
}} }}
> >
<TrashIcon className="h-3.5 w-3.5" aria-hidden="true" /> <TrashIcon className="h-3.5 w-3.5" aria-hidden="true" />
@ -9705,7 +9825,34 @@ export default function TrainingTab(props: {
</div> </div>
) )
}) })
) )}
<button
type="button"
disabled={uiLocked}
onClick={addPosePerson}
className={[
'group flex w-full items-center gap-2.5 rounded-2xl border border-dashed px-3 py-3 text-left transition-all duration-200',
'border-blue-200 bg-blue-50/35 text-blue-800 hover:border-blue-300 hover:bg-blue-50 hover:shadow-sm',
'focus:outline-none focus:ring-2 focus:ring-blue-500/30',
'disabled:cursor-not-allowed disabled:opacity-50',
'dark:border-blue-400/25 dark:bg-blue-500/10 dark:text-blue-100 dark:hover:border-blue-300/40 dark:hover:bg-blue-500/15',
].join(' ')}
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white text-blue-700 ring-1 ring-blue-200 transition group-hover:bg-blue-100 dark:bg-blue-500/15 dark:text-blue-100 dark:ring-blue-300/30">
<UserPlusIcon className="h-4.5 w-4.5" aria-hidden="true" />
</span>
<span className="min-w-0">
<span className="block text-[13px] font-bold leading-tight">
Neue Person
</span>
<span className="mt-0.5 block text-[11px] leading-snug text-blue-700/75 dark:text-blue-100/70">
Pose-Skelett hinzufügen
</span>
</span>
</button>
</>
) : !hasBoxes ? ( ) : !hasBoxes ? (
<div className="flex min-h-32 items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50 px-3 py-6 text-center dark:border-white/10 dark:bg-white/[0.03]"> <div className="flex min-h-32 items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50 px-3 py-6 text-center dark:border-white/10 dark:bg-white/[0.03]">
<div> <div>
@ -10579,6 +10726,14 @@ export default function TrainingTab(props: {
style={imageLayerStyle} style={imageLayerStyle}
> >
{posePersons.map((person, personIndex) => { {posePersons.map((person, personIndex) => {
if (!shouldRenderPoseOverlayPerson(personIndex)) {
return null
}
if (poseFaceImageZoomActive) {
return null
}
if (!hasVisiblePoseBox(person) && !person.keypoints?.some(isPoseKeypointVisible)) { if (!hasVisiblePoseBox(person) && !person.keypoints?.some(isPoseKeypointVisible)) {
return null return null
} }
@ -10669,7 +10824,7 @@ export default function TrainingTab(props: {
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
removePosePerson(personIndex) deletePosePerson(personIndex)
}} }}
title="Diese Pose aus dem Training entfernen" title="Diese Pose aus dem Training entfernen"
aria-label="Pose entfernen" aria-label="Pose entfernen"
@ -10688,6 +10843,10 @@ export default function TrainingTab(props: {
preserveAspectRatio="none" preserveAspectRatio="none"
> >
{posePersons.map((person, personIndex) => { {posePersons.map((person, personIndex) => {
if (!shouldRenderPoseOverlayPerson(personIndex)) {
return null
}
const reliable = isPosePersonReliable(person) const reliable = isPosePersonReliable(person)
const color = reliable const color = reliable
? POSE_PERSON_COLORS[personIndex % POSE_PERSON_COLORS.length] ? POSE_PERSON_COLORS[personIndex % POSE_PERSON_COLORS.length]
@ -10699,6 +10858,14 @@ export default function TrainingTab(props: {
return ( return (
<g key={`pose-lines-${personIndex}`}> <g key={`pose-lines-${personIndex}`}>
{POSE_SKELETON_EDGES.map(([from, to]) => { {POSE_SKELETON_EDGES.map(([from, to]) => {
if (
poseFaceImageZoomActive &&
(!POSE_FACE_KEYPOINT_NAME_SET.has(from) ||
!POSE_FACE_KEYPOINT_NAME_SET.has(to))
) {
return null
}
const fromPoint = keypointsByName.get(from) const fromPoint = keypointsByName.get(from)
const toPoint = keypointsByName.get(to) const toPoint = keypointsByName.get(to)
@ -10731,6 +10898,10 @@ export default function TrainingTab(props: {
</svg> </svg>
{posePersons.map((person, personIndex) => { {posePersons.map((person, personIndex) => {
if (!shouldRenderPoseOverlayPerson(personIndex)) {
return null
}
const reliable = isPosePersonReliable(person) const reliable = isPosePersonReliable(person)
const color = reliable const color = reliable
? POSE_PERSON_COLORS[personIndex % POSE_PERSON_COLORS.length] ? POSE_PERSON_COLORS[personIndex % POSE_PERSON_COLORS.length]
@ -10740,9 +10911,20 @@ export default function TrainingTab(props: {
.map((point, pointIndex) => { .map((point, pointIndex) => {
if (!isPoseKeypointVisible(point)) return null if (!isPoseKeypointVisible(point)) return null
const keypointId = poseKeypointId(point.name)
if (
poseFaceImageZoomActive &&
!POSE_FACE_KEYPOINT_NAME_SET.has(keypointId)
) {
return null
}
const left = poseCoordPx(point.x, layerWidth) const left = poseCoordPx(point.x, layerWidth)
const top = poseCoordPx(point.y, layerHeight) const top = poseCoordPx(point.y, layerHeight)
const labelToLeft = left > layerWidth * 0.72 const facePlacement = poseFaceImageZoomActive
? poseFaceKeypointLabelPlacement(keypointId)
: null
const labelToLeft = facePlacement?.labelToLeft ?? left > layerWidth * 0.72
const label = poseKeypointLabel(point.name) const label = poseKeypointLabel(point.name)
const confidence = Math.round(clamp01(Number(point.conf)) * 100) const confidence = Math.round(clamp01(Number(point.conf)) * 100)
@ -10774,10 +10956,13 @@ export default function TrainingTab(props: {
style={{ style={{
left, left,
top, top,
transform: labelToLeft transform: facePlacement?.transform ?? (
? 'translate(calc(-100% + 4px), -50%)' labelToLeft
: 'translate(-4px, -50%)', ? 'translate(calc(-100% + 4px), -50%)'
: 'translate(-4px, -50%)'
),
opacity: reliable ? 1 : 0.72, opacity: reliable ? 1 : 0.72,
zIndex: facePlacement?.zIndex,
}} }}
title={`${label} | ${confidence}%`} title={`${label} | ${confidence}%`}
> >
@ -11234,6 +11419,24 @@ export default function TrainingTab(props: {
) )
const size = boxNeedsLargeMagnifier ? largeBoxSize : baseSize const size = boxNeedsLargeMagnifier ? largeBoxSize : baseSize
const poseMagnifierInteraction = poseInteraction
const activePoseMagnifierPoint = poseMagnifierInteraction
? correction.posePersons?.[poseMagnifierInteraction.personIndex]?.keypoints?.[
poseMagnifierInteraction.keypointIndex
] ?? null
: null
const activePoseMagnifierX = Number(activePoseMagnifierPoint?.x)
const activePoseMagnifierY = Number(activePoseMagnifierPoint?.y)
const hasUsablePoseMagnifierPoint =
Boolean(poseMagnifierInteraction) &&
Number.isFinite(activePoseMagnifierX) &&
Number.isFinite(activePoseMagnifierY)
const magnifierFocusX = hasUsablePoseMagnifierPoint
? clamp01(activePoseMagnifierX)
: boxCenterX
const magnifierFocusY = hasUsablePoseMagnifierPoint
? clamp01(activePoseMagnifierY)
: boxCenterY
const fitPadding = isTouchLike ? 14 : 18 const fitPadding = isTouchLike ? 14 : 18
@ -11255,15 +11458,15 @@ export default function TrainingTab(props: {
// Wenn gerade noch keine echte Box existiert, fällt sie auf den aktuellen Bildpunkt zurück. // Wenn gerade noch keine echte Box existiert, fällt sie auf den aktuellen Bildpunkt zurück.
const anchorX = hasUsableBox const anchorX = hasUsableBox
? rect.left + boxCenterX * rect.width ? rect.left + boxCenterX * rect.width
: rect.left + touchMagnifier.imageX * rect.width : rect.left + magnifierFocusX * rect.width
const anchorTop = hasUsableBox const anchorTop = hasUsableBox
? rect.top + activeBox.y * rect.height ? rect.top + activeBox.y * rect.height
: rect.top + touchMagnifier.imageY * rect.height : rect.top + magnifierFocusY * rect.height
const anchorBottom = hasUsableBox const anchorBottom = hasUsableBox
? rect.top + (activeBox.y + activeBox.h) * rect.height ? rect.top + (activeBox.y + activeBox.h) * rect.height
: rect.top + touchMagnifier.imageY * rect.height : rect.top + magnifierFocusY * rect.height
let left = anchorX - size / 2 let left = anchorX - size / 2
@ -11294,17 +11497,16 @@ export default function TrainingTab(props: {
const imageWidth = rect.width * zoom const imageWidth = rect.width * zoom
const imageHeight = rect.height * zoom const imageHeight = rect.height * zoom
const imageLeft = size / 2 - boxCenterX * imageWidth const imageLeft = size / 2 - magnifierFocusX * imageWidth
const imageTop = size / 2 - boxCenterY * imageHeight const imageTop = size / 2 - magnifierFocusY * imageHeight
const pointerX = imageLeft + touchMagnifier.imageX * imageWidth const pointerX = imageLeft + magnifierFocusX * imageWidth
const pointerY = imageTop + touchMagnifier.imageY * imageHeight const pointerY = imageTop + magnifierFocusY * imageHeight
const boxLeft = hasUsableBox ? imageLeft + activeBox.x * imageWidth : 0 const boxLeft = hasUsableBox ? imageLeft + activeBox.x * imageWidth : 0
const boxTop = hasUsableBox ? imageTop + activeBox.y * imageHeight : 0 const boxTop = hasUsableBox ? imageTop + activeBox.y * imageHeight : 0
const boxWidth = hasUsableBox ? activeBox.w * imageWidth : 0 const boxWidth = hasUsableBox ? activeBox.w * imageWidth : 0
const boxHeight = hasUsableBox ? activeBox.h * imageHeight : 0 const boxHeight = hasUsableBox ? activeBox.h * imageHeight : 0
const poseMagnifierInteraction = poseInteraction
const showPoseMagnifierSkeleton = poseSkeletonVisible && Boolean(poseMagnifierInteraction) const showPoseMagnifierSkeleton = poseSkeletonVisible && Boolean(poseMagnifierInteraction)
return typeof document !== 'undefined' return typeof document !== 'undefined'
@ -11338,6 +11540,10 @@ export default function TrainingTab(props: {
preserveAspectRatio="none" preserveAspectRatio="none"
> >
{posePersons.map((person, personIndex) => { {posePersons.map((person, personIndex) => {
if (!shouldRenderPoseOverlayPerson(personIndex)) {
return null
}
if (!person.keypoints?.some(isPoseKeypointVisible)) { if (!person.keypoints?.some(isPoseKeypointVisible)) {
return null return null
} }
@ -11357,6 +11563,14 @@ export default function TrainingTab(props: {
opacity={isActivePose ? 1 : reliable ? 0.78 : 0.52} opacity={isActivePose ? 1 : reliable ? 0.78 : 0.52}
> >
{POSE_SKELETON_EDGES.map(([from, to]) => { {POSE_SKELETON_EDGES.map(([from, to]) => {
if (
poseFaceImageZoomActive &&
(!POSE_FACE_KEYPOINT_NAME_SET.has(from) ||
!POSE_FACE_KEYPOINT_NAME_SET.has(to))
) {
return null
}
const fromPoint = keypointsByName.get(from) const fromPoint = keypointsByName.get(from)
const toPoint = keypointsByName.get(to) const toPoint = keypointsByName.get(to)
@ -11386,6 +11600,12 @@ export default function TrainingTab(props: {
{person.keypoints.map((point, pointIndex) => { {person.keypoints.map((point, pointIndex) => {
if (!isPoseKeypointVisible(point)) return null if (!isPoseKeypointVisible(point)) return null
if (
poseFaceImageZoomActive &&
!POSE_FACE_KEYPOINT_NAME_SET.has(poseKeypointId(point.name))
) {
return null
}
const isActivePoint = const isActivePoint =
poseMagnifierInteraction?.personIndex === personIndex && poseMagnifierInteraction?.personIndex === personIndex &&
@ -11501,11 +11721,12 @@ export default function TrainingTab(props: {
<button <button
type="button" type="button"
className="inline-flex h-7 items-center gap-1 rounded-full bg-red-500/90 px-2 text-[11px] font-bold leading-none text-white ring-1 ring-red-200/30 transition hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-200/70" className="inline-flex h-7 items-center gap-1 rounded-full bg-red-500/90 px-2 text-[11px] font-bold leading-none text-white ring-1 ring-red-200/30 transition hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-200/70"
onClick={() => removePoseFaceKeypoints(poseCorrectionPersonIndex)} onClick={() => deletePosePerson(poseCorrectionPersonIndex)}
title="Gesichtspunkte dieser Pose verwerfen" title="Ausgewählte Pose löschen"
aria-label="Ausgewählte Pose löschen"
> >
<XCircleIcon className="h-3.5 w-3.5" aria-hidden="true" /> <TrashIcon className="h-3.5 w-3.5" aria-hidden="true" />
Gesicht verwerfen Pose löschen
</button> </button>
</div> </div>
) : null} ) : null}