Spaces:
Runtime error
Runtime error
Commit
Β·
ef62b3e
1
Parent(s):
0c16a3a
Add matcher
Browse files- matcher/__init__.py +0 -0
- matcher/__pycache__/__init__.cpython-313.pyc +0 -0
- matcher/__pycache__/admin.cpython-313.pyc +0 -0
- matcher/__pycache__/apps.cpython-313.pyc +0 -0
- matcher/__pycache__/models.cpython-313.pyc +0 -0
- matcher/__pycache__/similarity.cpython-313.pyc +0 -0
- matcher/__pycache__/urls.cpython-313.pyc +0 -0
- matcher/__pycache__/views.cpython-313.pyc +0 -0
- matcher/admin.py +0 -0
- matcher/apps.py +0 -0
- matcher/management/commands/__pycache__/populate_products.cpython-313.pyc +0 -0
- matcher/management/commands/populate_products.py +55 -0
- matcher/matcher/__init__.py +0 -0
- matcher/matcher/__pycache__/__init__.cpython-313.pyc +0 -0
- matcher/matcher/__pycache__/admin.cpython-313.pyc +0 -0
- matcher/matcher/__pycache__/apps.cpython-313.pyc +0 -0
- matcher/matcher/__pycache__/models.cpython-313.pyc +0 -0
- matcher/matcher/__pycache__/similarity.cpython-313.pyc +0 -0
- matcher/matcher/__pycache__/urls.cpython-313.pyc +0 -0
- matcher/matcher/__pycache__/views.cpython-313.pyc +0 -0
- matcher/matcher/admin.py +0 -0
- matcher/matcher/apps.py +0 -0
- matcher/matcher/management/commands/__pycache__/populate_products.cpython-313.pyc +0 -0
- matcher/matcher/management/commands/populate_products.py +55 -0
- matcher/matcher/migrations/0001_initial.py +32 -0
- matcher/matcher/migrations/__init__.py +0 -0
- matcher/matcher/migrations/__pycache__/0001_initial.cpython-313.pyc +0 -0
- matcher/matcher/migrations/__pycache__/__init__.cpython-313.pyc +0 -0
- matcher/matcher/models.py +14 -0
- matcher/matcher/similarity.py +66 -0
- matcher/matcher/tests.py +0 -0
- matcher/matcher/urls.py +6 -0
- matcher/matcher/views.py +76 -0
- matcher/migrations/0001_initial.py +32 -0
- matcher/migrations/__init__.py +0 -0
- matcher/migrations/__pycache__/0001_initial.cpython-313.pyc +0 -0
- matcher/migrations/__pycache__/__init__.cpython-313.pyc +0 -0
- matcher/models.py +14 -0
- matcher/similarity.py +66 -0
- matcher/tests.py +0 -0
- matcher/urls.py +6 -0
- matcher/views.py +76 -0
matcher/__init__.py
ADDED
|
File without changes
|
matcher/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (169 Bytes). View file
|
|
|
matcher/__pycache__/admin.cpython-313.pyc
ADDED
|
Binary file (166 Bytes). View file
|
|
|
matcher/__pycache__/apps.cpython-313.pyc
ADDED
|
Binary file (165 Bytes). View file
|
|
|
matcher/__pycache__/models.cpython-313.pyc
ADDED
|
Binary file (1.34 kB). View file
|
|
|
matcher/__pycache__/similarity.cpython-313.pyc
ADDED
|
Binary file (3.33 kB). View file
|
|
|
matcher/__pycache__/urls.cpython-313.pyc
ADDED
|
Binary file (345 Bytes). View file
|
|
|
matcher/__pycache__/views.cpython-313.pyc
ADDED
|
Binary file (3.64 kB). View file
|
|
|
matcher/admin.py
ADDED
|
File without changes
|
matcher/apps.py
ADDED
|
File without changes
|
matcher/management/commands/__pycache__/populate_products.cpython-313.pyc
ADDED
|
Binary file (3.43 kB). View file
|
|
|
matcher/management/commands/populate_products.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
#
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from django.core.management.base import BaseCommand
|
| 7 |
+
from django.conf import settings
|
| 8 |
+
from django.core.files import File
|
| 9 |
+
from matcher.models import Product
|
| 10 |
+
from matcher.similarity import extract_features
|
| 11 |
+
from PIL import Image as PILImage
|
| 12 |
+
|
| 13 |
+
class Command(BaseCommand):
|
| 14 |
+
help = "Populate the database with products from images in media/uploads/."
|
| 15 |
+
|
| 16 |
+
def handle(self, *args, **kwargs):
|
| 17 |
+
uploads_dir = os.path.join(settings.MEDIA_ROOT, "uploads")
|
| 18 |
+
|
| 19 |
+
if not os.path.exists(uploads_dir):
|
| 20 |
+
self.stdout.write(self.style.ERROR(f"Folder not found: {uploads_dir}"))
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
# Clear old products
|
| 24 |
+
Product.objects.all().delete()
|
| 25 |
+
self.stdout.write("Deleted old products.")
|
| 26 |
+
|
| 27 |
+
for filename in os.listdir(uploads_dir):
|
| 28 |
+
file_path = os.path.join(uploads_dir, filename)
|
| 29 |
+
|
| 30 |
+
if not filename.lower().endswith((".jpg", ".jpeg", ".png")):
|
| 31 |
+
continue
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
pil_image = PILImage.open(file_path).convert("RGB")
|
| 35 |
+
features = extract_features(pil_image)
|
| 36 |
+
|
| 37 |
+
product = Product(
|
| 38 |
+
name=os.path.splitext(filename)[0],
|
| 39 |
+
category="General",
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
with open(file_path, "rb") as f:
|
| 43 |
+
product.image.save(filename, File(f), save=False)
|
| 44 |
+
|
| 45 |
+
if features is not None:
|
| 46 |
+
product.feature_vector = features.tobytes()
|
| 47 |
+
|
| 48 |
+
product.save()
|
| 49 |
+
self.stdout.write(self.style.SUCCESS(f"Added {filename}"))
|
| 50 |
+
|
| 51 |
+
except Exception as e:
|
| 52 |
+
self.stdout.write(self.style.ERROR(f"Error processing {filename}: {e}"))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
self.stdout.write(self.style.SUCCESS("Finished populating products."))
|
matcher/matcher/__init__.py
ADDED
|
File without changes
|
matcher/matcher/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (169 Bytes). View file
|
|
|
matcher/matcher/__pycache__/admin.cpython-313.pyc
ADDED
|
Binary file (166 Bytes). View file
|
|
|
matcher/matcher/__pycache__/apps.cpython-313.pyc
ADDED
|
Binary file (165 Bytes). View file
|
|
|
matcher/matcher/__pycache__/models.cpython-313.pyc
ADDED
|
Binary file (1.34 kB). View file
|
|
|
matcher/matcher/__pycache__/similarity.cpython-313.pyc
ADDED
|
Binary file (3.33 kB). View file
|
|
|
matcher/matcher/__pycache__/urls.cpython-313.pyc
ADDED
|
Binary file (345 Bytes). View file
|
|
|
matcher/matcher/__pycache__/views.cpython-313.pyc
ADDED
|
Binary file (3.64 kB). View file
|
|
|
matcher/matcher/admin.py
ADDED
|
File without changes
|
matcher/matcher/apps.py
ADDED
|
File without changes
|
matcher/matcher/management/commands/__pycache__/populate_products.cpython-313.pyc
ADDED
|
Binary file (3.43 kB). View file
|
|
|
matcher/matcher/management/commands/populate_products.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
#
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from django.core.management.base import BaseCommand
|
| 7 |
+
from django.conf import settings
|
| 8 |
+
from django.core.files import File
|
| 9 |
+
from matcher.models import Product
|
| 10 |
+
from matcher.similarity import extract_features
|
| 11 |
+
from PIL import Image as PILImage
|
| 12 |
+
|
| 13 |
+
class Command(BaseCommand):
|
| 14 |
+
help = "Populate the database with products from images in media/uploads/."
|
| 15 |
+
|
| 16 |
+
def handle(self, *args, **kwargs):
|
| 17 |
+
uploads_dir = os.path.join(settings.MEDIA_ROOT, "uploads")
|
| 18 |
+
|
| 19 |
+
if not os.path.exists(uploads_dir):
|
| 20 |
+
self.stdout.write(self.style.ERROR(f"Folder not found: {uploads_dir}"))
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
# Clear old products
|
| 24 |
+
Product.objects.all().delete()
|
| 25 |
+
self.stdout.write("Deleted old products.")
|
| 26 |
+
|
| 27 |
+
for filename in os.listdir(uploads_dir):
|
| 28 |
+
file_path = os.path.join(uploads_dir, filename)
|
| 29 |
+
|
| 30 |
+
if not filename.lower().endswith((".jpg", ".jpeg", ".png")):
|
| 31 |
+
continue
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
pil_image = PILImage.open(file_path).convert("RGB")
|
| 35 |
+
features = extract_features(pil_image)
|
| 36 |
+
|
| 37 |
+
product = Product(
|
| 38 |
+
name=os.path.splitext(filename)[0],
|
| 39 |
+
category="General",
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
with open(file_path, "rb") as f:
|
| 43 |
+
product.image.save(filename, File(f), save=False)
|
| 44 |
+
|
| 45 |
+
if features is not None:
|
| 46 |
+
product.feature_vector = features.tobytes()
|
| 47 |
+
|
| 48 |
+
product.save()
|
| 49 |
+
self.stdout.write(self.style.SUCCESS(f"Added {filename}"))
|
| 50 |
+
|
| 51 |
+
except Exception as e:
|
| 52 |
+
self.stdout.write(self.style.ERROR(f"Error processing {filename}: {e}"))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
self.stdout.write(self.style.SUCCESS("Finished populating products."))
|
matcher/matcher/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.5 on 2025-08-26 08:59
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
initial = True
|
| 9 |
+
|
| 10 |
+
dependencies = [
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
operations = [
|
| 14 |
+
migrations.CreateModel(
|
| 15 |
+
name='Product',
|
| 16 |
+
fields=[
|
| 17 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 18 |
+
('name', models.CharField(max_length=255)),
|
| 19 |
+
('category', models.CharField(max_length=255)),
|
| 20 |
+
('image', models.ImageField(upload_to='products/')),
|
| 21 |
+
('feature_vector', models.BinaryField(blank=True, null=True)),
|
| 22 |
+
],
|
| 23 |
+
),
|
| 24 |
+
migrations.CreateModel(
|
| 25 |
+
name='UploadedImage',
|
| 26 |
+
fields=[
|
| 27 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 28 |
+
('image', models.ImageField(upload_to='uploads/')),
|
| 29 |
+
('uploaded_at', models.DateTimeField(auto_now_add=True)),
|
| 30 |
+
],
|
| 31 |
+
),
|
| 32 |
+
]
|
matcher/matcher/migrations/__init__.py
ADDED
|
File without changes
|
matcher/matcher/migrations/__pycache__/0001_initial.cpython-313.pyc
ADDED
|
Binary file (1.47 kB). View file
|
|
|
matcher/matcher/migrations/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (180 Bytes). View file
|
|
|
matcher/matcher/models.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.db import models
|
| 2 |
+
|
| 3 |
+
class Product(models.Model):
|
| 4 |
+
name = models.CharField(max_length=255)
|
| 5 |
+
category = models.CharField(max_length=255)
|
| 6 |
+
image = models.ImageField(upload_to='products/')
|
| 7 |
+
feature_vector = models.BinaryField(null=True, blank=True)
|
| 8 |
+
|
| 9 |
+
def __str__(self):
|
| 10 |
+
return self.name
|
| 11 |
+
|
| 12 |
+
class UploadedImage(models.Model):
|
| 13 |
+
image = models.ImageField(upload_to='uploads/')
|
| 14 |
+
uploaded_at = models.DateTimeField(auto_now_add=True)
|
matcher/matcher/similarity.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from transformers import ViTImageProcessor, ViTModel
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import numpy as np
|
| 5 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 6 |
+
import requests
|
| 7 |
+
from io import BytesIO
|
| 8 |
+
|
| 9 |
+
# We are now using a much smaller "tiny" version of the Vision Transformer because render was not able to handle and demanding to pay for larger models.
|
| 10 |
+
MODEL_NAME = 'WinKawaks/vit-tiny-patch16-224'
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
image_processor = ViTImageProcessor.from_pretrained(MODEL_NAME)
|
| 14 |
+
model = ViTModel.from_pretrained(MODEL_NAME)
|
| 15 |
+
except Exception as e:
|
| 16 |
+
print(f"Error loading model: {e}")
|
| 17 |
+
image_processor = None
|
| 18 |
+
model = None
|
| 19 |
+
|
| 20 |
+
def extract_features(image: Image.Image) -> np.ndarray | None:
|
| 21 |
+
"""
|
| 22 |
+
Extracts a feature vector from an image using the ViT model.
|
| 23 |
+
"""
|
| 24 |
+
if not model or not image_processor:
|
| 25 |
+
print("Model or image processor not loaded.")
|
| 26 |
+
return None
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
inputs = image_processor(images=image, return_tensors="pt")
|
| 30 |
+
|
| 31 |
+
with torch.no_grad():
|
| 32 |
+
outputs = model(**inputs)
|
| 33 |
+
|
| 34 |
+
last_hidden_states = outputs.last_hidden_state
|
| 35 |
+
features = last_hidden_states.mean(dim=1).cpu().numpy()
|
| 36 |
+
return features
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"An error occurred during feature extraction: {e}")
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
def find_similar_products(uploaded_features, all_products, top_n=20):
|
| 42 |
+
"""
|
| 43 |
+
Finds similar products by comparing feature vectors using cosine similarity.
|
| 44 |
+
"""
|
| 45 |
+
if uploaded_features is None or not all_products:
|
| 46 |
+
return []
|
| 47 |
+
|
| 48 |
+
product_features = np.array([np.frombuffer(p.feature_vector, dtype=np.float32) for p in all_products])
|
| 49 |
+
|
| 50 |
+
if product_features.ndim == 1:
|
| 51 |
+
product_features = product_features.reshape(1, -1)
|
| 52 |
+
if uploaded_features.ndim == 1:
|
| 53 |
+
uploaded_features = uploaded_features.reshape(1, -1)
|
| 54 |
+
|
| 55 |
+
similarities = cosine_similarity(uploaded_features, product_features)[0]
|
| 56 |
+
|
| 57 |
+
similar_products_with_scores = []
|
| 58 |
+
for i, product in enumerate(all_products):
|
| 59 |
+
similar_products_with_scores.append({
|
| 60 |
+
'product': product,
|
| 61 |
+
'similarity': similarities[i]
|
| 62 |
+
})
|
| 63 |
+
|
| 64 |
+
similar_products_with_scores.sort(key=lambda x: x['similarity'], reverse=True)
|
| 65 |
+
|
| 66 |
+
return similar_products_with_scores[:top_n]
|
matcher/matcher/tests.py
ADDED
|
File without changes
|
matcher/matcher/urls.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path
|
| 2 |
+
from . import views
|
| 3 |
+
|
| 4 |
+
urlpatterns = [
|
| 5 |
+
path('', views.index, name='index'),
|
| 6 |
+
]
|
matcher/matcher/views.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from io import BytesIO
|
| 3 |
+
from django.shortcuts import render
|
| 4 |
+
from django.core.files.base import ContentFile
|
| 5 |
+
from .models import Product, UploadedImage
|
| 6 |
+
from .similarity import extract_features, find_similar_products
|
| 7 |
+
from PIL import Image as PILImage
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
def index(request):
|
| 11 |
+
similar_products = []
|
| 12 |
+
uploaded_image_url = None
|
| 13 |
+
error_message = None
|
| 14 |
+
loading = False
|
| 15 |
+
|
| 16 |
+
if request.method == 'POST':
|
| 17 |
+
loading = True
|
| 18 |
+
image_file = request.FILES.get('image_file')
|
| 19 |
+
image_url_from_post = request.POST.get('image_url')
|
| 20 |
+
|
| 21 |
+
image_to_process = None
|
| 22 |
+
image_bytes = None
|
| 23 |
+
image_filename = 'uploaded_image.jpg'
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
if image_file:
|
| 27 |
+
image_filename = image_file.name
|
| 28 |
+
image_bytes = image_file.read()
|
| 29 |
+
image_to_process = PILImage.open(BytesIO(image_bytes)).convert("RGB")
|
| 30 |
+
|
| 31 |
+
elif image_url_from_post:
|
| 32 |
+
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
|
| 33 |
+
response = requests.get(image_url_from_post, headers=headers, timeout=15)
|
| 34 |
+
response.raise_for_status()
|
| 35 |
+
image_bytes = response.content
|
| 36 |
+
image_to_process = PILImage.open(BytesIO(image_bytes)).convert("RGB")
|
| 37 |
+
image_filename = image_url_from_post.split('/')[-1]
|
| 38 |
+
|
| 39 |
+
else:
|
| 40 |
+
error_message = "Please upload an image or provide a URL."
|
| 41 |
+
|
| 42 |
+
if image_to_process and image_bytes:
|
| 43 |
+
uploaded_image_instance = UploadedImage()
|
| 44 |
+
uploaded_image_instance.image.save(image_filename, ContentFile(image_bytes))
|
| 45 |
+
|
| 46 |
+
uploaded_image_url = uploaded_image_instance.image.url
|
| 47 |
+
|
| 48 |
+
uploaded_features = extract_features(image_to_process)
|
| 49 |
+
|
| 50 |
+
if uploaded_features is not None:
|
| 51 |
+
all_products = list(Product.objects.exclude(feature_vector__isnull=True))
|
| 52 |
+
similar_products = find_similar_products(uploaded_features, all_products)
|
| 53 |
+
|
| 54 |
+
min_similarity = request.POST.get('similarity_score')
|
| 55 |
+
if min_similarity:
|
| 56 |
+
min_similarity = float(min_similarity)
|
| 57 |
+
similar_products = [p for p in similar_products if p['similarity'] >= min_similarity]
|
| 58 |
+
else:
|
| 59 |
+
error_message = "Could not extract features from the image."
|
| 60 |
+
|
| 61 |
+
except PILImage.UnidentifiedImageError:
|
| 62 |
+
error_message = "Could not identify the file as an image. Please check the file or URL."
|
| 63 |
+
except requests.exceptions.RequestException as e:
|
| 64 |
+
error_message = f"Failed to retrieve image from URL: {e}"
|
| 65 |
+
except Exception as e:
|
| 66 |
+
error_message = f"An unexpected error occurred: {e}"
|
| 67 |
+
|
| 68 |
+
loading = False
|
| 69 |
+
|
| 70 |
+
return render(request, 'matcher/index.html', {
|
| 71 |
+
'similar_products': similar_products,
|
| 72 |
+
'uploaded_image_url': uploaded_image_url,
|
| 73 |
+
'error_message': error_message,
|
| 74 |
+
'loading': loading,
|
| 75 |
+
})
|
| 76 |
+
|
matcher/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.5 on 2025-08-26 08:59
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
initial = True
|
| 9 |
+
|
| 10 |
+
dependencies = [
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
operations = [
|
| 14 |
+
migrations.CreateModel(
|
| 15 |
+
name='Product',
|
| 16 |
+
fields=[
|
| 17 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 18 |
+
('name', models.CharField(max_length=255)),
|
| 19 |
+
('category', models.CharField(max_length=255)),
|
| 20 |
+
('image', models.ImageField(upload_to='products/')),
|
| 21 |
+
('feature_vector', models.BinaryField(blank=True, null=True)),
|
| 22 |
+
],
|
| 23 |
+
),
|
| 24 |
+
migrations.CreateModel(
|
| 25 |
+
name='UploadedImage',
|
| 26 |
+
fields=[
|
| 27 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 28 |
+
('image', models.ImageField(upload_to='uploads/')),
|
| 29 |
+
('uploaded_at', models.DateTimeField(auto_now_add=True)),
|
| 30 |
+
],
|
| 31 |
+
),
|
| 32 |
+
]
|
matcher/migrations/__init__.py
ADDED
|
File without changes
|
matcher/migrations/__pycache__/0001_initial.cpython-313.pyc
ADDED
|
Binary file (1.47 kB). View file
|
|
|
matcher/migrations/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (180 Bytes). View file
|
|
|
matcher/models.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.db import models
|
| 2 |
+
|
| 3 |
+
class Product(models.Model):
|
| 4 |
+
name = models.CharField(max_length=255)
|
| 5 |
+
category = models.CharField(max_length=255)
|
| 6 |
+
image = models.ImageField(upload_to='products/')
|
| 7 |
+
feature_vector = models.BinaryField(null=True, blank=True)
|
| 8 |
+
|
| 9 |
+
def __str__(self):
|
| 10 |
+
return self.name
|
| 11 |
+
|
| 12 |
+
class UploadedImage(models.Model):
|
| 13 |
+
image = models.ImageField(upload_to='uploads/')
|
| 14 |
+
uploaded_at = models.DateTimeField(auto_now_add=True)
|
matcher/similarity.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from transformers import ViTImageProcessor, ViTModel
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import numpy as np
|
| 5 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 6 |
+
import requests
|
| 7 |
+
from io import BytesIO
|
| 8 |
+
|
| 9 |
+
# We are now using a much smaller "tiny" version of the Vision Transformer because render was not able to handle and demanding to pay for larger models.
|
| 10 |
+
MODEL_NAME = 'WinKawaks/vit-tiny-patch16-224'
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
image_processor = ViTImageProcessor.from_pretrained(MODEL_NAME)
|
| 14 |
+
model = ViTModel.from_pretrained(MODEL_NAME)
|
| 15 |
+
except Exception as e:
|
| 16 |
+
print(f"Error loading model: {e}")
|
| 17 |
+
image_processor = None
|
| 18 |
+
model = None
|
| 19 |
+
|
| 20 |
+
def extract_features(image: Image.Image) -> np.ndarray | None:
|
| 21 |
+
"""
|
| 22 |
+
Extracts a feature vector from an image using the ViT model.
|
| 23 |
+
"""
|
| 24 |
+
if not model or not image_processor:
|
| 25 |
+
print("Model or image processor not loaded.")
|
| 26 |
+
return None
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
inputs = image_processor(images=image, return_tensors="pt")
|
| 30 |
+
|
| 31 |
+
with torch.no_grad():
|
| 32 |
+
outputs = model(**inputs)
|
| 33 |
+
|
| 34 |
+
last_hidden_states = outputs.last_hidden_state
|
| 35 |
+
features = last_hidden_states.mean(dim=1).cpu().numpy()
|
| 36 |
+
return features
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"An error occurred during feature extraction: {e}")
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
def find_similar_products(uploaded_features, all_products, top_n=20):
|
| 42 |
+
"""
|
| 43 |
+
Finds similar products by comparing feature vectors using cosine similarity.
|
| 44 |
+
"""
|
| 45 |
+
if uploaded_features is None or not all_products:
|
| 46 |
+
return []
|
| 47 |
+
|
| 48 |
+
product_features = np.array([np.frombuffer(p.feature_vector, dtype=np.float32) for p in all_products])
|
| 49 |
+
|
| 50 |
+
if product_features.ndim == 1:
|
| 51 |
+
product_features = product_features.reshape(1, -1)
|
| 52 |
+
if uploaded_features.ndim == 1:
|
| 53 |
+
uploaded_features = uploaded_features.reshape(1, -1)
|
| 54 |
+
|
| 55 |
+
similarities = cosine_similarity(uploaded_features, product_features)[0]
|
| 56 |
+
|
| 57 |
+
similar_products_with_scores = []
|
| 58 |
+
for i, product in enumerate(all_products):
|
| 59 |
+
similar_products_with_scores.append({
|
| 60 |
+
'product': product,
|
| 61 |
+
'similarity': similarities[i]
|
| 62 |
+
})
|
| 63 |
+
|
| 64 |
+
similar_products_with_scores.sort(key=lambda x: x['similarity'], reverse=True)
|
| 65 |
+
|
| 66 |
+
return similar_products_with_scores[:top_n]
|
matcher/tests.py
ADDED
|
File without changes
|
matcher/urls.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path
|
| 2 |
+
from . import views
|
| 3 |
+
|
| 4 |
+
urlpatterns = [
|
| 5 |
+
path('', views.index, name='index'),
|
| 6 |
+
]
|
matcher/views.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from io import BytesIO
|
| 3 |
+
from django.shortcuts import render
|
| 4 |
+
from django.core.files.base import ContentFile
|
| 5 |
+
from .models import Product, UploadedImage
|
| 6 |
+
from .similarity import extract_features, find_similar_products
|
| 7 |
+
from PIL import Image as PILImage
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
def index(request):
|
| 11 |
+
similar_products = []
|
| 12 |
+
uploaded_image_url = None
|
| 13 |
+
error_message = None
|
| 14 |
+
loading = False
|
| 15 |
+
|
| 16 |
+
if request.method == 'POST':
|
| 17 |
+
loading = True
|
| 18 |
+
image_file = request.FILES.get('image_file')
|
| 19 |
+
image_url_from_post = request.POST.get('image_url')
|
| 20 |
+
|
| 21 |
+
image_to_process = None
|
| 22 |
+
image_bytes = None
|
| 23 |
+
image_filename = 'uploaded_image.jpg'
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
if image_file:
|
| 27 |
+
image_filename = image_file.name
|
| 28 |
+
image_bytes = image_file.read()
|
| 29 |
+
image_to_process = PILImage.open(BytesIO(image_bytes)).convert("RGB")
|
| 30 |
+
|
| 31 |
+
elif image_url_from_post:
|
| 32 |
+
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
|
| 33 |
+
response = requests.get(image_url_from_post, headers=headers, timeout=15)
|
| 34 |
+
response.raise_for_status()
|
| 35 |
+
image_bytes = response.content
|
| 36 |
+
image_to_process = PILImage.open(BytesIO(image_bytes)).convert("RGB")
|
| 37 |
+
image_filename = image_url_from_post.split('/')[-1]
|
| 38 |
+
|
| 39 |
+
else:
|
| 40 |
+
error_message = "Please upload an image or provide a URL."
|
| 41 |
+
|
| 42 |
+
if image_to_process and image_bytes:
|
| 43 |
+
uploaded_image_instance = UploadedImage()
|
| 44 |
+
uploaded_image_instance.image.save(image_filename, ContentFile(image_bytes))
|
| 45 |
+
|
| 46 |
+
uploaded_image_url = uploaded_image_instance.image.url
|
| 47 |
+
|
| 48 |
+
uploaded_features = extract_features(image_to_process)
|
| 49 |
+
|
| 50 |
+
if uploaded_features is not None:
|
| 51 |
+
all_products = list(Product.objects.exclude(feature_vector__isnull=True))
|
| 52 |
+
similar_products = find_similar_products(uploaded_features, all_products)
|
| 53 |
+
|
| 54 |
+
min_similarity = request.POST.get('similarity_score')
|
| 55 |
+
if min_similarity:
|
| 56 |
+
min_similarity = float(min_similarity)
|
| 57 |
+
similar_products = [p for p in similar_products if p['similarity'] >= min_similarity]
|
| 58 |
+
else:
|
| 59 |
+
error_message = "Could not extract features from the image."
|
| 60 |
+
|
| 61 |
+
except PILImage.UnidentifiedImageError:
|
| 62 |
+
error_message = "Could not identify the file as an image. Please check the file or URL."
|
| 63 |
+
except requests.exceptions.RequestException as e:
|
| 64 |
+
error_message = f"Failed to retrieve image from URL: {e}"
|
| 65 |
+
except Exception as e:
|
| 66 |
+
error_message = f"An unexpected error occurred: {e}"
|
| 67 |
+
|
| 68 |
+
loading = False
|
| 69 |
+
|
| 70 |
+
return render(request, 'matcher/index.html', {
|
| 71 |
+
'similar_products': similar_products,
|
| 72 |
+
'uploaded_image_url': uploaded_image_url,
|
| 73 |
+
'error_message': error_message,
|
| 74 |
+
'loading': loading,
|
| 75 |
+
})
|
| 76 |
+
|