From 3ee49a60a382b7fc8e97e5cf7796620efb950ef6 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 7 Apr 2021 10:28:02 +1000 Subject: [PATCH] [pal] Improvements - Improve memory safety and clarify ownership - Try harder to merge connected features, to better handle situations like T intersections or roads which divide into two lanes - Fix labels are sometimes placed in conflict with line features when the conflicting line has the same label yet we couldn't successfully merge the two line parts --- src/core/pal/labelposition.h | 6 +- src/core/pal/layer.cpp | 88 ++++++++++-------- src/core/pal/layer.h | 4 +- src/core/pal/pal.cpp | 8 +- src/core/pal/pal.h | 4 +- tests/src/core/testqgslabelingengine.cpp | 7 +- .../expected_label_merged_minimum_size.png | Bin 2211 -> 2436 bytes ...cted_label_multipart_touching_branches.png | Bin 2990 -> 3389 bytes ...label_multipart_touching_branches_mask.png | Bin 3869 -> 0 bytes 9 files changed, 65 insertions(+), 52 deletions(-) delete mode 100644 tests/testdata/control_images/labelingengine/expected_label_multipart_touching_branches/expected_label_multipart_touching_branches_mask.png diff --git a/src/core/pal/labelposition.h b/src/core/pal/labelposition.h index ae04f649e4fa..15a6bc188c0b 100644 --- a/src/core/pal/labelposition.h +++ b/src/core/pal/labelposition.h @@ -326,7 +326,7 @@ namespace pal * * \see setGlobalId() */ - long long globalId() const { return mGlobalId; } + unsigned int globalId() const { return mGlobalId; } /** * Sets the global \a id for the candidate, which is unique for a single run of the pal @@ -334,7 +334,7 @@ namespace pal * * \see globalId() */ - void setGlobalId( long long id ) { mGlobalId = id; } + void setGlobalId( unsigned int id ) { mGlobalId = id; } protected: @@ -364,7 +364,7 @@ namespace pal private: - long long mGlobalId = 0; + unsigned int mGlobalId = 0; std::unique_ptr< LabelPosition > mNextPart; double mCost; diff --git a/src/core/pal/layer.cpp b/src/core/pal/layer.cpp index 76294c915f2c..a779f75b3adf 100644 --- a/src/core/pal/layer.cpp +++ b/src/core/pal/layer.cpp @@ -66,7 +66,6 @@ Layer::~Layer() { mMutex.lock(); - qDeleteAll( mFeatureParts ); qDeleteAll( mObstacleParts ); mMutex.unlock(); @@ -104,7 +103,7 @@ bool Layer::registerFeature( QgsLabelFeature *lf ) bool addedFeature = false; double geom_size = -1, biggest_size = -1; - std::unique_ptr biggest_part; + std::unique_ptr biggestPart; // break the (possibly multi-part) geometry into simple geometries std::unique_ptr> simpleGeometries( Util::unmulti( lf->geometry() ) ); @@ -183,14 +182,14 @@ bool Layer::registerFeature( QgsLabelFeature *lf ) if ( geom_size > biggest_size ) { biggest_size = geom_size; - biggest_part.reset( fpart.release() ); + biggestPart = std::move( fpart ); } // don't add the feature part now, do it later } else { // feature part is ready! - addFeaturePart( fpart.release(), lf->labelText() ); + addFeaturePart( std::move( fpart ), lf->labelText() ); addedFeature = true; } } @@ -249,9 +248,9 @@ bool Layer::registerFeature( QgsLabelFeature *lf ) locker.unlock(); // if using only biggest parts... - if ( ( !lf->labelAllParts() || lf->hasFixedPosition() ) && biggest_part ) + if ( ( !lf->labelAllParts() || lf->hasFixedPosition() ) && biggestPart ) { - addFeaturePart( biggest_part.release(), lf->labelText() ); + addFeaturePart( std::move( biggestPart ), lf->labelText() ); addedFeature = true; } @@ -265,16 +264,16 @@ bool Layer::registerFeature( QgsLabelFeature *lf ) } -void Layer::addFeaturePart( FeaturePart *fpart, const QString &labelText ) +void Layer::addFeaturePart( std::unique_ptr fpart, const QString &labelText ) { - // add to list of layer's feature parts - mFeatureParts << fpart; - // add to hashtable with equally named feature parts if ( mMergeLines && !labelText.isEmpty() ) { - mConnectedHashtable[ labelText ].append( fpart ); + mConnectedHashtable[ labelText ].append( fpart.get() ); } + + // add to list of layer's feature parts + mFeatureParts.emplace_back( std::move( fpart ) ); } void Layer::addObstaclePart( FeaturePart *fpart ) @@ -306,33 +305,46 @@ void Layer::joinConnectedFeatures() int connectedFeaturesId = 0; for ( auto it = mConnectedHashtable.constBegin(); it != mConnectedHashtable.constEnd(); ++it ) { - QVector parts = it.value(); - connectedFeaturesId++; + QVector partsToMerge = it.value(); // need to start with biggest parts first, to avoid merging in side branches before we've // merged the whole of the longest parts of the joined network - std::sort( parts.begin(), parts.end(), []( FeaturePart * a, FeaturePart * b ) + std::sort( partsToMerge.begin(), partsToMerge.end(), []( FeaturePart * a, FeaturePart * b ) { return a->length() > b->length(); } ); // go one-by-one part, try to merge - while ( parts.count() > 1 ) + while ( partsToMerge.count() > 1 ) { + connectedFeaturesId++; + // part we'll be checking against other in this round - FeaturePart *partCheck = parts.takeFirst(); + FeaturePart *partToJoinTo = partsToMerge.takeFirst(); + mConnectedFeaturesIds.insert( partToJoinTo->featureId(), connectedFeaturesId ); - FeaturePart *otherPart = _findConnectedPart( partCheck, parts ); - if ( otherPart ) + // loop through all other parts + QVector< FeaturePart *> partsLeftToTryThisRound = partsToMerge; + while ( !partsLeftToTryThisRound.empty() ) { - // merge points from partCheck to p->item - if ( otherPart->mergeWithFeaturePart( partCheck ) ) + if ( FeaturePart *otherPart = _findConnectedPart( partToJoinTo, partsLeftToTryThisRound ) ) { - mConnectedFeaturesIds.insert( partCheck->featureId(), connectedFeaturesId ); - mConnectedFeaturesIds.insert( otherPart->featureId(), connectedFeaturesId ); + partsLeftToTryThisRound.removeOne( otherPart ); + if ( partToJoinTo->mergeWithFeaturePart( otherPart ) ) + { + mConnectedFeaturesIds.insert( otherPart->featureId(), connectedFeaturesId ); - mFeatureParts.removeOne( partCheck ); - delete partCheck; + // otherPart was merged into partToJoinTo, so now we completely delete the redundant feature part which was merged in + partsToMerge.removeAll( otherPart ); + auto matchingPartIt = std::find_if( mFeatureParts.begin(), mFeatureParts.end(), [otherPart]( const std::unique_ptr< FeaturePart> &part ) { return part.get() == otherPart; } ); + Q_ASSERT( matchingPartIt != mFeatureParts.end() ); + mFeatureParts.erase( matchingPartIt ); + } + } + else + { + // no candidate parts remain which we could possibly merge in + break; } } } @@ -340,11 +352,10 @@ void Layer::joinConnectedFeatures() mConnectedHashtable.clear(); // Expunge feature parts that are smaller than the minimum size required - mFeatureParts.erase( std::remove_if( mFeatureParts.begin(), mFeatureParts.end(), []( FeaturePart * part ) + mFeatureParts.erase( std::remove_if( mFeatureParts.begin(), mFeatureParts.end(), []( const std::unique_ptr< FeaturePart > &part ) { if ( part->feature()->minimumSize() != 0.0 && part->length() < part->feature()->minimumSize() ) { - delete part; return true; } return false; @@ -359,10 +370,12 @@ int Layer::connectedFeatureId( QgsFeatureId featureId ) const void Layer::chopFeaturesAtRepeatDistance() { GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); - QLinkedList newFeatureParts; - while ( !mFeatureParts.isEmpty() ) + std::deque< std::unique_ptr< FeaturePart > > newFeatureParts; + while ( !mFeatureParts.empty() ) { - std::unique_ptr< FeaturePart > fpart( mFeatureParts.takeFirst() ); + std::unique_ptr< FeaturePart > fpart = std::move( mFeatureParts.front() ); + mFeatureParts.pop_front(); + const GEOSGeometry *geom = fpart->geos(); double chopInterval = fpart->repeatDistance(); @@ -460,10 +473,9 @@ void Layer::chopFeaturesAtRepeatDistance() #endif } GEOSGeometry *newgeom = GEOSGeom_createLineString_r( geosctxt, cooSeq ); - FeaturePart *newfpart = new FeaturePart( fpart->feature(), newgeom ); - newFeatureParts.append( newfpart ); - repeatParts.push_back( newfpart ); - + std::unique_ptr< FeaturePart > newfpart = std::make_unique< FeaturePart >( fpart->feature(), newgeom ); + repeatParts.push_back( newfpart.get() ); + newFeatureParts.emplace_back( std::move( newfpart ) ); break; } double c = ( lambda - len[cur - 1] ) / ( len[cur] - len[cur - 1] ); @@ -483,11 +495,11 @@ void Layer::chopFeaturesAtRepeatDistance() } GEOSGeometry *newgeom = GEOSGeom_createLineString_r( geosctxt, cooSeq ); - FeaturePart *newfpart = new FeaturePart( fpart->feature(), newgeom ); - newFeatureParts.append( newfpart ); + std::unique_ptr< FeaturePart > newfpart = std::make_unique< FeaturePart >( fpart->feature(), newgeom ); + repeatParts.push_back( newfpart.get() ); + newFeatureParts.emplace_back( std::move( newfpart ) ); part.clear(); part.push_back( p ); - repeatParts.push_back( newfpart ); } for ( FeaturePart *partPtr : repeatParts ) @@ -495,11 +507,11 @@ void Layer::chopFeaturesAtRepeatDistance() } else { - newFeatureParts.append( fpart.release() ); + newFeatureParts.emplace_back( std::move( fpart ) ); } } - mFeatureParts = newFeatureParts; + mFeatureParts = std::move( newFeatureParts ); } diff --git a/src/core/pal/layer.h b/src/core/pal/layer.h index 2d1a2b9c9d3b..b72b3b5a0dca 100644 --- a/src/core/pal/layer.h +++ b/src/core/pal/layer.h @@ -322,7 +322,7 @@ namespace pal QString mName; //! List of feature parts - QLinkedList mFeatureParts; + std::deque< std::unique_ptr< FeaturePart > > mFeatureParts; //! List of obstacle parts QList mObstacleParts; @@ -355,7 +355,7 @@ namespace pal QMutex mMutex; //! Add newly created feature part into r tree and to the list - void addFeaturePart( FeaturePart *fpart, const QString &labelText = QString() ); + void addFeaturePart( std::unique_ptr< FeaturePart > fpart, const QString &labelText = QString() ); //! Add newly created obstacle part into r tree and to the list void addObstaclePart( FeaturePart *fpart ); diff --git a/src/core/pal/pal.cpp b/src/core/pal/pal.cpp index 87d18663f90f..2e988d67d291 100644 --- a/src/core/pal/pal.cpp +++ b/src/core/pal/pal.cpp @@ -156,7 +156,7 @@ std::unique_ptr Pal::extract( const QgsRectangle &extent, const QgsGeom QMutexLocker locker( &layer->mMutex ); // generate candidates for all features - for ( FeaturePart *featurePart : std::as_const( layer->mFeatureParts ) ) + for ( const std::unique_ptr< FeaturePart > &featurePart : std::as_const( layer->mFeatureParts ) ) { if ( isCanceled() ) break; @@ -204,7 +204,7 @@ std::unique_ptr Pal::extract( const QgsRectangle &extent, const QgsGeom // valid features are added to fFeats std::unique_ptr< Feats > ft = std::make_unique< Feats >(); - ft->feature = featurePart; + ft->feature = featurePart.get(); ft->shape = nullptr; ft->candidates = std::move( candidates ); ft->priority = featurePart->calculatePriority(); @@ -213,7 +213,7 @@ std::unique_ptr Pal::extract( const QgsRectangle &extent, const QgsGeom else { // no candidates, so generate a default "point on surface" one - std::unique_ptr< LabelPosition > unplacedPosition = featurePart->createCandidatePointOnSurface( featurePart ); + std::unique_ptr< LabelPosition > unplacedPosition = featurePart->createCandidatePointOnSurface( featurePart.get() ); if ( !unplacedPosition ) continue; @@ -226,7 +226,7 @@ std::unique_ptr Pal::extract( const QgsRectangle &extent, const QgsGeom // valid features are added to fFeats std::unique_ptr< Feats > ft = std::make_unique< Feats >(); - ft->feature = featurePart; + ft->feature = featurePart.get(); ft->shape = nullptr; ft->candidates = std::move( candidates ); ft->priority = featurePart->calculatePriority(); diff --git a/src/core/pal/pal.h b/src/core/pal/pal.h index 43bd72e69884..e6b75293fb2b 100644 --- a/src/core/pal/pal.h +++ b/src/core/pal/pal.h @@ -264,8 +264,8 @@ namespace pal int mTenure = 10; double mCandListSize = 0.2; - long long mNextCandidateId = 1; - mutable QHash< QPair< long long, long long >, bool > mCandidateConflicts; + unsigned int mNextCandidateId = 1; + mutable QHash< QPair< unsigned int, unsigned int >, bool > mCandidateConflicts; /** * \brief show partial labels (cut-off by the map canvas) or not diff --git a/tests/src/core/testqgslabelingengine.cpp b/tests/src/core/testqgslabelingengine.cpp index e3e7d6b12c18..bda9918a2ccc 100644 --- a/tests/src/core/testqgslabelingengine.cpp +++ b/tests/src/core/testqgslabelingengine.cpp @@ -1150,11 +1150,12 @@ void TestQgsLabelingEngine::testMergingLinesWithForks() settings.isExpression = true; settings.placement = QgsPalLayerSettings::Curved; settings.labelPerPart = false; + settings.dist = 1; settings.lineSettings().setMergeLines( true ); // if treated individually, none of these parts are long enough for the label to fit -- but the label should be rendered if the mergeLines setting is true std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3946&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); - vl2->setRenderer( new QgsNullSymbolRenderer() ); + vl2->setRenderer( new QgsSingleSymbolRenderer( QgsLineSymbol::createSimple( { {QStringLiteral( "color" ), QStringLiteral( "#000000" )}, {QStringLiteral( "outline_width" ), 0.6} } ) ) ); QgsFeature f; f.setAttributes( QgsAttributes() << 1 ); @@ -1219,9 +1220,9 @@ void TestQgsLabelingEngine::testMergingLinesWithMinimumSize() settings.lineSettings().setMergeLines( true ); settings.thinningSettings().setMinimumFeatureSize( 90.0 ); - // if treated individually, none of these parts are long enough for the label to fit -- but the label should be rendered if the mergeLines setting is true + // if treated individually, none of these parts exceed the minimum feature size set above -- but the label should be rendered if the mergeLines setting is true std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3946&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); - vl2->setRenderer( new QgsNullSymbolRenderer() ); + vl2->setRenderer( new QgsSingleSymbolRenderer( QgsLineSymbol::createSimple( { {QStringLiteral( "color" ), QStringLiteral( "#000000" )}, {QStringLiteral( "outline_width" ), 0.6} } ) ) ); QgsFeature f; f.setAttributes( QgsAttributes() << 1 ); diff --git a/tests/testdata/control_images/labelingengine/expected_label_merged_minimum_size/expected_label_merged_minimum_size.png b/tests/testdata/control_images/labelingengine/expected_label_merged_minimum_size/expected_label_merged_minimum_size.png index 5b84aaf0b302e4de092c7669d6762364ad13d3c0..69971f9284f02142fc14bbc0b6d5e24597fa6a42 100644 GIT binary patch delta 726 zcmV;{0xA8Y5rh+vI9UlcNklsq}40Bj?( zK?N-Xe_*Fx09e-Z@%(TcEB6%w#*1afUyhdx z&%!pl_WASY`Fvil0bI;%Haj{xIyyR<&1Us3z#?{T-@bk0#*I67?%ch5cXxO9=i_ub zolGY6p1?mGA0Iz>@L-Jb{{8#0UvC3k+|C%|f7`cjCnqN-CnvxBg**emrQ3P?_U-BE z>Bo;B|8@NI>C@M*Uxz#cz!lgzI5;>sIQaRC`F#H6%a@VQFp9yeGo&mO#0IxK8 z2G~>ryvF1iV0{Vj#*$}%RVKh&N}d5)0?aSSGr(07U@lCa0hTAgTrznE_#FbwA0^KK zKL{`@M4o}|A;7E+c?LF#0J9%c6N4pdOF6~ z-QB%@{d&C)1Oyl|my%~-9Ze<^0frTIex5&k_;7Z1R__QbNPuBo9UdM&dGch8F7f*H z>%G0bdYub%2^!pl1#9W%&6__y?CtIC@9($EEau_EhxH0jx>hd$0NY5B@iHC$4;a?c zY&JVSJ|1J-yLWGA$N!4L`Vn9N2)G0dv!MY20<-V|HUSp+8-kGQ5QZG(VgLXD07*qo IM6N<$fR delta 487 zcmZn>UMx7l!iOW&)5S5QV$Rz;2Ys~yB^oZi?&ghPi?-gt#Jz!emV%ih`;|oYJl~wQ z?FNd%H4h~n?Agyn8kUO{m&dCLsH#aicGEJVq_^^J*T5|^R zM~OB)hGNNvu`&InTVx+>7f!I4z5L^r?Vkdl%2u+Ku}JK@_*})`_wygSd-BI0e_3a9 zI8`{|gwN+aQmcO#+V6e+g=e~N!@})StDk?|v;D`{%bz74r|y|kAAO))D8ZunX@uOn zr#F4&w@c2RUt>}rX6MY*(7?d#;Lwoxt^V7;`K)J}z`Tb%_l`yHWlcz9kzinA;$dK9 zWNT<}lo9y-Tw=}o+daL85eDvl2MSirmv3f`VE{=&47t~HuxX}H0s{w!!JJbPIuAHt zdV$ghRCF{Xn_0t8b4vIn@rV`3AhaC=YWtZ9)prD_&jO~;QKn$h(TF{L*Zc2RuU}j% z&m&W?`EK58+XJaleNVr9DxTMSuqpc)3(x&j+iUFa^p1bZyJKm4_^MDs#vk+Zm%S_7 zPCphkIdfj`e7iQ^pEQuE^A#C42Qua|ZJxj=&M5XkKfy4N{lZtNR>FVdQ&MBb@ E0PFq8kN^Mx diff --git a/tests/testdata/control_images/labelingengine/expected_label_multipart_touching_branches/expected_label_multipart_touching_branches.png b/tests/testdata/control_images/labelingengine/expected_label_multipart_touching_branches/expected_label_multipart_touching_branches.png index 6bc0047793f35520ceb41999f56808eda070cceb..31ee298d03501223ce54079bf2224d72ca2125cc 100644 GIT binary patch literal 3389 zcmeHK`#0O!7XK#HqaIbgotjporq-a^LAg;)yc$ZySc7^^iFA}?s8mHuL`dt7PH9oE zlw1)Vk4{xXQ}wDxM~6}-hI&gpf(Vglf{3Paa}RA8Zd1OT)}7hGK~ zCjNVsjf~Fc8FkOX4)4#<*nj(QhU%Zr5*%P1y{u-qZrDg53wT z05Vh`4QS|n;g=ZvkL1AP3uFpq$O0gD+A8yWD0-<=IxX7CLsD*r?8qf=M!>*tRBucA zH6*eX#rDS%)aVn-2WB`)JlUJO;?;&pN@gYu^=W}pcTy8@WSE15mm@0V>wXN&v3g=5 z2Iykwf>0<&bQACQ3zVpaHUqe(;QTi%)c_z!RY8H6o6UC<{LL$)QMnkq z22j^;d=SL?7DD8*vJy^{9(AHRs2JwCX!6r-2-eJG^?Gjm%`r{!MWa9|!wm=+{*rMs3-$i0-0DmNQqwDm->6J9S?( zaYxv)inMFE<@qF;JW0?F2x)~VI0d{=E{2Jr=bkV^r~CCAxnvn&rLP);QWPYjT(AhW zf`UW*Hzv|XMmccB_R8k>hnN`jRn^3+0+%CbI$IJ+6LHwZ&dN)#zh44iYMFHtcB}?m z%=h$u+AZ3Xqv&q5PgvStG^BZg2zS49;p-pi6QJ}YzM}G78=;LvRIu@Nx6;-0(Oa%= zi@NHZt#SXQ;o;LZexBpklY(uu=es$au^=j#63qPVZ&%sQ6d~7UHCSG_)v9yCi2ctph z)}-xqet36Qi9LQ%FwVr|@x;^;C5nqPPSpbCjmKNU)$g?4d zIISBS8yFUCb$df5;a!_bQ+4*$Sfu$;6PqeG*J2 zMtjf{J1wKt5FHilzs~PzY)GG%b+~tWmrOvMMzNcHW`=X1f4+YNNJlc@u)^}^Ucxex zYNMr258f#|y}$43{ZjT6Xe>8!V9n9!46aXG2LiT z%r_mEYu+-)C5`=k<$-{<7ull5m>`q{<@SHTOOz!qa3@DUJxIXG1z%$=K)9#r^G%BE zH)n1N{NVRXR+!z%F( z4l>wgo8qRw3w&lc7f0^VL+b`3#8V1EQ-0h^Kyc6)$9Z2d#?&5Rp=otsxDPIBwk4`< zAM3d2fe^z71Oyva7i+)~>=pCk8`c6{C*B8({q=WT0~MumFSL{(jL|0uyX&JX^6jp1 z;?I`&_6`mX_T-sL`SFT?KzG94r(M?=qF&~%L7Hb~sw+$reN_#jig@nXkkH21vV8H{ zY^Q|BGMYOps(mFQDUJtFVZGGoCQEeF;73E1Td!+o2`9oDkj>#U3{RX)JBGe&qZDz0n zS&6zL4Y(T}Cd}-kG{eABKU-VyM{Qr~#Yfd@R`%}+*vIms? P5OBfG+x6Kw^zHuueId6( literal 2990 zcmeHJ>raz+6#lhkAPNO_SfB_bi<%AOA_fo&#RU{afvHf$Nnc>WI2q9cqdk!I&K93SRV@yJp}+71^^}Bf;Lq;co95Pvdj!Wn+*WA?_;2Dx?%AEY=e%49zH{> zS(qeVbV6Ku-uNj=_p>`H$ITxem+h19)C0kl$p=+gR}YG<{~8d$LJv%&D|9s*t~@BAs%#4J-7#(`_}sx&P~q)dF@Pfz9inb^ zFJB1Q+rO|qCtLLCk&w2Lu@MymEu1nQ}I+g!r;*_Vcgn*HCoV8#Lr4ctMt5%t#X0-B1 zkyxTY^6f@%zts6D*u{!B{rAKa*v)dOmPJ3jI39X0DDIPH*B%mE96`WJ%5bg3@(LXO z0^J-dWn!52Tw&6FiHP?zHEXh)Kk1VhuEaxa-`srFTqq*ih^{6cv|AWi$k`rSj97lZS;QPbKwiD*oOO#Ns&mvY;-MbH=lCt8tC!nHdg z?6o5FS{PitjYRcZS=M(XpqrmnV-w4~r^@nNM8&hjG382N;N82Rft?C-nIesWCSbPJ zq)-y1+5?RZZ6MHR1dvl_hA>s*gy35L={mwhDB7vy^46tm5fnj=KB+Sg`xES;J2}sx_*X>j+^+(j5dCsKn{@2s;d=m z>n)x3p#;KsJ`%=R;8c_qq4BozrV|J~DUC6xtFbql=SIPc!|y#y`5uxR)-42X*x@T( zpY>D^X*)A%^jy2VWOBLZ{2suzl~<4ZOHvf>VqOutF$`Y)Gl%2HmgU;vpei#xpm?=+ zMK5qr<(0stwoFIBYNmWb!eB#RJl)xM7_fIhe|yy;8dZ)|7?nPKeSh)jOVL=bW@*GUnbg{8y-hi;kp z2~d{;p2Y(lVQ+D(&DxZ%PBRC2D0?4ohp@uF9F|1`Oy2=vWo0(8lP;@9r$P_xtb^16 zrTmSelj))_K-6n&hMka9mUFM`mKn>nCzF)U+%INplqy<> zG$bQ>jz?X|?<_dLN$Wag`j5748ezQ(S3DNdi?W4Sbum*Xw5BVwo@dyTbg7-F&R{+o z`H(TK`6#y;h)>&;@}1k|djTUi%k5&fT#~n=u92^Ua K41M$^RP-;S9*occ diff --git a/tests/testdata/control_images/labelingengine/expected_label_multipart_touching_branches/expected_label_multipart_touching_branches_mask.png b/tests/testdata/control_images/labelingengine/expected_label_multipart_touching_branches/expected_label_multipart_touching_branches_mask.png deleted file mode 100644 index 1d49c2a281fd206285a390652546f2392c433822..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3869 zcmeH~=~I)*7Ki&yScfH1aA6S;8OsHtBM@XW;1Y0QQyC0kh{zC!fb6>=ua~*vhT!#5 zs1Q)VLPOXlB4LL?MS&0)B`A@=1tdTq2myi#gq!&=|G|8utGZ6rsp{_2=lo9B)3>}m z-E@91`~d)hbh{WYQIxYAz%(B+;y?s`5c ze|CsV3G_AXn4fdX!dTwiKW|!fed}-L`a5fO*{tn%2(ar353Hvwn6f(sZ&uZ(JNNBo z@0&ham5q7ryAAVYi(e1x+oSA|-4Ws9am7j_5q;`%OzJyTxwuZ4@5o@BfBO#FQN-At(#`VdSs z3IPIkba9`^YU`c)#5v;Z>}U*U<^i_cqG+VEK=JU%kP^Pb(@1E)S^+0R_EHmGmJJB215V`!iVg*^LJV zDuW`ELqIPoU8|Kr6Ce&1a``4|l<*i{>=4r&RE1?j?$-YyJLgjL2g;R)HtWI2`^J>K zs(cTdGL2TWX6sgqNQL{Wnwt19dJ)GGy!^!*nzS;rc`-%ke;*PPhVQE&gTV!VKGKZ4 z+r$rL|BfP_VXY`%e}zkx0muE=2yb)#5qAkvIVfz*$2uR zRZF_MQ$in~*z9#{5rI;L*NpkOk z#XJ+p--n!abadRGM@UmvJ*ToMdhG3>xBeZMLr~Hi*};ZCLfKMIeZuTur;um0p0qIj ze$YGc?Pz&G?et&`C0@iU#|zdPZ%N&yQ;$7ba1E$|I9ffGmE#WNG^UFa}6)c^OJH51ftZR$<55@eJQqDob<&|}`y zF!z~7jzB>pr>2WmxWrfarJxZS%tFXc;)PLLUh|QF`FY-`=x`st^`7E@^39e@M4wP%P zCk|6<6A}|evL%rR>drlD7VqL*qr`Je{WBttZqTY@ABnGK+@n3q>t@F1rXqW_p--_& zQBW_GDYZs;ZRnDy*;)`#;S(-%*S?hVW=-v#Ik11_WFj0FlPSgZZcHeOfXKewH5PqO zIR^F$>S9aJH5AC-UhWmADpu=aRj(gLWN&-yS*k2NFZuKQVafN#=GBF+vh54|^(deO z4ci0{RR|vx9HIJWnc}X_7@x>;m0!~HvHA#Mxb|@1i?00Qq=i)H3QECqwRbzgR7vi? zM`JQa`H?NuE$vgrED{L3;nuo17>-T-I@gpenRqeG6-qK^Qm`x@m8SSk<{ty>vxw{q zEee_F#jxp)9sT4&YG`PAG4>~s{i8F6xMO-xP%38m-vp_1O$$;C*T-pZe0h=EM=-P- zJl)x)d>3SkAG}0;#crhu&V|&)z<%*{zKIWVd=p&d`eEv&E(+zfBP({fFScKXtR3L& zNmG0m2J675_26Der2BkTAN$S>-VH8f$_Y;wszRCff!UVrHGS1&RZd1*4KS}m-_vJY-2cO zSO%jDcNzju?3@s48|=41!5i?u0ssFL{7Y~pMv|h+z4z;t|6OqMgy->!qojZRAJ1{m AlmGw#