Epidemiology & Technology

CVAT Converting Attribute Annotations to CSV – v2

This is an update on my previous post – Version 1

Recently I started working with Glaucoma Images preparation for AI/CV applications. Computer Vision Annotation Tool (CVAT) was deployed locally and fundus images uploaded into an organizational project within CVAT. Each Image is being annotated as follows

  • Grading [Tag]
    • Glaucoma Status [SELECT RADIO BUTTON]
      • —– [DEFAULT]
      • Normal
      • Suspect
      • Glaucoma
      • Non-Gradable
      • Other Retinal
    • Grader Remarks [TEXT]
    • Consultant Remarks [TEXT]
Attributes
  • rading [Tag]
  • Glaucoma Status [SELECT RADIO BUTTON]
    • —– [DEFAULT]
    • Normal
    • Suspect
    • Glaucoma
    • Non-Gradable
    • Other Retinal
  • Grader Remarks [TEXT]
  • Consultant Remarks [TEXT]
Constructor Code
 [
  {
    "name": "Grading",
    "id": 60,
    "color": "#97af27",
    "type": "tag",
    "attributes": [
      {
        "id": 215,
        "name": "Glaucoma Status",
        "input_type": "radio",
        "mutable": false,
        "values": [
          "—-",
          "Normal",
          "Suspect",
          "Glaucoma",
          "Not Gradable",
          "Other Retinal"
        ],
        "default_value": "—-"
      },
      {
        "id": 216,
        "name": "Grader Remarks",
        "input_type": "text",
        "mutable": false,
        "values": [
          ""
        ],
        "default_value": ""
      },
      {
        "id": 217,
        "name": "Consultant Remarks",
        "input_type": "text",
        "mutable": false,
        "values": [
          ""
        ],
        "default_value": ""
      }
    ]
  }
]Code language: JSON / JSON with Comments (json)
XML Annotation Sample
<?xml version="1.0" encoding="utf-8"?>
<annotations>
  <version>1.1</version>
  <meta>
    <task>
      <id>2339</id>
      <name>XXXXXXXX__set_9</name>
      <size>30</size>
      <mode>annotation</mode>
      <overlap>0</overlap>
      <bugtracker></bugtracker>
      <created>2024-09-20 08:27:30.680256+00:00</created>
      <updated>2024-09-30 10:46:50.686355+00:00</updated>
      <subset>default</subset>
      <start_frame>0</start_frame>
      <stop_frame>239</stop_frame>
      <frame_filter></frame_filter>
      <segments>
        <segment>
          <id>203</id>
          <start>0</start>
          <stop>129</stop>
          <url>http://cvat.XXXXXXXX_.edu/XXXXXXXX_/XXXXXXXX_/XXXXXXXX_</url>
        </segment>
      </segments>
      <owner>
        <username>XXXXXXXX_</username>
        <email>XXXXXXXX_@aiims.edu</email>
      </owner>
      <assignee>
        <username>XXXXXXXX_</username>
        <email>XXXXXXXX_@XXXXXXXX_.edu</email>
      </assignee>
      <labels>
        <label>
          <name>Grading</name>
          <color>#97af27</color>
          <type>tag</type>
          <attributes>
            <attribute>
              <name>Glaucoma Status</name>
              <mutable>False</mutable>
              <input_type>radio</input_type>
              <default_value>---</default_value>
              <values>---
Normal
Suspect
Glaucoma
Not Gradable
Other Retinal</values>
            </attribute>
            <attribute>
              <name>Grader Remarks</name>
              <mutable>False</mutable>
              <input_type>text</input_type>
              <default_value></default_value>
              <values></values>
            </attribute>
            <attribute>
              <name>Consultant Remarks</name>
              <mutable>False</mutable>
              <input_type>text</input_type>
              <default_value></default_value>
              <values></values>
            </attribute>
          </attributes>
        </label>
      </labels>
    </task>
    <dumped>2024-09-30 10:47:00.963353+00:00</dumped>
  </meta>
  <image id="0" name="XXXXXXXX_000381.jpg" width="2196" height="1958">
    <tag label="Grading" source="manual">
      <attribute name="Grader Remarks"></attribute>
      <attribute name="Glaucoma Status">Normal</attribute>
      <attribute name="Consultant Remarks"></attribute>
    </tag>
  </image>
  <image id="1" name="XXXXXXXX_000382.jpg" width="2196" height="1958">
    <tag label="Grading" source="manual">
      <attribute name="Grader Remarks">SUPERIOR RNFL DEFECT</attribute>
      <attribute name="Glaucoma Status">Glaucoma</attribute>
      <attribute name="Consultant Remarks">G</attribute>
    </tag>
  </image>
  <image id="2" name="XXXXXXXX_000383.jpg" width="2196" height="1958">
    <tag label="Grading" source="manual">
      <attribute name="Glaucoma Status">Normal</attribute>
      <attribute name="Consultant Remarks"></attribute>
      <attribute name="Grader Remarks"></attribute>
    </tag>
  </image>
</annotations>
Code language: HTML, XML (xml)

Exporting the Attribute annotations

I thought the best format would be the CVAT for Images format as that supports all attributes. Hence that was chosen. Export can be done at project level [for all tasks in that project] as well as at task level [for only that task and its jobs]

Annotations were therefore exported and saved as a zip file. Within the zip file is an ANNOTATIONS.XML that containes the metadata as well as the data.

How to convert the annotations as a simple CSV file that can be easily shared and analysed in Stata etc

Python to the rescue

from zipfile import ZipFile, BadZipFile
from lxml import etree 
import csv
import sys

set_name=""
set_created=""
set_updated=""
set_dumped=""



def zipextractor(inFileName):
  try:
    with  ZipFile(inFileName, 'r') as zip:
      zip.printdir()
      zip.extractall()
      print ( "\n \n Extracted Zip File -----", inFileName) 
    return
  except BadZipFile:
    print ("BAD Input Zip File", inFileName) 

  
def cvatXmlParser():
  try:
    tree = etree.parse('annotations.xml')
    root = tree.getroot() 
    return root
  except:
    print("Cannot Parse XML")


def cvatXmlMetadataExtractor(type):
  print("cvatXmlMetadataExtractor Called")
  if type=="project":
    print("Looking for Project Metadata")

    try:
        print("entered Project metadata finding")
        root = cvatXmlParser()
        print("Root is - ", root.tag, "as per cvatXmlParser called from cvatXmlMetadataExtractor PROJERCT LOOP")
        print("Finding PROJECT", root.find('meta/project/name').text)
        set_name= root.find('meta/project/name').text
        print(set_name)
        for i in range(1):
             if len(set_name) <=2:
                print("PROJECT METADATA NOT FOUND")
                sys.exit("Exiting as PROJECT METADATA NOT FOUND")
        set_created= root.find('meta/project/created').text
        set_updated= root.find('meta/project/updated').text
        set_dumped= root.find('meta/dumped').text
        print("PROJECT MetaData = ", set_name, set_created, set_updated, set_dumped)
        return set_name, set_created, set_updated, set_dumped 
    except:
      print("PROJECT METADATA NOT FOUND")
      sys.exit("Exiting as PROJECT METADATA NOT FOUND")

  elif type=="task":
    print("Looking for Task Metadata")
    try:
        print("entered task metadata finding")
        root = cvatXmlParser()
        print("Root is - ", root.tag, "as per cvatXmlParser called from cvatXmlMetadataExtractor TASK LOOP")
        print("Found TASK", root.find('meta/task/name').text)
        set_name= root.find('meta/task/name').text
        print("set_name ", set_name)
        print("set_name  Length", len(set_name))
        for i in range(1):
            if len(set_name) <=2:
                print("TASK METADATA NOT FOUND")
                sys.exit("Exiting as TASK METADATA NOT FOUND")
        set_created= root.find('meta/task/created').text
        set_updated= root.find('meta/task/updated').text
        set_dumped= root.find('meta/dumped').text
        print("TASK MetaData = ", set_name, set_created, set_updated, set_dumped)
        return set_name, set_created, set_updated, set_dumped
    except:
      print("TASK METADATA NOT FOUND")  
      sys.exit("Exiting as Task METADATA NOT FOUND")

  elif type=="job":
    print("Looking for job Metadata")
    try:
        print("entered job metadata finding")
        root = cvatXmlParser()
        print("Root is - ", root.tag, "as per cvatXmlParser called from cvatXmlMetadataExtractor JOB LOOP")
        print("Found TASK", root.find('meta/job/id').text)
        set_name= root.find('meta/job/id').text
        print("set_name ", set_name)
        print("set_name  Length", len(set_name))
        for i in range(1):
            if len(set_name) <=0:
                print("job METADATA NOT FOUND")
                sys.exit("Exiting as job METADATA NOT FOUND")
        set_created= root.find('meta/job/created').text
        set_updated= root.find('meta/job/updated').text
        set_dumped= root.find('meta/dumped').text
        print("TASK MetaData = ", set_name, set_created, set_updated, set_dumped)
        return set_name, set_created, set_updated, set_dumped
    except:
      print("TASK METADATA NOT FOUND")  
      sys.exit("Exiting as Task METADATA NOT FOUND")

  else:
    print("Specify whether it is a project or task export")


def csvWriter(outfile, annotator, set_name, set_created, set_updated, set_dumped):
  print("csvWriter Called")
  root = cvatXmlParser()
  # opening the csv file in 'w' mode
  csvfile = open(outfile+'.csv', 'w', newline ='', encoding='utf-8')
  with csvfile:
    fieldnames  = ['set_name','image_name', 'Glaucoma_Status', 'Grader_Remarks', 'Consultant_Remarks',  'set_created', 'set_updated', 'set_dumped', 'annotator']
    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
    writer.writeheader()

    # Looping over images and getting relevant tag attributes
    list_images = root.findall(".//image")
    for image in list_images:
      name = image.attrib.get("name")

      Glaucoma_Status_find = image.find('.//tag/attribute[@name="Glaucoma Status"]')
      
      if Glaucoma_Status_find is not None:
        Glaucoma_Status = image.find('.//tag/attribute[@name="Glaucoma Status"]').text
        Grader_Remarks = image.find('.//tag/attribute[@name="Grader Remarks"]').text
        Consultant_Remarks = image.find('.//tag/attribute[@name="Consultant Remarks"]').text

        data = {'image_name': name, 
                'Glaucoma_Status': Glaucoma_Status, 
                'Grader_Remarks': Grader_Remarks, 
                'Consultant_Remarks': Consultant_Remarks, 
                'set_name':set_name, 
                'set_created': set_created, 
                'set_updated':set_updated, 
                'set_dumped': set_dumped,
                'annotator': annotator}
        # print(data['Glaucoma_Status'])
        writer.writerow(data)
        #print("Written results for: ", name)
      else:
         print("\n \n Image not graded",   name, "\n \n")
    return 



def inToOut(inFileName, typeofexport, annotator):
  zipextractor(inFileName)
  set_name, set_created, set_updated, set_dumped = cvatXmlMetadataExtractor(typeofexport)
  print("Extarcted MetaData  = ", set_name, set_created, set_updated, set_dumped)
  csvWriter(inFileName, annotator, set_name, set_created, set_updated, set_dumped)
  return inFileName 


if __name__ == "__main__": 
  inToOut()

Code language: Python (python)

Let us break it down

  1. Open the exported ZIP file and Extract its contents
  2. Parse the XML using LXML ElementTree and save the root element in a variable
  3. Extract meta-data information from the relevant XML nodes <– Change this as you deem fit
  4. Open a new CSV dile and name it the same as start file
  5. Write the CSV header using dictionary writer
  6. Use the findall method on root element to identify all ‘image’ elements
  7. Loop over the list of all image elements and extract the three relevant attributes along with image name
  8. Create a ‘data’ python dictionary mapping the image level fields as well as the meta fields into the dictionary
  9. Write the ‘data’ dictionary to the CSV file

Done !

Append_csv.py

import pandas as pd
import os

def appendCSV(folder_path, out_file):
    all_files = os.listdir(folder_path)
    # Delete the  combined file if it exists
    if os.path.exists(out_file+'.csv'):
        print("Deleting the  combined file ",  out_file+'.csv')
        os.remove(out_file+'.csv')
    else:
        print("The file ", out_file+'.csv', "does not exist")

    # Filter out non-CSV files
    csv_files = [f for f in all_files if f.endswith('.csv')]
    # Create a list to hold the dataframes
    df_list = []
    for csv in csv_files:
        file_path = os.path.join(folder_path, csv)
        print("Appening File - ", file_path)
        try:
            # Try reading the file using default UTF-8 encoding
            df = pd.read_csv(file_path)
            df_list.append(df)
        except UnicodeDecodeError:
            try:
                # If UTF-8 fails, try reading the file using UTF-16 encoding with tab separator
                df = pd.read_csv(file_path, sep='\t', encoding='utf-16')
                df_list.append(df)
            except Exception as e:
                print(f"Could not read file {csv} because of error: {e}")
        except Exception as e:
            print(f"Could not read file {csv} because of error: {e}")

    # Concatenate all data into one DataFrame
    big_df = pd.concat(df_list, ignore_index=True)

    # Save the final result to a new CSV file
    big_df.to_csv(os.path.join(folder_path, out_file+'.csv'), index=False)
    print(out_file+'.csv', " Saved and should have ROWS = ", big_df.shape[0])  

if __name__ == "__main__": 
  appendCSV()Code language: PHP (php)

check_csv.py

import os
import csv

def checkCSV(folder_path, out_file):
    print(folder_path+'\\'+out_file+'.csv')
    if os.path.exists(folder_path+'\\'+out_file+'.csv'):
        # read the csv file 
        
        results = pd.read_csv(folder_path+'\\'+out_file+'.csv') 
  
        # display dataset 
        print("The file ", out_file+'.csv', "does not exist has ROWS = ", results.shape[0]) 
     
    else:
        print("The file ", out_file+'.csv', "does not exist")
 
if __name__ == "__main__": 
  checkCSV()Code language: PHP (php)

Function Call

import  function_generic as fp
import append_csv as ac
import check_csv as cc

fp.inToOut("C:\workspace\cvat_csv\set1.zip", "job", "ANNOTATOR_1")
fp.inToOut("C:\workspace\cvat_csv\set2.zip", "task", "ANNOTATOR_1")
fp.inToOut("C:\workspace\cvat_csv\set1.zip", "job", "ANNOTATOR_2")
fp.inToOut("C:\workspace\cvat_csv\set2.zip", "task", "ANNOTATOR_2")

ac.appendCSV("C:\workspace\cvat_csv", "combined")
cc.checkCSV("C:\workspace\cvat_csv", "combined")Code language: JavaScript (javascript)

Related Posts