Image classification with Cloudflare Worker

Today we are experimenting with a useful AI feature that concerns image classification, that is, an AI model created specifically to return a written explanation of an image via the image’s URL. What can it be useful for? As a first application, to know what’s inside an image without even opening it; this opens the doors to a series of other applications such as recognizing abuse or inappropriate behavior by scanning a list of text transcriptions from images. It is also useful for automatically having a description of the image and saving it in some database. What do we need?
1) An account- [Account: an Account contains the personal information that is assigned to those who register and access with email and password] - in Cloudflare (even free)
2) A worker within your Cloudflare account (even free)
In Cloudflare workers there is already a pre-set script for generating text transcriptions from images, but the pre-set scripts are not at all safe to use in a production environment; in this post we will see how we can customize the script to make it safer and use it in a PHP environment.

What do we want to do? The main goal is to create a script that, starting from an image URL sent with the POST method, returns the description of the image through the artificial intelligence model called “resnet-50”. ResNet is a very deep convolutional neural network- [Network of Contents: is the channel in which owners can post their content and the audience can see the contents posted by the owners] - (CNN), composed of 152 layers, which with the help of a technique known as skip connection has paved the way for residual networks (residual network).

What do we need to generate? For the request to our worker we will use a PHP file; while for the script to insert into our worker we will use a customizable one that is more secure than the default one.

Worker script

export default {
  async fetch(request, env) {
    // Check if the Authorization header contains a valid token
    const authHeader = request.headers.get("Authorization");
    const validToken = "YOUR_AUTHENTICATION_TOKEN";

    if (!authHeader || authHeader !== `Bearer ${validToken}`) {
      // If the token is missing or invalid, return a 401 Unauthorized error
      return new Response(
        JSON.stringify({ error: "Invalid or missing authentication token." }),
        { status: 401, headers: { "Content-Type": "application/json" } }
      );
    }

    // Process only valid POST requests
    if (request.method !== "POST") {
      // If the method is not POST, return a 405 Method Not Allowed error
      return new Response("Invalid request method. Use POST with the required data.", {
        status: 405,
        headers: { "Content-Type": "text/plain" },
      });
    }

    try {
      // Validate the content type of the request
      const contentType = request.headers.get("Content-Type") || "";
      if (!contentType.includes("application/json")) {
        // If Content-Type is not JSON, return a 400 Bad Request error
        return new Response(
          JSON.stringify({ error: "Invalid Content-Type. Expected application/json." }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }

      // Parse the body of the request
      const body = await request.text();
      if (!body) {
        // If the request body is empty, return a 400 Bad Request error
        return new Response(
          JSON.stringify({ error: "The request body is empty." }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }

      const { imageUrl } = JSON.parse(body);
      if (!imageUrl) {
        // If the image URL is not provided in the body, return a 400 Bad Request error
        return new Response(
          JSON.stringify({ error: "No image URL provided." }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }

      // Fetch the image from the provided URL
      const imageResponse = await fetch(imageUrl);
      const blob = await imageResponse.arrayBuffer();

      // Prepare the inputs for the AI model
      const inputs = {
        image: [...new Uint8Array(blob)],
      };

      // Run the AI model and get the response
      const response = await env.AI.run("@cf/microsoft/resnet-50", inputs);

      // Return the AI model's response
      return new Response(
        JSON.stringify({ response }),
        { headers: { "Content-Type": "application/json" } }
      );
    } catch (error) {
      // Handle any unexpected errors and return a 500 Internal Server Error
      return new Response(
        JSON.stringify({ error: error.message }),
        { status: 500, headers: { "Content-Type": "application/json" } }
      );
    }
  },
};

What to consider when customizing this script? The constant validToken must contain your custom token to pass at the time of the request, insert a password and then remember it to re-enter it in the PHP file further down; if the tokens are not equal the script will return a 401 error and will save further CPU work (this constant can be changed but must be the same on the PHP script) ; furthermore if the request does not come from a POST method the script will return a 405 error also used to save CPU work; there is also a check to verify that the received request is in JSON format, the only acceptable format; at this point a check to verify if the sent URL exists. If everything is correct, then we can move on to sending the image file to the artificial intelligence, which will return its description in JSON format ready to be captured by the PHP script that we are going to build.

PHP script

Let call this script imageai.php and save it in a protected directory

<?php
// URL of your Cloudflare Worker
$cloudflareUrl = "https://yoururl.workers.dev/";
$authToken = "YOUR_AUTHENTICATION_TOKEN"; // Authentication token to match the Worker

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
	$imageUrl = $_POST['imageUrl'];

	if (empty($imageUrl)) {
		// Check if the image URL is provided; if not, return an error
		echo json_encode(['error' => 'Image URL is missing.']);
		exit;
	}

	// Verify if the URL points to an actual image by checking its Content-Type header
	$headers = get_headers($imageUrl, 1);
	if (!isset($headers['Content-Type']) || strpos($headers['Content-Type'], 'image/') === false) {
		// If the URL does not point to an image, return an error
		echo json_encode(['error' => 'The URL does not contain a valid image.']);
		exit;
	}

	// Prepare the JSON payload for the request
	$data = json_encode(['imageUrl' => $imageUrl]);

	// Set the request options, including the Authorization header with the token
	$options = [
		'http' => [
			'header' => "Content-Type: application/json\r\n" .
				"Authorization: Bearer $authToken\r\n",
			// Include the token in the Authorization header
			'method' => 'POST',
			'content' => $data,
		]
	];

	// Send the request to the Worker and get the response
	$context = stream_context_create($options);
	$response = file_get_contents($cloudflareUrl, false, $context);

	if ($response === FALSE) {
		// If the Worker is unreachable, return an error
		echo json_encode(['error' => 'Unable to contact the Worker.']);
		exit;
	}

	// Decode the JSON response from the Worker
	$decodedResponse = json_decode($response, true);
	if ($decodedResponse === null) {
		// If the response is not valid JSON, return an error
		echo json_encode(['error' => 'The response is not valid JSON.']);
		exit;
	}

	// Output the decoded response
	echo json_encode($decodedResponse);
}
?>

What to consider when customizing this script? The $authToken variable contains the authentication tokens we set in the worker script (previously); the value must be the same in both scripts. The variable $cloudflareUrl contains the entire URL where the worker script is saved (previously saved), usually it starts with https:// and ends with workers.dev but you can also associate a custom subdomain (at your preference). The script can be called from a
in html and must pass the image url from a field with id “imageUrl”, obviously it can also be called via POST from another script in javascript (as you prefer); the PHP script at this point checks that the url passed via POST is there and checks it to verify if it is actually an image; once the checks have passed it calls the worker script, checks if the service is available and reachable, and returns the response that must be in JSON format.

HTML form

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Image Classification</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            padding: 0;
            background-color: #f4f4f9;
            color: #333;
        }
        form {
            max-width: 400px;
            margin: 0 auto;
            padding: 20px;
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
        }
        input[type="text"] {
            width: 100%;
            padding: 10px;
            margin: 10px 0;
            border: 1px solid #ccc;
            border-radius: 3px;
        }
        button {
            width: 100%;
            padding: 10px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
        }
        button:hover {
            background-color: #0056b3;
        }
        .description {
            margin-top: 20px;
            font-size: 16px;
            text-align: center;
            padding: 10px;
            border-radius: 5px;
        }
        .description.success {
            background-color: #d4edda;
            color: #155724;
        }
        .description.error {
            background-color: #f8d7da;
            color: #721c24;
        }
    </style>
</head>
<body>
    <h1>Image Classification</h1>
    <p>Enter the URL of an image to classify it:</p>
    
    <form id="classificationForm" method="POST">
        <input type="text" name="imageUrl" id="imageUrl" placeholder="Enter image URL" required>
        <button type="submit">Classify Image</button>
    </form>
<br>
    <div id="descriptionContainer" class="description" style="display: none;"></div>

    <script>
        document.getElementById('classificationForm').addEventListener('submit', async function (event) {
            event.preventDefault(); // Prevent form submission and page reload

            const imageUrl = document.getElementById('imageUrl').value;
            const descriptionContainer = document.getElementById('descriptionContainer');
            
            try {
                // Send the image URL via POST to the PHP script
                const response = await fetch('imageai.php', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                    },
                    body: 'imageUrl='+encodeURIComponent(imageUrl)
                });

                const result = await response.json();

                if (response.ok) {
                    // Display the description received from the PHP script
                    descriptionContainer.textContent = 'Description: '+result.response;
                    descriptionContainer.className = 'description success';
                } else {
                    // Display an error message
                    descriptionContainer.textContent = 'Error: '+result.error+' An unexpected error occurred.';
                    descriptionContainer.className = 'description error';
                }

                descriptionContainer.style.display = 'block'; // Make the container visible
            } catch (error) {
                // Handle network or server errors
                descriptionContainer.textContent = Error: '+error.message;
                descriptionContainer.className = 'description error';
                descriptionContainer.style.display = 'block';
            }
        });
    </script>
</body>
</html>

Conclusion

This script can be used through an HTML form (as presented here) or even through a javascript script that will take care of sending the POST request, but it can also be used entirely through PHP perhaps making sure that the result is saved directly to a database to be returned at a later time (when requested). The application can have different functions depending on the need; remember that it is always better to make fewer complicated requests to workers because, although they have high limits even with a free account, they still have limits. It is good practice to save the information received in a database, and for the same url (or the same image) and retrieve the information through the database (caching) to not clog up the worker with too many requests. The implemented controls (token and host verification) are used precisely to not send too many requests that could reach the worker limits. In addition, in Cloudflare, you can also set your application firewall (WAF) to ensure that requests to that worker must come from the specified host (so as not to increase the worker limits counter). Remember that for a better performance the resnet-50 model expect the image size 224×224 pixels, so it is advised to resize the image to that dimensions before sending it.