{
  "id": 159176,
  "title": "Advanced hair augmentation",
  "url": "/competitions/siim-isic-melanoma-classification/discussion/159176",
  "author_name": "Roman",
  "post_date": "2020-06-16T16:54:29.013000",
  "votes": 130,
  "comment_count": 30,
  "views": 0,
  "content": "<p>After reading <a href=\"https://arxiv.org/pdf/1809.02568.pdf\">this paper</a> I got inspired by a hair augmentation as another way to diversify a dataset. So I have implemented my version of this 'pseudo hair' augmentation technique in 19th version of <a href=\"https://www.kaggle.com/nroman/melanoma-pytorch-starter-efficientnet?scriptVersionId=36430890\">my kernel</a>. And it definitely worked, giving a better score on the leader board (although I am still struggling to come up with a proper validation which result would conform with the LB score).</p>\n\n<p>But if you think about this 'pseudo hair' technique - it is nothing else than just a cutout, simply implemented in a slightly different way. So I've decided to go further and implement an advanced hair augmentation technique: to take an image of an actual hair and impose it to the image from the dataset.</p>\n\n<p>In order to do that I have created a dataset with images of only hairs (I manually cut them out from some of the images from the dataset. For now there are only 5 of them but I will add more later): <a href=\"https://www.kaggle.com/nroman/melanoma-hairs\">https://www.kaggle.com/nroman/melanoma-hairs</a></p>\n\n<p>In here is the augmentation class:</p>\n\n<p>```\nclass AdvancedHairAugmentation:\n    def <strong>init</strong>(self, hairs: int = 4, hairs_folder: str = \"\"):\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder</p>\n\n<pre><code>def __call__(self, img):\n    n_hairs = random.randint(0, self.hairs)\n\n    if not n_hairs:\n        return img\n\n    height, width, _ = img.shape  # target image width and height\n    hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n    for _ in range(n_hairs):\n        hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n        hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n        hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n        h_height, h_width, _ = hair.shape  # hair image width and height\n        roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n        roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n        roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n        img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n        ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n        mask_inv = cv2.bitwise_not(mask)\n        img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n        hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n        dst = cv2.add(img_bg, hair_fg)\n        img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n    return img\n</code></pre>\n\n<p>```</p>\n\n<p>And the result of the augmentation:</p>\n\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F1696976%2F1f2bb0d323573d9ac53a87cbe50f2fd1%2Faugmented_hairs.png?generation=1592326051386966&amp;alt=media\" alt=\"\"></p>\n\n<p>'Pseudo hairs' technique for the comparison:\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F1696976%2F41a66a688b7f23aff91edc2e0b2797f4%2Faug_old.png?generation=1592326308016210&amp;alt=media\" alt=\"\"></p>",
  "messages": [
    {
      "id": 888935,
      "postDate": "2020-06-16T16:54:29.013Z",
      "content": "<p>After reading <a href=\"https://arxiv.org/pdf/1809.02568.pdf\">this paper</a> I got inspired by a hair augmentation as another way to diversify a dataset. So I have implemented my version of this 'pseudo hair' augmentation technique in 19th version of <a href=\"https://www.kaggle.com/nroman/melanoma-pytorch-starter-efficientnet?scriptVersionId=36430890\">my kernel</a>. And it definitely worked, giving a better score on the leader board (although I am still struggling to come up with a proper validation which result would conform with the LB score).</p>\n\n<p>But if you think about this 'pseudo hair' technique - it is nothing else than just a cutout, simply implemented in a slightly different way. So I've decided to go further and implement an advanced hair augmentation technique: to take an image of an actual hair and impose it to the image from the dataset.</p>\n\n<p>In order to do that I have created a dataset with images of only hairs (I manually cut them out from some of the images from the dataset. For now there are only 5 of them but I will add more later): <a href=\"https://www.kaggle.com/nroman/melanoma-hairs\">https://www.kaggle.com/nroman/melanoma-hairs</a></p>\n\n<p>In here is the augmentation class:</p>\n\n<p>```\nclass AdvancedHairAugmentation:\n    def <strong>init</strong>(self, hairs: int = 4, hairs_folder: str = \"\"):\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder</p>\n\n<pre><code>def __call__(self, img):\n    n_hairs = random.randint(0, self.hairs)\n\n    if not n_hairs:\n        return img\n\n    height, width, _ = img.shape  # target image width and height\n    hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n    for _ in range(n_hairs):\n        hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n        hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n        hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n        h_height, h_width, _ = hair.shape  # hair image width and height\n        roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n        roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n        roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n        img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n        ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n        mask_inv = cv2.bitwise_not(mask)\n        img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n        hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n        dst = cv2.add(img_bg, hair_fg)\n        img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n    return img\n</code></pre>\n\n<p>```</p>\n\n<p>And the result of the augmentation:</p>\n\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F1696976%2F1f2bb0d323573d9ac53a87cbe50f2fd1%2Faugmented_hairs.png?generation=1592326051386966&amp;alt=media\" alt=\"\"></p>\n\n<p>'Pseudo hairs' technique for the comparison:\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F1696976%2F41a66a688b7f23aff91edc2e0b2797f4%2Faug_old.png?generation=1592326308016210&amp;alt=media\" alt=\"\"></p>",
      "rawMarkdown": "After reading [this paper](https://arxiv.org/pdf/1809.02568.pdf) I got inspired by a hair augmentation as another way to diversify a dataset. So I have implemented my version of this 'pseudo hair' augmentation technique in 19th version of [my kernel](https://www.kaggle.com/nroman/melanoma-pytorch-starter-efficientnet?scriptVersionId=36430890). And it definitely worked, giving a better score on the leader board (although I am still struggling to come up with a proper validation which result would conform with the LB score).\n\nBut if you think about this 'pseudo hair' technique - it is nothing else than just a cutout, simply implemented in a slightly different way. So I've decided to go further and implement an advanced hair augmentation technique: to take an image of an actual hair and impose it to the image from the dataset.\n\nIn order to do that I have created a dataset with images of only hairs (I manually cut them out from some of the images from the dataset. For now there are only 5 of them but I will add more later): https://www.kaggle.com/nroman/melanoma-hairs\n\nIn here is the augmentation class:\n\n```\nclass AdvancedHairAugmentation:\n    def __init__(self, hairs: int = 4, hairs_folder: str = \"\"):\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder\n\n    def __call__(self, img):\n        n_hairs = random.randint(0, self.hairs)\n        \n        if not n_hairs:\n            return img\n        \n        height, width, _ = img.shape  # target image width and height\n        hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n        \n        for _ in range(n_hairs):\n            hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n            hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n            hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n            h_height, h_width, _ = hair.shape  # hair image width and height\n            roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n            roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n            img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n            ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n            mask_inv = cv2.bitwise_not(mask)\n            img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n            hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n            dst = cv2.add(img_bg, hair_fg)\n            img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n                \n        return img\n```\n\nAnd the result of the augmentation:\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F1696976%2F1f2bb0d323573d9ac53a87cbe50f2fd1%2Faugmented_hairs.png?generation=1592326051386966&amp;alt=media)\n\n'Pseudo hairs' technique for the comparison:\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F1696976%2F41a66a688b7f23aff91edc2e0b2797f4%2Faug_old.png?generation=1592326308016210&amp;alt=media)\n",
      "votes": 130
    },
    {
      "id": 961899,
      "postDate": "2020-08-07T15:38:53.730Z",
      "content": "<p>This + Coarse dropout gave me a nice improvement in both CV and LB. Thanks. here is the albumentation friendly version:</p>\n<p>`<br>\nfrom albumentations.core.transforms_interface import ImageOnlyTransform<br>\nclass AdvancedHairAugmentation(ImageOnlyTransform):</p>\n<pre><code>def __init__(self, hairs: int = 4, hairs_folder: str = \"\", always_apply=False, p=0.5):\n    super(AdvancedHairAugmentation, self).__init__(always_apply=always_apply, p=p)\n    self.hairs = hairs\n    self.hairs_folder = hairs_folder\n\ndef apply(self, image, **params):\n    n_hairs = random.randint(0, self.hairs)\n\n    if not n_hairs:\n        return image\n\n    height, width, _ = image.shape  # target image width and height\n    hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n    for _ in range(n_hairs):\n        hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n        hair = cv2.cvtColor(hair, cv2.COLOR_BGR2RGB)\n        hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n        hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n        h_height, h_width, _ = hair.shape  # hair image width and height\n        roi_ho = random.randint(0, image.shape[0] - hair.shape[0])\n        roi_wo = random.randint(0, image.shape[1] - hair.shape[1])\n        roi = image[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n        img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n        ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n        mask_inv = cv2.bitwise_not(mask)\n        img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n        hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n        dst = cv2.add(img_bg, hair_fg)\n        image[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n    return image\n\ndef get_params_dependent_on_targets(self, params):\n    return {}\n\n@property\ndef targets_as_params(self):\n    return [\"image\"]\n\ndef get_transform_init_args_names(self):\n    return ()`\n</code></pre>",
      "rawMarkdown": "This + Coarse dropout gave me a nice improvement in both CV and LB. Thanks. here is the albumentation friendly version:\n\n`\nfrom albumentations.core.transforms_interface import ImageOnlyTransform\nclass AdvancedHairAugmentation(ImageOnlyTransform):\n\n    def __init__(self, hairs: int = 4, hairs_folder: str = \"\", always_apply=False, p=0.5):\n        super(AdvancedHairAugmentation, self).__init__(always_apply=always_apply, p=p)\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder\n\n    def apply(self, image, **params):\n        n_hairs = random.randint(0, self.hairs)\n\n        if not n_hairs:\n            return image\n\n        height, width, _ = image.shape  # target image width and height\n        hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n        for _ in range(n_hairs):\n            hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n            hair = cv2.cvtColor(hair, cv2.COLOR_BGR2RGB)\n            hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n            hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n            h_height, h_width, _ = hair.shape  # hair image width and height\n            roi_ho = random.randint(0, image.shape[0] - hair.shape[0])\n            roi_wo = random.randint(0, image.shape[1] - hair.shape[1])\n            roi = image[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n            img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n            ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n            mask_inv = cv2.bitwise_not(mask)\n            img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n            hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n            dst = cv2.add(img_bg, hair_fg)\n            image[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n        return image\n\n    def get_params_dependent_on_targets(self, params):\n        return {}\n\n    @property\n    def targets_as_params(self):\n        return [\"image\"]\n\n    def get_transform_init_args_names(self):\n        return ()`",
      "votes": 9,
      "replies": [
        {
          "id": 962035,
          "postDate": "2020-08-07T18:30:36.953Z",
          "content": "<p>thanks. i was migrating to albumentations from torchvision. it's great timing. </p>\n\n<p>it only works for image size &gt; 256 tho. there are hairs with size &gt; 128, so </p>\n\n<p>andom.randint(0, image.shape[0] - hair.shape[0])</p>\n\n<p>would break. </p>",
          "rawMarkdown": "thanks. i was migrating to albumentations from torchvision. it's great timing. \n\nit only works for image size &gt; 256 tho. there are hairs with size &gt; 128, so \n\nandom.randint(0, image.shape[0] - hair.shape[0])\n\nwould break. \n"
        },
        {
          "id": 962123,
          "postDate": "2020-08-07T20:40:16.360Z",
          "content": "<p><a href=\"https://www.kaggle.com/yimacs\" target=\"_blank\">@yimacs</a> Good catch. I was only using it on 256 x 256 at this stage. Will work a bit on the code as it was a straight copy paste from a post here.</p>",
          "rawMarkdown": "@yimacs Good catch. I was only using it on 256 x 256 at this stage. Will work a bit on the code as it was a straight copy paste from a post here."
        }
      ]
    },
    {
      "id": 961813,
      "postDate": "2020-08-07T14:16:42.090Z",
      "content": "<p>White hairs we made.\n<a href=\"https://www.kaggle.com/shogoaraki/whitehairs\">https://www.kaggle.com/shogoaraki/whitehairs</a></p>",
      "rawMarkdown": "White hairs we made.\nhttps://www.kaggle.com/shogoaraki/whitehairs",
      "votes": 4
    },
    {
      "id": 896937,
      "postDate": "2020-06-22T14:29:29.680Z",
      "content": "<p>Very cool idea! I just quickly tweak the code so that the augmentation is a subclass of albumentations augmentation. I think it is more rigorous this way and more easily fit in albumentations pipeline. </p>\n\n<p>`\nclass HairAugmentation(albumentations.ImageOnlyTransform):\n    def <strong>init</strong>(self, max_hairs:int = 4, hairs_folder: str = \"/kaggle/input/melanoma-hairs\", p=0.5):\n        super().<strong>init</strong>(p=p)\n        self.max_hairs = max_hairs\n        self.hairs_folder = hairs_folder</p>\n\n<pre><code>def apply(self, img, **params):\n    n_hairs = random.randint(0, self.max_hairs)\n\n    if not n_hairs:\n        return img\n\n    height, width, _ = img.shape  # target image width and height\n    hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n    for _ in range(n_hairs):\n        hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n        hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n        hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n        h_height, h_width, _ = hair.shape  # hair image width and height\n        hair = cv2.resize(hair, (int(h_width*0.8), int(h_height*0.8)))\n\n        h_height, h_width, _ = hair.shape  # hair image width and height\n        roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n        roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n        roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n        img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n        ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n        mask_inv = cv2.bitwise_not(mask)\n        img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n        hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n        dst = cv2.add(img_bg, hair_fg)\n        img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n    return img\n</code></pre>\n\n<p>`</p>",
      "rawMarkdown": "Very cool idea! I just quickly tweak the code so that the augmentation is a subclass of albumentations augmentation. I think it is more rigorous this way and more easily fit in albumentations pipeline. \n\n`\nclass HairAugmentation(albumentations.ImageOnlyTransform):\n    def __init__(self, max_hairs:int = 4, hairs_folder: str = \"/kaggle/input/melanoma-hairs\", p=0.5):\n        super().__init__(p=p)\n        self.max_hairs = max_hairs\n        self.hairs_folder = hairs_folder\n\n    def apply(self, img, **params):\n        n_hairs = random.randint(0, self.max_hairs)\n\n        if not n_hairs:\n            return img\n\n        height, width, _ = img.shape  # target image width and height\n        hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n        for _ in range(n_hairs):\n            hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n            hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n            hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n            \n            h_height, h_width, _ = hair.shape  # hair image width and height\n            hair = cv2.resize(hair, (int(h_width*0.8), int(h_height*0.8)))\n            \n            h_height, h_width, _ = hair.shape  # hair image width and height\n            roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n            roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n            img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n            ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n            mask_inv = cv2.bitwise_not(mask)\n            img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n            hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n            dst = cv2.add(img_bg, hair_fg)\n            img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n        return img\n`",
      "votes": 3,
      "replies": [
        {
          "id": 898122,
          "postDate": "2020-06-23T10:17:37.227Z",
          "content": "<p>I had a few errors in this one , so I tweaked it a bit more to make it work- \nHere's the albumentations version I'm using: </p>\n\n<p>`class AdvancedHairAugmentation(ImageOnlyTransform):</p>\n\n<pre><code>def __init__(self, hairs: int = 5, hairs_folder: str = \"\" , always_apply=False, p=0.5):\n    self.hairs = hairs\n    self.hairs_folder = hairs_folder\n    super().__init__(always_apply, p)\n\ndef apply(self, img, **params):\n    n_hairs = random.randint(0, self.hairs)\n\n    if not n_hairs:\n        return img\n\n    height, width, _ = img.shape  # target image width and height\n    hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n    for _ in range(n_hairs):\n        hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n        hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n        hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n        h_height, h_width, _ = hair.shape  # hair image width and height\n        roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n        roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n        roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n        img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n        ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n        mask_inv = cv2.bitwise_not(mask)\n        img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n        hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n        dst = cv2.add(img_bg, hair_fg, dtype=cv2.CV_64F)\n        img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n    return img`\n</code></pre>",
          "rawMarkdown": "I had a few errors in this one , so I tweaked it a bit more to make it work- \nHere's the albumentations version I'm using: \n\n`class AdvancedHairAugmentation(ImageOnlyTransform):\n    \n\n    def __init__(self, hairs: int = 5, hairs_folder: str = \"\" , always_apply=False, p=0.5):\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder\n        super().__init__(always_apply, p)\n\n    def apply(self, img, **params):\n        n_hairs = random.randint(0, self.hairs)\n\n        if not n_hairs:\n            return img\n\n        height, width, _ = img.shape  # target image width and height\n        hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n        for _ in range(n_hairs):\n            hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n            hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n            hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n            h_height, h_width, _ = hair.shape  # hair image width and height\n            roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n            roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n            img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n            ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n            mask_inv = cv2.bitwise_not(mask)\n            img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n            hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n            dst = cv2.add(img_bg, hair_fg, dtype=cv2.CV_64F)\n            img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n        return img`",
          "votes": 2
        }
      ]
    },
    {
      "id": 890407,
      "postDate": "2020-06-17T13:48:44.233Z",
      "content": "<p>Interesting , would use it. Nice work, thanks !</p>",
      "rawMarkdown": "Interesting , would use it. Nice work, thanks !",
      "votes": 2
    },
    {
      "id": 956745,
      "postDate": "2020-08-03T18:41:12.220Z",
      "content": "<p>This is a great augmentation technique, would like to try this. </p>",
      "rawMarkdown": "This is a great augmentation technique, would like to try this. "
    },
    {
      "id": 929967,
      "postDate": "2020-07-15T05:29:41.527Z",
      "content": "<p>What happens if img is a tensor?</p>",
      "rawMarkdown": "What happens if img is a tensor?"
    },
    {
      "id": 925582,
      "postDate": "2020-07-12T07:01:24.270Z",
      "content": "<p>Nice</p>",
      "rawMarkdown": "Nice"
    },
    {
      "id": 912050,
      "postDate": "2020-07-02T07:15:14.187Z",
      "content": "<p>Hey! Thanks for this; I've been trying out the code but for some reason all my hair strands are coming out as blue. Any ideas as to why this might be?</p>",
      "rawMarkdown": "Hey! Thanks for this; I've been trying out the code but for some reason all my hair strands are coming out as blue. Any ideas as to why this might be?",
      "replies": [
        {
          "id": 916521,
          "postDate": "2020-07-05T18:10:32.433Z",
          "content": "<p><a href=\"/fallswatt1\">@fallswatt1</a> OpenCV assumes that it is given images in the <code>BGR</code> format. But Roman's images are in the <code>RGB</code> format. So you need to fix the color scheme. It can be easily done as follows. After your read the hair image with something like  <code>hair_img=cv2.imread(hair_img_path)</code> convert the <code>BGR</code> colors to <code>RGB</code> 's by calling <code>hair_img = cv2.cvtColor(hair_img, cv2.COLOR_BGR2RGB)</code></p>",
          "rawMarkdown": "@fallswatt1 OpenCV assumes that it is given images in the `BGR` format. But Roman's images are in the `RGB` format. So you need to fix the color scheme. It can be easily done as follows. After your read the hair image with something like  `hair_img=cv2.imread(hair_img_path)` convert the `BGR` colors to `RGB` 's by calling `hair_img = cv2.cvtColor(hair_img, cv2.COLOR_BGR2RGB)`",
          "votes": 5
        }
      ]
    },
    {
      "id": 911283,
      "postDate": "2020-07-01T16:36:13.853Z",
      "content": "<p>Thanks for sharing, it is interesting. But on the other hand, have you tried remove the hairs from the image? Do you think it would help with the classification since hairs probably don’t relate to the disease? </p>",
      "rawMarkdown": "Thanks for sharing, it is interesting. But on the other hand, have you tried remove the hairs from the image? Do you think it would help with the classification since hairs probably don’t relate to the disease? ",
      "replies": [
        {
          "id": 911356,
          "postDate": "2020-07-01T17:12:49.760Z",
          "content": "<p>I think people were having hard time removing hair from images, as suggested in the referred <a href=\"https://arxiv.org/pdf/1809.02568.pdf\">paper</a>:</p>\n\n<p>&gt; Body hair augmentation Based on our observation, the lesion image showsthat there are samples with body hair overlapping in the lesion area. <strong><em>Several hair removal methodologies have been proposed to address this issue. However, some problems such as how to interpolate overlapped part were left behind</em>.</strong> So we take the opposite approach. We propose a body hair augmentation which applies pseudo body hair to skin lesion images. Body hair augmentation is based on Buffon’s needele [11] and gives a line simulating body hair in a pseudo manner. Example of body hair augmentation is show in Fig. 4.</p>",
          "rawMarkdown": "I think people were having hard time removing hair from images, as suggested in the referred [paper](https://arxiv.org/pdf/1809.02568.pdf):\n\n&gt; Body hair augmentation Based on our observation, the lesion image showsthat there are samples with body hair overlapping in the lesion area. ***Several hair removal methodologies have been proposed to address this issue. However, some problems such as how to interpolate overlapped part were left behind*.** So we take the opposite approach. We propose a body hair augmentation which applies pseudo body hair to skin lesion images. Body hair augmentation is based on Buffon’s needele [11] and gives a line simulating body hair in a pseudo manner. Example of body hair augmentation is show in Fig. 4.",
          "votes": 1
        },
        {
          "id": 912760,
          "postDate": "2020-07-02T17:52:59.770Z",
          "content": "<p>I see, thanks, I think it makes sense that removing hair is difficult. </p>",
          "rawMarkdown": "I see, thanks, I think it makes sense that removing hair is difficult. "
        }
      ]
    },
    {
      "id": 911197,
      "postDate": "2020-07-01T15:55:37.647Z",
      "content": "<p>How could we perform masking on tf.image format. To be more precise, how would we implement the following line for tf.image \n<code>img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)</code></p>\n\n<p>The bitwise and can be achieved through <code>tf.bitwise.bitwise_and()</code>, but how do we perform masking</p>",
      "rawMarkdown": "How could we perform masking on tf.image format. To be more precise, how would we implement the following line for tf.image \n`img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)`\n\nThe bitwise and can be achieved through `tf.bitwise.bitwise_and()`, but how do we perform masking\n\n"
    },
    {
      "id": 909443,
      "postDate": "2020-06-30T15:40:23.853Z",
      "content": "<p>Working on a script to remove hair and noticed that my script also removes the mm scale marking that is common to many of the images.  Wonder if adding that scale to images as an augment would help?   Think I have a PC with Photoshop loaded - will see if I can extract a few scale images to add to the hairs.</p>",
      "rawMarkdown": "Working on a script to remove hair and noticed that my script also removes the mm scale marking that is common to many of the images.  Wonder if adding that scale to images as an augment would help?   Think I have a PC with Photoshop loaded - will see if I can extract a few scale images to add to the hairs."
    },
    {
      "id": 904010,
      "postDate": "2020-06-27T09:17:33.003Z",
      "content": "<p>Title really cracked me up. LMAO</p>",
      "rawMarkdown": "Title really cracked me up. LMAO"
    },
    {
      "id": 898868,
      "postDate": "2020-06-23T19:56:51.607Z",
      "content": "<p>Very nice - it worked pretty much as you wrote it for a tensorflow model.  I changed it to include a probability.   Running the model now (512x512) on my PC so will not know it's impact until tomorrow.  (PS I really hate how hard it is to paste code into these messages- I finally gave up on getting it to look right)</p>\n\n<p>`hairs_folder = '/home/james/my_server/Melanoma/hairs'</p>\n\n<p><code>class AdvancedHairAugmentation:\n    def __init__(self, hairs: int = 4, hairs_folder: str = \"\", p: float = 0.5):\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder\n        self.p = p</code></p>\n\n<pre><code>def __call__(self, img):\n    if random.random() &lt; self.p:\n        n_hairs = random.randint(0, self.hairs)\n\n        if not n_hairs:\n            return img\n\n        # height, width, _ = img.shape  # target image width and height\n        hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n        for _ in range(n_hairs):\n            hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n            hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n            hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n            h_height, h_width, _ = hair.shape  # hair image width and height\n            roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n            roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n            img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n            ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n            mask_inv = cv2.bitwise_not(mask)\n            img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n            hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n            dst = cv2.add(img_bg, hair_fg)\n            img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n        return img\n</code></pre>",
      "rawMarkdown": "Very nice - it worked pretty much as you wrote it for a tensorflow model.  I changed it to include a probability.   Running the model now (512x512) on my PC so will not know it's impact until tomorrow.  (PS I really hate how hard it is to paste code into these messages- I finally gave up on getting it to look right)\n\n`hairs_folder = '/home/james/my_server/Melanoma/hairs'\n\n`class AdvancedHairAugmentation:\n    def __init__(self, hairs: int = 4, hairs_folder: str = \"\", p: float = 0.5):\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder\n        self.p = p`\n\n    def __call__(self, img):\n        if random.random() &lt; self.p:\n            n_hairs = random.randint(0, self.hairs)\n\n            if not n_hairs:\n                return img\n\n            # height, width, _ = img.shape  # target image width and height\n            hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n            for _ in range(n_hairs):\n                hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n                hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n                hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n                h_height, h_width, _ = hair.shape  # hair image width and height\n                roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n                roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n                roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n                img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n                ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n                mask_inv = cv2.bitwise_not(mask)\n                img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n                hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n                dst = cv2.add(img_bg, hair_fg)\n                img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n            return img\n  ",
      "replies": [
        {
          "id": 900642,
          "postDate": "2020-06-25T01:02:16.977Z",
          "content": "<p>Hair worked - but Microscope did not help - it hurt. <br>\nAny more hairs added to the dataset in the near future ?</p>",
          "rawMarkdown": "Hair worked - but Microscope did not help - it hurt.  \nAny more hairs added to the dataset in the near future ?",
          "votes": 1
        },
        {
          "id": 900980,
          "postDate": "2020-06-25T07:28:08.697Z",
          "content": "<p>Yes, will add more hairs soon.\nMicroscope is a little tricky. I managed to gain from it, but it takes some time and effort.</p>",
          "rawMarkdown": "Yes, will add more hairs soon.\nMicroscope is a little tricky. I managed to gain from it, but it takes some time and effort.",
          "votes": 2
        },
        {
          "id": 903606,
          "postDate": "2020-06-27T01:36:10.723Z",
          "rawMarkdown": "",
          "isDeleted": true
        }
      ]
    },
    {
      "id": 895315,
      "postDate": "2020-06-21T08:51:20.583Z",
      "content": "<p>nice work</p>",
      "rawMarkdown": "nice work"
    },
    {
      "id": 893480,
      "postDate": "2020-06-19T16:35:36.430Z",
      "content": "<p>Nice augmentation. However it fails when randomly selected hair image and input image have the same dimensions. The problem originates from the following lines:</p>\n\n<p><code>roi_ho = random.randint(0, img.shape[0] - hair.shape[0]);\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])</code></p>",
      "rawMarkdown": "Nice augmentation. However it fails when randomly selected hair image and input image have the same dimensions. The problem originates from the following lines:\n\n`          roi_ho = random.randint(0, img.shape[0] - hair.shape[0]);\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])`",
      "replies": [
        {
          "id": 893568,
          "postDate": "2020-06-19T18:12:11.370Z",
          "content": "<p>I honestly did my best to understand what you have said but I did not.\nWhat deminsions are you talking about?</p>",
          "rawMarkdown": "I honestly did my best to understand what you have said but I did not.\nWhat deminsions are you talking about?",
          "votes": 1
        },
        {
          "id": 893743,
          "postDate": "2020-06-19T21:06:01.813Z",
          "content": "<p>Sorry for the lack of clarity :( I accidentally resized my input images to be the size of one image from your hair dataset. As a result, <code>img.shape[0] - hair.shape[0]</code> and <code>img.shape[1] - hair.shape[1]</code> became zero. I fixed it now. Thanks anyway. :) </p>",
          "rawMarkdown": "Sorry for the lack of clarity :( I accidentally resized my input images to be the size of one image from your hair dataset. As a result, `img.shape[0] - hair.shape[0]` and `img.shape[1] - hair.shape[1]` became zero. I fixed it now. Thanks anyway. :) "
        }
      ]
    },
    {
      "id": 891805,
      "postDate": "2020-06-18T13:09:56.533Z",
      "content": "<p>Thank you for the nice work! I also think generated hair would improve the performance. Because many images have hair but others don't and it is seems irrelevant at all!</p>",
      "rawMarkdown": "Thank you for the nice work! I also think generated hair would improve the performance. Because many images have hair but others don't and it is seems irrelevant at all!"
    },
    {
      "id": 891624,
      "postDate": "2020-06-18T10:21:11.940Z",
      "content": "<p>That is actually a pretty neat idea. Surely would use it!</p>",
      "rawMarkdown": "That is actually a pretty neat idea. Surely would use it!"
    },
    {
      "id": 1844049,
      "postDate": "2022-07-05T10:09:32.967Z",
      "content": "<p>Thanks for sharing!</p>",
      "rawMarkdown": "Thanks for sharing!"
    },
    {
      "id": 901259,
      "postDate": "2020-06-25T10:45:52.253Z",
      "content": "<p>Thanks. Hair augs helped to gain score. </p>",
      "rawMarkdown": "Thanks. Hair augs helped to gain score. "
    }
  ],
  "comments": [
    {
      "id": 961899,
      "author_name": "Arnaud Roussel",
      "author_url": "",
      "post_date": "2020-08-07T15:38:53.730000",
      "content": "<p>This + Coarse dropout gave me a nice improvement in both CV and LB. Thanks. here is the albumentation friendly version:</p>\n<p>`<br>\nfrom albumentations.core.transforms_interface import ImageOnlyTransform<br>\nclass AdvancedHairAugmentation(ImageOnlyTransform):</p>\n<pre><code>def __init__(self, hairs: int = 4, hairs_folder: str = \"\", always_apply=False, p=0.5):\n    super(AdvancedHairAugmentation, self).__init__(always_apply=always_apply, p=p)\n    self.hairs = hairs\n    self.hairs_folder = hairs_folder\n\ndef apply(self, image, **params):\n    n_hairs = random.randint(0, self.hairs)\n\n    if not n_hairs:\n        return image\n\n    height, width, _ = image.shape  # target image width and height\n    hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n    for _ in range(n_hairs):\n        hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n        hair = cv2.cvtColor(hair, cv2.COLOR_BGR2RGB)\n        hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n        hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n        h_height, h_width, _ = hair.shape  # hair image width and height\n        roi_ho = random.randint(0, image.shape[0] - hair.shape[0])\n        roi_wo = random.randint(0, image.shape[1] - hair.shape[1])\n        roi = image[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n        img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n        ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n        mask_inv = cv2.bitwise_not(mask)\n        img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n        hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n        dst = cv2.add(img_bg, hair_fg)\n        image[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n    return image\n\ndef get_params_dependent_on_targets(self, params):\n    return {}\n\n@property\ndef targets_as_params(self):\n    return [\"image\"]\n\ndef get_transform_init_args_names(self):\n    return ()`\n</code></pre>",
      "votes": 9,
      "replies": [
        {
          "id": 962035,
          "author_name": "yimacs",
          "author_url": "",
          "post_date": "2020-08-07T18:30:36.953000",
          "content": "<p>thanks. i was migrating to albumentations from torchvision. it's great timing. </p>\n\n<p>it only works for image size &gt; 256 tho. there are hairs with size &gt; 128, so </p>\n\n<p>andom.randint(0, image.shape[0] - hair.shape[0])</p>\n\n<p>would break. </p>",
          "votes": 0,
          "replies": []
        },
        {
          "id": 962123,
          "author_name": "Arnaud Roussel",
          "author_url": "",
          "post_date": "2020-08-07T20:40:16.360000",
          "content": "<p><a href=\"https://www.kaggle.com/yimacs\" target=\"_blank\">@yimacs</a> Good catch. I was only using it on 256 x 256 at this stage. Will work a bit on the code as it was a straight copy paste from a post here.</p>",
          "votes": 0,
          "replies": []
        }
      ]
    },
    {
      "id": 961813,
      "author_name": "Shogo Araki",
      "author_url": "",
      "post_date": "2020-08-07T14:16:42.090000",
      "content": "<p>White hairs we made.\n<a href=\"https://www.kaggle.com/shogoaraki/whitehairs\">https://www.kaggle.com/shogoaraki/whitehairs</a></p>",
      "votes": 4,
      "replies": []
    },
    {
      "id": 896937,
      "author_name": "PAB97",
      "author_url": "",
      "post_date": "2020-06-22T14:29:29.680000",
      "content": "<p>Very cool idea! I just quickly tweak the code so that the augmentation is a subclass of albumentations augmentation. I think it is more rigorous this way and more easily fit in albumentations pipeline. </p>\n\n<p>`\nclass HairAugmentation(albumentations.ImageOnlyTransform):\n    def <strong>init</strong>(self, max_hairs:int = 4, hairs_folder: str = \"/kaggle/input/melanoma-hairs\", p=0.5):\n        super().<strong>init</strong>(p=p)\n        self.max_hairs = max_hairs\n        self.hairs_folder = hairs_folder</p>\n\n<pre><code>def apply(self, img, **params):\n    n_hairs = random.randint(0, self.max_hairs)\n\n    if not n_hairs:\n        return img\n\n    height, width, _ = img.shape  # target image width and height\n    hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n    for _ in range(n_hairs):\n        hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n        hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n        hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n        h_height, h_width, _ = hair.shape  # hair image width and height\n        hair = cv2.resize(hair, (int(h_width*0.8), int(h_height*0.8)))\n\n        h_height, h_width, _ = hair.shape  # hair image width and height\n        roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n        roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n        roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n        img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n        ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n        mask_inv = cv2.bitwise_not(mask)\n        img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n        hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n        dst = cv2.add(img_bg, hair_fg)\n        img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n    return img\n</code></pre>\n\n<p>`</p>",
      "votes": 3,
      "replies": [
        {
          "id": 898122,
          "author_name": "Satwik",
          "author_url": "",
          "post_date": "2020-06-23T10:17:37.227000",
          "content": "<p>I had a few errors in this one , so I tweaked it a bit more to make it work- \nHere's the albumentations version I'm using: </p>\n\n<p>`class AdvancedHairAugmentation(ImageOnlyTransform):</p>\n\n<pre><code>def __init__(self, hairs: int = 5, hairs_folder: str = \"\" , always_apply=False, p=0.5):\n    self.hairs = hairs\n    self.hairs_folder = hairs_folder\n    super().__init__(always_apply, p)\n\ndef apply(self, img, **params):\n    n_hairs = random.randint(0, self.hairs)\n\n    if not n_hairs:\n        return img\n\n    height, width, _ = img.shape  # target image width and height\n    hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n    for _ in range(n_hairs):\n        hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n        hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n        hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n        h_height, h_width, _ = hair.shape  # hair image width and height\n        roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n        roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n        roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n        img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n        ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n        mask_inv = cv2.bitwise_not(mask)\n        img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n        hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n        dst = cv2.add(img_bg, hair_fg, dtype=cv2.CV_64F)\n        img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n    return img`\n</code></pre>",
          "votes": 2,
          "replies": []
        }
      ]
    },
    {
      "id": 890407,
      "author_name": "yash chaudhary",
      "author_url": "",
      "post_date": "2020-06-17T13:48:44.233000",
      "content": "<p>Interesting , would use it. Nice work, thanks !</p>",
      "votes": 2,
      "replies": []
    },
    {
      "id": 956745,
      "author_name": "srv",
      "author_url": "",
      "post_date": "2020-08-03T18:41:12.220000",
      "content": "<p>This is a great augmentation technique, would like to try this. </p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 929967,
      "author_name": "Bhaskar Dey",
      "author_url": "",
      "post_date": "2020-07-15T05:29:41.527000",
      "content": "<p>What happens if img is a tensor?</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 925582,
      "author_name": "Wici777",
      "author_url": "",
      "post_date": "2020-07-12T07:01:24.270000",
      "content": "<p>Nice</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 912050,
      "author_name": "Michael Ding",
      "author_url": "",
      "post_date": "2020-07-02T07:15:14.187000",
      "content": "<p>Hey! Thanks for this; I've been trying out the code but for some reason all my hair strands are coming out as blue. Any ideas as to why this might be?</p>",
      "votes": 0,
      "replies": [
        {
          "id": 916521,
          "author_name": "Alexey Pronin",
          "author_url": "",
          "post_date": "2020-07-05T18:10:32.433000",
          "content": "<p><a href=\"/fallswatt1\">@fallswatt1</a> OpenCV assumes that it is given images in the <code>BGR</code> format. But Roman's images are in the <code>RGB</code> format. So you need to fix the color scheme. It can be easily done as follows. After your read the hair image with something like  <code>hair_img=cv2.imread(hair_img_path)</code> convert the <code>BGR</code> colors to <code>RGB</code> 's by calling <code>hair_img = cv2.cvtColor(hair_img, cv2.COLOR_BGR2RGB)</code></p>",
          "votes": 5,
          "replies": []
        }
      ]
    },
    {
      "id": 911283,
      "author_name": "Licheng Zhang",
      "author_url": "",
      "post_date": "2020-07-01T16:36:13.853000",
      "content": "<p>Thanks for sharing, it is interesting. But on the other hand, have you tried remove the hairs from the image? Do you think it would help with the classification since hairs probably don’t relate to the disease? </p>",
      "votes": 0,
      "replies": [
        {
          "id": 911356,
          "author_name": "sajwankit",
          "author_url": "",
          "post_date": "2020-07-01T17:12:49.760000",
          "content": "<p>I think people were having hard time removing hair from images, as suggested in the referred <a href=\"https://arxiv.org/pdf/1809.02568.pdf\">paper</a>:</p>\n\n<p>&gt; Body hair augmentation Based on our observation, the lesion image showsthat there are samples with body hair overlapping in the lesion area. <strong><em>Several hair removal methodologies have been proposed to address this issue. However, some problems such as how to interpolate overlapped part were left behind</em>.</strong> So we take the opposite approach. We propose a body hair augmentation which applies pseudo body hair to skin lesion images. Body hair augmentation is based on Buffon’s needele [11] and gives a line simulating body hair in a pseudo manner. Example of body hair augmentation is show in Fig. 4.</p>",
          "votes": 1,
          "replies": []
        },
        {
          "id": 912760,
          "author_name": "Licheng Zhang",
          "author_url": "",
          "post_date": "2020-07-02T17:52:59.770000",
          "content": "<p>I see, thanks, I think it makes sense that removing hair is difficult. </p>",
          "votes": 0,
          "replies": []
        }
      ]
    },
    {
      "id": 911197,
      "author_name": "Abhishek Bhat",
      "author_url": "",
      "post_date": "2020-07-01T15:55:37.647000",
      "content": "<p>How could we perform masking on tf.image format. To be more precise, how would we implement the following line for tf.image \n<code>img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)</code></p>\n\n<p>The bitwise and can be achieved through <code>tf.bitwise.bitwise_and()</code>, but how do we perform masking</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 909443,
      "author_name": "PC Jimmmy",
      "author_url": "",
      "post_date": "2020-06-30T15:40:23.853000",
      "content": "<p>Working on a script to remove hair and noticed that my script also removes the mm scale marking that is common to many of the images.  Wonder if adding that scale to images as an augment would help?   Think I have a PC with Photoshop loaded - will see if I can extract a few scale images to add to the hairs.</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 904010,
      "author_name": "G_R_S",
      "author_url": "",
      "post_date": "2020-06-27T09:17:33.003000",
      "content": "<p>Title really cracked me up. LMAO</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 898868,
      "author_name": "PC Jimmmy",
      "author_url": "",
      "post_date": "2020-06-23T19:56:51.607000",
      "content": "<p>Very nice - it worked pretty much as you wrote it for a tensorflow model.  I changed it to include a probability.   Running the model now (512x512) on my PC so will not know it's impact until tomorrow.  (PS I really hate how hard it is to paste code into these messages- I finally gave up on getting it to look right)</p>\n\n<p>`hairs_folder = '/home/james/my_server/Melanoma/hairs'</p>\n\n<p><code>class AdvancedHairAugmentation:\n    def __init__(self, hairs: int = 4, hairs_folder: str = \"\", p: float = 0.5):\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder\n        self.p = p</code></p>\n\n<pre><code>def __call__(self, img):\n    if random.random() &lt; self.p:\n        n_hairs = random.randint(0, self.hairs)\n\n        if not n_hairs:\n            return img\n\n        # height, width, _ = img.shape  # target image width and height\n        hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n        for _ in range(n_hairs):\n            hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n            hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n            hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n            h_height, h_width, _ = hair.shape  # hair image width and height\n            roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n            roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n            img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n            ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n            mask_inv = cv2.bitwise_not(mask)\n            img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n            hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n            dst = cv2.add(img_bg, hair_fg)\n            img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n        return img\n</code></pre>",
      "votes": 0,
      "replies": [
        {
          "id": 900642,
          "author_name": "PC Jimmmy",
          "author_url": "",
          "post_date": "2020-06-25T01:02:16.977000",
          "content": "<p>Hair worked - but Microscope did not help - it hurt. <br>\nAny more hairs added to the dataset in the near future ?</p>",
          "votes": 1,
          "replies": []
        },
        {
          "id": 900980,
          "author_name": "Roman",
          "author_url": "",
          "post_date": "2020-06-25T07:28:08.697000",
          "content": "<p>Yes, will add more hairs soon.\nMicroscope is a little tricky. I managed to gain from it, but it takes some time and effort.</p>",
          "votes": 2,
          "replies": []
        },
        {
          "id": 903606,
          "author_name": "",
          "author_url": "",
          "post_date": "2020-06-27T01:36:10.723000",
          "content": "",
          "votes": 0,
          "replies": []
        }
      ]
    },
    {
      "id": 895315,
      "author_name": "Satya Muralidhar",
      "author_url": "",
      "post_date": "2020-06-21T08:51:20.583000",
      "content": "<p>nice work</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 893480,
      "author_name": "Tahsin Mostafiz",
      "author_url": "",
      "post_date": "2020-06-19T16:35:36.430000",
      "content": "<p>Nice augmentation. However it fails when randomly selected hair image and input image have the same dimensions. The problem originates from the following lines:</p>\n\n<p><code>roi_ho = random.randint(0, img.shape[0] - hair.shape[0]);\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])</code></p>",
      "votes": 0,
      "replies": [
        {
          "id": 893568,
          "author_name": "Roman",
          "author_url": "",
          "post_date": "2020-06-19T18:12:11.370000",
          "content": "<p>I honestly did my best to understand what you have said but I did not.\nWhat deminsions are you talking about?</p>",
          "votes": 1,
          "replies": []
        },
        {
          "id": 893743,
          "author_name": "Tahsin Mostafiz",
          "author_url": "",
          "post_date": "2020-06-19T21:06:01.813000",
          "content": "<p>Sorry for the lack of clarity :( I accidentally resized my input images to be the size of one image from your hair dataset. As a result, <code>img.shape[0] - hair.shape[0]</code> and <code>img.shape[1] - hair.shape[1]</code> became zero. I fixed it now. Thanks anyway. :) </p>",
          "votes": 0,
          "replies": []
        }
      ]
    },
    {
      "id": 891805,
      "author_name": "raoby",
      "author_url": "",
      "post_date": "2020-06-18T13:09:56.533000",
      "content": "<p>Thank you for the nice work! I also think generated hair would improve the performance. Because many images have hair but others don't and it is seems irrelevant at all!</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 891624,
      "author_name": "Nitesh Chaudhry",
      "author_url": "",
      "post_date": "2020-06-18T10:21:11.940000",
      "content": "<p>That is actually a pretty neat idea. Surely would use it!</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 1844049,
      "author_name": "Hleb Jakimovič",
      "author_url": "",
      "post_date": "2022-07-05T10:09:32.967000",
      "content": "<p>Thanks for sharing!</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 901259,
      "author_name": "Aykhan.py [dsmlkz]",
      "author_url": "",
      "post_date": "2020-06-25T10:45:52.253000",
      "content": "<p>Thanks. Hair augs helped to gain score. </p>",
      "votes": 0,
      "replies": []
    }
  ],
  "raw_markdown_by_id": {
    "888935": "After reading [this paper](https://arxiv.org/pdf/1809.02568.pdf) I got inspired by a hair augmentation as another way to diversify a dataset. So I have implemented my version of this 'pseudo hair' augmentation technique in 19th version of [my kernel](https://www.kaggle.com/nroman/melanoma-pytorch-starter-efficientnet?scriptVersionId=36430890). And it definitely worked, giving a better score on the leader board (although I am still struggling to come up with a proper validation which result would conform with the LB score).\n\nBut if you think about this 'pseudo hair' technique - it is nothing else than just a cutout, simply implemented in a slightly different way. So I've decided to go further and implement an advanced hair augmentation technique: to take an image of an actual hair and impose it to the image from the dataset.\n\nIn order to do that I have created a dataset with images of only hairs (I manually cut them out from some of the images from the dataset. For now there are only 5 of them but I will add more later): https://www.kaggle.com/nroman/melanoma-hairs\n\nIn here is the augmentation class:\n\n```\nclass AdvancedHairAugmentation:\n    def __init__(self, hairs: int = 4, hairs_folder: str = \"\"):\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder\n\n    def __call__(self, img):\n        n_hairs = random.randint(0, self.hairs)\n        \n        if not n_hairs:\n            return img\n        \n        height, width, _ = img.shape  # target image width and height\n        hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n        \n        for _ in range(n_hairs):\n            hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n            hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n            hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n            h_height, h_width, _ = hair.shape  # hair image width and height\n            roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n            roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n            img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n            ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n            mask_inv = cv2.bitwise_not(mask)\n            img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n            hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n            dst = cv2.add(img_bg, hair_fg)\n            img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n                \n        return img\n```\n\nAnd the result of the augmentation:\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F1696976%2F1f2bb0d323573d9ac53a87cbe50f2fd1%2Faugmented_hairs.png?generation=1592326051386966&amp;alt=media)\n\n'Pseudo hairs' technique for the comparison:\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F1696976%2F41a66a688b7f23aff91edc2e0b2797f4%2Faug_old.png?generation=1592326308016210&amp;alt=media)\n",
    "961899": "This + Coarse dropout gave me a nice improvement in both CV and LB. Thanks. here is the albumentation friendly version:\n\n`\nfrom albumentations.core.transforms_interface import ImageOnlyTransform\nclass AdvancedHairAugmentation(ImageOnlyTransform):\n\n    def __init__(self, hairs: int = 4, hairs_folder: str = \"\", always_apply=False, p=0.5):\n        super(AdvancedHairAugmentation, self).__init__(always_apply=always_apply, p=p)\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder\n\n    def apply(self, image, **params):\n        n_hairs = random.randint(0, self.hairs)\n\n        if not n_hairs:\n            return image\n\n        height, width, _ = image.shape  # target image width and height\n        hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n        for _ in range(n_hairs):\n            hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n            hair = cv2.cvtColor(hair, cv2.COLOR_BGR2RGB)\n            hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n            hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n            h_height, h_width, _ = hair.shape  # hair image width and height\n            roi_ho = random.randint(0, image.shape[0] - hair.shape[0])\n            roi_wo = random.randint(0, image.shape[1] - hair.shape[1])\n            roi = image[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n            img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n            ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n            mask_inv = cv2.bitwise_not(mask)\n            img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n            hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n            dst = cv2.add(img_bg, hair_fg)\n            image[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n        return image\n\n    def get_params_dependent_on_targets(self, params):\n        return {}\n\n    @property\n    def targets_as_params(self):\n        return [\"image\"]\n\n    def get_transform_init_args_names(self):\n        return ()`",
    "961813": "White hairs we made.\nhttps://www.kaggle.com/shogoaraki/whitehairs",
    "896937": "Very cool idea! I just quickly tweak the code so that the augmentation is a subclass of albumentations augmentation. I think it is more rigorous this way and more easily fit in albumentations pipeline. \n\n`\nclass HairAugmentation(albumentations.ImageOnlyTransform):\n    def __init__(self, max_hairs:int = 4, hairs_folder: str = \"/kaggle/input/melanoma-hairs\", p=0.5):\n        super().__init__(p=p)\n        self.max_hairs = max_hairs\n        self.hairs_folder = hairs_folder\n\n    def apply(self, img, **params):\n        n_hairs = random.randint(0, self.max_hairs)\n\n        if not n_hairs:\n            return img\n\n        height, width, _ = img.shape  # target image width and height\n        hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n        for _ in range(n_hairs):\n            hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n            hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n            hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n            \n            h_height, h_width, _ = hair.shape  # hair image width and height\n            hair = cv2.resize(hair, (int(h_width*0.8), int(h_height*0.8)))\n            \n            h_height, h_width, _ = hair.shape  # hair image width and height\n            roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n            roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n            img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n            ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n            mask_inv = cv2.bitwise_not(mask)\n            img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n            hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n            dst = cv2.add(img_bg, hair_fg)\n            img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n        return img\n`",
    "890407": "Interesting , would use it. Nice work, thanks !",
    "956745": "This is a great augmentation technique, would like to try this. ",
    "929967": "What happens if img is a tensor?",
    "925582": "Nice",
    "912050": "Hey! Thanks for this; I've been trying out the code but for some reason all my hair strands are coming out as blue. Any ideas as to why this might be?",
    "911283": "Thanks for sharing, it is interesting. But on the other hand, have you tried remove the hairs from the image? Do you think it would help with the classification since hairs probably don’t relate to the disease? ",
    "911197": "How could we perform masking on tf.image format. To be more precise, how would we implement the following line for tf.image \n`img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)`\n\nThe bitwise and can be achieved through `tf.bitwise.bitwise_and()`, but how do we perform masking\n\n",
    "909443": "Working on a script to remove hair and noticed that my script also removes the mm scale marking that is common to many of the images.  Wonder if adding that scale to images as an augment would help?   Think I have a PC with Photoshop loaded - will see if I can extract a few scale images to add to the hairs.",
    "904010": "Title really cracked me up. LMAO",
    "898868": "Very nice - it worked pretty much as you wrote it for a tensorflow model.  I changed it to include a probability.   Running the model now (512x512) on my PC so will not know it's impact until tomorrow.  (PS I really hate how hard it is to paste code into these messages- I finally gave up on getting it to look right)\n\n`hairs_folder = '/home/james/my_server/Melanoma/hairs'\n\n`class AdvancedHairAugmentation:\n    def __init__(self, hairs: int = 4, hairs_folder: str = \"\", p: float = 0.5):\n        self.hairs = hairs\n        self.hairs_folder = hairs_folder\n        self.p = p`\n\n    def __call__(self, img):\n        if random.random() &lt; self.p:\n            n_hairs = random.randint(0, self.hairs)\n\n            if not n_hairs:\n                return img\n\n            # height, width, _ = img.shape  # target image width and height\n            hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im]\n\n            for _ in range(n_hairs):\n                hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images)))\n                hair = cv2.flip(hair, random.choice([-1, 0, 1]))\n                hair = cv2.rotate(hair, random.choice([0, 1, 2]))\n\n                h_height, h_width, _ = hair.shape  # hair image width and height\n                roi_ho = random.randint(0, img.shape[0] - hair.shape[0])\n                roi_wo = random.randint(0, img.shape[1] - hair.shape[1])\n                roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n                img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY)\n                ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n                mask_inv = cv2.bitwise_not(mask)\n                img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n                hair_fg = cv2.bitwise_and(hair, hair, mask=mask)\n\n                dst = cv2.add(img_bg, hair_fg)\n                img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n\n            return img\n  ",
    "895315": "nice work",
    "893480": "Nice augmentation. However it fails when randomly selected hair image and input image have the same dimensions. The problem originates from the following lines:\n\n`          roi_ho = random.randint(0, img.shape[0] - hair.shape[0]);\n            roi_wo = random.randint(0, img.shape[1] - hair.shape[1])`",
    "891805": "Thank you for the nice work! I also think generated hair would improve the performance. Because many images have hair but others don't and it is seems irrelevant at all!",
    "891624": "That is actually a pretty neat idea. Surely would use it!",
    "1844049": "Thanks for sharing!",
    "901259": "Thanks. Hair augs helped to gain score. "
  }
}