Skip to content

Latest commit

Β 

History

15 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ”€ 7uruf Vision

Arabic Handwritten Letter Recognition using CNNs

A personal learning project β€” building an OCR pipeline from scratch with PyTorch to recognize the 28 letters of the Arabic alphabet.


Why This Project

I wanted to understand deep learning by doing β€” not just reading. Arabic OCR felt right: it's a real problem, the dataset is manageable, and it forced me to think about image preprocessing, model architecture, and evaluation in a hands-on way.


Project Structure

ocr_7uruf/
β”œβ”€β”€ main.py                  # entry point β€” runs the full pipeline
β”œβ”€β”€ requirements.txt         # dependencies
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ config.py            # all hyperparameters & paths in one place
β”‚   β”œβ”€β”€ dataset.py           # data loading, transforms, train/val split
β”‚   β”œβ”€β”€ model.py             # CNN architecture definition
β”‚   β”œβ”€β”€ train.py             # training loop + validation
β”‚   β”œβ”€β”€ evaluate.py          # metrics, confusion matrix, reports
β”‚   └── utils.py             # seed, gpu check, helpers
β”œβ”€β”€ notebooks/
β”‚   β”œβ”€β”€ 01_data_exploration  # understanding the dataset
β”‚   β”œβ”€β”€ 02_model_experiments # testing different approaches
β”‚   └── 03_final_training    # full training run + results
β”œβ”€β”€ 7uruf_data/              # dataset (images organized by label)
└── outputs/                 # saved plots, reports, models

Architecture Overview

The big picture β€” how everything connects, from raw images to predictions:

graph TB
    subgraph ENTRY["πŸš€ Entry Point"]
        MAIN["main.py<br>orchestrates the full pipeline"]
    end

    subgraph CONFIG["βš™οΈ Configuration"]
        CFG["config.py<br>single source of truth"]
        CFG_PATHS["paths: data, models, outputs"]
        CFG_HYPER["hyperparams: lr, batch, epochs"]
        CFG_ARCH["architecture: channels, hidden size"]
        CFG_DEVICE["device: auto-detect GPU/CPU"]
        CFG --> CFG_PATHS
        CFG --> CFG_HYPER
        CFG --> CFG_ARCH
        CFG --> CFG_DEVICE
    end

    subgraph DATA["πŸ“ Data Pipeline"]
        RAW["7uruf_data/<br>raw .png images"]
        DS["dataset.py"]
        TRANSFORM["transforms<br>grayscale β†’ resize β†’ normalize"]
        AUG["augmentation<br>rotation, affine - train only"]
        SPLIT["train/val split<br>80/20, seeded"]
        LOADER["DataLoaders<br>batched, shuffled, pinned"]

        RAW --> DS
        DS --> TRANSFORM
        TRANSFORM --> AUG
        AUG --> SPLIT
        SPLIT --> LOADER
    end

    subgraph MODEL["🧠 CNN Model"]
        direction TB
        INPUT["Input<br>1Γ—64Γ—64 grayscale"]
        CONV1["Conv Block 1<br>Conv2d β†’ ReLU β†’ MaxPool<br>1β†’32 channels, 64β†’32"]
        CONV2["Conv Block 2<br>Conv2d β†’ ReLU β†’ MaxPool<br>32β†’64 channels, 32β†’16"]
        FLAT["Flatten<br>64Γ—16Γ—16 β†’ 16384"]
        FC1["FC Layer 1<br>16384 β†’ 128 + ReLU"]
        FC2["FC Layer 2<br>128 β†’ 28 classes"]
        OUTPUT["Output<br>28 scores, one per letter"]

        INPUT --> CONV1 --> CONV2 --> FLAT --> FC1 --> FC2 --> OUTPUT
    end

    subgraph TRAINING["πŸ‹οΈ Training Loop"]
        LOOP["train.py"]
        FWD["forward pass"]
        LOSS["CrossEntropyLoss"]
        BWD["backward pass"]
        OPT["Adam optimizer"]
        VAL["validation step<br>no gradients"]
        SAVE["save best model<br>by val accuracy"]

        LOOP --> FWD --> LOSS --> BWD --> OPT
        LOOP --> VAL
        VAL --> SAVE
    end

    subgraph EVAL["πŸ“Š Evaluation"]
        EV["evaluate.py"]
        ACC["accuracy"]
        CM["confusion matrix"]
        CR["classification report"]
        VIZ["prediction samples"]

        EV --> ACC
        EV --> CM
        EV --> CR
        EV --> VIZ
    end

    subgraph UTILS["πŸ”§ Utilities"]
        UT["utils.py"]
        SEED["set_seed()<br>reproducibility"]
        GPU["check_gpu()<br>CUDA detection"]

        UT --> SEED
        UT --> GPU
    end

    subgraph OUTPUTS["πŸ’Ύ Outputs"]
        BEST["models/best_model.pth"]
        PLOTS["outputs/training_history.png"]
        CMPLOT["outputs/confusion_matrix.png"]
        REPORT["outputs/classification_report.txt"]
    end

    MAIN -->|"step 0: setup"| UTILS
    MAIN -->|"step 1: load data"| DATA
    MAIN -->|"step 2: build model"| MODEL
    MAIN -->|"step 3: train"| TRAINING
    MAIN -->|"step 4: evaluate"| EVAL

    CFG -.->|"imported by all modules"| DATA
    CFG -.->|"imported by all modules"| MODEL
    CFG -.->|"imported by all modules"| TRAINING
    CFG -.->|"imported by all modules"| EVAL

    LOADER -->|"feeds"| TRAINING
    TRAINING -->|"trained model"| EVAL
    TRAINING -->|"weights"| OUTPUTS
    EVAL -->|"plots & reports"| OUTPUTS

    style ENTRY fill:#1a1a2e,stroke:#e94560,color:#fff
    style CONFIG fill:#16213e,stroke:#0f3460,color:#fff
    style DATA fill:#1a1a2e,stroke:#533483,color:#fff
    style MODEL fill:#16213e,stroke:#e94560,color:#fff
    style TRAINING fill:#1a1a2e,stroke:#0f3460,color:#fff
    style EVAL fill:#16213e,stroke:#533483,color:#fff
    style UTILS fill:#1a1a2e,stroke:#0f3460,color:#fff
    style OUTPUTS fill:#16213e,stroke:#e94560,color:#fff
Loading

CNN Architecture Detail

A closer look at how the model processes a single image:

graph LR
    subgraph INPUT
        I["πŸ–ΌοΈ Image<br>1 Γ— 64 Γ— 64"]
    end

    subgraph BLOCK1["Conv Block 1"]
        C1["Conv2d<br>3Γ—3, pad=1<br>1 β†’ 32 ch"]
        R1["ReLU"]
        P1["MaxPool<br>2Γ—2"]
    end

    subgraph BLOCK2["Conv Block 2"]
        C2["Conv2d<br>3Γ—3, pad=1<br>32 β†’ 64 ch"]
        R2["ReLU"]
        P2["MaxPool<br>2Γ—2"]
    end

    subgraph CLASSIFIER["Classifier"]
        FL["Flatten<br>16384"]
        F1["Linear<br>16384β†’128"]
        R3["ReLU"]
        F2["Linear<br>128β†’28"]
    end

    subgraph OUTPUT
        O["πŸ“ Prediction<br>28 Arabic letters"]
    end

    I --> C1 --> R1 --> P1 --> C2 --> R2 --> P2 --> FL --> F1 --> R3 --> F2 --> O

    style BLOCK1 fill:#2d3436,stroke:#6c5ce7,color:#fff
    style BLOCK2 fill:#2d3436,stroke:#6c5ce7,color:#fff
    style CLASSIFIER fill:#2d3436,stroke:#00b894,color:#fff
Loading

Training Pipeline Flow

What happens when you run python main.py:

sequenceDiagram
    participant M as main.py
    participant U as utils
    participant D as dataset
    participant N as model
    participant T as train
    participant E as evaluate

    M->>U: set_seed(42)
    M->>U: check_gpu()

    M->>D: create_data_loaders()
    D-->>M: train_loader, val_loader

    M->>N: ArabicCNN()
    N-->>M: model

    M->>T: train_model(model, loaders)
    loop each epoch
        T->>T: train_one_epoch()
        T->>T: validate()
        T->>T: save if best
    end
    T-->>M: history

    M->>E: evaluate_model()
    M->>E: plot_confusion_matrix()
    M->>E: plot_training_history()
    M->>E: get_classification_report()
    E-->>M: results saved to outputs/
Loading

Notebook Workflow

The exploration process before the final pipeline:

graph LR
    N1["πŸ““ 01<br>Data Exploration<br>understand the dataset<br>class distribution<br>sample visualization"]
    N2["πŸ““ 02<br>Model Experiments<br>test architectures<br>tune hyperparams<br>compare results"]
    N3["πŸ““ 03<br>Final Training<br>full run<br>best config<br>final evaluation"]
    PROD["⚑ main.py<br>production pipeline"]

    N1 -->|"insights"| N2 -->|"best approach"| N3 -->|"finalized into"| PROD

    style N1 fill:#2d3436,stroke:#fdcb6e,color:#fff
    style N2 fill:#2d3436,stroke:#e17055,color:#fff
    style N3 fill:#2d3436,stroke:#00b894,color:#fff
    style PROD fill:#2d3436,stroke:#e94560,color:#fff
Loading

Reflections

Structuring the project was harder than the model itself. Deciding what goes where β€” config vs. utils, dataset vs. transforms, what the notebooks should contain vs. what becomes production code β€” took more thought than expected.

What I struggled with:

  • Getting the tensor dimensions right through conv β†’ pool β†’ flatten β†’ linear (a single miscalculation breaks everything)
  • Finding the right balance between notebook exploration and clean modular code
  • Data augmentation choices β€” too much hurts, too little doesn't help

What was interesting:

  • Seeing how a simple 2-layer CNN can already distinguish 28 classes reasonably well
  • The confusion matrix reveals which letters the model confuses β€” often the ones that look similar to humans too
  • How much config.py as a single source of truth simplifies everything

Stack

Tool Purpose
PyTorch model, training, tensors
torchvision transforms, augmentation
matplotlib / seaborn visualization
scikit-learn metrics, confusion matrix
Pillow image loading
tqdm progress bars

keep coding greatness awaits

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages