Ketika pertama kali membangun pipeline OCR untuk dokumen, saya sempat berpikir bahwa tantangan utamanya adalah memilih model OCR yang tepat.
Saya menggunakan Qwen3-VL 8b yang dijalankan secara self-hosted melalui Ollama di server dengan 2× NVIDIA Tesla T4. Namun setelah memproses image dokumen dari kondisi nyata, saya menemukan bahwa masalahnya tidak selalu ada pada model.
Dokumen bisa miring, orientasinya salah, background terlalu luas, kualitas gambar berbeda-beda, bahkan satu foto bisa berisi lebih dari satu dokumen.
Dari situ saya menyadari satu hal:
Sebelum meminta AI membaca dokumen, image yang diberikan harus dipastikan layak untuk dibaca.
Karena itu, saya mulai membangun image preprocessing pipeline khusus untuk OCR.
Dari Raw Image ke Data
Kalau dilihat dari luar, proses OCR mungkin terlihat sederhana:
Image
↓
OCR
↓
Text
Tetapi pipeline yang saya bangun akhirnya menjadi jauh lebih panjang:
Raw Image
↓
Input Validation
↓
Document Detection
↓
Crop Document
↓
Orientation Detection
↓
Rotate
↓
Image Enhancement
↓
OCR / Vision Model
↓
Structured JSON
↓
Mapping
↓
Database
Jadi image preprocessing bukan tujuan akhir.
Preprocessing adalah tahap untuk menyiapkan image sebelum image tersebut diproses oleh OCR atau vision model.
Output akhirnya bukan sekadar gambar yang sudah diperbaiki, tetapi data terstruktur yang nantinya bisa digunakan oleh sistem.
Tantangan Pertama: Tidak Semua Image Siap Diproses
Image yang masuk ke sistem bisa berasal dari berbagai kondisi.
Misalnya:
- foto dokumen dari kamera
- scan dokumen
- dokumen dengan background
- dokumen yang sedikit miring
- dokumen dengan orientasi 90° atau 180°
- beberapa dokumen dalam satu image
Kalau seluruh image langsung diberikan ke vision model, model harus melakukan banyak pekerjaan tambahan.
Model bukan hanya harus membaca teks, tetapi juga harus memahami:
“Dokumennya ada di mana?”
“Orientasinya bagaimana?”
“Bagian mana yang sebenarnya perlu dibaca?”
Saya kemudian memindahkan sebagian pekerjaan tersebut ke tahap preprocessing.
Pipeline Preprocessing
Secara keseluruhan, service preprocessing yang saya bangun kurang lebih seperti ini:
POST /preprocess
│
▼
HTTP + Guard
│
▼
Image Information
ImageMagick
│
▼
YOLO Document Detection
│
┌────────┴────────┐
│ │
Dokumen ditemukan Tidak ditemukan
│ │
▼ ▼
Sort Detection Text Detection
│ PP-OCRv5
│ │
│ ▼
│ Text Focus
└────────┬────────┘
▼
Crop + Padding
│
▼
Orientation Detection
PP-LCNet
│
▼
Rotate
│
▼
Enhancement
ImageMagick
│
▼
Processed Image
Ada beberapa keputusan yang saya buat di setiap tahap.
1. Memeriksa Image Sebelum Diproses
Request masuk melalui:
POST /preprocess
Service menerima image melalui multipart/form-data.
Sebelum masuk ke model, saya terlebih dahulu mengambil informasi dasar image menggunakan ImageMagick identify.
Tujuannya untuk mengetahui ukuran image dan memastikan input yang diterima memang bisa diproses.
Selain itu, service juga memiliki guard untuk membatasi concurrency.
Saya menggunakan batas concurrency 6 request.
Kalau semua request langsung menjalankan inference dan ImageMagick secara bersamaan tanpa pembatasan, penggunaan CPU dan proses subprocess bisa meningkat cukup cepat.
Jika slot penuh, service dapat mengembalikan:
503 Server is busy
Retry-After: 5
Dengan cara ini, service tidak membiarkan request masuk tanpa batas.
2. YOLO untuk Mencari Dokumen
Tahap utama pertama adalah document detection menggunakan:
document-yolo.onnx
Image di-resize secara paksa menjadi:
640 × 640
kemudian diubah menjadi tensor RGB yang sudah dinormalisasi.
Tujuan YOLO di sini bukan membaca teks.
YOLO hanya menjawab pertanyaan:
“Di mana dokumennya?”
Misalnya image berisi:
┌──────────────────────────────┐
│ │
│ Background │
│ │
│ ┌───────────────┐ │
│ │ DOCUMENT │ │
│ │ │ │
│ └───────────────┘ │
│ │
└──────────────────────────────┘
Yang saya butuhkan adalah bounding box dokumennya.
Output detection kemudian difilter menggunakan beberapa aturan:
- confidence ≥ 0.35
- NMS IoU 0.45
- bounding box minimal sekitar 8% dari ukuran image
- maksimal 12 dokumen
Detection juga diurutkan dari posisi atas ke bawah dan kiri ke kanan.
Dengan begitu, jika terdapat beberapa dokumen dalam satu foto, hasilnya memiliki urutan yang konsisten.
3. Tidak Menemukan Dokumen? Gunakan Text Detection
Saya tidak ingin pipeline langsung berhenti ketika YOLO tidak menemukan dokumen.
Karena itu saya menambahkan fallback menggunakan:
pp-ocrv5-mobile-det.onnx
Model ini digunakan untuk mencari area yang mengandung teks.
Berbeda dengan YOLO, model ini tidak mencari “dokumen”.
Ia mencari area teks yang bisa digunakan sebagai indikasi bahwa di area tersebut terdapat informasi yang kemungkinan perlu dibaca.
Image di-resize menjadi:
960 × 960
Kemudian bounding box teks digunakan sebagai textFocus.
Jika area teks yang ditemukan terlalu kecil, misalnya kurang dari:
- 5% lebar image
- 3% tinggi image
maka hasilnya dianggap tidak valid.
Jadi fallback ini hanya digunakan ketika:
YOLO
↓
Tidak menemukan dokumen
↓
PP-OCRv5 Text Detection
↓
Text Focus
Ini penting karena PP-OCRv5 bukan selalu menjadi tahap utama.
Ia hanya menjadi fallback ketika document detection tidak menemukan target.
4. Crop dan Padding
Setelah mendapatkan bounding box, area dokumen kemudian di-crop.
Saya juga menambahkan padding sekitar 2.5%.
Tujuannya supaya crop tidak terlalu mepet dengan batas bounding box.
Secara sederhana:
Bounding Box
┌──────────────┐
│ DOCUMENT │
└──────────────┘
menjadi:
Padding
┌────────────────┐
│ ┌──────────┐ │
│ │ DOCUMENT │ │
│ └──────────┘ │
└────────────────┘
Hal kecil seperti ini cukup penting karena bounding box hasil detection tidak selalu tepat sampai ke batas sebenarnya dari dokumen.
5. Menentukan Orientasi Dokumen
Setelah crop, image belum tentu berada dalam posisi tegak.
Untuk itu saya menggunakan:
pp-lcnet-doc-ori.onnx
Crop di-resize menjadi:
224 × 224
Model kemudian melakukan klasifikasi ke empat kelas:
0°
90°
180°
270°
Misalnya hasilnya:
orientation = 90°
maka image akan diputar hingga kembali ke posisi normal.
Jadi model ini tidak bertugas membaca teks.
Tugasnya hanya menentukan:
Image ini harus diputar berapa derajat?
6. Image Enhancement Menggunakan ImageMagick
Setelah posisi dokumen benar, image masuk ke tahap enhancement.
Di sini saya tidak menggunakan machine learning.
Saya menggunakan ImageMagick.
Pipeline enhancement yang digunakan antara lain:
deskew 40%
↓
resize 1800x1800>
↓
contrast-stretch 0.25%x0.1%
↓
sigmoidal-contrast 4.5x45%
↓
unsharp 0x1.25+1.6+0.008
↓
sharpen 0x0.65
Tujuannya adalah membuat image lebih siap untuk tahap OCR.
Deskew
Membantu meluruskan image yang sedikit miring.
Resize
Image yang terlalu besar diperkecil agar tidak diproses dalam ukuran yang berlebihan.
Contrast
Membantu memperjelas perbedaan antara teks dan background.
Unsharp dan Sharpen
Digunakan untuk membantu mempertajam detail teks.
Tetapi saya juga menemukan bahwa enhancement tidak boleh dilakukan secara berlebihan.
Misalnya terlalu banyak sharpening atau contrast enhancement justru dapat menghasilkan artefak.
Jadi prinsip yang saya gunakan bukan:
Semakin banyak preprocessing, semakin bagus.
Tetapi:
Lakukan preprocessing secukupnya untuk membuat image lebih mudah dibaca.
Perbandingan nyatanya kurang lebih seperti ini. Yang pertama contoh hasil OCR sebelum preprocessing, di mana dokumen masih mengandung banyak noise dan miring:

Dan yang kedua hasil setelah melewati pipeline image preprocessing: dokumen sudah lurus, bersih, dan jauh lebih mudah dibaca:

7. Kalau Ada Banyak Dokumen
Ada kemungkinan satu image berisi lebih dari satu dokumen.
Misalnya:
Document 1
Document 2
Document 3
Daripada mengembalikan banyak image secara terpisah, hasil crop kemudian digabungkan secara vertikal.
┌──────────────────┐
│ Document 1 │
└──────────────────┘
┌──────────────────┐
│ Document 2 │
└──────────────────┘
┌──────────────────┐
│ Document 3 │
└──────────────────┘
Background yang digunakan adalah putih.
Untuk satu dokumen, image langsung dikembalikan.
Untuk lebih dari satu dokumen, ImageMagick melakukan proses append secara vertikal.
8. Output Tidak Hanya Image
Service preprocessing mengembalikan PNG sebagai output utama.
Tetapi saya juga mengirimkan metadata melalui response header.
Contohnya:
X-Cropped
X-Orientation-Angle
X-Detector
X-Document-Count
X-Deskew-Angle
X-Detection-Status
Misalnya:
X-Cropped: true
X-Orientation-Angle: 90
X-Detector: yolo
X-Document-Count: 1
Metadata ini sangat membantu ketika debugging.
Kalau hasil OCR ternyata salah, saya bisa melihat apakah image sebelumnya berhasil di-crop, diputar, dan diproses sesuai yang diharapkan.
9. Dari Preprocessed Image ke Structured JSON
Setelah preprocessing selesai, image tersebut kemudian diteruskan ke tahap OCR atau vision model.
Di project saya, preprocessing bukan tahap terakhir.
Flow yang lebih lengkap adalah:
Raw Image
↓
Preprocessing
↓
OCR / Vision Model
↓
Extract Information
↓
Structured JSON
Misalnya dari sebuah dokumen, model perlu mengambil beberapa field:
{
"reference_number": "POD-20260831-001",
"container_number": "MSCU1234567",
"driver_name": "Budi Santoso",
"document_date": "2026-08-31",
"destination": "Jakarta"
}
Format seperti ini jauh lebih berguna dibandingkan hanya mendapatkan hasil OCR berupa text panjang.
Karena data tersebut sudah memiliki struktur yang bisa langsung diproses oleh sistem.
10. JSON Kemudian Di-mapping ke Database
Pipeline saya dijalankan menggunakan n8n sebagai orchestrator.
Kurang lebih alurnya:
Database
↓
n8n
↓
Ambil Document
↓
Preprocessing API
↓
OCR / Vision
↓
Structured JSON
↓
Mapping
↓
Database
Contohnya:
{
"reference_number": "POD-20260831-001",
"container_number": "MSCU1234567",
"driver_name": "Budi Santoso",
"document_date": "2026-08-31"
}
Kemudian n8n melakukan mapping:
reference_number → reference_number
container_number → container_number
driver_name → driver_name
document_date → document_date
dan hasil akhirnya disimpan ke database.
Di sinilah preprocessing menjadi bagian dari sistem yang lebih besar.
Tujuan akhirnya bukan sekadar:
Image → Image
tetapi:
Image
↓
Better Image
↓
AI
↓
Structured JSON
↓
Database
11. Kenapa n8n dan Preprocessing Saya Pisahkan?
Saya menggunakan n8n untuk menjalankan workflow, bukan untuk menangani seluruh logic computer vision.
Preprocessing saya deploy sebagai API service tersendiri.
Pembagiannya kira-kira seperti ini:
n8n
│
│ HTTP
▼
┌───────────────────┐
│ Preprocessing API │
├───────────────────┤
│ YOLO │
│ PP-OCRv5 │
│ PP-LCNet │
│ ImageMagick │
└───────────────────┘
│
▼
Processed Image
│
▼
OCR / Vision
│
▼
Structured JSON
│
▼
Database
Dengan cara ini, workflow n8n tetap relatif sederhana.
Kalau saya ingin mengubah algoritma crop, menambahkan deskew, mengganti model orientation, atau mengubah enhancement, saya cukup mengubah preprocessing service.
Workflow utamanya tidak perlu ikut berubah.
12. Performa: Ternyata ImageMagick Cukup Berpengaruh
Satu hal menarik yang saya temukan setelah mengukur pipeline adalah bahwa inference model bukan satu-satunya sumber waktu.
Ketiga model ONNX:
document-yolo.onnx
pp-ocrv5-mobile-det.onnx
pp-lcnet-doc-ori.onnx
berjalan menggunakan CPU melalui onnxruntime-node.
Selain itu, pipeline cukup sering menjalankan ImageMagick melalui subprocess.
Beberapa operasi bahkan membutuhkan pola:
create temporary directory
↓
write image
↓
run magick
↓
read result
↓
delete temporary files
Untuk satu dokumen, berdasarkan pengukuran pipeline, proporsi waktunya kira-kira:
YOLO 28%
Orientation 14%
ImageMagick 42%
HTTP + temp I/O 16%
Yang cukup mengejutkan adalah ImageMagick justru mengambil porsi terbesar.
Artinya, ketika melakukan optimasi, saya tidak bisa hanya melihat waktu inference model.
Saya juga harus melihat:
- jumlah subprocess
- temporary file
- disk I/O
- jumlah transformasi image
- concurrency
- ukuran image
Ini menjadi salah satu pelajaran penting dari project ini.
13. Concurrency Juga Berpengaruh
Karena sebagian besar preprocessing berjalan di CPU, menjalankan beberapa request secara paralel dapat membantu meningkatkan throughput.
Saya menggunakan guard concurrency dengan batas sekitar:
MAX_CONCURRENT_PREPROCESS = 6
Dengan demikian service tidak menerima pekerjaan tanpa batas.
Jika semua slot sedang digunakan, request berikutnya dapat menerima:
503 Server is busy
Retry-After: 5
Ini lebih aman daripada membiarkan terlalu banyak request masuk dan akhirnya membuat server kehabisan resource.
Apa yang Sebenarnya Saya Pelajari dari Project Ini?
Project ini awalnya saya mulai karena ingin mendapatkan hasil OCR yang lebih baik.
Tetapi akhirnya saya justru belajar bahwa OCR bukan hanya masalah model.
Ada banyak hal yang terjadi sebelum model membaca teks.
Image Quality
↓
Document Detection
↓
Cropping
↓
Orientation
↓
Deskew
↓
Enhancement
↓
OCR / Vision
↓
Extraction
↓
JSON
↓
Database
Setiap tahap mempunyai tanggung jawab sendiri.
Dan tidak semuanya harus menggunakan AI.
YOLO digunakan untuk mengetahui posisi dokumen.
PP-LCNet digunakan untuk mengetahui orientasi.
PP-OCRv5 digunakan sebagai fallback ketika dokumen tidak ditemukan.
Sementara crop, rotate, deskew, contrast, dan sharpening dilakukan menggunakan image processing.
Menurut saya, kombinasi ini justru membuat pipeline menjadi lebih fleksibel.
Kesimpulan
Hal yang paling saya ubah dari cara berpikir saya adalah:
Jangan langsung menyalahkan model OCR ketika hasilnya buruk.
Periksa dulu image yang diberikan kepadanya.
Apakah dokumennya sudah ditemukan?
Apakah crop-nya benar?
Apakah orientasinya sudah benar?
Apakah image terlalu besar?
Apakah dokumennya miring?
Apakah contrast dan ketajaman cukup?
Setelah image dipersiapkan dengan baik, barulah OCR atau vision model melakukan tugasnya.
Pada akhirnya pipeline yang saya bangun bukan sekadar:
Image → OCR
tetapi:
┌─ Document Detection
│
Raw Image ────────┼─ Orientation
│
├─ Image Enhancement
│
▼
Preprocessed Image
│
▼
OCR / AI
│
▼
Structured JSON
│
▼
Mapping
│
▼
Database
Dan mungkin itu pelajaran paling penting dari project ini:
Model yang bagus membutuhkan input yang baik.
Image preprocessing bukan sekadar tahap tambahan sebelum OCR.
Dalam pipeline yang saya bangun, preprocessing menjadi bagian penting untuk menjembatani raw image dengan data terstruktur yang akhirnya disimpan ke database.



