{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":83922,"databundleVersionId":9626915,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport cv2\nimport csv\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nfrom numpy.linalg import norm\n\nBASE_PATH = \"/kaggle/input/11785-hw-2-p-2-face-verification-fall-2024/11-785-f24-hw2p2-verification\"\nOUTPUT_PATH = \"/kaggle/working\"\n\n# ---- USTAWIENIA ----\nVER_FOLDER = os.path.join(BASE_PATH, \"ver_data\")\nVAL_PAIRS = os.path.join(BASE_PATH, \"val_pairs.txt\")\nTHRESHOLD = 0.30\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device)\n\n# POPRAWIONY DETEKTOR TWARZY\nclass SimpleFaceDetector(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        self.conv1 = nn.Conv2d(3, 16, 3, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(16)\n\n        self.conv2 = nn.Conv2d(16, 32, 3, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(32)\n\n        self.conv3 = nn.Conv2d(32, 64, 3, padding=1, bias=False)\n        self.bn3 = nn.BatchNorm2d(64)\n\n        self.pool = nn.AdaptiveAvgPool2d(1)\n        self.fc = nn.Linear(64, 4)\n\n    def forward(self, x):\n        x = F.relu(self.bn1(self.conv1(x)))\n        x = F.relu(self.bn2(self.conv2(x)))\n        x = F.relu(self.bn3(self.conv3(x)))\n\n        x = self.pool(x)\n        x = x.view(x.size(0), -1)\n\n        bbox = self.fc(x)\n        bbox = torch.clamp(bbox, 0, 250)\n        return bbox\n\n# RESIDUAL BLOCK (ResNet)\nclass ResidualBlock(nn.Module):\n    # Tworzymy własny blok ResNet dziedziczący po nn.Module\n    def __init__(self, in_ch, out_ch, stride=1):\n        super().__init__()  # Inicjalizacja klasy bazowej PyTorch\n\n        # Pierwsza warstwa konwolucyjna 3x3\n        # Może zmieniać liczbę kanałów i rozdzielczość (stride)\n        self.conv1 = nn.Conv2d(in_ch, out_ch, 3, stride=stride, padding=1, bias=False)\n\n        # BatchNorm dla stabilizacji i normalizacji wyjścia z conv1\n        self.bn1 = nn.BatchNorm2d(out_ch)\n\n        # Druga warstwa konwolucyjna 3x3\n        # Zwiększa reprezentacyjność cech\n        self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False)\n\n        # BatchNorm dla stabilizacji i normalizacji wyjścia z conv2\n        self.bn2 = nn.BatchNorm2d(out_ch)\n\n        # Funkcja aktywacji ReLU\n        self.relu = nn.ReLU(inplace=True)\n\n        # Skip connection – domyślnie brak (identity)\n        self.skip = None\n\n        # Jeśli liczba kanałów się zmienia lub stride !=1 (downsampling)\n        # trzeba dopasować wejście, aby można było je dodać do F(x)\n        if in_ch != out_ch or stride != 1:\n            self.skip = nn.Sequential(\n                nn.Conv2d(in_ch, out_ch, 1, stride=stride, bias=False),  # Conv 1x1 dopasowujący wymiary\n                nn.BatchNorm2d(out_ch)  # BatchNorm po conv\n            )\n\n    def forward(self, x):\n        identity = x  # Zachowujemy wejście do późniejszego dodania (skip connection)\n\n        # Pierwszy etap transformacji F(x): Conv -> BN -> ReLU\n        out = self.relu(self.bn1(self.conv1(x)))\n\n        # Drugi etap F(x): Conv -> BN (bez ReLU, ReLU dodamy po dodaniu skip)\n        out = self.bn2(self.conv2(out))\n\n        # Jeśli potrzeba, dopasowujemy identity do wymiarów F(x)\n        if self.skip is not None:\n            identity = self.skip(identity)\n\n        # Dodajemy wejście (identity) do transformacji F(x)\n        out += identity\n\n        # ReLU po dodaniu, klasyczna aktywacja w ResNet\n        return self.relu(out)\n\n\n# PRAWDZIWY FaceNet-style ResNet EMBEDDER\nclass SimpleFaceEmbedder(nn.Module):\n    def __init__(self, embedding_size=512):\n        super().__init__()  # Inicjalizacja klasy bazowej\n\n        # Stem – pierwsza warstwa przetwarzająca obraz\n        self.stem = nn.Sequential(\n            nn.Conv2d(3, 32, 3, padding=1, bias=False),  # Conv 3x3 z RGB na 32 kanały\n            nn.BatchNorm2d(32),                           # Normalizacja\n            nn.ReLU(inplace=True)                          # Aktywacja\n        )\n\n        # Warstwy ResNet (stages) – stopniowe zwiększanie liczby kanałów i downsampling\n        self.layer1 = ResidualBlock(32, 64, stride=2)     # 32 → 64 kanały, rozdzielczość /2\n        self.layer2 = ResidualBlock(64, 128, stride=2)    # 64 → 128 kanałów, rozdzielczość /2\n        self.layer3 = ResidualBlock(128, 256, stride=2)   # 128 → 256 kanałów, rozdzielczość /2\n        self.layer4 = ResidualBlock(256, 256)             # 256 → 256 kanałów, brak downsampling\n\n        # Global Average Pooling – redukcja wymiarów przestrzennych do 1x1\n        self.pool = nn.AdaptiveAvgPool2d(1)\n\n        # Fully connected – zamiana cech na embedding 512-wymiarowy\n        self.fc = nn.Linear(256, embedding_size)\n\n    def forward(self, x):\n        x = self.stem(x)          # Pierwsze wydobycie cech z obrazu\n\n        x = self.layer1(x)        # Przepuszczenie przez pierwszy blok ResNet\n        x = self.layer2(x)        # Drugi blok\n        x = self.layer3(x)        # Trzeci blok\n        x = self.layer4(x)        # Czwarty blok\n\n        x = self.pool(x)          # Global Average Pooling, tensor -> (batch, 256, 1, 1)\n        x = x.view(x.size(0), -1) # Spłaszczenie do wektora (batch, 256)\n\n        x = self.fc(x)            # FC -> embedding\n        x = F.normalize(x, p=2, dim=1)  # Normalizacja embeddingu do wektora jednostkowego\n        return x                  # Zwrócenie embeddingu twarzy\n\n# INICJALIZACJA\ndetector = SimpleFaceDetector().to(device)\nembedder = SimpleFaceEmbedder().to(device).eval()\n\n# EMBEDDING Z OBRAZU\ndef get_embedding(img_path):\n    img = cv2.imread(img_path)\n    if img is None:\n        raise FileNotFoundError(img_path)\n\n    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img_tensor = torch.tensor(img_rgb).float().permute(2, 0, 1).unsqueeze(0).to(device) / 255.0\n\n    # detekcja twarzy\n    bbox = detector(img_tensor)[0].detach().cpu().numpy()\n    x1, y1, x2, y2 = map(int, bbox)\n\n    if x2 <= x1 or y2 <= y1:\n        face_crop = img_rgb\n    else:\n        face_crop = img_rgb[y1:y2, x1:x2]\n\n    face_crop = cv2.resize(face_crop, (160, 160))\n    face_tensor = torch.tensor(face_crop).float().permute(2, 0, 1).unsqueeze(0).to(device) / 255.0\n\n    with torch.no_grad():\n        emb = embedder(face_tensor).cpu().numpy()[0]\n\n    return emb / norm(emb)\n\n# COSINE SIMILARITY\ndef cosine_similarity(a, b):\n    return float(np.dot(a, b))\n\n# WALIDACJA\ntotal = 0\ncorrect = 0\n\nwith open(VAL_PAIRS) as f:\n    reader = csv.reader(f, delimiter=' ')\n    for img1, img2, label in reader:\n        label = int(label)\n\n        try:\n            emb1 = get_embedding(f\"{VER_FOLDER}/{img1}\")\n            emb2 = get_embedding(f\"{VER_FOLDER}/{img2}\")\n        except Exception as e:\n            print(\"ERROR:\", e)\n            continue\n\n        sim = cosine_similarity(emb1, emb2)\n        pred = 1 if sim > THRESHOLD else 0\n\n        correct += (pred == label)\n        total += 1\n\n        print(f\"{img1} <-> {img2} | sim={sim:.4f} | gt={label} | pred={pred}\")\n\naccuracy = correct / total if total > 0 else 0","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-04T14:12:36.197566Z","iopub.execute_input":"2026-02-04T14:12:36.198560Z","iopub.status.idle":"2026-02-04T14:12:49.097483Z","shell.execute_reply.started":"2026-02-04T14:12:36.198521Z","shell.execute_reply":"2026-02-04T14:12:49.096696Z"}},"outputs":[],"execution_count":null}]}