As someone who works on computer vision projects, I know how crucial properly annotated images are for training machine learning models. Accurate annotations are the backbone of object detection and classification tasks, especially when you’re working with models like YOLOv8. That’s why I decided to build the YOLOv8 Image Annotation Tool using AutoIt—a powerful scripting language that allows me to efficiently annotate images with both bounding boxes and polygons.

My goal was to make image annotation as simple, accurate, and user-friendly as possible. Whether you’re working with basic bounding boxes or more complex polygons, this tool is designed to streamline the process.


Why I Chose YOLOv8 and AutoIt

For object detection, I prefer using the YOLO (You Only Look Once) algorithm because of its balance between speed and accuracy. YOLOv8, in particular, brings enhanced performance when it comes to detecting small objects and working across multiple scales. But while YOLO excels at detection, the task of annotating images for training the model can be tedious.

That’s where my AutoIt-based YOLOv8 Image Annotation Tool comes in. I chose AutoIt for this project because it’s lightweight, easy to use, and offers great flexibility in creating GUI-based applications. AutoIt also allows me to leverage GDI+ for drawing annotations on the image, which makes for a seamless experience when labeling.


Key Features of My YOLOv8 Image Annotation Tool

Here’s what I’ve built into the tool to make it as functional and efficient as possible:

  1. Supports Both Bounding Boxes and Polygons:
  • Whether you’re working with standard bounding boxes (like those used in YOLO) or more intricate polygon-based annotations, this tool can handle both. Bounding boxes are automatically scaled and positioned, while polygons allow for more precise object outlining.
  1. Automatic Image Scaling and Centering:
  • One challenge I faced was maintaining image proportions during annotation, especially with high-resolution images. The tool automatically scales the image while preserving its aspect ratio, ensuring that your annotations are accurate regardless of screen resolution.
  1. Class Labels with Custom Colors:
  • The tool automatically loads class names from a classes.txt file and assigns distinct colors to each class. This makes it easy to differentiate between the objects you’re annotating. The labels are displayed at the top center of the polygon, so they’re easy to see without cluttering the image.
  1. Annotation Redrawing on Window Resize:
  • If the window is resized, the tool automatically recalculates and redraws the annotations to fit the new window size. This keeps everything in sync without having to manually readjust the annotations.
  1. Easy Image Navigation:
  • I’ve made it simple to move through your dataset with ‘Previous’ and ‘Next’ buttons. You can quickly switch between images, allowing you to focus on the task at hand rather than fiddling with the interface.
  1. Customizable and Flexible:
  • Since the tool is built in AutoIt, it’s easy to customize. Whether you want to adjust how the annotations are drawn or modify the GUI to suit your preferences, it’s all doable within the script.

How It Works

  1. Loading Images and Labels:
  • You can select the image directory and the corresponding label directory (where the YOLOv8-format .txt files are stored). The tool reads the labels and automatically draws the bounding boxes or polygons based on the coordinates.
  1. Displaying and Annotating:
  • Once the image is displayed, the tool dynamically resizes it based on the available window size. Annotations (bounding boxes or polygons) are drawn on the image, and class labels are displayed at the top center of the polygon.
  1. Class Label Colors:
  • The class labels are loaded from a classes.txt file and are automatically assigned a unique color. This helps keep your annotations visually distinct and organized, especially when working with multiple classes.
  1. Resizing and Redrawing:
  • If you resize the window, the tool recalculates the scaling factors and redraws both the image and the annotations. You won’t have to worry about things getting misaligned when working in different window sizes.

Introduction to the AutoIt Code

The backbone of the YOLOv8 Image Annotation Tool is written in AutoIt, a scripting language known for its simplicity and flexibility in building GUIs. The code leverages AutoIt’s GDI+ library for drawing and managing annotations, and the GUI framework to handle image navigation, scaling, and interaction. Below, you’ll find the latest version of the script, which you can customize to fit your own needs.

This script handles tasks like:

  • Loading images and corresponding annotations in the YOLO format.
  • Drawing bounding boxes and polygons on images.
  • Managing resizing and redrawing to ensure everything stays aligned.
  • Displaying class labels for each annotation with unique colors.

If you’re familiar with AutoIt, you’ll find the code easy to follow and modify. If you’re new to it, the structure is intuitive enough to quickly get up to speed.

Here’s the latest version of the script:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

Global $imageDir, $labelDir, $annotations, $classes, $classes_colors = []
Global $imageFiles, $currentIndex = 1
Global $originalImageWidth, $originalImageHeight, $scaleX, $scaleY

; Create the GUI (with resizing and maximizing options)
$mainGUI = GUICreate("YOLOv8 Annotation Tool", 1024, 768, -1, -1, BitOR($WS_SIZEBOX, $WS_MAXIMIZEBOX, $WS_SYSMENU, $WS_MINIMIZEBOX, $WS_CAPTION))

; Input fields
GUICtrlCreateLabel("Image Directory:", 10, 10)
GUICtrlSetResizing(-1, $GUI_DOCKAUTO)
$imageDirInput = GUICtrlCreateInput("C:\Users\Joe\Downloads\YOLO_Mask_Model\images\test", 120, 10, 700, 20)
GUICtrlSetResizing(-1, $GUI_DOCKAUTO)

GUICtrlCreateLabel("Label Directory:", 10, 40)
GUICtrlSetResizing(-1, $GUI_DOCKAUTO)
$labelDirInput = GUICtrlCreateInput("C:\Users\Joe\Downloads\YOLO_Mask_Model\labels\test", 120, 40, 700, 20)
GUICtrlSetResizing(-1, $GUI_DOCKAUTO)

; Browse buttons
$imageDirButton = GUICtrlCreateButton("Browse", 830, 10, 75, 20)
GUICtrlSetResizing(-1, $GUI_DOCKAUTO)
$labelDirButton = GUICtrlCreateButton("Browse", 830, 40, 75, 20)
GUICtrlSetResizing(-1, $GUI_DOCKAUTO)

; Load button
$loadButton = GUICtrlCreateButton("Load Annotations", 10, 70, 200, 30)
GUICtrlSetResizing(-1, $GUI_DOCKAUTO)

; Navigation buttons
$prevButton = GUICtrlCreateButton("Previous", 10, 710, 100, 30)
GUICtrlSetResizing(-1, $GUI_DOCKAUTO)
$nextButton = GUICtrlCreateButton("Next", 920, 710, 100, 30)
GUICtrlSetResizing(-1, $GUI_DOCKAUTO)

; Image display area, scalable (with the anchor set)
$picDisplay = GUICtrlCreatePic("", 10, 110, 1000, 600)
GUICtrlSetResizing($picDisplay, $GUI_DOCKHCENTER)

; Show the GUI
GUISetState(@SW_SHOW)


; Event Loop
While 1
    $msg = GUIGetMsg()
    Switch $msg
        Case $GUI_EVENT_CLOSE
            Exit

        Case $GUI_EVENT_RESTORE
			ConsoleWrite("RESTORE")
			Sleep(10)
			DisplayCurrentImage(0)
			
        Case $GUI_EVENT_MAXIMIZE
			ConsoleWrite("MAXIMIZE")
			Sleep(10)
			DisplayCurrentImage(0)
			
        Case $GUI_EVENT_RESIZED
			ConsoleWrite("RESIZED")
			Sleep(10)
			DisplayCurrentImage(0)
			
        Case $imageDirButton
            $imageDir = FileSelectFolder("Select Image Directory", "C:\Users\Joe\Downloads\YOLO_Mask_Model\images\test")
            If Not @error Then GUICtrlSetData($imageDirInput, $imageDir)

        Case $labelDirButton
            $labelDir = FileSelectFolder("Select Label Directory", "C:\Users\Joe\Downloads\YOLO_Mask_Model\labels\test")
            If Not @error Then GUICtrlSetData($labelDirInput, $labelDir)

        Case $loadButton
            $imageDir = GUICtrlRead($imageDirInput)
            $labelDir = GUICtrlRead($labelDirInput)
            If $imageDir = "" Or $labelDir = "" Then
                MsgBox($MB_ICONERROR, "Error", "Please select both image and label directories.")
            Else
                LoadImages($imageDir, $labelDir)
            EndIf

        Case $prevButton
            If $currentIndex > 1 Then
                DisplayCurrentImage(-1)
            EndIf

        Case $nextButton
            If $currentIndex < $imageFiles[0] Then
                DisplayCurrentImage(1)
            EndIf
			
    EndSwitch
WEnd

Func AdjustImageDisplay($imageWidth, $imageHeight)
    ; Get the available space in the GUI for the image display
    Local $winSize = WinGetClientSize($mainGUI)
    Local $availableWidth = $winSize[0]
    Local $availableHeight = $winSize[1] - 220 ; Adjust for top GUI elements (like buttons)

    ; Calculate the aspect ratio of the image
    Local $imageAspectRatio = $imageWidth / $imageHeight

    ; Initialize new dimensions while maintaining the aspect ratio
    Local $newWidth, $newHeight

    ; Adjust the dimensions to fit within the available space, maintaining aspect ratio
    If ($availableWidth / $availableHeight) > $imageAspectRatio Then
        ; Available space is wider than the image's aspect ratio, so limit by height
        $newHeight = $availableHeight
        $newWidth = $newHeight * $imageAspectRatio
    Else
        ; Available space is taller than the image's aspect ratio, so limit by width
        $newWidth = $availableWidth
        $newHeight = $newWidth / $imageAspectRatio
    EndIf

    ; Calculate new position to center the image control horizontally and vertically
    Local $xPos = ($availableWidth - $newWidth) / 2
    Local $yPos = ($availableHeight - $newHeight) / 2 + 110

    ; Resize and reposition the image display control
    GUICtrlSetPos($picDisplay, $xPos, $yPos, $newWidth, $newHeight)
EndFunc


Func LoadClasses($labelDir)
    Local $classFile = $labelDir & "\classes.txt"
    
    ; Check if the classes.txt file exists in the label directory
    If FileExists($classFile) Then
        ; Read the file into an array
        Local $lines = []
        _FileReadToArray($classFile, $lines)
        
        ; Check if the file was read successfully
        If IsArray($lines) Then
			;_ArrayDisplay($lines)
            ; Copy the lines to the global classes array
            Global $classes = $lines
			AssignClassColors()
            ConsoleWrite("Classes loaded successfully: " & _ArrayToString($classes, ", ") & @CRLF)
        Else
            MsgBox($MB_ICONERROR, "Error", "Failed to read the classes.txt file.")
        EndIf
    Else
        MsgBox($MB_ICONERROR, "Error", "classes.txt file not found in: " & $labelDir)
    EndIf
EndFunc

Func LoadImages($imageDir, $labelDir)
    ; Load images
	$labelDir = GUICtrlRead($labelDirInput)
    $imageFiles = _FileListToArrayRec($imageDir, "*.jpg;*.png", 1)
	_ArrayDelete($classes, 0)
	LoadClasses($labelDir)
    If @error Then
        MsgBox($MB_ICONERROR, "Error", "No images found in directory: " & $imageDir)
        Return
    EndIf

    ; Set the current index to the first image
    $currentIndex = 1

    ; Display the first image
    DisplayCurrentImage(0)
EndFunc

Func DisplayCurrentImage($nextIndex)
	If $imageFiles = "" Then Return
	$currentIndex += $nextIndex
    Local $filename = StringTrimLeft($imageFiles[$currentIndex], StringInStr($imageFiles[$currentIndex], "\", 0, -1))

    ; Replace the extension to match the label file
    Local $labelFilename = StringReplace($filename, ".jpg", ".txt")
    $labelFilename = StringReplace($labelFilename, ".png", ".txt")

    Local $imagePath = $imageDir & "\" & $filename
    Local $labelPath = $labelDir & "\" & $labelFilename

    If FileExists($labelPath) Then
		ConsoleWrite("EXISTS "&$labelPath&@crlf)
		; Get the original image dimensions
		Local $imageWidth = _GDIPlus_ImageGetWidth($imagePath)
		Local $imageHeight = _GDIPlus_ImageGetHeight($imagePath)
		AdjustImageDisplay($imageWidth, $imageHeight)
		; Load and display the image
		GUICtrlSetImage($picDisplay, $imagePath)
        DisplayImageWithAnnotations($imagePath, $labelPath)
    Else
        MsgBox($MB_ICONERROR, "Error", "Label file not found: " & $labelFilename)
    EndIf
EndFunc

Func DisplayImageWithAnnotations($imagePath, $labelPath)
    ; Clear previous annotations by deleting all elements
    While UBound($annotations) > 0
        _ArrayDelete($annotations, 0)
    WEnd

    _GDIPlus_Startup()
    Local $hGraphic = _GDIPlus_GraphicsCreateFromHWND(GUICtrlGetHandle($picDisplay))
    Local $hBitmap = _GDIPlus_BitmapCreateFromFile($imagePath)

    ; Get the image dimensions
    $originalImageWidth = _GDIPlus_ImageGetWidth($hBitmap)
    $originalImageHeight = _GDIPlus_ImageGetHeight($hBitmap)

    ; Get the dimensions of the displayed image area (accounting for GUI scaling)
    Local $displayWidth = ControlGetPos($mainGUI, "", $picDisplay)[2]
    Local $displayHeight = ControlGetPos($mainGUI, "", $picDisplay)[3]

    ; Calculate scaling factors for X and Y
    $scaleX = $displayWidth / $originalImageWidth
    $scaleY = $displayHeight / $originalImageHeight

    ; Draw the image
    _GDIPlus_GraphicsDrawImage($hGraphic, $hBitmap, 0, 0)
    sleep(50)
    ; Load and parse the annotation file
    Local $lines = []
    _FileReadToArray($labelPath, $lines)
    
    If IsArray($lines) Then
        For $i = 1 To $lines[0]
            ConsoleWrite($lines[$i] & @CRLF)
            ; Ensure the line is not empty and $lines[$i] is within bounds
            If IsString($lines[$i]) And StringStripWS($lines[$i], 3) <> "" Then
                Local $elements = StringSplit($lines[$i], " ")
                
                ; Ensure $elements is an array and has enough elements
                If IsArray($elements) And UBound($elements) >= 5 Then
                    ; Handle bounding box (4 points) or polygon (more than 4 points)
                    If UBound($elements) > 6 Then
                        ; Draw the polygon and label it
                        DrawPolygon($hGraphic, $elements)
                        LabelPolygon($hGraphic, $elements)
                    Else
                        ; Draw the bounding box
                        DrawBoundingBox($hGraphic, $elements)
                        LabelPolygon($hGraphic, $elements)
                    EndIf
                    
                    ; Store annotation data
                    _ArrayAdd($annotations, $lines[$i])
                EndIf
            EndIf
        Next
    Else
        ;MsgBox($MB_ICONERROR, "Error", "Failed to read label file: " & $labelPath)
    EndIf
    
    ; Cleanup
    _GDIPlus_GraphicsDispose($hGraphic)
    _GDIPlus_BitmapDispose($hBitmap)
    _GDIPlus_Shutdown()
EndFunc

Func DrawPolygon($hGraphic, $elements)
    Local $classIndex = $elements[1] ; Class index is the first element of the annotation
    ; Calculate the number of points in the polygon
    Local $numPoints = (UBound($elements) - 2) / 2

    ; Declare the array to hold the polygon points as a 2D array
    Local $points[$numPoints + 1][2] ; +1 to hold the number of vertices at index [0][0]

    ; The first element contains the number of vertices
    $points[0][0] = $numPoints

    ; Convert normalized coordinates to scaled pixel coordinates
    For $i = 0 To $numPoints - 1
        $points[$i + 1][0] = Round($elements[2 + $i * 2] * $originalImageWidth * $scaleX)   ; X coordinate
        $points[$i + 1][1] = Round($elements[3 + $i * 2] * $originalImageHeight * $scaleY)  ; Y coordinate
    Next

    ; Create a pen for drawing
    Local $hPen = _GDIPlus_PenCreate($classes_colors[$classIndex+1], 2)

    ; Draw the polygon using the 2D points array
    _GDIPlus_GraphicsDrawPolygon($hGraphic, $points, $hPen)

    ; Clean up
    _GDIPlus_PenDispose($hPen)
EndFunc


Func DrawBoundingBox($hGraphic, $elements)
    ; Scale the bounding box coordinates according to the display scaling
    Local $classIndex = $elements[1] ; Class index is the first element of the annotation

    ; Convert normalized coordinates to pixel coordinates
    Local $xCenter = $elements[2] * $originalImageWidth * $scaleX
    Local $yCenter = $elements[3] * $originalImageHeight * $scaleY
    Local $width = $elements[4] * $originalImageWidth * $scaleX
    Local $height = $elements[5] * $originalImageHeight * $scaleY

    ; Calculate the top-left corner from center coordinates and width/height
    Local $x = $xCenter - ($width / 2)
    Local $y = $yCenter - ($height / 2)

    ; Create a pen for drawing, using the class color
    Local $hPen = _GDIPlus_PenCreate($classes_colors[$classIndex + 1], 2)

    ; Draw the rectangle (bounding box)
    _GDIPlus_GraphicsDrawRect($hGraphic, $x, $y, $width, $height, $hPen)

    ; Clean up
    _GDIPlus_PenDispose($hPen)
EndFunc


Func LabelPolygon($hGraphic, $elements)
    ; Get the class index (first element) and calculate the top-center for the label
    Local $classIndex = $elements[1] ; Class index is the first element of the annotation

    Local $minX = $originalImageWidth * $scaleX
    Local $minY = $originalImageHeight * $scaleY
    Local $maxX = 0
    Local $maxY = 0

    Local $numPoints = (UBound($elements) - 2) / 2

    ; Find the min and max X coordinates and the minimum Y (top of the polygon)
    For $i = 0 To $numPoints - 1
        Local $x = $elements[2 + $i * 2] * $originalImageWidth * $scaleX
        Local $y = $elements[3 + $i * 2] * $originalImageHeight * $scaleY
        
        ; Get the smallest Y (topmost point) and calculate the min/max X values
        If $x < $minX Then $minX = $x
        If $x > $maxX Then $maxX = $x
        If $y < $minY Then $minY = $y
        If $y > $maxY Then $maxY = $y
    Next

    ; Calculate the horizontal center of the polygon
    Local $centerX = ($minX + $maxX) / 2

    ; Draw the label (class index) at the top-center of the polygon
    Local $hBrush = _GDIPlus_BrushCreateSolid($classes_colors[$classIndex + 1]) ; White color for text
    Local $hFormat = _GDIPlus_StringFormatCreate()
    Local $hFamily = _GDIPlus_FontFamilyCreate("Arial")
    Local $hFont = _GDIPlus_FontCreate($hFamily, 16, 4) ; Font size 12, bold
    Local $textRect = _GDIPlus_RectFCreate($minX - 35, $maxY - $minY + $minY, 100, 20) ; Draw the label centered horizontally

    ; Draw the text label at the top-center of the polygon
    _GDIPlus_GraphicsDrawStringEx($hGraphic, $classes[$classIndex + 1], $hFont, $textRect, $hFormat, $hBrush)

    ; Clean up
    _GDIPlus_BrushDispose($hBrush)
    _GDIPlus_FontDispose($hFont)
    _GDIPlus_StringFormatDispose($hFormat)
    _GDIPlus_FontFamilyDispose($hFamily)
EndFunc


Func AssignClassColors()
    Local $predefinedColors = [0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFFFFFF00, 0xFFFF00FF, 0xFF00FFFF, 0xFFFFA500, 0xFF800080, 0xFF808080, 0xFF008080]
    Local $numPredefined = UBound($predefinedColors)

    ; Loop through the classes array
    For $i = 0 To UBound($classes) - 1
        ; Assign a predefined color if available, otherwise generate a random color
        If $i < $numPredefined Then
			_ArrayAdd($classes_colors, $predefinedColors[$i])
        Else
            ; Generate a random color if predefined colors are exhausted
			_ArrayAdd($classes_colors, RandomColor())
        EndIf
    Next
    
    ConsoleWrite("Class colors assigned successfully: " & _ArrayToString($classes_colors, ", ") & @CRLF)
EndFunc

; Function to generate a random color in RGB format
Func RandomColor()
    Local $r = Random(0, 255, 1)
    Local $g = Random(0, 255, 1)
    Local $b = Random(0, 255, 1)
    Return ($r * 0x10000 + $g * 0x100 + $b)
EndFunc

Join the Conversation

If you're interested in trying out the tool or have feedback to share, I'd love to hear from you! I’ve posted the project on the AutoIt Script Forum, where I’m also open to suggestions and feature requests.

You can check out the forum post and get involved in the discussion here:
YOLOv8 Image Annotation Tool - AutoIt Script Forum

Feel free to ask questions, report bugs, or suggest features you'd like to see added. I’m always looking for ways to improve the tool, and the feedback from the community is invaluable.


Conclusion

I created this YOLOv8 Image Annotation Tool to simplify the often tedious process of image labeling for object detection projects. Whether you're dealing with simple bounding boxes or more detailed polygons, the tool is built to help you get the job done faster and more accurately. Plus, the customization options in AutoIt mean that you can tweak it to fit your workflow perfectly.

Give it a try and let me know what you think. And don’t forget to join the conversation on the AutoIt forum—together, we can make this tool even better!

Happy annotating!

By Joe