{"cells":[{"metadata":{},"cell_type":"markdown","source":"Hi folks !! This is my first kernel.\n\nThis kernel is about fast preprocessing of images using some of the most popular techniques being used in this competition like :\n\n* Ben's Preprocessing Technique\n* Tight crop to remove black edges\n* Boundary cropping\n* Circle Crop\n* Image resizing\n\nI've implemented both parallel processing and the good-old sequential flow. Feel free to use whichever !! If you have any suggestions for my code, please let me know ..."},{"metadata":{"trusted":true},"cell_type":"code","source":"import multiprocessing\nfrom tqdm import tqdm\nimport numpy as np\nimport glob\nimport cv2\nimport sys\nimport os\n\n\nclass ImagePreprocessor(object):\n    def __init__(self, root_dir: str, save_dir: str, img_size: int, tolerance: int = 10, remove_outer_pixels: float = 0.0):\n        \"\"\"\n        Preprocess images for kaggle competitions and general training purposes.\n\n        args:\n            root_dir  = absolute path to images folder\n            save_dir  = folder in which to store processed images\n            img_size  = final image dimensions, common values : 224, 512\n            tolerance = tolerance value for pitch_black_remover func\n            remove_outer_pixels = remove boundary pixels of image\n        \"\"\"\n        if remove_outer_pixels > 0.50:\n            print(\"ERROR: eroding more than 50% of image\")\n            raise InterruptedError\n\n        self.root_dir = root_dir\n        self.img_size = img_size\n        self.tolerance = tolerance\n        self.remove_outer_pixels = remove_outer_pixels\n\n        self.images = glob.glob(f\"{self.root_dir}/*.png\") + glob.glob(\n            f\"{self.root_dir}/*.jpeg\") + glob.glob(f\"{self.root_dir}/*.jpg\")\n        self.save_dir = os.path.join(root_dir, save_dir)\n        os.makedirs(self.save_dir, exist_ok=True)\n        self.total_count = len(self.images)\n\n    # counter decorator\n    @staticmethod\n    def _counter(func):\n        def wrapper(self, *args, **kwargs):\n            func(self, *args, **kwargs)\n        return wrapper\n\n    # different preprocessing methods\n\n    @staticmethod\n    def light_sensitivity_reducer(img: np.ndarray, alpha: int = 4, beta: int = -4, gamma: int = 128):\n        \"\"\"smooth image and apply ben's preprocessing\"\"\"\n        return cv2.addWeighted(img, alpha, cv2.GaussianBlur(img, (0, 0), 10), beta, gamma)\n\n    @staticmethod\n    def outer_pixels_remover(img: np.ndarray, scale: float):\n        \"\"\"remove outer/boundary pixels of image\"\"\"\n        scale_2 = scale / 2.0\n        miny = int(img.shape[0]*scale_2)\n        maxy = int(img.shape[0]-miny)\n        minx = int(img.shape[1]*scale_2)\n        maxx = int(img.shape[1]-minx)\n        return img[miny:maxy, minx:maxx]\n\n    @staticmethod\n    def scale_image(img: np.ndarray, img_size: int):\n        \"\"\"resize image based on given scale\"\"\"\n        return cv2.resize(img, (img_size, img_size))\n\n    @staticmethod\n    def pitch_black_remover(img: np.ndarray, tolerance: int = 10):\n        \"\"\"remove black pixels in image edges\"\"\"\n        if img.ndim == 2:\n            img_mask = img > tolerance\n            return img[np.ix_(img_mask.any(1), img_mask.any(0))]\n        elif img.ndim == 3:\n            greyed = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n            img_mask = greyed > tolerance\n            img_1 = img[:, :, 0][np.ix_(img_mask.any(1), img_mask.any(0))]\n            if img_1.shape[0] == 0:\n                return img\n            img_2 = img[:, :, 1][np.ix_(img_mask.any(1), img_mask.any(0))]\n            img_3 = img[:, :, 2][np.ix_(img_mask.any(1), img_mask.any(0))]\n            return np.stack([img_1, img_2, img_3], axis=-1)\n        else:\n            print(\"Image has more than 3 dimensions\")\n            raise InterruptedError\n\n    # collaging different methods together and preprocessing images\n\n    def replace_existing(self):\n        import shutil\n        shutil.rmtree(self.root_dir)\n        os.makedirs(self.root_dir)\n        processed_images = [i.path for i in os.scandir(\n            self.save_dir) if i.is_file()]\n        processor_pool = multiprocessing.Pool(64)\n        for counter, _ in enumerate(processor_pool.imap_unordered(lambda x: shutil.move(x, self.root_dir), processed_images), 1):\n            sys.stdout.write(\n                f\"\\rMoving : {(counter/self.total_count)*100:3.2f}% \\t[ {counter}/{self.total_count} ]\")\n        os.rmdir(self.save_dir)\n        sys.stdout.write(\"\\n\\n\")\n\n    def forward(self, image: str):\n        \"\"\"take a single image path, preprocesse image and store preprocessed image\"\"\"\n        img = cv2.imread(image)\n        img = self.pitch_black_remover(img, tolerance=self.tolerance)\n        img = self.scale_image(img, img_size=self.img_size)\n        img = self.light_sensitivity_reducer(img)\n        if self.remove_outer_pixels > 0.0:\n            img = self.outer_pixels_remover(img, self.remove_outer_pixels)\n        cv2.imwrite(os.path.join(self.save_dir, image.split('/')[-1]), img)\n\n    def run(self, replace: bool = False):\n        \"\"\"process all images in root_dir in an iterative way\"\"\"\n        for image in tqdm(self.images):\n            # add logging if required\n            self.forward(image)\n        if replace:\n            self.replace_existing()\n\n    def parallel_run(self, workers: int = multiprocessing.cpu_count(), replace: bool = False):\n        \"\"\"process all images in root_dir parallely using python's multiprocessing library\"\"\"\n        # haven't figured out a stable way for logging in case of multiprocessing\n        processor_pool = multiprocessing.Pool(workers)\n\n        for counter, _ in enumerate(processor_pool.imap_unordered(self.forward, self.images), 1):\n            sys.stdout.write(\n                f\"\\rProgress : {(counter/self.total_count)*100:3.2f}% \\t[ {counter}/{self.total_count} ]\")\n        if replace:\n            self.replace_existing()\n        sys.stdout.write(\"\\n\\n\")\n\n\nif __name__ == \"__main__\":\n    if True:\n        sys.exit(1)\n    # remove the above two lines\n        \n    # arguments\n    import argparse\n    import multiprocessing\n\n    parser = argparse.ArgumentParser()\n    parser.add_argument('-root_dir', required=True, type=str)\n    parser.add_argument('-save_dir', required=True, type=str)\n    parser.add_argument('-img_size', required=True, type=int)\n    parser.add_argument('-tolerance', type=int, default=10)\n    parser.add_argument('-remove_outer_pixels', type=float, default=0.0)\n    parser.add_argument('-parallel', default=False, action='store_true')\n    parser.add_argument('-workers', type=int,\n                        default=multiprocessing.cpu_count())\n    parser.add_argument('-replace', default=False, action='store_true')\n    args = parser.parse_args()\n    #\n\n    preprocessor = ImagePreprocessor(root_dir=args.root_dir, save_dir=args.save_dir, img_size=args.img_size,\n                                     tolerance=args.tolerance, remove_outer_pixels=args.remove_outer_pixels)\n    if args.parallel:\n        preprocessor.parallel_run(args.workers, args.replace)\n    else:\n        preprocessor.run(args.replace)\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Big shoutout to @taindow, @tanlikesmath, @ratthachat for their kernels from which I've collated the above code. For more information and in-depth analysis of these techniques, Read :\n\n* https://www.kaggle.com/taindow/pre-processing-train-and-test-images\n* https://www.kaggle.com/tanlikesmath/intro-aptos-diabetic-retinopathy-eda-starter\n* https://www.kaggle.com/ratthachat/aptos-updatedv14-preprocessing-ben-s-cropping"},{"metadata":{},"cell_type":"markdown","source":"The above boilerplate can be used for any competition or otherwise. You only need to implement the preprocessing method code as a `function` and call that function from `forward` method."}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.4","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}