# 12st Place Solution for the SenNet + HOA - Hacking the Human Vasculature in 3D Context This solution was implemented as part of a blood vessel segmentation competition organized by the Common Fund’s Cellular Senescence Network (SenNet) Programm in cooperation with the Human Organ Atlas (HOA). Competition overview page: [SenNet + HOA - Hacking the Human Vasculature in 3D](https://www.kaggle.com/competitions/blood-vessel-segmentation) Competition dataset is [here](https://www.kaggle.com/competitions/blood-vessel-segmentation/data) Many thanks to the organizers for the opportunity! Overview Framework — TensorFlow Data pipeline — 2d, roi, resize ( 1024x704 ), tfrecord Model — almost classic U-net (details below) The solution is presented in two notebooks: [train](https://www.kaggle.com/code/pib73nl/sennet-hoa-bvs-12th-place-solution-train) [inference](https://www.kaggle.com/code/pib73nl/sennet-hoa-bvs-12th-place-solution-infer) Disclamer This solution was developed in December 2023, before Santa's New Year's gift, which ultimately helped more than seven hundred participants to jump above 0.8. I left for the holidays in 238th place with a score of 0.567. When I next opened the leaderboard a week later, I had dropped over 150 positions! And that was just the beginning! ;) I worked on this approach for another week. I must say that I did this without much enthusiasm, since by improving the metric I found myself lower and lower in the ranking. Finaly, with a result of 0.636, which was achieved by increasing the image size and minor architecture changes, I began to look for other approaches (see below in chapter „Fruitless attempts“ ). Data preparation All data (except for the kidney_3_dense labels) were used as training data. The images have large fields that contain no useful information. To reduce these fields, the images were preprocessed to extract roi using statistical methods. (): row_mask = image.std(axis=)> clmn_mask = image.std(axis=)> row_mask = cleaning_mask(row_mask) clmn_mask = cleaning_mask(clmn_mask) image = image[row_mask,:][:, clmn_mask] label = label[row_mask,:][:, clmn_mask] (label, np.ndarray) row_pad = (row_mask.argmax(), row_mask[::-].argmax()) clmn_pad = (clmn_mask.argmax(), clmn_mask[::-].argmax()) image, label, (row_pad, clmn_pad) (): mask[] = mask[-] = frames = np.nonzero(mask[:-]!=mask[:])[] delta = frames[:]-frames[:-] max_solid_block_begin = np.argmax(delta) garbage = np.delete(frames, [max_solid_block_begin, max_solid_block_begin+]) a, b (garbage[::], garbage[::]): mask[a+:b+] = mask Next, all images were reduced to a single size of 1024x704. The experiments started with a size of 384x256, and as the size increased, the result expectedly improved. 1024x704 is the maximum size that did not result in an OOM error. An example of the processed image is below. ![Example of a processed image](assets/63fcbcbe16b61407024cf3e1.png) Every 25 images (4%) were used for validation, since the density of the labels varies greatly along the z-axis. The resulting images and tags were packed into tfrecord files to organize a multi-threaded pipeline (total files - 92, 162 MB each). The maximum possible batch size for the 1024x704 shape turned out to be 32. Augmentation was not used - I just couldn’t get around to it! Model The more or less classical U-net architecture was used as a model. ![U-net architect](assets/62820b3e64f67d37063fc210.png) Losses were estimated using binary crosentropy . The Adam optimizer was used for optimization. The learning rate was changed according to the cosine decay schedule with warmup. Since there is a significant class imbalance, weights were used. The idea was to set the weights at the instance level, since the class ratios vary greatly as we move from the center of the kidney to the edges (along the z-axis). But to begin with, I hardcode the weights, and it worked tolerably well. I didn't return to this issue later, so there is room for improvement. The model was created from scratch and trained for 60 epochs. For prediction, epochs with a minimum value of validation losses were taken. The prediction result looked something like this: ![Prediction result](assets/f9146956a41c602d9b5b1f84.png) There are quite a lot of FP here… However, it makes sense to work on the sample weights 🤔 Fruitless attempts Obviously, given the large number of small details, any resizing harms the result. I tried to solve this problem by dividing the image into fragments (intersecting tiles of 256x256 size). I used the same model architecture. But the labels turned out to be exclusively in the places where the tiles overlapped, and having assembled a mask from the tiles, I got a blank sheet! I haven't had time to figure this out. Second. I tried to solve the problem of label resizing by changing the architecture - I added another “kinda u-net” to the end of decoder - 2 convolution layers and two reconvolution ones. Didn't do well here either, but would have been in 67th place on the private leaderboard 😉 ![](assets/8ecdbc1da7119368e3c75960.png) Some observations Yes, yes… There was a big quake… For some reason, most of the solutions failed in suspiciously similar ways ;) This is clearly noticeable in the interval of about 100-600 places. ![LB shake plot](assets/6387dc15815a522a2360dfd6.png) My main solutions, similar in network architecture and image size to the winning one, gave stable results on a public and private dataset. On a private dataset - even a little better! ![Score of the winning model](assets/479018b6040b6dd1f2755380.png) The difference between the public and private data sets was 0.012 points. Such stable results in the first thousand can be counted on the fingers of one hand. In general, the variance is already normal, all that remains is to work on the bias 😁 Thanks to everyone who worked on the problem! It was interesting with you! Good luck! ✋