Skip to content
English
On this page

Ejercicio: Sistema de Análisis de Imágenes Médicas con AWS Rekognition

Escenario

Desarrollaremos un sistema que permita:

  • Análisis de imágenes médicas usando Rekognition Custom Labels
  • Almacenamiento seguro de imágenes en S3
  • Interfaz de usuario con Vue.js y Amplify
  • Sistema de etiquetado y categorización
  • Reportes y seguimiento
  • Integración con registros médicos
  • Control de acceso basado en roles

Estructura del Proyecto

plaintext
medical-imaging-analyzer/
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   │   ├── ImageAnalysis/
│   │   │   │   ├── UploadForm.vue
│   │   │   │   ├── AnalysisViewer.vue
│   │   │   │   └── ResultsPanel.vue
│   │   │   ├── Reports/
│   │   │   │   ├── ReportGenerator.vue
│   │   │   │   └── ReportViewer.vue
│   │   │   └── common/
│   │   │       ├── Navigation.vue
│   │   │       └── Loading.vue
│   │   │
│   │   ├── views/
│   │   │   ├── Dashboard.vue
│   │   │   ├── Analysis.vue
│   │   │   ├── Reports.vue
│   │   │   └── Settings.vue
│   │   │
│   │   ├── store/
│   │   │   ├── modules/
│   │   │   │   ├── analysis.js
│   │   │   │   └── auth.js
│   │   │   └── index.js
│   │   │
│   │   └── services/
│   │       ├── api.js
│   │       └── rekognition.js
│   │
│   ├── public/
│   │   └── index.html
│   │
│   └── amplify/
│       └── backend/

├── backend/
│   ├── functions/
│   │   ├── analyzeImage/
│   │   │   └── index.js
│   │   ├── generateReport/
│   │   │   └── index.js
│   │   └── processResults/
│   │       └── index.js
│   │
│   ├── lib/
│   │   ├── rekognition.js
│   │   ├── s3.js
│   │   └── dynamodb.js
│   │
│   └── models/
│       ├── Analysis.js
│       └── Report.js

├── infrastructure/
│   ├── terraform/
│   │   ├── modules/
│   │   │   ├── storage/
│   │   │   ├── rekognition/
│   │   │   └── database/
│   │   └── environments/
│   │       ├── dev/
│   │       ├── stg/
│   │       └── prod/
│   │
│   └── scripts/
│       ├── setup.sh
│       └── deploy.sh

├── docs/
│   ├── api/
│   ├── architecture/
│   └── user-guide/

└── tests/
    ├── unit/
    ├── integration/
    └── e2e/

Etapas del Ejercicio:

Etapa 1: Infraestructura Base

  • Configurar S3 para almacenamiento
  • Implementar DynamoDB
  • Configurar Cognito
  • Establecer roles IAM

Etapa 2: Configuración de Rekognition

  • Configurar Custom Labels
  • Implementar modelo de entrenamiento
  • Establecer pipeline de procesamiento
  • Configurar endpoints

Etapa 3: Backend Base

  • Implementar Lambda functions
  • Configurar API Gateway
  • Manejar almacenamiento
  • Procesar resultados

Etapa 4: Frontend Base

  • Implementar UI con Vue.js
  • Configurar Amplify
  • Crear componentes base
  • Implementar autenticación

Etapa 5: Análisis y Procesamiento

  • Implementar carga de imágenes
  • Procesar resultados de Rekognition
  • Generar análisis
  • Manejar respuestas

Etapa 6: Reportes y Visualización

  • Implementar generación de reportes
  • Crear visualizaciones
  • Exportar resultados
  • Manejar históricos

Etapa 7: Seguridad y Producción

  • Implementar HIPAA compliance
  • Configurar auditoría
  • Optimizar rendimiento
  • Preparar para producción

Etapa 1: Infraestructura Base

Objetivos:

  1. Configurar S3 para almacenamiento seguro de imágenes médicas
  2. Implementar DynamoDB para metadatos y resultados
  3. Configurar Cognito para autenticación
  4. Establecer roles IAM y permisos

1.1 Configuración S3

hcl
# infrastructure/terraform/modules/storage/s3.tf

resource "aws_s3_bucket" "medical_images" {
  bucket = "medical-images-${var.environment}-${data.aws_caller_identity.current.account_id}"

  tags = {
    Environment = var.environment
    Purpose     = "Medical Image Storage"
  }
}

resource "aws_s3_bucket_versioning" "medical_images" {
  bucket = aws_s3_bucket.medical_images.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "medical_images" {
  bucket = aws_s3_bucket.medical_images.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.medical_images.arn
    }
  }
}

resource "aws_s3_bucket_public_access_block" "medical_images" {
  bucket = aws_s3_bucket.medical_images.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_kms_key" "medical_images" {
  description             = "KMS key for medical images encryption"
  deletion_window_in_days = 7
  enable_key_rotation     = true

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "Enable IAM User Permissions"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Action   = "kms:*"
        Resource = "*"
      },
      {
        Sid    = "Allow S3 to use the key"
        Effect = "Allow"
        Principal = {
          Service = "s3.amazonaws.com"
        }
        Action = [
          "kms:Decrypt",
          "kms:GenerateDataKey"
        ]
        Resource = "*"
      }
    ]
  })

  tags = {
    Environment = var.environment
    Purpose     = "Medical Image Encryption"
  }
}

1.2 Configuración DynamoDB

hcl
# infrastructure/terraform/modules/database/dynamodb.tf

resource "aws_dynamodb_table" "medical_analyses" {
  name           = "medical-analyses-${var.environment}"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "analysisId"
  range_key      = "timestamp"

  attribute {
    name = "analysisId"
    type = "S"
  }

  attribute {
    name = "timestamp"
    type = "N"
  }

  attribute {
    name = "patientId"
    type = "S"
  }

  attribute {
    name = "imageType"
    type = "S"
  }

  global_secondary_index {
    name               = "PatientIndex"
    hash_key           = "patientId"
    range_key          = "timestamp"
    projection_type    = "ALL"
  }

  global_secondary_index {
    name               = "ImageTypeIndex"
    hash_key           = "imageType"
    range_key          = "timestamp"
    projection_type    = "ALL"
  }

  point_in_time_recovery {
    enabled = true
  }

  server_side_encryption {
    enabled     = true
    kms_key_arn = aws_kms_key.dynamodb.arn
  }

  tags = {
    Environment = var.environment
    Purpose     = "Medical Analysis Storage"
  }
}

resource "aws_dynamodb_table" "medical_reports" {
  name           = "medical-reports-${var.environment}"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "reportId"
  range_key      = "createdAt"

  attribute {
    name = "reportId"
    type = "S"
  }

  attribute {
    name = "createdAt"
    type = "N"
  }

  attribute {
    name = "patientId"
    type = "S"
  }

  global_secondary_index {
    name               = "PatientReportsIndex"
    hash_key           = "patientId"
    range_key          = "createdAt"
    projection_type    = "ALL"
  }

  point_in_time_recovery {
    enabled = true
  }

  server_side_encryption {
    enabled     = true
    kms_key_arn = aws_kms_key.dynamodb.arn
  }

  tags = {
    Environment = var.environment
    Purpose     = "Medical Reports Storage"
  }
}

resource "aws_kms_key" "dynamodb" {
  description             = "KMS key for DynamoDB encryption"
  deletion_window_in_days = 7
  enable_key_rotation     = true

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "Enable IAM User Permissions"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Action   = "kms:*"
        Resource = "*"
      }
    ]
  })

  tags = {
    Environment = var.environment
    Purpose     = "DynamoDB Encryption"
  }
}

1.3 Configuración Cognito

hcl
# infrastructure/terraform/modules/auth/cognito.tf

resource "aws_cognito_user_pool" "medical_users" {
  name = "medical-users-${var.environment}"

  admin_create_user_config {
    allow_admin_create_user_only = true
  }

  password_policy {
    minimum_length                   = 12
    require_lowercase               = true
    require_numbers                 = true
    require_symbols                 = true
    require_uppercase               = true
    temporary_password_validity_days = 7
  }

  mfa_configuration = "OPTIONAL"
  
  software_token_mfa_configuration {
    enabled = true
  }

  account_recovery_setting {
    recovery_mechanism {
      name     = "verified_email"
      priority = 1
    }
  }

  schema {
    attribute_data_type = "String"
    name               = "medical_role"
    required           = true
    mutable            = true

    string_attribute_constraints {
      min_length = 1
      max_length = 256
    }
  }

  schema {
    attribute_data_type = "String"
    name               = "department"
    required           = true
    mutable            = true

    string_attribute_constraints {
      min_length = 1
      max_length = 256
    }
  }

  user_pool_add_ons {
    advanced_security_mode = "ENFORCED"
  }

  tags = {
    Environment = var.environment
  }
}

resource "aws_cognito_user_pool_client" "medical_app" {
  name = "medical-app-${var.environment}"

  user_pool_id = aws_cognito_user_pool.medical_users.id

  generate_secret = false
  
  explicit_auth_flows = [
    "ALLOW_USER_SRP_AUTH",
    "ALLOW_REFRESH_TOKEN_AUTH",
    "ALLOW_USER_PASSWORD_AUTH"
  ]

  token_validity_units {
    access_token  = "hours"
    id_token      = "hours"
    refresh_token = "days"
  }

  access_token_validity  = 1
  id_token_validity     = 1
  refresh_token_validity = 30

  prevent_user_existence_errors = "ENABLED"

  read_attributes = [
    "email",
    "email_verified",
    "medical_role",
    "department",
    "custom:access_level"
  ]

  write_attributes = [
    "email",
    "medical_role",
    "department"
  ]
}

resource "aws_cognito_user_group" "doctors" {
  name         = "Doctors"
  user_pool_id = aws_cognito_user_pool.medical_users.id
  description  = "Medical doctors group"
  precedence   = 1
}

resource "aws_cognito_user_group" "radiologists" {
  name         = "Radiologists"
  user_pool_id = aws_cognito_user_pool.medical_users.id
  description  = "Radiologists group"
  precedence   = 2
}

resource "aws_cognito_user_group" "admins" {
  name         = "Administrators"
  user_pool_id = aws_cognito_user_pool.medical_users.id
  description  = "System administrators"
  precedence   = 0
}

1.4 Configuración IAM

hcl
# infrastructure/terraform/modules/iam/policies.tf

# Política base para acceso a recursos médicos
resource "aws_iam_policy" "medical_base_access" {
  name        = "medical-base-access-${var.environment}"
  description = "Base policy for medical resources access"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:ListBucket"
        ]
        Resource = [
          aws_s3_bucket.medical_images.arn,
          "${aws_s3_bucket.medical_images.arn}/*"
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "dynamodb:Query",
          "dynamodb:GetItem"
        ]
        Resource = [
          aws_dynamodb_table.medical_analyses.arn,
          aws_dynamodb_table.medical_reports.arn
        ]
      }
    ]
  })
}

# Política para médicos
resource "aws_iam_policy" "doctor_access" {
  name        = "doctor-access-${var.environment}"
  description = "Access policy for doctors"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:PutObject",
          "s3:GetObject",
          "s3:ListBucket"
        ]
        Resource = [
          aws_s3_bucket.medical_images.arn,
          "${aws_s3_bucket.medical_images.arn}/*"
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "dynamodb:Query",
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:UpdateItem"
        ]
        Resource = [
          aws_dynamodb_table.medical_analyses.arn,
          aws_dynamodb_table.medical_reports.arn
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "rekognition:DetectLabels",
          "rekognition:DetectModerationLabels"
        ]
        Resource = "*"
      }
    ]
  })
}

# Política para administradores
resource "aws_iam_policy" "admin_access" {
  name        = "admin-access-${var.environment}"
  description = "Access policy for administrators"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:*"
        ]
        Resource = [
          aws_s3_bucket.medical_images.arn,
          "${aws_s3_bucket.medical_images.arn}/*"
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "dynamodb:*"
        ]
        Resource = [
          aws_dynamodb_table.medical_analyses.arn,
          aws_dynamodb_table.medical_reports.arn
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "rekognition:*"
        ]
        Resource = "*"
      },
      {
        Effect = "Allow"
        Action = [
          "cognito-idp:ListUsers",
          "cognito-idp:AdminCreateUser",
          "cognito-idp:AdminUpdateUserAttributes"
        ]
        Resource = aws_cognito_user_pool.medical_users.arn
      }
    ]
  })
}

Verificación de la Etapa 1

Checklist:

  • [ ] S3 configurado con encriptación
  • [ ] DynamoDB configurado con índices
  • [ ] Cognito configurado con grupos
  • [ ] IAM policies establecidas
  • [ ] KMS keys creadas
  • [ ] Permisos verificados
  • [ ] HIPAA compliance verificado

Pruebas Recomendadas:

  1. Verificar encriptación S3
  2. Probar acceso DynamoDB
  3. Validar autenticación
  4. Comprobar permisos

Troubleshooting Común:

  1. Problemas de S3:

    • Verificar encriptación
    • Revisar políticas
    • Comprobar KMS
  2. Errores de DynamoDB:

    • Verificar índices
    • Revisar encriptación
    • Comprobar throughput
  3. Problemas de Cognito:

    • Verificar configuración
    • Revisar grupos
    • Comprobar atributos
  4. Errores de IAM:

    • Verificar políticas
    • Revisar roles
    • Comprobar permisos

Etapa 2: Configuración de Rekognition

Objetivos:

  1. Configurar Rekognition Custom Labels
  2. Implementar modelo de entrenamiento
  3. Configurar pipeline de procesamiento
  4. Establecer API endpoints

2.1 Configuración de Rekognition Custom Labels

hcl
# infrastructure/terraform/modules/rekognition/main.tf

resource "aws_rekognition_project" "medical_imaging" {
  project_name = "medical-imaging-${var.environment}"
}

resource "aws_rekognition_dataset" "training" {
  project_name = aws_rekognition_project.medical_imaging.project_name
  dataset_type = "TRAIN"
  dataset_source {
    s3_bucket {
      bucket = aws_s3_bucket.training_data.id
      prefix = "training/"
    }
  }
}

resource "aws_rekognition_dataset" "testing" {
  project_name = aws_rekognition_project.medical_imaging.project_name
  dataset_type = "TEST"
  dataset_source {
    s3_bucket {
      bucket = aws_s3_bucket.training_data.id
      prefix = "testing/"
    }
  }
}

resource "aws_s3_bucket" "training_data" {
  bucket = "medical-training-${var.environment}-${data.aws_caller_identity.current.account_id}"
  
  versioning {
    enabled = true
  }
  
  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        sse_algorithm = "AES256"
      }
    }
  }
}

resource "aws_s3_bucket_policy" "training_data" {
  bucket = aws_s3_bucket.training_data.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid = "AllowRekognitionAccess"
        Effect = "Allow"
        Principal = {
          Service = "rekognition.amazonaws.com"
        }
        Action = [
          "s3:GetBucketLocation",
          "s3:GetObject",
          "s3:ListBucket"
        ]
        Resource = [
          aws_s3_bucket.training_data.arn,
          "${aws_s3_bucket.training_data.arn}/*"
        ]
      }
    ]
  })
}

2.2 Configuración del Modelo de Entrenamiento

python
# backend/lib/rekognition_training.py

import boto3
import json
import time
from botocore.exceptions import ClientError

class RekognitionTraining:
    def __init__(self, project_name, version_name):
        self.rekognition = boto3.client('rekognition')
        self.project_name = project_name
        self.version_name = version_name

    async def start_model_training(self):
        """Inicia el entrenamiento del modelo"""
        try:
            response = self.rekognition.create_project_version(
                ProjectArn=self.project_name,
                VersionName=self.version_name,
                OutputConfig={
                    'S3Bucket': self.training_bucket,
                    'S3KeyPrefix': f'output/{self.version_name}'
                },
                TrainingParameters={
                    'MAX_ITERATIONS': 100,
                    'LEARNING_RATE': '0.001',
                    'AUTO_STOP': True
                }
            )
            
            return response['ProjectVersionArn']
        except ClientError as e:
            print(f"Error starting model training: {e}")
            raise

    async def check_training_status(self, version_arn):
        """Verifica el estado del entrenamiento"""
        try:
            while True:
                response = self.rekognition.describe_project_versions(
                    ProjectArn=self.project_name,
                    VersionNames=[self.version_name]
                )
                
                status = response['ProjectVersionDescriptions'][0]['Status']
                
                if status == 'TRAINING_COMPLETED':
                    return True
                elif status in ['TRAINING_FAILED', 'FAILED']:
                    raise Exception(f"Training failed: {response['ProjectVersionDescriptions'][0]['StatusMessage']}")
                
                await asyncio.sleep(60)  # Check every minute
                
        except ClientError as e:
            print(f"Error checking training status: {e}")
            raise

    async def start_model(self, version_arn):
        """Inicia el modelo entrenado"""
        try:
            response = self.rekognition.start_project_version(
                ProjectVersionArn=version_arn,
                MinInferenceUnits=1
            )
            return response
        except ClientError as e:
            print(f"Error starting model: {e}")
            raise

    async def evaluate_model(self, version_arn):
        """Evalúa el rendimiento del modelo"""
        try:
            response = self.rekognition.evaluate_project_version(
                ProjectVersionArn=version_arn
            )
            
            evaluation_results = {
                'f1_score': response['EvaluationResults']['F1Score'],
                'precision': response['EvaluationResults']['Precision'],
                'recall': response['EvaluationResults']['Recall'],
                'confidence_threshold': response['EvaluationResults']['ConfidenceThreshold']
            }
            
            return evaluation_results
        except ClientError as e:
            print(f"Error evaluating model: {e}")
            raise

    def stop_model(self, version_arn):
        """Detiene el modelo"""
        try:
            response = self.rekognition.stop_project_version(
                ProjectVersionArn=version_arn
            )
            return response
        except ClientError as e:
            print(f"Error stopping model: {e}")
            raise

2.3 Pipeline de Procesamiento

python
# backend/lib/image_processing.py

import boto3
import json
from datetime import datetime
from typing import Dict, List, Optional

class ImageProcessor:
    def __init__(self):
        self.rekognition = boto3.client('rekognition')
        self.s3 = boto3.client('s3')
        self.dynamodb = boto3.resource('dynamodb')

    async def process_medical_image(
        self,
        bucket: str,
        key: str,
        metadata: Dict,
        model_arn: str
    ) -> Dict:
        """Procesa una imagen médica usando Rekognition Custom Labels"""
        try:
            # Detectar etiquetas personalizadas
            custom_labels = await self._detect_custom_labels(bucket, key, model_arn)
            
            # Análisis de calidad de imagen
            quality_analysis = await self._analyze_image_quality(bucket, key)
            
            # Metadata enriquecida
            enriched_metadata = self._enrich_metadata(metadata, custom_labels, quality_analysis)
            
            # Guardar resultados
            analysis_id = await self._save_analysis_results(
                bucket,
                key,
                enriched_metadata,
                custom_labels,
                quality_analysis
            )
            
            return {
                'analysis_id': analysis_id,
                'custom_labels': custom_labels,
                'quality_analysis': quality_analysis,
                'metadata': enriched_metadata
            }
            
        except Exception as e:
            print(f"Error processing image: {e}")
            raise

    async def _detect_custom_labels(
        self,
        bucket: str,
        key: str,
        model_arn: str
    ) -> List[Dict]:
        """Detecta etiquetas personalizadas en la imagen"""
        try:
            response = self.rekognition.detect_custom_labels(
                Image={
                    'S3Object': {
                        'Bucket': bucket,
                        'Name': key
                    }
                },
                ProjectVersionArn=model_arn,
                MinConfidence=90.0
            )
            
            return [{
                'name': label['Name'],
                'confidence': label['Confidence'],
                'coordinates': label.get('Geometry', {})
            } for label in response['CustomLabels']]
            
        except Exception as e:
            print(f"Error detecting custom labels: {e}")
            raise

    async def _analyze_image_quality(
        self,
        bucket: str,
        key: str
    ) -> Dict:
        """Analiza la calidad de la imagen"""
        try:
            # Obtener metadata de S3
            image_metadata = self.s3.head_object(Bucket=bucket, Key=key)
            
            # Analizar calidad
            quality_analysis = {
                'format': key.split('.')[-1].upper(),
                'size_bytes': image_metadata['ContentLength'],
                'content_type': image_metadata['ContentType'],
                'timestamp': image_metadata['LastModified'].isoformat()
            }
            
            # Análisis adicional con Rekognition
            quality_response = self.rekognition.detect_moderation_labels(
                Image={
                    'S3Object': {
                        'Bucket': bucket,
                        'Name': key
                    }
                }
            )
            
            quality_analysis['moderation'] = {
                'issues_detected': len(quality_response['ModerationLabels']) == 0,
                'confidence': quality_response['ModerationLabels'][0]['Confidence'] if quality_response['ModerationLabels'] else 100
            }
            
            return quality_analysis
            
        except Exception as e:
            print(f"Error analyzing image quality: {e}")
            raise

    def _enrich_metadata(
        self,
        original_metadata: Dict,
        custom_labels: List[Dict],
        quality_analysis: Dict
    ) -> Dict:
        """Enriquece los metadatos con los resultados del análisis"""
        return {
            **original_metadata,
            'analysis_timestamp': datetime.utcnow().isoformat(),
            'detected_conditions': [label['name'] for label in custom_labels],
            'quality_metrics': {
                'format': quality_analysis['format'],
                'size': quality_analysis['size_bytes'],
                'quality_score': quality_analysis['moderation']['confidence']
            }
        }

    async def _save_analysis_results(
        self,
        bucket: str,
        key: str,
        metadata: Dict,
        custom_labels: List[Dict],
        quality_analysis: Dict
    ) -> str:
        """Guarda los resultados del análisis en DynamoDB"""
        try:
            table = self.dynamodb.Table(os.environ['ANALYSES_TABLE'])
            
            analysis_id = f"analysis_{int(datetime.utcnow().timestamp())}"
            
            item = {
                'analysis_id': analysis_id,
                'image_bucket': bucket,
                'image_key': key,
                'timestamp': int(datetime.utcnow().timestamp()),
                'metadata': metadata,
                'custom_labels': custom_labels,
                'quality_analysis': quality_analysis,
                'status': 'COMPLETED'
            }
            
            table.put_item(Item=item)
            
            return analysis_id
            
        except Exception as e:
            print(f"Error saving analysis results: {e}")
            raise

2.4 API Endpoints

python
# backend/functions/rekognition/handlers.py

import json
import os
from typing import Dict, Any
from lib.image_processing import ImageProcessor
from lib.rekognition_training import RekognitionTraining
from lib.models import AnalysisRequest, TrainingRequest, ModelStatus

class RekognitionAPI:
    def __init__(self):
        self.processor = ImageProcessor()
        self.trainer = RekognitionTraining(
            project_name=os.environ['REKOGNITION_PROJECT_NAME'],
            version_name=f"v{int(datetime.utcnow().timestamp())}"
        )

    async def handle_image_analysis(self, event: Dict[str, Any]) -> Dict:
        """Maneja el análisis de imágenes médicas"""
        try:
            body = json.loads(event['body'])
            request = AnalysisRequest(**body)

            # Validar imagen
            if not await self._validate_image(request.bucket, request.key):
                return self._error_response(400, 'Invalid image format or size')

            # Procesar imagen
            results = await self.processor.process_medical_image(
                bucket=request.bucket,
                key=request.key,
                metadata=request.metadata,
                model_arn=os.environ['REKOGNITION_MODEL_ARN']
            )

            return self._success_response(results)

        except Exception as e:
            return self._error_response(500, str(e))

    async def handle_model_training(self, event: Dict[str, Any]) -> Dict:
        """Maneja el entrenamiento del modelo"""
        try:
            body = json.loads(event['body'])
            request = TrainingRequest(**body)

            # Iniciar entrenamiento
            version_arn = await self.trainer.start_model_training()

            # Verificar estado
            if not await self.trainer.check_training_status(version_arn):
                return self._error_response(500, 'Model training failed')

            # Evaluar modelo
            evaluation_results = await self.trainer.evaluate_model(version_arn)

            # Iniciar modelo si la evaluación es satisfactoria
            if evaluation_results['f1_score'] >= 0.90:
                await self.trainer.start_model(version_arn)
                
                return self._success_response({
                    'version_arn': version_arn,
                    'evaluation': evaluation_results,
                    'status': 'DEPLOYED'
                })
            else:
                return self._success_response({
                    'version_arn': version_arn,
                    'evaluation': evaluation_results,
                    'status': 'EVALUATION_FAILED'
                })

        except Exception as e:
            return self._error_response(500, str(e))

    async def handle_model_status(self, event: Dict[str, Any]) -> Dict:
        """Obtiene el estado del modelo"""
        try:
            model_arn = event['pathParameters']['modelArn']
            status = await self.trainer.get_model_status(model_arn)
            
            return self._success_response({
                'model_arn': model_arn,
                'status': status.value,
                'details': await self.trainer.get_model_details(model_arn)
            })

        except Exception as e:
            return self._error_response(500, str(e))

    async def handle_batch_analysis(self, event: Dict[str, Any]) -> Dict:
        """Maneja el análisis por lotes de imágenes"""
        try:
            body = json.loads(event['body'])
            images = body['images']
            
            results = []
            for image in images:
                result = await self.processor.process_medical_image(
                    bucket=image['bucket'],
                    key=image['key'],
                    metadata=image.get('metadata', {}),
                    model_arn=os.environ['REKOGNITION_MODEL_ARN']
                )
                results.append(result)

            return self._success_response({
                'batch_id': f"batch_{int(datetime.utcnow().timestamp())}",
                'results': results,
                'summary': self._generate_batch_summary(results)
            })

        except Exception as e:
            return self._error_response(500, str(e))

    async def handle_model_metrics(self, event: Dict[str, Any]) -> Dict:
        """Obtiene métricas del modelo"""
        try:
            model_arn = event['pathParameters']['modelArn']
            time_range = event.get('queryStringParameters', {}).get('timeRange', '24h')
            
            metrics = await self.trainer.get_model_metrics(model_arn, time_range)
            
            return self._success_response({
                'model_arn': model_arn,
                'time_range': time_range,
                'metrics': metrics
            })

        except Exception as e:
            return self._error_response(500, str(e))

    async def handle_custom_labels_management(self, event: Dict[str, Any]) -> Dict:
        """Gestiona las etiquetas personalizadas"""
        try:
            operation = event['httpMethod']
            
            if operation == 'GET':
                labels = await self.trainer.get_custom_labels()
                return self._success_response({'labels': labels})
                
            elif operation == 'POST':
                body = json.loads(event['body'])
                new_label = await self.trainer.add_custom_label(
                    name=body['name'],
                    description=body.get('description', ''),
                    metadata=body.get('metadata', {})
                )
                return self._success_response({'label': new_label})
                
            elif operation == 'DELETE':
                label_name = event['pathParameters']['labelName']
                await self.trainer.delete_custom_label(label_name)
                return self._success_response({
                    'message': f'Label {label_name} deleted successfully'
                })

        except Exception as e:
            return self._error_response(500, str(e))

    def _generate_batch_summary(self, results: List[Dict]) -> Dict:
        """Genera un resumen del análisis por lotes"""
        total_images = len(results)
        successful = len([r for r in results if r.get('status') == 'SUCCESS'])
        failed = total_images - successful
        
        conditions_found = {}
        for result in results:
            for label in result.get('custom_labels', []):
                conditions_found[label['name']] = conditions_found.get(label['name'], 0) + 1

        return {
            'total_images': total_images,
            'successful_analyses': successful,
            'failed_analyses': failed,
            'conditions_found': conditions_found,
            'average_confidence': sum(r.get('confidence', 0) for r in results) / total_images
        }

    def _success_response(self, body: Dict) -> Dict:
        """Genera una respuesta exitosa"""
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps(body)
        }

    def _error_response(self, status_code: int, message: str) -> Dict:
        """Genera una respuesta de error"""
        return {
            'statusCode': status_code,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'error': message
            })
        }

# Rutas disponibles:
# POST /analyze - Analiza una imagen individual
# POST /analyze/batch - Analiza un lote de imágenes
# POST /models/train - Inicia el entrenamiento de un nuevo modelo
# GET /models/{modelArn}/status - Obtiene el estado de un modelo
# GET /models/{modelArn}/metrics - Obtiene métricas de un modelo
# GET /labels - Obtiene etiquetas personalizadas
# POST /labels - Crea una nueva etiqueta personalizada
# DELETE /labels/{labelName} - Elimina una etiqueta personalizada

Este código proporciona una API completa para:

  1. Análisis de imágenes individuales y por lotes
  2. Gestión de modelos (entrenamiento, estado, métricas)
  3. Gestión de etiquetas personalizadas
  4. Manejo de errores y respuestas estandarizadas
  5. Validación de entradas
  6. Soporte para CORS
  7. Métricas y seguimiento

Cada endpoint está documentado y maneja diferentes casos de uso del sistema de análisis de imágenes médicas.

Etapa 3: Backend Base

Objetivos:

  1. Implementar funciones Lambda
  2. Configurar API Gateway
  3. Manejar resultados y almacenamiento
  4. Implementar lógica de negocio

3.1 Funciones Lambda Base

javascript
// backend/functions/analyzeImage/index.js

const { ImageAnalyzer } = require('../../lib/imageAnalyzer');
const { ResponseBuilder } = require('../../lib/responseBuilder');
const { ValidationService } = require('../../lib/validationService');

exports.handler = async (event) => {
    const analyzer = new ImageAnalyzer();
    const validator = new ValidationService();
    
    try {
        // Validar entrada
        const body = JSON.parse(event.body);
        const validationResult = await validator.validateImageRequest(body);
        
        if (!validationResult.isValid) {
            return ResponseBuilder.error(400, {
                message: 'Validation failed',
                errors: validationResult.errors
            });
        }

        // Analizar imagen
        const analysisResult = await analyzer.analyzeImage({
            imageId: body.imageId,
            patientId: body.patientId,
            imageType: body.imageType,
            studyMetadata: body.metadata,
            s3Location: body.s3Location
        });

        // Responder
        return ResponseBuilder.success(200, {
            analysisId: analysisResult.analysisId,
            results: analysisResult.results,
            confidence: analysisResult.confidence,
            processingTime: analysisResult.processingTime
        });

    } catch (error) {
        console.error('Error processing image:', error);
        return ResponseBuilder.error(500, {
            message: 'Internal server error',
            error: error.message
        });
    }
};

// backend/functions/processResults/index.js
const { ResultProcessor } = require('../../lib/resultProcessor');
const { NotificationService } = require('../../lib/notificationService');

exports.handler = async (event) => {
    const processor = new ResultProcessor();
    const notifier = new NotificationService();

    try {
        const records = event.Records;
        for (const record of records) {
            if (record.eventName === 'INSERT') {
                const analysisResult = record.dynamodb.NewImage;

                // Procesar resultados
                const processedResults = await processor.processAnalysisResults(analysisResult);

                // Notificar si es necesario
                if (processedResults.requiresAttention) {
                    await notifier.notifyDoctors({
                        patientId: processedResults.patientId,
                        analysisId: processedResults.analysisId,
                        findings: processedResults.findings,
                        priority: processedResults.priority
                    });
                }

                // Actualizar registro
                await processor.updateAnalysisStatus(
                    processedResults.analysisId,
                    'PROCESSED'
                );
            }
        }

        return {
            statusCode: 200,
            body: JSON.stringify({
                message: 'Results processed successfully'
            })
        };

    } catch (error) {
        console.error('Error processing results:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({
                message: 'Error processing results',
                error: error.message
            })
        };
    }
};

3.2 Servicios de Soporte

javascript
// backend/lib/imageAnalyzer.js
const AWS = require('aws-sdk');
const rekognition = new AWS.Rekognition();
const s3 = new AWS.S3();

class ImageAnalyzer {
    constructor() {
        this.rekognition = rekognition;
        this.s3 = s3;
    }

    async analyzeImage(params) {
        const startTime = Date.now();
        
        try {
            // Obtener imagen de S3
            const imageData = await this.s3.getObject({
                Bucket: params.s3Location.bucket,
                Key: params.s3Location.key
            }).promise();

            // Analizar con Rekognition
            const rekognitionResponse = await this.rekognition.detectCustomLabels({
                Image: {
                    Bytes: imageData.Body
                },
                ProjectVersionArn: process.env.REKOGNITION_MODEL_ARN,
                MinConfidence: 90
            }).promise();

            // Procesar resultados
            const results = this._processRekognitionResults(
                rekognitionResponse,
                params.imageType
            );

            return {
                analysisId: `analysis_${Date.now()}`,
                results: results,
                confidence: this._calculateOverallConfidence(results),
                processingTime: Date.now() - startTime,
                metadata: {
                    ...params.studyMetadata,
                    imageType: params.imageType,
                    patientId: params.patientId,
                    analysisDate: new Date().toISOString()
                }
            };
        } catch (error) {
            console.error('Error analyzing image:', error);
            throw error;
        }
    }

    _processRekognitionResults(rekognitionResponse, imageType) {
        const customLabels = rekognitionResponse.CustomLabels;
        return customLabels.map(label => ({
            condition: label.Name,
            confidence: label.Confidence,
            location: label.Geometry,
            severity: this._calculateSeverity(label, imageType)
        }));
    }

    _calculateSeverity(label, imageType) {
        // Lógica personalizada para calcular severidad basada en el tipo de imagen
        const confidenceThresholds = {
            xray: { high: 95, medium: 85 },
            mri: { high: 92, medium: 82 },
            ct: { high: 93, medium: 83 }
        };

        const thresholds = confidenceThresholds[imageType] || 
                          confidenceThresholds.xray;

        if (label.Confidence >= thresholds.high) return 'HIGH';
        if (label.Confidence >= thresholds.medium) return 'MEDIUM';
        return 'LOW';
    }

    _calculateOverallConfidence(results) {
        if (!results.length) return 0;
        return results.reduce((acc, curr) => acc + curr.confidence, 0) / results.length;
    }
}

// backend/lib/resultProcessor.js
class ResultProcessor {
    constructor() {
        this.dynamodb = new AWS.DynamoDB.DocumentClient();
    }

    async processAnalysisResults(analysisResult) {
        try {
            const findings = this._analyzeFindingsSeverity(
                analysisResult.results.L
            );

            const requiresAttention = findings.some(
                f => f.severity === 'HIGH'
            );

            const priority = this._calculatePriority(findings);

            await this._saveResults({
                analysisId: analysisResult.analysisId.S,
                findings,
                priority,
                requiresAttention,
                processedDate: new Date().toISOString()
            });

            return {
                analysisId: analysisResult.analysisId.S,
                patientId: analysisResult.metadata.M.patientId.S,
                findings,
                priority,
                requiresAttention
            };
        } catch (error) {
            console.error('Error processing results:', error);
            throw error;
        }
    }

    _analyzeFindingsSeverity(findings) {
        return findings.map(finding => {
            const severity = this._calculateFindingSeverity(
                finding.M.confidence.N,
                finding.M.condition.S
            );
            
            return {
                condition: finding.M.condition.S,
                confidence: parseFloat(finding.M.confidence.N),
                severity,
                recommendations: this._getRecommendations(
                    finding.M.condition.S,
                    severity
                )
            };
        });
    }

    _calculateFindingSeverity(confidence, condition) {
        // Implementar lógica de severidad basada en condición y confianza
        // Esto podría consultar una tabla de referencia médica
        return 'HIGH'; // Placeholder
    }

    _getRecommendations(condition, severity) {
        // Implementar lógica para recomendaciones médicas
        // Esto podría consultar una base de conocimientos médicos
        return []; // Placeholder
    }

    _calculatePriority(findings) {
        const severityScores = {
            HIGH: 3,
            MEDIUM: 2,
            LOW: 1
        };

        const totalScore = findings.reduce((acc, finding) => 
            acc + severityScores[finding.severity], 0
        );

        if (totalScore > findings.length * 2) return 'URGENT';
        if (totalScore > findings.length) return 'HIGH';
        return 'NORMAL';
    }

    async _saveResults(results) {
        await this.dynamodb.put({
            TableName: process.env.ANALYSIS_RESULTS_TABLE,
            Item: results
        }).promise();
    }

    async updateAnalysisStatus(analysisId, status) {
        await this.dynamodb.update({
            TableName: process.env.ANALYSIS_RESULTS_TABLE,
            Key: { analysisId },
            UpdateExpression: 'SET #status = :status, updatedAt = :now',
            ExpressionAttributeNames: {
                '#status': 'status'
            },
            ExpressionAttributeValues: {
                ':status': status,
                ':now': new Date().toISOString()
            }
        }).promise();
    }
}

module.exports = {
    ImageAnalyzer,
    ResultProcessor
};

3.3 API Gateway Configuration

yaml
# infrastructure/cloudformation/api-gateway.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: API Gateway configuration for Medical Image Analysis

Resources:
  MedicalApi:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
      Name: medical-image-analysis-api
      Description: API for medical image analysis
      EndpointConfiguration:
        Types:
          - REGIONAL

  ApiGatewayAuthorizer:
    Type: 'AWS::ApiGateway::Authorizer'
    Properties:
      Name: CognitoAuthorizer
      Type: COGNITO_USER_POOLS
      IdentitySource: method.request.header.Authorization
      RestApiId: !Ref MedicalApi
      ProviderARNs:
        - !Ref CognitoUserPoolArn

  AnalyzeImageResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref MedicalApi
      ParentId: !GetAtt MedicalApi.RootResourceId
      PathPart: 'analyze'

  AnalyzeImageMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      RestApiId: !Ref MedicalApi
      ResourceId: !Ref AnalyzeImageResource
      HttpMethod: POST
      AuthorizationType: COGNITO_USER_POOLS
      AuthorizerId: !Ref ApiGatewayAuthorizer
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 
          - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaArn}/invocations
          - LambdaArn: !GetAtt AnalyzeImageFunction.Arn
      MethodResponses:
        - StatusCode: 200
        - StatusCode: 400
        - StatusCode: 500

  ResultsResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref MedicalApi
      ParentId: !GetAtt MedicalApi.RootResourceId
      PathPart: 'results'

  GetResultsMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      RestApiId: !Ref MedicalApi
      ResourceId: !Ref ResultsResource
      HttpMethod: GET
      AuthorizationType: COGNITO_USER_POOLS
      AuthorizerId: !Ref ApiGatewayAuthorizer
      RequestParameters:
        method.request.querystring.analysisId: true
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 
          - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaArn}/invocations
          - LambdaArn: !GetAtt GetResultsFunction.Arn

  ApiGatewayDeployment:
    Type: 'AWS::ApiGateway::Deployment'
    DependsOn:
      - AnalyzeImageMethod
      - GetResultsMethod
    Properties:
      RestApiId: !Ref MedicalApi

  ApiGatewayStage:
    Type: 'AWS::ApiGateway::Stage'
    Properties:
      DeploymentId: !Ref ApiGatewayDeployment
      RestApiId: !Ref MedicalApi
      StageName: !Ref Environment
      MethodSettings:
        - ResourcePath: '/*'
          HttpMethod: '*'
          MetricsEnabled: true
          DataTraceEnabled: true
          LoggingLevel: INFO

Outputs:
  ApiEndpoint:
    Description: API Gateway endpoint URL
    Value: !Sub https://${MedicalApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}

Verificación de la Etapa 3

Checklist:

  • [ ] Funciones Lambda desplegadas
  • [ ] API Gateway configurado
  • [ ] Servicios de soporte funcionando
  • [ ] Endpoints probados
  • [ ] Autenticación verificada
  • [ ] Logging configurado
  • [ ] Errores manejados

Pruebas Recomendadas:

  1. Verificar análisis de imágenes
  2. Probar procesamiento de resultados
  3. Validar autenticación
  4. Comprobar endpoints

Troubleshooting Común:

  1. Errores de Lambda:

    • Verificar permisos
    • Revisar tiempo de ejecución
    • Comprobar memoria
  2. Problemas de API Gateway:

    • Verificar rutas
    • Revisar autorización
    • Comprobar integración
  3. Errores de Procesamiento:

    • Verificar formato de datos
    • Revisar integración con servicios
    • Comprobar manejo de errores
  4. Problemas de Autenticación:

    • Verificar tokens
    • Revisar roles
    • Comprobar permisos

Me disculpo por la confusión anterior. Tienes razón, debemos usar Vue como se especificó inicialmente.

Etapa 4: Frontend Base con Vue.js

Objetivos:

  1. Configurar proyecto Vue.js
  2. Implementar componentes base
  3. Configurar manejo de estado
  4. Implementar UI principal

4.1 Configuración Base de Vue

javascript
// frontend/src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

const app = createApp(App);
app.use(router);
app.use(store);
app.mount('#app');

// frontend/src/App.vue
<template>
  <div id="app">
    <nav-header />
    <div class="main-content">
      <router-view></router-view>
    </div>
    <app-footer />
  </div>
</template>

<script>
import NavHeader from './components/common/NavHeader.vue';
import AppFooter from './components/common/AppFooter.vue';

export default {
  name: 'App',
  components: {
    NavHeader,
    AppFooter
  }
};
</script>

<style>
.main-content {
  padding: 20px;
  min-height: calc(100vh - 120px);
}
</style>

// frontend/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/analysis',
    name: 'Analysis',
    component: () => import('../views/Analysis.vue')
  },
  {
    path: '/results',
    name: 'Results',
    component: () => import('../views/Results.vue')
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

// frontend/src/store/index.js
import { createStore } from 'vuex';

export default createStore({
  state: {
    currentAnalysis: null,
    analysisHistory: [],
    loading: false,
    error: null
  },
  mutations: {
    SET_CURRENT_ANALYSIS(state, analysis) {
      state.currentAnalysis = analysis;
    },
    SET_ANALYSIS_HISTORY(state, history) {
      state.analysisHistory = history;
    },
    SET_LOADING(state, loading) {
      state.loading = loading;
    },
    SET_ERROR(state, error) {
      state.error = error;
    }
  },
  actions: {
    async fetchAnalysisHistory({ commit }) {
      try {
        commit('SET_LOADING', true);
        // Implementar lógica de fetch
        commit('SET_LOADING', false);
      } catch (error) {
        commit('SET_ERROR', error);
        commit('SET_LOADING', false);
      }
    }
  },
  getters: {
    hasCurrentAnalysis: state => !!state.currentAnalysis,
    sortedHistory: state => {
      return [...state.analysisHistory].sort(
        (a, b) => new Date(b.date) - new Date(a.date)
      );
    }
  }
});

4.2 Componentes Base

vue
<!-- frontend/src/components/ImageAnalysis/ImageUploader.vue -->
<template>
  <div class="image-uploader">
    <div
      class="upload-area"
      :class="{ 'is-dragging': isDragging }"
      @drop.prevent="handleDrop"
      @dragover.prevent="isDragging = true"
      @dragleave.prevent="isDragging = false"
    >
      <div v-if="!selectedFile" class="upload-prompt">
        <div class="upload-icon">
          <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
            <polyline points="17 8 12 3 7 8"/>
            <line x1="12" y1="3" x2="12" y2="15"/>
          </svg>
        </div>
        <p>Arrastra tu imagen o haz click aquí</p>
        <input
          type="file"
          ref="fileInput"
          class="hidden-input"
          accept="image/*"
          @change="handleFileSelect"
        />
        <button class="btn-primary" @click="$refs.fileInput.click()">
          Seleccionar Archivo
        </button>
      </div>
      
      <div v-else class="file-preview">
        <img v-if="previewUrl" :src="previewUrl" alt="Preview" class="preview-image"/>
        <div class="file-info">
          <span class="file-name">{{ selectedFile.name }}</span>
          <span class="file-size">{{ formatSize(selectedFile.size) }}</span>
        </div>
        <button class="btn-secondary" @click="clearSelection">
          Eliminar
        </button>
      </div>
    </div>

    <div v-if="selectedFile" class="metadata-section">
      <h3>Información de la Imagen</h3>
      <div class="form-grid">
        <div class="form-group">
          <label>Tipo de Imagen</label>
          <select v-model="metadata.imageType">
            <option value="xray">Radiografía</option>
            <option value="mri">Resonancia Magnética</option>
            <option value="ct">Tomografía</option>
          </select>
        </div>
        
        <div class="form-group">
          <label>ID del Paciente</label>
          <input type="text" v-model="metadata.patientId">
        </div>
      </div>

      <button 
        class="btn-submit"
        :disabled="!isValid || isProcessing"
        @click="handleSubmit"
      >
        {{ isProcessing ? 'Procesando...' : 'Analizar Imagen' }}
      </button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ImageUploader',
  data() {
    return {
      selectedFile: null,
      previewUrl: null,
      isDragging: false,
      isProcessing: false,
      metadata: {
        imageType: '',
        patientId: '',
        studyDate: new Date().toISOString().split('T')[0]
      }
    };
  },
  computed: {
    isValid() {
      return this.metadata.imageType && 
             this.metadata.patientId && 
             this.selectedFile;
    }
  },
  methods: {
    handleFileSelect(event) {
      const file = event.target.files[0];
      if (file) this.processFile(file);
    },
    handleDrop(event) {
      this.isDragging = false;
      const file = event.dataTransfer.files[0];
      if (file) this.processFile(file);
    },
    processFile(file) {
      if (!file.type.startsWith('image/')) {
        this.$emit('error', 'Formato de archivo no soportado');
        return;
      }

      this.selectedFile = file;
      const reader = new FileReader();
      reader.onload = e => this.previewUrl = e.target.result;
      reader.readAsDataURL(file);
    },
    clearSelection() {
      this.selectedFile = null;
      this.previewUrl = null;
      this.metadata = {
        imageType: '',
        patientId: '',
        studyDate: new Date().toISOString().split('T')[0]
      };
    },
    formatSize(bytes) {
      const units = ['B', 'KB', 'MB', 'GB'];
      let size = bytes;
      let unitIndex = 0;
      
      while (size >= 1024 && unitIndex < units.length - 1) {
        size /= 1024;
        unitIndex++;
      }
      
      return `${size.toFixed(1)} ${units[unitIndex]}`;
    },
    async handleSubmit() {
      try {
        this.isProcessing = true;
        
        const formData = new FormData();
        formData.append('image', this.selectedFile);
        formData.append('metadata', JSON.stringify(this.metadata));
        
        this.$emit('upload', formData);
        this.clearSelection();
      } catch (error) {
        this.$emit('error', error.message);
      } finally {
        this.isProcessing = false;
      }
    }
  }
};
</script>

<style scoped>
.image-uploader {
  max-width: 800px;
  margin: 0 auto;
}

.upload-area {
  border: 2px dashed #ccc;
  border-radius: 8px;
  padding: 2rem;
  text-align: center;
  transition: all 0.3s ease;
}

.upload-area.is-dragging {
  border-color: #4CAF50;
  background-color: rgba(76, 175, 80, 0.1);
}

.hidden-input {
  display: none;
}

.preview-image {
  max-width: 300px;
  max-height: 300px;
  margin: 1rem 0;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
  margin: 1rem 0;
}

.btn-primary,
.btn-secondary,
.btn-submit {
  padding: 0.5rem 1rem;
  border-radius: 4px;
  border: none;
  cursor: pointer;
  transition: all 0.3s ease;
}

.btn-primary {
  background-color: #4CAF50;
  color: white;
}

.btn-secondary {
  background-color: #f44336;
  color: white;
}

.btn-submit {
  background-color: #2196F3;
  color: white;
  width: 100%;
  padding: 1rem;
  margin-top: 1rem;
}

.btn-submit:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}
</style>

4.3 View Components

vue
<!-- frontend/src/views/Analysis.vue -->
<template>
  <div class="analysis-page">
    <h1>Análisis de Imagen Médica</h1>
    
    <div class="analysis-container">
      <image-uploader
        @upload="handleImageUpload"
        @error="handleError"
      />
      
      <div v-if="currentAnalysis" class="results-section">
        <h2>Resultados del Análisis</h2>
        <div class="analysis-grid">
          <div class="image-section">
            <img :src="currentAnalysis.imageUrl" alt="Imagen analizada"/>
          </div>
          
          <div class="findings-section">
            <h3>Hallazgos</h3>
            <ul class="findings-list">
              <li 
                v-for="finding in currentAnalysis.findings"
                :key="finding.id"
                :class="{ 'high-confidence': finding.confidence > 90 }"
              >
                <span class="finding-label">{{ finding.label }}</span>
                <span class="finding-confidence">
                  {{ finding.confidence.toFixed(1) }}%
                </span>
              </li>
            </ul>
          </div>
        </div>
        
        <div class="actions">
          <button @click="downloadReport" class="btn-primary">
            Descargar Reporte
          </button>
          <button @click="saveAnalysis" class="btn-secondary">
            Guardar Análisis
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import ImageUploader from '../components/ImageAnalysis/ImageUploader.vue';

export default {
  name: 'Analysis',
  components: {
    ImageUploader
  },
  data() {
    return {
      currentAnalysis: null,
      error: null,
      loading: false
    };
  },
  methods: {
    async handleImageUpload(formData) {
      try {
        this.loading = true;
        this.error = null;
        
        // Aquí iría la llamada al backend
        const response = await fetch('/api/analyze', {
          method: 'POST',
          body: formData
        });
        
        if (!response.ok) {
          throw new Error('Error en el análisis');
        }
        
        const result = await response.json();
        this.currentAnalysis = result;
      } catch (error) {
        this.handleError(error.message);
      } finally {
        this.loading = false;
      }
    },
    handleError(message) {
      this.error = message;
      // Implementar lógica de notificación de error
    },
    async downloadReport() {
      // Implementar lógica de descarga de reporte
    },
    async saveAnalysis() {
      // Implementar lógica de guardado
    }
  }
};
</script>

<style scoped>
.analysis-page {
  padding: 2rem;
}

.analysis-container {
  max-width: 1200px;
  margin: 0 auto;
}

.analysis-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2rem;
  margin-top: 2rem;
}

.findings-list {
  list-style: none;
  padding: 0;
}

.findings-list li {
  display: flex;
  justify-content: space-between;
  padding: 0.5rem;
  border-bottom: 1px solid #eee;
}

.high-confidence {
  background-color: rgba(76, 175, 80, 0.1);
}

.actions {
  display: flex;
  gap: 1rem;
  margin-top: 2rem;
}

.btn-primary,
.btn-secondary {
  padding: 0.5rem 1rem;
  border-radius: 4px;
  border: none;
  cursor: pointer;
  transition: all 0.3s ease;
}

.btn-primary {
  background-color: #4CAF50;
  color: white;
}

.btn-secondary {
  background-color: #2196F3;
  color: white;
}
</style>

Etapa 5: Seguridad y Control de Acceso

Objetivos:

  1. Implementar manejo de sesiones
  2. Configurar roles y permisos
  3. Establecer reglas de acceso
  4. Implementar auditoría

5.1 Manejo de Sesiones

javascript
// frontend/src/services/auth.js
import { API } from '@aws-amplify/api';
import { Auth } from '@aws-amplify/auth';

export const AuthService = {
  async signIn(username, password) {
    try {
      const user = await Auth.signIn(username, password);
      return {
        user,
        token: (await Auth.currentSession()).getIdToken().getJwtToken()
      };
    } catch (error) {
      console.error('Error signing in:', error);
      throw error;
    }
  },

  async signOut() {
    try {
      await Auth.signOut();
    } catch (error) {
      console.error('Error signing out:', error);
      throw error;
    }
  },

  async getCurrentSession() {
    try {
      const session = await Auth.currentSession();
      return session;
    } catch (error) {
      console.error('Error getting session:', error);
      return null;
    }
  },

  async checkUserRole() {
    try {
      const user = await Auth.currentAuthenticatedUser();
      const { groups = [] } = user.signInUserSession.accessToken.payload;
      return {
        isAdmin: groups.includes('Administrators'),
        isDoctor: groups.includes('Doctors'),
        isRadiologist: groups.includes('Radiologists')
      };
    } catch (error) {
      console.error('Error checking user role:', error);
      return {
        isAdmin: false,
        isDoctor: false,
        isRadiologist: false
      };
    }
  }
};

// frontend/src/middleware/auth.js
export const authMiddleware = {
  async beforeEach(to, from, next) {
    const publicPages = ['/login', '/register', '/forgot-password'];
    const authRequired = !publicPages.includes(to.path);
    const auth = await AuthService.getCurrentSession();

    if (authRequired && !auth) {
      return next('/login');
    }

    if (to.meta.roles) {
      const userRoles = await AuthService.checkUserRole();
      const hasRequiredRole = to.meta.roles.some(role => 
        userRoles[`is${role}`]
      );

      if (!hasRequiredRole) {
        return next('/unauthorized');
      }
    }

    next();
  }
};

5.2 Control de Acceso Basado en Roles

javascript
// frontend/src/directives/access-control.js
export const accessControl = {
  mounted(el, binding) {
    const { roles, operation } = binding.value;
    const userRoles = JSON.parse(localStorage.getItem('userRoles') || '{}');
    
    const hasPermission = roles.some(role => 
      userRoles[`is${role}`] && 
      userRoles.permissions?.includes(operation)
    );

    if (!hasPermission) {
      el.style.display = 'none';
    }
  }
};

// frontend/src/config/permissions.js
export const PERMISSIONS = {
  ANALYZE_IMAGE: 'analyze_image',
  VIEW_REPORTS: 'view_reports',
  MANAGE_USERS: 'manage_users',
  EXPORT_DATA: 'export_data',
  MODIFY_SETTINGS: 'modify_settings'
};

export const ROLE_PERMISSIONS = {
  Administrators: [
    PERMISSIONS.ANALYZE_IMAGE,
    PERMISSIONS.VIEW_REPORTS,
    PERMISSIONS.MANAGE_USERS,
    PERMISSIONS.EXPORT_DATA,
    PERMISSIONS.MODIFY_SETTINGS
  ],
  Doctors: [
    PERMISSIONS.ANALYZE_IMAGE,
    PERMISSIONS.VIEW_REPORTS,
    PERMISSIONS.EXPORT_DATA
  ],
  Radiologists: [
    PERMISSIONS.ANALYZE_IMAGE,
    PERMISSIONS.VIEW_REPORTS
  ]
};

// frontend/src/components/common/AccessControl.vue
<template>
  <div v-if="hasAccess">
    <slot></slot>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue';
import { AuthService } from '@/services/auth';
import { ROLE_PERMISSIONS } from '@/config/permissions';

export default {
  name: 'AccessControl',
  props: {
    roles: {
      type: Array,
      required: true
    },
    operation: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const hasAccess = ref(false);

    onMounted(async () => {
      const userRoles = await AuthService.checkUserRole();
      hasAccess.value = props.roles.some(role => 
        userRoles[`is${role}`] && 
        ROLE_PERMISSIONS[role]?.includes(props.operation)
      );
    });

    return {
      hasAccess
    };
  }
};
</script>

5.3 Auditoría y Logging

javascript
// frontend/src/services/audit.js
import { API } from '@aws-amplify/api';

export const AuditService = {
  async logAction(action, details) {
    try {
      await API.post('audit', '/log', {
        body: {
          action,
          details,
          timestamp: new Date().toISOString(),
          userAgent: navigator.userAgent
        }
      });
    } catch (error) {
      console.error('Error logging action:', error);
    }
  },

  async getAuditLogs(filters = {}) {
    try {
      const params = new URLSearchParams(filters).toString();
      const response = await API.get('audit', `/logs?${params}`);
      return response.data;
    } catch (error) {
      console.error('Error getting audit logs:', error);
      throw error;
    }
  }
};

// frontend/src/components/admin/AuditLog.vue
<template>
  <div class="audit-log">
    <h2>Registro de Auditoría</h2>

    <div class="filters">
      <div class="filter-group">
        <label>Fecha Inicio:</label>
        <input 
          type="date" 
          v-model="filters.startDate"
          @change="loadLogs"
        />
      </div>

      <div class="filter-group">
        <label>Fecha Fin:</label>
        <input 
          type="date" 
          v-model="filters.endDate"
          @change="loadLogs"
        />
      </div>

      <div class="filter-group">
        <label>Acción:</label>
        <select 
          v-model="filters.action"
          @change="loadLogs"
        >
          <option value="">Todas</option>
          <option value="IMAGE_ANALYSIS">Análisis de Imagen</option>
          <option value="REPORT_GENERATION">Generación de Reporte</option>
          <option value="USER_LOGIN">Inicio de Sesión</option>
          <option value="SETTINGS_CHANGE">Cambio de Configuración</option>
        </select>
      </div>
    </div>

    <div class="log-table">
      <table>
        <thead>
          <tr>
            <th>Fecha</th>
            <th>Usuario</th>
            <th>Acción</th>
            <th>Detalles</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="log in logs" :key="log.id">
            <td>{{ formatDate(log.timestamp) }}</td>
            <td>{{ log.user }}</td>
            <td>{{ formatAction(log.action) }}</td>
            <td>
              <button 
                @click="showDetails(log)"
                class="btn-details"
              >
                Ver Detalles
              </button>
            </td>
          </tr>
        </tbody>
      </table>

      <div class="pagination">
        <button 
          :disabled="currentPage === 1"
          @click="changePage(-1)"
        >
          Anterior
        </button>
        <span>Página {{ currentPage }} de {{ totalPages }}</span>
        <button 
          :disabled="currentPage === totalPages"
          @click="changePage(1)"
        >
          Siguiente
        </button>
      </div>
    </div>

    <!-- Modal de Detalles -->
    <div v-if="selectedLog" class="modal">
      <div class="modal-content">
        <h3>Detalles de la Acción</h3>
        <div class="details-grid">
          <div class="detail-item">
            <strong>ID:</strong>
            <span>{{ selectedLog.id }}</span>
          </div>
          <div class="detail-item">
            <strong>Usuario:</strong>
            <span>{{ selectedLog.user }}</span>
          </div>
          <div class="detail-item">
            <strong>Acción:</strong>
            <span>{{ formatAction(selectedLog.action) }}</span>
          </div>
          <div class="detail-item">
            <strong>Fecha:</strong>
            <span>{{ formatDate(selectedLog.timestamp) }}</span>
          </div>
          <div class="detail-item">
            <strong>IP:</strong>
            <span>{{ selectedLog.ip }}</span>
          </div>
          <div class="detail-item">
            <strong>User Agent:</strong>
            <span>{{ selectedLog.userAgent }}</span>
          </div>
        </div>
        <pre class="details-json">{{ JSON.stringify(selectedLog.details, null, 2) }}</pre>
        <button @click="selectedLog = null" class="btn-close">
          Cerrar
        </button>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue';
import { AuditService } from '@/services/audit';

export default {
  name: 'AuditLog',
  setup() {
    const logs = ref([]);
    const selectedLog = ref(null);
    const currentPage = ref(1);
    const totalPages = ref(1);
    const filters = ref({
      startDate: '',
      endDate: '',
      action: ''
    });

    const loadLogs = async () => {
      try {
        const response = await AuditService.getAuditLogs({
          ...filters.value,
          page: currentPage.value
        });
        logs.value = response.logs;
        totalPages.value = response.totalPages;
      } catch (error) {
        console.error('Error loading logs:', error);
      }
    };

    const changePage = (delta) => {
      currentPage.value += delta;
      loadLogs();
    };

    const formatDate = (timestamp) => {
      return new Date(timestamp).toLocaleString();
    };

    const formatAction = (action) => {
      return action.replace(/_/g, ' ').toLowerCase();
    };

    const showDetails = (log) => {
      selectedLog.value = log;
    };

    onMounted(() => {
      loadLogs();
    });

    return {
      logs,
      selectedLog,
      currentPage,
      totalPages,
      filters,
      changePage,
      formatDate,
      formatAction,
      showDetails,
      loadLogs
    };
  }
};
</script>

<style scoped>
.audit-log {
  padding: 1rem;
}

.filters {
  display: flex;
  gap: 1rem;
  margin-bottom: 1rem;
}

.filter-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.log-table {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th, td {
  padding: 0.75rem;
  text-align: left;
  border-bottom: 1px solid #eee;
}

.btn-details {
  background-color: #4CAF50;
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
}

.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-content {
  background-color: white;
  padding: 2rem;
  border-radius: 8px;
  max-width: 800px;
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
}

.details-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
  margin: 1rem 0;
}

.details-json {
  background-color: #f5f5f5;
  padding: 1rem;
  border-radius: 4px;
  overflow-x: auto;
}

.pagination {
  display: flex;
  justify-content: center;
  gap: 1rem;
  margin-top: 1rem;
}
</style>