Initial commit for push folder

This commit is contained in:
Nguyen Hong Van 2026-09-18 09:13:53 +09:00
commit c4af9fcbb5
82 changed files with 3294 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.DS_Store

22
recognization/.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*.class
# OS artifacts
.DS_Store
Thumbs.db
# Virtual environments
venv/
env/
ENV/
.venv/
# IDE files
.vscode/
.idea/
# Temporary weights
runs/detect/train/weights/last.pt
*.log

99
recognization/README.md Normal file
View file

@ -0,0 +1,99 @@
# 🍡 Dorayaki - Smart Supermarket Shelf AI & Inventory Recognition
Hệ thống nhận diện sản phẩm trên kệ siêu thị và quản lý tồn kho thời gian thực dựa trên kiến trúc kết hợp **YOLO** (Object Detection) và **Local Feature Matching** (SURF / SIFT, ORB, BRISK, Hybrid) theo paper quốc tế (*Electronics 2023, 12, 3640*).
---
## 📋 Yêu cầu hệ thống (System Requirements)
- **Hệ điều hành:** Windows 10/11, macOS hoặc Linux (Ubuntu/Debian).
- **Python:** Python 3.9, 3.10 hoặc 3.11 (khuyên dùng Python 3.10).
- **Phần cứng:** CPU tối thiểu 4 lõi, RAM 8GB (có GPU NVIDIA là lợi thế nhưng CPU chạy hoàn toàn mượt mà).
---
## 🚀 Hướng dẫn cài đặt và chạy Step-by-Step
### Bước 1: Sao chép toàn bộ thư mục dự án sang máy mới
Đảm bảo bạn copy toàn bộ thư mục dự án, đặc biệt là các thư mục cốt lõi:
- code/: Toàn bộ mã nguồn backend, frontend, thuật toán nhận diện.
-
uns/: Chứa file trọng số mô hình YOLO đã train (
uns/detect/train/weights/best.pt).
- stock/: Chứa file catalog stock.csv và ảnh mẫu sản phẩm chuẩn sku_001 -> sku_016.
- dd/ & image.jpg: Các ảnh kệ hàng mẫu để test và demo.
-
equirements.txt: Danh sách thư viện cần thiết.
### Bước 2: Tạo môi trường ảo Python (Khuyên dùng)
Mở Terminal / PowerShell / Command Prompt tại thư mục dự án:
#### Cách 1: Sử dụng Conda (Khuyên dùng nếu đã cài Anaconda/Miniconda)
`ash
conda create -n dorayaki python=3.10 -y
conda activate dorayaki
`
#### Cách 2: Sử dụng venv chuẩn của Python
- **Trên Windows:**
`ash
python -m venv venv
venv\Scripts\activate
`
- **Trên macOS / Linux:**
`ash
python3 -m venv venv
source venv/bin/activate
`
### Bước 3: Cài đặt các thư viện phụ thuộc
Chạy lệnh sau để cài đặt toàn bộ package:
`ash
pip install -r requirements.txt
`
*(Ghi chú nếu máy có GPU NVIDIA và muốn tăng tốc độ YOLO, có thể cài PyTorch CUDA theo hướng dẫn tại pytorch.org).*
### Bước 4: Khởi chạy ứng dụng Dorayaki Web App
#### Cách 1: Chạy bằng lệnh Python
`ash
python code/app.py --port 8080
`
*(Nếu dùng macOS/Linux có thể dùng python3 code/app.py --port 8080)*
#### Cách 2: Chạy nhanh bằng script có sẵn
- **Trên Windows:** Click đúp vào file
un_app.bat
- **Trên Linux/macOS:** Chạy ./run_app.sh
### Bước 5: Mở ứng dụng trên trình duyệt
Mở trình duyệt (Chrome, Edge, Safari, Firefox) và truy cập:
👉 **http://localhost:8080** (hoặc **http://127.0.0.1:8080**)
---
## 🛠️ Các lệnh kiểm tra hệ thống (Test Scripts)
1. **Test toàn diện hệ thống (Test Suite):**
`ash
python code/test_suite.py
`
2. **Test nhận diện sản phẩm trên ảnh image.jpg:**
`ash
python code/test_image.py
`
3. **Test nhận diện các chỗ trống trên kệ (Empty Shelf Slots):**
`ash
python code/test_empty.py
`
*(Kết quả trực quan sẽ được xuất ra file detected_empty_slots.jpg với viền đỏ bao quanh các vị trí hết hàng).*
---
## 🌟 Các tính năng chính của ứng dụng
1. **Phát hiện sản phẩm (YOLO):** Tự động detect hơn 170+ vật phẩm trên kệ trong 1 lượt scan.
2. **Phát hiện chỗ trống trên kệ (Out-of-Stock Detection):** Tự động phát hiện các vị trí hết hàng và khoanh đỏ, ước lượng số lượng sản phẩm đang thiếu.
3. **Phân loại đa thuật toán:** Cho phép chọn giữa **Hybrid (Paper)**, **SURF / SIFT**, **ORB**, **BRISK**.
4. **Tương tác trực quan:** Click vào bất kỳ sản phẩm nào trên ảnh kệ để xem tên, giá, ngày nhập, hạn sử dụng, số lượng tồn kho và xem các điểm matching inliers.
5. **Đa ngôn ngữ:** Hỗ trợ chuyển đổi nhanh Tiếng Anh và Tiếng Nhật.
6. **Tải ảnh linh hoạt:** Chọn ảnh từ danh mục mẫu, chụp ảnh trực tiếp từ webcam/camera điện thoại, hoặc upload ảnh từ máy tính.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,000 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View file

@ -0,0 +1 @@
"""Supermarket Shelf Recognition Package."""

1246
recognization/code/app.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,103 @@
"""
Benchmark Suite for Supermarket Product Classification
Compares SURF, ORB, BRISK, and Hybrid methods following
MDPI Electronics 2023, 12(17), 3640.
"""
import os
import sys
import time
import cv2
import numpy as np
from typing import Dict, List, Any
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
from feature_matcher import ProductFeatureMatcher
def run_benchmark(stock_dir: str = "stock", num_runs: int = 3):
print("=" * 70)
print("RETAIL SHELF PRODUCT RECOGNITION - ALGORITHM BENCHMARK")
print("Methods Evaluated: SURF | ORB | BRISK | HYBRID (Paper Method)")
print("=" * 70)
matcher = ProductFeatureMatcher(stock_dir=stock_dir)
templates = matcher.db.templates
if not templates:
print("[Error] No templates found in stock directory.")
return
methods = ["surf", "orb", "brisk", "hybrid"]
results = {m: {"latencies": [], "inliers": [], "keypoints_query": []} for m in methods}
# Test images: We will evaluate using crops synthesized with slight transformations
# (scale, rotation, noise) as well as the template images themselves
print(f"\nEvaluating across {len(templates)} catalog SKUs (repeat count: {num_runs})...\n")
for sku_id, tmpl in templates.items():
original_img = tmpl["image"]
h, w = original_img.shape[:2]
# Apply synthetic shelf condition (rotation by 10 deg, resize by 0.85)
M = cv2.getRotationMatrix2D((w / 2, h / 2), 10, 0.85)
query_img = cv2.warpAffine(original_img, M, (w, h))
for method in methods:
times = []
for _ in range(num_runs):
t0 = time.perf_counter()
res = matcher.classify_crop(query_img, method=method)
t1 = time.perf_counter()
times.append((t1 - t0) * 1000.0) # ms
avg_latency = np.mean(times)
results[method]["latencies"].append(avg_latency)
# Record inliers/score
if res.get("status") == "success":
top_sku = res["best_sku"]
score = res["score"]
results[method]["inliers"].append(score)
# Summarize metrics
summary_rows = []
for method in methods:
latencies = results[method]["latencies"]
scores = results[method]["inliers"]
avg_lat = np.mean(latencies) if latencies else 0.0
p95_lat = np.percentile(latencies, 95) if latencies else 0.0
avg_score = np.mean(scores) if scores else 0.0
# Paper characteristics notes
notes = {
"surf": "Scale & rotation robust; Hessian gradient features",
"orb": "Ultra fast binary descriptor; FAST + BRIEF",
"brisk": "Scale-space AGAST; circular pattern descriptor",
"hybrid": "Paper fusion (SURF+ORB+BRISK); optimal accuracy/speed"
}[method]
summary_rows.append([
method.upper(),
f"{avg_lat:.2f} ms",
f"{p95_lat:.2f} ms",
f"{avg_score:.1f}",
notes
])
headers = ["Method", "Avg Latency (ms)", "P95 Latency (ms)", "Avg Match Score", "Characteristics / Paper Findings"]
print("\n" + "-" * 105)
print(f"{headers[0]:<10} | {headers[1]:<18} | {headers[2]:<18} | {headers[3]:<16} | {headers[4]}")
print("-" * 105)
for row in summary_rows:
print(f"{row[0]:<10} | {row[1]:<18} | {row[2]:<18} | {row[3]:<16} | {row[4]}")
print("-" * 105)
print("\n[Benchmark Complete] All methods operational.")
if __name__ == "__main__":
run_benchmark()

View file

@ -0,0 +1,284 @@
"""
Object Detection Module using YOLO
Handles inference, bounding box extraction, and spatial hit-testing for shelf images.
"""
import os
import cv2
import numpy as np
from typing import List, Dict, Optional, Tuple, Any
import torch
import torch.nn as nn
try:
import ultralytics.nn.modules.block as _B
import ultralytics.nn.modules as _M
if not hasattr(_B, "C3k"):
class C3k(_B.C3):
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, k=3):
super().__init__(c1, c2, n, shortcut, g, e)
c_ = int(c2 * e)
self.m = nn.Sequential(*(_B.Bottleneck(c_, c_, shortcut, g, k=((k, k), (k, k)), e=1.0) for _ in range(n)))
class C3k2(_B.C2f):
def __init__(self, c1, c2, n=1, c3k=False, e=0.5, g=1, shortcut=True):
super().__init__(c1, c2, n, shortcut, g, e)
self.m = nn.ModuleList(
C3k(self.c, self.c, 2, shortcut, g) if c3k else _B.Bottleneck(self.c, self.c, shortcut, g) for _ in range(n)
)
class Attention(nn.Module):
def __init__(self, dim, num_heads=8, attn_ratio=0.5):
super().__init__()
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.key_dim = int(self.head_dim * attn_ratio)
self.scale = self.key_dim ** -0.5
nh_kd = self.key_dim * num_heads
h = dim + nh_kd * 2
self.qkv = _B.Conv(dim, h, 1, act=False)
self.proj = _B.Conv(dim, dim, 1, act=False)
self.pe = _B.Conv(dim, dim, 3, 1, g=dim, act=False)
def forward(self, x):
b, c, h, w = x.shape
n = h * w
qkv = self.qkv(x)
q, k, v = qkv.view(b, self.num_heads, self.key_dim * 2 + self.head_dim, n).split(
[self.key_dim, self.key_dim, self.head_dim], dim=2
)
attn = (q.transpose(-2, -1) @ k) * self.scale
attn = attn.softmax(dim=-1)
x = (v @ attn.transpose(-2, -1)).view(b, c, h, w) + self.pe(x)
return self.proj(x)
class PSABlock(nn.Module):
def __init__(self, c, attn_ratio=0.5, num_heads=4, shortcut=True):
super().__init__()
self.attn = Attention(c, attn_ratio=attn_ratio, num_heads=num_heads)
self.ffn = nn.Sequential(_B.Conv(c, c * 2, 1), _B.Conv(c * 2, c, 1, act=False))
self.add = shortcut
def forward(self, x):
x = x + self.attn(x) if self.add else self.attn(x)
x = x + self.ffn(x) if self.add else self.ffn(x)
return x
class C2PSA(nn.Module):
def __init__(self, c1, c2, n=1, e=0.5):
super().__init__()
assert c1 == c2
self.c = int(c1 * e)
self.cv1 = _B.Conv(c1, 2 * self.c, 1, 1)
self.cv2 = _B.Conv(2 * self.c, c1, 1)
self.m = nn.Sequential(*(PSABlock(self.c) for _ in range(n)))
def forward(self, x):
a, b = self.cv1(x).split((self.c, self.c), dim=1)
b = self.m(b)
return self.cv2(torch.cat((a, b), 1))
_B.C3k = C3k
_B.C3k2 = C3k2
_B.C2PSA = C2PSA
_B.PSABlock = PSABlock
_B.Attention = Attention
_M.C3k = C3k
_M.C3k2 = C3k2
_M.C2PSA = C2PSA
_M.PSABlock = PSABlock
_M.Attention = Attention
except Exception as _e:
print(f"[Warning] YOLO26 patch not applied: {_e}")
from ultralytics import YOLO
class ShelfProductDetector:
"""Wraps the YOLO detector for supermarket shelf products."""
def __init__(self, weights_path: str = "runs/detect/train/weights/best.pt"):
self.weights_path = weights_path
if not os.path.exists(weights_path):
raise FileNotFoundError(f"YOLO weights not found at: {weights_path}")
print(f"[Detector] Loading YOLO model from {weights_path}...")
self.model = YOLO(weights_path)
print("[Detector] Model loaded successfully.")
def detect(
self,
image_input: Any,
conf_threshold: float = 0.25,
iou_threshold: float = 0.45
) -> Dict[str, Any]:
"""
Runs object detection on a shelf image.
image_input can be a filepath (str) or a numpy BGR array.
"""
if isinstance(image_input, str):
if not os.path.exists(image_input):
raise FileNotFoundError(f"Image not found at: {image_input}")
image_bgr = cv2.imread(image_input)
else:
image_bgr = image_input
if image_bgr is None:
return {"status": "error", "message": "Failed to read image", "boxes": []}
h, w = image_bgr.shape[:2]
results = self.model(image_bgr, conf=conf_threshold, iou=iou_threshold, verbose=False)
boxes_list = []
if results and len(results) > 0:
boxes_data = results[0].boxes
for idx, box in enumerate(boxes_data):
xyxy = box.xyxy[0].cpu().numpy()
x1, y1, x2, y2 = map(int, xyxy)
# Clamp to image boundaries
x1, y1 = max(0, x1), max(0, y1)
x2, y2 = min(w, x2), min(h, y2)
conf = float(box.conf[0].cpu().numpy())
cls_id = int(box.cls[0].cpu().numpy()) if box.cls is not None else 0
boxes_list.append({
"id": idx,
"box": [x1, y1, x2, y2],
"confidence": round(conf, 4),
"class_id": cls_id,
"width": x2 - x1,
"height": y2 - y1,
"center": [(x1 + x2) // 2, (y1 + y2) // 2]
})
return {
"status": "success",
"image_shape": (h, w),
"total_detected": len(boxes_list),
"boxes": boxes_list
}
def find_box_at_point(
self,
x: int,
y: int,
boxes: List[Dict[str, Any]],
tolerance_px: int = 40
) -> Optional[Dict[str, Any]]:
"""
Finds the bounding box enclosing the click (x, y).
If clicked on overlapping boxes, selects the one with minimum distance to center.
If clicked slightly outside, selects the nearest box within tolerance_px.
"""
enclosing_boxes = []
for b_info in boxes:
x1, y1, x2, y2 = b_info["box"]
if x1 <= x <= x2 and y1 <= y <= y2:
# Calculate distance from click to center
cx, cy = b_info["center"]
dist = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
enclosing_boxes.append((dist, b_info))
if enclosing_boxes:
# Return box closest to click center
enclosing_boxes.sort(key=lambda item: item[0])
return enclosing_boxes[0][1]
# Check nearest within tolerance
nearest_box = None
min_dist = float("inf")
for b_info in boxes:
x1, y1, x2, y2 = b_info["box"]
# Distance to rectangle
dx = max(x1 - x, 0, x - x2)
dy = max(y1 - y, 0, y - y2)
dist = np.sqrt(dx ** 2 + dy ** 2)
if dist < min_dist and dist <= tolerance_px:
min_dist = dist
nearest_box = b_info
return nearest_box
def crop_roi(self, image_bgr: np.ndarray, box: List[int]) -> np.ndarray:
"""Crops a product ROI bounding box from the shelf image."""
x1, y1, x2, y2 = box
h, w = image_bgr.shape[:2]
x1, y1 = max(0, x1), max(0, y1)
x2, y2 = min(w, x2), min(h, y2)
return image_bgr[y1:y2, x1:x2].copy()
def draw_detections(
self,
image_bgr: np.ndarray,
boxes: List[Dict[str, Any]],
selected_box_id: Optional[int] = None,
draw_labels: bool = False
) -> np.ndarray:
"""
Draws bounding boxes on the shelf image.
By default (draw_labels=False), ONLY bounding boxes are drawn:
no class names, no confidence numbers.
"""
annotated = image_bgr.copy()
for b in boxes:
x1, y1, x2, y2 = b["box"]
is_selected = (selected_box_id is not None and b["id"] == selected_box_id)
color = (0, 0, 255) if is_selected else (0, 255, 0)
thickness = 3 if is_selected else 2
# Draw ONLY bounding box
cv2.rectangle(annotated, (x1, y1), (x2, y2), color, thickness)
if draw_labels:
label = f"#{b['id']} ({b['confidence']:.2f})"
if is_selected:
label = f"SELECTED #{b['id']}"
cv2.putText(
annotated,
label,
(x1, max(15, y1 - 6)),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
color,
2
)
return annotated
def detect_empty_slots(
self,
image_shape: Tuple[int, int],
boxes: List[Dict[str, Any]],
min_gap_ratio: float = 0.75
) -> List[Dict[str, Any]]:
"""
Detects empty shelf slots / out-of-stock gaps along shelf rows.
Uses planogram geometric row-clustering and architectural pillar exclusion.
"""
from empty_shelf_detector import EmptyShelfDetector
empty_detector = EmptyShelfDetector(min_gap_ratio=min_gap_ratio)
return empty_detector.detect_empty_slots(image_shape, boxes)
def draw_empty_slots(
self,
image_bgr: np.ndarray,
empty_slots: List[Dict[str, Any]],
box_color: Tuple[int, int, int] = (0, 0, 255),
thickness: int = 3,
draw_fill: bool = True
) -> np.ndarray:
"""Draws RED bounding boxes around empty shelf spaces with semi-transparent fill."""
from empty_shelf_detector import EmptyShelfDetector
empty_detector = EmptyShelfDetector()
return empty_detector.draw_empty_slots(
image_bgr,
empty_slots,
box_color=box_color,
thickness=thickness,
draw_fill=draw_fill
)

View file

@ -0,0 +1,41 @@
import os
import sys
import io
import urllib.request
from PIL import Image
# Ensure stdout handles utf-8 cleanly
if sys.platform.startswith('win'):
sys.stdout.reconfigure(encoding='utf-8')
urls = {
'sku_011': ('https://www.kagome.co.jp/products/brand/ys100/regular/img/img_products_apple@2x_v2.png', '野菜生活100 アップルサラダ'),
'sku_012': ('https://www.kagome.co.jp/products/brand/ys100/regular/img/img_products_lemon@2x_v2.png', '野菜生活100 レモンサラダ'),
'sku_013': ('https://www.kagome.co.jp/products/brand/ys100/yssmoothie/img/green-product-pic_2.png', '野菜生活100 Smoothie グリーンスムージー'),
'sku_014': ('https://www.kagome.co.jp/products/brand/ys100/yssmoothie/img/orange-product-pic_2.png', '野菜生活100 Smoothie ビタミンスムージー'),
'sku_015': ('https://www.kagome.co.jp/products/brand/ys100/yssmoothie/img/yellow-product-pic_2.png', '野菜生活100 Smoothie バナナ&アーモンドmix'),
'sku_016': ('https://www.kagome.co.jp/products/brand/ys100/yssmoothie/img/red-product-pic_2.png', '野菜生活100 Smoothie アサイー&ベリーmix')
}
os.makedirs('stock', exist_ok=True)
for sku, (url, name) in urls.items():
print(f'Fetching {sku}: {name} from {url}...')
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'})
try:
with urllib.request.urlopen(req, timeout=15) as resp:
data = resp.read()
img = Image.open(io.BytesIO(data))
if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info):
bg = Image.new('RGB', img.size, (255, 255, 255))
if img.mode != 'RGBA':
img = img.convert('RGBA')
bg.paste(img, mask=img.split()[3])
img = bg
else:
img = img.convert('RGB')
out_path = f'stock/{sku}.jpeg'
img.save(out_path, 'JPEG', quality=95)
print(f'Successfully saved {sku}: {name} -> {out_path} ({img.size})')
except Exception as e:
print(f'Error downloading {sku} ({url}): {e}')

View file

@ -0,0 +1,286 @@
"""
Empty Shelf / Out-of-Stock (OOS) Slot Detector
Algorithm: Planogram Geometric Row-Clustering & Gap Analysis
Methodology:
1. Clusters YOLO product bounding boxes into horizontal shelf tiers/rows.
2. Filters out vertical architectural dividers / central pillars.
3. Identifies horizontal gaps exceeding expected product pitch.
4. Computes precise red bounding boxes for empty shelf slots.
"""
import cv2
import numpy as np
from typing import List, Dict, Any, Tuple, Optional
class EmptyShelfDetector:
def __init__(
self,
min_gap_ratio: float = 0.75,
min_row_products: int = 4,
row_height_tol: float = 0.35,
pillar_margin: int = 25
):
"""
Args:
min_gap_ratio: Minimum ratio of gap width to median item width in row (default 0.75).
min_row_products: Minimum products required to establish a valid shelf row.
row_height_tol: Vertical clustering tolerance relative to item height.
pillar_margin: Buffer zone around detected vertical pillars/dividers.
"""
self.min_gap_ratio = min_gap_ratio
self.min_row_products = min_row_products
self.row_height_tol = row_height_tol
self.pillar_margin = pillar_margin
def cluster_rows(self, boxes: List[List[int]]) -> List[List[List[int]]]:
"""Clusters bounding boxes [x1, y1, x2, y2] into horizontal shelf rows."""
if not boxes:
return []
# Sort by vertical center
sorted_boxes = sorted(boxes, key=lambda b: (b[1] + b[3]) / 2)
rows: List[List[List[int]]] = []
for b in sorted_boxes:
yc = (b[1] + b[3]) / 2
placed = False
for r in rows:
ryc = float(np.mean([(x[1] + x[3]) / 2 for x in r]))
rh = float(np.mean([x[3] - x[1] for x in r]))
if abs(yc - ryc) < self.row_height_tol * rh:
r.append(b)
placed = True
break
if not placed:
rows.append([b])
# Filter rows with sufficient products and sort each row left-to-right
valid_rows = []
for r in rows:
if len(r) >= self.min_row_products:
r.sort(key=lambda b: b[0])
valid_rows.append(r)
return valid_rows
def detect_vertical_pillars(
self,
rows: List[List[List[int]]],
image_width: int
) -> List[Tuple[int, int]]:
"""
Identifies structural vertical shelf dividers (pillars) across rows.
A pillar exhibits consistent gaps across multiple vertical shelf rows.
"""
if len(rows) < 2:
return []
# Count gap intersections across rows in 10px bins
bin_size = 10
num_bins = int(np.ceil(image_width / bin_size))
gap_votes = np.zeros(num_bins, dtype=int)
for row in rows:
for j in range(len(row) - 1):
x1 = row[j][2]
x2 = row[j + 1][0]
gap_w = x2 - x1
# Architectural dividers typically span 40px - 180px
if 40 <= gap_w <= 180:
start_bin = int(x1 // bin_size)
end_bin = min(num_bins, int(np.ceil(x2 / bin_size)))
gap_votes[start_bin:end_bin] += 1
# A pillar should have gap votes in at least 50% of the shelf rows
min_votes = max(2, int(len(rows) * 0.5))
pillars = []
in_pillar = False
p_start = 0
for b in range(num_bins):
if gap_votes[b] >= min_votes:
if not in_pillar:
in_pillar = True
p_start = b * bin_size
else:
if in_pillar:
in_pillar = False
p_end = b * bin_size
pillars.append((max(0, p_start - self.pillar_margin), min(image_width, p_end + self.pillar_margin)))
if in_pillar:
pillars.append((max(0, p_start - self.pillar_margin), image_width))
return pillars
def detect_empty_slots(
self,
image_shape: Tuple[int, int],
product_boxes: List[Any],
custom_pillars: Optional[List[Tuple[int, int]]] = None
) -> List[Dict[str, Any]]:
"""
Detects empty shelf slots from product boxes.
Args:
image_shape: (height, width) of the shelf image.
product_boxes: List of bounding boxes [x1, y1, x2, y2] or dicts with 'box' key.
custom_pillars: Optional pre-defined pillar intervals [(x1, x2), ...].
Returns:
List of empty slot dictionaries:
[
{
"id": "empty_1",
"box": [x1, y1, x2, y2],
"width": w,
"height": h,
"missing_items_est": int,
"center": [cx, cy],
"row_index": int
}, ...
]
"""
img_h, img_w = image_shape[:2]
if not product_boxes:
return []
# Standardize box format
boxes: List[List[int]] = []
for b in product_boxes:
if isinstance(b, dict) and "box" in b:
boxes.append([int(v) for v in b["box"]])
elif isinstance(b, (list, tuple)) and len(b) >= 4:
boxes.append([int(b[0]), int(b[1]), int(b[2]), int(b[3])])
rows = self.cluster_rows(boxes)
if not rows:
return []
# Detect structural vertical pillars/frames
pillars = custom_pillars if custom_pillars is not None else self.detect_vertical_pillars(rows, img_w)
empty_slots: List[Dict[str, Any]] = []
slot_counter = 1
for row_idx, row in enumerate(rows):
med_y1 = int(np.median([b[1] for b in row]))
med_y2 = int(np.median([b[3] for b in row]))
med_w = float(np.median([b[2] - b[0] for b in row]))
med_h = med_y2 - med_y1
for j in range(len(row) - 1):
x1 = row[j][2]
x2 = row[j + 1][0]
gap_w = x2 - x1
if gap_w < self.min_gap_ratio * med_w:
continue
# Check if this gap overlaps significantly with a known structural pillar
is_pillar = False
for px1, px2 in pillars:
overlap_start = max(x1, px1)
overlap_end = min(x2, px2)
if overlap_end > overlap_start:
overlap_len = overlap_end - overlap_start
if overlap_len >= 0.6 * gap_w or (x1 >= px1 and x2 <= px2) or (x1 <= px1 and x2 >= px2):
is_pillar = True
break
if is_pillar:
continue
missing_est = max(1, int(round(gap_w / med_w)))
cx = int((x1 + x2) / 2)
cy = int((med_y1 + med_y2) / 2)
empty_slots.append({
"id": f"empty_{slot_counter}",
"box": [int(x1), int(med_y1), int(x2), int(med_y2)],
"width": int(gap_w),
"height": int(med_h),
"missing_items_est": missing_est,
"center": [cx, cy],
"row_index": row_idx + 1
})
slot_counter += 1
return empty_slots
def draw_empty_slots(
self,
image: np.ndarray,
empty_slots: List[Dict[str, Any]],
box_color: Tuple[int, int, int] = (0, 0, 255),
thickness: int = 3,
draw_fill: bool = True,
draw_label: bool = False
) -> np.ndarray:
"""
Draws red bounding boxes around empty shelf slots.
Args:
image: BGR numpy image.
empty_slots: List of empty slot dictionaries.
box_color: BGR color (default Red: (0, 0, 255)).
thickness: Bounding box line thickness.
draw_fill: Whether to render a semi-transparent fill tint.
draw_label: Whether to render label text.
"""
annotated = image.copy()
if draw_fill and empty_slots:
overlay = annotated.copy()
for slot in empty_slots:
x1, y1, x2, y2 = slot["box"]
cv2.rectangle(overlay, (x1, y1), (x2, y2), box_color, -1)
cv2.addWeighted(overlay, 0.22, annotated, 0.78, 0, annotated)
for slot in empty_slots:
x1, y1, x2, y2 = slot["box"]
cv2.rectangle(annotated, (x1, y1), (x2, y2), box_color, thickness)
if draw_label:
est = slot.get("missing_items_est", 1)
text = f"Empty (~{est})"
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 0.5
tw, th = cv2.getTextSize(text, font, font_scale, 1)[0]
cv2.rectangle(annotated, (x1, max(0, y1 - th - 6)), (x1 + tw + 6, y1), box_color, -1)
cv2.putText(annotated, text, (x1 + 3, y1 - 4), font, font_scale, (255, 255, 255), 1, cv2.LINE_AA)
return annotated
def detect_and_draw_empty_slots(
image_path: str,
detector_weights: str = "runs/detect/train/weights/best.pt",
output_path: Optional[str] = "detected_empty_slots.jpg"
) -> Tuple[np.ndarray, List[Dict[str, Any]]]:
"""
Convenience function: runs YOLO detection, extracts empty shelf slots,
and draws red bounding boxes.
"""
from detector import ShelfProductDetector
image = cv2.imread(image_path)
if image is None:
raise ValueError(f"Failed to read image at: {image_path}")
detector = ShelfProductDetector(detector_weights)
det_res = detector.detect(image)
boxes = det_res.get("boxes", [])
empty_detector = EmptyShelfDetector()
empty_slots = empty_detector.detect_empty_slots(image.shape, boxes)
# Draw products in existing clean bounding boxes (green)
annotated = detector.draw_detections(image, boxes, draw_labels=False)
# Draw empty slots in RED
annotated = empty_detector.draw_empty_slots(annotated, empty_slots, box_color=(0, 0, 255), thickness=3)
if output_path:
cv2.imwrite(output_path, annotated)
return annotated, empty_slots

View file

@ -0,0 +1,6 @@
from ultralytics import YOLO
model = YOLO("/Users/nguyenhongvan/recongnize/runs/detect/train/weights/best.pt")
image_path = "/Users/nguyenhongvan/recongnize/data/add/image7.jpg"
results = model(image_path)
results[0].show()

View file

@ -0,0 +1,327 @@
"""
Feature Matching & Classification Module
Implements SURF, ORB, BRISK, and Hybrid matching according to
the methodology of Electronics 2023, 12(17), 3640.
"""
import os
import cv2
import numpy as np
from typing import Dict, List, Tuple, Optional, Any
class LocalFeatureExtractor:
"""Encapsulates individual OpenCV local feature algorithms."""
def __init__(self, method: str = "orb"):
self.method = method.lower()
self.detector = self._create_detector(self.method)
self.norm_type = self._get_norm_type(self.method)
def _create_detector(self, method: str):
if method == "surf":
# Check if SURF is supported in the current OpenCV build
if hasattr(cv2, "xfeatures2d") and hasattr(cv2.xfeatures2d, "SURF_create"):
try:
return cv2.xfeatures2d.SURF_create(hessianThreshold=400, nOctaves=4, nOctaveLayers=3)
except Exception as e:
print(f"[Warning] SURF initialization failed: {e}. Falling back to SIFT.")
else:
print("[Info] SURF not available in this OpenCV build. Using SIFT as high-grade floating point descriptor.")
return cv2.SIFT_create(nfeatures=1000)
elif method == "orb":
return cv2.ORB_create(
nfeatures=1500,
scaleFactor=1.2,
nlevels=8,
edgeThreshold=31,
firstLevel=0,
WTA_K=2,
scoreType=cv2.ORB_HARRIS_SCORE,
patchSize=31,
fastThreshold=20
)
elif method == "brisk":
return cv2.BRISK_create(thresh=30, octaves=3, patternScale=1.0)
elif method == "sift":
return cv2.SIFT_create(nfeatures=1000)
else:
raise ValueError(f"Unsupported feature extractor method: {method}")
def _get_norm_type(self, method: str):
# Binary descriptors use Hamming distance; floating-point descriptors use L2 distance
if method in ["orb", "brisk"]:
return cv2.NORM_HAMMING
return cv2.NORM_L2
def detect_and_compute(self, image_gray: np.ndarray) -> Tuple[List[cv2.KeyPoint], Optional[np.ndarray]]:
"""Detects keypoints and computes their descriptors."""
if image_gray is None or image_gray.size == 0:
return [], None
return self.detector.detectAndCompute(image_gray, None)
class TemplateFeatureDatabase:
"""Precomputes and caches local feature descriptors for all stock templates."""
def __init__(self, stock_dir: str = "stock"):
self.stock_dir = stock_dir
self.templates: Dict[str, Dict[str, Any]] = {}
self.extractors = {
"surf": LocalFeatureExtractor("surf"),
"orb": LocalFeatureExtractor("orb"),
"brisk": LocalFeatureExtractor("brisk")
}
self.load_and_cache_templates()
def load_and_cache_templates(self):
"""Loads all SKU images and extracts their multi-feature descriptors."""
if not os.path.exists(self.stock_dir):
return
image_files = [
f for f in os.listdir(self.stock_dir)
if f.lower().endswith((".jpeg", ".jpg", ".png"))
]
print(f"[FeatureDatabase] Pre-caching {len(image_files)} stock SKU templates...")
for filename in image_files:
sku_id = os.path.splitext(filename)[0]
filepath = os.path.join(self.stock_dir, filename)
img = cv2.imread(filepath)
if img is None:
continue
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
features = {}
for name, extractor in self.extractors.items():
kp, des = extractor.detect_and_compute(gray)
features[name] = {"keypoints": kp, "descriptors": des}
self.templates[sku_id] = {
"sku_id": sku_id,
"filepath": filepath,
"image": img,
"gray": gray,
"features": features
}
print(f"[FeatureDatabase] Successfully cached {len(self.templates)} templates.")
class ProductFeatureMatcher:
"""
Main product classification engine.
Matches a query cropped bounding box against cached SKU templates using
SURF, ORB, BRISK, or the Paper's Hybrid combination.
"""
def __init__(self, stock_dir: str = "stock"):
self.db = TemplateFeatureDatabase(stock_dir)
self.extractors = self.db.extractors
def match_descriptors(
self,
des_query: Optional[np.ndarray],
des_template: Optional[np.ndarray],
norm_type: int,
ratio_thresh: float = 0.75
) -> List[cv2.DMatch]:
"""Performs k-NN matching with Lowe's ratio test."""
if des_query is None or des_template is None:
return []
if len(des_query) < 2 or len(des_template) < 2:
return []
bf = cv2.BFMatcher(norm_type, crossCheck=False)
try:
raw_matches = bf.knnMatch(des_query, des_template, k=2)
except Exception:
return []
good_matches = []
for match_pair in raw_matches:
if len(match_pair) == 2:
m, n = match_pair
if m.distance < ratio_thresh * n.distance:
good_matches.append(m)
return good_matches
def compute_geometric_inliers(
self,
kp_query: List[cv2.KeyPoint],
kp_template: List[cv2.KeyPoint],
good_matches: List[cv2.DMatch]
) -> Tuple[int, List[cv2.DMatch], Optional[np.ndarray]]:
"""Applies RANSAC Homography to verify geometric consistency."""
if len(good_matches) < 4:
return len(good_matches), good_matches, None
src_pts = np.float32([kp_query[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
dst_pts = np.float32([kp_template[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
try:
H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
if mask is None:
return len(good_matches), good_matches, None
inlier_matches = [good_matches[i] for i in range(len(good_matches)) if mask[i][0] == 1]
return len(inlier_matches), inlier_matches, H
except Exception:
return len(good_matches), good_matches, None
def classify_crop(
self,
crop_bgr: np.ndarray,
method: str = "hybrid"
) -> Dict[str, Any]:
"""
Classifies a product crop against all SKU templates.
Methods supported: 'surf', 'orb', 'brisk', 'hybrid'.
"""
if crop_bgr is None or crop_bgr.size == 0:
return {"status": "error", "message": "Empty crop image"}
crop_gray = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2GRAY)
method = method.lower()
scores: Dict[str, float] = {}
details: Dict[str, Dict[str, Any]] = {}
if method in ["surf", "orb", "brisk"]:
extractor = self.extractors.get(method)
if extractor is None:
extractor = LocalFeatureExtractor(method)
kp_query, des_query = extractor.detect_and_compute(crop_gray)
for sku_id, tmpl in self.db.templates.items():
kp_tmpl = tmpl["features"][method]["keypoints"]
des_tmpl = tmpl["features"][method]["descriptors"]
good_matches = self.match_descriptors(
des_query, des_tmpl, extractor.norm_type, ratio_thresh=0.75
)
inlier_count, inliers, _ = self.compute_geometric_inliers(
kp_query, kp_tmpl, good_matches
)
# Score based on geometric inliers & match ratio
denom = max(1, min(len(kp_query), len(kp_tmpl)))
inlier_ratio = inlier_count / denom
score = inlier_count * 2.0 + inlier_ratio * 100.0
scores[sku_id] = score
details[sku_id] = {
"inliers": inlier_count,
"inlier_matches": inliers,
"total_good": len(good_matches),
"kp_query": kp_query,
"kp_tmpl": kp_tmpl,
"score": score
}
elif method == "hybrid":
# Paper's hybrid strategy combining SURF, ORB, and BRISK
sub_methods = ["surf", "orb", "brisk"]
weights = {"surf": 0.45, "orb": 0.25, "brisk": 0.30}
# Pre-compute features for query crop across all 3 extractors
query_features = {}
for sm in sub_methods:
kp_q, des_q = self.extractors[sm].detect_and_compute(crop_gray)
query_features[sm] = (kp_q, des_q)
for sku_id, tmpl in self.db.templates.items():
composite_score = 0.0
sku_sub_details = {}
for sm in sub_methods:
kp_q, des_q = query_features[sm]
kp_t = tmpl["features"][sm]["keypoints"]
des_t = tmpl["features"][sm]["descriptors"]
norm_t = self.extractors[sm].norm_type
matches = self.match_descriptors(des_q, des_t, norm_t, ratio_thresh=0.75)
inliers, inlier_list, _ = self.compute_geometric_inliers(kp_q, kp_t, matches)
denom = max(1, min(len(kp_q), len(kp_t)))
sub_score = inliers * 1.5 + (inliers / denom) * 80.0
composite_score += weights[sm] * sub_score
sku_sub_details[sm] = {
"inliers": inliers,
"matches": len(matches),
"kp_q_count": len(kp_q),
"kp_t_count": len(kp_t),
"sub_score": round(sub_score, 2)
}
scores[sku_id] = composite_score
details[sku_id] = {
"score": composite_score,
"sub_details": sku_sub_details,
"kp_query": query_features["surf"][0],
"kp_tmpl": tmpl["features"]["surf"]["keypoints"]
}
else:
return {"status": "error", "message": f"Unknown method: {method}"}
# Rank templates by score
if not scores:
return {"status": "error", "message": "No templates available"}
sorted_candidates = sorted(scores.items(), key=lambda x: x[1], reverse=True)
top_sku, top_score = sorted_candidates[0]
runner_up_sku, runner_up_score = sorted_candidates[1] if len(sorted_candidates) > 1 else (None, 0.0)
confidence = round(min(1.0, top_score / max(1.0, (top_score + runner_up_score * 0.5))), 4)
return {
"status": "success",
"method": method,
"best_sku": top_sku,
"score": round(top_score, 2),
"confidence": confidence,
"ranked_candidates": [
{"sku_id": s, "score": round(sc, 2)} for s, sc in sorted_candidates[:5]
],
"details": details[top_sku]
}
def generate_match_visualization(
self,
crop_bgr: np.ndarray,
matched_sku: str,
method: str = "surf",
max_matches: int = 30
) -> Optional[np.ndarray]:
"""Draws visual matching lines between crop and matched template."""
if matched_sku not in self.db.templates:
return None
tmpl = self.db.templates[matched_sku]
crop_gray = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2GRAY)
eval_method = method if method in ["surf", "orb", "brisk"] else "surf"
extractor = self.extractors[eval_method]
kp_q, des_q = extractor.detect_and_compute(crop_gray)
kp_t = tmpl["features"][eval_method]["keypoints"]
des_t = tmpl["features"][eval_method]["descriptors"]
matches = self.match_descriptors(des_q, des_t, extractor.norm_type, ratio_thresh=0.75)
_, inliers, _ = self.compute_geometric_inliers(kp_q, kp_t, matches)
matches_to_draw = inliers[:max_matches] if inliers else matches[:max_matches]
vis = cv2.drawMatches(
crop_bgr, kp_q,
tmpl["image"], kp_t,
matches_to_draw, None,
matchColor=(0, 255, 0),
singlePointColor=(0, 0, 255),
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
)
return vis

View file

@ -0,0 +1,92 @@
"""
Native Desktop GUI for Supermarket Shelf Product Recognition
Opens a desktop OpenCV window displaying image.jpg with bounding boxes only.
Users can click on any product directly on screen to inspect price, stock,
and feature matching inliers in real-time.
"""
import os
import sys
import cv2
import numpy as np
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
from pipeline import ShelfRecognitionPipeline
def main():
print("=" * 60)
print("STARTING NATIVE DESKTOP INTERFACE")
print("Image: image.jpg | Mode: Hybrid (Paper Method)")
print("Instructions: Click on ANY product to identify & view info.")
print("Press 'q' or 'ESC' on the window to exit.")
print("=" * 60)
pipeline = ShelfRecognitionPipeline()
init_res = pipeline.set_active_shelf_image("image.jpg")
if init_res.get("status") != "success":
print("[Error] Failed to load image.jpg")
return
boxes = init_res["boxes"]
base_image = pipeline.current_image.copy()
# Draw ONLY bounding boxes (no class names, no confidence labels)
display_image = pipeline.detector.draw_detections(base_image, boxes, draw_labels=False)
window_name = "Smart Shelf - Click Any Product (Press Q to quit)"
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
cv2.resizeWindow(window_name, 1280, 850)
def on_mouse(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
print(f"\n[Click detected at coordinates: ({x}, {y})]")
res = pipeline.process_click(x, y, method="hybrid")
if res.get("status") == "success":
prod = res["inventory"]
cls_info = res["classification"]
box_info = res["target_box"]
print("-" * 55)
print(f"📦 Product Identified : {prod['product_name']}")
print(f"🏷️ SKU ID : {prod['sku_id']}")
print(f"💰 Price : ¥{prod['price']}")
print(f"📊 In Stock (On-Shelf): {prod['number_on']} units")
print(f"🚚 Received / Sold : {prod['number_in']} in / {prod['number_out']} out")
print(f"📅 Expiration Date : {prod['expiration_date']}")
print(f"🎯 Match Confidence : {cls_info['confidence'] * 100:.1f}% (Score: {cls_info['score']})")
print("-" * 55)
# Update main window with selected box highlight (in red)
selected_display = pipeline.detector.draw_detections(
base_image, boxes, selected_box_id=box_info["id"], draw_labels=False
)
cv2.imshow(window_name, selected_display)
# Show visual match comparison window if available
if res.get("match_visualization") is not None:
match_win = "Feature Matcher (Shelf Crop vs Stock Template)"
cv2.namedWindow(match_win, cv2.WINDOW_NORMAL)
cv2.resizeWindow(match_win, 800, 400)
cv2.imshow(match_win, res["match_visualization"])
else:
print(f"[Info] {res.get('message', 'No product found at coordinates.')}")
cv2.setMouseCallback(window_name, on_mouse)
while True:
cv2.imshow(window_name, display_image)
key = cv2.waitKey(20) & 0xFF
if key in [ord('q'), ord('Q'), 27]:
break
cv2.destroyAllWindows()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,90 @@
"""
Inventory Management Module
Handles reading and querying product metadata from stock.csv.
"""
import os
import sys
import csv
from typing import Dict, Optional, Any, List
if sys.platform.startswith('win'):
try:
sys.stdout.reconfigure(encoding='utf-8')
except Exception:
pass
class InventoryManager:
def __init__(self, stock_dir: str = "stock", csv_filename: str = "stock.csv"):
self.stock_dir = stock_dir
self.csv_path = os.path.join(stock_dir, csv_filename)
self.products: Dict[str, Dict[str, Any]] = {}
self.load_inventory()
def load_inventory(self) -> None:
"""Loads inventory data from the CSV file."""
if not os.path.exists(self.csv_path):
print(f"[Warning] Inventory CSV not found at: {self.csv_path}")
return
with open(self.csv_path, mode="r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
sku_id = row.get("SKU_ID", "").strip()
if not sku_id:
continue
# Locate the corresponding template image in stock_dir
local_image = self._find_template_image(sku_id)
self.products[sku_id] = {
"sku_id": sku_id,
"product_name": row.get("product_name", "").strip(),
"receipt_date": row.get("receipt_date", "").strip(),
"expiration_date": row.get("expiration_date", "").strip(),
"number_in": int(row.get("number_in", 0) or 0),
"number_out": int(row.get("number_out", 0) or 0),
"number_on": int(row.get("number_on", 0) or 0),
"price": float(row.get("price", 0) or 0),
"raw_image_link": row.get("image_link", "").strip(),
"local_image_path": local_image
}
def _find_template_image(self, sku_id: str) -> Optional[str]:
"""Finds the local image file for the given SKU_ID."""
extensions = [".jpeg", ".jpg", ".png"]
for ext in extensions:
candidate = os.path.join(self.stock_dir, f"{sku_id}{ext}")
if os.path.exists(candidate):
return candidate
return None
def get_product(self, sku_id: str) -> Optional[Dict[str, Any]]:
"""Retrieves product information for a given SKU_ID."""
return self.products.get(sku_id)
def list_all_skus(self) -> List[str]:
"""Returns all available SKU IDs."""
return list(self.products.keys())
def update_stock(self, sku_id: str, quantity_delta: int) -> bool:
"""Updates the on-shelf quantity (e.g., when purchased or restocked)."""
if sku_id in self.products:
current = self.products[sku_id]["number_on"]
new_val = max(0, current + quantity_delta)
self.products[sku_id]["number_on"] = new_val
if quantity_delta < 0:
self.products[sku_id]["number_out"] += abs(quantity_delta)
else:
self.products[sku_id]["number_in"] += quantity_delta
return True
return False
if __name__ == "__main__":
inv = InventoryManager("stock")
print(f"Loaded {len(inv.products)} products.")
for sku in inv.list_all_skus():
p = inv.get_product(sku)
print(f"[{sku}] {p['product_name']} | Price: {p['price']} | In-Stock: {p['number_on']} | Image: {p['local_image_path']}")

View file

View file

@ -0,0 +1,181 @@
"""
End-to-End Supermarket Shelf Recognition Pipeline
Integrates:
1. YOLO object detector for shelf product localization
2. SURF / ORB / BRISK / Hybrid local feature matcher for classification
3. Stock CSV inventory lookup for real-time pricing and stock quantities
"""
import os
import sys
import cv2
import numpy as np
from typing import Dict, Optional, Any
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
from detector import ShelfProductDetector
from feature_matcher import ProductFeatureMatcher
from inventory import InventoryManager
class ShelfRecognitionPipeline:
"""Master pipeline managing interactive supermarket shelf recognition."""
def __init__(
self,
weights_path: str = "runs/detect/train/weights/best.pt",
stock_dir: str = "stock",
stock_csv: str = "stock.csv"
):
print("[Pipeline] Initializing Inventory Manager...")
self.inventory = InventoryManager(stock_dir=stock_dir, csv_filename=stock_csv)
print("[Pipeline] Initializing Feature Matcher...")
self.matcher = ProductFeatureMatcher(stock_dir=stock_dir)
print("[Pipeline] Initializing Shelf Detector...")
self.detector = ShelfProductDetector(weights_path=weights_path)
self.current_image_path: Optional[str] = None
self.current_image: Optional[np.ndarray] = None
self.current_detections: list = []
self.current_empty_slots: list = []
def set_active_shelf_image(self, image_input: Any) -> Dict[str, Any]:
"""Loads and runs detection on a new shelf image."""
if isinstance(image_input, str):
self.current_image_path = image_input
self.current_image = cv2.imread(image_input)
else:
self.current_image_path = "memory_buffer"
self.current_image = image_input
if self.current_image is None:
return {"status": "error", "message": "Invalid or unreadable image"}
det_result = self.detector.detect(self.current_image)
self.current_detections = det_result.get("boxes", [])
# Detect empty shelf spaces (out-of-stock gaps)
self.current_empty_slots = self.detector.detect_empty_slots(
self.current_image.shape, self.current_detections
)
return {
"status": "success",
"image_shape": self.current_image.shape,
"total_detected": len(self.current_detections),
"boxes": self.current_detections,
"empty_slots": self.current_empty_slots
}
def process_click(
self,
x: int,
y: int,
method: str = "hybrid",
tolerance_px: int = 40
) -> Dict[str, Any]:
"""
Handles user click at coordinate (x, y) on the shelf image:
1. Identifies the bounding box
2. Crops product ROI
3. Classifies via SURF/ORB/BRISK/Hybrid matching
4. Queries inventory for price, quantity, and metadata
"""
if self.current_image is None:
return {"status": "error", "message": "No active shelf image loaded."}
target_box = self.detector.find_box_at_point(
x, y, self.current_detections, tolerance_px=tolerance_px
)
if target_box is None:
# Check if user clicked an empty shelf space
for slot in getattr(self, "current_empty_slots", []):
sx1, sy1, sx2, sy2 = slot["box"]
if (sx1 - 10 <= x <= sx2 + 10) and (sy1 - 10 <= y <= sy2 + 10):
return {
"status": "empty_slot",
"clicked_point": (x, y),
"empty_slot": slot,
"message": f"Chỗ trống trên kệ (Khu vực hàng hết / Row {slot['row_index']}). Ước tính thiếu ~{slot['missing_items_est']} sản phẩm."
}
return {
"status": "not_found",
"clicked_point": (x, y),
"message": f"No product detected near coordinate ({x}, {y}). Please click inside a product packaging."
}
# Crop ROI
crop = self.detector.crop_roi(self.current_image, target_box["box"])
# Classify with selected method
cls_result = self.matcher.classify_crop(crop, method=method)
if cls_result.get("status") != "success":
return {
"status": "classification_failed",
"target_box": target_box,
"message": cls_result.get("message", "Feature matching failed.")
}
best_sku = cls_result["best_sku"]
product_info = self.inventory.get_product(best_sku)
# Generate match visualization
vis_match = self.matcher.generate_match_visualization(crop, best_sku, method=method)
# Generate annotated shelf image highlighting the selected item
annotated_shelf = self.detector.draw_detections(
self.current_image, self.current_detections, selected_box_id=target_box["id"]
)
return {
"status": "success",
"clicked_point": (x, y),
"target_box": target_box,
"classification": {
"method": method,
"matched_sku": best_sku,
"confidence": cls_result["confidence"],
"score": cls_result["score"],
"top_candidates": cls_result["ranked_candidates"],
},
"inventory": product_info,
"crop_image": crop,
"annotated_shelf": annotated_shelf,
"match_visualization": vis_match
}
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Test Shelf Recognition Pipeline")
parser.add_argument("--image", type=str, default="runs/detect/train/train_batch0.jpg")
parser.add_argument("--method", type=str, default="hybrid", choices=["surf", "orb", "brisk", "hybrid"])
args = parser.parse_args()
pipeline = ShelfRecognitionPipeline()
print(f"Loading image: {args.image}")
init_res = pipeline.set_active_shelf_image(args.image)
print(f"Total detected items: {init_res['total_detected']}")
if init_res['boxes']:
# Simulate click on the center of the first detected product
first_box = init_res['boxes'][0]
cx, cy = first_box['center']
print(f"Simulating click on Product #{first_box['id']} at ({cx}, {cy})...")
res = pipeline.process_click(cx, cy, method=args.method)
print("\n--- Recognition & Inventory Result ---")
print(f"Status: {res['status']}")
if res['status'] == 'success':
prod = res['inventory']
print(f"Matched SKU: {res['classification']['matched_sku']} (Score: {res['classification']['score']})")
print(f"Product Name: {prod['product_name']}")
print(f"Price: {prod['price']} JPY")
print(f"In Stock: {prod['number_on']} units (Received: {prod['number_in']}, Sold: {prod['number_out']})")
print(f"Expiration Date: {prod['expiration_date']}")

View file

@ -0,0 +1,84 @@
import cv2
import matplotlib.pyplot as plt
import numpy as np
def suft_function(image):
image1 = cv2.imread(image)
# Convert the training image to RGB
training_image = cv2.cvtColor(image1, cv2.COLOR_BGR2RGB)
# Convert the training image to gray scale
training_gray = cv2.cvtColor(training_image, cv2.COLOR_RGB2GRAY)
# Create test image by adding Scale Invariance and Rotational Invariance
test_image = cv2.pyrDown(training_image)
test_image = cv2.pyrDown(test_image)
num_rows, num_cols = test_image.shape[:2]
rotation_matrix = cv2.getRotationMatrix2D((num_cols/2, num_rows/2), 30, 1)
test_image = cv2.warpAffine(test_image, rotation_matrix, (num_cols, num_rows))
test_gray = cv2.cvtColor(test_image, cv2.COLOR_RGB2GRAY)
# Display traning image and testing image
fx, plots = plt.subplots(1, 2, figsize=(20,10))
plots[0].set_title("Training Image")
plots[0].imshow(training_image)
plots[1].set_title("Testing Image")
plots[1].imshow(test_image)
# ## Detect keypoints and Create Descriptor
surf = cv2.xfeatures2d.SURF_create(800)
train_keypoints, train_descriptor = surf.detectAndCompute(training_gray, None)
test_keypoints, test_descriptor = surf.detectAndCompute(test_gray, None)
keypoints_without_size = np.copy(training_image)
keypoints_with_size = np.copy(training_image)
cv2.drawKeypoints(training_image, train_keypoints, keypoints_without_size, color = (0, 255, 0))
cv2.drawKeypoints(training_image, train_keypoints, keypoints_with_size, flags = cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# Display image with and without keypoints size
fx, plots = plt.subplots(1, 2, figsize=(20,10))
plots[0].set_title("Train keypoints With Size")
plots[0].imshow(keypoints_with_size, cmap='gray')
plots[1].set_title("Train keypoints Without Size")
plots[1].imshow(keypoints_without_size, cmap='gray')
# Print the number of keypoints detected in the training image
print("Number of Keypoints Detected In The Training Image: ", len(train_keypoints))
# Print the number of keypoints detected in the query image
print("Number of Keypoints Detected In The Query Image: ", len(test_keypoints))
# ## Matching Keypoints
# Create a Brute Force Matcher object.
bf = cv2.BFMatcher(cv2.NORM_L1, crossCheck = False)
# Perform the matching between the SURF descriptors of the training image and the test image
matches = bf.match(train_descriptor, test_descriptor)
# The matches with shorter distance are the ones we want.
matches = sorted(matches, key = lambda x : x.distance)
result = cv2.drawMatches(training_image, train_keypoints, test_gray, test_keypoints, matches, test_gray, flags = 2)
# Display the best matching points
plt.rcParams['figure.figsize'] = [14.0, 7.0]
plt.title('Best Matching Points')
plt.imshow(result)
plt.show()
# Print total number of matching points between the training and query images
print("\nNumber of Matching Keypoints Between The Training and Query Images: ", len(matches))
return keypoints_with_size

View file

@ -0,0 +1,25 @@
from ultralytics import YOLO
import cv2
model = YOLO("/Users/nguyenhongvan/recongnize/runs/detect/train/weights/best.pt")
image_path = "/Users/nguyenhongvan/recongnize/data/add/image9.jpg"
results = model(image_path)
image = cv2.imread(image_path)
for box in results[0].boxes.xyxy.cpu().numpy():
x1, y1, x2, y2 = map(int, box)
cv2.rectangle(
image,
(x1, y1),
(x2, y2),
(0, 255, 0),
2
)
cv2.imshow("Bounding Boxes Only", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

View file

@ -0,0 +1,20 @@
import sys
import os
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
from empty_shelf_detector import detect_and_draw_empty_slots
if __name__ == "__main__":
image_path = "image.jpg"
print(f"Processing empty shelf detection on {image_path}...")
annotated, empty_slots = detect_and_draw_empty_slots(
image_path,
output_path="detected_empty_slots.jpg"
)
print(f"Detected {len(empty_slots)} empty shelf slots:")
for s in empty_slots:
print(f" - Slot [{s['id']}]: Row {s['row_index']}, Box={s['box']}, Width={s['width']}px, Est. Missing Items={s['missing_items_est']}")
print(f"Visualization saved to: detected_empty_slots.jpg")

View file

@ -0,0 +1,89 @@
"""
Test Script for image.jpg
1. Runs YOLO detection on image.jpg.
2. Draws ONLY bounding box rectangles (no class names, no confidence labels).
3. Saves output to detected_image.jpg.
4. Matches the detected bounding boxes against stock templates.
"""
import os
import sys
import cv2
import numpy as np
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
from detector import ShelfProductDetector
from feature_matcher import ProductFeatureMatcher
from inventory import InventoryManager
def main():
image_path = "image.jpg"
print(f"[1/4] Loading {image_path}...")
image = cv2.imread(image_path)
if image is None:
print(f"[Error] Failed to read {image_path}")
return
h, w = image.shape[:2]
print(f" Image resolution: {w}x{h}")
print("[2/4] Detecting products using YOLO...")
detector = ShelfProductDetector("runs/detect/train/weights/best.pt")
# You can adjust conf_threshold as needed (default 0.25)
det_res = detector.detect(image, conf_threshold=0.25)
boxes = det_res.get("boxes", [])
print(f" Total detected items: {len(boxes)}")
print("[3/4] Drawing bounding boxes ONLY (no class names, no confidence)...")
annotated = detector.draw_detections(image, boxes, draw_labels=False)
output_path = "detected_image.jpg"
cv2.imwrite(output_path, annotated)
print(f" Saved detection visualization to: {output_path}")
print("[4/4] Matching against stock catalogue (SURF, ORB, BRISK, Hybrid)...")
matcher = ProductFeatureMatcher("stock")
inv = InventoryManager("stock")
print("\n--- Stock Templates Present in image.jpg ---")
matched_skus = set()
sample_matches = []
# Test top detections to find corresponding stock SKUs
for b in boxes:
crop = detector.crop_roi(image, b["box"])
# Use hybrid method from the paper
res = matcher.classify_crop(crop, method="hybrid")
if res.get("status") == "success":
sku_id = res["best_sku"]
score = res["score"]
conf = res["confidence"]
prod = inv.get_product(sku_id)
if prod:
matched_skus.add(sku_id)
sample_matches.append({
"box_id": b["id"],
"box": b["box"],
"sku_id": sku_id,
"product_name": prod["product_name"],
"price": prod["price"],
"number_on": prod["number_on"],
"score": score,
"confidence": conf
})
# Sort matches by confidence
sample_matches.sort(key=lambda x: x["score"], reverse=True)
print(f"\nSuccessfully matched {len(matched_skus)} distinct SKUs across detected shelf boxes:")
for sm in sample_matches[:10]:
print(f"Box #{sm['box_id']:<3} -> [{sm['sku_id']}] {sm['product_name']:<30} | Price: ¥{sm['price']:<5} | On-Shelf: {sm['number_on']} | Score: {sm['score']}")
print("\n[Complete] Tested image.jpg successfully.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,91 @@
"""
Unit and Integration Test Suite
Validates InventoryManager, ShelfProductDetector, ProductFeatureMatcher, and Pipeline.
"""
import os
import sys
import cv2
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
from inventory import InventoryManager
from feature_matcher import ProductFeatureMatcher
from detector import ShelfProductDetector
from pipeline import ShelfRecognitionPipeline
def test_inventory():
print("[TEST] Testing InventoryManager...")
inv = InventoryManager("stock")
assert len(inv.products) >= 10, f"Expected products >= 10 in stock.csv, found {len(inv.products)}"
p1 = inv.get_product("sku_001")
assert p1 is not None, "sku_001 not found"
assert p1["product_name"] == "明治 おいしい牛乳"
assert p1["price"] == 258.0
assert p1["number_on"] == 73
print(" -> InventoryManager test: PASSED")
def test_feature_matcher():
print("[TEST] Testing ProductFeatureMatcher...")
matcher = ProductFeatureMatcher("stock")
assert len(matcher.db.templates) >= 7, f"Expected templates >= 7, got {len(matcher.db.templates)}"
for method in ["hybrid", "orb", "brisk"]:
tmpl_img = matcher.db.templates["sku_001"]["image"]
res = matcher.classify_crop(tmpl_img, method=method)
assert res["status"] == "success", f"Method {method} failed"
assert res["best_sku"] == "sku_001", f"Expected sku_001 with {method}, got {res['best_sku']}"
print(f" -> {method.upper()} classification test: PASSED (Score: {res['score']}, Conf: {res['confidence']})")
def test_detector():
print("[TEST] Testing ShelfProductDetector...")
detector = ShelfProductDetector("runs/detect/train/weights/best.pt")
sample_shelf = "runs/detect/train/train_batch0.jpg"
res = detector.detect(sample_shelf)
assert res["status"] == "success", "Detection failed"
assert res["total_detected"] > 0, "No boxes detected"
print(f" -> Detector test: PASSED ({res['total_detected']} items localized)")
first_box = res["boxes"][0]
cx, cy = first_box["center"]
hit = detector.find_box_at_point(cx, cy, res["boxes"])
assert hit is not None, "Hit test failed on center coordinate"
assert hit["id"] == first_box["id"], "Hit test returned incorrect box"
print(" -> Spatial hit-test: PASSED")
def test_pipeline():
print("[TEST] Testing End-to-End Pipeline...")
pipeline = ShelfRecognitionPipeline(
weights_path="runs/detect/train/weights/best.pt",
stock_dir="stock"
)
init_res = pipeline.set_active_shelf_image("runs/detect/train/train_batch0.jpg")
assert init_res["status"] == "success"
assert len(init_res["boxes"]) > 0
first_box = init_res["boxes"][0]
cx, cy = first_box["center"]
click_res = pipeline.process_click(cx, cy, method="hybrid")
assert click_res["status"] == "success", f"Click processing failed: {click_res.get('message')}"
assert "inventory" in click_res
assert click_res["inventory"] is not None
print(f" -> End-to-End Pipeline test: PASSED (Matched: {click_res['classification']['matched_sku']} - {click_res['inventory']['product_name']})")
if __name__ == "__main__":
print("=" * 60)
print("STARTING TEST SUITE")
print("=" * 60)
test_inventory()
test_feature_matcher()
test_detector()
test_pipeline()
print("=" * 60)
print("ALL TESTS PASSED SUCCESSFULLY!")
print("=" * 60)

View file

@ -0,0 +1,32 @@
from ultralytics import YOLO
# Load a model
model = YOLO("yolo26n.pt") # load a pretrained model (recommended for training)
EPOCHS = 10
IMG_SIZE = 640
BATCH_SIZE = 8
WORKERS = 2
PATIENCE = 5
SEED = 42
# Train the model
results = model.train(data="/Users/nguyenhongvan/recongnize/data/SKU/data_kaggle.yaml", epochs=EPOCHS,
imgsz=IMG_SIZE,
batch=BATCH_SIZE,
workers=WORKERS,
patience=PATIENCE,
cache=True,
amp=True,
cos_lr=True,
optimizer="AdamW",
seed=SEED,
exist_ok=True
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

BIN
recognization/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

View file

@ -0,0 +1,9 @@
torch>=2.0.0
torchvision>=0.15.0
ultralytics>=8.0.0
opencv-python>=4.8.0
numpy>=1.23.0,<2.0.0
fastapi>=0.100.0
uvicorn>=0.20.0
python-multipart>=0.0.6
requests>=2.28.0

View file

@ -0,0 +1,9 @@
@echo off
echo ==========================================================
echo DORAYAKI - SMART SHELF RECOGNITION AI APP
echo ==========================================================
echo [1/2] Installing / Checking dependencies...
python -m pip install -r requirements.txt
echo [2/2] Starting Dorayaki Server on port 8080...
python code/app.py --port 8080
pause

8
recognization/run_app.sh Normal file
View file

@ -0,0 +1,8 @@
#!/bin/bash
echo '=========================================================='
echo ' DORAYAKI - SMART SHELF RECOGNITION AI APP'
echo '=========================================================='
echo '[1/2] Installing / Checking dependencies...'
pip install -r requirements.txt
echo '[2/2] Starting Dorayaki Server on port 8080...'
python3 code/app.py --port 8080

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View file

@ -0,0 +1,115 @@
task: detect
mode: train
model: yolo26n.pt
data: /Users/nguyenhongvan/recongnize/data/SKU/data_kaggle.yaml
epochs: 10
time: null
patience: 5
batch: 8
imgsz: 640
save: true
save_period: -1
cache: true
device: ''
workers: 2
project: null
name: train
exist_ok: true
pretrained: true
cls_remap: true
optimizer: AdamW
verbose: true
seed: 42
deterministic: true
single_cls: false
rect: false
cos_lr: true
close_mosaic: 10
resume: false
amp: true
fraction: 1.0
profile: false
freeze: null
multi_scale: 0.0
compile: false
channels_last: null
overlap_mask: true
mask_ratio: 4
dropout: 0.0
val: true
split: val
save_json: false
conf: null
iou: 0.7
max_det: 300
quantize: null
dnn: false
plots: true
nms: null
source: null
vid_stride: 1
stream_buffer: false
visualize: false
augment: false
agnostic_nms: false
classes: null
retina_masks: false
embed: null
show: false
save_frames: false
save_txt: false
save_conf: false
save_crop: false
show_labels: true
show_conf: true
show_boxes: true
line_width: null
format: torchscript
keras: false
optimize: false
dynamic: false
simplify: true
opset: null
workspace: null
lr0: 0.01
lrf: 0.01
momentum: 0.937
weight_decay: 0.0005
warmup_epochs: 3.0
warmup_momentum: 0.8
warmup_bias_lr: 0.1
distill_model: null
dis: 6.0
box: 7.5
cls: 0.5
cls_pw: 0.0
dfl: 1.5
pose: 12.0
kobj: 1.0
rle: 1.0
angle: 1.0
dlog: 1.0
dgrad: 0.5
dlam: 1.0
nbs: 64
hsv_h: 0.015
hsv_s: 0.7
hsv_v: 0.4
degrees: 0.0
translate: 0.1
scale: 0.5
shear: 0.0
perspective: 0.0
flipud: 0.0
fliplr: 0.5
bgr: 0.0
mosaic: 1.0
mixup: 0.0
cutmix: 0.0
copy_paste: 0.0
copy_paste_mode: flip
auto_augment: randaugment
erasing: 0.4
cfg: null
tracker: tracktrack.yaml
save_dir: /Users/nguyenhongvan/recongnize/runs/detect/train

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View file

@ -0,0 +1,11 @@
epoch,time,train/box_loss,train/cls_loss,train/l1_loss,metrics/precision(B),metrics/recall(B),metrics/mAP50(B),metrics/mAP50-95(B),val/box_loss,val/cls_loss,val/l1_loss,lr/pg0,lr/pg1,lr/pg2
1,1565.97,1.87842,1.31062,0.00473,0.77594,0.66625,0.7326,0.37541,1.82395,1.09779,0.00496,0.00333008,0.00333008,0.0700293
2,3114.04,1.84885,1.03949,0.00464,0.82083,0.71551,0.78275,0.40906,1.71736,0.96438,0.00463,0.00650198,0.00650198,0.0398679
3,4713.02,1.79522,0.94506,0.0044,0.83292,0.73762,0.80286,0.44416,1.73388,0.90931,0.00467,0.00905169,0.00905169,0.00908424
4,6295.49,1.74914,0.88703,0.00428,0.85104,0.7522,0.82324,0.46497,1.64389,0.80414,0.00435,0.00795954,0.00795954,0.00795954
5,7858.73,1.7071,0.84335,0.00412,0.85919,0.7726,0.84039,0.48024,1.65108,0.78838,0.00438,0.00657963,0.00657963,0.00657963
6,9442.04,1.68114,0.80819,0.004,0.87317,0.78189,0.8509,0.49609,1.59614,0.74834,0.00414,0.00505,0.00505,0.00505
7,11003.4,1.64466,0.77151,0.00386,0.87628,0.79337,0.85823,0.50336,1.5728,0.70957,0.0041,0.00352037,0.00352037,0.00352037
8,12622.9,1.63356,0.75494,0.00384,0.88156,0.79801,0.86493,0.50986,1.56183,0.69742,0.00404,0.00214046,0.00214046,0.00214046
9,14222.6,1.60481,0.73602,0.00376,0.88322,0.8031,0.86886,0.51655,1.53246,0.69223,0.00395,0.00104537,0.00104537,0.00104537
10,15812.1,1.59291,0.72805,0.0037,0.88426,0.80407,0.8692,0.518,1.51821,0.68363,0.00389,0.00034227,0.00034227,0.00034227
1 epoch time train/box_loss train/cls_loss train/l1_loss metrics/precision(B) metrics/recall(B) metrics/mAP50(B) metrics/mAP50-95(B) val/box_loss val/cls_loss val/l1_loss lr/pg0 lr/pg1 lr/pg2
2 1 1565.97 1.87842 1.31062 0.00473 0.77594 0.66625 0.7326 0.37541 1.82395 1.09779 0.00496 0.00333008 0.00333008 0.0700293
3 2 3114.04 1.84885 1.03949 0.00464 0.82083 0.71551 0.78275 0.40906 1.71736 0.96438 0.00463 0.00650198 0.00650198 0.0398679
4 3 4713.02 1.79522 0.94506 0.0044 0.83292 0.73762 0.80286 0.44416 1.73388 0.90931 0.00467 0.00905169 0.00905169 0.00908424
5 4 6295.49 1.74914 0.88703 0.00428 0.85104 0.7522 0.82324 0.46497 1.64389 0.80414 0.00435 0.00795954 0.00795954 0.00795954
6 5 7858.73 1.7071 0.84335 0.00412 0.85919 0.7726 0.84039 0.48024 1.65108 0.78838 0.00438 0.00657963 0.00657963 0.00657963
7 6 9442.04 1.68114 0.80819 0.004 0.87317 0.78189 0.8509 0.49609 1.59614 0.74834 0.00414 0.00505 0.00505 0.00505
8 7 11003.4 1.64466 0.77151 0.00386 0.87628 0.79337 0.85823 0.50336 1.5728 0.70957 0.0041 0.00352037 0.00352037 0.00352037
9 8 12622.9 1.63356 0.75494 0.00384 0.88156 0.79801 0.86493 0.50986 1.56183 0.69742 0.00404 0.00214046 0.00214046 0.00214046
10 9 14222.6 1.60481 0.73602 0.00376 0.88322 0.8031 0.86886 0.51655 1.53246 0.69223 0.00395 0.00104537 0.00104537 0.00104537
11 10 15812.1 1.59291 0.72805 0.0037 0.88426 0.80407 0.8692 0.518 1.51821 0.68363 0.00389 0.00034227 0.00034227 0.00034227

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,007 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View file

@ -0,0 +1,22 @@
SKU_ID,product_name,receipt_date,expiration_date,number_in,number_out,number_on,image_link,price
sku_001,明治 おいしい牛乳,2026-09-12,2026-09-25,120,47,73,'/Users/nguyenhongvan/app/recognization/stock/sku_001.jpeg',258
sku_002,野菜生活100 グリーンサラダ,2026-09-11,2027-03-11,130,45,85,'/Users/nguyenhongvan/app/recognization/stock/sku_002.jpeg,179
sku_003,Tropicana エッセンシャルズ マルチビタミン,2026-09-12,2027-03-12,140,52,88,'/Users/nguyenhongvan/app/recognization/stock/sku_003.jpg',168
sku_004,伊藤園 1日分の野菜,2026-09-09,2027-03-09,200,96,104,'/Users/nguyenhongvan/app/recognization/stock/sku_004.jpeg',198
sku_005,野菜生活100 マンゴーサラダ,2026-09-11,2027-03-11,140,58,82,'/Users/nguyenhongvan/app/recognization/stock/sku_005.jpeg',179
sku_007,野菜生活100 オリジナル,2026-09-10,2027-03-10,180,82,98,'/Users/nguyenhongvan/app/recognization/stock/sku_007.jpeg',179
sku_008,明治プロビオヨーグルト R-1 ドリンクタイプ,2026-09-14,2026-09-28,120,45,75,'/Users/nguyenhongvan/app/recognization/stock/sku_008.png',250
sku_009,野菜生活100 ベリーサラダ,2026-09-11,2027-03-11,150,63,87,'/Users/nguyenhongvan/app/recognization/stock/sku_009.jpeg',179
sku_010,明治プロビオヨーグルト R-1 ドリンクタイプ 低糖・低カロリー,2026-09-14,2026-09-28,100,39,61,'/Users/nguyenhongvan/app/recognization/stock/sku_010.jpeg',250
sku_011,野菜生活100 アップルサラダ,2026-09-15,2027-03-15,150,65,85,'/Users/nguyenhongvan/app/recognization/stock/sku_011.jpeg',179
sku_012,ハッピーターン,2026-09-15,2027-03-15,180,72,108,'/Users/nguyenhongvan/app/recognization/stock/sku_012.jpeg',198
sku_012,手塩屋 8枚,2026-09-15,2027-03-15,150,65,85,'/Users/nguyenhongvan/app/recognization/stock/sku_013.jpeg',168
sku_016,新潟仕込み こだわりのり塩味 24枚,2026-09-15,2027-03-15,160,58,102,'/Users/nguyenhongvan/app/recognization/stock/sku_016.jpeg',178
sku_014,おばあちゃんのぽたぽた焼 20枚,2026-09-15,2027-03-15,170,82,88,'/Users/nguyenhongvan/app/recognization/stock/sku_014.jpeg',188
sku_013,ソフトサラダ 20枚,2026-09-15,2027-03-15,140,55,85,'/Users/nguyenhongvan/app/recognization/stock/sku_013.jpeg',188
sku_015,ばかうけ 青のりしょうゆ味,2026-09-15,2027-03-15,155,67,88,'/Users/nguyenhongvan/app/recognization/stock/sku_015.jpeg',168
sku_020,亀田のまがりせんべい 16枚,2026-09-15,2027-03-15,145,61,84,'/Users/nguyenhongvan/app/recognization/stock/sku_020.jpeg',188
sku_017,味しらべ,2026-09-15,2027-03-15,125,43,82,'/Users/nguyenhongvan/app/recognization/stock/sku_017.jpeg',188
sku_019,歌舞伎揚,2026-09-15,2027-03-15,135,54,81,'/Users/nguyenhongvan/rapp/recognization/stock/sku_019.jpeg',218
sku_021,岩塚の黒豆せんべい,2026-09-15,2027-03-15,120,46,74,'/Users/nguyenhongvan/app/recognization/stock/sku_021.jpeg',198
sku_022,持ちのこ,2026-09-15,2027-03-15,120,46,74,'/Users/nguyenhongvan/app/recognization/stock/sku_022.jpg',210
1 SKU_ID product_name receipt_date expiration_date number_in number_out number_on image_link price
2 sku_001 明治 おいしい牛乳 2026-09-12 2026-09-25 120 47 73 '/Users/nguyenhongvan/app/recognization/stock/sku_001.jpeg' 258
3 sku_002 野菜生活100 グリーンサラダ 2026-09-11 2027-03-11 130 45 85 '/Users/nguyenhongvan/app/recognization/stock/sku_002.jpeg 179
4 sku_003 Tropicana エッセンシャルズ マルチビタミン 2026-09-12 2027-03-12 140 52 88 '/Users/nguyenhongvan/app/recognization/stock/sku_003.jpg' 168
5 sku_004 伊藤園 1日分の野菜 2026-09-09 2027-03-09 200 96 104 '/Users/nguyenhongvan/app/recognization/stock/sku_004.jpeg' 198
6 sku_005 野菜生活100 マンゴーサラダ 2026-09-11 2027-03-11 140 58 82 '/Users/nguyenhongvan/app/recognization/stock/sku_005.jpeg' 179
7 sku_007 野菜生活100 オリジナル 2026-09-10 2027-03-10 180 82 98 '/Users/nguyenhongvan/app/recognization/stock/sku_007.jpeg' 179
8 sku_008 明治プロビオヨーグルト R-1 ドリンクタイプ 2026-09-14 2026-09-28 120 45 75 '/Users/nguyenhongvan/app/recognization/stock/sku_008.png' 250
9 sku_009 野菜生活100 ベリーサラダ 2026-09-11 2027-03-11 150 63 87 '/Users/nguyenhongvan/app/recognization/stock/sku_009.jpeg' 179
10 sku_010 明治プロビオヨーグルト R-1 ドリンクタイプ 低糖・低カロリー 2026-09-14 2026-09-28 100 39 61 '/Users/nguyenhongvan/app/recognization/stock/sku_010.jpeg' 250
11 sku_011 野菜生活100 アップルサラダ 2026-09-15 2027-03-15 150 65 85 '/Users/nguyenhongvan/app/recognization/stock/sku_011.jpeg' 179
12 sku_012 ハッピーターン 2026-09-15 2027-03-15 180 72 108 '/Users/nguyenhongvan/app/recognization/stock/sku_012.jpeg' 198
13 sku_012 手塩屋 8枚 2026-09-15 2027-03-15 150 65 85 '/Users/nguyenhongvan/app/recognization/stock/sku_013.jpeg' 168
14 sku_016 新潟仕込み こだわりのり塩味 24枚 2026-09-15 2027-03-15 160 58 102 '/Users/nguyenhongvan/app/recognization/stock/sku_016.jpeg' 178
15 sku_014 おばあちゃんのぽたぽた焼 20枚 2026-09-15 2027-03-15 170 82 88 '/Users/nguyenhongvan/app/recognization/stock/sku_014.jpeg' 188
16 sku_013 ソフトサラダ 20枚 2026-09-15 2027-03-15 140 55 85 '/Users/nguyenhongvan/app/recognization/stock/sku_013.jpeg' 188
17 sku_015 ばかうけ 青のりしょうゆ味 2026-09-15 2027-03-15 155 67 88 '/Users/nguyenhongvan/app/recognization/stock/sku_015.jpeg' 168
18 sku_020 亀田のまがりせんべい 16枚 2026-09-15 2027-03-15 145 61 84 '/Users/nguyenhongvan/app/recognization/stock/sku_020.jpeg' 188
19 sku_017 味しらべ 2026-09-15 2027-03-15 125 43 82 '/Users/nguyenhongvan/app/recognization/stock/sku_017.jpeg' 188
20 sku_019 歌舞伎揚 2026-09-15 2027-03-15 135 54 81 '/Users/nguyenhongvan/rapp/recognization/stock/sku_019.jpeg' 218
21 sku_021 岩塚の黒豆せんべい 2026-09-15 2027-03-15 120 46 74 '/Users/nguyenhongvan/app/recognization/stock/sku_021.jpeg' 198
22 sku_022 持ちのこ 2026-09-15 2027-03-15 120 46 74 '/Users/nguyenhongvan/app/recognization/stock/sku_022.jpg' 210

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 KiB