From e62cbae53295b292321547b465b76a5ca7dd5e6c Mon Sep 17 00:00:00 2001 From: Oisin Date: Mon, 10 Feb 2025 21:29:19 +0000 Subject: [PATCH 01/11] Added classify image script via torch model --- model/arch/classify_image_torch.py | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 model/arch/classify_image_torch.py diff --git a/model/arch/classify_image_torch.py b/model/arch/classify_image_torch.py new file mode 100644 index 0000000..463361f --- /dev/null +++ b/model/arch/classify_image_torch.py @@ -0,0 +1,84 @@ +# python arch/classify_image_torch.py --image_fpath E:/GitHub/CatClassifier/data/train/cat.0.jpg + +import logging +import argparse +import platform +import os +import pandas as pd +import numpy as np +import sys +import re + +# set root file directories +root_dir_re_match = re.findall(string=os.getcwd(), pattern="^.+CatClassifier") +root_fdir = root_dir_re_match[0] if len(root_dir_re_match) > 0 else os.path.join(".", "CatClassifier") +model_fdir = os.path.join(root_fdir, 'model') +sys.path.append(model_fdir) + +# set huggingface hub directory +huggingface_hub_dir = 'E:\\huggingface' +if (platform.system() == 'Windows') and (os.path.exists(huggingface_hub_dir)): + os.environ['TORCH_HOME'] = huggingface_hub_dir + os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0' + +import torch +import torch.nn as nn +from torch.utils.data import DataLoader +from torchvision import transforms + +# load custom scripts +import cons +from model.torch.VGG16_pretrained import VGG16_pretrained +from model.torch.CustomDataset import CustomDataset + +# device configuration +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + +torch_transforms = transforms.Compose([ + transforms.Resize(size=[cons.IMAGE_WIDTH, cons.IMAGE_HEIGHT]) # resize the input image to a uniform size + #,transforms.RandomRotation(30) + #,transforms.RandomHorizontalFlip(p=0.05) + #,transforms.RandomPerspective(distortion_scale=0.05, p=0.05) + ,transforms.ToTensor() # convert PIL Image or numpy.ndarray to tensor and normalize to somewhere between [0,1] + ,transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # standardized processing +]) + +if __name__ == "__main__": + + # set up logging + lgr = logging.getLogger() + lgr.setLevel(logging.INFO) + + # define argument parser object + parser = argparse.ArgumentParser(description="Classify Image (Torch Model)") + # add input arguments + parser.add_argument("--image_fpath", action="store", dest="image_fpath", type=str, help="String, the full file path to the image to classify") + # create an output dictionary to hold the results + input_params_dict = {} + # extract input arguments + args = parser.parse_args() + # map input arguments into output dictionary + input_params_dict["image_fpath"] = args.image_fpath + + logging.info("Loading torch model...") + # load model + #model = AlexNet8(num_classes=2).to(device) + model = VGG16_pretrained(num_classes=2).to(device) + model.load(input_fpath=cons.torch_model_pt_fpath) + + logging.info("Generating dataset...") + # prepare test data + filenames = os.listdir(cons.test_fdir) + df = pd.DataFrame({'filepath': [input_params_dict["image_fpath"]]}) + + logging.info("Creating dataloader...") + # set train data loader + dataset = CustomDataset(df, transforms=torch_transforms, mode='test') + loader = DataLoader(dataset, batch_size=cons.batch_size, shuffle=False, num_workers=cons.num_workers, pin_memory=True) + + logging.info("Classifying image...") + # make test set predictions + predict = model.predict(loader, device) + df['category'] = np.argmax(predict, axis=-1) + df["category"] = df["category"].replace(cons.category_mapper) + logging.info(df.to_dict(orient="records")) From 363cefee637c7f07bf6c8348e201177c740947ae Mon Sep 17 00:00:00 2001 From: Oisin Date: Tue, 11 Feb 2025 08:54:06 +0000 Subject: [PATCH 02/11] Replaced ec2 compute instances with ec2 accelorated instances --- aws/ref/create_fleet_config.json | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/aws/ref/create_fleet_config.json b/aws/ref/create_fleet_config.json index b83d510..6a00656 100644 --- a/aws/ref/create_fleet_config.json +++ b/aws/ref/create_fleet_config.json @@ -12,40 +12,37 @@ }, "Overrides": [ { - "InstanceType": "c3.2xlarge" + "InstanceType": "g4ad.xlarge" }, { - "InstanceType": "c3.4xlarge" + "InstanceType": "g4ad.2xlarge" }, { - "InstanceType": "c4.2xlarge" + "InstanceType": "g4ad.4xlarge" }, { - "InstanceType": "c4.4xlarge" + "InstanceType": "g4dn.xlarge" }, { - "InstanceType": "c5.2xlarge" + "InstanceType": "g4dn.2xlarge" }, { - "InstanceType": "c5.4xlarge" + "InstanceType": "g4dn.4xlarge" }, { - "InstanceType": "c5a.2xlarge" + "InstanceType": "g5.xlarge" }, { - "InstanceType": "c5a.4xlarge" + "InstanceType": "g5.2xlarge" }, { - "InstanceType": "c5ad.2xlarge" + "InstanceType": "g6.xlarge" }, { - "InstanceType": "c5ad.4xlarge" + "InstanceType": "g6.2xlarge" }, { - "InstanceType": "c5d.2xlarge" - }, - { - "InstanceType": "c5d.4xlarge" + "InstanceType": "g6.4xlarge" } ] } From 62aff9f1a47cf279da4a0c72bc8c342d4344a42a Mon Sep 17 00:00:00 2001 From: Oisin Date: Tue, 11 Feb 2025 08:54:40 +0000 Subject: [PATCH 03/11] Functionalised classify image torch --- model/arch/classify_image_torch.py | 71 ++++++++++++++++++------------ 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/model/arch/classify_image_torch.py b/model/arch/classify_image_torch.py index 463361f..aaa1e48 100644 --- a/model/arch/classify_image_torch.py +++ b/model/arch/classify_image_torch.py @@ -1,4 +1,4 @@ -# python arch/classify_image_torch.py --image_fpath E:/GitHub/CatClassifier/data/train/cat.0.jpg +# python model/arch/classify_image_torch.py --image_fpath E:/GitHub/CatClassifier/data/train/cat.0.jpg import logging import argparse @@ -8,6 +8,7 @@ import numpy as np import sys import re +from beartype import beartype # set root file directories root_dir_re_match = re.findall(string=os.getcwd(), pattern="^.+CatClassifier") @@ -43,42 +44,58 @@ ,transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # standardized processing ]) -if __name__ == "__main__": - - # set up logging - lgr = logging.getLogger() - lgr.setLevel(logging.INFO) - - # define argument parser object - parser = argparse.ArgumentParser(description="Classify Image (Torch Model)") - # add input arguments - parser.add_argument("--image_fpath", action="store", dest="image_fpath", type=str, help="String, the full file path to the image to classify") - # create an output dictionary to hold the results - input_params_dict = {} - # extract input arguments - args = parser.parse_args() - # map input arguments into output dictionary - input_params_dict["image_fpath"] = args.image_fpath - +@beartype +def classify_image_torch(image_fpath:str): + """Classifies an input image using the torch model + + Parameters + ---------- + image_fpath : str + The image file to classify using the torch model + + Returns + ------- + list + The image file classification results as a recordset + """ + logging.info("Loading torch model...") # load model #model = AlexNet8(num_classes=2).to(device) model = VGG16_pretrained(num_classes=2).to(device) model.load(input_fpath=cons.torch_model_pt_fpath) - + logging.info("Generating dataset...") # prepare test data - filenames = os.listdir(cons.test_fdir) - df = pd.DataFrame({'filepath': [input_params_dict["image_fpath"]]}) - + dataframe = pd.DataFrame({'filepath': [image_fpath]}) + logging.info("Creating dataloader...") # set train data loader - dataset = CustomDataset(df, transforms=torch_transforms, mode='test') + dataset = CustomDataset(dataframe, transforms=torch_transforms, mode='test') loader = DataLoader(dataset, batch_size=cons.batch_size, shuffle=False, num_workers=cons.num_workers, pin_memory=True) - + logging.info("Classifying image...") # make test set predictions predict = model.predict(loader, device) - df['category'] = np.argmax(predict, axis=-1) - df["category"] = df["category"].replace(cons.category_mapper) - logging.info(df.to_dict(orient="records")) + dataframe['category'] = np.argmax(predict, axis=-1) + dataframe["category"] = dataframe["category"].replace(cons.category_mapper) + response = dataframe.to_dict(orient="records") + logging.info(response) + return response + +if __name__ == "__main__": + + # set up logging + lgr = logging.getLogger() + lgr.setLevel(logging.INFO) + + # define argument parser object + parser = argparse.ArgumentParser(description="Classify Image (Torch Model)") + # add input arguments + parser.add_argument("--image_fpath", action="store", dest="image_fpath", type=str, help="String, the full file path to the image to classify") + # create an output dictionary to hold the results + input_params_dict = {} + # extract input arguments + args = parser.parse_args() + # classify image using torch model + response = classify_image_torch(image_fpath=args.image_fpath) \ No newline at end of file From f2554c70ce44af67dbe44f37987b9388ac0baa23 Mon Sep 17 00:00:00 2001 From: Oisin Date: Tue, 11 Feb 2025 14:50:40 +0000 Subject: [PATCH 04/11] Added thumbnail to repo, and linked to serialised models on kaggle --- README.md | 4 ++++ doc/woof_meow.jpg | Bin 0 -> 54476 bytes 2 files changed, 4 insertions(+) create mode 100644 doc/woof_meow.jpg diff --git a/README.md b/README.md index 192a0fa..0d23333 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ See the analysis results notebook for a further details on the analysis; includi * https://nbviewer.org/github/oislen/CatClassifier/blob/main/report/torch_analysis_results.ipynb +Master serialised copies of the trainined models are available on Kaggle: + +* https://www.kaggle.com/models/oislen/cat-classifier-cnn-models + ## Running the Application (Windows) ### Anaconda diff --git a/doc/woof_meow.jpg b/doc/woof_meow.jpg new file mode 100644 index 0000000000000000000000000000000000000000..32cb7f422932f03c55c6dafbd0277606d5f6a984 GIT binary patch literal 54476 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 literal 0 HcmV?d00001 From bbcdc82035a061960f48605f9f4884e4075635b2 Mon Sep 17 00:00:00 2001 From: Oisin Date: Tue, 11 Feb 2025 17:23:50 +0000 Subject: [PATCH 05/11] Updated classify image torch function --- model/arch/classify_image_torch.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/model/arch/classify_image_torch.py b/model/arch/classify_image_torch.py index aaa1e48..1703ac5 100644 --- a/model/arch/classify_image_torch.py +++ b/model/arch/classify_image_torch.py @@ -1,4 +1,4 @@ -# python model/arch/classify_image_torch.py --image_fpath E:/GitHub/CatClassifier/data/train/cat.0.jpg +# python model/arch/classify_image_torch.py --image_fpath E:/GitHub/CatClassifier/data/train/cat.0.jpg --model_fpath E:/GitHub/CatClassifier/data/models/VGG16.pt import logging import argparse @@ -45,13 +45,15 @@ ]) @beartype -def classify_image_torch(image_fpath:str): +def classify_image_torch(image_fpath:str, model_fpath:str=cons.torch_model_pt_fpath): """Classifies an input image using the torch model Parameters ---------- image_fpath : str - The image file to classify using the torch model + The full filepath to the image to classify using the torch model + model_fpath : str + The full filepath to the torch model to use for classification, default is cons.torch_model_pt_fpath Returns ------- @@ -63,7 +65,7 @@ def classify_image_torch(image_fpath:str): # load model #model = AlexNet8(num_classes=2).to(device) model = VGG16_pretrained(num_classes=2).to(device) - model.load(input_fpath=cons.torch_model_pt_fpath) + model.load(input_fpath=model_fpath) logging.info("Generating dataset...") # prepare test data @@ -93,9 +95,10 @@ def classify_image_torch(image_fpath:str): parser = argparse.ArgumentParser(description="Classify Image (Torch Model)") # add input arguments parser.add_argument("--image_fpath", action="store", dest="image_fpath", type=str, help="String, the full file path to the image to classify") + parser.add_argument("--model_fpath", action="store", dest="model_fpath", type=str, default=cons.torch_model_pt_fpath, help="String, the full file path to the model to use for classification") # create an output dictionary to hold the results input_params_dict = {} # extract input arguments args = parser.parse_args() # classify image using torch model - response = classify_image_torch(image_fpath=args.image_fpath) \ No newline at end of file + response = classify_image_torch(image_fpath=args.image_fpath, model_fpath=args.model_fpath) \ No newline at end of file From b8095a4ef1c73f8f2879bb363dfc9368e173f9da Mon Sep 17 00:00:00 2001 From: Oisin Date: Tue, 11 Feb 2025 17:33:23 +0000 Subject: [PATCH 06/11] Created function for classifying images using keras model --- model/arch/classify_image_keras.py | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 model/arch/classify_image_keras.py diff --git a/model/arch/classify_image_keras.py b/model/arch/classify_image_keras.py new file mode 100644 index 0000000..4d4993e --- /dev/null +++ b/model/arch/classify_image_keras.py @@ -0,0 +1,84 @@ +# python model/arch/classify_image_keras.py --image_fpath E:/GitHub/CatClassifier/data/train/cat.0.jpg --model_fpath E:/GitHub/CatClassifier/data/models/AlexNet8.keras + +import logging +import argparse +import platform +import os +import pandas as pd +import numpy as np +import sys +import re +from beartype import beartype + +# set root file directories +root_dir_re_match = re.findall(string=os.getcwd(), pattern="^.+CatClassifier") +root_fdir = root_dir_re_match[0] if len(root_dir_re_match) > 0 else os.path.join(".", "CatClassifier") +model_fdir = os.path.join(root_fdir, 'model') +sys.path.append(model_fdir) + +# load custom scripts +import cons + +# load tensorflow / keras modules +from tensorflow.keras.preprocessing.image import ImageDataGenerator +from tensorflow.keras.preprocessing.image import load_img +from keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint +from keras.models import load_model +from keras import optimizers + +@beartype +def classify_image_keras(image_fpath:str, model_fpath:str=cons.keras_model_pickle_fpath): + """Classifies an input image using the keras model + + Parameters + ---------- + image_fpath : str + The full filepath to the image to classify using the keras model + model_fpath : str + The full filepath to the keras model to use for classification, default is cons.keras_model_pickle_fpath + + Returns + ------- + list + The image file classification results as a recordset + """ + + logging.info("Loading keras model...") + # load model + model = load_model(model_fpath) + + logging.info("Generating dataset...") + # prepare test data + dataframe = pd.DataFrame({'filepath': [image_fpath]}) + + logging.info("Creating dataloader...") + # set data generator + imagedatagenerator = ImageDataGenerator(rescale=cons.rescale) + generator = imagedatagenerator.flow_from_dataframe(dataframe=dataframe, directory=cons.test_fdir, x_col='filepath', y_col=None, class_mode=None, target_size=cons.IMAGE_SIZE, batch_size=cons.batch_size, shuffle=cons.shuffle) + + logging.info("Classifying image...") + # make test set predictions + predict = model.predict(generator, steps=int(np.ceil(dataframe.shape[0]/cons.batch_size))) + dataframe['category'] = np.argmax(predict, axis=-1) + dataframe['category'] = dataframe['category'].replace(cons.category_mapper) + response = dataframe.to_dict(orient="records") + logging.info(response) + return response + +if __name__ == "__main__": + + # set up logging + lgr = logging.getLogger() + lgr.setLevel(logging.INFO) + + # define argument parser object + parser = argparse.ArgumentParser(description="Classify Image (Torch Model)") + # add input arguments + parser.add_argument("--image_fpath", action="store", dest="image_fpath", type=str, help="String, the full file path to the image to classify") + parser.add_argument("--model_fpath", action="store", dest="model_fpath", type=str, default=cons.keras_model_pickle_fpath, help="String, the full file path to the model to use for classification") + # create an output dictionary to hold the results + input_params_dict = {} + # extract input arguments + args = parser.parse_args() + # classify image using keras model + response = classify_image_keras(image_fpath=args.image_fpath, model_fpath=args.model_fpath) \ No newline at end of file From 1ed1af332ec3ea8e2f94b47fe3b5eb77c0efd8ba Mon Sep 17 00:00:00 2001 From: Oisin Date: Tue, 11 Feb 2025 18:14:47 +0000 Subject: [PATCH 07/11] Added function for downloading pretrained models from kaggle --- webscrapers/utilities/download_comp_data.py | 70 ++++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/webscrapers/utilities/download_comp_data.py b/webscrapers/utilities/download_comp_data.py index 3a1a70c..baed2ba 100644 --- a/webscrapers/utilities/download_comp_data.py +++ b/webscrapers/utilities/download_comp_data.py @@ -6,8 +6,14 @@ from beartype import beartype @beartype -def download_comp_data(comp_name:str, data_dir:str, download_data:bool=True, unzip_data:bool=True, del_zip:bool=True): - """Download Competition Data Documentation +def download_comp_data( + comp_name:str, + data_dir:str, + download_data:bool=True, + unzip_data:bool=True, + del_zip:bool=True + ): + """Download Competition Data Parameters ---------- @@ -25,11 +31,21 @@ def download_comp_data(comp_name:str, data_dir:str, download_data:bool=True, unz Returns ------- + + Example + ------- + download_comp_data( + comp_name="dogs-vs-cats", + data_dir="E:\\GitHub\\CatClassifier\\data", + download_data=True, + unzip_data=True, + del_zip=True + ) """ logging.info('create zip file path ...') # define filenames - zip_data_fname = '{}.zip'.format(comp_name) + zip_data_fname = f'{comp_name}.zip' # create file paths zip_data_fpath = os.path.join(data_dir, zip_data_fname) zip_train_fpath = os.path.join(data_dir, 'train.zip') @@ -42,18 +58,18 @@ def download_comp_data(comp_name:str, data_dir:str, download_data:bool=True, unz if os.path.exists(data_dir) == False: os.makedirs(data_dir) else: - logging.info('data directory exists: {}'.format(data_dir)) + logging.info(f'data directory exists: {data_dir}') - # if redownloading the data + # if downloading the data if download_data == True: logging.info('downing kaggle data ..') - kaggle_cmd = 'kaggle competitions download --competition {} --path {} --force'.format(comp_name, data_dir) + kaggle_cmd = f'kaggle competitions download --competition {comp_name} --path {data_dir} --force' subprocess.run(kaggle_cmd.split()) # if unzipping the data if unzip_data == True: if os.path.exists(zip_data_fpath) == False: - raise OSError('file not found: {}'.format(zip_data_fpath)) + raise OSError(f'file not found: {zip_data_fpath}') else: for zip_fpath in zip_fpaths_list: logging.info(f'unzipping data {zip_fpath} ...') @@ -64,4 +80,42 @@ def download_comp_data(comp_name:str, data_dir:str, download_data:bool=True, unz if del_zip == True: for zip_fpath in zip_fpaths_list: logging.info('deleting zip file {zip_fpath} ...') - os.remove(path = zip_fpath) \ No newline at end of file + os.remove(path = zip_fpath) + +@beartype +def download_models( + model_instance_url:str, + model_dir:str + ): + """Download Kaggle Models + + Parameters + ---------- + + model_instance_url : str + Model Instance Version URL suffix in format ////. + model_dir : str + Folder where file(s) will be downloaded. + + Returns + ------- + + Example + ------- + download_models( + model_instance_url="oislen/cat-classifier-cnn-models/pyTorch/default/1", + model_dir="E:\\GitHub\\CatClassifier\\data\\models" + ) + """ + + logging.info('checking for data directory ...') + # check data directory exists + if os.path.exists(model_dir) == False: + os.makedirs(model_dir) + else: + logging.info(f'model directory exists: {model_dir}') + + # downloading the model + logging.info('downloading kaggle model ..') + kaggle_cmd = f'kaggle models instances versions download --path {model_dir} --untar --force {model_instance_url}' + subprocess.run(kaggle_cmd.split()) From 857417dfba4032a0110499f5c58017f477dde092 Mon Sep 17 00:00:00 2001 From: Oisin Date: Tue, 11 Feb 2025 18:15:27 +0000 Subject: [PATCH 08/11] Downloading exmaple to data subdirectory --- webscrapers/utilities/download_comp_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webscrapers/utilities/download_comp_data.py b/webscrapers/utilities/download_comp_data.py index baed2ba..ec889cb 100644 --- a/webscrapers/utilities/download_comp_data.py +++ b/webscrapers/utilities/download_comp_data.py @@ -104,7 +104,7 @@ def download_models( ------- download_models( model_instance_url="oislen/cat-classifier-cnn-models/pyTorch/default/1", - model_dir="E:\\GitHub\\CatClassifier\\data\\models" + model_dir="E:\\GitHub\\CatClassifier\\data" ) """ From 20bdd6757319317d4617c87eac9d67e8aee3f8e4 Mon Sep 17 00:00:00 2001 From: Oisin Date: Tue, 11 Feb 2025 18:15:58 +0000 Subject: [PATCH 09/11] Reverted last change --- webscrapers/utilities/download_comp_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webscrapers/utilities/download_comp_data.py b/webscrapers/utilities/download_comp_data.py index ec889cb..baed2ba 100644 --- a/webscrapers/utilities/download_comp_data.py +++ b/webscrapers/utilities/download_comp_data.py @@ -104,7 +104,7 @@ def download_models( ------- download_models( model_instance_url="oislen/cat-classifier-cnn-models/pyTorch/default/1", - model_dir="E:\\GitHub\\CatClassifier\\data" + model_dir="E:\\GitHub\\CatClassifier\\data\\models" ) """ From 0e70937b0f69f530fb739e80914d003711dea4a5 Mon Sep 17 00:00:00 2001 From: Oisin Date: Tue, 11 Feb 2025 18:23:33 +0000 Subject: [PATCH 10/11] Added logic to pull kaggle master models as part of the initial web scraping process --- webscrapers/cons.py | 24 ++++++++------- webscrapers/exeWebscraper.cmd | 2 +- webscrapers/exeWebscraper.sh | 2 +- webscrapers/prg_scrape_imgs.py | 29 ++++++++++++------ .../utilities/commandline_interface.py | 2 ++ webscrapers/utilities/download_comp_data.py | 30 +++++++++---------- 6 files changed, 53 insertions(+), 36 deletions(-) diff --git a/webscrapers/cons.py b/webscrapers/cons.py index 71e0f54..006d829 100644 --- a/webscrapers/cons.py +++ b/webscrapers/cons.py @@ -5,13 +5,14 @@ # set root file directories root_dir_re_match = re.findall(string=os.getcwd(), pattern="^.+CatClassifier") root_fdir = root_dir_re_match[0] if len(root_dir_re_match) > 0 else os.path.join(".", "CatClassifier") -data_fdir = os.path.join(root_fdir, 'data') -creds_fdir = os.path.join(root_fdir, '.creds') -dataprep_fdir = os.path.join(root_fdir, 'data_prep') -report_fdir = os.path.join(root_fdir, 'report') -test_fdir = os.path.join(data_fdir, 'test1') -train_fdir = os.path.join(data_fdir, 'train') -webscrapers_fdir = os.path.join(root_fdir, 'webscrapers') +data_fdir = os.path.join(root_fdir, "data") +creds_fdir = os.path.join(root_fdir, ".creds") +dataprep_fdir = os.path.join(root_fdir, "data_prep") +report_fdir = os.path.join(root_fdir, "report") +test_fdir = os.path.join(data_fdir, "test1") +train_fdir = os.path.join(data_fdir, "train") +models_fir = os.path.join(data_fdir, "models") +webscrapers_fdir = os.path.join(root_fdir, "webscrapers") # set list containing all required directories root_fdirs = [root_fdir, data_fdir, dataprep_fdir, report_fdir, test_fdir, train_fdir, webscrapers_fdir] @@ -22,12 +23,15 @@ # set kaggle competition name os.environ["KAGGLE_CONFIG_DIR"] = creds_fdir -comp_name = 'dogs-vs-cats' +comp_name = "dogs-vs-cats" download_data = True unzip_data = True del_zip = True +# set kaggle model detailes +model_instance_url="oislen/cat-classifier-cnn-models/pyTorch/default/1" + # webscraping constants n_images = 6000 -home_url = 'https://free-images.com' -output_dir = os.path.join(data_fdir, '{search}') \ No newline at end of file +home_url = "https://free-images.com" +output_dir = os.path.join(data_fdir, "{search}") \ No newline at end of file diff --git a/webscrapers/exeWebscraper.cmd b/webscrapers/exeWebscraper.cmd index 9f31194..c2de592 100644 --- a/webscrapers/exeWebscraper.cmd +++ b/webscrapers/exeWebscraper.cmd @@ -1 +1 @@ -call python -m pdb prg_scrape_imgs.py --run_download_comp_data --run_webscraper \ No newline at end of file +call python -m pdb prg_scrape_imgs.py --run_download_models --run_download_comp_data --run_webscraper \ No newline at end of file diff --git a/webscrapers/exeWebscraper.sh b/webscrapers/exeWebscraper.sh index 8ec69c9..214c488 100644 --- a/webscrapers/exeWebscraper.sh +++ b/webscrapers/exeWebscraper.sh @@ -1 +1 @@ -python -m pdb prg_scrape_imgs.py --run_download_comp_data --run_webscraper \ No newline at end of file +python -m pdb prg_scrape_imgs.py --run_download_models --run_download_comp_data --run_webscraper \ No newline at end of file diff --git a/webscrapers/prg_scrape_imgs.py b/webscrapers/prg_scrape_imgs.py index 7d45bc6..bc4abf0 100644 --- a/webscrapers/prg_scrape_imgs.py +++ b/webscrapers/prg_scrape_imgs.py @@ -2,11 +2,12 @@ from beartype import beartype import cons from utilities.commandline_interface import commandline_interface -from utilities.download_comp_data import download_comp_data +from utilities.download_comp_data import download_comp_data, download_models from utilities.webscraper import webscraper @beartype def scrape_imags( + run_download_models:bool=False, run_download_comp_data:bool=False, run_webscraper:bool=False ): @@ -14,6 +15,8 @@ def scrape_imags( Parameters ---------- + run_download_models : bool + Whether to run the download Kaggle master models, default is False run_download_comp_data : bool Whether to run the download Kaggle competition data, default is False run_webscraper : bool @@ -22,8 +25,16 @@ def scrape_imags( Returns ------- """ + if run_download_models: + logging.info("Downloading kaggle models ...") + # download competition data + download_models( + model_instance_url=cons.model_instance_url, + model_dir=cons.models_fir + ) + if run_download_comp_data: - logging.info('Downloading kaggle data ...') + logging.info("Downloading kaggle data ...") # download competition data download_comp_data( comp_name=cons.comp_name, @@ -33,25 +44,25 @@ def scrape_imags( del_zip=cons.del_zip ) if run_webscraper: - logging.info('Running cat image webscraper ...') + logging.info("Running cat image webscraper ...") # run cat webscraper webscraper( - search='cat', + search="cat", n_images=cons.n_images, home_url=cons.home_url, output_dir=cons.train_fdir ) - logging.info('Running dog image webscraper ...') + logging.info("Running dog image webscraper ...") # run dog webscraper webscraper( - search='dog', + search="dog", n_images=cons.n_images, home_url=cons.home_url, output_dir=cons.train_fdir ) # if running as main programme -if __name__ == '__main__': +if __name__ == "__main__": # set up logging lgr = logging.getLogger() @@ -62,6 +73,6 @@ def scrape_imags( # run the scrape images programme scrape_imags( - run_download_comp_data=input_params_dict['run_download_comp_data'], - run_webscraper=input_params_dict['run_webscraper'] + run_download_comp_data=input_params_dict["run_download_comp_data"], + run_webscraper=input_params_dict["run_webscraper"] ) \ No newline at end of file diff --git a/webscrapers/utilities/commandline_interface.py b/webscrapers/utilities/commandline_interface.py index a2abe26..df55c0e 100644 --- a/webscrapers/utilities/commandline_interface.py +++ b/webscrapers/utilities/commandline_interface.py @@ -15,6 +15,7 @@ def commandline_interface(): # define argument parser object parser = argparse.ArgumentParser(description="Execute Webscrapers.") # add input arguments + parser.add_argument("--run_download_models", action=argparse.BooleanOptionalAction, dest="run_download_models", type=bool, default=False, help="Boolean, whether to run the download master Kaggle models, default is False",) parser.add_argument("--run_download_comp_data", action=argparse.BooleanOptionalAction, dest="run_download_comp_data", type=bool, default=False, help="Boolean, whether to run the download Kaggle competition data, default is False",) parser.add_argument("--run_webscraper", action=argparse.BooleanOptionalAction, dest="run_webscraper", type=bool, default=False, help="Boolean, whether to run the image webscraper, default is False",) # create an output dictionary to hold the results @@ -22,6 +23,7 @@ def commandline_interface(): # extract input arguments args = parser.parse_args() # map input arguments into output dictionary + input_params_dict["run_download_models"] = args.run_download_models input_params_dict["run_download_comp_data"] = args.run_download_comp_data input_params_dict["run_webscraper"] = args.run_webscraper return input_params_dict diff --git a/webscrapers/utilities/download_comp_data.py b/webscrapers/utilities/download_comp_data.py index baed2ba..c46b524 100644 --- a/webscrapers/utilities/download_comp_data.py +++ b/webscrapers/utilities/download_comp_data.py @@ -43,43 +43,43 @@ def download_comp_data( ) """ - logging.info('create zip file path ...') + logging.info("create zip file path ...") # define filenames - zip_data_fname = f'{comp_name}.zip' + zip_data_fname = f"{comp_name}.zip" # create file paths zip_data_fpath = os.path.join(data_dir, zip_data_fname) - zip_train_fpath = os.path.join(data_dir, 'train.zip') - zip_test_fpath = os.path.join(data_dir, 'test1.zip') + zip_train_fpath = os.path.join(data_dir, "train.zip") + zip_test_fpath = os.path.join(data_dir, "test1.zip") # combine paths in a list zip_fpaths_list = [zip_data_fpath, zip_train_fpath, zip_test_fpath] - logging.info('checking for data directory ...') + logging.info("checking for data directory ...") # check data directory exists if os.path.exists(data_dir) == False: os.makedirs(data_dir) else: - logging.info(f'data directory exists: {data_dir}') + logging.info(f"data directory exists: {data_dir}") # if downloading the data if download_data == True: - logging.info('downing kaggle data ..') - kaggle_cmd = f'kaggle competitions download --competition {comp_name} --path {data_dir} --force' + logging.info("downing kaggle data ..") + kaggle_cmd = f"kaggle competitions download --competition {comp_name} --path {data_dir} --force" subprocess.run(kaggle_cmd.split()) # if unzipping the data if unzip_data == True: if os.path.exists(zip_data_fpath) == False: - raise OSError(f'file not found: {zip_data_fpath}') + raise OSError(f"file not found: {zip_data_fpath}") else: for zip_fpath in zip_fpaths_list: - logging.info(f'unzipping data {zip_fpath} ...') + logging.info(f"unzipping data {zip_fpath} ...") with zipfile.ZipFile(zip_fpath, "r") as zip_ref: zip_ref.extractall(data_dir) # if deleting zip file if del_zip == True: for zip_fpath in zip_fpaths_list: - logging.info('deleting zip file {zip_fpath} ...') + logging.info("deleting zip file {zip_fpath} ...") os.remove(path = zip_fpath) @beartype @@ -108,14 +108,14 @@ def download_models( ) """ - logging.info('checking for data directory ...') + logging.info("checking for data directory ...") # check data directory exists if os.path.exists(model_dir) == False: os.makedirs(model_dir) else: - logging.info(f'model directory exists: {model_dir}') + logging.info(f"model directory exists: {model_dir}") # downloading the model - logging.info('downloading kaggle model ..') - kaggle_cmd = f'kaggle models instances versions download --path {model_dir} --untar --force {model_instance_url}' + logging.info("downloading kaggle model ..") + kaggle_cmd = f"kaggle models instances versions download --path {model_dir} --untar --force {model_instance_url}" subprocess.run(kaggle_cmd.split()) From 18c131b1c7d22fe2f6e7942971b948325ac47e30 Mon Sep 17 00:00:00 2001 From: Oisin Date: Tue, 11 Feb 2025 19:00:15 +0000 Subject: [PATCH 11/11] Removed run_download_models step. Added run_download_models step to scrape images programme --- webscrapers/exeWebscraper.cmd | 2 +- webscrapers/exeWebscraper.sh | 2 +- webscrapers/prg_scrape_imgs.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/webscrapers/exeWebscraper.cmd b/webscrapers/exeWebscraper.cmd index c2de592..9f31194 100644 --- a/webscrapers/exeWebscraper.cmd +++ b/webscrapers/exeWebscraper.cmd @@ -1 +1 @@ -call python -m pdb prg_scrape_imgs.py --run_download_models --run_download_comp_data --run_webscraper \ No newline at end of file +call python -m pdb prg_scrape_imgs.py --run_download_comp_data --run_webscraper \ No newline at end of file diff --git a/webscrapers/exeWebscraper.sh b/webscrapers/exeWebscraper.sh index 214c488..8ec69c9 100644 --- a/webscrapers/exeWebscraper.sh +++ b/webscrapers/exeWebscraper.sh @@ -1 +1 @@ -python -m pdb prg_scrape_imgs.py --run_download_models --run_download_comp_data --run_webscraper \ No newline at end of file +python -m pdb prg_scrape_imgs.py --run_download_comp_data --run_webscraper \ No newline at end of file diff --git a/webscrapers/prg_scrape_imgs.py b/webscrapers/prg_scrape_imgs.py index bc4abf0..1230038 100644 --- a/webscrapers/prg_scrape_imgs.py +++ b/webscrapers/prg_scrape_imgs.py @@ -73,6 +73,7 @@ def scrape_imags( # run the scrape images programme scrape_imags( + run_download_models=input_params_dict["run_download_models"], run_download_comp_data=input_params_dict["run_download_comp_data"], run_webscraper=input_params_dict["run_webscraper"] ) \ No newline at end of file