AI Product Extractor for Catalogues

Train a YOLO11 model to detect, crop, and OCR products from catalogue pages.

Introduction

30 Second Summary

Every week, supermarkets and retail stores publish promotional catalogues packed with dozens of product listings. Extracting product names, prices, and discounts from these pages by hand is tedious and error-prone.

In this project, you will build an AI-powered pipeline that automatically detects individual product boxes on catalogue pages, reads the Arabic and English text inside each one, and stores everything in a searchable database.

What You'll Build

Point your pipeline at a raw catalogue page image and watch it draw bounding boxes around every product, crop each one out, read the bilingual text, and insert a clean database row you can query instantly.

By the end of this project, you'll have:

  • A custom-trained YOLO11 model that detects every product listing on a catalogue page and draws bounding boxes around them.
  • A cropping step that saves each detected product as its own image file, ready for OCR processing.
  • A bilingual text extraction pipeline using EasyOCR that reads Arabic and English text from each product box and stores results in a PostgreSQL database with source page and coordinates.
  • Secret Mission: Parse raw OCR text into structured fields like product name, price, and discount, then build a search-by-price query tool.

Are there any prerequisites?

This project assumes you have Python installed, an NVIDIA GPU with CUDA for model training, and PostgreSQL running locally. You should also have catalogue PDF pages already converted to images using PyMuPDF.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Environment

Your extraction pipeline depends on several specialized tools working together. You need a deep learning framework for object detection, an OCR engine for reading bilingual text, an annotation tool for labelling training data, and a database driver for storing results.

In this step, you'll install all the required Python packages and set up a clean project folder structure that YOLO11 expects for training.

In this step, get ready to:
  • Install Python packages for object detection, OCR, and database access.
  • Set up LabelImg for bounding box annotation.
  • Create the project folder structure with training and validation directories.
Install the core Python packages

Ultralytics bundles PyTorch and the YOLO11 model architecture in one install. EasyOCR handles Arabic and English text recognition. psycopg2-binary connects Python to PostgreSQL without needing a C compiler.

  • Install the four core packages by running these commands:
pip install -U ultralytics
pip install easyocr
pip install psycopg2-binary
pip install Pillow

What do these packages do?

  • ultralytics installs PyTorch and the YOLO11 detection framework together.
  • easyocr provides a pre-trained OCR engine that supports Arabic and English text.
  • psycopg2-binary is a PostgreSQL adapter for Python that ships pre-compiled.
  • Pillow adds image manipulation capabilities like cropping detected regions.

Before you run the verification, what do you think will happen if one of the packages failed to install?

  • Verify all critical imports work by running:
python -c "from ultralytics import YOLO; import easyocr; import psycopg2; print('All imports OK')"

You should see All imports OK printed with no errors.

Seeing an ImportError?

  • Check that you ran each pip install command from the same Python environment you are using to run the verification.
  • If you have multiple Python versions, try pip3 install instead of pip install.
  • Help me debug my import error for ultralytics, easyocr, or psycopg2.
Install LabelImg for annotation

LabelImg is a graphical tool for drawing bounding boxes on images and saving them in YOLO format. You will use it in the next step to teach YOLO11 what product boxes look like.

  • Install LabelImg by running:
pip install labelImg
  • Verify it launches by running:
labelImg

A GUI window should appear with an image viewing area and annotation controls. Close it for now.

LabelImg not launching?

  • On some systems, labelImg requires PyQt5. Run pip install PyQt5 and try again.
  • If you see a ModuleNotFoundError, make sure you installed it in the same environment as your other packages.
  • Help me get LabelImg working on my system.
Create your project folder structure

YOLO11 training expects a specific folder layout: images and labels split into training and validation sets. You also need an output folder for the cropped product images your pipeline will generate later.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder with all subdirectories by running these commands:

macOS

mkdir catalogue-extractor
cd catalogue-extractor
mkdir -p dataset/images/train dataset/images/val dataset/labels/train dataset/labels/val
mkdir -p output/crops

Windows

mkdir catalogue-extractor
cd catalogue-extractor
mkdir dataset\images\train, dataset\images\val, dataset\labels\train, dataset\labels\val
mkdir output\crops

What do these folders do?

  • dataset/images/train and dataset/images/val hold your catalogue page images for training and validation.
  • dataset/labels/train and dataset/labels/val will hold YOLO annotation files (one .txt per image).
  • output/crops will store individual product images cropped from detected bounding boxes.
  • Copy approximately 15-20 of your catalogue page images (.jpg files) into dataset/images/train.
  • Copy 3-5 different catalogue page images into dataset/images/val.

Choose varied pages

Pick pages that represent the range of layouts in your catalogue. Use different page numbers with different product arrangements so the model learns to generalise.

  • Confirm your training images are in place by running:
ls dataset/images/train

You should see your catalogue page image files listed. If the folder is empty, copy your images in before continuing.

Folder is empty or not found?

  • Make sure you ran cd catalogue-extractor after creating the folder. The ls command runs relative to your current directory.
  • Verify you copied images into dataset/images/train and not into dataset/images directly.
  • Help me find where my catalogue images ended up.

Your environment is ready and your dataset is in place. Next up, you'll annotate product boxes on these catalogue pages so YOLO11 knows what to look for.

Annotate Product Boxes with LabelImg

Your environment is set up with all the tools you need. Now it's time to teach your model what a product box actually looks like.

YOLO11 learns from examples. Without labeled training data showing exactly where product boxes appear on each catalogue page, the model has nothing to learn from. You'll use LabelImg to draw bounding boxes around every product listing on your pages, creating the annotations that power the training process.

In this step, get ready to:
  • Create a class definition file and launch LabelImg.
  • Annotate all training and validation images with bounding boxes.
  • Create a dataset configuration file that tells YOLO11 where your images and labels live.
Create the class definition and launch LabelImg

Before you can annotate, LabelImg needs to know which classes you're labeling. Your model detects a single class: product boxes on catalogue pages.

  • Create a file called classes.txt inside your dataset folder with this single line:
product_box

What is this file for?

LabelImg reads classes.txt to populate the class selection dialog when you draw a bounding box. Since you're only detecting one thing (product boxes), the file has a single line.

  • Launch LabelImg pointing at your training images by running this command:
labelImg dataset\images\train dataset\classes.txt dataset\labels\train

A GUI window opens showing your first catalogue page image.

  • In the LabelImg toolbar, find the format button directly below the Save button. It likely says PascalVOC.
  • Click it to switch to YOLO format. This ensures annotations save as YOLO-format .txt files instead of XML.

Why YOLO format?

LabelImg supports two annotation formats. PascalVOC saves as XML files, while YOLO saves as simple .txt files with one line per object. Each line contains the class ID and the bounding box coordinates normalized between 0 and 1.

The Ultralytics training pipeline reads YOLO-format labels directly. Choosing this format now means zero conversion work later.

LabelImg window not appearing?

Make sure you're running the command from inside your catalogue-extractor folder so the relative paths resolve correctly.

If you see a Qt-related error, try closing and relaunching your terminal, then run the command again.

help me debug why LabelImg won't open on my machine

Annotate your training images

Now you'll draw bounding boxes around every product listing on each catalogue page. A typical page has 6-12 product boxes.

  • Press W to activate the rectangle drawing tool.
  • Click and drag to draw a tight rectangle around one product listing. Include the product's image, name, price, and any discount text within the box.
  • In the class selection dialog that appears, select product_box.
  • Repeat for every product on the page. Draw a separate box for each individual product listing.
  • Press Ctrl+S to save the annotations for this image.
  • Press D to move to the next image.
  • Continue annotating all 15-20 training images using this workflow for each one.

Draw tight boxes for better results

Keep your bounding boxes as close to the product listing edges as possible. Loose boxes that include background areas or overlap with neighboring products teach the model imprecise boundaries.

  • After annotating all training images, confirm the annotation files were created by running:
dir dataset\labels\train

You should see one .txt file for each image you annotated.

Not seeing any .txt files?

Confirm you switched LabelImg to YOLO format before saving. If you saved in PascalVOC mode, you'll see .xml files instead. Delete those, switch to YOLO format, and re-annotate.

Also check that the save directory is set to dataset\labels\train. If labels saved elsewhere, use File > Change Save Dir to correct it.

help me fix my LabelImg annotation save location

Annotate validation images and create the dataset config
  • Close LabelImg and relaunch it pointing at your validation images by running:
labelImg dataset\images\val dataset\classes.txt dataset\labels\val
  • Confirm the format button below Save says YOLO.
  • Annotate all 3-5 validation images using the same workflow: press W, draw boxes, select product_box, save with Ctrl+S, advance with D.
  • Close LabelImg after saving your last annotation.
  • Verify your validation labels were saved by running:
dir dataset\labels\val

You should see one .txt file per validation image.

Now you need a YAML configuration file that tells YOLO11 where your images, labels, and class definitions are located.

  • Create a file called dataset.yaml in your project root (the catalogue-extractor folder) with this content:
path: ./dataset
train: images/train
val: images/val

names:
  0: product_box

What does this config do?

The path field points to the root of your dataset. The train and val fields give paths relative to path for your training and validation images.

The names dictionary maps class IDs to class names. Your annotation files use 0 as the class ID, and this config tells YOLO11 that class 0 means product_box.

  • Save dataset.yaml.

✔️ Awesome, I've got everything!

Great. Double check you have saved both classes.txt and dataset.yaml, and that your labels folders contain .txt files.

ⓧ I'd like to double check the full code

Your dataset.yaml should look exactly like this:

path: ./dataset
train: images/train
val: images/val

names:
  0: product_box

Your dataset/classes.txt should contain a single line:

product_box

Before moving on, take a look inside one of your annotation files to confirm the format is correct.

  • What do you think you'll see inside a YOLO annotation file for a page with 8 products?
  • Open any .txt file in dataset\labels\train with a text editor or run:
type dataset\labels\train\*.txt | more

You should see lines like 0 0.45 0.32 0.20 0.35. Each line represents one product box: class ID 0 followed by normalized x_center, y_center, width, and height values between 0 and 1. A page with 8 products has 8 lines.

Your annotations are ready. Next up, you'll feed these labeled examples into YOLO11 and train it to detect product boxes on its own.

Train Your YOLO11 Model

Your annotated dataset teaches the model what a product box looks like on a catalogue page. The problem is that a pretrained YOLO11 model already knows how to detect common objects like cars, people, and dogs. It has no idea what a product listing on a promotional leaflet looks like.

In this step, you will fine-tune the pretrained model on your annotated catalogue images. This process is called transfer learning. The model keeps its general understanding of shapes and edges, but learns your specific layout patterns on top.

In this step, get ready to:
  • Fine-tune YOLO11 on your annotated catalogue dataset.
  • Monitor training progress and verify loss curves are decreasing.
  • Validate the trained model's detection accuracy with mAP metrics.
Create and run the training script

The training script loads a pretrained YOLO11 nano model and fine-tunes it on your dataset.yaml configuration for 50 epochs.

  • Create a file called train.py in your catalogue-extractor folder with the following code:
from ultralytics import YOLO

# Load the pretrained YOLO11 nano model
model = YOLO("yolo11n.pt")

# Fine-tune on our catalogue dataset
results = model.train(
    data="dataset.yaml",
    epochs=50,
    imgsz=640,
    batch=8,
    device=0,
    name="catalogue_detector"
)

print("Training complete!")
print(f"Best model saved to: {results.save_dir / 'weights' / 'best.pt'}")

What does this code do?

YOLO("yolo11n.pt") loads the pretrained YOLO11 nano weights. The "n" stands for nano, the smallest and fastest variant. On first run, Ultralytics downloads these weights automatically.

model.train() starts the fine-tuning process. epochs=50 means the model sees every training image 50 times. imgsz=640 resizes images to 640x640 pixels for consistency. device=0 tells it to use your first GPU.

name="catalogue_detector" sets the output folder name, so results save to runs/detect/catalogue_detector/ instead of a generic numbered folder.

  • Save train.py.
  • Run the training script by running this command:
python train.py

The first run downloads the pretrained yolo11n.pt weights automatically. Training on 15-20 images with a GPU takes approximately 5-10 minutes.

Seeing a CUDA error?

If you see a CUDA out-of-memory error, reduce batch=8 to batch=4 in train.py and try again. Smaller batches use less GPU memory.

If you see a message about no CUDA device available, make sure your NVIDIA drivers are installed and device=0 matches your GPU index. You can change to device="cpu" but training will be much slower.

Help me fix my YOLO11 training error

Monitor training and check results

While training runs, the console prints progress for each epoch. You should see loss values decreasing over time.

  • Watch the console output as training progresses. Look for three loss values: box_loss, cls_loss, and dfl_loss. All three should decrease over epochs.
  • After training completes, open the runs/detect/catalogue_detector/ folder.
  • Open results.png to see training curves. The loss graphs should trend downward, showing the model improved over 50 epochs.
  • Open confusion_matrix.png to verify the model learned to distinguish product_box from background.

What do the loss values mean?

box_loss measures how accurately the model draws bounding boxes around objects. cls_loss measures how well it classifies what it found. dfl_loss refines the exact edges of each bounding box.

All three decreasing together means the model is learning your catalogue layout patterns. If they plateau early, you may need more annotated images.

  • Confirm that best.pt exists inside runs/detect/catalogue_detector/weights/ by running:
ls runs/detect/catalogue_detector/weights/

You should see best.pt and last.pt listed. The best.pt file is the checkpoint with the lowest validation loss across all 50 epochs.

Validate the model's detection accuracy

Training curves show progress, but you need a concrete number to know if the model is good enough. The mAP50 metric (mean Average Precision at 50% overlap) is the standard measure for object detection accuracy.

  • Before you run the validation, predict: do you think the model will perform well with only 15-20 training images?
  • Validate your trained model by running this code:
from ultralytics import YOLO

# Load the best weights from training
model = YOLO("runs/detect/catalogue_detector/weights/best.pt")

# Run validation against the val split
metrics = model.val(data="dataset.yaml")
print(f"mAP50: {metrics.box.map50:.3f}")
print(f"mAP50-95: {metrics.box.map:.3f}")

What is mAP50?

mAP50 measures what fraction of the model's detections overlap at least 50% with the real product boxes you annotated. A score of 0.7 means the model correctly finds 70% of products with accurate bounding boxes.

mAP50-95 is stricter. It averages accuracy across overlap thresholds from 50% to 95%. This number is always lower than mAP50.

Even with a small dataset of 15-20 images, you should see mAP50 above 0.7 for well-annotated product boxes. Catalogue pages have consistent, grid-like layouts that YOLO learns quickly.

mAP50 below 0.7?

If your mAP50 is low, go back to Step 2 and add more annotated images. Tight, accurate bounding boxes matter more than quantity. Make sure every product on each page is annotated, with no gaps.

Also check that your .txt label files are in dataset/labels/train/ (not inside dataset/images/train/). YOLO expects labels in a parallel folder structure.

Help me improve my YOLO model's mAP score

✔️ Awesome, I've got everything!

Great. Double check you have saved train.py and confirmed best.pt exists in your weights folder.

ⓧ I'd like to double check the full code

Here is the complete train.py file:

from ultralytics import YOLO

model = YOLO("yolo11n.pt")

results = model.train(
    data="dataset.yaml",
    epochs=50,
    imgsz=640,
    batch=8,
    device=0,
    name="catalogue_detector"
)

print("Training complete!")
print(f"Best model saved to: {results.save_dir / 'weights' / 'best.pt'}")

Your model can now detect product boxes on catalogue pages. Next up, you will run it on full catalogue images to automatically find and crop every product listing.

Detect and Crop Product Boxes

Your custom YOLO11 model is trained and validated. It knows what product boxes look like on your catalogue pages.

Now it is time to run that model on real pages and automatically crop out each product listing. However, running inference with default settings often produces noisy false positives. Headers, logos, and decorative elements can all trigger detections. You will see this first-hand and learn to tune the thresholds that filter the noise.

In this step, get ready to:
  • Run your trained model on catalogue page images to detect product boxes.
  • Crop each detected product region into its own image file.
  • Tune confidence and IoU thresholds to eliminate false positives.
Write the detection and cropping script

Your trained model lives at runs/detect/catalogue_detector/weights/best.pt. You will load it, point it at your validation images, and save each detected product region as a separate cropped image.

  • Create a new file called detect_and_crop.py in your catalogue-extractor folder.
  • Add the imports and setup code by pasting this into the file:
from ultralytics import YOLO
from pathlib import Path
from PIL import Image

# Load the trained model from your fine-tuning run
model = YOLO("runs/detect/catalogue_detector/weights/best.pt")

# Point at validation images and set up the output folder
source_dir = Path("dataset/images/val")
output_dir = Path("output/crops")
output_dir.mkdir(parents=True, exist_ok=True)

# Store detection metadata for each product found
all_detections = []

What does this code do?

This loads your custom-trained best.pt model instead of the generic pretrained weights.

output_dir.mkdir(parents=True, exist_ok=True) creates the output folder and any missing parent directories. The all_detections list will collect metadata about every product the model finds.

  • Now add the detection loop below the setup code. This runs inference on each image and crops out every detected product:
for img_path in source_dir.glob("*.jpg"):
    # Run detection with default-ish confidence threshold
    results = model.predict(
        source=str(img_path),
        conf=0.25,
        iou=0.7,
        save=True
    )

    img = Image.open(img_path)

    # Loop through each detected box and crop it
    for idx, box in enumerate(results[0].boxes):
        x1, y1, x2, y2 = box.xyxy[0].int().tolist()
        confidence = box.conf[0].item()

        crop = img.crop((x1, y1, x2, y2))
        crop_filename = f"{img_path.stem}_product_{idx}.jpg"
        crop.save(output_dir / crop_filename)

        all_detections.append({
            "source_page": img_path.name,
            "crop_path": crop_filename,
            "bbox": (x1, y1, x2, y2),
            "confidence": confidence
        })

print(f"Detected and cropped {len(all_detections)} product boxes")

What does this code do?

  • model.predict() runs your trained model on each image. conf=0.25 means the model reports any detection it is at least 25% confident about.
  • box.xyxy[0] gives the pixel coordinates of each bounding box (top-left and bottom-right corners).
  • img.crop() uses Pillow to cut out the detected region and save it as its own image file.
  • save=True also saves annotated full-page images with bounding boxes drawn on them so you can visually inspect results.
  • Save detect_and_crop.py.
Run inference on your catalogue pages

Before you run the script, think about what you expect to see. The model was trained on product boxes, and you are using a low confidence threshold of 0.25. Do you think every detection will be a real product, or might some noise slip through?

  • Run the detection script by executing this command:
python detect_and_crop.py
  • Check the output/crops/ folder by running:
ls output/crops/

Open some of the cropped images. You will likely see false positives: areas detected as products that are actually page headers, logos, or background decorations. This is because conf=0.25 is too permissive for your use case. Any detection the model is only 25% confident about slips through.

Why does this happen?

The default conf threshold of 0.25 is designed for general-purpose detection where missing an object is worse than a false alarm. For your pipeline, false positives clutter the OCR stage with garbage text. You need higher confidence.

Similarly, the default iou=0.7 (Intersection over Union) for Non-Maximum Suppression is lenient. Overlapping boxes that share less than 70% area both survive. Lowering this value suppresses duplicate detections of the same product.

Tune confidence and IoU thresholds

The fix is simple. Raise the confidence threshold to reject weak detections, and lower the IoU threshold to suppress overlapping duplicates.

  • In detect_and_crop.py, find the model.predict() call inside the loop.
  • Find these two lines:
        conf=0.25,
        iou=0.7,
  • Replace them with these values:
        conf=0.5,
        iou=0.5,

What do these thresholds mean?

  • conf=0.5 means only detections the model is at least 50% confident about survive. This filters out weak guesses that are usually false positives.
  • iou=0.5 means if two detections overlap by 50% or more, only the highest-confidence one is kept. This eliminates duplicate boxes around the same product.
  • Save detect_and_crop.py.
  • Delete the old noisy crops so you start fresh:
rm output/crops/*
  • Re-run the detection script:
python detect_and_crop.py

What do you expect the count to look like now compared to the first run? It should be lower, because the stricter thresholds rejected the weak detections.

  • Check your output/crops/ folder again:
ls output/crops/

Open a few of the new crops. You should now see clean images containing only actual product listings, one per file. The false positives from headers and logos are gone.

Still seeing false positives?

  • Try raising conf to 0.6 or 0.7. The right value depends on your specific catalogue layout and how many training images you annotated.
  • If you are missing products that should be detected, your confidence threshold may be too high. Lower it slightly and re-run.
  • If overall detection quality is poor, go back to Step 2 and add more annotated training images. Even 5-10 extra pages can significantly improve the model.

Help me tune my YOLO detection thresholds for catalogue product boxes

✔️ Awesome, I've got everything!

Great. Make sure you have saved detect_and_crop.py with the updated conf=0.5 and iou=0.5 values.

ⓧ I'd like to double check the full code

Your detect_and_crop.py should contain two sections in this order:

  • The imports and setup section (loading the model, defining source_dir, output_dir, and all_detections).
  • The detection loop with conf=0.5 and iou=0.5 in the model.predict() call, followed by the inner loop that crops, saves, and appends detection metadata.

Scroll up to both code blocks in this step and confirm your file matches them, with the updated threshold values from the line edit applied.

You now have a clean set of cropped product images, one per detected product box on your catalogue pages. Next up, you will feed these crops into EasyOCR to read the Arabic and English text and store everything in a searchable PostgreSQL database.

OCR and Store in PostgreSQL

Your detection pipeline produces neatly cropped product images. Cropped images alone are not searchable or queryable though.

In this step, you will use EasyOCR to read Arabic and English text from each product crop, then store everything in a PostgreSQL database you can query later.

In this step, get ready to:
  • Create a PostgreSQL database and products table to hold extraction results.
  • Build an OCR pipeline that reads bilingual text from each cropped product image.
  • Query the database to confirm all extracted products are stored correctly.
Create the PostgreSQL database and table

Each detected product needs a home where you can store its source page, bounding box coordinates, confidence score, and the OCR text you will extract.

  • Create the catalogue_db database by running this command in your terminal:
psql -U postgres -c "CREATE DATABASE catalogue_db;"

Connection refused or role does not exist?

Make sure your PostgreSQL server is running. On Windows, check the Services app for "postgresql". On macOS, run brew services start postgresql if you installed via Homebrew.

If your PostgreSQL user is not postgres, replace -U postgres with your actual username.

Help me connect to my local PostgreSQL server.

  • Now create a file called setup_db.py in your catalogue-extractor folder. This script creates the products table:
import psycopg2

# Connect to the catalogue database
conn = psycopg2.connect(
    dbname="catalogue_db",
    user="postgres",
    password="your-password-here",
    host="localhost",
    port=5432
)
cur = conn.cursor()

# Create the products table to store detection and OCR results
cur.execute("""
    CREATE TABLE IF NOT EXISTS products (
        id SERIAL PRIMARY KEY,
        source_page TEXT NOT NULL,
        crop_path TEXT NOT NULL,
        bbox_x1 INTEGER,
        bbox_y1 INTEGER,
        bbox_x2 INTEGER,
        bbox_y2 INTEGER,
        confidence REAL,
        raw_text TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
""")

conn.commit()
cur.close()
conn.close()
print("Database table 'products' created successfully!")

What does this code do?

  • psycopg2.connect() opens a connection to your local PostgreSQL server and the catalogue_db database you just created.
  • The CREATE TABLE IF NOT EXISTS statement defines columns for the source page filename, crop path, bounding box coordinates, detection confidence, and the raw OCR text.
  • created_at automatically timestamps each inserted row so you know when products were extracted.
  • Replace your-password-here with your actual PostgreSQL password.
  • Save setup_db.py and run it:
python setup_db.py

You should see Database table 'products' created successfully! printed in your terminal.

Seeing a connection error?

Double check that the dbname, user, and password values match your local PostgreSQL setup. The database name must be catalogue_db (the one you created with psql earlier).

Help me debug my psycopg2 connection to PostgreSQL.

Run OCR and store results in the database

Now you need a script that ties everything together: detect products on each page, crop them, read the Arabic and English text with EasyOCR, and insert each result into the database.

  • Create a file called ocr_and_store.py in your catalogue-extractor folder. Start with the imports and initialization:
import easyocr
import psycopg2
from pathlib import Path
from PIL import Image
from ultralytics import YOLO

# Initialize the OCR reader for Arabic and English
reader = easyocr.Reader(['ar', 'en'], gpu=True)

# Load your trained product detection model
model = YOLO("runs/detect/catalogue_detector/weights/best.pt")

# Connect to PostgreSQL
conn = psycopg2.connect(
    dbname="catalogue_db",
    user="postgres",
    password="your-password-here",
    host="localhost",
    port=5432
)
cur = conn.cursor()

# Set up source and output directories
source_dir = Path("dataset/images/val")
crop_dir = Path("output/crops")
crop_dir.mkdir(parents=True, exist_ok=True)

What does this setup do?

  • easyocr.Reader(['ar', 'en'], gpu=True) creates an OCR reader that recognizes both Arabic and English text. The Arabic model handles both scripts together.
  • The trained YOLO model and PostgreSQL connection are reused from previous steps. Replace your-password-here with your actual password.
  • Below the initialization code, add the detection and OCR loop that processes each page:
for img_path in source_dir.glob("*.jpg"):
    results = model.predict(source=str(img_path), conf=0.5, iou=0.5)
    img = Image.open(img_path)

    for idx, box in enumerate(results[0].boxes):
        x1, y1, x2, y2 = box.xyxy[0].int().tolist()
        confidence = box.conf[0].item()

        # Crop and save each detected product
        crop = img.crop((x1, y1, x2, y2))
        crop_filename = f"{img_path.stem}_product_{idx}.jpg"
        crop.save(crop_dir / crop_filename)

        # Run OCR and insert results into PostgreSQL
        ocr_results = reader.readtext(str(crop_dir / crop_filename))
        raw_text = " ".join([text for (_, text, _) in ocr_results])

        cur.execute("""
            INSERT INTO products (source_page, crop_path, bbox_x1, bbox_y1, bbox_x2, bbox_y2, confidence, raw_text)
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
        """, (img_path.name, crop_filename, x1, y1, x2, y2, confidence, raw_text))

conn.commit()
print("All products processed and stored in database!")

cur.execute("SELECT id, crop_path, raw_text FROM products LIMIT 5")
for row in cur.fetchall():
    print(f"  ID {row[0]}: {row[1]} -> {row[2][:80]}...")

cur.close()
conn.close()

What does this loop do?

  • For each validation image, the trained model detects product boxes using your tuned conf=0.5 and iou=0.5 thresholds.
  • reader.readtext() returns a list of tuples containing bounding box, text, and confidence for each text region found. The script joins all text fragments into one raw_text string.
  • Each product is inserted into the database with its source page, crop filename, pixel-level bounding box, detection confidence, and the full OCR text.
  • After committing all inserts, the script prints a preview of the first 5 stored rows so you can immediately verify results.
  • Save ocr_and_store.py and run it:
python ocr_and_store.py

First run downloads OCR models

The first time you run this script, EasyOCR downloads its Arabic and English recognition models (approximately 200MB total). This is a one-time download that gets cached locally for future runs.

After processing completes, you should see a confirmation message followed by a preview of the first 5 stored products with their crop paths and extracted text.

Seeing CUDA out of memory?

EasyOCR and YOLO both load models onto your GPU. If you run out of GPU memory, change gpu=True to gpu=False in the EasyOCR Reader initialization. OCR will run on CPU (slower but still functional).

Help me fix CUDA memory errors running EasyOCR with YOLO.

✔️ Awesome, I've got everything!

Great. Double check you have saved ocr_and_store.py and confirmed product previews printed in your terminal.

ⓧ I'd like to double check the full code

Your ocr_and_store.py should contain both chunks you added above, in order:

  • Chunk 1: Imports, EasyOCR reader initialization, YOLO model load, PostgreSQL connection, and directory setup.
  • Chunk 2: The nested for loop that iterates over images, detects products, crops them, runs OCR, inserts into the database, commits, prints previews, and closes connections.

Scroll up and confirm each chunk is present in the correct order.

Verify results in the database

The script's built-in preview shows a few rows. Now confirm the full count matches your detected products by querying PostgreSQL directly.

Before you run this query, predict: how many rows do you expect in the products table? It should match the number of product crops in your output/crops/ folder.

  • Check the total row count by running:
psql -d catalogue_db -c "SELECT COUNT(*) FROM products;"

The count should match the number of cropped product images you extracted in the previous step.

  • Now view the actual product data with source pages and text previews:
psql -d catalogue_db -c "SELECT id, source_page, LEFT(raw_text, 60) as text_preview FROM products ORDER BY id;"

You should see each product row with its source page filename and a preview of the extracted Arabic and English text.

Seeing zero rows or empty raw_text?

If the count is 0, make sure your validation images are .jpg files (the glob pattern filters on this extension). Check that images exist in dataset/images/val.

If raw_text is empty for some rows, the crop might be too small or blurry for EasyOCR to detect text. Try lowering the confidence threshold to include larger product regions.

Help me debug why my OCR results are empty.

You now have a fully working extraction pipeline. Raw catalogue page images go in, and structured product data comes out in a queryable database. Next up, you will take this further by parsing the raw OCR text into meaningful product fields.

Secret mission

Parse Products into Structured Fields

Your database holds raw OCR text blobs, but they're not searchable or structured. Can you parse out product names, prices, and discounts using regex, then build a command-line tool that searches products by price range?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • PostgreSQL database catalogue_db with products table (local, no ongoing cost).
  • Trained YOLO11 model weights in runs/detect/catalogue_detector/weights/best.pt (local file).
  • Cropped product images in output/crops/ (local files).
  • Project folder catalogue-extractor containing all scripts, dataset, and annotations.

Keep everything running

No action needed. Choose this if you're still actively building or plan to expand the pipeline with more catalogue pages.

  • All resources are local files and a local database. They consume disk space but cost nothing.
  • Your trained model weights are ready to use any time you run detect_and_crop.py or ocr_and_store.py on new catalogue pages.
  • The runs/ folder may contain training checkpoints from multiple experiments. These can grow large over time.

Pause - I'll come back to this later

Free up system resources by stopping PostgreSQL, but keep all your project files for later.

  • Stop the PostgreSQL service to free memory. On macOS, run brew services stop postgresql. On Windows, open Services and stop the PostgreSQL service.
  • Your catalogue-extractor folder stays on your Desktop. No files are deleted.
  • When you return, start PostgreSQL again and your catalogue_db database will be exactly as you left it.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your trained model, cropped images, and database permanently.

  • Drop the PostgreSQL database by running:
psql -U postgres -c "DROP DATABASE catalogue_db;"
  • Delete the project folder. On macOS/Linux, run:
rm -rf ~/Desktop/catalogue-extractor
  • On Windows, run:
rmdir /s /q %USERPROFILE%\Desktop\catalogue-extractor
  • Optionally, uninstall the Python packages you installed for this project by running:
pip uninstall ultralytics easyocr psycopg2-binary labelImg Pillow -y

Nice Work!

Nice Work!

You just built a complete AI-powered product extraction pipeline. Your system takes raw catalogue page images, detects every product listing with a custom-trained model, reads bilingual text, and stores structured results in a searchable database.

You've learned how to:

  • Trained a custom YOLO11 object detection model on your own annotated catalogue pages using transfer learning to find product boxes that no off-the-shelf model could detect.
  • Built a detection-to-OCR pipeline that crops individual products and extracts bilingual Arabic/English text using EasyOCR, turning visual catalogue data into machine-readable strings.
  • Stored all extracted product data in a queryable PostgreSQL database with source page references, bounding box coordinates, and confidence scores.
  • Secret Mission: Parsed raw OCR text into structured product fields and built a price-range search tool.

Ready to quiz yourself?