garmentiq.classification.train_test_split
Unpacking a dataset archive and splitting it into train and test sets.
1"""Unpacking a dataset archive and splitting it into train and test sets.""" 2import os 3import pandas as pd 4import shutil 5import random 6from typing import Optional 7from garmentiq.utils.check_unzipped_dir import check_unzipped_dir 8from garmentiq.utils.unzip import unzip 9from garmentiq.utils.check_filenames_metadata import check_filenames_metadata 10 11 12def train_test_split( 13 output_dir: str, 14 train_zip_dir: str, 15 metadata_csv: str, 16 label_column: str, 17 test_zip_dir: Optional[str] = None, 18 test_size: float = 0.2, 19 seed: int = 88, 20 verbose: bool = False, 21): 22 """ 23 Prepares training and testing datasets from zipped image data and associated metadata. 24 25 This function supports two operation modes: 26 27 1. **Two-Zip Mode**: If both `train_zip_dir` and `test_zip_dir` are provided, each dataset is unzipped, 28 validated against the metadata, and returned as-is. 29 2. **Split Mode**: If only `train_zip_dir` is provided, the function splits the training data 30 into new training and test sets based on `test_size`. 31 32 It ensures: 33 - The unzipped directories contain the expected structure (`images/` and metadata CSV). 34 - Image filenames match those specified in the metadata. 35 - Output datasets are organized into `train/` and `test/` folders under `output_dir`. 36 37 Args: 38 output_dir (str): Directory where the processed datasets will be saved. 39 train_zip_dir (str): Path to the ZIP file containing training data (with `images/` and metadata CSV). 40 metadata_csv (str): Filename of the metadata CSV inside each ZIP archive (e.g., 'metadata.csv'). 41 label_column (str): Name of the column in the metadata CSV to use for class distribution summaries. 42 test_zip_dir (Optional[str]): Optional path to the ZIP file containing testing data. 43 If not provided, the function performs a split. 44 test_size (float): Proportion of data to use for testing if splitting from training data. 45 Ignored if `test_zip_dir` is provided. 46 seed (int): Random seed used for reproducible splitting of training data. 47 verbose (bool): Whether to print class distribution summaries after processing. 48 49 Raises: 50 FileNotFoundError: If any expected files or images are missing. 51 ValueError: If metadata is missing required columns or if filenames and metadata don't align. 52 53 Returns: 54 dict: A dictionary containing: 55 - 'train_images': Path to the directory with training images. 56 - 'train_metadata': DataFrame of training metadata. 57 - 'test_images': Path to the directory with testing images. 58 - 'test_metadata': DataFrame of testing metadata. 59 """ 60 os.makedirs(output_dir, exist_ok=True) 61 62 # Unzip train data 63 train_out = os.path.join(output_dir, "train") 64 unzip(train_zip_dir, train_out) 65 print("\n") 66 check_unzipped_dir(train_out) 67 68 # If a test zip is provided, unzip and process it 69 if test_zip_dir: 70 test_out = os.path.join(output_dir, "test") 71 unzip(test_zip_dir, test_out) 72 check_unzipped_dir(test_out) 73 74 # Load train metadata and check filenames 75 train_metadata_path = os.path.join(train_out, metadata_csv) 76 df_train = pd.read_csv(train_metadata_path) 77 if "filename" not in df_train.columns: 78 raise ValueError("Train metadata must contain a 'filename' column.") 79 check_filenames_metadata( 80 output_dir, os.path.join(train_out, "images"), df_train 81 ) 82 83 # Load test metadata and check filenames 84 test_metadata_path = os.path.join(test_out, metadata_csv) 85 df_test = pd.read_csv(test_metadata_path) 86 if "filename" not in df_test.columns: 87 raise ValueError("Test metadata must contain a 'filename' column.") 88 check_filenames_metadata(output_dir, os.path.join(test_out, "images"), df_test) 89 90 # Summary information 91 if verbose: 92 print(f"\n\nTrain set summary (sample size: {len(df_train)}):\n") 93 print(f"{df_train[label_column].value_counts()}\n") 94 95 print(f"Test set summary (sample size: {len(df_test)}):\n") 96 print(f"{df_test[label_column].value_counts()}\n") 97 98 return { 99 "train_images": f"{train_out}/images", 100 "train_metadata": pd.read_csv(f"{train_out}/metadata.csv"), 101 "test_images": f"{test_out}/images", 102 "test_metadata": pd.read_csv(f"{test_out}/metadata.csv"), 103 } 104 105 # If no test zip is provided, split from train data 106 print("Splitting train data into train/test sets...") 107 108 # Load train metadata 109 metadata_path = os.path.join(train_out, metadata_csv) 110 df = pd.read_csv(metadata_path) 111 112 if "filename" not in df.columns: 113 raise ValueError("metadata.csv must contain a 'filename' column.") 114 115 # Select test split 116 random.seed(seed) 117 filenames = df["filename"].tolist() 118 test_filenames = set(random.sample(filenames, int(len(filenames) * test_size))) 119 120 # Prepare test folder 121 test_out = os.path.join(output_dir, "test") 122 test_images_dir = os.path.join(test_out, "images") 123 os.makedirs(test_images_dir, exist_ok=True) 124 125 train_images_dir = os.path.join(train_out, "images") 126 127 # Move test files from train to test folder 128 for fname in test_filenames: 129 src = os.path.join(train_images_dir, fname) 130 dst = os.path.join(test_images_dir, fname) 131 if not os.path.exists(src): 132 raise FileNotFoundError(f"File listed in metadata not found: {fname}") 133 shutil.move(src, dst) 134 135 # Save updated CSVs 136 df_test = df[df["filename"].isin(test_filenames)] 137 df_train = df[~df["filename"].isin(test_filenames)] 138 139 df_test.to_csv(os.path.join(test_out, metadata_csv), index=False) 140 df_train.to_csv(metadata_path, index=False) 141 142 # Check if output images match the metadata records 143 check_filenames_metadata(output_dir, os.path.join(train_out, "images"), df_train) 144 check_filenames_metadata(output_dir, os.path.join(test_out, "images"), df_test) 145 146 # Summary information 147 if verbose: 148 print(f"\n\nTrain set summary (sample size: {len(df_train)}):\n") 149 print(f"{df_train[label_column].value_counts()}\n") 150 151 print(f"Test set summary (sample size: {len(df_test)}):\n") 152 print(f"{df_test[label_column].value_counts()}\n") 153 154 return { 155 "train_images": f"{train_out}/images", 156 "train_metadata": pd.read_csv(f"{train_out}/metadata.csv"), 157 "test_images": f"{test_out}/images", 158 "test_metadata": pd.read_csv(f"{test_out}/metadata.csv"), 159 } 160 161 # If any mismatch found, remove the output directory and raise an error 162 try: 163 check_filenames_metadata( 164 output_dir, os.path.join(train_out, "images"), df_train 165 ) 166 check_filenames_metadata(output_dir, os.path.join(test_out, "images"), df_test) 167 except ValueError as e: 168 shutil.rmtree(output_dir) # Clean up the output directory in case of error 169 raise e
def
train_test_split( output_dir: str, train_zip_dir: str, metadata_csv: str, label_column: str, test_zip_dir: Optional[str] = None, test_size: float = 0.2, seed: int = 88, verbose: bool = False):
13def train_test_split( 14 output_dir: str, 15 train_zip_dir: str, 16 metadata_csv: str, 17 label_column: str, 18 test_zip_dir: Optional[str] = None, 19 test_size: float = 0.2, 20 seed: int = 88, 21 verbose: bool = False, 22): 23 """ 24 Prepares training and testing datasets from zipped image data and associated metadata. 25 26 This function supports two operation modes: 27 28 1. **Two-Zip Mode**: If both `train_zip_dir` and `test_zip_dir` are provided, each dataset is unzipped, 29 validated against the metadata, and returned as-is. 30 2. **Split Mode**: If only `train_zip_dir` is provided, the function splits the training data 31 into new training and test sets based on `test_size`. 32 33 It ensures: 34 - The unzipped directories contain the expected structure (`images/` and metadata CSV). 35 - Image filenames match those specified in the metadata. 36 - Output datasets are organized into `train/` and `test/` folders under `output_dir`. 37 38 Args: 39 output_dir (str): Directory where the processed datasets will be saved. 40 train_zip_dir (str): Path to the ZIP file containing training data (with `images/` and metadata CSV). 41 metadata_csv (str): Filename of the metadata CSV inside each ZIP archive (e.g., 'metadata.csv'). 42 label_column (str): Name of the column in the metadata CSV to use for class distribution summaries. 43 test_zip_dir (Optional[str]): Optional path to the ZIP file containing testing data. 44 If not provided, the function performs a split. 45 test_size (float): Proportion of data to use for testing if splitting from training data. 46 Ignored if `test_zip_dir` is provided. 47 seed (int): Random seed used for reproducible splitting of training data. 48 verbose (bool): Whether to print class distribution summaries after processing. 49 50 Raises: 51 FileNotFoundError: If any expected files or images are missing. 52 ValueError: If metadata is missing required columns or if filenames and metadata don't align. 53 54 Returns: 55 dict: A dictionary containing: 56 - 'train_images': Path to the directory with training images. 57 - 'train_metadata': DataFrame of training metadata. 58 - 'test_images': Path to the directory with testing images. 59 - 'test_metadata': DataFrame of testing metadata. 60 """ 61 os.makedirs(output_dir, exist_ok=True) 62 63 # Unzip train data 64 train_out = os.path.join(output_dir, "train") 65 unzip(train_zip_dir, train_out) 66 print("\n") 67 check_unzipped_dir(train_out) 68 69 # If a test zip is provided, unzip and process it 70 if test_zip_dir: 71 test_out = os.path.join(output_dir, "test") 72 unzip(test_zip_dir, test_out) 73 check_unzipped_dir(test_out) 74 75 # Load train metadata and check filenames 76 train_metadata_path = os.path.join(train_out, metadata_csv) 77 df_train = pd.read_csv(train_metadata_path) 78 if "filename" not in df_train.columns: 79 raise ValueError("Train metadata must contain a 'filename' column.") 80 check_filenames_metadata( 81 output_dir, os.path.join(train_out, "images"), df_train 82 ) 83 84 # Load test metadata and check filenames 85 test_metadata_path = os.path.join(test_out, metadata_csv) 86 df_test = pd.read_csv(test_metadata_path) 87 if "filename" not in df_test.columns: 88 raise ValueError("Test metadata must contain a 'filename' column.") 89 check_filenames_metadata(output_dir, os.path.join(test_out, "images"), df_test) 90 91 # Summary information 92 if verbose: 93 print(f"\n\nTrain set summary (sample size: {len(df_train)}):\n") 94 print(f"{df_train[label_column].value_counts()}\n") 95 96 print(f"Test set summary (sample size: {len(df_test)}):\n") 97 print(f"{df_test[label_column].value_counts()}\n") 98 99 return { 100 "train_images": f"{train_out}/images", 101 "train_metadata": pd.read_csv(f"{train_out}/metadata.csv"), 102 "test_images": f"{test_out}/images", 103 "test_metadata": pd.read_csv(f"{test_out}/metadata.csv"), 104 } 105 106 # If no test zip is provided, split from train data 107 print("Splitting train data into train/test sets...") 108 109 # Load train metadata 110 metadata_path = os.path.join(train_out, metadata_csv) 111 df = pd.read_csv(metadata_path) 112 113 if "filename" not in df.columns: 114 raise ValueError("metadata.csv must contain a 'filename' column.") 115 116 # Select test split 117 random.seed(seed) 118 filenames = df["filename"].tolist() 119 test_filenames = set(random.sample(filenames, int(len(filenames) * test_size))) 120 121 # Prepare test folder 122 test_out = os.path.join(output_dir, "test") 123 test_images_dir = os.path.join(test_out, "images") 124 os.makedirs(test_images_dir, exist_ok=True) 125 126 train_images_dir = os.path.join(train_out, "images") 127 128 # Move test files from train to test folder 129 for fname in test_filenames: 130 src = os.path.join(train_images_dir, fname) 131 dst = os.path.join(test_images_dir, fname) 132 if not os.path.exists(src): 133 raise FileNotFoundError(f"File listed in metadata not found: {fname}") 134 shutil.move(src, dst) 135 136 # Save updated CSVs 137 df_test = df[df["filename"].isin(test_filenames)] 138 df_train = df[~df["filename"].isin(test_filenames)] 139 140 df_test.to_csv(os.path.join(test_out, metadata_csv), index=False) 141 df_train.to_csv(metadata_path, index=False) 142 143 # Check if output images match the metadata records 144 check_filenames_metadata(output_dir, os.path.join(train_out, "images"), df_train) 145 check_filenames_metadata(output_dir, os.path.join(test_out, "images"), df_test) 146 147 # Summary information 148 if verbose: 149 print(f"\n\nTrain set summary (sample size: {len(df_train)}):\n") 150 print(f"{df_train[label_column].value_counts()}\n") 151 152 print(f"Test set summary (sample size: {len(df_test)}):\n") 153 print(f"{df_test[label_column].value_counts()}\n") 154 155 return { 156 "train_images": f"{train_out}/images", 157 "train_metadata": pd.read_csv(f"{train_out}/metadata.csv"), 158 "test_images": f"{test_out}/images", 159 "test_metadata": pd.read_csv(f"{test_out}/metadata.csv"), 160 } 161 162 # If any mismatch found, remove the output directory and raise an error 163 try: 164 check_filenames_metadata( 165 output_dir, os.path.join(train_out, "images"), df_train 166 ) 167 check_filenames_metadata(output_dir, os.path.join(test_out, "images"), df_test) 168 except ValueError as e: 169 shutil.rmtree(output_dir) # Clean up the output directory in case of error 170 raise e
Prepares training and testing datasets from zipped image data and associated metadata.
This function supports two operation modes:
- Two-Zip Mode: If both
train_zip_dirandtest_zip_dirare provided, each dataset is unzipped, validated against the metadata, and returned as-is. - Split Mode: If only
train_zip_diris provided, the function splits the training data into new training and test sets based ontest_size.
It ensures:
- The unzipped directories contain the expected structure (
images/and metadata CSV). - Image filenames match those specified in the metadata.
- Output datasets are organized into
train/andtest/folders underoutput_dir.
Arguments:
- output_dir (str): Directory where the processed datasets will be saved.
- train_zip_dir (str): Path to the ZIP file containing training data (with
images/and metadata CSV). - metadata_csv (str): Filename of the metadata CSV inside each ZIP archive (e.g., 'metadata.csv').
- label_column (str): Name of the column in the metadata CSV to use for class distribution summaries.
- test_zip_dir (Optional[str]): Optional path to the ZIP file containing testing data. If not provided, the function performs a split.
- test_size (float): Proportion of data to use for testing if splitting from training data.
Ignored if
test_zip_diris provided. - seed (int): Random seed used for reproducible splitting of training data.
- verbose (bool): Whether to print class distribution summaries after processing.
Raises:
- FileNotFoundError: If any expected files or images are missing.
- ValueError: If metadata is missing required columns or if filenames and metadata don't align.
Returns:
dict: A dictionary containing: - 'train_images': Path to the directory with training images. - 'train_metadata': DataFrame of training metadata. - 'test_images': Path to the directory with testing images. - 'test_metadata': DataFrame of testing metadata.