{"cells":[{"metadata":{"trusted":true},"cell_type":"code","source":"%%writefile model.py\n\n\"\"\"\nPython script to prepare FasterRCNN model.\n\"\"\"\n\nimport torch\nimport torchvision\n\nfrom torchvision.models.detection.faster_rcnn import FastRCNNPredictor\nfrom torchvision.models.detection import  FasterRCNN\nfrom torchvision.models.detection.rpn import AnchorGenerator\n\ndef model():\n    # load the COCO pre-trained model\n    # we will keep the image size to 1024 pixels instead of the original 800,\n    # this will ensure better training and testing results, although it may...\n    # ... increase the training time (a tarde-off)\n    model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=True, \n                                                                 min_size=1024)\n    # one class is pneumonia, and the other is background\n    num_classes = 2\n    # get the input features for the classifier\n    in_features = model.roi_heads.box_predictor.cls_score.in_features\n    # replace pre-trained head with our features head\n    # the head layer will classify the images based on our data input features\n    model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)\n\n    return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%writefile dataset.py\n\n\"\"\"\nPython script to prepare the dataset\n\"\"\"\n\nimport numpy as np\nimport cv2\nimport re\nimport torch\n\nfrom torch.utils.data import Dataset\n\nclass RSNADataset(Dataset):\n    def __init__(self, dataframe, image_dir, transforms=None):\n        super().__init__()\n\n        self.image_ids = dataframe['patientId'].unique()\n        self.df = dataframe\n        self.image_dir = image_dir\n        self.transforms = transforms\n        \n    def __getitem__(self, index: int):\n\n        image_id = self.image_ids[index]\n        records = self.df[self.df['patientId'] == image_id]\n\n        image = cv2.imread(f'{self.image_dir}/{image_id}.jpg', cv2.IMREAD_COLOR)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        image /= 255.0\n\n        boxes = records[['x', 'y', 'width', 'height']].values\n        boxes[:, 2] = boxes[:, 0] + boxes[:, 2]\n        boxes[:, 3] = boxes[:, 1] + boxes[:, 3]\n        \n        area = (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0])\n        area = torch.as_tensor(area, dtype=torch.float32)\n\n        # there is only one class\n        labels = torch.ones((records.shape[0],), dtype=torch.int64)\n        \n        # suppose all instances are not crowd\n        iscrowd = torch.zeros((records.shape[0],), dtype=torch.int64)\n        \n        target = {}\n        target['boxes'] = boxes\n        target['labels'] = labels\n        # target['masks'] = None\n        target['patientId'] = torch.tensor([index])\n        target['area'] = area\n        target['iscrowd'] = iscrowd\n\n        if self.transforms:\n            sample = {\n                'image': image,\n                'bboxes': target['boxes'],\n                'labels': labels\n            }\n            sample = self.transforms(**sample)\n            image = sample['image']\n            \n            target['boxes'] = torch.stack(tuple(map(torch.FloatTensor, zip(*sample['bboxes'])))).permute(1, 0)\n\n        return image, target, image_id\n\n    def __len__(self) -> int:\n        return self.image_ids.shape[0]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%writefile engine.py\n\nimport pandas as pd\nimport dataset\nimport albumentations as A\nimport time\nimport torch\nimport numpy as np\n\nfrom torch.utils.data import DataLoader\nfrom albumentations.pytorch.transforms import ToTensorV2\nfrom tqdm import tqdm\nfrom albumentations import (\n    HorizontalFlip, IAAPerspective, ShiftScaleRotate, CLAHE, RandomRotate90,\n    Transpose, ShiftScaleRotate, Blur, OpticalDistortion, GridDistortion, HueSaturationValue,\n    IAAAdditiveGaussianNoise, GaussNoise, MotionBlur, MedianBlur, IAAPiecewiseAffine,\n    IAASharpen, IAAEmboss, RandomBrightnessContrast, Flip, OneOf, Compose\n)\n\n\"\"\"\nComplete mAP code here => https://gist.github.com/tarlen5/008809c3decf19313de216b9208f3734\n\"\"\"\n\ndef calculate_image_precision(gts, preds, thresholds = (0.5, ), form = 'coco') -> float:\n    # https://www.kaggle.com/sadmanaraf/wheat-detection-using-faster-rcnn-train\n    \"\"\"Calculates image precision.\n\n    Args:\n        gts: (List[List[Union[int, float]]]) Coordinates of the available ground-truth boxes\n        preds: (List[List[Union[int, float]]]) Coordinates of the predicted boxes,\n               sorted by confidence value (descending)\n        thresholds: (float) Different thresholds\n        form: (str) Format of the coordinates\n\n    Return:\n        (float) Precision\n    \"\"\"\n    n_threshold = len(thresholds)\n    image_precision = 0.0\n    \n    ious = np.ones((len(gts), len(preds))) * -1\n    # ious = None\n\n    for threshold in thresholds:\n        precision_at_threshold = calculate_precision(gts.copy(), preds, threshold=threshold,\n                                                     form=form, ious=ious)\n        image_precision += precision_at_threshold / n_threshold\n\n    return image_precision\n\n\ndef calculate_iou(gt, pr, form='pascal_voc') -> float:\n    # https://www.kaggle.com/sadmanaraf/wheat-detection-using-faster-rcnn-train\n    \"\"\"Calculates the Intersection over Union.\n\n    Args:\n        gt: (np.ndarray[Union[int, float]]) coordinates of the ground-truth box\n        pr: (np.ndarray[Union[int, float]]) coordinates of the prdected box\n        form: (str) gt/pred coordinates format\n            - pascal_voc: [xmin, ymin, xmax, ymax]\n            - coco: [xmin, ymin, w, h]\n    Returns:\n        (float) Intersection over union (0.0 <= iou <= 1.0)\n    \"\"\"\n    if form == 'coco':\n        gt = gt.copy()\n        pr = pr.copy()\n\n        gt[2] = gt[0] + gt[2]\n        gt[3] = gt[1] + gt[3]\n        pr[2] = pr[0] + pr[2]\n        pr[3] = pr[1] + pr[3]\n\n    # Calculate overlap area\n    dx = min(gt[2], pr[2]) - max(gt[0], pr[0]) + 1\n    \n    if dx < 0:\n        return 0.0\n    dy = min(gt[3], pr[3]) - max(gt[1], pr[1]) + 1\n\n    if dy < 0:\n        return 0.0\n\n    overlap_area = dx * dy\n\n    # Calculate union area\n    union_area = (\n            (gt[2] - gt[0] + 1) * (gt[3] - gt[1] + 1) +\n            (pr[2] - pr[0] + 1) * (pr[3] - pr[1] + 1) -\n            overlap_area\n    )\n\n    return overlap_area / union_area\n\n\ndef find_best_match(gts, pred, pred_idx, threshold = 0.5, form = 'pascal_voc', ious=None) -> int:\n    # https://www.kaggle.com/sadmanaraf/wheat-detection-using-faster-rcnn-train\n    \"\"\"Returns the index of the 'best match' between the\n    ground-truth boxes and the prediction. The 'best match'\n    is the highest IoU. (0.0 IoUs are ignored).\n\n    Args:\n        gts: (List[List[Union[int, float]]]) Coordinates of the available ground-truth boxes\n        pred: (List[Union[int, float]]) Coordinates of the predicted box\n        pred_idx: (int) Index of the current predicted box\n        threshold: (float) Threshold\n        form: (str) Format of the coordinates\n        ious: (np.ndarray) len(gts) x len(preds) matrix for storing calculated ious.\n\n    Return:\n        (int) Index of the best match GT box (-1 if no match above threshold)\n    \"\"\"\n    best_match_iou = -np.inf\n    best_match_idx = -1\n    for gt_idx in range(len(gts)):\n        \n        if gts[gt_idx][0] < 0:\n            # Already matched GT-box\n            continue\n        \n        iou = -1 if ious is None else ious[gt_idx][pred_idx]\n\n        if iou < 0:\n            iou = calculate_iou(gts[gt_idx], pred, form=form)\n            \n            if ious is not None:\n                ious[gt_idx][pred_idx] = iou\n\n        if iou < threshold:\n            continue\n\n        if iou > best_match_iou:\n            best_match_iou = iou\n            best_match_idx = gt_idx\n\n    return best_match_idx\n\ndef calculate_precision(gts, preds, threshold = 0.5, form = 'coco', ious=None) -> float:\n    # https://www.kaggle.com/sadmanaraf/wheat-detection-using-faster-rcnn-train\n    \"\"\"Calculates precision for GT - prediction pairs at one threshold.\n\n    Args:\n        gts: (List[List[Union[int, float]]]) Coordinates of the available ground-truth boxes\n        preds: (List[List[Union[int, float]]]) Coordinates of the predicted boxes,\n               sorted by confidence value (descending)\n        threshold: (float) Threshold\n        form: (str) Format of the coordinates\n        ious: (np.ndarray) len(gts) x len(preds) matrix for storing calculated ious.\n\n    Return:\n        (float) Precision\n    \"\"\"\n    n = len(preds)\n    tp = 0\n    fp = 0\n    \n    for pred_idx in range(n):\n\n        best_match_gt_idx = find_best_match(gts, preds[pred_idx], pred_idx,\n                                            threshold=threshold, form=form, ious=ious)\n\n        if best_match_gt_idx >= 0:\n            # True positive: The predicted box matches a gt box with an IoU above the threshold.\n            tp += 1\n            # Remove the matched GT box\n            gts[best_match_gt_idx] = -1\n        else:\n            # No match\n            # False positive: indicates a predicted box had no associated gt box.\n            fp += 1\n\n    # False negative: indicates a gt box had no associated predicted box.\n    fn = (gts.sum(axis=1) > 0).sum()\n\n    return tp / (tp + fp + fn)\n\n\n# Albumentations\ndef get_train_transform():\n    return A.Compose([\n        A.Flip(0.5),\n        A.RandomRotate90(0.5),\n        MotionBlur(p=0.2),\n        MedianBlur(blur_limit=3, p=0.1),\n        Blur(blur_limit=3, p=0.1),\n        ToTensorV2(p=1.0)\n    ], bbox_params={'format': 'pascal_voc', 'label_fields': ['labels']})\n\ndef get_valid_transform():\n    return A.Compose([\n        ToTensorV2(p=1.0)\n    ], bbox_params={'format': 'pascal_voc', 'label_fields': ['labels']})\n\ndef collate_fn(batch):\n    return tuple(zip(*batch))\n\ndef prepare_data():\n    DIR_INPUT = '../input/rsna-pneumonia-detection-2018/input'\n    DIR_TRAIN = f\"{DIR_INPUT}/images/\"\n\n    train_df = pd.read_csv(f\"{DIR_INPUT}/stage_2_train_labels.csv\")\n    print(train_df.shape)\n    train_df.head()\n\n    train_df_pos = pd.DataFrame(columns=['patientId', 'x', 'y', 'width', 'height'])\n\n    k = 0\n    for i in range(len(train_df)):\n        if train_df.loc[i]['Target'] == 1:\n            train_df_pos.loc[k] = train_df.loc[i]\n            k += 1\n\n    image_ids = train_df_pos['patientId'].unique()\n    valid_ids = image_ids[-300:]\n    train_ids = image_ids[:-300]\n    print(f\"Training instance: {len(train_ids)}\")\n    print(f\"Validation instances: {len(valid_ids)}\")\n\n    valid_df = train_df_pos[train_df_pos['patientId'].isin(valid_ids)]\n    train_df = train_df_pos[train_df_pos['patientId'].isin(train_ids)]\n\n    valid_df.shape, train_df.shape\n    \n    train_dataset = dataset.RSNADataset(train_df, DIR_TRAIN, get_train_transform())\n    valid_dataset = dataset.RSNADataset(valid_df, DIR_TRAIN, get_valid_transform())\n    \n    return train_dataset, valid_dataset\n    \ndef get_data_loader(batch_size):\n    \n    train_dataset, valid_dataset = prepare_data()\n    \n    train_data_loader = DataLoader(\n        train_dataset,\n        batch_size=batch_size,\n        shuffle=False,\n        num_workers=4, # else showing broken pipe error\n        collate_fn=collate_fn\n    )\n\n    valid_data_loader = DataLoader(\n        valid_dataset,\n        batch_size=batch_size,\n        shuffle=False,\n        num_workers=4, # else showing broken pipe error\n        collate_fn=collate_fn\n    )\n    return train_data_loader, valid_data_loader\n\nclass Averager:\n    def __init__(self):\n        self.current_total = 0.0\n        self.iterations = 0.0\n\n    def send(self, value):\n        self.current_total += value\n        self.iterations += 1\n\n    @property\n    def value(self):\n        if self.iterations == 0:\n            return 0\n        else:\n            return 1.0 * self.current_total / self.iterations\n\n    def reset(self):\n        self.current_total = 0.0\n        self.iterations = 0.0\n        \ndef train(dataloader, lr_scheduler, model, optimizer, \n          device, epoch, loss_hist, itr):\n    model.train()\n    start = time.time()\n    loss_hist.reset()\n    for images, targets, image_ids in dataloader:\n        \n        images = list(image.to(device) for image in images)\n        targets = [{k: v.to(device) for k, v in t.items()} for t in targets]\n\n        loss_dict = model(images, targets)\n\n\n        losses = sum(loss for loss in loss_dict.values())\n        loss_value = losses.item()\n\n        loss_hist.send(loss_value)\n\n        optimizer.zero_grad()\n        losses.backward()\n        optimizer.step()\n\n        if itr % 50 == 0:\n            print(f\"Epoch #{epoch} iteration #{itr} loss: {loss_value}\")\n\n        itr += 1\n    \n    end = time.time()\n    return loss_hist, end, start\n\ndef validate(dataloader, model, device, iou_thresholds):\n    valid_image_precision = []\n    model.eval()\n    with torch.no_grad():\n        for images, targets, image_ids in dataloader:\n\n            images = list(image.to(device) for image in images)\n            targets = [{k: v.to(device) for k, v in t.items()} for t in targets]\n\n            outputs = model(images)\n            \n    for i, image in enumerate(images):\n        boxes = outputs[i]['boxes'].data.cpu().numpy()\n        scores = outputs[i]['scores'].data.cpu().numpy()\n        gt_boxes = targets[i]['boxes'].cpu().numpy()\n        preds_sorted_idx = np.argsort(scores)[::-1]\n        preds_sorted = boxes[preds_sorted_idx]\n        image_precision = calculate_image_precision(preds_sorted,\n                                                        gt_boxes,\n                                                        thresholds=iou_thresholds,\n                                                        form='coco')\n        valid_image_precision.append(image_precision)\n\n    valid_prec = np.mean(valid_image_precision)\n    return valid_prec","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"%%writefile train.py\n\nimport torch\nimport engine\nimport matplotlib\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport argparse\nimport cv2\n\nfrom engine import get_data_loader, Averager, train, validate\nfrom model import model\n# from torch.utils.data.sampler import SequentialSampler\n\nmatplotlib.style.use('ggplot')\n\ndevice = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')\n\nparser = argparse.ArgumentParser()\nparser.add_argument('-s', '--show-sample', dest='show_sample', default='no', \n                 help='whether to visualize a wheat sample with bboxes or not')\nargs = vars(parser.parse_args())\n\n# learning parameters\nnum_epochs = 30\nlr = 0.001\nbatch_size = 8\n\nmodel = model().to(device)\nparams = [p for p in model.parameters() if p.requires_grad]\noptimizer = torch.optim.SGD(params, lr=lr, momentum=0.9, weight_decay=0.0005)\n# optimizer = torch.optim.Adam(params, lr=0.01)\n# lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)\nlr_scheduler = None\n\n# initialize the Averager\nloss_hist = engine.Averager()\n# get the dataloader\ntrain_data_loader, valid_data_loader = get_data_loader(batch_size)\n\nif args['show_sample'] == 'yes':\n    images, targets, image_ids = next(iter(train_data_loader))\n    images = list(image.to(device) for image in images)\n    targets = [{k: v.to(device) for k, v in t.items()} for t in targets]\n    boxes = targets[2]['boxes'].cpu().numpy().astype(np.int32)\n    sample = images[2].permute(1,2,0).cpu().numpy()\n    fig, ax = plt.subplots(1, 1, figsize=(16, 8))\n\n    for box in boxes:\n        cv2.rectangle(sample,\n                      (box[0], box[1]),\n                      (box[2], box[3]),\n                      (220, 0, 0), 3)\n    \n    ax.set_axis_off()\n    ax.imshow(sample)\n    plt.show()\n\niou_thresholds = [x for x in np.arange(0.5, 0.76, 0.05)]\n\ntrain_loss = []\nprecision = []\nfor epoch in range(num_epochs):\n    itr = 1\n    train_loss_hist, end, start = train(train_data_loader, lr_scheduler,\n                                        model, optimizer, device,\n                                        epoch, loss_hist, itr)\n    valid_prec = validate(valid_data_loader, model, device, iou_thresholds)\n    print(f\"Took {(end-start)/60:.3f} minutes for epoch# {epoch} to train\")\n    print(f\"Epoch #{epoch} Train loss: {train_loss_hist.value}\")  \n    print(f\"Epoch #{epoch} Validation Precision: {valid_prec}\")  \n    train_loss.append(train_loss_hist.value)\n    precision.append(valid_prec)\n    \n    # update the learning rate\n    if lr_scheduler is not None:\n        lr_scheduler.step()\n\ntorch.save(model.state_dict(), 'fasterrcnn_resnet50_fpn.pth')\n\n# plot and save the training loss\nplt.figure()\nplt.plot(train_loss, label='Training loss')\nplt.legend()\nplt.show()\nplt.savefig('loss.png')\n\n# plot and save the validation precision\nplt.figure()\nplt.plot(precision, label='Validation precision')\nplt.legend()\nplt.show()\nplt.savefig('precision.png')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"!python train.py --show-sample yes","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}