5

I used labelImg to create some bounding boxes around items of interest on a TIF image. I am wondering if I can convert those XML bounding boxes into shapefiles? I've looked at a few other posts for converting xml to shp or even csv to shp, but those posts are all dealing with lat/lon coordinates. Unfortunately, I have xmin, ymin, xmax, ymax values that seem to be specifically attached to the image/tif that I drew them on.

So I am confused on how I could convert to a shapefile when the xml does not have any georeference information. I could add the georeference information because my image/tif contains it, but I am still not working with lat/lon coordinates. Perhaps I could convert the mins/maxs into lat/lon?

Any suggestions on how I might approach this problem?

enter image description here

Edits:

I found this solution that someone has posted. I however am using a .tif for an image and they are using a .jpg. I am unaware if that will screw up the encoded parameter? The code is running, but I do get the printed error: error in labelme conversion: 'NoneType' object has no attribute 'shape'. The "data_test.json" is saved, but only contains {}`.

Edits 2:

enter image description here

2
  • what is the unit in bnbbox?
    – Mazu_R
    Jan 21, 2021 at 19:58
  • Are xmin, ymin, xmax, ymax pixel coordinates? Jan 21, 2021 at 20:21

2 Answers 2

7

You can use rasterio for getting geo-coordinates from pixel coordinates, and pyshp (shapefile) for creating a shapefile.

import rasterio
import shapefile # -> pyshp module
import xml.etree.ElementTree as ET

# get pixel coordinates from XML
root = ET.parse('/path/to/file.xml').getroot()
xmin = float(root.findall('object/bndbox/xmin')[0].text)
ymin = float(root.findall('object/bndbox/ymin')[0].text)
xmax = float(root.findall('object/bndbox/xmax')[0].text)
ymax = float(root.findall('object/bndbox/ymax')[0].text)

tif_file = '/path/to/tif_file.tif' # georeferenced TIFF
shp_path = '/path/to/shapefile' # don't add extension

# open georeferenced tif file
with rasterio.open(tif_file) as image:
    ## get vertices of the bounding box
    # geocoordinates from pixel coordinates
    p1 = image.xy(xmin, ymin)
    p2 = image.xy(xmax, ymin) 
    p3 = image.xy(xmax, ymax) 
    p4 = image.xy(xmin, ymax) 

    # save shapefile containing one bounding box shape
    w = shapefile.Writer(shp_path + '.shp')
    w.field("name", "C") # pyshp needs at least one field
    w.poly([[p1, p2, p3, p4]]) # generate bbox polygon
    w.record('bbox')
    w.close()
    
    # generate .PRJ file
    crs_wkt = image.crs.to_wkt()
    prj = open(shp_path + '.prj', "w")
    prj.write(crs_wkt)
    prj.close()

enter image description here

6
  • I am continuing to run into an error: NoneType object has no attribute 'to_wkt'. Also, does this code work for multiple bounding boxes within an xml file?
    – Binx
    Jan 22, 2021 at 17:14
  • Are you sure the tif file is georeferenced? If image.crs returns nothing or None, the tif file is not georeferenced. Jan 22, 2021 at 17:16
  • image.crs returns nothing. I apologize, I thought it was georeferenced.
    – Binx
    Jan 22, 2021 at 17:27
  • I assumed the tif was georeferenced because when I opened it up in Label Img with the corresponding xml, the bounding box would show. So somehow they are tied together.
    – Binx
    Jan 22, 2021 at 17:40
  • I've attached a picture of the information I have in my tif
    – Binx
    Jan 22, 2021 at 17:58
4

To achieve this, use the code below, install rasterio, geopandas and shapely modules before running this code.

Code to do the task.

import os
import shapely
import rasterio
import pandas as pd
import geopandas as gpd
import xml.etree.ElementTree as ET
from shapely.geometry import Polygon


#Once the environment and libraries are installed, 
#ensure all the XML files are in the directory.
#In this cell, we use the listdir to get all files in the directory, 
#and using the if condition, select only the .xml files
all_input_files = [file for file in os.listdir() if file.endswith(".xml")]


#An empty list is created to append all dataframes (each XML file will create one dataframe). This list is used to concatenate all the dataframes into a single dataframe.
conc = []

'''
Step1: In the first for loop we are iterating over all .xml file in the directory .getroot() will return the root element for a particular tree (xml file).
Step2: Access the XML tree (ET=Element Tree), get the root of the tree structure, then from the root get the particular filename of XML.
Step3: Now replace the .png with .tif extension tag, so we can read the same GeoTIFF file and get the spatial data.
Step4: Let us find all bounding boxes and iterate over them to get the CRS for each box. This is a loop inside another loop. Inside the loop, we get the name for each bounding box and bounding box extent.
Step5: Extracting the max and min values of the X and Y-axis. X and Y are different in image space and coordinate space. xmin in the image is Ymin in the coordinate space.
Step6: Getting the point data from X and Y coordinates.
Step7: Create a polygon from the point data generated.
Step8: Creating metadata from shapefiles, after this, the inner loop end, Step 4 to 8 are iterated, all bounding box extents are calculated, and polygon geometry is formed.
Step9: Now, a pandas DataFrame is created, and the data frame is added to the conc list.
'''

# Step 1 # Loop Start
for input_file in all_input_files:
    
    #Step 2
    tree = ET.parse(input_file)
    root = tree.getroot()
    file_name = root.find('filename').text
    
    #Step 3
    geotiff_name = file_name.replace('png', 'tif')
    data = []
    
    #Step4 Loop start
    for el in root.findall('object'):
        name = el.find('name').text
        bndbox = el.find('bndbox')
        
        #Step5
        Ymin = int(bndbox.find('xmin').text)
        Ymax = int(bndbox.find('xmax').text)
        Xmin = int(bndbox.find('ymin').text)
        Xmax = int(bndbox.find('ymax').text)
        
        #Step6
        with rasterio.open(geotiff_name) as image:
            crs = image.crs
            p1 = image.xy(Xmin, Ymin)
            p2 = image.xy(Xmax, Ymin) 
            p3 = image.xy(Xmax, Ymax) 
            p4 = image.xy(Xmin, Ymax)
          
        #Step 7 
        poly = shapely.geometry.Polygon([[p[0], p[1]] for p in [p1, p2, p3, p4, p1]])
        
        #Step 8
        datadict = {}
        datadict['name'] = name
        datadict['geometry'] = poly
        datadict['image_name'] = file_name
        data.append(datadict)
        # Inner loop End
    
    #Step 9
    df = pd.DataFrame(data)
    conc.append(df)
# Loop End


#By using the pandas concatenate, we are merging all the dataframes into a single dataframe.
df = pd.concat(conc)

#Let's use the pandas dataframe, and add the geometry and the CRS to make it a spatial layer.
gdf = gpd.GeoDataFrame(df, geometry='geometry', crs=crs)

#Now the GeoDataFrame is exported as Shapefile.
output_file = 'output.shp'
gdf.to_file(output_file)
print("ShapeFile is successfully created.")

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.