====== Deploy Face Blur Script to Azure Function App ====== Prerequisites Install Azure CLI # Windows curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash # macOS brew install azure-cli # Or download from: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli Install Azure Functions Core Tools # Windows (via npm) npm install -g azure-functions-core-tools@4 --unsafe-perm true # macOS brew tap azure/functions brew install azure-functions-core-tools@4 # Linux curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list' sudo apt-get update sudo apt-get install azure-functions-core-tools-4 Step 1: Login to Azure az login Step 2: Set Your Subscription (if you have multiple) bash# List subscriptions az account list --output table # Set active subscription az account set --subscription "Your-Subscription-Name-Or-ID" Step 3: Create Resource Group (if needed) bash az group create --name myResourceGroup --location eastus Step 4: Create Storage Account (for Function App) bash az storage account create \ --name myfunctionstorageacct \ --location eastus \ --resource-group myResourceGroup \ --sku Standard_LRS Step 5: Create Function App bash az functionapp create \ --resource-group myResourceGroup \ --consumption-plan-location eastus \ --runtime python \ --runtime-version 3.9 \ --functions-version 4 \ --name myFaceBlurFunctionApp \ --storage-account myfunctionstorageacct \ --os-type linux Step 6: Prepare Your Project Structure Create the following folder structure: face-blur-function/ ├── requirements.txt ├── host.json ├── local.settings.json ├── FaceBlurTimer/ │ ├── __init__.py │ └── function.json └── shared/ └── face_blur_processor.py Step 7: Create Project Files requirements.txt txtazure-functions azure-storage-blob requests python-dotenv Pillow schedule host.json { "version": "2.0", "logging": { "applicationInsights": { "samplingSettings": { "isEnabled": true, "excludedTypes": "Request" } } }, "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[2.*, 3.0.0)" }, "functionTimeout": "00:10:00" } local.settings.json { "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "your_function_storage_connection_string", "FUNCTIONS_WORKER_RUNTIME": "python", "AZURE_STORAGE_CONNECTION_STRING": "your_blob_storage_connection_string", "AZURE_CONTAINER_NAME": "your_source_container_name", "WEBAPP_URL": "https://your-webapp.com/blur", "WEBAPP_TIMEOUT": "60" } } FaceBlurTimer/function.json { "scriptFile": "__init__.py", "bindings": [ { "name": "mytimer", "type": "timerTrigger", "direction": "in", "schedule": "0 0 * * * *" } ] } Step 8: Set Application Settings # Set your environment variables az functionapp config appsettings set \ --name myFaceBlurFunctionApp \ --resource-group myResourceGroup \ --settings \ "AZURE_STORAGE_CONNECTION_STRING=your_blob_storage_connection_string" \ "AZURE_CONTAINER_NAME=your_source_container_name" \ "WEBAPP_URL=https://your-webapp.com/blur" \ "WEBAPP_TIMEOUT=60" Step 9: Initialize Function Project Locally # Create project directory mkdir face-blur-function cd face-blur-function # Initialize function project func init --python Step 10: Create Timer Function func new --name FaceBlurTimer --template "Timer trigger" Step 11: Deploy to Azure # Deploy the function func azure functionapp publish myFaceBlurFunctionApp Alternative: Deploy using Azure CLI (Zip Deploy) If you prefer to deploy using zip: Create deployment package zip -r deployment.zip . # Deploy using az cli az functionapp deployment source config-zip \ --resource-group myResourceGroup \ --name myFaceBlurFunctionApp \ --src deployment.zip function_app.py import azure.functions as func import datetime import json import logging app = func.FunctionApp() face_blur_processor.py """ Face Blur Processor Module for Azure Functions """ import os import requests import logging import random from datetime import datetime from azure.storage.blob import BlobServiceClient from azure.core.exceptions import AzureError # Configure logging for Azure Functions logger = logging.getLogger(__name__) class FaceBlurProcessor: def __init__(self): # Azure Blob Storage configuration self.connection_string = os.environ.get('AZURE_STORAGE_CONNECTION_STRING') self.container_name = os.environ.get('AZURE_CONTAINER_NAME', 'images') # Web app configuration webapp_url = os.environ.get('WEBAPP_URL', 'https://your-webapp.com/blur') # Auto-fix URLs missing scheme if not webapp_url.startswith(('http://', 'https://')): webapp_url = f'https://{webapp_url}' self.webapp_url = webapp_url self.webapp_timeout = int(os.environ.get('WEBAPP_TIMEOUT', '60')) # Initialize blob service client if not self.connection_string: raise ValueError("AZURE_STORAGE_CONNECTION_STRING environment variable is required") self.blob_service_client = BlobServiceClient.from_connection_string(self.connection_string) def ensure_container_exists(self): """Ensure the container exists, create it if it does not exist.""" try: container_client = self.blob_service_client.get_container_client(self.container_name) if not container_client.exists(): logger.info(f"Container '{self.container_name}' does not exist. Creating it...") container_client.create_container() logger.info(f"Created container: {self.container_name}") return True except AzureError as e: logger.error(f"Error ensuring container exists: {e}") return False def get_random_image(self): """Get a random image from the blob container.""" try: # Ensure container exists first if not self.ensure_container_exists(): return None, None container_client = self.blob_service_client.get_container_client(self.container_name) # List all blobs blobs = list(container_client.list_blobs()) if not blobs: logger.warning(f"No images found in container '{self.container_name}'") logger.info("Please upload some images to the container first") return None, None # Select a random blob random_blob = random.choice(blobs) logger.info(f"Randomly selected image: {random_blob.name}") logger.info(f"Total images available: {len(blobs)}") return random_blob.name, random_blob except AzureError as e: logger.error(f"Error accessing blob storage: {e}") return None, None def get_latest_image(self): """Get the most recent image from the blob container (kept for backward compatibility).""" try: # Ensure container exists first if not self.ensure_container_exists(): return None, None container_client = self.blob_service_client.get_container_client(self.container_name) # List all blobs and find the most recent one blobs = list(container_client.list_blobs()) if not blobs: logger.warning(f"No images found in container '{self.container_name}'") logger.info("Please upload some images to the container first") return None, None # Sort by last_modified to get the latest latest_blob = max(blobs, key=lambda x: x.last_modified) logger.info(f"Found latest image: {latest_blob.name}") return latest_blob.name, latest_blob except AzureError as e: logger.error(f"Error accessing blob storage: {e}") return None, None def call_blur_webapp(self, blob_name): """Call the web app to blur faces in the image by sending the blob name.""" try: # Send the blob name as form data (matching your webapp's expectation) data = {'file': blob_name} logger.info(f"Making request to: {self.webapp_url} with blob name: {blob_name}") response = requests.post( self.webapp_url, data=data, timeout=self.webapp_timeout ) logger.info(f"Response status code: {response.status_code}") logger.info(f"Response headers: {dict(response.headers)}") if response.status_code == 200: logger.info("Successfully called face blur web app") try: response_data = response.json() logger.info(f"Response JSON: {response_data}") blob_url = response_data.get('blob_url') if blob_url: logger.info(f"Received blob URL: {blob_url}") return blob_url else: logger.error("No blob_url in response") return None except ValueError as e: logger.error(f"Response is not valid JSON: {e}") logger.error(f"Response text: {response.text}") return None else: logger.error(f"Web app returned status code: {response.status_code}") logger.error(f"Response text: {response.text}") # Parse error response try: error_data = response.json() logger.error(f"Error details: {error_data}") # Check for specific Azure errors error_msg = error_data.get('error', '') if 'OutOfRangeInput' in error_msg: logger.error("Azure Face API OutOfRangeInput error - image may not meet requirements") elif 'InvalidImageFormat' in error_msg: logger.error("Azure Face API InvalidImageFormat error - image format not supported") elif 'InvalidImageSize' in error_msg: logger.error("Azure Face API InvalidImageSize error - image size not supported") elif 'NoFaceDetected' in error_msg: logger.info("No faces detected in image - this might be normal") return None # This might not be an error except ValueError: logger.error("Error response is not JSON") return None except Exception as e: logger.error(f"Error calling web app: {e}") return None def log_processed_image(self, blob_url, original_blob_name): """Log the processed image URL (since your web app already uploads to blob storage).""" try: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # Log the processing completion logger.info(f"Processed image available at: {blob_url}") logger.info(f"Original image: {original_blob_name}") logger.info(f"Processing completed at: {timestamp}") return blob_url except Exception as e: logger.error(f"Error logging processed image: {e}") return None def process_random_image(self): """Main processing function - pull random image, blur faces, and log result.""" logger.info("Starting face blur processing (random image selection)...") # Get random image blob_name, blob_info = self.get_random_image() if not blob_name: return False try: # Call web app to blur faces directly with blob name (no need to download) blob_url = self.call_blur_webapp(blob_name) if not blob_url: return False # Log the processed image info (web app already uploaded it) result = self.log_processed_image(blob_url, blob_name) if result: logger.info(f"Successfully processed random image. Blurred version at: {blob_url}") return True else: return False except Exception as e: logger.error(f"Unexpected error during processing: {e}") return False def process_latest_image(self): """Main processing function - pull latest image, blur faces, and log result (kept for backward compatibility).""" logger.info("Starting face blur processing (latest image selection)...") # Get latest image blob_name, blob_info = self.get_latest_image() if not blob_name: return False try: # Call web app to blur faces directly with blob name (no need to download) blob_url = self.call_blur_webapp(blob_name) if not blob_url: return False # Log the processed image info (web app already uploaded it) result = self.log_processed_image(blob_url, blob_name) if result: logger.info(f"Successfully processed image. Blurred version at: {blob_url}") return True else: return False except Exception as e: logger.error(f"Unexpected error during processing: {e}") return False __init__.py import datetime import logging import azure.functions as func import sys import os # Add the parent directory to the path so we can import our modules sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from shared.face_blur_processor import FaceBlurProcessor def main(mytimer: func.TimerRequest) -> None: """ Azure Function that runs on a timer to process random images for face blurring. """ utc_timestamp = datetime.datetime.utcnow().replace( tzinfo=datetime.timezone.utc).isoformat() if mytimer.past_due: logging.info('The timer is past due!') logging.info('Face blur timer trigger function ran at %s', utc_timestamp) try: # Initialize the face blur processor processor = FaceBlurProcessor() # Process a random image success = processor.process_random_image() if success: logging.info("Face blur processing completed successfully") else: logging.error("Face blur processing failed") except Exception as e: logging.error(f"Error in face blur function: {str(e)}") raise e logging.info('Face blur timer trigger function completed at %s', datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat())