fix: apply crop bounds before EXIF rotation to match AI analysis

Gemini analyzes raw image and returns crop_bounds for that coordinate space.
Previous code applied EXIF rotation first, changing image dimensions, then
used crop_bounds on the rotated image (coordinate mismatch).

Now: crop on raw image (matches AI) → then apply EXIF + manual rotation.
This ensures cropped region contains the actual item, not background.
This commit is contained in:
2026-04-22 09:50:08 +03:00
parent 59565c9b8a
commit 197dcadfee
5 changed files with 149 additions and 9 deletions

View File

@@ -97,7 +97,8 @@
"Bash(sed -i 's/as jest\\\\.Mock/as any/g' tests/hooks/useAIExtraction.test.ts)",
"Bash(grep -E \"\\\\.tsx?$\")",
"Bash(sqlite3 *)",
"Skill(gsd-resume-work)"
"Skill(gsd-resume-work)",
"Bash(git revert *)"
]
}
}

View File

@@ -78,25 +78,22 @@ class ImageProcessor:
'thumbnail_bytes': None,
}
# Open image with PIL
# Open image with PIL (without applying EXIF yet, so crop_bounds match AI analysis)
image = Image.open(io.BytesIO(file_bytes))
original_size = image.size
self.logger.info(f"[PROCESS] Input: {len(file_bytes)} bytes, size={original_size}, crop_bounds={crop_bounds}, rotation={rotation_degrees}°")
# Extract and apply EXIF orientation
# Extract EXIF orientation (but don't apply yet)
exif_orientation = self._extract_exif_orientation(image)
if exif_orientation and exif_orientation > 1:
image = self._rotate_by_orientation(image, exif_orientation)
self.logger.info(f"Applied EXIF rotation: {exif_orientation}")
# Smart cropping
# Smart cropping (on raw, un-rotated image so crop_bounds match AI analysis)
cropped_image = image
crop_size = None
text_angle = None
crop_method = 'none'
if crop_bounds:
# Manual crop bounds provided
# Manual crop bounds provided (from AI, based on raw image)
crop_rect = (
crop_bounds['x'],
crop_bounds['y'],
@@ -109,7 +106,7 @@ class ImageProcessor:
crop_method = 'manual'
self.logger.info(f"[CROP] Result size: {crop_size}, pixels={crop_size[0]*crop_size[1]}")
else:
# Try OpenCV smart crop
# Try OpenCV smart crop on raw image
self.logger.info("[CROP] Attempting OpenCV smart crop...")
try:
crop_result = self._smart_crop_opencv(image)
@@ -140,6 +137,11 @@ class ImageProcessor:
)
crop_method = 'pillow'
# Now apply EXIF orientation to the cropped image
if exif_orientation and exif_orientation > 1:
cropped_image = self._rotate_by_orientation(cropped_image, exif_orientation)
self.logger.info(f"Applied EXIF rotation: {exif_orientation}")
# Apply manual rotation if provided
if abs(rotation_degrees) > 0.5:
cropped_image = cropped_image.rotate(-rotation_degrees, expand=True)

137
config/ai_prompt.md.old Normal file
View File

@@ -0,0 +1,137 @@
# Technical Inventory Hardware Extraction Protocol
Extract ALL relevant hardware items from the image with precise, standardized formatting.
## Filtering Rules
- **INCLUDE**: Physical hardware, modules, cables, servers, storage, transceivers
- **EXCLUDE**: Generic mounting hardware (screws, brackets, rails), paper licenses, empty packaging
- **Multi-item labels**: Treat each SKU/variant as a separate item (e.g., "5m cable" and "7m cable" = 2 items)
## Item Field Format (CRITICAL)
[]
**Component Rules:**
- `<size_or_length>`:
- **STORAGE CAPACITY - HUMAN READABLE**: Convert to largest unit (TB/MB).
- Examples: "1600GB" → "1.6TB", "256GB" → "256GB", "512MB" → "512MB"
- Rule: If ≥1000GB, use TB. If ≥1000MB, use GB. Otherwise use MB.
- **CABLE/WIRE LENGTH**: Meters only. Examples: "5m", "10m", "50m"
- **RAM DIMM**: Capacity in GB. Examples: "128GB", "32GB", "8GB"
- `<part_number>`: Part number ONLY if visible. If the item has an identifiable Part Number, search web for what item this is and extract needed informations or compare with what was already identified, and correct all fields. **Omit serial numbers.**
- `<type>`: Asset class. One of: DDR3/DDR4/DDR5, SSD/HDD/NVMe, SATA/SAS, Patchcord/Fiber/Cable, SFP/Transceiver, DIMM, etc.
- `<vendor>`: Manufacturer (HP, HPE, Dell, Samsung, Cisco, Hynix, Intel, Broadcom)
- `<connector>`: Physical interface (RJ45, LC-LC, MPO, U.3, SATA, SAS, ST, SC). Omit if N/A.
**Item Examples (WITH HUMAN-READABLE SIZES):**
- `1.6TB NVMe HPE U.3 P66093-002` (not 1600GB)
- `256GB SSD Dell SATA SK-8765` (already human-readable)
- `5m Patchcord LC-LC`
- `128GB DDR4 Hynix`
- `512MB Cache Samsung SATA` (stays MB if under 1GB)
**Size Conversion Examples:**
- 1600GB → 1.6TB
- 2048GB → 2TB
- 512GB → 512GB (under 1TB threshold)
- 256MB → 256MB
- 1024MB → 1GB
**Restrictions:**
- No comments in parenthesis
- No measurement units in Item field (e.g., "1.6TB" not "1.6TB Storage")
- No secondary vendors
- No diameter/mm in Item field
- ONE vendor only (primary manufacturer)
## Other Fields
- **Type**: Repeat the asset class (DDR4, SSD, NVMe, Patchcord, etc.)
- **Description**: Technical summary, max 5 words. Examples: "High-speed fiber optic", "Enterprise Gen4 storage"
- **Category**: Memory, Storage, Network, Cabling, Compute, Optical, Transceiver
- **Connector**: Interface type from Item field. Examples: "LC-LC", "RJ45", "U.3"
- **Size**: **HUMAN-READABLE capacity or length.** Examples: "1.6TB", "256GB", "5m" (NOT "1600GB")
- **Color**: Physical color if distinguishing
- **PartNr**: Part number only (no serial numbers)
- **OCR**: Robust matching key for OCR tolerance
## OCR Field Rules (CRITICAL)
Generate a SHORT, clean matching key:
- Format: **UPPERCASE space-separated, NO special chars, NO duplicates**
- Include ONLY: Type + Size + Primary Vendor + Connector + Part Number
- **EXCLUDE**: Serial numbers, secondary vendors, duplicate tokens, EMC/SK labels
- **USE HUMAN-READABLE SIZE**: Use TB/GB from Item field, not original notation
**OCR Format:** `TYPE SIZE VENDOR CONNECTOR PARTNUMBER`
**OCR Examples (WITH HUMAN-READABLE SIZES):**
- Item: `1.6TB NVMe HPE U.3 P66093-002` → OCR: `NVME 1.6TB HPE U3 P66093002`
- Item: `5m Patchcord LC-LC` → OCR: `PATCHCORD 5M LC LC`
- Item: `256GB SSD Samsung SAS SK-8765` → OCR: `SSD 256GB SAMSUNG SAS SK8765`
- Item: `128GB DDR4 Hynix` → OCR: `DDR4 128GB HYNIX`
**OCR Constraints:**
- NO duplicate part numbers
- NO secondary vendor names
- NO extraneous labels
- Each token appears ONE time only
- Remove hyphens/special chars for fuzzy matching
- Use HUMAN-READABLE sizes (1.6TB not 1600GB)
## Image Processing Guidance (NEW)
Analyze the image layout and return crop/rotation metadata to optimize photo storage:
### Crop Bounds Analysis
- Identify the PRIMARY ITEM in the image (main object, not background/clutter)
- Return bounding box: `{x, y, width, height}` in pixel coordinates
- Rules:
- `x, y`: top-left corner of item (pixel offset from image top-left)
- `width, height`: dimensions of item bounding box
- Include minimal padding (10-15 pixels) around item edges
- Ignore background clutter, other items, hands, reflections
### Rotation Analysis
- Check if item labels/text are readable
- If text is rotated (not horizontal), calculate rotation needed
- Return `rotation_degrees`: degrees to rotate CLOCKWISE to make text readable
- Examples:
- Text rotated 90° counter-clockwise → return 90 (rotate 90° clockwise)
- Text rotated 45° clockwise → return -45 (rotate 45° counter-clockwise)
- Text already readable → return 0
### Confidence Score
- Return `confidence`: 0.0-1.0 indicating reliability of crop/rotation analysis
- 0.9+ = High confidence (clear item, readable text)
- 0.7-0.89 = Medium confidence (some ambiguity or text partially obscured)
- <0.7 = Low confidence (cluttered image, unclear item boundaries)
### Output Format (Extended)
```json
{
"items": [
{
"Item": "[size] type vendor connector partnumber",
"Type": "type",
"Description": "technical details (max 5 words)",
"Category": "category",
"Connector": "connector_type",
"Size": "human_readable_size",
"Color": "color",
"PartNr": "part_number",
"OCR": "TYPE SIZE VENDOR CONNECTOR PARTNUMBER",
"image_processing": {
"crop_bounds": {
"x": 50,
"y": 100,
"width": 300,
"height": 200
},
"rotation_degrees": 15,
"confidence": 0.92
}
}
]
}
```
**Return ONLY JSON. No markdown. No text.**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB