Image Classification - annotating labels

Hi Prodigy team,

I was wondering if Prodigy could be used for annotating labels to images? (simply choose 1 label per image from a list of possible labels, no need to select anything within the image)

I tried looking into the documentation but it redirects to https://prodi.gy/docs/workflow-computer-vision which is a broken link.

Thanks!

Hi! The page you're looking for is this one (where did you find the broken link? I checked on the site and couldn't find it):

If you just want to select from a list of labels, you probably want to use a multiple choice interface, but with an "image" in the main task instead of a "text". For example, something like this:

import prodigy
from prodigy.components.loaders import Images

def add_options(stream):
    options = [{"id": label, "text": label} for label in ("A", "B", "C")]
    for eg in stream:
        eg["options"] = options
        yield eg

@prodigy.recipe('image-choice')
def image_choice(dataset, source):
    stream = Images(source)
    stream = add_options(stream)
    return {
        "dataset": dataset,
        "stream": stream,
        "view_id": "choice"
    }

Thanks Ines. The broken link was on the README file.

The interface is running and it is exactly what I was looking for :slight_smile:

Error when saving annotations:

However, when I try saving the annotations I get “Error: couldn’t save annotations. Make sure the server is running correctly.” This happens after I annotate multiple images and then hit Save. When I try saving only the 1st annotation, it works and the annotation appears in the database. But any subsequent tries to save cause the error. Any idea what might be causing this?


I added 'config': {'choice_style': 'multiple', 'show_stats': True} because I wanted to be able to select multiple labels and see the stats on accepted annotations.

PS: For anyone trying to implement this recipe, you have to import Images (from prodigy.components.loaders import Images)

1 Like

Yay, glad it's working! Will edit my code and add the imports so it can be copy-pasted easier.

That's strange – was there any error in the terminal? Usually, this error happens when the server dies during annotation (for whatever reason) and doesn't respond when the app is trying to send back the answers.

Thanks for the quick comeback!

No error in the terminal. I can even close the session with Ctrl+C and get the message “Saved 1 annotations to database SQLite”, dataset name and session id.

Okay, that’s definitely strange. Can you check your browser’s developer tools and the console and see if there’s an error there?

Got this two errors in the console of the browser, does it help?

/give_answers:1 Failed to load resource: the server responded with a status of 413 (Request Entity Too Large)
bundle.js:1 Uncaught (in promise) Error: SyntaxError: Unexpected token < in JSON at position 0
at bundle.js:1
at bundle.js:1
at dispatch (bundle.js:1)
at bundle.js:1

Thanks! And the above message is interesting, I hadn't seen that one before. Maybe your images are too large?

The thing is, by default, Prodigy will encode the images to base64 data URIs (i.e. to strings), so they can be stored with the example. This way, you'll never lose the reference to the original image. But if the images are very large, this also produces very large blobs of data that are sent back and forth, and also potentially makes your database really big.

There are mostly two options:

  1. Resize your images if they're large. Chances are you don't actually need to load the full image at its full resolution just to annotate it. Resizing should be pretty easy to automate using something like ImageMagick – if you run it from Python, you could even do it right in your recipe.

  2. Serve the image from somewhere else. The "image" value in the task doesn't have to be a base64 string – it can also be a URL or a path. Local paths are problematic, though, because most modern browsers will block them as insecure – so the easiest way is to run a simple web server and serve the directory of images, so you can access them via something like http://localhost:1234/image.jpg. You could also upload them somewhere and use the URLs – e.g. to an S3 bucket.

Hi Ines,

I resized all images in the directory and ran Prodigy again: it works! :tada: :pray:

This will get me going for now. Out of curiosity/maybe for later, could you also give more details about the 2nd option? I understood what you mean but I don’t know how this could be implemented. And is it possible to alter the recipe so that the image itself is not saved (only the name is saved)?

Another question: is there a method by default to prevent duplicate images from showing up in the same way Prodigy prevents duplicate texts?

Thanks!

Sure! So at the moment, the Image loader takes the directory of images, loads and converts every image to base64 and then creates a stream that looks something like this:

{"image": "data:image/jpeg;base64,dfkdofdokfdf......"}
{"image": "data:image/jpeg;base64,dfkdofdokfdf......"}

But this produces super long strings and large files, because the images are included in the data. Alternatively, you could also upload your images somewhere and make the data look like this:

{"image": "https://example.com/image1.jpg"}
{"image": "https://example.com/image2.jpg"}

Instead of a public URL, this could also be on localhost. To implement this in your recipe, one idea could be to serve up the directory of images via a simple HTTP server (or something more sophisticated, if you prefer) and then make your recipe load the same directory and create tasks with the respective file names on your local server. If a file image.jpg exists in your directory, you know it'll also exist at localhost:1234/image.jpg or whatever. Here's an example:

from pathlib import Path

local_server = "http://localhost:1234/"

def get_stream():
    source_path = Path(source)  # The directory with your images
    for image_path in source_path.iterdir():  # Iterate over the files in the directory
        # Make sure we're only using actual images from the directory
        if image_path.suffix in ('.jpg', '.jpeg', '.png'):
            # Create the local URL, e.g. http://localhost:1234/image.png
            image_url = local_server + image_path.name
            yield {"image": image_url}

stream = get_stream()

If you mean excluding already annotated examples when you restart the server: This should be handled automatically, because the incoming tasks still receive hashes, and the same image should receive the same hash.

If you mean excluding duplicate incoming images (e.g. the same image with different file names), that's a little trickier. If you use the base64-encoded images, it should work in theory, because the image is converted to a string and if it's the same image, that string should be identical. But otherwise, you might have to actually load and diff the image – I'm sure this can be done in Python, but it might not be worth the hassle.

3 Likes

Thanks Ines! Didn’t try this yet because the previous solution is working for me, but good to keep it in mind to try out when I have some free time.