2025, Sep 26 07:34

PDFium+cv2 के साथ PDF पेज और इमेज को numpy पर मिलाने की गाइड

PDF रेंडरिंग बनाम इमेज डिकोडिंग: PDFium से उच्च DPI पर रैस्टराइज़ करें, BGRA→RGB करें, और cv2 से एक समान रीसाइज़ कर numpy पर पिक्सेल-स्तर तुलना पाएं.

जब आप किसी PDF पेज को numpy array में रेंडर करते हैं और वही काम उस छवि के साथ करते हैं जिसमें बिल्कुल वही कंटेंट हो, तो स्वाभाविक है कि आप पिक्सेल-स्तर पर एकदम बराबरी की उम्मीद करें। लेकिन व्यवहार में, अगर दोनों पाइपलाइन सच में संरेखित नहीं हैं, तो एरेज़ में काफी अंतर दिख सकता है। यह गाइड बताती है कि असंगति कहां से आती है और PDFium छोड़े बिना दोनों रास्तों को तुलनीय कैसे बनाया जाए।

बेसलाइन: PDF पेज रेंडर करना और इमेज लोड करना

PDF वाला मार्ग PDFium का उपयोग करके पेज को BGRA बफ़र में रैस्टराइज़ करता है, जिसे बाद में numpy array के रूप में एक्सपोज़ किया जाता है। इमेज वाला मार्ग stb_image से डिकोड करता है, रीसाइज़ करता है, और अल्फा हटाकर RGB बनाता है।

py::array_t<uint8_t> page_to_ndarray(FPDF_PAGE pdf_page,
                                      int out_w = 0,
                                      int out_h = 0,
                                      int render_dpi = 80) {
    int px_w, px_h;
    if (out_w > 0 && out_h > 0) {
        px_w = out_w;
        px_h = out_h;
    } else {
        px_w = static_cast<int>(FPDF_GetPageWidth(pdf_page) * render_dpi / 72.0);
        px_h = static_cast<int>(FPDF_GetPageHeight(pdf_page) * render_dpi / 72.0);
    }
    FPDF_BITMAP bmp = FPDFBitmap_Create(px_w, px_h, 1);
    if (!bmp) throw std::runtime_error("Failed to create bitmap");
    FPDFBitmap_FillRect(bmp, 0, 0, px_w, px_h, 0xFFFFFFFF);
    FPDF_RenderPageBitmap(bmp, pdf_page, 0, 0, px_w, px_h, 0, FPDF_ANNOT);
    int row_stride = FPDFBitmap_GetStride(bmp);
    uint8_t* raw_ptr = static_cast<uint8_t*>(FPDFBitmap_GetBuffer(bmp));
    auto out = py::array_t<uint8_t>({px_h, px_w, 4}, raw_ptr); // BGRA
    FPDFBitmap_Destroy(bmp);
    return out;
}

Python में, BGRA आउटपुट को अल्फा हटाकर और चैनल क्रम बदलकर RGB बनाया जाता है।

rgb_from_bgra = bgra_view[:, :, [2, 1, 0]]

इमेज वाला मार्ग stb_image और stb_image_resize का इस्तेमाल करता है ताकि RGBA को फोर्स किया जाए, लक्ष्य रेज़ोल्यूशन तक रीसाइज़ किया जाए, और फिर RGB में बदला जाए।

py::array_t<uint8_t> load_image_rgb(const std::string& file_path,
                                   int out_w = 224,
                                   int out_h = 224) {
    int src_w, src_h, src_c;
    unsigned char* rgba_mem = stbi_load(file_path.c_str(), &src_w, &src_h, &src_c, 4);
    if (!rgba_mem) throw std::runtime_error("Failed to load image");
    std::vector<uint8_t> tmp(out_w * out_h * 4);
    stbir_resize_uint8(rgba_mem, src_w, src_h, 0,
                       tmp.data(), out_w, out_h, 0, 4);
    stbi_image_free(rgba_mem);
    py::array_t<uint8_t> rgb({out_h, out_w, 3});
    auto view = rgb.mutable_unchecked<3>();
    for (int yy = 0; yy < out_h; ++yy) {
        for (int xx = 0; xx < out_w; ++xx) {
            int p = (yy * out_w + xx) * 4;
            view(yy, xx, 0) = tmp[p + 0]; // लाल
            view(yy, xx, 1) = tmp[p + 1]; // हरा
            view(yy, xx, 2) = tmp[p + 2]; // नीला
        }
    }
    return rgb;
}

एरेज़ मेल क्यों नहीं खाते

अंतर इसलिए आता है क्योंकि दोनों तरफ मूल रूप से अलग प्रक्रियाएं चल रही हैं। PDF पेज को सीधे किसी तय आकार (जैसे 224×224) पर रेंडर किया गया ताकि गति बनी रहे, जबकि इमेज को पहले उसकी सोर्स रेज़ोल्यूशन पर डिकोड किया गया और बाद में डाउनस्केल किया गया। किसी लक्ष्य आकार पर PDF रेंडर करना और ऊंचे dpi पर रैस्टराइज़ करके फिर डाउनस्केल करना एक जैसी प्रक्रियाएं नहीं हैं। नतीजा: भले ही दृश्य सामग्री समान हो, पिक्सेल-स्तर के मान अलग निकलते हैं।

पाइपलाइन को तुलनीय बनाएं

परिणामों को पास लाने का सबसे विश्वसनीय तरीका है रीसाइज़िंग रणनीति को एक जैसा करना। PDF को ऊंचे dpi पर रेंडर करें, फिर उसी रिसाइज़र से डाउनस्केल करें जिसे आप इमेज के लिए इस्तेमाल करते हैं। आज़माए गए विकल्पों में, cv2 से एरेज़ को रीसाइज़ करना यूक्लिडियन दूरी के हिसाब से सबसे नज़दीकी मेल देता है, और इसका व्यावहारिक फायदा यह है कि यह सीधे numpy arrays पर काम करता है। Pillow और कस्टम C++ रीसाइज़ उससे पीछे रहे।

यह सब कुछ बिल्कुल एक जैसा नहीं बना देगा। कंटेंट समान होने पर भी दूरी बिल्कुल शून्य नहीं होगी; PDF रेंडर dpi बढ़ाने पर यह कम होती जाती है। प्रदर्शन और शुद्धता के बीच समझौता है, इसलिए आपको अपनी सीमाओं के हिसाब से dpi चुनना होगा।

cv2 के साथ एकीकृत रीसाइज़िंग पाथ

लक्ष्य सीधा है: PDF को पर्याप्त ऊंचे dpi पर रेंडर करें, BGRA को RGB में बदलें, और cv2 से रीसाइज़ करें। इमेज एरे पर भी यही करें ताकि दोनों एक ही numpy→cv2 पथ से गुजरें।

एक व्यवहारिक बदलाव यह है कि इमेज को C++ में रीसाइज़ करने से बचें और PDF की तरह यह काम cv2 को करने दें। यहां एक संशोधित इमेज लोडर है जो सोर्स रेज़ोल्यूशन पर RGB लौटाता है:

py::array_t<uint8_t> read_image_as_rgb(const std::string& file_path) {
    int iw, ih, ic;
    unsigned char* rgba_buf = stbi_load(file_path.c_str(), &iw, &ih, &ic, 4);
    if (!rgba_buf) throw std::runtime_error("Failed to load image");
    py::array_t<uint8_t> rgb({ih, iw, 3});
    auto dst = rgb.mutable_unchecked<3>();
    for (int y = 0; y < ih; ++y) {
        for (int x = 0; x < iw; ++x) {
            int k = (y * iw + x) * 4;
            dst(y, x, 0) = rgba_buf[k + 0]; // लाल
            dst(y, x, 1) = rgba_buf[k + 1]; // हरा
            dst(y, x, 2) = rgba_buf[k + 2]; // नीला
        }
    }
    stbi_image_free(rgba_buf);
    return rgb;
}

जब दोनों स्रोत numpy RGB arrays हों, तो Python साइड पर cv2 के साथ रीसाइज़िंग बिल्कुल एक जैसी रखी जा सकती है:

import cv2
# PDF: उच्च dpi पर रेंडर करें, फिर BGRA -> RGB में बदलें
pdf_bgra = page_to_ndarray(pdf_page, out_w=0, out_h=0, render_dpi=some_dpi)
pdf_rgb = pdf_bgra[:, :, [2, 1, 0]]
pdf_resized = cv2.resize(pdf_rgb, (224, 224))
# Image: स्रोत आकार पर RGB में डिकोड करें, फिर वही cv2 रिसाइज़र इस्तेमाल करें
img_rgb = read_image_as_rgb(path_to_image)
img_resized = cv2.resize(img_rgb, (224, 224))

इससे दोनों आउटपुट संख्यात्मक रूप से काफी नज़दीक आ जाते हैं। यूक्लिडियन दूरी के आधार पर, आज़माए गए विकल्पों में cv2 ने सबसे बेहतर मेल दिया, जबकि Pillow और कस्टम C++ इमेज पाथ उससे पीछे रहे।

यह क्यों मायने रखता है

कई डाउनस्ट्रीम कार्य मान लेते हैं कि समान कंटेंट वाले PDF रेंडर और रैस्टर इमेज पिक्सेल-दर-पिक्सेल मेल खाएंगे। वास्तव में, रैस्टराइज़ेशन की रणनीतियां फर्क डालती हैं। कब और कैसे आप रीसाइज़ करते हैं, उसे संरेखित करने से आपकी तुलना सार्थक बनती है और आपके मेट्रिक्स पाइपलाइन कलाकृतियों के बजाय कंटेंट को दर्शाते हैं। अगर सख्त संख्यात्मक समानता अहम है, तो PDF रेंडर के लिए चुना गया dpi प्रत्यक्ष असर डालता है, इसलिए ट्यून करना फायदेमंद है।

निष्कर्ष

किसी लक्ष्य आकार पर सीधे PDF रेंडर करना, ऊंचे dpi पर रैस्टराइज़ करके बाद में डाउनस्केल करने के बराबर नहीं है, और केवल यही फर्क “बहुत अलग एरेज़” पैदा कर सकता है। पाइपलाइन को एक जैसा करें: PDF को ऊंचे dpi पर रेंडर करें, BGRA को RGB में बदलें, और cv2 से रीसाइज़ करें। इमेज के साथ भी यही करें ताकि दोनों एक ही numpy→cv2 पाथ से गुजरें। नतीजे बहुत नज़दीक होंगे, बिल्कुल समान नहीं; इसलिए अपने वर्कलोड के हिसाब से प्रदर्शन और शुद्धता का संतुलन साधते हुए dpi चुनें।

यह लेख StackOverflow पर मौजूद प्रश्न और उसके उत्तर पर आधारित है, दोनों Something Something द्वारा।