cvat-ai / cvat

Annotate better with CVAT, the industry-leading data engine for machine learning. Used and trusted by teams at any scale, for data of any scale.

Home Page:https://cvat.ai

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Semi automatic annotation Skeleton w/ Mediapipe : TypeError Cannot read properties of undefined (reading 'attr')

H04K opened this issue · comments

commented

Actions before raising this issue

  • I searched the existing issues and did not find anything similar.
  • I read/searched the docs

Steps to Reproduce

  1. Use the provided function.yaml and main.py files to set up a Nuclio function.
  2. Call the annotate function.
  3. Observe the JavaScript error in the CVAT UI

Expected Behavior

Semi automatic annotation for right hand using media pipe, similar to open pose skeleton annotation but with media pipe

Possible Solution

No response

Context

I'm trying to use media pipe and cvat for semi automatic annotation of hands, I'm using the mmpose code as reference.
Here is the function.yaml

metadata:
  name: mediapipe
  namespace: cvat
  annotations:
    name: Mediapipe
    type: detector
    framework: tensorflow
    spec: |
      [{
            "name":"r_hand",
            "type":"skeleton",
            "svg":"<line x1=\"74.8981704711914\" y1=\"59.111854553222656\" x2=\"77.56928253173828\" y2=\"55.27212142944336\" data-type=\"edge\" data-node-from=\"19\" data-node-to=\"20\"></line>\n<line x1=\"70.72454833984375\" y1=\"64.12020111083984\" x2=\"74.8981704711914\" y2=\"59.111854553222656\" data-type=\"edge\" data-node-from=\"18\" data-node-to=\"19\"></line>\n<line x1=\"67.0517578125\" y1=\"68.79466247558594\" x2=\"70.72454833984375\" y2=\"64.12020111083984\" data-type=\"edge\" data-node-from=\"17\" data-node-to=\"18\"></line>\n<line x1=\"63.87980270385742\" y1=\"56.60768127441406\" x2=\"64.8814697265625\" y2=\"48.92821502685547\" data-type=\"edge\" data-node-from=\"15\" data-node-to=\"16\"></line>\n<line x1=\"62.71118927001953\" y1=\"62.617698669433594\" x2=\"63.87980270385742\" y2=\"56.60768127441406\" data-type=\"edge\" data-node-from=\"14\" data-node-to=\"15\"></line>\n<line x1=\"60.874794006347656\" y1=\"67.9599380493164\" x2=\"62.71118927001953\" y2=\"62.617698669433594\" data-type=\"edge\" data-node-from=\"13\" data-node-to=\"14\"></line>\n<line x1=\"54.196998596191406\" y1=\"52.767948150634766\" x2=\"54.36394500732422\" y2=\"40.914859771728516\" data-type=\"edge\" data-node-from=\"11\" data-node-to=\"12\"></line>\n<line x1=\"53.362274169921875\" y1=\"59.94657897949219\" x2=\"54.196998596191406\" y2=\"52.767948150634766\" data-type=\"edge\" data-node-from=\"10\" data-node-to=\"11\"></line>\n<line x1=\"53.86310958862305\" y1=\"67.45909881591797\" x2=\"53.362274169921875\" y2=\"59.94657897949219\" data-type=\"edge\" data-node-from=\"9\" data-node-to=\"10\"></line>\n<line x1=\"41.175296783447266\" y1=\"52.434059143066406\" x2=\"39.00501251220703\" y2=\"45.75625991821289\" data-type=\"edge\" data-node-from=\"7\" data-node-to=\"8\"></line>\n<line x1=\"43.3455810546875\" y1=\"60.28046798706055\" x2=\"41.175296783447266\" y2=\"52.434059143066406\" data-type=\"edge\" data-node-from=\"6\" data-node-to=\"7\"></line>\n<line x1=\"45.849754333496094\" y1=\"68.62771606445312\" x2=\"43.3455810546875\" y2=\"60.28046798706055\" data-type=\"edge\" data-node-from=\"5\" data-node-to=\"6\"></line>\n<line x1=\"67.0517578125\" y1=\"68.79466247558594\" x2=\"48.85476303100586\" y2=\"88.32721710205078\" data-type=\"edge\" data-node-from=\"17\" data-node-to=\"0\"></line>\n<line x1=\"60.874794006347656\" y1=\"67.9599380493164\" x2=\"67.0517578125\" y2=\"68.79466247558594\" data-type=\"edge\" data-node-from=\"13\" data-node-to=\"17\"></line>\n<line x1=\"53.86310958862305\" y1=\"67.45909881591797\" x2=\"60.874794006347656\" y2=\"67.9599380493164\" data-type=\"edge\" data-node-from=\"9\" data-node-to=\"13\"></line>\n<line x1=\"45.849754333496094\" y1=\"68.62771606445312\" x2=\"53.86310958862305\" y2=\"67.45909881591797\" data-type=\"edge\" data-node-from=\"5\" data-node-to=\"9\"></line>\n<line x1=\"48.85476303100586\" y1=\"88.32721710205078\" x2=\"45.849754333496094\" y2=\"68.62771606445312\" data-type=\"edge\" data-node-from=\"0\" data-node-to=\"5\"></line>\n<line x1=\"24.981639862060547\" y1=\"71.79966735839844\" x2=\"18.639402389526367\" y2=\"66.79132080078125\" data-type=\"edge\" data-node-from=\"3\" data-node-to=\"4\"></line>\n<line x1=\"29.155261993408203\" y1=\"78.31051635742188\" x2=\"24.981639862060547\" y2=\"71.79966735839844\" data-type=\"edge\" data-node-from=\"2\" data-node-to=\"3\"></line>\n<line x1=\"37.502506256103516\" y1=\"85.15525817871094\" x2=\"29.155261993408203\" y2=\"78.31051635742188\" data-type=\"edge\" data-node-from=\"1\" data-node-to=\"2\"></line>\n<line x1=\"48.85476303100586\" y1=\"88.32721710205078\" x2=\"37.502506256103516\" y2=\"85.15525817871094\" data-type=\"edge\" data-node-from=\"0\" data-node-to=\"1\"></line>\n<circle r=\"0.75\" cx=\"48.85476303100586\" cy=\"88.32721710205078\" data-type=\"element node\" data-element-id=\"0\" data-node-id=\"0\" data-label-id=\"0\" data-label-name=\"0\"></circle>\n<circle r=\"0.75\" cx=\"37.502506256103516\" cy=\"85.15525817871094\" data-type=\"element node\" data-element-id=\"1\" data-node-id=\"1\" data-label-id=\"1\" data-label-name=\"1\"></circle>\n<circle r=\"0.75\" cx=\"29.155261993408203\" cy=\"78.31051635742188\" data-type=\"element node\" data-element-id=\"2\" data-node-id=\"2\" data-label-id=\"2\" data-label-name=\"2\"></circle>\n<circle r=\"0.75\" cx=\"24.981639862060547\" cy=\"71.79966735839844\" data-type=\"element node\" data-element-id=\"3\" data-node-id=\"3\" data-label-id=\"3\" data-label-name=\"3\"></circle>\n<circle r=\"0.75\" cx=\"18.639402389526367\" cy=\"66.79132080078125\" data-type=\"element node\" data-element-id=\"4\" data-node-id=\"4\" data-label-id=\"4\" data-label-name=\"4\"></circle>\n<circle r=\"0.75\" cx=\"45.849754333496094\" cy=\"68.62771606445312\" data-type=\"element node\" data-element-id=\"5\" data-node-id=\"5\" data-label-id=\"5\" data-label-name=\"5\"></circle>\n<circle r=\"0.75\" cx=\"43.3455810546875\" cy=\"60.28046798706055\" data-type=\"element node\" data-element-id=\"6\" data-node-id=\"6\" data-label-id=\"6\" data-label-name=\"6\"></circle>\n<circle r=\"0.75\" cx=\"41.175296783447266\" cy=\"52.434059143066406\" data-type=\"element node\" data-element-id=\"7\" data-node-id=\"7\" data-label-id=\"7\" data-label-name=\"7\"></circle>\n<circle r=\"0.75\" cx=\"39.00501251220703\" cy=\"45.75625991821289\" data-type=\"element node\" data-element-id=\"8\" data-node-id=\"8\" data-label-id=\"8\" data-label-name=\"8\"></circle>\n<circle r=\"0.75\" cx=\"53.86310958862305\" cy=\"67.45909881591797\" data-type=\"element node\" data-element-id=\"9\" data-node-id=\"9\" data-label-id=\"9\" data-label-name=\"9\"></circle>\n<circle r=\"0.75\" cx=\"53.362274169921875\" cy=\"59.94657897949219\" data-type=\"element node\" data-element-id=\"10\" data-node-id=\"10\" data-label-id=\"10\" data-label-name=\"10\"></circle>\n<circle r=\"0.75\" cx=\"54.196998596191406\" cy=\"52.767948150634766\" data-type=\"element node\" data-element-id=\"11\" data-node-id=\"11\" data-label-id=\"11\" data-label-name=\"11\"></circle>\n<circle r=\"0.75\" cx=\"54.36394500732422\" cy=\"40.914859771728516\" data-type=\"element node\" data-element-id=\"12\" data-node-id=\"12\" data-label-id=\"12\" data-label-name=\"12\"></circle>\n<circle r=\"0.75\" cx=\"60.874794006347656\" cy=\"67.9599380493164\" data-type=\"element node\" data-element-id=\"13\" data-node-id=\"13\" data-label-id=\"13\" data-label-name=\"13\"></circle>\n<circle r=\"0.75\" cx=\"62.71118927001953\" cy=\"62.617698669433594\" data-type=\"element node\" data-element-id=\"14\" data-node-id=\"14\" data-label-id=\"14\" data-label-name=\"14\"></circle>\n<circle r=\"0.75\" cx=\"63.87980270385742\" cy=\"56.60768127441406\" data-type=\"element node\" data-element-id=\"15\" data-node-id=\"15\" data-label-id=\"15\" data-label-name=\"15\"></circle>\n<circle r=\"0.75\" cx=\"64.8814697265625\" cy=\"48.92821502685547\" data-type=\"element node\" data-element-id=\"16\" data-node-id=\"16\" data-label-id=\"16\" data-label-name=\"16\"></circle>\n<circle r=\"0.75\" cx=\"67.0517578125\" cy=\"68.79466247558594\" data-type=\"element node\" data-element-id=\"17\" data-node-id=\"17\" data-label-id=\"17\" data-label-name=\"17\"></circle>\n<circle r=\"0.75\" cx=\"70.72454833984375\" cy=\"64.12020111083984\" data-type=\"element node\" data-element-id=\"18\" data-node-id=\"18\" data-label-id=\"18\" data-label-name=\"18\" ></circle>\n<circle r=\"0.75\" cx=\"74.8981704711914\" cy=\"59.111854553222656\" data-type=\"element node\" data-element-id=\"19\" data-node-id=\"19\" data-label-id=\"19\" data-label-name=\"19\"></circle>\n<circle r=\"0.75\" cx=\"77.56928253173828\" cy=\"55.27212142944336\" data-type=\"element node\" data-element-id=\"20\" data-node-id=\"20\" data-label-id=\"20\" data-label-name=\"20\"></circle>",
            "sublabels":[
              {
                  "id":0,
                  "name":"0",
                  "type":"points"
              },
              {
                  "id":1,
                  "name":"1",
                  "type":"points"
              },
              {
                  "id":2,
                  "name":"2",
                  "type":"points"
              },
              {
                  "id":3,
                  "name":"3",
                  "type":"points"
              },
              {
                  "id":4,
                  "name":"4",
                  "type":"points"
              },
              {
                  "id":5,
                  "name":"5",
                  "type":"points"
              },
              {
                  "id":6,
                  "name":"6",
                  "type":"points"
              },
              {
                  "id":7,
                  "name":"7",
                  "type":"points"
              },
              {
                  "id":8,
                  "name":"8",
                  "type":"points"
              },
              {
                  "id":9,
                  "name":"9",
                  "type":"points"
              },
              {
                  "id":10,
                  "name":"10",
                  "type":"points"
              },
              {
                  "id":11,
                  "name":"11",
                  "type":"points"
              },
              {
                  "id":12,
                  "name":"12",
                  "type":"points"
              },
              {
                  "id":13,
                  "name":"13",
                  "type":"points"
              },
              {
                  "id":14,
                  "name":"14",
                  "type":"points"
              },
              {
                  "id":15,
                  "name":"15",
                  "type":"points"
              },
              {
                  "id":16,
                  "name":"16",
                  "type":"points"
              },
              {
                  "id":17,
                  "name":"17",
                  "type":"points"
              },
              {
                  "id":18,
                  "name":"18",
                  "type":"points"
              },
              {
                  "id":19,
                  "name":"19",
                  "type":"points"
              },
              {
                  "id":20,
                  "name":"20",
                  "type":"points"}
          ]
        }
      ]

spec: 
  description: mediapipe
  runtime: 'python:3.8'
  handler: main:handler
  eventTimeout: 30s

  build:
    image: cvat.tensortflow.mediapipe.base
    baseImage: ubuntu:20.04

    directives:
      preCopy:
        - kind: RUN
          value: |-
            apt update \
              && apt install -y --no-install-recommends \
                git \
                ca-certificates \
                python-is-python3 \
                python3 \
                python3-pip \
              && rm -rf /var/lib/apt/lists/*
        - kind: RUN
          value: pip3 install mediapipe
        - kind: RUN
          value: pip3 install opencv-python-headless
        - kind: RUN
          value: pip3 install numpy
        - kind: RUN
          value: pip3 install matplotlib
        - kind: RUN
          value: pip3 install pyyaml
        - kind: RUN
          value: pip3 install pillow
        - kind: WORKDIR
          value: /opt/nuclio

  triggers:
    myHttpTrigger:
      maxWorkers: 2
      kind: 'http'
      workerAvailabilityTimeoutMilliseconds: 10000
      attributes:
        maxRequestBodySize: 33554432 # 32MB

  platform:
    attributes:
      restartPolicy:
        name: always
        maximumRetryCount: 3
      mountMode: volume

  resources:
    limits:
      cpu: 2
      memory: 350M

and here is the main.py

import cv2
import mediapipe as mp
import json
import base64
import numpy as np
from PIL import Image
import io
import yaml

import cv2
import mediapipe as mp
import json
import base64
import numpy as np
from PIL import Image
import io
import yaml

def transform_data(data):
    transformed_data = []

    for item in data:
        if item['type'] == 'points':
            elements = []
            for point in item['points']:
                elements.append({
                    'label': point['name'],
                    'type': 'points',
                    'outside': 0,
                    'points': [float(point['x']), float(point['y'])],
                    'confidence': '0.8'
            
            })

            transformed_data.append({
                'confidence': '0.90301526',
                'label': item['name'],
                'type': 'skeleton',
                'elements': elements
            })

    return transformed_data

def init_context(context):
    context.logger.info("Init detector...")

    mp_holistic = mp.solutions.holistic
    holistic = mp_holistic.Holistic()

    context.logger.info("Init labels...")
    with open("/opt/nuclio/function.yaml", "rb") as function_file:
        functionconfig = yaml.safe_load(function_file)
        labels_spec = functionconfig["metadata"]["annotations"]["spec"]
        labels = json.loads(labels_spec)

    context.user_data.labels = labels
    context.user_data.holistic = holistic
    context.logger.info("Function initialized")





def convert_landmarks_to_cvat_format(landmarks, name):
    if landmarks is None:
        return None

    # Load the CVAT format from the YAML file
    with open('function.yaml', 'r') as f:
        cvat_format = yaml.safe_load(f)

    # Convert the spec string to a dictionary
    spec = yaml.safe_load(cvat_format['metadata']['annotations']['spec'])

    # Find the correct skeleton in the CVAT format
    skeleton = next((s for s in spec if s['name'] == name), None)

    # Convert the landmarks to the CVAT format
    points = []
    if skeleton is not None:
        for i, landmark in enumerate(landmarks.landmark):
            if i < len(skeleton['sublabels']):
                point = {
                    'id': skeleton['sublabels'][i]['id'],
                    'name': str(i),
                    'x': landmark.x,
                    'y': landmark.y,
                    'visibility': '0.8',
                }
                points.append(point)

    return points

def handler(context, event):
    context.logger.info("Run MediaPipe Holistic model")

    # Load the image from the event data
    data = event.body
    buf = io.BytesIO(base64.b64decode(data["image"]))
    image = Image.open(buf).convert("RGB")
    image = np.array(image)
    results = context.user_data.holistic.process(image)

    # Extract the pose landmarks
    right_hand_landmarks = results.right_hand_landmarks

    # Convert the landmarks to the format expected by CVAT
    right_hand_data = convert_landmarks_to_cvat_format(right_hand_landmarks, 'r_hand')

    # Combine the data for all the parts into a list of dictionaries
    data_list = []

    if right_hand_data is not None:
        data_list.append({
            'name': 'r_hand',
            'type': 'points',
            'points': right_hand_data
        })


    # Transform the data
    transformed_data = transform_data(data_list)
    print(transformed_data)
    # Return the data as a JSON object using context.Response
    return context.Response(body=json.dumps(transformed_data), headers={}, content_type="application/json", status_code=200)

Calling the annotate works great here the log from the nuclio function

[{'confidence': '0.90301526', 'label': 'r_hand', 'type': 'skeleton', 'elements': [{'label': '0', 'type': 'points', 'outside': 0, 'points': [0.4395136833190918, 0.5782752633094788], 'confidence': '0.0'}, {'label': '1', 'type': 'points', 'outside': 0, 'points': [0.47249719500541687, 0.5689968466758728], 'confidence': '0.0'}, {'label': '2', 'type': 'points', 'outside': 0, 'points': [0.49751439690589905, 0.5525428056716919], 'confidence': '0.0'}, {'label': '3', 'type': 'points', 'outside': 0, 'points': [0.5127729773521423, 0.5342648029327393], 'confidence': '0.0'}, {'label': '4', 'type': 'points', 'outside': 0, 'points': [0.5175373554229736, 0.510733962059021], 'confidence': '0.0'}, {'label': '5', 'type': 'points', 'outside': 0, 'points': [0.49938878417015076, 0.49907180666923523], 'confidence': '0.0'}, {'label': '6', 'type': 'points', 'outside': 0, 'points': [0.5178049206733704, 0.4843536615371704], 'confidence': '0.0'}, {'label': '7', 'type': 'points', 'outside': 0, 'points': [0.5204618573188782, 0.5074937343597412], 'confidence': '0.0'}, {'label': '8', 'type': 'points', 'outside': 0, 'points': [0.5153696537017822, 0.528052031993866], 'confidence': '0.0'}, {'label': '9', 'type': 'points', 'outside': 0, 'points': [0.4885108172893524, 0.4938916265964508], 'confidence': '0.0'}, {'label': '10', 'type': 'points', 'outside': 0, 'points': [0.5073314309120178, 0.48257797956466675], 'confidence': '0.0'}, {'label': '11', 'type': 'points', 'outside': 0, 'points': [0.5106727480888367, 0.5072055459022522], 'confidence': '0.0'}, {'label': '12', 'type': 'points', 'outside': 0, 'points': [0.5052586197853088, 0.5285157561302185], 'confidence': '0.0'}, {'label': '13', 'type': 'points', 'outside': 0, 'points': [0.4755867123603821, 0.4925291836261749], 'confidence': '0.0'}, {'label': '14', 'type': 'points', 'outside': 0, 'points': [0.49422499537467957, 0.48339420557022095], 'confidence': '0.0'}, {'label': '15', 'type': 'points', 'outside': 0, 'points': [0.49988627433776855, 0.5063366293907166], 'confidence': '0.0'}, {'label': '16', 'type': 'points', 'outside': 0, 'points': [0.4961303174495697, 0.5273551940917969], 'confidence': '0.0'}, {'label': '17', 'type': 'points', 'outside': 0, 'points': [0.4613873362541199, 0.49610546231269836], 'confidence': '0.0'}, {'label': '18', 'type': 'points', 'outside': 0, 'points': [0.48074111342430115, 0.4904457926750183], 'confidence': '0.0'}, {'label': '19', 'type': 'points', 'outside': 0, 'points': [0.4883764982223511, 0.5075832605361938], 'confidence': '0.0'}, {'label': '20', 'type': 'points', 'outside': 0, 'points': [0.4879501163959503, 0.5252431035041809], 'confidence': '0.0'}]}]

But I get this screen exactly when the inference is done

TypeError
Cannot read properties of undefined (reading 'attr')

TypeError: Cannot read properties of undefined (reading 'attr')
    at kF.addSkeleton (https://cvat.retorik.ai/assets/cvat-ui.121b31fdc29d9ddb5d35.min.js:2:9517117)
    at kF.addObjects (https://cvat.retorik.ai/assets/cvat-ui.121b31fdc29d9ddb5d35.min.js:2:9490407)
    at kF.setupObjects (https://cvat.retorik.ai/assets/cvat-ui.121b31fdc29d9ddb5d35.min.js:2:9433518)
    at kF.notify (https://cvat.retorik.ai/assets/cvat-ui.121b31fdc29d9ddb5d35.min.js:2:9469495)
    at NN.notify (https://cvat.retorik.ai/assets/cvat-ui.121b31fdc29d9ddb5d35.min.js:2:8442100)
    at NN.setup (https://cvat.retorik.ai/assets/cvat-ui.121b31fdc29d9ddb5d35.min.js:2:8503675)
    at TF.setup (https://cvat.retorik.ai/assets/cvat-ui.121b31fdc29d9ddb5d35.min.js:2:9540573)
    at WBe.updateCanvas (https://cvat.retorik.ai/assets/cvat-ui.121b31fdc29d9ddb5d35.min.js:2:11799318)
    at WBe.componentDidUpdate (https://cvat.retorik.ai/assets/cvat-ui.121b31fdc29d9ddb5d35.min.js:2:11795844)
    at ui (https://cvat.retorik.ai/assets/cvat-ui.121b31fdc29d9ddb5d35.min.js:2:5429432)
image

The results from the inference/Nuclio function are the same as mmpose in terms of logic and format.

Environment

- Browser is Chrome on Mac OS Ventura 13.6
- CVAT version
  Server: 2.12.0
  Core: 15.0.2
  Canvas: 2.20.0
  UI: 1.63.6
- Nuclio function conf is in function.yaml

I do not think that relative coordinates are correct:
'points': [0.4395136833190918, 0.5782752633094788]

The code on client expects pixel coordinates.

commented

Hello, thank you for the answer just got back working on it

So, I attempted the solution given in #622 (comment) to convert the relative coordinates into pixel coordinates

Here is the new data

[{'confidence': '0.90301526', 'label': 'r_hand', 'type': 'skeleton', 'elements': [{'label': '0', 'type': 'points', 'outside': 0, 'points': [228.1690216064453, 244.72214698791504], 'confidence': '0.8'}, {'label': '1', 'type': 'points', 'outside': 0, 'points': [244.52947616577148, 256.14699840545654], 'confidence': '0.8'}, {'label': '2', 'type': 'points', 'outside': 0, 'points': [255.15756607055664, 270.76623916625977], 'confidence': '0.8'}, {'label': '3', 'type': 'points', 'outside': 0, 'points': [259.13318634033203, 283.39852809906006], 'confidence': '0.8'}, {'label': '4', 'type': 'points', 'outside': 0, 'points': [262.84093856811523, 292.98545837402344], 'confidence': '0.8'}, {'label': '5', 'type': 'points', 'outside': 0, 'points': [236.5384864807129, 296.3512659072876], 'confidence': '0.8'}, {'label': '6', 'type': 'points', 'outside': 0, 'points': [238.6057472229004, 315.60779571533203], 'confidence': '0.8'}, {'label': '7', 'type': 'points', 'outside': 0, 'points': [238.4959602355957, 327.4267387390137], 'confidence': '0.8'}......

Out of curiosity I used matplotlib to plot the extracted coordinate (plot limits are matching image input size)
image
(looks exactly like the source picture)

Still no success whatsoever, the exact same error persists right after inference

Cannot read properties of undefined (reading 'attr')
image

Check the label specification in your function.yml.

On your instance I tried to add a label from a model and annotate it manually. It didn't work.

image

At least you have to remove data-label-id from the svg specification.

commented

Ok made some adjustemenst and removed data-label-id and pretty much got it to work, hand is upside down but nothing too painful to fix thanks for your inputs and help :)