From 6eebb8aa333ad75fcb9fc227641d6dcda26f59b9 Mon Sep 17 00:00:00 2001 From: Yash Raj Suman Date: Sun, 14 Sep 2025 19:43:58 +0530 Subject: [PATCH 01/23] Add tests/integration folder test: Add integration tests for Labellerr SDK This commit introduces integration tests for core SDK functionalities: Features tested: - Project creation with multiple annotation types: * Polygon annotations * Bounding box detection * Classification (select, dropdown, radio) * Text input fields * Combined annotation types - Project export functionality - Pre-annotation upload support (COCO JSON format) Test structure: /tests /integration - Create_Project.py # Project creation test cases - Export_project.py # Export functionality tests - Pre_annotation.py # Pre-annotation upload tests - main.py # Test runner - cred.py # Credentials config (gitignored) Requirements: - Valid API credentials in cred.py - Test image dataset in test_img/ - Sample annotations in annotations.json Note: Remember to update cred.py with valid credentials before running tests --- tests/integration/Create_Project.py | 567 ++++++++++++++++++ tests/integration/Export_project.py | 30 + tests/integration/Pre_annotation_uploading.py | 26 + .../Create_Project.cpython-310.pyc | Bin 0 -> 7643 bytes .../Create_Project.cpython-311.pyc | Bin 0 -> 14918 bytes .../Create_Project.cpython-312.pyc | Bin 0 -> 12836 bytes .../__pycache__/cred.cpython-310.pyc | Bin 0 -> 383 bytes tests/integration/cred.py | 6 + tests/integration/main.py | 64 ++ 9 files changed, 693 insertions(+) create mode 100644 tests/integration/Create_Project.py create mode 100644 tests/integration/Export_project.py create mode 100644 tests/integration/Pre_annotation_uploading.py create mode 100644 tests/integration/__pycache__/Create_Project.cpython-310.pyc create mode 100644 tests/integration/__pycache__/Create_Project.cpython-311.pyc create mode 100644 tests/integration/__pycache__/Create_Project.cpython-312.pyc create mode 100644 tests/integration/__pycache__/cred.cpython-310.pyc create mode 100644 tests/integration/cred.py create mode 100644 tests/integration/main.py diff --git a/tests/integration/Create_Project.py b/tests/integration/Create_Project.py new file mode 100644 index 0000000..8a797e2 --- /dev/null +++ b/tests/integration/Create_Project.py @@ -0,0 +1,567 @@ +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'SDKPython'))) + +# Add the root directory to Python path +root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(root_dir) + +from SDKPython.labellerr.client import LabellerrClient +from SDKPython.labellerr.exceptions import LabellerrError +import uuid + +def create_project_all_option_type(api_key, api_secret, client_id, email, path_to_images): + """Creates a project with all option types using the Labellerr SDK.""" + + client = LabellerrClient(api_key, api_secret) + project_payload = { + 'client_id': client_id, + 'dataset_name': 'Testing_dataset', + 'dataset_description': 'A sample dataset for image classification', + 'data_type': 'image', + 'created_by': email, + 'project_name': 'Testing_project-7', + 'annotation_guide': [ + { + "question_number": 1, # incremental series starting from 1 + "question": "Test", # question name + "question_id": "533bb0c8-fb2b-4394-a8e1-5042a944802f", # random uuid + "option_type": "polygon", + "required": True, + "options": [ + {"option_name": "#fe1236"}, # give the hex code of some random color + ] + }, + { + "question_number": 2, # Pixel annotation for bounding box format + "question": "Test2", + "question_id": "533bb0c8-fb2b-4394-a8e1-5042a944808d", + "option_type": "BoundingBox", + "required": True, + "options": [ + {"option_name": "#afe126"} + ] + }, + { + "question_number": 3, # Classification question for simple input field + "question": "Test-Input", + "option_type": "input", + "question_id": "81bc5c1a-5b95-4df2-8085-aca8d66a93ad", + "required": True, + "options": [] # this will be empty array only + }, + { + "question_number": 4, # Classification question for multi-select dropdown + "question": "Multi-Test", + "option_type": "select", + "question_id": "971c5c1a-5b95-4df2-8085-aca8d66a0351", + "required": True, + "options": [ + { + "option_id": "22b7942f-06ef-4293-9d73-d117eda8ec0d", + "option_name": "A" + }, + { + "option_id": "15e0e903-ed8f-43ff-a841-a0638ff08153", + "option_name": "B" + }, + { + "option_id": "c2e37dad-5034-4bed-920b-5fc14c4032e0", + "option_name": "C" + } + ] + }, + { + "question_number": 5, # Classification question for single-select dropdown + "question": "Test-Dropdown", + "option_type": "dropdown", + "question_id": "456c5c1a-5b95-4df2-8085-aca8d66a03049", + "required": True, + "options": [ + { + "option_id": "58k142f-06ef-4293-9d73-d117eda87254", + "option_name": "Sample A" + }, + { + "option_id": "43t56903-ed8f-43ff-a841-a0638ff08856", + "option_name": "Sample B" + } + ] + }, + { + "question_number": 6, # Classification question for radio + "question": "Radio test", + "option_type": "radio", + "question_id": "712v5c1a-5b95-4df2-8085-aca8d66a01048", + "required": True, + "options": [ + { + "option_id": "916v24h-06ef-4293-9d73-d117eda81112", + "option_name": "1" + }, + { + "option_id": "12ak879-ed8f-43ff-a841-a0638ff23115", + "option_name": "2" + } + ] + } + ], + 'rotation_config': { + 'annotation_rotation_count': 1, + 'review_rotation_count': 1, + 'client_review_rotation_count': 1 + }, + 'autolabel': False, + 'folder_to_upload': path_to_images + } + try: + result = client.initiate_create_project(project_payload) + print(f"[ALL OPTION TYPE] Project ID: {result['project_id']['response']['project_id']}") + except LabellerrError as e: + + print(f"Project creation failed: {str(e)}") + + +def create_project_polygon_boundingbox_project(api_key, api_secret, client_id, email, path_to_images): + + client = LabellerrClient(api_key, api_secret) + + project_payload = { + 'client_id': client_id, + 'dataset_name': 'Testing_dataset', + 'dataset_description': 'Dataset for object detection with polygon and bounding box annotations', + 'data_type': 'image', + 'created_by': email, + 'project_name': 'polygon_boundingbox_project', + 'annotation_guide': [ + { + "question_number": 1, + "question": "Vehicle Detection", + "question_id": str(uuid.uuid4()), + "option_type": "polygon", + "required": True, + "options": [ + {"option_name": "#ff6b35"} # Orange for vehicles + ] + }, + { + "question_number": 2, + "question": "Person Detection", + "question_id": str(uuid.uuid4()), + "option_type": "BoundingBox", + "required": True, + "options": [ + {"option_name": "#4ecdc4"} # Teal for persons + ] + } + ], + 'rotation_config': { + 'annotation_rotation_count': 1, + 'review_rotation_count': 1, + 'client_review_rotation_count': 1 + }, + 'autolabel': False, + 'folder_to_upload': path_to_images + } + + try: + result = client.initiate_create_project(project_payload) + print(f"[polygon_boundingbox] Project ID: {result['project_id']['response']['project_id']}") + except LabellerrError as e: + print(f"Project creation failed: {str(e)}") + +def create_project_select_dropdown_radio(api_key, api_secret, client_id, email, path_to_images): + + client = LabellerrClient(api_key, api_secret) + + project_payload = { + 'client_id': client_id, + 'dataset_name': 'Testing_dataset', + 'dataset_description': 'Dataset for multi-label image classification', + 'data_type': 'image', + 'created_by': email, + 'project_name': 'select_dropdown_radio_project', + 'annotation_guide': [ + { + "question_number": 1, + "question": "Object Categories", + "option_type": "select", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [ + { + "option_id": str(uuid.uuid4()), + "option_name": "Animals" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Vehicles" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Buildings" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Nature" + } + ] + }, + { + "question_number": 2, + "question": "Image Quality", + "option_type": "dropdown", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [ + { + "option_id": str(uuid.uuid4()), + "option_name": "High Quality" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Medium Quality" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Low Quality" + } + ] + }, + { + "question_number": 3, + "question": "Lighting Condition", + "option_type": "radio", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [ + { + "option_id": str(uuid.uuid4()), + "option_name": "Bright" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Dim" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Dark" + } + ] + } + ], + 'rotation_config': { + 'annotation_rotation_count': 1, + 'review_rotation_count': 1, + 'client_review_rotation_count': 1 + }, + 'autolabel': False, + 'folder_to_upload': path_to_images + } + + try: + result = client.initiate_create_project(project_payload) + print(f"[select_dropdown_radio] Project ID: {result['project_id']['response']['project_id']}") + except LabellerrError as e: + print(f"Project creation failed: {str(e)}") + +def create_project_polygon_input(api_key, api_secret, client_id, email, path_to_images): + + client = LabellerrClient(api_key, api_secret) + + project_payload = { + 'client_id': client_id, + 'dataset_name': 'Testing_dataset', + 'dataset_description': 'Medical images with detailed annotations and metadata', + 'data_type': 'image', + 'created_by': email, + 'project_name': 'polygon_input_project', + 'annotation_guide': [ + { + "question_number": 1, + "question": "Anomaly Region", + "question_id": str(uuid.uuid4()), + "option_type": "polygon", + "required": True, + "options": [ + {"option_name": "#ff4757"} # Red for anomalies + ] + }, + { + "question_number": 2, + "question": "Anomaly Description", + "question": "Describe the anomaly", + "option_type": "input", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [] + }, + { + "question_number": 3, + "question": "Additional Notes", + "option_type": "input", + "question_id": str(uuid.uuid4()), + "required": False, + "options": [] + } + ], + 'rotation_config': { + 'annotation_rotation_count': 1, + 'review_rotation_count': 1, + 'client_review_rotation_count': 1 + }, + 'autolabel': False, + 'folder_to_upload': path_to_images + } + + try: + result = client.initiate_create_project(project_payload) + print(f"[polygon_input_project] Project ID: {result['project_id']['response']['project_id']}") + except LabellerrError as e: + print(f"Project creation failed: {str(e)}") + +def create_project_input_select_radio(api_key, api_secret, client_id, email, path_to_images): + + client = LabellerrClient(api_key, api_secret) + + project_payload = { + 'client_id': client_id, + 'dataset_name': 'Testing_dataset', + 'dataset_description': 'Dataset for evaluating and moderating image content', + 'data_type': 'image', + 'created_by': email, + 'project_name': 'input_select_radio_project', + 'annotation_guide': [ + { + "question_number": 1, + "question": "Content Summary", + "option_type": "input", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [] + }, + { + "question_number": 2, + "question": "Content Categories", + "option_type": "select", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [ + { + "option_id": str(uuid.uuid4()), + "option_name": "Educational" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Entertainment" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Commercial" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "News" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Social" + } + ] + }, + { + "question_number": 3, + "question": "Content Appropriateness", + "option_type": "radio", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [ + { + "option_id": str(uuid.uuid4()), + "option_name": "Appropriate" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Needs Review" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Inappropriate" + } + ] + } + ], + 'rotation_config': { + 'annotation_rotation_count': 1, + 'review_rotation_count': 1, + 'client_review_rotation_count': 1 + }, + 'autolabel': False, + 'folder_to_upload': path_to_images + } + + try: + result = client.initiate_create_project(project_payload) + print(f"[input_select_radio] Project ID: {result['project_id']['response']['project_id']}") + except LabellerrError as e: + print(f"Project creation failed: {str(e)}") + +def create_project_boundingbox_dropdown_input(api_key, api_secret, client_id, email, path_to_images): + + client = LabellerrClient(api_key, api_secret) + + project_payload = { + 'client_id': client_id, + 'dataset_name': 'Testing_dataset', + 'dataset_description': 'Retail product images with bounding boxes and metadata', + 'data_type': 'image', + 'created_by': email, + 'project_name': 'boundingbox_dropdown_input_project', + 'annotation_guide': [ + { + "question_number": 1, + "question": "Product Bounding Box", + "question_id": str(uuid.uuid4()), + "option_type": "BoundingBox", + "required": True, + "options": [ + {"option_name": "#2ed573"} # Green for products + ] + }, + { + "question_number": 2, + "question": "Product Category", + "option_type": "dropdown", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [ + { + "option_id": str(uuid.uuid4()), + "option_name": "Electronics" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Clothing" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Home & Garden" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Sports" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Books" + } + ] + }, + { + "question_number": 3, + "question": "Product Name/Brand", + "option_type": "input", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [] + }, + { + "question_number": 4, + "question": "Product Condition Notes", + "option_type": "input", + "question_id": str(uuid.uuid4()), + "required": False, + "options": [] + } + ], + 'rotation_config': { + 'annotation_rotation_count': 1, + 'review_rotation_count': 1, + 'client_review_rotation_count': 1 + }, + 'autolabel': False, + 'folder_to_upload': path_to_images + } + + try: + result = client.initiate_create_project(project_payload) + print(f"[boundingbox_dropdown_input] Project ID: {result['project_id']['response']['project_id']}") + except LabellerrError as e: + print(f"Project creation failed: {str(e)}") + +def create_project_radio_dropdown(api_key, api_secret, client_id, email, path_to_images): + + client = LabellerrClient(api_key, api_secret) + + project_payload = { + 'client_id': client_id, + 'dataset_name': 'Testing_dataset', + 'dataset_description': 'Simple dataset for quick image classification', + 'data_type': 'image', + 'created_by': email, + 'project_name': 'radio_dropdown_project', + 'annotation_guide': [ + { + "question_number": 1, + "question": "Image Type", + "option_type": "radio", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [ + { + "option_id": str(uuid.uuid4()), + "option_name": "Indoor" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Outdoor" + } + ] + }, + { + "question_number": 2, + "question": "Primary Subject", + "option_type": "dropdown", + "question_id": str(uuid.uuid4()), + "required": True, + "options": [ + { + "option_id": str(uuid.uuid4()), + "option_name": "Person" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Animal" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Object" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Landscape" + }, + { + "option_id": str(uuid.uuid4()), + "option_name": "Architecture" + } + ] + } + ], + 'rotation_config': { + 'annotation_rotation_count': 1, + 'review_rotation_count': 1, + 'client_review_rotation_count': 1 + }, + 'autolabel': False, + 'folder_to_upload': path_to_images + } + + try: + result = client.initiate_create_project(project_payload) + print(f"[radio_dropdown] Project ID: {result['project_id']['response']['project_id']}") + except LabellerrError as e: + print(f"Project creation failed: {str(e)}") + + + diff --git a/tests/integration/Export_project.py b/tests/integration/Export_project.py new file mode 100644 index 0000000..100d56e --- /dev/null +++ b/tests/integration/Export_project.py @@ -0,0 +1,30 @@ +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'SDKPython'))) + +# Add the root directory to Python path +root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(root_dir) + +from SDKPython.labellerr.client import LabellerrClient +from SDKPython.labellerr.exceptions import LabellerrError +import uuid + + +def export_project(api_key, api_secret, client_id, project_id): + """Exports a project using the Labellerr SDK.""" + + client = LabellerrClient(api_key, api_secret) + export_config = { + "export_name": "Weekly Export", + "export_description": "Export of all accepted annotations", + "export_format": "coco_json", + "statuses": ['review', 'r_assigned','client_review', 'cr_assigned','accepted'] + } + try: + result = client.create_local_export(project_id, client_id, export_config) + + export_id = result["response"]['report_id'] + print(f"Local export created successfully. Export ID: {export_id}") + except LabellerrError as e: + print(f"Local export creation failed: {str(e)}") \ No newline at end of file diff --git a/tests/integration/Pre_annotation_uploading.py b/tests/integration/Pre_annotation_uploading.py new file mode 100644 index 0000000..ac4c5f5 --- /dev/null +++ b/tests/integration/Pre_annotation_uploading.py @@ -0,0 +1,26 @@ +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'SDKPython'))) + +# Add the root directory to Python path +root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(root_dir) + +from SDKPython.labellerr.client import LabellerrClient +from SDKPython.labellerr.exceptions import LabellerrError +import uuid + +def pre_annotation_uploading(api_key, api_secret, client_id, project_id, annotation_format, annotation_file): + + client = LabellerrClient(api_key, api_secret) + try: + # Upload and wait for processing to complete + result = client.upload_preannotation_by_project_id(project_id, client_id, annotation_format, annotation_file) + # Check the final status + if result['response']['status'] == 'completed': + print("Pre-annotations processed successfully") + # Access additional metadata if needed + metadata = result['response'].get('metadata', {}) + print("metadata",metadata) + except LabellerrError as e: + print(f"Pre-annotation upload failed: {str(e)}") \ No newline at end of file diff --git a/tests/integration/__pycache__/Create_Project.cpython-310.pyc b/tests/integration/__pycache__/Create_Project.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56e6ba82b80684008fc531ce1c1e96f49c0345ad GIT binary patch literal 7643 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 aCE7%j#&vJgB260AU8F_jQ~Xny_x}T)R9uq) literal 0 HcmV?d00001 diff --git a/tests/integration/__pycache__/Create_Project.cpython-312.pyc b/tests/integration/__pycache__/Create_Project.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55f02a2c652a9be03a7f44de712732149a2525fc GIT binary patch literal 12836 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 VXNMhssG}5h+M1%NT0>LY>L0~RWaj_? literal 0 HcmV?d00001 diff --git a/tests/integration/cred.py b/tests/integration/cred.py new file mode 100644 index 0000000..6b5b428 --- /dev/null +++ b/tests/integration/cred.py @@ -0,0 +1,6 @@ +API_KEY = '66ee9d.85405d401b9c58481b9e2b73f5' +API_SECRET = 'cf2f638ae901c8fd5611d097846ce6c806451818573ca6e92a412ff188df1b05' + +CLIENT_ID = "14051" +PROJECT_ID = None +EMAIL_ID = "yashsuman15@gmail.com" \ No newline at end of file diff --git a/tests/integration/main.py b/tests/integration/main.py new file mode 100644 index 0000000..3a4aa9e --- /dev/null +++ b/tests/integration/main.py @@ -0,0 +1,64 @@ +from Create_Project import * +from Export_project import export_project +import cred +from Pre_annotation_uploading import pre_annotation_uploading + +api_key = cred.API_KEY +api_secret = cred.API_SECRET +client_id = cred.CLIENT_ID +project_id = cred.PROJECT_ID +email = cred.EMAIL_ID + + + +def test_create_project(path_to_images): + + print("CREATING PROJECTS WITH DIFFERENT OPTION TYPE") + print("\n 1:project with all option type") + create_project_all_option_type(api_key, api_secret, client_id, email, path_to_images) + + print("\n 2:project with polygon and bounding box") + create_project_polygon_boundingbox_project(api_key, api_secret, client_id, email, path_to_images) + + print("\n 3:project with select, dropdown and radio") + create_project_select_dropdown_radio(api_key, api_secret, client_id, email, path_to_images) + + print("\n 4:project with polygon and input") + create_project_polygon_input(api_key, api_secret, client_id, email, path_to_images) + + print("\n 5:project with input, select and radio") + create_project_input_select_radio(api_key, api_secret, client_id, email, path_to_images) + + print("\n 6:project with bounding box, dropdown and input") + create_project_boundingbox_dropdown_input(api_key, api_secret, client_id, email, path_to_images) + + print("\n 7:project with radio and dropdown") + create_project_radio_dropdown(api_key, api_secret, client_id, email, path_to_images) + + print("\n Project creation completed.") + +def test_export_project(project_id): + print("\n EXPORTING PROJECT") + export_project(api_key, api_secret, client_id, project_id) + print("\n Project export completed.") + +def test_pre_annotation_uploading(project_id, annotation_format, annotation_file): + print("\n PRE-ANNOTATION UPLOADING") + pre_annotation_uploading(api_key, api_secret, client_id, project_id, annotation_format, annotation_file) + print("\n Pre-annotation uploading completed.") + +if __name__ == "__main__": + + test_dataset_path = r'D:\professional\LABELLERR\Task\LABIMP-7059-SDK-Testing\test_img_6' + test_create_project(test_dataset_path) + + test_export_project(project_id) + + json_annotation_file = r'D:\professional\LABELLERR\Task\LABIMP-7059-SDK-Testing\test_img_6_annotations.json' + test_pre_annotation_uploading(project_id, 'coco_json', json_annotation_file) + + + + + + \ No newline at end of file From 0e32505164b5083743e24ecdbf300f09d9cc801c Mon Sep 17 00:00:00 2001 From: Yash Raj Suman Date: Tue, 16 Sep 2025 04:55:00 +0000 Subject: [PATCH 02/23] -removed api keys, secrets, client id -removed __pychace__ -added .gitignore --- tests/__pycache__/__init__.cpython-310.pyc | Bin 155 -> 0 bytes tests/__pycache__/test_client.cpython-310.pyc | Bin 1542 -> 0 bytes tests/__pycache__/test_client.cpython-312.pyc | Bin 2626 -> 0 bytes tests/integration/.gitignore | 3 +++ .../__pycache__/Create_Project.cpython-310.pyc | Bin 7643 -> 0 bytes .../__pycache__/Create_Project.cpython-311.pyc | Bin 14918 -> 0 bytes .../__pycache__/Create_Project.cpython-312.pyc | Bin 12836 -> 0 bytes .../__pycache__/cred.cpython-310.pyc | Bin 383 -> 0 bytes tests/integration/cred.py | 10 +++++----- 9 files changed, 8 insertions(+), 5 deletions(-) delete mode 100644 tests/__pycache__/__init__.cpython-310.pyc delete mode 100644 tests/__pycache__/test_client.cpython-310.pyc delete mode 100644 tests/__pycache__/test_client.cpython-312.pyc create mode 100644 tests/integration/.gitignore delete mode 100644 tests/integration/__pycache__/Create_Project.cpython-310.pyc delete mode 100644 tests/integration/__pycache__/Create_Project.cpython-311.pyc delete mode 100644 tests/integration/__pycache__/Create_Project.cpython-312.pyc delete mode 100644 tests/integration/__pycache__/cred.cpython-310.pyc diff --git a/tests/__pycache__/__init__.cpython-310.pyc b/tests/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 1b1c8d7e4ced92b29e6b0157ffd6327ad9baa53b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155 zcmd1j<>g`kf>Yi6=^*+sh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vGKeRZts8~NS zFFi4@I5kh-B|o_|H#M)MSU)E*DK#gxs7OE9#XF#~BqKjhza+I7C=nl@nU`4-AFo$X Xd5gm)H$SB`C)EyQZZQ*(U||3N=TRg+ diff --git a/tests/__pycache__/test_client.cpython-310.pyc b/tests/__pycache__/test_client.cpython-310.pyc deleted file mode 100644 index 610757eb79424c4945a59b2e4e7f63914f3a5830..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1542 zcma)6&2Hm15GEzsmYv`3?xjErs0DiP#s6{aqCY8;pobPg0dIQXMW98>t1KnDq`cTQ z9ORHa_7V1w9Q!hT09|`3`UXXTc1S0w)Ampb9L|h5!{IkGR69Ej1Y_&BceCZ^2>oJ( z%|Spof@xj>;fQ02s9imYGmm=M^1k#lLW$#vtYlSMbv%|eT5~o54cuC%b+n# zy!saLsz4W8*FN1AJG}PN(_?{dc5e}Qz!Nu{!LM*OI&0#x8p~AV1#o-cvZ;_#XnlOI z+I>)+YOQo^jW&k|!VygK0T6*G=7@S6bMGypKKD6+mGH_0HLK;*=fV^Z{?Yl&V*B-24Z9`jx-csLz!5sweXQ`U>)QGeX&Gk)0VAN2d(!C)xj1Hr|37_**e zdfLXJ?d<>;VuVb7Lb?Xx?_aggjnJme@)^sG$lE6>USt5EX-f+qpx2(9eDmF#;)TlF z1zg!U8m(b&&EJ$2BZ~96y`Zrt?0d`i{{xzq+w};b3hcJi0dxWk$Cm)mrDu`0sh?q9 zdq*z)6}mt-c!gKq%D?q5$qL)|=&;-Eb|@JQM!n^hi}LjKT<2p+OLR|P9lm79`W z7?soD{$G>OE#a)AoMnPOxf8fBu}11-&B(8?&(^v~uh{)}V)^5uNGf+vzbG;B{Wv)}`!HTXzR%v6d z)rE+QDCM-it{cnV#}lhONoQqM3j?`vWw|B+7?p_ zo3_Pke;O+VD8T4y`%9;_&NG(gQPkX{!6Hu!8v?Dnp0Z7~26F#FZmn@~jY^;Wtro9s z@@>kseP^|OiYZwvER@9yNP{ivk`!qs%D`Uz7;4vrplwH%#740Zx4VV|Hu!9P=P&F# Z?wK?9o_o%j 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 aCE7%j#&vJgB260AU8F_jQ~Xny_x}T)R9uq) diff --git a/tests/integration/__pycache__/Create_Project.cpython-312.pyc b/tests/integration/__pycache__/Create_Project.cpython-312.pyc deleted file mode 100644 index 55f02a2c652a9be03a7f44de712732149a2525fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12836 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 VXNMhssG}5h+M1%NT0>LY>L0~RWaj_? diff --git a/tests/integration/cred.py b/tests/integration/cred.py index 6b5b428..27f7313 100644 --- a/tests/integration/cred.py +++ b/tests/integration/cred.py @@ -1,6 +1,6 @@ -API_KEY = '66ee9d.85405d401b9c58481b9e2b73f5' -API_SECRET = 'cf2f638ae901c8fd5611d097846ce6c806451818573ca6e92a412ff188df1b05' +API_KEY = "" +API_SECRET = "" -CLIENT_ID = "14051" -PROJECT_ID = None -EMAIL_ID = "yashsuman15@gmail.com" \ No newline at end of file +CLIENT_ID = "" +PROJECT_ID = "" +EMAIL_ID = "" \ No newline at end of file From 90572d8e9fd07967df87f630c9a339610f5abe7c Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:14:55 +0530 Subject: [PATCH 03/23] - created SDKPython subdir in labellerr - added utils/client_utils - added video_sampling/pyscene_detect.py --- .../services/video_sampling/pyscene_detect.py | 135 +++++++++ .../services/video_sampling/requirements.txt | 3 + labellerr/Python_SDK/utils/client_utils.py | 281 ++++++++++++++++++ 3 files changed, 419 insertions(+) create mode 100644 labellerr/Python_SDK/services/video_sampling/pyscene_detect.py create mode 100644 labellerr/Python_SDK/services/video_sampling/requirements.txt create mode 100644 labellerr/Python_SDK/utils/client_utils.py diff --git a/labellerr/Python_SDK/services/video_sampling/pyscene_detect.py b/labellerr/Python_SDK/services/video_sampling/pyscene_detect.py new file mode 100644 index 0000000..406b584 --- /dev/null +++ b/labellerr/Python_SDK/services/video_sampling/pyscene_detect.py @@ -0,0 +1,135 @@ +import os +from scenedetect import detect, AdaptiveDetector +from PIL import Image +import cv2 +from dataclasses import dataclass, asdict +from typing import List +import json + + +@dataclass +class SceneFrame: + """Represents a detected scene with its extracted frame.""" + frame_path: str + frame_no: int + + +@dataclass +class DetectionResult: + """Contains all detection results for a video.""" + file_id: str + output_folder: str + total_frames: int + selected_frames: List[SceneFrame] + + +class PySceneDetect: + """Scene detection and frame extraction for videos.""" + + def __init__(self, video_path: str, file_id: str): + """ + Initialize the scene detector. + + Args: + video_path: Path to the video file + file_id: Unique identifier for the video (used as output folder name) + """ + self.video_path = video_path + self.file_id = file_id + self.output_folder = file_id + + def detect_and_extract(self) -> DetectionResult: + """ + Detect scenes and extract representative frames. + + Returns: + DetectionResult containing file_id, output_folder, total_frames, and list of SceneFrame objects + """ + # Detect scene transitions + scenes = detect(self.video_path, AdaptiveDetector()) + + # Create output folder + os.makedirs(self.output_folder, exist_ok=True) + + # Open video for frame extraction + video = cv2.VideoCapture(self.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 + frame_filename = f"{frame_no}.jpg" + frame_path = os.path.join(self.output_folder, frame_filename) + frame.save(frame_path) + + # Create SceneFrame object + scene_frame = SceneFrame( + frame_path=frame_path, + frame_no=frame_no + ) + scene_frames.append(scene_frame) + + video.release() + + # Create result + result = DetectionResult( + file_id=self.file_id, + output_folder=self.output_folder, + total_frames=total_frames, + selected_frames=scene_frames + ) + + # Save JSON mapping + self._save_json_mapping(result) + + return result + + def _get_frame(self, video: cv2.VideoCapture, frame_no: int) -> Image.Image: + """ + Extract a specific frame from video. + + Args: + video: OpenCV video capture object + frame_no: Frame number to extract + + Returns: + PIL Image of the frame + """ + video.set(cv2.CAP_PROP_POS_FRAMES, frame_no) + _, frame = video.read() + return Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + + def _save_json_mapping(self, result: DetectionResult) -> None: + """ + Save JSON mapping of file_id to extracted scenes. + + Args: + result: DetectionResult object + """ + mapping = { + "file_id": result.file_id, + "output_folder": result.output_folder, + "total_frames": result.total_frames, + "total_selected_frames": len(result.selected_frames), + "selected_frames": [asdict(frame) for frame in result.selected_frames] + } + + json_path = os.path.join(self.output_folder, f"{self.file_id}_mapping.json") + with open(json_path, 'w', encoding='utf-8') as f: + json.dump(mapping, f, indent=2, ensure_ascii=False) + + print(f"JSON mapping saved to: {json_path}") + + +if __name__ == "__main__": + video_path = r"D:\professional\LABELLERR\Task\Python_SDK\services\video_sampling\video.mp4" + result = PySceneDetect(video_path, "video_001").detect_and_extract() \ No newline at end of file diff --git a/labellerr/Python_SDK/services/video_sampling/requirements.txt b/labellerr/Python_SDK/services/video_sampling/requirements.txt new file mode 100644 index 0000000..43f10f6 --- /dev/null +++ b/labellerr/Python_SDK/services/video_sampling/requirements.txt @@ -0,0 +1,3 @@ +scenedetect +opencv-python +matplotlib \ No newline at end of file diff --git a/labellerr/Python_SDK/utils/client_utils.py b/labellerr/Python_SDK/utils/client_utils.py new file mode 100644 index 0000000..58d3c76 --- /dev/null +++ b/labellerr/Python_SDK/utils/client_utils.py @@ -0,0 +1,281 @@ +from ...client import LabellerrClient +from ...exceptions import LabellerrError +from ... import constants +import uuid +import os +import subprocess +import requests +import pprint + + +# https://api.labellerr.com/data/file_data?file_id=c44f38f6-0186-436f-8c2d-ffb50a539c76&include_answers=false&project_id=gabrila_artificial_duck_74237&uuid=1d4c9b58-c6a4-4ca8-9583-b6b6cd25ef12 + + +class FileMetadataService: + def __init__(self, client: LabellerrClient): + self.client = client + + def get_file_metadata(self, client_id: str, file_id: str, project_id: str, include_answers: bool = False): + """ + Retrieve file metadata from Labellerr API. + """ + try: + unique_id = str(uuid.uuid4()) + + # Build query parameters - include client_id here + params = { + 'file_id': file_id, + 'include_answers': str(include_answers).lower(), + 'project_id': project_id, + 'uuid': unique_id, + 'client_id': client_id + } + + url = f"{constants.BASE_URL}/data/file_data" + + + headers = self.client._build_headers( + client_id=client_id, + extra_headers={ + "Content-Type": "application/json", + "Origin": constants.ALLOWED_ORIGINS + } + ) + + # Make request using client's session if available + response = self.client._make_request("GET", url, headers=headers, params=params) + + # Use client's response handler + return self.client._handle_response(response, request_id=unique_id) + + except Exception as e: + raise LabellerrError(f"Failed to fetch file metadata: {str(e)}") + + def get_video_frames(self, client_id: str, file_id: str, project_id: str, dataset_id: str, frame_start: int = 0, frame_end: int = None): + """ + Retrieve video frames data from Labellerr API. + + :param client_id: Client ID + :param file_id: Unique file identifier in Labellerr + :param project_id: The project ID to which the file belongs + :param dataset_id: The dataset ID containing the video file + :param frame_start: Starting frame index (default: 0) + :param frame_end: Ending frame index (if None, retrieves all frames from frame_start) + :return: Dictionary containing video frames data + """ + try: + unique_id = str(uuid.uuid4()) + url = f"{constants.BASE_URL}/data/video_frames" + + # Build query parameters + params = { + 'dataset_id': dataset_id, + 'file_id': file_id, + 'frame_start': frame_start, + 'project_id': project_id, + 'uuid': unique_id, + 'client_id': client_id + } + + # Add frame_end only if specified + if frame_end is not None: + params['frame_end'] = frame_end + + # Build headers using client's build_headers method + headers = self.client._build_headers( + client_id=client_id, + extra_headers={ + "Content-Type": "application/json", + "Origin": constants.ALLOWED_ORIGINS + } + ) + + # Make request using client's session + response = self.client._make_request("GET", url, headers=headers, params=params) + + # Use client's response handler + return self.client._handle_response(response, request_id=unique_id) + + except Exception as e: + raise LabellerrError(f"Failed to fetch video frames data: {str(e)}") + + def download_video_frames(self, frames_data: dict, output_folder: str = None, file_id: str = None): + """ + Download video frames from URLs to a local folder. + + :param frames_data: Dictionary with frame numbers as keys and URLs as values + :param output_folder: Base folder path where frames will be saved (default: current directory) + :param file_id: File ID to use as folder name. If None, uses 'frames' as folder name + :return: Dictionary with download statistics + """ + try: + # Determine folder name + if file_id: + folder_name = file_id + else: + folder_name = "frames" + + # Set base output folder + if output_folder: + save_path = os.path.join(output_folder, folder_name) + else: + save_path = folder_name + + # Create directory if it doesn't exist + os.makedirs(save_path, exist_ok=True) + + success_count = 0 + failed_frames = [] + + print(f"Downloading {len(frames_data)} frames to: {save_path}") + + for frame_number, frame_url in frames_data.items(): + try: + # Create filename with frame number + filename = f"{frame_number}.jpg" + filepath = os.path.join(save_path, filename) + + # Download the frame + response = requests.get(frame_url, timeout=30) + + if response.status_code == 200: + with open(filepath, 'wb') as f: + f.write(response.content) + success_count += 1 + print(f"Downloaded: {filename}") + else: + failed_frames.append({ + 'frame': frame_number, + 'status': response.status_code + }) + print(f"Failed to download frame {frame_number}: Status {response.status_code}") + + except Exception as e: + failed_frames.append({ + 'frame': frame_number, + 'error': str(e) + }) + print(f"Error downloading frame {frame_number}: {str(e)}") + + result = { + 'total_frames': len(frames_data), + 'successful_downloads': success_count, + 'failed_downloads': len(failed_frames), + 'save_path': save_path, + 'failed_frames': failed_frames + } + + print(f"\nDownload complete: {success_count}/{len(frames_data)} frames downloaded successfully") + + return result + + except Exception as e: + raise LabellerrError(f"Failed to download video frames: {str(e)}") + + + +class JoinVideoFrames: + def __init__(self, frames_folder, output_file="output.mp4", framerate=30): + """ + Initialize the JoinFrames class. + + :param frames_folder: Path to folder containing sequential frames (e.g., 1.jpg, 2.jpg). + :param output_file: Name of the output video file. + :param framerate: Desired video framerate (default: 30 fps). + """ + self.frames_folder = frames_folder + self.output_file = output_file + self.framerate = framerate + + def join(self, pattern="%d.jpg"): + """ + Join frames into a video using ffmpeg. + + :param pattern: Pattern for sequential frames inside frames_folder + (default: frame%03d.jpg → frame001.jpg, frame002.jpg, ...). + """ + input_pattern = os.path.join(self.frames_folder, pattern) + + # FFmpeg command + command = [ + "ffmpeg", + "-y", # Overwrite output file if exists + "-framerate", str(self.framerate), + "-i", input_pattern, + "-c:v", "libx264", + "-pix_fmt", "yuv420p", + self.output_file + ] + + try: + print("Running command:", " ".join(command)) + subprocess.run(command, check=True) + print(f"Video saved as {self.output_file}") + except subprocess.CalledProcessError as e: + print("Error while joining frames:", e) + + +if __name__ == "__main__": + + api_key = "" + api_secret = "" + client_id = "14078" + dataset_id = "16257fd6-b91b-4d00-a680-9ece9f3f241c" + project_id = "gabrila_artificial_duck_74237" + file_id = "c44f38f6-0186-436f-8c2d-ffb50a539c76" + + client = LabellerrClient(api_key=api_key, api_secret=api_secret) + + file_service = FileMetadataService(client) + + + # try: + # metadata = file_service.get_file_metadata( + # client_id, + # file_id, + # project_id, + # include_answers=False) + # # pprint.pprint(metadata) + + # total_frames = metadata['file_metadata']['total_frames'] + # print(total_frames) + # except LabellerrError as e: + # print("Error:", e) + + # try: + # frames_data = file_service.get_video_frames( + # client_id=client_id, + # file_id=file_id, + # project_id=project_id, + # dataset_id=dataset_id, + # frame_start=0, + # frame_end=total_frames + # ) + + # print(len(frames_data.keys())) + # pprint.pprint(frames_data) + # except LabellerrError as e: + # print("Error:", e) + + # try: + # download_result = file_service.download_video_frames( + # frames_data=frames_data, + # output_folder="labellerr\download", # Optional: specify base folder + # file_id=file_id # Will create folder named with file_id + # ) + # # pprint.pprint(frames_data) + # except LabellerrError as e: + # print("Error:", e) + + # frames = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\download\c44f38f6-0186-436f-8c2d-ffb50a539c76" + # try: + # JoinVideoFrames(frames, + # output_file="labellerr/download/resultjoinvideo.mp4", + # framerate=30).join() + + # except LabellerrError as e: + # print("Error:", e) + + + + \ No newline at end of file From 9178b37df861e443071f3bb84ec4297a71eacf3a Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:14:05 +0530 Subject: [PATCH 04/23] added ffmpeg sampling frames method --- .../services/video_sampling/ffmpeg.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 labellerr/Python_SDK/services/video_sampling/ffmpeg.py diff --git a/labellerr/Python_SDK/services/video_sampling/ffmpeg.py b/labellerr/Python_SDK/services/video_sampling/ffmpeg.py new file mode 100644 index 0000000..0016a6a --- /dev/null +++ b/labellerr/Python_SDK/services/video_sampling/ffmpeg.py @@ -0,0 +1,34 @@ +import subprocess +import os + +class FFMPEG: + def __init__(self, video_path: str, file_id: str): + self.video_path = video_path + self.file_id = file_id + self.save_folder = file_id + + def detect_and_extract(self): + """Extract keyframes from video and save to file_id folder""" + os.makedirs(self.save_folder, exist_ok=True) + + output_pattern = os.path.join(self.save_folder, "%d.jpg") + + command = [ + "ffmpeg", + "-i", self.video_path, + "-vf", "select='eq(pict_type,PICT_TYPE_I)',showinfo", + "-vsync", "vfr", + "-frame_pts", "1", + output_pattern + ] + + try: + subprocess.run(command, check=True) + print(f"Keyframes extracted to {self.save_folder}") + except subprocess.CalledProcessError as e: + print(f"Error extracting keyframes: {e}") + + +if __name__ == "__main__": + video_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\Python_SDK\services\video_sampling\video.mp4" + result = FFMPEG(video_path, "FFMPEG_sample_video_011").detect_and_extract() \ No newline at end of file From 03dfa0a4182a130cde4012658f869d368b107597 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:02:27 +0530 Subject: [PATCH 05/23] added SSIM based video keyframe sampling method --- .../services/video_sampling/ssim.py | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 labellerr/Python_SDK/services/video_sampling/ssim.py diff --git a/labellerr/Python_SDK/services/video_sampling/ssim.py b/labellerr/Python_SDK/services/video_sampling/ssim.py new file mode 100644 index 0000000..5da532c --- /dev/null +++ b/labellerr/Python_SDK/services/video_sampling/ssim.py @@ -0,0 +1,208 @@ +import os +import cv2 +import numpy as np +from PIL import Image +from dataclasses import dataclass, asdict +from typing import List +import json +from skimage.metrics import structural_similarity as ssim + + +@dataclass +class SceneFrame: + """Represents a detected scene with its extracted frame.""" + frame_path: str + frame_no: int + ssim_score: float + + +@dataclass +class DetectionResult: + """Contains all detection results for a video.""" + file_id: str + output_folder: str + total_frames: int + selected_frames: List[SceneFrame] + + +class SSIMSceneDetect: + """SSIM-based scene detection and frame extraction for videos.""" + + def __init__(self, video_path: str, file_id: str, threshold: float = 0.6, resize_dim: tuple = (320, 240)): + """ + Initialize the SSIM scene detector. + + Args: + video_path: Path to the video file + file_id: Unique identifier for the video (used as output folder name) + threshold: SSIM threshold for scene detection (lower = stricter, default: 0.6) + resize_dim: Dimensions to resize frames for SSIM calculation (default: (320, 240)) + """ + self.video_path = video_path + self.file_id = file_id + self.output_folder = file_id + self.threshold = threshold + self.resize_dim = resize_dim + + def detect_and_extract(self) -> DetectionResult: + """ + Detect scenes using SSIM and extract representative frames. + + Returns: + DetectionResult containing file_id, output_folder, total_frames, and list of SceneFrame objects + """ + # Create output folder + os.makedirs(self.output_folder, exist_ok=True) + + # Open video for processing + video = cv2.VideoCapture(self.video_path) + + if not video.isOpened(): + raise ValueError(f"Cannot open video: {self.video_path}") + + # Get total frames in video + total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + + print(f"Processing video: {self.video_path}") + print(f"Total frames: {total_frames}") + print(f"SSIM threshold: {self.threshold}") + + # Read first frame + success, prev_frame = video.read() + if not success: + video.release() + raise ValueError(f"Cannot read first frame from: {self.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) + 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) + + # If SSIM is below threshold, it's a scene change + if ssim_score < self.threshold: + self._save_frame(curr_frame, frame_count, ssim_score, scene_frames) + 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: {self.threshold})") + + video.release() + + print(f"\nExtracted {len(scene_frames)} keyframes from {frame_count + 1} frames.") + + # Create result + result = DetectionResult( + file_id=self.file_id, + output_folder=self.output_folder, + total_frames=total_frames, + selected_frames=scene_frames + ) + + # Save JSON mapping + self._save_json_mapping(result) + + return result + + def _calculate_ssim(self, frame1: np.ndarray, frame2: np.ndarray) -> float: + """ + Calculate SSIM score between two frames. + + Args: + frame1: First frame (BGR format) + frame2: Second frame (BGR format) + + Returns: + SSIM score (0-1, where 1 is identical) + """ + # Resize frames for faster computation + gray1 = cv2.cvtColor(cv2.resize(frame1, self.resize_dim), cv2.COLOR_BGR2GRAY) + gray2 = cv2.cvtColor(cv2.resize(frame2, self.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]) -> 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 + """ + # 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 + frame_filename = f"{frame_no}.jpg" + frame_path = os.path.join(self.output_folder, frame_filename) + pil_image.save(frame_path) + + # Create SceneFrame object + scene_frame = SceneFrame( + frame_path=frame_path, + frame_no=frame_no, + ssim_score=ssim_score + ) + scene_frames.append(scene_frame) + + def _save_json_mapping(self, result: DetectionResult) -> None: + """ + Save JSON mapping of file_id to extracted scenes. + + Args: + result: DetectionResult object + """ + mapping = { + "file_id": result.file_id, + "output_folder": result.output_folder, + "total_frames": result.total_frames, + "total_selected_frames": len(result.selected_frames), + "threshold": self.threshold, + "resize_dim": self.resize_dim, + "selected_frames": [asdict(frame) for frame in result.selected_frames] + } + + json_path = os.path.join(self.output_folder, f"{self.file_id}_mapping.json") + with open(json_path, 'w', encoding='utf-8') as f: + json.dump(mapping, 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\SDKPython\labellerr\Python_SDK\services\video_sampling\video.mp4" + +# # Create detector with custom parameters +# detector = SSIMSceneDetect( +# video_path=video_path, +# file_id="video_001", +# threshold=0.6, # Lower value = more sensitive to changes +# resize_dim=(320, 240) +# ) + +# # Detect and extract frames +# result = detector.detect_and_extract() + +# print(f"\nDetection complete!") +# print(f"Total frames extracted: {len(result.selected_frames)}") +# print(f"Output folder: {result.output_folder}") \ No newline at end of file From e6e6bba3069abe961fee0b4f1dc236a173add0fd Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:39:54 +0530 Subject: [PATCH 06/23] - added gemini method for video sampling - add videointelligence module in requirements --- .../services/video_sampling/gemini.py | 254 ++++++++++++++++++ .../services/video_sampling/requirements.txt | 2 +- 2 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 labellerr/Python_SDK/services/video_sampling/gemini.py diff --git a/labellerr/Python_SDK/services/video_sampling/gemini.py b/labellerr/Python_SDK/services/video_sampling/gemini.py new file mode 100644 index 0000000..350d3f4 --- /dev/null +++ b/labellerr/Python_SDK/services/video_sampling/gemini.py @@ -0,0 +1,254 @@ +import os +import cv2 +from PIL import Image +from dataclasses import dataclass, asdict +from typing import List, Optional +import json +from google.cloud import videointelligence + + +@dataclass +class SceneFrame: + """Represents a detected scene with its extracted frame.""" + frame_path: str + frame_no: int + start_time_offset: float + end_time_offset: float + + +@dataclass +class DetectionResult: + """Contains all detection results for a video.""" + file_id: str + output_folder: str + total_frames: int + selected_frames: List[SceneFrame] + + +class GeminiSceneDetect: + """Google Cloud Video Intelligence API scene detection and frame extraction.""" + + def __init__(self, video_path: str, file_id: str, gcs_uri: Optional[str] = None, credentials_path: Optional[str] = None): + """ + Initialize the Google Cloud Video Intelligence scene detector. + + Args: + video_path: Path to the local video file (for frame extraction) + file_id: Unique identifier for the video (used as output folder name) + gcs_uri: Google Cloud Storage URI (gs://bucket/video.mp4) for API processing. + If None, the video will be uploaded as bytes (limited to 10MB) + credentials_path: Path to service account JSON key file. + If None, uses GOOGLE_APPLICATION_CREDENTIALS environment variable + """ + self.video_path = video_path + self.file_id = file_id + self.output_folder = file_id + self.gcs_uri = gcs_uri + + # Set credentials if provided + if credentials_path: + os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = credentials_path + + # Initialize Video Intelligence client + self.client = videointelligence.VideoIntelligenceServiceClient() + + def detect_and_extract(self) -> DetectionResult: + """ + Detect scenes using Google Cloud Video Intelligence API and extract representative frames. + + Returns: + DetectionResult containing file_id, output_folder, total_frames, and list of SceneFrame objects + """ + print(f"Processing video: {self.video_path}") + print("Detecting shot changes using Google Cloud Video Intelligence API...") + + # Detect shots using Video Intelligence API + shots = self._detect_shots() + + if not shots: + raise ValueError("No shot changes detected in the video") + + print(f"Detected {len(shots)} shots") + + # Create output folder + os.makedirs(self.output_folder, exist_ok=True) + + # Open video for frame extraction + video = cv2.VideoCapture(self.video_path) + + if not video.isOpened(): + raise ValueError(f"Cannot open video: {self.video_path}") + + # Get video properties + total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + fps = video.get(cv2.CAP_PROP_FPS) + + print(f"Total frames: {total_frames}") + print(f"FPS: {fps}") + + # Extract and save frames + scene_frames = [] + + for idx, shot in enumerate(shots): + # Calculate middle frame number from shot timestamps + start_time = shot.start_time_offset.total_seconds() + end_time = shot.end_time_offset.total_seconds() + middle_time = (start_time + end_time) / 2 + frame_no = int(middle_time * fps) + + # Ensure frame number is within bounds + frame_no = max(0, min(frame_no, total_frames - 1)) + + # Extract frame + frame = self._get_frame(video, frame_no) + + if frame is None: + print(f"Warning: Could not extract frame {frame_no} for shot {idx}") + continue + + # Save frame with frame number as filename + frame_filename = f"{frame_no}.jpg" + frame_path = os.path.join(self.output_folder, frame_filename) + frame.save(frame_path) + + # Create SceneFrame object + scene_frame = SceneFrame( + frame_path=frame_path, + frame_no=frame_no, + start_time_offset=start_time, + end_time_offset=end_time + ) + scene_frames.append(scene_frame) + + print(f"Saved keyframe {idx} at frame {frame_no} (time: {middle_time:.2f}s)") + + video.release() + + print(f"\nExtracted {len(scene_frames)} keyframes from {total_frames} frames.") + + # Create result + result = DetectionResult( + file_id=self.file_id, + output_folder=self.output_folder, + total_frames=total_frames, + selected_frames=scene_frames + ) + + # Save JSON mapping + self._save_json_mapping(result) + + return result + + def _detect_shots(self) -> List: + """ + Detect shot changes using Google Cloud Video Intelligence API. + + Returns: + List of shot annotation objects + """ + features = [videointelligence.Feature.SHOT_CHANGE_DETECTION] + + if self.gcs_uri: + # Use GCS URI for large videos + print(f"Analyzing video from GCS: {self.gcs_uri}") + operation = self.client.annotate_video( + request={ + "input_uri": self.gcs_uri, + "features": features + } + ) + else: + # Read video file and send as bytes (limited to 10MB) + with open(self.video_path, "rb") as video_file: + input_content = video_file.read() + + print(f"Analyzing video from local file (size: {len(input_content) / (1024*1024):.2f} MB)") + + if len(input_content) > 10 * 1024 * 1024: # 10MB limit + raise ValueError( + "Video file is larger than 10MB. Please upload to Google Cloud Storage " + "and provide gcs_uri parameter (gs://bucket/video.mp4)" + ) + + operation = self.client.annotate_video( + request={ + "input_content": input_content, + "features": features + } + ) + + print("Waiting for operation to complete...") + result = operation.result(timeout=600) # 10 minute timeout + + # Get shot annotations + annotation_result = result.annotation_results[0] + shots = annotation_result.shot_annotations + + return shots + + def _get_frame(self, video: cv2.VideoCapture, frame_no: int) -> Optional[Image.Image]: + """ + Extract a specific frame from video. + + Args: + video: OpenCV video capture object + frame_no: Frame number to extract + + Returns: + PIL Image of the frame, or None if extraction fails + """ + video.set(cv2.CAP_PROP_POS_FRAMES, frame_no) + success, frame = video.read() + + if not success: + return None + + return Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + + def _save_json_mapping(self, result: DetectionResult) -> None: + """ + Save JSON mapping of file_id to extracted scenes. + + Args: + result: DetectionResult object + """ + mapping = { + "file_id": result.file_id, + "output_folder": result.output_folder, + "total_frames": result.total_frames, + "total_selected_frames": len(result.selected_frames), + "detection_method": "Google Cloud Video Intelligence API - Shot Change Detection", + "gcs_uri": self.gcs_uri if self.gcs_uri else "local file", + "selected_frames": [asdict(frame) for frame in result.selected_frames] + } + + json_path = os.path.join(self.output_folder, f"{self.file_id}_mapping.json") + with open(json_path, 'w', encoding='utf-8') as f: + json.dump(mapping, f, indent=2, ensure_ascii=False) + + print(f"JSON mapping saved to: {json_path}") + + +# if __name__ == "__main__": +# # Example usage - Local video file (must be < 10MB) +# video_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\Python_SDK\services\video_sampling\video2.mp4" +# cred_json_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\Python_SDK\services\video_sampling\yash-suman-prod.json" + +# # ---------------------------------------------- +# # Option 1: Process local video file (< 10MB) +# # ---------------------------------------------- + +# detector = GeminiSceneDetect( +# video_path=video_path, +# file_id="video_001", +# credentials_path=cred_json_path # Uses GOOGLE_APPLICATION_CREDENTIALS env var +# ) + +# # Detect and extract frames +# try: +# result = detector.detect_and_extract() +# print(f"\nDetection complete!") +# print(f"Total frames extracted: {len(result.selected_frames)}") +# print(f"Output folder: {result.output_folder}") +# except Exception as e: +# print(f"Error: {e}") diff --git a/labellerr/Python_SDK/services/video_sampling/requirements.txt b/labellerr/Python_SDK/services/video_sampling/requirements.txt index 43f10f6..c4225b1 100644 --- a/labellerr/Python_SDK/services/video_sampling/requirements.txt +++ b/labellerr/Python_SDK/services/video_sampling/requirements.txt @@ -1,3 +1,3 @@ scenedetect opencv-python -matplotlib \ No newline at end of file +google-cloud-videointelligence \ No newline at end of file From 6cc7e52708fbb4c45e64eb65262a2211a9eac3c6 Mon Sep 17 00:00:00 2001 From: Ximi Hoque Date: Sat, 4 Oct 2025 15:56:47 +0530 Subject: [PATCH 07/23] Review comments --- .../services/video_sampling/gemini.py | 42 ++++++++++--------- labellerr/Python_SDK/utils/client_utils.py | 12 +++++- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/labellerr/Python_SDK/services/video_sampling/gemini.py b/labellerr/Python_SDK/services/video_sampling/gemini.py index 350d3f4..d26afc3 100644 --- a/labellerr/Python_SDK/services/video_sampling/gemini.py +++ b/labellerr/Python_SDK/services/video_sampling/gemini.py @@ -229,26 +229,28 @@ def _save_json_mapping(self, result: DetectionResult) -> None: print(f"JSON mapping saved to: {json_path}") -# if __name__ == "__main__": -# # Example usage - Local video file (must be < 10MB) -# video_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\Python_SDK\services\video_sampling\video2.mp4" -# cred_json_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\Python_SDK\services\video_sampling\yash-suman-prod.json" +if __name__ == "__main__": + # Example usage - Local video file (must be < 10MB) + video_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\Python_SDK\services\video_sampling\video2.mp4" + cred_json_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\Python_SDK\services\video_sampling\yash-suman-prod.json" -# # ---------------------------------------------- -# # Option 1: Process local video file (< 10MB) -# # ---------------------------------------------- + # ---------------------------------------------- + # Option 1: Process local video file (< 10MB) + # ---------------------------------------------- -# detector = GeminiSceneDetect( -# video_path=video_path, -# file_id="video_001", -# credentials_path=cred_json_path # Uses GOOGLE_APPLICATION_CREDENTIALS env var -# ) + detector = GeminiSceneDetect( + credentials_path=cred_json_path # Uses GOOGLE_APPLICATION_CREDENTIALS env var + ) + labellerr_file = LabellerrFile( + file_id="video_001" + ) + detector.detect_and_extract() -# # Detect and extract frames -# try: -# result = detector.detect_and_extract() -# print(f"\nDetection complete!") -# print(f"Total frames extracted: {len(result.selected_frames)}") -# print(f"Output folder: {result.output_folder}") -# except Exception as e: -# print(f"Error: {e}") + # Detect and extract frames + try: + result = detector.detect_and_extract() + print(f"\nDetection complete!") + print(f"Total frames extracted: {len(result.selected_frames)}") + print(f"Output folder: {result.output_folder}") + except Exception as e: + print(f"Error: {e}") diff --git a/labellerr/Python_SDK/utils/client_utils.py b/labellerr/Python_SDK/utils/client_utils.py index 58d3c76..004337e 100644 --- a/labellerr/Python_SDK/utils/client_utils.py +++ b/labellerr/Python_SDK/utils/client_utils.py @@ -278,4 +278,14 @@ def join(self, pattern="%d.jpg"): - \ No newline at end of file +class ImageFileService(FileMetadataService): + pass + +class VideoFileService(FileMetadataService): + + def download_video_frames(self, client_id: str, file_id: str, project_id: str): + pass + def get_video_frames(self, client_id: str, file_id: str, project_id: str): + pass + def create_video_from_frames(self, client_id: str, file_id: str, project_id: str): + pass \ No newline at end of file From 18fb31b1504960afe174e76ded002184cbe50cbf Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:42:55 +0530 Subject: [PATCH 08/23] fixed scripts --- .gitignore | 2 + labellerr/Python_SDK/.gitignore | 4 + .../services/video_sampling/.gitignore | 3 + labellerr/Python_SDK/utils/client_utils.py | 126 ++++++++---------- .../__pycache__/__init__.cpython-310.pyc | Bin 182 -> 643 bytes labellerr/__pycache__/client.cpython-310.pyc | Bin 31934 -> 34068 bytes .../__pycache__/exceptions.cpython-310.pyc | Bin 393 -> 393 bytes labellerr/base/singleton.py | 21 +-- 8 files changed, 79 insertions(+), 77 deletions(-) create mode 100644 labellerr/Python_SDK/.gitignore create mode 100644 labellerr/Python_SDK/services/video_sampling/.gitignore diff --git a/.gitignore b/.gitignore index 74b2fb6..5db4a82 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,5 @@ site/ # Test data tests/test_data +download +labellerr/__pycache__/ diff --git a/labellerr/Python_SDK/.gitignore b/labellerr/Python_SDK/.gitignore new file mode 100644 index 0000000..590db49 --- /dev/null +++ b/labellerr/Python_SDK/.gitignore @@ -0,0 +1,4 @@ +video_001 +videoframes +joinvideo.mp4 +__pycache__ \ No newline at end of file diff --git a/labellerr/Python_SDK/services/video_sampling/.gitignore b/labellerr/Python_SDK/services/video_sampling/.gitignore new file mode 100644 index 0000000..1f2f0d9 --- /dev/null +++ b/labellerr/Python_SDK/services/video_sampling/.gitignore @@ -0,0 +1,3 @@ +video.mp4 +video2.mp4 +yash-suman-prod.json \ No newline at end of file diff --git a/labellerr/Python_SDK/utils/client_utils.py b/labellerr/Python_SDK/utils/client_utils.py index 58d3c76..0f902b9 100644 --- a/labellerr/Python_SDK/utils/client_utils.py +++ b/labellerr/Python_SDK/utils/client_utils.py @@ -1,6 +1,7 @@ from ...client import LabellerrClient from ...exceptions import LabellerrError from ... import constants +from ...base.singleton import Singleton # Import your Singleton class import uuid import os import subprocess @@ -8,11 +9,23 @@ import pprint -# https://api.labellerr.com/data/file_data?file_id=c44f38f6-0186-436f-8c2d-ffb50a539c76&include_answers=false&project_id=gabrila_artificial_duck_74237&uuid=1d4c9b58-c6a4-4ca8-9583-b6b6cd25ef12 - - -class FileMetadataService: - def __init__(self, client: LabellerrClient): +class FileMetadataService(Singleton): + + def __init__(self, client: LabellerrClient = None): + # Prevent re-initialization of singleton + if hasattr(self, '_initialized'): + return + + if client is None: + raise ValueError("Client must be provided on first initialization") + + self.client = client + self._initialized = True + + def set_client(self, client: LabellerrClient): + """ + Update the client instance (useful for reconfiguration). + """ self.client = client def get_file_metadata(self, client_id: str, file_id: str, project_id: str, include_answers: bool = False): @@ -33,7 +46,6 @@ def get_file_metadata(self, client_id: str, file_id: str, project_id: str, inclu url = f"{constants.BASE_URL}/data/file_data" - headers = self.client._build_headers( client_id=client_id, extra_headers={ @@ -173,52 +185,79 @@ def download_video_frames(self, frames_data: dict, output_folder: str = None, fi raise LabellerrError(f"Failed to download video frames: {str(e)}") - -class JoinVideoFrames: - def __init__(self, frames_folder, output_file="output.mp4", framerate=30): +class JoinVideoFrames(Singleton): + + def __init__(self, frames_folder=None, output_file="output.mp4", framerate=30): """ - Initialize the JoinFrames class. + Initialize the JoinVideoFrames class. :param frames_folder: Path to folder containing sequential frames (e.g., 1.jpg, 2.jpg). :param output_file: Name of the output video file. :param framerate: Desired video framerate (default: 30 fps). """ + # Prevent re-initialization of singleton + if hasattr(self, '_initialized'): + return + self.frames_folder = frames_folder self.output_file = output_file self.framerate = framerate + self._initialized = True + + def configure(self, frames_folder=None, output_file=None, framerate=None): + """ + Reconfigure the singleton instance parameters. + """ + if frames_folder is not None: + self.frames_folder = frames_folder + if output_file is not None: + self.output_file = output_file + if framerate is not None: + self.framerate = framerate - def join(self, pattern="%d.jpg"): + def join(self, pattern="%d.jpg", frames_folder=None, output_file=None, framerate=None): """ Join frames into a video using ffmpeg. :param pattern: Pattern for sequential frames inside frames_folder - (default: frame%03d.jpg → frame001.jpg, frame002.jpg, ...). + (default: %d.jpg → 1.jpg, 2.jpg, ...). + :param frames_folder: Override frames folder for this operation + :param output_file: Override output file for this operation + :param framerate: Override framerate for this operation """ - input_pattern = os.path.join(self.frames_folder, pattern) + # Use provided parameters or fall back to instance attributes + folder = frames_folder or self.frames_folder + output = output_file or self.output_file + fps = framerate or self.framerate + + if folder is None: + raise ValueError("frames_folder must be provided either during initialization or when calling join()") + + input_pattern = os.path.join(folder, pattern) # FFmpeg command command = [ "ffmpeg", "-y", # Overwrite output file if exists - "-framerate", str(self.framerate), + "-framerate", str(fps), "-i", input_pattern, "-c:v", "libx264", "-pix_fmt", "yuv420p", - self.output_file + output ] try: print("Running command:", " ".join(command)) subprocess.run(command, check=True) - print(f"Video saved as {self.output_file}") + print(f"Video saved as {output}") except subprocess.CalledProcessError as e: print("Error while joining frames:", e) if __name__ == "__main__": - api_key = "" - api_secret = "" + api_key = "66f4d8.9f402742f58a89568f5bcc0f86" + api_secret = "1e2478b930d4a842a526beb585e60d2a9ee6a6f1e3aa89cb3c8ead751f418215" client_id = "14078" dataset_id = "16257fd6-b91b-4d00-a680-9ece9f3f241c" project_id = "gabrila_artificial_duck_74237" @@ -226,56 +265,9 @@ def join(self, pattern="%d.jpg"): client = LabellerrClient(api_key=api_key, api_secret=api_secret) + # First initialization - creates the singleton instance file_service = FileMetadataService(client) - - # try: - # metadata = file_service.get_file_metadata( - # client_id, - # file_id, - # project_id, - # include_answers=False) - # # pprint.pprint(metadata) - - # total_frames = metadata['file_metadata']['total_frames'] - # print(total_frames) - # except LabellerrError as e: - # print("Error:", e) - - # try: - # frames_data = file_service.get_video_frames( - # client_id=client_id, - # file_id=file_id, - # project_id=project_id, - # dataset_id=dataset_id, - # frame_start=0, - # frame_end=total_frames - # ) - - # print(len(frames_data.keys())) - # pprint.pprint(frames_data) - # except LabellerrError as e: - # print("Error:", e) - - # try: - # download_result = file_service.download_video_frames( - # frames_data=frames_data, - # output_folder="labellerr\download", # Optional: specify base folder - # file_id=file_id # Will create folder named with file_id - # ) - # # pprint.pprint(frames_data) - # except LabellerrError as e: - # print("Error:", e) - - # frames = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\download\c44f38f6-0186-436f-8c2d-ffb50a539c76" - # try: - # JoinVideoFrames(frames, - # output_file="labellerr/download/resultjoinvideo.mp4", - # framerate=30).join() - - # except LabellerrError as e: - # print("Error:", e) - - + print(file_service.get_file_metadata(client_id, file_id, project_id)) \ No newline at end of file diff --git a/labellerr/__pycache__/__init__.cpython-310.pyc b/labellerr/__pycache__/__init__.cpython-310.pyc index 4a735b8993434a23d5d37d6730bfcb26219de59b..ca131d4a2de6527bea19b612d1d2886fa3c9028e 100644 GIT binary patch literal 643 zcmZWmL2uJA6n;*!ByG}7LL5QLwTD$m+z=oN?O~#trVa^VMY6KkV^JMDvNJ|{<-+gS zZ9j!Sz`@s@_7}K-owXeZNB;EQ^Y=bKf03in5bXM)eqNLwz^zOEUa?Ky*sZ@?pn!oQ z>Ty(i*h4P%xnBo3=;MIz)gcZMtW&y2Lmt&JjuEh!M&A$~d<5|{05-WF@bdXtQ&v zvIoZW;uNgJf7-Kl)JNcJ>S+3{x9wCN^_;8y0ZdKA^O6UuB2Fx_;!XvO;;wm z6e}UG#5gbsAsZ4Q%C~LGdJ8JBa6H~O5g}BTgy6Jr!;t%-dkzjaiR&^$_O)b9YcJD% zvw2k2jZ`hK&L%Z$3tF^=3Z1n3PbN9)Py1={1C?y4W-#CCxfu%13Aq7v-~=4}n$2G1 zjgsd~y9X9LUrgW57mN9FnSUtsDqpfj>f9QB?{>a3ASa{}RZGaE=}i2xmh_Ud*UobN W#6ke6m!cGTzC{;#4rAo~PW?aHd$02V literal 182 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 z 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 B^`8I$ diff --git a/labellerr/__pycache__/exceptions.cpython-310.pyc b/labellerr/__pycache__/exceptions.cpython-310.pyc index d984744d59e2681d7f7080a6e313f00a46991cee..2e97c23622614a742a93fccc186451f60932f629 100644 GIT binary patch delta 118 zcmeBV?quf8=jG*M0D_Z6*D{(X@;;1mv5F}u%1=uzF3!x)OU#M!addL^@o^0biU~<9 z&W;I6Eyyp933l-gs4U6I&x^@POiInkNi8ahNv%juEhqtMD%LBgEMf$j$pXa1Y(Rp8 PfrXKUX|e~S10yp49ik&^ delta 118 zcmeBV?quf8=jG*M0D@^<>oTe*@;;2x4=qkDD%MX=EGkY6qXQ!o00`+L82|tP diff --git a/labellerr/base/singleton.py b/labellerr/base/singleton.py index a4c429e..93fc392 100644 --- a/labellerr/base/singleton.py +++ b/labellerr/base/singleton.py @@ -2,18 +2,19 @@ class Singleton: - __instance = None - __lock = None + _instances = {} + _locks = {} def __new__(cls, *args, **kwargs): - if cls.__lock is None: - cls.__lock = threading.Lock() - if cls.__instance is None: - with cls.__lock: - if cls.__instance is None: - cls.__instance = super().__new__(cls) - return cls.__instance + if cls not in cls._locks: + cls._locks[cls] = threading.Lock() + + if cls not in cls._instances: + with cls._locks[cls]: + if cls not in cls._instances: + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] def __init__(self, *args): if type(self) is Singleton: - raise TypeError("Can't instantiate Singleton class") + raise TypeError("Can't instantiate Singleton class") \ No newline at end of file From 868101fb72cbae245361761d6c18d5c7d1518585 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Sun, 5 Oct 2025 23:21:40 +0530 Subject: [PATCH 09/23] files restructiing --- labellerr/Python_SDK/.gitignore | 4 ---- labellerr/Python_SDK/services/video_sampling/.gitignore | 3 --- .../utils => services/labellerr_files}/client_utils.py | 0 labellerr/{Python_SDK => }/services/video_sampling/ffmpeg.py | 0 labellerr/services/video_sampling/ffmpeg_sampling.py | 0 labellerr/{Python_SDK => }/services/video_sampling/gemini.py | 0 labellerr/services/video_sampling/gemini_sampling.py | 0 .../services/video_sampling/pyscene_detect.py | 0 .../{Python_SDK => }/services/video_sampling/requirements.txt | 0 labellerr/{Python_SDK => }/services/video_sampling/ssim.py | 0 10 files changed, 7 deletions(-) delete mode 100644 labellerr/Python_SDK/.gitignore delete mode 100644 labellerr/Python_SDK/services/video_sampling/.gitignore rename labellerr/{Python_SDK/utils => services/labellerr_files}/client_utils.py (100%) rename labellerr/{Python_SDK => }/services/video_sampling/ffmpeg.py (100%) delete mode 100644 labellerr/services/video_sampling/ffmpeg_sampling.py rename labellerr/{Python_SDK => }/services/video_sampling/gemini.py (100%) delete mode 100644 labellerr/services/video_sampling/gemini_sampling.py rename labellerr/{Python_SDK => }/services/video_sampling/pyscene_detect.py (100%) rename labellerr/{Python_SDK => }/services/video_sampling/requirements.txt (100%) rename labellerr/{Python_SDK => }/services/video_sampling/ssim.py (100%) diff --git a/labellerr/Python_SDK/.gitignore b/labellerr/Python_SDK/.gitignore deleted file mode 100644 index 590db49..0000000 --- a/labellerr/Python_SDK/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -video_001 -videoframes -joinvideo.mp4 -__pycache__ \ No newline at end of file diff --git a/labellerr/Python_SDK/services/video_sampling/.gitignore b/labellerr/Python_SDK/services/video_sampling/.gitignore deleted file mode 100644 index 1f2f0d9..0000000 --- a/labellerr/Python_SDK/services/video_sampling/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -video.mp4 -video2.mp4 -yash-suman-prod.json \ No newline at end of file diff --git a/labellerr/Python_SDK/utils/client_utils.py b/labellerr/services/labellerr_files/client_utils.py similarity index 100% rename from labellerr/Python_SDK/utils/client_utils.py rename to labellerr/services/labellerr_files/client_utils.py diff --git a/labellerr/Python_SDK/services/video_sampling/ffmpeg.py b/labellerr/services/video_sampling/ffmpeg.py similarity index 100% rename from labellerr/Python_SDK/services/video_sampling/ffmpeg.py rename to labellerr/services/video_sampling/ffmpeg.py diff --git a/labellerr/services/video_sampling/ffmpeg_sampling.py b/labellerr/services/video_sampling/ffmpeg_sampling.py deleted file mode 100644 index e69de29..0000000 diff --git a/labellerr/Python_SDK/services/video_sampling/gemini.py b/labellerr/services/video_sampling/gemini.py similarity index 100% rename from labellerr/Python_SDK/services/video_sampling/gemini.py rename to labellerr/services/video_sampling/gemini.py diff --git a/labellerr/services/video_sampling/gemini_sampling.py b/labellerr/services/video_sampling/gemini_sampling.py deleted file mode 100644 index e69de29..0000000 diff --git a/labellerr/Python_SDK/services/video_sampling/pyscene_detect.py b/labellerr/services/video_sampling/pyscene_detect.py similarity index 100% rename from labellerr/Python_SDK/services/video_sampling/pyscene_detect.py rename to labellerr/services/video_sampling/pyscene_detect.py diff --git a/labellerr/Python_SDK/services/video_sampling/requirements.txt b/labellerr/services/video_sampling/requirements.txt similarity index 100% rename from labellerr/Python_SDK/services/video_sampling/requirements.txt rename to labellerr/services/video_sampling/requirements.txt diff --git a/labellerr/Python_SDK/services/video_sampling/ssim.py b/labellerr/services/video_sampling/ssim.py similarity index 100% rename from labellerr/Python_SDK/services/video_sampling/ssim.py rename to labellerr/services/video_sampling/ssim.py From bd1da4c7b75f8a6b8ee38cdd1994edbb5f5427db Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:42:42 +0530 Subject: [PATCH 10/23] updated the scripts based on comments --- labellerr/client.py | 27 +++ .../services/labellerr_files/client_utils.py | 156 ++++++------------ labellerr/services/video_sampling/ffmpeg.py | 135 +++++++++++++-- .../services/video_sampling/pyscene_detect.py | 67 ++++---- .../services/video_sampling/requirements.txt | 4 +- labellerr/services/video_sampling/ssim.py | 151 +++++++++-------- 6 files changed, 315 insertions(+), 225 deletions(-) diff --git a/labellerr/client.py b/labellerr/client.py index aaa7281..c89865f 100644 --- a/labellerr/client.py +++ b/labellerr/client.py @@ -1335,3 +1335,30 @@ def create_batches(): raise e except Exception as e: raise LabellerrError(f"Failed to upload files: {str(e)}") + + def make_api_request(self, client_id, url, params=None, unique_id=None): + """ + Make an API request using the client's session and response handling. + + Args: + client_id: Client identifier for authentication + url: The endpoint URL to make the request to + params: Optional query parameters for the request + unique_id: Optional unique identifier for request tracking + + Returns: + The processed response from the API + """ + headers = self._build_headers( + client_id=client_id, + extra_headers={ + "Content-Type": "application/json", + "Origin": constants.ALLOWED_ORIGINS + } + ) + + # Make request using client's session if available + response = self._make_request("GET", url, headers=headers, params=params) + + # Use client's response handler + return self._handle_response(response, request_id=unique_id) diff --git a/labellerr/services/labellerr_files/client_utils.py b/labellerr/services/labellerr_files/client_utils.py index f7824b2..f9f5b5c 100644 --- a/labellerr/services/labellerr_files/client_utils.py +++ b/labellerr/services/labellerr_files/client_utils.py @@ -1,17 +1,16 @@ -from ...client import LabellerrClient -from ...exceptions import LabellerrError -from ... import constants -from ...base.singleton import Singleton # Import your Singleton class +from labellerr.client import LabellerrClient +from labellerr.exceptions import LabellerrError +from labellerr import constants +from labellerr.base.singleton import Singleton import uuid import os import subprocess import requests -import pprint class FileMetadataService(Singleton): - def __init__(self, client: LabellerrClient = None): + def __init__(self, client: LabellerrClient): # Prevent re-initialization of singleton if hasattr(self, '_initialized'): return @@ -21,12 +20,6 @@ def __init__(self, client: LabellerrClient = None): self.client = client self._initialized = True - - def set_client(self, client: LabellerrClient): - """ - Update the client instance (useful for reconfiguration). - """ - self.client = client def get_file_metadata(self, client_id: str, file_id: str, project_id: str, include_answers: bool = False): """ @@ -46,24 +39,26 @@ def get_file_metadata(self, client_id: str, file_id: str, project_id: str, inclu url = f"{constants.BASE_URL}/data/file_data" - headers = self.client._build_headers( - client_id=client_id, - extra_headers={ - "Content-Type": "application/json", - "Origin": constants.ALLOWED_ORIGINS - } - ) - - # Make request using client's session if available - response = self.client._make_request("GET", url, headers=headers, params=params) + response = self.client.make_api_request(client_id, url, params, unique_id) - # Use client's response handler - return self.client._handle_response(response, request_id=unique_id) + return response except Exception as e: raise LabellerrError(f"Failed to fetch file metadata: {str(e)}") + + +class VideoFileService(FileMetadataService): + """ + Service class for handling video file operations including fetching frames, + downloading frames, and creating videos from frames. + """ + + def __init__(self, client: LabellerrClient): + + super().__init__(client) - def get_video_frames(self, client_id: str, file_id: str, project_id: str, dataset_id: str, frame_start: int = 0, frame_end: int = None): + def get_video_frames(self, client_id: str, file_id: str, project_id: str, dataset_id: str, + frame_start: int = 0, frame_end: int = None): """ Retrieve video frames data from Labellerr API. @@ -93,24 +88,13 @@ def get_video_frames(self, client_id: str, file_id: str, project_id: str, datase if frame_end is not None: params['frame_end'] = frame_end - # Build headers using client's build_headers method - headers = self.client._build_headers( - client_id=client_id, - extra_headers={ - "Content-Type": "application/json", - "Origin": constants.ALLOWED_ORIGINS - } - ) + response = self.client.make_api_request(client_id, url, params, unique_id) - # Make request using client's session - response = self.client._make_request("GET", url, headers=headers, params=params) - - # Use client's response handler - return self.client._handle_response(response, request_id=unique_id) + return response except Exception as e: raise LabellerrError(f"Failed to fetch video frames data: {str(e)}") - + def download_video_frames(self, frames_data: dict, output_folder: str = None, file_id: str = None): """ Download video frames from URLs to a local folder. @@ -183,101 +167,67 @@ def download_video_frames(self, frames_data: dict, output_folder: str = None, fi except Exception as e: raise LabellerrError(f"Failed to download video frames: {str(e)}") - - -class JoinVideoFrames(Singleton): - def __init__(self, frames_folder=None, output_file="output.mp4", framerate=30): + def create_video_from_frames(self, frames_folder: str, output_file: str = "output.mp4", + framerate: int = 30, pattern: str = "%d.jpg"): """ - Initialize the JoinVideoFrames class. + Join frames into a video using ffmpeg. :param frames_folder: Path to folder containing sequential frames (e.g., 1.jpg, 2.jpg). - :param output_file: Name of the output video file. + :param output_file: Name of the output video file (default: output.mp4). :param framerate: Desired video framerate (default: 30 fps). - """ - # Prevent re-initialization of singleton - if hasattr(self, '_initialized'): - return - - self.frames_folder = frames_folder - self.output_file = output_file - self.framerate = framerate - self._initialized = True - - def configure(self, frames_folder=None, output_file=None, framerate=None): - """ - Reconfigure the singleton instance parameters. - """ - if frames_folder is not None: - self.frames_folder = frames_folder - if output_file is not None: - self.output_file = output_file - if framerate is not None: - self.framerate = framerate - - def join(self, pattern="%d.jpg", frames_folder=None, output_file=None, framerate=None): - """ - Join frames into a video using ffmpeg. - :param pattern: Pattern for sequential frames inside frames_folder (default: %d.jpg → 1.jpg, 2.jpg, ...). - :param frames_folder: Override frames folder for this operation - :param output_file: Override output file for this operation - :param framerate: Override framerate for this operation """ - # Use provided parameters or fall back to instance attributes - folder = frames_folder or self.frames_folder - output = output_file or self.output_file - fps = framerate or self.framerate - - if folder is None: - raise ValueError("frames_folder must be provided either during initialization or when calling join()") + if frames_folder is None: + raise ValueError("frames_folder must be provided") - input_pattern = os.path.join(folder, pattern) + input_pattern = os.path.join(frames_folder, pattern) # FFmpeg command command = [ "ffmpeg", "-y", # Overwrite output file if exists - "-framerate", str(fps), + "-framerate", str(framerate), "-i", input_pattern, "-c:v", "libx264", "-pix_fmt", "yuv420p", - output + output_file ] try: print("Running command:", " ".join(command)) subprocess.run(command, check=True) - print(f"Video saved as {output}") + print(f"Video saved as {output_file}") except subprocess.CalledProcessError as e: - print("Error while joining frames:", e) - - + raise LabellerrError(f"Error while joining frames: {str(e)}") + + +# Example usage if __name__ == "__main__": - api_key = "66f4d8.9f402742f58a89568f5bcc0f86" - api_secret = "1e2478b930d4a842a526beb585e60d2a9ee6a6f1e3aa89cb3c8ead751f418215" - client_id = "14078" + api_key = "" + api_secret = "" + client_id = "" dataset_id = "16257fd6-b91b-4d00-a680-9ece9f3f241c" project_id = "gabrila_artificial_duck_74237" file_id = "c44f38f6-0186-436f-8c2d-ffb50a539c76" client = LabellerrClient(api_key=api_key, api_secret=api_secret) - # First initialization - creates the singleton instance - file_service = FileMetadataService(client) + # Create VideoFileService instance + video_service = VideoFileService(client) - print(file_service.get_file_metadata(client_id, file_id, project_id)) + # Get file metadata + # print(video_service.get_file_metadata(client_id, file_id, project_id)) -class ImageFileService(FileMetadataService): - pass - -class VideoFileService(FileMetadataService): + # Get video frames + # total_frame = video_service.get_file_metadata(client_id, file_id, project_id)['file_metadata']['total_frames'] + # frames = video_service.get_video_frames(client_id, file_id, project_id, dataset_id, frame_end=total_frame) + # print(frames) + + # Download frames + # video_service.download_video_frames(frames, output_folder="./output", file_id=file_id) - def download_video_frames(self, client_id: str, file_id: str, project_id: str): - pass - def get_video_frames(self, client_id: str, file_id: str, project_id: str): - pass - def create_video_from_frames(self, client_id: str, file_id: str, project_id: str): - pass \ No newline at end of file + # Create video from frames + # video_service.create_video_from_frames(frames_folder="./output/frame_folder", output_file="final_video.mp4", framerate=30) \ No newline at end of file diff --git a/labellerr/services/video_sampling/ffmpeg.py b/labellerr/services/video_sampling/ffmpeg.py index 0016a6a..eaf8f7f 100644 --- a/labellerr/services/video_sampling/ffmpeg.py +++ b/labellerr/services/video_sampling/ffmpeg.py @@ -1,21 +1,46 @@ import subprocess import os +import json +from pydantic import BaseModel, Field +from typing import List +from labellerr.base.singleton import Singleton -class FFMPEG: - def __init__(self, video_path: str, file_id: str): - self.video_path = video_path - self.file_id = file_id - self.save_folder = file_id + +class SceneFrame(BaseModel): + """Represents an extracted keyframe.""" + frame_path: str + frame_no: 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, file_id: str) -> DetectionResult: + """ + Extract keyframes from video and save to file_id folder. - def detect_and_extract(self): - """Extract keyframes from video and save to file_id folder""" - os.makedirs(self.save_folder, exist_ok=True) + Args: + video_path: Path to the video file + file_id: Unique identifier for the video (used as output folder name) + + Returns: + DetectionResult containing file_id, output_folder, and list of SceneFrame objects + """ + save_folder = file_id + os.makedirs(save_folder, exist_ok=True) - output_pattern = os.path.join(self.save_folder, "%d.jpg") + output_pattern = os.path.join(save_folder, "%d.jpg") command = [ "ffmpeg", - "-i", self.video_path, + "-i", video_path, "-vf", "select='eq(pict_type,PICT_TYPE_I)',showinfo", "-vsync", "vfr", "-frame_pts", "1", @@ -23,12 +48,94 @@ def detect_and_extract(self): ] try: - subprocess.run(command, check=True) - print(f"Keyframes extracted to {self.save_folder}") + result = subprocess.run(command, check=True, capture_output=True, text=True) + print(f"Keyframes extracted to {save_folder}") + + # Parse frame information from FFMPEG output + selected_frames = self._parse_ffmpeg_output(result.stderr, save_folder) + + # Create result + detection_result = DetectionResult( + file_id=file_id, + output_folder=save_folder, + selected_frames=selected_frames + ) + + # Save JSON mapping + self._save_json_mapping(detection_result, save_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, save_folder: str) -> List[SceneFrame]: + """ + Parse FFMPEG stderr output to extract frame information. + + Args: + stderr_output: FFMPEG stderr output containing showinfo data + save_folder: Folder where frames are saved + + 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(save_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_no=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 + 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\labellerr\Python_SDK\services\video_sampling\video.mp4" - result = FFMPEG(video_path, "FFMPEG_sample_video_011").detect_and_extract() \ No newline at end of file + video_path = r"D:\professional\LABELLERR\Task\Repos\Python_SDK\services\video_sampling\video2.mp4" + + # Get singleton instance + detector = FFMPEGSceneDetect() + result = detector.detect_and_extract(video_path, "FFMPEG_sample_video_011") diff --git a/labellerr/services/video_sampling/pyscene_detect.py b/labellerr/services/video_sampling/pyscene_detect.py index 406b584..a1aed76 100644 --- a/labellerr/services/video_sampling/pyscene_detect.py +++ b/labellerr/services/video_sampling/pyscene_detect.py @@ -2,57 +2,50 @@ from scenedetect import detect, AdaptiveDetector from PIL import Image import cv2 -from dataclasses import dataclass, asdict +from pydantic import BaseModel, Field from typing import List import json +from labellerr.base.singleton import Singleton -@dataclass -class SceneFrame: +class SceneFrame(BaseModel): """Represents a detected scene with its extracted frame.""" frame_path: str frame_no: int -@dataclass -class DetectionResult: +class DetectionResult(BaseModel): """Contains all detection results for a video.""" file_id: str output_folder: str total_frames: int - selected_frames: List[SceneFrame] + selected_frames: List[SceneFrame] = Field(default_factory=list) -class PySceneDetect: - """Scene detection and frame extraction for videos.""" +class PySceneDetect(Singleton): + """Scene detection and frame extraction for videos (Singleton).""" - def __init__(self, video_path: str, file_id: str): + def detect_and_extract(self, video_path: str, file_id: str) -> DetectionResult: """ - Initialize the scene detector. + Detect scenes and extract representative frames. Args: video_path: Path to the video file file_id: Unique identifier for the video (used as output folder name) - """ - self.video_path = video_path - self.file_id = file_id - self.output_folder = file_id - - def detect_and_extract(self) -> DetectionResult: - """ - Detect scenes and extract representative frames. Returns: DetectionResult containing file_id, output_folder, total_frames, and list of SceneFrame objects """ + output_folder = file_id + # Detect scene transitions - scenes = detect(self.video_path, AdaptiveDetector()) + scenes = detect(video_path, AdaptiveDetector()) # Create output folder - os.makedirs(self.output_folder, exist_ok=True) + os.makedirs(output_folder, exist_ok=True) # Open video for frame extraction - video = cv2.VideoCapture(self.video_path) + video = cv2.VideoCapture(video_path) # Get total frames in video total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) @@ -68,7 +61,7 @@ def detect_and_extract(self) -> DetectionResult: # Save frame with frame number as filename frame_filename = f"{frame_no}.jpg" - frame_path = os.path.join(self.output_folder, frame_filename) + frame_path = os.path.join(output_folder, frame_filename) frame.save(frame_path) # Create SceneFrame object @@ -82,14 +75,14 @@ def detect_and_extract(self) -> DetectionResult: # Create result result = DetectionResult( - file_id=self.file_id, - output_folder=self.output_folder, + file_id=file_id, + output_folder=output_folder, total_frames=total_frames, selected_frames=scene_frames ) # Save JSON mapping - self._save_json_mapping(result) + self._save_json_mapping(result, output_folder, file_id) return result @@ -108,28 +101,28 @@ def _get_frame(self, video: cv2.VideoCapture, frame_no: int) -> Image.Image: _, frame = video.read() return Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) - def _save_json_mapping(self, result: DetectionResult) -> None: + 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 """ - mapping = { - "file_id": result.file_id, - "output_folder": result.output_folder, - "total_frames": result.total_frames, - "total_selected_frames": len(result.selected_frames), - "selected_frames": [asdict(frame) for frame in result.selected_frames] - } + # Use Pydantic's model_dump instead of asdict + result_dict = result.model_dump() + result_dict["total_selected_frames"] = len(result.selected_frames) - json_path = os.path.join(self.output_folder, f"{self.file_id}_mapping.json") + json_path = os.path.join(output_folder, f"{file_id}_mapping.json") with open(json_path, 'w', encoding='utf-8') as f: - json.dump(mapping, f, indent=2, ensure_ascii=False) + 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\Python_SDK\services\video_sampling\video.mp4" - result = PySceneDetect(video_path, "video_001").detect_and_extract() \ No newline at end of file + video_path = r"D:\professional\LABELLERR\Task\Repos\Python_SDK\services\video_sampling\video2.mp4" + + detector = PySceneDetect() + result = detector.detect_and_extract(video_path, "video_001") \ No newline at end of file diff --git a/labellerr/services/video_sampling/requirements.txt b/labellerr/services/video_sampling/requirements.txt index c4225b1..aa716f2 100644 --- a/labellerr/services/video_sampling/requirements.txt +++ b/labellerr/services/video_sampling/requirements.txt @@ -1,3 +1,3 @@ scenedetect -opencv-python -google-cloud-videointelligence \ No newline at end of file +google-cloud-videointelligence +scikit-image \ No newline at end of file diff --git a/labellerr/services/video_sampling/ssim.py b/labellerr/services/video_sampling/ssim.py index 5da532c..a5c971f 100644 --- a/labellerr/services/video_sampling/ssim.py +++ b/labellerr/services/video_sampling/ssim.py @@ -2,84 +2,81 @@ import cv2 import numpy as np from PIL import Image -from dataclasses import dataclass, asdict +from pydantic import BaseModel, Field from typing import List import json from skimage.metrics import structural_similarity as ssim +from labellerr.base.singleton import Singleton -@dataclass -class SceneFrame: +class SceneFrame(BaseModel): """Represents a detected scene with its extracted frame.""" frame_path: str frame_no: int ssim_score: float -@dataclass -class DetectionResult: +class DetectionResult(BaseModel): """Contains all detection results for a video.""" file_id: str output_folder: str total_frames: int - selected_frames: List[SceneFrame] + selected_frames: List[SceneFrame] = Field(default_factory=list) -class SSIMSceneDetect: - """SSIM-based scene detection and frame extraction for videos.""" +class SSIMSceneDetect(Singleton): + """SSIM-based scene detection and frame extraction for videos (Singleton).""" - def __init__(self, video_path: str, file_id: str, threshold: float = 0.6, resize_dim: tuple = (320, 240)): + def detect_and_extract( + self, + video_path: str, + file_id: str, + threshold: float = 0.6, + resize_dim: tuple = (320, 240) + ) -> DetectionResult: """ - Initialize the SSIM scene detector. + Detect scenes using SSIM and extract representative frames. Args: video_path: Path to the video file file_id: Unique identifier for the video (used as output folder name) threshold: SSIM threshold for scene detection (lower = stricter, default: 0.6) resize_dim: Dimensions to resize frames for SSIM calculation (default: (320, 240)) - """ - self.video_path = video_path - self.file_id = file_id - self.output_folder = file_id - self.threshold = threshold - self.resize_dim = resize_dim - - def detect_and_extract(self) -> DetectionResult: - """ - Detect scenes using SSIM and extract representative frames. Returns: DetectionResult containing file_id, output_folder, total_frames, and list of SceneFrame objects """ + output_folder = file_id + # Create output folder - os.makedirs(self.output_folder, exist_ok=True) + os.makedirs(output_folder, exist_ok=True) # Open video for processing - video = cv2.VideoCapture(self.video_path) + video = cv2.VideoCapture(video_path) if not video.isOpened(): - raise ValueError(f"Cannot open video: {self.video_path}") + 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: {self.video_path}") + print(f"Processing video: {video_path}") print(f"Total frames: {total_frames}") - print(f"SSIM threshold: {self.threshold}") + 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: {self.video_path}") + 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) - print(f"Saved keyframe 0 at frame {frame_count} (First frame)") + self._save_frame(prev_frame, frame_count, 1.0, scene_frames, output_folder) + # print(f"Saved keyframe 0 at frame {frame_count} (First frame)") # Process remaining frames while True: @@ -90,54 +87,62 @@ def detect_and_extract(self) -> DetectionResult: frame_count += 1 # Calculate SSIM between current and previous frame - ssim_score = self._calculate_ssim(prev_frame, curr_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 < self.threshold: - self._save_frame(curr_frame, frame_count, ssim_score, scene_frames) + if ssim_score < threshold: + self._save_frame(curr_frame, frame_count, ssim_score, scene_frames, output_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: {self.threshold})") + 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.") + # print(f"\nExtracted {len(scene_frames)} keyframes from {frame_count + 1} frames.") # Create result result = DetectionResult( - file_id=self.file_id, - output_folder=self.output_folder, + file_id=file_id, + output_folder=output_folder, total_frames=total_frames, selected_frames=scene_frames ) # Save JSON mapping - self._save_json_mapping(result) + self._save_json_mapping(result, output_folder, file_id, threshold, resize_dim) return result - def _calculate_ssim(self, frame1: np.ndarray, frame2: np.ndarray) -> float: + 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, self.resize_dim), cv2.COLOR_BGR2GRAY) - gray2 = cv2.cvtColor(cv2.resize(frame2, self.resize_dim), cv2.COLOR_BGR2GRAY) + 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]) -> None: + def _save_frame( + self, + frame: np.ndarray, + frame_no: int, + ssim_score: float, + scene_frames: List[SceneFrame], + output_folder: str + ) -> None: """ Save a frame to disk and add to scene_frames list. @@ -146,6 +151,7 @@ def _save_frame(self, frame: np.ndarray, frame_no: int, ssim_score: float, scene frame_no: Frame number ssim_score: SSIM score that triggered this frame scene_frames: List to append SceneFrame object to + output_folder: Folder to save the frame """ # Convert BGR to RGB for PIL frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) @@ -153,7 +159,7 @@ def _save_frame(self, frame: np.ndarray, frame_no: int, ssim_score: float, scene # Save frame with frame number as filename frame_filename = f"{frame_no}.jpg" - frame_path = os.path.join(self.output_folder, frame_filename) + frame_path = os.path.join(output_folder, frame_filename) pil_image.save(frame_path) # Create SceneFrame object @@ -164,45 +170,52 @@ def _save_frame(self, frame: np.ndarray, frame_no: int, ssim_score: float, scene ) scene_frames.append(scene_frame) - def _save_json_mapping(self, result: DetectionResult) -> None: + 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 + resize_dim: Resize dimensions used """ - mapping = { - "file_id": result.file_id, - "output_folder": result.output_folder, - "total_frames": result.total_frames, - "total_selected_frames": len(result.selected_frames), - "threshold": self.threshold, - "resize_dim": self.resize_dim, - "selected_frames": [asdict(frame) for frame in result.selected_frames] - } - - json_path = os.path.join(self.output_folder, f"{self.file_id}_mapping.json") + # 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(mapping, f, indent=2, ensure_ascii=False) + 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\SDKPython\labellerr\Python_SDK\services\video_sampling\video.mp4" +if __name__ == "__main__": + # Example usage + video_path = r"D:\professional\LABELLERR\Task\Repos\Python_SDK\services\video_sampling\video2.mp4" -# # Create detector with custom parameters -# detector = SSIMSceneDetect( -# video_path=video_path, -# file_id="video_001", -# threshold=0.6, # Lower value = more sensitive to changes -# resize_dim=(320, 240) -# ) + # Get singleton instance + detector = SSIMSceneDetect() -# # Detect and extract frames -# result = detector.detect_and_extract() + # Detect and extract frames + result = detector.detect_and_extract( + video_path=video_path, + file_id="video_001", + threshold=0.6, # Lower value = more sensitive to changes + resize_dim=(320, 240) + ) -# print(f"\nDetection complete!") -# print(f"Total frames extracted: {len(result.selected_frames)}") -# print(f"Output folder: {result.output_folder}") \ No newline at end of file + print(f"\nDetection complete!") + print(f"Total frames extracted: {len(result.selected_frames)}") + print(f"Output folder: {result.output_folder}") \ No newline at end of file From 5addbaa76cd8319ace6c6c7d79e787a27915bce5 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:06:01 +0530 Subject: [PATCH 11/23] added threading to frames downloading --- .../services/labellerr_files/client_utils.py | 116 +++++++++++++----- 1 file changed, 82 insertions(+), 34 deletions(-) diff --git a/labellerr/services/labellerr_files/client_utils.py b/labellerr/services/labellerr_files/client_utils.py index f9f5b5c..621ac77 100644 --- a/labellerr/services/labellerr_files/client_utils.py +++ b/labellerr/services/labellerr_files/client_utils.py @@ -6,6 +6,8 @@ import os import subprocess import requests +from concurrent.futures import ThreadPoolExecutor, as_completed +from threading import Lock class FileMetadataService(Singleton): @@ -95,13 +97,59 @@ def get_video_frames(self, client_id: str, file_id: str, project_id: str, datase except Exception as e: raise LabellerrError(f"Failed to fetch video frames data: {str(e)}") - def download_video_frames(self, frames_data: dict, output_folder: str = None, file_id: str = None): + def _download_single_frame(self, frame_number, frame_url, save_path, print_lock): """ - Download video frames from URLs to a local folder. + Download a single frame (helper method for threading). + + :param frame_number: Frame number + :param frame_url: URL to download from + :param save_path: Directory to save the frame + :param print_lock: Lock for thread-safe printing + :return: Tuple of (success: bool, frame_number: str, error_info: dict or None) + """ + try: + filename = f"{frame_number}.jpg" + filepath = os.path.join(save_path, filename) + + response = requests.get(frame_url, timeout=30) + + if response.status_code == 200: + with open(filepath, 'wb') as f: + f.write(response.content) + + with print_lock: + print(f"Downloaded: {filename}") + + return True, frame_number, None + else: + error_info = { + 'frame': frame_number, + 'status': response.status_code + } + with print_lock: + print(f"Failed to download frame {frame_number}: Status {response.status_code}") + + return False, frame_number, error_info + + except Exception as e: + error_info = { + 'frame': frame_number, + 'error': str(e) + } + with print_lock: + print(f"Error downloading frame {frame_number}: {str(e)}") + + return False, frame_number, error_info + + def download_video_frames(self, frames_data: dict, output_folder: str, + file_id: str, max_workers: int = 20): + """ + Download video frames from URLs to a local folder using multithreading. :param frames_data: Dictionary with frame numbers as keys and URLs as values :param output_folder: Base folder path where frames will be saved (default: current directory) :param file_id: File ID to use as folder name. If None, uses 'frames' as folder name + :param max_workers: Maximum number of concurrent download threads (default: 10) :return: Dictionary with download statistics """ try: @@ -122,36 +170,33 @@ def download_video_frames(self, frames_data: dict, output_folder: str = None, fi success_count = 0 failed_frames = [] + print_lock = Lock() - print(f"Downloading {len(frames_data)} frames to: {save_path}") + # print(f"Downloading {len(frames_data)} frames to: {save_path}") + # print(f"Using {max_workers} concurrent threads") - for frame_number, frame_url in frames_data.items(): - try: - # Create filename with frame number - filename = f"{frame_number}.jpg" - filepath = os.path.join(save_path, filename) - - # Download the frame - response = requests.get(frame_url, timeout=30) + # Use ThreadPoolExecutor for concurrent downloads + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # Submit all download tasks + future_to_frame = { + executor.submit( + self._download_single_frame, + frame_number, + frame_url, + save_path, + print_lock + ): frame_number + for frame_number, frame_url in frames_data.items() + } + + # Process completed downloads + for future in as_completed(future_to_frame): + success, frame_number, error_info = future.result() - if response.status_code == 200: - with open(filepath, 'wb') as f: - f.write(response.content) + if success: success_count += 1 - print(f"Downloaded: {filename}") else: - failed_frames.append({ - 'frame': frame_number, - 'status': response.status_code - }) - print(f"Failed to download frame {frame_number}: Status {response.status_code}") - - except Exception as e: - failed_frames.append({ - 'frame': frame_number, - 'error': str(e) - }) - print(f"Error downloading frame {frame_number}: {str(e)}") + failed_frames.append(error_info) result = { 'total_frames': len(frames_data), @@ -206,9 +251,9 @@ def create_video_from_frames(self, frames_folder: str, output_file: str = "outpu # Example usage if __name__ == "__main__": - api_key = "" - api_secret = "" - client_id = "" + api_key = "66f4d8.9f402742f58a89568f5bcc0f86" + api_secret = "1e2478b930d4a842a526beb585e60d2a9ee6a6f1e3aa89cb3c8ead751f418215" + client_id = "14078" dataset_id = "16257fd6-b91b-4d00-a680-9ece9f3f241c" project_id = "gabrila_artificial_duck_74237" file_id = "c44f38f6-0186-436f-8c2d-ffb50a539c76" @@ -222,12 +267,15 @@ def create_video_from_frames(self, frames_folder: str, output_file: str = "outpu # print(video_service.get_file_metadata(client_id, file_id, project_id)) # Get video frames - # total_frame = video_service.get_file_metadata(client_id, file_id, project_id)['file_metadata']['total_frames'] - # frames = video_service.get_video_frames(client_id, file_id, project_id, dataset_id, frame_end=total_frame) + total_frame = video_service.get_file_metadata(client_id, file_id, project_id)['file_metadata']['total_frames'] + frames = video_service.get_video_frames(client_id, file_id, project_id, dataset_id, frame_end=total_frame) # print(frames) - # Download frames - # video_service.download_video_frames(frames, output_folder="./output", file_id=file_id) + # Download frames with threading (default 10 workers) + video_service.download_video_frames(frames, output_folder="./output", file_id=file_id) + + # Or specify custom number of workers + # video_service.download_video_frames(frames, output_folder="./output", file_id=file_id, max_workers=20) # Create video from frames # video_service.create_video_from_frames(frames_folder="./output/frame_folder", output_file="final_video.mp4", framerate=30) \ No newline at end of file From bad6f537e443510aa845884fe17d7920fb45b1c3 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:10:50 +0530 Subject: [PATCH 12/23] Revert "added threading to frames downloading" This reverts commit 5addbaa76cd8319ace6c6c7d79e787a27915bce5. --- .../services/labellerr_files/client_utils.py | 116 +++++------------- 1 file changed, 34 insertions(+), 82 deletions(-) diff --git a/labellerr/services/labellerr_files/client_utils.py b/labellerr/services/labellerr_files/client_utils.py index 621ac77..f9f5b5c 100644 --- a/labellerr/services/labellerr_files/client_utils.py +++ b/labellerr/services/labellerr_files/client_utils.py @@ -6,8 +6,6 @@ import os import subprocess import requests -from concurrent.futures import ThreadPoolExecutor, as_completed -from threading import Lock class FileMetadataService(Singleton): @@ -97,59 +95,13 @@ def get_video_frames(self, client_id: str, file_id: str, project_id: str, datase except Exception as e: raise LabellerrError(f"Failed to fetch video frames data: {str(e)}") - def _download_single_frame(self, frame_number, frame_url, save_path, print_lock): + def download_video_frames(self, frames_data: dict, output_folder: str = None, file_id: str = None): """ - Download a single frame (helper method for threading). - - :param frame_number: Frame number - :param frame_url: URL to download from - :param save_path: Directory to save the frame - :param print_lock: Lock for thread-safe printing - :return: Tuple of (success: bool, frame_number: str, error_info: dict or None) - """ - try: - filename = f"{frame_number}.jpg" - filepath = os.path.join(save_path, filename) - - response = requests.get(frame_url, timeout=30) - - if response.status_code == 200: - with open(filepath, 'wb') as f: - f.write(response.content) - - with print_lock: - print(f"Downloaded: {filename}") - - return True, frame_number, None - else: - error_info = { - 'frame': frame_number, - 'status': response.status_code - } - with print_lock: - print(f"Failed to download frame {frame_number}: Status {response.status_code}") - - return False, frame_number, error_info - - except Exception as e: - error_info = { - 'frame': frame_number, - 'error': str(e) - } - with print_lock: - print(f"Error downloading frame {frame_number}: {str(e)}") - - return False, frame_number, error_info - - def download_video_frames(self, frames_data: dict, output_folder: str, - file_id: str, max_workers: int = 20): - """ - Download video frames from URLs to a local folder using multithreading. + Download video frames from URLs to a local folder. :param frames_data: Dictionary with frame numbers as keys and URLs as values :param output_folder: Base folder path where frames will be saved (default: current directory) :param file_id: File ID to use as folder name. If None, uses 'frames' as folder name - :param max_workers: Maximum number of concurrent download threads (default: 10) :return: Dictionary with download statistics """ try: @@ -170,33 +122,36 @@ def download_video_frames(self, frames_data: dict, output_folder: str, success_count = 0 failed_frames = [] - print_lock = Lock() - # print(f"Downloading {len(frames_data)} frames to: {save_path}") - # print(f"Using {max_workers} concurrent threads") + print(f"Downloading {len(frames_data)} frames to: {save_path}") - # Use ThreadPoolExecutor for concurrent downloads - with ThreadPoolExecutor(max_workers=max_workers) as executor: - # Submit all download tasks - future_to_frame = { - executor.submit( - self._download_single_frame, - frame_number, - frame_url, - save_path, - print_lock - ): frame_number - for frame_number, frame_url in frames_data.items() - } - - # Process completed downloads - for future in as_completed(future_to_frame): - success, frame_number, error_info = future.result() + for frame_number, frame_url in frames_data.items(): + try: + # Create filename with frame number + filename = f"{frame_number}.jpg" + filepath = os.path.join(save_path, filename) + + # Download the frame + response = requests.get(frame_url, timeout=30) - if success: + if response.status_code == 200: + with open(filepath, 'wb') as f: + f.write(response.content) success_count += 1 + print(f"Downloaded: {filename}") else: - failed_frames.append(error_info) + failed_frames.append({ + 'frame': frame_number, + 'status': response.status_code + }) + print(f"Failed to download frame {frame_number}: Status {response.status_code}") + + except Exception as e: + failed_frames.append({ + 'frame': frame_number, + 'error': str(e) + }) + print(f"Error downloading frame {frame_number}: {str(e)}") result = { 'total_frames': len(frames_data), @@ -251,9 +206,9 @@ def create_video_from_frames(self, frames_folder: str, output_file: str = "outpu # Example usage if __name__ == "__main__": - api_key = "66f4d8.9f402742f58a89568f5bcc0f86" - api_secret = "1e2478b930d4a842a526beb585e60d2a9ee6a6f1e3aa89cb3c8ead751f418215" - client_id = "14078" + api_key = "" + api_secret = "" + client_id = "" dataset_id = "16257fd6-b91b-4d00-a680-9ece9f3f241c" project_id = "gabrila_artificial_duck_74237" file_id = "c44f38f6-0186-436f-8c2d-ffb50a539c76" @@ -267,15 +222,12 @@ def create_video_from_frames(self, frames_folder: str, output_file: str = "outpu # print(video_service.get_file_metadata(client_id, file_id, project_id)) # Get video frames - total_frame = video_service.get_file_metadata(client_id, file_id, project_id)['file_metadata']['total_frames'] - frames = video_service.get_video_frames(client_id, file_id, project_id, dataset_id, frame_end=total_frame) + # total_frame = video_service.get_file_metadata(client_id, file_id, project_id)['file_metadata']['total_frames'] + # frames = video_service.get_video_frames(client_id, file_id, project_id, dataset_id, frame_end=total_frame) # print(frames) - # Download frames with threading (default 10 workers) - video_service.download_video_frames(frames, output_folder="./output", file_id=file_id) - - # Or specify custom number of workers - # video_service.download_video_frames(frames, output_folder="./output", file_id=file_id, max_workers=20) + # Download frames + # video_service.download_video_frames(frames, output_folder="./output", file_id=file_id) # Create video from frames # video_service.create_video_from_frames(frames_folder="./output/frame_folder", output_file="final_video.mp4", framerate=30) \ No newline at end of file From dac6ffb62e370bd1902ffd50aae2d8f9c768b37f Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:13:12 +0530 Subject: [PATCH 13/23] minor changes in client_utils --- .../services/labellerr_files/client_utils.py | 102 +++++++++++++----- 1 file changed, 75 insertions(+), 27 deletions(-) diff --git a/labellerr/services/labellerr_files/client_utils.py b/labellerr/services/labellerr_files/client_utils.py index f9f5b5c..3f3f950 100644 --- a/labellerr/services/labellerr_files/client_utils.py +++ b/labellerr/services/labellerr_files/client_utils.py @@ -6,6 +6,8 @@ import os import subprocess import requests +from concurrent.futures import ThreadPoolExecutor, as_completed +from threading import Lock class FileMetadataService(Singleton): @@ -95,13 +97,59 @@ def get_video_frames(self, client_id: str, file_id: str, project_id: str, datase except Exception as e: raise LabellerrError(f"Failed to fetch video frames data: {str(e)}") - def download_video_frames(self, frames_data: dict, output_folder: str = None, file_id: str = None): + def _download_single_frame(self, frame_number, frame_url, save_path, print_lock): """ - Download video frames from URLs to a local folder. + Download a single frame (helper method for threading). + + :param frame_number: Frame number + :param frame_url: URL to download from + :param save_path: Directory to save the frame + :param print_lock: Lock for thread-safe printing + :return: Tuple of (success: bool, frame_number: str, error_info: dict or None) + """ + try: + filename = f"{frame_number}.jpg" + filepath = os.path.join(save_path, filename) + + response = requests.get(frame_url, timeout=30) + + if response.status_code == 200: + with open(filepath, 'wb') as f: + f.write(response.content) + + with print_lock: + print(f"Downloaded: {filename}") + + return True, frame_number, None + else: + error_info = { + 'frame': frame_number, + 'status': response.status_code + } + with print_lock: + print(f"Failed to download frame {frame_number}: Status {response.status_code}") + + return False, frame_number, error_info + + except Exception as e: + error_info = { + 'frame': frame_number, + 'error': str(e) + } + with print_lock: + print(f"Error downloading frame {frame_number}: {str(e)}") + + return False, frame_number, error_info + + def download_video_frames(self, frames_data: dict, output_folder: str = None, + file_id: str = None, max_workers: int = 10): + """ + Download video frames from URLs to a local folder using multithreading. :param frames_data: Dictionary with frame numbers as keys and URLs as values :param output_folder: Base folder path where frames will be saved (default: current directory) :param file_id: File ID to use as folder name. If None, uses 'frames' as folder name + :param max_workers: Maximum number of concurrent download threads (default: 10) :return: Dictionary with download statistics """ try: @@ -122,36 +170,33 @@ def download_video_frames(self, frames_data: dict, output_folder: str = None, fi success_count = 0 failed_frames = [] + print_lock = Lock() print(f"Downloading {len(frames_data)} frames to: {save_path}") + print(f"Using {max_workers} concurrent threads") - for frame_number, frame_url in frames_data.items(): - try: - # Create filename with frame number - filename = f"{frame_number}.jpg" - filepath = os.path.join(save_path, filename) - - # Download the frame - response = requests.get(frame_url, timeout=30) + # Use ThreadPoolExecutor for concurrent downloads + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # Submit all download tasks + future_to_frame = { + executor.submit( + self._download_single_frame, + frame_number, + frame_url, + save_path, + print_lock + ): frame_number + for frame_number, frame_url in frames_data.items() + } + + # Process completed downloads + for future in as_completed(future_to_frame): + success, frame_number, error_info = future.result() - if response.status_code == 200: - with open(filepath, 'wb') as f: - f.write(response.content) + if success: success_count += 1 - print(f"Downloaded: {filename}") else: - failed_frames.append({ - 'frame': frame_number, - 'status': response.status_code - }) - print(f"Failed to download frame {frame_number}: Status {response.status_code}") - - except Exception as e: - failed_frames.append({ - 'frame': frame_number, - 'error': str(e) - }) - print(f"Error downloading frame {frame_number}: {str(e)}") + failed_frames.append(error_info) result = { 'total_frames': len(frames_data), @@ -226,8 +271,11 @@ def create_video_from_frames(self, frames_folder: str, output_file: str = "outpu # frames = video_service.get_video_frames(client_id, file_id, project_id, dataset_id, frame_end=total_frame) # print(frames) - # Download frames + # Download frames with threading (default 10 workers) # video_service.download_video_frames(frames, output_folder="./output", file_id=file_id) + # Or specify custom number of workers + # video_service.download_video_frames(frames, output_folder="./output", file_id=file_id, max_workers=20) + # Create video from frames # video_service.create_video_from_frames(frames_folder="./output/frame_folder", output_file="final_video.mp4", framerate=30) \ No newline at end of file From 3fb23e96ebfc88ed220fcd487166a683c5d618ea Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:55:24 +0530 Subject: [PATCH 14/23] remove the file_id argument, only video_path is needed --- labellerr/services/video_sampling/ffmpeg.py | 9 +++++---- labellerr/services/video_sampling/pyscene_detect.py | 7 ++++--- labellerr/services/video_sampling/ssim.py | 5 ++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/labellerr/services/video_sampling/ffmpeg.py b/labellerr/services/video_sampling/ffmpeg.py index eaf8f7f..2d8836e 100644 --- a/labellerr/services/video_sampling/ffmpeg.py +++ b/labellerr/services/video_sampling/ffmpeg.py @@ -22,17 +22,18 @@ class DetectionResult(BaseModel): class FFMPEGSceneDetect(Singleton): """Keyframe extraction from videos using FFMPEG (Singleton).""" - def detect_and_extract(self, video_path: str, file_id: str) -> DetectionResult: + def detect_and_extract(self, video_path: str) -> DetectionResult: """ - Extract keyframes from video and save to file_id folder. + Extract keyframes from video and save to folder named after video file. Args: video_path: Path to the video file - file_id: Unique identifier for the video (used as output folder name) 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] save_folder = file_id os.makedirs(save_folder, exist_ok=True) @@ -138,4 +139,4 @@ def _save_json_mapping(self, result: DetectionResult, output_folder: str, file_i # Get singleton instance detector = FFMPEGSceneDetect() - result = detector.detect_and_extract(video_path, "FFMPEG_sample_video_011") + result = detector.detect_and_extract(video_path) \ No newline at end of file diff --git a/labellerr/services/video_sampling/pyscene_detect.py b/labellerr/services/video_sampling/pyscene_detect.py index a1aed76..f2d62f8 100644 --- a/labellerr/services/video_sampling/pyscene_detect.py +++ b/labellerr/services/video_sampling/pyscene_detect.py @@ -25,17 +25,18 @@ class DetectionResult(BaseModel): class PySceneDetect(Singleton): """Scene detection and frame extraction for videos (Singleton).""" - def detect_and_extract(self, video_path: str, file_id: str) -> DetectionResult: + def detect_and_extract(self, video_path: str) -> DetectionResult: """ Detect scenes and extract representative frames. Args: video_path: Path to the video file - file_id: Unique identifier for the video (used as output folder name) 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] output_folder = file_id # Detect scene transitions @@ -125,4 +126,4 @@ def _save_json_mapping(self, result: DetectionResult, output_folder: str, file_i video_path = r"D:\professional\LABELLERR\Task\Repos\Python_SDK\services\video_sampling\video2.mp4" detector = PySceneDetect() - result = detector.detect_and_extract(video_path, "video_001") \ No newline at end of file + result = detector.detect_and_extract(video_path) \ No newline at end of file diff --git a/labellerr/services/video_sampling/ssim.py b/labellerr/services/video_sampling/ssim.py index a5c971f..5e08468 100644 --- a/labellerr/services/video_sampling/ssim.py +++ b/labellerr/services/video_sampling/ssim.py @@ -30,7 +30,6 @@ class SSIMSceneDetect(Singleton): def detect_and_extract( self, video_path: str, - file_id: str, threshold: float = 0.6, resize_dim: tuple = (320, 240) ) -> DetectionResult: @@ -39,13 +38,14 @@ def detect_and_extract( Args: video_path: Path to the video file - file_id: Unique identifier for the video (used as output folder name) 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] output_folder = file_id # Create output folder @@ -211,7 +211,6 @@ def _save_json_mapping( # Detect and extract frames result = detector.detect_and_extract( video_path=video_path, - file_id="video_001", threshold=0.6, # Lower value = more sensitive to changes resize_dim=(320, 240) ) From 5a16b512012bbdd53b9b0a07b5214c352c026262 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:53:53 +0530 Subject: [PATCH 15/23] added the cookbook added the updated client.py added the restructure files --- labellerr/client.py | 3 + labellerr/core/files/base.py | 131 +++ labellerr/core/files/video_file.py | 211 ++++ labellerr/notebooks/SDK.ipynb | 1763 ++++++++++++++++++++++++++++ 4 files changed, 2108 insertions(+) create mode 100644 labellerr/core/files/base.py create mode 100644 labellerr/core/files/video_file.py create mode 100644 labellerr/notebooks/SDK.ipynb diff --git a/labellerr/client.py b/labellerr/client.py index c89865f..77dbbb1 100644 --- a/labellerr/client.py +++ b/labellerr/client.py @@ -29,6 +29,7 @@ def __init__( self, api_key, api_secret, + client_id, enable_connection_pooling=True, pool_connections=10, pool_maxsize=20, @@ -38,12 +39,14 @@ def __init__( :param api_key: The API key for authentication. :param api_secret: The API secret for authentication. + :param client_id: The client ID for the Labellerr account. :param enable_connection_pooling: Whether to enable connection pooling :param pool_connections: Number of connection pools to cache :param pool_maxsize: Maximum number of connections to save in the pool """ self.api_key = api_key self.api_secret = api_secret + self.client_id = client_id self.base_url = constants.BASE_URL self._session = None self._enable_pooling = enable_connection_pooling diff --git a/labellerr/core/files/base.py b/labellerr/core/files/base.py new file mode 100644 index 0000000..c62dcc9 --- /dev/null +++ b/labellerr/core/files/base.py @@ -0,0 +1,131 @@ +from labellerr.client import LabellerrClient +from labellerr.exceptions import LabellerrError +from labellerr import constants +import uuid +from abc import ABCMeta + + +class LabellerrFileMeta(ABCMeta): + """Metaclass that combines ABC functionality with factory pattern""" + + _registry = {} + + @classmethod + def register(cls, data_type, file_class): + """Register a file type handler""" + cls._registry[data_type.lower()] = file_class + + + def __call__(cls, client, file_id, project_id, dataset_id = None, **kwargs): + + if cls.__name__ != 'LabellerrFile': + + instance = cls.__new__(cls) + if isinstance(instance, cls): + instance.__init__(client, file_id, project_id, dataset_id=dataset_id, **kwargs) + return instance + + + try: + unique_id = str(uuid.uuid4()) + client_id = client.client_id + params = { + 'file_id': file_id, + 'include_answers': 'false', + 'project_id': project_id, + 'uuid': unique_id, + 'client_id': client_id + } + + # TODO: Add dataset_id to params based on precedence logic + # Priority: project_id > dataset_id + + url = f"{constants.BASE_URL}/data/file_data" + response = client.make_api_request(client_id, url, params, unique_id) + + # Extract data_type from response + file_metadata = response.get('file_metadata', {}) + data_type = response.get('data_type', '').lower() + + # print(f"Detected file type: {data_type}") + + file_class = cls._registry.get(data_type) + if file_class is None: + raise LabellerrError(f"Unsupported file type: {data_type}") + + return file_class(client, file_id, project_id, dataset_id=dataset_id, file_metadata=file_metadata) + + except Exception as e: + raise LabellerrError(f"Failed to create file instance: {str(e)}") + + + + # # Route to appropriate subclass + # if data_type == 'image': + # return LabellerrImageFile(client, file_id, project_id, dataset_id=dataset_id, + # file_metadata=file_metadata) + # elif data_type == 'video': + # return LabellerrVideoFile(client, file_id, project_id, dataset_id=dataset_id, + # file_metadata=file_metadata) + # else: + # raise LabellerrError(f"Unsupported file type: {data_type}") + + + # except Exception as e: + # raise LabellerrError(f"Failed to create file instance: {str(e)}") + + +class LabellerrFile(metaclass=LabellerrFileMeta): + """Base class for all Labellerr files with factory behavior""" + + def __init__(self, client: LabellerrClient, file_id: str, project_id: str, + dataset_id: str | None = None, **kwargs): + """ + Initialize base file attributes + + :param client: LabellerrClient instance + :param file_id: Unique file identifier + :param project_id: Project ID containing the file + :param dataset_id: Optional dataset ID + :param kwargs: Additional file data (file_metadata, response, etc.) + """ + self.client = client + self.file_id = file_id + self.project_id = project_id + self.client_id = client.client_id + self.dataset_id = dataset_id + + # Store metadata from factory creation + self.metadata = kwargs.get('file_metadata', {}) + + + def get_metadata(self, include_answers: bool = False): + """ + Refresh and retrieve file metadata from Labellerr API. + + :param include_answers: Whether to include annotation answers + :return: Dictionary containing file metadata + """ + try: + unique_id = str(uuid.uuid4()) + + params = { + 'file_id': self.file_id, + 'include_answers': str(include_answers).lower(), + 'project_id': self.project_id, + 'uuid': unique_id, + 'client_id': self.client_id + } + + # TODO: Add dataset_id handling if needed + + url = f"{constants.BASE_URL}/data/file_data" + response = self.client.make_api_request(self.client_id, url, params, unique_id) + + # Update cached metadata + self.metadata = response.get('file_metadata', {}) + + return response + + except Exception as e: + raise LabellerrError(f"Failed to fetch file metadata: {str(e)}") diff --git a/labellerr/core/files/video_file.py b/labellerr/core/files/video_file.py new file mode 100644 index 0000000..4a12513 --- /dev/null +++ b/labellerr/core/files/video_file.py @@ -0,0 +1,211 @@ +from labellerr.client import LabellerrClient +from labellerr.exceptions import LabellerrError +from labellerr import constants +import uuid +import os +import subprocess +import requests +from concurrent.futures import ThreadPoolExecutor, as_completed +from threading import Lock +from labellerr.core.files.base import LabellerrFile, LabellerrFileMeta + +class LabellerrVideoFile(LabellerrFile): + """Specialized class for handling video files including frame operations""" + + def __init__(self, client: LabellerrClient, file_id: str, project_id: str, dataset_id: str | None = None, **kwargs): + super().__init__(client, file_id, project_id, dataset_id=dataset_id, **kwargs) + + @property + def total_frames(self): + """Get total number of frames in the video.""" + return self.metadata.get('total_frames', 0) + + def get_frames(self, frame_start: int = 0, frame_end: int | None = None): + """ + Retrieve video frames data from Labellerr API. + + :param frame_start: Starting frame index (default: 0) + :param frame_end: Ending frame index (default: total_frames) + :return: Dictionary containing video frames data with frame numbers as keys and URLs as values + """ + try: + if self.dataset_id is None: + raise ValueError("dataset_id is required for fetching video frames") + + # Use total_frames as default for frame_end + if frame_end is None: + frame_end = self.total_frames + + unique_id = str(uuid.uuid4()) + url = f"{constants.BASE_URL}/data/video_frames" + + params = { + 'dataset_id': self.dataset_id, + 'file_id': self.file_id, + 'frame_start': frame_start, + 'frame_end': frame_end, + 'project_id': self.project_id, + 'uuid': unique_id, + 'client_id': self.client_id + } + + response = self.client.make_api_request(self.client_id, url, params, unique_id) + + return response + + except Exception as e: + raise LabellerrError(f"Failed to fetch video frames data: {str(e)}") + + def _download_single_frame(self, frame_number, frame_url, save_path, print_lock): + """ + Download a single frame (helper method for threading). + + :param frame_number: Frame number + :param frame_url: URL to download from + :param save_path: Directory to save the frame + :param print_lock: Lock for thread-safe printing + :return: Tuple of (success: bool, frame_number: str, error_info: dict or None) + """ + try: + filename = f"{frame_number}.jpg" + filepath = os.path.join(save_path, filename) + + response = requests.get(frame_url, timeout=30) + + if response.status_code == 200: + with open(filepath, 'wb') as f: + f.write(response.content) + + with print_lock: + print(f"Downloaded: {filename}") + + return True, frame_number, None + else: + error_info = { + 'frame': frame_number, + 'status': response.status_code + } + with print_lock: + print(f"Failed to download frame {frame_number}: Status {response.status_code}") + + return False, frame_number, error_info + + except Exception as e: + error_info = { + 'frame': frame_number, + 'error': str(e) + } + with print_lock: + print(f"Error downloading frame {frame_number}: {str(e)}") + + return False, frame_number, error_info + + def download_frames(self, frames_data: dict, output_folder: str | None = None, + max_workers: int = 30): + """ + Download video frames from URLs to a local folder using multithreading. + + :param frames_data: Dictionary with frame numbers as keys and URLs as values + :param output_folder: Base folder path where frames will be saved (default: current directory) + :param max_workers: Maximum number of concurrent download threads (default: 10) + :return: Dictionary with download statistics + """ + try: + # Use file_id as folder name + folder_name = self.file_id + + # Set output path + if output_folder: + save_path = os.path.join(output_folder, folder_name) + else: + save_path = folder_name + + # Create directory if it doesn't exist + os.makedirs(save_path, exist_ok=True) + + success_count = 0 + failed_frames = [] + print_lock = Lock() + + print(f"Downloading {len(frames_data)} frames to: {save_path}") + print(f"Using {max_workers} concurrent threads") + + # Use ThreadPoolExecutor for concurrent downloads + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # Submit all download tasks + future_to_frame = { + executor.submit( + self._download_single_frame, + frame_number, + frame_url, + save_path, + print_lock + ): frame_number + for frame_number, frame_url in frames_data.items() + } + + # Process completed downloads + for future in as_completed(future_to_frame): + success, frame_number, error_info = future.result() + + if success: + success_count += 1 + else: + failed_frames.append(error_info) + + result = { + 'file_id': self.file_id, + 'total_frames': len(frames_data), + 'successful_downloads': success_count, + 'failed_downloads': len(failed_frames), + 'save_path': save_path, + 'failed_frames': failed_frames + } + + # print(f"\nDownload complete: {success_count}/{len(frames_data)} frames downloaded successfully") + + return result + + except Exception as e: + raise LabellerrError(f"Failed to download video frames: {str(e)}") + + def create_video(self, frames_folder: str, + framerate: int = 30, pattern: str = "%d.jpg", output_file: str | None = None): + """ + Join frames into a video using ffmpeg. + + :param frames_folder: Path to folder containing sequential frames (e.g., 1.jpg, 2.jpg). + :param output_file: Name of the output video file (default: output.mp4). + :param framerate: Desired video framerate (default: 30 fps). + :param pattern: Pattern for sequential frames (default: %d.jpg → 1.jpg, 2.jpg, ...). + :return: Path to created video file + """ + if frames_folder is None: + raise ValueError("frames_folder must be provided") + + input_pattern = os.path.join(frames_folder, pattern) + if output_file is None: + output_file = f"{self.file_id}.mp4" + + # FFmpeg command + command = [ + "ffmpeg", + "-y", # Overwrite output file if exists + "-start_number", "0", + "-framerate", str(framerate), + "-i", input_pattern, + "-c:v", "libx264", + "-pix_fmt", "yuv420p", + output_file + ] + + try: + print("Running command:", " ".join(command)) + subprocess.run(command, check=True) + print(f"Video saved as {output_file}") + return output_file + except subprocess.CalledProcessError as e: + raise LabellerrError(f"Error while joining frames: {str(e)}") + + +LabellerrFileMeta.register('video', LabellerrVideoFile) diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb new file mode 100644 index 0000000..81cdfad --- /dev/null +++ b/labellerr/notebooks/SDK.ipynb @@ -0,0 +1,1763 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d6488b6b", + "metadata": {}, + "source": [ + "# " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "edcdab6a", + "metadata": {}, + "outputs": [], + "source": [ + "from labellerr.client import LabellerrClient\n", + "from labellerr.core.files import LabellerrFile" + ] + }, + { + "cell_type": "markdown", + "id": "84b7917a", + "metadata": {}, + "source": [ + "### Fill your credentials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab12f168", + "metadata": {}, + "outputs": [], + "source": [ + "api_key = \"\"\n", + "api_secret = \"\"\n", + "client_id = \"\"" + ] + }, + { + "cell_type": "markdown", + "id": "3d05bd0f", + "metadata": {}, + "source": [ + "### Fill the ids" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "07dcfae9", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "dataset_id = \"16257fd6-b91b-4d00-a680-9ece9f3f241c\"\n", + "project_id = \"gabrila_artificial_duck_74237\"\n", + "file_id = \"c44f38f6-0186-436f-8c2d-ffb50a539c76\"" + ] + }, + { + "cell_type": "markdown", + "id": "1b2c7aee", + "metadata": {}, + "source": [ + "### Create LabellerrClient Instance" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9eaec7e1", + "metadata": {}, + "outputs": [], + "source": [ + "client = LabellerrClient(api_key=api_key, api_secret=api_secret, client_id=client_id)" + ] + }, + { + "cell_type": "markdown", + "id": "41b440b4", + "metadata": {}, + "source": [ + "### Create a LabellerrFile Instance" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "aaa7120e", + "metadata": {}, + "outputs": [], + "source": [ + "# create file instance\n", + "file = LabellerrFile(client=client, file_id=file_id, project_id=project_id, dataset_id=dataset_id)" + ] + }, + { + "cell_type": "markdown", + "id": "eb6d5547", + "metadata": {}, + "source": [ + "### Use that to retrive file metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d6cccc14", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'audio_segments': 0,\n", + " 'file_metadata': {'duration': 60,\n", + " 'bitrate': 1525920,\n", + " 'audio_bitrate': 191999,\n", + " 'total_frames': 1440,\n", + " 'size': 1440,\n", + " 'height': 720,\n", + " 'sample_rate': 44100,\n", + " 'fps': 23,\n", + " 'audio_channels': 2,\n", + " 'width': 1280,\n", + " 'keyframes': []},\n", + " 'completed_at': 1759311661144,\n", + " 'email_id': 'yashsuman15@gmail.com',\n", + " 'frames_uri': 'labellerr-processed/videos/datasets/16257fd6-b91b-4d00-a680-9ece9f3f241c/files/c44f38f6-0186-436f-8c2d-ffb50a539c76/frames',\n", + " 'audio_uri': 'labellerr-processed/videos/datasets/16257fd6-b91b-4d00-a680-9ece9f3f241c/files/c44f38f6-0186-436f-8c2d-ffb50a539c76/audio',\n", + " 'project_id': 'gabrila_artificial_duck_74237',\n", + " 'dataset_id': '16257fd6-b91b-4d00-a680-9ece9f3f241c',\n", + " 'file_id': 'c44f38f6-0186-436f-8c2d-ffb50a539c76',\n", + " 'video_url': 'local_upload/b76cdf41-900f-40dd-8013-5008a328d122/video.mp4',\n", + " 'file_name': 'video.mp4',\n", + " 'status_code': 300,\n", + " 'file_reference': 'gs://labellerr-connector-files/local_upload/b76cdf41-900f-40dd-8013-5008a328d122/video.mp4',\n", + " 'video_processing_job_id': '8baadcf9-51d8-4995-b796-34dbe2bfadf2-c44f38f6-0186-436f-8c2d-ffb50a539c76',\n", + " 'file_name_original': 'video.mp4',\n", + " 'data_type': 'video',\n", + " 'created_by': 'yashsuman15@gmail.com',\n", + " 'total_frames': 1440,\n", + " 'connection_id': 'b76cdf41-900f-40dd-8013-5008a328d122',\n", + " 'created_at': 1759311650156,\n", + " 'updated_at': 1759311661144,\n", + " 'annotation_rotation_count': 0,\n", + " 'status': 'assigned',\n", + " 'es_multimodal_index': False}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.get_metadata()" + ] + }, + { + "cell_type": "markdown", + "id": "36ffba0a", + "metadata": {}, + "source": [ + "### Download video frames" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b123193c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloading 1440 frames to: c44f38f6-0186-436f-8c2d-ffb50a539c76\n", + "Using 30 concurrent threads\n", + "Downloaded: 0.jpg\n", + "Downloaded: 4.jpg\n", + "Downloaded: 3.jpg\n", + "Downloaded: 2.jpg\n", + "Downloaded: 6.jpg\n", + "Downloaded: 14.jpg\n", + "Downloaded: 10.jpg\n", + "Downloaded: 24.jpg\n", + "Downloaded: 20.jpg\n", + "Downloaded: 23.jpg\n", + "Downloaded: 16.jpg\n", + "Downloaded: 12.jpg\n", + "Downloaded: 8.jpg\n", + "Downloaded: 7.jpg\n", + "Downloaded: 9.jpg\n", + "Downloaded: 17.jpg\n", + "Downloaded: 28.jpg\n", + "Downloaded: 25.jpg\n", + "Downloaded: 5.jpg\n", + "Downloaded: 27.jpg\n", + "Downloaded: 13.jpg\n", + "Downloaded: 18.jpg\n", + "Downloaded: 26.jpg\n", + "Downloaded: 32.jpg\n", + "Downloaded: 33.jpg\n", + "Downloaded: 35.jpg\n", + "Downloaded: 36.jpg\n", + "Downloaded: 31.jpg\n", + "Downloaded: 43.jpg\n", + "Downloaded: 45.jpg\n", + "Downloaded: 38.jpg\n", + "Downloaded: 48.jpg\n", + "Downloaded: 1.jpg\n", + "Downloaded: 44.jpg\n", + "Downloaded: 37.jpg\n", + "Downloaded: 39.jpg\n", + "Downloaded: 52.jpg\n", + "Downloaded: 22.jpg\n", + "Downloaded: 15.jpg\n", + "Downloaded: 19.jpg\n", + "Downloaded: 46.jpg\n", + "Downloaded: 51.jpg\n", + "Downloaded: 41.jpg\n", + "Downloaded: 21.jpg\n", + "Downloaded: 29.jpg\n", + "Downloaded: 11.jpg\n", + "Downloaded: 57.jpg\n", + "Downloaded: 55.jpg\n", + "Downloaded: 54.jpg\n", + "Downloaded: 30.jpg\n", + "Downloaded: 56.jpg\n", + "Downloaded: 34.jpg\n", + "Downloaded: 53.jpg\n", + "Downloaded: 61.jpg\n", + "Downloaded: 60.jpg\n", + "Downloaded: 62.jpg\n", + "Downloaded: 59.jpg\n", + "Downloaded: 40.jpg\n", + "Downloaded: 47.jpg\n", + "Downloaded: 58.jpg\n", + "Downloaded: 42.jpg\n", + "Downloaded: 49.jpg\n", + "Downloaded: 50.jpg\n", + "Downloaded: 65.jpg\n", + "Downloaded: 63.jpg\n", + "Downloaded: 69.jpg\n", + "Downloaded: 72.jpg\n", + "Downloaded: 68.jpg\n", + "Downloaded: 70.jpg\n", + "Downloaded: 67.jpg\n", + "Downloaded: 66.jpg\n", + "Downloaded: 75.jpg\n", + "Downloaded: 73.jpg\n", + "Downloaded: 83.jpg\n", + "Downloaded: 71.jpg\n", + "Downloaded: 84.jpg\n", + "Downloaded: 81.jpg\n", + "Downloaded: 76.jpg\n", + "Downloaded: 77.jpg\n", + "Downloaded: 82.jpg\n", + "Downloaded: 85.jpg\n", + "Downloaded: 78.jpg\n", + "Downloaded: 91.jpg\n", + "Downloaded: 86.jpg\n", + "Downloaded: 79.jpg\n", + "Downloaded: 80.jpg\n", + "Downloaded: 88.jpg\n", + "Downloaded: 92.jpg\n", + "Downloaded: 95.jpg\n", + "Downloaded: 93.jpg\n", + "Downloaded: 96.jpg\n", + "Downloaded: 94.jpg\n", + "Downloaded: 74.jpg\n", + "Downloaded: 64.jpg\n", + "Downloaded: 99.jpg\n", + "Downloaded: 101.jpg\n", + "Downloaded: 97.jpg\n", + "Downloaded: 100.jpg\n", + "Downloaded: 103.jpg\n", + "Downloaded: 107.jpg\n", + "Downloaded: 105.jpg\n", + "Downloaded: 104.jpg\n", + "Downloaded: 120.jpg\n", + "Downloaded: 109.jpg\n", + "Downloaded: 87.jpg\n", + "Downloaded: 114.jpg\n", + "Downloaded: 110.jpg\n", + "Downloaded: 119.jpg\n", + "Downloaded: 113.jpg\n", + "Downloaded: 116.jpg\n", + "Downloaded: 111.jpg\n", + "Downloaded: 115.jpg\n", + "Downloaded: 106.jpg\n", + "Downloaded: 90.jpg\n", + "Downloaded: 89.jpg\n", + "Downloaded: 108.jpg\n", + "Downloaded: 123.jpg\n", + "Downloaded: 121.jpg\n", + "Downloaded: 117.jpg\n", + "Downloaded: 98.jpg\n", + "Downloaded: 126.jpg\n", + "Downloaded: 125.jpg\n", + "Downloaded: 124.jpg\n", + "Downloaded: 127.jpg\n", + "Downloaded: 118.jpg\n", + "Downloaded: 102.jpg\n", + "Downloaded: 112.jpg\n", + "Downloaded: 122.jpg\n", + "Downloaded: 130.jpg\n", + "Downloaded: 133.jpg\n", + "Downloaded: 132.jpg\n", + "Downloaded: 134.jpg\n", + "Downloaded: 136.jpg\n", + "Downloaded: 140.jpg\n", + "Downloaded: 129.jpg\n", + "Downloaded: 137.jpg\n", + "Downloaded: 139.jpg\n", + "Downloaded: 138.jpg\n", + "Downloaded: 151.jpg\n", + "Downloaded: 135.jpg\n", + "Downloaded: 146.jpg\n", + "Downloaded: 145.jpg\n", + "Downloaded: 143.jpg\n", + "Downloaded: 147.jpg\n", + "Downloaded: 149.jpg\n", + "Downloaded: 148.jpg\n", + "Downloaded: 152.jpg\n", + "Downloaded: 144.jpg\n", + "Downloaded: 155.jpg\n", + "Downloaded: 153.jpg\n", + "Downloaded: 141.jpg\n", + "Downloaded: 156.jpg\n", + "Downloaded: 157.jpg\n", + "Downloaded: 128.jpg\n", + "Downloaded: 131.jpg\n", + "Downloaded: 164.jpg\n", + "Downloaded: 159.jpg\n", + "Downloaded: 160.jpg\n", + "Downloaded: 158.jpg\n", + "Downloaded: 162.jpg\n", + "Downloaded: 169.jpg\n", + "Downloaded: 166.jpg\n", + "Downloaded: 163.jpg\n", + "Downloaded: 165.jpg\n", + "Downloaded: 168.jpg\n", + "Downloaded: 167.jpg\n", + "Downloaded: 171.jpg\n", + "Downloaded: 170.jpg\n", + "Downloaded: 172.jpg\n", + "Downloaded: 174.jpg\n", + "Downloaded: 173.jpg\n", + "Downloaded: 176.jpg\n", + "Downloaded: 175.jpg\n", + "Downloaded: 142.jpg\n", + "Downloaded: 179.jpg\n", + "Downloaded: 177.jpg\n", + "Downloaded: 154.jpg\n", + "Downloaded: 150.jpg\n", + "Downloaded: 180.jpg\n", + "Downloaded: 182.jpg\n", + "Downloaded: 183.jpg\n", + "Downloaded: 184.jpg\n", + "Downloaded: 185.jpg\n", + "Downloaded: 161.jpg\n", + "Downloaded: 186.jpg\n", + "Downloaded: 190.jpg\n", + "Downloaded: 188.jpg\n", + "Downloaded: 196.jpg\n", + "Downloaded: 194.jpg\n", + "Downloaded: 195.jpg\n", + "Downloaded: 193.jpg\n", + "Downloaded: 200.jpg\n", + "Downloaded: 199.jpg\n", + "Downloaded: 202.jpg\n", + "Downloaded: 204.jpg\n", + "Downloaded: 201.jpg\n", + "Downloaded: 197.jpg\n", + "Downloaded: 208.jpg\n", + "Downloaded: 205.jpg\n", + "Downloaded: 207.jpg\n", + "Downloaded: 178.jpg\n", + "Downloaded: 206.jpg\n", + "Downloaded: 181.jpg\n", + "Downloaded: 209.jpg\n", + "Downloaded: 191.jpg\n", + "Downloaded: 210.jpg\n", + "Downloaded: 211.jpg\n", + "Downloaded: 213.jpg\n", + "Downloaded: 212.jpg\n", + "Downloaded: 219.jpg\n", + "Downloaded: 218.jpg\n", + "Downloaded: 216.jpg\n", + "Downloaded: 220.jpg\n", + "Downloaded: 215.jpg\n", + "Downloaded: 217.jpg\n", + "Downloaded: 187.jpg\n", + "Downloaded: 189.jpg\n", + "Downloaded: 221.jpg\n", + "Downloaded: 229.jpg\n", + "Downloaded: 223.jpg\n", + "Downloaded: 222.jpg\n", + "Downloaded: 228.jpg\n", + "Downloaded: 192.jpg\n", + "Downloaded: 198.jpg\n", + "Downloaded: 224.jpg\n", + "Downloaded: 231.jpg\n", + "Downloaded: 203.jpg\n", + "Downloaded: 232.jpg\n", + "Downloaded: 233.jpg\n", + "Downloaded: 234.jpg\n", + "Downloaded: 235.jpg\n", + "Downloaded: 236.jpg\n", + "Downloaded: 238.jpg\n", + "Downloaded: 241.jpg\n", + "Downloaded: 239.jpg\n", + "Downloaded: 242.jpg\n", + "Downloaded: 243.jpg\n", + "Downloaded: 244.jpg\n", + "Downloaded: 214.jpg\n", + "Downloaded: 248.jpg\n", + "Downloaded: 251.jpg\n", + "Downloaded: 225.jpg\n", + "Downloaded: 249.jpg\n", + "Downloaded: 247.jpg\n", + "Downloaded: 256.jpg\n", + "Downloaded: 227.jpg\n", + "Downloaded: 257.jpg\n", + "Downloaded: 250.jpg\n", + "Downloaded: 253.jpg\n", + "Downloaded: 226.jpg\n", + "Downloaded: 230.jpg\n", + "Downloaded: 254.jpg\n", + "Downloaded: 258.jpg\n", + "Downloaded: 260.jpg\n", + "Downloaded: 262.jpg\n", + "Downloaded: 261.jpg\n", + "Downloaded: 263.jpg\n", + "Downloaded: 237.jpg\n", + "Downloaded: 240.jpg\n", + "Downloaded: 245.jpg\n", + "Downloaded: 246.jpg\n", + "Downloaded: 264.jpg\n", + "Downloaded: 252.jpg\n", + "Downloaded: 255.jpg\n", + "Downloaded: 265.jpg\n", + "Downloaded: 268.jpg\n", + "Downloaded: 259.jpg\n", + "Downloaded: 269.jpg\n", + "Downloaded: 270.jpg\n", + "Downloaded: 267.jpg\n", + "Downloaded: 272.jpg\n", + "Downloaded: 275.jpg\n", + "Downloaded: 271.jpg\n", + "Downloaded: 274.jpg\n", + "Downloaded: 276.jpg\n", + "Downloaded: 277.jpg\n", + "Downloaded: 281.jpg\n", + "Downloaded: 273.jpg\n", + "Downloaded: 282.jpg\n", + "Downloaded: 278.jpg\n", + "Downloaded: 279.jpg\n", + "Downloaded: 280.jpg\n", + "Downloaded: 283.jpg\n", + "Downloaded: 284.jpg\n", + "Downloaded: 285.jpg\n", + "Downloaded: 286.jpg\n", + "Downloaded: 289.jpg\n", + "Downloaded: 290.jpg\n", + "Downloaded: 291.jpg\n", + "Downloaded: 287.jpg\n", + "Downloaded: 292.jpg\n", + "Downloaded: 293.jpg\n", + "Downloaded: 301.jpg\n", + "Downloaded: 303.jpg\n", + "Downloaded: 304.jpg\n", + "Downloaded: 302.jpg\n", + "Downloaded: 300.jpg\n", + "Downloaded: 266.jpg\n", + "Downloaded: 295.jpg\n", + "Downloaded: 294.jpg\n", + "Downloaded: 299.jpg\n", + "Downloaded: 297.jpg\n", + "Downloaded: 309.jpg\n", + "Downloaded: 296.jpg\n", + "Downloaded: 298.jpg\n", + "Downloaded: 306.jpg\n", + "Downloaded: 305.jpg\n", + "Downloaded: 307.jpg\n", + "Downloaded: 308.jpg\n", + "Downloaded: 310.jpg\n", + "Downloaded: 313.jpg\n", + "Downloaded: 314.jpg\n", + "Downloaded: 315.jpg\n", + "Downloaded: 316.jpg\n", + "Downloaded: 319.jpg\n", + "Downloaded: 321.jpg\n", + "Downloaded: 317.jpg\n", + "Downloaded: 320.jpg\n", + "Downloaded: 322.jpg\n", + "Downloaded: 323.jpg\n", + "Downloaded: 325.jpg\n", + "Downloaded: 324.jpg\n", + "Downloaded: 288.jpg\n", + "Downloaded: 329.jpg\n", + "Downloaded: 326.jpg\n", + "Downloaded: 328.jpg\n", + "Downloaded: 327.jpg\n", + "Downloaded: 330.jpg\n", + "Downloaded: 331.jpg\n", + "Downloaded: 333.jpg\n", + "Downloaded: 332.jpg\n", + "Downloaded: 334.jpg\n", + "Downloaded: 341.jpg\n", + "Downloaded: 339.jpg\n", + "Downloaded: 338.jpg\n", + "Downloaded: 337.jpg\n", + "Downloaded: 340.jpg\n", + "Downloaded: 343.jpg\n", + "Downloaded: 347.jpg\n", + "Downloaded: 312.jpg\n", + "Downloaded: 344.jpg\n", + "Downloaded: 345.jpg\n", + "Downloaded: 311.jpg\n", + "Downloaded: 348.jpg\n", + "Downloaded: 346.jpg\n", + "Downloaded: 352.jpg\n", + "Downloaded: 318.jpg\n", + "Downloaded: 351.jpg\n", + "Downloaded: 354.jpg\n", + "Downloaded: 358.jpg\n", + "Downloaded: 355.jpg\n", + "Downloaded: 350.jpg\n", + "Downloaded: 356.jpg\n", + "Downloaded: 357.jpg\n", + "Downloaded: 361.jpg\n", + "Downloaded: 362.jpg\n", + "Downloaded: 365.jpg\n", + "Downloaded: 364.jpg\n", + "Downloaded: 363.jpg\n", + "Downloaded: 366.jpg\n", + "Downloaded: 367.jpg\n", + "Downloaded: 335.jpg\n", + "Downloaded: 336.jpg\n", + "Downloaded: 371.jpg\n", + "Downloaded: 370.jpg\n", + "Downloaded: 342.jpg\n", + "Downloaded: 369.jpg\n", + "Downloaded: 375.jpg\n", + "Downloaded: 372.jpg\n", + "Downloaded: 374.jpg\n", + "Downloaded: 373.jpg\n", + "Downloaded: 378.jpg\n", + "Downloaded: 377.jpg\n", + "Downloaded: 376.jpg\n", + "Downloaded: 379.jpg\n", + "Downloaded: 380.jpg\n", + "Downloaded: 349.jpg\n", + "Downloaded: 359.jpg\n", + "Downloaded: 353.jpg\n", + "Downloaded: 382.jpg\n", + "Downloaded: 360.jpg\n", + "Downloaded: 383.jpg\n", + "Downloaded: 385.jpg\n", + "Downloaded: 384.jpg\n", + "Downloaded: 387.jpg\n", + "Downloaded: 368.jpg\n", + "Downloaded: 388.jpg\n", + "Downloaded: 389.jpg\n", + "Downloaded: 397.jpg\n", + "Downloaded: 390.jpg\n", + "Downloaded: 392.jpg\n", + "Downloaded: 395.jpg\n", + "Downloaded: 394.jpg\n", + "Downloaded: 396.jpg\n", + "Downloaded: 402.jpg\n", + "Downloaded: 398.jpg\n", + "Downloaded: 391.jpg\n", + "Downloaded: 399.jpg\n", + "Downloaded: 405.jpg\n", + "Downloaded: 404.jpg\n", + "Downloaded: 400.jpg\n", + "Downloaded: 408.jpg\n", + "Downloaded: 406.jpg\n", + "Downloaded: 409.jpg\n", + "Downloaded: 410.jpg\n", + "Downloaded: 381.jpg\n", + "Downloaded: 412.jpg\n", + "Downloaded: 411.jpg\n", + "Downloaded: 415.jpg\n", + "Downloaded: 413.jpg\n", + "Downloaded: 414.jpg\n", + "Downloaded: 386.jpg\n", + "Downloaded: 407.jpg\n", + "Downloaded: 417.jpg\n", + "Downloaded: 419.jpg\n", + "Downloaded: 420.jpg\n", + "Downloaded: 393.jpg\n", + "Downloaded: 422.jpg\n", + "Downloaded: 421.jpg\n", + "Downloaded: 433.jpg\n", + "Downloaded: 401.jpg\n", + "Downloaded: 428.jpg\n", + "Downloaded: 434.jpg\n", + "Downloaded: 427.jpg\n", + "Downloaded: 432.jpg\n", + "Downloaded: 426.jpg\n", + "Downloaded: 425.jpg\n", + "Downloaded: 423.jpg\n", + "Downloaded: 430.jpg\n", + "Downloaded: 437.jpg\n", + "Downloaded: 403.jpg\n", + "Downloaded: 435.jpg\n", + "Downloaded: 436.jpg\n", + "Downloaded: 440.jpg\n", + "Downloaded: 441.jpg\n", + "Downloaded: 442.jpg\n", + "Downloaded: 416.jpg\n", + "Downloaded: 443.jpg\n", + "Downloaded: 418.jpg\n", + "Downloaded: 445.jpg\n", + "Downloaded: 444.jpg\n", + "Downloaded: 446.jpg\n", + "Downloaded: 448.jpg\n", + "Downloaded: 455.jpg\n", + "Downloaded: 457.jpg\n", + "Downloaded: 431.jpg\n", + "Downloaded: 424.jpg\n", + "Downloaded: 449.jpg\n", + "Downloaded: 429.jpg\n", + "Downloaded: 458.jpg\n", + "Downloaded: 462.jpg\n", + "Downloaded: 463.jpg\n", + "Downloaded: 453.jpg\n", + "Downloaded: 451.jpg\n", + "Downloaded: 450.jpg\n", + "Downloaded: 459.jpg\n", + "Downloaded: 456.jpg\n", + "Downloaded: 464.jpg\n", + "Downloaded: 454.jpg\n", + "Downloaded: 438.jpg\n", + "Downloaded: 439.jpg\n", + "Downloaded: 467.jpg\n", + "Downloaded: 466.jpg\n", + "Downloaded: 479.jpg\n", + "Downloaded: 477.jpg\n", + "Downloaded: 482.jpg\n", + "Downloaded: 474.jpg\n", + "Downloaded: 476.jpg\n", + "Downloaded: 484.jpg\n", + "Downloaded: 473.jpg\n", + "Downloaded: 486.jpg\n", + "Downloaded: 488.jpg\n", + "Downloaded: 472.jpg\n", + "Downloaded: 471.jpg\n", + "Downloaded: 487.jpg\n", + "Downloaded: 475.jpg\n", + "Downloaded: 468.jpg\n", + "Downloaded: 489.jpg\n", + "Downloaded: 447.jpg\n", + "Downloaded: 469.jpg\n", + "Downloaded: 452.jpg\n", + "Downloaded: 461.jpg\n", + "Downloaded: 460.jpg\n", + "Downloaded: 491.jpg\n", + "Downloaded: 493.jpg\n", + "Downloaded: 465.jpg\n", + "Downloaded: 496.jpg\n", + "Downloaded: 494.jpg\n", + "Downloaded: 498.jpg\n", + "Downloaded: 499.jpg\n", + "Downloaded: 501.jpg\n", + "Downloaded: 509.jpg\n", + "Downloaded: 510.jpg\n", + "Downloaded: 507.jpg\n", + "Downloaded: 503.jpg\n", + "Downloaded: 505.jpg\n", + "Downloaded: 497.jpg\n", + "Downloaded: 508.jpg\n", + "Downloaded: 481.jpg\n", + "Downloaded: 480.jpg\n", + "Downloaded: 512.jpg\n", + "Downloaded: 500.jpg\n", + "Downloaded: 478.jpg\n", + "Downloaded: 485.jpg\n", + "Downloaded: 504.jpg\n", + "Downloaded: 483.jpg\n", + "Downloaded: 513.jpg\n", + "Downloaded: 490.jpg\n", + "Downloaded: 495.jpg\n", + "Downloaded: 511.jpg\n", + "Downloaded: 470.jpg\n", + "Downloaded: 492.jpg\n", + "Downloaded: 502.jpg\n", + "Downloaded: 506.jpg\n", + "Downloaded: 514.jpg\n", + "Downloaded: 517.jpg\n", + "Downloaded: 520.jpg\n", + "Downloaded: 518.jpg\n", + "Downloaded: 516.jpg\n", + "Downloaded: 522.jpg\n", + "Downloaded: 519.jpg\n", + "Downloaded: 523.jpg\n", + "Downloaded: 521.jpg\n", + "Downloaded: 526.jpg\n", + "Downloaded: 527.jpg\n", + "Downloaded: 530.jpg\n", + "Downloaded: 528.jpg\n", + "Downloaded: 533.jpg\n", + "Downloaded: 536.jpg\n", + "Downloaded: 531.jpg\n", + "Downloaded: 537.jpg\n", + "Downloaded: 539.jpg\n", + "Downloaded: 535.jpg\n", + "Downloaded: 529.jpg\n", + "Downloaded: 534.jpg\n", + "Downloaded: 538.jpg\n", + "Downloaded: 540.jpg\n", + "Downloaded: 541.jpg\n", + "Downloaded: 543.jpg\n", + "Downloaded: 542.jpg\n", + "Downloaded: 544.jpg\n", + "Downloaded: 552.jpg\n", + "Downloaded: 545.jpg\n", + "Downloaded: 546.jpg\n", + "Downloaded: 547.jpg\n", + "Downloaded: 554.jpg\n", + "Downloaded: 549.jpg\n", + "Downloaded: 551.jpg\n", + "Downloaded: 550.jpg\n", + "Downloaded: 553.jpg\n", + "Downloaded: 555.jpg\n", + "Downloaded: 548.jpg\n", + "Downloaded: 556.jpg\n", + "Downloaded: 557.jpg\n", + "Downloaded: 561.jpg\n", + "Downloaded: 563.jpg\n", + "Downloaded: 562.jpg\n", + "Downloaded: 565.jpg\n", + "Downloaded: 566.jpg\n", + "Downloaded: 515.jpg\n", + "Downloaded: 525.jpg\n", + "Downloaded: 524.jpg\n", + "Downloaded: 532.jpg\n", + "Downloaded: 567.jpg\n", + "Downloaded: 569.jpg\n", + "Downloaded: 568.jpg\n", + "Downloaded: 570.jpg\n", + "Downloaded: 575.jpg\n", + "Downloaded: 577.jpg\n", + "Downloaded: 576.jpg\n", + "Downloaded: 578.jpg\n", + "Downloaded: 581.jpg\n", + "Downloaded: 579.jpg\n", + "Downloaded: 583.jpg\n", + "Downloaded: 574.jpg\n", + "Downloaded: 580.jpg\n", + "Downloaded: 571.jpg\n", + "Downloaded: 573.jpg\n", + "Downloaded: 582.jpg\n", + "Downloaded: 572.jpg\n", + "Downloaded: 585.jpg\n", + "Downloaded: 591.jpg\n", + "Downloaded: 590.jpg\n", + "Downloaded: 586.jpg\n", + "Downloaded: 587.jpg\n", + "Downloaded: 589.jpg\n", + "Downloaded: 593.jpg\n", + "Downloaded: 594.jpg\n", + "Downloaded: 595.jpg\n", + "Downloaded: 559.jpg\n", + "Downloaded: 558.jpg\n", + "Downloaded: 560.jpg\n", + "Downloaded: 564.jpg\n", + "Downloaded: 592.jpg\n", + "Downloaded: 596.jpg\n", + "Downloaded: 597.jpg\n", + "Downloaded: 602.jpg\n", + "Downloaded: 608.jpg\n", + "Downloaded: 605.jpg\n", + "Downloaded: 604.jpg\n", + "Downloaded: 598.jpg\n", + "Downloaded: 599.jpg\n", + "Downloaded: 612.jpg\n", + "Downloaded: 584.jpg\n", + "Downloaded: 610.jpg\n", + "Downloaded: 609.jpg\n", + "Downloaded: 616.jpg\n", + "Downloaded: 615.jpg\n", + "Downloaded: 601.jpg\n", + "Downloaded: 611.jpg\n", + "Downloaded: 613.jpg\n", + "Downloaded: 614.jpg\n", + "Downloaded: 617.jpg\n", + "Downloaded: 603.jpg\n", + "Downloaded: 600.jpg\n", + "Downloaded: 588.jpg\n", + "Downloaded: 618.jpg\n", + "Downloaded: 619.jpg\n", + "Downloaded: 621.jpg\n", + "Downloaded: 620.jpg\n", + "Downloaded: 622.jpg\n", + "Downloaded: 623.jpg\n", + "Downloaded: 606.jpg\n", + "Downloaded: 607.jpg\n", + "Downloaded: 624.jpg\n", + "Downloaded: 630.jpg\n", + "Downloaded: 627.jpg\n", + "Downloaded: 625.jpg\n", + "Downloaded: 631.jpg\n", + "Downloaded: 629.jpg\n", + "Downloaded: 633.jpg\n", + "Downloaded: 647.jpg\n", + "Downloaded: 650.jpg\n", + "Downloaded: 642.jpg\n", + "Downloaded: 626.jpg\n", + "Downloaded: 649.jpg\n", + "Downloaded: 648.jpg\n", + "Downloaded: 632.jpg\n", + "Downloaded: 636.jpg\n", + "Downloaded: 638.jpg\n", + "Downloaded: 651.jpg\n", + "Downloaded: 634.jpg\n", + "Downloaded: 640.jpg\n", + "Downloaded: 639.jpg\n", + "Downloaded: 637.jpg\n", + "Downloaded: 643.jpg\n", + "Downloaded: 645.jpg\n", + "Downloaded: 644.jpg\n", + "Downloaded: 641.jpg\n", + "Downloaded: 652.jpg\n", + "Downloaded: 635.jpg\n", + "Downloaded: 653.jpg\n", + "Downloaded: 657.jpg\n", + "Downloaded: 656.jpg\n", + "Downloaded: 658.jpg\n", + "Downloaded: 628.jpg\n", + "Downloaded: 660.jpg\n", + "Downloaded: 661.jpg\n", + "Downloaded: 663.jpg\n", + "Downloaded: 668.jpg\n", + "Downloaded: 664.jpg\n", + "Downloaded: 669.jpg\n", + "Downloaded: 665.jpg\n", + "Downloaded: 666.jpg\n", + "Downloaded: 671.jpg\n", + "Downloaded: 674.jpg\n", + "Downloaded: 678.jpg\n", + "Downloaded: 673.jpg\n", + "Downloaded: 677.jpg\n", + "Downloaded: 679.jpg\n", + "Downloaded: 675.jpg\n", + "Downloaded: 676.jpg\n", + "Downloaded: 646.jpg\n", + "Downloaded: 681.jpg\n", + "Downloaded: 682.jpg\n", + "Downloaded: 655.jpg\n", + "Downloaded: 683.jpg\n", + "Downloaded: 654.jpg\n", + "Downloaded: 684.jpg\n", + "Downloaded: 687.jpg\n", + "Downloaded: 659.jpg\n", + "Downloaded: 689.jpg\n", + "Downloaded: 691.jpg\n", + "Downloaded: 690.jpg\n", + "Downloaded: 672.jpg\n", + "Downloaded: 667.jpg\n", + "Downloaded: 662.jpg\n", + "Downloaded: 701.jpg\n", + "Downloaded: 699.jpg\n", + "Downloaded: 700.jpg\n", + "Downloaded: 697.jpg\n", + "Downloaded: 688.jpg\n", + "Downloaded: 670.jpg\n", + "Downloaded: 694.jpg\n", + "Downloaded: 693.jpg\n", + "Downloaded: 696.jpg\n", + "Downloaded: 692.jpg\n", + "Downloaded: 702.jpg\n", + "Downloaded: 695.jpg\n", + "Downloaded: 706.jpg\n", + "Downloaded: 704.jpg\n", + "Downloaded: 680.jpg\n", + "Downloaded: 698.jpg\n", + "Downloaded: 707.jpg\n", + "Downloaded: 705.jpg\n", + "Downloaded: 708.jpg\n", + "Downloaded: 711.jpg\n", + "Downloaded: 713.jpg\n", + "Downloaded: 712.jpg\n", + "Downloaded: 719.jpg\n", + "Downloaded: 720.jpg\n", + "Downloaded: 685.jpg\n", + "Downloaded: 718.jpg\n", + "Downloaded: 686.jpg\n", + "Downloaded: 721.jpg\n", + "Downloaded: 723.jpg\n", + "Downloaded: 727.jpg\n", + "Downloaded: 724.jpg\n", + "Downloaded: 729.jpg\n", + "Downloaded: 726.jpg\n", + "Downloaded: 730.jpg\n", + "Downloaded: 728.jpg\n", + "Downloaded: 703.jpg\n", + "Downloaded: 733.jpg\n", + "Downloaded: 734.jpg\n", + "Downloaded: 736.jpg\n", + "Downloaded: 739.jpg\n", + "Downloaded: 740.jpg\n", + "Downloaded: 741.jpg\n", + "Downloaded: 742.jpg\n", + "Downloaded: 738.jpg\n", + "Downloaded: 743.jpg\n", + "Downloaded: 709.jpg\n", + "Downloaded: 710.jpg\n", + "Downloaded: 744.jpg\n", + "Downloaded: 714.jpg\n", + "Downloaded: 717.jpg\n", + "Downloaded: 716.jpg\n", + "Downloaded: 745.jpg\n", + "Downloaded: 750.jpg\n", + "Downloaded: 749.jpg\n", + "Downloaded: 747.jpg\n", + "Downloaded: 751.jpg\n", + "Downloaded: 715.jpg\n", + "Downloaded: 722.jpg\n", + "Downloaded: 753.jpg\n", + "Downloaded: 752.jpg\n", + "Downloaded: 754.jpg\n", + "Downloaded: 725.jpg\n", + "Downloaded: 731.jpg\n", + "Downloaded: 732.jpg\n", + "Downloaded: 755.jpg\n", + "Downloaded: 735.jpg\n", + "Downloaded: 737.jpg\n", + "Downloaded: 756.jpg\n", + "Downloaded: 757.jpg\n", + "Downloaded: 758.jpg\n", + "Downloaded: 775.jpg\n", + "Downloaded: 759.jpg\n", + "Downloaded: 760.jpg\n", + "Downloaded: 763.jpg\n", + "Downloaded: 762.jpg\n", + "Downloaded: 761.jpg\n", + "Downloaded: 778.jpg\n", + "Downloaded: 764.jpg\n", + "Downloaded: 766.jpg\n", + "Downloaded: 765.jpg\n", + "Downloaded: 776.jpg\n", + "Downloaded: 777.jpg\n", + "Downloaded: 768.jpg\n", + "Downloaded: 746.jpg\n", + "Downloaded: 767.jpg\n", + "Downloaded: 771.jpg\n", + "Downloaded: 748.jpg\n", + "Downloaded: 773.jpg\n", + "Downloaded: 770.jpg\n", + "Downloaded: 779.jpg\n", + "Downloaded: 769.jpg\n", + "Downloaded: 781.jpg\n", + "Downloaded: 780.jpg\n", + "Downloaded: 782.jpg\n", + "Downloaded: 774.jpg\n", + "Downloaded: 783.jpg\n", + "Downloaded: 784.jpg\n", + "Downloaded: 786.jpg\n", + "Downloaded: 792.jpg\n", + "Downloaded: 790.jpg\n", + "Downloaded: 787.jpg\n", + "Downloaded: 788.jpg\n", + "Downloaded: 789.jpg\n", + "Downloaded: 793.jpg\n", + "Downloaded: 801.jpg\n", + "Downloaded: 795.jpg\n", + "Downloaded: 798.jpg\n", + "Downloaded: 800.jpg\n", + "Downloaded: 791.jpg\n", + "Downloaded: 797.jpg\n", + "Downloaded: 796.jpg\n", + "Downloaded: 794.jpg\n", + "Downloaded: 803.jpg\n", + "Downloaded: 804.jpg\n", + "Downloaded: 799.jpg\n", + "Downloaded: 802.jpg\n", + "Downloaded: 805.jpg\n", + "Downloaded: 806.jpg\n", + "Downloaded: 809.jpg\n", + "Downloaded: 810.jpg\n", + "Downloaded: 807.jpg\n", + "Downloaded: 772.jpg\n", + "Downloaded: 812.jpg\n", + "Downloaded: 813.jpg\n", + "Downloaded: 814.jpg\n", + "Downloaded: 785.jpg\n", + "Downloaded: 816.jpg\n", + "Downloaded: 815.jpg\n", + "Downloaded: 824.jpg\n", + "Downloaded: 821.jpg\n", + "Downloaded: 827.jpg\n", + "Downloaded: 822.jpg\n", + "Downloaded: 823.jpg\n", + "Downloaded: 817.jpg\n", + "Downloaded: 819.jpg\n", + "Downloaded: 818.jpg\n", + "Downloaded: 834.jpg\n", + "Downloaded: 833.jpg\n", + "Downloaded: 835.jpg\n", + "Downloaded: 836.jpg\n", + "Downloaded: 837.jpg\n", + "Downloaded: 832.jpg\n", + "Downloaded: 829.jpg\n", + "Downloaded: 838.jpg\n", + "Downloaded: 831.jpg\n", + "Downloaded: 830.jpg\n", + "Downloaded: 811.jpg\n", + "Downloaded: 839.jpg\n", + "Downloaded: 808.jpg\n", + "Downloaded: 840.jpg\n", + "Downloaded: 841.jpg\n", + "Downloaded: 842.jpg\n", + "Downloaded: 826.jpg\n", + "Downloaded: 858.jpg\n", + "Downloaded: 825.jpg\n", + "Downloaded: 820.jpg\n", + "Downloaded: 845.jpg\n", + "Downloaded: 828.jpg\n", + "Downloaded: 843.jpg\n", + "Downloaded: 848.jpg\n", + "Downloaded: 844.jpg\n", + "Downloaded: 860.jpg\n", + "Downloaded: 861.jpg\n", + "Downloaded: 854.jpg\n", + "Downloaded: 847.jpg\n", + "Downloaded: 852.jpg\n", + "Downloaded: 851.jpg\n", + "Downloaded: 862.jpg\n", + "Downloaded: 853.jpg\n", + "Downloaded: 857.jpg\n", + "Downloaded: 859.jpg\n", + "Downloaded: 856.jpg\n", + "Downloaded: 855.jpg\n", + "Downloaded: 863.jpg\n", + "Downloaded: 846.jpg\n", + "Downloaded: 864.jpg\n", + "Downloaded: 849.jpg\n", + "Downloaded: 865.jpg\n", + "Downloaded: 866.jpg\n", + "Downloaded: 867.jpg\n", + "Downloaded: 868.jpg\n", + "Downloaded: 880.jpg\n", + "Downloaded: 878.jpg\n", + "Downloaded: 879.jpg\n", + "Downloaded: 885.jpg\n", + "Downloaded: 883.jpg\n", + "Downloaded: 882.jpg\n", + "Downloaded: 884.jpg\n", + "Downloaded: 875.jpg\n", + "Downloaded: 881.jpg\n", + "Downloaded: 876.jpg\n", + "Downloaded: 888.jpg\n", + "Downloaded: 886.jpg\n", + "Downloaded: 889.jpg\n", + "Downloaded: 890.jpg\n", + "Downloaded: 877.jpg\n", + "Downloaded: 892.jpg\n", + "Downloaded: 869.jpg\n", + "Downloaded: 891.jpg\n", + "Downloaded: 871.jpg\n", + "Downloaded: 870.jpg\n", + "Downloaded: 874.jpg\n", + "Downloaded: 872.jpg\n", + "Downloaded: 873.jpg\n", + "Downloaded: 850.jpg\n", + "Downloaded: 894.jpg\n", + "Downloaded: 893.jpg\n", + "Downloaded: 897.jpg\n", + "Downloaded: 887.jpg\n", + "Downloaded: 898.jpg\n", + "Downloaded: 917.jpg\n", + "Downloaded: 915.jpg\n", + "Downloaded: 899.jpg\n", + "Downloaded: 916.jpg\n", + "Downloaded: 911.jpg\n", + "Downloaded: 901.jpg\n", + "Downloaded: 919.jpg\n", + "Downloaded: 909.jpg\n", + "Downloaded: 907.jpg\n", + "Downloaded: 903.jpg\n", + "Downloaded: 908.jpg\n", + "Downloaded: 904.jpg\n", + "Downloaded: 918.jpg\n", + "Downloaded: 910.jpg\n", + "Downloaded: 905.jpg\n", + "Downloaded: 906.jpg\n", + "Downloaded: 913.jpg\n", + "Downloaded: 912.jpg\n", + "Downloaded: 914.jpg\n", + "Downloaded: 921.jpg\n", + "Downloaded: 920.jpg\n", + "Downloaded: 922.jpg\n", + "Downloaded: 895.jpg\n", + "Downloaded: 896.jpg\n", + "Downloaded: 924.jpg\n", + "Downloaded: 925.jpg\n", + "Downloaded: 929.jpg\n", + "Downloaded: 930.jpg\n", + "Downloaded: 928.jpg\n", + "Downloaded: 927.jpg\n", + "Downloaded: 934.jpg\n", + "Downloaded: 900.jpg\n", + "Downloaded: 933.jpg\n", + "Downloaded: 931.jpg\n", + "Downloaded: 941.jpg\n", + "Downloaded: 937.jpg\n", + "Downloaded: 939.jpg\n", + "Downloaded: 940.jpg\n", + "Downloaded: 902.jpg\n", + "Downloaded: 932.jpg\n", + "Downloaded: 943.jpg\n", + "Downloaded: 944.jpg\n", + "Downloaded: 938.jpg\n", + "Downloaded: 946.jpg\n", + "Downloaded: 936.jpg\n", + "Downloaded: 935.jpg\n", + "Downloaded: 947.jpg\n", + "Downloaded: 948.jpg\n", + "Downloaded: 949.jpg\n", + "Downloaded: 923.jpg\n", + "Downloaded: 950.jpg\n", + "Downloaded: 951.jpg\n", + "Downloaded: 961.jpg\n", + "Downloaded: 953.jpg\n", + "Downloaded: 954.jpg\n", + "Downloaded: 952.jpg\n", + "Downloaded: 959.jpg\n", + "Downloaded: 958.jpg\n", + "Downloaded: 955.jpg\n", + "Downloaded: 956.jpg\n", + "Downloaded: 957.jpg\n", + "Downloaded: 942.jpg\n", + "Downloaded: 967.jpg\n", + "Downloaded: 962.jpg\n", + "Downloaded: 965.jpg\n", + "Downloaded: 964.jpg\n", + "Downloaded: 969.jpg\n", + "Downloaded: 971.jpg\n", + "Downloaded: 945.jpg\n", + "Downloaded: 963.jpg\n", + "Downloaded: 970.jpg\n", + "Downloaded: 972.jpg\n", + "Downloaded: 926.jpg\n", + "Downloaded: 968.jpg\n", + "Downloaded: 974.jpg\n", + "Downloaded: 966.jpg\n", + "Downloaded: 975.jpg\n", + "Downloaded: 976.jpg\n", + "Downloaded: 977.jpg\n", + "Downloaded: 978.jpg\n", + "Downloaded: 980.jpg\n", + "Downloaded: 983.jpg\n", + "Downloaded: 982.jpg\n", + "Downloaded: 979.jpg\n", + "Downloaded: 985.jpg\n", + "Downloaded: 987.jpg\n", + "Downloaded: 981.jpg\n", + "Downloaded: 960.jpg\n", + "Downloaded: 984.jpg\n", + "Downloaded: 994.jpg\n", + "Downloaded: 995.jpg\n", + "Downloaded: 986.jpg\n", + "Downloaded: 989.jpg\n", + "Downloaded: 998.jpg\n", + "Downloaded: 999.jpg\n", + "Downloaded: 997.jpg\n", + "Downloaded: 996.jpg\n", + "Downloaded: 993.jpg\n", + "Downloaded: 990.jpg\n", + "Downloaded: 991.jpg\n", + "Downloaded: 1001.jpg\n", + "Downloaded: 1002.jpg\n", + "Downloaded: 1003.jpg\n", + "Downloaded: 973.jpg\n", + "Downloaded: 1004.jpg\n", + "Downloaded: 1005.jpg\n", + "Downloaded: 1006.jpg\n", + "Downloaded: 1015.jpg\n", + "Downloaded: 1014.jpg\n", + "Downloaded: 1016.jpg\n", + "Downloaded: 1013.jpg\n", + "Downloaded: 1020.jpg\n", + "Downloaded: 1018.jpg\n", + "Downloaded: 1012.jpg\n", + "Downloaded: 1010.jpg\n", + "Downloaded: 1022.jpg\n", + "Downloaded: 988.jpg\n", + "Downloaded: 1019.jpg\n", + "Downloaded: 1021.jpg\n", + "Downloaded: 1023.jpg\n", + "Downloaded: 1007.jpg\n", + "Downloaded: 1026.jpg\n", + "Downloaded: 1009.jpg\n", + "Downloaded: 1024.jpg\n", + "Downloaded: 1028.jpg\n", + "Downloaded: 1025.jpg\n", + "Downloaded: 1008.jpg\n", + "Downloaded: 1030.jpg\n", + "Downloaded: 1000.jpg\n", + "Downloaded: 992.jpg\n", + "Downloaded: 1031.jpg\n", + "Downloaded: 1032.jpg\n", + "Downloaded: 1029.jpg\n", + "Downloaded: 1052.jpg\n", + "Downloaded: 1033.jpg\n", + "Downloaded: 1041.jpg\n", + "Downloaded: 1035.jpg\n", + "Downloaded: 1043.jpg\n", + "Downloaded: 1042.jpg\n", + "Downloaded: 1040.jpg\n", + "Downloaded: 1044.jpg\n", + "Downloaded: 1038.jpg\n", + "Downloaded: 1037.jpg\n", + "Downloaded: 1034.jpg\n", + "Downloaded: 1050.jpg\n", + "Downloaded: 1039.jpg\n", + "Downloaded: 1046.jpg\n", + "Downloaded: 1045.jpg\n", + "Downloaded: 1048.jpg\n", + "Downloaded: 1051.jpg\n", + "Downloaded: 1049.jpg\n", + "Downloaded: 1047.jpg\n", + "Downloaded: 1011.jpg\n", + "Downloaded: 1017.jpg\n", + "Downloaded: 1053.jpg\n", + "Downloaded: 1027.jpg\n", + "Downloaded: 1054.jpg\n", + "Downloaded: 1055.jpg\n", + "Downloaded: 1056.jpg\n", + "Downloaded: 1057.jpg\n", + "Downloaded: 1059.jpg\n", + "Downloaded: 1060.jpg\n", + "Downloaded: 1036.jpg\n", + "Downloaded: 1080.jpg\n", + "Downloaded: 1062.jpg\n", + "Downloaded: 1064.jpg\n", + "Downloaded: 1069.jpg\n", + "Downloaded: 1067.jpg\n", + "Downloaded: 1068.jpg\n", + "Downloaded: 1066.jpg\n", + "Downloaded: 1065.jpg\n", + "Downloaded: 1063.jpg\n", + "Downloaded: 1070.jpg\n", + "Downloaded: 1077.jpg\n", + "Downloaded: 1071.jpg\n", + "Downloaded: 1073.jpg\n", + "Downloaded: 1081.jpg\n", + "Downloaded: 1074.jpg\n", + "Downloaded: 1076.jpg\n", + "Downloaded: 1075.jpg\n", + "Downloaded: 1078.jpg\n", + "Downloaded: 1082.jpg\n", + "Downloaded: 1079.jpg\n", + "Downloaded: 1072.jpg\n", + "Downloaded: 1058.jpg\n", + "Downloaded: 1084.jpg\n", + "Downloaded: 1085.jpg\n", + "Downloaded: 1087.jpg\n", + "Downloaded: 1086.jpg\n", + "Downloaded: 1106.jpg\n", + "Downloaded: 1108.jpg\n", + "Downloaded: 1109.jpg\n", + "Downloaded: 1088.jpg\n", + "Downloaded: 1107.jpg\n", + "Downloaded: 1110.jpg\n", + "Downloaded: 1112.jpg\n", + "Downloaded: 1111.jpg\n", + "Downloaded: 1089.jpg\n", + "Downloaded: 1061.jpg\n", + "Downloaded: 1115.jpg\n", + "Downloaded: 1114.jpg\n", + "Downloaded: 1091.jpg\n", + "Downloaded: 1090.jpg\n", + "Downloaded: 1092.jpg\n", + "Downloaded: 1094.jpg\n", + "Downloaded: 1093.jpg\n", + "Downloaded: 1096.jpg\n", + "Downloaded: 1095.jpg\n", + "Downloaded: 1100.jpg\n", + "Downloaded: 1097.jpg\n", + "Downloaded: 1104.jpg\n", + "Downloaded: 1083.jpg\n", + "Downloaded: 1101.jpg\n", + "Downloaded: 1103.jpg\n", + "Downloaded: 1098.jpg\n", + "Downloaded: 1102.jpg\n", + "Downloaded: 1099.jpg\n", + "Downloaded: 1105.jpg\n", + "Downloaded: 1116.jpg\n", + "Downloaded: 1118.jpg\n", + "Downloaded: 1117.jpg\n", + "Downloaded: 1120.jpg\n", + "Downloaded: 1125.jpg\n", + "Downloaded: 1123.jpg\n", + "Downloaded: 1122.jpg\n", + "Downloaded: 1126.jpg\n", + "Downloaded: 1127.jpg\n", + "Downloaded: 1124.jpg\n", + "Downloaded: 1121.jpg\n", + "Downloaded: 1129.jpg\n", + "Downloaded: 1113.jpg\n", + "Downloaded: 1132.jpg\n", + "Downloaded: 1128.jpg\n", + "Downloaded: 1134.jpg\n", + "Downloaded: 1131.jpg\n", + "Downloaded: 1135.jpg\n", + "Downloaded: 1133.jpg\n", + "Downloaded: 1139.jpg\n", + "Downloaded: 1138.jpg\n", + "Downloaded: 1130.jpg\n", + "Downloaded: 1140.jpg\n", + "Downloaded: 1137.jpg\n", + "Downloaded: 1143.jpg\n", + "Downloaded: 1142.jpg\n", + "Downloaded: 1144.jpg\n", + "Downloaded: 1145.jpg\n", + "Downloaded: 1146.jpg\n", + "Downloaded: 1147.jpg\n", + "Downloaded: 1148.jpg\n", + "Downloaded: 1136.jpg\n", + "Downloaded: 1119.jpg\n", + "Downloaded: 1157.jpg\n", + "Downloaded: 1149.jpg\n", + "Downloaded: 1158.jpg\n", + "Downloaded: 1150.jpg\n", + "Downloaded: 1160.jpg\n", + "Downloaded: 1155.jpg\n", + "Downloaded: 1162.jpg\n", + "Downloaded: 1154.jpg\n", + "Downloaded: 1156.jpg\n", + "Downloaded: 1151.jpg\n", + "Downloaded: 1164.jpg\n", + "Downloaded: 1166.jpg\n", + "Downloaded: 1152.jpg\n", + "Downloaded: 1167.jpg\n", + "Downloaded: 1170.jpg\n", + "Downloaded: 1169.jpg\n", + "Downloaded: 1163.jpg\n", + "Downloaded: 1168.jpg\n", + "Downloaded: 1173.jpg\n", + "Downloaded: 1141.jpg\n", + "Downloaded: 1177.jpg\n", + "Downloaded: 1181.jpg\n", + "Downloaded: 1180.jpg\n", + "Downloaded: 1185.jpg\n", + "Downloaded: 1179.jpg\n", + "Downloaded: 1183.jpg\n", + "Downloaded: 1182.jpg\n", + "Downloaded: 1184.jpg\n", + "Downloaded: 1187.jpg\n", + "Downloaded: 1186.jpg\n", + "Downloaded: 1188.jpg\n", + "Downloaded: 1192.jpg\n", + "Downloaded: 1190.jpg\n", + "Downloaded: 1191.jpg\n", + "Downloaded: 1189.jpg\n", + "Downloaded: 1194.jpg\n", + "Downloaded: 1193.jpg\n", + "Downloaded: 1159.jpg\n", + "Downloaded: 1161.jpg\n", + "Downloaded: 1195.jpg\n", + "Downloaded: 1153.jpg\n", + "Downloaded: 1171.jpg\n", + "Downloaded: 1196.jpg\n", + "Downloaded: 1165.jpg\n", + "Downloaded: 1197.jpg\n", + "Downloaded: 1198.jpg\n", + "Downloaded: 1172.jpg\n", + "Downloaded: 1200.jpg\n", + "Downloaded: 1175.jpg\n", + "Downloaded: 1199.jpg\n", + "Downloaded: 1202.jpg\n", + "Downloaded: 1176.jpg\n", + "Downloaded: 1174.jpg\n", + "Downloaded: 1205.jpg\n", + "Downloaded: 1178.jpg\n", + "Downloaded: 1204.jpg\n", + "Downloaded: 1206.jpg\n", + "Downloaded: 1207.jpg\n", + "Downloaded: 1208.jpg\n", + "Downloaded: 1218.jpg\n", + "Downloaded: 1210.jpg\n", + "Downloaded: 1212.jpg\n", + "Downloaded: 1215.jpg\n", + "Downloaded: 1213.jpg\n", + "Downloaded: 1209.jpg\n", + "Downloaded: 1216.jpg\n", + "Downloaded: 1219.jpg\n", + "Downloaded: 1217.jpg\n", + "Downloaded: 1220.jpg\n", + "Downloaded: 1223.jpg\n", + "Downloaded: 1222.jpg\n", + "Downloaded: 1221.jpg\n", + "Downloaded: 1225.jpg\n", + "Downloaded: 1226.jpg\n", + "Downloaded: 1224.jpg\n", + "Downloaded: 1233.jpg\n", + "Downloaded: 1227.jpg\n", + "Downloaded: 1234.jpg\n", + "Downloaded: 1228.jpg\n", + "Downloaded: 1237.jpg\n", + "Downloaded: 1235.jpg\n", + "Downloaded: 1236.jpg\n", + "Downloaded: 1230.jpg\n", + "Downloaded: 1241.jpg\n", + "Downloaded: 1201.jpg\n", + "Downloaded: 1232.jpg\n", + "Downloaded: 1231.jpg\n", + "Downloaded: 1242.jpg\n", + "Downloaded: 1238.jpg\n", + "Downloaded: 1240.jpg\n", + "Downloaded: 1244.jpg\n", + "Downloaded: 1243.jpg\n", + "Downloaded: 1203.jpg\n", + "Downloaded: 1245.jpg\n", + "Downloaded: 1211.jpg\n", + "Downloaded: 1246.jpg\n", + "Downloaded: 1214.jpg\n", + "Downloaded: 1247.jpg\n", + "Downloaded: 1248.jpg\n", + "Downloaded: 1249.jpg\n", + "Downloaded: 1257.jpg\n", + "Downloaded: 1263.jpg\n", + "Downloaded: 1259.jpg\n", + "Downloaded: 1262.jpg\n", + "Downloaded: 1264.jpg\n", + "Downloaded: 1266.jpg\n", + "Downloaded: 1252.jpg\n", + "Downloaded: 1261.jpg\n", + "Downloaded: 1265.jpg\n", + "Downloaded: 1260.jpg\n", + "Downloaded: 1258.jpg\n", + "Downloaded: 1250.jpg\n", + "Downloaded: 1267.jpg\n", + "Downloaded: 1269.jpg\n", + "Downloaded: 1271.jpg\n", + "Downloaded: 1270.jpg\n", + "Downloaded: 1272.jpg\n", + "Downloaded: 1253.jpg\n", + "Downloaded: 1229.jpg\n", + "Downloaded: 1254.jpg\n", + "Downloaded: 1239.jpg\n", + "Downloaded: 1255.jpg\n", + "Downloaded: 1274.jpg\n", + "Downloaded: 1273.jpg\n", + "Downloaded: 1256.jpg\n", + "Downloaded: 1275.jpg\n", + "Downloaded: 1278.jpg\n", + "Downloaded: 1277.jpg\n", + "Downloaded: 1283.jpg\n", + "Downloaded: 1288.jpg\n", + "Downloaded: 1282.jpg\n", + "Downloaded: 1289.jpg\n", + "Downloaded: 1284.jpg\n", + "Downloaded: 1285.jpg\n", + "Downloaded: 1279.jpg\n", + "Downloaded: 1281.jpg\n", + "Downloaded: 1290.jpg\n", + "Downloaded: 1286.jpg\n", + "Downloaded: 1292.jpg\n", + "Downloaded: 1291.jpg\n", + "Downloaded: 1293.jpg\n", + "Downloaded: 1294.jpg\n", + "Downloaded: 1295.jpg\n", + "Downloaded: 1296.jpg\n", + "Downloaded: 1297.jpg\n", + "Downloaded: 1298.jpg\n", + "Downloaded: 1299.jpg\n", + "Downloaded: 1302.jpg\n", + "Downloaded: 1301.jpg\n", + "Downloaded: 1251.jpg\n", + "Downloaded: 1300.jpg\n", + "Downloaded: 1303.jpg\n", + "Downloaded: 1268.jpg\n", + "Downloaded: 1320.jpg\n", + "Downloaded: 1319.jpg\n", + "Downloaded: 1315.jpg\n", + "Downloaded: 1316.jpg\n", + "Downloaded: 1314.jpg\n", + "Downloaded: 1321.jpg\n", + "Downloaded: 1313.jpg\n", + "Downloaded: 1312.jpg\n", + "Downloaded: 1322.jpg\n", + "Downloaded: 1309.jpg\n", + "Downloaded: 1304.jpg\n", + "Downloaded: 1305.jpg\n", + "Downloaded: 1311.jpg\n", + "Downloaded: 1323.jpg\n", + "Downloaded: 1306.jpg\n", + "Downloaded: 1310.jpg\n", + "Downloaded: 1324.jpg\n", + "Downloaded: 1325.jpg\n", + "Downloaded: 1326.jpg\n", + "Downloaded: 1327.jpg\n", + "Downloaded: 1329.jpg\n", + "Downloaded: 1328.jpg\n", + "Downloaded: 1276.jpg\n", + "Downloaded: 1330.jpg\n", + "Downloaded: 1332.jpg\n", + "Downloaded: 1331.jpg\n", + "Downloaded: 1333.jpg\n", + "Downloaded: 1336.jpg\n", + "Downloaded: 1338.jpg\n", + "Downloaded: 1335.jpg\n", + "Downloaded: 1339.jpg\n", + "Downloaded: 1337.jpg\n", + "Downloaded: 1340.jpg\n", + "Downloaded: 1341.jpg\n", + "Downloaded: 1342.jpg\n", + "Downloaded: 1343.jpg\n", + "Downloaded: 1344.jpg\n", + "Downloaded: 1280.jpg\n", + "Downloaded: 1346.jpg\n", + "Downloaded: 1347.jpg\n", + "Downloaded: 1345.jpg\n", + "Downloaded: 1348.jpg\n", + "Downloaded: 1349.jpg\n", + "Downloaded: 1287.jpg\n", + "Downloaded: 1350.jpg\n", + "Downloaded: 1353.jpg\n", + "Downloaded: 1351.jpg\n", + "Downloaded: 1352.jpg\n", + "Downloaded: 1354.jpg\n", + "Downloaded: 1357.jpg\n", + "Downloaded: 1355.jpg\n", + "Downloaded: 1356.jpg\n", + "Downloaded: 1358.jpg\n", + "Downloaded: 1359.jpg\n", + "Downloaded: 1360.jpg\n", + "Downloaded: 1364.jpg\n", + "Downloaded: 1361.jpg\n", + "Downloaded: 1363.jpg\n", + "Downloaded: 1367.jpg\n", + "Downloaded: 1362.jpg\n", + "Downloaded: 1365.jpg\n", + "Downloaded: 1368.jpg\n", + "Downloaded: 1369.jpg\n", + "Downloaded: 1371.jpg\n", + "Downloaded: 1372.jpg\n", + "Downloaded: 1373.jpg\n", + "Downloaded: 1374.jpg\n", + "Downloaded: 1318.jpg\n", + "Downloaded: 1375.jpg\n", + "Downloaded: 1317.jpg\n", + "Downloaded: 1376.jpg\n", + "Downloaded: 1378.jpg\n", + "Downloaded: 1307.jpg\n", + "Downloaded: 1308.jpg\n", + "Downloaded: 1380.jpg\n", + "Downloaded: 1382.jpg\n", + "Downloaded: 1384.jpg\n", + "Downloaded: 1383.jpg\n", + "Downloaded: 1381.jpg\n", + "Downloaded: 1385.jpg\n", + "Downloaded: 1387.jpg\n", + "Downloaded: 1391.jpg\n", + "Downloaded: 1390.jpg\n", + "Downloaded: 1389.jpg\n", + "Downloaded: 1388.jpg\n", + "Downloaded: 1392.jpg\n", + "Downloaded: 1393.jpg\n", + "Downloaded: 1396.jpg\n", + "Downloaded: 1399.jpg\n", + "Downloaded: 1400.jpg\n", + "Downloaded: 1334.jpg\n", + "Downloaded: 1401.jpg\n", + "Downloaded: 1402.jpg\n", + "Downloaded: 1403.jpg\n", + "Downloaded: 1404.jpg\n", + "Downloaded: 1405.jpg\n", + "Downloaded: 1407.jpg\n", + "Downloaded: 1410.jpg\n", + "Downloaded: 1408.jpg\n", + "Downloaded: 1411.jpg\n", + "Downloaded: 1409.jpg\n", + "Downloaded: 1412.jpg\n", + "Downloaded: 1414.jpg\n", + "Downloaded: 1413.jpg\n", + "Downloaded: 1416.jpg\n", + "Downloaded: 1415.jpg\n", + "Downloaded: 1417.jpg\n", + "Downloaded: 1419.jpg\n", + "Downloaded: 1420.jpg\n", + "Downloaded: 1366.jpg\n", + "Downloaded: 1421.jpg\n", + "Downloaded: 1370.jpg\n", + "Downloaded: 1422.jpg\n", + "Downloaded: 1424.jpg\n", + "Downloaded: 1423.jpg\n", + "Downloaded: 1425.jpg\n", + "Downloaded: 1426.jpg\n", + "Downloaded: 1377.jpg\n", + "Downloaded: 1427.jpg\n", + "Downloaded: 1429.jpg\n", + "Downloaded: 1379.jpg\n", + "Downloaded: 1432.jpg\n", + "Downloaded: 1430.jpg\n", + "Downloaded: 1435.jpg\n", + "Downloaded: 1433.jpg\n", + "Downloaded: 1431.jpg\n", + "Downloaded: 1437.jpg\n", + "Downloaded: 1436.jpg\n", + "Downloaded: 1438.jpg\n", + "Downloaded: 1386.jpg\n", + "Downloaded: 1394.jpg\n", + "Downloaded: 1439.jpg\n", + "Downloaded: 1395.jpg\n", + "Downloaded: 1398.jpg\n", + "Downloaded: 1397.jpg\n", + "Downloaded: 1406.jpg\n", + "Downloaded: 1418.jpg\n", + "Downloaded: 1434.jpg\n", + "Downloaded: 1428.jpg\n" + ] + } + ], + "source": [ + "response = file.download_frames(file.get_frames())" + ] + }, + { + "cell_type": "markdown", + "id": "d1ce0bb4", + "metadata": {}, + "source": [ + "### Create video from frames" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e2d0f608", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running command: ffmpeg -y -start_number 0 -framerate 30 -i c44f38f6-0186-436f-8c2d-ffb50a539c76\\%d.jpg -c:v libx264 -pix_fmt yuv420p c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", + "Video saved as c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n" + ] + }, + { + "data": { + "text/plain": [ + "'c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.create_video(response['save_path'])" + ] + }, + { + "cell_type": "markdown", + "id": "f6db8522", + "metadata": {}, + "source": [ + "## Import Scene change detect Algo" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5c41073", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\HP\\.conda\\envs\\SDk\\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": [ + "from labellerr.services.video_sampling.pyscene_detect import PySceneDetect" + ] + }, + { + "cell_type": "markdown", + "id": "db88da50", + "metadata": {}, + "source": [ + "### Create instance of detector" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b908dbd3", + "metadata": {}, + "outputs": [], + "source": [ + "detector = PySceneDetect()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "a3052f25", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "JSON mapping saved to: c44f38f6-0186-436f-8c2d-ffb50a539c76\\c44f38f6-0186-436f-8c2d-ffb50a539c76_mapping.json\n" + ] + }, + { + "data": { + "text/plain": [ + "DetectionResult(file_id='c44f38f6-0186-436f-8c2d-ffb50a539c76', output_folder='c44f38f6-0186-436f-8c2d-ffb50a539c76', total_frames=1440, selected_frames=[SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\18.jpg', frame_no=18), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\50.jpg', frame_no=50), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\73.jpg', frame_no=73), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\91.jpg', frame_no=91), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\109.jpg', frame_no=109), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\130.jpg', frame_no=130), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\153.jpg', frame_no=153), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\199.jpg', frame_no=199), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\248.jpg', frame_no=248), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\322.jpg', frame_no=322), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\394.jpg', frame_no=394), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\432.jpg', frame_no=432), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\470.jpg', frame_no=470), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\496.jpg', frame_no=496), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\530.jpg', frame_no=530), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\563.jpg', frame_no=563), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\582.jpg', frame_no=582), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\604.jpg', frame_no=604), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\633.jpg', frame_no=633), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\665.jpg', frame_no=665), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\694.jpg', frame_no=694), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\713.jpg', frame_no=713), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\739.jpg', frame_no=739), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\767.jpg', frame_no=767), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\790.jpg', frame_no=790), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\814.jpg', frame_no=814), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\829.jpg', frame_no=829), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\847.jpg', frame_no=847), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\866.jpg', frame_no=866), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\884.jpg', frame_no=884), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\904.jpg', frame_no=904), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\932.jpg', frame_no=932), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\978.jpg', frame_no=978), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1034.jpg', frame_no=1034), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1071.jpg', frame_no=1071), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1094.jpg', frame_no=1094), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1112.jpg', frame_no=1112), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1128.jpg', frame_no=1128), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1147.jpg', frame_no=1147), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1166.jpg', frame_no=1166), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1182.jpg', frame_no=1182), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1203.jpg', frame_no=1203), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1225.jpg', frame_no=1225), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1245.jpg', frame_no=1245), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1267.jpg', frame_no=1267), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1359.jpg', frame_no=1359)])" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "video_path = r\"D:\\professional\\LABELLERR\\Task\\Repos\\SDKPython\\labellerr\\notebooks\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\"\n", + "\n", + "detector.detect_and_extract(video_path)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "SDk", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 375884552d932c104ffbd27d96fe6d6f09b38a67 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:54:37 +0530 Subject: [PATCH 16/23] minor update --- labellerr/core/files/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 labellerr/core/files/__init__.py diff --git a/labellerr/core/files/__init__.py b/labellerr/core/files/__init__.py new file mode 100644 index 0000000..51a721f --- /dev/null +++ b/labellerr/core/files/__init__.py @@ -0,0 +1,14 @@ +# Import base classes +from labellerr.core.files.base import LabellerrFile, LabellerrFileMeta + +# Import subclasses to trigger registration +# These imports register each file type with the metaclass +from labellerr.core.files.image_file import LabellerrImageFile +from labellerr.core.files.video_file import LabellerrVideoFile + +__all__ = [ + 'LabellerrFile', + 'LabellerrImageFile', + 'LabellerrVideoFile', + 'LabellerrFileMeta' +] \ No newline at end of file From 194cb26e3065e75ae78fb7152d1aa9a732df74e1 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Thu, 9 Oct 2025 02:22:18 +0530 Subject: [PATCH 17/23] minor chages to pyscene storing pattern --- labellerr/services/video_sampling/pyscene_detect.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/labellerr/services/video_sampling/pyscene_detect.py b/labellerr/services/video_sampling/pyscene_detect.py index f2d62f8..09e9f89 100644 --- a/labellerr/services/video_sampling/pyscene_detect.py +++ b/labellerr/services/video_sampling/pyscene_detect.py @@ -37,12 +37,15 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: """ # Derive file_id from video_path (base name without extension) file_id = os.path.splitext(os.path.basename(video_path))[0] - output_folder = file_id + + # Create base detect folder and file_id specific folder + base_detect_folder = "detects" + output_folder = os.path.join(base_detect_folder, file_id) # Detect scene transitions scenes = detect(video_path, AdaptiveDetector()) - # Create output folder + # Create nested output folders os.makedirs(output_folder, exist_ok=True) # Open video for frame extraction From 6d118b60b74bde42c7c87d982e1a7297000590d1 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Thu, 9 Oct 2025 18:37:26 +0530 Subject: [PATCH 18/23] added video dataset class, minor update video sampling algo --- labellerr/core/datasets/base.py | 194 +++++++++++ labellerr/core/files/video_file.py | 102 ++++++ .../services/labellerr_files/client_utils.py | 301 +++++++++++++----- labellerr/services/video_sampling/ffmpeg.py | 39 ++- labellerr/services/video_sampling/gemini.py | 139 ++++---- .../services/video_sampling/pyscene_detect.py | 13 +- 6 files changed, 621 insertions(+), 167 deletions(-) create mode 100644 labellerr/core/datasets/base.py diff --git a/labellerr/core/datasets/base.py b/labellerr/core/datasets/base.py new file mode 100644 index 0000000..0971e3c --- /dev/null +++ b/labellerr/core/datasets/base.py @@ -0,0 +1,194 @@ +from labellerr.client import LabellerrClient +from labellerr.exceptions import LabellerrError +from labellerr.core.files import LabellerrFile +from labellerr import constants +import uuid +from abc import ABCMeta +import pprint + +class LabellerrVideoDataset: + """ + Class for handling video dataset operations and fetching multiple video files. + """ + + def __init__(self, client: LabellerrClient, dataset_id: str, project_id: str): + """ + Initialize video dataset instance. + + :param client: LabellerrClient instance + :param dataset_id: Dataset ID + :param project_id: Project ID containing the dataset + """ + self.client = client + self.dataset_id = dataset_id + self.project_id = project_id + self.client_id = client.client_id + + def fetch_files(self, page_size: int = 1000): + """ + Fetch all video files in this dataset as LabellerrVideoFile instances. + + :param page_size: Number of files to fetch per API request (default: 10) + :return: List of file IDs + """ + try: + all_file_ids = [] + next_search_after = None # Start with None for first page + + while True: + unique_id = str(uuid.uuid4()) + url = f"{constants.BASE_URL}/search/files/all" + params = { + 'sort_by': 'created_at', + 'sort_order': 'desc', + 'size': page_size, + 'uuid': unique_id, + 'dataset_id': self.dataset_id, + 'client_id': self.client_id + } + + # Add next_search_after only if it exists (don't send on first request) + print(next_search_after) + if next_search_after: + url+= f"?next_search_after={next_search_after}" + + # print(params) + + response = self.client.make_api_request(self.client_id, url, params, unique_id) + + # pprint.pprint(response) + + # Extract files from the response + files = response.get('response', {}).get('files', []) + + # Collect file IDs + for file_info in files: + file_id = file_info.get('file_id') + if file_id: + all_file_ids.append(file_id) + + # Get next_search_after for pagination + next_search_after = response.get('response', {}).get('next_search_after') + + + # Break if no more pages or no files returned + if not next_search_after or not files: + print("No more pages to fetch.") + break + + print(f"Fetched total: {len(all_file_ids)}") + + print(f"Total file IDs extracted: {len(all_file_ids)}") + # return all_file_ids + + # Create LabellerrVideoFile instances for each file_id + video_files = [] + print(f"\nCreating LabellerrFile instances for {len(all_file_ids)} files...") + + for file_id in all_file_ids: + try: + video_file = LabellerrFile( + client=self.client, + file_id=file_id, + project_id=self.project_id, + dataset_id=self.dataset_id + ) + video_files.append(video_file) + except Exception as e: + print(f"Warning: Failed to create file instance for {file_id}: {str(e)}") + + print(f"Successfully created {len(video_files)} LabellerrFile instances") + return video_files + + except Exception as e: + raise LabellerrError(f"Failed to fetch dataset files: {str(e)}") + + def process_all_videos(self, output_folder: str, framerate: int = 30, + max_workers: int = 30): + """ + Process all video files in the dataset: download frames, create videos, + and automatically clean up temporary files. + + :param output_folder: Base folder where dataset folder will be created + :param framerate: Video framerate in fps (default: 30) + :param max_workers: Max concurrent download threads (default: 30) + :return: List of processing results for all files + """ + try: + print(f"\n{'#'*70}") + print(f"# Starting batch video processing for dataset: {self.dataset_id}") + print(f"# Output folder: {output_folder}") + print(f"{'#'*70}\n") + + # Fetch all video files + video_files = self.fetch_files() + + if not video_files: + print("No video files found in dataset") + return [] + + print(f"\nProcessing {len(video_files)} video files...\n") + + results = [] + successful = 0 + failed = 0 + + for idx, video_file in enumerate(video_files, 1): + try: + print(f"[{idx}/{len(video_files)}] Processing {video_file.file_id}...") + + # Call the new all-in-one method + result = video_file.download_create_video_auto_cleanup( + output_folder=output_folder, + framerate=framerate, + max_workers=max_workers + ) + + results.append(result) + successful += 1 + + except Exception as e: + error_result = { + 'status': 'failed', + 'file_id': video_file.file_id, + 'error': str(e) + } + results.append(error_result) + failed += 1 + print(f"✗ Error processing {video_file.file_id}: {str(e)}\n") + + # Summary + print(f"\n{'#'*70}") + print(f"# Batch Processing Complete") + print(f"# Total files: {len(video_files)}") + print(f"# Successful: {successful}") + print(f"# Failed: {failed}") + print(f"{'#'*70}\n") + + return results + + except Exception as e: + raise LabellerrError(f"Failed to process dataset videos: {str(e)}") + + +if __name__ == "__main__": + # Example usage (requires valid LabellerrClient instance) + api_key = "66f4d8.9f402742f58a89568f5bcc0f86" + api_secret = "1e2478b930d4a842a526beb585e60d2a9ee6a6f1e3aa89cb3c8ead751f418215" + client_id = "14078" + + dataset_id = "59438ec3-12e0-4687-8847-1e6e01b0bf25" + project_id = "farrah_supposed_hookworm_34155" + + client = LabellerrClient(api_key, api_secret, client_id) + + dataset = LabellerrVideoDataset(client, dataset_id, project_id) + + results = dataset.process_all_videos( + output_folder="./videos", + framerate=30, + max_workers=30 + ) + + pprint.pprint(results) + \ No newline at end of file diff --git a/labellerr/core/files/video_file.py b/labellerr/core/files/video_file.py index 4a12513..2fb7460 100644 --- a/labellerr/core/files/video_file.py +++ b/labellerr/core/files/video_file.py @@ -5,6 +5,7 @@ import os import subprocess import requests +import shutil from concurrent.futures import ThreadPoolExecutor, as_completed from threading import Lock from labellerr.core.files.base import LabellerrFile, LabellerrFileMeta @@ -207,5 +208,106 @@ def create_video(self, frames_folder: str, except subprocess.CalledProcessError as e: raise LabellerrError(f"Error while joining frames: {str(e)}") + def download_create_video_auto_cleanup(self, output_folder: str, + framerate: int = 30, + pattern: str = "%d.jpg", + max_workers: int = 30, + frame_start: int = 0, + frame_end: int | None = None): + """ + Download frames, create video, and automatically clean up temporary frames. + This is an all-in-one method for processing video files. + + :param output_folder: Base folder where video will be saved (organized by dataset_id) + :param framerate: Video framerate in fps (default: 30) + :param pattern: Frame filename pattern (default: "%d.jpg") + :param max_workers: Max concurrent download threads (default: 30) + :param frame_start: Starting frame index (default: 0) + :param frame_end: Ending frame index (default: total_frames) + :return: Dictionary with operation results + """ + try: + print(f"\n{'='*60}") + print(f"Processing file: {self.file_id}") + print(f"{'='*60}") + + # Step 1: Fetch frame data from API + print("\n[1/4] Fetching frame data from API...") + frames_response = self.get_frames(frame_start=frame_start, frame_end=frame_end) + frames_data = frames_response.get('frames', {}) + + if not frames_data: + raise LabellerrError("No frame data retrieved from API") + + print(f"Retrieved {len(frames_data)} frames") + + # Step 2: Create dataset folder structure + print(f"\n[2/4] Setting up output folders...") + dataset_folder = os.path.join(output_folder, self.dataset_id) + os.makedirs(dataset_folder, exist_ok=True) + + temp_frames_folder = os.path.join(dataset_folder, f".temp_{self.file_id}") + os.makedirs(temp_frames_folder, exist_ok=True) + print(f"Temporary frames folder: {temp_frames_folder}") + + # Step 3: Download frames to temporary location + print(f"\n[3/4] Downloading {len(frames_data)} frames...") + download_result = self.download_frames( + frames_data=frames_data, + output_folder=dataset_folder, + max_workers=max_workers + ) + + # Update temp folder path in result (since download_frames uses file_id as folder name) + actual_frames_folder = os.path.join(dataset_folder, self.file_id) + + if download_result['failed_downloads'] > 0: + print(f"Warning: {download_result['failed_downloads']} frames failed to download") + + # Step 4: Create video from downloaded frames + print(f"\n[4/4] Creating video from frames...") + video_output_path = os.path.join(dataset_folder, f"{self.file_id}.mp4") + + self.create_video( + frames_folder=actual_frames_folder, + framerate=framerate, + pattern=pattern, + output_file=video_output_path + ) + + # Step 5: Clean up temporary frames folder + print(f"\nCleaning up temporary frames...") + if os.path.exists(actual_frames_folder): + shutil.rmtree(actual_frames_folder) + print(f"Removed temporary frames folder: {actual_frames_folder}") + + result = { + 'status': 'success', + 'file_id': self.file_id, + 'dataset_id': self.dataset_id, + 'video_path': video_output_path, + 'output_folder': dataset_folder, + 'frames_downloaded': download_result['successful_downloads'], + 'frames_failed': download_result['failed_downloads'], + 'failed_frames_info': download_result['failed_frames'] + } + + print(f"\n{'='*60}") + print(f"✓ Processing complete!") + print(f"Video saved to: {video_output_path}") + print(f"{'='*60}\n") + + return result + + except Exception as e: + # Attempt cleanup on error + try: + if actual_frames_folder and os.path.exists(actual_frames_folder): + shutil.rmtree(actual_frames_folder) + except: + pass + + raise LabellerrError(f"Failed in video processing: {str(e)}") + LabellerrFileMeta.register('video', LabellerrVideoFile) diff --git a/labellerr/services/labellerr_files/client_utils.py b/labellerr/services/labellerr_files/client_utils.py index 3f3f950..8a8af1e 100644 --- a/labellerr/services/labellerr_files/client_utils.py +++ b/labellerr/services/labellerr_files/client_utils.py @@ -1,47 +1,117 @@ from labellerr.client import LabellerrClient from labellerr.exceptions import LabellerrError from labellerr import constants -from labellerr.base.singleton import Singleton import uuid import os import subprocess import requests from concurrent.futures import ThreadPoolExecutor, as_completed from threading import Lock +from abc import ABCMeta, abstractmethod - -class FileMetadataService(Singleton): +class LabellerrFileMeta(ABCMeta): + """Metaclass that combines ABC functionality with factory pattern""" - def __init__(self, client: LabellerrClient): - # Prevent re-initialization of singleton - if hasattr(self, '_initialized'): - return + def __call__(cls, client, file_id, project_id, dataset_id = None, **kwargs): + + if cls.__name__ != 'LabellerrFile': - if client is None: - raise ValueError("Client must be provided on first initialization") + instance = cls.__new__(cls) + if isinstance(instance, cls): + instance.__init__(client, file_id, project_id, dataset_id=dataset_id, **kwargs) + return instance + + try: + unique_id = str(uuid.uuid4()) + client_id = client.client_id + params = { + 'file_id': file_id, + 'include_answers': 'false', + 'project_id': project_id, + 'uuid': unique_id, + 'client_id': client_id + } + + # TODO: Add dataset_id to params based on precedence logic + # Priority: project_id > dataset_id + + url = f"{constants.BASE_URL}/data/file_data" + response = client.make_api_request(client_id, url, params, unique_id) + + # Extract data_type from response + file_metadata = response.get('file_metadata', {}) + data_type = response.get('data_type', '').lower() + + # print(f"Detected file type: {data_type}") + + # Route to appropriate subclass + if data_type == 'image': + return LabellerrImageFile(client, file_id, project_id, dataset_id=dataset_id, + file_metadata=file_metadata) + elif data_type == 'video': + return LabellerrVideoFile(client, file_id, project_id, dataset_id=dataset_id, + file_metadata=file_metadata) + elif data_type == 'pdf': + return LabellerrPDFFile(client, file_id, project_id, dataset_id=dataset_id, + file_metadata=file_metadata) + else: + raise LabellerrError(f"Unsupported file type: {data_type}") + + + except Exception as e: + raise LabellerrError(f"Failed to create file instance: {str(e)}") + + +class LabellerrFile(metaclass=LabellerrFileMeta): + """Base class for all Labellerr files with factory behavior""" + + def __init__(self, client: LabellerrClient, file_id: str, project_id: str, + dataset_id: str | None = None, **kwargs): + """ + Initialize base file attributes + + :param client: LabellerrClient instance + :param file_id: Unique file identifier + :param project_id: Project ID containing the file + :param dataset_id: Optional dataset ID + :param kwargs: Additional file data (file_metadata, response, etc.) + """ self.client = client - self._initialized = True + self.file_id = file_id + self.project_id = project_id + self.client_id = client.client_id + self.dataset_id = dataset_id - def get_file_metadata(self, client_id: str, file_id: str, project_id: str, include_answers: bool = False): + # Store metadata from factory creation + self.metadata = kwargs.get('file_metadata', {}) + + + def get_metadata(self, include_answers: bool = False): """ - Retrieve file metadata from Labellerr API. + Refresh and retrieve file metadata from Labellerr API. + + :param include_answers: Whether to include annotation answers + :return: Dictionary containing file metadata """ try: unique_id = str(uuid.uuid4()) - # Build query parameters - include client_id here params = { - 'file_id': file_id, + 'file_id': self.file_id, 'include_answers': str(include_answers).lower(), - 'project_id': project_id, + 'project_id': self.project_id, 'uuid': unique_id, - 'client_id': client_id + 'client_id': self.client_id } + # TODO: Add dataset_id handling if needed + url = f"{constants.BASE_URL}/data/file_data" + response = self.client.make_api_request(self.client_id, url, params, unique_id) - response = self.client.make_api_request(client_id, url, params, unique_id) + # Update cached metadata + self.metadata = response.get('file_metadata', {}) return response @@ -49,48 +119,50 @@ def get_file_metadata(self, client_id: str, file_id: str, project_id: str, inclu raise LabellerrError(f"Failed to fetch file metadata: {str(e)}") -class VideoFileService(FileMetadataService): - """ - Service class for handling video file operations including fetching frames, - downloading frames, and creating videos from frames. - """ +class LabellerrImageFile(LabellerrFile): + pass + +class LabellerrVideoFile(LabellerrFile): + """Specialized class for handling video files including frame operations""" - def __init__(self, client: LabellerrClient): - - super().__init__(client) - - def get_video_frames(self, client_id: str, file_id: str, project_id: str, dataset_id: str, - frame_start: int = 0, frame_end: int = None): + def __init__(self, client: LabellerrClient, file_id: str, project_id: str, dataset_id: str | None = None, **kwargs): + super().__init__(client, file_id, project_id, dataset_id=dataset_id, **kwargs) + + @property + def total_frames(self): + """Get total number of frames in the video.""" + return self.metadata.get('total_frames', 0) + + def get_frames(self, frame_start: int = 0, frame_end: int | None = None): """ Retrieve video frames data from Labellerr API. - :param client_id: Client ID - :param file_id: Unique file identifier in Labellerr - :param project_id: The project ID to which the file belongs - :param dataset_id: The dataset ID containing the video file :param frame_start: Starting frame index (default: 0) - :param frame_end: Ending frame index (if None, retrieves all frames from frame_start) - :return: Dictionary containing video frames data + :param frame_end: Ending frame index (default: total_frames) + :return: Dictionary containing video frames data with frame numbers as keys and URLs as values """ try: + if self.dataset_id is None: + raise ValueError("dataset_id is required for fetching video frames") + + # Use total_frames as default for frame_end + if frame_end is None: + frame_end = self.total_frames + unique_id = str(uuid.uuid4()) url = f"{constants.BASE_URL}/data/video_frames" - # Build query parameters params = { - 'dataset_id': dataset_id, - 'file_id': file_id, + 'dataset_id': self.dataset_id, + 'file_id': self.file_id, 'frame_start': frame_start, - 'project_id': project_id, + 'frame_end': frame_end, + 'project_id': self.project_id, 'uuid': unique_id, - 'client_id': client_id + 'client_id': self.client_id } - # Add frame_end only if specified - if frame_end is not None: - params['frame_end'] = frame_end - - response = self.client.make_api_request(client_id, url, params, unique_id) + response = self.client.make_api_request(self.client_id, url, params, unique_id) return response @@ -141,25 +213,21 @@ def _download_single_frame(self, frame_number, frame_url, save_path, print_lock) return False, frame_number, error_info - def download_video_frames(self, frames_data: dict, output_folder: str = None, - file_id: str = None, max_workers: int = 10): + def download_frames(self, frames_data: dict, output_folder: str | None = None, + max_workers: int = 10): """ Download video frames from URLs to a local folder using multithreading. :param frames_data: Dictionary with frame numbers as keys and URLs as values :param output_folder: Base folder path where frames will be saved (default: current directory) - :param file_id: File ID to use as folder name. If None, uses 'frames' as folder name :param max_workers: Maximum number of concurrent download threads (default: 10) :return: Dictionary with download statistics """ try: - # Determine folder name - if file_id: - folder_name = file_id - else: - folder_name = "frames" + # Use file_id as folder name + folder_name = self.file_id - # Set base output folder + # Set output path if output_folder: save_path = os.path.join(output_folder, folder_name) else: @@ -206,23 +274,23 @@ def download_video_frames(self, frames_data: dict, output_folder: str = None, 'failed_frames': failed_frames } - print(f"\nDownload complete: {success_count}/{len(frames_data)} frames downloaded successfully") + # print(f"\nDownload complete: {success_count}/{len(frames_data)} frames downloaded successfully") return result except Exception as e: raise LabellerrError(f"Failed to download video frames: {str(e)}") - def create_video_from_frames(self, frames_folder: str, output_file: str = "output.mp4", - framerate: int = 30, pattern: str = "%d.jpg"): + def create_video(self, frames_folder: str, output_file: str = "output.mp4", + framerate: int = 30, pattern: str = "%d.jpg"): """ Join frames into a video using ffmpeg. :param frames_folder: Path to folder containing sequential frames (e.g., 1.jpg, 2.jpg). :param output_file: Name of the output video file (default: output.mp4). :param framerate: Desired video framerate (default: 30 fps). - :param pattern: Pattern for sequential frames inside frames_folder - (default: %d.jpg → 1.jpg, 2.jpg, ...). + :param pattern: Pattern for sequential frames (default: %d.jpg → 1.jpg, 2.jpg, ...). + :return: Path to created video file """ if frames_folder is None: raise ValueError("frames_folder must be provided") @@ -244,38 +312,113 @@ def create_video_from_frames(self, frames_folder: str, output_file: str = "outpu print("Running command:", " ".join(command)) subprocess.run(command, check=True) print(f"Video saved as {output_file}") + return output_file except subprocess.CalledProcessError as e: raise LabellerrError(f"Error while joining frames: {str(e)}") - +class LabellerrVideoDataset: + """ + Class for handling video dataset operations and fetching multiple video files. + """ + + def __init__(self, client: LabellerrClient, dataset_id: str, project_id: str): + """ + Initialize video dataset instance. + + :param client: LabellerrClient instance + :param dataset_id: Dataset ID + :param project_id: Project ID containing the dataset + """ + self.client = client + self.dataset_id = dataset_id + self.project_id = project_id + self.client_id = client.client_id + + def fetch_files(self, limit: int | None = None, page_size: int = 10): + """ + Fetch all video files in this dataset as LabellerrVideoFile instances. + + :param limit: Maximum number of files to fetch (None for all) + :param page_size: Number of files to fetch per API request (default: 10) + :return: List of LabellerrVideoFile instances + """ + try: + all_file_ids = [] + next_search_after = "" # Start with empty string for first page + + # while True: + unique_id = str(uuid.uuid4()) + url = f"{constants.BASE_URL}/search/files/all" + params = { + 'sort_by': 'created_at', + 'sort_order': 'desc', + 'size': page_size, + 'next_search_after': next_search_after, + 'uuid': unique_id, + 'dataset_id': self.dataset_id, + 'client_id': self.client_id + } + + response = self.client.make_api_request(self.client_id, url, params, unique_id) + print(response) + + + # Create LabellerrVideoFile instances for each file_id + # video_files = [] + # print(f"\nCreating LabellerrFile instances for {len(all_file_ids)} files...") + + # for file_id in all_file_ids: + # try: + # video_file = LabellerrFile( + # client=self.client, + # file_id=file_id, + # project_id=self.project_id, + # dataset_id=self.dataset_id + # ) + # video_files.append(video_file) + # except Exception as e: + # print(f"Warning: Failed to create file instance for {file_id}: {str(e)}") + + except Exception as e: + raise LabellerrError(f"Failed to fetch dataset files: {str(e)}") + # Example usage if __name__ == "__main__": - api_key = "" - api_secret = "" - client_id = "" + api_key = "66f4d8.9f402742f58a89568f5bcc0f86" + api_secret = "1e2478b930d4a842a526beb585e60d2a9ee6a6f1e3aa89cb3c8ead751f418215" + client_id = "14078" dataset_id = "16257fd6-b91b-4d00-a680-9ece9f3f241c" project_id = "gabrila_artificial_duck_74237" file_id = "c44f38f6-0186-436f-8c2d-ffb50a539c76" - client = LabellerrClient(api_key=api_key, api_secret=api_secret) - - # Create VideoFileService instance - video_service = VideoFileService(client) - - # Get file metadata - # print(video_service.get_file_metadata(client_id, file_id, project_id)) + client = LabellerrClient(api_key=api_key, api_secret=api_secret, client_id=client_id) - # Get video frames - # total_frame = video_service.get_file_metadata(client_id, file_id, project_id)['file_metadata']['total_frames'] - # frames = video_service.get_video_frames(client_id, file_id, project_id, dataset_id, frame_end=total_frame) - # print(frames) + lb_file = LabellerrFile( + client=client, + file_id=file_id, + project_id=project_id, + dataset_id=dataset_id + ) - # Download frames with threading (default 10 workers) - # video_service.download_video_frames(frames, output_folder="./output", file_id=file_id) - # Or specify custom number of workers - # video_service.download_video_frames(frames, output_folder="./output", file_id=file_id, max_workers=20) + # print(f"File type: {type(lb_file).__name__}") - # Create video from frames - # video_service.create_video_from_frames(frames_folder="./output/frame_folder", output_file="final_video.mp4", framerate=30) \ No newline at end of file + # if isinstance(lb_file, LabellerrVideoFile): + # print(f"Total frames: {lb_file.total_frames}") + + # Get video frames + # frames = lb_file.get_frames() + + # Download frames + # lb_file.download_frames(frames, output_folder="./output") + + # Create video from frames + # frames_path = f"./output/{file_id}" + # lb_file.create_video(frames_folder=frames_path, output_file="final_video.mp4", framerate=30) + + lb_dataset = LabellerrVideoDataset(client=client, dataset_id=dataset_id, project_id=project_id) + lb_dataset.fetch_files() + # print(f"Fetched {len(video_files)} video files from dataset {dataset_id}") + # print(video_files) + \ No newline at end of file diff --git a/labellerr/services/video_sampling/ffmpeg.py b/labellerr/services/video_sampling/ffmpeg.py index 2d8836e..977fe04 100644 --- a/labellerr/services/video_sampling/ffmpeg.py +++ b/labellerr/services/video_sampling/ffmpeg.py @@ -9,7 +9,7 @@ class SceneFrame(BaseModel): """Represents an extracted keyframe.""" frame_path: str - frame_no: int + frame_index: int class DetectionResult(BaseModel): @@ -24,7 +24,7 @@ class FFMPEGSceneDetect(Singleton): def detect_and_extract(self, video_path: str) -> DetectionResult: """ - Extract keyframes from video and save to folder named after video file. + Extract keyframes from video and save to detects folder structure. Args: video_path: Path to the video file @@ -34,10 +34,17 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: """ # Derive file_id from video_path (base name without extension) file_id = os.path.splitext(os.path.basename(video_path))[0] - save_folder = file_id - os.makedirs(save_folder, exist_ok=True) - output_pattern = os.path.join(save_folder, "%d.jpg") + # Create detects folder structure + base_detect_folder = "FFMPEG_detects" + output_folder = os.path.join(base_detect_folder, 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", @@ -50,34 +57,34 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: try: result = subprocess.run(command, check=True, capture_output=True, text=True) - print(f"Keyframes extracted to {save_folder}") + print(f"Keyframes extracted to {frames_folder}") # Parse frame information from FFMPEG output - selected_frames = self._parse_ffmpeg_output(result.stderr, save_folder) + selected_frames = self._parse_ffmpeg_output(result.stderr, frames_folder) # Create result detection_result = DetectionResult( file_id=file_id, - output_folder=save_folder, + output_folder=output_folder, # Main detects/file_id folder selected_frames=selected_frames ) # Save JSON mapping - self._save_json_mapping(detection_result, save_folder, file_id) + 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, save_folder: str) -> List[SceneFrame]: + + 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 - save_folder: Folder where frames are saved + frames_folder: Folder where frames are saved (detects/file_id/frames) Returns: List of SceneFrame objects @@ -89,7 +96,7 @@ def _parse_ffmpeg_output(self, stderr_output: str, save_folder: str) -> List[Sce 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(save_folder, f"{frame_counter}.jpg") + 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 ... @@ -106,21 +113,21 @@ def _parse_ffmpeg_output(self, stderr_output: str, save_folder: str) -> List[Sce frames.append(SceneFrame( frame_path=frame_path, - frame_no=frame_no + 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 + output_folder: Folder to save the JSON file (detects/file_id/) file_id: Unique identifier for the video """ # Use Pydantic's model_dump diff --git a/labellerr/services/video_sampling/gemini.py b/labellerr/services/video_sampling/gemini.py index d26afc3..16a4d65 100644 --- a/labellerr/services/video_sampling/gemini.py +++ b/labellerr/services/video_sampling/gemini.py @@ -1,14 +1,14 @@ import os import cv2 from PIL import Image -from dataclasses import dataclass, asdict +from pydantic import BaseModel, Field from typing import List, Optional import json from google.cloud import videointelligence +from labellerr.base.singleton import Singleton -@dataclass -class SceneFrame: +class SceneFrame(BaseModel): """Represents a detected scene with its extracted frame.""" frame_path: str frame_no: int @@ -16,21 +16,26 @@ class SceneFrame: end_time_offset: float -@dataclass -class DetectionResult: +class DetectionResult(BaseModel): """Contains all detection results for a video.""" file_id: str output_folder: str total_frames: int - selected_frames: List[SceneFrame] + selected_frames: List[SceneFrame] = Field(default_factory=list) -class GeminiSceneDetect: +class GeminiSceneDetect(Singleton): """Google Cloud Video Intelligence API scene detection and frame extraction.""" - def __init__(self, video_path: str, file_id: str, gcs_uri: Optional[str] = None, credentials_path: Optional[str] = None): + def detect_and_extract( + self, + video_path: str, + file_id: str, + gcs_uri: Optional[str] = None, + credentials_path: Optional[str] = None + ) -> DetectionResult: """ - Initialize the Google Cloud Video Intelligence scene detector. + Detect scenes using Google Cloud Video Intelligence API and extract representative frames. Args: video_path: Path to the local video file (for frame extraction) @@ -39,31 +44,24 @@ def __init__(self, video_path: str, file_id: str, gcs_uri: Optional[str] = None, If None, the video will be uploaded as bytes (limited to 10MB) credentials_path: Path to service account JSON key file. If None, uses GOOGLE_APPLICATION_CREDENTIALS environment variable + + Returns: + DetectionResult containing file_id, output_folder, total_frames, and list of SceneFrame objects """ - self.video_path = video_path - self.file_id = file_id - self.output_folder = file_id - self.gcs_uri = gcs_uri + output_folder = file_id # Set credentials if provided if credentials_path: os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = credentials_path # Initialize Video Intelligence client - self.client = videointelligence.VideoIntelligenceServiceClient() + client = videointelligence.VideoIntelligenceServiceClient() - def detect_and_extract(self) -> DetectionResult: - """ - Detect scenes using Google Cloud Video Intelligence API and extract representative frames. - - Returns: - DetectionResult containing file_id, output_folder, total_frames, and list of SceneFrame objects - """ - print(f"Processing video: {self.video_path}") + print(f"Processing video: {video_path}") print("Detecting shot changes using Google Cloud Video Intelligence API...") # Detect shots using Video Intelligence API - shots = self._detect_shots() + shots = self._detect_shots(client, video_path, gcs_uri) if not shots: raise ValueError("No shot changes detected in the video") @@ -71,13 +69,13 @@ def detect_and_extract(self) -> DetectionResult: print(f"Detected {len(shots)} shots") # Create output folder - os.makedirs(self.output_folder, exist_ok=True) + os.makedirs(output_folder, exist_ok=True) # Open video for frame extraction - video = cv2.VideoCapture(self.video_path) + video = cv2.VideoCapture(video_path) if not video.isOpened(): - raise ValueError(f"Cannot open video: {self.video_path}") + raise ValueError(f"Cannot open video: {video_path}") # Get video properties total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) @@ -108,7 +106,7 @@ def detect_and_extract(self) -> DetectionResult: # Save frame with frame number as filename frame_filename = f"{frame_no}.jpg" - frame_path = os.path.join(self.output_folder, frame_filename) + frame_path = os.path.join(output_folder, frame_filename) frame.save(frame_path) # Create SceneFrame object @@ -128,38 +126,48 @@ def detect_and_extract(self) -> DetectionResult: # Create result result = DetectionResult( - file_id=self.file_id, - output_folder=self.output_folder, + file_id=file_id, + output_folder=output_folder, total_frames=total_frames, selected_frames=scene_frames ) # Save JSON mapping - self._save_json_mapping(result) + self._save_json_mapping(result, output_folder, file_id, gcs_uri) return result - def _detect_shots(self) -> List: + def _detect_shots( + self, + client: videointelligence.VideoIntelligenceServiceClient, + video_path: str, + gcs_uri: Optional[str] + ) -> List: """ Detect shot changes using Google Cloud Video Intelligence API. + Args: + client: Video Intelligence client instance + video_path: Path to the local video file + gcs_uri: Google Cloud Storage URI + Returns: List of shot annotation objects """ features = [videointelligence.Feature.SHOT_CHANGE_DETECTION] - if self.gcs_uri: + if gcs_uri: # Use GCS URI for large videos - print(f"Analyzing video from GCS: {self.gcs_uri}") - operation = self.client.annotate_video( + print(f"Analyzing video from GCS: {gcs_uri}") + operation = client.annotate_video( request={ - "input_uri": self.gcs_uri, + "input_uri": gcs_uri, "features": features } ) else: # Read video file and send as bytes (limited to 10MB) - with open(self.video_path, "rb") as video_file: + with open(video_path, "rb") as video_file: input_content = video_file.read() print(f"Analyzing video from local file (size: {len(input_content) / (1024*1024):.2f} MB)") @@ -170,7 +178,7 @@ def _detect_shots(self) -> List: "and provide gcs_uri parameter (gs://bucket/video.mp4)" ) - operation = self.client.annotate_video( + operation = client.annotate_video( request={ "input_content": input_content, "features": features @@ -205,26 +213,30 @@ def _get_frame(self, video: cv2.VideoCapture, frame_no: int) -> Optional[Image.I return Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) - def _save_json_mapping(self, result: DetectionResult) -> None: + def _save_json_mapping( + self, + result: DetectionResult, + output_folder: str, + file_id: str, + gcs_uri: Optional[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 + gcs_uri: Google Cloud Storage URI (if used) """ - mapping = { - "file_id": result.file_id, - "output_folder": result.output_folder, - "total_frames": result.total_frames, - "total_selected_frames": len(result.selected_frames), - "detection_method": "Google Cloud Video Intelligence API - Shot Change Detection", - "gcs_uri": self.gcs_uri if self.gcs_uri else "local file", - "selected_frames": [asdict(frame) for frame in result.selected_frames] - } - - json_path = os.path.join(self.output_folder, f"{self.file_id}_mapping.json") + # Use Pydantic's model_dump + result_dict = result.model_dump() + result_dict["total_selected_frames"] = len(result.selected_frames) + result_dict["gcs_uri"] = gcs_uri if gcs_uri else "local file" + + json_path = os.path.join(output_folder, f"{file_id}_mapping.json") with open(json_path, 'w', encoding='utf-8') as f: - json.dump(mapping, f, indent=2, ensure_ascii=False) + json.dump(result_dict, f, indent=2, ensure_ascii=False) print(f"JSON mapping saved to: {json_path}") @@ -234,23 +246,18 @@ def _save_json_mapping(self, result: DetectionResult) -> None: video_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\Python_SDK\services\video_sampling\video2.mp4" cred_json_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\Python_SDK\services\video_sampling\yash-suman-prod.json" - # ---------------------------------------------- - # Option 1: Process local video file (< 10MB) - # ---------------------------------------------- - - detector = GeminiSceneDetect( - credentials_path=cred_json_path # Uses GOOGLE_APPLICATION_CREDENTIALS env var - ) - labellerr_file = LabellerrFile( - file_id="video_001" - ) - detector.detect_and_extract() + # Get singleton instance + detector = GeminiSceneDetect() # Detect and extract frames try: - result = detector.detect_and_extract() - print(f"\nDetection complete!") - print(f"Total frames extracted: {len(result.selected_frames)}") - print(f"Output folder: {result.output_folder}") + result = detector.detect_and_extract( + video_path=video_path, + file_id="video_001", + gcs_uri=None, # Set to gs://bucket/video.mp4 for large videos + credentials_path=cred_json_path + ) + + except Exception as e: - print(f"Error: {e}") + print(f"Error: {e}") \ No newline at end of file diff --git a/labellerr/services/video_sampling/pyscene_detect.py b/labellerr/services/video_sampling/pyscene_detect.py index 09e9f89..e2d1ad7 100644 --- a/labellerr/services/video_sampling/pyscene_detect.py +++ b/labellerr/services/video_sampling/pyscene_detect.py @@ -11,7 +11,7 @@ class SceneFrame(BaseModel): """Represents a detected scene with its extracted frame.""" frame_path: str - frame_no: int + frame_index: int class DetectionResult(BaseModel): @@ -39,14 +39,15 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: file_id = os.path.splitext(os.path.basename(video_path))[0] # Create base detect folder and file_id specific folder - base_detect_folder = "detects" + base_detect_folder = "PyScene_detects" output_folder = os.path.join(base_detect_folder, 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(output_folder, exist_ok=True) + os.makedirs(frames_folder, exist_ok=True) # Create frames subfolder # Open video for frame extraction video = cv2.VideoCapture(video_path) @@ -63,15 +64,15 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: # Extract frame frame = self._get_frame(video, frame_no) - # Save frame with frame number as filename + # Save frame with frame number as filename inside frames folder frame_filename = f"{frame_no}.jpg" - frame_path = os.path.join(output_folder, frame_filename) + 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_no=frame_no + frame_index=frame_no ) scene_frames.append(scene_frame) From 11b33f8303123468abd1b216907d57637596a3b9 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Thu, 9 Oct 2025 18:38:09 +0530 Subject: [PATCH 19/23] minor update --- labellerr/services/video_sampling/__init__.py | 11 +++++- labellerr/services/video_sampling/ssim.py | 38 ++++++++++--------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/labellerr/services/video_sampling/__init__.py b/labellerr/services/video_sampling/__init__.py index 8c37956..2ab785a 100644 --- a/labellerr/services/video_sampling/__init__.py +++ b/labellerr/services/video_sampling/__init__.py @@ -1,4 +1,13 @@ """All the code for video sampling will go here. All algorithms for video sampling will go in separate files. -""" \ No newline at end of file +""" +from .ffmpeg import FFMPEGSceneDetect +from .pyscene_detect import PySceneDetect +from .ssim import SSIMSceneDetect + +__all__ = [ + 'FFMPEGSceneDetect', + 'PySceneDetect', + 'SSIMSceneDetect', +] \ No newline at end of file diff --git a/labellerr/services/video_sampling/ssim.py b/labellerr/services/video_sampling/ssim.py index 5e08468..7f7528a 100644 --- a/labellerr/services/video_sampling/ssim.py +++ b/labellerr/services/video_sampling/ssim.py @@ -12,7 +12,7 @@ class SceneFrame(BaseModel): """Represents a detected scene with its extracted frame.""" frame_path: str - frame_no: int + frame_index: int ssim_score: float @@ -46,10 +46,14 @@ def detect_and_extract( """ # Derive file_id from video_path (base name without extension) file_id = os.path.splitext(os.path.basename(video_path))[0] - output_folder = file_id - # Create output folder - os.makedirs(output_folder, exist_ok=True) + # Create detects folder structure + base_detect_folder = "SSIM_detects" + output_folder = os.path.join(base_detect_folder, 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) @@ -75,7 +79,7 @@ def detect_and_extract( frame_count = 0 # Always save first frame - self._save_frame(prev_frame, frame_count, 1.0, scene_frames, output_folder) + 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 @@ -91,7 +95,7 @@ def detect_and_extract( # 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, output_folder) + 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: @@ -104,7 +108,7 @@ def detect_and_extract( # Create result result = DetectionResult( file_id=file_id, - output_folder=output_folder, + output_folder=output_folder, # Main detects/file_id folder total_frames=total_frames, selected_frames=scene_frames ) @@ -113,7 +117,7 @@ def detect_and_extract( 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. @@ -134,14 +138,14 @@ def _calculate_ssim(self, frame1: np.ndarray, frame2: np.ndarray, resize_dim: tu 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], - output_folder: str + frames_folder: str ) -> None: """ Save a frame to disk and add to scene_frames list. @@ -151,29 +155,29 @@ def _save_frame( frame_no: Frame number ssim_score: SSIM score that triggered this frame scene_frames: List to append SceneFrame object to - output_folder: Folder to save the frame + 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 + # Save frame with frame number as filename in frames folder frame_filename = f"{frame_no}.jpg" - frame_path = os.path.join(output_folder, frame_filename) + 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_no=frame_no, + frame_index=frame_no, ssim_score=ssim_score ) scene_frames.append(scene_frame) - + def _save_json_mapping( self, result: DetectionResult, - output_folder: str, + output_folder: str, # This is now detects/file_id/ file_id: str, threshold: float, resize_dim: tuple @@ -183,7 +187,7 @@ def _save_json_mapping( Args: result: DetectionResult object - output_folder: Folder to save the JSON file + 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 From e58ba820262d054e570cee3d8953c4f557a8a1e6 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Thu, 9 Oct 2025 21:25:45 +0530 Subject: [PATCH 20/23] Refactor video processing methods --- labellerr/core/datasets/base.py | 36 ++++++++++------------------- labellerr/core/files/image_file.py | 4 ++++ labellerr/core/files/video_file.py | 37 +++++++++++------------------- 3 files changed, 30 insertions(+), 47 deletions(-) create mode 100644 labellerr/core/files/image_file.py diff --git a/labellerr/core/datasets/base.py b/labellerr/core/datasets/base.py index 0971e3c..fd5ee8b 100644 --- a/labellerr/core/datasets/base.py +++ b/labellerr/core/datasets/base.py @@ -48,7 +48,6 @@ def fetch_files(self, page_size: int = 1000): } # Add next_search_after only if it exists (don't send on first request) - print(next_search_after) if next_search_after: url+= f"?next_search_after={next_search_after}" @@ -73,7 +72,6 @@ def fetch_files(self, page_size: int = 1000): # Break if no more pages or no files returned if not next_search_after or not files: - print("No more pages to fetch.") break print(f"Fetched total: {len(all_file_ids)}") @@ -99,19 +97,16 @@ def fetch_files(self, page_size: int = 1000): print(f"Successfully created {len(video_files)} LabellerrFile instances") return video_files - + except Exception as e: raise LabellerrError(f"Failed to fetch dataset files: {str(e)}") - - def process_all_videos(self, output_folder: str, framerate: int = 30, - max_workers: int = 30): + + def process_all_videos(self, output_folder: str): """ Process all video files in the dataset: download frames, create videos, and automatically clean up temporary files. :param output_folder: Base folder where dataset folder will be created - :param framerate: Video framerate in fps (default: 30) - :param max_workers: Max concurrent download threads (default: 30) :return: List of processing results for all files """ try: @@ -138,11 +133,7 @@ def process_all_videos(self, output_folder: str, framerate: int = 30, print(f"[{idx}/{len(video_files)}] Processing {video_file.file_id}...") # Call the new all-in-one method - result = video_file.download_create_video_auto_cleanup( - output_folder=output_folder, - framerate=framerate, - max_workers=max_workers - ) + result = video_file.download_create_video_auto_cleanup() results.append(result) successful += 1 @@ -169,10 +160,10 @@ def process_all_videos(self, output_folder: str, framerate: int = 30, except Exception as e: raise LabellerrError(f"Failed to process dataset videos: {str(e)}") - - + + if __name__ == "__main__": - # Example usage (requires valid LabellerrClient instance) + # Example usage api_key = "66f4d8.9f402742f58a89568f5bcc0f86" api_secret = "1e2478b930d4a842a526beb585e60d2a9ee6a6f1e3aa89cb3c8ead751f418215" client_id = "14078" @@ -184,11 +175,8 @@ def process_all_videos(self, output_folder: str, framerate: int = 30, dataset = LabellerrVideoDataset(client, dataset_id, project_id) - results = dataset.process_all_videos( - output_folder="./videos", - framerate=30, - max_workers=30 - ) - - pprint.pprint(results) - \ No newline at end of file + # Process all videos in the dataset + results = dataset.process_all_videos(output_folder="./videos") + + # Print summary + pprint.pprint(results) \ No newline at end of file diff --git a/labellerr/core/files/image_file.py b/labellerr/core/files/image_file.py new file mode 100644 index 0000000..1e66938 --- /dev/null +++ b/labellerr/core/files/image_file.py @@ -0,0 +1,4 @@ +from labellerr.core.files.base import LabellerrFile + +class LabellerrImageFile(LabellerrFile): + pass \ No newline at end of file diff --git a/labellerr/core/files/video_file.py b/labellerr/core/files/video_file.py index 2fb7460..9fc8c98 100644 --- a/labellerr/core/files/video_file.py +++ b/labellerr/core/files/video_file.py @@ -207,23 +207,13 @@ def create_video(self, frames_folder: str, return output_file except subprocess.CalledProcessError as e: raise LabellerrError(f"Error while joining frames: {str(e)}") - - def download_create_video_auto_cleanup(self, output_folder: str, - framerate: int = 30, - pattern: str = "%d.jpg", - max_workers: int = 30, - frame_start: int = 0, - frame_end: int | None = None): + + def download_create_video_auto_cleanup(self, output_folder: str = "./download_video"): """ Download frames, create video, and automatically clean up temporary frames. This is an all-in-one method for processing video files. + Downloads all frames from 0 to total_frames automatically. - :param output_folder: Base folder where video will be saved (organized by dataset_id) - :param framerate: Video framerate in fps (default: 30) - :param pattern: Frame filename pattern (default: "%d.jpg") - :param max_workers: Max concurrent download threads (default: 30) - :param frame_start: Starting frame index (default: 0) - :param frame_end: Ending frame index (default: total_frames) :return: Dictionary with operation results """ try: @@ -231,10 +221,14 @@ def download_create_video_auto_cleanup(self, output_folder: str, print(f"Processing file: {self.file_id}") print(f"{'='*60}") - # Step 1: Fetch frame data from API - print("\n[1/4] Fetching frame data from API...") - frames_response = self.get_frames(frame_start=frame_start, frame_end=frame_end) - frames_data = frames_response.get('frames', {}) + # Step 1: Get total frames + total_frames = self.total_frames + if total_frames == 0: + raise LabellerrError("No frames found for this video file") + + # Step 2: Fetch frame data from API + 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) if not frames_data: raise LabellerrError("No frame data retrieved from API") @@ -254,8 +248,7 @@ def download_create_video_auto_cleanup(self, output_folder: str, print(f"\n[3/4] Downloading {len(frames_data)} frames...") download_result = self.download_frames( frames_data=frames_data, - output_folder=dataset_folder, - max_workers=max_workers + output_folder=dataset_folder ) # Update temp folder path in result (since download_frames uses file_id as folder name) @@ -270,8 +263,6 @@ def download_create_video_auto_cleanup(self, output_folder: str, self.create_video( frames_folder=actual_frames_folder, - framerate=framerate, - pattern=pattern, output_file=video_output_path ) @@ -308,6 +299,6 @@ def download_create_video_auto_cleanup(self, output_folder: str, pass raise LabellerrError(f"Failed in video processing: {str(e)}") - -LabellerrFileMeta.register('video', LabellerrVideoFile) + +LabellerrFileMeta.register('video', LabellerrVideoFile) \ No newline at end of file From c8382888e6c5f4c4a4a1e45a643073884c939b23 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Fri, 10 Oct 2025 22:22:32 +0530 Subject: [PATCH 21/23] Added SDK workflow cookbook Refactor video sampling services to include dataset ID in output folder structure - Updated FFMPEGSceneDetect to derive dataset ID from video path and include it in the output folder. - Modified PySceneDetect to incorporate dataset ID in the output folder structure. - Adjusted SSIMSceneDetect to also use dataset ID for organizing output folders. --- labellerr/core/datasets/__init__.py | 7 +- labellerr/core/datasets/base.py | 36 +- labellerr/core/files/video_file.py | 53 +- labellerr/notebooks/SDK.ipynb | 2194 +++++------------ labellerr/services/video_sampling/ffmpeg.py | 6 +- .../services/video_sampling/pyscene_detect.py | 12 +- labellerr/services/video_sampling/ssim.py | 3 +- 7 files changed, 666 insertions(+), 1645 deletions(-) diff --git a/labellerr/core/datasets/__init__.py b/labellerr/core/datasets/__init__.py index c9f2cce..a07ba27 100644 --- a/labellerr/core/datasets/__init__.py +++ b/labellerr/core/datasets/__init__.py @@ -1,2 +1,7 @@ """This module will contain all CRUD for datasets. Example, create, list datasets, get dataset, delete dataset, update dataset, etc. -""" \ No newline at end of file +""" +from labellerr.core.datasets.base import LabellerrVideoDataset + +__all__ = [ + 'LabellerrVideoDataset' + ] \ No newline at end of file diff --git a/labellerr/core/datasets/base.py b/labellerr/core/datasets/base.py index fd5ee8b..d56292b 100644 --- a/labellerr/core/datasets/base.py +++ b/labellerr/core/datasets/base.py @@ -101,7 +101,7 @@ def fetch_files(self, page_size: int = 1000): except Exception as e: raise LabellerrError(f"Failed to fetch dataset files: {str(e)}") - def process_all_videos(self, output_folder: str): + def process_all_videos(self): """ Process all video files in the dataset: download frames, create videos, and automatically clean up temporary files. @@ -112,7 +112,6 @@ def process_all_videos(self, output_folder: str): try: print(f"\n{'#'*70}") print(f"# Starting batch video processing for dataset: {self.dataset_id}") - print(f"# Output folder: {output_folder}") print(f"{'#'*70}\n") # Fetch all video files @@ -128,15 +127,14 @@ def process_all_videos(self, output_folder: str): successful = 0 failed = 0 + print(f"\nStarting download of {len(video_files)} files...") for idx, video_file in enumerate(video_files, 1): try: - print(f"[{idx}/{len(video_files)}] Processing {video_file.file_id}...") - # Call the new all-in-one method result = video_file.download_create_video_auto_cleanup() - results.append(result) successful += 1 + print(f"\rFiles processed: {idx}/{len(video_files)} ({successful} successful, {failed} failed)", end="", flush=True) except Exception as e: error_result = { @@ -146,7 +144,7 @@ def process_all_videos(self, output_folder: str): } results.append(error_result) failed += 1 - print(f"✗ Error processing {video_file.file_id}: {str(e)}\n") + print(f"\rFiles processed: {idx}/{len(video_files)} ({successful} successful, {failed} failed)", end="", flush=True) # Summary print(f"\n{'#'*70}") @@ -162,21 +160,21 @@ def process_all_videos(self, output_folder: str): raise LabellerrError(f"Failed to process dataset videos: {str(e)}") -if __name__ == "__main__": - # Example usage - api_key = "66f4d8.9f402742f58a89568f5bcc0f86" - api_secret = "1e2478b930d4a842a526beb585e60d2a9ee6a6f1e3aa89cb3c8ead751f418215" - client_id = "14078" +# if __name__ == "__main__": +# # Example usage +# api_key = "" +# api_secret = "" +# client_id = "" - dataset_id = "59438ec3-12e0-4687-8847-1e6e01b0bf25" - project_id = "farrah_supposed_hookworm_34155" +# dataset_id = "59438ec3-12e0-4687-8847-1e6e01b0bf25" +# project_id = "farrah_supposed_hookworm_34155" - client = LabellerrClient(api_key, api_secret, client_id) +# client = LabellerrClient(api_key, api_secret, client_id) - dataset = LabellerrVideoDataset(client, dataset_id, project_id) +# dataset = LabellerrVideoDataset(client, dataset_id, project_id) - # Process all videos in the dataset - results = dataset.process_all_videos(output_folder="./videos") +# # Process all videos in the dataset +# results = dataset.process_all_videos() - # Print summary - pprint.pprint(results) \ No newline at end of file +# # Print summary +# pprint.pprint(results) \ No newline at end of file diff --git a/labellerr/core/files/video_file.py b/labellerr/core/files/video_file.py index 9fc8c98..605b5c0 100644 --- a/labellerr/core/files/video_file.py +++ b/labellerr/core/files/video_file.py @@ -76,19 +76,12 @@ def _download_single_frame(self, frame_number, frame_url, save_path, print_lock) if response.status_code == 200: with open(filepath, 'wb') as f: f.write(response.content) - - with print_lock: - print(f"Downloaded: {filename}") - return True, frame_number, None else: error_info = { 'frame': frame_number, 'status': response.status_code } - with print_lock: - print(f"Failed to download frame {frame_number}: Status {response.status_code}") - return False, frame_number, error_info except Exception as e: @@ -127,9 +120,9 @@ def download_frames(self, frames_data: dict, output_folder: str | None = None, success_count = 0 failed_frames = [] print_lock = Lock() + total_frames = len(frames_data) - print(f"Downloading {len(frames_data)} frames to: {save_path}") - print(f"Using {max_workers} concurrent threads") + print(f"Starting download of {total_frames} frames...") # Use ThreadPoolExecutor for concurrent downloads with ThreadPoolExecutor(max_workers=max_workers) as executor: @@ -145,18 +138,27 @@ def download_frames(self, frames_data: dict, output_folder: str | None = None, for frame_number, frame_url in frames_data.items() } + completed = 0 # Process completed downloads for future in as_completed(future_to_frame): success, frame_number, error_info = future.result() + completed += 1 if success: success_count += 1 else: failed_frames.append(error_info) + + # Update progress + with print_lock: + print(f"\rFrames downloaded: {completed}/{total_frames} ({success_count} successful, {len(failed_frames)} failed)", end="", flush=True) + + # Print newline after progress + print() result = { 'file_id': self.file_id, - 'total_frames': len(frames_data), + 'total_frames': total_frames, 'successful_downloads': success_count, 'failed_downloads': len(failed_frames), 'save_path': save_path, @@ -208,7 +210,7 @@ def create_video(self, frames_folder: str, except subprocess.CalledProcessError as e: raise LabellerrError(f"Error while joining frames: {str(e)}") - def download_create_video_auto_cleanup(self, output_folder: str = "./download_video"): + def download_create_video_auto_cleanup(self, output_folder: str = "./Labellerr_datastets"): """ Download frames, create video, and automatically clean up temporary frames. This is an all-in-one method for processing video files. @@ -237,25 +239,24 @@ def download_create_video_auto_cleanup(self, output_folder: str = "./download_vi # Step 2: Create dataset folder structure print(f"\n[2/4] Setting up output folders...") - dataset_folder = os.path.join(output_folder, self.dataset_id) + if self.dataset_id is None: + dataset_folder = output_folder + else: + dataset_folder = os.path.join(output_folder, self.dataset_id) os.makedirs(dataset_folder, exist_ok=True) - temp_frames_folder = os.path.join(dataset_folder, f".temp_{self.file_id}") - os.makedirs(temp_frames_folder, exist_ok=True) - print(f"Temporary frames folder: {temp_frames_folder}") + # Define actual frames folder path + actual_frames_folder = os.path.join(dataset_folder, self.file_id) - # Step 3: Download frames to temporary location - print(f"\n[3/4] Downloading {len(frames_data)} frames...") + # Step 3: Download frames + print(f"\n[3/4] Downloading frames...") download_result = self.download_frames( frames_data=frames_data, output_folder=dataset_folder ) - # Update temp folder path in result (since download_frames uses file_id as folder name) - actual_frames_folder = os.path.join(dataset_folder, self.file_id) - if download_result['failed_downloads'] > 0: - print(f"Warning: {download_result['failed_downloads']} frames failed to download") + print(f"\nWarning: {download_result['failed_downloads']} frames failed to download") # Step 4: Create video from downloaded frames print(f"\n[4/4] Creating video from frames...") @@ -293,8 +294,14 @@ def download_create_video_auto_cleanup(self, output_folder: str = "./download_vi except Exception as e: # Attempt cleanup on error try: - if actual_frames_folder and os.path.exists(actual_frames_folder): - shutil.rmtree(actual_frames_folder) + # Get the frames folder path + 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 os.path.exists(cleanup_folder): + shutil.rmtree(cleanup_folder) except: pass diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index 81cdfad..dfa02d4 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -5,7 +5,12 @@ "id": "d6488b6b", "metadata": {}, "source": [ - "# " + "# Getting Started 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:" ] }, { @@ -16,7 +21,8 @@ "outputs": [], "source": [ "from labellerr.client import LabellerrClient\n", - "from labellerr.core.files import LabellerrFile" + "from labellerr.core.datasets import LabellerrVideoDataset\n", + "import os" ] }, { @@ -24,12 +30,27 @@ "id": "84b7917a", "metadata": {}, "source": [ - "### Fill your credentials" + "## 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", + "### Required Credentials:\n", + "\n", + "1. **API Key & API Secret**\n", + " - Log in to your Labellerr account\n", + " - Navigate to the \"Get API\" tab\n", + " - Copy your unique API key and secret\n", + "\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." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "ab12f168", "metadata": {}, "outputs": [], @@ -44,7 +65,18 @@ "id": "3d05bd0f", "metadata": {}, "source": [ - "### Fill the ids" + "## Project Configuration\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", + "\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", + "\n", + "Note: The dataset_id is a UUID format string, while the project_id is typically a human-readable string." ] }, { @@ -54,10 +86,9 @@ "metadata": {}, "outputs": [], "source": [ - "\n", + "# 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\"\n", - "file_id = \"c44f38f6-0186-436f-8c2d-ffb50a539c76\"" + "project_id = \"gabrila_artificial_duck_74237\"" ] }, { @@ -65,7 +96,15 @@ "id": "1b2c7aee", "metadata": {}, "source": [ - "### Create LabellerrClient Instance" + "## Initializing the Labellerr SDK\n", + "\n", + "### Create LabellerrClient Instance\n", + "Now we'll create instances of the main SDK classes:\n", + "\n", + "1. **LabellerrClient**: The main client that handles communication with the Labellerr API\n", + "2. **LabellerrVideoDataset**: A specialized class for working with video datasets\n", + "\n", + "These instances will be used for all subsequent operations with the platform." ] }, { @@ -75,1592 +114,87 @@ "metadata": {}, "outputs": [], "source": [ - "client = LabellerrClient(api_key=api_key, api_secret=api_secret, client_id=client_id)" - ] - }, - { - "cell_type": "markdown", - "id": "41b440b4", - "metadata": {}, - "source": [ - "### Create a LabellerrFile Instance" + "client = LabellerrClient(api_key, api_secret, client_id) \n", + "dataset = LabellerrVideoDataset(client, dataset_id, project_id)" ] }, { "cell_type": "code", "execution_count": 5, - "id": "aaa7120e", - "metadata": {}, - "outputs": [], - "source": [ - "# create file instance\n", - "file = LabellerrFile(client=client, file_id=file_id, project_id=project_id, dataset_id=dataset_id)" - ] - }, - { - "cell_type": "markdown", - "id": "eb6d5547", - "metadata": {}, - "source": [ - "### Use that to retrive file metadata" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "d6cccc14", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'audio_segments': 0,\n", - " 'file_metadata': {'duration': 60,\n", - " 'bitrate': 1525920,\n", - " 'audio_bitrate': 191999,\n", - " 'total_frames': 1440,\n", - " 'size': 1440,\n", - " 'height': 720,\n", - " 'sample_rate': 44100,\n", - " 'fps': 23,\n", - " 'audio_channels': 2,\n", - " 'width': 1280,\n", - " 'keyframes': []},\n", - " 'completed_at': 1759311661144,\n", - " 'email_id': 'yashsuman15@gmail.com',\n", - " 'frames_uri': 'labellerr-processed/videos/datasets/16257fd6-b91b-4d00-a680-9ece9f3f241c/files/c44f38f6-0186-436f-8c2d-ffb50a539c76/frames',\n", - " 'audio_uri': 'labellerr-processed/videos/datasets/16257fd6-b91b-4d00-a680-9ece9f3f241c/files/c44f38f6-0186-436f-8c2d-ffb50a539c76/audio',\n", - " 'project_id': 'gabrila_artificial_duck_74237',\n", - " 'dataset_id': '16257fd6-b91b-4d00-a680-9ece9f3f241c',\n", - " 'file_id': 'c44f38f6-0186-436f-8c2d-ffb50a539c76',\n", - " 'video_url': 'local_upload/b76cdf41-900f-40dd-8013-5008a328d122/video.mp4',\n", - " 'file_name': 'video.mp4',\n", - " 'status_code': 300,\n", - " 'file_reference': 'gs://labellerr-connector-files/local_upload/b76cdf41-900f-40dd-8013-5008a328d122/video.mp4',\n", - " 'video_processing_job_id': '8baadcf9-51d8-4995-b796-34dbe2bfadf2-c44f38f6-0186-436f-8c2d-ffb50a539c76',\n", - " 'file_name_original': 'video.mp4',\n", - " 'data_type': 'video',\n", - " 'created_by': 'yashsuman15@gmail.com',\n", - " 'total_frames': 1440,\n", - " 'connection_id': 'b76cdf41-900f-40dd-8013-5008a328d122',\n", - " 'created_at': 1759311650156,\n", - " 'updated_at': 1759311661144,\n", - " 'annotation_rotation_count': 0,\n", - " 'status': 'assigned',\n", - " 'es_multimodal_index': False}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "file.get_metadata()" - ] - }, - { - "cell_type": "markdown", - "id": "36ffba0a", - "metadata": {}, - "source": [ - "### Download video frames" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "b123193c", + "id": "7b6a7052", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Downloading 1440 frames to: c44f38f6-0186-436f-8c2d-ffb50a539c76\n", - "Using 30 concurrent threads\n", - "Downloaded: 0.jpg\n", - "Downloaded: 4.jpg\n", - "Downloaded: 3.jpg\n", - "Downloaded: 2.jpg\n", - "Downloaded: 6.jpg\n", - "Downloaded: 14.jpg\n", - "Downloaded: 10.jpg\n", - "Downloaded: 24.jpg\n", - "Downloaded: 20.jpg\n", - "Downloaded: 23.jpg\n", - "Downloaded: 16.jpg\n", - "Downloaded: 12.jpg\n", - "Downloaded: 8.jpg\n", - "Downloaded: 7.jpg\n", - "Downloaded: 9.jpg\n", - "Downloaded: 17.jpg\n", - "Downloaded: 28.jpg\n", - "Downloaded: 25.jpg\n", - "Downloaded: 5.jpg\n", - "Downloaded: 27.jpg\n", - "Downloaded: 13.jpg\n", - "Downloaded: 18.jpg\n", - "Downloaded: 26.jpg\n", - "Downloaded: 32.jpg\n", - "Downloaded: 33.jpg\n", - "Downloaded: 35.jpg\n", - "Downloaded: 36.jpg\n", - "Downloaded: 31.jpg\n", - "Downloaded: 43.jpg\n", - "Downloaded: 45.jpg\n", - "Downloaded: 38.jpg\n", - "Downloaded: 48.jpg\n", - "Downloaded: 1.jpg\n", - "Downloaded: 44.jpg\n", - "Downloaded: 37.jpg\n", - "Downloaded: 39.jpg\n", - "Downloaded: 52.jpg\n", - "Downloaded: 22.jpg\n", - "Downloaded: 15.jpg\n", - "Downloaded: 19.jpg\n", - "Downloaded: 46.jpg\n", - "Downloaded: 51.jpg\n", - "Downloaded: 41.jpg\n", - "Downloaded: 21.jpg\n", - "Downloaded: 29.jpg\n", - "Downloaded: 11.jpg\n", - "Downloaded: 57.jpg\n", - "Downloaded: 55.jpg\n", - "Downloaded: 54.jpg\n", - "Downloaded: 30.jpg\n", - "Downloaded: 56.jpg\n", - "Downloaded: 34.jpg\n", - "Downloaded: 53.jpg\n", - "Downloaded: 61.jpg\n", - "Downloaded: 60.jpg\n", - "Downloaded: 62.jpg\n", - "Downloaded: 59.jpg\n", - "Downloaded: 40.jpg\n", - "Downloaded: 47.jpg\n", - "Downloaded: 58.jpg\n", - "Downloaded: 42.jpg\n", - "Downloaded: 49.jpg\n", - "Downloaded: 50.jpg\n", - "Downloaded: 65.jpg\n", - "Downloaded: 63.jpg\n", - "Downloaded: 69.jpg\n", - "Downloaded: 72.jpg\n", - "Downloaded: 68.jpg\n", - "Downloaded: 70.jpg\n", - "Downloaded: 67.jpg\n", - "Downloaded: 66.jpg\n", - "Downloaded: 75.jpg\n", - "Downloaded: 73.jpg\n", - "Downloaded: 83.jpg\n", - "Downloaded: 71.jpg\n", - "Downloaded: 84.jpg\n", - "Downloaded: 81.jpg\n", - "Downloaded: 76.jpg\n", - "Downloaded: 77.jpg\n", - "Downloaded: 82.jpg\n", - "Downloaded: 85.jpg\n", - "Downloaded: 78.jpg\n", - "Downloaded: 91.jpg\n", - "Downloaded: 86.jpg\n", - "Downloaded: 79.jpg\n", - "Downloaded: 80.jpg\n", - "Downloaded: 88.jpg\n", - "Downloaded: 92.jpg\n", - "Downloaded: 95.jpg\n", - "Downloaded: 93.jpg\n", - "Downloaded: 96.jpg\n", - "Downloaded: 94.jpg\n", - "Downloaded: 74.jpg\n", - "Downloaded: 64.jpg\n", - "Downloaded: 99.jpg\n", - "Downloaded: 101.jpg\n", - "Downloaded: 97.jpg\n", - "Downloaded: 100.jpg\n", - "Downloaded: 103.jpg\n", - "Downloaded: 107.jpg\n", - "Downloaded: 105.jpg\n", - "Downloaded: 104.jpg\n", - "Downloaded: 120.jpg\n", - "Downloaded: 109.jpg\n", - "Downloaded: 87.jpg\n", - "Downloaded: 114.jpg\n", - "Downloaded: 110.jpg\n", - "Downloaded: 119.jpg\n", - "Downloaded: 113.jpg\n", - "Downloaded: 116.jpg\n", - "Downloaded: 111.jpg\n", - "Downloaded: 115.jpg\n", - "Downloaded: 106.jpg\n", - "Downloaded: 90.jpg\n", - "Downloaded: 89.jpg\n", - "Downloaded: 108.jpg\n", - "Downloaded: 123.jpg\n", - "Downloaded: 121.jpg\n", - "Downloaded: 117.jpg\n", - "Downloaded: 98.jpg\n", - "Downloaded: 126.jpg\n", - "Downloaded: 125.jpg\n", - "Downloaded: 124.jpg\n", - "Downloaded: 127.jpg\n", - "Downloaded: 118.jpg\n", - "Downloaded: 102.jpg\n", - "Downloaded: 112.jpg\n", - "Downloaded: 122.jpg\n", - "Downloaded: 130.jpg\n", - "Downloaded: 133.jpg\n", - "Downloaded: 132.jpg\n", - "Downloaded: 134.jpg\n", - "Downloaded: 136.jpg\n", - "Downloaded: 140.jpg\n", - "Downloaded: 129.jpg\n", - "Downloaded: 137.jpg\n", - "Downloaded: 139.jpg\n", - "Downloaded: 138.jpg\n", - "Downloaded: 151.jpg\n", - "Downloaded: 135.jpg\n", - "Downloaded: 146.jpg\n", - "Downloaded: 145.jpg\n", - "Downloaded: 143.jpg\n", - "Downloaded: 147.jpg\n", - "Downloaded: 149.jpg\n", - "Downloaded: 148.jpg\n", - "Downloaded: 152.jpg\n", - "Downloaded: 144.jpg\n", - "Downloaded: 155.jpg\n", - "Downloaded: 153.jpg\n", - "Downloaded: 141.jpg\n", - "Downloaded: 156.jpg\n", - "Downloaded: 157.jpg\n", - "Downloaded: 128.jpg\n", - "Downloaded: 131.jpg\n", - "Downloaded: 164.jpg\n", - "Downloaded: 159.jpg\n", - "Downloaded: 160.jpg\n", - "Downloaded: 158.jpg\n", - "Downloaded: 162.jpg\n", - "Downloaded: 169.jpg\n", - "Downloaded: 166.jpg\n", - "Downloaded: 163.jpg\n", - "Downloaded: 165.jpg\n", - "Downloaded: 168.jpg\n", - "Downloaded: 167.jpg\n", - "Downloaded: 171.jpg\n", - "Downloaded: 170.jpg\n", - "Downloaded: 172.jpg\n", - "Downloaded: 174.jpg\n", - "Downloaded: 173.jpg\n", - "Downloaded: 176.jpg\n", - "Downloaded: 175.jpg\n", - "Downloaded: 142.jpg\n", - "Downloaded: 179.jpg\n", - "Downloaded: 177.jpg\n", - "Downloaded: 154.jpg\n", - "Downloaded: 150.jpg\n", - "Downloaded: 180.jpg\n", - "Downloaded: 182.jpg\n", - "Downloaded: 183.jpg\n", - "Downloaded: 184.jpg\n", - "Downloaded: 185.jpg\n", - "Downloaded: 161.jpg\n", - "Downloaded: 186.jpg\n", - "Downloaded: 190.jpg\n", - "Downloaded: 188.jpg\n", - "Downloaded: 196.jpg\n", - "Downloaded: 194.jpg\n", - "Downloaded: 195.jpg\n", - "Downloaded: 193.jpg\n", - "Downloaded: 200.jpg\n", - "Downloaded: 199.jpg\n", - "Downloaded: 202.jpg\n", - "Downloaded: 204.jpg\n", - "Downloaded: 201.jpg\n", - "Downloaded: 197.jpg\n", - "Downloaded: 208.jpg\n", - "Downloaded: 205.jpg\n", - "Downloaded: 207.jpg\n", - "Downloaded: 178.jpg\n", - "Downloaded: 206.jpg\n", - "Downloaded: 181.jpg\n", - "Downloaded: 209.jpg\n", - "Downloaded: 191.jpg\n", - "Downloaded: 210.jpg\n", - "Downloaded: 211.jpg\n", - "Downloaded: 213.jpg\n", - "Downloaded: 212.jpg\n", - "Downloaded: 219.jpg\n", - "Downloaded: 218.jpg\n", - "Downloaded: 216.jpg\n", - "Downloaded: 220.jpg\n", - "Downloaded: 215.jpg\n", - "Downloaded: 217.jpg\n", - "Downloaded: 187.jpg\n", - "Downloaded: 189.jpg\n", - "Downloaded: 221.jpg\n", - "Downloaded: 229.jpg\n", - "Downloaded: 223.jpg\n", - "Downloaded: 222.jpg\n", - "Downloaded: 228.jpg\n", - "Downloaded: 192.jpg\n", - "Downloaded: 198.jpg\n", - "Downloaded: 224.jpg\n", - "Downloaded: 231.jpg\n", - "Downloaded: 203.jpg\n", - "Downloaded: 232.jpg\n", - "Downloaded: 233.jpg\n", - "Downloaded: 234.jpg\n", - "Downloaded: 235.jpg\n", - "Downloaded: 236.jpg\n", - "Downloaded: 238.jpg\n", - "Downloaded: 241.jpg\n", - "Downloaded: 239.jpg\n", - "Downloaded: 242.jpg\n", - "Downloaded: 243.jpg\n", - "Downloaded: 244.jpg\n", - "Downloaded: 214.jpg\n", - "Downloaded: 248.jpg\n", - "Downloaded: 251.jpg\n", - "Downloaded: 225.jpg\n", - "Downloaded: 249.jpg\n", - "Downloaded: 247.jpg\n", - "Downloaded: 256.jpg\n", - "Downloaded: 227.jpg\n", - "Downloaded: 257.jpg\n", - "Downloaded: 250.jpg\n", - "Downloaded: 253.jpg\n", - "Downloaded: 226.jpg\n", - "Downloaded: 230.jpg\n", - "Downloaded: 254.jpg\n", - "Downloaded: 258.jpg\n", - "Downloaded: 260.jpg\n", - "Downloaded: 262.jpg\n", - "Downloaded: 261.jpg\n", - "Downloaded: 263.jpg\n", - "Downloaded: 237.jpg\n", - "Downloaded: 240.jpg\n", - "Downloaded: 245.jpg\n", - "Downloaded: 246.jpg\n", - "Downloaded: 264.jpg\n", - "Downloaded: 252.jpg\n", - "Downloaded: 255.jpg\n", - "Downloaded: 265.jpg\n", - "Downloaded: 268.jpg\n", - "Downloaded: 259.jpg\n", - "Downloaded: 269.jpg\n", - "Downloaded: 270.jpg\n", - "Downloaded: 267.jpg\n", - "Downloaded: 272.jpg\n", - "Downloaded: 275.jpg\n", - "Downloaded: 271.jpg\n", - "Downloaded: 274.jpg\n", - "Downloaded: 276.jpg\n", - "Downloaded: 277.jpg\n", - "Downloaded: 281.jpg\n", - "Downloaded: 273.jpg\n", - "Downloaded: 282.jpg\n", - "Downloaded: 278.jpg\n", - "Downloaded: 279.jpg\n", - "Downloaded: 280.jpg\n", - "Downloaded: 283.jpg\n", - "Downloaded: 284.jpg\n", - "Downloaded: 285.jpg\n", - "Downloaded: 286.jpg\n", - "Downloaded: 289.jpg\n", - "Downloaded: 290.jpg\n", - "Downloaded: 291.jpg\n", - "Downloaded: 287.jpg\n", - "Downloaded: 292.jpg\n", - "Downloaded: 293.jpg\n", - "Downloaded: 301.jpg\n", - "Downloaded: 303.jpg\n", - "Downloaded: 304.jpg\n", - "Downloaded: 302.jpg\n", - "Downloaded: 300.jpg\n", - "Downloaded: 266.jpg\n", - "Downloaded: 295.jpg\n", - "Downloaded: 294.jpg\n", - "Downloaded: 299.jpg\n", - "Downloaded: 297.jpg\n", - "Downloaded: 309.jpg\n", - "Downloaded: 296.jpg\n", - "Downloaded: 298.jpg\n", - "Downloaded: 306.jpg\n", - "Downloaded: 305.jpg\n", - "Downloaded: 307.jpg\n", - "Downloaded: 308.jpg\n", - "Downloaded: 310.jpg\n", - "Downloaded: 313.jpg\n", - "Downloaded: 314.jpg\n", - "Downloaded: 315.jpg\n", - "Downloaded: 316.jpg\n", - "Downloaded: 319.jpg\n", - "Downloaded: 321.jpg\n", - "Downloaded: 317.jpg\n", - "Downloaded: 320.jpg\n", - "Downloaded: 322.jpg\n", - "Downloaded: 323.jpg\n", - "Downloaded: 325.jpg\n", - "Downloaded: 324.jpg\n", - "Downloaded: 288.jpg\n", - "Downloaded: 329.jpg\n", - "Downloaded: 326.jpg\n", - "Downloaded: 328.jpg\n", - "Downloaded: 327.jpg\n", - "Downloaded: 330.jpg\n", - "Downloaded: 331.jpg\n", - "Downloaded: 333.jpg\n", - "Downloaded: 332.jpg\n", - "Downloaded: 334.jpg\n", - "Downloaded: 341.jpg\n", - "Downloaded: 339.jpg\n", - "Downloaded: 338.jpg\n", - "Downloaded: 337.jpg\n", - "Downloaded: 340.jpg\n", - "Downloaded: 343.jpg\n", - "Downloaded: 347.jpg\n", - "Downloaded: 312.jpg\n", - "Downloaded: 344.jpg\n", - "Downloaded: 345.jpg\n", - "Downloaded: 311.jpg\n", - "Downloaded: 348.jpg\n", - "Downloaded: 346.jpg\n", - "Downloaded: 352.jpg\n", - "Downloaded: 318.jpg\n", - "Downloaded: 351.jpg\n", - "Downloaded: 354.jpg\n", - "Downloaded: 358.jpg\n", - "Downloaded: 355.jpg\n", - "Downloaded: 350.jpg\n", - "Downloaded: 356.jpg\n", - "Downloaded: 357.jpg\n", - "Downloaded: 361.jpg\n", - "Downloaded: 362.jpg\n", - "Downloaded: 365.jpg\n", - "Downloaded: 364.jpg\n", - "Downloaded: 363.jpg\n", - "Downloaded: 366.jpg\n", - "Downloaded: 367.jpg\n", - "Downloaded: 335.jpg\n", - "Downloaded: 336.jpg\n", - "Downloaded: 371.jpg\n", - "Downloaded: 370.jpg\n", - "Downloaded: 342.jpg\n", - "Downloaded: 369.jpg\n", - "Downloaded: 375.jpg\n", - "Downloaded: 372.jpg\n", - "Downloaded: 374.jpg\n", - "Downloaded: 373.jpg\n", - "Downloaded: 378.jpg\n", - "Downloaded: 377.jpg\n", - "Downloaded: 376.jpg\n", - "Downloaded: 379.jpg\n", - "Downloaded: 380.jpg\n", - "Downloaded: 349.jpg\n", - "Downloaded: 359.jpg\n", - "Downloaded: 353.jpg\n", - "Downloaded: 382.jpg\n", - "Downloaded: 360.jpg\n", - "Downloaded: 383.jpg\n", - "Downloaded: 385.jpg\n", - "Downloaded: 384.jpg\n", - "Downloaded: 387.jpg\n", - "Downloaded: 368.jpg\n", - "Downloaded: 388.jpg\n", - "Downloaded: 389.jpg\n", - "Downloaded: 397.jpg\n", - "Downloaded: 390.jpg\n", - "Downloaded: 392.jpg\n", - "Downloaded: 395.jpg\n", - "Downloaded: 394.jpg\n", - "Downloaded: 396.jpg\n", - "Downloaded: 402.jpg\n", - "Downloaded: 398.jpg\n", - "Downloaded: 391.jpg\n", - "Downloaded: 399.jpg\n", - "Downloaded: 405.jpg\n", - "Downloaded: 404.jpg\n", - "Downloaded: 400.jpg\n", - "Downloaded: 408.jpg\n", - "Downloaded: 406.jpg\n", - "Downloaded: 409.jpg\n", - "Downloaded: 410.jpg\n", - "Downloaded: 381.jpg\n", - "Downloaded: 412.jpg\n", - "Downloaded: 411.jpg\n", - "Downloaded: 415.jpg\n", - "Downloaded: 413.jpg\n", - "Downloaded: 414.jpg\n", - "Downloaded: 386.jpg\n", - "Downloaded: 407.jpg\n", - "Downloaded: 417.jpg\n", - "Downloaded: 419.jpg\n", - "Downloaded: 420.jpg\n", - "Downloaded: 393.jpg\n", - "Downloaded: 422.jpg\n", - "Downloaded: 421.jpg\n", - "Downloaded: 433.jpg\n", - "Downloaded: 401.jpg\n", - "Downloaded: 428.jpg\n", - "Downloaded: 434.jpg\n", - "Downloaded: 427.jpg\n", - "Downloaded: 432.jpg\n", - "Downloaded: 426.jpg\n", - "Downloaded: 425.jpg\n", - "Downloaded: 423.jpg\n", - "Downloaded: 430.jpg\n", - "Downloaded: 437.jpg\n", - "Downloaded: 403.jpg\n", - "Downloaded: 435.jpg\n", - "Downloaded: 436.jpg\n", - "Downloaded: 440.jpg\n", - "Downloaded: 441.jpg\n", - "Downloaded: 442.jpg\n", - "Downloaded: 416.jpg\n", - "Downloaded: 443.jpg\n", - "Downloaded: 418.jpg\n", - "Downloaded: 445.jpg\n", - "Downloaded: 444.jpg\n", - "Downloaded: 446.jpg\n", - "Downloaded: 448.jpg\n", - "Downloaded: 455.jpg\n", - "Downloaded: 457.jpg\n", - "Downloaded: 431.jpg\n", - "Downloaded: 424.jpg\n", - "Downloaded: 449.jpg\n", - "Downloaded: 429.jpg\n", - "Downloaded: 458.jpg\n", - "Downloaded: 462.jpg\n", - "Downloaded: 463.jpg\n", - "Downloaded: 453.jpg\n", - "Downloaded: 451.jpg\n", - "Downloaded: 450.jpg\n", - "Downloaded: 459.jpg\n", - "Downloaded: 456.jpg\n", - "Downloaded: 464.jpg\n", - "Downloaded: 454.jpg\n", - "Downloaded: 438.jpg\n", - "Downloaded: 439.jpg\n", - "Downloaded: 467.jpg\n", - "Downloaded: 466.jpg\n", - "Downloaded: 479.jpg\n", - "Downloaded: 477.jpg\n", - "Downloaded: 482.jpg\n", - "Downloaded: 474.jpg\n", - "Downloaded: 476.jpg\n", - "Downloaded: 484.jpg\n", - "Downloaded: 473.jpg\n", - "Downloaded: 486.jpg\n", - "Downloaded: 488.jpg\n", - "Downloaded: 472.jpg\n", - "Downloaded: 471.jpg\n", - "Downloaded: 487.jpg\n", - "Downloaded: 475.jpg\n", - "Downloaded: 468.jpg\n", - "Downloaded: 489.jpg\n", - "Downloaded: 447.jpg\n", - "Downloaded: 469.jpg\n", - "Downloaded: 452.jpg\n", - "Downloaded: 461.jpg\n", - "Downloaded: 460.jpg\n", - "Downloaded: 491.jpg\n", - "Downloaded: 493.jpg\n", - "Downloaded: 465.jpg\n", - "Downloaded: 496.jpg\n", - "Downloaded: 494.jpg\n", - "Downloaded: 498.jpg\n", - "Downloaded: 499.jpg\n", - "Downloaded: 501.jpg\n", - "Downloaded: 509.jpg\n", - "Downloaded: 510.jpg\n", - "Downloaded: 507.jpg\n", - "Downloaded: 503.jpg\n", - "Downloaded: 505.jpg\n", - "Downloaded: 497.jpg\n", - "Downloaded: 508.jpg\n", - "Downloaded: 481.jpg\n", - "Downloaded: 480.jpg\n", - "Downloaded: 512.jpg\n", - "Downloaded: 500.jpg\n", - "Downloaded: 478.jpg\n", - "Downloaded: 485.jpg\n", - "Downloaded: 504.jpg\n", - "Downloaded: 483.jpg\n", - "Downloaded: 513.jpg\n", - "Downloaded: 490.jpg\n", - "Downloaded: 495.jpg\n", - "Downloaded: 511.jpg\n", - "Downloaded: 470.jpg\n", - "Downloaded: 492.jpg\n", - "Downloaded: 502.jpg\n", - "Downloaded: 506.jpg\n", - "Downloaded: 514.jpg\n", - "Downloaded: 517.jpg\n", - "Downloaded: 520.jpg\n", - "Downloaded: 518.jpg\n", - "Downloaded: 516.jpg\n", - "Downloaded: 522.jpg\n", - "Downloaded: 519.jpg\n", - "Downloaded: 523.jpg\n", - "Downloaded: 521.jpg\n", - "Downloaded: 526.jpg\n", - "Downloaded: 527.jpg\n", - "Downloaded: 530.jpg\n", - "Downloaded: 528.jpg\n", - "Downloaded: 533.jpg\n", - "Downloaded: 536.jpg\n", - "Downloaded: 531.jpg\n", - "Downloaded: 537.jpg\n", - "Downloaded: 539.jpg\n", - "Downloaded: 535.jpg\n", - "Downloaded: 529.jpg\n", - "Downloaded: 534.jpg\n", - "Downloaded: 538.jpg\n", - "Downloaded: 540.jpg\n", - "Downloaded: 541.jpg\n", - "Downloaded: 543.jpg\n", - "Downloaded: 542.jpg\n", - "Downloaded: 544.jpg\n", - "Downloaded: 552.jpg\n", - "Downloaded: 545.jpg\n", - "Downloaded: 546.jpg\n", - "Downloaded: 547.jpg\n", - "Downloaded: 554.jpg\n", - "Downloaded: 549.jpg\n", - "Downloaded: 551.jpg\n", - "Downloaded: 550.jpg\n", - "Downloaded: 553.jpg\n", - "Downloaded: 555.jpg\n", - "Downloaded: 548.jpg\n", - "Downloaded: 556.jpg\n", - "Downloaded: 557.jpg\n", - "Downloaded: 561.jpg\n", - "Downloaded: 563.jpg\n", - "Downloaded: 562.jpg\n", - "Downloaded: 565.jpg\n", - "Downloaded: 566.jpg\n", - "Downloaded: 515.jpg\n", - "Downloaded: 525.jpg\n", - "Downloaded: 524.jpg\n", - "Downloaded: 532.jpg\n", - "Downloaded: 567.jpg\n", - "Downloaded: 569.jpg\n", - "Downloaded: 568.jpg\n", - "Downloaded: 570.jpg\n", - "Downloaded: 575.jpg\n", - "Downloaded: 577.jpg\n", - "Downloaded: 576.jpg\n", - "Downloaded: 578.jpg\n", - "Downloaded: 581.jpg\n", - "Downloaded: 579.jpg\n", - "Downloaded: 583.jpg\n", - "Downloaded: 574.jpg\n", - "Downloaded: 580.jpg\n", - "Downloaded: 571.jpg\n", - "Downloaded: 573.jpg\n", - "Downloaded: 582.jpg\n", - "Downloaded: 572.jpg\n", - "Downloaded: 585.jpg\n", - "Downloaded: 591.jpg\n", - "Downloaded: 590.jpg\n", - "Downloaded: 586.jpg\n", - "Downloaded: 587.jpg\n", - "Downloaded: 589.jpg\n", - "Downloaded: 593.jpg\n", - "Downloaded: 594.jpg\n", - "Downloaded: 595.jpg\n", - "Downloaded: 559.jpg\n", - "Downloaded: 558.jpg\n", - "Downloaded: 560.jpg\n", - "Downloaded: 564.jpg\n", - "Downloaded: 592.jpg\n", - "Downloaded: 596.jpg\n", - "Downloaded: 597.jpg\n", - "Downloaded: 602.jpg\n", - "Downloaded: 608.jpg\n", - "Downloaded: 605.jpg\n", - "Downloaded: 604.jpg\n", - "Downloaded: 598.jpg\n", - "Downloaded: 599.jpg\n", - "Downloaded: 612.jpg\n", - "Downloaded: 584.jpg\n", - "Downloaded: 610.jpg\n", - "Downloaded: 609.jpg\n", - "Downloaded: 616.jpg\n", - "Downloaded: 615.jpg\n", - "Downloaded: 601.jpg\n", - "Downloaded: 611.jpg\n", - "Downloaded: 613.jpg\n", - "Downloaded: 614.jpg\n", - "Downloaded: 617.jpg\n", - "Downloaded: 603.jpg\n", - "Downloaded: 600.jpg\n", - "Downloaded: 588.jpg\n", - "Downloaded: 618.jpg\n", - "Downloaded: 619.jpg\n", - "Downloaded: 621.jpg\n", - "Downloaded: 620.jpg\n", - "Downloaded: 622.jpg\n", - "Downloaded: 623.jpg\n", - "Downloaded: 606.jpg\n", - "Downloaded: 607.jpg\n", - "Downloaded: 624.jpg\n", - "Downloaded: 630.jpg\n", - "Downloaded: 627.jpg\n", - "Downloaded: 625.jpg\n", - "Downloaded: 631.jpg\n", - "Downloaded: 629.jpg\n", - "Downloaded: 633.jpg\n", - "Downloaded: 647.jpg\n", - "Downloaded: 650.jpg\n", - "Downloaded: 642.jpg\n", - "Downloaded: 626.jpg\n", - "Downloaded: 649.jpg\n", - "Downloaded: 648.jpg\n", - "Downloaded: 632.jpg\n", - "Downloaded: 636.jpg\n", - "Downloaded: 638.jpg\n", - "Downloaded: 651.jpg\n", - "Downloaded: 634.jpg\n", - "Downloaded: 640.jpg\n", - "Downloaded: 639.jpg\n", - "Downloaded: 637.jpg\n", - "Downloaded: 643.jpg\n", - "Downloaded: 645.jpg\n", - "Downloaded: 644.jpg\n", - "Downloaded: 641.jpg\n", - "Downloaded: 652.jpg\n", - "Downloaded: 635.jpg\n", - "Downloaded: 653.jpg\n", - "Downloaded: 657.jpg\n", - "Downloaded: 656.jpg\n", - "Downloaded: 658.jpg\n", - "Downloaded: 628.jpg\n", - "Downloaded: 660.jpg\n", - "Downloaded: 661.jpg\n", - "Downloaded: 663.jpg\n", - "Downloaded: 668.jpg\n", - "Downloaded: 664.jpg\n", - "Downloaded: 669.jpg\n", - "Downloaded: 665.jpg\n", - "Downloaded: 666.jpg\n", - "Downloaded: 671.jpg\n", - "Downloaded: 674.jpg\n", - "Downloaded: 678.jpg\n", - "Downloaded: 673.jpg\n", - "Downloaded: 677.jpg\n", - "Downloaded: 679.jpg\n", - "Downloaded: 675.jpg\n", - "Downloaded: 676.jpg\n", - "Downloaded: 646.jpg\n", - "Downloaded: 681.jpg\n", - "Downloaded: 682.jpg\n", - "Downloaded: 655.jpg\n", - "Downloaded: 683.jpg\n", - "Downloaded: 654.jpg\n", - "Downloaded: 684.jpg\n", - "Downloaded: 687.jpg\n", - "Downloaded: 659.jpg\n", - "Downloaded: 689.jpg\n", - "Downloaded: 691.jpg\n", - "Downloaded: 690.jpg\n", - "Downloaded: 672.jpg\n", - "Downloaded: 667.jpg\n", - "Downloaded: 662.jpg\n", - "Downloaded: 701.jpg\n", - "Downloaded: 699.jpg\n", - "Downloaded: 700.jpg\n", - "Downloaded: 697.jpg\n", - "Downloaded: 688.jpg\n", - "Downloaded: 670.jpg\n", - "Downloaded: 694.jpg\n", - "Downloaded: 693.jpg\n", - "Downloaded: 696.jpg\n", - "Downloaded: 692.jpg\n", - "Downloaded: 702.jpg\n", - "Downloaded: 695.jpg\n", - "Downloaded: 706.jpg\n", - "Downloaded: 704.jpg\n", - "Downloaded: 680.jpg\n", - "Downloaded: 698.jpg\n", - "Downloaded: 707.jpg\n", - "Downloaded: 705.jpg\n", - "Downloaded: 708.jpg\n", - "Downloaded: 711.jpg\n", - "Downloaded: 713.jpg\n", - "Downloaded: 712.jpg\n", - "Downloaded: 719.jpg\n", - "Downloaded: 720.jpg\n", - "Downloaded: 685.jpg\n", - "Downloaded: 718.jpg\n", - "Downloaded: 686.jpg\n", - "Downloaded: 721.jpg\n", - "Downloaded: 723.jpg\n", - "Downloaded: 727.jpg\n", - "Downloaded: 724.jpg\n", - "Downloaded: 729.jpg\n", - "Downloaded: 726.jpg\n", - "Downloaded: 730.jpg\n", - "Downloaded: 728.jpg\n", - "Downloaded: 703.jpg\n", - "Downloaded: 733.jpg\n", - "Downloaded: 734.jpg\n", - "Downloaded: 736.jpg\n", - "Downloaded: 739.jpg\n", - "Downloaded: 740.jpg\n", - "Downloaded: 741.jpg\n", - "Downloaded: 742.jpg\n", - "Downloaded: 738.jpg\n", - "Downloaded: 743.jpg\n", - "Downloaded: 709.jpg\n", - "Downloaded: 710.jpg\n", - "Downloaded: 744.jpg\n", - "Downloaded: 714.jpg\n", - "Downloaded: 717.jpg\n", - "Downloaded: 716.jpg\n", - "Downloaded: 745.jpg\n", - "Downloaded: 750.jpg\n", - "Downloaded: 749.jpg\n", - "Downloaded: 747.jpg\n", - "Downloaded: 751.jpg\n", - "Downloaded: 715.jpg\n", - "Downloaded: 722.jpg\n", - "Downloaded: 753.jpg\n", - "Downloaded: 752.jpg\n", - "Downloaded: 754.jpg\n", - "Downloaded: 725.jpg\n", - "Downloaded: 731.jpg\n", - "Downloaded: 732.jpg\n", - "Downloaded: 755.jpg\n", - "Downloaded: 735.jpg\n", - "Downloaded: 737.jpg\n", - "Downloaded: 756.jpg\n", - "Downloaded: 757.jpg\n", - "Downloaded: 758.jpg\n", - "Downloaded: 775.jpg\n", - "Downloaded: 759.jpg\n", - "Downloaded: 760.jpg\n", - "Downloaded: 763.jpg\n", - "Downloaded: 762.jpg\n", - "Downloaded: 761.jpg\n", - "Downloaded: 778.jpg\n", - "Downloaded: 764.jpg\n", - "Downloaded: 766.jpg\n", - "Downloaded: 765.jpg\n", - "Downloaded: 776.jpg\n", - "Downloaded: 777.jpg\n", - "Downloaded: 768.jpg\n", - "Downloaded: 746.jpg\n", - "Downloaded: 767.jpg\n", - "Downloaded: 771.jpg\n", - "Downloaded: 748.jpg\n", - "Downloaded: 773.jpg\n", - "Downloaded: 770.jpg\n", - "Downloaded: 779.jpg\n", - "Downloaded: 769.jpg\n", - "Downloaded: 781.jpg\n", - "Downloaded: 780.jpg\n", - "Downloaded: 782.jpg\n", - "Downloaded: 774.jpg\n", - "Downloaded: 783.jpg\n", - "Downloaded: 784.jpg\n", - "Downloaded: 786.jpg\n", - "Downloaded: 792.jpg\n", - "Downloaded: 790.jpg\n", - "Downloaded: 787.jpg\n", - "Downloaded: 788.jpg\n", - "Downloaded: 789.jpg\n", - "Downloaded: 793.jpg\n", - "Downloaded: 801.jpg\n", - "Downloaded: 795.jpg\n", - "Downloaded: 798.jpg\n", - "Downloaded: 800.jpg\n", - "Downloaded: 791.jpg\n", - "Downloaded: 797.jpg\n", - "Downloaded: 796.jpg\n", - "Downloaded: 794.jpg\n", - "Downloaded: 803.jpg\n", - "Downloaded: 804.jpg\n", - "Downloaded: 799.jpg\n", - "Downloaded: 802.jpg\n", - "Downloaded: 805.jpg\n", - "Downloaded: 806.jpg\n", - "Downloaded: 809.jpg\n", - "Downloaded: 810.jpg\n", - "Downloaded: 807.jpg\n", - "Downloaded: 772.jpg\n", - "Downloaded: 812.jpg\n", - "Downloaded: 813.jpg\n", - "Downloaded: 814.jpg\n", - "Downloaded: 785.jpg\n", - "Downloaded: 816.jpg\n", - "Downloaded: 815.jpg\n", - "Downloaded: 824.jpg\n", - "Downloaded: 821.jpg\n", - "Downloaded: 827.jpg\n", - "Downloaded: 822.jpg\n", - "Downloaded: 823.jpg\n", - "Downloaded: 817.jpg\n", - "Downloaded: 819.jpg\n", - "Downloaded: 818.jpg\n", - "Downloaded: 834.jpg\n", - "Downloaded: 833.jpg\n", - "Downloaded: 835.jpg\n", - "Downloaded: 836.jpg\n", - "Downloaded: 837.jpg\n", - "Downloaded: 832.jpg\n", - "Downloaded: 829.jpg\n", - "Downloaded: 838.jpg\n", - "Downloaded: 831.jpg\n", - "Downloaded: 830.jpg\n", - "Downloaded: 811.jpg\n", - "Downloaded: 839.jpg\n", - "Downloaded: 808.jpg\n", - "Downloaded: 840.jpg\n", - "Downloaded: 841.jpg\n", - "Downloaded: 842.jpg\n", - "Downloaded: 826.jpg\n", - "Downloaded: 858.jpg\n", - "Downloaded: 825.jpg\n", - "Downloaded: 820.jpg\n", - "Downloaded: 845.jpg\n", - "Downloaded: 828.jpg\n", - "Downloaded: 843.jpg\n", - "Downloaded: 848.jpg\n", - "Downloaded: 844.jpg\n", - "Downloaded: 860.jpg\n", - "Downloaded: 861.jpg\n", - "Downloaded: 854.jpg\n", - "Downloaded: 847.jpg\n", - "Downloaded: 852.jpg\n", - "Downloaded: 851.jpg\n", - "Downloaded: 862.jpg\n", - "Downloaded: 853.jpg\n", - "Downloaded: 857.jpg\n", - "Downloaded: 859.jpg\n", - "Downloaded: 856.jpg\n", - "Downloaded: 855.jpg\n", - "Downloaded: 863.jpg\n", - "Downloaded: 846.jpg\n", - "Downloaded: 864.jpg\n", - "Downloaded: 849.jpg\n", - "Downloaded: 865.jpg\n", - "Downloaded: 866.jpg\n", - "Downloaded: 867.jpg\n", - "Downloaded: 868.jpg\n", - "Downloaded: 880.jpg\n", - "Downloaded: 878.jpg\n", - "Downloaded: 879.jpg\n", - "Downloaded: 885.jpg\n", - "Downloaded: 883.jpg\n", - "Downloaded: 882.jpg\n", - "Downloaded: 884.jpg\n", - "Downloaded: 875.jpg\n", - "Downloaded: 881.jpg\n", - "Downloaded: 876.jpg\n", - "Downloaded: 888.jpg\n", - "Downloaded: 886.jpg\n", - "Downloaded: 889.jpg\n", - "Downloaded: 890.jpg\n", - "Downloaded: 877.jpg\n", - "Downloaded: 892.jpg\n", - "Downloaded: 869.jpg\n", - "Downloaded: 891.jpg\n", - "Downloaded: 871.jpg\n", - "Downloaded: 870.jpg\n", - "Downloaded: 874.jpg\n", - "Downloaded: 872.jpg\n", - "Downloaded: 873.jpg\n", - "Downloaded: 850.jpg\n", - "Downloaded: 894.jpg\n", - "Downloaded: 893.jpg\n", - "Downloaded: 897.jpg\n", - "Downloaded: 887.jpg\n", - "Downloaded: 898.jpg\n", - "Downloaded: 917.jpg\n", - "Downloaded: 915.jpg\n", - "Downloaded: 899.jpg\n", - "Downloaded: 916.jpg\n", - "Downloaded: 911.jpg\n", - "Downloaded: 901.jpg\n", - "Downloaded: 919.jpg\n", - "Downloaded: 909.jpg\n", - "Downloaded: 907.jpg\n", - "Downloaded: 903.jpg\n", - "Downloaded: 908.jpg\n", - "Downloaded: 904.jpg\n", - "Downloaded: 918.jpg\n", - "Downloaded: 910.jpg\n", - "Downloaded: 905.jpg\n", - "Downloaded: 906.jpg\n", - "Downloaded: 913.jpg\n", - "Downloaded: 912.jpg\n", - "Downloaded: 914.jpg\n", - "Downloaded: 921.jpg\n", - "Downloaded: 920.jpg\n", - "Downloaded: 922.jpg\n", - "Downloaded: 895.jpg\n", - "Downloaded: 896.jpg\n", - "Downloaded: 924.jpg\n", - "Downloaded: 925.jpg\n", - "Downloaded: 929.jpg\n", - "Downloaded: 930.jpg\n", - "Downloaded: 928.jpg\n", - "Downloaded: 927.jpg\n", - "Downloaded: 934.jpg\n", - "Downloaded: 900.jpg\n", - "Downloaded: 933.jpg\n", - "Downloaded: 931.jpg\n", - "Downloaded: 941.jpg\n", - "Downloaded: 937.jpg\n", - "Downloaded: 939.jpg\n", - "Downloaded: 940.jpg\n", - "Downloaded: 902.jpg\n", - "Downloaded: 932.jpg\n", - "Downloaded: 943.jpg\n", - "Downloaded: 944.jpg\n", - "Downloaded: 938.jpg\n", - "Downloaded: 946.jpg\n", - "Downloaded: 936.jpg\n", - "Downloaded: 935.jpg\n", - "Downloaded: 947.jpg\n", - "Downloaded: 948.jpg\n", - "Downloaded: 949.jpg\n", - "Downloaded: 923.jpg\n", - "Downloaded: 950.jpg\n", - "Downloaded: 951.jpg\n", - "Downloaded: 961.jpg\n", - "Downloaded: 953.jpg\n", - "Downloaded: 954.jpg\n", - "Downloaded: 952.jpg\n", - "Downloaded: 959.jpg\n", - "Downloaded: 958.jpg\n", - "Downloaded: 955.jpg\n", - "Downloaded: 956.jpg\n", - "Downloaded: 957.jpg\n", - "Downloaded: 942.jpg\n", - "Downloaded: 967.jpg\n", - "Downloaded: 962.jpg\n", - "Downloaded: 965.jpg\n", - "Downloaded: 964.jpg\n", - "Downloaded: 969.jpg\n", - "Downloaded: 971.jpg\n", - "Downloaded: 945.jpg\n", - "Downloaded: 963.jpg\n", - "Downloaded: 970.jpg\n", - "Downloaded: 972.jpg\n", - "Downloaded: 926.jpg\n", - "Downloaded: 968.jpg\n", - "Downloaded: 974.jpg\n", - "Downloaded: 966.jpg\n", - "Downloaded: 975.jpg\n", - "Downloaded: 976.jpg\n", - "Downloaded: 977.jpg\n", - "Downloaded: 978.jpg\n", - "Downloaded: 980.jpg\n", - "Downloaded: 983.jpg\n", - "Downloaded: 982.jpg\n", - "Downloaded: 979.jpg\n", - "Downloaded: 985.jpg\n", - "Downloaded: 987.jpg\n", - "Downloaded: 981.jpg\n", - "Downloaded: 960.jpg\n", - "Downloaded: 984.jpg\n", - "Downloaded: 994.jpg\n", - "Downloaded: 995.jpg\n", - "Downloaded: 986.jpg\n", - "Downloaded: 989.jpg\n", - "Downloaded: 998.jpg\n", - "Downloaded: 999.jpg\n", - "Downloaded: 997.jpg\n", - "Downloaded: 996.jpg\n", - "Downloaded: 993.jpg\n", - "Downloaded: 990.jpg\n", - "Downloaded: 991.jpg\n", - "Downloaded: 1001.jpg\n", - "Downloaded: 1002.jpg\n", - "Downloaded: 1003.jpg\n", - "Downloaded: 973.jpg\n", - "Downloaded: 1004.jpg\n", - "Downloaded: 1005.jpg\n", - "Downloaded: 1006.jpg\n", - "Downloaded: 1015.jpg\n", - "Downloaded: 1014.jpg\n", - "Downloaded: 1016.jpg\n", - "Downloaded: 1013.jpg\n", - "Downloaded: 1020.jpg\n", - "Downloaded: 1018.jpg\n", - "Downloaded: 1012.jpg\n", - "Downloaded: 1010.jpg\n", - "Downloaded: 1022.jpg\n", - "Downloaded: 988.jpg\n", - "Downloaded: 1019.jpg\n", - "Downloaded: 1021.jpg\n", - "Downloaded: 1023.jpg\n", - "Downloaded: 1007.jpg\n", - "Downloaded: 1026.jpg\n", - "Downloaded: 1009.jpg\n", - "Downloaded: 1024.jpg\n", - "Downloaded: 1028.jpg\n", - "Downloaded: 1025.jpg\n", - "Downloaded: 1008.jpg\n", - "Downloaded: 1030.jpg\n", - "Downloaded: 1000.jpg\n", - "Downloaded: 992.jpg\n", - "Downloaded: 1031.jpg\n", - "Downloaded: 1032.jpg\n", - "Downloaded: 1029.jpg\n", - "Downloaded: 1052.jpg\n", - "Downloaded: 1033.jpg\n", - "Downloaded: 1041.jpg\n", - "Downloaded: 1035.jpg\n", - "Downloaded: 1043.jpg\n", - "Downloaded: 1042.jpg\n", - "Downloaded: 1040.jpg\n", - "Downloaded: 1044.jpg\n", - "Downloaded: 1038.jpg\n", - "Downloaded: 1037.jpg\n", - "Downloaded: 1034.jpg\n", - "Downloaded: 1050.jpg\n", - "Downloaded: 1039.jpg\n", - "Downloaded: 1046.jpg\n", - "Downloaded: 1045.jpg\n", - "Downloaded: 1048.jpg\n", - "Downloaded: 1051.jpg\n", - "Downloaded: 1049.jpg\n", - "Downloaded: 1047.jpg\n", - "Downloaded: 1011.jpg\n", - "Downloaded: 1017.jpg\n", - "Downloaded: 1053.jpg\n", - "Downloaded: 1027.jpg\n", - "Downloaded: 1054.jpg\n", - "Downloaded: 1055.jpg\n", - "Downloaded: 1056.jpg\n", - "Downloaded: 1057.jpg\n", - "Downloaded: 1059.jpg\n", - "Downloaded: 1060.jpg\n", - "Downloaded: 1036.jpg\n", - "Downloaded: 1080.jpg\n", - "Downloaded: 1062.jpg\n", - "Downloaded: 1064.jpg\n", - "Downloaded: 1069.jpg\n", - "Downloaded: 1067.jpg\n", - "Downloaded: 1068.jpg\n", - "Downloaded: 1066.jpg\n", - "Downloaded: 1065.jpg\n", - "Downloaded: 1063.jpg\n", - "Downloaded: 1070.jpg\n", - "Downloaded: 1077.jpg\n", - "Downloaded: 1071.jpg\n", - "Downloaded: 1073.jpg\n", - "Downloaded: 1081.jpg\n", - "Downloaded: 1074.jpg\n", - "Downloaded: 1076.jpg\n", - "Downloaded: 1075.jpg\n", - "Downloaded: 1078.jpg\n", - "Downloaded: 1082.jpg\n", - "Downloaded: 1079.jpg\n", - "Downloaded: 1072.jpg\n", - "Downloaded: 1058.jpg\n", - "Downloaded: 1084.jpg\n", - "Downloaded: 1085.jpg\n", - "Downloaded: 1087.jpg\n", - "Downloaded: 1086.jpg\n", - "Downloaded: 1106.jpg\n", - "Downloaded: 1108.jpg\n", - "Downloaded: 1109.jpg\n", - "Downloaded: 1088.jpg\n", - "Downloaded: 1107.jpg\n", - "Downloaded: 1110.jpg\n", - "Downloaded: 1112.jpg\n", - "Downloaded: 1111.jpg\n", - "Downloaded: 1089.jpg\n", - "Downloaded: 1061.jpg\n", - "Downloaded: 1115.jpg\n", - "Downloaded: 1114.jpg\n", - "Downloaded: 1091.jpg\n", - "Downloaded: 1090.jpg\n", - "Downloaded: 1092.jpg\n", - "Downloaded: 1094.jpg\n", - "Downloaded: 1093.jpg\n", - "Downloaded: 1096.jpg\n", - "Downloaded: 1095.jpg\n", - "Downloaded: 1100.jpg\n", - "Downloaded: 1097.jpg\n", - "Downloaded: 1104.jpg\n", - "Downloaded: 1083.jpg\n", - "Downloaded: 1101.jpg\n", - "Downloaded: 1103.jpg\n", - "Downloaded: 1098.jpg\n", - "Downloaded: 1102.jpg\n", - "Downloaded: 1099.jpg\n", - "Downloaded: 1105.jpg\n", - "Downloaded: 1116.jpg\n", - "Downloaded: 1118.jpg\n", - "Downloaded: 1117.jpg\n", - "Downloaded: 1120.jpg\n", - "Downloaded: 1125.jpg\n", - "Downloaded: 1123.jpg\n", - "Downloaded: 1122.jpg\n", - "Downloaded: 1126.jpg\n", - "Downloaded: 1127.jpg\n", - "Downloaded: 1124.jpg\n", - "Downloaded: 1121.jpg\n", - "Downloaded: 1129.jpg\n", - "Downloaded: 1113.jpg\n", - "Downloaded: 1132.jpg\n", - "Downloaded: 1128.jpg\n", - "Downloaded: 1134.jpg\n", - "Downloaded: 1131.jpg\n", - "Downloaded: 1135.jpg\n", - "Downloaded: 1133.jpg\n", - "Downloaded: 1139.jpg\n", - "Downloaded: 1138.jpg\n", - "Downloaded: 1130.jpg\n", - "Downloaded: 1140.jpg\n", - "Downloaded: 1137.jpg\n", - "Downloaded: 1143.jpg\n", - "Downloaded: 1142.jpg\n", - "Downloaded: 1144.jpg\n", - "Downloaded: 1145.jpg\n", - "Downloaded: 1146.jpg\n", - "Downloaded: 1147.jpg\n", - "Downloaded: 1148.jpg\n", - "Downloaded: 1136.jpg\n", - "Downloaded: 1119.jpg\n", - "Downloaded: 1157.jpg\n", - "Downloaded: 1149.jpg\n", - "Downloaded: 1158.jpg\n", - "Downloaded: 1150.jpg\n", - "Downloaded: 1160.jpg\n", - "Downloaded: 1155.jpg\n", - "Downloaded: 1162.jpg\n", - "Downloaded: 1154.jpg\n", - "Downloaded: 1156.jpg\n", - "Downloaded: 1151.jpg\n", - "Downloaded: 1164.jpg\n", - "Downloaded: 1166.jpg\n", - "Downloaded: 1152.jpg\n", - "Downloaded: 1167.jpg\n", - "Downloaded: 1170.jpg\n", - "Downloaded: 1169.jpg\n", - "Downloaded: 1163.jpg\n", - "Downloaded: 1168.jpg\n", - "Downloaded: 1173.jpg\n", - "Downloaded: 1141.jpg\n", - "Downloaded: 1177.jpg\n", - "Downloaded: 1181.jpg\n", - "Downloaded: 1180.jpg\n", - "Downloaded: 1185.jpg\n", - "Downloaded: 1179.jpg\n", - "Downloaded: 1183.jpg\n", - "Downloaded: 1182.jpg\n", - "Downloaded: 1184.jpg\n", - "Downloaded: 1187.jpg\n", - "Downloaded: 1186.jpg\n", - "Downloaded: 1188.jpg\n", - "Downloaded: 1192.jpg\n", - "Downloaded: 1190.jpg\n", - "Downloaded: 1191.jpg\n", - "Downloaded: 1189.jpg\n", - "Downloaded: 1194.jpg\n", - "Downloaded: 1193.jpg\n", - "Downloaded: 1159.jpg\n", - "Downloaded: 1161.jpg\n", - "Downloaded: 1195.jpg\n", - "Downloaded: 1153.jpg\n", - "Downloaded: 1171.jpg\n", - "Downloaded: 1196.jpg\n", - "Downloaded: 1165.jpg\n", - "Downloaded: 1197.jpg\n", - "Downloaded: 1198.jpg\n", - "Downloaded: 1172.jpg\n", - "Downloaded: 1200.jpg\n", - "Downloaded: 1175.jpg\n", - "Downloaded: 1199.jpg\n", - "Downloaded: 1202.jpg\n", - "Downloaded: 1176.jpg\n", - "Downloaded: 1174.jpg\n", - "Downloaded: 1205.jpg\n", - "Downloaded: 1178.jpg\n", - "Downloaded: 1204.jpg\n", - "Downloaded: 1206.jpg\n", - "Downloaded: 1207.jpg\n", - "Downloaded: 1208.jpg\n", - "Downloaded: 1218.jpg\n", - "Downloaded: 1210.jpg\n", - "Downloaded: 1212.jpg\n", - "Downloaded: 1215.jpg\n", - "Downloaded: 1213.jpg\n", - "Downloaded: 1209.jpg\n", - "Downloaded: 1216.jpg\n", - "Downloaded: 1219.jpg\n", - "Downloaded: 1217.jpg\n", - "Downloaded: 1220.jpg\n", - "Downloaded: 1223.jpg\n", - "Downloaded: 1222.jpg\n", - "Downloaded: 1221.jpg\n", - "Downloaded: 1225.jpg\n", - "Downloaded: 1226.jpg\n", - "Downloaded: 1224.jpg\n", - "Downloaded: 1233.jpg\n", - "Downloaded: 1227.jpg\n", - "Downloaded: 1234.jpg\n", - "Downloaded: 1228.jpg\n", - "Downloaded: 1237.jpg\n", - "Downloaded: 1235.jpg\n", - "Downloaded: 1236.jpg\n", - "Downloaded: 1230.jpg\n", - "Downloaded: 1241.jpg\n", - "Downloaded: 1201.jpg\n", - "Downloaded: 1232.jpg\n", - "Downloaded: 1231.jpg\n", - "Downloaded: 1242.jpg\n", - "Downloaded: 1238.jpg\n", - "Downloaded: 1240.jpg\n", - "Downloaded: 1244.jpg\n", - "Downloaded: 1243.jpg\n", - "Downloaded: 1203.jpg\n", - "Downloaded: 1245.jpg\n", - "Downloaded: 1211.jpg\n", - "Downloaded: 1246.jpg\n", - "Downloaded: 1214.jpg\n", - "Downloaded: 1247.jpg\n", - "Downloaded: 1248.jpg\n", - "Downloaded: 1249.jpg\n", - "Downloaded: 1257.jpg\n", - "Downloaded: 1263.jpg\n", - "Downloaded: 1259.jpg\n", - "Downloaded: 1262.jpg\n", - "Downloaded: 1264.jpg\n", - "Downloaded: 1266.jpg\n", - "Downloaded: 1252.jpg\n", - "Downloaded: 1261.jpg\n", - "Downloaded: 1265.jpg\n", - "Downloaded: 1260.jpg\n", - "Downloaded: 1258.jpg\n", - "Downloaded: 1250.jpg\n", - "Downloaded: 1267.jpg\n", - "Downloaded: 1269.jpg\n", - "Downloaded: 1271.jpg\n", - "Downloaded: 1270.jpg\n", - "Downloaded: 1272.jpg\n", - "Downloaded: 1253.jpg\n", - "Downloaded: 1229.jpg\n", - "Downloaded: 1254.jpg\n", - "Downloaded: 1239.jpg\n", - "Downloaded: 1255.jpg\n", - "Downloaded: 1274.jpg\n", - "Downloaded: 1273.jpg\n", - "Downloaded: 1256.jpg\n", - "Downloaded: 1275.jpg\n", - "Downloaded: 1278.jpg\n", - "Downloaded: 1277.jpg\n", - "Downloaded: 1283.jpg\n", - "Downloaded: 1288.jpg\n", - "Downloaded: 1282.jpg\n", - "Downloaded: 1289.jpg\n", - "Downloaded: 1284.jpg\n", - "Downloaded: 1285.jpg\n", - "Downloaded: 1279.jpg\n", - "Downloaded: 1281.jpg\n", - "Downloaded: 1290.jpg\n", - "Downloaded: 1286.jpg\n", - "Downloaded: 1292.jpg\n", - "Downloaded: 1291.jpg\n", - "Downloaded: 1293.jpg\n", - "Downloaded: 1294.jpg\n", - "Downloaded: 1295.jpg\n", - "Downloaded: 1296.jpg\n", - "Downloaded: 1297.jpg\n", - "Downloaded: 1298.jpg\n", - "Downloaded: 1299.jpg\n", - "Downloaded: 1302.jpg\n", - "Downloaded: 1301.jpg\n", - "Downloaded: 1251.jpg\n", - "Downloaded: 1300.jpg\n", - "Downloaded: 1303.jpg\n", - "Downloaded: 1268.jpg\n", - "Downloaded: 1320.jpg\n", - "Downloaded: 1319.jpg\n", - "Downloaded: 1315.jpg\n", - "Downloaded: 1316.jpg\n", - "Downloaded: 1314.jpg\n", - "Downloaded: 1321.jpg\n", - "Downloaded: 1313.jpg\n", - "Downloaded: 1312.jpg\n", - "Downloaded: 1322.jpg\n", - "Downloaded: 1309.jpg\n", - "Downloaded: 1304.jpg\n", - "Downloaded: 1305.jpg\n", - "Downloaded: 1311.jpg\n", - "Downloaded: 1323.jpg\n", - "Downloaded: 1306.jpg\n", - "Downloaded: 1310.jpg\n", - "Downloaded: 1324.jpg\n", - "Downloaded: 1325.jpg\n", - "Downloaded: 1326.jpg\n", - "Downloaded: 1327.jpg\n", - "Downloaded: 1329.jpg\n", - "Downloaded: 1328.jpg\n", - "Downloaded: 1276.jpg\n", - "Downloaded: 1330.jpg\n", - "Downloaded: 1332.jpg\n", - "Downloaded: 1331.jpg\n", - "Downloaded: 1333.jpg\n", - "Downloaded: 1336.jpg\n", - "Downloaded: 1338.jpg\n", - "Downloaded: 1335.jpg\n", - "Downloaded: 1339.jpg\n", - "Downloaded: 1337.jpg\n", - "Downloaded: 1340.jpg\n", - "Downloaded: 1341.jpg\n", - "Downloaded: 1342.jpg\n", - "Downloaded: 1343.jpg\n", - "Downloaded: 1344.jpg\n", - "Downloaded: 1280.jpg\n", - "Downloaded: 1346.jpg\n", - "Downloaded: 1347.jpg\n", - "Downloaded: 1345.jpg\n", - "Downloaded: 1348.jpg\n", - "Downloaded: 1349.jpg\n", - "Downloaded: 1287.jpg\n", - "Downloaded: 1350.jpg\n", - "Downloaded: 1353.jpg\n", - "Downloaded: 1351.jpg\n", - "Downloaded: 1352.jpg\n", - "Downloaded: 1354.jpg\n", - "Downloaded: 1357.jpg\n", - "Downloaded: 1355.jpg\n", - "Downloaded: 1356.jpg\n", - "Downloaded: 1358.jpg\n", - "Downloaded: 1359.jpg\n", - "Downloaded: 1360.jpg\n", - "Downloaded: 1364.jpg\n", - "Downloaded: 1361.jpg\n", - "Downloaded: 1363.jpg\n", - "Downloaded: 1367.jpg\n", - "Downloaded: 1362.jpg\n", - "Downloaded: 1365.jpg\n", - "Downloaded: 1368.jpg\n", - "Downloaded: 1369.jpg\n", - "Downloaded: 1371.jpg\n", - "Downloaded: 1372.jpg\n", - "Downloaded: 1373.jpg\n", - "Downloaded: 1374.jpg\n", - "Downloaded: 1318.jpg\n", - "Downloaded: 1375.jpg\n", - "Downloaded: 1317.jpg\n", - "Downloaded: 1376.jpg\n", - "Downloaded: 1378.jpg\n", - "Downloaded: 1307.jpg\n", - "Downloaded: 1308.jpg\n", - "Downloaded: 1380.jpg\n", - "Downloaded: 1382.jpg\n", - "Downloaded: 1384.jpg\n", - "Downloaded: 1383.jpg\n", - "Downloaded: 1381.jpg\n", - "Downloaded: 1385.jpg\n", - "Downloaded: 1387.jpg\n", - "Downloaded: 1391.jpg\n", - "Downloaded: 1390.jpg\n", - "Downloaded: 1389.jpg\n", - "Downloaded: 1388.jpg\n", - "Downloaded: 1392.jpg\n", - "Downloaded: 1393.jpg\n", - "Downloaded: 1396.jpg\n", - "Downloaded: 1399.jpg\n", - "Downloaded: 1400.jpg\n", - "Downloaded: 1334.jpg\n", - "Downloaded: 1401.jpg\n", - "Downloaded: 1402.jpg\n", - "Downloaded: 1403.jpg\n", - "Downloaded: 1404.jpg\n", - "Downloaded: 1405.jpg\n", - "Downloaded: 1407.jpg\n", - "Downloaded: 1410.jpg\n", - "Downloaded: 1408.jpg\n", - "Downloaded: 1411.jpg\n", - "Downloaded: 1409.jpg\n", - "Downloaded: 1412.jpg\n", - "Downloaded: 1414.jpg\n", - "Downloaded: 1413.jpg\n", - "Downloaded: 1416.jpg\n", - "Downloaded: 1415.jpg\n", - "Downloaded: 1417.jpg\n", - "Downloaded: 1419.jpg\n", - "Downloaded: 1420.jpg\n", - "Downloaded: 1366.jpg\n", - "Downloaded: 1421.jpg\n", - "Downloaded: 1370.jpg\n", - "Downloaded: 1422.jpg\n", - "Downloaded: 1424.jpg\n", - "Downloaded: 1423.jpg\n", - "Downloaded: 1425.jpg\n", - "Downloaded: 1426.jpg\n", - "Downloaded: 1377.jpg\n", - "Downloaded: 1427.jpg\n", - "Downloaded: 1429.jpg\n", - "Downloaded: 1379.jpg\n", - "Downloaded: 1432.jpg\n", - "Downloaded: 1430.jpg\n", - "Downloaded: 1435.jpg\n", - "Downloaded: 1433.jpg\n", - "Downloaded: 1431.jpg\n", - "Downloaded: 1437.jpg\n", - "Downloaded: 1436.jpg\n", - "Downloaded: 1438.jpg\n", - "Downloaded: 1386.jpg\n", - "Downloaded: 1394.jpg\n", - "Downloaded: 1439.jpg\n", - "Downloaded: 1395.jpg\n", - "Downloaded: 1398.jpg\n", - "Downloaded: 1397.jpg\n", - "Downloaded: 1406.jpg\n", - "Downloaded: 1418.jpg\n", - "Downloaded: 1434.jpg\n", - "Downloaded: 1428.jpg\n" + "\n", + "######################################################################\n", + "# Starting batch video processing for dataset: 16257fd6-b91b-4d00-a680-9ece9f3f241c\n", + "######################################################################\n", + "\n", + "Total file IDs extracted: 1\n", + "\n", + "Creating LabellerrFile instances for 1 files...\n", + "Successfully created 1 LabellerrFile instances\n", + "\n", + "Processing 1 video files...\n", + "\n", + "\n", + "Starting download of 1 files...\n", + "\n", + "============================================================\n", + "Processing file: c44f38f6-0186-436f-8c2d-ffb50a539c76\n", + "============================================================\n", + "\n", + "[1/4] Fetching frame data from API (0 to 1440)...\n", + "Retrieved 1440 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", + "\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", + "\n", + "Cleaning up temporary frames...\n", + "Removed temporary frames folder: ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\n", + "\n", + "============================================================\n", + "✓ Processing complete!\n", + "Video saved to: ./Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", + "============================================================\n", + "\n", + "Files processed: 1/1 (1 successful, 0 failed)\n", + "######################################################################\n", + "# Batch Processing Complete\n", + "# Total files: 1\n", + "# Successful: 1\n", + "# Failed: 0\n", + "######################################################################\n", + "\n" ] } ], "source": [ - "response = file.download_frames(file.get_frames())" + "results = dataset.process_all_videos()" ] }, { "cell_type": "markdown", - "id": "d1ce0bb4", + "id": "900ea5a7", "metadata": {}, "source": [ - "### Create video from frames" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "e2d0f608", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running command: ffmpeg -y -start_number 0 -framerate 30 -i c44f38f6-0186-436f-8c2d-ffb50a539c76\\%d.jpg -c:v libx264 -pix_fmt yuv420p c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", - "Video saved as c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n" - ] - }, - { - "data": { - "text/plain": [ - "'c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4'" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "file.create_video(response['save_path'])" + "### Processing Videos\n", + "The `process_all_videos()` 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." ] }, { @@ -1668,12 +202,32 @@ "id": "f6db8522", "metadata": {}, "source": [ - "## Import Scene change detect Algo" + "## Scene Detection\n", + "\n", + "### Available Scene Detection Methods\n", + "Labellerr SDK provides multiple algorithms for scene detection in videos:\n", + "\n", + "1. **PySceneDetect**: \n", + " - Python-based scene detection\n", + " - Uses content-aware detection\n", + " - Good for general-purpose scene detection\n", + "\n", + "2. **SSIMSceneDetect**:\n", + " - Uses Structural Similarity Index (SSIM)\n", + " - Better for detecting subtle scene changes\n", + " - More computationally intensive but more accurate\n", + "\n", + "3. **FFMPEGSceneDetect**:\n", + " - Uses FFMPEG for scene detection\n", + " - Fastest method\n", + " - Good for quick analysis of large video files\n", + "\n", + "Choose the method that best suits your needs based on accuracy requirements and processing speed constraints." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "f5c41073", "metadata": {}, "outputs": [ @@ -1687,7 +241,9 @@ } ], "source": [ - "from labellerr.services.video_sampling.pyscene_detect import PySceneDetect" + "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" ] }, { @@ -1695,22 +251,50 @@ "id": "db88da50", "metadata": {}, "source": [ - "### Create instance of detector" + "## 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": 11, - "id": "b908dbd3", + "id": "49a6f89d", + "metadata": {}, + "outputs": [], + "source": [ + "dataset_dir = f\".\\Labellerr_datastets\\{dataset_id}\"" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "dd96be8c", "metadata": {}, "outputs": [], "source": [ - "detector = PySceneDetect()" + "detector = SSIMSceneDetect()" + ] + }, + { + "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": 13, + "execution_count": 17, "id": "a3052f25", "metadata": {}, "outputs": [ @@ -1718,25 +302,447 @@ "name": "stdout", "output_type": "stream", "text": [ - "JSON mapping saved to: c44f38f6-0186-436f-8c2d-ffb50a539c76\\c44f38f6-0186-436f-8c2d-ffb50a539c76_mapping.json\n" + "Processing video: .\\Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", + "Total frames: 1440\n", + "SSIM threshold: 0.6\n", + "Saved keyframe 1 at frame 37 (SSIM: 0.322)\n", + "Saved keyframe 2 at frame 38 (SSIM: 0.561)\n", + "Saved keyframe 3 at frame 40 (SSIM: 0.473)\n", + "Saved keyframe 4 at frame 43 (SSIM: 0.486)\n", + "Saved keyframe 5 at frame 45 (SSIM: 0.471)\n", + "Saved keyframe 6 at frame 46 (SSIM: 0.590)\n", + "Saved keyframe 7 at frame 47 (SSIM: 0.582)\n", + "Saved keyframe 8 at frame 49 (SSIM: 0.452)\n", + "Saved keyframe 9 at frame 50 (SSIM: 0.555)\n", + "Saved keyframe 10 at frame 51 (SSIM: 0.511)\n", + "Saved keyframe 11 at frame 52 (SSIM: 0.473)\n", + "Saved keyframe 12 at frame 53 (SSIM: 0.454)\n", + "Saved keyframe 13 at frame 54 (SSIM: 0.449)\n", + "Saved keyframe 14 at frame 55 (SSIM: 0.475)\n", + "Saved keyframe 15 at frame 56 (SSIM: 0.498)\n", + "Saved keyframe 16 at frame 57 (SSIM: 0.519)\n", + "Saved keyframe 17 at frame 58 (SSIM: 0.521)\n", + "Saved keyframe 18 at frame 59 (SSIM: 0.521)\n", + "Saved keyframe 19 at frame 60 (SSIM: 0.535)\n", + "Saved keyframe 20 at frame 61 (SSIM: 0.553)\n", + "Saved keyframe 21 at frame 62 (SSIM: 0.583)\n", + "Saved keyframe 22 at frame 63 (SSIM: 0.252)\n", + "Saved keyframe 23 at frame 65 (SSIM: 0.591)\n", + "Saved keyframe 24 at frame 67 (SSIM: 0.501)\n", + "Saved keyframe 25 at frame 69 (SSIM: 0.489)\n", + "Saved keyframe 26 at frame 73 (SSIM: 0.563)\n", + "Saved keyframe 27 at frame 75 (SSIM: 0.442)\n", + "Saved keyframe 28 at frame 77 (SSIM: 0.468)\n", + "Saved keyframe 29 at frame 81 (SSIM: 0.468)\n", + "Saved keyframe 30 at frame 83 (SSIM: 0.198)\n", + "Saved keyframe 31 at frame 86 (SSIM: 0.564)\n", + "Saved keyframe 32 at frame 88 (SSIM: 0.529)\n", + "Saved keyframe 33 at frame 90 (SSIM: 0.501)\n", + "Saved keyframe 34 at frame 92 (SSIM: 0.555)\n", + "Saved keyframe 35 at frame 94 (SSIM: 0.572)\n", + "Saved keyframe 36 at frame 97 (SSIM: 0.549)\n", + "Saved keyframe 37 at frame 99 (SSIM: 0.183)\n", + "Saved keyframe 38 at frame 100 (SSIM: 0.507)\n", + "Saved keyframe 39 at frame 101 (SSIM: 0.417)\n", + "Saved keyframe 40 at frame 102 (SSIM: 0.400)\n", + "Saved keyframe 41 at frame 103 (SSIM: 0.394)\n", + "Saved keyframe 42 at frame 104 (SSIM: 0.401)\n", + "Saved keyframe 43 at frame 105 (SSIM: 0.447)\n", + "Saved keyframe 44 at frame 106 (SSIM: 0.503)\n", + "Saved keyframe 45 at frame 107 (SSIM: 0.485)\n", + "Saved keyframe 46 at frame 108 (SSIM: 0.432)\n", + "Saved keyframe 47 at frame 109 (SSIM: 0.429)\n", + "Saved keyframe 48 at frame 110 (SSIM: 0.427)\n", + "Saved keyframe 49 at frame 111 (SSIM: 0.453)\n", + "Saved keyframe 50 at frame 112 (SSIM: 0.479)\n", + "Saved keyframe 51 at frame 113 (SSIM: 0.473)\n", + "Saved keyframe 52 at frame 114 (SSIM: 0.488)\n", + "Saved keyframe 53 at frame 115 (SSIM: 0.512)\n", + "Saved keyframe 54 at frame 116 (SSIM: 0.505)\n", + "Saved keyframe 55 at frame 117 (SSIM: 0.503)\n", + "Saved keyframe 56 at frame 118 (SSIM: 0.493)\n", + "Saved keyframe 57 at frame 119 (SSIM: 0.166)\n", + "Saved keyframe 58 at frame 141 (SSIM: 0.297)\n", + "Saved keyframe 59 at frame 165 (SSIM: 0.417)\n", + "Saved keyframe 60 at frame 177 (SSIM: 0.582)\n", + "Saved keyframe 61 at frame 185 (SSIM: 0.600)\n", + "Saved keyframe 62 at frame 198 (SSIM: 0.583)\n", + "Frame 200: SSIM = 0.687 (threshold: 0.6)\n", + "Saved keyframe 63 at frame 206 (SSIM: 0.598)\n", + "Saved keyframe 64 at frame 222 (SSIM: 0.598)\n", + "Saved keyframe 65 at frame 233 (SSIM: 0.216)\n", + "Saved keyframe 66 at frame 237 (SSIM: 0.597)\n", + "Saved keyframe 67 at frame 239 (SSIM: 0.536)\n", + "Saved keyframe 68 at frame 241 (SSIM: 0.543)\n", + "Saved keyframe 69 at frame 251 (SSIM: 0.521)\n", + "Saved keyframe 70 at frame 253 (SSIM: 0.596)\n", + "Saved keyframe 71 at frame 256 (SSIM: 0.600)\n", + "Saved keyframe 72 at frame 261 (SSIM: 0.561)\n", + "Saved keyframe 73 at frame 263 (SSIM: 0.165)\n", + "Saved keyframe 74 at frame 265 (SSIM: 0.528)\n", + "Saved keyframe 75 at frame 267 (SSIM: 0.347)\n", + "Saved keyframe 76 at frame 268 (SSIM: 0.488)\n", + "Saved keyframe 77 at frame 269 (SSIM: 0.514)\n", + "Saved keyframe 78 at frame 271 (SSIM: 0.445)\n", + "Saved keyframe 79 at frame 273 (SSIM: 0.494)\n", + "Saved keyframe 80 at frame 276 (SSIM: 0.534)\n", + "Saved keyframe 81 at frame 279 (SSIM: 0.598)\n", + "Saved keyframe 82 at frame 282 (SSIM: 0.575)\n", + "Saved keyframe 83 at frame 284 (SSIM: 0.533)\n", + "Saved keyframe 84 at frame 286 (SSIM: 0.583)\n", + "Saved keyframe 85 at frame 288 (SSIM: 0.575)\n", + "Saved keyframe 86 at frame 290 (SSIM: 0.565)\n", + "Saved keyframe 87 at frame 292 (SSIM: 0.559)\n", + "Saved keyframe 88 at frame 294 (SSIM: 0.471)\n", + "Saved keyframe 89 at frame 296 (SSIM: 0.394)\n", + "Saved keyframe 90 at frame 297 (SSIM: 0.439)\n", + "Saved keyframe 91 at frame 298 (SSIM: 0.316)\n", + "Saved keyframe 92 at frame 299 (SSIM: 0.347)\n", + "Saved keyframe 93 at frame 300 (SSIM: 0.367)\n", + "Saved keyframe 94 at frame 301 (SSIM: 0.415)\n", + "Saved keyframe 95 at frame 302 (SSIM: 0.419)\n", + "Saved keyframe 96 at frame 303 (SSIM: 0.423)\n", + "Saved keyframe 97 at frame 304 (SSIM: 0.447)\n", + "Saved keyframe 98 at frame 305 (SSIM: 0.443)\n", + "Saved keyframe 99 at frame 306 (SSIM: 0.453)\n", + "Saved keyframe 100 at frame 307 (SSIM: 0.478)\n", + "Saved keyframe 101 at frame 308 (SSIM: 0.500)\n", + "Saved keyframe 102 at frame 309 (SSIM: 0.529)\n", + "Saved keyframe 103 at frame 310 (SSIM: 0.558)\n", + "Saved keyframe 104 at frame 312 (SSIM: 0.596)\n", + "Saved keyframe 105 at frame 314 (SSIM: 0.596)\n", + "Saved keyframe 106 at frame 324 (SSIM: 0.574)\n", + "Saved keyframe 107 at frame 328 (SSIM: 0.570)\n", + "Saved keyframe 108 at frame 373 (SSIM: 0.596)\n", + "Saved keyframe 109 at frame 381 (SSIM: 0.241)\n", + "Saved keyframe 110 at frame 383 (SSIM: 0.427)\n", + "Saved keyframe 111 at frame 384 (SSIM: 0.536)\n", + "Saved keyframe 112 at frame 386 (SSIM: 0.486)\n", + "Saved keyframe 113 at frame 387 (SSIM: 0.573)\n", + "Saved keyframe 114 at frame 388 (SSIM: 0.585)\n", + "Saved keyframe 115 at frame 390 (SSIM: 0.518)\n", + "Saved keyframe 116 at frame 392 (SSIM: 0.492)\n", + "Saved keyframe 117 at frame 394 (SSIM: 0.484)\n", + "Saved keyframe 118 at frame 396 (SSIM: 0.394)\n", + "Saved keyframe 119 at frame 399 (SSIM: 0.511)\n", + "Frame 400: SSIM = 0.772 (threshold: 0.6)\n", + "Saved keyframe 120 at frame 401 (SSIM: 0.499)\n", + "Saved keyframe 121 at frame 403 (SSIM: 0.507)\n", + "Saved keyframe 122 at frame 405 (SSIM: 0.486)\n", + "Saved keyframe 123 at frame 408 (SSIM: 0.145)\n", + "Saved keyframe 124 at frame 410 (SSIM: 0.450)\n", + "Saved keyframe 125 at frame 411 (SSIM: 0.597)\n", + "Saved keyframe 126 at frame 412 (SSIM: 0.573)\n", + "Saved keyframe 127 at frame 414 (SSIM: 0.455)\n", + "Saved keyframe 128 at frame 416 (SSIM: 0.494)\n", + "Saved keyframe 129 at frame 419 (SSIM: 0.488)\n", + "Saved keyframe 130 at frame 421 (SSIM: 0.557)\n", + "Saved keyframe 131 at frame 423 (SSIM: 0.472)\n", + "Saved keyframe 132 at frame 424 (SSIM: 0.486)\n", + "Saved keyframe 133 at frame 425 (SSIM: 0.450)\n", + "Saved keyframe 134 at frame 426 (SSIM: 0.475)\n", + "Saved keyframe 135 at frame 427 (SSIM: 0.452)\n", + "Saved keyframe 136 at frame 428 (SSIM: 0.427)\n", + "Saved keyframe 137 at frame 429 (SSIM: 0.427)\n", + "Saved keyframe 138 at frame 430 (SSIM: 0.433)\n", + "Saved keyframe 139 at frame 431 (SSIM: 0.499)\n", + "Saved keyframe 140 at frame 432 (SSIM: 0.573)\n", + "Saved keyframe 141 at frame 436 (SSIM: 0.589)\n", + "Saved keyframe 142 at frame 438 (SSIM: 0.562)\n", + "Saved keyframe 143 at frame 439 (SSIM: 0.503)\n", + "Saved keyframe 144 at frame 440 (SSIM: 0.432)\n", + "Saved keyframe 145 at frame 441 (SSIM: 0.351)\n", + "Saved keyframe 146 at frame 442 (SSIM: 0.278)\n", + "Saved keyframe 147 at frame 443 (SSIM: 0.226)\n", + "Saved keyframe 148 at frame 444 (SSIM: 0.238)\n", + "Saved keyframe 149 at frame 445 (SSIM: 0.294)\n", + "Saved keyframe 150 at frame 446 (SSIM: 0.258)\n", + "Saved keyframe 151 at frame 447 (SSIM: 0.232)\n", + "Saved keyframe 152 at frame 448 (SSIM: 0.297)\n", + "Saved keyframe 153 at frame 450 (SSIM: 0.553)\n", + "Saved keyframe 154 at frame 451 (SSIM: 0.541)\n", + "Saved keyframe 155 at frame 452 (SSIM: 0.508)\n", + "Saved keyframe 156 at frame 453 (SSIM: 0.423)\n", + "Saved keyframe 157 at frame 454 (SSIM: 0.457)\n", + "Saved keyframe 158 at frame 455 (SSIM: 0.466)\n", + "Saved keyframe 159 at frame 456 (SSIM: 0.580)\n", + "Saved keyframe 160 at frame 457 (SSIM: 0.228)\n", + "Saved keyframe 161 at frame 459 (SSIM: 0.551)\n", + "Saved keyframe 162 at frame 461 (SSIM: 0.499)\n", + "Saved keyframe 163 at frame 465 (SSIM: 0.544)\n", + "Saved keyframe 164 at frame 467 (SSIM: 0.427)\n", + "Saved keyframe 165 at frame 468 (SSIM: 0.581)\n", + "Saved keyframe 166 at frame 469 (SSIM: 0.494)\n", + "Saved keyframe 167 at frame 470 (SSIM: 0.361)\n", + "Saved keyframe 168 at frame 471 (SSIM: 0.295)\n", + "Saved keyframe 169 at frame 472 (SSIM: 0.301)\n", + "Saved keyframe 170 at frame 473 (SSIM: 0.352)\n", + "Saved keyframe 171 at frame 474 (SSIM: 0.373)\n", + "Saved keyframe 172 at frame 475 (SSIM: 0.397)\n", + "Saved keyframe 173 at frame 476 (SSIM: 0.443)\n", + "Saved keyframe 174 at frame 477 (SSIM: 0.451)\n", + "Saved keyframe 175 at frame 478 (SSIM: 0.463)\n", + "Saved keyframe 176 at frame 479 (SSIM: 0.479)\n", + "Saved keyframe 177 at frame 480 (SSIM: 0.492)\n", + "Saved keyframe 178 at frame 481 (SSIM: 0.499)\n", + "Saved keyframe 179 at frame 482 (SSIM: 0.516)\n", + "Saved keyframe 180 at frame 483 (SSIM: 0.529)\n", + "Saved keyframe 181 at frame 484 (SSIM: 0.249)\n", + "Frame 500: SSIM = 0.957 (threshold: 0.6)\n", + "Saved keyframe 182 at frame 508 (SSIM: 0.336)\n", + "Saved keyframe 183 at frame 552 (SSIM: 0.379)\n", + "Saved keyframe 184 at frame 557 (SSIM: 0.586)\n", + "Saved keyframe 185 at frame 563 (SSIM: 0.580)\n", + "Saved keyframe 186 at frame 566 (SSIM: 0.594)\n", + "Saved keyframe 187 at frame 569 (SSIM: 0.575)\n", + "Saved keyframe 188 at frame 574 (SSIM: 0.600)\n", + "Saved keyframe 189 at frame 575 (SSIM: 0.340)\n", + "Saved keyframe 190 at frame 587 (SSIM: 0.595)\n", + "Saved keyframe 191 at frame 590 (SSIM: 0.293)\n", + "Saved keyframe 192 at frame 593 (SSIM: 0.555)\n", + "Saved keyframe 193 at frame 599 (SSIM: 0.564)\n", + "Frame 600: SSIM = 0.845 (threshold: 0.6)\n", + "Saved keyframe 194 at frame 607 (SSIM: 0.584)\n", + "Saved keyframe 195 at frame 611 (SSIM: 0.598)\n", + "Saved keyframe 196 at frame 619 (SSIM: 0.296)\n", + "Saved keyframe 197 at frame 622 (SSIM: 0.540)\n", + "Saved keyframe 198 at frame 625 (SSIM: 0.517)\n", + "Saved keyframe 199 at frame 627 (SSIM: 0.560)\n", + "Saved keyframe 200 at frame 630 (SSIM: 0.577)\n", + "Saved keyframe 201 at frame 635 (SSIM: 0.572)\n", + "Saved keyframe 202 at frame 639 (SSIM: 0.596)\n", + "Saved keyframe 203 at frame 647 (SSIM: 0.326)\n", + "Saved keyframe 204 at frame 651 (SSIM: 0.592)\n", + "Saved keyframe 205 at frame 666 (SSIM: 0.590)\n", + "Saved keyframe 206 at frame 683 (SSIM: 0.275)\n", + "Saved keyframe 207 at frame 686 (SSIM: 0.507)\n", + "Saved keyframe 208 at frame 688 (SSIM: 0.460)\n", + "Saved keyframe 209 at frame 690 (SSIM: 0.492)\n", + "Saved keyframe 210 at frame 692 (SSIM: 0.498)\n", + "Saved keyframe 211 at frame 694 (SSIM: 0.555)\n", + "Saved keyframe 212 at frame 697 (SSIM: 0.462)\n", + "Saved keyframe 213 at frame 699 (SSIM: 0.500)\n", + "Frame 700: SSIM = 0.800 (threshold: 0.6)\n", + "Saved keyframe 214 at frame 702 (SSIM: 0.545)\n", + "Saved keyframe 215 at frame 705 (SSIM: 0.545)\n", + "Saved keyframe 216 at frame 706 (SSIM: 0.233)\n", + "Saved keyframe 217 at frame 721 (SSIM: 0.291)\n", + "Saved keyframe 218 at frame 728 (SSIM: 0.587)\n", + "Saved keyframe 219 at frame 736 (SSIM: 0.594)\n", + "Saved keyframe 220 at frame 743 (SSIM: 0.584)\n", + "Saved keyframe 221 at frame 748 (SSIM: 0.571)\n", + "Saved keyframe 222 at frame 753 (SSIM: 0.584)\n", + "Saved keyframe 223 at frame 757 (SSIM: 0.575)\n", + "Saved keyframe 224 at frame 758 (SSIM: 0.254)\n", + "Saved keyframe 225 at frame 776 (SSIM: 0.275)\n", + "Saved keyframe 226 at frame 799 (SSIM: 0.591)\n", + "Frame 800: SSIM = 0.902 (threshold: 0.6)\n", + "Saved keyframe 227 at frame 805 (SSIM: 0.282)\n", + "Saved keyframe 228 at frame 811 (SSIM: 0.558)\n", + "Saved keyframe 229 at frame 813 (SSIM: 0.512)\n", + "Saved keyframe 230 at frame 815 (SSIM: 0.465)\n", + "Saved keyframe 231 at frame 817 (SSIM: 0.486)\n", + "Saved keyframe 232 at frame 819 (SSIM: 0.580)\n", + "Saved keyframe 233 at frame 822 (SSIM: 0.578)\n", + "Saved keyframe 234 at frame 823 (SSIM: 0.239)\n", + "Saved keyframe 235 at frame 827 (SSIM: 0.599)\n", + "Saved keyframe 236 at frame 831 (SSIM: 0.526)\n", + "Saved keyframe 237 at frame 834 (SSIM: 0.535)\n", + "Saved keyframe 238 at frame 836 (SSIM: 0.241)\n", + "Saved keyframe 239 at frame 839 (SSIM: 0.565)\n", + "Saved keyframe 240 at frame 842 (SSIM: 0.585)\n", + "Saved keyframe 241 at frame 845 (SSIM: 0.549)\n", + "Saved keyframe 242 at frame 848 (SSIM: 0.563)\n", + "Saved keyframe 243 at frame 850 (SSIM: 0.597)\n", + "Saved keyframe 244 at frame 853 (SSIM: 0.594)\n", + "Saved keyframe 245 at frame 854 (SSIM: 0.547)\n", + "Saved keyframe 246 at frame 855 (SSIM: 0.595)\n", + "Saved keyframe 247 at frame 856 (SSIM: 0.580)\n", + "Saved keyframe 248 at frame 857 (SSIM: 0.593)\n", + "Saved keyframe 249 at frame 858 (SSIM: 0.282)\n", + "Saved keyframe 250 at frame 859 (SSIM: 0.584)\n", + "Saved keyframe 251 at frame 861 (SSIM: 0.505)\n", + "Saved keyframe 252 at frame 863 (SSIM: 0.534)\n", + "Saved keyframe 253 at frame 865 (SSIM: 0.525)\n", + "Saved keyframe 254 at frame 870 (SSIM: 0.474)\n", + "Saved keyframe 255 at frame 872 (SSIM: 0.460)\n", + "Saved keyframe 256 at frame 873 (SSIM: 0.564)\n", + "Saved keyframe 257 at frame 874 (SSIM: 0.583)\n", + "Saved keyframe 258 at frame 875 (SSIM: 0.434)\n", + "Saved keyframe 259 at frame 893 (SSIM: 0.317)\n", + "Saved keyframe 260 at frame 895 (SSIM: 0.499)\n", + "Saved keyframe 261 at frame 898 (SSIM: 0.592)\n", + "Frame 900: SSIM = 0.728 (threshold: 0.6)\n", + "Saved keyframe 262 at frame 901 (SSIM: 0.572)\n", + "Saved keyframe 263 at frame 903 (SSIM: 0.588)\n", + "Saved keyframe 264 at frame 906 (SSIM: 0.581)\n", + "Saved keyframe 265 at frame 909 (SSIM: 0.578)\n", + "Saved keyframe 266 at frame 911 (SSIM: 0.597)\n", + "Saved keyframe 267 at frame 913 (SSIM: 0.570)\n", + "Saved keyframe 268 at frame 915 (SSIM: 0.177)\n", + "Saved keyframe 269 at frame 920 (SSIM: 0.592)\n", + "Saved keyframe 270 at frame 932 (SSIM: 0.579)\n", + "Saved keyframe 271 at frame 936 (SSIM: 0.595)\n", + "Saved keyframe 272 at frame 941 (SSIM: 0.593)\n", + "Saved keyframe 273 at frame 944 (SSIM: 0.549)\n", + "Saved keyframe 274 at frame 947 (SSIM: 0.543)\n", + "Saved keyframe 275 at frame 949 (SSIM: 0.301)\n", + "Saved keyframe 276 at frame 959 (SSIM: 0.591)\n", + "Saved keyframe 277 at frame 965 (SSIM: 0.586)\n", + "Saved keyframe 278 at frame 981 (SSIM: 0.594)\n", + "Saved keyframe 279 at frame 995 (SSIM: 0.597)\n", + "Frame 1000: SSIM = 0.695 (threshold: 0.6)\n", + "Saved keyframe 280 at frame 1008 (SSIM: 0.261)\n", + "Saved keyframe 281 at frame 1009 (SSIM: 0.549)\n", + "Saved keyframe 282 at frame 1010 (SSIM: 0.475)\n", + "Saved keyframe 283 at frame 1011 (SSIM: 0.482)\n", + "Saved keyframe 284 at frame 1012 (SSIM: 0.531)\n", + "Saved keyframe 285 at frame 1013 (SSIM: 0.513)\n", + "Saved keyframe 286 at frame 1014 (SSIM: 0.520)\n", + "Saved keyframe 287 at frame 1015 (SSIM: 0.460)\n", + "Saved keyframe 288 at frame 1016 (SSIM: 0.294)\n", + "Saved keyframe 289 at frame 1017 (SSIM: 0.340)\n", + "Saved keyframe 290 at frame 1018 (SSIM: 0.335)\n", + "Saved keyframe 291 at frame 1019 (SSIM: 0.368)\n", + "Saved keyframe 292 at frame 1020 (SSIM: 0.384)\n", + "Saved keyframe 293 at frame 1021 (SSIM: 0.503)\n", + "Saved keyframe 294 at frame 1022 (SSIM: 0.537)\n", + "Saved keyframe 295 at frame 1023 (SSIM: 0.537)\n", + "Saved keyframe 296 at frame 1024 (SSIM: 0.545)\n", + "Saved keyframe 297 at frame 1026 (SSIM: 0.470)\n", + "Saved keyframe 298 at frame 1027 (SSIM: 0.511)\n", + "Saved keyframe 299 at frame 1028 (SSIM: 0.265)\n", + "Saved keyframe 300 at frame 1029 (SSIM: 0.597)\n", + "Saved keyframe 301 at frame 1030 (SSIM: 0.525)\n", + "Saved keyframe 302 at frame 1031 (SSIM: 0.513)\n", + "Saved keyframe 303 at frame 1032 (SSIM: 0.512)\n", + "Saved keyframe 304 at frame 1033 (SSIM: 0.500)\n", + "Saved keyframe 305 at frame 1034 (SSIM: 0.543)\n", + "Saved keyframe 306 at frame 1035 (SSIM: 0.548)\n", + "Saved keyframe 307 at frame 1036 (SSIM: 0.504)\n", + "Saved keyframe 308 at frame 1037 (SSIM: 0.497)\n", + "Saved keyframe 309 at frame 1038 (SSIM: 0.507)\n", + "Saved keyframe 310 at frame 1039 (SSIM: 0.530)\n", + "Saved keyframe 311 at frame 1040 (SSIM: 0.566)\n", + "Saved keyframe 312 at frame 1041 (SSIM: 0.572)\n", + "Saved keyframe 313 at frame 1042 (SSIM: 0.569)\n", + "Saved keyframe 314 at frame 1044 (SSIM: 0.510)\n", + "Saved keyframe 315 at frame 1046 (SSIM: 0.572)\n", + "Saved keyframe 316 at frame 1049 (SSIM: 0.541)\n", + "Saved keyframe 317 at frame 1053 (SSIM: 0.563)\n", + "Saved keyframe 318 at frame 1056 (SSIM: 0.554)\n", + "Saved keyframe 319 at frame 1058 (SSIM: 0.579)\n", + "Saved keyframe 320 at frame 1060 (SSIM: 0.165)\n", + "Saved keyframe 321 at frame 1066 (SSIM: 0.595)\n", + "Saved keyframe 322 at frame 1076 (SSIM: 0.549)\n", + "Saved keyframe 323 at frame 1082 (SSIM: 0.139)\n", + "Saved keyframe 324 at frame 1084 (SSIM: 0.593)\n", + "Saved keyframe 325 at frame 1085 (SSIM: 0.565)\n", + "Saved keyframe 326 at frame 1086 (SSIM: 0.517)\n", + "Saved keyframe 327 at frame 1087 (SSIM: 0.495)\n", + "Saved keyframe 328 at frame 1088 (SSIM: 0.491)\n", + "Saved keyframe 329 at frame 1089 (SSIM: 0.507)\n", + "Saved keyframe 330 at frame 1090 (SSIM: 0.522)\n", + "Saved keyframe 331 at frame 1091 (SSIM: 0.538)\n", + "Saved keyframe 332 at frame 1093 (SSIM: 0.431)\n", + "Saved keyframe 333 at frame 1095 (SSIM: 0.535)\n", + "Frame 1100: SSIM = 0.687 (threshold: 0.6)\n", + "Saved keyframe 334 at frame 1106 (SSIM: 0.275)\n", + "Saved keyframe 335 at frame 1111 (SSIM: 0.600)\n", + "Saved keyframe 336 at frame 1117 (SSIM: 0.583)\n", + "Saved keyframe 337 at frame 1119 (SSIM: 0.418)\n", + "Saved keyframe 338 at frame 1133 (SSIM: 0.594)\n", + "Saved keyframe 339 at frame 1137 (SSIM: 0.261)\n", + "Saved keyframe 340 at frame 1148 (SSIM: 0.524)\n", + "Saved keyframe 341 at frame 1150 (SSIM: 0.502)\n", + "Saved keyframe 342 at frame 1151 (SSIM: 0.556)\n", + "Saved keyframe 343 at frame 1152 (SSIM: 0.501)\n", + "Saved keyframe 344 at frame 1153 (SSIM: 0.595)\n", + "Saved keyframe 345 at frame 1155 (SSIM: 0.577)\n", + "Saved keyframe 346 at frame 1156 (SSIM: 0.578)\n", + "Saved keyframe 347 at frame 1157 (SSIM: 0.244)\n", + "Saved keyframe 348 at frame 1161 (SSIM: 0.595)\n", + "Saved keyframe 349 at frame 1165 (SSIM: 0.600)\n", + "Saved keyframe 350 at frame 1173 (SSIM: 0.575)\n", + "Saved keyframe 351 at frame 1175 (SSIM: 0.377)\n", + "Saved keyframe 352 at frame 1189 (SSIM: 0.330)\n", + "Frame 1200: SSIM = 0.769 (threshold: 0.6)\n", + "Saved keyframe 353 at frame 1201 (SSIM: 0.311)\n", + "Saved keyframe 354 at frame 1205 (SSIM: 0.555)\n", + "Saved keyframe 355 at frame 1217 (SSIM: 0.580)\n", + "Saved keyframe 356 at frame 1218 (SSIM: 0.348)\n", + "Saved keyframe 357 at frame 1220 (SSIM: 0.579)\n", + "Saved keyframe 358 at frame 1222 (SSIM: 0.593)\n", + "Saved keyframe 359 at frame 1225 (SSIM: 0.537)\n", + "Saved keyframe 360 at frame 1227 (SSIM: 0.517)\n", + "Saved keyframe 361 at frame 1232 (SSIM: 0.581)\n", + "Saved keyframe 362 at frame 1233 (SSIM: 0.316)\n", + "Saved keyframe 363 at frame 1237 (SSIM: 0.546)\n", + "Saved keyframe 364 at frame 1239 (SSIM: 0.592)\n", + "Saved keyframe 365 at frame 1242 (SSIM: 0.563)\n", + "Saved keyframe 366 at frame 1245 (SSIM: 0.596)\n", + "Saved keyframe 367 at frame 1246 (SSIM: 0.341)\n", + "Saved keyframe 368 at frame 1247 (SSIM: 0.367)\n", + "Saved keyframe 369 at frame 1248 (SSIM: 0.429)\n", + "Saved keyframe 370 at frame 1249 (SSIM: 0.540)\n", + "Saved keyframe 371 at frame 1250 (SSIM: 0.441)\n", + "Saved keyframe 372 at frame 1252 (SSIM: 0.425)\n", + "Saved keyframe 373 at frame 1256 (SSIM: 0.580)\n", + "Saved keyframe 374 at frame 1257 (SSIM: 0.139)\n", + "Saved keyframe 375 at frame 1258 (SSIM: 0.560)\n", + "Saved keyframe 376 at frame 1259 (SSIM: 0.547)\n", + "Saved keyframe 377 at frame 1260 (SSIM: 0.577)\n", + "Saved keyframe 378 at frame 1262 (SSIM: 0.560)\n", + "Saved keyframe 379 at frame 1264 (SSIM: 0.480)\n", + "Saved keyframe 380 at frame 1265 (SSIM: 0.578)\n", + "Saved keyframe 381 at frame 1267 (SSIM: 0.499)\n", + "Saved keyframe 382 at frame 1269 (SSIM: 0.502)\n", + "Saved keyframe 383 at frame 1270 (SSIM: 0.594)\n", + "Saved keyframe 384 at frame 1271 (SSIM: 0.554)\n", + "Saved keyframe 385 at frame 1272 (SSIM: 0.537)\n", + "Saved keyframe 386 at frame 1273 (SSIM: 0.505)\n", + "Saved keyframe 387 at frame 1274 (SSIM: 0.507)\n", + "Saved keyframe 388 at frame 1275 (SSIM: 0.577)\n", + "Saved keyframe 389 at frame 1277 (SSIM: 0.510)\n", + "Saved keyframe 390 at frame 1278 (SSIM: 0.417)\n", + "Frame 1300: SSIM = 0.809 (threshold: 0.6)\n", + "Saved keyframe 391 at frame 1316 (SSIM: 0.594)\n", + "Frame 1400: SSIM = 0.708 (threshold: 0.6)\n", + "JSON mapping saved to: SSIM_detects\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\c44f38f6-0186-436f-8c2d-ffb50a539c76_mapping.json\n" ] - }, - { - "data": { - "text/plain": [ - "DetectionResult(file_id='c44f38f6-0186-436f-8c2d-ffb50a539c76', output_folder='c44f38f6-0186-436f-8c2d-ffb50a539c76', total_frames=1440, selected_frames=[SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\18.jpg', frame_no=18), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\50.jpg', frame_no=50), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\73.jpg', frame_no=73), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\91.jpg', frame_no=91), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\109.jpg', frame_no=109), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\130.jpg', frame_no=130), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\153.jpg', frame_no=153), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\199.jpg', frame_no=199), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\248.jpg', frame_no=248), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\322.jpg', frame_no=322), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\394.jpg', frame_no=394), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\432.jpg', frame_no=432), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\470.jpg', frame_no=470), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\496.jpg', frame_no=496), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\530.jpg', frame_no=530), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\563.jpg', frame_no=563), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\582.jpg', frame_no=582), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\604.jpg', frame_no=604), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\633.jpg', frame_no=633), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\665.jpg', frame_no=665), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\694.jpg', frame_no=694), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\713.jpg', frame_no=713), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\739.jpg', frame_no=739), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\767.jpg', frame_no=767), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\790.jpg', frame_no=790), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\814.jpg', frame_no=814), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\829.jpg', frame_no=829), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\847.jpg', frame_no=847), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\866.jpg', frame_no=866), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\884.jpg', frame_no=884), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\904.jpg', frame_no=904), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\932.jpg', frame_no=932), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\978.jpg', frame_no=978), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1034.jpg', frame_no=1034), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1071.jpg', frame_no=1071), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1094.jpg', frame_no=1094), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1112.jpg', frame_no=1112), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1128.jpg', frame_no=1128), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1147.jpg', frame_no=1147), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1166.jpg', frame_no=1166), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1182.jpg', frame_no=1182), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1203.jpg', frame_no=1203), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1225.jpg', frame_no=1225), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1245.jpg', frame_no=1245), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1267.jpg', frame_no=1267), SceneFrame(frame_path='c44f38f6-0186-436f-8c2d-ffb50a539c76\\\\1359.jpg', frame_no=1359)])" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "video_path = r\"D:\\professional\\LABELLERR\\Task\\Repos\\SDKPython\\labellerr\\notebooks\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\"\n", + "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", - "detector.detect_and_extract(video_path)" + "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": "code", + "execution_count": null, + "id": "40c70986", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/labellerr/services/video_sampling/ffmpeg.py b/labellerr/services/video_sampling/ffmpeg.py index 977fe04..6f72c42 100644 --- a/labellerr/services/video_sampling/ffmpeg.py +++ b/labellerr/services/video_sampling/ffmpeg.py @@ -34,10 +34,12 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: """ # 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, file_id) + + output_folder = os.path.join(base_detect_folder, dataset_id, file_id) frames_folder = os.path.join(output_folder, "frames") # Create nested folders @@ -142,7 +144,7 @@ def _save_json_mapping(self, result: DetectionResult, output_folder: str, file_i if __name__ == "__main__": - video_path = r"D:\professional\LABELLERR\Task\Repos\Python_SDK\services\video_sampling\video2.mp4" + 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() diff --git a/labellerr/services/video_sampling/pyscene_detect.py b/labellerr/services/video_sampling/pyscene_detect.py index e2d1ad7..d430283 100644 --- a/labellerr/services/video_sampling/pyscene_detect.py +++ b/labellerr/services/video_sampling/pyscene_detect.py @@ -37,10 +37,12 @@ def detect_and_extract(self, video_path: str) -> DetectionResult: """ # 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 base detect folder and file_id specific folder base_detect_folder = "PyScene_detects" - output_folder = os.path.join(base_detect_folder, file_id) + + 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 @@ -126,8 +128,8 @@ def _save_json_mapping(self, result: DetectionResult, output_folder: str, file_i print(f"JSON mapping saved to: {json_path}") -if __name__ == "__main__": - video_path = r"D:\professional\LABELLERR\Task\Repos\Python_SDK\services\video_sampling\video2.mp4" +# if __name__ == "__main__": +# video_path = r"D:\professional\LABELLERR\Task\Repos\SDKPython\labellerr\notebooks\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4" - detector = PySceneDetect() - result = detector.detect_and_extract(video_path) \ No newline at end of file +# detector = PySceneDetect() +# result = detector.detect_and_extract(video_path) \ No newline at end of file diff --git a/labellerr/services/video_sampling/ssim.py b/labellerr/services/video_sampling/ssim.py index 7f7528a..0e1f9ba 100644 --- a/labellerr/services/video_sampling/ssim.py +++ b/labellerr/services/video_sampling/ssim.py @@ -46,10 +46,11 @@ def detect_and_extract( """ # 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, file_id) + output_folder = os.path.join(base_detect_folder, dataset_id, file_id) frames_folder = os.path.join(output_folder, "frames") # Create nested output folders From 67f864ebcefc3f8fddaa9ca7bab060581f97fe43 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Fri, 10 Oct 2025 22:47:40 +0530 Subject: [PATCH 22/23] minor changes to SDK --- labellerr/notebooks/SDK.ipynb | 415 +--------------------------------- 1 file changed, 5 insertions(+), 410 deletions(-) diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index dfa02d4..45ffebf 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -275,12 +275,12 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 18, "id": "dd96be8c", "metadata": {}, "outputs": [], "source": [ - "detector = SSIMSceneDetect()" + "detector = FFMPEGSceneDetect()" ] }, { @@ -294,7 +294,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 19, "id": "a3052f25", "metadata": {}, "outputs": [ @@ -302,413 +302,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Processing video: .\\Labellerr_datastets\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76.mp4\n", - "Total frames: 1440\n", - "SSIM threshold: 0.6\n", - "Saved keyframe 1 at frame 37 (SSIM: 0.322)\n", - "Saved keyframe 2 at frame 38 (SSIM: 0.561)\n", - "Saved keyframe 3 at frame 40 (SSIM: 0.473)\n", - "Saved keyframe 4 at frame 43 (SSIM: 0.486)\n", - "Saved keyframe 5 at frame 45 (SSIM: 0.471)\n", - "Saved keyframe 6 at frame 46 (SSIM: 0.590)\n", - "Saved keyframe 7 at frame 47 (SSIM: 0.582)\n", - "Saved keyframe 8 at frame 49 (SSIM: 0.452)\n", - "Saved keyframe 9 at frame 50 (SSIM: 0.555)\n", - "Saved keyframe 10 at frame 51 (SSIM: 0.511)\n", - "Saved keyframe 11 at frame 52 (SSIM: 0.473)\n", - "Saved keyframe 12 at frame 53 (SSIM: 0.454)\n", - "Saved keyframe 13 at frame 54 (SSIM: 0.449)\n", - "Saved keyframe 14 at frame 55 (SSIM: 0.475)\n", - "Saved keyframe 15 at frame 56 (SSIM: 0.498)\n", - "Saved keyframe 16 at frame 57 (SSIM: 0.519)\n", - "Saved keyframe 17 at frame 58 (SSIM: 0.521)\n", - "Saved keyframe 18 at frame 59 (SSIM: 0.521)\n", - "Saved keyframe 19 at frame 60 (SSIM: 0.535)\n", - "Saved keyframe 20 at frame 61 (SSIM: 0.553)\n", - "Saved keyframe 21 at frame 62 (SSIM: 0.583)\n", - "Saved keyframe 22 at frame 63 (SSIM: 0.252)\n", - "Saved keyframe 23 at frame 65 (SSIM: 0.591)\n", - "Saved keyframe 24 at frame 67 (SSIM: 0.501)\n", - "Saved keyframe 25 at frame 69 (SSIM: 0.489)\n", - "Saved keyframe 26 at frame 73 (SSIM: 0.563)\n", - "Saved keyframe 27 at frame 75 (SSIM: 0.442)\n", - "Saved keyframe 28 at frame 77 (SSIM: 0.468)\n", - "Saved keyframe 29 at frame 81 (SSIM: 0.468)\n", - "Saved keyframe 30 at frame 83 (SSIM: 0.198)\n", - "Saved keyframe 31 at frame 86 (SSIM: 0.564)\n", - "Saved keyframe 32 at frame 88 (SSIM: 0.529)\n", - "Saved keyframe 33 at frame 90 (SSIM: 0.501)\n", - "Saved keyframe 34 at frame 92 (SSIM: 0.555)\n", - "Saved keyframe 35 at frame 94 (SSIM: 0.572)\n", - "Saved keyframe 36 at frame 97 (SSIM: 0.549)\n", - "Saved keyframe 37 at frame 99 (SSIM: 0.183)\n", - "Saved keyframe 38 at frame 100 (SSIM: 0.507)\n", - "Saved keyframe 39 at frame 101 (SSIM: 0.417)\n", - "Saved keyframe 40 at frame 102 (SSIM: 0.400)\n", - "Saved keyframe 41 at frame 103 (SSIM: 0.394)\n", - "Saved keyframe 42 at frame 104 (SSIM: 0.401)\n", - "Saved keyframe 43 at frame 105 (SSIM: 0.447)\n", - "Saved keyframe 44 at frame 106 (SSIM: 0.503)\n", - "Saved keyframe 45 at frame 107 (SSIM: 0.485)\n", - "Saved keyframe 46 at frame 108 (SSIM: 0.432)\n", - "Saved keyframe 47 at frame 109 (SSIM: 0.429)\n", - "Saved keyframe 48 at frame 110 (SSIM: 0.427)\n", - "Saved keyframe 49 at frame 111 (SSIM: 0.453)\n", - "Saved keyframe 50 at frame 112 (SSIM: 0.479)\n", - "Saved keyframe 51 at frame 113 (SSIM: 0.473)\n", - "Saved keyframe 52 at frame 114 (SSIM: 0.488)\n", - "Saved keyframe 53 at frame 115 (SSIM: 0.512)\n", - "Saved keyframe 54 at frame 116 (SSIM: 0.505)\n", - "Saved keyframe 55 at frame 117 (SSIM: 0.503)\n", - "Saved keyframe 56 at frame 118 (SSIM: 0.493)\n", - "Saved keyframe 57 at frame 119 (SSIM: 0.166)\n", - "Saved keyframe 58 at frame 141 (SSIM: 0.297)\n", - "Saved keyframe 59 at frame 165 (SSIM: 0.417)\n", - "Saved keyframe 60 at frame 177 (SSIM: 0.582)\n", - "Saved keyframe 61 at frame 185 (SSIM: 0.600)\n", - "Saved keyframe 62 at frame 198 (SSIM: 0.583)\n", - "Frame 200: SSIM = 0.687 (threshold: 0.6)\n", - "Saved keyframe 63 at frame 206 (SSIM: 0.598)\n", - "Saved keyframe 64 at frame 222 (SSIM: 0.598)\n", - "Saved keyframe 65 at frame 233 (SSIM: 0.216)\n", - "Saved keyframe 66 at frame 237 (SSIM: 0.597)\n", - "Saved keyframe 67 at frame 239 (SSIM: 0.536)\n", - "Saved keyframe 68 at frame 241 (SSIM: 0.543)\n", - "Saved keyframe 69 at frame 251 (SSIM: 0.521)\n", - "Saved keyframe 70 at frame 253 (SSIM: 0.596)\n", - "Saved keyframe 71 at frame 256 (SSIM: 0.600)\n", - "Saved keyframe 72 at frame 261 (SSIM: 0.561)\n", - "Saved keyframe 73 at frame 263 (SSIM: 0.165)\n", - "Saved keyframe 74 at frame 265 (SSIM: 0.528)\n", - "Saved keyframe 75 at frame 267 (SSIM: 0.347)\n", - "Saved keyframe 76 at frame 268 (SSIM: 0.488)\n", - "Saved keyframe 77 at frame 269 (SSIM: 0.514)\n", - "Saved keyframe 78 at frame 271 (SSIM: 0.445)\n", - "Saved keyframe 79 at frame 273 (SSIM: 0.494)\n", - "Saved keyframe 80 at frame 276 (SSIM: 0.534)\n", - "Saved keyframe 81 at frame 279 (SSIM: 0.598)\n", - "Saved keyframe 82 at frame 282 (SSIM: 0.575)\n", - "Saved keyframe 83 at frame 284 (SSIM: 0.533)\n", - "Saved keyframe 84 at frame 286 (SSIM: 0.583)\n", - "Saved keyframe 85 at frame 288 (SSIM: 0.575)\n", - "Saved keyframe 86 at frame 290 (SSIM: 0.565)\n", - "Saved keyframe 87 at frame 292 (SSIM: 0.559)\n", - "Saved keyframe 88 at frame 294 (SSIM: 0.471)\n", - "Saved keyframe 89 at frame 296 (SSIM: 0.394)\n", - "Saved keyframe 90 at frame 297 (SSIM: 0.439)\n", - "Saved keyframe 91 at frame 298 (SSIM: 0.316)\n", - "Saved keyframe 92 at frame 299 (SSIM: 0.347)\n", - "Saved keyframe 93 at frame 300 (SSIM: 0.367)\n", - "Saved keyframe 94 at frame 301 (SSIM: 0.415)\n", - "Saved keyframe 95 at frame 302 (SSIM: 0.419)\n", - "Saved keyframe 96 at frame 303 (SSIM: 0.423)\n", - "Saved keyframe 97 at frame 304 (SSIM: 0.447)\n", - "Saved keyframe 98 at frame 305 (SSIM: 0.443)\n", - "Saved keyframe 99 at frame 306 (SSIM: 0.453)\n", - "Saved keyframe 100 at frame 307 (SSIM: 0.478)\n", - "Saved keyframe 101 at frame 308 (SSIM: 0.500)\n", - "Saved keyframe 102 at frame 309 (SSIM: 0.529)\n", - "Saved keyframe 103 at frame 310 (SSIM: 0.558)\n", - "Saved keyframe 104 at frame 312 (SSIM: 0.596)\n", - "Saved keyframe 105 at frame 314 (SSIM: 0.596)\n", - "Saved keyframe 106 at frame 324 (SSIM: 0.574)\n", - "Saved keyframe 107 at frame 328 (SSIM: 0.570)\n", - "Saved keyframe 108 at frame 373 (SSIM: 0.596)\n", - "Saved keyframe 109 at frame 381 (SSIM: 0.241)\n", - "Saved keyframe 110 at frame 383 (SSIM: 0.427)\n", - "Saved keyframe 111 at frame 384 (SSIM: 0.536)\n", - "Saved keyframe 112 at frame 386 (SSIM: 0.486)\n", - "Saved keyframe 113 at frame 387 (SSIM: 0.573)\n", - "Saved keyframe 114 at frame 388 (SSIM: 0.585)\n", - "Saved keyframe 115 at frame 390 (SSIM: 0.518)\n", - "Saved keyframe 116 at frame 392 (SSIM: 0.492)\n", - "Saved keyframe 117 at frame 394 (SSIM: 0.484)\n", - "Saved keyframe 118 at frame 396 (SSIM: 0.394)\n", - "Saved keyframe 119 at frame 399 (SSIM: 0.511)\n", - "Frame 400: SSIM = 0.772 (threshold: 0.6)\n", - "Saved keyframe 120 at frame 401 (SSIM: 0.499)\n", - "Saved keyframe 121 at frame 403 (SSIM: 0.507)\n", - "Saved keyframe 122 at frame 405 (SSIM: 0.486)\n", - "Saved keyframe 123 at frame 408 (SSIM: 0.145)\n", - "Saved keyframe 124 at frame 410 (SSIM: 0.450)\n", - "Saved keyframe 125 at frame 411 (SSIM: 0.597)\n", - "Saved keyframe 126 at frame 412 (SSIM: 0.573)\n", - "Saved keyframe 127 at frame 414 (SSIM: 0.455)\n", - "Saved keyframe 128 at frame 416 (SSIM: 0.494)\n", - "Saved keyframe 129 at frame 419 (SSIM: 0.488)\n", - "Saved keyframe 130 at frame 421 (SSIM: 0.557)\n", - "Saved keyframe 131 at frame 423 (SSIM: 0.472)\n", - "Saved keyframe 132 at frame 424 (SSIM: 0.486)\n", - "Saved keyframe 133 at frame 425 (SSIM: 0.450)\n", - "Saved keyframe 134 at frame 426 (SSIM: 0.475)\n", - "Saved keyframe 135 at frame 427 (SSIM: 0.452)\n", - "Saved keyframe 136 at frame 428 (SSIM: 0.427)\n", - "Saved keyframe 137 at frame 429 (SSIM: 0.427)\n", - "Saved keyframe 138 at frame 430 (SSIM: 0.433)\n", - "Saved keyframe 139 at frame 431 (SSIM: 0.499)\n", - "Saved keyframe 140 at frame 432 (SSIM: 0.573)\n", - "Saved keyframe 141 at frame 436 (SSIM: 0.589)\n", - "Saved keyframe 142 at frame 438 (SSIM: 0.562)\n", - "Saved keyframe 143 at frame 439 (SSIM: 0.503)\n", - "Saved keyframe 144 at frame 440 (SSIM: 0.432)\n", - "Saved keyframe 145 at frame 441 (SSIM: 0.351)\n", - "Saved keyframe 146 at frame 442 (SSIM: 0.278)\n", - "Saved keyframe 147 at frame 443 (SSIM: 0.226)\n", - "Saved keyframe 148 at frame 444 (SSIM: 0.238)\n", - "Saved keyframe 149 at frame 445 (SSIM: 0.294)\n", - "Saved keyframe 150 at frame 446 (SSIM: 0.258)\n", - "Saved keyframe 151 at frame 447 (SSIM: 0.232)\n", - "Saved keyframe 152 at frame 448 (SSIM: 0.297)\n", - "Saved keyframe 153 at frame 450 (SSIM: 0.553)\n", - "Saved keyframe 154 at frame 451 (SSIM: 0.541)\n", - "Saved keyframe 155 at frame 452 (SSIM: 0.508)\n", - "Saved keyframe 156 at frame 453 (SSIM: 0.423)\n", - "Saved keyframe 157 at frame 454 (SSIM: 0.457)\n", - "Saved keyframe 158 at frame 455 (SSIM: 0.466)\n", - "Saved keyframe 159 at frame 456 (SSIM: 0.580)\n", - "Saved keyframe 160 at frame 457 (SSIM: 0.228)\n", - "Saved keyframe 161 at frame 459 (SSIM: 0.551)\n", - "Saved keyframe 162 at frame 461 (SSIM: 0.499)\n", - "Saved keyframe 163 at frame 465 (SSIM: 0.544)\n", - "Saved keyframe 164 at frame 467 (SSIM: 0.427)\n", - "Saved keyframe 165 at frame 468 (SSIM: 0.581)\n", - "Saved keyframe 166 at frame 469 (SSIM: 0.494)\n", - "Saved keyframe 167 at frame 470 (SSIM: 0.361)\n", - "Saved keyframe 168 at frame 471 (SSIM: 0.295)\n", - "Saved keyframe 169 at frame 472 (SSIM: 0.301)\n", - "Saved keyframe 170 at frame 473 (SSIM: 0.352)\n", - "Saved keyframe 171 at frame 474 (SSIM: 0.373)\n", - "Saved keyframe 172 at frame 475 (SSIM: 0.397)\n", - "Saved keyframe 173 at frame 476 (SSIM: 0.443)\n", - "Saved keyframe 174 at frame 477 (SSIM: 0.451)\n", - "Saved keyframe 175 at frame 478 (SSIM: 0.463)\n", - "Saved keyframe 176 at frame 479 (SSIM: 0.479)\n", - "Saved keyframe 177 at frame 480 (SSIM: 0.492)\n", - "Saved keyframe 178 at frame 481 (SSIM: 0.499)\n", - "Saved keyframe 179 at frame 482 (SSIM: 0.516)\n", - "Saved keyframe 180 at frame 483 (SSIM: 0.529)\n", - "Saved keyframe 181 at frame 484 (SSIM: 0.249)\n", - "Frame 500: SSIM = 0.957 (threshold: 0.6)\n", - "Saved keyframe 182 at frame 508 (SSIM: 0.336)\n", - "Saved keyframe 183 at frame 552 (SSIM: 0.379)\n", - "Saved keyframe 184 at frame 557 (SSIM: 0.586)\n", - "Saved keyframe 185 at frame 563 (SSIM: 0.580)\n", - "Saved keyframe 186 at frame 566 (SSIM: 0.594)\n", - "Saved keyframe 187 at frame 569 (SSIM: 0.575)\n", - "Saved keyframe 188 at frame 574 (SSIM: 0.600)\n", - "Saved keyframe 189 at frame 575 (SSIM: 0.340)\n", - "Saved keyframe 190 at frame 587 (SSIM: 0.595)\n", - "Saved keyframe 191 at frame 590 (SSIM: 0.293)\n", - "Saved keyframe 192 at frame 593 (SSIM: 0.555)\n", - "Saved keyframe 193 at frame 599 (SSIM: 0.564)\n", - "Frame 600: SSIM = 0.845 (threshold: 0.6)\n", - "Saved keyframe 194 at frame 607 (SSIM: 0.584)\n", - "Saved keyframe 195 at frame 611 (SSIM: 0.598)\n", - "Saved keyframe 196 at frame 619 (SSIM: 0.296)\n", - "Saved keyframe 197 at frame 622 (SSIM: 0.540)\n", - "Saved keyframe 198 at frame 625 (SSIM: 0.517)\n", - "Saved keyframe 199 at frame 627 (SSIM: 0.560)\n", - "Saved keyframe 200 at frame 630 (SSIM: 0.577)\n", - "Saved keyframe 201 at frame 635 (SSIM: 0.572)\n", - "Saved keyframe 202 at frame 639 (SSIM: 0.596)\n", - "Saved keyframe 203 at frame 647 (SSIM: 0.326)\n", - "Saved keyframe 204 at frame 651 (SSIM: 0.592)\n", - "Saved keyframe 205 at frame 666 (SSIM: 0.590)\n", - "Saved keyframe 206 at frame 683 (SSIM: 0.275)\n", - "Saved keyframe 207 at frame 686 (SSIM: 0.507)\n", - "Saved keyframe 208 at frame 688 (SSIM: 0.460)\n", - "Saved keyframe 209 at frame 690 (SSIM: 0.492)\n", - "Saved keyframe 210 at frame 692 (SSIM: 0.498)\n", - "Saved keyframe 211 at frame 694 (SSIM: 0.555)\n", - "Saved keyframe 212 at frame 697 (SSIM: 0.462)\n", - "Saved keyframe 213 at frame 699 (SSIM: 0.500)\n", - "Frame 700: SSIM = 0.800 (threshold: 0.6)\n", - "Saved keyframe 214 at frame 702 (SSIM: 0.545)\n", - "Saved keyframe 215 at frame 705 (SSIM: 0.545)\n", - "Saved keyframe 216 at frame 706 (SSIM: 0.233)\n", - "Saved keyframe 217 at frame 721 (SSIM: 0.291)\n", - "Saved keyframe 218 at frame 728 (SSIM: 0.587)\n", - "Saved keyframe 219 at frame 736 (SSIM: 0.594)\n", - "Saved keyframe 220 at frame 743 (SSIM: 0.584)\n", - "Saved keyframe 221 at frame 748 (SSIM: 0.571)\n", - "Saved keyframe 222 at frame 753 (SSIM: 0.584)\n", - "Saved keyframe 223 at frame 757 (SSIM: 0.575)\n", - "Saved keyframe 224 at frame 758 (SSIM: 0.254)\n", - "Saved keyframe 225 at frame 776 (SSIM: 0.275)\n", - "Saved keyframe 226 at frame 799 (SSIM: 0.591)\n", - "Frame 800: SSIM = 0.902 (threshold: 0.6)\n", - "Saved keyframe 227 at frame 805 (SSIM: 0.282)\n", - "Saved keyframe 228 at frame 811 (SSIM: 0.558)\n", - "Saved keyframe 229 at frame 813 (SSIM: 0.512)\n", - "Saved keyframe 230 at frame 815 (SSIM: 0.465)\n", - "Saved keyframe 231 at frame 817 (SSIM: 0.486)\n", - "Saved keyframe 232 at frame 819 (SSIM: 0.580)\n", - "Saved keyframe 233 at frame 822 (SSIM: 0.578)\n", - "Saved keyframe 234 at frame 823 (SSIM: 0.239)\n", - "Saved keyframe 235 at frame 827 (SSIM: 0.599)\n", - "Saved keyframe 236 at frame 831 (SSIM: 0.526)\n", - "Saved keyframe 237 at frame 834 (SSIM: 0.535)\n", - "Saved keyframe 238 at frame 836 (SSIM: 0.241)\n", - "Saved keyframe 239 at frame 839 (SSIM: 0.565)\n", - "Saved keyframe 240 at frame 842 (SSIM: 0.585)\n", - "Saved keyframe 241 at frame 845 (SSIM: 0.549)\n", - "Saved keyframe 242 at frame 848 (SSIM: 0.563)\n", - "Saved keyframe 243 at frame 850 (SSIM: 0.597)\n", - "Saved keyframe 244 at frame 853 (SSIM: 0.594)\n", - "Saved keyframe 245 at frame 854 (SSIM: 0.547)\n", - "Saved keyframe 246 at frame 855 (SSIM: 0.595)\n", - "Saved keyframe 247 at frame 856 (SSIM: 0.580)\n", - "Saved keyframe 248 at frame 857 (SSIM: 0.593)\n", - "Saved keyframe 249 at frame 858 (SSIM: 0.282)\n", - "Saved keyframe 250 at frame 859 (SSIM: 0.584)\n", - "Saved keyframe 251 at frame 861 (SSIM: 0.505)\n", - "Saved keyframe 252 at frame 863 (SSIM: 0.534)\n", - "Saved keyframe 253 at frame 865 (SSIM: 0.525)\n", - "Saved keyframe 254 at frame 870 (SSIM: 0.474)\n", - "Saved keyframe 255 at frame 872 (SSIM: 0.460)\n", - "Saved keyframe 256 at frame 873 (SSIM: 0.564)\n", - "Saved keyframe 257 at frame 874 (SSIM: 0.583)\n", - "Saved keyframe 258 at frame 875 (SSIM: 0.434)\n", - "Saved keyframe 259 at frame 893 (SSIM: 0.317)\n", - "Saved keyframe 260 at frame 895 (SSIM: 0.499)\n", - "Saved keyframe 261 at frame 898 (SSIM: 0.592)\n", - "Frame 900: SSIM = 0.728 (threshold: 0.6)\n", - "Saved keyframe 262 at frame 901 (SSIM: 0.572)\n", - "Saved keyframe 263 at frame 903 (SSIM: 0.588)\n", - "Saved keyframe 264 at frame 906 (SSIM: 0.581)\n", - "Saved keyframe 265 at frame 909 (SSIM: 0.578)\n", - "Saved keyframe 266 at frame 911 (SSIM: 0.597)\n", - "Saved keyframe 267 at frame 913 (SSIM: 0.570)\n", - "Saved keyframe 268 at frame 915 (SSIM: 0.177)\n", - "Saved keyframe 269 at frame 920 (SSIM: 0.592)\n", - "Saved keyframe 270 at frame 932 (SSIM: 0.579)\n", - "Saved keyframe 271 at frame 936 (SSIM: 0.595)\n", - "Saved keyframe 272 at frame 941 (SSIM: 0.593)\n", - "Saved keyframe 273 at frame 944 (SSIM: 0.549)\n", - "Saved keyframe 274 at frame 947 (SSIM: 0.543)\n", - "Saved keyframe 275 at frame 949 (SSIM: 0.301)\n", - "Saved keyframe 276 at frame 959 (SSIM: 0.591)\n", - "Saved keyframe 277 at frame 965 (SSIM: 0.586)\n", - "Saved keyframe 278 at frame 981 (SSIM: 0.594)\n", - "Saved keyframe 279 at frame 995 (SSIM: 0.597)\n", - "Frame 1000: SSIM = 0.695 (threshold: 0.6)\n", - "Saved keyframe 280 at frame 1008 (SSIM: 0.261)\n", - "Saved keyframe 281 at frame 1009 (SSIM: 0.549)\n", - "Saved keyframe 282 at frame 1010 (SSIM: 0.475)\n", - "Saved keyframe 283 at frame 1011 (SSIM: 0.482)\n", - "Saved keyframe 284 at frame 1012 (SSIM: 0.531)\n", - "Saved keyframe 285 at frame 1013 (SSIM: 0.513)\n", - "Saved keyframe 286 at frame 1014 (SSIM: 0.520)\n", - "Saved keyframe 287 at frame 1015 (SSIM: 0.460)\n", - "Saved keyframe 288 at frame 1016 (SSIM: 0.294)\n", - "Saved keyframe 289 at frame 1017 (SSIM: 0.340)\n", - "Saved keyframe 290 at frame 1018 (SSIM: 0.335)\n", - "Saved keyframe 291 at frame 1019 (SSIM: 0.368)\n", - "Saved keyframe 292 at frame 1020 (SSIM: 0.384)\n", - "Saved keyframe 293 at frame 1021 (SSIM: 0.503)\n", - "Saved keyframe 294 at frame 1022 (SSIM: 0.537)\n", - "Saved keyframe 295 at frame 1023 (SSIM: 0.537)\n", - "Saved keyframe 296 at frame 1024 (SSIM: 0.545)\n", - "Saved keyframe 297 at frame 1026 (SSIM: 0.470)\n", - "Saved keyframe 298 at frame 1027 (SSIM: 0.511)\n", - "Saved keyframe 299 at frame 1028 (SSIM: 0.265)\n", - "Saved keyframe 300 at frame 1029 (SSIM: 0.597)\n", - "Saved keyframe 301 at frame 1030 (SSIM: 0.525)\n", - "Saved keyframe 302 at frame 1031 (SSIM: 0.513)\n", - "Saved keyframe 303 at frame 1032 (SSIM: 0.512)\n", - "Saved keyframe 304 at frame 1033 (SSIM: 0.500)\n", - "Saved keyframe 305 at frame 1034 (SSIM: 0.543)\n", - "Saved keyframe 306 at frame 1035 (SSIM: 0.548)\n", - "Saved keyframe 307 at frame 1036 (SSIM: 0.504)\n", - "Saved keyframe 308 at frame 1037 (SSIM: 0.497)\n", - "Saved keyframe 309 at frame 1038 (SSIM: 0.507)\n", - "Saved keyframe 310 at frame 1039 (SSIM: 0.530)\n", - "Saved keyframe 311 at frame 1040 (SSIM: 0.566)\n", - "Saved keyframe 312 at frame 1041 (SSIM: 0.572)\n", - "Saved keyframe 313 at frame 1042 (SSIM: 0.569)\n", - "Saved keyframe 314 at frame 1044 (SSIM: 0.510)\n", - "Saved keyframe 315 at frame 1046 (SSIM: 0.572)\n", - "Saved keyframe 316 at frame 1049 (SSIM: 0.541)\n", - "Saved keyframe 317 at frame 1053 (SSIM: 0.563)\n", - "Saved keyframe 318 at frame 1056 (SSIM: 0.554)\n", - "Saved keyframe 319 at frame 1058 (SSIM: 0.579)\n", - "Saved keyframe 320 at frame 1060 (SSIM: 0.165)\n", - "Saved keyframe 321 at frame 1066 (SSIM: 0.595)\n", - "Saved keyframe 322 at frame 1076 (SSIM: 0.549)\n", - "Saved keyframe 323 at frame 1082 (SSIM: 0.139)\n", - "Saved keyframe 324 at frame 1084 (SSIM: 0.593)\n", - "Saved keyframe 325 at frame 1085 (SSIM: 0.565)\n", - "Saved keyframe 326 at frame 1086 (SSIM: 0.517)\n", - "Saved keyframe 327 at frame 1087 (SSIM: 0.495)\n", - "Saved keyframe 328 at frame 1088 (SSIM: 0.491)\n", - "Saved keyframe 329 at frame 1089 (SSIM: 0.507)\n", - "Saved keyframe 330 at frame 1090 (SSIM: 0.522)\n", - "Saved keyframe 331 at frame 1091 (SSIM: 0.538)\n", - "Saved keyframe 332 at frame 1093 (SSIM: 0.431)\n", - "Saved keyframe 333 at frame 1095 (SSIM: 0.535)\n", - "Frame 1100: SSIM = 0.687 (threshold: 0.6)\n", - "Saved keyframe 334 at frame 1106 (SSIM: 0.275)\n", - "Saved keyframe 335 at frame 1111 (SSIM: 0.600)\n", - "Saved keyframe 336 at frame 1117 (SSIM: 0.583)\n", - "Saved keyframe 337 at frame 1119 (SSIM: 0.418)\n", - "Saved keyframe 338 at frame 1133 (SSIM: 0.594)\n", - "Saved keyframe 339 at frame 1137 (SSIM: 0.261)\n", - "Saved keyframe 340 at frame 1148 (SSIM: 0.524)\n", - "Saved keyframe 341 at frame 1150 (SSIM: 0.502)\n", - "Saved keyframe 342 at frame 1151 (SSIM: 0.556)\n", - "Saved keyframe 343 at frame 1152 (SSIM: 0.501)\n", - "Saved keyframe 344 at frame 1153 (SSIM: 0.595)\n", - "Saved keyframe 345 at frame 1155 (SSIM: 0.577)\n", - "Saved keyframe 346 at frame 1156 (SSIM: 0.578)\n", - "Saved keyframe 347 at frame 1157 (SSIM: 0.244)\n", - "Saved keyframe 348 at frame 1161 (SSIM: 0.595)\n", - "Saved keyframe 349 at frame 1165 (SSIM: 0.600)\n", - "Saved keyframe 350 at frame 1173 (SSIM: 0.575)\n", - "Saved keyframe 351 at frame 1175 (SSIM: 0.377)\n", - "Saved keyframe 352 at frame 1189 (SSIM: 0.330)\n", - "Frame 1200: SSIM = 0.769 (threshold: 0.6)\n", - "Saved keyframe 353 at frame 1201 (SSIM: 0.311)\n", - "Saved keyframe 354 at frame 1205 (SSIM: 0.555)\n", - "Saved keyframe 355 at frame 1217 (SSIM: 0.580)\n", - "Saved keyframe 356 at frame 1218 (SSIM: 0.348)\n", - "Saved keyframe 357 at frame 1220 (SSIM: 0.579)\n", - "Saved keyframe 358 at frame 1222 (SSIM: 0.593)\n", - "Saved keyframe 359 at frame 1225 (SSIM: 0.537)\n", - "Saved keyframe 360 at frame 1227 (SSIM: 0.517)\n", - "Saved keyframe 361 at frame 1232 (SSIM: 0.581)\n", - "Saved keyframe 362 at frame 1233 (SSIM: 0.316)\n", - "Saved keyframe 363 at frame 1237 (SSIM: 0.546)\n", - "Saved keyframe 364 at frame 1239 (SSIM: 0.592)\n", - "Saved keyframe 365 at frame 1242 (SSIM: 0.563)\n", - "Saved keyframe 366 at frame 1245 (SSIM: 0.596)\n", - "Saved keyframe 367 at frame 1246 (SSIM: 0.341)\n", - "Saved keyframe 368 at frame 1247 (SSIM: 0.367)\n", - "Saved keyframe 369 at frame 1248 (SSIM: 0.429)\n", - "Saved keyframe 370 at frame 1249 (SSIM: 0.540)\n", - "Saved keyframe 371 at frame 1250 (SSIM: 0.441)\n", - "Saved keyframe 372 at frame 1252 (SSIM: 0.425)\n", - "Saved keyframe 373 at frame 1256 (SSIM: 0.580)\n", - "Saved keyframe 374 at frame 1257 (SSIM: 0.139)\n", - "Saved keyframe 375 at frame 1258 (SSIM: 0.560)\n", - "Saved keyframe 376 at frame 1259 (SSIM: 0.547)\n", - "Saved keyframe 377 at frame 1260 (SSIM: 0.577)\n", - "Saved keyframe 378 at frame 1262 (SSIM: 0.560)\n", - "Saved keyframe 379 at frame 1264 (SSIM: 0.480)\n", - "Saved keyframe 380 at frame 1265 (SSIM: 0.578)\n", - "Saved keyframe 381 at frame 1267 (SSIM: 0.499)\n", - "Saved keyframe 382 at frame 1269 (SSIM: 0.502)\n", - "Saved keyframe 383 at frame 1270 (SSIM: 0.594)\n", - "Saved keyframe 384 at frame 1271 (SSIM: 0.554)\n", - "Saved keyframe 385 at frame 1272 (SSIM: 0.537)\n", - "Saved keyframe 386 at frame 1273 (SSIM: 0.505)\n", - "Saved keyframe 387 at frame 1274 (SSIM: 0.507)\n", - "Saved keyframe 388 at frame 1275 (SSIM: 0.577)\n", - "Saved keyframe 389 at frame 1277 (SSIM: 0.510)\n", - "Saved keyframe 390 at frame 1278 (SSIM: 0.417)\n", - "Frame 1300: SSIM = 0.809 (threshold: 0.6)\n", - "Saved keyframe 391 at frame 1316 (SSIM: 0.594)\n", - "Frame 1400: SSIM = 0.708 (threshold: 0.6)\n", - "JSON mapping saved to: SSIM_detects\\16257fd6-b91b-4d00-a680-9ece9f3f241c\\c44f38f6-0186-436f-8c2d-ffb50a539c76\\c44f38f6-0186-436f-8c2d-ffb50a539c76_mapping.json\n" + "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" ] } ], From 590cd82bb3b8808332203b5d1b6a11d588b86703 Mon Sep 17 00:00:00 2001 From: yashsuman15 <114386148+yashsuman15@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:58:01 +0530 Subject: [PATCH 23/23] Refactor dataset handling: Rename LabellerrVideoDataset to LabellerrDataset and update related methods in SDK notebook - Updated import statements in __init__.py and SDK.ipynb to reflect the new class name. - Renamed method process_all_videos() to download() in LabellerrDataset class. - Adjusted SDK notebook to use the new download() method for processing videos. - Corrected dataset directory path in SDK notebook. - Added sections for project creation and image dataset handling in the SDK notebook. --- labellerr/core/datasets/__init__.py | 4 +- labellerr/core/datasets/base.py | 6 +- labellerr/notebooks/SDK.ipynb | 428 ++++++++++++++++++++++++++-- 3 files changed, 402 insertions(+), 36 deletions(-) diff --git a/labellerr/core/datasets/__init__.py b/labellerr/core/datasets/__init__.py index a07ba27..8870dc0 100644 --- a/labellerr/core/datasets/__init__.py +++ b/labellerr/core/datasets/__init__.py @@ -1,7 +1,7 @@ """This module will contain all CRUD for datasets. Example, create, list datasets, get dataset, delete dataset, update dataset, etc. """ -from labellerr.core.datasets.base import LabellerrVideoDataset +from labellerr.core.datasets.base import LabellerrDataset __all__ = [ - 'LabellerrVideoDataset' + 'LabellerrDataset' ] \ No newline at end of file diff --git a/labellerr/core/datasets/base.py b/labellerr/core/datasets/base.py index d56292b..c270061 100644 --- a/labellerr/core/datasets/base.py +++ b/labellerr/core/datasets/base.py @@ -6,7 +6,7 @@ from abc import ABCMeta import pprint -class LabellerrVideoDataset: +class LabellerrDataset: """ Class for handling video dataset operations and fetching multiple video files. """ @@ -101,7 +101,7 @@ def fetch_files(self, page_size: int = 1000): except Exception as e: raise LabellerrError(f"Failed to fetch dataset files: {str(e)}") - def process_all_videos(self): + def download(self): """ Process all video files in the dataset: download frames, create videos, and automatically clean up temporary files. @@ -174,7 +174,7 @@ def process_all_videos(self): # dataset = LabellerrVideoDataset(client, dataset_id, project_id) # # Process all videos in the dataset -# results = dataset.process_all_videos() +# results = dataset.download() # # Print summary # pprint.pprint(results) \ No newline at end of file diff --git a/labellerr/notebooks/SDK.ipynb b/labellerr/notebooks/SDK.ipynb index 45ffebf..1efd28f 100644 --- a/labellerr/notebooks/SDK.ipynb +++ b/labellerr/notebooks/SDK.ipynb @@ -21,8 +21,9 @@ "outputs": [], "source": [ "from labellerr.client import LabellerrClient\n", - "from labellerr.core.datasets import LabellerrVideoDataset\n", - "import os" + "from labellerr.core.datasets import LabellerrDataset\n", + "import os\n", + "from tqdm.notebook import tqdm\n" ] }, { @@ -30,7 +31,7 @@ "id": "84b7917a", "metadata": {}, "source": [ - "## Authentication Setup\n", + "## 1. 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", @@ -55,9 +56,12 @@ "metadata": {}, "outputs": [], "source": [ - "api_key = \"\"\n", - "api_secret = \"\"\n", - "client_id = \"\"" + "from dotenv import dotenv_values\n", + "config = dotenv_values(\".env\")\n", + "\n", + "api_key = config[\"API_KEY\"]\n", + "api_secret = config[\"API_SECRET\"]\n", + "client_id = config[\"CLIENT_ID\"]" ] }, { @@ -65,7 +69,7 @@ "id": "3d05bd0f", "metadata": {}, "source": [ - "## Project Configuration\n", + "## 2. Project Configuration\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", @@ -96,13 +100,13 @@ "id": "1b2c7aee", "metadata": {}, "source": [ - "## Initializing the Labellerr SDK\n", + "## 3. Initializing the Labellerr SDK\n", "\n", "### Create LabellerrClient Instance\n", "Now we'll create instances of the main SDK classes:\n", "\n", "1. **LabellerrClient**: The main client that handles communication with the Labellerr API\n", - "2. **LabellerrVideoDataset**: A specialized class for working with video datasets\n", + "2. **LabellerrDataset**: A specialized class for working with datasets\n", "\n", "These instances will be used for all subsequent operations with the platform." ] @@ -115,7 +119,7 @@ "outputs": [], "source": [ "client = LabellerrClient(api_key, api_secret, client_id) \n", - "dataset = LabellerrVideoDataset(client, dataset_id, project_id)" + "dataset = LabellerrDataset(client, dataset_id, project_id)" ] }, { @@ -180,7 +184,7 @@ } ], "source": [ - "results = dataset.process_all_videos()" + "results = dataset.download()" ] }, { @@ -188,8 +192,8 @@ "id": "900ea5a7", "metadata": {}, "source": [ - "### Processing Videos\n", - "The `process_all_videos()` method will:\n", + "### 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", @@ -202,7 +206,7 @@ "id": "f6db8522", "metadata": {}, "source": [ - "## Scene Detection\n", + "## 4. Scene Change Detection\n", "\n", "### Available Scene Detection Methods\n", "Labellerr SDK provides multiple algorithms for scene detection in videos:\n", @@ -227,19 +231,10 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 6, "id": "f5c41073", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "c:\\Users\\HP\\.conda\\envs\\SDk\\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" - ] - } - ], + "outputs": [], "source": [ "from labellerr.services.video_sampling.pyscene_detect import PySceneDetect\n", "from labellerr.services.video_sampling.ssim import SSIMSceneDetect\n", @@ -265,17 +260,17 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "49a6f89d", "metadata": {}, "outputs": [], "source": [ - "dataset_dir = f\".\\Labellerr_datastets\\{dataset_id}\"" + "dataset_dir = f\".\\Labellerr_datasets\\{dataset_id}\"" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 10, "id": "dd96be8c", "metadata": {}, "outputs": [], @@ -294,7 +289,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 11, "id": "a3052f25", "metadata": {}, "outputs": [ @@ -331,13 +326,384 @@ "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" + ] + }, + { + "cell_type": "markdown", + "id": "f5ba527d", + "metadata": {}, + "source": [ + "### Image Dataset Creation from Sampled Frames\n" + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, + "id": "1b364362", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 52 image files\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", + "\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\")" + ] + }, + { + "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", "metadata": {}, "outputs": [], - "source": [] + "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}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "a1d96b25", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset created successfully!\n", + "Dataset ID: 6a680901-fe81-49f0-9120-bb754d63a341\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)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "958fc75e", + "metadata": {}, + "outputs": [], + "source": [ + "new_dataset_id = '6a680901-fe81-49f0-9120-bb754d63a341'" + ] + }, + { + "cell_type": "markdown", + "id": "b454c4f4", + "metadata": {}, + "source": [ + "### Image Annotation Project Creation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d32106c5", + "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b71d2aa0", + "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83565ec3", + "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" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "9f682f4f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Project created successfully!\n", + "Project ID: sherri_puny_rattlesnake_84247\n" + ] + } + ], + "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']" + ] + }, + { + "cell_type": "markdown", + "id": "8645aa60", + "metadata": {}, + "source": [ + "## 6. Performing Annotations of Image Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6eea565", + "metadata": {}, + "outputs": [], + "source": [ + "# annotations of image project on labellerr platform" + ] + }, + { + "cell_type": "markdown", + "id": "8f0611f5", + "metadata": {}, + "source": [ + "### Exporting the Annotation Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b78ad296", + "metadata": {}, + "outputs": [], + "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", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "18529760", + "metadata": {}, + "source": [ + "## 7. Uploading annotations to Video Project" + ] + }, + { + "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" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "df6b3ac7", + "metadata": {}, + "outputs": [], + "source": [ + "# code to create video annotation project from image annotations export" + ] } ], "metadata": {