diff --git a/MisplaceAI/admin_app/serializers.py b/MisplaceAI/admin_app/serializers.py index 3472a17c71b60745281b4b7116c57a39540d2c4b..49eacaeeb8d19c298ea7c59c6633c19e7320fe06 100644 --- a/MisplaceAI/admin_app/serializers.py +++ b/MisplaceAI/admin_app/serializers.py @@ -1,9 +1,20 @@ # MisplaceAI/admin_app/serializers.py +# This file defines serializers for the admin_app. +# Serializers are used to convert complex data types such as querysets and model instances +# into native Python datatypes that can then be easily rendered into JSON, XML, or other content types. +# They also provide deserialization, allowing parsed data to be converted back into complex types, +# after first validating the incoming data. + from rest_framework import serializers from django.contrib.auth.models import User +# Serializer for the User model class UserSerializer(serializers.ModelSerializer): + """ + Serializer for the User model. + """ class Meta: model = User + # Specify the fields to be included in the serialized output fields = ['id', 'username', 'email', 'first_name', 'last_name', 'date_joined', 'last_login', 'is_active', 'is_staff', 'is_superuser', 'groups'] diff --git a/MisplaceAI/admin_app/urls.py b/MisplaceAI/admin_app/urls.py index 3648c708b3725ea9098d302cd4e882a9d0ccdeb5..bd0fbb35e62ec3866e3202ba0415795f590b56c8 100644 --- a/MisplaceAI/admin_app/urls.py +++ b/MisplaceAI/admin_app/urls.py @@ -1,4 +1,8 @@ # MisplaceAI/admin_app/urls.py + +# This file defines the URL patterns for the admin_app. +# It includes routes for the admin dashboard, user management, and user status updates. + from django.urls import path from .views import ( admin_dashboard_view, @@ -8,10 +12,20 @@ from .views import ( admin_delete_user_view ) +# Define the URL patterns for the admin_app urlpatterns = [ + # Route for the admin dashboard path('dashboard/', admin_dashboard_view, name='admin_dashboard'), + + # Route for listing all users path('users/', admin_users_view, name='admin_users'), + + # Route for deactivating a specific user path('users/deactivate/<int:user_id>/', admin_deactivate_user_view, name='admin_deactivate_user'), + + # Route for activating a specific user path('users/activate/<int:user_id>/', admin_activate_user_view, name='admin_activate_user'), + + # Route for deleting a specific user path('users/delete/<int:user_id>/', admin_delete_user_view, name='admin_delete_user'), ] diff --git a/MisplaceAI/admin_app/views.py b/MisplaceAI/admin_app/views.py index 46dd4a4bc742dc62758acd2925e06dbb19babfed..b1bb4f31bfb244f88aa8c4a5473b6ad02f834d98 100644 --- a/MisplaceAI/admin_app/views.py +++ b/MisplaceAI/admin_app/views.py @@ -1,4 +1,9 @@ # MisplaceAI/admin_app/views.py + +# This file defines the views for the admin_app. +# Views are used to handle the logic for different endpoints, providing appropriate responses +# based on the request and the business logic. + from django.shortcuts import get_object_or_404 from rest_framework import generics, status from rest_framework.response import Response @@ -11,18 +16,22 @@ from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required from .serializers import UserSerializer - @api_view(['GET']) @permission_classes([IsAuthenticated]) def admin_dashboard_view(request): + """ + View for the admin dashboard. Only accessible by superusers. + """ if not request.user.is_superuser: return Response({'error': 'You do not have the necessary permissions to access this page.'}, status=status.HTTP_403_FORBIDDEN) return Response({'message': 'Welcome to the admin dashboard.'}, status=status.HTTP_200_OK) - @api_view(['GET']) @permission_classes([IsAuthenticated]) def admin_users_view(request): + """ + View to list all users. Only accessible by staff users. + """ if not request.user.is_staff: return Response({'error': 'You do not have the necessary permissions to access this page.'}, status=status.HTTP_403_FORBIDDEN) @@ -30,10 +39,12 @@ def admin_users_view(request): serializer = UserSerializer(users, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - @api_view(['POST']) @permission_classes([IsAuthenticated]) def admin_deactivate_user_view(request, user_id): + """ + View to deactivate a user. Only accessible by staff users. + """ if not request.user.is_staff: return Response({'error': 'You do not have the necessary permissions to access this page.'}, status=status.HTTP_403_FORBIDDEN) @@ -42,10 +53,12 @@ def admin_deactivate_user_view(request, user_id): user.save() return Response({'message': f'User {user.username} has been deactivated.'}, status=status.HTTP_200_OK) - @api_view(['POST']) @permission_classes([IsAuthenticated]) def admin_activate_user_view(request, user_id): + """ + View to activate a user. Only accessible by staff users. + """ if not request.user.is_staff: return Response({'error': 'You do not have the necessary permissions to access this page.'}, status=status.HTTP_403_FORBIDDEN) @@ -54,10 +67,12 @@ def admin_activate_user_view(request, user_id): user.save() return Response({'message': f'User {user.username} has been activated.'}, status=status.HTTP_200_OK) - @api_view(['DELETE']) @permission_classes([IsAuthenticated]) def admin_delete_user_view(request, user_id): + """ + View to delete a user. Only accessible by staff users. + """ if not request.user.is_staff: return Response({'error': 'You do not have the necessary permissions to access this page.'}, status=status.HTTP_403_FORBIDDEN) diff --git a/MisplaceAI/authentication/serializers.py b/MisplaceAI/authentication/serializers.py index d73b408511c083bda54efc58073f53321ca938b7..8cdde2a1aae2840012892ebd9732c9163a865872 100644 --- a/MisplaceAI/authentication/serializers.py +++ b/MisplaceAI/authentication/serializers.py @@ -1,19 +1,32 @@ # MisplaceAI/authentication/serializers.py +# This file defines serializers for the authentication app. +# Serializers are used to convert complex data types such as querysets and model instances +# into native Python datatypes that can then be easily rendered into JSON, XML, or other content types. +# They also provide deserialization, allowing parsed data to be converted back into complex types, +# after first validating the incoming data. from django.contrib.auth import get_user_model from rest_framework import serializers from django.contrib.auth import authenticate +# Get the user model UserModel = get_user_model() class RegisterSerializer(serializers.ModelSerializer): + """ + Serializer for user registration. + """ class Meta: model = UserModel + # Specify the fields to be included in the serialized output fields = ('username', 'email', 'password') extra_kwargs = {'password': {'write_only': True}} def create(self, validated_data): + """ + Create a new user with the validated data. + """ user = UserModel.objects.create_user( username=validated_data['username'], email=validated_data['email'], @@ -22,14 +35,21 @@ class RegisterSerializer(serializers.ModelSerializer): return user class LoginSerializer(serializers.Serializer): + """ + Serializer for user login. + """ username = serializers.CharField() password = serializers.CharField(style={'input_type': 'password'}, trim_whitespace=False) def validate(self, data): + """ + Validate the login data. + """ username = data.get('username') password = data.get('password') if username and password: + # Authenticate the user user = authenticate(request=self.context.get('request'), username=username, password=password) if not user: diff --git a/MisplaceAI/authentication/urls.py b/MisplaceAI/authentication/urls.py index 9f611ab4f098b3be27e4b264918bc13a2398343c..36637db463addc4be77e249cb5a725522d5e4d54 100644 --- a/MisplaceAI/authentication/urls.py +++ b/MisplaceAI/authentication/urls.py @@ -1,13 +1,29 @@ # MisplaceAI/authentication/urls.py + +# This file defines the URL patterns for the authentication app. +# It includes routes for user registration, login, and logout. + from django.urls import path from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView from .views import RegisterView, LoginView, AdminLoginView, LogoutView +# Define the URL patterns for the authentication app urlpatterns = [ + # Route for user registration path('register/', RegisterView.as_view(), name='register'), + + # Route for user login path('login/', LoginView.as_view(), name='login'), + + # Route for admin user login path('admin/login/', AdminLoginView.as_view(), name='admin_login'), + + # Route for obtaining JWT tokens path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + + # Route for refreshing JWT tokens path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + + # Route for user logout path('logout/', LogoutView.as_view(), name='logout'), ] diff --git a/MisplaceAI/authentication/views.py b/MisplaceAI/authentication/views.py index 1413323f7e0b263d0898e95ec86b3731b6218f43..554a474ec1c978e52622ea0f339211cbb002a540 100644 --- a/MisplaceAI/authentication/views.py +++ b/MisplaceAI/authentication/views.py @@ -1,5 +1,9 @@ # MisplaceAI/authentication/views.py +# This file defines the views for the authentication app. +# Views handle the logic for different endpoints, providing appropriate responses +# based on the request and the business logic. This includes user registration, login, and logout. + from django.contrib.auth import authenticate from rest_framework import generics, status from rest_framework.response import Response @@ -9,20 +13,31 @@ from .serializers import RegisterSerializer, LoginSerializer from django.contrib.auth.models import User class RegisterView(generics.CreateAPIView): - queryset = User.objects.all() - serializer_class = RegisterSerializer + """ + View for user registration. + """ + queryset = User.objects.all() # Queryset of all users + serializer_class = RegisterSerializer # Serializer class for user registration class LoginView(APIView): + """ + View for user login. + """ def post(self, request, *args, **kwargs): + # Deserialize the incoming data serializer = LoginSerializer(data=request.data) + # Validate the data serializer.is_valid(raise_exception=True) + # Authenticate the user user = authenticate( username=serializer.validated_data['username'], password=serializer.validated_data['password'] ) if user: + # Prevent admin users from logging in here if user.is_superuser: return Response({'error': 'Admins cannot log in here'}, status=status.HTTP_403_FORBIDDEN) + # Generate JWT tokens for the authenticated user refresh = RefreshToken.for_user(user) return Response({ 'refresh': str(refresh), @@ -31,14 +46,21 @@ class LoginView(APIView): return Response({'error': 'Invalid credentials'}, status=status.HTTP_400_BAD_REQUEST) class AdminLoginView(APIView): + """ + View for admin user login. + """ def post(self, request, *args, **kwargs): + # Deserialize the incoming data serializer = LoginSerializer(data=request.data) + # Validate the data serializer.is_valid(raise_exception=True) + # Authenticate the user user = authenticate( username=serializer.validated_data['username'], password=serializer.validated_data['password'] ) if user and user.is_superuser: + # Generate JWT tokens for the authenticated admin user refresh = RefreshToken.for_user(user) return Response({ 'refresh': str(refresh), @@ -49,9 +71,14 @@ class AdminLoginView(APIView): return Response({'error': 'Invalid username or password or you do not have the necessary permissions to access this page.'}, status=status.HTTP_400_BAD_REQUEST) class LogoutView(APIView): + """ + View for user logout. + """ def post(self, request): try: + # Extract the refresh token from the request refresh_token = request.data["refresh_token"] + # Blacklist the refresh token token = RefreshToken(refresh_token) token.blacklist() return Response(status=status.HTTP_205_RESET_CONTENT) diff --git a/MisplaceAI/item_detector/utils.py b/MisplaceAI/item_detector/utils.py index 279853f00f75695452839e014f1815a5921b9567..b0dc2752c93e0dda237cacd1997a1b23c6d41cd8 100644 --- a/MisplaceAI/item_detector/utils.py +++ b/MisplaceAI/item_detector/utils.py @@ -1,4 +1,8 @@ # MisplaceAI/item_detector/utils.py + +# This file contains utility functions for loading models, creating category indices, +# loading images, and running inference for object detection using TensorFlow. + import os import glob import numpy as np @@ -7,38 +11,75 @@ from PIL import Image from io import BytesIO from object_detection.utils import ops as utils_ops, label_map_util -# Patch tf1 functions into `utils.ops` for compatibility +# Patch tf1 functions into `utils.ops` for compatibility with TensorFlow 2 utils_ops.tf = tf.compat.v1 # Patch the location of gfile for TensorFlow file operations tf.gfile = tf.io.gfile def load_model(model_path): - """Load the object detection model from the specified path.""" + """ + Load the object detection model from the specified path. + + Args: + model_path (str): Path to the saved model directory. + + Returns: + model: Loaded TensorFlow model. + """ model = tf.saved_model.load(model_path) return model def create_category_index_from_labelmap(label_map_path, use_display_name=True): - """Create a category index from a label map file.""" + """ + Create a category index from a label map file. + + Args: + label_map_path (str): Path to the label map file. + use_display_name (bool): Whether to use display names in the category index. + + Returns: + dict: Category index mapping class IDs to class names. + """ return label_map_util.create_category_index_from_labelmap(label_map_path, use_display_name=use_display_name) def load_image_into_numpy_array(image): - """Load an image from file or PIL image into a numpy array.""" - if isinstance(image, str): # image is a path + """ + Load an image from file or PIL image into a numpy array. + + Args: + image (str or PIL.Image.Image): Path to the image file or PIL image object. + + Returns: + np.ndarray: Image as a numpy array. + """ + if isinstance(image, str): # If the image is a file path img_data = tf.io.gfile.GFile(image, "rb").read() image = Image.open(BytesIO(img_data)) (im_width, im_height) = image.size return np.array(image.getdata()).reshape((im_height, im_width, 3)).astype(np.uint8) def run_inference(model, category_index, image): - """Run inference on a single image.""" + """ + Run inference on a single image. + + Args: + model: Loaded TensorFlow model. + category_index (dict): Category index mapping class IDs to class names. + image (str or PIL.Image.Image): Path to the image file or PIL image object. + + Returns: + list: List of detected objects with their bounding box coordinates and class names. + """ + # Convert the image to a numpy array image_np = load_image_into_numpy_array(image) + # Run inference on the image output_dict = run_inference_for_single_image(model, image_np) dataLocation = [] print(f"Detected objects in {image if isinstance(image, str) else 'provided image'}:") for i, box in enumerate(output_dict["detection_boxes"]): - if output_dict["detection_scores"][i] > 0.5: + if output_dict["detection_scores"][i] > 0.5: # Only consider detections with a score above 0.5 ymin, xmin, ymax, xmax = box class_id = output_dict["detection_classes"][i] class_name = category_index[class_id]["name"] if class_id in category_index else "N/A" @@ -55,7 +96,16 @@ def run_inference(model, category_index, image): return dataLocation def run_inference_for_single_image(model, image): - """Run inference on a single image and return the output dictionary.""" + """ + Run inference on a single image and return the output dictionary. + + Args: + model: Loaded TensorFlow model. + image (np.ndarray): Image as a numpy array. + + Returns: + dict: Output dictionary with detection results. + """ # Convert image to tensor and add batch dimension input_tensor = tf.convert_to_tensor(image) input_tensor = input_tensor[tf.newaxis, ...] diff --git a/MisplaceAI/process_misplaced_manager/forms.py b/MisplaceAI/process_misplaced_manager/forms.py index 0dfcd476b297f1acb0deeaa1a7d3a41420b379aa..13cc892bc6eff41a9b0ba9710ac883e15b345e51 100644 --- a/MisplaceAI/process_misplaced_manager/forms.py +++ b/MisplaceAI/process_misplaced_manager/forms.py @@ -1,14 +1,30 @@ # MisplaceAI/process_misplaced_manager/forms.py +# This file defines the forms for the process_misplaced_manager app. +# Forms are used to handle user input and validate the data before it is saved to the database. +# In this file, we define forms for uploading images and videos. + from django import forms from .models import UploadedImage, UploadedVideo +# Form for uploading images class ImageUploadForm(forms.ModelForm): + """ + A form for uploading images. + """ class Meta: + # Specify the model associated with the form model = UploadedImage + # Specify the fields to be included in the form fields = ['image'] +# Form for uploading videos class VideoUploadForm(forms.ModelForm): + """ + A form for uploading videos. + """ class Meta: + # Specify the model associated with the form model = UploadedVideo + # Specify the fields to be included in the form fields = ['video'] diff --git a/MisplaceAI/process_misplaced_manager/models.py b/MisplaceAI/process_misplaced_manager/models.py index e107bc62b1cc544fd61b61f764a5fec27a1cdf7d..3deb13adec8719f4b8a9aa5fc9f24a729204ee24 100644 --- a/MisplaceAI/process_misplaced_manager/models.py +++ b/MisplaceAI/process_misplaced_manager/models.py @@ -1,52 +1,97 @@ # MisplaceAI/process_misplaced_manager/models.py +# This file defines the models for the process_misplaced_manager app. +# Models are used to define the structure of the database tables and the relationships between them. +# Each model maps to a single table in the database. + from django.db import models from django.contrib.auth.models import User from django.utils import timezone class UploadedImage(models.Model): + """ + Model to represent an uploaded image. + """ + # Field to store the image file, which will be uploaded to the 'images/' directory image = models.ImageField(upload_to='images/') + # Field to store the timestamp when the image was uploaded, set automatically when the image is created uploaded_at = models.DateTimeField(auto_now_add=True) + # ForeignKey field to create a relationship with the User model, indicating the user who uploaded the image user = models.ForeignKey(User, on_delete=models.CASCADE) - class UserVideoFramePreference(models.Model): + """ + Model to represent user preferences for video frame processing. + """ + # One-to-one relationship with the User model user = models.OneToOneField(User, on_delete=models.CASCADE) + # Integer field to store the frame interval preference frame_interval = models.IntegerField(default=1) + # Integer field to store the frame delay preference frame_delay = models.IntegerField(default=1) def __str__(self): + """ + String representation of the UserVideoFramePreference model. + """ return f'{self.user.username} - Frame Interval: {self.frame_interval}, Frame Delay: {self.frame_delay}' class UploadedVideo(models.Model): + """ + Model to represent an uploaded video. + """ + # Field to store the video file, which will be uploaded to the 'videos/' directory video = models.FileField(upload_to='videos/') + # Field to store the timestamp when the video was uploaded, set automatically when the video is created uploaded_at = models.DateTimeField(auto_now_add=True) + # ForeignKey field to create a relationship with the User model, indicating the user who uploaded the video user = models.ForeignKey(User, on_delete=models.CASCADE) + # ForeignKey field to create a relationship with the UserVideoFramePreference model user_video_frame_preference = models.ForeignKey(UserVideoFramePreference, on_delete=models.SET_NULL, null=True) - class DailyDetectionLimit(models.Model): + """ + Model to represent daily detection limits for a user. + """ + # One-to-one relationship with the User model user = models.OneToOneField(User, on_delete=models.CASCADE) + # Integer field to store the count of image detections for the current day image_detection_count = models.IntegerField(default=0) + # Integer field to store the count of video detections for the current day video_detection_count = models.IntegerField(default=0) + # Date field to store the date when the detection counts were last reset last_reset = models.DateField(auto_now_add=True) def reset_limits(self): + """ + Reset the detection limits if the day has changed. + """ + # Check if the last reset date is earlier than today if self.last_reset < timezone.now().date(): + # Reset the detection counts to zero self.image_detection_count = 0 self.video_detection_count = 0 + # Update the last reset date to today self.last_reset = timezone.now().date() self.save() def __str__(self): + """ + String representation of the DailyDetectionLimit model. + """ return f'{self.user.username} - Images: {self.image_detection_count}, Videos: {self.video_detection_count}' - class DetectionLimitSetting(models.Model): - daily_image_limit = models.IntegerField(default=10) # Default limit for images - daily_video_limit = models.IntegerField(default=5) # Default limit for videos + """ + Model to represent global detection limit settings. + """ + # Integer field to store the daily limit for image detections + daily_image_limit = models.IntegerField(default=10) + # Integer field to store the daily limit for video detections + daily_video_limit = models.IntegerField(default=5) def __str__(self): + """ + String representation of the DetectionLimitSetting model. + """ return f'Daily Limits - Images: {self.daily_image_limit}, Videos: {self.daily_video_limit}' - - diff --git a/MisplaceAI/process_misplaced_manager/serializers.py b/MisplaceAI/process_misplaced_manager/serializers.py index 8324cc74854117bfa45d337662adeed52388d353..b61830380fd9928384afc1b3e87fe2e6242511ed 100644 --- a/MisplaceAI/process_misplaced_manager/serializers.py +++ b/MisplaceAI/process_misplaced_manager/serializers.py @@ -1,21 +1,42 @@ # MisplaceAI/process_misplaced_manager/serializers.py +# This file defines the serializers for the process_misplaced_manager app. +# Serializers convert complex data types such as querysets and model instances +# into native Python datatypes that can then be easily rendered into JSON, XML, or other content types. +# They also provide deserialization, allowing parsed data to be converted back into complex types, +# after first validating the incoming data. + from rest_framework import serializers from .models import UploadedImage, UploadedVideo, UserVideoFramePreference +# Serializer for the UserVideoFramePreference model class UserVideoFramePreferenceSerializer(serializers.ModelSerializer): + """ + Serializer for UserVideoFramePreference model, which handles user preferences for video frame processing. + """ class Meta: model = UserVideoFramePreference + # Specify the fields that should be included in the serialized output fields = ['frame_interval', 'frame_delay'] +# Serializer for the UploadedImage model class UploadedImageSerializer(serializers.ModelSerializer): + """ + Serializer for UploadedImage model, which handles the metadata of uploaded images. + """ class Meta: model = UploadedImage + # Specify the fields that should be included in the serialized output fields = ['id', 'image', 'uploaded_at', 'user'] + # 'uploaded_at' is read-only and will be set automatically when the image is saved read_only_fields = ['uploaded_at'] - +# Serializer for the UploadedVideo model class UploadedVideoSerializer(serializers.ModelSerializer): + """ + Serializer for UploadedVideo model, which handles the metadata of uploaded videos. + """ class Meta: model = UploadedVideo + # Specify the fields that should be included in the serialized output fields = ['id', 'video', 'uploaded_at', 'user', 'user_video_frame_preference'] diff --git a/MisplaceAI/process_misplaced_manager/urls.py b/MisplaceAI/process_misplaced_manager/urls.py index 4b6d21ed4882fc5f6612268d299e1cc97d8e2d3a..8f429652f5788940644032dcbcf54cf2585247cb 100644 --- a/MisplaceAI/process_misplaced_manager/urls.py +++ b/MisplaceAI/process_misplaced_manager/urls.py @@ -1,4 +1,9 @@ # MisplaceAI/process_misplaced_manager/urls.py + +# This file defines the URL patterns for the process_misplaced_manager app. +# It includes routes for uploading images and videos, running object detection, +# displaying results, downloading and deleting media files, and managing user detection limits. + from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import ( @@ -17,24 +22,53 @@ from .views import ( increment_detection ) +# Initialize the default router router = DefaultRouter() +# Register viewsets for uploaded images and videos router.register(r'images', UploadedImageViewSet) router.register(r'videos', UploadedVideoViewSet) +# Define the app name for namespacing URL names app_name = 'process_misplaced_manager' +# Define the URL patterns urlpatterns = [ + # Include the router's URLs path('', include(router.urls)), + + # Route for normal image detection path('normal-detection/', normal_detection, name='normal_detection'), + + # Route for uploading videos path('upload-video/', upload_video, name='upload_video'), + + # Route for displaying results of a specific video path('video-results/<int:video_id>/', display_video_results, name='display_video_results'), + + # Route for displaying results of a specific image path('display-results/<int:image_id>/', display_results, name='display_results'), + + # Route for downloading an image path('download/<path:file_path>/', download_image, name='download_image'), + + # Route for deleting a specific image by name path('delete-image/<str:image_name>/', delete_image, name='delete_image_by_name'), + + # Route for deleting a specific video by name path('delete-video/<str:video_name>/', delete_video, name='delete_video'), + + # Route for downloading a media file (image or video) path('download_video/<str:file_path>/', download_media, name='download_media'), + + # Route for getting daily detection limits path('daily-limits/', get_daily_limits, name='get_daily_limits'), + + # Route for setting daily detection limits (admin only) path('set-daily-limits/', set_daily_limits, name='set_daily_limits'), + + # Route for checking the remaining daily limit for the user path('check-daily-limit/', check_daily_limit, name='check_daily_limit'), + + # Route for incrementing the detection count for the user path('increment-detection/', increment_detection, name='increment_detection'), ] diff --git a/MisplaceAI/process_misplaced_manager/utils.py b/MisplaceAI/process_misplaced_manager/utils.py index dc1c3d450ca85ea3704b6c83dbd0496fa3b34664..52d2efa6f9bbd8871475c6d2e4268af5b3d23bef 100644 --- a/MisplaceAI/process_misplaced_manager/utils.py +++ b/MisplaceAI/process_misplaced_manager/utils.py @@ -1,5 +1,9 @@ # MisplaceAI/process_misplaced_manager/utils.py +# This file contains utility functions used for various purposes in the MisplaceAI project. +# These include functions to increment the detection count for a user, correct image orientation, +# and process videos to detect and visualize misplaced objects. + from django.utils import timezone from .models import DailyDetectionLimit from PIL import Image, ExifTags @@ -13,62 +17,116 @@ import numpy as np import os def increment_detection_count(user, detection_type): + """ + Increment the detection count for a user based on the detection type (image or video). + + Args: + user: The user for whom the detection count needs to be incremented. + detection_type: The type of detection ('image' or 'video') to be incremented. + """ + # Retrieve the daily detection limit object for the user detection_limit = DailyDetectionLimit.objects.get(user=user) + + # Increment the appropriate detection count based on the detection type if detection_type == 'image': detection_limit.image_detection_count += 1 elif detection_type == 'video': detection_limit.video_detection_count += 1 + + # Save the updated detection limit object detection_limit.save() def correct_image_orientation(image_path): + """ + Correct the orientation of an image based on its EXIF data. + + Args: + image_path: The path to the image file that needs orientation correction. + """ try: + # Open the image file image = Image.open(image_path) + + # Get the orientation tag from the EXIF data for orientation in ExifTags.TAGS.keys(): if ExifTags.TAGS[orientation] == 'Orientation': break + exif = image._getexif() if exif is not None: orientation = exif.get(orientation) + + # Rotate the image based on the orientation value if orientation == 3: image = image.rotate(180, expand=True) elif orientation == 6: image = image.rotate(270, expand=True) elif orientation == 8: image = image.rotate(90, expand=True) + + # Save the corrected image back to the same path image.save(image_path) except (AttributeError, KeyError, IndexError): + # If there is an error accessing the EXIF data, pass without making changes pass def process_video_for_misplaced_objects(video_path, frame_interval, frame_delay, detection_model, category_index): + """ + Process a video to detect and visualize misplaced objects at specified frame intervals. + + Args: + video_path: The path to the video file that needs processing. + frame_interval: The interval (in seconds) at which frames should be analyzed. + frame_delay: The delay (in seconds) between frames in the output video. + detection_model: The object detection model to be used for inference. + category_index: The category index mapping class IDs to class names. + + Returns: + detected_objects_all_frames: A list of detected objects for each analyzed frame. + misplaced_objects_all_frames: A list of misplaced objects for each analyzed frame. + output_video_path: The path to the output video with annotated frames. + """ + # Open the video file cap = cv2.VideoCapture(video_path) + + # Get the frames per second (FPS) of the video fps = int(cap.get(cv2.CAP_PROP_FPS)) + + # Initialize lists to store detected and misplaced objects for each frame misplaced_objects_all_frames = [] detected_objects_all_frames = [] + frame_count = 0 annotated_frame_count = 1 # Start frame count from 1 for annotated frames - frame_interval_frames = frame_interval * fps + frame_interval_frames = frame_interval * fps # Convert frame interval from seconds to frames annotated_frames = [] + # Process the video frame by frame while cap.isOpened(): ret, frame = cap.read() if not ret: break + # Analyze the frame at the specified intervals if frame_count % frame_interval_frames == 0: + # Convert the frame to RGB format image_np = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - # Convert the frame to PIL image + # Convert the frame to a PIL image image_pil = Image.fromarray(image_np) + # Run object detection on the frame detected_objects = run_inference(detection_model, category_index, image_pil) + # Check for misplaced objects using placement rules placement_rules = PlacementRules() misplaced_objects = placement_rules.check_placement(detected_objects) + # Store the detected and misplaced objects for the current frame detected_objects_all_frames.append(detected_objects) misplaced_objects_all_frames.append(misplaced_objects) - # Annotate the frame with bounding boxes, labels, and annotated frame number + # Annotate the frame with bounding boxes, labels, and frame number annotated_image_pil = visualize_pil_misplaced_objects(image_pil, detected_objects, misplaced_objects, annotated_frame_count) annotated_image_np = np.array(annotated_image_pil) annotated_frames.append(annotated_image_np) @@ -78,11 +136,13 @@ def process_video_for_misplaced_objects(video_path, frame_interval, frame_delay, frame_count += 1 + # Release the video capture object cap.release() - # Create a video with a specified delay between each frame + # Create an annotated video from the processed frames output_video_path = os.path.join(settings.MEDIA_ROOT, 'videos', os.path.basename(video_path).replace('.mp4', '_annotated.mp4')) annotated_clip = ImageSequenceClip(annotated_frames, fps=1/frame_delay) annotated_clip.write_videofile(output_video_path, fps=fps, codec='libx264', audio_codec='aac') + # Return the detected objects, misplaced objects, and path to the output video return detected_objects_all_frames, misplaced_objects_all_frames, output_video_path diff --git a/MisplaceAI/process_misplaced_manager/views.py b/MisplaceAI/process_misplaced_manager/views.py index 29ff42af2d27e8e345accd2bf0c2dbbba5851b7f..0ade34979e883f0ecd2945ed56a4da63cf1bbf4f 100644 --- a/MisplaceAI/process_misplaced_manager/views.py +++ b/MisplaceAI/process_misplaced_manager/views.py @@ -1,5 +1,11 @@ # MisplaceAI/process_misplaced_manager/views.py +# This file contains the views for handling image and video uploads, running object detection, +# applying placement rules to detect misplaced objects, and providing results to the users. +# The views utilize Django Rest Framework to create API endpoints that are secured with +# authentication and permission classes. The file also includes utility functions for image +# and video processing, and daily detection limit checks per user. + from rest_framework import viewsets, status from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated, IsAdminUser @@ -18,34 +24,41 @@ from django.conf import settings from django.http import JsonResponse, HttpResponse, Http404 from .utils import increment_detection_count, correct_image_orientation, process_video_for_misplaced_objects +# Set up logging logger = logging.getLogger(__name__) +# Paths to the model and label map used for object detection MODEL_PATH = "models/research/object_detection/faster_rcnn_resnet50_v1_1024x1024_coco17_tpu-8/saved_model" LABEL_MAP_PATH = "models/research/object_detection/data/mscoco_label_map.pbtxt" +# Load the detection model and create the category index from the label map detection_model = load_model(MODEL_PATH) category_index = create_category_index_from_labelmap(LABEL_MAP_PATH) +# ViewSet for managing uploaded images class UploadedImageViewSet(viewsets.ModelViewSet): - queryset = UploadedImage.objects.all() - serializer_class = UploadedImageSerializer - permission_classes = [IsAuthenticated] + queryset = UploadedImage.objects.all() # Queryset of all uploaded images + serializer_class = UploadedImageSerializer # Serializer class for uploaded images + permission_classes = [IsAuthenticated] # Only authenticated users can access +# API view for normal detection on uploaded images @api_view(['POST']) @permission_classes([IsAuthenticated]) def normal_detection(request): + """Handle image upload, run object detection, and check for misplaced objects.""" print("Received request for normal detection") print("Request data:", request.data) print("Request FILES:", request.FILES) try: if 'capturedImageData' in request.data: + # Process base64 encoded image data captured_image_data = request.data['capturedImageData'] format, imgstr = captured_image_data.split(';base64,') ext = format.split('/')[-1] image_data = ContentFile(base64.b64decode(imgstr), name='temp.' + ext) - new_image = UploadedImage.objects.create(image=image_data, user=request.user) else: + # Process uploaded image file data = request.data.copy() data['user'] = request.user.id serializer = UploadedImageSerializer(data=data) @@ -55,17 +68,27 @@ def normal_detection(request): print("Serializer errors:", serializer.errors) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + # Path to the newly uploaded image image_path = new_image.image.path + + # Correct the orientation of the image correct_image_orientation(image_path) + + # Run object detection on the image detected_objects = run_inference(detection_model, category_index, image_path) + + # Check for misplaced objects using placement rules placement_rules = PlacementRules() misplaced_objects = placement_rules.check_placement(detected_objects) + + # Visualize the detected and misplaced objects on the image output_image_path = visualize_misplaced_objects(image_path, detected_objects, misplaced_objects) - # Delete the original uploaded image + # Delete the original uploaded image to save space if os.path.exists(image_path): os.remove(image_path) + # Prepare response data response_data = { 'image_id': new_image.id, 'image_url': new_image.image.url, @@ -78,19 +101,28 @@ def normal_detection(request): print(f"Error processing image: {str(e)}") return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +# API view for displaying detection results for a specific image @api_view(['GET']) @permission_classes([IsAuthenticated]) def display_results(request, image_id): + """Display the detection results for a specific uploaded image.""" image = get_object_or_404(UploadedImage, id=image_id) image_path = image.image.path + # Correct the orientation of the image correct_image_orientation(image_path) + # Run object detection on the image detected_objects = run_inference(detection_model, category_index, image_path) + + # Check for misplaced objects using placement rules placement_rules = PlacementRules() misplaced_objects = placement_rules.check_placement(detected_objects) + + # Visualize the detected and misplaced objects on the image output_image_path = visualize_misplaced_objects(image_path, detected_objects, misplaced_objects) + # Prepare response data response_data = { 'image_url': image.image.url, 'output_image_url': "/media/" + os.path.basename(output_image_path), @@ -98,41 +130,35 @@ def display_results(request, image_id): } return Response(response_data, status=status.HTTP_200_OK) - - ################################################################################################# ################################## Video Upload and Processing ################################# - - - - - - - +# ViewSet for managing uploaded videos class UploadedVideoViewSet(viewsets.ModelViewSet): - queryset = UploadedVideo.objects.all() - serializer_class = UploadedVideoSerializer - permission_classes = [IsAuthenticated] + queryset = UploadedVideo.objects.all() # Queryset of all uploaded videos + serializer_class = UploadedVideoSerializer # Serializer class for uploaded videos + permission_classes = [IsAuthenticated] # Only authenticated users can access +# API view for uploading a video and setting frame preferences @api_view(['POST']) @permission_classes([IsAuthenticated]) def upload_video(request): - print("Received request to upload video") - print("Request data:", request.data) - print("Request FILES:", request.FILES) - + """Handle video upload and set user preferences for frame processing.""" if 'video' not in request.FILES: print("No video file provided in request") return Response({'error': 'No video file provided'}, status=status.HTTP_400_BAD_REQUEST) + # Get frame processing preferences from the request frame_interval = int(request.data.get('frames_jump', 1)) frame_delay = int(request.data.get('frame_delay', 1)) + + # Retrieve or create user video frame preferences user_video_frame_preference, created = UserVideoFramePreference.objects.get_or_create(user=request.user) user_video_frame_preference.frame_interval = frame_interval user_video_frame_preference.frame_delay = frame_delay user_video_frame_preference.save() + # Serialize the video data and save the uploaded video serializer = UploadedVideoSerializer(data={'video': request.FILES['video'], 'user': request.user.id, 'user_video_frame_preference': user_video_frame_preference.id}) if serializer.is_valid(): print("Serializer data is valid") @@ -143,34 +169,42 @@ def upload_video(request): print("Serializer errors:", serializer.errors) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +# API view for displaying detection results for a specific video @api_view(['GET']) @permission_classes([IsAuthenticated]) def display_video_results(request, video_id): + """Display the detection results for a specific uploaded video.""" print("Received request to display video results for video ID", video_id) try: + # Retrieve the uploaded video object video = get_object_or_404(UploadedVideo, id=video_id) video_path = video.video.path + + # Retrieve user preferences for frame processing user_video_frame_preference = get_object_or_404(UserVideoFramePreference, user=video.user) frame_interval = user_video_frame_preference.frame_interval frame_delay = user_video_frame_preference.frame_delay print("Processing video at path:", video_path) print(f"Frame interval: {frame_interval}, Frame delay: {frame_delay}") + + # Process the video to detect and visualize misplaced objects detected_objects, misplaced_objects, output_video_path = process_video_for_misplaced_objects( video_path, frame_interval, frame_delay, detection_model, category_index ) - # Delete the original uploaded video + # Delete the original uploaded video to save space if os.path.exists(video_path): os.remove(video_path) + # Prepare response data response_data = { 'video_url': request.build_absolute_uri(video.video.url), 'output_video_url': request.build_absolute_uri(settings.MEDIA_URL + 'videos/' + os.path.basename(output_video_path)), 'detected_objects': detected_objects, 'misplaced_objects': misplaced_objects # Ensure misplaced_objects include 'allowed_locations' } - + print("\nDEBUG FOR misplaced_objects: ", misplaced_objects) return Response(response_data, status=status.HTTP_200_OK) @@ -178,9 +212,11 @@ def display_video_results(request, video_id): print(f"Error processing video results: {e}") return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +# API view for deleting an image @api_view(['DELETE']) @permission_classes([IsAuthenticated]) def delete_image(request, image_name): + """Delete a specific image by its name.""" try: print(f"Attempting to delete image: {image_name}") # Construct the file path @@ -199,11 +235,11 @@ def delete_image(request, image_name): print(f"Error deleting image {image_name}: {str(e)}") return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - +# API view for deleting a video @api_view(['DELETE']) @permission_classes([IsAuthenticated]) def delete_video(request, video_name): + """Delete a specific video by its name.""" try: print(f"Attempting to delete video: {video_name}") video_path = os.path.join(settings.MEDIA_ROOT, 'videos', video_name) @@ -218,14 +254,14 @@ def delete_video(request, video_name): print(f"Error deleting video {video_name}: {str(e)}") return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - ################################################################################################# ####################################### Download Results ######################################## + +# API view for downloading an image file @api_view(['GET']) @permission_classes([IsAuthenticated]) def download_image(request, file_path): + """Download a specific image file.""" file_path = os.path.join(settings.MEDIA_ROOT, file_path) if os.path.exists(file_path): with open(file_path, 'rb') as f: @@ -234,14 +270,11 @@ def download_image(request, file_path): return response else: raise Http404 - - - - +# API view for downloading a media file (image or video) @api_view(['GET']) -# @permission_classes([IsAuthenticated]) def download_media(request, file_path): + """Download a specific media file, either an image or a video.""" # Determine if the file is in the 'videos' directory video_path = os.path.join(settings.MEDIA_ROOT, 'videos', file_path) if os.path.exists(video_path): @@ -260,9 +293,12 @@ def download_media(request, file_path): ################################################################################################# ####################################### LIMITS PER USER ######################################### + +# API view for getting the daily detection limits for the user @api_view(['GET']) @permission_classes([IsAuthenticated]) def get_daily_limits(request): + """Get the daily detection limits for the user.""" limit_setting = DetectionLimitSetting.objects.first() if not limit_setting: limit_setting = DetectionLimitSetting.objects.create(daily_image_limit=10, daily_video_limit=5) @@ -271,9 +307,11 @@ def get_daily_limits(request): 'daily_video_limit': limit_setting.daily_video_limit }) +# API view for setting the daily detection limits (admin only) @api_view(['POST']) @permission_classes([IsAdminUser]) def set_daily_limits(request): + """Set the daily detection limits for all users (admin only).""" daily_image_limit = request.data.get('daily_image_limit', 10) daily_video_limit = request.data.get('daily_video_limit', 5) limit_setting, created = DetectionLimitSetting.objects.get_or_create(id=1) @@ -285,9 +323,11 @@ def set_daily_limits(request): 'daily_video_limit': limit_setting.daily_video_limit }) +# API view for checking the remaining daily limit for the user @api_view(['GET']) @permission_classes([IsAuthenticated]) def check_daily_limit(request): + """Check the remaining daily detection limit for the user.""" user = request.user detection_type = request.GET.get('type') @@ -308,9 +348,11 @@ def check_daily_limit(request): 'limit': limit_setting.daily_image_limit if detection_type == 'image' else limit_setting.daily_video_limit }) +# API view for incrementing the detection count for the user @api_view(['POST']) @permission_classes([IsAuthenticated]) def increment_detection(request): + """Increment the detection count for the user.""" user = request.user detection_type = request.data.get('type') @@ -318,4 +360,4 @@ def increment_detection(request): return Response({'error': 'Invalid detection type'}, status=400) increment_detection_count(user, detection_type) - return Response({'status': 'success'}) \ No newline at end of file + return Response({'status': 'success'}) diff --git a/MisplaceAI/results_viewer/urls.py b/MisplaceAI/results_viewer/urls.py index 1f9e8dfc110baed73ae227610bafa73ea1deeac5..7bc3cb45530300c1c0f0c5cd1b1714fa610a5180 100644 --- a/MisplaceAI/results_viewer/urls.py +++ b/MisplaceAI/results_viewer/urls.py @@ -1,10 +1,16 @@ # MisplaceAI/results_viewer/urls.py +# This file defines the URL patterns for the results_viewer app. +# It includes routes for generating annotated images with misplaced object annotations. + from django.urls import path from .views import generate_annotated_image +# Define the app name for namespacing URL names app_name = 'results_viewer' +# Define the URL patterns for the results_viewer app urlpatterns = [ + # Route for generating annotated images path('generate/<int:image_id>/', generate_annotated_image, name='generate_annotated_image'), ] diff --git a/MisplaceAI/results_viewer/utils.py b/MisplaceAI/results_viewer/utils.py index 6d3a8a759be862b143cc9030d018f010c5a7a25e..a9388e2506f83c20af590c8cd60f11bca926e203 100644 --- a/MisplaceAI/results_viewer/utils.py +++ b/MisplaceAI/results_viewer/utils.py @@ -1,13 +1,27 @@ -# MisplaceAi/results_viewer/utils.py +# MisplaceAI/results_viewer/utils.py + +# This file contains utility functions for the results_viewer app. +# These functions handle visualization tasks such as annotating images with misplaced objects. + import matplotlib.pyplot as plt import matplotlib.patches as patches from PIL import Image, ImageDraw, ImageFont import os from django.conf import settings - def visualize_misplaced_objects(image_path, detected_objects, misplaced_objects): - """Visualize misplaced objects with annotations.""" + """ + Visualize misplaced objects with annotations on an image. + + Args: + image_path (str): Path to the image file. + detected_objects (list): List of detected objects with their bounding box coordinates and class names. + misplaced_objects (list): List of misplaced objects with their class names. + + Returns: + str: Path to the annotated image. + """ + # Open the image file image = Image.open(image_path) width, height = image.size @@ -16,10 +30,12 @@ def visualize_misplaced_objects(image_path, detected_objects, misplaced_objects) ax = plt.gca() + # List of class names for misplaced objects misplaced_names = [obj["class_name"] for obj in misplaced_objects] print(f"Misplaced object names: {misplaced_names}") # Debugging for obj in detected_objects: + # Get the bounding box coordinates ymin, xmin, ymax, xmax = [ obj["ymin"] * height, obj["xmin"] * width, @@ -27,6 +43,7 @@ def visualize_misplaced_objects(image_path, detected_objects, misplaced_objects) obj["xmax"] * width, ] + # Draw the bounding box around the object rect = patches.Rectangle( (xmin, ymin), xmax - xmin, @@ -37,6 +54,7 @@ def visualize_misplaced_objects(image_path, detected_objects, misplaced_objects) ) ax.add_patch(rect) + # Annotate the object ax.text( xmin, ymin, @@ -48,28 +66,35 @@ def visualize_misplaced_objects(image_path, detected_objects, misplaced_objects) plt.axis("off") + # Define the output path for the annotated image output_image_path = os.path.join("media", os.path.splitext(os.path.basename(image_path))[0] + "_annotated.png") + # Save the annotated image plt.savefig(output_image_path, bbox_inches='tight', pad_inches=0.0) plt.close() print(f"Output image saved at: {output_image_path}") # Debugging return output_image_path - - - - - def visualize_pil_misplaced_objects(image_pil, detected_objects, misplaced_objects, frame_number): - """Visualize misplaced objects with annotations on a PIL image.""" - + """ + Visualize misplaced objects with annotations on a PIL image. + + Args: + image_pil (PIL.Image.Image): PIL image object. + detected_objects (list): List of detected objects with their bounding box coordinates and class names. + misplaced_objects (list): List of misplaced objects with their class names. + frame_number (int): Frame number for the annotated image. + + Returns: + PIL.Image.Image: Annotated PIL image object. + """ # Create a drawing context for the image draw = ImageDraw.Draw(image_pil) # Get the dimensions of the image width, height = image_pil.size - # Create a list of class names for misplaced objects + # List of class names for misplaced objects misplaced_names = [obj["class_name"] for obj in misplaced_objects] # Load a font using absolute path to the static directory @@ -97,6 +122,7 @@ def visualize_pil_misplaced_objects(image_pil, detected_objects, misplaced_objec obj["xmax"] * width, ] + # Determine the color of the bounding box and text based on whether the object is misplaced color = "green" if obj["class_name"] not in misplaced_names else "red" draw.rectangle([xmin, ymin, xmax, ymax], outline=color, width=3) diff --git a/MisplaceAI/results_viewer/views.py b/MisplaceAI/results_viewer/views.py index 78a205a2ed0082c91d2318f0ffdc7aa75d44d5c5..3b9ac7c06e271dc04ba4f134ac7410eb0b547f2b 100644 --- a/MisplaceAI/results_viewer/views.py +++ b/MisplaceAI/results_viewer/views.py @@ -1,20 +1,38 @@ -# MisplaceAi/results_viewer/views.py +# MisplaceAI/results_viewer/views.py + +# This file defines the views for the results_viewer app. +# Views handle the logic for different endpoints, providing appropriate responses +# based on the request and the business logic. from django.http import JsonResponse from .utils import visualize_misplaced_objects import os def generate_annotated_image(request): + """ + View to generate an annotated image with misplaced object annotations. + + Args: + request (HttpRequest): The HTTP request object containing image path and misplaced objects. + + Returns: + JsonResponse: JSON response with the path to the annotated image or an error message. + """ if request.method == 'POST': + # Get the image path and misplaced objects data from the request image_path = request.POST.get('image_path') misplaced_objects = request.POST.get('misplaced_objects') + # Check if image path and misplaced objects are provided if not image_path or not misplaced_objects: return JsonResponse({'error': 'Invalid input data'}, status=400) + # Define the output path for the annotated image output_image_path = os.path.join("media", "annotated_" + os.path.basename(image_path)) + # Generate the annotated image visualize_misplaced_objects(image_path, misplaced_objects, output_image_path) + # Return the path to the annotated image return JsonResponse({'annotated_image_path': output_image_path}) else: return JsonResponse({'error': 'Invalid request method'}, status=400)