From e833f5512cdc2906e2b82ac528c3a0fa82040ade Mon Sep 17 00:00:00 2001 From: yashsuman Date: Tue, 18 Nov 2025 02:05:24 +0530 Subject: [PATCH 01/14] Add test script for preannotation API functionality - Created a new script to test the upload of keyframe preannotations. - added method for pre-annotation upload to video project - modify SDK notebook accordingly --- labellerr/core/constants.py | 2 +- labellerr/core/files/video_file.py | 9 +- labellerr/core/projects/video_project.py | 54 +- labellerr/core/schemas/__init__.py | 40 +- labellerr/core/schemas/projects.py | 1 - labellerr/notebooks/SDK.ipynb | 923 ++++++++++-------- labellerr/notebooks/test_preannotation_api.py | 34 + 7 files changed, 636 insertions(+), 427 deletions(-) create mode 100644 labellerr/notebooks/test_preannotation_api.py diff --git a/labellerr/core/constants.py b/labellerr/core/constants.py index 5484189..6d1893e 100644 --- a/labellerr/core/constants.py +++ b/labellerr/core/constants.py @@ -1,4 +1,4 @@ -BASE_URL = "https://api.labellerr.com" +BASE_URL = "https://api-gateway-qcb3iv2gaa-uc.a.run.app" ALLOWED_ORIGINS = "https://pro.labellerr.com" diff --git a/labellerr/core/files/video_file.py b/labellerr/core/files/video_file.py index cb97956..97138b3 100644 --- a/labellerr/core/files/video_file.py +++ b/labellerr/core/files/video_file.py @@ -4,7 +4,7 @@ import uuid from concurrent.futures import ThreadPoolExecutor, as_completed from threading import Lock -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import requests @@ -61,6 +61,7 @@ def get_frames(self, frame_start: int = 0, frame_end: int | None = None): "frame_end": frame_end, "project_id": self.project_id, "uuid": unique_id, + "client_id": self.client.client_id, } response = self.client.make_request( @@ -235,7 +236,7 @@ def create_video( raise LabellerrError(f"Error while joining frames: {str(e)}") def download_create_video_auto_cleanup( - self, output_folder: str = "./Labellerr_datastets" + self, output_folder: str = "./Labellerr_datasets" ): """ Download frames, create video, and automatically clean up temporary frames. @@ -258,6 +259,8 @@ def download_create_video_auto_cleanup( print(f"\n[1/4] Fetching frame data from API (0 to {total_frames})...") frames_data = self.get_frames(frame_start=0, frame_end=total_frames) + # print(frames_data) + if not frames_data: raise LabellerrError("No frame data retrieved from API") @@ -313,7 +316,7 @@ def download_create_video_auto_cleanup( print(f"\n{'='*60}") print("Processing complete!") print(f"Video saved to: {video_output_path}") - print("{'='*60}\n") + print(f"{'='*60}\n") return result diff --git a/labellerr/core/projects/video_project.py b/labellerr/core/projects/video_project.py index a70d21c..6d6b912 100644 --- a/labellerr/core/projects/video_project.py +++ b/labellerr/core/projects/video_project.py @@ -1,5 +1,6 @@ +import os import uuid -from typing import List +from typing import Any, Dict, List from .. import constants from ..exceptions import LabellerrError @@ -91,5 +92,56 @@ def delete_keyframes(self, file_id: str, keyframes: List[int]): except Exception as e: raise LabellerrError(f"Failed to delete key frames: {str(e)}") + def upload_keyframe_preannotations(self, video_json_file_path: str = None) -> Any: + """ + Uploads pre-annotations for video project. + + Supports both the parent signature and a video-specific signature for backward compatibility. + + :param annotation_format: (Deprecated) The format of the preannotation data + :param annotation_file: (Deprecated) The file path of the preannotation data + :param conf_bucket: (Deprecated) Confidence bucket [low, medium, high] + :param _async: (Deprecated) Whether to return a future object + :param video_json_file_path: Path to the video JSON file containing pre-annotations + :return: Response from the API + """ + # Support both old and new signatures + file_path = video_json_file_path + + # Parameter validation + if not isinstance(file_path, str): + raise LabellerrError("file_path must be a str") + + try: + # Validate if the file exists + if not os.path.exists(file_path): + raise LabellerrError(f"File not found: {file_path}") + + unique_id = str(uuid.uuid4()) + url = f"{constants.BASE_URL}/actions/upload_answers?project_id={self.project_id}&answer_format=video_json&client_id={self.client.client_id}&uuid={unique_id}" + + # Get file name from path + file_name = os.path.basename(file_path) + + # Open file and prepare multipart form data + with open(file_path, "rb") as f: + files = [("file", (file_name, f, "application/json"))] + payload: Dict[Any, Any] = {} + + response = self.client.make_request( + "POST", + url, + request_id=unique_id, + handle_response=False, + data=payload, + files=files, + ) + + return self.client.handle_upload_response(response, unique_id) + except LabellerrError: + raise + except Exception as e: + raise LabellerrError(f"Failed to upload pre-annotations: {str(e)}") + LabellerrProjectMeta._register(DatasetDataType.video, VideoProject) diff --git a/labellerr/core/schemas/__init__.py b/labellerr/core/schemas/__init__.py index 5579571..ab09f11 100644 --- a/labellerr/core/schemas/__init__.py +++ b/labellerr/core/schemas/__init__.py @@ -14,18 +14,29 @@ # Import from autolabel.typings for backward compatibility from labellerr.core.autolabel.typings import * # noqa: F403, F401 +# Export annotation templates +from labellerr.core.schemas.annotation_templates import ( + AnnotationQuestion, + CreateTemplateParams, + Option, + QuestionType, +) + +# Autolabel schemas +from labellerr.core.schemas.autolabel import Hyperparameters, KeyFrame, TrainingRequest + # Base custom types from labellerr.core.schemas.base import DirPathStr, FilePathStr, NonEmptyStr # Connection schemas from labellerr.core.schemas.connectors import ( AWSConnectionParams, - DatasetDataType, - DeleteConnectionParams, - GCSConnectionParams, AWSConnectionTestParams, ConnectionType, ConnectorType, + DatasetDataType, + DeleteConnectionParams, + GCSConnectionParams, GCSConnectionTestParams, ) @@ -43,6 +54,9 @@ UploadFilesParams, ) +# Export schemas +from labellerr.core.schemas.exports import CreateExportParams, ExportDestination + # File operation schemas from labellerr.core.schemas.files import BulkAssignFilesParams, ListFileParams @@ -65,25 +79,6 @@ UpdateUserRoleParams, ) -# Autolabel schemas -from labellerr.core.schemas.autolabel import ( - Hyperparameters, - KeyFrame, - TrainingRequest, -) - -# Export schemas -from labellerr.core.schemas.exports import CreateExportParams, ExportDestination - - -# Export annotation templates -from labellerr.core.schemas.annotation_templates import ( - AnnotationQuestion, - Option, - QuestionType, -) - - __all__ = [ # Base types "NonEmptyStr", @@ -136,4 +131,5 @@ "AnnotationQuestion", "Option", "QuestionType", + "CreateTemplateParams", ] diff --git a/labellerr/core/schemas/projects.py b/labellerr/core/schemas/projects.py index 830853b..a70f0b5 100644 --- a/labellerr/core/schemas/projects.py +++ b/labellerr/core/schemas/projects.py @@ -51,7 +51,6 @@ class CreateProjectParams(BaseModel): class CreateTemplateParams(BaseModel): """Parameters for creating an annotation template.""" - client_id: str = Field(min_length=1) data_type: Literal["image", "video", "audio", "document", "text"] template_name: str = Field(min_length=1) questions: List[Question] = Field(min_length=1) diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index 1efd28f..97f1426 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -5,12 +5,9 @@ "id": "d6488b6b", "metadata": {}, "source": [ - "# Getting Started with Labellerr SDK\n", + "# Keyframe Scene detection with Labellerr SDK\n", "\n", - "This notebook demonstrates how to use the Labellerr SDK for video processing and scene detection. The SDK provides powerful tools for managing video datasets, processing videos, and detecting scene changes using various algorithms.\n", - "\n", - "### Import the required Classes from Labellerr SDK\n", - "We'll start by importing the essential classes needed for working with the SDK:" + "This notebook demonstrates how to use the Labellerr SDK for video processing and scene detection. The SDK provides powerful tools for managing video datasets, processing videos, and detecting scene changes using various algorithms.\n" ] }, { @@ -21,9 +18,16 @@ "outputs": [], "source": [ "from labellerr.client import LabellerrClient\n", - "from labellerr.core.datasets import LabellerrDataset\n", - "import os\n", - "from tqdm.notebook import tqdm\n" + "from labellerr.core.datasets import create_dataset_from_local, LabellerrDataset\n", + "from labellerr.core.annotation_templates import create_template\n", + "from labellerr.core.projects import create_project\n", + "from labellerr.core.schemas.annotation_templates import AnnotationQuestion, QuestionType, CreateTemplateParams, DatasetDataType\n", + "from labellerr.core.schemas import DatasetConfig\n", + "from labellerr.core.schemas.projects import CreateProjectParams, RotationConfig\n", + "\n", + "import uuid\n", + "from pathlib import Path\n", + "import os\n" ] }, { @@ -31,7 +35,8 @@ "id": "84b7917a", "metadata": {}, "source": [ - "## 1. Authentication Setup\n", + "---\n", + "## ***Authentication Setup***\n", "\n", "Before using the Labellerr SDK, you need to set up your authentication credentials. These credentials ensure secure access to the Labellerr platform and its services.\n", "\n", @@ -44,9 +49,7 @@ "\n", "2. **Client ID**\n", " - This is a unique identifier for your application\n", - " - Contact Labellerr support to obtain your client ID\n", - " \n", - "⚠️ Important: Never share these credentials or commit them to version control." + " - Contact Labellerr support to obtain your client ID\n" ] }, { @@ -61,71 +64,362 @@ "\n", "api_key = config[\"API_KEY\"]\n", "api_secret = config[\"API_SECRET\"]\n", - "client_id = config[\"CLIENT_ID\"]" + "client_id = config[\"CLIENT_ID\"]\n", + "email = config[\"EMAIL\"]\n", + "\n", + "client = LabellerrClient(api_key, api_secret, client_id)" ] }, { "cell_type": "markdown", - "id": "3d05bd0f", + "id": "d2646549", + "metadata": {}, + "source": [ + "---\n", + "## ***Kaggle Dataset Download***" + ] + }, + { + "cell_type": "markdown", + "id": "c2d2a744", "metadata": {}, "source": [ - "## 2. Project Configuration\n", + "Before downloading the dataset from Kaggle, you need to:\n", "\n", - "### Dataset and Project IDs\n", - "To work with specific datasets and projects in Labellerr, you need their respective IDs. These IDs are unique identifiers that link your code to the correct resources on the platform.\n", + "1. Install kagglehub package using pip\n", + "2. Authenticate with Kaggle\n", + "3. Download the CCTV footage dataset\n", "\n", - "How to obtain the IDs:\n", - "1. Go to the Labellerr platform\n", - "2. Create or select an existing dataset\n", - "3. Create or select an existing project\n", - "4. Copy the dataset_id and project_id from their respective pages\n", + "The kagglehub package provides a simple interface to download datasets directly from Kaggle. Make sure you have a Kaggle account and API credentials set up before proceeding.\n", "\n", - "Note: The dataset_id is a UUID format string, while the project_id is typically a human-readable string." + "Note: If you haven't set up Kaggle authentication before, you'll need to:\n", + "1. Create a Kaggle account at https://www.kaggle.com\n", + "2. Go to \"Account\" settings\n", + "3. Scroll to API section and click \"Create New API Token\"\n", + "4. This will download a kaggle.json file with your credentials" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "07dcfae9", + "execution_count": null, + "id": "be12bf3f", "metadata": {}, "outputs": [], "source": [ - "# go to our platform to create dataset and project then get their ids\n", - "dataset_id = \"16257fd6-b91b-4d00-a680-9ece9f3f241c\"\n", - "project_id = \"gabrila_artificial_duck_74237\"" + "# !pip install kagglehub ipywidgets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e05889d7", + "metadata": {}, + "outputs": [], + "source": [ + "import kagglehub\n", + "\n", + "kagglehub.login()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5b93e97", + "metadata": {}, + "outputs": [], + "source": [ + "# Download 1000 videos(~1 min) dataset\n", + "\n", + "# large video dataset(1000 videos)\n", + "# path_to_dataset = kagglehub.dataset_download(\"yashsuman/cctv-footage\")\n", + "\n", + "# small video dataset(5 videos)\n", + "path_to_dataset = kagglehub.dataset_download(\"mistag/short-videos\")\n", + "\n", + "print(\"Path to dataset files:\", path_to_dataset)" ] }, { "cell_type": "markdown", - "id": "1b2c7aee", + "id": "3d05bd0f", "metadata": {}, "source": [ - "## 3. Initializing the Labellerr SDK\n", + "---\n", + "## ***Video Project Creation***\n", "\n", - "### Create LabellerrClient Instance\n", - "Now we'll create instances of the main SDK classes:\n", + "Create a Labellerr Video Project with kaggle dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "52e00dbc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DATASET_PATH = path_to_dataset\n", + "KAGGLE_DATASET_PATH = Path(r\"..\\..\\..\\.cache\\kagglehub\\datasets\\mistag\\short-videos\\versions\\4\")\n", + "\n", + "KAGGLE_DATASET_PATH.exists()" + ] + }, + { + "cell_type": "markdown", + "id": "d861c36f", + "metadata": {}, + "source": [ + "### Create Labellerr Dataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1e2e2f3", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:root:Total file count: 2\n", + "INFO:root:Total file size: 42.2 MB\n", + "INFO:root:CPU count: 24, Batch Count: 2\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api-gateway-qcb3iv2gaa-uc.a.run.app:443\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api-gateway-qcb3iv2gaa-uc.a.run.app:443\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"POST /connectors/connect/local?client_id=1 HTTP/1.1\" 200 1085\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"POST /connectors/connect/local?client_id=1 HTTP/1.1\" 200 1079\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", + "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"POST /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/seafood_1280p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101252Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=abb81e14c8815af5553818d4456594c59e80daf8d1843c4b822a277b2191904a53f0449c4da0dafb04be6055d47e63cb7dc1e6a99bd466bb1a82aa7e8484af661e126917a444fbbaa03532224f7915f061f16860c4a4e7010846733d48f13be90e0af96ed39e155e85644661f59373e15619c7a8e6d1feb9bb2b7e8ce82192acbc86d98a4ef48463717bf14611a1a90b9b16245a9fb0cf7e38962dcf3761c5fddf82b0d141f5eff7f0b0f4d0743957af87a9606563e99ca41b6a48d2961fb8cb70bf2408e8a669b5d42f85b3d4bffaaf488430c5b71df018bd39d104b6a3f3114d669b8dd29762f433999c8f7ac1ef35f653c614522e1ff934690129fd4c6060 HTTP/1.1\" 201 0\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", + "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"POST /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/butterflies_960p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101251Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=23e640fedb8bde8db621437d85075adb326eccb5969891a150fe3a4c2769170332ddef85aee49e81168dba63ffbbb021a4e8290ff77eb73daaff6e56c7d0087edd9502a2bfd6e31404e38ba02424e4e2ea6b52106a2681aae5c9e76c0e19b0d8c198811c577f6408eceb1a04ac8d21d1f05608e83114eb56a09c078733e0ae2c5c71294ba0f7ab6c04a0bf158a289f3c300b619e5b1b39382370b2457cade3987eac9f304dc52cb975ae2ce45a628a76496846d80c7aff125afbd8ab999edfeca42a296aff65bf3c8c7f6f48c30f6f4c9caf2fa917e4c0261303a325db68e31401816fe3cf20693459417156c41aec770ffa5e2ee88b77e5d746dacee71225a2 HTTP/1.1\" 201 0\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", + "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"PUT /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/butterflies_960p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101251Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=23e640fedb8bde8db621437d85075adb326eccb5969891a150fe3a4c2769170332ddef85aee49e81168dba63ffbbb021a4e8290ff77eb73daaff6e56c7d0087edd9502a2bfd6e31404e38ba02424e4e2ea6b52106a2681aae5c9e76c0e19b0d8c198811c577f6408eceb1a04ac8d21d1f05608e83114eb56a09c078733e0ae2c5c71294ba0f7ab6c04a0bf158a289f3c300b619e5b1b39382370b2457cade3987eac9f304dc52cb975ae2ce45a628a76496846d80c7aff125afbd8ab999edfeca42a296aff65bf3c8c7f6f48c30f6f4c9caf2fa917e4c0261303a325db68e31401816fe3cf20693459417156c41aec770ffa5e2ee88b77e5d746dacee71225a2&upload_id=AOCedOEk1QTBtYylxPr3uH4ac4enQIiFVX-WRK66hGbV0b3MSd0MYrQUnrEUsOmbR2ZkjlujH3zdLrGX2v11KdAVScEBhe_qr_4nQdUqck5u_nk HTTP/1.1\" 200 0\n", + "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"PUT /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/seafood_1280p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101252Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=abb81e14c8815af5553818d4456594c59e80daf8d1843c4b822a277b2191904a53f0449c4da0dafb04be6055d47e63cb7dc1e6a99bd466bb1a82aa7e8484af661e126917a444fbbaa03532224f7915f061f16860c4a4e7010846733d48f13be90e0af96ed39e155e85644661f59373e15619c7a8e6d1feb9bb2b7e8ce82192acbc86d98a4ef48463717bf14611a1a90b9b16245a9fb0cf7e38962dcf3761c5fddf82b0d141f5eff7f0b0f4d0743957af87a9606563e99ca41b6a48d2961fb8cb70bf2408e8a669b5d42f85b3d4bffaaf488430c5b71df018bd39d104b6a3f3114d669b8dd29762f433999c8f7ac1ef35f653c614522e1ff934690129fd4c6060&upload_id=AOCedOGtu8tVgMoqoFCcI18CqQhY_CARXt2x5laIObdMBYybxcvoZxuxl109xF29WQm1LRs4fg0cgVABEFGzBSEtpQfhE8mQNrmK8wWicqyBIXw HTTP/1.1\" 200 0\n", + "INFO:root:Folder uploaded successfully. {'success': ['..\\\\..\\\\..\\\\.cache\\\\kagglehub\\\\datasets\\\\mistag\\\\short-videos\\\\versions\\\\4\\\\butterflies_960p.mp4', '..\\\\..\\\\..\\\\.cache\\\\kagglehub\\\\datasets\\\\mistag\\\\short-videos\\\\versions\\\\4\\\\seafood_1280p.mp4'], 'fail': []}\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api-gateway-qcb3iv2gaa-uc.a.run.app:443\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"POST /datasets/create?client_id=1&uuid=7f7f78dc-4c7f-4ab3-a5f3-7bc308733a98 HTTP/1.1\" 200 361\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=a3768b9c-17d2-40f0-8f06-4b0538b8f0db HTTP/1.1\" 200 361\n" + ] + } + ], + "source": [ + "# import logging\n", + "\n", + "# logging.basicConfig(level=logging.DEBUG)\n", + "# logger = logging.getLogger(__name__)\n", "\n", - "1. **LabellerrClient**: The main client that handles communication with the Labellerr API\n", - "2. **LabellerrDataset**: A specialized class for working with datasets\n", "\n", - "These instances will be used for all subsequent operations with the platform." + "dataset = create_dataset_from_local(\n", + " client=client,\n", + " dataset_config=DatasetConfig(dataset_name=\"SDK VIDEO DATASET\", \n", + " data_type=\"video\"),\n", + " folder_to_upload=KAGGLE_DATASET_PATH,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "3d82b343", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=d4eef5b7-31da-4188-87d1-cb42176f6f5a HTTP/1.1\" 200 361\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=85af0df1-2466-4987-bda3-36bf7bc8f87b HTTP/1.1\" 200 361\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=d73a6c0a-9077-49f0-939b-1ea2cb4bb04d HTTP/1.1\" 200 361\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=81833057-1650-425a-b547-94fae62e7de9 HTTP/1.1\" 200 422\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=9b4ea0fe-6ed8-48f0-adb4-f5da409a70a0 HTTP/1.1\" 200 422\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=7f4b488b-2aa4-4fb2-ba95-a6c08e678f2c HTTP/1.1\" 200 422\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=a9686c6c-c8b1-4abe-9809-2551c2ded379 HTTP/1.1\" 200 422\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=6b1de878-8a47-49c8-a64d-ec54647d7129 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=d3ff7197-37fa-4b0d-b70e-fbb9d1af6cf0 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=e9644e97-69ef-423c-8015-c69f42970687 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=1575a956-f3ea-4421-9db0-bb0b78aaa6b2 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=00e88efd-93c5-4f83-9b07-ccf5a009764c HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=30058fd5-1d2f-4306-84dd-79abd23eff15 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=fd2f80b2-c251-4e35-84ef-3eec7c96f20c HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=3f3d5118-6663-4573-a7b1-02ca22bb3588 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=264d975f-1c10-4583-ae07-7f57534fad5f HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=bf74e8a6-4136-4cc5-b44d-329b0b4261e5 HTTP/1.1\" 200 643\n", + "INFO:root:Dataset 354681d3-034a-4d66-b070-365f4bd11d8a processing completed successfully!\n" + ] + }, + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset.status()\n", + "dataset.dataset_id\n", + "dataset.files_count" ] }, { "cell_type": "code", "execution_count": 4, - "id": "9eaec7e1", + "id": "5205f618", + "metadata": {}, + "outputs": [], + "source": [ + "dataset = LabellerrDataset(client=client,\n", + " dataset_id=\"354681d3-034a-4d66-b070-365f4bd11d8a\")" + ] + }, + { + "cell_type": "markdown", + "id": "1a1a37ec", + "metadata": {}, + "source": [ + "### Create Labellerr Annotation Template" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7be23d9a", + "metadata": {}, + "outputs": [], + "source": [ + "template = create_template(\n", + " client=client,\n", + " params=CreateTemplateParams(\n", + " template_name=\"SDK VIDEO TEMPLATE\",\n", + " data_type=DatasetDataType.video,\n", + " questions=[\n", + " AnnotationQuestion(\n", + " question_number=1,\n", + " question=\"Class polygon \",\n", + " question_id=str(uuid.uuid4()),\n", + " question_type=QuestionType.polygon,\n", + " required=True,\n", + " color=\"#FF0000\"\n", + " )\n", + " ]\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e281c33", "metadata": {}, "outputs": [], "source": [ - "client = LabellerrClient(api_key, api_secret, client_id) \n", - "dataset = LabellerrDataset(client, dataset_id, project_id)" + "template.annotation_template_id" ] }, { "cell_type": "code", "execution_count": 5, - "id": "7b6a7052", + "id": "62360ea1", + "metadata": {}, + "outputs": [], + "source": [ + "from labellerr.core.annotation_templates import LabellerrAnnotationTemplate\n", + "template = LabellerrAnnotationTemplate(client=client,\n", + " annotation_template_id='35d44c7d-9b02-4eb0-9dee-9a7ff1165331')" + ] + }, + { + "cell_type": "markdown", + "id": "a493938f", + "metadata": {}, + "source": [ + "### Create Labellerr Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8996dd01", + "metadata": {}, + "outputs": [], + "source": [ + "video_project = create_project(\n", + " client=client,\n", + " params=CreateProjectParams(\n", + " project_name=\"SDK VIDEO PROJECT\",\n", + " data_type=DatasetDataType.video,\n", + " rotations=RotationConfig(\n", + " annotation_rotation_count=1,\n", + " review_rotation_count=1,\n", + " client_review_rotation_count=1\n", + " )\n", + " ),\n", + " datasets=[dataset],\n", + " annotation_template=template\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "724c67cc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'gusella_late_marmoset_23922'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "video_project.project_id" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f74cba85", + "metadata": {}, + "outputs": [], + "source": [ + "from labellerr.core.projects import LabellerrProject\n", + "video_project = LabellerrProject(client=client,\n", + " project_id='gusella_late_marmoset_23922')" + ] + }, + { + "cell_type": "markdown", + "id": "0d806eaf", + "metadata": {}, + "source": [ + "---\n", + "## ***Download Labellerr Indexed Dataset***" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6558a9e9", "metadata": {}, "outputs": [ { @@ -134,71 +428,106 @@ "text": [ "\n", "######################################################################\n", - "# Starting batch video processing for dataset: 16257fd6-b91b-4d00-a680-9ece9f3f241c\n", + "# Starting batch video processing for dataset: 354681d3-034a-4d66-b070-365f4bd11d8a\n", "######################################################################\n", "\n", - "Total file IDs extracted: 1\n", - "\n", - "Creating LabellerrFile instances for 1 files...\n", - "Successfully created 1 LabellerrFile instances\n", + "Fetching files for dataset: 354681d3-034a-4d66-b070-365f4bd11d8a\n", + "{'message': '200: Success', 'response': {'files': [{'has_embedding': False, 'file_id': '2a8d96ca-9161-4dee-ad3b-a5faf301bc6c', 'created_at': 1763374470963, 'file_name_original': 'butterflies_960p.mp4', 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a', 'connection_id': 'fa03a1f3-3b77-42f9-b8de-eef499af4ee9', 'email_id': 'e0811e.ba8447468b95374970256d3c2b', 'file_name': 'butterflies_960p.mp4', 'file_reference': 'gs://labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/butterflies_960p.mp4', 'file_metadata': {'file_size': 25.047, 'image_width': None, 'file_format': 'mp4', 'additional_metadata': None, 'image_height': None}, 'created_by': 'e0811e.ba8447468b95374970256d3c2b', 'data_type': 'video'}, {'has_embedding': False, 'file_id': '7db3f60c-f6e5-4d3d-a63b-cb38530ee265', 'created_at': 1763374470963, 'file_name_original': 'seafood_1280p.mp4', 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a', 'connection_id': 'fa03a1f3-3b77-42f9-b8de-eef499af4ee9', 'email_id': 'e0811e.ba8447468b95374970256d3c2b', 'file_name': 'seafood_1280p.mp4', 'file_reference': 'gs://labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/seafood_1280p.mp4', 'file_metadata': {'file_size': 17.156, 'image_width': None, 'file_format': 'mp4', 'additional_metadata': None, 'image_height': None}, 'created_by': 'e0811e.ba8447468b95374970256d3c2b', 'data_type': 'video'}], 'total_count': 2, 'next_search_after': None}, 'error': None, 'tracking_id': '8d4669aea326833e9788f71d56c3a0ed'}\n", "\n", - "Processing 1 video files...\n", + "Processing 2 video files...\n", "\n", "\n", - "Starting download of 1 files...\n", + "Starting download of 2 files...\n", "\n", "============================================================\n", - "Processing file: c44f38f6-0186-436f-8c2d-ffb50a539c76\n", + "Processing file: 2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\n", "============================================================\n", "\n", - "[1/4] Fetching frame data from API (0 to 1440)...\n", - "Retrieved 1440 frames\n", + "[1/4] Fetching frame data from API (0 to 1572)...\n", + "Retrieved 1572 frames\n", "\n", "[2/4] Setting up output folders...\n", "\n", "[3/4] Downloading frames...\n", - "Starting download of 1440 frames...\n", - "Frames downloaded: 1440/1440 (1440 successful, 0 failed)\n", + "Starting download of 1572 frames...\n", + "Frames downloaded: 1572/1572 (1572 successful, 0 failed)\n", "\n", "[4/4] Creating video from frames...\n", - "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", - "Video saved as ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", + "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4\n", + "Video saved as ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4\n", "\n", "Cleaning up temporary frames...\n", - "Removed temporary frames folder: ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\n", + "Removed temporary frames folder: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\n", "\n", "============================================================\n", - "✓ Processing complete!\n", - "Video saved to: ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", + "Processing complete!\n", + "Video saved to: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4\n", + "{'='*60}\n", + "\n", + "Files processed: 1/2 (1 successful, 0 failed)\n", + "============================================================\n", + "Processing file: 7db3f60c-f6e5-4d3d-a63b-cb38530ee265\n", + "============================================================\n", + "\n", + "[1/4] Fetching frame data from API (0 to 389)...\n", + "Retrieved 389 frames\n", + "\n", + "[2/4] Setting up output folders...\n", + "\n", + "[3/4] Downloading frames...\n", + "Starting download of 389 frames...\n", + "Frames downloaded: 389/389 (389 successful, 0 failed)\n", + "\n", + "[4/4] Creating video from frames...\n", + "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4\n", + "Video saved as ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4\n", + "\n", + "Cleaning up temporary frames...\n", + "Removed temporary frames folder: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265\n", + "\n", "============================================================\n", + "Processing complete!\n", + "Video saved to: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4\n", + "{'='*60}\n", "\n", - "Files processed: 1/1 (1 successful, 0 failed)\n", + "Files processed: 2/2 (2 successful, 0 failed)\n", "######################################################################\n", "# Batch Processing Complete\n", - "# Total files: 1\n", - "# Successful: 1\n", + "# Total files: 2\n", + "# Successful: 2\n", "# Failed: 0\n", "######################################################################\n", "\n" ] + }, + { + "data": { + "text/plain": [ + "[{'status': 'success',\n", + " 'file_id': '2a8d96ca-9161-4dee-ad3b-a5faf301bc6c',\n", + " 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a',\n", + " 'video_path': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a\\\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4',\n", + " 'output_folder': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a',\n", + " 'frames_downloaded': 1572,\n", + " 'frames_failed': 0,\n", + " 'failed_frames_info': []},\n", + " {'status': 'success',\n", + " 'file_id': '7db3f60c-f6e5-4d3d-a63b-cb38530ee265',\n", + " 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a',\n", + " 'video_path': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a\\\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4',\n", + " 'output_folder': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a',\n", + " 'frames_downloaded': 389,\n", + " 'frames_failed': 0,\n", + " 'failed_frames_info': []}]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "results = dataset.download()" - ] - }, - { - "cell_type": "markdown", - "id": "900ea5a7", - "metadata": {}, - "source": [ - "### download Videos\n", - "The `download()` method will:\n", - "- Fetch all videos in the dataset\n", - "- Process them according to the configured settings\n", - "- Return the results of the processing\n", - "\n", - "This is typically used as the first step in video analysis to ensure all videos are properly prepared for further processing." + "dataset.download()" ] }, { @@ -206,9 +535,9 @@ "id": "f6db8522", "metadata": {}, "source": [ - "## 4. Scene Change Detection\n", + "---\n", + "## ***Scene Change Detection on Dataset***\n", "\n", - "### Available Scene Detection Methods\n", "Labellerr SDK provides multiple algorithms for scene detection in videos:\n", "\n", "1. **PySceneDetect**: \n", @@ -229,396 +558,196 @@ "Choose the method that best suits your needs based on accuracy requirements and processing speed constraints." ] }, - { - "cell_type": "code", - "execution_count": 6, - "id": "f5c41073", - "metadata": {}, - "outputs": [], - "source": [ - "from labellerr.services.video_sampling.pyscene_detect import PySceneDetect\n", - "from labellerr.services.video_sampling.ssim import SSIMSceneDetect\n", - "from labellerr.services.video_sampling.ffmpeg import FFMPEGSceneDetect" - ] - }, - { - "cell_type": "markdown", - "id": "db88da50", - "metadata": {}, - "source": [ - "## Scene Detection Implementation\n", - "\n", - "### Setting up the Scene Detector\n", - "Now we'll set up the scene detection process:\n", - "\n", - "1. First, we'll define the dataset directory where our videos are stored\n", - "2. Then we'll create an instance of our chosen detector\n", - "3. Finally, we'll process each video in the dataset\n", - "\n", - "Note: Make sure you have sufficient disk space for storing the extracted scenes, as this process can generate multiple files per video." - ] - }, { "cell_type": "code", "execution_count": null, - "id": "49a6f89d", + "id": "fd0febab", "metadata": {}, "outputs": [], "source": [ - "dataset_dir = f\".\\Labellerr_datasets\\{dataset_id}\"" + "# !pip install opencv-python pillow scenedetect scikit-image" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "dd96be8c", - "metadata": {}, - "outputs": [], - "source": [ - "detector = FFMPEGSceneDetect()" - ] - }, - { - "cell_type": "markdown", - "id": "995d99ec", - "metadata": {}, - "source": [ - "### Initialize the Scene Detector\n", - "Here we create an instance of the SSIMSceneDetect class. This detector uses the Structural Similarity Index Measure (SSIM) to identify scene changes in videos. SSIM is particularly effective at detecting subtle changes between frames." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "a3052f25", + "execution_count": null, + "id": "f5c41073", "metadata": {}, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "Keyframes extracted to FFMPEG_detects\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\frames\n", - "JSON mapping saved to: FFMPEG_detects\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\c44f38f6-0186-436f-8c2d-ffb50a539c76_mapping.json\n" + "d:\\Professional\\Labellerr_SDK\\.venv\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" ] } ], "source": [ - "for filename in os.listdir(dataset_dir):\n", - " file_path = os.path.join(dataset_dir, filename)\n", - " \n", - " if os.path.isfile(file_path):\n", - " detector.detect_and_extract(file_path)" - ] - }, - { - "cell_type": "markdown", - "id": "8d64ac26", - "metadata": {}, - "source": [ - "### Process Videos for Scene Detection\n", - "\n", - "The following code block:\n", - "1. Iterates through all files in the dataset directory\n", - "2. Constructs the full file path for each video\n", - "3. Verifies that each path points to a file (not a directory)\n", - "4. Applies scene detection to each video using the `detect_and_extract` method\n", - "\n", - "The detected scenes will be saved in a subdirectory with the same name as the input video file. Each scene will be saved as a separate video file." - ] - }, - { - "cell_type": "markdown", - "id": "6c3eac46", - "metadata": {}, - "source": [ - "## 5. Project Creation\n", - "\n", - "In this section, we'll explore how to create and manage projects in Labellerr. Projects are essential containers that organize your data and annotations. We'll cover:\n", - "\n", - "1. Creating image datasets from video frames\n", - "2. Setting up annotation projects\n", - "3. Managing project configurations" + "from labellerr.services.video_sampling import PySceneDetect" ] }, { "cell_type": "markdown", - "id": "f5ba527d", + "id": "db88da50", "metadata": {}, "source": [ - "### Image Dataset Creation from Sampled Frames\n" + "### Scene Detection Implementation\n" ] }, { "cell_type": "code", - "execution_count": 13, - "id": "1b364362", + "execution_count": 8, + "id": "49a6f89d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Found 52 image files\n" + "Path exists ✅\n" ] } ], "source": [ - "import os\n", - "\n", - "images_files = []\n", - "# Clear existing entries in images_files\n", - "images_files.clear()\n", - "\n", - "# Construct the base directory path for detected frames\n", - "base_dir = os.path.join(\"FFMPEG_detects\", dataset_id)\n", + "dataset_dir = Path(f\".\\\\Labellerr_datasets\\\\{dataset.dataset_id}\")\n", "\n", - "# Walk through all subdirectories\n", - "for root, dirs, files in os.walk(base_dir):\n", - " for file in files:\n", - " if file.endswith('.jpg'): # Only collect jpg files\n", - " file_path = os.path.join(root, file)\n", - " images_files.append(file_path)\n", - "\n", - "print(f\"Found {len(images_files)} image files\")" + "if dataset_dir.exists():\n", + " print(\"Path exists ✅\")\n", + "else:\n", + " print(\"Path does not exist ❌\")\n" ] }, { "cell_type": "code", "execution_count": 14, - "id": "f39153ab", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\0.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1008.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1016.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1028.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1060.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1082.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1106.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1119.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1137.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1157.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1175.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1189.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\119.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1201.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1218.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1233.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1246.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1257.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1278.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1312.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1319.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\141.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\233.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\263.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\37.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\381.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\408.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\437.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\457.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\484.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\508.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\552.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\575.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\590.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\619.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\63.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\647.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\683.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\706.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\721.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\758.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\776.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\805.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\823.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\83.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\836.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\858.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\876.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\893.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\915.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\949.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\99.jpg']" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "images_files" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "40c70986", + "id": "dd96be8c", "metadata": {}, "outputs": [], "source": [ - "# code to create dataset from sampled frames\n", - "\n", - "def upload_images_from_files(images_files, client, client_id):\n", - " \"\"\"Upload specific image files to create a dataset\"\"\"\n", - " \n", - " client.enable_connection_pooling = True\n", - " \n", - " dataset_config = {\n", - " \"client_id\": client_id,\n", - " \"dataset_name\": \"video_sampling_1\",\n", - " \"dataset_description\": \"video sampling dataset from frames\",\n", - " \"data_type\": \"image\", \n", - " }\n", - " \n", - " try:\n", - " response = client.create_dataset(\n", - " dataset_config=dataset_config,\n", - " files_to_upload=images_files \n", - " )\n", - " print(f\"Dataset created successfully!\")\n", - " print(f\"Dataset ID: {response['dataset_id']}\")\n", - " return response['dataset_id']\n", - " except Exception as e:\n", - " print(f\"Error creating dataset: {e}\")" + "detector = PySceneDetect()" ] }, { "cell_type": "code", - "execution_count": 16, - "id": "a1d96b25", + "execution_count": 15, + "id": "a3052f25", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Dataset created successfully!\n", - "Dataset ID: 6a680901-fe81-49f0-9120-bb754d63a341\n" + "JSON mapping saved to: PyScene_detects\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c_mapping.json\n", + "JSON mapping saved to: PyScene_detects\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265_mapping.json\n" ] - }, - { - "data": { - "text/plain": [ - "'6a680901-fe81-49f0-9120-bb754d63a341'" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "upload_images_from_files(images_files, client, client_id)" + "for filename in os.listdir(dataset_dir):\n", + " file_path = os.path.join(dataset_dir, filename)\n", + " \n", + " if os.path.isfile(file_path):\n", + " detector.detect_and_extract(file_path)" ] }, { - "cell_type": "code", - "execution_count": 19, - "id": "958fc75e", + "cell_type": "markdown", + "id": "6c3eac46", "metadata": {}, - "outputs": [], "source": [ - "new_dataset_id = '6a680901-fe81-49f0-9120-bb754d63a341'" + "---\n", + "## ***Image Project Creation***\n", + "\n", + "Create Image project of extracted keyframe from video" ] }, { "cell_type": "markdown", - "id": "b454c4f4", + "id": "f5dba054", "metadata": {}, "source": [ - "### Image Annotation Project Creation" + "### Create Labellerr Dataset of keyframe" ] }, { "cell_type": "code", "execution_count": null, - "id": "d32106c5", + "id": "82629d12", "metadata": {}, "outputs": [], "source": [ - "# modify to add questions to image project\n", - "\n", - "questions = [\n", - " {\n", - " \"question_number\": 1,\n", - " \"question\": \"Test\",\n", - " \"question_id\": \"533bb0c8-fb2b-4394-a8e1-5042a944802f\",\n", - " \"option_type\": \"polygon\",\n", - " \"required\": True,\n", - " \"options\": [\n", - " { \"option_name\": \"#fe1236\" }\n", - " ],\n", - " \"question_metadata\": []\n", - " }\n", - " ]\n" + "dataset = create_dataset_from_local(\n", + " client=client,\n", + " dataset_config=DatasetConfig(dataset_name=\"SDK VIDEO KEYFRAME DATASET\", \n", + " data_type=\"image\"),\n", + " folder_to_upload=dataset_dir,\n", + " )" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "b71d2aa0", + "cell_type": "markdown", + "id": "756764dd", "metadata": {}, - "outputs": [], "source": [ - "# creeate the annotation guideline template\n", - "\n", - "template_id = client.create_annotation_guideline(\n", - " client_id=client_id,\n", - " questions=questions,\n", - " template_name=\"video_sampling_template_1\",\n", - " data_type=\"image\",\n", - ")\n" + "### Create Annotation template of Keyframe Image Project" ] }, { "cell_type": "code", "execution_count": null, - "id": "83565ec3", + "id": "5d5e23c5", "metadata": {}, "outputs": [], "source": [ - "# create the image annotation project\n", - "\n", - "response = client.create_project(\n", - " project_name=\"Video_sampling_project_1\",\n", - " data_type=\"image\",\n", - " client_id= client_id,\n", - " dataset_id=new_dataset_id,\n", - " annotation_template_id=template_id,\n", - " rotation_config={\n", - " \"annotation_rotation_count\": 1,\n", - " \"review_rotation_count\": 1,\n", - " \"client_review_rotation_count\": 1,\n", - " },\n", - " created_by=\"yashsuman15@gmail.com\"\n", - " )\n" + "template = create_template(\n", + " client=client,\n", + " params=CreateTemplateParams(\n", + " template_name=\"SDK VIDEO KEYFRAME DATASET\",\n", + " data_type=DatasetDataType.image,\n", + " questions=[\n", + " AnnotationQuestion(\n", + " question_number=1,\n", + " question=\"Class polygon \",\n", + " question_id=str(uuid.uuid4()),\n", + " question_type=QuestionType.polygon,\n", + " required=True,\n", + " color=\"#FF0000\"\n", + " )\n", + " ]\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1bef8bfe", + "metadata": {}, + "source": [ + "### Create Image Annotation Project" ] }, { "cell_type": "code", - "execution_count": 31, - "id": "9f682f4f", + "execution_count": null, + "id": "b235e08d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Project created successfully!\n", - "Project ID: sherri_puny_rattlesnake_84247\n" - ] - } - ], + "outputs": [], "source": [ - "if response['response']['project_id']:\n", - " print(f\"Project created successfully!\")\n", - " print(f\"Project ID: {response['response']['project_id']}\")\n", - " image_project_id = response['response']['project_id']" + "img_project = create_project(\n", + " client=client,\n", + " params=CreateProjectParams(\n", + " project_name=\"SDK VIDEO PROJECT\",\n", + " data_type=DatasetDataType.video,\n", + " rotations=RotationConfig(\n", + " annotation_rotation_count=1,\n", + " review_rotation_count=1,\n", + " client_review_rotation_count=1\n", + " )\n", + " ),\n", + " datasets=[dataset],\n", + " annotation_template=template\n", + ")" ] }, { @@ -626,7 +755,8 @@ "id": "8645aa60", "metadata": {}, "source": [ - "## 6. Performing Annotations of Image Project" + "---\n", + "## ***Performing Annotations of Keyframe Image Project***" ] }, { @@ -644,71 +774,66 @@ "id": "8f0611f5", "metadata": {}, "source": [ - "### Exporting the Annotation Data" + "### Downloading the Annotation" ] }, { "cell_type": "code", "execution_count": null, - "id": "b78ad296", + "id": "ccf0e882", "metadata": {}, "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "18529760", + "metadata": {}, "source": [ - "# code to export the annotations from image project\n", - "\n", - "export_config = {\n", - " \"export_name\": \"Weekly Export\",\n", - " \"export_description\": \"Export of all accepted annotations\",\n", - " \"export_format\": \"coco_json\",\n", - " \"statuses\": [\n", - " \"review\",\n", - " \"r_assigned\",\n", - " \"client_review\",\n", - " \"cr_assigned\",\n", - " \"accepted\",\n", - " ],\n", - " }\n", - "\n", - "\n", - "response = client.create_local_export(\n", - " project_id=image_project_id,\n", - " client_id=client_id,\n", - " export_config=export_config\n", - ")" + "---\n", + "## ***Uploading KeyFrames Pre-Annotation to Video Project***" ] }, { "cell_type": "markdown", - "id": "18529760", + "id": "5c5ed594", "metadata": {}, "source": [ - "## 7. Uploading annotations to Video Project" + "### Converting Annotation JSON to required format" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "14b59cef", + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "id": "deae26b8", "metadata": {}, "source": [ - "### Trigger SAM2 tracking on Video annotation project\n", - "\n", - "Using the export, retrive the prompt to run SAM2 tracking on video" + "### Uploading pre-annotation" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "df6b3ac7", "metadata": {}, "outputs": [], "source": [ - "# code to create video annotation project from image annotations export" + "VIDEO_JSON_PATH = r\"path_to_your_video_preannotation_file.json\"\n", + "\n", + "video_project.upload_preannotations(video_json_file_path=VIDEO_JSON_PATH)" ] } ], "metadata": { "kernelspec": { - "display_name": "SDk", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -722,7 +847,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.18" + "version": "3.12.0" } }, "nbformat": 4, diff --git a/labellerr/notebooks/test_preannotation_api.py b/labellerr/notebooks/test_preannotation_api.py new file mode 100644 index 0000000..e82c9a9 --- /dev/null +++ b/labellerr/notebooks/test_preannotation_api.py @@ -0,0 +1,34 @@ +import os + +from dotenv import load_dotenv + +from labellerr.client import LabellerrClient +from labellerr.core.projects.video_project import LabellerrProject + +load_dotenv() + +API_KEY = os.getenv("QA_API_KEY") +API_SECRET = os.getenv("QA_API_SECRET") +CLIENT_ID = os.getenv("QA_CLIENT_ID") + +PROJECT_ID = "jeanna_mixed_aphid_93841" +VIDEO_JSON_FILE_PATH = r"C:\Users\yashs\Downloads\dumy_anotation.json" + + +def main(): + + client = LabellerrClient( + api_key=API_KEY, api_secret=API_SECRET, client_id=CLIENT_ID + ) + + project = LabellerrProject(client=client, project_id=PROJECT_ID) + + response = project.upload_keyframe_preannotations( + video_json_file_path=VIDEO_JSON_FILE_PATH + ) + + print(response) + + +if __name__ == "__main__": + main() From 722769645f8f30bc2a596ef7296722704abbf1ea Mon Sep 17 00:00:00 2001 From: yashsuman Date: Mon, 1 Dec 2025 11:24:51 +0530 Subject: [PATCH 02/14] Add test script for preannotation API functionality - Created a new script `test_preannotation_api.py` to test the upload of keyframe preannotations. - added method for pre-annotation upload to video project - modify SDK notebook accordingly --- labellerr/core/schemas/annotation_templates.py | 10 ++++++---- labellerr/notebooks/test_preannotation_api.py | 15 ++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/labellerr/core/schemas/annotation_templates.py b/labellerr/core/schemas/annotation_templates.py index 4885737..8120ce9 100644 --- a/labellerr/core/schemas/annotation_templates.py +++ b/labellerr/core/schemas/annotation_templates.py @@ -1,8 +1,10 @@ -from pydantic import BaseModel, Field -from typing import List, Optional -from enum import Enum -from ..schemas import DatasetDataType import uuid +from enum import Enum +from typing import List, Optional + +from pydantic import BaseModel, Field + +from .base import DatasetDataType class QuestionType(str, Enum): diff --git a/labellerr/notebooks/test_preannotation_api.py b/labellerr/notebooks/test_preannotation_api.py index e82c9a9..9771111 100644 --- a/labellerr/notebooks/test_preannotation_api.py +++ b/labellerr/notebooks/test_preannotation_api.py @@ -5,14 +5,18 @@ from labellerr.client import LabellerrClient from labellerr.core.projects.video_project import LabellerrProject -load_dotenv() +print(os.path.exists(r"labellerr\notebooks\dev.env")) +load_dotenv(r"labellerr\notebooks\dev.env") API_KEY = os.getenv("QA_API_KEY") API_SECRET = os.getenv("QA_API_SECRET") CLIENT_ID = os.getenv("QA_CLIENT_ID") +# print(API_KEY) +# print(API_SECRET) +# print(CLIENT_ID) PROJECT_ID = "jeanna_mixed_aphid_93841" -VIDEO_JSON_FILE_PATH = r"C:\Users\yashs\Downloads\dumy_anotation.json" +VIDEO_JSON_FILE_PATH = r"D:\Professional\Labellerr_SDK\dumy_anotation.json" def main(): @@ -23,10 +27,11 @@ def main(): project = LabellerrProject(client=client, project_id=PROJECT_ID) - response = project.upload_keyframe_preannotations( - video_json_file_path=VIDEO_JSON_FILE_PATH - ) + print(project.project_id) + response = project.upload_preannotations( + annotation_format="video_json", annotation_file=VIDEO_JSON_FILE_PATH + ) print(response) From 677e2e2aad8fb73a40f9d4a15d3ca9f6aba19701 Mon Sep 17 00:00:00 2001 From: yashsuman Date: Tue, 2 Dec 2025 12:24:17 +0530 Subject: [PATCH 03/14] minor changes --- .gitignore | 9 ++++++++ labellerr/notebooks/SDK.ipynb | 21 ++++++++++++++----- labellerr/notebooks/test_preannotation_api.py | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 1812a6f..fffd143 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,12 @@ download labellerr/__pycache__/ env.* claude.md +labellerr/notebooks/Labellerr_datasets/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4 +labellerr/notebooks/Labellerr_datasets/354681d3-034a-4d66-b070-365f4bd11d8a/7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4 +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c_mapping.json +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/316.jpg +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/7db3f60c-f6e5-4d3d-a63b-cb38530ee265/7db3f60c-f6e5-4d3d-a63b-cb38530ee265_mapping.json +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/1406.jpg +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/1064.jpg +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/760.jpg +labellerr/notebooks/dev.env diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index 97f1426..efad8fb 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -54,13 +54,13 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "ab12f168", "metadata": {}, "outputs": [], "source": [ "from dotenv import dotenv_values\n", - "config = dotenv_values(\".env\")\n", + "config = dotenv_values(\"dev.env\")\n", "\n", "api_key = config[\"API_KEY\"]\n", "api_secret = config[\"API_SECRET\"]\n", @@ -297,7 +297,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "7be23d9a", "metadata": {}, "outputs": [], @@ -323,10 +323,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "7e281c33", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'cea9f8f1-11cb-472f-97b1-e2619be47051'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "template.annotation_template_id" ] diff --git a/labellerr/notebooks/test_preannotation_api.py b/labellerr/notebooks/test_preannotation_api.py index 9771111..fb60c02 100644 --- a/labellerr/notebooks/test_preannotation_api.py +++ b/labellerr/notebooks/test_preannotation_api.py @@ -5,7 +5,7 @@ from labellerr.client import LabellerrClient from labellerr.core.projects.video_project import LabellerrProject -print(os.path.exists(r"labellerr\notebooks\dev.env")) +# Load environment variables from .env file load_dotenv(r"labellerr\notebooks\dev.env") API_KEY = os.getenv("QA_API_KEY") From a2cadb66bd88d9ba1ae6cc2a781f085d3fcaff67 Mon Sep 17 00:00:00 2001 From: yashsuman Date: Tue, 18 Nov 2025 02:05:24 +0530 Subject: [PATCH 04/14] Add test script for preannotation API functionality - Created a new script to test the upload of keyframe preannotations. - added method for pre-annotation upload to video project - modify SDK notebook accordingly --- labellerr/core/constants.py | 2 +- labellerr/core/files/video_file.py | 9 +- labellerr/core/projects/video_project.py | 54 +- labellerr/core/schemas/__init__.py | 40 +- labellerr/core/schemas/projects.py | 1 - labellerr/notebooks/SDK.ipynb | 923 ++++++++++-------- labellerr/notebooks/test_preannotation_api.py | 34 + 7 files changed, 636 insertions(+), 427 deletions(-) create mode 100644 labellerr/notebooks/test_preannotation_api.py diff --git a/labellerr/core/constants.py b/labellerr/core/constants.py index 5484189..6d1893e 100644 --- a/labellerr/core/constants.py +++ b/labellerr/core/constants.py @@ -1,4 +1,4 @@ -BASE_URL = "https://api.labellerr.com" +BASE_URL = "https://api-gateway-qcb3iv2gaa-uc.a.run.app" ALLOWED_ORIGINS = "https://pro.labellerr.com" diff --git a/labellerr/core/files/video_file.py b/labellerr/core/files/video_file.py index cb97956..97138b3 100644 --- a/labellerr/core/files/video_file.py +++ b/labellerr/core/files/video_file.py @@ -4,7 +4,7 @@ import uuid from concurrent.futures import ThreadPoolExecutor, as_completed from threading import Lock -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import requests @@ -61,6 +61,7 @@ def get_frames(self, frame_start: int = 0, frame_end: int | None = None): "frame_end": frame_end, "project_id": self.project_id, "uuid": unique_id, + "client_id": self.client.client_id, } response = self.client.make_request( @@ -235,7 +236,7 @@ def create_video( raise LabellerrError(f"Error while joining frames: {str(e)}") def download_create_video_auto_cleanup( - self, output_folder: str = "./Labellerr_datastets" + self, output_folder: str = "./Labellerr_datasets" ): """ Download frames, create video, and automatically clean up temporary frames. @@ -258,6 +259,8 @@ def download_create_video_auto_cleanup( print(f"\n[1/4] Fetching frame data from API (0 to {total_frames})...") frames_data = self.get_frames(frame_start=0, frame_end=total_frames) + # print(frames_data) + if not frames_data: raise LabellerrError("No frame data retrieved from API") @@ -313,7 +316,7 @@ def download_create_video_auto_cleanup( print(f"\n{'='*60}") print("Processing complete!") print(f"Video saved to: {video_output_path}") - print("{'='*60}\n") + print(f"{'='*60}\n") return result diff --git a/labellerr/core/projects/video_project.py b/labellerr/core/projects/video_project.py index a70d21c..6d6b912 100644 --- a/labellerr/core/projects/video_project.py +++ b/labellerr/core/projects/video_project.py @@ -1,5 +1,6 @@ +import os import uuid -from typing import List +from typing import Any, Dict, List from .. import constants from ..exceptions import LabellerrError @@ -91,5 +92,56 @@ def delete_keyframes(self, file_id: str, keyframes: List[int]): except Exception as e: raise LabellerrError(f"Failed to delete key frames: {str(e)}") + def upload_keyframe_preannotations(self, video_json_file_path: str = None) -> Any: + """ + Uploads pre-annotations for video project. + + Supports both the parent signature and a video-specific signature for backward compatibility. + + :param annotation_format: (Deprecated) The format of the preannotation data + :param annotation_file: (Deprecated) The file path of the preannotation data + :param conf_bucket: (Deprecated) Confidence bucket [low, medium, high] + :param _async: (Deprecated) Whether to return a future object + :param video_json_file_path: Path to the video JSON file containing pre-annotations + :return: Response from the API + """ + # Support both old and new signatures + file_path = video_json_file_path + + # Parameter validation + if not isinstance(file_path, str): + raise LabellerrError("file_path must be a str") + + try: + # Validate if the file exists + if not os.path.exists(file_path): + raise LabellerrError(f"File not found: {file_path}") + + unique_id = str(uuid.uuid4()) + url = f"{constants.BASE_URL}/actions/upload_answers?project_id={self.project_id}&answer_format=video_json&client_id={self.client.client_id}&uuid={unique_id}" + + # Get file name from path + file_name = os.path.basename(file_path) + + # Open file and prepare multipart form data + with open(file_path, "rb") as f: + files = [("file", (file_name, f, "application/json"))] + payload: Dict[Any, Any] = {} + + response = self.client.make_request( + "POST", + url, + request_id=unique_id, + handle_response=False, + data=payload, + files=files, + ) + + return self.client.handle_upload_response(response, unique_id) + except LabellerrError: + raise + except Exception as e: + raise LabellerrError(f"Failed to upload pre-annotations: {str(e)}") + LabellerrProjectMeta._register(DatasetDataType.video, VideoProject) diff --git a/labellerr/core/schemas/__init__.py b/labellerr/core/schemas/__init__.py index 5579571..ab09f11 100644 --- a/labellerr/core/schemas/__init__.py +++ b/labellerr/core/schemas/__init__.py @@ -14,18 +14,29 @@ # Import from autolabel.typings for backward compatibility from labellerr.core.autolabel.typings import * # noqa: F403, F401 +# Export annotation templates +from labellerr.core.schemas.annotation_templates import ( + AnnotationQuestion, + CreateTemplateParams, + Option, + QuestionType, +) + +# Autolabel schemas +from labellerr.core.schemas.autolabel import Hyperparameters, KeyFrame, TrainingRequest + # Base custom types from labellerr.core.schemas.base import DirPathStr, FilePathStr, NonEmptyStr # Connection schemas from labellerr.core.schemas.connectors import ( AWSConnectionParams, - DatasetDataType, - DeleteConnectionParams, - GCSConnectionParams, AWSConnectionTestParams, ConnectionType, ConnectorType, + DatasetDataType, + DeleteConnectionParams, + GCSConnectionParams, GCSConnectionTestParams, ) @@ -43,6 +54,9 @@ UploadFilesParams, ) +# Export schemas +from labellerr.core.schemas.exports import CreateExportParams, ExportDestination + # File operation schemas from labellerr.core.schemas.files import BulkAssignFilesParams, ListFileParams @@ -65,25 +79,6 @@ UpdateUserRoleParams, ) -# Autolabel schemas -from labellerr.core.schemas.autolabel import ( - Hyperparameters, - KeyFrame, - TrainingRequest, -) - -# Export schemas -from labellerr.core.schemas.exports import CreateExportParams, ExportDestination - - -# Export annotation templates -from labellerr.core.schemas.annotation_templates import ( - AnnotationQuestion, - Option, - QuestionType, -) - - __all__ = [ # Base types "NonEmptyStr", @@ -136,4 +131,5 @@ "AnnotationQuestion", "Option", "QuestionType", + "CreateTemplateParams", ] diff --git a/labellerr/core/schemas/projects.py b/labellerr/core/schemas/projects.py index 830853b..a70f0b5 100644 --- a/labellerr/core/schemas/projects.py +++ b/labellerr/core/schemas/projects.py @@ -51,7 +51,6 @@ class CreateProjectParams(BaseModel): class CreateTemplateParams(BaseModel): """Parameters for creating an annotation template.""" - client_id: str = Field(min_length=1) data_type: Literal["image", "video", "audio", "document", "text"] template_name: str = Field(min_length=1) questions: List[Question] = Field(min_length=1) diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index 1efd28f..97f1426 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -5,12 +5,9 @@ "id": "d6488b6b", "metadata": {}, "source": [ - "# Getting Started with Labellerr SDK\n", + "# Keyframe Scene detection with Labellerr SDK\n", "\n", - "This notebook demonstrates how to use the Labellerr SDK for video processing and scene detection. The SDK provides powerful tools for managing video datasets, processing videos, and detecting scene changes using various algorithms.\n", - "\n", - "### Import the required Classes from Labellerr SDK\n", - "We'll start by importing the essential classes needed for working with the SDK:" + "This notebook demonstrates how to use the Labellerr SDK for video processing and scene detection. The SDK provides powerful tools for managing video datasets, processing videos, and detecting scene changes using various algorithms.\n" ] }, { @@ -21,9 +18,16 @@ "outputs": [], "source": [ "from labellerr.client import LabellerrClient\n", - "from labellerr.core.datasets import LabellerrDataset\n", - "import os\n", - "from tqdm.notebook import tqdm\n" + "from labellerr.core.datasets import create_dataset_from_local, LabellerrDataset\n", + "from labellerr.core.annotation_templates import create_template\n", + "from labellerr.core.projects import create_project\n", + "from labellerr.core.schemas.annotation_templates import AnnotationQuestion, QuestionType, CreateTemplateParams, DatasetDataType\n", + "from labellerr.core.schemas import DatasetConfig\n", + "from labellerr.core.schemas.projects import CreateProjectParams, RotationConfig\n", + "\n", + "import uuid\n", + "from pathlib import Path\n", + "import os\n" ] }, { @@ -31,7 +35,8 @@ "id": "84b7917a", "metadata": {}, "source": [ - "## 1. Authentication Setup\n", + "---\n", + "## ***Authentication Setup***\n", "\n", "Before using the Labellerr SDK, you need to set up your authentication credentials. These credentials ensure secure access to the Labellerr platform and its services.\n", "\n", @@ -44,9 +49,7 @@ "\n", "2. **Client ID**\n", " - This is a unique identifier for your application\n", - " - Contact Labellerr support to obtain your client ID\n", - " \n", - "⚠️ Important: Never share these credentials or commit them to version control." + " - Contact Labellerr support to obtain your client ID\n" ] }, { @@ -61,71 +64,362 @@ "\n", "api_key = config[\"API_KEY\"]\n", "api_secret = config[\"API_SECRET\"]\n", - "client_id = config[\"CLIENT_ID\"]" + "client_id = config[\"CLIENT_ID\"]\n", + "email = config[\"EMAIL\"]\n", + "\n", + "client = LabellerrClient(api_key, api_secret, client_id)" ] }, { "cell_type": "markdown", - "id": "3d05bd0f", + "id": "d2646549", + "metadata": {}, + "source": [ + "---\n", + "## ***Kaggle Dataset Download***" + ] + }, + { + "cell_type": "markdown", + "id": "c2d2a744", "metadata": {}, "source": [ - "## 2. Project Configuration\n", + "Before downloading the dataset from Kaggle, you need to:\n", "\n", - "### Dataset and Project IDs\n", - "To work with specific datasets and projects in Labellerr, you need their respective IDs. These IDs are unique identifiers that link your code to the correct resources on the platform.\n", + "1. Install kagglehub package using pip\n", + "2. Authenticate with Kaggle\n", + "3. Download the CCTV footage dataset\n", "\n", - "How to obtain the IDs:\n", - "1. Go to the Labellerr platform\n", - "2. Create or select an existing dataset\n", - "3. Create or select an existing project\n", - "4. Copy the dataset_id and project_id from their respective pages\n", + "The kagglehub package provides a simple interface to download datasets directly from Kaggle. Make sure you have a Kaggle account and API credentials set up before proceeding.\n", "\n", - "Note: The dataset_id is a UUID format string, while the project_id is typically a human-readable string." + "Note: If you haven't set up Kaggle authentication before, you'll need to:\n", + "1. Create a Kaggle account at https://www.kaggle.com\n", + "2. Go to \"Account\" settings\n", + "3. Scroll to API section and click \"Create New API Token\"\n", + "4. This will download a kaggle.json file with your credentials" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "07dcfae9", + "execution_count": null, + "id": "be12bf3f", "metadata": {}, "outputs": [], "source": [ - "# go to our platform to create dataset and project then get their ids\n", - "dataset_id = \"16257fd6-b91b-4d00-a680-9ece9f3f241c\"\n", - "project_id = \"gabrila_artificial_duck_74237\"" + "# !pip install kagglehub ipywidgets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e05889d7", + "metadata": {}, + "outputs": [], + "source": [ + "import kagglehub\n", + "\n", + "kagglehub.login()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5b93e97", + "metadata": {}, + "outputs": [], + "source": [ + "# Download 1000 videos(~1 min) dataset\n", + "\n", + "# large video dataset(1000 videos)\n", + "# path_to_dataset = kagglehub.dataset_download(\"yashsuman/cctv-footage\")\n", + "\n", + "# small video dataset(5 videos)\n", + "path_to_dataset = kagglehub.dataset_download(\"mistag/short-videos\")\n", + "\n", + "print(\"Path to dataset files:\", path_to_dataset)" ] }, { "cell_type": "markdown", - "id": "1b2c7aee", + "id": "3d05bd0f", "metadata": {}, "source": [ - "## 3. Initializing the Labellerr SDK\n", + "---\n", + "## ***Video Project Creation***\n", "\n", - "### Create LabellerrClient Instance\n", - "Now we'll create instances of the main SDK classes:\n", + "Create a Labellerr Video Project with kaggle dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "52e00dbc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DATASET_PATH = path_to_dataset\n", + "KAGGLE_DATASET_PATH = Path(r\"..\\..\\..\\.cache\\kagglehub\\datasets\\mistag\\short-videos\\versions\\4\")\n", + "\n", + "KAGGLE_DATASET_PATH.exists()" + ] + }, + { + "cell_type": "markdown", + "id": "d861c36f", + "metadata": {}, + "source": [ + "### Create Labellerr Dataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1e2e2f3", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:root:Total file count: 2\n", + "INFO:root:Total file size: 42.2 MB\n", + "INFO:root:CPU count: 24, Batch Count: 2\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api-gateway-qcb3iv2gaa-uc.a.run.app:443\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api-gateway-qcb3iv2gaa-uc.a.run.app:443\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"POST /connectors/connect/local?client_id=1 HTTP/1.1\" 200 1085\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"POST /connectors/connect/local?client_id=1 HTTP/1.1\" 200 1079\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", + "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"POST /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/seafood_1280p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101252Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=abb81e14c8815af5553818d4456594c59e80daf8d1843c4b822a277b2191904a53f0449c4da0dafb04be6055d47e63cb7dc1e6a99bd466bb1a82aa7e8484af661e126917a444fbbaa03532224f7915f061f16860c4a4e7010846733d48f13be90e0af96ed39e155e85644661f59373e15619c7a8e6d1feb9bb2b7e8ce82192acbc86d98a4ef48463717bf14611a1a90b9b16245a9fb0cf7e38962dcf3761c5fddf82b0d141f5eff7f0b0f4d0743957af87a9606563e99ca41b6a48d2961fb8cb70bf2408e8a669b5d42f85b3d4bffaaf488430c5b71df018bd39d104b6a3f3114d669b8dd29762f433999c8f7ac1ef35f653c614522e1ff934690129fd4c6060 HTTP/1.1\" 201 0\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", + "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"POST /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/butterflies_960p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101251Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=23e640fedb8bde8db621437d85075adb326eccb5969891a150fe3a4c2769170332ddef85aee49e81168dba63ffbbb021a4e8290ff77eb73daaff6e56c7d0087edd9502a2bfd6e31404e38ba02424e4e2ea6b52106a2681aae5c9e76c0e19b0d8c198811c577f6408eceb1a04ac8d21d1f05608e83114eb56a09c078733e0ae2c5c71294ba0f7ab6c04a0bf158a289f3c300b619e5b1b39382370b2457cade3987eac9f304dc52cb975ae2ce45a628a76496846d80c7aff125afbd8ab999edfeca42a296aff65bf3c8c7f6f48c30f6f4c9caf2fa917e4c0261303a325db68e31401816fe3cf20693459417156c41aec770ffa5e2ee88b77e5d746dacee71225a2 HTTP/1.1\" 201 0\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", + "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"PUT /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/butterflies_960p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101251Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=23e640fedb8bde8db621437d85075adb326eccb5969891a150fe3a4c2769170332ddef85aee49e81168dba63ffbbb021a4e8290ff77eb73daaff6e56c7d0087edd9502a2bfd6e31404e38ba02424e4e2ea6b52106a2681aae5c9e76c0e19b0d8c198811c577f6408eceb1a04ac8d21d1f05608e83114eb56a09c078733e0ae2c5c71294ba0f7ab6c04a0bf158a289f3c300b619e5b1b39382370b2457cade3987eac9f304dc52cb975ae2ce45a628a76496846d80c7aff125afbd8ab999edfeca42a296aff65bf3c8c7f6f48c30f6f4c9caf2fa917e4c0261303a325db68e31401816fe3cf20693459417156c41aec770ffa5e2ee88b77e5d746dacee71225a2&upload_id=AOCedOEk1QTBtYylxPr3uH4ac4enQIiFVX-WRK66hGbV0b3MSd0MYrQUnrEUsOmbR2ZkjlujH3zdLrGX2v11KdAVScEBhe_qr_4nQdUqck5u_nk HTTP/1.1\" 200 0\n", + "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"PUT /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/seafood_1280p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101252Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=abb81e14c8815af5553818d4456594c59e80daf8d1843c4b822a277b2191904a53f0449c4da0dafb04be6055d47e63cb7dc1e6a99bd466bb1a82aa7e8484af661e126917a444fbbaa03532224f7915f061f16860c4a4e7010846733d48f13be90e0af96ed39e155e85644661f59373e15619c7a8e6d1feb9bb2b7e8ce82192acbc86d98a4ef48463717bf14611a1a90b9b16245a9fb0cf7e38962dcf3761c5fddf82b0d141f5eff7f0b0f4d0743957af87a9606563e99ca41b6a48d2961fb8cb70bf2408e8a669b5d42f85b3d4bffaaf488430c5b71df018bd39d104b6a3f3114d669b8dd29762f433999c8f7ac1ef35f653c614522e1ff934690129fd4c6060&upload_id=AOCedOGtu8tVgMoqoFCcI18CqQhY_CARXt2x5laIObdMBYybxcvoZxuxl109xF29WQm1LRs4fg0cgVABEFGzBSEtpQfhE8mQNrmK8wWicqyBIXw HTTP/1.1\" 200 0\n", + "INFO:root:Folder uploaded successfully. {'success': ['..\\\\..\\\\..\\\\.cache\\\\kagglehub\\\\datasets\\\\mistag\\\\short-videos\\\\versions\\\\4\\\\butterflies_960p.mp4', '..\\\\..\\\\..\\\\.cache\\\\kagglehub\\\\datasets\\\\mistag\\\\short-videos\\\\versions\\\\4\\\\seafood_1280p.mp4'], 'fail': []}\n", + "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api-gateway-qcb3iv2gaa-uc.a.run.app:443\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"POST /datasets/create?client_id=1&uuid=7f7f78dc-4c7f-4ab3-a5f3-7bc308733a98 HTTP/1.1\" 200 361\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=a3768b9c-17d2-40f0-8f06-4b0538b8f0db HTTP/1.1\" 200 361\n" + ] + } + ], + "source": [ + "# import logging\n", + "\n", + "# logging.basicConfig(level=logging.DEBUG)\n", + "# logger = logging.getLogger(__name__)\n", "\n", - "1. **LabellerrClient**: The main client that handles communication with the Labellerr API\n", - "2. **LabellerrDataset**: A specialized class for working with datasets\n", "\n", - "These instances will be used for all subsequent operations with the platform." + "dataset = create_dataset_from_local(\n", + " client=client,\n", + " dataset_config=DatasetConfig(dataset_name=\"SDK VIDEO DATASET\", \n", + " data_type=\"video\"),\n", + " folder_to_upload=KAGGLE_DATASET_PATH,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "3d82b343", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=d4eef5b7-31da-4188-87d1-cb42176f6f5a HTTP/1.1\" 200 361\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=85af0df1-2466-4987-bda3-36bf7bc8f87b HTTP/1.1\" 200 361\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=d73a6c0a-9077-49f0-939b-1ea2cb4bb04d HTTP/1.1\" 200 361\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=81833057-1650-425a-b547-94fae62e7de9 HTTP/1.1\" 200 422\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=9b4ea0fe-6ed8-48f0-adb4-f5da409a70a0 HTTP/1.1\" 200 422\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=7f4b488b-2aa4-4fb2-ba95-a6c08e678f2c HTTP/1.1\" 200 422\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=a9686c6c-c8b1-4abe-9809-2551c2ded379 HTTP/1.1\" 200 422\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=6b1de878-8a47-49c8-a64d-ec54647d7129 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=d3ff7197-37fa-4b0d-b70e-fbb9d1af6cf0 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=e9644e97-69ef-423c-8015-c69f42970687 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=1575a956-f3ea-4421-9db0-bb0b78aaa6b2 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=00e88efd-93c5-4f83-9b07-ccf5a009764c HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=30058fd5-1d2f-4306-84dd-79abd23eff15 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=fd2f80b2-c251-4e35-84ef-3eec7c96f20c HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=3f3d5118-6663-4573-a7b1-02ca22bb3588 HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=264d975f-1c10-4583-ae07-7f57534fad5f HTTP/1.1\" 200 438\n", + "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=bf74e8a6-4136-4cc5-b44d-329b0b4261e5 HTTP/1.1\" 200 643\n", + "INFO:root:Dataset 354681d3-034a-4d66-b070-365f4bd11d8a processing completed successfully!\n" + ] + }, + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset.status()\n", + "dataset.dataset_id\n", + "dataset.files_count" ] }, { "cell_type": "code", "execution_count": 4, - "id": "9eaec7e1", + "id": "5205f618", + "metadata": {}, + "outputs": [], + "source": [ + "dataset = LabellerrDataset(client=client,\n", + " dataset_id=\"354681d3-034a-4d66-b070-365f4bd11d8a\")" + ] + }, + { + "cell_type": "markdown", + "id": "1a1a37ec", + "metadata": {}, + "source": [ + "### Create Labellerr Annotation Template" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7be23d9a", + "metadata": {}, + "outputs": [], + "source": [ + "template = create_template(\n", + " client=client,\n", + " params=CreateTemplateParams(\n", + " template_name=\"SDK VIDEO TEMPLATE\",\n", + " data_type=DatasetDataType.video,\n", + " questions=[\n", + " AnnotationQuestion(\n", + " question_number=1,\n", + " question=\"Class polygon \",\n", + " question_id=str(uuid.uuid4()),\n", + " question_type=QuestionType.polygon,\n", + " required=True,\n", + " color=\"#FF0000\"\n", + " )\n", + " ]\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e281c33", "metadata": {}, "outputs": [], "source": [ - "client = LabellerrClient(api_key, api_secret, client_id) \n", - "dataset = LabellerrDataset(client, dataset_id, project_id)" + "template.annotation_template_id" ] }, { "cell_type": "code", "execution_count": 5, - "id": "7b6a7052", + "id": "62360ea1", + "metadata": {}, + "outputs": [], + "source": [ + "from labellerr.core.annotation_templates import LabellerrAnnotationTemplate\n", + "template = LabellerrAnnotationTemplate(client=client,\n", + " annotation_template_id='35d44c7d-9b02-4eb0-9dee-9a7ff1165331')" + ] + }, + { + "cell_type": "markdown", + "id": "a493938f", + "metadata": {}, + "source": [ + "### Create Labellerr Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8996dd01", + "metadata": {}, + "outputs": [], + "source": [ + "video_project = create_project(\n", + " client=client,\n", + " params=CreateProjectParams(\n", + " project_name=\"SDK VIDEO PROJECT\",\n", + " data_type=DatasetDataType.video,\n", + " rotations=RotationConfig(\n", + " annotation_rotation_count=1,\n", + " review_rotation_count=1,\n", + " client_review_rotation_count=1\n", + " )\n", + " ),\n", + " datasets=[dataset],\n", + " annotation_template=template\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "724c67cc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'gusella_late_marmoset_23922'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "video_project.project_id" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f74cba85", + "metadata": {}, + "outputs": [], + "source": [ + "from labellerr.core.projects import LabellerrProject\n", + "video_project = LabellerrProject(client=client,\n", + " project_id='gusella_late_marmoset_23922')" + ] + }, + { + "cell_type": "markdown", + "id": "0d806eaf", + "metadata": {}, + "source": [ + "---\n", + "## ***Download Labellerr Indexed Dataset***" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6558a9e9", "metadata": {}, "outputs": [ { @@ -134,71 +428,106 @@ "text": [ "\n", "######################################################################\n", - "# Starting batch video processing for dataset: 16257fd6-b91b-4d00-a680-9ece9f3f241c\n", + "# Starting batch video processing for dataset: 354681d3-034a-4d66-b070-365f4bd11d8a\n", "######################################################################\n", "\n", - "Total file IDs extracted: 1\n", - "\n", - "Creating LabellerrFile instances for 1 files...\n", - "Successfully created 1 LabellerrFile instances\n", + "Fetching files for dataset: 354681d3-034a-4d66-b070-365f4bd11d8a\n", + "{'message': '200: Success', 'response': {'files': [{'has_embedding': False, 'file_id': '2a8d96ca-9161-4dee-ad3b-a5faf301bc6c', 'created_at': 1763374470963, 'file_name_original': 'butterflies_960p.mp4', 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a', 'connection_id': 'fa03a1f3-3b77-42f9-b8de-eef499af4ee9', 'email_id': 'e0811e.ba8447468b95374970256d3c2b', 'file_name': 'butterflies_960p.mp4', 'file_reference': 'gs://labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/butterflies_960p.mp4', 'file_metadata': {'file_size': 25.047, 'image_width': None, 'file_format': 'mp4', 'additional_metadata': None, 'image_height': None}, 'created_by': 'e0811e.ba8447468b95374970256d3c2b', 'data_type': 'video'}, {'has_embedding': False, 'file_id': '7db3f60c-f6e5-4d3d-a63b-cb38530ee265', 'created_at': 1763374470963, 'file_name_original': 'seafood_1280p.mp4', 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a', 'connection_id': 'fa03a1f3-3b77-42f9-b8de-eef499af4ee9', 'email_id': 'e0811e.ba8447468b95374970256d3c2b', 'file_name': 'seafood_1280p.mp4', 'file_reference': 'gs://labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/seafood_1280p.mp4', 'file_metadata': {'file_size': 17.156, 'image_width': None, 'file_format': 'mp4', 'additional_metadata': None, 'image_height': None}, 'created_by': 'e0811e.ba8447468b95374970256d3c2b', 'data_type': 'video'}], 'total_count': 2, 'next_search_after': None}, 'error': None, 'tracking_id': '8d4669aea326833e9788f71d56c3a0ed'}\n", "\n", - "Processing 1 video files...\n", + "Processing 2 video files...\n", "\n", "\n", - "Starting download of 1 files...\n", + "Starting download of 2 files...\n", "\n", "============================================================\n", - "Processing file: c44f38f6-0186-436f-8c2d-ffb50a539c76\n", + "Processing file: 2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\n", "============================================================\n", "\n", - "[1/4] Fetching frame data from API (0 to 1440)...\n", - "Retrieved 1440 frames\n", + "[1/4] Fetching frame data from API (0 to 1572)...\n", + "Retrieved 1572 frames\n", "\n", "[2/4] Setting up output folders...\n", "\n", "[3/4] Downloading frames...\n", - "Starting download of 1440 frames...\n", - "Frames downloaded: 1440/1440 (1440 successful, 0 failed)\n", + "Starting download of 1572 frames...\n", + "Frames downloaded: 1572/1572 (1572 successful, 0 failed)\n", "\n", "[4/4] Creating video from frames...\n", - "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", - "Video saved as ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", + "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4\n", + "Video saved as ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4\n", "\n", "Cleaning up temporary frames...\n", - "Removed temporary frames folder: ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\n", + "Removed temporary frames folder: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\n", "\n", "============================================================\n", - "✓ Processing complete!\n", - "Video saved to: ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", + "Processing complete!\n", + "Video saved to: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4\n", + "{'='*60}\n", + "\n", + "Files processed: 1/2 (1 successful, 0 failed)\n", + "============================================================\n", + "Processing file: 7db3f60c-f6e5-4d3d-a63b-cb38530ee265\n", + "============================================================\n", + "\n", + "[1/4] Fetching frame data from API (0 to 389)...\n", + "Retrieved 389 frames\n", + "\n", + "[2/4] Setting up output folders...\n", + "\n", + "[3/4] Downloading frames...\n", + "Starting download of 389 frames...\n", + "Frames downloaded: 389/389 (389 successful, 0 failed)\n", + "\n", + "[4/4] Creating video from frames...\n", + "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4\n", + "Video saved as ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4\n", + "\n", + "Cleaning up temporary frames...\n", + "Removed temporary frames folder: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265\n", + "\n", "============================================================\n", + "Processing complete!\n", + "Video saved to: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4\n", + "{'='*60}\n", "\n", - "Files processed: 1/1 (1 successful, 0 failed)\n", + "Files processed: 2/2 (2 successful, 0 failed)\n", "######################################################################\n", "# Batch Processing Complete\n", - "# Total files: 1\n", - "# Successful: 1\n", + "# Total files: 2\n", + "# Successful: 2\n", "# Failed: 0\n", "######################################################################\n", "\n" ] + }, + { + "data": { + "text/plain": [ + "[{'status': 'success',\n", + " 'file_id': '2a8d96ca-9161-4dee-ad3b-a5faf301bc6c',\n", + " 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a',\n", + " 'video_path': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a\\\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4',\n", + " 'output_folder': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a',\n", + " 'frames_downloaded': 1572,\n", + " 'frames_failed': 0,\n", + " 'failed_frames_info': []},\n", + " {'status': 'success',\n", + " 'file_id': '7db3f60c-f6e5-4d3d-a63b-cb38530ee265',\n", + " 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a',\n", + " 'video_path': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a\\\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4',\n", + " 'output_folder': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a',\n", + " 'frames_downloaded': 389,\n", + " 'frames_failed': 0,\n", + " 'failed_frames_info': []}]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "results = dataset.download()" - ] - }, - { - "cell_type": "markdown", - "id": "900ea5a7", - "metadata": {}, - "source": [ - "### download Videos\n", - "The `download()` method will:\n", - "- Fetch all videos in the dataset\n", - "- Process them according to the configured settings\n", - "- Return the results of the processing\n", - "\n", - "This is typically used as the first step in video analysis to ensure all videos are properly prepared for further processing." + "dataset.download()" ] }, { @@ -206,9 +535,9 @@ "id": "f6db8522", "metadata": {}, "source": [ - "## 4. Scene Change Detection\n", + "---\n", + "## ***Scene Change Detection on Dataset***\n", "\n", - "### Available Scene Detection Methods\n", "Labellerr SDK provides multiple algorithms for scene detection in videos:\n", "\n", "1. **PySceneDetect**: \n", @@ -229,396 +558,196 @@ "Choose the method that best suits your needs based on accuracy requirements and processing speed constraints." ] }, - { - "cell_type": "code", - "execution_count": 6, - "id": "f5c41073", - "metadata": {}, - "outputs": [], - "source": [ - "from labellerr.services.video_sampling.pyscene_detect import PySceneDetect\n", - "from labellerr.services.video_sampling.ssim import SSIMSceneDetect\n", - "from labellerr.services.video_sampling.ffmpeg import FFMPEGSceneDetect" - ] - }, - { - "cell_type": "markdown", - "id": "db88da50", - "metadata": {}, - "source": [ - "## Scene Detection Implementation\n", - "\n", - "### Setting up the Scene Detector\n", - "Now we'll set up the scene detection process:\n", - "\n", - "1. First, we'll define the dataset directory where our videos are stored\n", - "2. Then we'll create an instance of our chosen detector\n", - "3. Finally, we'll process each video in the dataset\n", - "\n", - "Note: Make sure you have sufficient disk space for storing the extracted scenes, as this process can generate multiple files per video." - ] - }, { "cell_type": "code", "execution_count": null, - "id": "49a6f89d", + "id": "fd0febab", "metadata": {}, "outputs": [], "source": [ - "dataset_dir = f\".\\Labellerr_datasets\\{dataset_id}\"" + "# !pip install opencv-python pillow scenedetect scikit-image" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "dd96be8c", - "metadata": {}, - "outputs": [], - "source": [ - "detector = FFMPEGSceneDetect()" - ] - }, - { - "cell_type": "markdown", - "id": "995d99ec", - "metadata": {}, - "source": [ - "### Initialize the Scene Detector\n", - "Here we create an instance of the SSIMSceneDetect class. This detector uses the Structural Similarity Index Measure (SSIM) to identify scene changes in videos. SSIM is particularly effective at detecting subtle changes between frames." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "a3052f25", + "execution_count": null, + "id": "f5c41073", "metadata": {}, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "Keyframes extracted to FFMPEG_detects\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\frames\n", - "JSON mapping saved to: FFMPEG_detects\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\c44f38f6-0186-436f-8c2d-ffb50a539c76_mapping.json\n" + "d:\\Professional\\Labellerr_SDK\\.venv\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" ] } ], "source": [ - "for filename in os.listdir(dataset_dir):\n", - " file_path = os.path.join(dataset_dir, filename)\n", - " \n", - " if os.path.isfile(file_path):\n", - " detector.detect_and_extract(file_path)" - ] - }, - { - "cell_type": "markdown", - "id": "8d64ac26", - "metadata": {}, - "source": [ - "### Process Videos for Scene Detection\n", - "\n", - "The following code block:\n", - "1. Iterates through all files in the dataset directory\n", - "2. Constructs the full file path for each video\n", - "3. Verifies that each path points to a file (not a directory)\n", - "4. Applies scene detection to each video using the `detect_and_extract` method\n", - "\n", - "The detected scenes will be saved in a subdirectory with the same name as the input video file. Each scene will be saved as a separate video file." - ] - }, - { - "cell_type": "markdown", - "id": "6c3eac46", - "metadata": {}, - "source": [ - "## 5. Project Creation\n", - "\n", - "In this section, we'll explore how to create and manage projects in Labellerr. Projects are essential containers that organize your data and annotations. We'll cover:\n", - "\n", - "1. Creating image datasets from video frames\n", - "2. Setting up annotation projects\n", - "3. Managing project configurations" + "from labellerr.services.video_sampling import PySceneDetect" ] }, { "cell_type": "markdown", - "id": "f5ba527d", + "id": "db88da50", "metadata": {}, "source": [ - "### Image Dataset Creation from Sampled Frames\n" + "### Scene Detection Implementation\n" ] }, { "cell_type": "code", - "execution_count": 13, - "id": "1b364362", + "execution_count": 8, + "id": "49a6f89d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Found 52 image files\n" + "Path exists ✅\n" ] } ], "source": [ - "import os\n", - "\n", - "images_files = []\n", - "# Clear existing entries in images_files\n", - "images_files.clear()\n", - "\n", - "# Construct the base directory path for detected frames\n", - "base_dir = os.path.join(\"FFMPEG_detects\", dataset_id)\n", + "dataset_dir = Path(f\".\\\\Labellerr_datasets\\\\{dataset.dataset_id}\")\n", "\n", - "# Walk through all subdirectories\n", - "for root, dirs, files in os.walk(base_dir):\n", - " for file in files:\n", - " if file.endswith('.jpg'): # Only collect jpg files\n", - " file_path = os.path.join(root, file)\n", - " images_files.append(file_path)\n", - "\n", - "print(f\"Found {len(images_files)} image files\")" + "if dataset_dir.exists():\n", + " print(\"Path exists ✅\")\n", + "else:\n", + " print(\"Path does not exist ❌\")\n" ] }, { "cell_type": "code", "execution_count": 14, - "id": "f39153ab", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\0.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1008.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1016.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1028.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1060.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1082.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1106.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1119.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1137.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1157.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1175.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1189.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\119.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1201.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1218.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1233.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1246.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1257.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1278.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1312.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\1319.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\141.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\233.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\263.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\37.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\381.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\408.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\437.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\457.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\484.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\508.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\552.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\575.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\590.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\619.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\63.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\647.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\683.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\706.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\721.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\758.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\776.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\805.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\823.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\83.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\836.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\858.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\876.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\893.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\915.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\949.jpg',\n", - " 'FFMPEG_detects\\\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\frames\\\\99.jpg']" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "images_files" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "40c70986", + "id": "dd96be8c", "metadata": {}, "outputs": [], "source": [ - "# code to create dataset from sampled frames\n", - "\n", - "def upload_images_from_files(images_files, client, client_id):\n", - " \"\"\"Upload specific image files to create a dataset\"\"\"\n", - " \n", - " client.enable_connection_pooling = True\n", - " \n", - " dataset_config = {\n", - " \"client_id\": client_id,\n", - " \"dataset_name\": \"video_sampling_1\",\n", - " \"dataset_description\": \"video sampling dataset from frames\",\n", - " \"data_type\": \"image\", \n", - " }\n", - " \n", - " try:\n", - " response = client.create_dataset(\n", - " dataset_config=dataset_config,\n", - " files_to_upload=images_files \n", - " )\n", - " print(f\"Dataset created successfully!\")\n", - " print(f\"Dataset ID: {response['dataset_id']}\")\n", - " return response['dataset_id']\n", - " except Exception as e:\n", - " print(f\"Error creating dataset: {e}\")" + "detector = PySceneDetect()" ] }, { "cell_type": "code", - "execution_count": 16, - "id": "a1d96b25", + "execution_count": 15, + "id": "a3052f25", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Dataset created successfully!\n", - "Dataset ID: 6a680901-fe81-49f0-9120-bb754d63a341\n" + "JSON mapping saved to: PyScene_detects\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c_mapping.json\n", + "JSON mapping saved to: PyScene_detects\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265_mapping.json\n" ] - }, - { - "data": { - "text/plain": [ - "'6a680901-fe81-49f0-9120-bb754d63a341'" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "upload_images_from_files(images_files, client, client_id)" + "for filename in os.listdir(dataset_dir):\n", + " file_path = os.path.join(dataset_dir, filename)\n", + " \n", + " if os.path.isfile(file_path):\n", + " detector.detect_and_extract(file_path)" ] }, { - "cell_type": "code", - "execution_count": 19, - "id": "958fc75e", + "cell_type": "markdown", + "id": "6c3eac46", "metadata": {}, - "outputs": [], "source": [ - "new_dataset_id = '6a680901-fe81-49f0-9120-bb754d63a341'" + "---\n", + "## ***Image Project Creation***\n", + "\n", + "Create Image project of extracted keyframe from video" ] }, { "cell_type": "markdown", - "id": "b454c4f4", + "id": "f5dba054", "metadata": {}, "source": [ - "### Image Annotation Project Creation" + "### Create Labellerr Dataset of keyframe" ] }, { "cell_type": "code", "execution_count": null, - "id": "d32106c5", + "id": "82629d12", "metadata": {}, "outputs": [], "source": [ - "# modify to add questions to image project\n", - "\n", - "questions = [\n", - " {\n", - " \"question_number\": 1,\n", - " \"question\": \"Test\",\n", - " \"question_id\": \"533bb0c8-fb2b-4394-a8e1-5042a944802f\",\n", - " \"option_type\": \"polygon\",\n", - " \"required\": True,\n", - " \"options\": [\n", - " { \"option_name\": \"#fe1236\" }\n", - " ],\n", - " \"question_metadata\": []\n", - " }\n", - " ]\n" + "dataset = create_dataset_from_local(\n", + " client=client,\n", + " dataset_config=DatasetConfig(dataset_name=\"SDK VIDEO KEYFRAME DATASET\", \n", + " data_type=\"image\"),\n", + " folder_to_upload=dataset_dir,\n", + " )" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "b71d2aa0", + "cell_type": "markdown", + "id": "756764dd", "metadata": {}, - "outputs": [], "source": [ - "# creeate the annotation guideline template\n", - "\n", - "template_id = client.create_annotation_guideline(\n", - " client_id=client_id,\n", - " questions=questions,\n", - " template_name=\"video_sampling_template_1\",\n", - " data_type=\"image\",\n", - ")\n" + "### Create Annotation template of Keyframe Image Project" ] }, { "cell_type": "code", "execution_count": null, - "id": "83565ec3", + "id": "5d5e23c5", "metadata": {}, "outputs": [], "source": [ - "# create the image annotation project\n", - "\n", - "response = client.create_project(\n", - " project_name=\"Video_sampling_project_1\",\n", - " data_type=\"image\",\n", - " client_id= client_id,\n", - " dataset_id=new_dataset_id,\n", - " annotation_template_id=template_id,\n", - " rotation_config={\n", - " \"annotation_rotation_count\": 1,\n", - " \"review_rotation_count\": 1,\n", - " \"client_review_rotation_count\": 1,\n", - " },\n", - " created_by=\"yashsuman15@gmail.com\"\n", - " )\n" + "template = create_template(\n", + " client=client,\n", + " params=CreateTemplateParams(\n", + " template_name=\"SDK VIDEO KEYFRAME DATASET\",\n", + " data_type=DatasetDataType.image,\n", + " questions=[\n", + " AnnotationQuestion(\n", + " question_number=1,\n", + " question=\"Class polygon \",\n", + " question_id=str(uuid.uuid4()),\n", + " question_type=QuestionType.polygon,\n", + " required=True,\n", + " color=\"#FF0000\"\n", + " )\n", + " ]\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1bef8bfe", + "metadata": {}, + "source": [ + "### Create Image Annotation Project" ] }, { "cell_type": "code", - "execution_count": 31, - "id": "9f682f4f", + "execution_count": null, + "id": "b235e08d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Project created successfully!\n", - "Project ID: sherri_puny_rattlesnake_84247\n" - ] - } - ], + "outputs": [], "source": [ - "if response['response']['project_id']:\n", - " print(f\"Project created successfully!\")\n", - " print(f\"Project ID: {response['response']['project_id']}\")\n", - " image_project_id = response['response']['project_id']" + "img_project = create_project(\n", + " client=client,\n", + " params=CreateProjectParams(\n", + " project_name=\"SDK VIDEO PROJECT\",\n", + " data_type=DatasetDataType.video,\n", + " rotations=RotationConfig(\n", + " annotation_rotation_count=1,\n", + " review_rotation_count=1,\n", + " client_review_rotation_count=1\n", + " )\n", + " ),\n", + " datasets=[dataset],\n", + " annotation_template=template\n", + ")" ] }, { @@ -626,7 +755,8 @@ "id": "8645aa60", "metadata": {}, "source": [ - "## 6. Performing Annotations of Image Project" + "---\n", + "## ***Performing Annotations of Keyframe Image Project***" ] }, { @@ -644,71 +774,66 @@ "id": "8f0611f5", "metadata": {}, "source": [ - "### Exporting the Annotation Data" + "### Downloading the Annotation" ] }, { "cell_type": "code", "execution_count": null, - "id": "b78ad296", + "id": "ccf0e882", "metadata": {}, "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "18529760", + "metadata": {}, "source": [ - "# code to export the annotations from image project\n", - "\n", - "export_config = {\n", - " \"export_name\": \"Weekly Export\",\n", - " \"export_description\": \"Export of all accepted annotations\",\n", - " \"export_format\": \"coco_json\",\n", - " \"statuses\": [\n", - " \"review\",\n", - " \"r_assigned\",\n", - " \"client_review\",\n", - " \"cr_assigned\",\n", - " \"accepted\",\n", - " ],\n", - " }\n", - "\n", - "\n", - "response = client.create_local_export(\n", - " project_id=image_project_id,\n", - " client_id=client_id,\n", - " export_config=export_config\n", - ")" + "---\n", + "## ***Uploading KeyFrames Pre-Annotation to Video Project***" ] }, { "cell_type": "markdown", - "id": "18529760", + "id": "5c5ed594", "metadata": {}, "source": [ - "## 7. Uploading annotations to Video Project" + "### Converting Annotation JSON to required format" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "14b59cef", + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "id": "deae26b8", "metadata": {}, "source": [ - "### Trigger SAM2 tracking on Video annotation project\n", - "\n", - "Using the export, retrive the prompt to run SAM2 tracking on video" + "### Uploading pre-annotation" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "df6b3ac7", "metadata": {}, "outputs": [], "source": [ - "# code to create video annotation project from image annotations export" + "VIDEO_JSON_PATH = r\"path_to_your_video_preannotation_file.json\"\n", + "\n", + "video_project.upload_preannotations(video_json_file_path=VIDEO_JSON_PATH)" ] } ], "metadata": { "kernelspec": { - "display_name": "SDk", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -722,7 +847,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.18" + "version": "3.12.0" } }, "nbformat": 4, diff --git a/labellerr/notebooks/test_preannotation_api.py b/labellerr/notebooks/test_preannotation_api.py new file mode 100644 index 0000000..e82c9a9 --- /dev/null +++ b/labellerr/notebooks/test_preannotation_api.py @@ -0,0 +1,34 @@ +import os + +from dotenv import load_dotenv + +from labellerr.client import LabellerrClient +from labellerr.core.projects.video_project import LabellerrProject + +load_dotenv() + +API_KEY = os.getenv("QA_API_KEY") +API_SECRET = os.getenv("QA_API_SECRET") +CLIENT_ID = os.getenv("QA_CLIENT_ID") + +PROJECT_ID = "jeanna_mixed_aphid_93841" +VIDEO_JSON_FILE_PATH = r"C:\Users\yashs\Downloads\dumy_anotation.json" + + +def main(): + + client = LabellerrClient( + api_key=API_KEY, api_secret=API_SECRET, client_id=CLIENT_ID + ) + + project = LabellerrProject(client=client, project_id=PROJECT_ID) + + response = project.upload_keyframe_preannotations( + video_json_file_path=VIDEO_JSON_FILE_PATH + ) + + print(response) + + +if __name__ == "__main__": + main() From 9a7bf4dc597e03437586b332ecf4a5c97627f0dd Mon Sep 17 00:00:00 2001 From: yashsuman Date: Mon, 1 Dec 2025 11:24:51 +0530 Subject: [PATCH 05/14] Add test script for preannotation API functionality - Created a new script `test_preannotation_api.py` to test the upload of keyframe preannotations. - added method for pre-annotation upload to video project - modify SDK notebook accordingly --- labellerr/core/schemas/annotation_templates.py | 10 ++++++---- labellerr/notebooks/test_preannotation_api.py | 15 ++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/labellerr/core/schemas/annotation_templates.py b/labellerr/core/schemas/annotation_templates.py index 4885737..8120ce9 100644 --- a/labellerr/core/schemas/annotation_templates.py +++ b/labellerr/core/schemas/annotation_templates.py @@ -1,8 +1,10 @@ -from pydantic import BaseModel, Field -from typing import List, Optional -from enum import Enum -from ..schemas import DatasetDataType import uuid +from enum import Enum +from typing import List, Optional + +from pydantic import BaseModel, Field + +from .base import DatasetDataType class QuestionType(str, Enum): diff --git a/labellerr/notebooks/test_preannotation_api.py b/labellerr/notebooks/test_preannotation_api.py index e82c9a9..9771111 100644 --- a/labellerr/notebooks/test_preannotation_api.py +++ b/labellerr/notebooks/test_preannotation_api.py @@ -5,14 +5,18 @@ from labellerr.client import LabellerrClient from labellerr.core.projects.video_project import LabellerrProject -load_dotenv() +print(os.path.exists(r"labellerr\notebooks\dev.env")) +load_dotenv(r"labellerr\notebooks\dev.env") API_KEY = os.getenv("QA_API_KEY") API_SECRET = os.getenv("QA_API_SECRET") CLIENT_ID = os.getenv("QA_CLIENT_ID") +# print(API_KEY) +# print(API_SECRET) +# print(CLIENT_ID) PROJECT_ID = "jeanna_mixed_aphid_93841" -VIDEO_JSON_FILE_PATH = r"C:\Users\yashs\Downloads\dumy_anotation.json" +VIDEO_JSON_FILE_PATH = r"D:\Professional\Labellerr_SDK\dumy_anotation.json" def main(): @@ -23,10 +27,11 @@ def main(): project = LabellerrProject(client=client, project_id=PROJECT_ID) - response = project.upload_keyframe_preannotations( - video_json_file_path=VIDEO_JSON_FILE_PATH - ) + print(project.project_id) + response = project.upload_preannotations( + annotation_format="video_json", annotation_file=VIDEO_JSON_FILE_PATH + ) print(response) From cffbb319f84cc12f0756c8be348b992c94b58f02 Mon Sep 17 00:00:00 2001 From: yashsuman Date: Tue, 2 Dec 2025 12:24:17 +0530 Subject: [PATCH 06/14] minor changes --- .gitignore | 9 ++++++++ labellerr/notebooks/SDK.ipynb | 21 ++++++++++++++----- labellerr/notebooks/test_preannotation_api.py | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 1812a6f..fffd143 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,12 @@ download labellerr/__pycache__/ env.* claude.md +labellerr/notebooks/Labellerr_datasets/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4 +labellerr/notebooks/Labellerr_datasets/354681d3-034a-4d66-b070-365f4bd11d8a/7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4 +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c_mapping.json +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/316.jpg +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/7db3f60c-f6e5-4d3d-a63b-cb38530ee265/7db3f60c-f6e5-4d3d-a63b-cb38530ee265_mapping.json +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/1406.jpg +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/1064.jpg +labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/760.jpg +labellerr/notebooks/dev.env diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index 97f1426..efad8fb 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -54,13 +54,13 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "ab12f168", "metadata": {}, "outputs": [], "source": [ "from dotenv import dotenv_values\n", - "config = dotenv_values(\".env\")\n", + "config = dotenv_values(\"dev.env\")\n", "\n", "api_key = config[\"API_KEY\"]\n", "api_secret = config[\"API_SECRET\"]\n", @@ -297,7 +297,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "7be23d9a", "metadata": {}, "outputs": [], @@ -323,10 +323,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "7e281c33", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'cea9f8f1-11cb-472f-97b1-e2619be47051'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "template.annotation_template_id" ] diff --git a/labellerr/notebooks/test_preannotation_api.py b/labellerr/notebooks/test_preannotation_api.py index 9771111..fb60c02 100644 --- a/labellerr/notebooks/test_preannotation_api.py +++ b/labellerr/notebooks/test_preannotation_api.py @@ -5,7 +5,7 @@ from labellerr.client import LabellerrClient from labellerr.core.projects.video_project import LabellerrProject -print(os.path.exists(r"labellerr\notebooks\dev.env")) +# Load environment variables from .env file load_dotenv(r"labellerr\notebooks\dev.env") API_KEY = os.getenv("QA_API_KEY") From 52f6b3c1c2828e220fc7e517c70feb8fe6d54f4e Mon Sep 17 00:00:00 2001 From: yashsuman Date: Wed, 3 Dec 2025 15:48:44 +0530 Subject: [PATCH 07/14] modified video sampling scripts --- labellerr/services/video_sampling/__init__.py | 4 +- labellerr/services/video_sampling/ffmpeg.py | 162 ------- .../services/video_sampling/ffmpeg_detect.py | 394 ++++++++++++++++ .../services/video_sampling/pyscene_detect.py | 370 ++++++++++++--- labellerr/services/video_sampling/ssim.py | 234 ---------- .../services/video_sampling/ssim_detect.py | 441 ++++++++++++++++++ 6 files changed, 1142 insertions(+), 463 deletions(-) delete mode 100644 labellerr/services/video_sampling/ffmpeg.py create mode 100644 labellerr/services/video_sampling/ffmpeg_detect.py delete mode 100644 labellerr/services/video_sampling/ssim.py create mode 100644 labellerr/services/video_sampling/ssim_detect.py diff --git a/labellerr/services/video_sampling/__init__.py b/labellerr/services/video_sampling/__init__.py index c788244..4adb92f 100644 --- a/labellerr/services/video_sampling/__init__.py +++ b/labellerr/services/video_sampling/__init__.py @@ -3,9 +3,9 @@ All algorithms for video sampling will go in separate files. """ -from .ffmpeg import FFMPEGSceneDetect +from .ffmpeg_detect import FFMPEGSceneDetect from .pyscene_detect import PySceneDetect -from .ssim import SSIMSceneDetect +from .ssim_detect import SSIMSceneDetect __all__ = [ "FFMPEGSceneDetect", diff --git a/labellerr/services/video_sampling/ffmpeg.py b/labellerr/services/video_sampling/ffmpeg.py deleted file mode 100644 index 43f1c59..0000000 --- a/labellerr/services/video_sampling/ffmpeg.py +++ /dev/null @@ -1,162 +0,0 @@ -import json -import os -import subprocess -from typing import List - -from pydantic import BaseModel, Field - -from labellerr.core.base.singleton import Singleton - - -class SceneFrame(BaseModel): - """Represents an extracted keyframe.""" - - frame_path: str - frame_index: int - - -class DetectionResult(BaseModel): - """Contains all extraction results for a video.""" - - file_id: str - output_folder: str - selected_frames: List[SceneFrame] = Field(default_factory=list) - - -class FFMPEGSceneDetect(Singleton): - """Keyframe extraction from videos using FFMPEG (Singleton).""" - - def detect_and_extract(self, video_path: str) -> DetectionResult: - """ - Extract keyframes from video and save to detects folder structure. - - Args: - video_path: Path to the video file - - Returns: - DetectionResult containing file_id, output_folder, and list of SceneFrame objects - """ - # Derive file_id from video_path (base name without extension) - file_id = os.path.splitext(os.path.basename(video_path))[0] - dataset_id = os.path.basename(os.path.dirname(video_path)) - - # Create detects folder structure - base_detect_folder = "FFMPEG_detects" - - output_folder = os.path.join(base_detect_folder, dataset_id, file_id) - frames_folder = os.path.join(output_folder, "frames") - - # Create nested folders - os.makedirs(frames_folder, exist_ok=True) - - # Update output pattern to use frames subfolder in detects structure - output_pattern = os.path.join(frames_folder, "%d.jpg") - - command = [ - "ffmpeg", - "-i", - video_path, - "-vf", - "select='eq(pict_type,PICT_TYPE_I)',showinfo", - "-vsync", - "vfr", - "-frame_pts", - "1", - output_pattern, - ] - - try: - result = subprocess.run(command, check=True, capture_output=True, text=True) - print(f"Keyframes extracted to {frames_folder}") - - # Parse frame information from FFMPEG output - selected_frames = self._parse_ffmpeg_output(result.stderr, frames_folder) - - # Create result - detection_result = DetectionResult( - file_id=file_id, - output_folder=output_folder, # Main detects/file_id folder - selected_frames=selected_frames, - ) - - # Save JSON mapping - self._save_json_mapping(detection_result, output_folder, file_id) - - return detection_result - - except subprocess.CalledProcessError as e: - print(f"Error extracting keyframes: {e}") - raise - - def _parse_ffmpeg_output( - self, stderr_output: str, frames_folder: str - ) -> List[SceneFrame]: - """ - Parse FFMPEG stderr output to extract frame information. - - Args: - stderr_output: FFMPEG stderr output containing showinfo data - frames_folder: Folder where frames are saved (detects/file_id/frames) - - Returns: - List of SceneFrame objects - """ - frames = [] - frame_counter = 1 - - # Parse showinfo output from stderr - for line in stderr_output.split("\n"): - if "showinfo" in line and "n:" in line: - # The frame file is named sequentially starting from 1 - frame_path = os.path.join(frames_folder, f"{frame_counter}.jpg") - - # Extract frame number from showinfo line if needed - # Example: [Parsed_showinfo_1 @ 0x...] n: 0 pts: 0 ... - try: - if "pts_time:" in line: - # Extract the actual frame number from the source - parts = line.split("n:") - if len(parts) > 1: - frame_no = int(parts[1].split()[0]) - else: - frame_no = frame_counter - 1 - else: - frame_no = frame_counter - 1 - - frames.append( - SceneFrame(frame_path=frame_path, frame_index=frame_no) - ) - frame_counter += 1 - except (ValueError, IndexError): - continue - - return frames - - def _save_json_mapping( - self, result: DetectionResult, output_folder: str, file_id: str - ) -> None: - """ - Save JSON mapping of file_id to extracted keyframes. - - Args: - result: DetectionResult object - output_folder: Folder to save the JSON file (detects/file_id/) - file_id: Unique identifier for the video - """ - # Use Pydantic's model_dump - result_dict = result.model_dump() - result_dict["total_selected_frames"] = len(result.selected_frames) - - json_path = os.path.join(output_folder, f"{file_id}_mapping.json") - with open(json_path, "w", encoding="utf-8") as f: - json.dump(result_dict, f, indent=2, ensure_ascii=False) - - print(f"JSON mapping saved to: {json_path}") - - -if __name__ == "__main__": - video_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\download_video\59438ec3-12e0-4687-8847-1e6e01b0bf25\1cb2eec4-5125-4272-ad09-c249f40fffb3.mp4" - - # Get singleton instance - detector = FFMPEGSceneDetect() - result = detector.detect_and_extract(video_path) diff --git a/labellerr/services/video_sampling/ffmpeg_detect.py b/labellerr/services/video_sampling/ffmpeg_detect.py new file mode 100644 index 0000000..260096d --- /dev/null +++ b/labellerr/services/video_sampling/ffmpeg_detect.py @@ -0,0 +1,394 @@ +import json +import os +import shutil +import subprocess +from pathlib import Path +from typing import List + +from pydantic import BaseModel, Field + +from labellerr.core.base.singleton import Singleton + + +class FFMPEGError(Exception): + """Base exception for FFMPEG-related errors.""" + + pass + + +class FFMPEGNotFoundError(FFMPEGError): + """Raised when FFMPEG is not installed or not found in PATH.""" + + pass + + +class VideoFileError(FFMPEGError): + """Raised when there are issues with the video file.""" + + pass + + +class NoKeyframesError(FFMPEGError): + """Raised when no I-frames are found in the video.""" + + pass + + +class SceneFrame(BaseModel): + """Represents an extracted keyframe.""" + + frame_path: str + frame_index: int + + +class DetectionResult(BaseModel): + """Contains all extraction results for a video.""" + + file_id: str + output_folder: str + selected_frames: List[SceneFrame] = Field(default_factory=list) + + +class FFMPEGSceneDetect(Singleton): + """Keyframe extraction from videos using FFMPEG (Singleton).""" + + # Supported video extensions + SUPPORTED_EXTENSIONS = { + ".mp4", + ".avi", + ".mov", + ".mkv", + ".flv", + ".wmv", + ".webm", + ".m4v", + } + + def __init__(self): + """Initialize and verify FFMPEG is available.""" + super().__init__() + self._verify_ffmpeg() + + def _verify_ffmpeg(self) -> None: + """Verify that FFMPEG is installed and accessible.""" + if not shutil.which("ffmpeg"): + raise FFMPEGNotFoundError( + "FFMPEG is not installed or not found in PATH. " + "Please install FFMPEG from https://ffmpeg.org/download.html" + ) + + def _validate_video_file(self, video_path: str) -> None: + """Validate that the video file exists and is a supported format. + + Args: + video_path: Path to the video file + + Raises: + VideoFileError: If file doesn't exist or format is unsupported + """ + path = Path(video_path) + + if not path.exists(): + raise VideoFileError(f"Video file not found: {video_path}") + + if not path.is_file(): + raise VideoFileError(f"Path is not a file: {video_path}") + + if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS: + raise VideoFileError( + f"Unsupported video format: {path.suffix}. " + f"Supported formats: {', '.join(sorted(self.SUPPORTED_EXTENSIONS))}" + ) + + # Check if file is readable + if not os.access(video_path, os.R_OK): + raise VideoFileError(f"Video file is not readable: {video_path}") + + def detect_and_extract(self, video_path: str) -> DetectionResult: + """ + Extract keyframes from video and save to detects folder structure. + Frames are saved with their actual frame numbers (e.g., 5.jpg for frame 5). + + Args: + video_path: Path to the video file + + Returns: + DetectionResult containing file_id, output_folder, and list of SceneFrame objects + + Raises: + VideoFileError: If video file is invalid or inaccessible + NoKeyframesError: If no I-frames are found in the video + FFMPEGError: If FFMPEG processing fails + """ + # Validate input file before processing + self._validate_video_file(video_path) + + # Extract identifiers from the video path + # file_id: Video filename without extension (e.g., "video_123") + # dataset_id: Parent directory name (used for organizing outputs) + file_id = os.path.splitext(os.path.basename(video_path))[0] + dataset_id = os.path.basename(os.path.dirname(video_path)) + + # Create hierarchical output folder structure: + # FFMPEG_detects/ + # └── / + # └── / + # ├── frames/ (extracted frame images) + # └── _mapping.json (metadata) + base_detect_folder = "FFMPEG_detects" + + output_folder = os.path.join(base_detect_folder, dataset_id, file_id) + frames_folder = os.path.join(output_folder, "frames") + + # Create all necessary directories (no error if they already exist) + os.makedirs(frames_folder, exist_ok=True) + + try: + # ================================================================ + # PHASE 1: Identify I-frame positions + # ================================================================ + # First pass: Scan the video to find all I-frame positions + # This is done WITHOUT extracting frames to get the complete list + # of frame numbers before extraction begins + print("Identifying I-frame positions...") + frame_numbers = self._get_iframe_numbers(video_path) + + # Validate that at least one I-frame was found + if not frame_numbers: + raise NoKeyframesError( + f"No I-frames (keyframes) found in video: {video_path}. " + "The video may be corrupted or in an unsupported format." + ) + + # Show preview of detected I-frames (limit to first 10 for readability) + print( + f"Found {len(frame_numbers)} I-frames at positions: {frame_numbers[:10]}{'...' if len(frame_numbers) > 10 else ''}" + ) + + # ================================================================ + # PHASE 2: Extract each I-frame individually + # ================================================================ + # Second pass: Extract each I-frame and save with its actual frame number + # Using actual frame numbers ensures frames are named correctly + # (e.g., frame 250 from video → 250.jpg) + selected_frames = [] + for idx, frame_num in enumerate(frame_numbers, 1): + frame_path = os.path.join(frames_folder, f"{frame_num}.jpg") + try: + self._extract_single_frame(video_path, frame_num, frame_path) + selected_frames.append( + SceneFrame(frame_path=frame_path, frame_index=frame_num) + ) + if idx % 10 == 0: # Progress update every 10 frames + print(f"Extracted {idx}/{len(frame_numbers)} frames...") + except Exception as e: + print(f"Warning: Failed to extract frame {frame_num}: {e}") + continue + + if not selected_frames: + raise FFMPEGError( + f"Failed to extract any frames from video: {video_path}. " + "All frame extractions failed." + ) + + print( + f"Successfully extracted {len(selected_frames)}/{len(frame_numbers)} keyframes to {frames_folder}" + ) + + # Create result + detection_result = DetectionResult( + file_id=file_id, + output_folder=output_folder, + selected_frames=selected_frames, + ) + + # Save JSON mapping + self._save_json_mapping(detection_result, output_folder, file_id) + + return detection_result + + except subprocess.CalledProcessError as e: + error_msg = e.stderr if hasattr(e, "stderr") and e.stderr else str(e) + raise FFMPEGError(f"FFMPEG command failed: {error_msg}") from e + except (FFMPEGError, VideoFileError, NoKeyframesError): + # Re-raise our custom exceptions + raise + except Exception as e: + raise FFMPEGError( + f"Unexpected error during keyframe extraction: {e}" + ) from e + + def _get_iframe_numbers(self, video_path: str) -> List[int]: + """ + Identify all I-frame (keyframe) positions in the video. + + Args: + video_path: Path to the video file + + Returns: + List of frame numbers (0-indexed) where I-frames occur + + Raises: + FFMPEGError: If FFMPEG command fails + """ + # Build FFMPEG command to identify I-frames without extracting them + # - select filter: Only pass through I-frames (PICT_TYPE_I) + # - showinfo: Print detailed information about each frame to stderr + # - null output: Don't actually save frames, just analyze + command = [ + "ffmpeg", + "-i", + video_path, + "-vf", + "select='eq(pict_type,PICT_TYPE_I)',showinfo", + "-vsync", + "vfr", # Variable frame rate to preserve original timing + "-f", + "null", # Null muxer - discard output, we only need stderr info + "-", + ] + + result = subprocess.run(command, capture_output=True, text=True) + + # ================================================================ + # STEP 1: Extract frame rate from video metadata + # ================================================================ + # We need the frame rate to convert pts_time (seconds) to frame numbers + # Frame number = pts_time × frame_rate + frame_rate = None + for line in result.stderr.split("\n"): + if "Stream #" in line and "Video:" in line: + # Extract frame rate from stream info + # Example: Stream #0:0: Video: h264, 1920x1080, 30 fps + parts = line.split(",") + for part in parts: + if "fps" in part or "tbr" in part: + try: + fps_str = part.strip().split()[0] + frame_rate = float(fps_str) + break + except (ValueError, IndexError): + continue + if frame_rate: + break + + # Fallback to 30 fps if frame rate detection fails + if not frame_rate: + frame_rate = 30.0 + print( + f"Warning: Could not detect frame rate, defaulting to {frame_rate} fps" + ) + + # ================================================================ + # STEP 2: Parse showinfo output to get actual frame numbers + # ================================================================ + # IMPORTANT: The 'n:' value in showinfo is the FILTERED output index (0, 1, 2...) + # NOT the source frame number. We must use pts_time to calculate the real frame number. + frame_numbers = [] + for line in result.stderr.split("\n"): + if "showinfo" in line and "pts_time:" in line: + try: + # Extract pts_time (presentation timestamp in seconds) + # This tells us the exact time position of this frame in the video + pts_time_str = line.split("pts_time:")[1].split()[0] + pts_time = float(pts_time_str) + + # Calculate frame number from pts_time and frame rate + frame_num = int(round(pts_time * frame_rate)) + frame_numbers.append(frame_num) + except (ValueError, IndexError): + # If pts_time parsing fails, skip this frame + continue + + # Always ensure frame 0 (first frame) is included + if 0 not in frame_numbers: + frame_numbers.insert(0, 0) + + return frame_numbers + + def _extract_single_frame( + self, video_path: str, frame_num: int, output_path: str + ) -> None: + """ + Extract a specific frame from the video. + + Args: + video_path: Path to the video file + frame_num: Frame number to extract (0-indexed) + output_path: Path where the frame should be saved + + Raises: + FFMPEGError: If frame extraction fails + """ + command = [ + "ffmpeg", + "-i", + video_path, + "-vf", + f"select='eq(n,{frame_num})'", + "-vsync", + "vfr", + "-frames:v", + "1", + "-y", # Overwrite output file if it exists + output_path, + ] + + try: + subprocess.run( + command, + check=True, + capture_output=True, + text=True, + timeout=30, # 30 second timeout per frame + ) + + # Verify the output file was created + if not os.path.exists(output_path): + raise FFMPEGError( + f"Frame extraction succeeded but output file not found: {output_path}" + ) + + # Verify the output file has content + if os.path.getsize(output_path) == 0: + raise FFMPEGError(f"Extracted frame is empty: {output_path}") + + except subprocess.TimeoutExpired: + raise FFMPEGError(f"Frame extraction timed out for frame {frame_num}") + except subprocess.CalledProcessError as e: + raise FFMPEGError(f"Failed to extract frame {frame_num}: {e.stderr}") from e + + def _save_json_mapping( + self, result: DetectionResult, output_folder: str, file_id: str + ) -> None: + """ + Save JSON mapping of file_id to extracted keyframes. + + Args: + result: DetectionResult object + output_folder: Folder to save the JSON file (detects/file_id/) + file_id: Unique identifier for the video + + Raises: + FFMPEGError: If JSON file cannot be saved + """ + try: + # Use Pydantic's model_dump + result_dict = result.model_dump() + result_dict["total_selected_frames"] = len(result.selected_frames) + + json_path = os.path.join(output_folder, f"{file_id}_mapping.json") + with open(json_path, "w", encoding="utf-8") as f: + json.dump(result_dict, f, indent=2, ensure_ascii=False) + + print(f"JSON mapping saved to: {json_path}") + except (IOError, OSError) as e: + raise FFMPEGError(f"Failed to save JSON mapping to {json_path}: {e}") from e + + +if __name__ == "__main__": + video_path = r"D:\Professional\Labellerr_SDK\SDKPython\labellerr\notebooks\Labellerr_datasets\354681d3-034a-4d66-b070-365f4bd11d8a\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4" + + # Get singleton instance + detector = FFMPEGSceneDetect() + result = detector.detect_and_extract(video_path) diff --git a/labellerr/services/video_sampling/pyscene_detect.py b/labellerr/services/video_sampling/pyscene_detect.py index bc93070..956e419 100644 --- a/labellerr/services/video_sampling/pyscene_detect.py +++ b/labellerr/services/video_sampling/pyscene_detect.py @@ -1,5 +1,6 @@ import json import os +from pathlib import Path from typing import List import cv2 @@ -9,16 +10,95 @@ from labellerr.core.base.singleton import Singleton +# ============================================================================ +# Exception Classes +# ============================================================================ + + +class PySceneDetectError(Exception): + """Base exception for all PySceneDetect-related errors. + + This is the parent exception class for all PySceneDetect-specific errors in this module. + Catching this exception will catch all scene detection-related issues including: + - Video file errors + - Scene detection failures + - Frame extraction failures + - No scenes detected + """ + + pass + + +class VideoFileError(PySceneDetectError): + """Raised when there are issues with the input video file. + + Common causes: + - File does not exist + - Path points to a directory instead of a file + - Unsupported video format + - File is not readable (permission issues) + - Video file is corrupted + """ + + pass + + +class NoScenesError(PySceneDetectError): + """Raised when no scene changes are found in the video. + + This can occur if: + - The video is very short (single scene) + - The video has no significant visual changes + - The video file is corrupted + """ + + pass + + +class FrameExtractionError(PySceneDetectError): + """Raised when frame extraction fails. + + This can occur if: + - OpenCV cannot read the video + - Frame number is out of range + - Video codec is unsupported + """ + + pass + + +# ============================================================================ +# Data Models +# ============================================================================ + class SceneFrame(BaseModel): - """Represents a detected scene with its extracted frame.""" + """Represents a single extracted frame from a detected scene. + + Attributes: + frame_path (str): Absolute or relative path to the extracted frame image file. + Example: "PyScene_detects/video_id/frames/250.jpg" + frame_index (int): The 0-indexed frame number in the source video. + Example: 250 means this is the 250th frame of the video. + """ frame_path: str frame_index: int class DetectionResult(BaseModel): - """Contains all detection results for a video.""" + """Contains all scene detection results for a video file. + + This model encapsulates the complete output of the scene detection process, + including metadata about the video and a list of all extracted frames. + + Attributes: + file_id (str): Unique identifier for the video (filename without extension). + output_folder (str): Path to the folder containing extracted frames and metadata. + total_frames (int): Total number of frames in the source video. + selected_frames (List[SceneFrame]): List of all successfully extracted scene frames. + Each frame includes its path and frame index. + """ file_id: str output_folder: str @@ -26,73 +106,215 @@ class DetectionResult(BaseModel): selected_frames: List[SceneFrame] = Field(default_factory=list) +# ============================================================================ +# Main Scene Detection Class +# ============================================================================ + + class PySceneDetect(Singleton): - """Scene detection and frame extraction for videos (Singleton).""" + """Scene change detection and frame extraction using PySceneDetect. + + This singleton class provides methods to detect scene changes in video files + and extract representative frames from each scene. It uses PySceneDetect's + AdaptiveDetector algorithm for robust scene detection. + + The class implements the Singleton pattern to ensure only one instance exists, + which is useful for managing video processing and avoiding redundant initialization. + + Attributes: + SUPPORTED_EXTENSIONS (set): Set of supported video file extensions. + + Example: + >>> detector = PySceneDetect() + >>> result = detector.detect_and_extract("video.mp4") + >>> print(f"Detected {len(result.selected_frames)} scenes") + """ + + # Supported video file extensions + SUPPORTED_EXTENSIONS = { + ".mp4", + ".avi", + ".mov", + ".mkv", + ".flv", + ".wmv", + ".webm", + ".m4v", + } + + def _validate_video_file(self, video_path: str) -> None: + """Validate that the video file exists and is a supported format. + + Args: + video_path: Path to the video file + + Raises: + VideoFileError: If file doesn't exist or format is unsupported + """ + path = Path(video_path) + + if not path.exists(): + raise VideoFileError(f"Video file not found: {video_path}") + + if not path.is_file(): + raise VideoFileError(f"Path is not a file: {video_path}") + + if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS: + raise VideoFileError( + f"Unsupported video format: {path.suffix}. " + f"Supported formats: {', '.join(sorted(self.SUPPORTED_EXTENSIONS))}" + ) + + # Check if file is readable + if not os.access(video_path, os.R_OK): + raise VideoFileError(f"Video file is not readable: {video_path}") def detect_and_extract(self, video_path: str) -> DetectionResult: """ Detect scenes and extract representative frames. + Always extracts the first frame (frame 0) of the video. Args: video_path: Path to the video file Returns: DetectionResult containing file_id, output_folder, total_frames, and list of SceneFrame objects + + Raises: + VideoFileError: If video file is invalid or inaccessible + NoScenesError: If no scene changes are detected + FrameExtractionError: If frame extraction fails + PySceneDetectError: If scene detection processing fails """ - # Derive file_id from video_path (base name without extension) + # Validate input file before processing + self._validate_video_file(video_path) + + # Extract identifiers from the video path + # file_id: Video filename without extension (e.g., "video_123") + # dataset_id: Parent directory name (used for organizing outputs) file_id = os.path.splitext(os.path.basename(video_path))[0] dataset_id = os.path.basename(os.path.dirname(video_path)) - # Create base detect folder and file_id specific folder + # Create hierarchical output folder structure: + # PyScene_detects/ + # └── / + # └── / + # ├── frames/ (extracted frame images) + # └── _mapping.json (metadata) base_detect_folder = "PyScene_detects" output_folder = os.path.join(base_detect_folder, dataset_id, file_id) - frames_folder = os.path.join(output_folder, "frames") # New frames subfolder - - # Detect scene transitions - scenes = detect(video_path, AdaptiveDetector()) - - # Create nested output folders - os.makedirs(frames_folder, exist_ok=True) # Create frames subfolder - - # Open video for frame extraction - video = cv2.VideoCapture(video_path) - - # Get total frames in video - total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) - - # Extract and save frames - scene_frames = [] - for scene in scenes: - # Calculate middle frame number - frame_no = (scene[1] - scene[0]).frame_num // 2 + scene[0].frame_num - - # Extract frame - frame = self._get_frame(video, frame_no) - - # Save frame with frame number as filename inside frames folder - frame_filename = f"{frame_no}.jpg" - frame_path = os.path.join(frames_folder, frame_filename) # Updated path - frame.save(frame_path) - - # Create SceneFrame object - scene_frame = SceneFrame(frame_path=frame_path, frame_index=frame_no) - scene_frames.append(scene_frame) - - video.release() - - # Create result - result = DetectionResult( - file_id=file_id, - output_folder=output_folder, - total_frames=total_frames, - selected_frames=scene_frames, - ) - - # Save JSON mapping - self._save_json_mapping(result, output_folder, file_id) - - return result + frames_folder = os.path.join(output_folder, "frames") + + try: + # ================================================================ + # PHASE 1: Detect scene changes + # ================================================================ + print("Detecting scene changes...") + scenes = detect(video_path, AdaptiveDetector()) + + # Create all necessary directories (no error if they already exist) + os.makedirs(frames_folder, exist_ok=True) + + # ================================================================ + # PHASE 2: Extract frames from detected scenes + # ================================================================ + # Open video for frame extraction + video = cv2.VideoCapture(video_path) + + if not video.isOpened(): + raise FrameExtractionError(f"Failed to open video file: {video_path}") + + # Get total frames in video + total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + + print(f"Detected {len(scenes)} scene changes") + print(f"Total frames in video: {total_frames}") + + # Extract and save frames from detected scenes + scene_frames = [] + frame_numbers_extracted = set() # Track which frames we've extracted + + for idx, scene in enumerate(scenes, 1): + # Calculate middle frame number of the scene + frame_no = (scene[1] - scene[0]).frame_num // 2 + scene[0].frame_num + + # Extract frame + try: + frame = self._get_frame(video, frame_no) + + # Save frame with frame number as filename inside frames folder + frame_filename = f"{frame_no}.jpg" + frame_path = os.path.join(frames_folder, frame_filename) + frame.save(frame_path) + + # Create SceneFrame object + scene_frame = SceneFrame( + frame_path=frame_path, frame_index=frame_no + ) + scene_frames.append(scene_frame) + frame_numbers_extracted.add(frame_no) + + # Progress update: Print every 10 scenes to avoid console spam + if idx % 10 == 0: + print(f"Extracted {idx}/{len(scenes)} scene frames...") + except Exception as e: + # Log warning but continue with other frames (graceful degradation) + print(f"Warning: Failed to extract frame {frame_no}: {e}") + continue + + # ================================================================ + # PHASE 3: Always extract first frame (frame 0) + # ================================================================ + # Ensure frame 0 is always extracted, even if it's not a scene change + if 0 not in frame_numbers_extracted: + try: + print("Extracting first frame (frame 0)...") + frame = self._get_frame(video, 0) + + frame_filename = "0.jpg" + frame_path = os.path.join(frames_folder, frame_filename) + frame.save(frame_path) + + # Insert at the beginning of the list + scene_frame = SceneFrame(frame_path=frame_path, frame_index=0) + scene_frames.insert(0, scene_frame) + except Exception as e: + print(f"Warning: Failed to extract first frame: {e}") + + video.release() + + # Validate that at least one frame was successfully extracted + if not scene_frames: + raise NoScenesError( + f"No scenes detected and failed to extract first frame from video: {video_path}" + ) + + # Final success message with extraction statistics + print( + f"Successfully extracted {len(scene_frames)} frames to {frames_folder}" + ) + + # Create result + result = DetectionResult( + file_id=file_id, + output_folder=output_folder, + total_frames=total_frames, + selected_frames=scene_frames, + ) + + # Save JSON mapping + self._save_json_mapping(result, output_folder, file_id) + + return result + + except (VideoFileError, NoScenesError, FrameExtractionError): + # Re-raise our custom exceptions + raise + except Exception as e: + raise PySceneDetectError( + f"Unexpected error during scene detection: {e}" + ) from e def _get_frame(self, video: cv2.VideoCapture, frame_no: int) -> Image.Image: """ @@ -104,10 +326,20 @@ def _get_frame(self, video: cv2.VideoCapture, frame_no: int) -> Image.Image: Returns: PIL Image of the frame + + Raises: + FrameExtractionError: If frame extraction fails """ - video.set(cv2.CAP_PROP_POS_FRAMES, frame_no) - _, frame = video.read() - return Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + try: + video.set(cv2.CAP_PROP_POS_FRAMES, frame_no) + ret, frame = video.read() + + if not ret or frame is None: + raise FrameExtractionError(f"Failed to read frame {frame_no}") + + return Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + except Exception as e: + raise FrameExtractionError(f"Error extracting frame {frame_no}: {e}") from e def _save_json_mapping( self, result: DetectionResult, output_folder: str, file_id: str @@ -119,20 +351,28 @@ def _save_json_mapping( result: DetectionResult object output_folder: Folder to save the JSON file file_id: Unique identifier for the video + + Raises: + PySceneDetectError: If JSON file cannot be saved """ - # Use Pydantic's model_dump instead of asdict - result_dict = result.model_dump() - result_dict["total_selected_frames"] = len(result.selected_frames) + try: + # Use Pydantic's model_dump + result_dict = result.model_dump() + result_dict["total_selected_frames"] = len(result.selected_frames) - json_path = os.path.join(output_folder, f"{file_id}_mapping.json") - with open(json_path, "w", encoding="utf-8") as f: - json.dump(result_dict, f, indent=2, ensure_ascii=False) + json_path = os.path.join(output_folder, f"{file_id}_mapping.json") + with open(json_path, "w", encoding="utf-8") as f: + json.dump(result_dict, f, indent=2, ensure_ascii=False) - print(f"JSON mapping saved to: {json_path}") + print(f"JSON mapping saved to: {json_path}") + except (IOError, OSError) as e: + raise PySceneDetectError( + f"Failed to save JSON mapping to {json_path}: {e}" + ) from e -# if __name__ == "__main__": -# video_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\notebooks\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4" +if __name__ == "__main__": + video_path = r"D:\Professional\Labellerr_SDK\SDKPython\labellerr\notebooks\Labellerr_datasets\354681d3-034a-4d66-b070-365f4bd11d8a\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4" -# detector = PySceneDetect() -# result = detector.detect_and_extract(video_path) + detector = PySceneDetect() + result = detector.detect_and_extract(video_path) diff --git a/labellerr/services/video_sampling/ssim.py b/labellerr/services/video_sampling/ssim.py deleted file mode 100644 index 285a4c9..0000000 --- a/labellerr/services/video_sampling/ssim.py +++ /dev/null @@ -1,234 +0,0 @@ -import json -import os -from typing import List - -import cv2 -import numpy as np -from PIL import Image -from pydantic import BaseModel, Field -from skimage.metrics import structural_similarity as ssim - -from labellerr.core.base.singleton import Singleton - - -class SceneFrame(BaseModel): - """Represents a detected scene with its extracted frame.""" - - frame_path: str - frame_index: int - ssim_score: float - - -class DetectionResult(BaseModel): - """Contains all detection results for a video.""" - - file_id: str - output_folder: str - total_frames: int - selected_frames: List[SceneFrame] = Field(default_factory=list) - - -class SSIMSceneDetect(Singleton): - """SSIM-based scene detection and frame extraction for videos (Singleton).""" - - def detect_and_extract( - self, video_path: str, threshold: float = 0.6, resize_dim: tuple = (320, 240) - ) -> DetectionResult: - """ - Detect scenes using SSIM and extract representative frames. - - Args: - video_path: Path to the video file - threshold: SSIM threshold for scene detection (lower = stricter, default: 0.6) - resize_dim: Dimensions to resize frames for SSIM calculation (default: (320, 240)) - - Returns: - DetectionResult containing file_id, output_folder, total_frames, and list of SceneFrame objects - """ - # Derive file_id from video_path (base name without extension) - file_id = os.path.splitext(os.path.basename(video_path))[0] - dataset_id = os.path.basename(os.path.dirname(video_path)) - - # Create detects folder structure - base_detect_folder = "SSIM_detects" - output_folder = os.path.join(base_detect_folder, dataset_id, file_id) - frames_folder = os.path.join(output_folder, "frames") - - # Create nested output folders - os.makedirs(frames_folder, exist_ok=True) - - # Open video for processing - video = cv2.VideoCapture(video_path) - - if not video.isOpened(): - raise ValueError(f"Cannot open video: {video_path}") - - # Get total frames in video - total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) - - print(f"Processing video: {video_path}") - print(f"Total frames: {total_frames}") - print(f"SSIM threshold: {threshold}") - - # Read first frame - success, prev_frame = video.read() - if not success: - video.release() - raise ValueError(f"Cannot read first frame from: {video_path}") - - # Extract and save frames - scene_frames = [] - frame_count = 0 - - # Always save first frame - self._save_frame(prev_frame, frame_count, 1.0, scene_frames, frames_folder) - # print(f"Saved keyframe 0 at frame {frame_count} (First frame)") - - # Process remaining frames - while True: - success, curr_frame = video.read() - if not success: - break - - frame_count += 1 - - # Calculate SSIM between current and previous frame - ssim_score = self._calculate_ssim(prev_frame, curr_frame, resize_dim) - - # If SSIM is below threshold, it's a scene change - if ssim_score < threshold: - self._save_frame( - curr_frame, frame_count, ssim_score, scene_frames, frames_folder - ) - print( - f"Saved keyframe {len(scene_frames) - 1} at frame {frame_count} (SSIM: {ssim_score:.3f})" - ) - prev_frame = curr_frame - elif frame_count % 100 == 0: - print( - f"Frame {frame_count}: SSIM = {ssim_score:.3f} (threshold: {threshold})" - ) - - video.release() - - # print(f"\nExtracted {len(scene_frames)} keyframes from {frame_count + 1} frames.") - - # Create result - result = DetectionResult( - file_id=file_id, - output_folder=output_folder, # Main detects/file_id folder - total_frames=total_frames, - selected_frames=scene_frames, - ) - - # Save JSON mapping - self._save_json_mapping(result, output_folder, file_id, threshold, resize_dim) - - return result - - def _calculate_ssim( - self, frame1: np.ndarray, frame2: np.ndarray, resize_dim: tuple - ) -> float: - """ - Calculate SSIM score between two frames. - - Args: - frame1: First frame (BGR format) - frame2: Second frame (BGR format) - resize_dim: Dimensions to resize frames for SSIM calculation - - Returns: - SSIM score (0-1, where 1 is identical) - """ - # Resize frames for faster computation - gray1 = cv2.cvtColor(cv2.resize(frame1, resize_dim), cv2.COLOR_BGR2GRAY) - gray2 = cv2.cvtColor(cv2.resize(frame2, resize_dim), cv2.COLOR_BGR2GRAY) - - # Calculate SSIM - score, _ = ssim(gray1, gray2, full=True) - - return score - - def _save_frame( - self, - frame: np.ndarray, - frame_no: int, - ssim_score: float, - scene_frames: List[SceneFrame], - frames_folder: str, - ) -> None: - """ - Save a frame to disk and add to scene_frames list. - - Args: - frame: Frame to save (BGR format) - frame_no: Frame number - ssim_score: SSIM score that triggered this frame - scene_frames: List to append SceneFrame object to - frames_folder: Folder to save the frame (detects/file_id/frames) - """ - # Convert BGR to RGB for PIL - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - pil_image = Image.fromarray(frame_rgb) - - # Save frame with frame number as filename in frames folder - frame_filename = f"{frame_no}.jpg" - frame_path = os.path.join( - frames_folder, frame_filename - ) # Now uses frames_folder - pil_image.save(frame_path) - - # Create SceneFrame object - scene_frame = SceneFrame( - frame_path=frame_path, frame_index=frame_no, ssim_score=ssim_score - ) - scene_frames.append(scene_frame) - - def _save_json_mapping( - self, - result: DetectionResult, - output_folder: str, # This is now detects/file_id/ - file_id: str, - threshold: float, - resize_dim: tuple, - ) -> None: - """ - Save JSON mapping of file_id to extracted scenes. - - Args: - result: DetectionResult object - output_folder: Folder to save the JSON file (detects/file_id/) - file_id: Unique identifier for the video - threshold: SSIM threshold used - resize_dim: Resize dimensions used - """ - # Use Pydantic's model_dump - result_dict = result.model_dump() - result_dict["total_selected_frames"] = len(result.selected_frames) - result_dict["threshold"] = threshold - result_dict["resize_dim"] = resize_dim - - json_path = os.path.join(output_folder, f"{file_id}_mapping.json") - with open(json_path, "w", encoding="utf-8") as f: - json.dump(result_dict, f, indent=2, ensure_ascii=False) - - print(f"JSON mapping saved to: {json_path}") - - -if __name__ == "__main__": - # Example usage - video_path = r"D:\professional\LABELLERR\Task\Repos\Python_SDK\services\video_sampling\video2.mp4" - - # Get singleton instance - detector = SSIMSceneDetect() - - # Detect and extract frames - result = detector.detect_and_extract( - video_path=video_path, - threshold=0.6, # Lower value = more sensitive to changes - resize_dim=(320, 240), - ) - - print("\nDetection complete!") - print(f"Total frames extracted: {len(result.selected_frames)}") - print(f"Output folder: {result.output_folder}") diff --git a/labellerr/services/video_sampling/ssim_detect.py b/labellerr/services/video_sampling/ssim_detect.py new file mode 100644 index 0000000..716ec79 --- /dev/null +++ b/labellerr/services/video_sampling/ssim_detect.py @@ -0,0 +1,441 @@ +import json +import os +from pathlib import Path +from typing import List + +import cv2 +import numpy as np +from PIL import Image +from pydantic import BaseModel, Field +from skimage.metrics import structural_similarity as ssim + +from labellerr.core.base.singleton import Singleton + +# ============================================================================ +# Exception Classes +# ============================================================================ + + +class SSIMDetectError(Exception): + """Base exception for all SSIM detection-related errors. + + This is the parent exception class for all SSIM-specific errors in this module. + Catching this exception will catch all SSIM detection-related issues including: + - Video file errors + - Frame extraction failures + - SSIM calculation errors + """ + + pass + + +class VideoFileError(SSIMDetectError): + """Raised when there are issues with the input video file. + + Common causes: + - File does not exist + - Path points to a directory instead of a file + - Unsupported video format + - File is not readable (permission issues) + - Video file is corrupted + - OpenCV cannot open the video + """ + + pass + + +class FrameExtractionError(SSIMDetectError): + """Raised when frame extraction fails. + + This can occur if: + - OpenCV cannot read the video + - Frame number is out of range + - Video codec is unsupported + - Frame data is corrupted + """ + + pass + + +# ============================================================================ +# Data Models +# ============================================================================ + + +class SceneFrame(BaseModel): + """Represents a single extracted frame from a detected scene. + + Attributes: + frame_path (str): Absolute or relative path to the extracted frame image file. + Example: "SSIM_detects/video_id/frames/250.jpg" + frame_index (int): The 0-indexed frame number in the source video. + Example: 250 means this is the 250th frame of the video. + ssim_score (float): The SSIM score that triggered this frame extraction. + Range: 0.0 to 1.0 (lower = more different from previous frame) + """ + + frame_path: str + frame_index: int + ssim_score: float + + +class DetectionResult(BaseModel): + """Contains all SSIM detection results for a video file. + + This model encapsulates the complete output of the SSIM detection process, + including metadata about the video and a list of all extracted frames. + + Attributes: + file_id (str): Unique identifier for the video (filename without extension). + output_folder (str): Path to the folder containing extracted frames and metadata. + total_frames (int): Total number of frames in the source video. + selected_frames (List[SceneFrame]): List of all successfully extracted scene frames. + Each frame includes its path, frame index, and SSIM score. + """ + + file_id: str + output_folder: str + total_frames: int + selected_frames: List[SceneFrame] = Field(default_factory=list) + + +# ============================================================================ +# Main SSIM Detection Class +# ============================================================================ + + +class SSIMSceneDetect(Singleton): + """SSIM-based scene change detection and frame extraction. + + This singleton class provides methods to detect scene changes in video files + using SSIM (Structural Similarity Index) metric. SSIM measures perceptual + similarity between frames, making it effective for scene change detection. + + The class implements the Singleton pattern to ensure only one instance exists, + which is useful for managing video processing and avoiding redundant initialization. + + Attributes: + SUPPORTED_EXTENSIONS (set): Set of supported video file extensions. + + Example: + >>> detector = SSIMSceneDetect() + >>> result = detector.detect_and_extract("video.mp4", threshold=0.6) + >>> print(f"Detected {len(result.selected_frames)} scenes") + """ + + # Supported video file extensions + SUPPORTED_EXTENSIONS = { + ".mp4", + ".avi", + ".mov", + ".mkv", + ".flv", + ".wmv", + ".webm", + ".m4v", + } + + def _validate_video_file(self, video_path: str) -> None: + """Validate that the video file exists and is a supported format. + + Args: + video_path: Path to the video file + + Raises: + VideoFileError: If file doesn't exist or format is unsupported + """ + path = Path(video_path) + + if not path.exists(): + raise VideoFileError(f"Video file not found: {video_path}") + + if not path.is_file(): + raise VideoFileError(f"Path is not a file: {video_path}") + + if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS: + raise VideoFileError( + f"Unsupported video format: {path.suffix}. " + f"Supported formats: {', '.join(sorted(self.SUPPORTED_EXTENSIONS))}" + ) + + # Check if file is readable + if not os.access(video_path, os.R_OK): + raise VideoFileError(f"Video file is not readable: {video_path}") + + def detect_and_extract( + self, video_path: str, threshold: float = 0.3, resize_dim: tuple = (320, 240) + ) -> DetectionResult: + """ + Detect scenes using SSIM and extract representative frames. + Always extracts the first frame (frame 0) of the video. + + Args: + video_path: Path to the video file + threshold: SSIM threshold for scene detection (lower = stricter, default: 0.3) + Range: 0.0 to 1.0. Values below threshold indicate scene change. + resize_dim: Dimensions to resize frames for SSIM calculation (default: (320, 240)) + Smaller dimensions = faster computation + + Returns: + DetectionResult containing file_id, output_folder, total_frames, and list of SceneFrame objects + + Raises: + VideoFileError: If video file is invalid or inaccessible + FrameExtractionError: If frame extraction fails + SSIMDetectError: If SSIM detection processing fails + """ + # Validate input file before processing + self._validate_video_file(video_path) + + # Extract identifiers from the video path + # file_id: Video filename without extension (e.g., "video_123") + # dataset_id: Parent directory name (used for organizing outputs) + file_id = os.path.splitext(os.path.basename(video_path))[0] + dataset_id = os.path.basename(os.path.dirname(video_path)) + + # Create hierarchical output folder structure: + # SSIM_detects/ + # └── / + # └── / + # ├── frames/ (extracted frame images) + # └── _mapping.json (metadata) + base_detect_folder = "SSIM_detects" + output_folder = os.path.join(base_detect_folder, dataset_id, file_id) + frames_folder = os.path.join(output_folder, "frames") + + # Create all necessary directories (no error if they already exist) + os.makedirs(frames_folder, exist_ok=True) + + try: + # ================================================================ + # PHASE 1: Open video and validate + # ================================================================ + video = cv2.VideoCapture(video_path) + + if not video.isOpened(): + raise VideoFileError(f"Cannot open video: {video_path}") + + # Get total frames in video + total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + + print(f"Processing video: {video_path}") + print(f"Total frames: {total_frames}") + print(f"SSIM threshold: {threshold}") + + # ================================================================ + # PHASE 2: Extract first frame (always included) + # ================================================================ + success, prev_frame = video.read() + if not success: + video.release() + raise FrameExtractionError( + f"Cannot read first frame from: {video_path}" + ) + + scene_frames: List[SceneFrame] = [] + frame_count = 0 + + # Always save first frame with SSIM score of 1.0 (perfect match with itself) + self._save_frame(prev_frame, frame_count, 1.0, scene_frames, frames_folder) + print("Saved first frame (frame 0)") + + # ================================================================ + # PHASE 3: Process remaining frames with SSIM detection + # ================================================================ + while True: + success, curr_frame = video.read() + if not success: + break + + frame_count += 1 + + try: + # Calculate SSIM between current and previous frame + ssim_score = self._calculate_ssim( + prev_frame, curr_frame, resize_dim + ) + + # If SSIM is below threshold, it's a scene change + if ssim_score < threshold: + self._save_frame( + curr_frame, + frame_count, + ssim_score, + scene_frames, + frames_folder, + ) + print( + f"Saved keyframe {len(scene_frames) - 1} at frame {frame_count} (SSIM: {ssim_score:.3f})" + ) + prev_frame = curr_frame + elif frame_count % 100 == 0: + # Progress update every 100 frames + print( + f"Frame {frame_count}/{total_frames}: SSIM = {ssim_score:.3f} (threshold: {threshold})" + ) + except Exception as e: + # Log warning but continue with other frames (graceful degradation) + print(f"Warning: Failed to process frame {frame_count}: {e}") + continue + + video.release() + + # Validate that at least one frame was successfully extracted + if not scene_frames: + raise SSIMDetectError( + f"No frames extracted from video: {video_path}. " + "All frame extractions failed." + ) + + # Final success message with extraction statistics + print( + f"\nSuccessfully extracted {len(scene_frames)} frames from {frame_count + 1} total frames" + ) + + # Create result + result = DetectionResult( + file_id=file_id, + output_folder=output_folder, + total_frames=total_frames, + selected_frames=scene_frames, + ) + + # Save JSON mapping + self._save_json_mapping( + result, output_folder, file_id, threshold, resize_dim + ) + + return result + + except (VideoFileError, FrameExtractionError): + # Re-raise our custom exceptions + raise + except Exception as e: + raise SSIMDetectError(f"Unexpected error during SSIM detection: {e}") from e + + def _calculate_ssim( + self, frame1: np.ndarray, frame2: np.ndarray, resize_dim: tuple + ) -> float: + """ + Calculate SSIM score between two frames. + + Args: + frame1: First frame (BGR format from OpenCV) + frame2: Second frame (BGR format from OpenCV) + resize_dim: Dimensions to resize frames for SSIM calculation + + Returns: + SSIM score (0-1, where 1 is identical, 0 is completely different) + + Raises: + SSIMDetectError: If SSIM calculation fails + """ + try: + # Resize frames for faster computation + # Convert to grayscale for SSIM calculation + gray1 = cv2.cvtColor(cv2.resize(frame1, resize_dim), cv2.COLOR_BGR2GRAY) + gray2 = cv2.cvtColor(cv2.resize(frame2, resize_dim), cv2.COLOR_BGR2GRAY) + + # Calculate SSIM using scikit-image + # full=True returns the full SSIM image, we only need the score + score, _ = ssim(gray1, gray2, full=True) + + return float(score) + except Exception as e: + raise SSIMDetectError(f"Failed to calculate SSIM: {e}") from e + + def _save_frame( + self, + frame: np.ndarray, + frame_no: int, + ssim_score: float, + scene_frames: List[SceneFrame], + frames_folder: str, + ) -> None: + """ + Save a frame to disk and add to scene_frames list. + + Args: + frame: Frame to save (BGR format from OpenCV) + frame_no: Frame number (0-indexed) + ssim_score: SSIM score that triggered this frame extraction + scene_frames: List to append SceneFrame object to + frames_folder: Folder to save the frame + + Raises: + FrameExtractionError: If frame saving fails + """ + try: + # Convert BGR (OpenCV format) to RGB (PIL format) + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + pil_image = Image.fromarray(frame_rgb) + + # Save frame with frame number as filename + frame_filename = f"{frame_no}.jpg" + frame_path = os.path.join(frames_folder, frame_filename) + pil_image.save(frame_path) + + # Create SceneFrame object with SSIM score + scene_frame = SceneFrame( + frame_path=frame_path, frame_index=frame_no, ssim_score=ssim_score + ) + scene_frames.append(scene_frame) + except Exception as e: + raise FrameExtractionError(f"Failed to save frame {frame_no}: {e}") from e + + def _save_json_mapping( + self, + result: DetectionResult, + output_folder: str, + file_id: str, + threshold: float, + resize_dim: tuple, + ) -> None: + """ + Save JSON mapping of file_id to extracted scenes. + + Args: + result: DetectionResult object + output_folder: Folder to save the JSON file + file_id: Unique identifier for the video + threshold: SSIM threshold used for detection + resize_dim: Resize dimensions used for SSIM calculation + + Raises: + SSIMDetectError: If JSON file cannot be saved + """ + try: + # Use Pydantic's model_dump + result_dict = result.model_dump() + result_dict["total_selected_frames"] = len(result.selected_frames) + result_dict["threshold"] = threshold + result_dict["resize_dim"] = resize_dim + + json_path = os.path.join(output_folder, f"{file_id}_mapping.json") + with open(json_path, "w", encoding="utf-8") as f: + json.dump(result_dict, f, indent=2, ensure_ascii=False) + + print(f"JSON mapping saved to: {json_path}") + except (IOError, OSError) as e: + raise SSIMDetectError( + f"Failed to save JSON mapping to {json_path}: {e}" + ) from e + + +if __name__ == "__main__": + # Example usage + video_path = r"D:\Professional\Labellerr_SDK\SDKPython\labellerr\notebooks\Labellerr_datasets\354681d3-034a-4d66-b070-365f4bd11d8a\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4" + + # Get singleton instance + detector = SSIMSceneDetect() + + # Detect and extract frames + result = detector.detect_and_extract( + video_path=video_path, + threshold=0.6, # Lower value = more sensitive to changes + resize_dim=(320, 240), + ) + + print("\nDetection complete!") + print(f"Total frames extracted: {len(result.selected_frames)}") + print(f"Output folder: {result.output_folder}") From 012762e7828cfb110fb788bb68c9947f7adbfb82 Mon Sep 17 00:00:00 2001 From: yashsuman Date: Mon, 8 Dec 2025 11:25:07 +0530 Subject: [PATCH 08/14] - added video_json support to constants - minor changes to SDK --- labellerr/core/constants.py | 2 +- labellerr/notebooks/SDK.ipynb | 2 +- labellerr/notebooks/test_preannotation_api.py | 15 ++++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/labellerr/core/constants.py b/labellerr/core/constants.py index 6d1893e..ccd2d06 100644 --- a/labellerr/core/constants.py +++ b/labellerr/core/constants.py @@ -7,7 +7,7 @@ TOTAL_FILES_SIZE_LIMIT_PER_DATASET = 2.5 * 1024 * 1024 * 1024 # 2.5GB TOTAL_FILES_COUNT_LIMIT_PER_DATASET = 2500 -ANNOTATION_FORMAT = ["json", "coco_json", "csv", "png"] +ANNOTATION_FORMAT = ["json", "coco_json", "csv", "png", "video_json"] LOCAL_EXPORT_FORMAT = ["json", "coco_json", "csv", "png"] LOCAL_EXPORT_STATUS = [ "review", diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index efad8fb..1ef36b4 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -7,7 +7,7 @@ "source": [ "# Keyframe Scene detection with Labellerr SDK\n", "\n", - "This notebook demonstrates how to use the Labellerr SDK for video processing and scene detection. The SDK provides powerful tools for managing video datasets, processing videos, and detecting scene changes using various algorithms.\n" + "This notebook demonstrates how to use the Labellerr SDK for video processing and scene detection.\n" ] }, { diff --git a/labellerr/notebooks/test_preannotation_api.py b/labellerr/notebooks/test_preannotation_api.py index fb60c02..4f048bd 100644 --- a/labellerr/notebooks/test_preannotation_api.py +++ b/labellerr/notebooks/test_preannotation_api.py @@ -6,17 +6,22 @@ from labellerr.core.projects.video_project import LabellerrProject # Load environment variables from .env file -load_dotenv(r"labellerr\notebooks\dev.env") +load_dotenv(r"D:\Professional\Labellerr_SDK\dev.env") API_KEY = os.getenv("QA_API_KEY") API_SECRET = os.getenv("QA_API_SECRET") CLIENT_ID = os.getenv("QA_CLIENT_ID") -# print(API_KEY) -# print(API_SECRET) -# print(CLIENT_ID) + +# Validate that all required credentials are present +if not API_KEY: + raise ValueError("QA_API_KEY is not set") +if not API_SECRET: + raise ValueError("QA_API_SECRET is not set") +if not CLIENT_ID: + raise ValueError("QA_CLIENT_ID is not set") PROJECT_ID = "jeanna_mixed_aphid_93841" -VIDEO_JSON_FILE_PATH = r"D:\Professional\Labellerr_SDK\dumy_anotation.json" +VIDEO_JSON_FILE_PATH = r"D:\Professional\Labellerr_SDK\dummy_annotation.json" def main(): From 8f6e5563d53661dd8e40ebd405a898459926faea Mon Sep 17 00:00:00 2001 From: yashsuman Date: Tue, 9 Dec 2025 15:37:19 +0530 Subject: [PATCH 09/14] - Fixed the fiile naming convention for algo - Add keyframe annotation upload to Notebook - added coco to video json converter --- labellerr/core/files/base.py | 4 + labellerr/core/files/video_file.py | 68 ++- labellerr/notebooks/SDK.ipynb | 530 +++++++++++++----- labellerr/services/video_sampling/__init__.py | 410 +++++++++++++- .../services/video_sampling/ffmpeg_detect.py | 8 +- .../services/video_sampling/pyscene_detect.py | 78 +-- .../services/video_sampling/ssim_detect.py | 13 +- 7 files changed, 876 insertions(+), 235 deletions(-) diff --git a/labellerr/core/files/base.py b/labellerr/core/files/base.py index b504e1d..cc43647 100644 --- a/labellerr/core/files/base.py +++ b/labellerr/core/files/base.py @@ -112,3 +112,7 @@ def dataset_id(self): @property def metadata(self): return self.__file_data.get("file_metadata", {}) + + @property + def file_name(self): + return self.__file_data.get("file_name", "") diff --git a/labellerr/core/files/video_file.py b/labellerr/core/files/video_file.py index 97138b3..43fab0f 100644 --- a/labellerr/core/files/video_file.py +++ b/labellerr/core/files/video_file.py @@ -4,7 +4,7 @@ import uuid from concurrent.futures import ThreadPoolExecutor, as_completed from threading import Lock -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import requests @@ -116,8 +116,15 @@ def download_frames( :return: Dictionary with download statistics """ try: - # Use file_id as folder name - folder_name = self.file_id + # Use [Dataset_id]+[File_id]+[File_name] as folder name + if self.dataset_id and self.file_name: + # Remove extension from file_name if present + base_name = os.path.splitext(self.file_name)[0] + folder_name = f"{self.dataset_id}+{self.file_id}+{base_name}" + elif self.dataset_id: + folder_name = f"{self.dataset_id}+{self.file_id}" + else: + folder_name = self.file_id # Set output path if output_folder: @@ -208,7 +215,13 @@ def create_video( input_pattern = os.path.join(frames_folder, pattern) if output_file is None: - output_file = f"{self.file_id}.mp4" + # Use [Dataset_id]+[File_id]+[File_name] as default output filename + if self.dataset_id and self.file_name: + output_file = f"{self.dataset_id}+{self.file_id}+{self.file_name}" + elif self.dataset_id: + output_file = f"{self.dataset_id}+{self.file_id}.mp4" + else: + output_file = f"{self.file_id}.mp4" # FFmpeg command command = [ @@ -266,21 +279,26 @@ def download_create_video_auto_cleanup( print(f"Retrieved {len(frames_data)} frames") - # Step 2: Create dataset folder structure + # Step 2: Create output folder structure print("\n[2/4] Setting up output folders...") - if self.dataset_id is None: - dataset_folder = output_folder + # Videos will be saved directly in output_folder (labellerr_datasets) + os.makedirs(output_folder, exist_ok=True) + + # Define actual frames folder path using [Dataset_id]+[File_id]+[File_name] naming + # Frames will be temporarily stored in a subfolder for organization + if self.dataset_id and self.file_name: + base_name = os.path.splitext(self.file_name)[0] + folder_name = f"{self.dataset_id}+{self.file_id}+{base_name}" + elif self.dataset_id: + folder_name = f"{self.dataset_id}+{self.file_id}" else: - dataset_folder = os.path.join(output_folder, self.dataset_id) - os.makedirs(dataset_folder, exist_ok=True) - - # Define actual frames folder path - actual_frames_folder = os.path.join(dataset_folder, self.file_id) + folder_name = self.file_id + actual_frames_folder = os.path.join(output_folder, folder_name) # Step 3: Download frames print("\n[3/4] Downloading frames...") download_result = self.download_frames( - frames_data=frames_data, output_folder=dataset_folder + frames_data=frames_data, output_folder=output_folder ) if download_result["failed_downloads"] > 0: @@ -288,9 +306,16 @@ def download_create_video_auto_cleanup( f"\nWarning: {download_result['failed_downloads']} frames failed to download" ) - # Step 4: Create video from downloaded frames + # Step 4: Create video from downloaded frames using [Dataset_id]+[File_id]+[File_name] naming + # Save video directly in output_folder (labellerr_datasets) print("\n[4/4] Creating video from frames...") - video_output_path = os.path.join(dataset_folder, f"{self.file_id}.mp4") + if self.dataset_id and self.file_name: + video_filename = f"{self.dataset_id}+{self.file_id}+{self.file_name}" + elif self.dataset_id: + video_filename = f"{self.dataset_id}+{self.file_id}.mp4" + else: + video_filename = f"{self.file_id}.mp4" + video_output_path = os.path.join(output_folder, video_filename) self.create_video( frames_folder=actual_frames_folder, output_file=video_output_path @@ -307,7 +332,7 @@ def download_create_video_auto_cleanup( "file_id": self.file_id, "dataset_id": self.dataset_id, "video_path": video_output_path, - "output_folder": dataset_folder, + "output_folder": output_folder, "frames_downloaded": download_result["successful_downloads"], "frames_failed": download_result["failed_downloads"], "failed_frames_info": download_result["failed_frames"], @@ -322,13 +347,16 @@ def download_create_video_auto_cleanup( except Exception as e: # Attempt cleanup on error - # Get the frames folder path + # Get the frames folder path using [Dataset_id]+[File_id]+[File_name] naming if self.dataset_id is None: cleanup_folder = os.path.join(output_folder, self.file_id) else: - cleanup_folder = os.path.join( - output_folder, self.dataset_id, self.file_id - ) + if self.file_name: + base_name = os.path.splitext(self.file_name)[0] + folder_name = f"{self.dataset_id}+{self.file_id}+{base_name}" + else: + folder_name = f"{self.dataset_id}+{self.file_id}" + cleanup_folder = os.path.join(output_folder, folder_name) if os.path.exists(cleanup_folder): shutil.rmtree(cleanup_folder) diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index 1ef36b4..6c5a9b2 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 49, "id": "edcdab6a", "metadata": {}, "outputs": [], @@ -20,10 +20,13 @@ "from labellerr.client import LabellerrClient\n", "from labellerr.core.datasets import create_dataset_from_local, LabellerrDataset\n", "from labellerr.core.annotation_templates import create_template\n", - "from labellerr.core.projects import create_project\n", + "from labellerr.core.projects import create_project, LabellerrProject\n", "from labellerr.core.schemas.annotation_templates import AnnotationQuestion, QuestionType, CreateTemplateParams, DatasetDataType\n", "from labellerr.core.schemas import DatasetConfig\n", "from labellerr.core.schemas.projects import CreateProjectParams, RotationConfig\n", + "from labellerr.core.exceptions import LabellerrError\n", + "import requests\n", + "import json\n", "\n", "import uuid\n", "from pathlib import Path\n", @@ -54,20 +57,20 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "ab12f168", "metadata": {}, "outputs": [], "source": [ "from dotenv import dotenv_values\n", - "config = dotenv_values(\"dev.env\")\n", + "config = dotenv_values(r\"D:\\Professional\\Labellerr_SDK\\dev.env\")\n", "\n", - "api_key = config[\"API_KEY\"]\n", - "api_secret = config[\"API_SECRET\"]\n", - "client_id = config[\"CLIENT_ID\"]\n", - "email = config[\"EMAIL\"]\n", + "api_key = config[\"QA_API_KEY\"]\n", + "api_secret = config[\"QA_API_SECRET\"]\n", + "client_id = config[\"QA_CLIENT_ID\"]\n", + "email = config[\"QA_EMAIL\"]\n", "\n", - "client = LabellerrClient(api_key, api_secret, client_id)" + "client = LabellerrClient(api_key, api_secret, client_id)\n" ] }, { @@ -152,7 +155,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 13, "id": "52e00dbc", "metadata": {}, "outputs": [ @@ -162,7 +165,7 @@ "True" ] }, - "execution_count": 3, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -184,34 +187,39 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "c1e2e2f3", "metadata": {}, "outputs": [ { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:root:Total file count: 2\n", - "INFO:root:Total file size: 42.2 MB\n", - "INFO:root:CPU count: 24, Batch Count: 2\n", - "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api-gateway-qcb3iv2gaa-uc.a.run.app:443\n", - "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api-gateway-qcb3iv2gaa-uc.a.run.app:443\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"POST /connectors/connect/local?client_id=1 HTTP/1.1\" 200 1085\n", - "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"POST /connectors/connect/local?client_id=1 HTTP/1.1\" 200 1079\n", - "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", - "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"POST /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/seafood_1280p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101252Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=abb81e14c8815af5553818d4456594c59e80daf8d1843c4b822a277b2191904a53f0449c4da0dafb04be6055d47e63cb7dc1e6a99bd466bb1a82aa7e8484af661e126917a444fbbaa03532224f7915f061f16860c4a4e7010846733d48f13be90e0af96ed39e155e85644661f59373e15619c7a8e6d1feb9bb2b7e8ce82192acbc86d98a4ef48463717bf14611a1a90b9b16245a9fb0cf7e38962dcf3761c5fddf82b0d141f5eff7f0b0f4d0743957af87a9606563e99ca41b6a48d2961fb8cb70bf2408e8a669b5d42f85b3d4bffaaf488430c5b71df018bd39d104b6a3f3114d669b8dd29762f433999c8f7ac1ef35f653c614522e1ff934690129fd4c6060 HTTP/1.1\" 201 0\n", - "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", - "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"POST /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/butterflies_960p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101251Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=23e640fedb8bde8db621437d85075adb326eccb5969891a150fe3a4c2769170332ddef85aee49e81168dba63ffbbb021a4e8290ff77eb73daaff6e56c7d0087edd9502a2bfd6e31404e38ba02424e4e2ea6b52106a2681aae5c9e76c0e19b0d8c198811c577f6408eceb1a04ac8d21d1f05608e83114eb56a09c078733e0ae2c5c71294ba0f7ab6c04a0bf158a289f3c300b619e5b1b39382370b2457cade3987eac9f304dc52cb975ae2ce45a628a76496846d80c7aff125afbd8ab999edfeca42a296aff65bf3c8c7f6f48c30f6f4c9caf2fa917e4c0261303a325db68e31401816fe3cf20693459417156c41aec770ffa5e2ee88b77e5d746dacee71225a2 HTTP/1.1\" 201 0\n", - "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): storage.googleapis.com:443\n", - "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"PUT /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/butterflies_960p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101251Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=23e640fedb8bde8db621437d85075adb326eccb5969891a150fe3a4c2769170332ddef85aee49e81168dba63ffbbb021a4e8290ff77eb73daaff6e56c7d0087edd9502a2bfd6e31404e38ba02424e4e2ea6b52106a2681aae5c9e76c0e19b0d8c198811c577f6408eceb1a04ac8d21d1f05608e83114eb56a09c078733e0ae2c5c71294ba0f7ab6c04a0bf158a289f3c300b619e5b1b39382370b2457cade3987eac9f304dc52cb975ae2ce45a628a76496846d80c7aff125afbd8ab999edfeca42a296aff65bf3c8c7f6f48c30f6f4c9caf2fa917e4c0261303a325db68e31401816fe3cf20693459417156c41aec770ffa5e2ee88b77e5d746dacee71225a2&upload_id=AOCedOEk1QTBtYylxPr3uH4ac4enQIiFVX-WRK66hGbV0b3MSd0MYrQUnrEUsOmbR2ZkjlujH3zdLrGX2v11KdAVScEBhe_qr_4nQdUqck5u_nk HTTP/1.1\" 200 0\n", - "DEBUG:urllib3.connectionpool:https://storage.googleapis.com:443 \"PUT /labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/seafood_1280p.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251117%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251117T101252Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=content-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=abb81e14c8815af5553818d4456594c59e80daf8d1843c4b822a277b2191904a53f0449c4da0dafb04be6055d47e63cb7dc1e6a99bd466bb1a82aa7e8484af661e126917a444fbbaa03532224f7915f061f16860c4a4e7010846733d48f13be90e0af96ed39e155e85644661f59373e15619c7a8e6d1feb9bb2b7e8ce82192acbc86d98a4ef48463717bf14611a1a90b9b16245a9fb0cf7e38962dcf3761c5fddf82b0d141f5eff7f0b0f4d0743957af87a9606563e99ca41b6a48d2961fb8cb70bf2408e8a669b5d42f85b3d4bffaaf488430c5b71df018bd39d104b6a3f3114d669b8dd29762f433999c8f7ac1ef35f653c614522e1ff934690129fd4c6060&upload_id=AOCedOGtu8tVgMoqoFCcI18CqQhY_CARXt2x5laIObdMBYybxcvoZxuxl109xF29WQm1LRs4fg0cgVABEFGzBSEtpQfhE8mQNrmK8wWicqyBIXw HTTP/1.1\" 200 0\n", - "INFO:root:Folder uploaded successfully. {'success': ['..\\\\..\\\\..\\\\.cache\\\\kagglehub\\\\datasets\\\\mistag\\\\short-videos\\\\versions\\\\4\\\\butterflies_960p.mp4', '..\\\\..\\\\..\\\\.cache\\\\kagglehub\\\\datasets\\\\mistag\\\\short-videos\\\\versions\\\\4\\\\seafood_1280p.mp4'], 'fail': []}\n", - "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api-gateway-qcb3iv2gaa-uc.a.run.app:443\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"POST /datasets/create?client_id=1&uuid=7f7f78dc-4c7f-4ab3-a5f3-7bc308733a98 HTTP/1.1\" 200 361\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=a3768b9c-17d2-40f0-8f06-4b0538b8f0db HTTP/1.1\" 200 361\n" - ] + "data": { + "text/plain": [ + "{'es_multimodal_index': False,\n", + " 'metadata': {},\n", + " 'dataset_id': '15908795-09eb-4cdb-a39b-8689f8f936e5',\n", + " 'origin': 'https://pro.labellerr.com',\n", + " 'created_at': 1765198673509,\n", + " 'description': '',\n", + " 'created_by': '1c8800.8177f647b0b9bc6321bcec4d93',\n", + " 'client_id': '79',\n", + " 'tags': [],\n", + " 'data_type': 'video',\n", + " 'name': 'VIDEO_DATASET_(2)',\n", + " 'extraction_quality': ['normal'],\n", + " 'updated_at': 1765198765922,\n", + " 'progress': 'Processing 0/2 files',\n", + " 'files_count': 2,\n", + " 'status_code': 300,\n", + " 'video_processing_job_id': 'e08357f1-edaa-4b42-a9fb-de3bd9230b4a',\n", + " 'es_index_status': 101,\n", + " 'video_processing_job': {'job_id': 'e08357f1-edaa-4b42-a9fb-de3bd9230b4a',\n", + " 'status_code': 200,\n", + " 'updated_at': 1765198785019}}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -223,68 +231,45 @@ "\n", "dataset = create_dataset_from_local(\n", " client=client,\n", - " dataset_config=DatasetConfig(dataset_name=\"SDK VIDEO DATASET\", \n", + " dataset_config=DatasetConfig(dataset_name=\"VIDEO_DATASET_(2)\", \n", " data_type=\"video\"),\n", " folder_to_upload=KAGGLE_DATASET_PATH,\n", - " )" + " )\n", + "\n", + "dataset.status()" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 15, "id": "3d82b343", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=d4eef5b7-31da-4188-87d1-cb42176f6f5a HTTP/1.1\" 200 361\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=85af0df1-2466-4987-bda3-36bf7bc8f87b HTTP/1.1\" 200 361\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=d73a6c0a-9077-49f0-939b-1ea2cb4bb04d HTTP/1.1\" 200 361\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=81833057-1650-425a-b547-94fae62e7de9 HTTP/1.1\" 200 422\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=9b4ea0fe-6ed8-48f0-adb4-f5da409a70a0 HTTP/1.1\" 200 422\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=7f4b488b-2aa4-4fb2-ba95-a6c08e678f2c HTTP/1.1\" 200 422\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=a9686c6c-c8b1-4abe-9809-2551c2ded379 HTTP/1.1\" 200 422\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=6b1de878-8a47-49c8-a64d-ec54647d7129 HTTP/1.1\" 200 438\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=d3ff7197-37fa-4b0d-b70e-fbb9d1af6cf0 HTTP/1.1\" 200 438\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=e9644e97-69ef-423c-8015-c69f42970687 HTTP/1.1\" 200 438\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=1575a956-f3ea-4421-9db0-bb0b78aaa6b2 HTTP/1.1\" 200 438\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=00e88efd-93c5-4f83-9b07-ccf5a009764c HTTP/1.1\" 200 438\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=30058fd5-1d2f-4306-84dd-79abd23eff15 HTTP/1.1\" 200 438\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=fd2f80b2-c251-4e35-84ef-3eec7c96f20c HTTP/1.1\" 200 438\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=3f3d5118-6663-4573-a7b1-02ca22bb3588 HTTP/1.1\" 200 438\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=264d975f-1c10-4583-ae07-7f57534fad5f HTTP/1.1\" 200 438\n", - "DEBUG:urllib3.connectionpool:https://api-gateway-qcb3iv2gaa-uc.a.run.app:443 \"GET /datasets/354681d3-034a-4d66-b070-365f4bd11d8a?client_id=1&uuid=bf74e8a6-4136-4cc5-b44d-329b0b4261e5 HTTP/1.1\" 200 643\n", - "INFO:root:Dataset 354681d3-034a-4d66-b070-365f4bd11d8a processing completed successfully!\n" - ] - }, { "data": { "text/plain": [ "2" ] }, - "execution_count": 7, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "dataset.status()\n", "dataset.dataset_id\n", "dataset.files_count" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "5205f618", "metadata": {}, "outputs": [], "source": [ "dataset = LabellerrDataset(client=client,\n", - " dataset_id=\"354681d3-034a-4d66-b070-365f4bd11d8a\")" + " dataset_id='15908795-09eb-4cdb-a39b-8689f8f936e5')" ] }, { @@ -297,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 17, "id": "7be23d9a", "metadata": {}, "outputs": [], @@ -323,17 +308,17 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 18, "id": "7e281c33", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'cea9f8f1-11cb-472f-97b1-e2619be47051'" + "'4dd84aa0-1a06-4cea-a758-40076c7e3d8c'" ] }, - "execution_count": 5, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -344,14 +329,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "62360ea1", "metadata": {}, "outputs": [], "source": [ "from labellerr.core.annotation_templates import LabellerrAnnotationTemplate\n", "template = LabellerrAnnotationTemplate(client=client,\n", - " annotation_template_id='35d44c7d-9b02-4eb0-9dee-9a7ff1165331')" + " annotation_template_id='4dd84aa0-1a06-4cea-a758-40076c7e3d8c')" ] }, { @@ -364,7 +349,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "8996dd01", "metadata": {}, "outputs": [], @@ -372,7 +357,7 @@ "video_project = create_project(\n", " client=client,\n", " params=CreateProjectParams(\n", - " project_name=\"SDK VIDEO PROJECT\",\n", + " project_name=\"SDK VIDEO PROJECT TEST\",\n", " data_type=DatasetDataType.video,\n", " rotations=RotationConfig(\n", " annotation_rotation_count=1,\n", @@ -387,17 +372,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "724c67cc", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'gusella_late_marmoset_23922'" + "'caryl_geographical_turkey_21445'" ] }, - "execution_count": 14, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -408,14 +393,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 54, "id": "f74cba85", "metadata": {}, "outputs": [], "source": [ - "from labellerr.core.projects import LabellerrProject\n", "video_project = LabellerrProject(client=client,\n", - " project_id='gusella_late_marmoset_23922')" + " project_id='caryl_geographical_turkey_21445')" ] }, { @@ -429,7 +413,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "6558a9e9", "metadata": {}, "outputs": [ @@ -439,11 +423,11 @@ "text": [ "\n", "######################################################################\n", - "# Starting batch video processing for dataset: 354681d3-034a-4d66-b070-365f4bd11d8a\n", + "# Starting batch video processing for dataset: 15908795-09eb-4cdb-a39b-8689f8f936e5\n", "######################################################################\n", "\n", - "Fetching files for dataset: 354681d3-034a-4d66-b070-365f4bd11d8a\n", - "{'message': '200: Success', 'response': {'files': [{'has_embedding': False, 'file_id': '2a8d96ca-9161-4dee-ad3b-a5faf301bc6c', 'created_at': 1763374470963, 'file_name_original': 'butterflies_960p.mp4', 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a', 'connection_id': 'fa03a1f3-3b77-42f9-b8de-eef499af4ee9', 'email_id': 'e0811e.ba8447468b95374970256d3c2b', 'file_name': 'butterflies_960p.mp4', 'file_reference': 'gs://labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/butterflies_960p.mp4', 'file_metadata': {'file_size': 25.047, 'image_width': None, 'file_format': 'mp4', 'additional_metadata': None, 'image_height': None}, 'created_by': 'e0811e.ba8447468b95374970256d3c2b', 'data_type': 'video'}, {'has_embedding': False, 'file_id': '7db3f60c-f6e5-4d3d-a63b-cb38530ee265', 'created_at': 1763374470963, 'file_name_original': 'seafood_1280p.mp4', 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a', 'connection_id': 'fa03a1f3-3b77-42f9-b8de-eef499af4ee9', 'email_id': 'e0811e.ba8447468b95374970256d3c2b', 'file_name': 'seafood_1280p.mp4', 'file_reference': 'gs://labellerr-connector-files-dev/local_upload/fa03a1f3-3b77-42f9-b8de-eef499af4ee9/seafood_1280p.mp4', 'file_metadata': {'file_size': 17.156, 'image_width': None, 'file_format': 'mp4', 'additional_metadata': None, 'image_height': None}, 'created_by': 'e0811e.ba8447468b95374970256d3c2b', 'data_type': 'video'}], 'total_count': 2, 'next_search_after': None}, 'error': None, 'tracking_id': '8d4669aea326833e9788f71d56c3a0ed'}\n", + "Fetching files for dataset: 15908795-09eb-4cdb-a39b-8689f8f936e5\n", + "{'message': '200: Success', 'response': {'files': [{'has_embedding': False, 'file_id': '471163aa-19dc-4bc7-9aee-04780591281a', 'created_at': 1765198766042, 'file_name_original': 'butterflies_960p.mp4', 'dataset_id': '15908795-09eb-4cdb-a39b-8689f8f936e5', 'connection_id': 'fa6598b0-7a6c-4327-9d00-eb8ae6fd445f', 'email_id': '1c8800.8177f647b0b9bc6321bcec4d93', 'file_name': 'butterflies_960p.mp4', 'file_reference': 'gs://labellerr-connector-files-dev/local_upload/fa6598b0-7a6c-4327-9d00-eb8ae6fd445f/butterflies_960p.mp4', 'file_metadata': {'file_size': 25.047, 'image_width': None, 'file_format': 'mp4', 'additional_metadata': None, 'image_height': None}, 'created_by': '1c8800.8177f647b0b9bc6321bcec4d93', 'data_type': 'video', 'video_playlist': 'https://api-gateway-722091373895.us-central1.run.app/data/15908795-09eb-4cdb-a39b-8689f8f936e5/files/471163aa-19dc-4bc7-9aee-04780591281a/segments/master.m3u8'}, {'has_embedding': False, 'file_id': 'a878e61b-8aeb-46e1-ab10-5f1852bcdcbe', 'created_at': 1765198766042, 'file_name_original': 'seafood_1280p.mp4', 'dataset_id': '15908795-09eb-4cdb-a39b-8689f8f936e5', 'connection_id': 'fa6598b0-7a6c-4327-9d00-eb8ae6fd445f', 'email_id': '1c8800.8177f647b0b9bc6321bcec4d93', 'file_name': 'seafood_1280p.mp4', 'file_reference': 'gs://labellerr-connector-files-dev/local_upload/fa6598b0-7a6c-4327-9d00-eb8ae6fd445f/seafood_1280p.mp4', 'file_metadata': {'file_size': 17.156, 'image_width': None, 'file_format': 'mp4', 'additional_metadata': None, 'image_height': None}, 'created_by': '1c8800.8177f647b0b9bc6321bcec4d93', 'data_type': 'video', 'video_playlist': 'https://api-gateway-722091373895.us-central1.run.app/data/15908795-09eb-4cdb-a39b-8689f8f936e5/files/a878e61b-8aeb-46e1-ab10-5f1852bcdcbe/segments/master.m3u8'}], 'total_count': 2, 'next_search_after': None}, 'error': None, 'tracking_id': 'f2364207b402f43da228219f2209c267'}\n", "\n", "Processing 2 video files...\n", "\n", @@ -451,7 +435,7 @@ "Starting download of 2 files...\n", "\n", "============================================================\n", - "Processing file: 2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\n", + "Processing file: 471163aa-19dc-4bc7-9aee-04780591281a\n", "============================================================\n", "\n", "[1/4] Fetching frame data from API (0 to 1572)...\n", @@ -464,20 +448,20 @@ "Frames downloaded: 1572/1572 (1572 successful, 0 failed)\n", "\n", "[4/4] Creating video from frames...\n", - "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4\n", - "Video saved as ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4\n", + "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p.mp4\n", + "Video saved as ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p.mp4\n", "\n", "Cleaning up temporary frames...\n", - "Removed temporary frames folder: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\n", + "Removed temporary frames folder: ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p\n", "\n", "============================================================\n", "Processing complete!\n", - "Video saved to: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4\n", - "{'='*60}\n", + "Video saved to: ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p.mp4\n", + "============================================================\n", "\n", "Files processed: 1/2 (1 successful, 0 failed)\n", "============================================================\n", - "Processing file: 7db3f60c-f6e5-4d3d-a63b-cb38530ee265\n", + "Processing file: a878e61b-8aeb-46e1-ab10-5f1852bcdcbe\n", "============================================================\n", "\n", "[1/4] Fetching frame data from API (0 to 389)...\n", @@ -490,16 +474,16 @@ "Frames downloaded: 389/389 (389 successful, 0 failed)\n", "\n", "[4/4] Creating video from frames...\n", - "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4\n", - "Video saved as ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4\n", + "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p.mp4\n", + "Video saved as ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p.mp4\n", "\n", "Cleaning up temporary frames...\n", - "Removed temporary frames folder: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265\n", + "Removed temporary frames folder: ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p\n", "\n", "============================================================\n", "Processing complete!\n", - "Video saved to: ./Labellerr_datastets\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4\n", - "{'='*60}\n", + "Video saved to: ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p.mp4\n", + "============================================================\n", "\n", "Files processed: 2/2 (2 successful, 0 failed)\n", "######################################################################\n", @@ -515,24 +499,24 @@ "data": { "text/plain": [ "[{'status': 'success',\n", - " 'file_id': '2a8d96ca-9161-4dee-ad3b-a5faf301bc6c',\n", - " 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a',\n", - " 'video_path': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a\\\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4',\n", - " 'output_folder': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a',\n", + " 'file_id': '471163aa-19dc-4bc7-9aee-04780591281a',\n", + " 'dataset_id': '15908795-09eb-4cdb-a39b-8689f8f936e5',\n", + " 'video_path': './Labellerr_datasets\\\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p.mp4',\n", + " 'output_folder': './Labellerr_datasets',\n", " 'frames_downloaded': 1572,\n", " 'frames_failed': 0,\n", " 'failed_frames_info': []},\n", " {'status': 'success',\n", - " 'file_id': '7db3f60c-f6e5-4d3d-a63b-cb38530ee265',\n", - " 'dataset_id': '354681d3-034a-4d66-b070-365f4bd11d8a',\n", - " 'video_path': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a\\\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4',\n", - " 'output_folder': './Labellerr_datastets\\\\354681d3-034a-4d66-b070-365f4bd11d8a',\n", + " 'file_id': 'a878e61b-8aeb-46e1-ab10-5f1852bcdcbe',\n", + " 'dataset_id': '15908795-09eb-4cdb-a39b-8689f8f936e5',\n", + " 'video_path': './Labellerr_datasets\\\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p.mp4',\n", + " 'output_folder': './Labellerr_datasets',\n", " 'frames_downloaded': 389,\n", " 'frames_failed': 0,\n", " 'failed_frames_info': []}]" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -581,7 +565,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 57, "id": "f5c41073", "metadata": {}, "outputs": [ @@ -595,7 +579,13 @@ } ], "source": [ - "from labellerr.services.video_sampling import PySceneDetect" + "from labellerr.services.video_sampling import (\n", + " PySceneDetect,\n", + " FFMPEGSceneDetect,\n", + " SSIMSceneDetect,\n", + " process_videos_batch,\n", + " coco_to_video_json\n", + ")" ] }, { @@ -608,7 +598,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "49a6f89d", "metadata": {}, "outputs": [ @@ -616,22 +606,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "Path exists ✅\n" + "Path exists and is not empty ✅\n" ] } ], "source": [ - "dataset_dir = Path(f\".\\\\Labellerr_datasets\\\\{dataset.dataset_id}\")\n", - "\n", - "if dataset_dir.exists():\n", - " print(\"Path exists ✅\")\n", + "DATASET_DIR = Path(f\".\\\\Labellerr_datasets\")\n", + "if DATASET_DIR.exists() and any(DATASET_DIR.iterdir()):\n", + " print(\"Path exists and is not empty ✅\")\n", "else:\n", - " print(\"Path does not exist ❌\")\n" + " print(\"Path does not exist or is empty ❌\")\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "id": "dd96be8c", "metadata": {}, "outputs": [], @@ -641,7 +631,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 17, "id": "a3052f25", "metadata": {}, "outputs": [ @@ -649,17 +639,63 @@ "name": "stdout", "output_type": "stream", "text": [ - "JSON mapping saved to: PyScene_detects\\354681d3-034a-4d66-b070-365f4bd11d8a\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c\\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c_mapping.json\n", - "JSON mapping saved to: PyScene_detects\\354681d3-034a-4d66-b070-365f4bd11d8a\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265\\7db3f60c-f6e5-4d3d-a63b-cb38530ee265_mapping.json\n" + "Found 2 video files to process\n", + "======================================================================\n", + "\n", + "[1/2] Processing: 15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p.mp4\n", + "----------------------------------------------------------------------\n", + "Detecting scene changes...\n", + "Detected 4 scene changes\n", + "Total frames in video: 1572\n", + "Extracting first frame (frame 0)...\n", + "Successfully extracted 5 frames to pyscene_detect\n", + "✓ Successfully extracted 5 frames\n", + "\n", + "[2/2] Processing: 15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p.mp4\n", + "----------------------------------------------------------------------\n", + "Detecting scene changes...\n", + "Detected 0 scene changes\n", + "Total frames in video: 389\n", + "Extracting first frame (frame 0)...\n", + "Successfully extracted 1 frames to pyscene_detect\n", + "✓ Successfully extracted 1 frames\n", + "\n", + "======================================================================\n", + "PROCESSING SUMMARY\n", + "======================================================================\n", + "Total videos processed: 2\n", + "Successful: 2\n", + "Failed: 0\n", + "Total frames extracted: 6\n", + "\n", + "✓ Frames stored in: pyscene_detect/\n" ] } ], "source": [ - "for filename in os.listdir(dataset_dir):\n", - " file_path = os.path.join(dataset_dir, filename)\n", - " \n", - " if os.path.isfile(file_path):\n", - " detector.detect_and_extract(file_path)" + "response = process_videos_batch(detector, DATASET_DIR)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "c83cf401", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'.\\\\pyscene_detect'" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "keyframe_img_path =\".\\\\\" + response[0]['output_folder']\n", + "keyframe_img_path" ] }, { @@ -683,7 +719,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "id": "82629d12", "metadata": {}, "outputs": [], @@ -692,10 +728,42 @@ " client=client,\n", " dataset_config=DatasetConfig(dataset_name=\"SDK VIDEO KEYFRAME DATASET\", \n", " data_type=\"image\"),\n", - " folder_to_upload=dataset_dir,\n", + " folder_to_upload=keyframe_img_path,\n", " )" ] }, + { + "cell_type": "code", + "execution_count": 27, + "id": "d530faed", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'1addcef0-2c60-4442-8701-856efa573afc'" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset.dataset_id" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e0793ba6", + "metadata": {}, + "outputs": [], + "source": [ + "dataset = LabellerrDataset(client=client,\n", + " dataset_id='1addcef0-2c60-4442-8701-856efa573afc')" + ] + }, { "cell_type": "markdown", "id": "756764dd", @@ -706,7 +774,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "5d5e23c5", "metadata": {}, "outputs": [], @@ -730,6 +798,27 @@ ")" ] }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a05fac70", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'1502051f-dbe7-4216-9426-df5757afea85'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "template.annotation_template_id" + ] + }, { "cell_type": "markdown", "id": "1bef8bfe", @@ -740,7 +829,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "b235e08d", "metadata": {}, "outputs": [], @@ -748,8 +837,8 @@ "img_project = create_project(\n", " client=client,\n", " params=CreateProjectParams(\n", - " project_name=\"SDK VIDEO PROJECT\",\n", - " data_type=DatasetDataType.video,\n", + " project_name=\"SDK EXTRACTED KEYFRAMES\",\n", + " data_type=DatasetDataType.image,\n", " rotations=RotationConfig(\n", " annotation_rotation_count=1,\n", " review_rotation_count=1,\n", @@ -761,6 +850,38 @@ ")" ] }, + { + "cell_type": "code", + "execution_count": 15, + "id": "fc8b7e25", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'laurette_constitutional_herring_39772'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "img_project.project_id" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "55c824a7", + "metadata": {}, + "outputs": [], + "source": [ + "img_project = LabellerrProject(client=client,\n", + " project_id='laurette_constitutional_herring_39772')" + ] + }, { "cell_type": "markdown", "id": "8645aa60", @@ -782,10 +903,10 @@ }, { "cell_type": "markdown", - "id": "8f0611f5", + "id": "85d88826", "metadata": {}, "source": [ - "### Downloading the Annotation" + "### Create Export" ] }, { @@ -794,7 +915,115 @@ "id": "ccf0e882", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "export_config = {\n", + " \"export_name\": \"Test Export\",\n", + " \"export_description\": \"Export for testing\",\n", + " \"export_format\": \"coco_json\",\n", + " \"statuses\": ['review', 'r_assigned', 'client_review', 'cr_assigned', 'accepted']\n", + "}\n", + "\n", + "result = project.create_local_export(export_config)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "220eaf7a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'zmYykSJhCAJqAaXaJQ3g'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result.report_id" + ] + }, + { + "cell_type": "markdown", + "id": "c7ba3c97", + "metadata": {}, + "source": [ + "### Check Status of export" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef475591", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'status': [{'report_id': 'zmYykSJhCAJqAaXaJQ3g', 'export_status': 'Created', 'is_completed': True, 'download_url': {'url': 'https://storage.googleapis.com/labellerr-export-dev/92075cec-468b-4cc3-90e3-4b1f2691c57c.json?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251209%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251209T064534Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&response-content-type=json&response-content-disposition=attachment%3B%20filename%3D%22export-%23zmYykSJhCAJqAaXaJQ3g.json%22&X-Goog-Signature=6c7cf8c2357dc98b749ffa32bf234a952d5c715d5742a91e3ff4cee8e893334a56181e238f2066ce0f05bef2b94dd626c25d0b40fe102fde64e090768462413e5d4ca80da2b77850ec5d0a661b076a11da5b5f44868fc020547b0b53ae3f7c7d0ac47257733a322e4e6face965505dbe4fcb797c0f951505f27afbe62d31bbaf981f200b34f5b76b8ac890be8d009e68158ebb5adab04b7eb48461a373478ca384ea54049f93e8501b5784c740544cc518f86ecd05134f805c1cf1f26f43a7df0d3d05a04cbb261fe51b5448c580a1d991ae0ac9309aea07103b67917e0c0f1c6790cf95c16505231d491c93499b495f01a18d22c462510b12c8361fedc72fa3', 'expires_at': 1765266334139, 'is_expired': False}}]}\n" + ] + } + ], + "source": [ + "try:\n", + " # Get project instance\n", + " project = LabellerrProject(client=client, project_id=project_id)\n", + " \n", + " # Check export status\n", + " response_data = json.loads(project.check_export_status(\n", + " report_ids=[result.report_id]\n", + " ))\n", + " print(response_data)\n", + "except LabellerrError as e:\n", + " print(f\"Failed to check export status: {str(e)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "8f0611f5", + "metadata": {}, + "source": [ + "### Download the Annotation" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "1d5455c3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Downloaded: export_zmYykSJhCAJqAaXaJQ3g.json\n", + "Export saved at: export_zmYykSJhCAJqAaXaJQ3g.json\n" + ] + } + ], + "source": [ + "download_url = response_data['status'][0]['download_url']['url']\n", + "\n", + "# Download the file\n", + "response = requests.get(download_url)\n", + "if response.status_code == 200:\n", + " # Store the filename/path in a variable\n", + " export_json_path = f\"export_{response_data['status'][0]['report_id']}.json\"\n", + " \n", + " with open(export_json_path, 'wb') as f:\n", + " f.write(response.content)\n", + " print(f\"✓ Downloaded: {export_json_path}\")\n", + "else:\n", + " print(f\"✗ Download failed: HTTP {response.status_code}\")\n", + "# Now you can use export_json_path variable for further processing\n", + "print(f\"Export saved at: {export_json_path}\")" + ] }, { "cell_type": "markdown", @@ -815,30 +1044,51 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 58, "id": "14b59cef", "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Video JSON saved to: Video_Keyframe_annot.json\n" + ] + } + ], + "source": [ + "video_annotations = coco_to_video_json(export_json_path)" + ] }, { "cell_type": "markdown", "id": "deae26b8", "metadata": {}, "source": [ - "### Uploading pre-annotation" + "### Uploading keyframe pre-annotation" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 60, "id": "df6b3ac7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'message': '200: Success', 'response': {'metadata': {'questions_ignored': [], 'activity_id': 'ee67556d-ba08-4f71-9767-eff9300ee8d9', 'files_not_updated': [], 'videos_processed': [{'status': 'success', 'file_id': '471163aa-19dc-4bc7-9aee-04780591281a', 'file_name': 'butterflies_960p.mp4', 'frames_processed': 5, 'total_annotations': 5}, {'status': 'success', 'file_name': 'seafood_1280p.mp4', 'file_id': 'a878e61b-8aeb-46e1-ab10-5f1852bcdcbe', 'total_annotations': 1, 'frames_processed': 1}]}, 'job_type': 'pre-annotations', 'created_by': '1c8800.8177f647b0b9bc6321bcec4d93', 'activity_id': 'ee67556d-ba08-4f71-9767-eff9300ee8d9', 'status': 'completed', 'project_id': 'caryl_geographical_turkey_21445', 'job_id': 'ee67556d-ba08-4f71-9767-eff9300ee8d9', 'created_at': 1765273090056, 'updated_at': 1765273225613}, 'error': None, 'tracking_id': None}\n" + ] + } + ], "source": [ - "VIDEO_JSON_PATH = r\"path_to_your_video_preannotation_file.json\"\n", + "VIDEO_JSON_PATH = \"./Video_Keyframe_annot.json\"\n", "\n", - "video_project.upload_preannotations(video_json_file_path=VIDEO_JSON_PATH)" + "response = video_project.upload_preannotations(\n", + " annotation_format=\"video_json\", annotation_file=VIDEO_JSON_PATH\n", + " )\n", + "print(response)" ] } ], diff --git a/labellerr/services/video_sampling/__init__.py b/labellerr/services/video_sampling/__init__.py index 4adb92f..d9a00ed 100644 --- a/labellerr/services/video_sampling/__init__.py +++ b/labellerr/services/video_sampling/__init__.py @@ -3,12 +3,416 @@ All algorithms for video sampling will go in separate files. """ -from .ffmpeg_detect import FFMPEGSceneDetect -from .pyscene_detect import PySceneDetect -from .ssim_detect import SSIMSceneDetect +import json +import os +import re +from collections import defaultdict +from pathlib import Path +from typing import Any, Dict, List, Optional, Union + +# Try to import detectors (optional dependencies) +try: + from .ffmpeg_detect import FFMPEGSceneDetect + from .pyscene_detect import PySceneDetect + from .ssim_detect import SSIMSceneDetect + + _DETECTORS_AVAILABLE = True +except ImportError: + _DETECTORS_AVAILABLE = False + FFMPEGSceneDetect = None + PySceneDetect = None + SSIMSceneDetect = None __all__ = [ "FFMPEGSceneDetect", "PySceneDetect", "SSIMSceneDetect", + "process_videos_batch", + "coco_to_video_json", ] + + +# Supported video file extensions +VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv", ".webm", ".m4v"} + + +def process_videos_batch( + detector: Union[PySceneDetect, FFMPEGSceneDetect, SSIMSceneDetect], + dataset_dir: Union[str, Path], + **detector_kwargs, +) -> List[Dict[str, Any]]: + """ + Process all video files in a directory using the specified detector algorithm. + + This function works with any detector algorithm (PySceneDetect, FFMPEGSceneDetect, + SSIMSceneDetect) and processes all video files in the specified directory. + All extracted frames will be stored according to each detector's output structure. + + Args: + detector: Instance of any detector class (PySceneDetect, FFMPEGSceneDetect, or SSIMSceneDetect) + dataset_dir: Path to directory containing video files to process + **detector_kwargs: Additional keyword arguments to pass to the detector's detect_and_extract method + (e.g., threshold=0.3, resize_dim=(320, 240) for SSIMSceneDetect) + + Returns: + List of dictionaries containing processing results for each video: + - filename: Name of the video file + - status: 'success' or 'failed' + - frames_extracted: Number of frames extracted (if successful) + - output_folder: Path where frames were stored (if successful) + - error: Error message (if failed) + + Example: + >>> from labellerr.services.video_sampling import PySceneDetect, process_videos_batch + >>> detector = PySceneDetect() + >>> results = process_videos_batch(detector, "./Labellerr_datasets") + >>> print(f"Processed {len(results)} videos") + + >>> # Using SSIMSceneDetect with custom parameters + >>> from labellerr.services.video_sampling import SSIMSceneDetect, process_videos_batch + >>> detector = SSIMSceneDetect() + >>> results = process_videos_batch( + ... detector, + ... "./Labellerr_datasets", + ... threshold=0.3, + ... resize_dim=(320, 240) + ... ) + """ + # Convert to Path object for easier handling + dataset_path = Path(dataset_dir) + + # Verify dataset directory exists + if not dataset_path.exists(): + raise FileNotFoundError(f"Dataset directory not found: {dataset_dir}") + + if not dataset_path.is_dir(): + raise NotADirectoryError(f"Path is not a directory: {dataset_dir}") + + # Get all video files from the dataset directory + video_files = [ + f + for f in os.listdir(dataset_path) + if os.path.isfile(os.path.join(dataset_path, f)) + and os.path.splitext(f)[1].lower() in VIDEO_EXTENSIONS + ] + + if not video_files: + print(f"⚠️ No video files found in {dataset_dir}") + return [] + + print(f"Found {len(video_files)} video files to process") + print("=" * 70) + + # Process each video file + results = [] + for idx, filename in enumerate(video_files, 1): + file_path = os.path.join(dataset_path, filename) + print(f"\n[{idx}/{len(video_files)}] Processing: {filename}") + print("-" * 70) + + try: + # Call the detector's detect_and_extract method with optional kwargs + result = detector.detect_and_extract(str(file_path), **detector_kwargs) + + results.append( + { + "filename": filename, + "status": "success", + "frames_extracted": len(result.selected_frames), + "output_folder": result.output_folder, + } + ) + print(f"✓ Successfully extracted {len(result.selected_frames)} frames") + + except Exception as e: + results.append({"filename": filename, "status": "failed", "error": str(e)}) + print(f"✗ Failed: {str(e)}") + + # Print summary + print("\n" + "=" * 70) + print("PROCESSING SUMMARY") + print("=" * 70) + + successful = sum(1 for r in results if r["status"] == "success") + failed = sum(1 for r in results if r["status"] == "failed") + total_frames = sum( + r.get("frames_extracted", 0) for r in results if r["status"] == "success" + ) + + print(f"Total videos processed: {len(video_files)}") + print(f"Successful: {successful}") + print(f"Failed: {failed}") + print(f"Total frames extracted: {total_frames}") + + if successful > 0: + # Get output folder from first successful result + output_folder = next( + (r["output_folder"] for r in results if r["status"] == "success"), "N/A" + ) + print(f"\n✓ Frames stored in: {output_folder}/") + + # Print detailed results for failed videos + if failed > 0: + print("\n" + "=" * 70) + print("FAILED VIDEOS:") + print("=" * 70) + for r in results: + if r["status"] == "failed": + print(f" • {r['filename']}: {r['error']}") + + return results + + +# ============================================================================ +# COCO to Video JSON Converter +# ============================================================================ + + +def _extract_video_name_and_frame(filename: str) -> tuple[str, int]: + """ + Extract video name and frame number from keyframe filename. + + Format: {dataset_id}+{file_id}+{video_name}+frame_{frame_number}.jpg + Example: 15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p+frame_1064.jpg + Returns: ("butterflies_960p.mp4", 1064) + + Args: + filename: The keyframe filename + + Returns: + Tuple of (video_name, frame_number) + + Raises: + ValueError: If filename format is invalid + """ + # Split by '+' to get parts + parts = filename.split("+") + + if len(parts) < 4: + raise ValueError(f"Invalid filename format: {filename}") + + # Last part contains video_name+frame_X.jpg + last_part = parts[-1] + + # Extract frame number using regex + frame_match = re.search(r"frame_(\d+)\.jpg$", last_part) + if not frame_match: + raise ValueError(f"Could not extract frame number from: {filename}") + + frame_number = int(frame_match.group(1)) + + # Extract video name (everything before +frame_X.jpg) + video_name_part = parts[-2] # The part before the last '+' + video_name = f"{video_name_part}.mp4" + + return video_name, frame_number + + +def _convert_segmentation_to_polygon(segmentation: List[float]) -> List[Dict[str, int]]: + """ + Convert COCO segmentation format to video polygon format. + + COCO format: [x1, y1, x2, y2, x3, y3, ...] + Video format: [{"x": x1, "y": y1}, {"x": x2, "y": y2}, ...] + + Args: + segmentation: List of alternating x, y coordinates + + Returns: + List of coordinate dictionaries + """ + polygon = [] + for i in range(0, len(segmentation), 2): + polygon.append({"x": int(segmentation[i]), "y": int(segmentation[i + 1])}) + return polygon + + +def _convert_bbox_to_video_format(bbox: List[float]) -> Dict[str, Any]: + """ + Convert COCO bbox format to video bbox format. + + COCO format: [xmin, ymin, width, height] + Video format: {"xmin": x, "ymin": y, "xmax": x+w, "ymax": y+h, "rotation": 0} + + Args: + bbox: COCO bounding box [x, y, width, height] + + Returns: + Video format bounding box dictionary + """ + xmin, ymin, width, height = bbox + return { + "xmin": int(xmin), + "ymin": int(ymin), + "xmax": int(xmin + width), + "ymax": int(ymin + height), + "rotation": 0, + } + + +def coco_to_video_json( + coco_json_path: str, + output_path: Optional[str] = "Video_Keyframe_annot.json", + fps: int = 23, +) -> List[Dict[str, Any]]: + """ + Convert COCO JSON format (from keyframe exports) to Video JSON format. + + This function transforms annotations exported from keyframe image projects + into the format required for video project preannotation upload. + + Args: + coco_json_path: Path to the COCO JSON file + output_path: Path to save the converted JSON. Default: "Video_Keyframe_annot.json" + Set to None to skip saving + fps: Frames per second for the video (default: 23) + + Returns: + List of video annotation dictionaries + + Example: + >>> from labellerr.services.video_sampling import coco_to_video_json + >>> video_annotations = coco_to_video_json( + ... "export_zmYykSJhCAJqAaXaJQ3g.json", + ... "Video_Keyframe_annot.json" + ... ) + """ + # Load COCO JSON + with open(coco_json_path, "r", encoding="utf-8") as f: + coco_data = json.load(f) + + # Create mappings + images = {img["id"]: img for img in coco_data["images"]} + categories = {cat["id"]: cat for cat in coco_data["categories"]} + + # Group annotations by video file + video_annotations: Dict[str, Dict[str, Any]] = defaultdict( + lambda: { + "file_name": "", + "annotations": defaultdict( + lambda: {"question_type": "", "question_name": "", "answer": []} + ), + } + ) + + # Process each annotation + for annotation in coco_data["annotations"]: + image_id = annotation["image_id"] + category_id = annotation["category_id"] + + # Get image and category info + image = images[image_id] + category = categories[category_id] + + # Extract video name and frame number + try: + video_name, frame_number = _extract_video_name_and_frame(image["file_name"]) + except ValueError as e: + print(f"Warning: Skipping annotation - {e}") + continue + + # Determine question type based on annotation structure + if "segmentation" in annotation and annotation["segmentation"]: + question_type = "polygon" + # Convert segmentation to polygon format + answer_data = _convert_segmentation_to_polygon( + annotation["segmentation"][0] + ) + elif "bbox" in annotation: + question_type = "BoundingBox" + # Convert bbox to video format + answer_data = _convert_bbox_to_video_format(annotation["bbox"]) + else: + print( + f"Warning: Unknown annotation type for annotation {annotation.get('id')}" + ) + continue + + # Get or create video entry + video_key = video_name + if not video_annotations[video_key]["file_name"]: + video_annotations[video_key]["file_name"] = video_name + + # Get or create question entry + question_name = category["name"] + question_key = f"{category_id}_{question_type}" + + if not video_annotations[video_key]["annotations"][question_key][ + "question_type" + ]: + video_annotations[video_key]["annotations"][question_key][ + "question_type" + ] = question_type + video_annotations[video_key]["annotations"][question_key][ + "question_name" + ] = question_name + video_annotations[video_key]["annotations"][question_key]["answer"] = [] + + # # Find or create the answer group for this annotation + # # Each unique annotation should be in its own answer group + # answer_id = annotation.get("labellerr_answer_id", annotation.get("id")) + + # Check if we already have an answer group for this annotation + existing_answer = None + for ans_group in video_annotations[video_key]["annotations"][question_key][ + "answer" + ]: + # Check if this frame already exists in this answer group + if str(frame_number) in ans_group.get("frames", {}): + existing_answer = ans_group + break + + if existing_answer is None: + # Create new answer group + existing_answer = {"startFrame": frame_number, "frames": {}} + video_annotations[video_key]["annotations"][question_key]["answer"].append( + existing_answer + ) + else: + # Update startFrame if this frame is earlier + if frame_number < existing_answer["startFrame"]: + existing_answer["startFrame"] = frame_number + + # Add frame data + frame_data = { + "frame": frame_number, + "answer": answer_data, + "isManualAnnotation": True, + "fps": fps, + } + + existing_answer["frames"][str(frame_number)] = frame_data + + # Convert to list format + result = [] + for video_name, video_data in video_annotations.items(): + # Convert annotations dict to list + annotations_list = [] + for question_data in video_data["annotations"].values(): + # Ensure startFrame is set correctly for each answer group + for answer_group in question_data["answer"]: + frames = answer_group["frames"] + if frames: + # Set startFrame to the minimum frame number + min_frame = min(int(f) for f in frames.keys()) + answer_group["startFrame"] = min_frame + + annotations_list.append( + { + "question_type": question_data["question_type"], + "question_name": question_data["question_name"], + "answer": question_data["answer"], + } + ) + + result.append( + {"file_name": video_data["file_name"], "annotations": annotations_list} + ) + + # Save to file if output path is provided + if output_path: + with open(output_path, "w", encoding="utf-8") as f: + json.dump(result, f, indent=2, ensure_ascii=False) + print(f"Video JSON saved to: {output_path}") + + return result diff --git a/labellerr/services/video_sampling/ffmpeg_detect.py b/labellerr/services/video_sampling/ffmpeg_detect.py index 260096d..9685d1d 100644 --- a/labellerr/services/video_sampling/ffmpeg_detect.py +++ b/labellerr/services/video_sampling/ffmpeg_detect.py @@ -107,7 +107,7 @@ def _validate_video_file(self, video_path: str) -> None: def detect_and_extract(self, video_path: str) -> DetectionResult: """ Extract keyframes from video and save to detects folder structure. - Frames are saved with their actual frame numbers (e.g., 5.jpg for frame 5). + Frames are saved with pattern: video_name+frame_X.jpg (e.g., video_name+frame_5.jpg for frame 5). Args: video_path: Path to the video file @@ -170,10 +170,12 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: # ================================================================ # Second pass: Extract each I-frame and save with its actual frame number # Using actual frame numbers ensures frames are named correctly - # (e.g., frame 250 from video → 250.jpg) + # (e.g., frame 250 from video → video_name+frame_250.jpg) selected_frames = [] for idx, frame_num in enumerate(frame_numbers, 1): - frame_path = os.path.join(frames_folder, f"{frame_num}.jpg") + # Save frame with naming pattern: video_name+frame_X.jpg + frame_filename = f"{file_id}+frame_{frame_num}.jpg" + frame_path = os.path.join(frames_folder, frame_filename) try: self._extract_single_frame(video_path, frame_num, frame_path) selected_frames.append( diff --git a/labellerr/services/video_sampling/pyscene_detect.py b/labellerr/services/video_sampling/pyscene_detect.py index 956e419..f4e6c40 100644 --- a/labellerr/services/video_sampling/pyscene_detect.py +++ b/labellerr/services/video_sampling/pyscene_detect.py @@ -1,4 +1,3 @@ -import json import os from pathlib import Path from typing import List @@ -189,22 +188,13 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: # Validate input file before processing self._validate_video_file(video_path) - # Extract identifiers from the video path - # file_id: Video filename without extension (e.g., "video_123") - # dataset_id: Parent directory name (used for organizing outputs) - file_id = os.path.splitext(os.path.basename(video_path))[0] - dataset_id = os.path.basename(os.path.dirname(video_path)) + # Extract video filename without extension (e.g., "video_123") + video_name = os.path.splitext(os.path.basename(video_path))[0] - # Create hierarchical output folder structure: - # PyScene_detects/ - # └── / - # └── / - # ├── frames/ (extracted frame images) - # └── _mapping.json (metadata) - base_detect_folder = "PyScene_detects" - - output_folder = os.path.join(base_detect_folder, dataset_id, file_id) - frames_folder = os.path.join(output_folder, "frames") + # Create output folder structure: + # pyscene_detect/ (frames stored directly here) + output_folder = "pyscene_detect" + os.makedirs(output_folder, exist_ok=True) try: # ================================================================ @@ -213,9 +203,6 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: print("Detecting scene changes...") scenes = detect(video_path, AdaptiveDetector()) - # Create all necessary directories (no error if they already exist) - os.makedirs(frames_folder, exist_ok=True) - # ================================================================ # PHASE 2: Extract frames from detected scenes # ================================================================ @@ -243,9 +230,9 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: try: frame = self._get_frame(video, frame_no) - # Save frame with frame number as filename inside frames folder - frame_filename = f"{frame_no}.jpg" - frame_path = os.path.join(frames_folder, frame_filename) + # Save frame with naming pattern: video_name+frame_X.jpg + frame_filename = f"{video_name}+frame_{frame_no}.jpg" + frame_path = os.path.join(output_folder, frame_filename) frame.save(frame_path) # Create SceneFrame object @@ -272,8 +259,8 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: print("Extracting first frame (frame 0)...") frame = self._get_frame(video, 0) - frame_filename = "0.jpg" - frame_path = os.path.join(frames_folder, frame_filename) + frame_filename = f"{video_name}+frame_0.jpg" + frame_path = os.path.join(output_folder, frame_filename) frame.save(frame_path) # Insert at the beginning of the list @@ -292,20 +279,17 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: # Final success message with extraction statistics print( - f"Successfully extracted {len(scene_frames)} frames to {frames_folder}" + f"Successfully extracted {len(scene_frames)} frames to {output_folder}" ) # Create result result = DetectionResult( - file_id=file_id, + file_id=video_name, output_folder=output_folder, total_frames=total_frames, selected_frames=scene_frames, ) - # Save JSON mapping - self._save_json_mapping(result, output_folder, file_id) - return result except (VideoFileError, NoScenesError, FrameExtractionError): @@ -340,39 +324,3 @@ def _get_frame(self, video: cv2.VideoCapture, frame_no: int) -> Image.Image: return Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) except Exception as e: raise FrameExtractionError(f"Error extracting frame {frame_no}: {e}") from e - - def _save_json_mapping( - self, result: DetectionResult, output_folder: str, file_id: str - ) -> None: - """ - Save JSON mapping of file_id to extracted scenes. - - Args: - result: DetectionResult object - output_folder: Folder to save the JSON file - file_id: Unique identifier for the video - - Raises: - PySceneDetectError: If JSON file cannot be saved - """ - try: - # Use Pydantic's model_dump - result_dict = result.model_dump() - result_dict["total_selected_frames"] = len(result.selected_frames) - - json_path = os.path.join(output_folder, f"{file_id}_mapping.json") - with open(json_path, "w", encoding="utf-8") as f: - json.dump(result_dict, f, indent=2, ensure_ascii=False) - - print(f"JSON mapping saved to: {json_path}") - except (IOError, OSError) as e: - raise PySceneDetectError( - f"Failed to save JSON mapping to {json_path}: {e}" - ) from e - - -if __name__ == "__main__": - video_path = r"D:\Professional\Labellerr_SDK\SDKPython\labellerr\notebooks\Labellerr_datasets\354681d3-034a-4d66-b070-365f4bd11d8a\2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4" - - detector = PySceneDetect() - result = detector.detect_and_extract(video_path) diff --git a/labellerr/services/video_sampling/ssim_detect.py b/labellerr/services/video_sampling/ssim_detect.py index 716ec79..cf6b426 100644 --- a/labellerr/services/video_sampling/ssim_detect.py +++ b/labellerr/services/video_sampling/ssim_detect.py @@ -67,7 +67,7 @@ class SceneFrame(BaseModel): Attributes: frame_path (str): Absolute or relative path to the extracted frame image file. - Example: "SSIM_detects/video_id/frames/250.jpg" + Example: "SSIM_detects/video_id/frames/video_name+frame_250.jpg" frame_index (int): The 0-indexed frame number in the source video. Example: 250 means this is the 250th frame of the video. ssim_score (float): The SSIM score that triggered this frame extraction. @@ -236,7 +236,9 @@ def detect_and_extract( frame_count = 0 # Always save first frame with SSIM score of 1.0 (perfect match with itself) - self._save_frame(prev_frame, frame_count, 1.0, scene_frames, frames_folder) + self._save_frame( + prev_frame, frame_count, 1.0, scene_frames, frames_folder, file_id + ) print("Saved first frame (frame 0)") # ================================================================ @@ -263,6 +265,7 @@ def detect_and_extract( ssim_score, scene_frames, frames_folder, + file_id, ) print( f"Saved keyframe {len(scene_frames) - 1} at frame {frame_count} (SSIM: {ssim_score:.3f})" @@ -351,6 +354,7 @@ def _save_frame( ssim_score: float, scene_frames: List[SceneFrame], frames_folder: str, + file_id: str, ) -> None: """ Save a frame to disk and add to scene_frames list. @@ -361,6 +365,7 @@ def _save_frame( ssim_score: SSIM score that triggered this frame extraction scene_frames: List to append SceneFrame object to frames_folder: Folder to save the frame + file_id: Video filename without extension (for naming pattern) Raises: FrameExtractionError: If frame saving fails @@ -370,8 +375,8 @@ def _save_frame( frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) pil_image = Image.fromarray(frame_rgb) - # Save frame with frame number as filename - frame_filename = f"{frame_no}.jpg" + # Save frame with naming pattern: video_name+frame_X.jpg + frame_filename = f"{file_id}+frame_{frame_no}.jpg" frame_path = os.path.join(frames_folder, frame_filename) pil_image.save(frame_path) From 4e92d061f3251a7586a1fa38a7880d0c07509bce Mon Sep 17 00:00:00 2001 From: yashsuman Date: Tue, 9 Dec 2025 15:54:23 +0530 Subject: [PATCH 10/14] fixing git ci/cd failure --- labellerr/core/projects/__init__.py | 13 ++++++------- labellerr/core/schemas/__init__.py | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/labellerr/core/projects/__init__.py b/labellerr/core/projects/__init__.py index a5eb0c6..e17e8ac 100644 --- a/labellerr/core/projects/__init__.py +++ b/labellerr/core/projects/__init__.py @@ -1,23 +1,22 @@ import json import uuid +from concurrent.futures import ThreadPoolExecutor +from typing import List import requests -import requests + from labellerr import LabellerrClient from .. import client_utils, constants, schemas +from ..annotation_templates import LabellerrAnnotationTemplate from ..datasets import LabellerrDataset from ..exceptions import LabellerrError from .audio_project import AudioProject as LabellerrAudioProject +from .base import LabellerrProject from .document_project import DocucmentProject as LabellerrDocumentProject from .image_project import ImageProject as LabellerrImageProject -from .video_project import VideoProject as LabellerrVideoProject from .text_project import TextProject as LabellerrTextProject -from .base import LabellerrProject -from ..annotation_templates import LabellerrAnnotationTemplate -from typing import List -from concurrent.futures import ThreadPoolExecutor -from concurrent.futures import ThreadPoolExecutor +from .video_project import VideoProject as LabellerrVideoProject __all__ = [ "LabellerrProject", diff --git a/labellerr/core/schemas/__init__.py b/labellerr/core/schemas/__init__.py index ab09f11..a2597d3 100644 --- a/labellerr/core/schemas/__init__.py +++ b/labellerr/core/schemas/__init__.py @@ -64,7 +64,6 @@ from labellerr.core.schemas.projects import ( CreateLocalExportParams, CreateProjectParams, - CreateTemplateParams, Question, RotationConfig, ) From fb49c3772766b17136af59ae0cd227e380decce0 Mon Sep 17 00:00:00 2001 From: yashsuman Date: Mon, 22 Dec 2025 14:29:46 +0530 Subject: [PATCH 11/14] feat: implement dataset utilities for local file handling and parallel upload --- labellerr/core/datasets/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/labellerr/core/datasets/utils.py b/labellerr/core/datasets/utils.py index cf1737d..5f42e8f 100644 --- a/labellerr/core/datasets/utils.py +++ b/labellerr/core/datasets/utils.py @@ -5,8 +5,8 @@ from typing import List, Union from .. import client_utils, constants, gcs -from ..exceptions import LabellerrError from ..client import LabellerrClient +from ..exceptions import LabellerrError def get_total_folder_file_count_and_total_size(folder_path, data_type): @@ -254,7 +254,7 @@ def create_batches(): max_workers = min( os.cpu_count() or 1, # Number of CPU cores (default to 1 if None) len(batches), # Number of batches - 20, + 5, ) connection_id = str(uuid.uuid4()) # Process batches in parallel From ad327a7430318ad6e1d8c4908e0ab5f687e9c82c Mon Sep 17 00:00:00 2001 From: yashsuman Date: Mon, 29 Dec 2025 02:09:01 +0530 Subject: [PATCH 12/14] Fixed the FPS releated sync-up --- SDK_test.ipynb | 122 +++++--- labellerr/core/datasets/utils.py | 2 +- labellerr/core/datasets/video_dataset.py | 4 +- labellerr/core/files/video_file.py | 25 +- labellerr/core/projects/__init__.py | 2 +- labellerr/notebooks/SDK.ipynb | 286 +++++++++++------- labellerr/notebooks/test_coco_to_video.py | 30 ++ labellerr/notebooks/test_preannotation_api.py | 4 +- labellerr/services/video_sampling/__init__.py | 80 +++-- 9 files changed, 359 insertions(+), 196 deletions(-) create mode 100644 labellerr/notebooks/test_coco_to_video.py diff --git a/SDK_test.ipynb b/SDK_test.ipynb index 8ef6d1b..46d9d5f 100644 --- a/SDK_test.ipynb +++ b/SDK_test.ipynb @@ -2,17 +2,28 @@ "cells": [ { "cell_type": "code", - "execution_count": 6, + "execution_count": 1, "id": "171b00fb", "metadata": {}, "outputs": [], "source": [ "from labellerr.client import LabellerrClient\n", + "from dotenv import load_dotenv\n", + "import os\n", + "\n", + "# For Labellerr Dataset \n", "from labellerr.core.datasets import create_dataset_from_local, LabellerrDataset\n", + "from labellerr.core.schemas import DatasetConfig\n", + "\n", + "# For Annotation Template\n", "from labellerr.core.annotation_templates import create_template\n", - "from labellerr.core.projects import create_project\n", - "# from labellerr.core.schemas import * remove this code\n", - "from labellerr.core.schemas.annotation_templates import AnnotationQuestion, QuestionType, CreateTemplateParams\n", + "from labellerr.core.schemas.annotation_templates import AnnotationQuestion, QuestionType, CreateTemplateParams, DatasetDataType\n", + "# from template_helper import create_questions_from_prompts\n", + "import uuid\n", + "\n", + "# For labellerr Project\n", + "from labellerr.core.projects import create_project, LabellerrProject\n", + "from labellerr.core.schemas.projects import CreateProjectParams, RotationConfig\n", "\n", "import uuid\n" ] @@ -26,11 +37,11 @@ "source": [ "from dotenv import dotenv_values\n", "\n", - "config = dotenv_values(\".env\")\n", + "config = dotenv_values(\"../dev.env\")\n", "\n", - "API_KEY = config[\"API-KEY\"]\n", - "API_SECRET = config[\"API-SECRET\"]\n", - "CLIENT_ID = config[\"CLIENT_ID\"]" + "API_KEY = config[\"QA_API_KEY\"]\n", + "API_SECRET = config[\"QA_API_SECRET\"]\n", + "CLIENT_ID = config[\"QA_CLIENT_ID\"]" ] }, { @@ -41,7 +52,9 @@ "outputs": [], "source": [ "# Initialize client\n", - "client = LabellerrClient(api_key=API_KEY, api_secret=API_SECRET, client_id=CLIENT_ID)\n" + "CLIENT = LabellerrClient(api_key=API_KEY, \n", + " api_secret=API_SECRET, \n", + " client_id=CLIENT_ID)" ] }, { @@ -51,56 +64,97 @@ "metadata": {}, "outputs": [], "source": [ - "img_dataset_path = r\"D:\\Professional\\GitHub\\LABIMP-8041\\sample_img_dataset\"" + "img_dataset_path = r\"D:\\Professional\\Labellerr_SDK\\sample_img\"" ] }, { "cell_type": "code", - "execution_count": 15, - "id": "cd8b2606", + "execution_count": 12, + "id": "f5e214a7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Template created successfully with ID: 3979b246-b366-46e6-9120-13dee6542600\n" + ] + } + ], "source": [ - "# 1. Create dataset\n", + "template = create_template(\n", + " client=CLIENT,\n", + " params=CreateTemplateParams(\n", + " template_name=\"Test Template\",\n", + " data_type=DatasetDataType.image,\n", + " questions=[AnnotationQuestion(\n", + " question_number=1,\n", + " question=\"test question\",\n", + " question_id=str(uuid.uuid4()),\n", + " question_type=QuestionType.polygon,\n", + " required=True,\n", + " color=\"red\"\n", + " )]\n", + " )\n", + ")\n", "\n", + "if template.annotation_template_id is not None:\n", + " print(f\"Template created successfully with ID: {template.annotation_template_id}\")\n", + "else:\n", + " print(\"Failed to create template\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd8b2606", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for dataset to be processed\n", + "Dataset processed\n", + "Dataset ID: 64d10546-a5d7-41ee-9a16-299eb09813de\n" + ] + } + ], + "source": [ "dataset = create_dataset_from_local(\n", - " client=client,\n", - " dataset_config=DatasetConfig(dataset_name=\"My Dataset\", data_type=\"image\"),\n", - " folder_to_upload=img_dataset_path\n", - ")\n" + " client=CLIENT,\n", + " dataset_config=DatasetConfig(dataset_name=\"test_dataset\", \n", + " data_type=\"image\"),\n", + " folder_to_upload=img_dataset_path,\n", + " )\n", + "\n", + "print(\"Waiting for dataset to be processed\")\n", + "dataset.status()\n", + "print(\"Dataset processed\")\n", + "\n", + "print(\"Dataset ID: \", dataset.dataset_id)\n", + "print(\"Files count: \", dataset.files_count)" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 6, "id": "e25cd3c6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'1c8b2a05-0321-44fd-91e3-2ea911382cf9'" + "112" ] }, - "execution_count": 16, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "dataset.dataset_id" - ] - }, - { - "cell_type": "raw", - "id": "f4f315e0", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "'1c8b2a05-0321-44fd-91e3-2ea911382cf9'" + "dataset.files_count" ] }, { diff --git a/labellerr/core/datasets/utils.py b/labellerr/core/datasets/utils.py index 5f42e8f..791a138 100644 --- a/labellerr/core/datasets/utils.py +++ b/labellerr/core/datasets/utils.py @@ -254,7 +254,7 @@ def create_batches(): max_workers = min( os.cpu_count() or 1, # Number of CPU cores (default to 1 if None) len(batches), # Number of batches - 5, + 20, ) connection_id = str(uuid.uuid4()) # Process batches in parallel diff --git a/labellerr/core/datasets/video_dataset.py b/labellerr/core/datasets/video_dataset.py index 6e8538e..a7ccf11 100644 --- a/labellerr/core/datasets/video_dataset.py +++ b/labellerr/core/datasets/video_dataset.py @@ -21,8 +21,8 @@ def download(self): print(f"# Starting batch video processing for dataset: {self.dataset_id}") print(f"{'#'*70}\n") - # Fetch all video files - video_files = self.fetch_files() + # Fetch all video files (convert generator to list) + video_files = list(self.fetch_files()) if not video_files: print("No video files found in dataset") diff --git a/labellerr/core/files/video_file.py b/labellerr/core/files/video_file.py index 43fab0f..5f2000e 100644 --- a/labellerr/core/files/video_file.py +++ b/labellerr/core/files/video_file.py @@ -35,6 +35,11 @@ def total_frames(self): """Get total number of frames in the video.""" return self.metadata.get("total_frames", 0) + @property + def fps(self): + """Get frames per second of the video.""" + return self.metadata.get("fps", 25) + def get_frames(self, frame_start: int = 0, frame_end: int | None = None): """ Retrieve video frames data from Labellerr API. @@ -216,12 +221,10 @@ def create_video( input_pattern = os.path.join(frames_folder, pattern) if output_file is None: # Use [Dataset_id]+[File_id]+[File_name] as default output filename - if self.dataset_id and self.file_name: - output_file = f"{self.dataset_id}+{self.file_id}+{self.file_name}" - elif self.dataset_id: - output_file = f"{self.dataset_id}+{self.file_id}.mp4" + if self.dataset_id and self.file_name and self.metadata.get("fps"): + output_file = f"{self.dataset_id}+{self.file_id}+{self.file_name}+FPS{self.metadata.get('fps')}.mp4" else: - output_file = f"{self.file_id}.mp4" + raise ValueError("output_file must be provided") # FFmpeg command command = [ @@ -306,15 +309,15 @@ def download_create_video_auto_cleanup( f"\nWarning: {download_result['failed_downloads']} frames failed to download" ) - # Step 4: Create video from downloaded frames using [Dataset_id]+[File_id]+[File_name] naming + # Step 4: Create video from downloaded frames using [Dataset_id]+[File_id]+[File_name]+FPS[fps] naming # Save video directly in output_folder (labellerr_datasets) print("\n[4/4] Creating video from frames...") - if self.dataset_id and self.file_name: - video_filename = f"{self.dataset_id}+{self.file_id}+{self.file_name}" - elif self.dataset_id: - video_filename = f"{self.dataset_id}+{self.file_id}.mp4" + if self.dataset_id and self.file_name and self.fps: + # Remove extension from file_name if present, then add FPS and .mp4 + base_name = os.path.splitext(self.file_name)[0] + video_filename = f"{self.dataset_id}+{self.file_id}+{base_name}+FPS{self.fps}.mp4" else: - video_filename = f"{self.file_id}.mp4" + raise ValueError("dataset_id, file_name, and fps metadata are required") video_output_path = os.path.join(output_folder, video_filename) self.create_video( diff --git a/labellerr/core/projects/__init__.py b/labellerr/core/projects/__init__.py index 730a5b1..6e445a7 100644 --- a/labellerr/core/projects/__init__.py +++ b/labellerr/core/projects/__init__.py @@ -15,7 +15,7 @@ from .document_project import DocucmentProject as LabellerrDocumentProject from .image_project import ImageProject as LabellerrImageProject from .text_project import TextProject as LabellerrTextProject -from .base import LabellerrProject +from .video_project import VideoProject as LabellerrVideoProject from ..annotation_templates import LabellerrAnnotationTemplate from typing import List from concurrent.futures import ThreadPoolExecutor diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index 6c5a9b2..1025b45 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -12,7 +12,17 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": null, + "id": "be12bf3f", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install kagglehub ipywidgets" + ] + }, + { + "cell_type": "code", + "execution_count": 1, "id": "edcdab6a", "metadata": {}, "outputs": [], @@ -102,16 +112,6 @@ "4. This will download a kaggle.json file with your credentials" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "be12bf3f", - "metadata": {}, - "outputs": [], - "source": [ - "# !pip install kagglehub ipywidgets" - ] - }, { "cell_type": "code", "execution_count": null, @@ -155,7 +155,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 9, "id": "52e00dbc", "metadata": {}, "outputs": [ @@ -165,7 +165,7 @@ "True" ] }, - "execution_count": 13, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -187,7 +187,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 10, "id": "c1e2e2f3", "metadata": {}, "outputs": [ @@ -196,28 +196,28 @@ "text/plain": [ "{'es_multimodal_index': False,\n", " 'metadata': {},\n", - " 'dataset_id': '15908795-09eb-4cdb-a39b-8689f8f936e5',\n", + " 'dataset_id': 'a0d93479-4667-4574-b781-a530a6a243b9',\n", " 'origin': 'https://pro.labellerr.com',\n", - " 'created_at': 1765198673509,\n", + " 'created_at': 1766944223994,\n", " 'description': '',\n", - " 'created_by': '1c8800.8177f647b0b9bc6321bcec4d93',\n", + " 'created_by': '21dbbb.d54ba24efab37ba6b6c6f58916',\n", " 'client_id': '79',\n", " 'tags': [],\n", " 'data_type': 'video',\n", - " 'name': 'VIDEO_DATASET_(2)',\n", + " 'name': 'VIDEO_DATASET | size-2',\n", " 'extraction_quality': ['normal'],\n", - " 'updated_at': 1765198765922,\n", + " 'updated_at': 1766944307325,\n", " 'progress': 'Processing 0/2 files',\n", " 'files_count': 2,\n", " 'status_code': 300,\n", - " 'video_processing_job_id': 'e08357f1-edaa-4b42-a9fb-de3bd9230b4a',\n", - " 'es_index_status': 101,\n", - " 'video_processing_job': {'job_id': 'e08357f1-edaa-4b42-a9fb-de3bd9230b4a',\n", + " 'video_processing_job_id': '5a4ef8c6-fb46-450a-b9ff-73bfe2f162a0',\n", + " 'es_index_status': 501,\n", + " 'video_processing_job': {'job_id': '5a4ef8c6-fb46-450a-b9ff-73bfe2f162a0',\n", " 'status_code': 200,\n", - " 'updated_at': 1765198785019}}" + " 'updated_at': 1766944322254}}" ] }, - "execution_count": 14, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -231,7 +231,7 @@ "\n", "dataset = create_dataset_from_local(\n", " client=client,\n", - " dataset_config=DatasetConfig(dataset_name=\"VIDEO_DATASET_(2)\", \n", + " dataset_config=DatasetConfig(dataset_name=\"VIDEO_DATASET | size-2\", \n", " data_type=\"video\"),\n", " folder_to_upload=KAGGLE_DATASET_PATH,\n", " )\n", @@ -241,7 +241,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 11, "id": "3d82b343", "metadata": {}, "outputs": [ @@ -251,7 +251,7 @@ "2" ] }, - "execution_count": 15, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -263,13 +263,55 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "5205f618", "metadata": {}, "outputs": [], "source": [ "dataset = LabellerrDataset(client=client,\n", - " dataset_id='15908795-09eb-4cdb-a39b-8689f8f936e5')" + " dataset_id='a0d93479-4667-4574-b781-a530a6a243b9')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d21ec493", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'es_multimodal_index': False,\n", + " 'metadata': {},\n", + " 'dataset_id': 'a0d93479-4667-4574-b781-a530a6a243b9',\n", + " 'origin': 'https://pro.labellerr.com',\n", + " 'created_at': 1766944223994,\n", + " 'description': '',\n", + " 'created_by': '21dbbb.d54ba24efab37ba6b6c6f58916',\n", + " 'client_id': '79',\n", + " 'tags': [],\n", + " 'data_type': 'video',\n", + " 'name': 'VIDEO_DATASET | size-2',\n", + " 'extraction_quality': ['normal'],\n", + " 'progress': 'Processing 0/2 files',\n", + " 'files_count': 2,\n", + " 'status_code': 300,\n", + " 'video_processing_job_id': '5a4ef8c6-fb46-450a-b9ff-73bfe2f162a0',\n", + " 'updated_at': 1766944676206,\n", + " 'status': 'none',\n", + " 'es_index_status': 300,\n", + " 'video_processing_job': {'job_id': '5a4ef8c6-fb46-450a-b9ff-73bfe2f162a0',\n", + " 'status_code': 300,\n", + " 'updated_at': 1766944676068}}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset.status()" ] }, { @@ -282,7 +324,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 6, "id": "7be23d9a", "metadata": {}, "outputs": [], @@ -308,17 +350,17 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 7, "id": "7e281c33", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'4dd84aa0-1a06-4cea-a758-40076c7e3d8c'" + "'50189e79-ea31-42c1-86e7-5139e665b22e'" ] }, - "execution_count": 18, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -334,9 +376,9 @@ "metadata": {}, "outputs": [], "source": [ - "from labellerr.core.annotation_templates import LabellerrAnnotationTemplate\n", - "template = LabellerrAnnotationTemplate(client=client,\n", - " annotation_template_id='4dd84aa0-1a06-4cea-a758-40076c7e3d8c')" + "# from labellerr.core.annotation_templates import LabellerrAnnotationTemplate\n", + "# template = LabellerrAnnotationTemplate(client=client,\n", + "# annotation_template_id='4dd84aa0-1a06-4cea-a758-40076c7e3d8c')" ] }, { @@ -349,7 +391,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 14, "id": "8996dd01", "metadata": {}, "outputs": [], @@ -372,17 +414,17 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 15, "id": "724c67cc", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'caryl_geographical_turkey_21445'" + "'ellie_zesty_vole_86978'" ] }, - "execution_count": 22, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -393,13 +435,13 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 5, "id": "f74cba85", "metadata": {}, "outputs": [], "source": [ "video_project = LabellerrProject(client=client,\n", - " project_id='caryl_geographical_turkey_21445')" + " project_id='ellie_zesty_vole_86978')" ] }, { @@ -423,11 +465,9 @@ "text": [ "\n", "######################################################################\n", - "# Starting batch video processing for dataset: 15908795-09eb-4cdb-a39b-8689f8f936e5\n", + "# Starting batch video processing for dataset: a0d93479-4667-4574-b781-a530a6a243b9\n", "######################################################################\n", "\n", - "Fetching files for dataset: 15908795-09eb-4cdb-a39b-8689f8f936e5\n", - "{'message': '200: Success', 'response': {'files': [{'has_embedding': False, 'file_id': '471163aa-19dc-4bc7-9aee-04780591281a', 'created_at': 1765198766042, 'file_name_original': 'butterflies_960p.mp4', 'dataset_id': '15908795-09eb-4cdb-a39b-8689f8f936e5', 'connection_id': 'fa6598b0-7a6c-4327-9d00-eb8ae6fd445f', 'email_id': '1c8800.8177f647b0b9bc6321bcec4d93', 'file_name': 'butterflies_960p.mp4', 'file_reference': 'gs://labellerr-connector-files-dev/local_upload/fa6598b0-7a6c-4327-9d00-eb8ae6fd445f/butterflies_960p.mp4', 'file_metadata': {'file_size': 25.047, 'image_width': None, 'file_format': 'mp4', 'additional_metadata': None, 'image_height': None}, 'created_by': '1c8800.8177f647b0b9bc6321bcec4d93', 'data_type': 'video', 'video_playlist': 'https://api-gateway-722091373895.us-central1.run.app/data/15908795-09eb-4cdb-a39b-8689f8f936e5/files/471163aa-19dc-4bc7-9aee-04780591281a/segments/master.m3u8'}, {'has_embedding': False, 'file_id': 'a878e61b-8aeb-46e1-ab10-5f1852bcdcbe', 'created_at': 1765198766042, 'file_name_original': 'seafood_1280p.mp4', 'dataset_id': '15908795-09eb-4cdb-a39b-8689f8f936e5', 'connection_id': 'fa6598b0-7a6c-4327-9d00-eb8ae6fd445f', 'email_id': '1c8800.8177f647b0b9bc6321bcec4d93', 'file_name': 'seafood_1280p.mp4', 'file_reference': 'gs://labellerr-connector-files-dev/local_upload/fa6598b0-7a6c-4327-9d00-eb8ae6fd445f/seafood_1280p.mp4', 'file_metadata': {'file_size': 17.156, 'image_width': None, 'file_format': 'mp4', 'additional_metadata': None, 'image_height': None}, 'created_by': '1c8800.8177f647b0b9bc6321bcec4d93', 'data_type': 'video', 'video_playlist': 'https://api-gateway-722091373895.us-central1.run.app/data/15908795-09eb-4cdb-a39b-8689f8f936e5/files/a878e61b-8aeb-46e1-ab10-5f1852bcdcbe/segments/master.m3u8'}], 'total_count': 2, 'next_search_after': None}, 'error': None, 'tracking_id': 'f2364207b402f43da228219f2209c267'}\n", "\n", "Processing 2 video files...\n", "\n", @@ -435,54 +475,54 @@ "Starting download of 2 files...\n", "\n", "============================================================\n", - "Processing file: 471163aa-19dc-4bc7-9aee-04780591281a\n", + "Processing file: b50c92ff-4bfb-40f7-9752-0871428d65ce\n", "============================================================\n", "\n", - "[1/4] Fetching frame data from API (0 to 1572)...\n", - "Retrieved 1572 frames\n", + "[1/4] Fetching frame data from API (0 to 389)...\n", + "Retrieved 389 frames\n", "\n", "[2/4] Setting up output folders...\n", "\n", "[3/4] Downloading frames...\n", - "Starting download of 1572 frames...\n", - "Frames downloaded: 1572/1572 (1572 successful, 0 failed)\n", + "Starting download of 389 frames...\n", + "Frames downloaded: 389/389 (389 successful, 0 failed)\n", "\n", "[4/4] Creating video from frames...\n", - "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p.mp4\n", - "Video saved as ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p.mp4\n", + "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datasets\\a0d93479-4667-4574-b781-a530a6a243b9+b50c92ff-4bfb-40f7-9752-0871428d65ce+seafood_1280p\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datasets\\a0d93479-4667-4574-b781-a530a6a243b9+b50c92ff-4bfb-40f7-9752-0871428d65ce+seafood_1280p+FPS29.mp4\n", + "Video saved as ./Labellerr_datasets\\a0d93479-4667-4574-b781-a530a6a243b9+b50c92ff-4bfb-40f7-9752-0871428d65ce+seafood_1280p+FPS29.mp4\n", "\n", "Cleaning up temporary frames...\n", - "Removed temporary frames folder: ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p\n", + "Removed temporary frames folder: ./Labellerr_datasets\\a0d93479-4667-4574-b781-a530a6a243b9+b50c92ff-4bfb-40f7-9752-0871428d65ce+seafood_1280p\n", "\n", "============================================================\n", "Processing complete!\n", - "Video saved to: ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p.mp4\n", + "Video saved to: ./Labellerr_datasets\\a0d93479-4667-4574-b781-a530a6a243b9+b50c92ff-4bfb-40f7-9752-0871428d65ce+seafood_1280p+FPS29.mp4\n", "============================================================\n", "\n", "Files processed: 1/2 (1 successful, 0 failed)\n", "============================================================\n", - "Processing file: a878e61b-8aeb-46e1-ab10-5f1852bcdcbe\n", + "Processing file: 9bf1ee94-ab41-435a-8935-38f6213b05f9\n", "============================================================\n", "\n", - "[1/4] Fetching frame data from API (0 to 389)...\n", - "Retrieved 389 frames\n", + "[1/4] Fetching frame data from API (0 to 1572)...\n", + "Retrieved 1572 frames\n", "\n", "[2/4] Setting up output folders...\n", "\n", "[3/4] Downloading frames...\n", - "Starting download of 389 frames...\n", - "Frames downloaded: 389/389 (389 successful, 0 failed)\n", + "Starting download of 1572 frames...\n", + "Frames downloaded: 1572/1572 (1572 successful, 0 failed)\n", "\n", "[4/4] Creating video from frames...\n", - "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p.mp4\n", - "Video saved as ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p.mp4\n", + "Running command: ffmpeg -y -start_number 0 -framerate 30 -i ./Labellerr_datasets\\a0d93479-4667-4574-b781-a530a6a243b9+9bf1ee94-ab41-435a-8935-38f6213b05f9+butterflies_960p\\%d.jpg -c:v libx264 -pix_fmt yuv420p ./Labellerr_datasets\\a0d93479-4667-4574-b781-a530a6a243b9+9bf1ee94-ab41-435a-8935-38f6213b05f9+butterflies_960p+FPS29.mp4\n", + "Video saved as ./Labellerr_datasets\\a0d93479-4667-4574-b781-a530a6a243b9+9bf1ee94-ab41-435a-8935-38f6213b05f9+butterflies_960p+FPS29.mp4\n", "\n", "Cleaning up temporary frames...\n", - "Removed temporary frames folder: ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p\n", + "Removed temporary frames folder: ./Labellerr_datasets\\a0d93479-4667-4574-b781-a530a6a243b9+9bf1ee94-ab41-435a-8935-38f6213b05f9+butterflies_960p\n", "\n", "============================================================\n", "Processing complete!\n", - "Video saved to: ./Labellerr_datasets\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p.mp4\n", + "Video saved to: ./Labellerr_datasets\\a0d93479-4667-4574-b781-a530a6a243b9+9bf1ee94-ab41-435a-8935-38f6213b05f9+butterflies_960p+FPS29.mp4\n", "============================================================\n", "\n", "Files processed: 2/2 (2 successful, 0 failed)\n", @@ -499,19 +539,19 @@ "data": { "text/plain": [ "[{'status': 'success',\n", - " 'file_id': '471163aa-19dc-4bc7-9aee-04780591281a',\n", - " 'dataset_id': '15908795-09eb-4cdb-a39b-8689f8f936e5',\n", - " 'video_path': './Labellerr_datasets\\\\15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p.mp4',\n", + " 'file_id': 'b50c92ff-4bfb-40f7-9752-0871428d65ce',\n", + " 'dataset_id': 'a0d93479-4667-4574-b781-a530a6a243b9',\n", + " 'video_path': './Labellerr_datasets\\\\a0d93479-4667-4574-b781-a530a6a243b9+b50c92ff-4bfb-40f7-9752-0871428d65ce+seafood_1280p+FPS29.mp4',\n", " 'output_folder': './Labellerr_datasets',\n", - " 'frames_downloaded': 1572,\n", + " 'frames_downloaded': 389,\n", " 'frames_failed': 0,\n", " 'failed_frames_info': []},\n", " {'status': 'success',\n", - " 'file_id': 'a878e61b-8aeb-46e1-ab10-5f1852bcdcbe',\n", - " 'dataset_id': '15908795-09eb-4cdb-a39b-8689f8f936e5',\n", - " 'video_path': './Labellerr_datasets\\\\15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p.mp4',\n", + " 'file_id': '9bf1ee94-ab41-435a-8935-38f6213b05f9',\n", + " 'dataset_id': 'a0d93479-4667-4574-b781-a530a6a243b9',\n", + " 'video_path': './Labellerr_datasets\\\\a0d93479-4667-4574-b781-a530a6a243b9+9bf1ee94-ab41-435a-8935-38f6213b05f9+butterflies_960p+FPS29.mp4',\n", " 'output_folder': './Labellerr_datasets',\n", - " 'frames_downloaded': 389,\n", + " 'frames_downloaded': 1572,\n", " 'frames_failed': 0,\n", " 'failed_frames_info': []}]" ] @@ -565,7 +605,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 7, "id": "f5c41073", "metadata": {}, "outputs": [ @@ -598,7 +638,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "49a6f89d", "metadata": {}, "outputs": [ @@ -621,7 +661,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 9, "id": "dd96be8c", "metadata": {}, "outputs": [], @@ -631,10 +671,17 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 10, "id": "a3052f25", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:pyscenedetect:Detecting scenes...\n" + ] + }, { "name": "stdout", "output_type": "stream", @@ -642,16 +689,29 @@ "Found 2 video files to process\n", "======================================================================\n", "\n", - "[1/2] Processing: 15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p.mp4\n", + "[1/2] Processing: a0d93479-4667-4574-b781-a530a6a243b9+9bf1ee94-ab41-435a-8935-38f6213b05f9+butterflies_960p+FPS29.mp4\n", "----------------------------------------------------------------------\n", "Detecting scene changes...\n", "Detected 4 scene changes\n", - "Total frames in video: 1572\n", + "Total frames in video: 1572\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:pyscenedetect:Detecting scenes...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "Extracting first frame (frame 0)...\n", "Successfully extracted 5 frames to pyscene_detect\n", "✓ Successfully extracted 5 frames\n", "\n", - "[2/2] Processing: 15908795-09eb-4cdb-a39b-8689f8f936e5+a878e61b-8aeb-46e1-ab10-5f1852bcdcbe+seafood_1280p.mp4\n", + "[2/2] Processing: a0d93479-4667-4574-b781-a530a6a243b9+b50c92ff-4bfb-40f7-9752-0871428d65ce+seafood_1280p+FPS29.mp4\n", "----------------------------------------------------------------------\n", "Detecting scene changes...\n", "Detected 0 scene changes\n", @@ -678,7 +738,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 11, "id": "c83cf401", "metadata": {}, "outputs": [ @@ -688,7 +748,7 @@ "'.\\\\pyscene_detect'" ] }, - "execution_count": 23, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -719,7 +779,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 12, "id": "82629d12", "metadata": {}, "outputs": [], @@ -734,17 +794,17 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 13, "id": "d530faed", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'1addcef0-2c60-4442-8701-856efa573afc'" + "'3c5f6f14-66b7-4fca-b2ad-3a6b9bf0901e'" ] }, - "execution_count": 27, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -755,13 +815,13 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 14, "id": "e0793ba6", "metadata": {}, "outputs": [], "source": [ "dataset = LabellerrDataset(client=client,\n", - " dataset_id='1addcef0-2c60-4442-8701-856efa573afc')" + " dataset_id='3c5f6f14-66b7-4fca-b2ad-3a6b9bf0901e')" ] }, { @@ -774,7 +834,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 15, "id": "5d5e23c5", "metadata": {}, "outputs": [], @@ -800,17 +860,17 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 16, "id": "a05fac70", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'1502051f-dbe7-4216-9426-df5757afea85'" + "'42951455-12a1-44b6-8be9-6a287e676b74'" ] }, - "execution_count": 5, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -829,7 +889,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 17, "id": "b235e08d", "metadata": {}, "outputs": [], @@ -852,17 +912,17 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 18, "id": "fc8b7e25", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'laurette_constitutional_herring_39772'" + "'melanie_external_perch_91510'" ] }, - "execution_count": 15, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -873,13 +933,13 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 28, "id": "55c824a7", "metadata": {}, "outputs": [], "source": [ "img_project = LabellerrProject(client=client,\n", - " project_id='laurette_constitutional_herring_39772')" + " project_id='melanie_external_perch_91510')" ] }, { @@ -914,7 +974,20 @@ "execution_count": null, "id": "ccf0e882", "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'dict' object has no attribute 'model_dump'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[29]\u001b[39m\u001b[32m, line 8\u001b[39m\n\u001b[32m 1\u001b[39m export_config = {\n\u001b[32m 2\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mexport_name\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mTest Export\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 3\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mexport_description\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mExport for testing\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 4\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mexport_format\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mcoco_json\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 5\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mstatuses\u001b[39m\u001b[33m\"\u001b[39m: [\u001b[33m'\u001b[39m\u001b[33mreview\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33mr_assigned\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33mclient_review\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33mcr_assigned\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33maccepted\u001b[39m\u001b[33m'\u001b[39m]\n\u001b[32m 6\u001b[39m }\n\u001b[32m----> \u001b[39m\u001b[32m8\u001b[39m result = \u001b[43mimg_project\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate_local_export\u001b[49m\u001b[43m(\u001b[49m\u001b[43mexport_config\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32mD:\\Professional\\Labellerr_SDK\\SDKPython\\labellerr\\core\\projects\\base.py:481\u001b[39m, in \u001b[36mLabellerrProject.create_local_export\u001b[39m\u001b[34m(self, export_config)\u001b[39m\n\u001b[32m 471\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 472\u001b[39m \u001b[33;03mCreates a local export with the given configuration.\u001b[39;00m\n\u001b[32m 473\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 476\u001b[39m \u001b[33;03m:raises LabellerrError: If the export creation fails\u001b[39;00m\n\u001b[32m 477\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 479\u001b[39m unique_id = client_utils.generate_request_id()\n\u001b[32m--> \u001b[39m\u001b[32m481\u001b[39m export_config_dict = \u001b[43mexport_config\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmodel_dump\u001b[49m()\n\u001b[32m 482\u001b[39m export_config_dict.update(\n\u001b[32m 483\u001b[39m {\u001b[33m\"\u001b[39m\u001b[33mexport_destination\u001b[39m\u001b[33m\"\u001b[39m: schemas.ExportDestination.LOCAL.value}\n\u001b[32m 484\u001b[39m )\n\u001b[32m 486\u001b[39m payload = json.dumps(export_config_dict)\n", + "\u001b[31mAttributeError\u001b[39m: 'dict' object has no attribute 'model_dump'" + ] + } + ], "source": [ "export_config = {\n", " \"export_name\": \"Test Export\",\n", @@ -923,8 +996,7 @@ " \"statuses\": ['review', 'r_assigned', 'client_review', 'cr_assigned', 'accepted']\n", "}\n", "\n", - "result = project.create_local_export(export_config)\n", - "\n" + "result = img_project.create_local_export(export_config)" ] }, { @@ -1044,19 +1116,25 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 33, "id": "14b59cef", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Video JSON saved to: Video_Keyframe_annot.json\n" + "ename": "TypeError", + "evalue": "list indices must be integers or slices, not str", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[33]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m export_json_path =\u001b[33mr\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mD:\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mProfessional\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mLabellerr_SDK\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mSDKPython\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mlabellerr\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mnotebooks\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mvideo_keyframe_annotations.json\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m video_annotations = \u001b[43mcoco_to_video_json\u001b[49m\u001b[43m(\u001b[49m\u001b[43mexport_json_path\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32mD:\\Professional\\Labellerr_SDK\\SDKPython\\labellerr\\services\\video_sampling\\__init__.py:285\u001b[39m, in \u001b[36mcoco_to_video_json\u001b[39m\u001b[34m(coco_json_path, output_path, fps)\u001b[39m\n\u001b[32m 268\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mcoco_to_video_json\u001b[39m(\n\u001b[32m 269\u001b[39m coco_json_path: \u001b[38;5;28mstr\u001b[39m,\n\u001b[32m 270\u001b[39m output_path: Optional[\u001b[38;5;28mstr\u001b[39m] = \u001b[33m\"\u001b[39m\u001b[33mVideo_Keyframe_annot.json\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 271\u001b[39m default_fps: \u001b[38;5;28mint\u001b[39m = \u001b[32m25\u001b[39m,\n\u001b[32m 272\u001b[39m ) -> List[Dict[\u001b[38;5;28mstr\u001b[39m, Any]]:\n\u001b[32m 273\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 274\u001b[39m \u001b[33;03m Convert COCO JSON format (from keyframe exports) to Video JSON format.\u001b[39;00m\n\u001b[32m 275\u001b[39m \n\u001b[32m 276\u001b[39m \u001b[33;03m This function transforms annotations exported from keyframe image projects\u001b[39;00m\n\u001b[32m 277\u001b[39m \u001b[33;03m into the format required for video project preannotation upload.\u001b[39;00m\n\u001b[32m 278\u001b[39m \n\u001b[32m 279\u001b[39m \u001b[33;03m Args:\u001b[39;00m\n\u001b[32m 280\u001b[39m \u001b[33;03m coco_json_path: Path to the COCO JSON file\u001b[39;00m\n\u001b[32m 281\u001b[39m \u001b[33;03m output_path: Path to save the converted JSON. Default: \"Video_Keyframe_annot.json\"\u001b[39;00m\n\u001b[32m 282\u001b[39m \u001b[33;03m Set to None to skip saving\u001b[39;00m\n\u001b[32m 283\u001b[39m \u001b[33;03m default_fps: Default frames per second if not found in filename (default: 25)\u001b[39;00m\n\u001b[32m 284\u001b[39m \n\u001b[32m--> \u001b[39m\u001b[32m285\u001b[39m \u001b[33;03m Returns:\u001b[39;00m\n\u001b[32m 286\u001b[39m \u001b[33;03m List of video annotation dictionaries\u001b[39;00m\n\u001b[32m 287\u001b[39m \n\u001b[32m 288\u001b[39m \u001b[33;03m Example:\u001b[39;00m\n\u001b[32m 289\u001b[39m \u001b[33;03m >>> from labellerr.services.video_sampling import coco_to_video_json\u001b[39;00m\n\u001b[32m 290\u001b[39m \u001b[33;03m >>> video_annotations = coco_to_video_json(\u001b[39;00m\n\u001b[32m 291\u001b[39m \u001b[33;03m ... \"export_zmYykSJhCAJqAaXaJQ3g.json\",\u001b[39;00m\n\u001b[32m 292\u001b[39m \u001b[33;03m ... \"Video_Keyframe_annot.json\"\u001b[39;00m\n\u001b[32m 293\u001b[39m \u001b[33;03m ... )\u001b[39;00m\n\u001b[32m 294\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m 295\u001b[39m \u001b[38;5;66;03m# Load COCO JSON\u001b[39;00m\n\u001b[32m 296\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(coco_json_path, \u001b[33m\"\u001b[39m\u001b[33mr\u001b[39m\u001b[33m\"\u001b[39m, encoding=\u001b[33m\"\u001b[39m\u001b[33mutf-8\u001b[39m\u001b[33m\"\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m f:\n", + "\u001b[31mTypeError\u001b[39m: list indices must be integers or slices, not str" ] } ], "source": [ + "export_json_path =r\"D:\\Professional\\Labellerr_SDK\\SDKPython\\labellerr\\notebooks\\video_keyframe_annotations.json\"\n", "video_annotations = coco_to_video_json(export_json_path)" ] }, diff --git a/labellerr/notebooks/test_coco_to_video.py b/labellerr/notebooks/test_coco_to_video.py new file mode 100644 index 0000000..3647aab --- /dev/null +++ b/labellerr/notebooks/test_coco_to_video.py @@ -0,0 +1,30 @@ +""" +Test script to convert COCO JSON export to Video JSON format. +Tests the import from labellerr.services.video_sampling module. +""" + +from labellerr.services.video_sampling import coco_to_video_json + +# Input COCO JSON file path +coco_json_path = r"D:\Professional\Labellerr_SDK\SDKPython\labellerr\notebooks\export-#huy0VWY14med4McdKd6h.json" + +# Output Video JSON file path +output_path = r"D:\Professional\Labellerr_SDK\SDKPython\labellerr\notebooks\video_keyframe_annotations.json" + +# Convert COCO to Video JSON format +# FPS will be extracted from filenames if available +# Falls back to default_fps=25 if not found in filename +video_annotations = coco_to_video_json( + coco_json_path=coco_json_path, + output_path=output_path, + default_fps=25 +) + +print(f"\n✅ Conversion complete!") +print(f"📁 Output saved to: {output_path}") +print(f"📊 Total videos processed: {len(video_annotations)}") + +# Print summary for each video +for video in video_annotations: + print(f"\n 🎬 {video['file_name']}") + print(f" Annotations: {len(video['annotations'])}") diff --git a/labellerr/notebooks/test_preannotation_api.py b/labellerr/notebooks/test_preannotation_api.py index 4f048bd..e10a8e8 100644 --- a/labellerr/notebooks/test_preannotation_api.py +++ b/labellerr/notebooks/test_preannotation_api.py @@ -20,8 +20,8 @@ if not CLIENT_ID: raise ValueError("QA_CLIENT_ID is not set") -PROJECT_ID = "jeanna_mixed_aphid_93841" -VIDEO_JSON_FILE_PATH = r"D:\Professional\Labellerr_SDK\dummy_annotation.json" +PROJECT_ID = "caryl_geographical_turkey_21445" +VIDEO_JSON_FILE_PATH = r"D:\Professional\Labellerr_SDK\SDKPython\labellerr\notebooks\video_keyframe_annotations.json" def main(): diff --git a/labellerr/services/video_sampling/__init__.py b/labellerr/services/video_sampling/__init__.py index d9a00ed..947577f 100644 --- a/labellerr/services/video_sampling/__init__.py +++ b/labellerr/services/video_sampling/__init__.py @@ -168,19 +168,19 @@ def process_videos_batch( # ============================================================================ -def _extract_video_name_and_frame(filename: str) -> tuple[str, int]: +def _extract_video_name_and_frame(filename: str) -> tuple[str, int, int]: """ - Extract video name and frame number from keyframe filename. + Extract video name, frame number, and FPS from keyframe filename. - Format: {dataset_id}+{file_id}+{video_name}+frame_{frame_number}.jpg - Example: 15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p+frame_1064.jpg - Returns: ("butterflies_960p.mp4", 1064) + Format: {dataset_id}+{file_id}+{video_name}+FPS{fps}+frame_{frame_number}.jpg + Example: 15908795-09eb-4cdb-a39b-8689f8f936e5+471163aa-19dc-4bc7-9aee-04780591281a+butterflies_960p+FPS29+frame_1064.jpg + Returns: ("butterflies_960p.mp4", 1064, 29) Args: filename: The keyframe filename Returns: - Tuple of (video_name, frame_number) + Tuple of (video_name, frame_number, fps) Raises: ValueError: If filename format is invalid @@ -191,7 +191,7 @@ def _extract_video_name_and_frame(filename: str) -> tuple[str, int]: if len(parts) < 4: raise ValueError(f"Invalid filename format: {filename}") - # Last part contains video_name+frame_X.jpg + # Last part contains frame_X.jpg last_part = parts[-1] # Extract frame number using regex @@ -201,11 +201,26 @@ def _extract_video_name_and_frame(filename: str) -> tuple[str, int]: frame_number = int(frame_match.group(1)) - # Extract video name (everything before +frame_X.jpg) - video_name_part = parts[-2] # The part before the last '+' + # Extract FPS from the parts (format: FPS{number}) + fps = 25 # Default FPS + video_name_part = None + + for i, part in enumerate(parts): + fps_match = re.match(r"FPS(\d+)$", part) + if fps_match: + fps = int(fps_match.group(1)) + # Video name is the part before FPS + if i > 0: + video_name_part = parts[i - 1] + break + + # If no FPS found, use the second-to-last part as video name (old format) + if video_name_part is None: + video_name_part = parts[-2] + video_name = f"{video_name_part}.mp4" - return video_name, frame_number + return video_name, frame_number, fps def _convert_segmentation_to_polygon(segmentation: List[float]) -> List[Dict[str, int]]: @@ -253,7 +268,7 @@ def _convert_bbox_to_video_format(bbox: List[float]) -> Dict[str, Any]: def coco_to_video_json( coco_json_path: str, output_path: Optional[str] = "Video_Keyframe_annot.json", - fps: int = 23, + default_fps: int = 25, ) -> List[Dict[str, Any]]: """ Convert COCO JSON format (from keyframe exports) to Video JSON format. @@ -265,7 +280,7 @@ def coco_to_video_json( coco_json_path: Path to the COCO JSON file output_path: Path to save the converted JSON. Default: "Video_Keyframe_annot.json" Set to None to skip saving - fps: Frames per second for the video (default: 23) + default_fps: Default frames per second if not found in filename (default: 25) Returns: List of video annotation dictionaries @@ -304,11 +319,12 @@ def coco_to_video_json( image = images[image_id] category = categories[category_id] - # Extract video name and frame number + # Extract video name, frame number, and FPS try: - video_name, frame_number = _extract_video_name_and_frame(image["file_name"]) + video_name, frame_number, fps = _extract_video_name_and_frame(image["file_name"]) except ValueError as e: print(f"Warning: Skipping annotation - {e}") + fps = default_fps # Use default if extraction fails continue # Determine question type based on annotation structure @@ -348,31 +364,10 @@ def coco_to_video_json( ] = question_name video_annotations[video_key]["annotations"][question_key]["answer"] = [] - # # Find or create the answer group for this annotation - # # Each unique annotation should be in its own answer group - # answer_id = annotation.get("labellerr_answer_id", annotation.get("id")) - - # Check if we already have an answer group for this annotation - existing_answer = None - for ans_group in video_annotations[video_key]["annotations"][question_key][ - "answer" - ]: - # Check if this frame already exists in this answer group - if str(frame_number) in ans_group.get("frames", {}): - existing_answer = ans_group - break - - if existing_answer is None: - # Create new answer group - existing_answer = {"startFrame": frame_number, "frames": {}} - video_annotations[video_key]["annotations"][question_key]["answer"].append( - existing_answer - ) - else: - # Update startFrame if this frame is earlier - if frame_number < existing_answer["startFrame"]: - existing_answer["startFrame"] = frame_number - + # Create a new answer group for each annotation + # This allows multiple annotations on the same frame + new_answer_group = {"startFrame": frame_number, "frames": {}} + # Add frame data frame_data = { "frame": frame_number, @@ -380,8 +375,11 @@ def coco_to_video_json( "isManualAnnotation": True, "fps": fps, } - - existing_answer["frames"][str(frame_number)] = frame_data + + new_answer_group["frames"][str(frame_number)] = frame_data + video_annotations[video_key]["annotations"][question_key]["answer"].append( + new_answer_group + ) # Convert to list format result = [] From 7ada84d01e21b139e5449bad8c2514aac1fca7b4 Mon Sep 17 00:00:00 2001 From: yashsuman Date: Mon, 29 Dec 2025 02:09:28 +0530 Subject: [PATCH 13/14] Fixed the FPS releated sync-up --- labellerr/core/files/video_file.py | 4 +++- labellerr/core/projects/__init__.py | 4 +--- labellerr/notebooks/test_coco_to_video.py | 4 +--- labellerr/services/video_sampling/__init__.py | 14 ++++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/labellerr/core/files/video_file.py b/labellerr/core/files/video_file.py index 5f2000e..88a0774 100644 --- a/labellerr/core/files/video_file.py +++ b/labellerr/core/files/video_file.py @@ -315,7 +315,9 @@ def download_create_video_auto_cleanup( if self.dataset_id and self.file_name and self.fps: # Remove extension from file_name if present, then add FPS and .mp4 base_name = os.path.splitext(self.file_name)[0] - video_filename = f"{self.dataset_id}+{self.file_id}+{base_name}+FPS{self.fps}.mp4" + video_filename = ( + f"{self.dataset_id}+{self.file_id}+{base_name}+FPS{self.fps}.mp4" + ) else: raise ValueError("dataset_id, file_name, and fps metadata are required") video_output_path = os.path.join(output_folder, video_filename) diff --git a/labellerr/core/projects/__init__.py b/labellerr/core/projects/__init__.py index 6e445a7..e17e8ac 100644 --- a/labellerr/core/projects/__init__.py +++ b/labellerr/core/projects/__init__.py @@ -4,6 +4,7 @@ from typing import List import requests + from labellerr import LabellerrClient from .. import client_utils, constants, schemas @@ -16,9 +17,6 @@ from .image_project import ImageProject as LabellerrImageProject from .text_project import TextProject as LabellerrTextProject from .video_project import VideoProject as LabellerrVideoProject -from ..annotation_templates import LabellerrAnnotationTemplate -from typing import List -from concurrent.futures import ThreadPoolExecutor __all__ = [ "LabellerrProject", diff --git a/labellerr/notebooks/test_coco_to_video.py b/labellerr/notebooks/test_coco_to_video.py index 3647aab..48603bb 100644 --- a/labellerr/notebooks/test_coco_to_video.py +++ b/labellerr/notebooks/test_coco_to_video.py @@ -15,9 +15,7 @@ # FPS will be extracted from filenames if available # Falls back to default_fps=25 if not found in filename video_annotations = coco_to_video_json( - coco_json_path=coco_json_path, - output_path=output_path, - default_fps=25 + coco_json_path=coco_json_path, output_path=output_path, default_fps=25 ) print(f"\n✅ Conversion complete!") diff --git a/labellerr/services/video_sampling/__init__.py b/labellerr/services/video_sampling/__init__.py index 947577f..31006cf 100644 --- a/labellerr/services/video_sampling/__init__.py +++ b/labellerr/services/video_sampling/__init__.py @@ -204,7 +204,7 @@ def _extract_video_name_and_frame(filename: str) -> tuple[str, int, int]: # Extract FPS from the parts (format: FPS{number}) fps = 25 # Default FPS video_name_part = None - + for i, part in enumerate(parts): fps_match = re.match(r"FPS(\d+)$", part) if fps_match: @@ -213,11 +213,11 @@ def _extract_video_name_and_frame(filename: str) -> tuple[str, int, int]: if i > 0: video_name_part = parts[i - 1] break - + # If no FPS found, use the second-to-last part as video name (old format) if video_name_part is None: video_name_part = parts[-2] - + video_name = f"{video_name_part}.mp4" return video_name, frame_number, fps @@ -321,7 +321,9 @@ def coco_to_video_json( # Extract video name, frame number, and FPS try: - video_name, frame_number, fps = _extract_video_name_and_frame(image["file_name"]) + video_name, frame_number, fps = _extract_video_name_and_frame( + image["file_name"] + ) except ValueError as e: print(f"Warning: Skipping annotation - {e}") fps = default_fps # Use default if extraction fails @@ -367,7 +369,7 @@ def coco_to_video_json( # Create a new answer group for each annotation # This allows multiple annotations on the same frame new_answer_group = {"startFrame": frame_number, "frames": {}} - + # Add frame data frame_data = { "frame": frame_number, @@ -375,7 +377,7 @@ def coco_to_video_json( "isManualAnnotation": True, "fps": fps, } - + new_answer_group["frames"][str(frame_number)] = frame_data video_annotations[video_key]["annotations"][question_key]["answer"].append( new_answer_group From 4567b201d563d4821e961c6ac6e90ad594fd077e Mon Sep 17 00:00:00 2001 From: yashsuman Date: Mon, 29 Dec 2025 17:04:27 +0530 Subject: [PATCH 14/14] minor fixes on annotation export download --- labellerr/notebooks/SDK.ipynb | 159 ++++++++++++++++------------------ 1 file changed, 73 insertions(+), 86 deletions(-) diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index 1025b45..dbb841f 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 16, "id": "edcdab6a", "metadata": {}, "outputs": [], @@ -32,7 +32,7 @@ "from labellerr.core.annotation_templates import create_template\n", "from labellerr.core.projects import create_project, LabellerrProject\n", "from labellerr.core.schemas.annotation_templates import AnnotationQuestion, QuestionType, CreateTemplateParams, DatasetDataType\n", - "from labellerr.core.schemas import DatasetConfig\n", + "from labellerr.core.schemas import DatasetConfig, CreateExportParams\n", "from labellerr.core.schemas.projects import CreateProjectParams, RotationConfig\n", "from labellerr.core.exceptions import LabellerrError\n", "import requests\n", @@ -263,13 +263,13 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "5205f618", "metadata": {}, "outputs": [], "source": [ - "dataset = LabellerrDataset(client=client,\n", - " dataset_id='a0d93479-4667-4574-b781-a530a6a243b9')" + "# dataset = LabellerrDataset(client=client,\n", + "# dataset_id='a0d93479-4667-4574-b781-a530a6a243b9')" ] }, { @@ -324,7 +324,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "7be23d9a", "metadata": {}, "outputs": [], @@ -350,17 +350,17 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "7e281c33", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'50189e79-ea31-42c1-86e7-5139e665b22e'" + "'85032be4-da4c-49d9-84a4-a24a2d6757ce'" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -391,7 +391,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 7, "id": "8996dd01", "metadata": {}, "outputs": [], @@ -414,17 +414,17 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 8, "id": "724c67cc", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'ellie_zesty_vole_86978'" + "'winifred_cute_bobcat_46128'" ] }, - "execution_count": 15, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -435,13 +435,13 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "f74cba85", "metadata": {}, "outputs": [], "source": [ - "video_project = LabellerrProject(client=client,\n", - " project_id='ellie_zesty_vole_86978')" + "# video_project = LabellerrProject(client=client,\n", + "# project_id='winifred_cute_bobcat_46128')" ] }, { @@ -605,7 +605,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 33, "id": "f5c41073", "metadata": {}, "outputs": [ @@ -815,13 +815,13 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "e0793ba6", "metadata": {}, "outputs": [], "source": [ - "dataset = LabellerrDataset(client=client,\n", - " dataset_id='3c5f6f14-66b7-4fca-b2ad-3a6b9bf0901e')" + "# dataset = LabellerrDataset(client=client,\n", + "# dataset_id='3c5f6f14-66b7-4fca-b2ad-3a6b9bf0901e')" ] }, { @@ -933,13 +933,13 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "id": "55c824a7", "metadata": {}, "outputs": [], "source": [ - "img_project = LabellerrProject(client=client,\n", - " project_id='melanie_external_perch_91510')" + "# img_project = LabellerrProject(client=client,\n", + "# project_id='melanie_external_perch_91510')" ] }, { @@ -976,84 +976,77 @@ "metadata": {}, "outputs": [ { - "ename": "AttributeError", - "evalue": "'dict' object has no attribute 'model_dump'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[29]\u001b[39m\u001b[32m, line 8\u001b[39m\n\u001b[32m 1\u001b[39m export_config = {\n\u001b[32m 2\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mexport_name\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mTest Export\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 3\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mexport_description\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mExport for testing\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 4\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mexport_format\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mcoco_json\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 5\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mstatuses\u001b[39m\u001b[33m\"\u001b[39m: [\u001b[33m'\u001b[39m\u001b[33mreview\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33mr_assigned\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33mclient_review\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33mcr_assigned\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33maccepted\u001b[39m\u001b[33m'\u001b[39m]\n\u001b[32m 6\u001b[39m }\n\u001b[32m----> \u001b[39m\u001b[32m8\u001b[39m result = \u001b[43mimg_project\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate_local_export\u001b[49m\u001b[43m(\u001b[49m\u001b[43mexport_config\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32mD:\\Professional\\Labellerr_SDK\\SDKPython\\labellerr\\core\\projects\\base.py:481\u001b[39m, in \u001b[36mLabellerrProject.create_local_export\u001b[39m\u001b[34m(self, export_config)\u001b[39m\n\u001b[32m 471\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 472\u001b[39m \u001b[33;03mCreates a local export with the given configuration.\u001b[39;00m\n\u001b[32m 473\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 476\u001b[39m \u001b[33;03m:raises LabellerrError: If the export creation fails\u001b[39;00m\n\u001b[32m 477\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 479\u001b[39m unique_id = client_utils.generate_request_id()\n\u001b[32m--> \u001b[39m\u001b[32m481\u001b[39m export_config_dict = \u001b[43mexport_config\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmodel_dump\u001b[49m()\n\u001b[32m 482\u001b[39m export_config_dict.update(\n\u001b[32m 483\u001b[39m {\u001b[33m\"\u001b[39m\u001b[33mexport_destination\u001b[39m\u001b[33m\"\u001b[39m: schemas.ExportDestination.LOCAL.value}\n\u001b[32m 484\u001b[39m )\n\u001b[32m 486\u001b[39m payload = json.dumps(export_config_dict)\n", - "\u001b[31mAttributeError\u001b[39m: 'dict' object has no attribute 'model_dump'" - ] + "data": { + "text/plain": [ + "{'status': [{'report_id': 'FsxNCB26Q31KBBOqpUw5',\n", + " 'export_status': 'Created',\n", + " 'is_completed': True,\n", + " 'download_url': {'url': 'https://storage.googleapis.com/labellerr-export-dev/6b18c5db-4af3-4f8f-89b2-67d3d0bbf16b.json?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251229%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251229T104644Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&response-content-type=json&response-content-disposition=attachment%3B%20filename%3D%22export-%23FsxNCB26Q31KBBOqpUw5.json%22&X-Goog-Signature=97171d6082ac7d0184d73fe0a65227676b13731997b23e373d8f28ff2bb76ea8eddcbbb6398030937abe7575fd4cd246ed7e0c474e2c597726057d06779bc113de7876d0dbaba573a714b65f1bf287dc9a8f1d4905e1537341ad58089c60cf3403dffbd2ef7eae54d43b3ce8440297a5274159a44a170d8f64cbba6bd48ca0176647aa997f53be89e56ae65c65b517625f260b5fc8ea888afb85495181d1474afd816b92b3ad79d3e3d5e4cc57f53aa279c2dd0e347b33aa863b0b8e53c5d3f5019f46aeed832af9bbb9f5b7cf70a3706ff2ea8c2af45e5b73e3b0092dd53fa2be617b7a6061509812d747bb332c0e1d260544788089d68e234f49b218f8bdce',\n", + " 'expires_at': 1767008804304}}]}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "export_config = {\n", - " \"export_name\": \"Test Export\",\n", - " \"export_description\": \"Export for testing\",\n", - " \"export_format\": \"coco_json\",\n", - " \"statuses\": ['review', 'r_assigned', 'client_review', 'cr_assigned', 'accepted']\n", - "}\n", + "export_config = CreateExportParams(\n", + " export_name=\"TEST Export\",\n", + " export_description=\"Export of all accepted annotations\",\n", + " export_format=\"coco_json\",\n", + " statuses=['review', 'r_assigned','client_review', 'cr_assigned','accepted']\n", + " )\n", + "\n", + "export = img_project.create_local_export(export_config)\n", "\n", - "result = img_project.create_local_export(export_config)" + "response_data = export.status()" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 18, "id": "220eaf7a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'zmYykSJhCAJqAaXaJQ3g'" + "'FsxNCB26Q31KBBOqpUw5'" ] }, - "execution_count": 10, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "result.report_id" - ] - }, - { - "cell_type": "markdown", - "id": "c7ba3c97", - "metadata": {}, - "source": [ - "### Check Status of export" + "export.report_id" ] }, { "cell_type": "code", - "execution_count": null, - "id": "ef475591", + "execution_count": 30, + "id": "15e63494", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'status': [{'report_id': 'zmYykSJhCAJqAaXaJQ3g', 'export_status': 'Created', 'is_completed': True, 'download_url': {'url': 'https://storage.googleapis.com/labellerr-export-dev/92075cec-468b-4cc3-90e3-4b1f2691c57c.json?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251209%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251209T064534Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&response-content-type=json&response-content-disposition=attachment%3B%20filename%3D%22export-%23zmYykSJhCAJqAaXaJQ3g.json%22&X-Goog-Signature=6c7cf8c2357dc98b749ffa32bf234a952d5c715d5742a91e3ff4cee8e893334a56181e238f2066ce0f05bef2b94dd626c25d0b40fe102fde64e090768462413e5d4ca80da2b77850ec5d0a661b076a11da5b5f44868fc020547b0b53ae3f7c7d0ac47257733a322e4e6face965505dbe4fcb797c0f951505f27afbe62d31bbaf981f200b34f5b76b8ac890be8d009e68158ebb5adab04b7eb48461a373478ca384ea54049f93e8501b5784c740544cc518f86ecd05134f805c1cf1f26f43a7df0d3d05a04cbb261fe51b5448c580a1d991ae0ac9309aea07103b67917e0c0f1c6790cf95c16505231d491c93499b495f01a18d22c462510b12c8361fedc72fa3', 'expires_at': 1765266334139, 'is_expired': False}}]}\n" - ] + "data": { + "text/plain": [ + "[{'report_id': 'FsxNCB26Q31KBBOqpUw5',\n", + " 'export_status': 'Created',\n", + " 'is_completed': True,\n", + " 'download_url': {'url': 'https://storage.googleapis.com/labellerr-export-dev/6b18c5db-4af3-4f8f-89b2-67d3d0bbf16b.json?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=local-dev%40labellerrdev.iam.gserviceaccount.com%2F20251229%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251229T110322Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&response-content-type=json&response-content-disposition=attachment%3B%20filename%3D%22export-%23FsxNCB26Q31KBBOqpUw5.json%22&X-Goog-Signature=d43ea1543020fa538b6d81383061d8e96f27ed301f47b7be24324dcdfd1993d9ddf54bbca9d20790b058897264c871625952e31201822895b781abf60ace05c273b0424e4546f837e955e0ee8330b9574b0bb7ae4738a89358776471b647ff2975900df0470065a5861a66be7c7b8f3f4cd3cd8548a34dac507767857e9a4f555f4ac50c6e6387860fec898ee0675c11a76018f3dc54bfc9729485d149d70f12cf1ae27c7a2a2f83ac31137aa8efecd3903f3b8046fa6ddb2e821e7a97c4d2be69a48a61ae2e476fb3cf0b77ac112c6661e062840eab66576aaf74fc24c0d6fd8059ea264629fb792b9dc4cdef528f6626d24e31aefb52a3a1e92d520780f638',\n", + " 'expires_at': 1767009802274}}]" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "try:\n", - " # Get project instance\n", - " project = LabellerrProject(client=client, project_id=project_id)\n", - " \n", - " # Check export status\n", - " response_data = json.loads(project.check_export_status(\n", - " report_ids=[result.report_id]\n", - " ))\n", - " print(response_data)\n", - "except LabellerrError as e:\n", - " print(f\"Failed to check export status: {str(e)}\")" + "response_data['status']" ] }, { @@ -1066,7 +1059,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 31, "id": "1d5455c3", "metadata": {}, "outputs": [ @@ -1074,8 +1067,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "✓ Downloaded: export_zmYykSJhCAJqAaXaJQ3g.json\n", - "Export saved at: export_zmYykSJhCAJqAaXaJQ3g.json\n" + "✓ Downloaded: export_FsxNCB26Q31KBBOqpUw5.json\n", + "Export saved at: export_FsxNCB26Q31KBBOqpUw5.json\n" ] } ], @@ -1116,25 +1109,19 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 34, "id": "14b59cef", "metadata": {}, "outputs": [ { - "ename": "TypeError", - "evalue": "list indices must be integers or slices, not str", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[33]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m export_json_path =\u001b[33mr\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mD:\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mProfessional\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mLabellerr_SDK\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mSDKPython\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mlabellerr\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mnotebooks\u001b[39m\u001b[33m\\\u001b[39m\u001b[33mvideo_keyframe_annotations.json\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m video_annotations = \u001b[43mcoco_to_video_json\u001b[49m\u001b[43m(\u001b[49m\u001b[43mexport_json_path\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32mD:\\Professional\\Labellerr_SDK\\SDKPython\\labellerr\\services\\video_sampling\\__init__.py:285\u001b[39m, in \u001b[36mcoco_to_video_json\u001b[39m\u001b[34m(coco_json_path, output_path, fps)\u001b[39m\n\u001b[32m 268\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mcoco_to_video_json\u001b[39m(\n\u001b[32m 269\u001b[39m coco_json_path: \u001b[38;5;28mstr\u001b[39m,\n\u001b[32m 270\u001b[39m output_path: Optional[\u001b[38;5;28mstr\u001b[39m] = \u001b[33m\"\u001b[39m\u001b[33mVideo_Keyframe_annot.json\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 271\u001b[39m default_fps: \u001b[38;5;28mint\u001b[39m = \u001b[32m25\u001b[39m,\n\u001b[32m 272\u001b[39m ) -> List[Dict[\u001b[38;5;28mstr\u001b[39m, Any]]:\n\u001b[32m 273\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 274\u001b[39m \u001b[33;03m Convert COCO JSON format (from keyframe exports) to Video JSON format.\u001b[39;00m\n\u001b[32m 275\u001b[39m \n\u001b[32m 276\u001b[39m \u001b[33;03m This function transforms annotations exported from keyframe image projects\u001b[39;00m\n\u001b[32m 277\u001b[39m \u001b[33;03m into the format required for video project preannotation upload.\u001b[39;00m\n\u001b[32m 278\u001b[39m \n\u001b[32m 279\u001b[39m \u001b[33;03m Args:\u001b[39;00m\n\u001b[32m 280\u001b[39m \u001b[33;03m coco_json_path: Path to the COCO JSON file\u001b[39;00m\n\u001b[32m 281\u001b[39m \u001b[33;03m output_path: Path to save the converted JSON. Default: \"Video_Keyframe_annot.json\"\u001b[39;00m\n\u001b[32m 282\u001b[39m \u001b[33;03m Set to None to skip saving\u001b[39;00m\n\u001b[32m 283\u001b[39m \u001b[33;03m default_fps: Default frames per second if not found in filename (default: 25)\u001b[39;00m\n\u001b[32m 284\u001b[39m \n\u001b[32m--> \u001b[39m\u001b[32m285\u001b[39m \u001b[33;03m Returns:\u001b[39;00m\n\u001b[32m 286\u001b[39m \u001b[33;03m List of video annotation dictionaries\u001b[39;00m\n\u001b[32m 287\u001b[39m \n\u001b[32m 288\u001b[39m \u001b[33;03m Example:\u001b[39;00m\n\u001b[32m 289\u001b[39m \u001b[33;03m >>> from labellerr.services.video_sampling import coco_to_video_json\u001b[39;00m\n\u001b[32m 290\u001b[39m \u001b[33;03m >>> video_annotations = coco_to_video_json(\u001b[39;00m\n\u001b[32m 291\u001b[39m \u001b[33;03m ... \"export_zmYykSJhCAJqAaXaJQ3g.json\",\u001b[39;00m\n\u001b[32m 292\u001b[39m \u001b[33;03m ... \"Video_Keyframe_annot.json\"\u001b[39;00m\n\u001b[32m 293\u001b[39m \u001b[33;03m ... )\u001b[39;00m\n\u001b[32m 294\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m 295\u001b[39m \u001b[38;5;66;03m# Load COCO JSON\u001b[39;00m\n\u001b[32m 296\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(coco_json_path, \u001b[33m\"\u001b[39m\u001b[33mr\u001b[39m\u001b[33m\"\u001b[39m, encoding=\u001b[33m\"\u001b[39m\u001b[33mutf-8\u001b[39m\u001b[33m\"\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m f:\n", - "\u001b[31mTypeError\u001b[39m: list indices must be integers or slices, not str" + "name": "stdout", + "output_type": "stream", + "text": [ + "Video JSON saved to: Video_Keyframe_annot.json\n" ] } ], "source": [ - "export_json_path =r\"D:\\Professional\\Labellerr_SDK\\SDKPython\\labellerr\\notebooks\\video_keyframe_annotations.json\"\n", "video_annotations = coco_to_video_json(export_json_path)" ] }, @@ -1148,7 +1135,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 36, "id": "df6b3ac7", "metadata": {}, "outputs": [ @@ -1156,7 +1143,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'message': '200: Success', 'response': {'metadata': {'questions_ignored': [], 'activity_id': 'ee67556d-ba08-4f71-9767-eff9300ee8d9', 'files_not_updated': [], 'videos_processed': [{'status': 'success', 'file_id': '471163aa-19dc-4bc7-9aee-04780591281a', 'file_name': 'butterflies_960p.mp4', 'frames_processed': 5, 'total_annotations': 5}, {'status': 'success', 'file_name': 'seafood_1280p.mp4', 'file_id': 'a878e61b-8aeb-46e1-ab10-5f1852bcdcbe', 'total_annotations': 1, 'frames_processed': 1}]}, 'job_type': 'pre-annotations', 'created_by': '1c8800.8177f647b0b9bc6321bcec4d93', 'activity_id': 'ee67556d-ba08-4f71-9767-eff9300ee8d9', 'status': 'completed', 'project_id': 'caryl_geographical_turkey_21445', 'job_id': 'ee67556d-ba08-4f71-9767-eff9300ee8d9', 'created_at': 1765273090056, 'updated_at': 1765273225613}, 'error': None, 'tracking_id': None}\n" + "{'message': '200: Success', 'response': {'job_type': 'pre-annotations', 'metadata': {'files_not_updated': [], 'videos_processed': [{'file_name': 'butterflies_960p.mp4', 'frames_processed': 15, 'status': 'success', 'file_id': '9bf1ee94-ab41-435a-8935-38f6213b05f9', 'total_annotations': 15}, {'file_id': 'b50c92ff-4bfb-40f7-9752-0871428d65ce', 'file_name': 'seafood_1280p.mp4', 'frames_processed': 8, 'total_annotations': 8, 'status': 'success'}], 'activity_id': '772a2520-e994-40c3-b512-820ada998af8', 'questions_ignored': []}, 'project_id': 'winifred_cute_bobcat_46128', 'updated_at': 1767006996651, 'status': 'completed', 'activity_id': '772a2520-e994-40c3-b512-820ada998af8', 'created_by': '21dbbb.d54ba24efab37ba6b6c6f58916', 'created_at': 1767006844798, 'job_id': '772a2520-e994-40c3-b512-820ada998af8'}, 'error': None, 'tracking_id': None}\n" ] } ],