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,
|
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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user