{"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_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip    install qunet\n#!pip -q install torchinfo        # model structure\n\nimport os, gc, sys, time, datetime, math, random, copy, psutil, glob\nimport numpy as np,  matplotlib.pyplot as plt, pandas as pd\nfrom pathlib import Path\nimport PIL.Image as Image\nfrom   tqdm.auto import tqdm\nimport torch, torch.nn as nn\nfrom torchinfo import summary  \n\nfrom qunet   import Info, Config, Callback, Data,  MLP, Transformer, plot_histogram","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2023-05-01T14:51:15.876764Z","iopub.execute_input":"2023-05-01T14:51:15.878038Z","iopub.status.idle":"2023-05-01T14:51:31.319120Z","shell.execute_reply.started":"2023-05-01T14:51:15.877975Z","shell.execute_reply":"2023-05-01T14:51:31.317433Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"CFG = Config(\n    folder_trn  = '/kaggle/input/vesuvius-challenge-ink-detection/train/',\n    folder_tst  = '/kaggle/input/vesuvius-challenge-ink-detection/test/',        \n    \n    layer_min = 0,\n    layer_max = 5,   # !!!!\n    \n    patch_h  = 512,  # the height and width of the patches into which the image is split\n    patch_w  = 512,  \n    \n    train    = True,  # the model is trained or loaded from a dataset trained before submission\n    \n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n)\nImage.MAX_IMAGE_PIXELS = None\ninfo = Info()","metadata":{"execution":{"iopub.status.busy":"2023-05-01T14:51:31.326466Z","iopub.execute_input":"2023-05-01T14:51:31.326921Z","iopub.status.idle":"2023-05-01T14:51:31.336613Z","shell.execute_reply.started":"2023-05-01T14:51:31.326874Z","shell.execute_reply":"2023-05-01T14:51:31.334808Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Class VesuviusData\n\n1. Изображения большие: `(H,W)` от `(7606, 5249)` до `(14830, 9506)`. Для 65 срезов, в памяти они занимают`4*(65*14830*9506) = 34 Gb`. Для работы модели, необходимо разбивать изображение на патчи небольших размеров (h,w). Для агуменации эти патчи стоит вырезать из случайных мест и и делать разных размеров.\n2. При тестировании, патчи должны полностью покрывать изображение с регулярным тайлингом. Если целое число патчей не помещается, необходимы краевые полосы с перекрывающимися патчами или патчами меньших размеров. Видимо лучше вариант с перекрытием и последующим усреднением или замещением. \n\n(!) Надо не забыть попробовать TTA (несколько оаз пройти разным размером, в том числе и случайно и усреднить \"накопительную маску\").<br>\n(!) Устранение эрозии\n\n**Проблема с валидацией**\n\nПри случайном разбиении, валидация может перекрываться с тренировочными патчами. Это не очень здорово. Но и выделять кусок из  3-х картинок это не хорошо. Повидимому лучше пусть перекрываются.\n\n**Загрузка нового файла**\n\nДля загрзки будем использовать класс Callback (см. ниже иммитация Trainer).\n\nВо всех режимах запрашиваемое число патчей n_patches должно выбираться, так чтобы они поместились в память (с учётом их размеров). \nВ тренировочном и валидационном режиме после загрузки масок в тензоры датасета отправляются все n_patches патчей (т.е. reload работает в один проход).\nВ тестовом режиме, максимальное число патчей определяется размером изображения и размером патча. Поэтому запрашиваемое число патчей n_patches может оказаться меньше (что-бы в память влезли). Тогда reload будет несколько раз выбирать сформированные тайлингом патчи.","metadata":{}},{"cell_type":"code","source":"class DataManagerCallback(Callback):\n    def __init__(self, data, train, n_patches, rand=True, \n                 patch_h = 64, patch_w = 64, patch_dh = 0, patch_dw = 0,\n                 shuffle=False, batch_size=64,  whole_batch=False, \n                 layer_min=0, layer_max=65):\n        \n        self.train       = train     # тренировка (валидация) или тестирование\n        self.n_patches   = n_patches # patch count in self.data        \n        self.patch_h0    = patch_h   # patch height (среднее значение)\n        self.patch_w0    = patch_w   # patch width\n        self.patch_dh    = patch_dh  # deviations from patch_h0 (случайные отклонения от средних размеров)\n        self.patch_dw    = patch_dw  # deviations from patch_w0\n        self.rand        = rand      # random patch positions\n        \n        self.layer_min   = layer_min\n        self.layer_max   = layer_max\n        self.n_layers    = layer_max-layer_min  # число слоёв фрагмента (65)\n        assert self.n_layers <= 65, \"Wrong number of layers\"\n        \n        self.data        = data      # instance of Data\n        self.period_reload = 1       # период в эпоках с которым загружаем новый фрагмент\n        \n        self.fast        = False     # быстрое формирование патчей без контроля mask, иначе только под фрагментом\n        self.patches_pos = None      # (N,2) tensor of N pathes pos (y,x)\n        self.patch_id    = 0         # current starting patch number in patches_pos        \n        self.folder_id   = 0         # current id of folder from list self.folders:                \n        self.next_new_file = False   # при следующем вызове reload будет загружен новый фрагмент\n        \n        # В режиме тренировки и валидации используем папку CFG.folder_trn, при тестировании (submission) CFG.folder_tst        \n        self.folders = sorted(list(Path(CFG.folder_trn if train else CFG.folder_tst).glob('*')))\n        info(f\"VesuviusData: tarin={train}, {len(self.folders)} subfoders (scroll fragments)\")\n        \n        # При содании экземпляра VesuviusData данные не грузятся. \n        # Для этого необходимо вызвать метод reset. Этот же метод вызывает тренер перед началом работы fit.\n        # Внутри fit с периодичноcтью period_reload вызывается метод reload. \n\n    #---------------------------------------------------------------------------\n\n    def load_masks(self, folder, verbose=1):\n        \"\"\"\n        Upload fragment and ink mask files from folder\n        \"\"\"\n        self.files = sorted( (folder / Path(\"surface_volume/\")).glob('*.tif') )\n        assert len(self.files) == 65, f\"Wrong numer of files: {len(self.files)}\"\n        \n        fname_mask  = folder / Path(\"mask.png\")\n        self.mask = torch.from_numpy(np.array(Image.open(fname_mask).convert('1')))\n        if verbose >= 1: info(f\"loaded mask: {self.mask.shape} from {folder}\")\n        if self.train: # В режиме тестирования файла с чернилами \"inklabels.png\" нет!\n            fname_ink = folder / Path(\"inklabels.png\")\n            self.ink  = torch.from_numpy(np.array(Image.open(fname_ink) .convert('1')))                         \n            if verbose >= 1: info(f\"loaded ink : {self.ink.shape}\")\n\n        # случайные отклонения от типичного раззмера патча:\n        self.patch_h = self.patch_h0 + torch.randint(-self.patch_dh, self.patch_dh+1,(1,)).item()\n        self.patch_w = self.patch_w0 + torch.randint(-self.patch_dw, self.patch_dw+1,(1,)).item()\n        \n        # После этого метода следует вызвать get_patches_pos, который формирует список координт патчей \n        # и выделяет память для их поджмножества. Метод reload будет заполнять эту память пока список\n        # патчей не закончится. После этого загрузится следующий фрагмент и история повториться.\n            \n    #---------------------------------------------------------------------------\n\n    def get_patches_pos(self):\n        \"\"\"\n        Creates (N,2) tensor with the positions of the top-left corners of the patches.  \n        Сreate data tensors\n        Работает в двух режимах: случайные патчи и плотный тайлинг из патчей\n        Для тренировки и валидации мы используем случайно вырезанные патчи из фрагмента\n        При тестировании необходим затайлить патчами всё изображение (на краях с перекрытием)                \n        \"\"\"\n        (H,W), h,w, n = self.mask.shape, self.patch_h, self.patch_w, self.n_patches\n\n        if self.rand:  # random positions\n            if self.fast:\n                posY = torch.randint(0, H-h, (self.n_patches,1))  # patch position top-lef corner\n                posX = torch.randint(0, W-w, (self.n_patches,1))  #\n                self.patches_pos = torch.hstack([posY,posX])      # список координта патчей\n            else:\n                self.patches_pos = torch.empty((self.n_patches,2), dtype=torch.long)\n                patch_id = 0\n                while patch_id < self.n_patches:\n                    y = torch.randint(0, H-h, (1,)).item()     # patch position top-lef corner\n                    x = torch.randint(0, W-w, (1,)).item()  \n                    if self.mask[y:y+h, x:x+w].sum():          # не пустая маска\n                        self.patches_pos[patch_id, 0] = y\n                        self.patches_pos[patch_id, 1] = x\n                        patch_id += 1                        \n                    \n                \n        else:          # tiling (maybe with overlap on the right and bottom of image)\n            posY = torch.IntTensor(list(range(0,H-h,h)) + ([H-h] if H % h else []) )\n            posX = torch.IntTensor(list(range(0,W-w,w)) + ([W-w] if W % w else []) )\n            self.patches_pos = torch.cartesian_prod(posY,posX)\n            self.n_patches = min(self.n_patches, len(self.patches_pos)) # не может превышать количество в patches_pos\n                \n        self.patch_id = 0 # номер первого примера для формирования примеров для тензоров\n\n    #---------------------------------------------------------------------------\n\n    def get_patches(self, verbose):\n        \"\"\"\n        Get tensors patches.\n        Return False if all positions from patches_pos are selected        \n        \"\"\"        \n        # Берём подмножество n патчей, начиная с патча с номером patch_id\n        patches_pos = self.patches_pos[self.patch_id: self.patch_id +  self.n_patches].numpy()\n        \n        # Выделяем память для тензоров датасет. Число примеров меньше или равно числу патчей в patches_pos\n        n, h,w = len(patches_pos), self.patch_h, self.patch_w\n        self.data.data = [torch.empty( n,self.n_layers, h,w ),            # patches by layers\n                          torch.empty((n,1,h,w), dtype=self.mask.dtype ), # fragment mask\n                          torch.empty((n,1,h,w), dtype=self.mask.dtype ), # ink mask (target)\n                          torch.empty((n,2),     dtype=torch.int32 ) ]    # patch positions\n        \n        # Заполняем тензоры датасет\n        # todo: ликвидировать циклы (???)\n        self.data.data[1][:,0,:,:] = torch.vstack([self.mask[y:y+h, x:x+w].view(1,h,w) for (y,x) in patches_pos])        \n        if self.train:\n            self.data.data[2][:,0,:,:] = torch.vstack([self.ink [y:y+h, x:x+w].view(1,h,w) for (y,x) in patches_pos])\n        else:\n            self.data.data[2] = self.data.data[1]\n        self.data.data[3] = torch.vstack([torch.IntTensor([y,x]).view(1,2) for (y,x) in patches_pos])\n\n        # грузим по очереди изобаржения срезов и каждый из них разбиваем на патчи с координатами из patches_pos\n        for d in range(self.layer_min,  self.layer_max):            \n            image = torch.tensor( np.array(Image.open(self.files[d] ), dtype=np.float32) / 65535.0 )\n            assert self.mask.shape == image.shape\n\n            patches = torch.vstack([image[y:y+h, x:x+w].view(1,h,w) for (y,x) in patches_pos])\n            self.data.data[0][:, d-self.layer_min, :, :] = patches\n\n            if verbose >= 2: print(f\"\\rdepth:{d:2d}, {self.data.data[0].shape};  {self.data.data[0].is_contiguous()}\", end =\"    \")\n                \n        self.patch_id += self.n_patches\n        if verbose >= 2: print(f\" layers loaded, patch_id={self.patch_id}.\")\n        \n        return self.patch_id >= len(self.patches_pos)  # True, если список self.patches_pos закончился\n        \n    #---------------------------------------------------------------------------\n\n    def reload(self, train=True, epoch=0, hist=Config(), best=Config(), verbose=1):\n        \"\"\"\n        Called in the trainer.fit() function, every period_reload epochs.\n        \"\"\"\n        # начальная загрузка масок и генерация списка координт патчей\n        # делаем это только в начале (а не когда выбераем список координат)\n        if self.next_new_file:\n            self.next_new_file = False\n            self.folder_id = (self.folder_id + 1) % len(self.folders)   # нумерация фрагментов идёт по кругу            \n            self.patch_id  = 0 \n            \n            self.load_masks(self.folders[self.folder_id], verbose)      # upload fragment and ink masks\n            self.get_patches_pos()                                      # create positions of patches\n        \n        # заполняем тензоры датасета патчами с масками  и слоями фрагмента под падчами\n        # get_patches вернёт True, когда весь список координат  будет выбран\n        # если это произошло, следующий раз грузим следующий фрагмент\n        # сразу нельзя, т.к. надо будет в итераторе по батчам сначала обработать загруженные двнные\n        if self.get_patches(verbose):                                   # all patches are selected\n            self.next_new_file = True                                   # загрузим новый файл при следующем вызове \n            return True\n        return False\n    \n    #---------------------------------------------------------------------------\n\n    def on_fit_start(self, trainer, model):\n        \"\"\"\n        Called before the trainer.fit() function starts running\n        Перед началом fit загрузим первый фрагмент, разобъём его на патчи \n        и сформируем тензоры датасета из начального подмножества патчей        \n        \"\"\"                \n        self.folder_id = -1\n        self.next_new_file = True                \n        \n    #---------------------------------------------------------------------------\n\n    def on_train_epoch_start(self, trainer, model):\n        \"\"\"\n        Called when epoch in fit ends.\n        \"\"\"            \n        if trainer is None or  (trainer.epoch == 1 or trainer.epoch % self.period_reload == 0):\n            self.reload(verbose = 2)          ","metadata":{"execution":{"iopub.status.busy":"2023-05-01T14:51:31.339953Z","iopub.execute_input":"2023-05-01T14:51:31.340416Z","iopub.status.idle":"2023-05-01T14:51:31.392790Z","shell.execute_reply.started":"2023-05-01T14:51:31.340372Z","shell.execute_reply":"2023-05-01T14:51:31.390779Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Creating validation and training datasets\n\nВалидацию сделаем статической, один раз загрузив 3 файла, разбив их на патчи и собрав все патчи в один датасет.<br> \nДля добавления одного датасета в другой, в классе `Data` есть метод `add`.\nРезультат разбивки можно видеть в разделе визуализации","metadata":{}},{"cell_type":"code","source":"# Валидация:\ninfo.reset()(\"beg\")\ndata_val = Data(batch_size=50) \ndata_tmp = Data() \ncallback = DataManagerCallback (data=data_tmp, train=True, n_patches=200, rand=True, \n                                patch_h=CFG.patch_h, patch_w=CFG.patch_w, layer_min=CFG.layer_min, layer_max=CFG.layer_max)\ncallback.on_fit_start(None,0)\nfor i in range(3):            # 200 патчей 3 раза (из каждого фрагмента)\n    callback.on_train_epoch_start(None,0)\n    data_val.add(callback.data)\ninfo(f\"data_val samples = {data_val.count()}  batches = {len(data_val)}\")\n    \n# Тренировка (валидационный callback не нужен уже):    \ndata_trn = Data(batch_size=100, shuffle=True) \ncallback = DataManagerCallback (data=data_trn, train=True, n_patches=1000, rand=True, \n                                patch_h=CFG.patch_h, patch_w=CFG.patch_w, patch_dh=16, patch_dw=16,    \n                                layer_min=CFG.layer_min, layer_max=CFG.layer_max) ","metadata":{"execution":{"iopub.status.busy":"2023-05-01T14:51:31.398016Z","iopub.execute_input":"2023-05-01T14:51:31.399894Z","iopub.status.idle":"2023-05-01T14:51:58.572857Z","shell.execute_reply.started":"2023-05-01T14:51:31.399802Z","shell.execute_reply":"2023-05-01T14:51:58.569444Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Иммитируем тренер","metadata":{}},{"cell_type":"code","source":"class Trainer:\n    def __init__(self, data_trn, data_val = data_val, callbacks=[]):        \n        self.data      = Config(trn=data_trn, val=data_val)\n        self.callbacks = callbacks\n        self.epoch     = 0\n    \n    def fit(self, epochs, period_reload=1):\n        \"\"\" иммитируем метод fit \"\"\"\n        for callback in self.callbacks: callback.on_fit_start(self, 0)\n        for epoch in range(1, epochs+1):\n            self.epoch = epoch        \n            \n            info(\"training start\")\n            for callback in self.callbacks: callback.on_train_epoch_start(self, 0)           \n            for batch_id, batch in enumerate(self.data.trn):\n                info.info(f\"\\r{batch_id+1}: {batch[0].shape}\", pref=\"\\r\", end=\"   \")\n            print(f\" {len(data_trn)} batches\")            \n\n            info(\"validation start\")            \n            for batch_id, batch in enumerate(self.data.trn):\n                info.info(f\"\\r{batch_id+1}: {batch[0].shape}\", pref=\"\\r\", end=\"   \")\n            print(f\" {len(data_trn)} batches\")\n            \n\ntrainer = Trainer(data_trn, callbacks=[callback])\ntrainer.fit(epochs=3)            ","metadata":{"execution":{"iopub.status.busy":"2023-05-01T14:51:58.578247Z","iopub.execute_input":"2023-05-01T14:51:58.579832Z","iopub.status.idle":"2023-05-01T14:53:39.906196Z","shell.execute_reply.started":"2023-05-01T14:51:58.579751Z","shell.execute_reply":"2023-05-01T14:53:39.904713Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Submission","metadata":{}},{"cell_type":"code","source":"def submission(patch_h, patch_w):    \n    result = {}\n    ink_pred = None\n    \n    data_tst = Data(batch_size=100) \n    callback = DataManagerCallback(data=data_tst, train=False, n_patches=1000, rand=False, \n                                   patch_h=CFG.patch_h, patch_w=CFG.patch_w, patch_dh=0, patch_dw=0,   \n                                   layer_min=CFG.layer_min, layer_max=CFG.layer_max)     \n    callback.on_fit_start(None, 0)                   \n    while True:                # on pathes\n        callback.on_train_epoch_start(None, 0)\n            \n        if ink_pred is None:\n            ink_pred = np.ones_like(callback.mask)            \n            folder = callback.folders[callback.folder_id].parts[-1]\n            print(f\"\\n*** Create new mask {ink_pred.shape}  folder:{folder}\\n\")                        \n        \n        for batch_id, batch in enumerate(data_tst):\n            patches, mask, ink, pos = batch        \n            for (y,x) in pos:\n                ink_pred[y:y+patch_h, x:x+patch_w] = 0  # проверяем, что все пиксели покрыты патчами                            \n        \n        if callback.next_new_file:\n            # перед загрузкой нового файла, сохраняем маску в submission.\n            ink_pred[0,1]=ink_pred[1,0]=1 # для csv\n            result[folder] = \"\"           # !                \n            print(f\"\\n*** Save submission to '{folder}'; ink_pred.shape:{ink_pred.shape}, sum:{ink_pred.sum()} == 2\\n\")                \n            ink_pred = None\n            \n            if callback.folder_id + 1 >=  len(callback.folders):        \n                break\n            \n    info(f\"result: {result}\")\n    #pd.DataFrame(result).to_csv(\"submission.csv\")  # todo\n#---------------------------------------------------------------------------                \ninfo(\"beg\")\n#del data_trn, data_val\nsubmission(patch_h=CFG.patch_h, patch_w=CFG.patch_w)\ninfo(f\"the End\");","metadata":{"execution":{"iopub.status.busy":"2023-05-01T14:53:39.910074Z","iopub.execute_input":"2023-05-01T14:53:39.910551Z","iopub.status.idle":"2023-05-01T14:53:46.192769Z","shell.execute_reply.started":"2023-05-01T14:53:39.910492Z","shell.execute_reply":"2023-05-01T14:53:46.191246Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Visualization","metadata":{}},{"cell_type":"code","source":"def plot_masks(folder=CFG.folder_trn,  subfolder=\"1/\", w=10, h=6):\n    path = Path(folder) / Path(subfolder)\n    \n    sample = Image.open(path / Path(\"ir.png\"))\n    label = torch.from_numpy(np.array(Image.open(path / Path(\"inklabels.png\")) )).gt(0).float().to(CFG.device)    \n    mask = np.array(Image.open(path / Path(\"mask.png\")).convert('1'))\n            \n    fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(w, h), facecolor ='w')\n    ax0.set_title(subfolder+\"ir.png\");         ax0.imshow(sample,      cmap='gray')    \n    ax1.set_title(subfolder+\"inklabels.png\");  ax1.imshow(label.cpu(), cmap='gray')\n    ax1.set_title(subfolder+\"mask.png\");       ax1.imshow(mask,        cmap='gray', alpha=0.5)    \n    plt.show()\n    print(f\"label: {label.shape},  mask: {mask.shape}\")\n    \ninfo(\"beg\"); plot_masks(subfolder=\"1/\"); info(\"end\")","metadata":{"execution":{"iopub.status.busy":"2023-05-01T14:53:46.194423Z","iopub.execute_input":"2023-05-01T14:53:46.194886Z","iopub.status.idle":"2023-05-01T14:53:56.143329Z","shell.execute_reply.started":"2023-05-01T14:53:46.194841Z","shell.execute_reply":"2023-05-01T14:53:56.141207Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def plot_data(folder=CFG.folder_trn,  subfolder=\"1/\", start=0, num=4,  w=12, h=6):\n    \"\"\" Load the 3d x-ray scan, one slice at a time \"\"\"\n    path  = Path(folder) / Path(subfolder) / Path(\"surface_volume/\")\n    files = sorted( path.glob('*.tif') )\n    print(f\"total files: {len(files)}\")    \n    images = [np.array(Image.open(fname), dtype=np.float32) / 65535.0 \\\n              for fname in tqdm(files[start:start+num])  ] \n\n    fig, axes = plt.subplots(1, len(images), figsize=(w, h))\n    for image, ax in zip(images, axes):\n        ax.imshow(np.array(Image.fromarray(image).resize((image.shape[1]//20, image.shape[0]//20)), dtype=np.float32), cmap='gray')\n        ax.set_xticks([]); ax.set_yticks([])\n    fig.tight_layout()\n    plt.show()\n\ninfo(\"beg\"); plot_data(subfolder=\"1/\"); info(\"end\") ","metadata":{"execution":{"iopub.status.busy":"2023-05-01T14:53:56.145284Z","iopub.execute_input":"2023-05-01T14:53:56.146302Z","iopub.status.idle":"2023-05-01T14:54:01.089240Z","shell.execute_reply.started":"2023-05-01T14:53:56.146246Z","shell.execute_reply":"2023-05-01T14:54:01.087900Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def plot_patches(data, idx=0, n_images=5, w=12, h=4, start=0):\n    data.reset()\n    for patch,  mask, ink, pos in data:    \n        print(patch.shape, mask.shape, ink.shape)\n        break\n            \n    images =  [mask[idx][0].float().numpy(), ink[idx][0].float().numpy()] \n    images += [patch[idx, start+i].numpy() for i in range(n_images-3) ] \n    images += [ patch[idx].mean(0).numpy()]\n    fig, axes = plt.subplots(1, n_images, figsize=(w, h))\n    for i, (image, ax) in enumerate(zip(images, axes)):\n        ax.imshow(image, cmap='gray', vmin=0, vmax=1)\n        ax.set_xticks([]); ax.set_yticks([])\n        if i==0:\n            ax.set_title(f\"y:{pos[idx][0]}, x:{pos[idx][1]}\", fontsize=8)\n        #elif i > 1:\n        #    ax.set_title(f\"{patch[idx,start+i-2].min()} {patch[idx,start+i-2].max()} {patch[idx,start+i-2].std():.2f}\")\n            \n    fig.tight_layout()\n    plt.show()\n    \nfor idx in range(20):\n    plot_patches(data_val, idx=idx)","metadata":{"execution":{"iopub.status.busy":"2023-05-01T14:54:01.091313Z","iopub.execute_input":"2023-05-01T14:54:01.091742Z","iopub.status.idle":"2023-05-01T14:54:12.509307Z","shell.execute_reply.started":"2023-05-01T14:54:01.091699Z","shell.execute_reply":"2023-05-01T14:54:12.507711Z"},"trusted":true},"execution_count":null,"outputs":[]}]}