{"metadata":{"kernelspec":{"display_name":"Python 3 (ipykernel)","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.9.14"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":71698,"databundleVersionId":7906362,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Sample submission based on 3D U-Net as a baseline\n\nIn this notebook, we'll go through a full attempt at a solution - albeit a poorly performing one.\n\n**Full disclosure:** this notebook was not developed on Kaggle, but on another machine. It may not work out of the box but hopefully, it can serve as a useful starting point for now.\n\nThe idea is to train a 3D U-Net to segment the bugs based on the single bug images. Here, there is only one bug per image, so we can use the 3D U-Net to segment the bug. Since we know the label of the bugs in the single bug images, we can also train the U-Net to predict the bug type, by casting it as a multi-class segmentation problem (13 classes, 12 bugs + background). We can then use the segmentation to find its center point and use that for our submission.","metadata":{}},{"cell_type":"code","source":"import os\nimport glob\n\nimport monai\nimport numpy as np\nimport torch\nfrom tqdm import tqdm, trange","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Path to individual bugs (change to where you store the bugs)\ndata_dir = 'path/to/train'","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"First, we gather all the files and their labels to prepare the data loaders.\nWe will store the labels as one-hot encoded vectors.","metadata":{}},{"cell_type":"code","source":"images = []\nlabels = []\n\nclass_names = os.listdir(data_dir)\nclass_image_files = [\n    glob.glob(os.path.join(data_dir, name, '*')) for name in class_names\n]\n\nfor i, files in enumerate(class_image_files):\n    images += files\n\n    labels_i = torch.zeros(len(files), len(class_names))\n    labels_i[:, i] = 1\n    labels.append(labels_i)\n\n\nlabels = torch.cat(labels, dim=0)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Some samples of the lists we made:","metadata":{}},{"cell_type":"code","source":"class_names","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"images[:5]","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"labels[:5]","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Looks good! Let's split the individual images into a train and validation set:","metadata":{}},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\ntrain_val_split = 0.6\n\ntrain_images, val_images, train_labels, val_labels = train_test_split(\n    images, labels, train_size=train_val_split, stratify=labels,\n    random_state=1337,\n)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"During the training pipeline, the rough idea is this:\n\n1. Load a batch of images and their labels.\n2. Segment the (single) bug using a threshold on the image intensity.\n3. Use the label to turn the binary thresholded image into an actual segmentation mask.\n4. Train the U-Net to predict the segmentation mask.\n\nThe solution here is based on the MONAI library, which provides building blocks for 3D image segmentations. Most of the above steps can be achieved via existing MONAI transforms we can add as data augmentations. However, we need two extra helper classes.\n\nFirst, a class to tell it how to read our 3D tiff-files:","metadata":{}},{"cell_type":"code","source":"from monai.data import ImageReader\nfrom skimage.io import imread\n\nclass TiffReader(ImageReader):\n    def __init__(self):\n        super().__init__()\n\n\n    def get_data(self, img):\n        return np.asarray(img), dict()\n\n\n    def read(self, data, **kwargs):\n        if isinstance(data, str):\n            data = [data]\n\n        return [imread(f) for f in data]\n\n\n    def verify_suffix(self, filename):\n        return filename.endswith('.tif') or filename.endswith('.tiff')\n","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Next a class to copy an image","metadata":{}},{"cell_type":"code","source":"from copy import deepcopy\nfrom monai.transforms import MapTransform\n\nclass CopyImaged(MapTransform):\n    def __init__(self, key_to_copy, new_key, allow_missing_keys=False):\n        super().__init__(key_to_copy, allow_missing_keys)\n        self.key_to_copy = key_to_copy\n        self.new_key = new_key\n\n\n    def __call__(self, data):\n        data = dict(data)\n        data[self.new_key] = deepcopy(data[self.key_to_copy])\n        return data\n","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can now set up our transforms:","metadata":{}},{"cell_type":"code","source":"import monai.transforms as T\nfrom skimage.io import imread\n\n# Precompute padding to get equal side lengths\nim0 = imread(images[0])\nshape = torch.tensor(im0.shape)\npadding = (0, int((shape[0] - shape[1]) // 2), int((shape[0] - shape[2]) // 2))\n\n# Prepare train and test data loaders\nkeys = ['image', 'mask']\ntrain_transforms = T.Compose([\n    # Load the image\n    T.LoadImaged(keys='image', reader=TiffReader, image_only=True),\n    T.Resized(keys='image', spatial_size=shape),\n    # Scale the intensity\n    T.ScaleIntensityd(keys='image'),\n    # Pad the image so all sides are equal\n    T.BorderPadd(keys='image', spatial_border=padding),\n    # Make a copy of the image, which is what will later be our mask\n    CopyImaged(key_to_copy='image', new_key='mask'),\n    # Smooth the image and then threshold it\n    T.GaussianSmoothd(keys='mask', sigma=2),\n    T.AsDiscreted(keys='mask', threshold=0.25, dtype=torch.long),\n    # Only keep the largest connected component as the bug mask\n    T.KeepLargestConnectedComponentd(keys='mask', applied_labels=[0]),\n    # Apply normal data augmentations\n    T.EnsureTyped(keys=['image', 'mask', 'label'], track_meta=False),\n    T.RandAffined(\n        keys=['image', 'mask'], prob=0.95, rotate_range=(np.pi/2,) * 3,\n        translate_range=shape // torch.tensor([2, 1, 1]), padding_mode='zeros'),\n    T.RandAxisFlipd(keys=keys, prob=0.5),\n    T.RandScaleIntensityd(keys='image', factors=0.25, prob=0.5),\n    T.RandZoomd(keys=keys, prob=0.5),\n    T.SqueezeDimd(keys='mask'),\n    T.CastToTyped(keys='mask', dtype=torch.long),\n])\n\nval_transforms = T.Compose([\n    # See train_transforms\n    T.LoadImaged(keys='image', reader=TiffReader, image_only=True),\n    T.Resized(keys='image', spatial_size=shape),\n    T.ScaleIntensityd(keys='image'),\n    T.BorderPadd(keys='image', spatial_border=padding),\n    CopyImaged(key_to_copy='image', new_key='mask'),\n    T.GaussianSmoothd(keys='mask', sigma=2),\n    T.AsDiscreted(keys='mask', threshold=0.25, dtype=torch.long),\n    T.KeepLargestConnectedComponentd(keys='mask', applied_labels=[0]),\n    T.EnsureTyped(keys=['image', 'mask', 'label'], track_meta=False),\n    T.SqueezeDimd(keys='mask'),\n    T.CastToTyped(keys='mask', dtype=torch.long),\n])","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's quickly run through how this looks like when the code runs.\n\nWe create our `Dataset`s","metadata":{}},{"cell_type":"code","source":"from monai.data import Dataset\n\ntrain_data = Dataset(\n    [{ 'image': f, 'label': l }\n        for f, l in zip(train_images, train_labels)],\n    transform=train_transforms,\n)\n\nval_data = Dataset(\n    [{ 'image': f, 'label': l }\n     for f, l in zip(val_images, val_labels)],\n    transform=val_transforms,\n)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"...and `DataLoaders`","metadata":{}},{"cell_type":"code","source":"from monai.data import DataLoader\n\ntrain_batch_size = 4\nval_batch_size = 2\n\ntrain_loader = DataLoader(\n    train_data,\n    shuffle=True,\n    num_workers=0,\n    batch_size=train_batch_size,\n)\nval_loader = DataLoader(\n    val_data,\n    num_workers=0,\n    shuffle=False,\n    batch_size=val_batch_size,\n)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Get a batch and look at it:","metadata":{}},{"cell_type":"code","source":"for batch in train_loader:\n    break","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"batch.keys()","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nfig, ax = plt.subplots(2, 4, figsize=(10, 5))\nfor i in range(4):\n    ax[0, i].imshow(batch['image'][i, 0].cpu().numpy().max(axis=0), cmap='gray')\n    ax[0, i].axis('off')\n    ax[1, i].imshow(batch['mask'][i].cpu().numpy().max(axis=0), cmap='tab20b', vmin=0, vmax=1)\n    ax[1, i].axis('off')\nfig.tight_layout()","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"mask_label = batch['mask'] * (batch['label'].argmax(dim=1).view(-1, 1, 1, 1) + 1)\n\nfig, ax = plt.subplots(2, 4, figsize=(10, 5))\nfor i in range(4):\n    ax[0, i].imshow(batch['image'][i, 0].cpu().numpy().max(axis=0), cmap='gray')\n    ax[0, i].axis('off')\n    ax[1, i].imshow(mask_label[i].cpu().numpy().max(axis=0), cmap='tab20b',\n                    vmin=0, vmax=len(class_names), interpolation='none')\n    ax[1, i].axis('off')\nfig.tight_layout()","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Alright, now for real:","metadata":{}},{"cell_type":"code","source":"train_batch_size = 4\nval_batch_size = 8\n\ntrain_loader = DataLoader(\n    train_data,\n    shuffle=True,\n    num_workers=8,\n    batch_size=train_batch_size,\n    drop_last=True,\n)\nval_loader = DataLoader(\n    val_data,\n    num_workers=8,\n    shuffle=False,\n    batch_size=val_batch_size,\n    drop_last=True,\n)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We use the built in U-Net implementation from MONAI:","metadata":{}},{"cell_type":"code","source":"model = monai.networks.nets.UNet(\n    spatial_dims=3,\n    in_channels=1,\n    out_channels=len(class_names) + 1,  # Bug classes and background\n    channels=(32, 64, 128, 256),\n    strides=(2, 2, 2),\n    num_res_units=2,\n)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"model = model.cuda()","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We use a mix of cross entropy and Dice loss. For the cross entropy loss, we weigh class 0 (background) with 0.1, since there's a lot of it.","metadata":{}},{"cell_type":"code","source":"ce_weight = torch.ones(len(class_names) + 1, device='cuda')\nce_weight[0] = 0.1\n\nloss_fn = monai.losses.DiceCELoss(\n    ce_weight=ce_weight,\n    to_onehot_y=True,\n    softmax=True,\n    include_background=True,\n)\noptimizer = torch.optim.Adam(\n    model.parameters(),\n    lr=1e-4,\n)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Run the training. This will take several hours sadly...","metadata":{}},{"cell_type":"code","source":"val_every = 1\n\ntrain_losses = []\nval_losses = []\nepoch_train_losses = []\nepoch_val_losses = []\nval_loss_mean = None\nbest_metric = float('inf')\nfor epoch in range(100):\n    model.train()\n\n    loss_mean = 0\n    num_batches = 0\n    for batch in tqdm(train_loader):\n        inputs = batch['image'].cuda()\n        label = batch['label'].argmax(dim=1).cuda()\n        expected = batch['mask'].cuda() * (label.view(-1, 1, 1, 1) + 1)\n\n        optimizer.zero_grad()\n        outputs = model(inputs)\n        loss = loss_fn(outputs, expected.unsqueeze(1))\n\n        loss.backward()\n        optimizer.step()\n\n        with torch.no_grad():\n            loss_mean += loss.item()\n            num_batches += 1\n            train_losses.append(loss.item())\n\n\n    loss_mean /= num_batches\n    epoch_train_losses.append(loss_mean)\n\n    if epoch % val_every == 0:\n        model.eval()\n\n        val_loss_mean = 0\n        num_batches = 0\n        for batch in tqdm(val_loader):\n            inputs = batch['image'].cuda()\n            label = batch['label'].argmax(dim=1).cuda()\n            expected = batch['mask'].cuda() * (label.view(-1, 1, 1, 1) + 1)\n            with torch.no_grad():\n                outputs = model(inputs)\n                loss = loss_fn(outputs, expected.unsqueeze(1))\n                val_loss_mean += loss.item()\n                num_batches += 1\n                val_losses.append(loss.item())\n\n        val_loss_mean /= num_batches\n        epoch_val_losses.append(val_loss_mean)\n\n        if val_loss_mean <= best_metric:\n            print('Saving new best model')\n            best_metric = val_loss_mean\n            torch.save(model.state_dict(), 'best_model.pth')\n\n    checkpoint = {\n        'model': model.state_dict(),\n        'optimizer': optimizer.state_dict(),\n        'epoch': epoch,\n        'train_losses': train_losses,\n        'val_losses': val_losses,\n        'epoch_train_losses': epoch_train_losses,\n        'epoch_val_losses': epoch_val_losses,\n        'best_metric': best_metric,\n    }\n    torch.save(checkpoint, 'latest_model.ckpt')\n\n    print(f\"Epoch {epoch + 1}, loss: {loss_mean:.4f}, val. loss: {val_loss_mean:.4f}\")","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"epoch","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's see the val losses","metadata":{}},{"cell_type":"code","source":"plt.plot(epoch_val_losses)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Not much and then we suddenly drop to good performance. Weird, but we'll take it for now.\n\nIt would likely be smart to look further into this - but for now, let's just use the trained model to predict the test set.","metadata":{}},{"cell_type":"markdown","source":"## Predictions on single bugs\n\nFirst, we'll try it out on the validation examples with single bugs, just as a sanity check that it works for the trained task. To do this, we'll make a helper function:","metadata":{}},{"cell_type":"code","source":"from skimage import measure\n\ndef predict(image):\n    with torch.no_grad():\n        pred = model(image)\n    pred_sm = pred.softmax(dim=1).cpu().numpy()\n    class_props = []\n    for i in range(1, 13):\n        props = measure.regionprops(\n            measure.label(pred_sm[0, i] > 0.5)\n        )\n        props = [p for p in props if p.area > 5**3]\n        class_props.append(props)\n\n    boxes = np.array([p.bbox for props in class_props for p in props])\n    centers = np.array([p.centroid for props in class_props for p in props])\n    labels = np.array([i + 1 for i, props in enumerate(class_props) for p in props])\n    scores = np.ones(len(boxes))\n    return {\n        'boxes': boxes,\n        'centers': centers,\n        'labels': labels,\n        'scores': scores,\n        'pred': pred_sm,\n    }","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Now, load an image and run a prediction:","metadata":{}},{"cell_type":"code","source":"from skimage.io import imread\n\nfname = val_images[70]\nimage = imread(fname)\nprint(fname)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"image = np.pad(image / 255.0, ((0, 0), (32, 32), (32, 32)))\npred = predict(torch.tensor(image[None, None]).float().cuda())","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"np.unique(pred['pred'][0].argmax(axis=0))","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"from matplotlib.patches import Rectangle\n\na = 1\n\n#fig, ax = plt.subplots(figsize=(10, 10))\nfig, ax = plt.subplots()\nax.imshow(image.max(axis=a), cmap='gray')\nax.imshow(pred['pred'][0].argmax(axis=0).max(axis=a), alpha=0.5, interpolation='none')\nfor box, center, label in zip(pred['boxes'], pred['centers'], pred['labels']):\n    if a == 0:\n        center = (center[2] - 0.5, center[1] - 0.5)\n        xy = (box[2] - 0.5, box[1] - 0.5)\n        height = box[4] - box[1]\n        width = box[5] - box[2]\n    elif a == 1:\n        center = (center[2] - 0.5, center[0] - 0.5)\n        xy = (box[2] - 0.5, box[0] - 0.5)\n        height = box[3] - box[0]\n        width = box[5] - box[2]\n    else:\n        center = (center[1] - 0.5, center[0] - 0.5)\n        xy = (box[1] - 0.5, box[0] - 0.5)\n        height = box[3] - box[0]\n        width = box[4] - box[1]\n    ax.add_patch(\n        Rectangle(xy, width, height, edgecolor='r', facecolor='none'))\n    ax.plot(center[0], center[1], 'ro')\n    ax.text(xy[0], xy[1] - 1, class_names[label - 1], color='r')","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Predictions on mixed volumes\n\nSatisfied it works on the single bugs, we'll now try the validation examples with a mixture of bugs:","metadata":{}},{"cell_type":"code","source":"mix_images = sorted(glob.glob('path/to/validation/*.tif'))","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fname = mix_images[10]\nimage = imread(fname)\nimage = np.pad(image / 255.0, ((0, 0), (18, 18), (18, 18)))  # Pad to even side length\nimage.shape","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plt.imshow(image.max(axis=1), cmap='gray')","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"pred = predict(torch.tensor(image[None, None]).float().cuda())","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"np.unique(pred['pred'][0].argmax(axis=0))","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"from matplotlib.patches import Rectangle\n\na = 2\n\n#fig, ax = plt.subplots(figsize=(10, 10))\nfig, ax = plt.subplots()\nax.imshow(image.max(axis=a), cmap='gray')\nax.imshow(pred['pred'][0].argmax(axis=0).max(axis=a), alpha=0.5, interpolation='none')\nfor box, center, label in zip(pred['boxes'], pred['centers'], pred['labels']):\n    if a == 0:\n        center = (center[2] - 0.5, center[1] - 0.5)\n        xy = (box[2] - 0.5, box[1] - 0.5)\n        height = box[4] - box[1]\n        width = box[5] - box[2]\n    elif a == 1:\n        center = (center[2] - 0.5, center[0] - 0.5)\n        xy = (box[2] - 0.5, box[0] - 0.5)\n        height = box[3] - box[0]\n        width = box[5] - box[2]\n    else:\n        center = (center[1] - 0.5, center[0] - 0.5)\n        xy = (box[1] - 0.5, box[0] - 0.5)\n        height = box[3] - box[0]\n        width = box[4] - box[1]\n    ax.add_patch(\n        Rectangle(xy, width, height, edgecolor='r', facecolor='none'))\n    ax.plot(center[0], center[1], 'ro')\n    ax.text(xy[0], xy[1] - 1, class_names[label - 1], color='r')","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Performance is not *great*, but it's finding something. This is to be expected when the model has only seen single bugs during training. Someone should get on that ;-)\n\nLet's now try to format our predictions in the competition format. First, we load the sample CSV file","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\nvalidation = pd.read_csv('path/to/validation/validation.csv')","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"validation.set_index('filename', inplace=True)\nvalidation","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Now, let's load the row corresponding to our current file","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\n\nfname = Path(fname)\nfname.name","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"and unpack it to get the points","metadata":{}},{"cell_type":"code","source":"val_string = validation.loc[fname.name][0]\ntrue_labels = val_string.split(';')[::4]\ntrue_centers = np.array([\n    [float(x) for x in val_string.split(';')[1::4]],\n    [float(x) for x in val_string.split(';')[2::4]],\n    [float(x) for x in val_string.split(';')[3::4]],\n]).T\n\ntrue_centers = true_centers[:, [2, 1, 0]]\ntrue_centers += np.array([[0, 18, 18]])\n\nprint(true_labels)\nprint(true_centers)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's now plot them. Importantly, we use the same plotting code as before, so we can be fairly confident that our predictions have the correct coordinates","metadata":{}},{"cell_type":"code","source":"a = 1\n\n#fig, ax = plt.subplots(figsize=(10, 10))\nfig, ax = plt.subplots()\nax.imshow(image.max(axis=a), cmap='gray')\nfor center, label in zip(true_centers, true_labels):\n    if a == 0:\n        center = (center[2] - 0.5, center[1] - 0.5)\n    elif a == 1:\n        center = (center[2] - 0.5, center[0] - 0.5)\n    else:\n        center = (center[1] - 0.5, center[0] - 0.5)\n    ax.plot(center[0], center[1], 'ro')\n    ax.text(center[0], center[1] - 1, label, color='r')","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Looks good. Let's now make a helper function that takes a list of points and packs into the format needed for a competition solution:","metadata":{}},{"cell_type":"code","source":"def pack_prediction(centers, labels):\n    centers = centers - np.array([[0, 18, 18]])\n    strings = []\n    for l, c in zip(labels, centers):\n        strings.append(f\"{l};{c[2]};{c[1]};{c[0]}\")\n    return \";\".join(strings)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Testing it out:","metadata":{}},{"cell_type":"code","source":"validation.loc[fname.name][0]","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"pack_prediction(true_centers, true_labels)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Looks good. It packs the data back to the same format.\n\nNow, we need to convert our prediction labels to the string labels used in the competition:","metadata":{}},{"cell_type":"code","source":"pred_text_labels = [class_names[l - 1].lower() for l in pred['labels']]\npred_text_labels","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"and we can now build a competition submission row for this image:","metadata":{}},{"cell_type":"code","source":"pack_prediction(pred['centers'], pred_text_labels)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's now run a prediction run on all the validation volumes","metadata":{}},{"cell_type":"code","source":"submission = {\n    'filenames': [],\n    'centerpoints': [],\n}\nfor fname in tqdm(mix_images):\n    image = imread(fname)\n    image = np.pad(image / 255.0, ((0, 0), (18, 18), (18, 18)))\n    pred = predict(torch.tensor(image[None, None]).float().cuda())\n    pred_text_labels = [class_names[l - 1].lower() for l in pred['labels']]\n    pred_text = pack_prediction(pred['centers'], pred_text_labels)\n    submission['filenames'].append(Path(fname).name)\n    submission['centerpoints'].append(pred_text)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Our submission would then be:","metadata":{}},{"cell_type":"code","source":"submission = pd.DataFrame(submission)\nsubmission","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Which matches the format from the supplied csv file:","metadata":{}},{"cell_type":"code","source":"validation","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Predictions on public test volumes\n\nFinally, we can run a prediction run for the actual test volumes to build an actual submission:","metadata":{}},{"cell_type":"code","source":"mix_images = sorted(glob.glob('path/to/test/*.tif'))","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"submission = {\n    'filename': [],\n    'centerpoints': [],\n}\nfor fname in tqdm(mix_images):\n    image = imread(fname)\n    image = np.pad(image / 255.0, ((0, 0), (18, 18), (18, 18)))\n    pred = predict(torch.tensor(image[None, None]).float().cuda())\n    pred_text_labels = [class_names[l - 1].lower() for l in pred['labels']]\n    pred_text = pack_prediction(pred['centers'], pred_text_labels)\n    submission['filename'].append(Path(fname).name)\n    submission['centerpoints'].append(pred_text)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"submission = pd.DataFrame(submission)\nsubmission","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"submission.to_csv('submission.csv', index=False)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"That's it - ready to submit!","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}}]}