Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bounding boxes are off center on import (custom implementation) + a script and work flow that works for me that I would like to share #5905

Closed
rlewkowicz opened this issue May 21, 2024 · 1 comment

Comments

@rlewkowicz
Copy link

rlewkowicz commented May 21, 2024

I'll describe the bug, and then talk about why I like this method over others.
Describe the bug
I import data through custom curl commands and the annotations are off. I did this through python before and think I had the same issue. I copy your exact logic here, just in bash:

convert_yolo() {
  local yolo_annotation="$1"

  local image_width=1024
  local image_height=1024

  local class=$(echo $yolo_annotation | cut -d' ' -f1)
  local x_center_norm=$(echo $yolo_annotation | cut -d' ' -f2)
  local y_center_norm=$(echo $yolo_annotation | cut -d' ' -f3)
  local width_norm=$(echo $yolo_annotation | cut -d' ' -f4)
  local height_norm=$(echo $yolo_annotation | cut -d' ' -f5)

  local x=$(echo "($x_center_norm - $width_norm / 2) * 100" | tr -d $'\r' | bc)
  local y=$(echo "($y_center_norm - $height_norm / 2) * 100" | tr -d $'\r' | bc)
  local width=$(echo "$width_norm * 100" | tr -d $'\r' | bc)
  local height=$(echo "$height_norm * 100" | tr -d $'\r' | bc)

  echo "$class $x $y $width $height"
}

There's other issues out there like this, but I'm pretty sure this is right. It's the same logic, and I hit this before with python too.

Everything is like off, but not even by the same margins. In this case I'm trying to import yolov9, but it should be the same as the rest.

So that's the bug. I do a lot with this stuff across many languages, and this one is weird. I'll dig more tomorrow, but I feel like yall do something different than everyone else on this. Why not use normalized or not?


Conversation and custom tooling:

You can say use the import tool, but I did not like that. It's clunky. Set these env vars and do all this other wonkey stuff. I know label studio handles a lot so it's more agnostic, but yolo is the most dead simple set up. You have a folder with some txt and image files. It does not get any easier. Then somehow this and cvat is like "what if we made it hard and inconvenient"

With the following script all I do is build a small base model and then save detection as images and txt files. All the yolo tooling across all repos are incestuous rip offs of each other and most have this feature. So crazy easy to make a nice feedback loop. Capture source material, set stride, detect and then clean and adjust here. It works well for me because my models are small number of classes <10. I know what all the numbers are off the top of my head

The idea of plugins and assisted annotations is great but I'm not interested in writing and swapping models and tool chains every time I try a new yolo version. The models do the txt and images and this imports them. I like the separation of responsibility.

This is a hack script and can stand to be cleaned up. No you cannot indent the EOFs. 2k images import in about 3 minutes.

usage: ./whateveryounameit.sh path/to/folder [jpg|png|etc]

search for 1024 and swap out your image size

#!/bin/bash

IFS=$'\n'

declare -a pids=()

spawn_process() {
    eval $1
    local pid=$!
    pids+=($pid)
}

define(){ IFS='\n' read -r -d '' ${1} || true; }

function find_files() {
  folder_path=$1
  file_extension=$2

  find "$folder_path" -type f -name "*.$file_extension" -print | sort | head -10
}

function find_txt_files() {
  folder_path=$1

  find "$folder_path" -type f -name "*.txt" -print | sort | head -10
}

convert_yolo() {
  local yolo_annotation="$1"

  local image_width=1024
  local image_height=1024

  local class=$(echo $yolo_annotation | cut -d' ' -f1)
  local x_center_norm=$(echo $yolo_annotation | cut -d' ' -f2)
  local y_center_norm=$(echo $yolo_annotation | cut -d' ' -f3)
  local width_norm=$(echo $yolo_annotation | cut -d' ' -f4)
  local height_norm=$(echo $yolo_annotation | cut -d' ' -f5)

  # local x=$(echo "($x_center_norm - $width_norm / 2) * 100" | tr -d $'\r' | bc)
  # local y=$(echo "($y_center_norm - $height_norm / 2) * 100" | tr -d $'\r' | bc)

define XMATH <<EOF
echo "($x_center_norm - $width_norm/2) * 100" | tr -d $'\r' | bc -l
EOF
define YMATH <<EOF
echo "($y_center_norm - $height_norm/2) * 100" | tr -d $'\r' | bc -l
EOF
define WMATH <<EOF
echo "$width_norm * 100" | tr -d $'\r' | bc -l
EOF
define HMATH <<EOF
echo "$height_norm * 100" | tr -d $'\r' | bc -l
EOF
  echo "$class $(eval $XMATH) $(eval $YMATH) $(eval $WMATH) $(eval $HMATH)"
}

generate_result() {
    local COUNT=0;
    FINAL=""
    for i in `cat $1`; do
        yolo_annotation=$(convert_yolo "$i")
        local class=$(echo $yolo_annotation | cut -d' ' -f1)
        local x=$(echo $yolo_annotation | cut -d' ' -f2)
        local y=$(echo $yolo_annotation | cut -d' ' -f3)
        local width=$(echo $yolo_annotation | cut -d' ' -f4)
        local height=$(echo $yolo_annotation | cut -d' ' -f5)
    if [ $COUNT = 0 ]; then
define JSON_BLOB_BEGIN <<EOF
{
  "result": [
    {
      "original_width": 1024,
      "original_height": 1024,
      "image_rotation": 0,
      "value": {
        "x": $x,
        "y": $y,
        "width": $width,
        "height": $height,
        "rotation": 0,
        "rectanglelabels": [
          "$class"
        ]
      },
      "from_name": "label",
      "to_name": "image",
      "type": "rectanglelabels",
      "origin": "manual"
    },
EOF
FINAL=$FINAL$JSON_BLOB_BEGIN
    else
define JSON_BLOB_FOLLOW <<EOF
    {
      "original_width": 1024,
      "original_height": 1024,
      "image_rotation": 0,
      "value": {
        "x": $x,
        "y": $y,
        "width": $width,
        "height": $height,
        "rotation": 0,
        "rectanglelabels": [
          "$class"
        ]
      },
      "from_name": "label",
      "to_name": "image",
      "type": "rectanglelabels",
      "origin": "manual"
    },
EOF
FINAL=$FINAL$JSON_BLOB_FOLLOW
fi
((COUNT++))
    done
FINAL=$(echo $FINAL | sed '$ s/,$//')
define JSON_BLOB_END <<EOF
  ],
  "draft_id": 1,
  "parent_prediction": null,
  "parent_annotation": null,
  "project": "1"
}
EOF
FINAL=$FINAL$JSON_BLOB_END
echo "$FINAL" | jq -c .
}

generate_manifest() {
  start=$1
  end=$2

  manifest="<View>\n  <Image name=\\\"image\\\" value=\\\"\$image\\\" zoom=\\\"true\\\" zoomControl=\\\"true\\\" rotateControl=\\\"true\\\"/>\n  <RectangleLabels name=\\\"label\\\" toName=\\\"image\\\">\n"

  for (( i=$start; i<=$end; i++ ))
  do
    color=$(printf "#%06X" $((RANDOM * RANDOM % 0xFFFFFF)))
    manifest+="    <Label value=\\\"$i\\\" background=\\\"$color\\\"/>\n"
  done

  manifest+="  </RectangleLabels>\n</View>"

  echo $manifest
}

docker run -dit -p 8080:8080 heartexlabs/label-studio:latest
until curl -sf http://localhost:8080/; do
echo "waiting for the host to become ready"
sleep 1
done
curl -s -c ./cjar.txt -b ./cjar.txt http://localhost:8080/user/login/ > /dev/null
curl -s -c ./cjar.txt -b ./cjar.txt 'http://localhost:8080/user/signup/?&next=/projects/' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-raw "csrfmiddlewaretoken=$(grep csrftoken cjar.txt | sed 's/^.*csrftoken[[:blank:]]*//')&email=test%40test.com&password=lolthispassword&allow_newsletters=true&allow_newsletters_visual=on" > /dev/null
curl -s -c ./cjar.txt -b ./cjar.txt 'http://localhost:8080/api/projects' \
-X 'POST' \
-H Content-Type:application/json \
--data '{"title":"yolo","description":"","label_config":"<View>\n  <Image name=\"image\" value=\"$image\" zoom=\"true\" zoomControl=\"true\" rotateControl=\"true\"/>\n  <RectangleLabels name=\"label\" toName=\"image\">\n    \n    \n  </RectangleLabels>\n</View>"}'
for i in `find_files $1 $2`; do 

define CURL_COMMAND <<EOF
curl -s -c ./cjar.txt -b ./cjar.txt 'http://localhost:8080/api/projects/1/import' \
-H 'Content-Type: multipart/form-data;' \
-F 'data=@$i'
EOF

if [ ${#pids[@]} -lt 20 ]; then
    spawn_process $CURL_COMMAND
else
    while [ ${#pids[@]} -gt 19 ]; do
            for i in ${!pids[@]}; do
                if ! kill -0 ${pids[$i]} 2> /dev/null; then
                    unset 'pids[$i]'
                fi
            done
            
            pids=("${pids[@]}")
    done
    spawn_process $CURL_COMMAND
fi
done

classes=$(cat $1/*.txt | awk '{print $1}' | sort | uniq | wc -l)

define CURL_COMMAND <<EOF
curl -s -c ./cjar.txt -b ./cjar.txt 'http://localhost:8080/api/projects/1' \
  -X 'PATCH' \
  -H 'content-type: application/json' \
  --data-raw '{"label_config":"$(generate_manifest 0 $(($classes - 1)))"}'
EOF

eval $CURL_COMMAND

declare -a pids=()

C=0
for i in `find_txt_files $1`; do 
((C++))

define CURL_COMMAND <<EOF
curl -s -c ./cjar.txt -b ./cjar.txt "http://localhost:8080/api/tasks/$C/annotations?project=1" \
  -H 'content-type: application/json' \
  --data-raw '$(generate_result $i)' 
EOF

if [ ${#pids[@]} -lt 30 ]; then
    spawn_process $CURL_COMMAND
else
    while [ ${#pids[@]} -gt 29 ]; do
            for i in ${!pids[@]}; do
                if ! kill -0 ${pids[$i]} 2> /dev/null; then
                    unset 'pids[$i]'
                fi
            done
            
            pids=("${pids[@]}")
    done
    spawn_process $CURL_COMMAND
fi
done

rm -f cjar.txt
@rlewkowicz
Copy link
Author

Bash was doing integer math before getting to bc. I fixed the script above. I'll do a write up on it. I really think command, folder, import is very nice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant