
๐ฎ๐ฉ Baca versi Bahasa Indonesia
๐ Table of Contents
- ๐ Overview & Architecture
- โก Key Features & Performance
- ๐ฆ Installation
- ๐ Quick Start Guide
- ๐งฉ Strategy Selection Guide & Recommendation Matrix
- ๐จ Match Status & Color System
- ๐ Exhaustive API Reference
- ๐ Web Framework Integration Examples
- ๐งช Running Tests & Benchmarks
- ๐ License
๐ 1. Overview & Architecture
Planogrid Go is an enterprise-grade, ultra-high-performance, framework-agnostic Go package designed for:
- 2D Spatial Grid Sorting: Converting raw Object Detection bounding boxes (AWS Rekognition Custom Labels, YOLO, Roboflow, OpenCV, etc.) into ordered retail shelf row & column matrices.
- Planogram Compliance Verification: Automated auditing of actual shelf product arrangements against target planogram matrices.
- Visual Bounding Box Annotation: Fast native rendering of 2px status-coded bounding boxes, product labels, and confidence tags directly onto output images.
Package Architecture & Directory Structure
planogrid-go/
โโโ go.mod # Go module definition (github.com/hanifalkauni/planogrid-go)
โโโ go.sum # Module checksums
โโโ README.md # Comprehensive English documentation
โโโ README_id.md # Comprehensive Indonesian documentation
โโโ planogrid.go # Top-level Facade & type aliases for root imports
โโโ processor_test.go # Root package end-to-end integration tests
โโโ docs/ # Detailed documentation guides
โ โโโ strategies/ # Dedicated documentation for each 6 spatial strategies
โ โโโ en/ # English Strategy Guides
โ โ โโโ STRATEGY_0_SEQUENTIAL_DELTA.md
โ โ โโโ STRATEGY_1_BASELINE_ANCHOR.md
โ โ โโโ STRATEGY_2_CENTER_Y_OVERLAP.md
โ โ โโโ STRATEGY_3_SPATIAL_CLUSTER.md
โ โ โโโ STRATEGY_4_VERTICAL_IOU.md
โ โ โโโ STRATEGY_5_SHELF_PROJECTION.md
โ โโโ id/ # Indonesian Strategy Guides
โ โโโ STRATEGY_0_SEQUENTIAL_DELTA.md
โ โโโ STRATEGY_1_BASELINE_ANCHOR.md
โ โโโ STRATEGY_2_CENTER_Y_OVERLAP.md
โ โโโ STRATEGY_3_SPATIAL_CLUSTER.md
โ โโโ STRATEGY_4_VERTICAL_IOU.md
โ โโโ STRATEGY_5_SHELF_PROJECTION.md
โโโ enums/ # Sub-package: enums
โ โโโ enums.go # MatchStatus enum & default color mappings
โ โโโ enums_test.go
โโโ dto/ # Sub-package: dto
โ โโโ dto.go # BoundingBox, CustomLabelDetection, AWSRawDetection, PlanogramGridResult
โ โโโ dto_test.go
โโโ strategies/ # Sub-package: strategies
โ โโโ strategies.go # RowSortingStrategy interface & 6 algorithm implementations
โ โโโ strategies_test.go
โโโ sorter/ # Sub-package: sorter
โ โโโ sorter.go # SpatialGridSorter implementation
โ โโโ sorter_test.go
โโโ matcher/ # Sub-package: matcher
โ โโโ matcher.go # PlanogramMatcherService & PlanogramVerificationResult
โ โโโ matcher_test.go
โโโ annotator/ # Sub-package: annotator
โ โโโ annotator.go # ImageAnnotatorService (gg rendering)
โ โโโ annotator_test.go
โโโ examples/ # Sub-package: usage examples
โโโ main.go # Complete runnable example app
- Zero Web Framework Lock-in: 100% pure Go standard library core. Seamlessly integrates into Gin, Fiber, Echo, Chi, Standard
net/http, or AWS Lambda Go.
- Ultra Fast & Low Memory Footprint: Sub-millisecond spatial sorting algorithms with minimal stack allocations (benchmarked at ~24 microseconds per verification).
- 6 Domain-Specific Row Sorting Algorithms: Tailored strategies handling front-facing shelves, tilted cameras, variable product heights, stacked products, and wide-angle full-bay shots.
- Automatic AWS Rekognition Ratio Coordinate Scaling: Seamlessly converts normalized ratio coordinates
(0.0 - 1.0) into pixel coordinates based on actual image dimensions.
- High-Quality 2D Bounding Box Drawing: Native image rendering using
github.com/fogleman/gg with 2px status borders, black label backgrounds, and crisp text overlays.
๐ฆ 3. Installation
go get github.com/hanifalkauni/planogrid-go
๐ 4. Quick Start Guide
package main
import (
"fmt"
"log"
"github.com/hanifalkauni/planogrid-go"
)
func main() {
// 1. Instantiate the primary Facade
processor := planogrid.NewPlanogramProcessor()
// Configure row sorting strategy and matching rules
processor.SetRowStrategy(planogrid.NewCenterYOverlapStrategy(0.50))
processor.SetConfidenceThreshold(75.0)
processor.SetCompetitorLabels([]string{"Competitor Brand X", "Competitor Soda"})
// 2. Sample AWS Rekognition Custom Labels detection payload
awsDetections := []planogrid.AWSRawDetection{
{
Name: "Product Alpha 600ml",
Confidence: 98.5,
Geometry: struct {
BoundingBox struct {
Width float64 `json:"Width"`
Height float64 `json:"Height"`
Left float64 `json:"Left"`
Top float64 `json:"Top"`
} `json:"BoundingBox"`
}{
BoundingBox: struct {
Width float64 `json:"Width"`
Height float64 `json:"Height"`
Left float64 `json:"Left"`
Top float64 `json:"Top"`
}{Left: 0.05, Top: 0.10, Width: 0.20, Height: 0.25},
},
},
{
Name: "Product Alpha 600ml",
Confidence: 96.0,
Geometry: struct {
BoundingBox struct {
Width float64 `json:"Width"`
Height float64 `json:"Height"`
Left float64 `json:"Left"`
Top float64 `json:"Top"`
} `json:"BoundingBox"`
}{
BoundingBox: struct {
Width float64 `json:"Width"`
Height float64 `json:"Height"`
Left float64 `json:"Left"`
Top float64 `json:"Top"`
}{Left: 0.30, Top: 0.11, Width: 0.20, Height: 0.24},
},
},
{
Name: "Product Beta 250ml",
Confidence: 92.0,
Geometry: struct {
BoundingBox struct {
Width float64 `json:"Width"`
Height float64 `json:"Height"`
Left float64 `json:"Left"`
Top float64 `json:"Top"`
} `json:"BoundingBox"`
}{
BoundingBox: struct {
Width float64 `json:"Width"`
Height float64 `json:"Height"`
Left float64 `json:"Left"`
Top float64 `json:"Top"`
}{Left: 0.05, Top: 0.50, Width: 0.20, Height: 0.25},
},
},
}
// 3. Define target expected planogram layout matrix
expectedPlanogram := [][]string{
{"Product Alpha 600ml", "Product Alpha 600ml"},
{"Product Beta 250ml"},
}
// 4. Perform Verification (imageBytes can be nil if visual annotation is not required)
evaluation, err := processor.Verify(nil, awsDetections, expectedPlanogram, 1000, 1000)
if err != nil {
log.Fatalf("Verification error: %v", err)
}
// 5. Inspect Results
fmt.Printf("Compliance Status: %s\n", evaluation.Status) // "COMPLIANT"
fmt.Printf("Compliance Score: %.2f%%\n", evaluation.ComplianceScore) // 100.00%
fmt.Printf("Matched Items: %d / %d\n", evaluation.MatchedCount, evaluation.TotalExpected)
jsonGrid, _ := evaluation.GridResult.ToJSON()
fmt.Println("\nExtracted Grid Matrix JSON:")
fmt.Println(jsonGrid)
}
Sample Output JSON Structure
{
"result": [
{
"Brand 1": "Product Alpha 600ml",
"Brand 2": "Product Alpha 600ml"
},
{
"Brand 1": "Product Beta 250ml"
}
],
"result_geometry": [
[
{
"confidence": 98.5,
"height": 250,
"left": 50,
"name": "Product Alpha 600ml",
"top": 100,
"width": 200
},
{
"confidence": 96,
"height": 240,
"left": 300,
"name": "Product Alpha 600ml",
"top": 110,
"width": 200
}
],
[
{
"confidence": 92,
"height": 250,
"left": 50,
"name": "Product Beta 250ml",
"top": 500,
"width": 200
}
]
]
}
๐งฉ 5. Strategy Selection Guide & Recommendation Matrix
Choosing the right row-sorting strategy depends on camera angle, product height variation, and shelf stacking. Use the guide below to pick the best algorithm for your use case:
๐งญ Strategy Comparison Matrix
| Strategy |
Struct Name |
Recommended Use Case & Situation |
Camera & Photo Conditions |
Detailed Guide |
| Strategy 0 |
SequentialDeltaStrategy |
Rigid Shelves & Homogeneous Sizes: Strict top-to-bottom product placement. |
โข Flat 0ยฐ camera angle (no tilt) โข Identical product heights (e.g. 330ml cans) |
Read Guide โ |
| Strategy 1 |
BaselineAnchorStrategy |
Standard Handheld Photos: Auditor taking straight front photos of retail shelves. |
โข Straight handheld phone camera โข Moderate product height variation |
Read Guide โ |
| Strategy 2 |
CenterYOverlapStrategy โญ (Default Recommended) |
Universal Production Default: Best for general retail mobile apps & production APIs. |
โข Camera tilt (10ยฐโ25ยฐ perspective skew) โข Mixed product heights (1.5L bottles alongside small cans) |
Read Guide โ |
| Strategy 3 |
SpatialClusterStrategy |
Wide-Angle & Dense Items: Wide bay photos or small dense product displays. |
โข Wide-angle lens or 3โ4m distant shots โข Dense displays (cosmetics, sachets) |
Read Guide โ |
| Strategy 4 |
VerticalIoUStrategy |
Vertically Stacked Products: Products resting directly on top of each other without partitions. |
โข Stacked beverage cans or promo bins โข Open chest freezer displays |
Read Guide โ |
| Strategy 5 |
ShelfProjectionStrategy |
Full Bay Top-to-Bottom: Full shelf bay photos covering top to bottom. |
โข Complete vertical bay photos โข Distinct empty vertical gaps between shelf levels |
Read Guide โ |
๐ณ Strategy Decision Flowchart
graph TD
A["Is the photo taken by a mobile camera with potential tilt or mixed product heights?"] -->|Yes| B["Use Strategy 2: CenterYOverlapStrategy (Default Recommended)"]
A -->|No| C{"Are products vertically stacked directly on top of each other?"}
C -->|Yes| D["Use Strategy 4: VerticalIoUStrategy"]
C -->|No| E{"Is it a wide-angle shot of a large 3-4m bay or dense small items?"}
E -->|Yes| F["Use Strategy 3: SpatialClusterStrategy"]
E -->|No| G{"Does the photo capture a full top-to-bottom bay with clear empty shelf gaps?"}
G -->|Yes| H["Use Strategy 5: ShelfProjectionStrategy"]
G -->|No| I{"Are all products 100% identical in size and camera strictly flat 0ยฐ?"}
I -->|Yes| J["Use Strategy 0: SequentialDeltaStrategy"]
I -->|No| K["Use Strategy 1: BaselineAnchorStrategy"]
๐จ 6. Match Status & Color System
During planogram evaluation, each detected item is assigned a MatchStatus used for visual rendering:
| MatchStatus |
Hex Color |
Visual Color |
Condition |
MatchStatusMatch |
#00d400 |
๐ข Green |
Detected item name matches expected planogram grid position & confidence $\ge$ threshold. |
MatchStatusMismatch |
#ff0000 |
๐ด Red |
Detected item name differs from expected planogram grid position. |
MatchStatusLowConfidence |
#ffcc00 |
๐ก Yellow |
Item name matches expected, but confidence score is below threshold. |
MatchStatusCompetitor |
#ff9900 |
๐ Orange |
Item name matches registered competitor brand list. |
MatchStatusUnmatched |
#888888 |
๐ฉถ Gray |
Unregistered label or neutral detection. |
๐ 7. Exhaustive API Reference
A. Facade Processor (planogrid.PlanogramProcessor)
planogrid.NewPlanogramProcessor() *PlanogramProcessor
Constructs a new PlanogramProcessor initialized with CenterYOverlapStrategy(0.50) and default confidence threshold (70.0).
(p *PlanogramProcessor) SetRowStrategy(strategy RowSortingStrategy) *PlanogramProcessor
Updates the active row-sorting strategy dynamically. Returns receiver for method chaining.
(p *PlanogramProcessor) SetConfidenceThreshold(threshold float64) *PlanogramProcessor
Sets the minimum confidence percentage required for an item to be evaluated as a valid match.
(p *PlanogramProcessor) SetCompetitorLabels(labels []string) *PlanogramProcessor
Registers a slice of brand names that should be flagged with MatchStatusCompetitor (Orange #ff9900).
(p *PlanogramProcessor) Process(items []CustomLabelDetection) PlanogramGridResult
Executes spatial 2D row-and-column grid sorting on a slice of detections. Returns PlanogramGridResult.
(p *PlanogramProcessor) Verify(imageBytes []byte, awsDetections []AWSRawDetection, expectedPlanogram [][]string, imageWidth, imageHeight float64) (*PlanogramVerificationResult, error)
Converts raw AWS Rekognition JSON detections into pixel coordinates, sorts them spatially into rows and columns, audits compliance against expectedPlanogram, and renders annotated bounding boxes onto imageBytes (if provided).
(p *PlanogramProcessor) VerifyCustomLabels(imageBytes []byte, detections []CustomLabelDetection, expectedPlanogram [][]string) (*PlanogramVerificationResult, error)
Performs planogram auditing and image annotation directly on custom detections without AWS coordinate scaling.
B. Strategy Constructor Functions
| Constructor Function |
Parameters |
Description |
planogrid.NewSequentialDeltaStrategy() |
None |
Constructs Strategy 0 (sequential top delta). |
planogrid.NewBaselineAnchorStrategy(multiplier float64) |
multiplier (default 0.50) |
Constructs Strategy 1 (top baseline anchor scaled by median height). |
planogrid.NewCenterYOverlapStrategy(minOverlapRatio float64) |
minOverlapRatio (default 0.50) |
Constructs Strategy 2 (CenterY containment & vertical overlap ratio). |
planogrid.NewSpatialClusterStrategy(epsFactor float64) |
epsFactor (default 0.45) |
Constructs Strategy 3 (1D DBSCAN spatial clustering). |
planogrid.NewVerticalIoUStrategy(minIoU float64) |
minIoU (default 0.40) |
Constructs Strategy 4 (1D Vertical Intersection over Union). |
planogrid.NewShelfProjectionStrategy(resolution int) |
resolution (default 200) |
Constructs Strategy 5 (1D vertical projection histogram binning). |
C. DTOs & Helper Functions
planogrid.NewCustomLabelDetectionFromAWS(aws AWSRawDetection, imageWidth, imageHeight float64) CustomLabelDetection
Converts raw AWS Rekognition detection payload to CustomLabelDetection. Automatically scales ratio coordinates (0.0 - 1.0) to absolute pixel coordinates if imageWidth > 1.0 or imageHeight > 1.0.
BoundingBox Methods (dto.BoundingBox)
(b BoundingBox) Right() float64: Returns rightmost X coordinate (Left + Width).
(b BoundingBox) Bottom() float64: Returns bottommost Y coordinate (Top + Height).
(b BoundingBox) CenterX() float64: Returns X-axis center coordinate (Left + Width/2).
(b BoundingBox) CenterY() float64: Returns Y-axis center coordinate (Top + Height/2).
(b BoundingBox) VerticalIoU(other BoundingBox) float64: Calculates 1D vertical IoU ratio against another bounding box.
(b BoundingBox) ToMap() map[string]float64: Returns rounded coordinates as a key-value map.
PlanogramGridResult Methods (dto.PlanogramGridResult)
(p PlanogramGridResult) GetResult() []map[string]string: Returns brand name mapping per row ("Brand 1": "Product Alpha", "Brand 2": "Product Beta").
(p PlanogramGridResult) GetResultGeometry() [][]map[string]interface{}: Returns spatial coordinates per cell.
(p PlanogramGridResult) ToMap() map[string]interface{}: Returns structured map.
(p PlanogramGridResult) ToJSON() (string, error): Returns formatted JSON string representation.
D. Direct Sub-package Services
sorter.SpatialGridSorter
sorter.NewSpatialGridSorter(strategy strategies.RowSortingStrategy) *SpatialGridSorter
(s *SpatialGridSorter) SetStrategy(strategy strategies.RowSortingStrategy)
(s *SpatialGridSorter) Sort(items []dto.CustomLabelDetection) dto.PlanogramGridResult
matcher.PlanogramMatcherService
matcher.NewPlanogramMatcherService() *PlanogramMatcherService
(m *PlanogramMatcherService) SetConfidenceThreshold(threshold float64)
(m *PlanogramMatcherService) SetCompetitorLabels(labels []string)
(m *PlanogramMatcherService) Evaluate(gridResult dto.PlanogramGridResult, expected [][]string, raw []dto.CustomLabelDetection) PlanogramVerificationResult
annotator.ImageAnnotatorService
annotator.NewImageAnnotatorService() *ImageAnnotatorService
(s *ImageAnnotatorService) Annotate(imageBytes []byte, detections []dto.CustomLabelDetection, matchStatuses map[int]enums.MatchStatus) ([]byte, error)
๐ 8. Web Framework Integration Examples
Gin Web Framework
package main
import (
"encoding/base64"
"net/http"
"github.com/gin-gonic/gin"
"github.com/hanifalkauni/planogrid-go"
)
func main() {
r := gin.Default()
processor := planogrid.NewPlanogramProcessor()
r.POST("/api/v1/planogram/verify", func(c *gin.Context) {
var req struct {
CustomLabels []planogrid.AWSRawDetection `json:"custom_labels"`
ImageBase64 string `json:"image_base64"`
Expected [][]string `json:"expected_planogram"`
Width float64 `json:"image_width"`
Height float64 `json:"image_height"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
imageBytes, _ := base64.StdEncoding.DecodeString(req.ImageBase64)
evaluation, err := processor.Verify(imageBytes, req.CustomLabels, req.Expected, req.Width, req.Height)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"status": evaluation.Status,
"compliance_score": evaluation.ComplianceScore,
"matched_count": evaluation.MatchedCount,
"total_expected": evaluation.TotalExpected,
"grid_result": evaluation.GridResult.ToMap(),
"annotated_image": base64.StdEncoding.EncodeToString(evaluation.AnnotatedImage),
})
})
r.Run(":8080")
}
Fiber Web Framework
package main
import (
"encoding/base64"
"github.com/gofiber/fiber/v2"
"github.com/hanifalkauni/planogrid-go"
)
func main() {
app := fiber.New()
processor := planogrid.NewPlanogramProcessor()
app.Post("/api/v1/planogram/verify", func(c *fiber.Ctx) error {
var req struct {
CustomLabels []planogrid.AWSRawDetection `json:"custom_labels"`
ImageBase64 string `json:"image_base64"`
Expected [][]string `json:"expected_planogram"`
Width float64 `json:"image_width"`
Height float64 `json:"image_height"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
imageBytes, _ := base64.StdEncoding.DecodeString(req.ImageBase64)
evaluation, err := processor.Verify(imageBytes, req.CustomLabels, req.Expected, req.Width, req.Height)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"status": evaluation.Status,
"compliance_score": evaluation.ComplianceScore,
"matched_count": evaluation.MatchedCount,
"grid_result": evaluation.GridResult.ToMap(),
"annotated_image": base64.StdEncoding.EncodeToString(evaluation.AnnotatedImage),
})
})
app.Listen(":3000")
}
Standard net/http
package main
import (
"encoding/base64"
"encoding/json"
"net/http"
"github.com/hanifalkauni/planogrid-go"
)
func main() {
processor := planogrid.NewPlanogramProcessor()
http.HandleFunc("/api/v1/planogram/verify", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
CustomLabels []planogrid.AWSRawDetection `json:"custom_labels"`
ImageBase64 string `json:"image_base64"`
Expected [][]string `json:"expected_planogram"`
Width float64 `json:"image_width"`
Height float64 `json:"image_height"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
imageBytes, _ := base64.StdEncoding.DecodeString(req.ImageBase64)
evaluation, err := processor.Verify(imageBytes, req.CustomLabels, req.Expected, req.Width, req.Height)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": evaluation.Status,
"compliance_score": evaluation.ComplianceScore,
"matched_count": evaluation.MatchedCount,
"grid_result": evaluation.GridResult.ToMap(),
"annotated_image": base64.StdEncoding.EncodeToString(evaluation.AnnotatedImage),
})
})
http.ListenAndServe(":8080", nil)
}
๐งช 9. Running Tests & Benchmarks
Run full test suite across all sub-packages:
go test -v ./...
Run benchmarks for spatial sorting performance:
go test -bench=. -benchmem ./...
๐ 10. License
Released under the open-source MIT License.