bekzhanK1 commited on
Commit
0256284
·
1 Parent(s): 5494efc

Clean deployment for HF Spaces - code only

Browse files
.gitignore ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ documents/
11
+
12
+ # Output directories
13
+ outputs/
14
+ output/
15
+ labelled/
16
+ pipeline_outputs/
17
+
18
+ # Model files (except stamp model which we'll handle separately)
19
+ # *.pt
20
+ *.pth
21
+ *.onnx
22
+ *.h5
23
+
24
+ # IDE
25
+ .vscode/
26
+ .idea/
27
+ *.swp
28
+ *.swo
29
+ *~
30
+
31
+ # OS
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # Environment
36
+ .env
37
+ venv/
38
+ env/
39
+ ENV/
40
+
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile for Hugging Face Spaces
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies for OpenCV and PyMuPDF
5
+ RUN apt-get update && apt-get install -y \
6
+ libgl1-mesa-glx \
7
+ libglib2.0-0 \
8
+ libsm6 \
9
+ libxext6 \
10
+ libxrender-dev \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Create user
14
+ RUN useradd -m -u 1000 user
15
+ USER user
16
+ ENV PATH="/home/user/.local/bin:$PATH"
17
+
18
+ WORKDIR /app
19
+
20
+ # Copy requirements and install Python dependencies
21
+ COPY --chown=user requirements.txt .
22
+ RUN pip install --no-cache-dir --user --upgrade -r requirements.txt
23
+
24
+ # Copy all application code
25
+ COPY --chown=user . .
26
+
27
+ # Create directories for models if needed
28
+ RUN mkdir -p stamp_detector signature qr
29
+
30
+ # Note: stamp_model.pt should be uploaded via HF Hub web interface or upload_model.py script
31
+ # The model will be available at stamp_detector/stamp_model.pt after upload
32
+
33
+ # Expose port (HF Spaces uses port 7860)
34
+ EXPOSE 7860
35
+
36
+ # Run FastAPI on port 7860
37
+ CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
38
+
README.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Document Processing Pipeline API
3
+ emoji: 📄
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # Document Processing Pipeline API
12
+
13
+ FastAPI service for detecting QR codes, signatures, and stamps in PDF documents.
14
+
15
+ ## Features
16
+
17
+ - **QR Code Detection**: Detects and decodes QR codes in documents
18
+ - **Signature Detection**: Uses YOLOv8s to detect signatures
19
+ - **Stamp Detection**: Uses YOLOv8 to detect stamps/seals
20
+ - **PDF Support**: Processes multi-page PDF documents
21
+
22
+ ## API Endpoints
23
+
24
+ - `POST /process-pdf` - Upload and process PDF file
25
+ - `POST /process-pdf-from-url` - Process PDF from URL (S3 or HTTP/HTTPS)
26
+ - `GET /docs` - Interactive API documentation
27
+ - `GET /health` - Health check
28
+
29
+ Visit `/docs` for interactive API documentation.
30
+
31
+ ## Usage
32
+
33
+ ### Process PDF via API
34
+
35
+ ```bash
36
+ curl -X POST "https://bekzhanK1-armeta-hackaton.hf.space/process-pdf" \
37
+ -F "file=@document.pdf" \
38
+ -F "dpi=200" \
39
+ -F "stamp_conf=0.25"
40
+ ```
41
+
42
+ ### Process PDF from URL
43
+
44
+ ```bash
45
+ curl -X POST "https://bekzhanK1-armeta-hackaton.hf.space/process-pdf-from-url?pdf_url=https://example.com/document.pdf"
46
+ ```
47
+
48
+ ## Model Requirements
49
+
50
+ - Signature model: Automatically downloaded from Hugging Face
51
+ - Stamp model: Must be uploaded to `stamp_detector/stamp_model.pt` in this repository
52
+
__pycache__/api.cpython-310.pyc DELETED
Binary file (7.29 kB)
 
__pycache__/pipeline.cpython-310.pyc DELETED
Binary file (12.8 kB)
 
api.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ FastAPI application for document processing pipeline.
4
+ Accepts PDF files and returns detection results in JSON format.
5
+ """
6
+
7
+ import os
8
+ import tempfile
9
+ from pathlib import Path
10
+ from typing import Optional
11
+ from urllib.parse import urlparse
12
+
13
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Query
14
+ from fastapi.responses import JSONResponse
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ import uvicorn
17
+ import httpx
18
+
19
+ from pipeline import process_pdf_pipeline, PDF_SUPPORT
20
+
21
+ app = FastAPI(
22
+ title="Document Processing Pipeline API",
23
+ description="API for QR code, signature, and stamp detection in PDF documents",
24
+ version="1.0.0"
25
+ )
26
+
27
+ # Enable CORS for all origins (adjust in production)
28
+ app.add_middleware(
29
+ CORSMiddleware,
30
+ allow_origins=["*"],
31
+ allow_credentials=True,
32
+ allow_methods=["*"],
33
+ allow_headers=["*"],
34
+ )
35
+
36
+
37
+ @app.get("/")
38
+ async def root():
39
+ """Health check endpoint."""
40
+ return {
41
+ "status": "ok",
42
+ "message": "Document Processing Pipeline API",
43
+ "pdf_support": PDF_SUPPORT
44
+ }
45
+
46
+
47
+ @app.get("/health")
48
+ async def health():
49
+ """Health check endpoint."""
50
+ return {"status": "healthy", "pdf_support": PDF_SUPPORT}
51
+
52
+
53
+ @app.post("/process-pdf")
54
+ async def process_pdf(
55
+ file: UploadFile = File(..., description="PDF file to process"),
56
+ dpi: int = 200,
57
+ stamp_conf: float = 0.25
58
+ ):
59
+ """
60
+ Process a PDF file and return detection results.
61
+
62
+ Args:
63
+ file: PDF file to upload
64
+ dpi: DPI for PDF to image conversion (default: 200)
65
+ stamp_conf: Confidence threshold for stamp detection (default: 0.25)
66
+
67
+ Returns:
68
+ JSON response with detection results
69
+ """
70
+ # Check if PDF support is available
71
+ if not PDF_SUPPORT:
72
+ raise HTTPException(
73
+ status_code=503,
74
+ detail="PDF processing is not available. Please install PyMuPDF: pip install PyMuPDF"
75
+ )
76
+
77
+ # Validate file type
78
+ if not file.filename.lower().endswith('.pdf'):
79
+ raise HTTPException(
80
+ status_code=400,
81
+ detail="Invalid file type. Only PDF files are supported."
82
+ )
83
+
84
+ # Create temporary file for uploaded PDF
85
+ temp_pdf = None
86
+ try:
87
+ # Save uploaded file to temporary location
88
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_pdf:
89
+ content = await file.read()
90
+ temp_pdf.write(content)
91
+ temp_pdf_path = temp_pdf.name
92
+
93
+ # Process the PDF
94
+ try:
95
+ result = process_pdf_pipeline(
96
+ pdf_path=temp_pdf_path,
97
+ output_dir=tempfile.gettempdir(), # Use temp directory
98
+ stamp_model_path="stamp_detector/stamp_model.pt",
99
+ stamp_conf=stamp_conf,
100
+ dpi=dpi,
101
+ save_intermediate=False
102
+ )
103
+
104
+ # Return the result as JSON
105
+ return JSONResponse(content=result)
106
+
107
+ except Exception as e:
108
+ raise HTTPException(
109
+ status_code=500,
110
+ detail=f"Error processing PDF: {str(e)}"
111
+ )
112
+
113
+ finally:
114
+ # Clean up temporary file
115
+ if temp_pdf and os.path.exists(temp_pdf_path):
116
+ try:
117
+ os.unlink(temp_pdf_path)
118
+ except Exception:
119
+ pass
120
+
121
+
122
+ @app.post("/process-pdf-advanced")
123
+ async def process_pdf_advanced(
124
+ file: UploadFile = File(..., description="PDF file to process"),
125
+ dpi: int = 200,
126
+ stamp_conf: float = 0.25,
127
+ stamp_model: Optional[str] = None
128
+ ):
129
+ """
130
+ Process a PDF file with advanced options.
131
+
132
+ Args:
133
+ file: PDF file to upload
134
+ dpi: DPI for PDF to image conversion (default: 200)
135
+ stamp_conf: Confidence threshold for stamp detection (default: 0.25)
136
+ stamp_model: Path to custom stamp model (optional)
137
+
138
+ Returns:
139
+ JSON response with detection results
140
+ """
141
+ # Check if PDF support is available
142
+ if not PDF_SUPPORT:
143
+ raise HTTPException(
144
+ status_code=503,
145
+ detail="PDF processing is not available. Please install PyMuPDF: pip install PyMuPDF"
146
+ )
147
+
148
+ # Validate file type
149
+ if not file.filename.lower().endswith('.pdf'):
150
+ raise HTTPException(
151
+ status_code=400,
152
+ detail="Invalid file type. Only PDF files are supported."
153
+ )
154
+
155
+ # Use default stamp model if not provided
156
+ stamp_model_path = stamp_model or "stamp_detector/stamp_model.pt"
157
+
158
+ # Validate stamp model exists
159
+ if not Path(stamp_model_path).exists():
160
+ raise HTTPException(
161
+ status_code=404,
162
+ detail=f"Stamp model not found: {stamp_model_path}"
163
+ )
164
+
165
+ # Create temporary file for uploaded PDF
166
+ temp_pdf = None
167
+ try:
168
+ # Save uploaded file to temporary location
169
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_pdf:
170
+ content = await file.read()
171
+ temp_pdf.write(content)
172
+ temp_pdf_path = temp_pdf.name
173
+
174
+ # Process the PDF
175
+ try:
176
+ result = process_pdf_pipeline(
177
+ pdf_path=temp_pdf_path,
178
+ output_dir=tempfile.gettempdir(), # Use temp directory
179
+ stamp_model_path=stamp_model_path,
180
+ stamp_conf=stamp_conf,
181
+ dpi=dpi,
182
+ save_intermediate=False
183
+ )
184
+
185
+ # Return the result as JSON
186
+ return JSONResponse(content=result)
187
+
188
+ except Exception as e:
189
+ raise HTTPException(
190
+ status_code=500,
191
+ detail=f"Error processing PDF: {str(e)}"
192
+ )
193
+
194
+ finally:
195
+ # Clean up temporary file
196
+ if temp_pdf and os.path.exists(temp_pdf_path):
197
+ try:
198
+ os.unlink(temp_pdf_path)
199
+ except Exception:
200
+ pass
201
+
202
+
203
+ @app.post("/process-pdf-from-url")
204
+ async def process_pdf_from_url(
205
+ pdf_url: str = Query(...,
206
+ description="URL to PDF file (S3 or HTTP/HTTPS)"),
207
+ dpi: int = Query(200, description="DPI for PDF to image conversion"),
208
+ stamp_conf: float = Query(
209
+ 0.25, description="Confidence threshold for stamp detection"),
210
+ stamp_model: Optional[str] = Query(
211
+ None, description="Path to custom stamp model")
212
+ ):
213
+ """
214
+ Process a PDF file from a URL (S3 or HTTP/HTTPS) and return detection results.
215
+
216
+ Args:
217
+ pdf_url: URL to the PDF file (e.g., s3://bucket/key or https://example.com/file.pdf)
218
+ dpi: DPI for PDF to image conversion (default: 200)
219
+ stamp_conf: Confidence threshold for stamp detection (default: 0.25)
220
+ stamp_model: Path to custom stamp model (optional)
221
+
222
+ Returns:
223
+ JSON response with detection results
224
+ """
225
+ # Check if PDF support is available
226
+ if not PDF_SUPPORT:
227
+ raise HTTPException(
228
+ status_code=503,
229
+ detail="PDF processing is not available. Please install PyMuPDF: pip install PyMuPDF"
230
+ )
231
+
232
+ # Validate URL
233
+ parsed_url = urlparse(pdf_url)
234
+ if not parsed_url.scheme:
235
+ raise HTTPException(
236
+ status_code=400,
237
+ detail="Invalid URL format. Must include scheme (http://, https://, or s3://)"
238
+ )
239
+
240
+ # Use default stamp model if not provided
241
+ stamp_model_path = stamp_model or "stamp_detector/stamp_model.pt"
242
+
243
+ # Validate stamp model exists
244
+ if not Path(stamp_model_path).exists():
245
+ raise HTTPException(
246
+ status_code=404,
247
+ detail=f"Stamp model not found: {stamp_model_path}"
248
+ )
249
+
250
+ temp_pdf_path = None
251
+ try:
252
+ # Download PDF from URL
253
+ print(f"Downloading PDF from: {pdf_url}")
254
+
255
+ if parsed_url.scheme == 's3':
256
+ # Handle S3 URLs
257
+ # For S3, we'll use boto3 if available, otherwise try presigned URL
258
+ try:
259
+ import boto3
260
+ from botocore.exceptions import ClientError
261
+
262
+ # Parse S3 URL: s3://bucket/key
263
+ bucket = parsed_url.netloc
264
+ key = parsed_url.path.lstrip('/')
265
+
266
+ # Download from S3
267
+ s3_client = boto3.client('s3')
268
+ temp_pdf_path = tempfile.mktemp(suffix='.pdf')
269
+
270
+ try:
271
+ s3_client.download_file(bucket, key, temp_pdf_path)
272
+ print(f"✓ Downloaded PDF from S3: s3://{bucket}/{key}")
273
+ except ClientError as e:
274
+ raise HTTPException(
275
+ status_code=404,
276
+ detail=f"Failed to download from S3: {str(e)}"
277
+ )
278
+
279
+ except ImportError:
280
+ # If boto3 is not available, try treating S3 URL as presigned URL
281
+ # Convert s3:// to https:// (assuming it's a presigned URL)
282
+ if pdf_url.startswith('s3://'):
283
+ raise HTTPException(
284
+ status_code=400,
285
+ detail="S3 URLs require boto3. Install with: pip install boto3, or use a presigned HTTPS URL"
286
+ )
287
+ # Fall through to HTTP handling
288
+ pdf_url = pdf_url.replace('s3://', 'https://', 1)
289
+
290
+ # Handle HTTP/HTTPS URLs (including presigned S3 URLs)
291
+ if parsed_url.scheme in ('http', 'https') or temp_pdf_path is None:
292
+ if temp_pdf_path is None:
293
+ temp_pdf_path = tempfile.mktemp(suffix='.pdf')
294
+
295
+ # 5 minute timeout
296
+ async with httpx.AsyncClient(timeout=300.0) as client:
297
+ try:
298
+ response = await client.get(pdf_url)
299
+ response.raise_for_status()
300
+
301
+ # Validate content type
302
+ content_type = response.headers.get(
303
+ 'content-type', '').lower()
304
+ if 'pdf' not in content_type and not pdf_url.lower().endswith('.pdf'):
305
+ raise HTTPException(
306
+ status_code=400,
307
+ detail=f"URL does not point to a PDF file. Content-Type: {content_type}"
308
+ )
309
+
310
+ # Save to temporary file
311
+ with open(temp_pdf_path, 'wb') as f:
312
+ f.write(response.content)
313
+ print(f"✓ Downloaded PDF from URL: {pdf_url}")
314
+
315
+ except httpx.HTTPStatusError as e:
316
+ raise HTTPException(
317
+ status_code=e.response.status_code,
318
+ detail=f"Failed to download PDF from URL: {str(e)}"
319
+ )
320
+ except httpx.RequestError as e:
321
+ raise HTTPException(
322
+ status_code=400,
323
+ detail=f"Error fetching PDF from URL: {str(e)}"
324
+ )
325
+
326
+ # Process the PDF
327
+ try:
328
+ result = process_pdf_pipeline(
329
+ pdf_path=temp_pdf_path,
330
+ output_dir=tempfile.gettempdir(),
331
+ stamp_model_path=stamp_model_path,
332
+ stamp_conf=stamp_conf,
333
+ dpi=dpi,
334
+ save_intermediate=False
335
+ )
336
+
337
+ # Return the result as JSON
338
+ return JSONResponse(content=result)
339
+
340
+ except Exception as e:
341
+ raise HTTPException(
342
+ status_code=500,
343
+ detail=f"Error processing PDF: {str(e)}"
344
+ )
345
+
346
+ finally:
347
+ # Clean up temporary file
348
+ if temp_pdf_path and os.path.exists(temp_pdf_path):
349
+ try:
350
+ os.unlink(temp_pdf_path)
351
+ except Exception:
352
+ pass
353
+
354
+
355
+ if __name__ == "__main__":
356
+ import os
357
+ port = int(os.environ.get("PORT", 8000))
358
+ uvicorn.run(
359
+ "api:app",
360
+ host="0.0.0.0",
361
+ port=port,
362
+ reload=False
363
+ )
pipeline.py ADDED
@@ -0,0 +1,526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unified Pipeline for Document Processing
4
+ Runs QR code detection, signature detection, and stamp detection in sequence
5
+ and combines all results into a single JSON file.
6
+ """
7
+
8
+ import sys
9
+ import json
10
+ import argparse
11
+ import cv2
12
+ import numpy as np
13
+ import tempfile
14
+ from pathlib import Path
15
+ from typing import Optional, Dict, Any, List
16
+
17
+ # Try to import PyMuPDF for PDF processing
18
+ try:
19
+ import fitz # PyMuPDF
20
+ PDF_SUPPORT = True
21
+ except ImportError:
22
+ PDF_SUPPORT = False
23
+ print("Warning: PyMuPDF not installed. PDF support disabled.")
24
+ print("Install with: pip install PyMuPDF")
25
+
26
+ # Add subdirectories to path for imports
27
+ sys.path.insert(0, str(Path(__file__).parent))
28
+
29
+ # Import detection functions
30
+ from qr.qr_extraction import process_image_no_save as process_qr
31
+ from signature.inference import detect_signatures
32
+ from stamp_detector.detect import detect_stamps_no_save
33
+
34
+
35
+ def pdf_to_images(pdf_path: str, dpi: int = 200) -> List[np.ndarray]:
36
+ """
37
+ Convert PDF pages to images.
38
+
39
+ Args:
40
+ pdf_path: Path to PDF file
41
+ dpi: Resolution for conversion (default: 200)
42
+
43
+ Returns:
44
+ List of images as numpy arrays (BGR format for OpenCV)
45
+ """
46
+ if not PDF_SUPPORT:
47
+ raise ImportError("PyMuPDF is required for PDF processing. Install with: pip install PyMuPDF")
48
+
49
+ doc = fitz.open(pdf_path)
50
+ images = []
51
+
52
+ for page_num in range(len(doc)):
53
+ page = doc[page_num]
54
+ # Convert to image with specified DPI
55
+ mat = fitz.Matrix(dpi / 72, dpi / 72) # 72 is default DPI
56
+ pix = page.get_pixmap(matrix=mat)
57
+
58
+ # Convert to numpy array
59
+ img_data = pix.tobytes("ppm")
60
+ # Use cv2 to decode PPM
61
+ nparr = np.frombuffer(img_data, np.uint8)
62
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
63
+
64
+ if img is not None:
65
+ images.append(img)
66
+
67
+ doc.close()
68
+ return images
69
+
70
+
71
+ def process_pdf_pipeline(
72
+ pdf_path: str,
73
+ output_dir: str = "pipeline_outputs",
74
+ stamp_model_path: str = "stamp_detector/stamp_model.pt",
75
+ stamp_conf: float = 0.25,
76
+ dpi: int = 200,
77
+ save_intermediate: bool = False
78
+ ) -> Dict[str, Any]:
79
+ """
80
+ Process a PDF file by converting each page to an image and running the pipeline.
81
+
82
+ Args:
83
+ pdf_path: Path to PDF file
84
+ output_dir: Directory for output files
85
+ stamp_model_path: Path to stamp model
86
+ stamp_conf: Confidence threshold for stamp detection
87
+ dpi: DPI for PDF to image conversion
88
+ save_intermediate: Whether to save intermediate results
89
+
90
+ Returns:
91
+ Combined results dictionary for all pages
92
+ """
93
+ pdf_path = Path(pdf_path)
94
+ output_dir = Path(output_dir)
95
+ output_dir.mkdir(exist_ok=True)
96
+
97
+ if not pdf_path.exists():
98
+ raise FileNotFoundError(f"PDF not found: {pdf_path}")
99
+
100
+ if not PDF_SUPPORT:
101
+ raise ImportError("PyMuPDF is required for PDF processing. Install with: pip install PyMuPDF")
102
+
103
+ print(f"\n{'='*70}")
104
+ print(f"Processing PDF: {pdf_path.name}")
105
+ print(f"{'='*70}\n")
106
+
107
+ # Convert PDF to images
108
+ print(f"📄 Converting PDF pages to images (DPI: {dpi})...")
109
+ try:
110
+ page_images = pdf_to_images(str(pdf_path), dpi=dpi)
111
+ print(f"✓ Converted {len(page_images)} page(s) to images\n")
112
+ except Exception as e:
113
+ raise RuntimeError(f"Failed to convert PDF to images: {e}")
114
+
115
+ # Process each page
116
+ all_pages = []
117
+ temp_dir = Path(tempfile.mkdtemp())
118
+
119
+ try:
120
+ for page_num, img in enumerate(page_images, 1):
121
+ print(f"\n{'='*70}")
122
+ print(f"Processing Page {page_num}/{len(page_images)}")
123
+ print(f"{'='*70}\n")
124
+
125
+ # Save temporary image for processing
126
+ temp_img_path = temp_dir / f"page_{page_num}.jpg"
127
+ cv2.imwrite(str(temp_img_path), img)
128
+
129
+ # Process the page
130
+ try:
131
+ page_result = process_image_pipeline(
132
+ str(temp_img_path),
133
+ output_dir=output_dir,
134
+ stamp_model_path=stamp_model_path,
135
+ stamp_conf=stamp_conf,
136
+ save_intermediate=save_intermediate
137
+ )
138
+
139
+ # Add page number to result
140
+ page_result["page_number"] = page_num
141
+ page_result["image"] = f"{pdf_path.stem}_page_{page_num}.jpg"
142
+ all_pages.append(page_result)
143
+
144
+ except Exception as e:
145
+ print(f"✗ Error processing page {page_num}: {str(e)}")
146
+ all_pages.append({
147
+ "page_number": page_num,
148
+ "image": f"{pdf_path.stem}_page_{page_num}.jpg",
149
+ "error": str(e)
150
+ })
151
+ finally:
152
+ # Clean up temporary directory
153
+ import shutil
154
+ shutil.rmtree(temp_dir, ignore_errors=True)
155
+
156
+ # Create combined summary
157
+ summary = {
158
+ "total_pages": len(all_pages),
159
+ "total_qr_codes": sum(p.get("summary", {}).get("qr_codes", 0) for p in all_pages),
160
+ "total_signatures": sum(p.get("summary", {}).get("signatures", 0) for p in all_pages),
161
+ "total_stamps": sum(p.get("summary", {}).get("stamps", 0) for p in all_pages),
162
+ "total_detections": sum(p.get("summary", {}).get("total", 0) for p in all_pages)
163
+ }
164
+
165
+ result = {
166
+ "pdf": pdf_path.name,
167
+ "pdf_path": str(pdf_path),
168
+ "summary": summary,
169
+ "pages": all_pages
170
+ }
171
+
172
+ print(f"\n{'='*70}")
173
+ print("PDF PROCESSING COMPLETE")
174
+ print(f"{'='*70}")
175
+ print(f"Total Pages: {summary['total_pages']}")
176
+ print(f"QR Codes: {summary['total_qr_codes']}")
177
+ print(f"Signatures: {summary['total_signatures']}")
178
+ print(f"Stamps: {summary['total_stamps']}")
179
+ print(f"Total: {summary['total_detections']}")
180
+ print(f"{'='*70}\n")
181
+
182
+ return result
183
+
184
+
185
+ def process_image_pipeline(
186
+ image_path: str,
187
+ output_dir: str = "pipeline_outputs",
188
+ qr_model_path: Optional[str] = None,
189
+ signature_model_path: Optional[str] = None,
190
+ stamp_model_path: str = "stamp_detector/stamp_model.pt",
191
+ stamp_conf: float = 0.25,
192
+ save_intermediate: bool = False
193
+ ) -> Dict[str, Any]:
194
+ """
195
+ Process a single image through all three detection models.
196
+
197
+ Args:
198
+ image_path: Path to input image
199
+ output_dir: Directory for output files
200
+ qr_model_path: Path to QR model (not used, kept for compatibility)
201
+ signature_model_path: Path to signature model (optional)
202
+ stamp_model_path: Path to stamp model
203
+ stamp_conf: Confidence threshold for stamp detection
204
+ save_intermediate: Whether to save intermediate results
205
+
206
+ Returns:
207
+ Combined results dictionary
208
+ """
209
+ image_path = Path(image_path)
210
+ output_dir = Path(output_dir)
211
+ output_dir.mkdir(exist_ok=True)
212
+
213
+ if not image_path.exists():
214
+ raise FileNotFoundError(f"Image not found: {image_path}")
215
+
216
+ print(f"\n{'='*70}")
217
+ print(f"Processing: {image_path.name}")
218
+ print(f"{'='*70}\n")
219
+
220
+ # Get image dimensions once (will be used to consolidate)
221
+ img_sample = cv2.imread(str(image_path))
222
+ if img_sample is None:
223
+ raise ValueError(f"Could not read image: {image_path}")
224
+ img_height, img_width = img_sample.shape[:2]
225
+
226
+ # Initialize result structure with consolidated image info
227
+ result = {
228
+ "image": image_path.name,
229
+ "image_dimensions": {
230
+ "width": img_width,
231
+ "height": img_height
232
+ },
233
+ "qr_codes": [],
234
+ "signatures": [],
235
+ "stamps": []
236
+ }
237
+
238
+ # Step 1: QR Code Detection
239
+ print("🔷 Step 1/3: QR Code Detection")
240
+ print("-" * 70)
241
+ try:
242
+ qr_result = process_qr(str(image_path))
243
+
244
+ if qr_result and qr_result.get("qr_codes", {}).get("items"):
245
+ result["qr_codes"] = qr_result["qr_codes"]["items"]
246
+ print(f"✓ Found {len(result['qr_codes'])} QR code(s)")
247
+ else:
248
+ print("✓ No QR codes detected")
249
+ except Exception as e:
250
+ print(f"✗ Error in QR detection: {str(e)}")
251
+ result["qr_error"] = str(e)
252
+
253
+ # Step 2: Signature Detection
254
+ print(f"\n🔷 Step 2/3: Signature Detection")
255
+ print("-" * 70)
256
+ try:
257
+ sig_result = detect_signatures(
258
+ str(image_path),
259
+ model=None, # Will auto-load
260
+ output_dir=None, # Don't save
261
+ signatures_dir=None, # Don't save
262
+ save_crops=False # Don't save crops
263
+ )
264
+
265
+ if sig_result and sig_result.get("signatures"):
266
+ # Clean up signature items (remove cropped_path if present, keep only essential data)
267
+ cleaned_signatures = []
268
+ for sig in sig_result["signatures"]:
269
+ cleaned_sig = {
270
+ "id": sig.get("signature_id"),
271
+ "confidence": sig.get("confidence"),
272
+ "bbox": sig.get("bbox")
273
+ }
274
+ cleaned_signatures.append(cleaned_sig)
275
+ result["signatures"] = cleaned_signatures
276
+ print(f"✓ Found {len(result['signatures'])} signature(s)")
277
+ else:
278
+ print("✓ No signatures detected")
279
+ except Exception as e:
280
+ print(f"✗ Error in signature detection: {str(e)}")
281
+ result["signature_error"] = str(e)
282
+
283
+ # Step 3: Stamp Detection
284
+ print(f"\n🔷 Step 3/3: Stamp Detection")
285
+ print("-" * 70)
286
+ try:
287
+ if not Path(stamp_model_path).exists():
288
+ raise FileNotFoundError(f"Stamp model not found: {stamp_model_path}")
289
+
290
+ stamp_result = detect_stamps_no_save(
291
+ str(image_path),
292
+ model_path=stamp_model_path,
293
+ conf=stamp_conf
294
+ )
295
+
296
+ if stamp_result and stamp_result.get("detections"):
297
+ # Clean up stamp items (keep only essential data, remove normalized bbox)
298
+ cleaned_stamps = []
299
+ for stamp in stamp_result["detections"]:
300
+ cleaned_stamp = {
301
+ "confidence": stamp.get("confidence"),
302
+ "bbox": stamp.get("bbox")
303
+ }
304
+ cleaned_stamps.append(cleaned_stamp)
305
+ result["stamps"] = cleaned_stamps
306
+ print(f"✓ Found {len(result['stamps'])} stamp(s)")
307
+ else:
308
+ print("✓ No stamps detected")
309
+ except Exception as e:
310
+ print(f"✗ Error in stamp detection: {str(e)}")
311
+ result["stamp_error"] = str(e)
312
+
313
+ # Create summary
314
+ result["summary"] = {
315
+ "qr_codes": len(result.get("qr_codes", [])),
316
+ "signatures": len(result.get("signatures", [])),
317
+ "stamps": len(result.get("stamps", [])),
318
+ "total": len(result.get("qr_codes", [])) + len(result.get("signatures", [])) + len(result.get("stamps", []))
319
+ }
320
+
321
+ print(f"\n{'='*70}")
322
+ print("SUMMARY")
323
+ print(f"{'='*70}")
324
+ print(f"QR Codes: {result['summary']['qr_codes']}")
325
+ print(f"Signatures: {result['summary']['signatures']}")
326
+ print(f"Stamps: {result['summary']['stamps']}")
327
+ print(f"Total: {result['summary']['total']}")
328
+ print(f"{'='*70}\n")
329
+
330
+ return result
331
+
332
+
333
+ def process_folder_pipeline(
334
+ input_folder: str,
335
+ output_dir: str = "pipeline_outputs",
336
+ stamp_model_path: str = "stamp_detector/stamp_model.pt",
337
+ stamp_conf: float = 0.25,
338
+ save_intermediate: bool = False
339
+ ) -> Dict[str, Any]:
340
+ """
341
+ Process all images in a folder through the pipeline.
342
+
343
+ Args:
344
+ input_folder: Folder containing input images
345
+ output_dir: Directory for output files
346
+ stamp_model_path: Path to stamp model
347
+ stamp_conf: Confidence threshold for stamp detection
348
+ save_intermediate: Whether to save intermediate results
349
+
350
+ Returns:
351
+ Combined results for all images
352
+ """
353
+ input_folder = Path(input_folder)
354
+ if not input_folder.exists():
355
+ raise FileNotFoundError(f"Input folder not found: {input_folder}")
356
+
357
+ # Supported image formats
358
+ image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.webp'}
359
+ image_files = [f for f in input_folder.iterdir()
360
+ if f.is_file() and f.suffix.lower() in image_extensions]
361
+
362
+ if not image_files:
363
+ print(f"No image files found in '{input_folder}'")
364
+ return {"images": [], "summary": {}}
365
+
366
+ print(f"\n{'='*70}")
367
+ print(f"Found {len(image_files)} image(s) to process")
368
+ print(f"{'='*70}\n")
369
+
370
+ all_results = []
371
+ for i, image_file in enumerate(image_files, 1):
372
+ print(f"\n[{i}/{len(image_files)}]")
373
+ try:
374
+ result = process_image_pipeline(
375
+ str(image_file),
376
+ output_dir=output_dir,
377
+ stamp_model_path=stamp_model_path,
378
+ stamp_conf=stamp_conf,
379
+ save_intermediate=save_intermediate
380
+ )
381
+ all_results.append(result)
382
+ except Exception as e:
383
+ print(f"✗ Error processing {image_file.name}: {str(e)}")
384
+ all_results.append({
385
+ "image": image_file.name,
386
+ "image_path": str(image_file),
387
+ "error": str(e)
388
+ })
389
+
390
+ # Create summary
391
+ summary = {
392
+ "total_images": len(all_results),
393
+ "total_qr_codes": sum(r.get("summary", {}).get("qr_codes", 0) for r in all_results),
394
+ "total_signatures": sum(r.get("summary", {}).get("signatures", 0) for r in all_results),
395
+ "total_stamps": sum(r.get("summary", {}).get("stamps", 0) for r in all_results),
396
+ "total_detections": sum(r.get("summary", {}).get("total", 0) for r in all_results)
397
+ }
398
+
399
+ final_result = {
400
+ "summary": summary,
401
+ "images": all_results
402
+ }
403
+
404
+ # Save combined JSON
405
+ output_dir = Path(output_dir)
406
+ output_dir.mkdir(exist_ok=True)
407
+ json_path = output_dir / "pipeline_results.json"
408
+ with open(json_path, 'w', encoding='utf-8') as f:
409
+ json.dump(final_result, f, indent=2, ensure_ascii=False)
410
+
411
+ print(f"\n{'='*70}")
412
+ print("PIPELINE COMPLETE")
413
+ print(f"{'='*70}")
414
+ print(f"Processed: {summary['total_images']} image(s)")
415
+ print(f"QR Codes: {summary['total_qr_codes']}")
416
+ print(f"Signatures: {summary['total_signatures']}")
417
+ print(f"Stamps: {summary['total_stamps']}")
418
+ print(f"Total: {summary['total_detections']}")
419
+ print(f"\nResults saved to: {json_path}")
420
+ print(f"{'='*70}\n")
421
+
422
+ return final_result
423
+
424
+
425
+ def main():
426
+ parser = argparse.ArgumentParser(
427
+ description="Unified pipeline for QR code, signature, and stamp detection"
428
+ )
429
+ parser.add_argument(
430
+ "input",
431
+ help="Input image file, PDF file, or folder containing images"
432
+ )
433
+ parser.add_argument(
434
+ "--output",
435
+ default="pipeline_outputs",
436
+ help="Output directory (default: pipeline_outputs)"
437
+ )
438
+ parser.add_argument(
439
+ "--stamp-model",
440
+ default="stamp_detector/stamp_model.pt",
441
+ help="Path to stamp model (default: stamp_detector/stamp_model.pt)"
442
+ )
443
+ parser.add_argument(
444
+ "--stamp-conf",
445
+ type=float,
446
+ default=0.25,
447
+ help="Confidence threshold for stamp detection (default: 0.25)"
448
+ )
449
+ parser.add_argument(
450
+ "--save-intermediate",
451
+ action="store_true",
452
+ help="Save intermediate results from each detection step"
453
+ )
454
+
455
+ parser.add_argument(
456
+ "--dpi",
457
+ type=int,
458
+ default=200,
459
+ help="DPI for PDF to image conversion (default: 200)"
460
+ )
461
+
462
+ args = parser.parse_args()
463
+
464
+ input_path = Path(args.input)
465
+
466
+ if input_path.is_file():
467
+ # Check if it's a PDF
468
+ if input_path.suffix.lower() == '.pdf':
469
+ if not PDF_SUPPORT:
470
+ print("Error: PyMuPDF is required for PDF processing.")
471
+ print("Install with: pip install PyMuPDF")
472
+ sys.exit(1)
473
+
474
+ # Process PDF
475
+ result = process_pdf_pipeline(
476
+ str(input_path),
477
+ output_dir=args.output,
478
+ stamp_model_path=args.stamp_model,
479
+ stamp_conf=args.stamp_conf,
480
+ dpi=args.dpi,
481
+ save_intermediate=args.save_intermediate
482
+ )
483
+
484
+ # Save JSON
485
+ output_dir = Path(args.output)
486
+ output_dir.mkdir(exist_ok=True)
487
+ json_path = output_dir / f"{input_path.stem}_pipeline_result.json"
488
+ with open(json_path, 'w', encoding='utf-8') as f:
489
+ json.dump(result, f, indent=2, ensure_ascii=False)
490
+ print(f"Results saved to: {json_path}")
491
+
492
+ else:
493
+ # Process single image
494
+ result = process_image_pipeline(
495
+ str(input_path),
496
+ output_dir=args.output,
497
+ stamp_model_path=args.stamp_model,
498
+ stamp_conf=args.stamp_conf,
499
+ save_intermediate=args.save_intermediate
500
+ )
501
+
502
+ # Save JSON
503
+ output_dir = Path(args.output)
504
+ output_dir.mkdir(exist_ok=True)
505
+ json_path = output_dir / f"{input_path.stem}_pipeline_result.json"
506
+ with open(json_path, 'w', encoding='utf-8') as f:
507
+ json.dump(result, f, indent=2, ensure_ascii=False)
508
+ print(f"Results saved to: {json_path}")
509
+
510
+ elif input_path.is_dir():
511
+ # Process folder
512
+ process_folder_pipeline(
513
+ str(input_path),
514
+ output_dir=args.output,
515
+ stamp_model_path=args.stamp_model,
516
+ stamp_conf=args.stamp_conf,
517
+ save_intermediate=args.save_intermediate
518
+ )
519
+ else:
520
+ print(f"Error: '{args.input}' is not a valid file or directory")
521
+ sys.exit(1)
522
+
523
+
524
+ if __name__ == "__main__":
525
+ main()
526
+
pipeline_outputs/docs_pipeline_result.json DELETED
@@ -1,101 +0,0 @@
1
- {
2
- "pdf": "docs.pdf",
3
- "pdf_path": "documents/docs.pdf",
4
- "summary": {
5
- "total_pages": 3,
6
- "total_qr_codes": 0,
7
- "total_signatures": 1,
8
- "total_stamps": 2,
9
- "total_detections": 3
10
- },
11
- "pages": [
12
- {
13
- "image": "docs_page_1.jpg",
14
- "image_dimensions": {
15
- "width": 3306,
16
- "height": 4678
17
- },
18
- "qr_codes": [],
19
- "signatures": [
20
- {
21
- "id": 1,
22
- "confidence": 0.5241817831993103,
23
- "bbox": {
24
- "x1": 1187.189453125,
25
- "y1": 2745.45556640625,
26
- "x2": 1849.0565185546875,
27
- "y2": 3305.53076171875,
28
- "width": 661.8670654296875,
29
- "height": 560.0751953125
30
- }
31
- }
32
- ],
33
- "stamps": [
34
- {
35
- "confidence": 0.7363,
36
- "bbox": {
37
- "x1": 1520,
38
- "y1": 2700,
39
- "x2": 2166,
40
- "y2": 3358,
41
- "width": 646,
42
- "height": 658
43
- }
44
- }
45
- ],
46
- "summary": {
47
- "qr_codes": 0,
48
- "signatures": 1,
49
- "stamps": 1,
50
- "total": 2
51
- },
52
- "page_number": 1
53
- },
54
- {
55
- "image": "docs_page_2.jpg",
56
- "image_dimensions": {
57
- "width": 3306,
58
- "height": 4678
59
- },
60
- "qr_codes": [],
61
- "signatures": [],
62
- "stamps": [],
63
- "summary": {
64
- "qr_codes": 0,
65
- "signatures": 0,
66
- "stamps": 0,
67
- "total": 0
68
- },
69
- "page_number": 2
70
- },
71
- {
72
- "image": "docs_page_3.jpg",
73
- "image_dimensions": {
74
- "width": 3306,
75
- "height": 4678
76
- },
77
- "qr_codes": [],
78
- "signatures": [],
79
- "stamps": [
80
- {
81
- "confidence": 0.7546,
82
- "bbox": {
83
- "x1": 1889,
84
- "y1": 3896,
85
- "x2": 2531,
86
- "y2": 4540,
87
- "width": 642,
88
- "height": 644
89
- }
90
- }
91
- ],
92
- "summary": {
93
- "qr_codes": 0,
94
- "signatures": 0,
95
- "stamps": 1,
96
- "total": 1
97
- },
98
- "page_number": 3
99
- }
100
- ]
101
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
qr/__pycache__/qr_extraction.cpython-310.pyc DELETED
Binary file (7.85 kB)
 
qr/qr_extraction.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Extract QR codes from images and save labeled images and JSON data."""
2
+ # ----------------------------------------------
3
+ # --- Author : Ahmet Ozlu
4
+ # --- Mail : ahmetozlu93@gmail.com
5
+ # --- Date : 17th September 2018
6
+ # --- Modified : QR code extraction only
7
+ # ----------------------------------------------
8
+
9
+ import cv2
10
+ import numpy as np
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+
15
+
16
+ def detect_qr_codes(img_original):
17
+ """
18
+ Detect QR codes in an image using multiple preprocessing approaches.
19
+
20
+ Parameters:
21
+ -----------
22
+ img_original : numpy.ndarray
23
+ Original BGR image
24
+
25
+ Returns:
26
+ --------
27
+ list
28
+ List of QR code dictionaries with 'x', 'y', 'width', 'height', 'data', 'points'
29
+ """
30
+ qr_detector = cv2.QRCodeDetector()
31
+ qr_codes = []
32
+ seen_qr_boxes = set()
33
+
34
+ def add_qr_code(qr_points, info, seen_set):
35
+ """Helper function to add QR code if not already detected"""
36
+ if qr_points is None or len(qr_points) == 0:
37
+ return False
38
+
39
+ qr_points = qr_points.astype(int)
40
+ x_coords = qr_points[:, 0]
41
+ y_coords = qr_points[:, 1]
42
+ x_min, x_max = int(x_coords.min()), int(x_coords.max())
43
+ y_min, y_max = int(y_coords.min()), int(y_coords.max())
44
+
45
+ # Check if we've already detected this QR code (within 10 pixels tolerance)
46
+ box_key = (x_min // 10, y_min // 10, x_max // 10, y_max // 10)
47
+ if box_key in seen_set:
48
+ return False
49
+
50
+ seen_set.add(box_key)
51
+ qr_codes.append({
52
+ 'x': x_min,
53
+ 'y': y_min,
54
+ 'width': x_max - x_min,
55
+ 'height': y_max - y_min,
56
+ 'data': info if info else '',
57
+ 'points': qr_points.tolist()
58
+ })
59
+ return True
60
+
61
+ # Try multiple preprocessing approaches for better QR code detection
62
+ test_images = [("original", img_original)]
63
+ gray = cv2.cvtColor(img_original, cv2.COLOR_BGR2GRAY)
64
+ test_images.append(("grayscale", gray))
65
+
66
+ # Apply CLAHE (Contrast Limited Adaptive Histogram Equalization)
67
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
68
+ gray_clahe = clahe.apply(gray)
69
+ test_images.append(("clahe", gray_clahe))
70
+
71
+ # Add thresholded versions
72
+ _, thresh1 = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
73
+ _, thresh2 = cv2.threshold(
74
+ gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
75
+ test_images.append(("binary", thresh1))
76
+ test_images.append(("otsu", thresh2))
77
+
78
+ # Add inverted versions (QR codes might be white on black)
79
+ test_images.append(("inverted", cv2.bitwise_not(gray)))
80
+ test_images.append(("inverted_clahe", cv2.bitwise_not(gray_clahe)))
81
+
82
+ # Try detection on each preprocessed image
83
+ for img_name, test_img in test_images:
84
+ if len(qr_codes) > 0:
85
+ print(f" QR code detected using: {img_name}")
86
+ break # Stop if we found QR codes
87
+
88
+ # Ensure image is in correct format (3-channel for color, 1-channel for grayscale)
89
+ if len(test_img.shape) == 2:
90
+ # Grayscale - convert to 3-channel for detection
91
+ test_img_3ch = cv2.cvtColor(test_img, cv2.COLOR_GRAY2BGR)
92
+ else:
93
+ test_img_3ch = test_img
94
+
95
+ # Try detectAndDecodeMulti first (for multiple QR codes)
96
+ try:
97
+ retval, decoded_info, points, straight_qrcode = qr_detector.detectAndDecodeMulti(
98
+ test_img_3ch)
99
+
100
+ if retval and points is not None:
101
+ # Handle both single and multiple QR codes
102
+ if isinstance(decoded_info, str):
103
+ decoded_info = [decoded_info]
104
+ points = [points]
105
+
106
+ for info, qr_points in zip(decoded_info, points):
107
+ if add_qr_code(qr_points, info, seen_qr_boxes):
108
+ print(f" QR code detected using: {img_name} (multi)")
109
+ except Exception as e:
110
+ pass
111
+
112
+ # Try single QR code detection as fallback
113
+ if len(qr_codes) == 0:
114
+ try:
115
+ retval, decoded_info, points, straight_qrcode = qr_detector.detectAndDecode(
116
+ test_img_3ch)
117
+ if retval and points is not None and len(points) > 0:
118
+ if add_qr_code(points, decoded_info, seen_qr_boxes):
119
+ print(f" QR code detected using: {img_name} (single)")
120
+ except Exception as e:
121
+ pass
122
+
123
+ return qr_codes
124
+
125
+
126
+ def process_image_no_save(input_path):
127
+ """
128
+ Process a single image and detect QR codes without saving images or JSON files.
129
+
130
+ Parameters:
131
+ -----------
132
+ input_path : str
133
+ Path to input image
134
+
135
+ Returns:
136
+ --------
137
+ dict
138
+ Dictionary with detection results (no files saved)
139
+ """
140
+ # Read the input image
141
+ img_original = cv2.imread(input_path)
142
+ if img_original is None:
143
+ print(f"Error: Could not read image {input_path}")
144
+ return None
145
+
146
+ # Detect QR codes
147
+ qr_codes = detect_qr_codes(img_original)
148
+
149
+ # Prepare QR codes for JSON
150
+ qr_codes_json = []
151
+ for i, qr in enumerate(qr_codes):
152
+ qr_json = {
153
+ "id": i + 1,
154
+ "x": qr['x'],
155
+ "y": qr['y'],
156
+ "width": qr['width'],
157
+ "height": qr['height'],
158
+ "data": qr['data']
159
+ }
160
+ # Optionally include corner points if needed
161
+ if 'points' in qr and len(qr['points']) > 0:
162
+ qr_json['corner_points'] = qr['points']
163
+ qr_codes_json.append(qr_json)
164
+
165
+ # Create output JSON structure
166
+ output_json = {
167
+ "image": Path(input_path).name,
168
+ "image_dimensions": {
169
+ "width": img_original.shape[1],
170
+ "height": img_original.shape[0]
171
+ },
172
+ "qr_codes": {
173
+ "count": len(qr_codes_json),
174
+ "items": qr_codes_json
175
+ }
176
+ }
177
+
178
+ return output_json
179
+
180
+
181
+ def process_image(input_path, output_folder='labelled', json_folder='outputs'):
182
+ """
183
+ Process a single image and detect QR codes.
184
+
185
+ Parameters:
186
+ -----------
187
+ input_path : str
188
+ Path to input image
189
+ output_folder : str
190
+ Folder to save labeled images
191
+ json_folder : str
192
+ Folder to save JSON files
193
+
194
+ Returns:
195
+ --------
196
+ dict
197
+ Dictionary with detection results
198
+ """
199
+ # Get filename without extension
200
+ filename = Path(input_path).stem
201
+ file_ext = Path(input_path).suffix
202
+
203
+ print(f"\n{'='*60}")
204
+ print(f"Processing: {Path(input_path).name}")
205
+ print(f"{'='*60}")
206
+
207
+ # Read the input image
208
+ img_original = cv2.imread(input_path)
209
+ if img_original is None:
210
+ print(f"Error: Could not read image {input_path}")
211
+ return None
212
+
213
+ # Detect QR codes
214
+ qr_codes = detect_qr_codes(img_original)
215
+
216
+ print(f"Found {len(qr_codes)} QR code(s)")
217
+
218
+ # Create labeled image
219
+ labeled_img = img_original.copy()
220
+
221
+ # Draw QR codes in blue color
222
+ for i, qr in enumerate(qr_codes):
223
+ # Draw bounding box
224
+ cv2.rectangle(labeled_img, (qr['x'], qr['y']),
225
+ (qr['x'] + qr['width'], qr['y'] + qr['height']),
226
+ (255, 0, 0), 2) # Blue color (BGR format)
227
+
228
+ # Draw QR code points/polygon
229
+ if len(qr['points']) >= 4:
230
+ pts = np.array(qr['points'], np.int32)
231
+ cv2.polylines(labeled_img, [pts], True, (255, 0, 0), 2)
232
+
233
+ # Add label
234
+ cv2.putText(labeled_img, f"QR {i+1}", (qr['x'], qr['y'] - 5),
235
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
236
+
237
+ # Add QR data text (if not too long)
238
+ if qr['data'] and len(qr['data']) < 50:
239
+ cv2.putText(labeled_img, qr['data'][:30],
240
+ (qr['x'], qr['y'] + qr['height'] + 20),
241
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
242
+
243
+ # Create output folders
244
+ os.makedirs(output_folder, exist_ok=True)
245
+ os.makedirs(json_folder, exist_ok=True)
246
+
247
+ # Save labeled image
248
+ output_image_path = os.path.join(
249
+ output_folder, f'qr_labelled_{filename}{file_ext}')
250
+ cv2.imwrite(output_image_path, labeled_img)
251
+
252
+ # Prepare QR codes for JSON
253
+ qr_codes_json = []
254
+ for i, qr in enumerate(qr_codes):
255
+ qr_json = {
256
+ "id": i + 1,
257
+ "x": qr['x'],
258
+ "y": qr['y'],
259
+ "width": qr['width'],
260
+ "height": qr['height'],
261
+ "data": qr['data']
262
+ }
263
+ # Optionally include corner points if needed
264
+ if 'points' in qr and len(qr['points']) > 0:
265
+ qr_json['corner_points'] = qr['points']
266
+ qr_codes_json.append(qr_json)
267
+
268
+ # Create output JSON
269
+ output_json = {
270
+ "image": Path(input_path).name,
271
+ "image_dimensions": {
272
+ "width": img_original.shape[1],
273
+ "height": img_original.shape[0]
274
+ },
275
+ "qr_codes": {
276
+ "count": len(qr_codes_json),
277
+ "items": qr_codes_json
278
+ }
279
+ }
280
+
281
+ # Save JSON
282
+ output_json_path = os.path.join(
283
+ json_folder, f'qr_detection_{filename}.json')
284
+ with open(output_json_path, 'w') as f:
285
+ json.dump(output_json, f, indent=2)
286
+
287
+ # Print summary
288
+ print(f"✓ Found {len(qr_codes_json)} QR code(s)")
289
+ print(f"✓ Labeled image saved: {output_image_path}")
290
+ print(f"✓ Detection data saved: {output_json_path}")
291
+
292
+ return output_json
293
+
294
+
295
+ def process_folder(input_folder='inputs', output_folder='labelled', json_folder='outputs'):
296
+ """
297
+ Process all images in the input folder.
298
+
299
+ Parameters:
300
+ -----------
301
+ input_folder : str
302
+ Folder containing input images
303
+ output_folder : str
304
+ Folder to save labeled images
305
+ json_folder : str
306
+ Folder to save JSON files
307
+ """
308
+ # Create output folders
309
+ os.makedirs(output_folder, exist_ok=True)
310
+ os.makedirs(json_folder, exist_ok=True)
311
+
312
+ # Supported image formats
313
+ image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif']
314
+
315
+ # Get all image files
316
+ input_path = Path(input_folder)
317
+ if not input_path.exists():
318
+ print(f"Error: Input folder '{input_folder}' does not exist!")
319
+ return
320
+
321
+ image_files = [f for f in input_path.iterdir()
322
+ if f.is_file() and f.suffix.lower() in image_extensions]
323
+
324
+ if not image_files:
325
+ print(f"No image files found in '{input_folder}'")
326
+ return
327
+
328
+ print(f"\n{'='*60}")
329
+ print(f"Found {len(image_files)} image(s) to process")
330
+ print(f"{'='*60}\n")
331
+
332
+ # Process each image
333
+ all_results = []
334
+ for i, image_file in enumerate(image_files, 1):
335
+ print(f"\n[{i}/{len(image_files)}] Processing: {image_file.name}")
336
+ try:
337
+ result = process_image(
338
+ str(image_file),
339
+ output_folder=output_folder,
340
+ json_folder=json_folder
341
+ )
342
+ if result:
343
+ all_results.append(result)
344
+ except Exception as e:
345
+ print(f"✗ Error processing {image_file.name}: {str(e)}")
346
+ continue
347
+
348
+ # Save summary JSON with all results
349
+ if all_results:
350
+ summary_path = os.path.join(json_folder, 'qr_detection_summary.json')
351
+ summary = {
352
+ "total_images": len(all_results),
353
+ "total_qr_codes": sum(r['qr_codes']['count'] for r in all_results),
354
+ "images": all_results
355
+ }
356
+ with open(summary_path, 'w') as f:
357
+ json.dump(summary, f, indent=2)
358
+
359
+ print(f"\n{'='*60}")
360
+ print(f"PROCESSING COMPLETE")
361
+ print(f"{'='*60}")
362
+ print(f"✓ Processed {len(all_results)} image(s)")
363
+ print(f"✓ Total QR codes detected: {summary['total_qr_codes']}")
364
+ print(f"✓ Summary saved: {summary_path}")
365
+ print(f"✓ Labeled images saved in: {output_folder}/")
366
+ print(f"✓ JSON files saved in: {json_folder}/")
367
+
368
+
369
+ if __name__ == "__main__":
370
+ # Process all images in the 'inputs' folder
371
+ process_folder(
372
+ input_folder='inputs',
373
+ output_folder='labelled',
374
+ json_folder='outputs'
375
+ )
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ opencv-python>=4.5.0
3
+ numpy>=1.21.0
4
+
5
+ # ML/AI models
6
+ ultralytics>=8.0.0
7
+ supervision
8
+ huggingface_hub
9
+
10
+ # PDF processing
11
+ PyMuPDF>=1.23.0
12
+
13
+ # API
14
+ fastapi>=0.104.0
15
+ uvicorn[standard]>=0.24.0
16
+ python-multipart>=0.0.6
17
+ httpx>=0.25.0
18
+ boto3>=1.28.0
19
+
signature/README.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # YOLOv8 Signature Detector
2
+
3
+ This repository implements signature detection using the YOLOv8s model from [tech4humans/yolov8s-signature-detector](https://huggingface.co/tech4humans/yolov8s-signature-detector).
4
+
5
+ ## Setup
6
+
7
+ Install dependencies:
8
+
9
+ ```bash
10
+ pip install -r requirements.txt
11
+ ```
12
+
13
+ ### Authentication
14
+
15
+ The model repository is gated and requires Hugging Face authentication. You need to:
16
+
17
+ 1. **Login via CLI** (recommended):
18
+ ```bash
19
+ huggingface-cli login
20
+ ```
21
+ Enter your Hugging Face token when prompted. Get your token from [https://huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)
22
+
23
+ 2. **Or set environment variable**:
24
+ ```bash
25
+ export HF_TOKEN=your_token_here
26
+ ```
27
+
28
+ 3. **Or manually download the model**:
29
+ ```bash
30
+ huggingface-cli download tech4humans/yolov8s-signature-detector yolov8s.pt
31
+ ```
32
+ Then place `yolov8s.pt` in the project root directory.
33
+
34
+ ## Usage
35
+
36
+ ### Python Script
37
+
38
+ Process all images in the `inputs/` directory:
39
+
40
+ ```bash
41
+ python inference.py
42
+ ```
43
+
44
+ The script will:
45
+ 1. Check for a local `yolov8s.pt` file first
46
+ 2. If not found, download the model from Hugging Face (requires authentication)
47
+ 3. Process all images in the `inputs/` directory
48
+ 4. Save annotated images with detected signatures to the `outputs/` directory
49
+ 5. **Save signature coordinates to `outputs/signature_coordinates.json`**
50
+ 6. **Crop and save individual signatures to `outputs/signatures/` directory**
51
+
52
+ ### CLI (Alternative)
53
+
54
+ You can also use the Ultralytics CLI:
55
+
56
+ ```bash
57
+ huggingface-cli download tech4humans/yolov8s-signature-detector yolov8s.pt
58
+ yolo predict model=yolov8s.pt source=inputs/
59
+ ```
60
+
61
+ ## Model Formats
62
+
63
+ The model is available in multiple formats:
64
+ - `yolov8s.pt` (PyTorch format) - used by default
65
+ - `yolov8s.onnx` (ONNX format) - for ONNX Runtime
66
+ - `yolov8s.engine` (TensorRT format) - for TensorRT inference
67
+
68
+ ## Output
69
+
70
+ The script generates several outputs:
71
+
72
+ 1. **Annotated images**: Images with bounding boxes around detected signatures saved to `outputs/` with the prefix `detected_`
73
+ 2. **Signature coordinates JSON**: All detection coordinates saved to `outputs/signature_coordinates.json` with the following structure:
74
+ ```json
75
+ [
76
+ {
77
+ "image": "image1.jpg",
78
+ "image_width": 1920,
79
+ "image_height": 1080,
80
+ "signatures": [
81
+ {
82
+ "signature_id": 1,
83
+ "confidence": 0.95,
84
+ "bbox": {
85
+ "x1": 100.5,
86
+ "y1": 200.3,
87
+ "x2": 300.7,
88
+ "y2": 400.9,
89
+ "width": 200.2,
90
+ "height": 200.6
91
+ },
92
+ "class_id": 0,
93
+ "cropped_path": "outputs/signatures/image1_signature_1.jpg"
94
+ }
95
+ ]
96
+ }
97
+ ]
98
+ ```
99
+
100
+ The `image_width` and `image_height` fields allow the frontend to properly scale coordinates when displaying images at different sizes. Coordinates are in pixels relative to the original image dimensions.
101
+ 3. **Cropped signatures**: Individual signature images saved to `outputs/signatures/` directory
102
+
103
+ ## Extracting Signatures from Coordinates
104
+
105
+ If you need to re-extract signatures using the saved coordinates, use the helper script:
106
+
107
+ ```bash
108
+ python extract_signatures.py
109
+ ```
110
+
111
+ Or specify a custom JSON file:
112
+
113
+ ```bash
114
+ python extract_signatures.py outputs/signature_coordinates.json
115
+ ```
116
+
117
+ This is useful if you want to extract signatures again without running inference, or if you need to adjust the extraction parameters.
118
+
signature/__pycache__/inference.cpython-310.pyc DELETED
Binary file (5.82 kB)
 
signature/extract_signatures.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Helper script to extract signatures from images using saved coordinates.
3
+ This script can be used to re-extract signatures from the JSON coordinates file.
4
+ """
5
+
6
+ import json
7
+ import cv2
8
+ from pathlib import Path
9
+
10
+ def extract_signatures_from_json(json_path="outputs/signature_coordinates.json",
11
+ input_dir="inputs",
12
+ output_dir="outputs/extracted_signatures"):
13
+ """
14
+ Extract signatures from images using saved coordinates in JSON file.
15
+
16
+ Args:
17
+ json_path: Path to the JSON file with coordinates
18
+ input_dir: Directory containing original images
19
+ output_dir: Directory to save extracted signatures
20
+ """
21
+ # Load coordinates
22
+ with open(json_path, 'r') as f:
23
+ all_detections = json.load(f)
24
+
25
+ # Create output directory
26
+ output_path = Path(output_dir)
27
+ output_path.mkdir(parents=True, exist_ok=True)
28
+
29
+ input_path = Path(input_dir)
30
+
31
+ print(f"Loaded coordinates for {len(all_detections)} image(s)")
32
+
33
+ for image_data in all_detections:
34
+ image_name = image_data["image"]
35
+ image_file = input_path / image_name
36
+
37
+ if not image_file.exists():
38
+ print(f"Warning: Image {image_name} not found, skipping...")
39
+ continue
40
+
41
+ # Read image
42
+ image = cv2.imread(str(image_file))
43
+ if image is None:
44
+ print(f"Error: Could not read {image_name}, skipping...")
45
+ continue
46
+
47
+ print(f"\nProcessing: {image_name}")
48
+ print(f" Found {len(image_data['signatures'])} signature(s)")
49
+
50
+ # Extract each signature
51
+ for sig_data in image_data["signatures"]:
52
+ sig_id = sig_data["signature_id"]
53
+ bbox = sig_data["bbox"]
54
+
55
+ # Get coordinates
56
+ x1, y1, x2, y2 = int(bbox["x1"]), int(bbox["y1"]), int(bbox["x2"]), int(bbox["y2"])
57
+
58
+ # Ensure coordinates are within image bounds
59
+ x1 = max(0, x1)
60
+ y1 = max(0, y1)
61
+ x2 = min(image.shape[1], x2)
62
+ y2 = min(image.shape[0], y2)
63
+
64
+ # Crop signature
65
+ signature_crop = image[y1:y2, x1:x2]
66
+
67
+ # Save cropped signature
68
+ output_filename = f"{Path(image_name).stem}_signature_{sig_id}.jpg"
69
+ output_file = output_path / output_filename
70
+ cv2.imwrite(str(output_file), signature_crop)
71
+
72
+ print(f" Signature {sig_id}: confidence={sig_data['confidence']:.2f}, saved to {output_file}")
73
+
74
+ if __name__ == "__main__":
75
+ import sys
76
+
77
+ json_path = sys.argv[1] if len(sys.argv) > 1 else "outputs/signature_coordinates.json"
78
+ extract_signatures_from_json(json_path)
79
+
signature/inference.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import os
3
+ import sys
4
+ import json
5
+ import supervision as sv
6
+ from huggingface_hub import hf_hub_download, login
7
+ from ultralytics import YOLO
8
+ from pathlib import Path
9
+
10
+
11
+ def detect_signatures(image_path, model=None, output_dir=None, signatures_dir=None, save_crops=True):
12
+ """
13
+ Detect signatures in a single image.
14
+
15
+ Args:
16
+ image_path: Path to the input image
17
+ model: YOLO model instance (if None, will load/create one)
18
+ output_dir: Directory for output files (optional)
19
+ signatures_dir: Directory for cropped signatures (optional)
20
+ save_crops: Whether to save cropped signature images
21
+
22
+ Returns:
23
+ dict: Detection results with structure:
24
+ {
25
+ "image": image_filename,
26
+ "image_width": int,
27
+ "image_height": int,
28
+ "signatures": [...]
29
+ }
30
+ """
31
+ # Load model if not provided
32
+ if model is None:
33
+ local_model_path = Path("yolov8s.pt")
34
+ if local_model_path.exists():
35
+ model_path = str(local_model_path)
36
+ else:
37
+ try:
38
+ model_path = hf_hub_download(
39
+ repo_id="tech4humans/yolov8s-signature-detector",
40
+ filename="yolov8s.pt"
41
+ )
42
+ except Exception as e:
43
+ raise RuntimeError(f"Failed to load signature model: {e}")
44
+ model = YOLO(model_path)
45
+
46
+ # Set up paths (only if we need to save crops)
47
+ image_file = Path(image_path)
48
+ if save_crops:
49
+ if output_dir is None:
50
+ output_dir = Path("outputs")
51
+ else:
52
+ output_dir = Path(output_dir)
53
+ output_dir.mkdir(exist_ok=True)
54
+
55
+ if signatures_dir is None:
56
+ signatures_dir = output_dir / "signatures"
57
+ else:
58
+ signatures_dir = Path(signatures_dir)
59
+ signatures_dir.mkdir(exist_ok=True)
60
+ else:
61
+ # Dummy paths when not saving
62
+ output_dir = None
63
+ signatures_dir = None
64
+
65
+ # Read image
66
+ image = cv2.imread(str(image_path))
67
+ if image is None:
68
+ raise ValueError(f"Could not read image: {image_path}")
69
+
70
+ # Get image dimensions
71
+ image_height, image_width = image.shape[:2]
72
+
73
+ # Run inference
74
+ results = model(str(image_path))
75
+ detections = sv.Detections.from_ultralytics(results[0])
76
+
77
+ # Store detection data
78
+ image_detections = {
79
+ "image": image_file.name,
80
+ "image_width": int(image_width),
81
+ "image_height": int(image_height),
82
+ "signatures": []
83
+ }
84
+
85
+ # Process detections
86
+ if len(detections) > 0:
87
+ for i, (xyxy, confidence, class_id) in enumerate(zip(
88
+ detections.xyxy, detections.confidence, detections.class_id
89
+ )):
90
+ x1, y1, x2, y2 = xyxy
91
+
92
+ # Store detection data
93
+ detection_data = {
94
+ "signature_id": i + 1,
95
+ "confidence": float(confidence),
96
+ "bbox": {
97
+ "x1": float(x1),
98
+ "y1": float(y1),
99
+ "x2": float(x2),
100
+ "y2": float(y2),
101
+ "width": float(x2 - x1),
102
+ "height": float(y2 - y1)
103
+ },
104
+ "class_id": int(class_id)
105
+ }
106
+
107
+ # Crop and save individual signature if requested
108
+ if save_crops and signatures_dir is not None:
109
+ x1_int, y1_int, x2_int, y2_int = int(
110
+ x1), int(y1), int(x2), int(y2)
111
+ x1_int = max(0, x1_int)
112
+ y1_int = max(0, y1_int)
113
+ x2_int = min(image.shape[1], x2_int)
114
+ y2_int = min(image.shape[0], y2_int)
115
+
116
+ signature_crop = image[y1_int:y2_int, x1_int:x2_int]
117
+ signature_filename = f"{image_file.stem}_signature_{i+1}.jpg"
118
+ signature_path = signatures_dir / signature_filename
119
+ cv2.imwrite(str(signature_path), signature_crop)
120
+ detection_data["cropped_path"] = str(signature_path)
121
+
122
+ image_detections["signatures"].append(detection_data)
123
+
124
+ return image_detections
125
+
126
+
127
+ def main():
128
+ # Check if model file exists locally first
129
+ local_model_path = Path("yolov8s.pt")
130
+
131
+ if local_model_path.exists():
132
+ print(f"Using local model file: {local_model_path}", flush=True)
133
+ model_path = str(local_model_path)
134
+ else:
135
+ # Try to download model from Hugging Face
136
+ print("Downloading model from Hugging Face...", flush=True)
137
+ try:
138
+ model_path = hf_hub_download(
139
+ repo_id="tech4humans/yolov8s-signature-detector",
140
+ filename="yolov8s.pt"
141
+ )
142
+ except Exception as e:
143
+ if "401" in str(e) or "GatedRepoError" in str(type(e).__name__) or "Unauthorized" in str(e):
144
+ print("\n" + "="*70)
145
+ print("ERROR: Authentication required to access this model.")
146
+ print("="*70)
147
+ print(
148
+ "\nThis repository is gated and requires Hugging Face authentication.")
149
+ print("\nTo authenticate, run one of the following:")
150
+ print(" 1. huggingface-cli login")
151
+ print(" 2. Or set your token: export HF_TOKEN=your_token_here")
152
+ print("\nAfter authentication, run this script again.")
153
+ print("="*70)
154
+ sys.exit(1)
155
+ else:
156
+ print(f"\nError downloading model: {e}")
157
+ print("\nYou can also download the model manually:")
158
+ print(
159
+ " huggingface-cli download tech4humans/yolov8s-signature-detector yolov8s.pt")
160
+ print("\nOr place yolov8s.pt in the current directory.")
161
+ sys.exit(1)
162
+
163
+ # Load the model
164
+ print("Loading model...")
165
+ model = YOLO(model_path)
166
+
167
+ # Set up paths
168
+ input_dir = Path("inputs")
169
+ output_dir = Path("outputs")
170
+ signatures_dir = output_dir / "signatures" # Directory for cropped signatures
171
+ output_dir.mkdir(exist_ok=True)
172
+ signatures_dir.mkdir(exist_ok=True)
173
+
174
+ # Store all detections for JSON export
175
+ all_detections = []
176
+
177
+ # Get all image files from inputs directory
178
+ image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'}
179
+ image_files = [f for f in input_dir.iterdir()
180
+ if f.suffix.lower() in image_extensions]
181
+
182
+ if not image_files:
183
+ print(f"No images found in {input_dir}/")
184
+ return
185
+
186
+ print(f"Found {len(image_files)} image(s) to process")
187
+
188
+ # Process each image
189
+ box_annotator = sv.BoxAnnotator()
190
+
191
+ for image_file in image_files:
192
+ print(f"\nProcessing: {image_file.name}")
193
+
194
+ try:
195
+ # Use the reusable function
196
+ image_detections = detect_signatures(
197
+ str(image_file),
198
+ model=model,
199
+ output_dir=output_dir,
200
+ signatures_dir=signatures_dir,
201
+ save_crops=True
202
+ )
203
+
204
+ # Read image for annotation
205
+ image = cv2.imread(str(image_file))
206
+ results = model(str(image_file))
207
+ detections = sv.Detections.from_ultralytics(results[0])
208
+
209
+ if len(detections) > 0:
210
+ print(f" Found {len(detections)} signature(s)")
211
+ for i, sig in enumerate(image_detections["signatures"]):
212
+ bbox = sig["bbox"]
213
+ print(
214
+ f" Signature {i+1}: confidence={sig['confidence']:.2f}, bbox=[{bbox['x1']:.1f}, {bbox['y1']:.1f}, {bbox['x2']:.1f}, {bbox['y2']:.1f}]")
215
+ if "cropped_path" in sig:
216
+ print(
217
+ f" Saved cropped signature to: {sig['cropped_path']}")
218
+ else:
219
+ print(" No signatures detected")
220
+
221
+ all_detections.append(image_detections)
222
+
223
+ # Annotate image with bounding boxes
224
+ annotated_image = box_annotator.annotate(
225
+ scene=image.copy(),
226
+ detections=detections
227
+ )
228
+
229
+ # Save annotated image
230
+ output_path = output_dir / f"detected_{image_file.name}"
231
+ cv2.imwrite(str(output_path), annotated_image)
232
+ print(f" Saved annotated image to: {output_path}")
233
+ except Exception as e:
234
+ print(f" Error processing {image_file.name}: {str(e)}")
235
+ continue
236
+
237
+ # Save all coordinates to JSON file
238
+ json_path = output_dir / "signature_coordinates.json"
239
+ with open(json_path, 'w') as f:
240
+ json.dump(all_detections, f, indent=2)
241
+ print(f"\n{'='*70}")
242
+ print(f"Saved all signature coordinates to: {json_path}")
243
+ print(f"{'='*70}")
244
+
245
+
246
+ if __name__ == "__main__":
247
+ main()
signature/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ ultralytics
2
+ supervision
3
+ huggingface_hub
4
+ opencv-python
5
+
stamp_detector/README.md ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stamp Detector
2
+
3
+ Простой инструмент для детекции печатей (stamp) на изображениях с использованием YOLOv8.
4
+
5
+ ## Установка
6
+
7
+ ```bash
8
+ pip install -r requirements.txt
9
+ ```
10
+
11
+ ## Использование
12
+
13
+ ### Базовое использование
14
+
15
+ ```bash
16
+ python detect.py path/to/image.jpg
17
+ ```
18
+
19
+ ### С кастомным порогом уверенности
20
+
21
+ ```bash
22
+ python detect.py path/to/image.jpg --conf 0.20
23
+ ```
24
+
25
+ ### С указанием пути к модели
26
+
27
+ ```bash
28
+ python detect.py path/to/image.jpg --model stamp_model.pt
29
+ ```
30
+
31
+ ### С указанием выходного файла
32
+
33
+ ```bash
34
+ python detect.py path/to/image.jpg --output result.jpg
35
+ ```
36
+
37
+ ### Сохранение JSON с координатами
38
+
39
+ ```bash
40
+ # Сохранить JSON в output/{имя_файла}_result.json
41
+ python detect.py path/to/image.jpg --json
42
+
43
+ # Сохранить JSON в указанный файл
44
+ python detect.py path/to/image.jpg --json-output results.json
45
+ ```
46
+
47
+ ## Параметры
48
+
49
+ - `image_path` (обязательный) - путь к входному изображению
50
+ - `--model` - путь к модели (по умолчанию: `stamp_model.pt`)
51
+ - `--output` - путь для сохранения результата (по умолчанию: `output/{имя_файла}_result.jpg`)
52
+ - `--conf` - порог уверенности (по умолчанию: 0.25)
53
+ - `--json` - сохранить JSON с координатами детекций
54
+ - `--json-output` - путь для сохранения JSON файла
55
+
56
+ ## Структура
57
+
58
+ ```
59
+ stamp_detector/
60
+ ├── stamp_model.pt # Обученная модель YOLOv8
61
+ ├── detect.py # Скрипт детекции
62
+ ├── requirements.txt # Зависимости
63
+ └── README.md # Документация
64
+ ```
65
+
66
+ ## Примеры
67
+
68
+ ```bash
69
+ # Детекция с порогом 0.25
70
+ python detect.py image.jpg
71
+
72
+ # Более чувствительная детекция (ниже порог)
73
+ python detect.py image.jpg --conf 0.15
74
+
75
+ # Менее чувствительная детекция (выше порог)
76
+ python detect.py image.jpg --conf 0.35
77
+
78
+ # Детекция с сохранением JSON координат
79
+ python detect.py image.jpg --json
80
+ ```
81
+
82
+ ## Формат JSON
83
+
84
+ При использовании флага `--json` создается JSON файл со следующей структурой:
85
+
86
+ ```json
87
+ {
88
+ "image_path": "output/image_result.jpg",
89
+ "image_size": {
90
+ "width": 1920,
91
+ "height": 1080
92
+ },
93
+ "detections_count": 2,
94
+ "detections": [
95
+ {
96
+ "class": "stamp",
97
+ "confidence": 0.8542,
98
+ "bbox": {
99
+ "x1": 100,
100
+ "y1": 200,
101
+ "x2": 300,
102
+ "y2": 400,
103
+ "width": 200,
104
+ "height": 200
105
+ },
106
+ "bbox_normalized": {
107
+ "x1": 0.052083,
108
+ "y1": 0.185185,
109
+ "x2": 0.15625,
110
+ "y2": 0.37037,
111
+ "width": 0.104167,
112
+ "height": 0.185185
113
+ }
114
+ }
115
+ ]
116
+ }
117
+ ```
118
+
119
+ - `bbox` - абсолютные координаты в пикселях
120
+ - `bbox_normalized` - нормализованные координаты (0.0 - 1.0) относительно размера изображения
121
+
stamp_detector/__pycache__/detect.cpython-310.pyc DELETED
Binary file (6.56 kB)
 
stamp_detector/detect.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Простой скрипт для детекции печатей (stamp)
3
+ Требуется только: модель и изображение
4
+ """
5
+ import cv2
6
+ import os
7
+ import sys
8
+ import json
9
+ from ultralytics import YOLO
10
+
11
+
12
+ def detect_stamps_no_save(image_path, model_path="stamp_model.pt", conf=0.25):
13
+ """
14
+ Detect stamps without saving images.
15
+
16
+ Args:
17
+ image_path: Path to input image
18
+ model_path: Path to model (or will download from HF Hub if not found)
19
+ conf: Confidence threshold
20
+
21
+ Returns:
22
+ dict: Detection results with detections and image_size
23
+ """
24
+ # Load model - try to download from HF Hub if not found locally
25
+ if not os.path.exists(model_path):
26
+ # Try to download from Hugging Face Hub
27
+ try:
28
+ from huggingface_hub import hf_hub_download
29
+ print(f"Model not found locally, attempting to download from HF Hub...")
30
+ # You can upload your model to HF Hub and use it here
31
+ # For now, try the default path in stamp_detector directory
32
+ default_path = os.path.join("stamp_detector", "stamp_model.pt")
33
+ if os.path.exists(default_path):
34
+ model_path = default_path
35
+ else:
36
+ raise FileNotFoundError(f"Stamp model not found: {model_path}. Please upload stamp_model.pt to the Space.")
37
+ except ImportError:
38
+ raise FileNotFoundError(f"Stamp model not found: {model_path}")
39
+
40
+ model = YOLO(model_path)
41
+
42
+ # Load image
43
+ if not os.path.exists(image_path):
44
+ raise FileNotFoundError(f"Image not found: {image_path}")
45
+
46
+ image = cv2.imread(image_path)
47
+ if image is None:
48
+ raise ValueError(f"Could not load image: {image_path}")
49
+
50
+ # Detection
51
+ results = model(image, conf=conf, verbose=False)
52
+
53
+ # Collect detections
54
+ detections = []
55
+ image_height, image_width = image.shape[:2]
56
+
57
+ for result in results:
58
+ boxes = result.boxes
59
+ for box in boxes:
60
+ class_id = int(box.cls[0])
61
+ confidence = float(box.conf[0])
62
+
63
+ # Filter only stamp (class_id == 0)
64
+ if class_id == 0 and confidence >= conf:
65
+ x1, y1, x2, y2 = map(int, box.xyxy[0])
66
+
67
+ detection = {
68
+ "class": "stamp",
69
+ "confidence": round(confidence, 4),
70
+ "bbox": {
71
+ "x1": int(x1),
72
+ "y1": int(y1),
73
+ "x2": int(x2),
74
+ "y2": int(y2),
75
+ "width": int(x2 - x1),
76
+ "height": int(y2 - y1)
77
+ },
78
+ "bbox_normalized": {
79
+ "x1": round(x1 / image_width, 6),
80
+ "y1": round(y1 / image_height, 6),
81
+ "x2": round(x2 / image_width, 6),
82
+ "y2": round(y2 / image_height, 6),
83
+ "width": round((x2 - x1) / image_width, 6),
84
+ "height": round((y2 - y1) / image_height, 6)
85
+ }
86
+ }
87
+ detections.append(detection)
88
+
89
+ return {
90
+ "image_size": {
91
+ "width": image_width,
92
+ "height": image_height
93
+ },
94
+ "detections_count": len(detections),
95
+ "detections": detections
96
+ }
97
+
98
+
99
+ def detect_stamps(image_path, model_path="stamp_model.pt", output_path=None, conf=0.25, return_json=False):
100
+ """
101
+ Детектирует печати на изображении
102
+
103
+ Args:
104
+ image_path: путь к входному изображению
105
+ model_path: путь к модели (по умолчанию: stamp_model.pt)
106
+ output_path: путь для сохранения результата (если None, создается автоматически)
107
+ conf: порог уверенности (по умолчанию: 0.25)
108
+ return_json: если True, возвращает также JSON с координатами
109
+
110
+ Returns:
111
+ если return_json=False: путь к сохраненному изображению
112
+ если return_json=True: словарь с 'image_path' и 'detections' (JSON структура)
113
+ """
114
+ # Загружаем модель
115
+ if not os.path.exists(model_path):
116
+ print(f"❌ Ошибка: модель не найдена: {model_path}")
117
+ sys.exit(1)
118
+
119
+ print(f"📥 Загружаю модель: {model_path}")
120
+ model = YOLO(model_path)
121
+ print("✅ Модель загружена")
122
+
123
+ # Загружаем изображение
124
+ if not os.path.exists(image_path):
125
+ print(f"❌ Ошибка: изображение не найдено: {image_path}")
126
+ sys.exit(1)
127
+
128
+ print(f"📷 Загружаю изображение: {image_path}")
129
+ image = cv2.imread(image_path)
130
+ if image is None:
131
+ print(f"❌ Ошибка: не удалось загрузить изображение")
132
+ sys.exit(1)
133
+
134
+ # Детекция
135
+ print(f"🔍 Выполняю детекцию (порог: {conf})...")
136
+ results = model(image, conf=conf, verbose=False)
137
+
138
+ # Собираем детекции и рисуем рамки
139
+ result_image = image.copy()
140
+ detections = []
141
+ image_height, image_width = image.shape[:2]
142
+
143
+ for result in results:
144
+ boxes = result.boxes
145
+ for box in boxes:
146
+ class_id = int(box.cls[0])
147
+ confidence = float(box.conf[0])
148
+
149
+ # Фильтруем только stamp (class_id == 0)
150
+ if class_id == 0 and confidence >= conf:
151
+ x1, y1, x2, y2 = map(int, box.xyxy[0])
152
+
153
+ # Сохраняем детекцию в JSON формате
154
+ detection = {
155
+ "class": "stamp",
156
+ "confidence": round(confidence, 4),
157
+ "bbox": {
158
+ "x1": int(x1),
159
+ "y1": int(y1),
160
+ "x2": int(x2),
161
+ "y2": int(y2),
162
+ "width": int(x2 - x1),
163
+ "height": int(y2 - y1)
164
+ },
165
+ "bbox_normalized": {
166
+ "x1": round(x1 / image_width, 6),
167
+ "y1": round(y1 / image_height, 6),
168
+ "x2": round(x2 / image_width, 6),
169
+ "y2": round(y2 / image_height, 6),
170
+ "width": round((x2 - x1) / image_width, 6),
171
+ "height": round((y2 - y1) / image_height, 6)
172
+ }
173
+ }
174
+ detections.append(detection)
175
+
176
+ # Рисуем рамку (красная)
177
+ cv2.rectangle(result_image, (x1, y1), (x2, y2), (0, 0, 255), 2)
178
+
179
+ # Подпись
180
+ label = f"stamp {confidence:.2f}"
181
+ (label_width, label_height), _ = cv2.getTextSize(
182
+ label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2
183
+ )
184
+ cv2.rectangle(
185
+ result_image,
186
+ (x1, y1 - label_height - 10),
187
+ (x1 + label_width, y1),
188
+ (0, 0, 255),
189
+ -1
190
+ )
191
+ cv2.putText(
192
+ result_image,
193
+ label,
194
+ (x1, y1 - 5),
195
+ cv2.FONT_HERSHEY_SIMPLEX,
196
+ 0.5,
197
+ (255, 255, 255),
198
+ 2
199
+ )
200
+
201
+ # Сохраняем результат
202
+ if output_path is None:
203
+ base_name = os.path.splitext(os.path.basename(image_path))[0]
204
+ output_dir = "output"
205
+ os.makedirs(output_dir, exist_ok=True)
206
+ output_path = os.path.join(output_dir, f"{base_name}_result.jpg")
207
+
208
+ cv2.imwrite(output_path, result_image)
209
+ print(f"✅ Найдено печатей: {len(detections)}")
210
+ print(f"📁 Результат сохранен: {output_path}")
211
+
212
+ # Возвращаем результат
213
+ if return_json:
214
+ result_data = {
215
+ "image_path": output_path,
216
+ "image_size": {
217
+ "width": image_width,
218
+ "height": image_height
219
+ },
220
+ "detections_count": len(detections),
221
+ "detections": detections
222
+ }
223
+ return result_data
224
+ else:
225
+ return output_path
226
+
227
+
228
+ if __name__ == "__main__":
229
+ import argparse
230
+
231
+ parser = argparse.ArgumentParser(
232
+ description="Детекция печатей на изображениях")
233
+ parser.add_argument("image_path", help="Путь к изображению")
234
+ parser.add_argument(
235
+ "--model",
236
+ default="stamp_model.pt",
237
+ help="Путь к модели (по умолчанию: stamp_model.pt)"
238
+ )
239
+ parser.add_argument(
240
+ "--output",
241
+ default=None,
242
+ help="Путь для сохранения результата (по умолчанию: output/{имя_файла}_result.jpg)"
243
+ )
244
+ parser.add_argument(
245
+ "--conf",
246
+ type=float,
247
+ default=0.25,
248
+ help="Порог уверенности (по умолчанию: 0.25)"
249
+ )
250
+ parser.add_argument(
251
+ "--json",
252
+ action="store_true",
253
+ help="Сохранить JSON с координатами детекций"
254
+ )
255
+ parser.add_argument(
256
+ "--json-output",
257
+ default=None,
258
+ help="Путь для сохранения JSON файла (по умолчанию: output/{имя_файла}_result.json)"
259
+ )
260
+
261
+ args = parser.parse_args()
262
+
263
+ print("=" * 60)
264
+ print("🔍 Детекция печатей (stamp)")
265
+ print("=" * 60)
266
+
267
+ result = detect_stamps(
268
+ args.image_path,
269
+ args.model,
270
+ args.output,
271
+ args.conf,
272
+ return_json=args.json or args.json_output is not None
273
+ )
274
+
275
+ # Сохраняем JSON если нужно
276
+ if args.json or args.json_output is not None:
277
+ if isinstance(result, dict):
278
+ json_data = {
279
+ "image_path": result["image_path"],
280
+ "image_size": result["image_size"],
281
+ "detections_count": result["detections_count"],
282
+ "detections": result["detections"]
283
+ }
284
+ else:
285
+ # Если result - это путь, нужно пересчитать
286
+ result = detect_stamps(
287
+ args.image_path,
288
+ args.model,
289
+ args.output,
290
+ args.conf,
291
+ return_json=True
292
+ )
293
+ json_data = {
294
+ "image_path": result["image_path"],
295
+ "image_size": result["image_size"],
296
+ "detections_count": result["detections_count"],
297
+ "detections": result["detections"]
298
+ }
299
+
300
+ # Определяем путь для JSON
301
+ if args.json_output:
302
+ json_path = args.json_output
303
+ else:
304
+ base_name = os.path.splitext(os.path.basename(args.image_path))[0]
305
+ output_dir = "output"
306
+ os.makedirs(output_dir, exist_ok=True)
307
+ json_path = os.path.join(output_dir, f"{base_name}_result.json")
308
+
309
+ # Сохраняем JSON
310
+ with open(json_path, "w", encoding="utf-8") as f:
311
+ json.dump(json_data, f, indent=2, ensure_ascii=False)
312
+
313
+ print(f"📄 JSON сохранен: {json_path}")
314
+
315
+ print("=" * 60)
stamp_detector/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ ultralytics>=8.0.0
2
+ opencv-python>=4.5.0
3
+ numpy>=1.21.0
4
+
upload_model.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to upload stamp_model.pt to Hugging Face Space.
4
+ Run this after the Space is created to upload the model file.
5
+ """
6
+ from huggingface_hub import HfApi, login
7
+ from pathlib import Path
8
+
9
+ # Login (will prompt for token if not already logged in)
10
+ # Or set HF_TOKEN environment variable
11
+ login()
12
+
13
+ api = HfApi()
14
+ model_path = Path("stamp_detector/stamp_model.pt")
15
+
16
+ if not model_path.exists():
17
+ print(f"Error: {model_path} not found!")
18
+ exit(1)
19
+
20
+ print(f"Uploading {model_path} to bekzhanK1/armeta_hackaton...")
21
+ api.upload_file(
22
+ path_or_fileobj=str(model_path),
23
+ path_in_repo="stamp_detector/stamp_model.pt",
24
+ repo_id="bekzhanK1/armeta_hackaton",
25
+ repo_type="space"
26
+ )
27
+ print("✓ Model uploaded successfully!")
28
+