Spaces:
Build error
Build error
Upload 12 files
Browse files- .env +3 -0
- Dockerfile +16 -0
- README.md +0 -10
- app.py +6 -0
- app/__init__.py +20 -0
- app/feedback.py +23 -0
- app/matcher.py +20 -0
- app/routes.py +112 -0
- app/segmenter.py +35 -0
- app/utils.py +15 -0
- main.py +6 -0
- requirements.txt +4 -0
.env
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FLASK_ENV=development
|
| 2 |
+
SQLALCHEMY_DATABASE_URI=sqlite:///feedback.db
|
| 3 |
+
SECRET_KEY=your-secret-key
|
Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
ENV FLASK_APP=app.py
|
| 11 |
+
ENV FLASK_RUN_HOST=0.0.0.0
|
| 12 |
+
ENV FLASK_RUN_PORT=7860 # Hugging Face Spaces default port
|
| 13 |
+
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
|
| 16 |
+
CMD ["flask", "run"]
|
README.md
CHANGED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Piecefinder
|
| 3 |
-
emoji: 👁
|
| 4 |
-
colorFrom: yellow
|
| 5 |
-
colorTo: red
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import create_app
|
| 2 |
+
|
| 3 |
+
app = create_app()
|
| 4 |
+
|
| 5 |
+
if __name__ == '__main__':
|
| 6 |
+
app.run()
|
app/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask
|
| 2 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 3 |
+
|
| 4 |
+
db = SQLAlchemy()
|
| 5 |
+
|
| 6 |
+
def create_app():
|
| 7 |
+
app = Flask(__name__)
|
| 8 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///feedback.db'
|
| 9 |
+
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
| 10 |
+
app.config['SECRET_KEY'] = 'your-secret-key' # Replace with env variable in production
|
| 11 |
+
|
| 12 |
+
db.init_app(app)
|
| 13 |
+
|
| 14 |
+
with app.app_context():
|
| 15 |
+
db.create_all() # Create database tables
|
| 16 |
+
|
| 17 |
+
from app.routes import bp
|
| 18 |
+
app.register_blueprint(bp)
|
| 19 |
+
|
| 20 |
+
return app
|
app/feedback.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import db
|
| 2 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
class Feedback(db.Model):
|
| 6 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 7 |
+
piece_id = db.Column(db.Integer, nullable=False)
|
| 8 |
+
slot_id = db.Column(db.Integer, nullable=False)
|
| 9 |
+
suggestions = db.Column(db.Text, nullable=False)
|
| 10 |
+
user_choice = db.Column(db.Integer, nullable=False)
|
| 11 |
+
is_correct = db.Column(db.Boolean, nullable=False)
|
| 12 |
+
|
| 13 |
+
class FeedbackManager:
|
| 14 |
+
def record_feedback(self, piece_id: int, slot_id: int, suggestions: dict, user_choice: int, is_correct: bool):
|
| 15 |
+
feedback = Feedback(
|
| 16 |
+
piece_id=piece_id,
|
| 17 |
+
slot_id=slot_id,
|
| 18 |
+
suggestions=json.dumps(suggestions),
|
| 19 |
+
user_choice=user_choice,
|
| 20 |
+
is_correct=is_correct
|
| 21 |
+
)
|
| 22 |
+
db.session.add(feedback)
|
| 23 |
+
db.session.commit()
|
app/matcher.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
class SimpleMatcher:
|
| 5 |
+
def find_matches(self, pieces: list, slots: list, top_k: int = 3) -> list:
|
| 6 |
+
matches = []
|
| 7 |
+
for slot in slots:
|
| 8 |
+
slot_hist = slot['features']['color']
|
| 9 |
+
slot_matches = []
|
| 10 |
+
for piece in pieces:
|
| 11 |
+
piece_hist = piece['features']['color']
|
| 12 |
+
confidence = cv2.compareHist(piece_hist, slot_hist, cv2.HISTCMP_CORREL)
|
| 13 |
+
slot_matches.append({
|
| 14 |
+
'piece_id': piece['id'],
|
| 15 |
+
'slot_id': slot['id'],
|
| 16 |
+
'confidence': float(confidence)
|
| 17 |
+
})
|
| 18 |
+
slot_matches.sort(key=lambda x: x['confidence'], reverse=True)
|
| 19 |
+
matches.extend(slot_matches[:top_k])
|
| 20 |
+
return matches
|
app/routes.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, request, jsonify
|
| 2 |
+
from app.segmenter import SimpleSegmenter
|
| 3 |
+
from app.matcher import SimpleMatcher
|
| 4 |
+
from app.feedback import FeedbackManager
|
| 5 |
+
import base64
|
| 6 |
+
import cv2
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
bp = Blueprint('api', __name__)
|
| 10 |
+
|
| 11 |
+
@bp.route('/segment', methods=['POST'])
|
| 12 |
+
def segment_image():
|
| 13 |
+
try:
|
| 14 |
+
data = request.get_json()
|
| 15 |
+
image_base64 = data.get('image_base64')
|
| 16 |
+
if not image_base64:
|
| 17 |
+
return jsonify({'error': 'No image provided'}), 400
|
| 18 |
+
|
| 19 |
+
# Decode base64 image
|
| 20 |
+
image_data = base64.b64decode(image_base64)
|
| 21 |
+
nparr = np.frombuffer(image_data, np.uint8)
|
| 22 |
+
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 23 |
+
|
| 24 |
+
# Segment pieces
|
| 25 |
+
segmenter = SimpleSegmenter()
|
| 26 |
+
pieces = segmenter.segment_pieces(image)
|
| 27 |
+
|
| 28 |
+
# Serialize pieces
|
| 29 |
+
serialized_pieces = [
|
| 30 |
+
{
|
| 31 |
+
'id': piece['id'],
|
| 32 |
+
'image': base64.b64encode(piece['image']).decode('utf-8'),
|
| 33 |
+
'features': piece['features']
|
| 34 |
+
}
|
| 35 |
+
for piece in pieces
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
return jsonify({'pieces': serialized_pieces}), 200
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return jsonify({'error': str(e)}), 500
|
| 41 |
+
|
| 42 |
+
@bp.route('/count', methods=['POST'])
|
| 43 |
+
def count_pieces():
|
| 44 |
+
try:
|
| 45 |
+
data = request.get_json()
|
| 46 |
+
image_base64 = data.get('image_base64')
|
| 47 |
+
if not image_base64:
|
| 48 |
+
return jsonify({'error': 'No image provided'}), 400
|
| 49 |
+
|
| 50 |
+
image_data = base64.b64decode(image_base64)
|
| 51 |
+
nparr = np.frombuffer(image_data, np.uint8)
|
| 52 |
+
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 53 |
+
|
| 54 |
+
segmenter = SimpleSegmenter()
|
| 55 |
+
pieces = segmenter.segment_pieces(image)
|
| 56 |
+
count = len(pieces)
|
| 57 |
+
|
| 58 |
+
return jsonify({'count': count}), 200
|
| 59 |
+
except Exception as e:
|
| 60 |
+
return jsonify({'error': str(e)}), 500
|
| 61 |
+
|
| 62 |
+
@bp.route('/match', methods=['POST'])
|
| 63 |
+
def match_pieces():
|
| 64 |
+
try:
|
| 65 |
+
data = request.get_json()
|
| 66 |
+
image_base64 = data.get('image_base64')
|
| 67 |
+
if not image_base64:
|
| 68 |
+
return jsonify({'error': 'No image provided'}), 400
|
| 69 |
+
|
| 70 |
+
image_data = base64.b64decode(image_base64)
|
| 71 |
+
nparr = np.frombuffer(image_data, np.uint8)
|
| 72 |
+
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 73 |
+
|
| 74 |
+
segmenter = SimpleSegmenter()
|
| 75 |
+
matcher = SimpleMatcher()
|
| 76 |
+
pieces = segmenter.segment_pieces(image)
|
| 77 |
+
slots = pieces # Simplified: assume slots are same as pieces
|
| 78 |
+
|
| 79 |
+
matches = matcher.find_matches(pieces, slots)
|
| 80 |
+
|
| 81 |
+
serialized_matches = [
|
| 82 |
+
{
|
| 83 |
+
'piece_id': match['piece_id'],
|
| 84 |
+
'slot_id': match['slot_id'],
|
| 85 |
+
'confidence': match['confidence']
|
| 86 |
+
}
|
| 87 |
+
for match in matches
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
return jsonify({'matches': serialized_matches, 'slots': [{'id': slot['id']} for slot in slots]}), 200
|
| 91 |
+
except Exception as e:
|
| 92 |
+
return jsonify({'error': str(e)}), 500
|
| 93 |
+
|
| 94 |
+
@bp.route('/feedback', methods=['POST'])
|
| 95 |
+
def submit_feedback():
|
| 96 |
+
try:
|
| 97 |
+
data = request.get_json()
|
| 98 |
+
piece_id = data.get('pieceId')
|
| 99 |
+
slot_id = data.get('slotId')
|
| 100 |
+
suggestions = data.get('suggestions')
|
| 101 |
+
user_choice = data.get('userChoice')
|
| 102 |
+
is_correct = data.get('isCorrect')
|
| 103 |
+
|
| 104 |
+
if not all([piece_id, slot_id, suggestions, user_choice is not None, is_correct is not None]):
|
| 105 |
+
return jsonify({'error': 'Missing required fields'}), 400
|
| 106 |
+
|
| 107 |
+
feedback_manager = FeedbackManager()
|
| 108 |
+
feedback_manager.record_feedback(piece_id, slot_id, suggestions, user_choice, is_correct)
|
| 109 |
+
|
| 110 |
+
return jsonify({'message': 'Feedback recorded successfully'}), 200
|
| 111 |
+
except Exception as e:
|
| 112 |
+
return jsonify({'error': str(e)}), 500
|
app/segmenter.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from app.utils import preprocess_image, extract_features
|
| 4 |
+
|
| 5 |
+
class SimpleSegmenter:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
self.min_piece_area = 500
|
| 8 |
+
self.max_piece_area = 50000
|
| 9 |
+
|
| 10 |
+
def segment_pieces(self, image: np.ndarray) -> list:
|
| 11 |
+
# Preprocess image
|
| 12 |
+
processed = preprocess_image(image)
|
| 13 |
+
gray = cv2.cvtColor(processed, cv2.COLOR_BGR2GRAY)
|
| 14 |
+
_, thresh = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY_INV)
|
| 15 |
+
|
| 16 |
+
# Find contours
|
| 17 |
+
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 18 |
+
pieces = []
|
| 19 |
+
piece_id = 0
|
| 20 |
+
|
| 21 |
+
for contour in contours:
|
| 22 |
+
area = cv2.contourArea(contour)
|
| 23 |
+
if self.min_piece_area <= area <= self.max_piece_area:
|
| 24 |
+
mask = np.zeros(image.shape[:2], dtype=np.uint8)
|
| 25 |
+
cv2.drawContours(mask, [contour], -1, 255, -1)
|
| 26 |
+
piece_image = cv2.bitwise_and(image, image, mask=mask)
|
| 27 |
+
features = extract_features(piece_image, mask)
|
| 28 |
+
pieces.append({
|
| 29 |
+
'id': piece_id,
|
| 30 |
+
'image': cv2.imencode('.jpg', piece_image)[1].tobytes(),
|
| 31 |
+
'features': features
|
| 32 |
+
})
|
| 33 |
+
piece_id += 1
|
| 34 |
+
|
| 35 |
+
return pieces
|
app/utils.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
def preprocess_image(image: np.ndarray) -> np.ndarray:
|
| 5 |
+
# Simple preprocessing: enhance contrast
|
| 6 |
+
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
|
| 7 |
+
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
| 8 |
+
lab[:, :, 0] = clahe.apply(lab[:, :, 0])
|
| 9 |
+
return cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
|
| 10 |
+
|
| 11 |
+
def extract_features(image: np.ndarray, mask: np.ndarray) -> dict:
|
| 12 |
+
# Extract color histogram
|
| 13 |
+
color_histogram = cv2.calcHist([image], [0, 1, 2], mask, [8, 8, 8], [0, 256, 0, 256, 0, 256])
|
| 14 |
+
color_histogram = cv2.normalize(color_histogram, color_histogram).flatten()
|
| 15 |
+
return {'color': color_histogram}
|
main.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import create_app
|
| 2 |
+
|
| 3 |
+
app = create_app()
|
| 4 |
+
|
| 5 |
+
if __name__ == '__main__':
|
| 6 |
+
app.run(debug=True, host='0.0.0.0', port=5000)
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
opencv-python
|
| 3 |
+
numpy
|
| 4 |
+
flask-sqlalchemy
|