Transform any photograph into a masterpiece β powered by VGG-19, Gram matrix optimization, and a production-grade full-stack deployment across Vercel, Render, and Hugging Face Spaces.
This is a production-grade Neural Style Transfer web application that applies the artistic style of famous paintings to your photographs using deep learning β in real time, from your browser.
This isn't a filter. This isn't a preset. This is genuine iterative pixel optimization β a frozen VGG-19 convolutional neural network repaints your image pixel-by-pixel, matching the texture statistics of a real painting using Gram matrix decomposition.
You upload a photo + You pick a painting β AI paints your photo in that style
πΈ Try it yourself β upload your own photo The app features a before/after drag slider with cinematic blur-reveal effect on every result.
β Add your own results: Drop before/after PNGs into
docs/results/and update this table.
Based on the landmark paper:
A Neural Algorithm of Artistic Style β Gatys, Ecker & Bethge, 2015
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NEURAL STYLE TRANSFER PIPELINE β
βββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β VGG-19 (weights FROZEN β never trained) β
β Content Image β β
β (your photo) ββββΌβββΊ conv1_1 βββββββββββββββββββββββββββΊ style loss β β
β β βΊ conv2_1 βββββββββββββββββββββββββββΊ style loss β‘ β
β Style Image β βΊ conv3_1 βββββββββββββββββββββββββββΊ style loss β’ β
β (the artwork) ββββΌβββΊ conv4_1 βββββββββββββββββββββββββββΊ style loss β£ β
β β βΊ conv4_2 βββββββββββββββββββββββββββΊ content loss β
β β βΊ conv5_1 βββββββββββββββββββββββββββΊ style loss β€ β
β β β
β β Style loss = MSE( Gram(F_gen), Gram(F_style) ) β
β β Content loss = MSE( F_gen[4_2], F_content[4_2] ) β
β β Total loss = Ξ± Β· content_loss + Ξ² Β· style_loss β
β β β
β Canvas image βββββΌββ L-BFGS optimizer (minimizes Total loss on PIXELS) β
β (starts as β ~300-400 iterations β
β content copy) β Ξ²/Ξ± β 1,000,000 (style dominates) β
βββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
Stylized Output β¨
The entire style capture mechanism is 3 lines of math:
def gram_matrix(features): # features shape: [B, C, H, W]
f = features.view(B * C, H * W) # flatten spatial dimensions
return torch.mm(f, f.t()) / (B*C*H*W) # channel correlation matrix
# Result: CΓC matrix where cell[i,j] = "do channels i and j co-activate?"
# β captures TEXTURE STATISTICS, completely ignoring spatial position
# β same Gram matrix = same artistic texture, any composition| Optimizer | Iterations to convergence | Use case |
|---|---|---|
| SGD | ~10,000+ | Large-scale training |
| Adam | ~2,000+ | Most deep learning |
| L-BFGS | ~300β400 | NST β small problem, needs curvature info |
L-BFGS uses quasi-Newton second-order information β like reading a topographic map instead of walking blindly. For NST's small pixel-space optimization, it converges 5β10Γ faster than Adam.
Browser (React + Vite)
β
β REST API (JSON + multipart/form-data)
βΌ
FastAPI Backend (Render β Python 3.11)
β
βββ POST /stylize ββββββββββββββββββββββΊ Background Thread
β ββ returns job_id instantly β
β β run_style_transfer()
βββ GET /status/{id} βββ poll / 3s βββββββββ β (3β8 min CPU)
β ββ progress 0.0 β 1.0 β
β β
βββ GET /result/{id} βββββ complete βββββββββββ
ββ returns stylized PNG
NST Engine (nst_engine.py)
βββ load_image() PIL β normalized tensor [1,3,H,W]
βββ ContentLoss MSE on conv4_2 feature maps
βββ StyleLoss MSE on Gram matrices (5 layers)
βββ build_model_and_losses() VGG-19 with loss modules inserted inline
βββ run_style_transfer() L-BFGS loop, progress callbacks
neural-style-transfer/
β
βββ π backend/
β βββ main.py # FastAPI β 6 endpoints, async job queue
β βββ nst_engine.py # VGG-19 NST core β content/style loss, L-BFGS
β βββ download_styles.py # Downloads 6 public domain artworks
β βββ requirements.txt
β βββ .python-version # Pins Python 3.11 for Render
β βββ uploads/ # Temp storage for incoming images
β βββ outputs/ # Generated stylized results
β βββ style_images/ # 6 preset masterworks (local only)
β
βββ βοΈ frontend/
β βββ src/
β β βββ App.jsx # State machine: idle β processing β done
β β βββ index.css # Wildflowers palette + dark/light mode vars
β β βββ components/
β β β βββ UploadPanel.jsx # Drag-and-drop with live preview
β β β βββ StyleGallery.jsx # 6-card preset art style picker
β β β βββ ResultViewer.jsx # Before/after blur-reveal slider + download
β β βββ hooks/
β β β βββ useJobPoller.js # Polls /status every 3s (custom hook)
β β βββ utils/
β β βββ api.js # All API calls in one place
β βββ public/style_images/ # Thumbnails served by Vercel CDN
β βββ vercel.json # Vercel deployment config
β βββ .env.production # Points to Render backend URL
β
βββ π notebook/
β βββ nst_colab.ipynb # GPU notebook β visualizes Gram matrices + loss curves
β
βββ π€ spaces/
β βββ app.py # Gradio UI for Hugging Face Spaces deployment
β
βββ βοΈ .github/
β βββ workflows/ci.yml # Test β Build β Deploy on every push to main
β βββ CONTRIBUTING.md
β βββ ISSUE_TEMPLATE.md
β βββ PULL_REQUEST_TEMPLATE.md
β
βββ π docs/
β βββ ARCHITECTURE.md # Deep-dive system design
β βββ results/ # Before/after example images
β
βββ requirements.txt # Root β used by Hugging Face Spaces
βββ .gitignore
βββ LICENSE # MIT
βββ README.md β you are here
| Platform | URL | Notes |
|---|---|---|
| π Vercel (React App) | neural-style-transfer-pied.vercel.app | Full UI, before/after slider |
| π‘ Render (API) | neural-style-transfer-api.onrender.com/docs | Interactive Swagger docs |
| π€ HF Spaces (Gradio) | huggingface.co/spaces/Tusharz/Neural-Style-Transfer | ML community demo |
| π Colab (Free GPU) | Open Notebook | T4 GPU, 30 sec/image |
Prerequisites: Python 3.10+ Β· Node.js 18+ Β· Git
# 1. Clone
git clone https://github.com/TUSHARTAMRAKAR/Neural-Style-Transfer.git
cd neural-style-transfer
# 2. Backend β Terminal 1
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
python download_styles.py # Downloads 6 artworks automatically
uvicorn main:app --reload --port 8000
# 3. Frontend β Terminal 2
cd frontend
npm install --legacy-peer-deps
npm run dev| URL | What |
|---|---|
http://localhost:5173 |
React web app |
http://localhost:8000/docs |
FastAPI interactive docs |
All endpoints are documented interactively at /docs (auto-generated by FastAPI).
GET / β Health check
GET /styles β List 6 preset artworks with metadata
POST /stylize β Submit job β returns job_id immediately
GET /status/{id} β Poll progress (0.0 β 1.0) + loss metrics
GET /result/{id} β Download stylized PNG
DEL /job/{id} β Clean up uploaded files
GET /jobs β List all jobs (debug)
# 1. Submit job
curl -X POST https://neural-style-transfer-api.onrender.com/stylize \
-F "content_image=@photo.jpg" \
-F "preset=starry_night" \
-F "num_steps=300"
# Response:
# { "job_id": "f3a2b1c4-...", "status": "pending" }
# 2. Poll until complete
curl https://neural-style-transfer-api.onrender.com/status/f3a2b1c4-...
# { "status": "processing", "progress": 0.47, "step": 141, "total_steps": 300,
# "content_loss": 1423.5, "style_loss": 98234.1 }
# 3. Download result
curl https://neural-style-transfer-api.onrender.com/result/f3a2b1c4-... \
--output stylized.png| Key | Artwork | Artist | Year | Content Weight | Style Weight |
|---|---|---|---|---|---|
starry_night |
The Starry Night | Van Gogh | 1889 | 1Γ10Β³ | 1Γ10βΉ |
the_scream |
The Scream | Edvard Munch | 1893 | 1Γ10Β³ | 8Γ10βΈ |
kandinsky |
Composition VIII | Kandinsky | 1923 | 5Γ10Β² | 1.5Γ10βΉ |
mosaic |
Ravenna Mosaic | Byzantine | 6th c. | 1Γ10Β³ | 1.2Γ10βΉ |
wave |
The Great Wave | Hokusai | 1831 | 1Γ10Β³ | 9Γ10βΈ |
udnie |
Udnie | Picabia | 1913 | 5Γ10Β² | 1Γ10βΉ |
π§ Portrait β steps: 300 Β· style: 80M Β· content: 15K (face preserved β
)
π Landscape β steps: 400 Β· style: 400M Β· content: 5K (bold effect β
)
π¨ Max Style β steps: 400 Β· style: 900M Β· content: 1K (full artistic β
)
| Layer | Technology | Version | Why This Choice |
|---|---|---|---|
| AI Core | PyTorch | 2.x | Industry standard, autograd, VGG-19 pretrained |
| Model | VGG-19 (ImageNet) | Pretrained | Sequential layers ideal for NST feature extraction |
| Backend | FastAPI | 0.110 | Async-native, auto-docs, Pydantic validation |
| Server | Uvicorn | 0.27 | ASGI, production-grade, supports --reload |
| Frontend | React | 18 | Component model, hooks, ecosystem |
| Build | Vite | 5.4 | 10Γ faster than CRA, HMR, tree-shaking |
| Styling | TailwindCSS | 3.4 | Utility-first, no CSS bloat |
| Fonts | Playfair Display + Inter + JetBrains Mono | β | Elegant display + clean body + precise mono |
| Notebook | Jupyter + Colab | β | Free T4 GPU, shareable, Gram matrix visualizations |
| Frontend Deploy | Vercel | β | Global CDN, instant deploy, env vars, free |
| Backend Deploy | Render | β | Docker-compatible, Python support, free tier |
| ML Demo | Hugging Face Spaces | β | Gradio, ML community, free CPU |
| CI/CD | GitHub Actions | β | Auto test + auto deploy on every push |
Why VGG-19 over ResNet, EfficientNet, or Vision Transformers?
VGG-19's simple sequential architecture is essential for NST. We need to insert loss modules at specific intermediate layers and collect gradients β ResNet's skip connections and ViT's attention blocks would complicate the gradient flow dramatically. VGG's uniform convβreluβpool structure gives us clean, hierarchical feature maps that perfectly separate low-level texture (early layers) from high-level structure (deep layers).
Why L-BFGS over Adam for optimization?
NST is an unusually small optimization problem β we're adjusting ~590K pixels (512Γ512Γ3) rather than millions of network parameters. L-BFGS's quasi-Newton line search with strong_wolfe conditions uses curvature information to take larger, smarter steps. It converges in ~300 iterations vs ~2000 for Adam. The max_iter=20 inner loop per step makes each outer iteration expensive but the total wall-clock time is dramatically lower.
Why does style_weight need to be 1,000,000Γ content_weight?
Raw MSE on content features (conv4_2: 512 channels Γ 32Γ32 = 524,288 values) produces much larger gradients than style loss (Gram matrix: 64Γ64 = 4,096 values per layer). Without the extreme ratio, content loss dominates and the result looks like "just contrast adjustment." The Ξ²/Ξ± β 1e6 ratio is what produces visible artistic transformation vs a subtle filter effect.
Why async job queue instead of synchronous response?
Style transfer takes 3β8 minutes on CPU. A synchronous HTTP response would timeout (default 30s), block the server thread, and give zero user feedback. The async job pattern β return job_id immediately, run NST in a background thread, poll /status/{id} every 3 seconds β is the correct architecture for any slow computation. It's what YouTube, Cloudinary, and every video/image processing service uses.
Why AvgPool instead of MaxPool in VGG-19?
We replace all MaxPool2d layers with AvgPool2d in the loss network. MaxPooling creates sharp edges in the gradient flow that produce visible artifacts in the stylized output. Average pooling produces smoother gradients β cleaner, more painterly results. This detail is from the original Gatys et al. paper and makes a visible quality difference.
| Hardware | Image Size | Steps | Time | Quality |
|---|---|---|---|---|
| CPU (Intel i7-12th gen) | 256Γ256 | 300 | ~3 min | Good |
| CPU (Intel i7-12th gen) | 384Γ384 | 400 | ~8 min | Great |
| GPU (T4 β Colab free) | 512Γ512 | 400 | ~35 sec | Excellent |
| GPU (RTX 3080) | 512Γ512 | 400 | ~12 sec | Excellent |
| GPU (A100 β Colab Pro) | 1024Γ1024 | 500 | ~45 sec | Maximum |
GitHub (source of truth)
β
ββββΊ GitHub Actions CI
β βββ Backend tests (Python)
β βββ Frontend build (Vite)
β βββ Lint checks
β
ββββΊ Vercel (automatic on push to main)
β βββ React frontend β global CDN
β env: VITE_API_URL=https://neural-style-transfer-api.onrender.com
β
ββββΊ Render (automatic on push to main)
β βββ FastAPI backend β Python 3.11
β uvicorn main:app --host 0.0.0.0 --port $PORT
β
ββββΊ Hugging Face Spaces (git push hf main)
βββ Gradio app β spaces/app.py
downloads style images at startup from GitHub raw URLs
Contributions are very welcome! Here's how:
# Fork β Clone β Branch
git checkout -b feature/your-feature
# Make changes, then test
cd backend && python nst_engine.py # smoke tests
cd frontend && npm run build # build check
# Commit with conventional commits
git commit -m "feat: add your feature"
git commit -m "fix: fix the thing"
git commit -m "docs: update readme"
# Push and open PR
git push origin feature/your-featureIdeas for contributions:
- Fast NST (feed-forward network β 100Γ faster inference)
- WebSocket real-time progress instead of polling
- User authentication + result history
- Additional style presets
- Mobile PWA wrapper
| Resource | Link |
|---|---|
| Original NST paper | Gatys et al., 2015 β arxiv.org/abs/1508.06576 |
| Fast NST paper | Johnson et al., 2016 β arxiv.org/abs/1603.08155 |
| VGG paper | Simonyan & Zisserman, 2014 β arxiv.org/abs/1409.1556 |
| PyTorch NST tutorial | pytorch.org/tutorials/advanced/neural_style_tutorial |
| FastAPI docs | fastapi.tiangolo.com |
| Vercel docs | vercel.com/docs |
Distributed under the MIT License β see LICENSE for details.
Free to use, modify, and distribute. Attribution appreciated but not required.
Built with passion Β· Deployed with precision Β· Painted with algorithms
Made with β€οΈ by Tushar Tamrakar
If this project helped you, please consider giving it a β β it means a lot!



