Spelt / ZXing.Delphi

ZXing Barcode Scanning object Pascal Library for Delphi VCL and Delphi Firemonkey
Apache License 2.0
471 stars 206 forks source link

Is ZXing reentrant? #155

Closed gcardi closed 1 year ago

gcardi commented 1 year ago

I am trying to use ZXing by analyzing the image from a webcam.

I realized that it does QR-Code recognition when they are black on white. If the background is black and the code is white, it seems it does not work. However, the white-on-black code works by inverting the bitmap (doing the negative).

So, first I try with the positive image and if the recognizer comes back an empty string then I try with the negative image. That way I get the desired result.

Now, I've tried to do the two operations in sequence and it works. Then, I tried distributing them on two different threads by making a negative copy of the image for one thread, but I got a memory error.

I'm on FMX using bcc64/bcc32c.

So, I'm wondering if the library is reentrant or not.

Thanks in advance.

Spelt commented 1 year ago

Do you create 2 different zxing objects with 2 threads or one zxing object with 2 different scans in different threads ?Op 16 jul. 2023 om 12:40 heeft Giuliano Cardinali @.***> het volgende geschreven: I am trying to use ZXing by analyzing the image from a webcam. I realized that it does QR-Code recognition when they are black on white. If the background is black and the code is white, it seems it does not work. However, the white-on-black code works by inverting the bitmap (doing the negative). So, first I try with the positive image and if the recognizer comes back an empty string then I try with the negative image. That way I get the desired result. Now, I've tried to do the two operations in sequence and it works. Then, I tried distributing them on two different threads by making a negative copy of the image for one thread, but I got a memory error. I'm on FMX using bcc64/bcc32c. So, I'm wondering if the library is reentrant or not. Thanks in advance.

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you are subscribed to this thread.Message ID: @.***>

gcardi commented 1 year ago

I create the TScanManager objects on the fly and on per thread basis. So there's more tha one instance at the same time.

Here a fragment of my code:

On receiving a frame (bmp_ member is a smart pointer containig a Bitmap filled with the image from FMX TCameraComponent) I run this code.

    ...
    // This code fragment is in a worker-thread (std::thread) 
    // which receives the frames from the camera

    // With std::launch::async it crash because 
    // ZXing for Delphi seems not to be re-entrant
    auto LaunchPolicy = std::launch::deferred;

    // Launches the two parsing operations 
    // (it would be nice to be able to launch them asynchronously)
    auto Inverse =
        std::async(
            LaunchPolicy,
            [this]() -> String {
                auto Bmp = make_unique<Fmx::Graphics::TBitmap>();
                Bmp->Assign( bmp_.get() );
                InvertEffect1->ProcessEffect( Bmp->Canvas, Bmp.get(), {} );
                return ParseImage( *Bmp );
            }
        );
    auto Normal =
        std::async(
            LaunchPolicy,
            [this]() -> String {
                return ParseImage( *bmp_ );
            }
        );

    // Waits for the results of the previous two parsing operations    
    auto NormText = Normal.get();
    auto InvText = Inverse.get();

    // Use NormText or InvText...
    ...

The ParseImage member function

String TfrmMain::ParseImage( TBitmap& ScanBitmap )
{
    // Create a local (non-static) TScanManager instance
    auto ScanManager =
        make_unique<TScanManager>(
            TBarcodeFormat::Auto,
            nullptr
        );

    // Parse image
    if ( auto ReadResult = ScanManager->Scan( &ScanBitmap ) ) {
        // Text found
        return ReadResult->text;
    }
    else {
        // No text found
        return {};
    }
}

If I change the launch policy from std::launch::deferred to std::launch::async the code breaks with an error on a TStringBuilder object within the ZXing code (I will show the call stack later).

In case you are unfamiliar with C++'s std::async, suffice it to say that it executes the function (a lambda in my case) asynchronously, potentially in a separate thread that might be part of a thread pool, and returns a std::future that will eventually contain the result of the function call. Using std::launch::async tries to concurrently execute the associated function. If you use std::launch::deferred, it executes the function are at the time of taking the result from std::future. Thus, in the last case, the execution of ScanManager->Scan( &ScanBitmap ) is definitely sequential and not parallel. In fact, with std::launch::deferred the application does not crash.

Here is the stack trace after the crash:

:00D6A6C6 System::Sysutils::TStringBuilder::SetLength(Self=NULL, Value=0)
:013fd86c ; decodeRow
:013fbb9d ; doDecode
:013fb877 ; decode
:0144cccd ; DecodeInternal
:0144c72d Zxing::Multiformatreader::TMultiFormatReader::decode + 0x5
:00C507EC TfrmMain::ParseImage(this=:03D42D60, ScanBitmap=:0BC71730)
:00C482AD TfrmMain::StartImgProcThread(this=:03E17764)

This is the error text from debugger:

First chance exception at $00D6A6C6. Exception class $C0000005 with message 'access violation at 0x00d6a6c6: read of address 0x0000000c'. Process CodeScannerG.exe (11684)

If you need further details or clarification, don't hesitate to ask.

Thanks.

Giuliano

gcardi commented 1 year ago

Ah, it's systematic.

Spelt commented 1 year ago

What do you mean with Systematic?That bitmap is not a copy, maybe thats a problem. I never intended to use it with multi instances because the goal was to get barcode scanner for Delphi for mobile devices and it was already fast. That being said. Multi threading is interesting. About inversion of the image. There is a flag:      ENABLE_INVERSION. Is this is something that can help you for your initial problem?Op 16 jul. 2023 om 16:00 heeft Giuliano Cardinali @.***> het volgende geschreven: Ah, it's systematic.

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you commented.Message ID: @.***>

gcardi commented 1 year ago

By systematic, I mean that it always happens immediately if I enable multitasking.

The bitmaps are 2 separate copies. The first one is contained in the instance of the main class. The second is a copy of the previous bitmap. So yes, they are two copies, the life of which is managed by smart pointers.

But you gave me a suggestion to do an experiment. I actually make a copy of the original bitmap in one of the two tasks (using Assign). Then this bitmap is inverted (made negative) and submitted to an instance of the recognizer. Naively I am assuming that the bitmap I feed to the recognizers is not modified by them (are just read). Assuming this is not the case, I could make the bitmap copy before launching the two tasks. I will try that shortly, just for the sake of curiosity.

ENABLE_INVERSION is a very interesting flag. I am not very good with Delphi, where should I define/enable it? Do I have to recompile the library or is it a flag that you pass at runtime maybe in construction?

However, your ZXing porting for Delphi has saved me a lot of headaches. I tried to compile the ZXing porting for C++ but the Embarcadero compilers fail: they fail with handling local thread storage.

But I'm lucky: C++ Builder nicely accepts Delphi Units, so I can use the best of both worlds.

Yes, your library is very fast. My intent is to maximize the utilization of the cores of modern CPUs by paralyzing tasks as much as possible. But it's great even so. With 800x600 bitmaps on an old 3.2 GHz Intel Haswell i5 4570 taking one frame out of four and doing recognition twice (positive and negative) it eats at most 5% CPU.

Good work.

Regards.

gcardi commented 1 year ago

Unfortunately, even by making a copy of the bitmap to be inverted in the context of the thread that then launches the two recognition tasks, the memory error still occurs.

void TfrmMain::StartImgProcThread()
{
    detectorState_ = DetState::Idle;
    thImgProc_ =
        thread(
            [this](){
                try {
                    while ( !imgProcStopReq_ ) {
                        bool Timeout { false };

                        unique_lock<mutex> Lock( mtxImgProc_ );
                        cvImgProc_.wait( Lock );

                        if ( imgProcStopReq_.load() ) {
                            // Thread exit
                            break;
                        }

                        // Con std::launch::async si spacca tutto perché
                        // ZXing per Delphi pare non essere rientrante
                        auto LaunchPolicy = std::launch::async;

                        // Makes a copy of the original bitmap then invert it
                        auto Bmp = make_unique<Fmx::Graphics::TBitmap>();
                        Bmp->Assign( bmp_.get() );
                        InvertEffect1->ProcessEffect( Bmp->Canvas, Bmp.get(), {} );

                        auto Inverse =
                            std::async(
                                LaunchPolicy,
                                [this,&Bmp]() -> String {
                                    //auto Bmp = make_unique<Fmx::Graphics::TBitmap>();
                                    //Bmp->Assign( bmp_.get() );
                                    //InvertEffect1->ProcessEffect( Bmp->Canvas, Bmp.get(), {} );
                                    return ParseImage( *Bmp );
                                }
                            );
                        auto Normal =
                            std::async(
                                LaunchPolicy,
                                [this]() -> String {
                                    return ParseImage( *bmp_ );
                                }
                            );
                        auto NormText = Normal.get();
                        auto InvText = Inverse.get();

                        if ( NormText == InvText ) {
                            SendText( NormText );
                        }
                        else if ( InvText.IsEmpty() ) {
                            SendText( NormText );
                        }
                        else {
                            SendText( InvText );
                        }

                        bmp_.reset();

                        scanInProgress_ = false;
                    }
                }
                catch ( Exception const & E ) {
//::OutputDebugString( E.Message.c_str() );
                }
                catch ( std::exception const & e ) {
//::OutputDebugStringA( e.what() );
                }
                catch ( ... ) {
//::OutputDebugString( _D( "Unkn" ) );
                }
            }
        );
}

Thanks anyway for the support.

gcardi commented 1 year ago

Hi!

I just tried the ENABLE_INVERSION flag and it works like a charm:

String TfrmMain::ParseImage( TBitmap& ScanBitmap )
{
    using HintDict = 
        TDictionary__2<Zxing::Decodehinttype::TDecodeHintType,System::TObject*>;

    auto Hints = make_unique<HintDict>();
    Hints->Add( ENABLE_INVERSION, nullptr );

    auto ScanManager =
        make_unique<TScanManager>(
            TBarcodeFormat::Auto,
            Hints.release()
        );

    if ( auto ReadResult = ScanManager->Scan( &ScanBitmap ) ) {
        return ReadResult->text;
    }
    else {
        return {};
    }
}

I removed the code executed in parallel because it no longer has any reason to exist. CPU consumption dropped by an additional 2%.

Thank you very much for the suggestion.

Have a good day.

Giuliano

Spelt commented 1 year ago

Awesome.

On Tue, 18 Jul 2023 at 11:12, Giuliano Cardinali @.***> wrote:

Hi!

I just tried the ENABLE_INVERSION flag and it works like a charm:

String TfrmMain::ParseImage( TBitmap& ScanBitmap ) { using HintDict = TDictionary__2<Zxing::Decodehinttype::TDecodeHintType,System::TObject*>;

auto Hints = make_unique(); Hints->Add( ENABLE_INVERSION, nullptr );

auto ScanManager =
    make_unique<TScanManager>(
        TBarcodeFormat::Auto,
        Hints.release()
    );

if ( auto ReadResult = ScanManager->Scan( &ScanBitmap ) ) {
    return ReadResult->text;
}
else {
    return {};
}

}

I removed the code executed in parallel because it no longer has any reason to exist. CPU consumption dropped by an additional 2%.

Thank you very much for the suggestion.

Have a good day.

Giuliano

— Reply to this email directly, view it on GitHub https://github.com/Spelt/ZXing.Delphi/issues/155#issuecomment-1639841868, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADAS3VMEHQB4VEFACLFY2SLXQZHQXANCNFSM6AAAAAA2L3RQEA . You are receiving this because you commented.Message ID: <Spelt/ZXing. @.***>