bugfixes
This commit is contained in:
parent
e571dd4d86
commit
5c9552447a
BIN
backend/dist/nsfwapp-linux-amd64
vendored
BIN
backend/dist/nsfwapp-linux-amd64
vendored
Binary file not shown.
BIN
backend/dist/nsfwapp.exe
vendored
BIN
backend/dist/nsfwapp.exe
vendored
Binary file not shown.
BIN
backend/dist/nsfwapp_amd64.deb
vendored
BIN
backend/dist/nsfwapp_amd64.deb
vendored
Binary file not shown.
@ -14,6 +14,7 @@ import {
|
||||
PhotoIcon,
|
||||
RectangleGroupIcon,
|
||||
SparklesIcon,
|
||||
UserGroupIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import Button from './Button'
|
||||
import Modal from './Modal'
|
||||
@ -29,6 +30,23 @@ export type TrainingFeedbackBox = {
|
||||
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 = {
|
||||
label: string
|
||||
score: number
|
||||
@ -45,6 +63,7 @@ export type TrainingFeedbackPrediction = {
|
||||
objectsPresent: TrainingFeedbackScoredLabel[]
|
||||
clothingPresent: TrainingFeedbackScoredLabel[]
|
||||
boxes?: TrainingFeedbackBox[]
|
||||
persons?: TrainingFeedbackPosePerson[]
|
||||
}
|
||||
|
||||
export type TrainingFeedbackCorrection = {
|
||||
@ -54,6 +73,7 @@ export type TrainingFeedbackCorrection = {
|
||||
objectsPresent: string[]
|
||||
clothingPresent: string[]
|
||||
boxes: TrainingFeedbackBox[]
|
||||
posePersons?: TrainingFeedbackPosePerson[]
|
||||
}
|
||||
|
||||
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(
|
||||
annotation: TrainingFeedbackAnnotation
|
||||
): TrainingFeedbackCorrection {
|
||||
@ -123,6 +262,7 @@ function annotationEffectiveCorrection(
|
||||
objectsPresent: [],
|
||||
clothingPresent: [],
|
||||
boxes: [],
|
||||
posePersons: [],
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,6 +270,9 @@ function annotationEffectiveCorrection(
|
||||
return {
|
||||
...annotation.correction,
|
||||
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),
|
||||
clothingPresent: (annotation.prediction.clothingPresent ?? []).map((item) => item.label),
|
||||
boxes: annotation.prediction.boxes ?? [],
|
||||
posePersons: normalizePosePersons(annotation.prediction.persons),
|
||||
}
|
||||
}
|
||||
|
||||
@ -370,6 +514,7 @@ function FilterButton(props: {
|
||||
function ReadonlyTrainingFrameViewer(props: {
|
||||
imageSrc: string
|
||||
boxes: TrainingFeedbackBox[]
|
||||
posePersons?: TrainingFeedbackPosePerson[]
|
||||
sourceFile?: string
|
||||
className?: string
|
||||
highlightedBoxIndex?: number | null
|
||||
@ -384,6 +529,10 @@ function ReadonlyTrainingFrameViewer(props: {
|
||||
.map(normalizeBox)
|
||||
.filter((box) => String(box.label || '').trim() && box.w > 0 && box.h > 0)
|
||||
}, [props.boxes])
|
||||
const posePersons = useMemo(
|
||||
() => visiblePosePersons(props.posePersons),
|
||||
[props.posePersons]
|
||||
)
|
||||
|
||||
type ImageContentRect = {
|
||||
left: number
|
||||
@ -612,6 +761,92 @@ function ReadonlyTrainingFrameViewer(props: {
|
||||
})}
|
||||
</div>
|
||||
) : 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>
|
||||
) : (
|
||||
@ -631,6 +866,7 @@ function FeedbackListItem(props: {
|
||||
}) {
|
||||
const effective = annotationEffectiveCorrection(props.item)
|
||||
const boxCount = effective.boxes?.length ?? 0
|
||||
const poseCount = visiblePosePersons(effective.posePersons).length
|
||||
const imageSrc = frameUrlForHistory(props.item)
|
||||
|
||||
return (
|
||||
@ -700,6 +936,10 @@ function FeedbackListItem(props: {
|
||||
{boxCount} Boxen
|
||||
</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">
|
||||
{normalizeSexPositionValue(effective.sexPosition)}
|
||||
</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) {
|
||||
const clean = String(path || '').trim()
|
||||
if (!clean) return ''
|
||||
@ -818,6 +1146,7 @@ function fileNameFromPath(path?: string) {
|
||||
function CompactSourceCard(props: {
|
||||
item: TrainingFeedbackAnnotation
|
||||
boxCount: number
|
||||
poseCount: number
|
||||
}) {
|
||||
const size = fileSizeLabel(props.item.sourceSizeBytes)
|
||||
|
||||
@ -853,6 +1182,10 @@ function CompactSourceCard(props: {
|
||||
{props.boxCount} Boxen
|
||||
</MetaPill>
|
||||
|
||||
<MetaPill icon={<UserGroupIcon className="h-3.5 w-3.5" aria-hidden="true" />}>
|
||||
{props.poseCount} Posen
|
||||
</MetaPill>
|
||||
|
||||
{size ? <MetaPill>{size}</MetaPill> : null}
|
||||
</div>
|
||||
</div>
|
||||
@ -984,6 +1317,7 @@ export default function TrainingFeedbackHistoryModal(props: {
|
||||
const negativeCount = props.items.filter((item) => item.negative).length
|
||||
const shownCount = props.items.length
|
||||
const selectedBoxCount = effective?.boxes?.length ?? 0
|
||||
const selectedPoseCount = visiblePosePersons(effective?.posePersons).length
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
if (props.onSearchChange) {
|
||||
@ -1015,6 +1349,9 @@ export default function TrainingFeedbackHistoryModal(props: {
|
||||
...effectiveItem.objectsPresent,
|
||||
...effectiveItem.clothingPresent,
|
||||
...(effectiveItem.boxes ?? []).map((box) => box.label),
|
||||
...visiblePosePersons(effectiveItem.posePersons).flatMap((person) =>
|
||||
(person.keypoints ?? []).map((point) => poseKeypointLabel(point.name))
|
||||
),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
@ -1319,6 +1656,7 @@ export default function TrainingFeedbackHistoryModal(props: {
|
||||
<ReadonlyTrainingFrameViewer
|
||||
imageSrc={selectedImageSrc}
|
||||
boxes={effective.boxes ?? []}
|
||||
posePersons={effective.posePersons}
|
||||
sourceFile={selected.sourceFile}
|
||||
className="shrink-0 mt-4"
|
||||
highlightedBoxIndex={hoveredBoxIndex}
|
||||
@ -1327,10 +1665,11 @@ export default function TrainingFeedbackHistoryModal(props: {
|
||||
<CompactSourceCard
|
||||
item={selected}
|
||||
boxCount={selectedBoxCount}
|
||||
poseCount={selectedPoseCount}
|
||||
/>
|
||||
</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
|
||||
selected={selected}
|
||||
effective={effective}
|
||||
@ -1341,6 +1680,8 @@ export default function TrainingFeedbackHistoryModal(props: {
|
||||
hoveredBoxIndex={hoveredBoxIndex}
|
||||
onHoveredBoxIndexChange={setHoveredBoxIndex}
|
||||
/>
|
||||
|
||||
<PoseDetailsList persons={effective.posePersons} />
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
PauseIcon,
|
||||
RectangleGroupIcon,
|
||||
UserGroupIcon,
|
||||
UserPlusIcon,
|
||||
TrashIcon,
|
||||
VideoCameraIcon,
|
||||
XCircleIcon,
|
||||
@ -435,6 +436,43 @@ const POSE_FACE_KEYPOINT_NAME_SET = new Set([
|
||||
'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 = {
|
||||
keypointName: keyof typeof POSE_KEYPOINT_LABELS
|
||||
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 {
|
||||
const points = (person.keypoints ?? []).filter((point) =>
|
||||
Number.isFinite(point.x) &&
|
||||
@ -4013,7 +4067,7 @@ export default function TrainingTab(props: {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const [showPoseSkeleton, setShowPoseSkeleton] = useState(true)
|
||||
const [showPoseSkeleton, setShowPoseSkeleton] = useState(false)
|
||||
|
||||
const [frameNaturalSize, setFrameNaturalSize] = useState<{
|
||||
width: number
|
||||
@ -4193,6 +4247,7 @@ export default function TrainingTab(props: {
|
||||
setTouchMagnifier(null)
|
||||
setBoxLabel('')
|
||||
setActiveBoxIndex(null)
|
||||
setShowPoseSkeleton(false)
|
||||
setActivePosePersonIndex(null)
|
||||
setPendingPoseKeypoint(null)
|
||||
if (poseKeypointMapAnimationFrameRef.current !== null) {
|
||||
@ -4508,6 +4563,39 @@ export default function TrainingTab(props: {
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
@ -5448,17 +5536,7 @@ export default function TrainingTab(props: {
|
||||
setTouchMagnifier(null)
|
||||
setBoxLabel('')
|
||||
setActiveBoxIndex(null)
|
||||
setActivePosePersonIndex(null)
|
||||
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)
|
||||
resetPoseUiToBoxes()
|
||||
setMobilePanel(trainingRunningRef.current ? 'training' : 'labels')
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
@ -5499,7 +5577,7 @@ export default function TrainingTab(props: {
|
||||
clothing: false,
|
||||
}
|
||||
)
|
||||
}, [])
|
||||
}, [resetPoseUiToBoxes])
|
||||
|
||||
const setQueuedTrainingSamples = useCallback((nextQueue: QueuedTrainingSample[]) => {
|
||||
importedSampleQueueRef.current = nextQueue
|
||||
@ -5660,6 +5738,7 @@ export default function TrainingTab(props: {
|
||||
setLoadingPreviewCandidate(previewUrl)
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
resetPoseUiToBoxes()
|
||||
setAnalysisSourceFile('')
|
||||
setAnalysisProgress(8)
|
||||
setAnalysisStep(
|
||||
@ -5798,6 +5877,7 @@ export default function TrainingTab(props: {
|
||||
}, [
|
||||
applyTrainingAnalysisEvent,
|
||||
completeNextFromData,
|
||||
resetPoseUiToBoxes,
|
||||
setLoadingPreviewCandidate,
|
||||
waitForNextResult,
|
||||
])
|
||||
@ -7295,31 +7375,11 @@ export default function TrainingTab(props: {
|
||||
? getPointerPosInImage(snapshot.clientX, snapshot.clientY, { clamp: false }) ?? 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) {
|
||||
markManualCorrection()
|
||||
|
||||
let nextPoseMagnifier: MagnifierState | null = null
|
||||
|
||||
setCorrection((prev) => {
|
||||
const persons = clonePosePersons(prev.posePersons)
|
||||
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),
|
||||
}
|
||||
|
||||
nextPoseMagnifier = {
|
||||
visible: true,
|
||||
clientX: snapshot.clientX,
|
||||
clientY: snapshot.clientY,
|
||||
imageX: nextPoint.x,
|
||||
imageY: nextPoint.y,
|
||||
}
|
||||
|
||||
const nextPerson = updatePosePersonQuality({
|
||||
...person,
|
||||
box: poseBoxFromKeypoints({
|
||||
@ -7360,9 +7428,35 @@ export default function TrainingTab(props: {
|
||||
return next
|
||||
})
|
||||
|
||||
if (nextPoseMagnifier) {
|
||||
setTouchMagnifier(nextPoseMagnifier)
|
||||
}
|
||||
|
||||
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) {
|
||||
const dx = pos.x - interaction.startX
|
||||
const dy = pos.y - interaction.startY
|
||||
@ -7922,6 +8016,31 @@ export default function TrainingTab(props: {
|
||||
})
|
||||
}, [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) => {
|
||||
markManualCorrection()
|
||||
|
||||
@ -7964,58 +8083,6 @@ export default function TrainingTab(props: {
|
||||
})
|
||||
}, [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(() => {
|
||||
markManualCorrection()
|
||||
const persons = clonePosePersons(sampleRef.current?.prediction.persons)
|
||||
@ -8075,6 +8142,12 @@ export default function TrainingTab(props: {
|
||||
if (!contentRect) return
|
||||
|
||||
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.stopPropagation()
|
||||
@ -8105,8 +8178,8 @@ export default function TrainingTab(props: {
|
||||
visible: true,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
imageX: pos.x,
|
||||
imageY: pos.y,
|
||||
imageX: hasPointPosition ? clamp01(pointX) : pos.x,
|
||||
imageY: hasPointPosition ? clamp01(pointY) : pos.y,
|
||||
})
|
||||
}, [getImageContentRect, getPointerPosFromRect, uiLocked])
|
||||
|
||||
@ -8163,12 +8236,17 @@ export default function TrainingTab(props: {
|
||||
setLoadingPreviewFailed(false)
|
||||
}, [loading, frameBusy, loadingPreviewFallbackUrl, loadingPreviewUrl])
|
||||
|
||||
const poseModeAvailable = hasPosePersons || originalPosePersonCount > 0
|
||||
const poseModeAvailable = Boolean(sample) || hasPosePersons || originalPosePersonCount > 0
|
||||
const poseModeActive = showPoseSkeleton && poseModeAvailable
|
||||
const annotationLayerHidden = frameBusy || trainingRunning || saving
|
||||
const poseSkeletonVisible = poseModeActive && hasPosePersons && !annotationLayerHidden
|
||||
const showImageBoxes = !annotationLayerHidden && !poseModeActive
|
||||
const showAnnotationMagnifier = showImageBoxes || poseSkeletonVisible
|
||||
const focusedPoseOverlayPersonIndex = poseCorrectionPersonIndex
|
||||
const shouldRenderPoseOverlayPerson = useCallback((personIndex: number) =>
|
||||
focusedPoseOverlayPersonIndex === null || personIndex === focusedPoseOverlayPersonIndex,
|
||||
[focusedPoseOverlayPersonIndex]
|
||||
)
|
||||
const poseFaceImageZoomActive =
|
||||
poseModeActive &&
|
||||
poseKeypointMapView === 'head' &&
|
||||
@ -8536,6 +8614,45 @@ export default function TrainingTab(props: {
|
||||
poseFaceImageCameraAnimationFrameRef.current = requestAnimationFrame(animateZoomOut)
|
||||
}, [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 gesture = poseFaceImagePanGestureRef.current
|
||||
if (!gesture || gesture.pointerId !== event.pointerId) return
|
||||
@ -9501,7 +9618,8 @@ export default function TrainingTab(props: {
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
{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>
|
||||
<div className="text-xs font-semibold text-gray-700 dark:text-gray-200">
|
||||
@ -9509,7 +9627,9 @@ export default function TrainingTab(props: {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@ -9636,7 +9756,7 @@ export default function TrainingTab(props: {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (uiLocked) return
|
||||
removePosePerson(personIndex)
|
||||
deletePosePerson(personIndex)
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
@ -9705,7 +9825,34 @@ export default function TrainingTab(props: {
|
||||
</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 ? (
|
||||
<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>
|
||||
@ -10579,6 +10726,14 @@ export default function TrainingTab(props: {
|
||||
style={imageLayerStyle}
|
||||
>
|
||||
{posePersons.map((person, personIndex) => {
|
||||
if (!shouldRenderPoseOverlayPerson(personIndex)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (poseFaceImageZoomActive) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!hasVisiblePoseBox(person) && !person.keypoints?.some(isPoseKeypointVisible)) {
|
||||
return null
|
||||
}
|
||||
@ -10669,7 +10824,7 @@ export default function TrainingTab(props: {
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
removePosePerson(personIndex)
|
||||
deletePosePerson(personIndex)
|
||||
}}
|
||||
title="Diese Pose aus dem Training entfernen"
|
||||
aria-label="Pose entfernen"
|
||||
@ -10688,6 +10843,10 @@ export default function TrainingTab(props: {
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
{posePersons.map((person, personIndex) => {
|
||||
if (!shouldRenderPoseOverlayPerson(personIndex)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const reliable = isPosePersonReliable(person)
|
||||
const color = reliable
|
||||
? POSE_PERSON_COLORS[personIndex % POSE_PERSON_COLORS.length]
|
||||
@ -10699,6 +10858,14 @@ export default function TrainingTab(props: {
|
||||
return (
|
||||
<g key={`pose-lines-${personIndex}`}>
|
||||
{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 toPoint = keypointsByName.get(to)
|
||||
|
||||
@ -10731,6 +10898,10 @@ export default function TrainingTab(props: {
|
||||
</svg>
|
||||
|
||||
{posePersons.map((person, personIndex) => {
|
||||
if (!shouldRenderPoseOverlayPerson(personIndex)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const reliable = isPosePersonReliable(person)
|
||||
const color = reliable
|
||||
? POSE_PERSON_COLORS[personIndex % POSE_PERSON_COLORS.length]
|
||||
@ -10740,9 +10911,20 @@ export default function TrainingTab(props: {
|
||||
.map((point, pointIndex) => {
|
||||
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 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 confidence = Math.round(clamp01(Number(point.conf)) * 100)
|
||||
|
||||
@ -10774,10 +10956,13 @@ export default function TrainingTab(props: {
|
||||
style={{
|
||||
left,
|
||||
top,
|
||||
transform: labelToLeft
|
||||
? 'translate(calc(-100% + 4px), -50%)'
|
||||
: 'translate(-4px, -50%)',
|
||||
transform: facePlacement?.transform ?? (
|
||||
labelToLeft
|
||||
? 'translate(calc(-100% + 4px), -50%)'
|
||||
: 'translate(-4px, -50%)'
|
||||
),
|
||||
opacity: reliable ? 1 : 0.72,
|
||||
zIndex: facePlacement?.zIndex,
|
||||
}}
|
||||
title={`${label} | ${confidence}%`}
|
||||
>
|
||||
@ -11234,6 +11419,24 @@ export default function TrainingTab(props: {
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@ -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.
|
||||
const anchorX = hasUsableBox
|
||||
? rect.left + boxCenterX * rect.width
|
||||
: rect.left + touchMagnifier.imageX * rect.width
|
||||
: rect.left + magnifierFocusX * rect.width
|
||||
|
||||
const anchorTop = hasUsableBox
|
||||
? rect.top + activeBox.y * rect.height
|
||||
: rect.top + touchMagnifier.imageY * rect.height
|
||||
: rect.top + magnifierFocusY * rect.height
|
||||
|
||||
const anchorBottom = hasUsableBox
|
||||
? rect.top + (activeBox.y + activeBox.h) * rect.height
|
||||
: rect.top + touchMagnifier.imageY * rect.height
|
||||
: rect.top + magnifierFocusY * rect.height
|
||||
|
||||
let left = anchorX - size / 2
|
||||
|
||||
@ -11294,17 +11497,16 @@ export default function TrainingTab(props: {
|
||||
const imageWidth = rect.width * zoom
|
||||
const imageHeight = rect.height * zoom
|
||||
|
||||
const imageLeft = size / 2 - boxCenterX * imageWidth
|
||||
const imageTop = size / 2 - boxCenterY * imageHeight
|
||||
const imageLeft = size / 2 - magnifierFocusX * imageWidth
|
||||
const imageTop = size / 2 - magnifierFocusY * imageHeight
|
||||
|
||||
const pointerX = imageLeft + touchMagnifier.imageX * imageWidth
|
||||
const pointerY = imageTop + touchMagnifier.imageY * imageHeight
|
||||
const pointerX = imageLeft + magnifierFocusX * imageWidth
|
||||
const pointerY = imageTop + magnifierFocusY * imageHeight
|
||||
|
||||
const boxLeft = hasUsableBox ? imageLeft + activeBox.x * imageWidth : 0
|
||||
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'
|
||||
@ -11338,6 +11540,10 @@ export default function TrainingTab(props: {
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
{posePersons.map((person, personIndex) => {
|
||||
if (!shouldRenderPoseOverlayPerson(personIndex)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!person.keypoints?.some(isPoseKeypointVisible)) {
|
||||
return null
|
||||
}
|
||||
@ -11357,6 +11563,14 @@ export default function TrainingTab(props: {
|
||||
opacity={isActivePose ? 1 : reliable ? 0.78 : 0.52}
|
||||
>
|
||||
{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 toPoint = keypointsByName.get(to)
|
||||
|
||||
@ -11386,6 +11600,12 @@ export default function TrainingTab(props: {
|
||||
|
||||
{person.keypoints.map((point, pointIndex) => {
|
||||
if (!isPoseKeypointVisible(point)) return null
|
||||
if (
|
||||
poseFaceImageZoomActive &&
|
||||
!POSE_FACE_KEYPOINT_NAME_SET.has(poseKeypointId(point.name))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isActivePoint =
|
||||
poseMagnifierInteraction?.personIndex === personIndex &&
|
||||
@ -11501,11 +11721,12 @@ export default function TrainingTab(props: {
|
||||
<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"
|
||||
onClick={() => removePoseFaceKeypoints(poseCorrectionPersonIndex)}
|
||||
title="Gesichtspunkte dieser Pose verwerfen"
|
||||
onClick={() => deletePosePerson(poseCorrectionPersonIndex)}
|
||||
title="Ausgewählte Pose löschen"
|
||||
aria-label="Ausgewählte Pose löschen"
|
||||
>
|
||||
<XCircleIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Gesicht verwerfen
|
||||
<TrashIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Pose löschen
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user