Back to Projects
Computer Vision Project

Facial Keypoints Detection

Download public Facial Keypoints training/test zips plus IdLookupTable, parse 96x96 image strings, train a baseline neural network, and create submission.csv.

Final deliverable: A computer-vision regression notebook that predicts facial landmark coordinates and writes RowId/Location submission.csv.

Dataset: Facial Keypoints Detection public mirror

The notebook downloads public training.zip, test.zip, and IdLookupTable.csv mirrors for the Kaggle Facial Keypoints Detection task. Training rows contain an Image string plus keypoint coordinate columns.

  • Image: space-separated 96x96 grayscale pixel values
  • keypoint columns such as left_eye_center_x and nose_tip_y: target coordinates
  • IdLookupTable.csv: maps each RowId to an ImageId and feature name required for submission
Step 1

Download Facial Keypoints Data

Download and extract the public facial keypoints zip.

Your task

Run the public download cell and inspect available CSV files.

Try writing this cell
# Run this cell in Colab, Kaggle, or a local notebook.
# It downloads public dataset files directly, so you do not need Kaggle credentials.

from pathlib import Path
from urllib.request import urlretrieve
import zipfile

DATA_DIR = Path("data/facial-keypoints-detection")
DATA_DIR.mkdir(parents=True, exist_ok=True)

def find_file(name):
    possible_roots = [DATA_DIR, Path("/kaggle/input")]
    for root in possible_roots:
        if root.exists():
            matches = list(root.rglob(name))
            if matches:
                return matches[0]
    raise FileNotFoundError(f"Could not find {name}. Run the dataset download cell first.")

files = {
    "training.zip": "https://raw.githubusercontent.com/ruchawaghulde/Facial-Keypoints-Detection/master/Data/training.zip",
    "test.zip": "https://raw.githubusercontent.com/ruchawaghulde/Facial-Keypoints-Detection/master/Data/test.zip",
    "IdLookupTable.csv": "https://raw.githubusercontent.com/ruchawaghulde/Facial-Keypoints-Detection/master/Data/IdLookupTable.csv"
}

for filename, url in files.items():
    target = DATA_DIR / filename
    if target.exists() and target.stat().st_size > 0:
        print(f"already exists: {target}")
        continue
    print(f"downloading {filename} ...")
    urlretrieve(url, target)
    print(f"saved: {target}")

for zip_path in DATA_DIR.glob("*.zip"):
    extract_dir = DATA_DIR / zip_path.stem
    extract_dir.mkdir(exist_ok=True)
    if any(extract_dir.iterdir()):
        print(f"already extracted: {extract_dir}")
        continue
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(extract_dir)
    print(f"extracted: {zip_path} -> {extract_dir}")

print("Files:")
all_files = [path for path in sorted(DATA_DIR.rglob("*")) if path.is_file()]
for path in all_files[:40]:
    print(path)
if len(all_files) > 40:
    print(f"... {len(all_files) - 40} more files not shown")
Expected output
A file list showing training.csv, test.csv, and IdLookupTable.csv after extraction/download.
Show reference solution
# Run this cell in Colab, Kaggle, or a local notebook.
# It downloads public dataset files directly, so you do not need Kaggle credentials.

from pathlib import Path
from urllib.request import urlretrieve
import zipfile

DATA_DIR = Path("data/facial-keypoints-detection")
DATA_DIR.mkdir(parents=True, exist_ok=True)

def find_file(name):
    possible_roots = [DATA_DIR, Path("/kaggle/input")]
    for root in possible_roots:
        if root.exists():
            matches = list(root.rglob(name))
            if matches:
                return matches[0]
    raise FileNotFoundError(f"Could not find {name}. Run the dataset download cell first.")

files = {
    "training.zip": "https://raw.githubusercontent.com/ruchawaghulde/Facial-Keypoints-Detection/master/Data/training.zip",
    "test.zip": "https://raw.githubusercontent.com/ruchawaghulde/Facial-Keypoints-Detection/master/Data/test.zip",
    "IdLookupTable.csv": "https://raw.githubusercontent.com/ruchawaghulde/Facial-Keypoints-Detection/master/Data/IdLookupTable.csv"
}

for filename, url in files.items():
    target = DATA_DIR / filename
    if target.exists() and target.stat().st_size > 0:
        print(f"already exists: {target}")
        continue
    print(f"downloading {filename} ...")
    urlretrieve(url, target)
    print(f"saved: {target}")

for zip_path in DATA_DIR.glob("*.zip"):
    extract_dir = DATA_DIR / zip_path.stem
    extract_dir.mkdir(exist_ok=True)
    if any(extract_dir.iterdir()):
        print(f"already extracted: {extract_dir}")
        continue
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(extract_dir)
    print(f"extracted: {zip_path} -> {extract_dir}")

print("Files:")
all_files = [path for path in sorted(DATA_DIR.rglob("*")) if path.is_file()]
for path in all_files[:40]:
    print(path)
if len(all_files) > 40:
    print(f"... {len(all_files) - 40} more files not shown")
Why this step matters

This project has a less standard submission format, so the first win is making the training CSV, test CSV, and lookup table visible.

Common mistake
Training predictions for keypoints but forgetting that Kaggle wants one RowId/Location row per requested coordinate.
Step 2

Load Images and Targets

Read the landmark CSV and identify image strings plus coordinate columns.

Your task

Load training.csv, print shape, inspect coordinate columns, and check missing values.

Try writing this cell
from pathlib import Path
import pandas as pd

possible_roots = [
    Path("data/facial-keypoints-detection"),
    Path("/kaggle/input"),
]

def find_file(name):
    for root in possible_roots:
        if root.exists():
            matches = list(root.rglob(name))
            if matches:
                return matches[0]
    raise FileNotFoundError(f"Could not find {name}. Run the dataset download/load cell first.")

train_path = find_file("training.csv")
df = pd.read_csv(train_path)

print(train_path)
print(df.shape)
df.head()

# Identify target columns

# Check missing values
Expected output
A training table with Image plus facial coordinate columns, many with missing labels.
Show reference solution
from pathlib import Path
import pandas as pd

possible_roots = [
    Path("data/facial-keypoints-detection"),
    Path("/kaggle/input"),
]

def find_file(name):
    for root in possible_roots:
        if root.exists():
            matches = list(root.rglob(name))
            if matches:
                return matches[0]
    raise FileNotFoundError(f"Could not find {name}. Run the dataset download/load cell first.")

train_path = find_file("training.csv")
df = pd.read_csv(train_path)

print(train_path)
print(df.shape)
df.head()

target_cols = [col for col in df.columns if col != "Image"]
display(df[target_cols[:8] + ["Image"]].head())
display(df[target_cols].isna().sum().sort_values(ascending=False).head())
print("number of coordinate columns:", len(target_cols))
Why this step matters

Each row stores the whole face as one long pixel string. The targets are named coordinates, so preserving those exact column names matters for submission.

Common mistake
Dropping the Image column too late or accidentally treating it as a target.
Step 3

Visualize Face Landmarks

Convert one Image string to a 96x96 array and overlay available keypoints.

Your task

Parse one image string, plot the face, and scatter x/y keypoint coordinates.

Try writing this cell
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

# Parse one Image string

# Overlay keypoints
Expected output
A face image with red landmark points on eyes, nose, mouth, and brows.
Show reference solution
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

clean_preview = df.dropna(subset=target_cols).copy()
row = clean_preview.iloc[0]
image = np.fromstring(row["Image"], sep=" ", dtype=np.float32).reshape(96, 96)

x_cols = [col for col in target_cols if col.endswith("_x")]
y_cols = [col.replace("_x", "_y") for col in x_cols]
xs = row[x_cols].to_numpy(dtype=np.float32)
ys = row[y_cols].to_numpy(dtype=np.float32)

plt.imshow(image, cmap="gray")
plt.scatter(xs, ys, c="red", s=10)
plt.title("Face with keypoints")
plt.axis("off")
plt.show()
Why this step matters

Computer-vision projects need visual sanity checks. A model cannot fix incorrectly parsed images or swapped x/y coordinates.

Common mistake
Forgetting to reshape 9216 pixel values into 96 by 96 before plotting.
Step 4

Prepare a Baseline Training Matrix

Create normalized image arrays and target coordinates for rows with complete labels.

Your task

Drop incomplete rows for the baseline, normalize pixels and coordinates, and split train/validation.

Try writing this cell
from sklearn.model_selection import train_test_split

# Drop incomplete target rows

# Create X image matrix and y target matrix with normalized coordinates

# Split train/validation
Expected output
X has 9216 pixel columns; y has one normalized column per coordinate target.
Show reference solution
import numpy as np
from sklearn.model_selection import train_test_split

clean = df.dropna(subset=target_cols).copy()

def parse_image_string(value):
    return np.fromstring(value, sep=" ", dtype=np.float32) / 255.0

X_images = np.vstack(clean["Image"].map(parse_image_string).to_numpy())
y_points = clean[target_cols].to_numpy(dtype=np.float32) / 96.0

X_train, X_val, y_train, y_val = train_test_split(
    X_images, y_points, test_size=0.2, random_state=42
)

print(X_train.shape, y_train.shape)
Why this step matters

Normalizing pixels and coordinates stabilizes neural-network training. Here every image is 96x96, so target coordinates can be divided by 96.

Common mistake
Training on rows with missing target coordinates without handling NaN values.
Step 5

Train a Small Neural Network

Train a baseline MLP that predicts all keypoint coordinates.

Your task

Use PyTorch to train a small fully connected network for coordinate regression.

Try writing this cell
# Train a small PyTorch model for keypoint regression
# Keep this baseline simple before trying CNNs
Expected output
Validation loss printed for each epoch, usually decreasing at first.
Show reference solution
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset

device = "cuda" if torch.cuda.is_available() else "cpu"

train_ds = TensorDataset(
    torch.tensor(X_train, dtype=torch.float32),
    torch.tensor(y_train, dtype=torch.float32),
)
val_x = torch.tensor(X_val, dtype=torch.float32).to(device)
val_y = torch.tensor(y_val, dtype=torch.float32).to(device)

loader = DataLoader(train_ds, batch_size=128, shuffle=True)

model = nn.Sequential(
    nn.Linear(96 * 96, 512),
    nn.ReLU(),
    nn.Dropout(0.2),
    nn.Linear(512, y_train.shape[1]),
).to(device)

optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
loss_fn = nn.MSELoss()

for epoch in range(8):
    model.train()
    for xb, yb in loader:
        xb, yb = xb.to(device), yb.to(device)
        optimizer.zero_grad()
        loss = loss_fn(model(xb), yb)
        loss.backward()
        optimizer.step()
    model.eval()
    with torch.no_grad():
        val_loss = loss_fn(model(val_x), val_y).item()
    print(epoch, round(val_loss, 5))
Why this step matters

This baseline is intentionally simple. It proves the data pipeline works before investing in CNN architecture.

Common mistake
Starting with a complicated CNN before verifying the target array and coordinate scaling.
Step 6

Create Facial Keypoints Submission

Predict test coordinates and save RowId/Location submission.csv using IdLookupTable.

Your task

Predict every test image, map requested feature names through IdLookupTable, clip coordinates, and save submission.csv.

Try writing this cell
# Predict keypoints for test images

# Use IdLookupTable.csv to create RowId/Location submission.csv
Expected output
submission.csv with RowId and Location columns.
Show reference solution
model.eval()
with torch.no_grad():
    val_pred = model(torch.tensor(X_val, dtype=torch.float32).to(device)).cpu().numpy()

val_rmse_pixels = np.sqrt(np.mean(((val_pred - y_val) * 96.0) ** 2))
print("validation RMSE in pixels:", round(float(val_rmse_pixels), 3))

test_df = pd.read_csv(find_file("test.csv"))
lookup = pd.read_csv(find_file("IdLookupTable.csv"))

X_test_images = np.vstack(test_df["Image"].map(parse_image_string).to_numpy())

with torch.no_grad():
    test_pred = model(torch.tensor(X_test_images, dtype=torch.float32).to(device)).cpu().numpy() * 96.0

pred_df = pd.DataFrame(test_pred, columns=target_cols)
pred_df["ImageId"] = np.arange(1, len(pred_df) + 1)

locations = []
for _, row in lookup.iterrows():
    image_id = int(row["ImageId"])
    feature_name = row["FeatureName"]
    value = pred_df.loc[pred_df["ImageId"] == image_id, feature_name].iloc[0]
    locations.append(value)

submission = pd.DataFrame({
    "RowId": lookup["RowId"],
    "Location": np.clip(locations, 0, 96),
})
submission.to_csv("submission.csv", index=False)
display(submission.head())
print("saved submission.csv", submission.shape)
Why this step matters

The lookup table expands each image into the exact coordinates Kaggle asks for. One image can produce many submission rows.

Common mistake
Writing one row per image instead of one row per requested RowId in IdLookupTable.