From 297edfaa03ec802dd1711cd3df72f520c1950aba Mon Sep 17 00:00:00 2001 From: littletomatodonkey Date: Sun, 12 Jul 2020 17:21:47 +0000 Subject: [PATCH 1/7] add cpp inference --- .gitignore | 1 + deploy/cpp_infer/CMakeLists.txt | 110 + deploy/cpp_infer/include/clipper.h | 423 ++ deploy/cpp_infer/include/ocr_det.h | 71 + deploy/cpp_infer/include/ocr_rec.h | 81 + deploy/cpp_infer/include/postprocess_op.h | 111 + deploy/cpp_infer/include/preprocess_op.h | 60 + deploy/cpp_infer/src/clipper.cpp | 4380 ++++++++++++++ deploy/cpp_infer/src/main.cpp | 67 + deploy/cpp_infer/src/ocr_det.cpp | 142 + deploy/cpp_infer/src/ocr_rec.cpp | 218 + deploy/cpp_infer/src/postprocess_op.cpp | 307 + deploy/cpp_infer/src/preprocess_op.cpp | 119 + deploy/cpp_infer/tools/build.sh | 24 + deploy/cpp_infer/tools/ppocr_keys_v1.txt | 6623 +++++++++++++++++++++ deploy/cpp_infer/tools/run.sh | 2 + 16 files changed, 12739 insertions(+) create mode 100644 deploy/cpp_infer/CMakeLists.txt create mode 100644 deploy/cpp_infer/include/clipper.h create mode 100644 deploy/cpp_infer/include/ocr_det.h create mode 100644 deploy/cpp_infer/include/ocr_rec.h create mode 100644 deploy/cpp_infer/include/postprocess_op.h create mode 100644 deploy/cpp_infer/include/preprocess_op.h create mode 100644 deploy/cpp_infer/src/clipper.cpp create mode 100644 deploy/cpp_infer/src/main.cpp create mode 100644 deploy/cpp_infer/src/ocr_det.cpp create mode 100644 deploy/cpp_infer/src/ocr_rec.cpp create mode 100644 deploy/cpp_infer/src/postprocess_op.cpp create mode 100644 deploy/cpp_infer/src/preprocess_op.cpp create mode 100755 deploy/cpp_infer/tools/build.sh create mode 100644 deploy/cpp_infer/tools/ppocr_keys_v1.txt create mode 100755 deploy/cpp_infer/tools/run.sh diff --git a/.gitignore b/.gitignore index 7d03b965..1a2dd675 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ +.ipynb_checkpoints/ *.py[cod] *$py.class diff --git a/deploy/cpp_infer/CMakeLists.txt b/deploy/cpp_infer/CMakeLists.txt new file mode 100644 index 00000000..282b245d --- /dev/null +++ b/deploy/cpp_infer/CMakeLists.txt @@ -0,0 +1,110 @@ +project(ocr_system CXX C) +option(WITH_MKL "Compile demo with MKL/OpenBlas support, default use MKL." ON) +option(WITH_GPU "Compile demo with GPU/CPU, default use CPU." OFF) +option(WITH_STATIC_LIB "Compile demo with static/shared library, default use static." ON) +option(USE_TENSORRT "Compile demo with TensorRT." OFF) + + +macro(safe_set_static_flag) + foreach(flag_var + CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE + CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO) + if(${flag_var} MATCHES "/MD") + string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + endif(${flag_var} MATCHES "/MD") + endforeach(flag_var) +endmacro() + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g -fpermissive") +set(CMAKE_STATIC_LIBRARY_PREFIX "") +message("flags" ${CMAKE_CXX_FLAGS}) +set(CMAKE_CXX_FLAGS_RELEASE "-O3") + +if(NOT DEFINED PADDLE_LIB) + message(FATAL_ERROR "please set PADDLE_LIB with -DPADDLE_LIB=/path/paddle/lib") +endif() +if(NOT DEFINED DEMO_NAME) + message(FATAL_ERROR "please set DEMO_NAME with -DDEMO_NAME=demo_name") +endif() + +# user ze +# find_package(OpenCV) + + +set(OPENCV_DIR "/paddle/libs/opencv-3.4.7/opencv3") +find_package(OpenCV REQUIRED PATHS ${OPENCV_DIR}/share/OpenCV NO_DEFAULT_PATH) +include_directories(${OpenCV_INCLUDE_DIRS}) + +include_directories("${PADDLE_LIB}/paddle/include") +include_directories("${PADDLE_LIB}/third_party/install/protobuf/include") +include_directories("${PADDLE_LIB}/third_party/install/glog/include") +include_directories("${PADDLE_LIB}/third_party/install/gflags/include") +include_directories("${PADDLE_LIB}/third_party/install/xxhash/include") +include_directories("${PADDLE_LIB}/third_party/install/zlib/include") +include_directories("${PADDLE_LIB}/third_party/boost") +include_directories("${PADDLE_LIB}/third_party/eigen3") + +include_directories("${CMAKE_SOURCE_DIR}/") + +if (USE_TENSORRT AND WITH_GPU) + include_directories("${TENSORRT_ROOT}/include") + link_directories("${TENSORRT_ROOT}/lib") +endif() + +link_directories("${PADDLE_LIB}/third_party/install/zlib/lib") + +link_directories("${PADDLE_LIB}/third_party/install/protobuf/lib") +link_directories("${PADDLE_LIB}/third_party/install/glog/lib") +link_directories("${PADDLE_LIB}/third_party/install/gflags/lib") +link_directories("${PADDLE_LIB}/third_party/install/xxhash/lib") +link_directories("${PADDLE_LIB}/paddle/lib") + + +add_executable(${DEMO_NAME} src/main.cpp src/ocr_det.cpp src/ocr_rec.cpp src/preprocess_op.cpp src/clipper.cpp src/postprocess_op.cpp ) + +if(WITH_MKL) + include_directories("${PADDLE_LIB}/third_party/install/mklml/include") + set(MATH_LIB ${PADDLE_LIB}/third_party/install/mklml/lib/libmklml_intel${CMAKE_SHARED_LIBRARY_SUFFIX} + ${PADDLE_LIB}/third_party/install/mklml/lib/libiomp5${CMAKE_SHARED_LIBRARY_SUFFIX}) + set(MKLDNN_PATH "${PADDLE_LIB}/third_party/install/mkldnn") + if(EXISTS ${MKLDNN_PATH}) + include_directories("${MKLDNN_PATH}/include") + set(MKLDNN_LIB ${MKLDNN_PATH}/lib/libmkldnn.so.0) + endif() +else() + set(MATH_LIB ${PADDLE_LIB}/third_party/install/openblas/lib/libopenblas${CMAKE_STATIC_LIBRARY_SUFFIX}) +endif() + +# Note: libpaddle_inference_api.so/a must put before libpaddle_fluid.so/a +if(WITH_STATIC_LIB) + set(DEPS + ${PADDLE_LIB}/paddle/lib/libpaddle_fluid${CMAKE_STATIC_LIBRARY_SUFFIX}) +else() + set(DEPS + ${PADDLE_LIB}/paddle/lib/libpaddle_fluid${CMAKE_SHARED_LIBRARY_SUFFIX}) +endif() + +# user ze +# set(EXTERNAL_LIB "-lrt -ldl -lpthread -lm -lopencv_world") +# gry +set(EXTERNAL_LIB "-lrt -ldl -lpthread -lm") + +set(DEPS ${DEPS} + ${MATH_LIB} ${MKLDNN_LIB} + glog gflags protobuf z xxhash + ${EXTERNAL_LIB} ${OpenCV_LIBS}) + +if(WITH_GPU) + if (USE_TENSORRT) + set(DEPS ${DEPS} + ${TENSORRT_ROOT}/lib/libnvinfer${CMAKE_SHARED_LIBRARY_SUFFIX}) + set(DEPS ${DEPS} + ${TENSORRT_ROOT}/lib/libnvinfer_plugin${CMAKE_SHARED_LIBRARY_SUFFIX}) + endif() + set(DEPS ${DEPS} ${CUDA_LIB}/libcudart${CMAKE_SHARED_LIBRARY_SUFFIX}) + set(DEPS ${DEPS} ${CUDA_LIB}/libcudart${CMAKE_SHARED_LIBRARY_SUFFIX} ) + set(DEPS ${DEPS} ${CUDA_LIB}/libcublas${CMAKE_SHARED_LIBRARY_SUFFIX} ) + set(DEPS ${DEPS} ${CUDNN_LIB}/libcudnn${CMAKE_SHARED_LIBRARY_SUFFIX} ) +endif() + +target_link_libraries(${DEMO_NAME} ${DEPS}) diff --git a/deploy/cpp_infer/include/clipper.h b/deploy/cpp_infer/include/clipper.h new file mode 100644 index 00000000..384a6cf4 --- /dev/null +++ b/deploy/cpp_infer/include/clipper.h @@ -0,0 +1,423 @@ +/******************************************************************************* +* * +* Author : Angus Johnson * +* Version : 6.4.2 * +* Date : 27 February 2017 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2017 * +* * +* License: * +* Use, modification & distribution is subject to Boost Software License Ver 1. * +* http://www.boost.org/LICENSE_1_0.txt * +* * +* Attributions: * +* The code in this library is an extension of Bala Vatti's clipping algorithm: * +* "A generic solution to polygon clipping" * +* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * +* http://portal.acm.org/citation.cfm?id=129906 * +* * +* Computer graphics and geometric modeling: implementation and algorithms * +* By Max K. Agoston * +* Springer; 1 edition (January 4, 2005) * +* http://books.google.com/books?q=vatti+clipping+agoston * +* * +* See also: * +* "Polygon Offsetting by Computing Winding Numbers" * +* Paper no. DETC2005-85513 pp. 565-575 * +* ASME 2005 International Design Engineering Technical Conferences * +* and Computers and Information in Engineering Conference (IDETC/CIE2005) * +* September 24-28, 2005 , Long Beach, California, USA * +* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * +* * +*******************************************************************************/ + +#ifndef clipper_hpp +#define clipper_hpp + +#define CLIPPER_VERSION "6.4.2" + +// use_int32: When enabled 32bit ints are used instead of 64bit ints. This +// improve performance but coordinate values are limited to the range +/- 46340 +//#define use_int32 + +// use_xyz: adds a Z member to IntPoint. Adds a minor cost to perfomance. +//#define use_xyz + +// use_lines: Enables line clipping. Adds a very minor cost to performance. +#define use_lines + +// use_deprecated: Enables temporary support for the obsolete functions +//#define use_deprecated + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ClipperLib { + +enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor }; +enum PolyType { ptSubject, ptClip }; +// By far the most widely used winding rules for polygon filling are +// EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32) +// Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL) +// see http://glprogramming.com/red/chapter11.html +enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative }; + +#ifdef use_int32 +typedef int cInt; +static cInt const loRange = 0x7FFF; +static cInt const hiRange = 0x7FFF; +#else +typedef signed long long cInt; +static cInt const loRange = 0x3FFFFFFF; +static cInt const hiRange = 0x3FFFFFFFFFFFFFFFLL; +typedef signed long long long64; // used by Int128 class +typedef unsigned long long ulong64; + +#endif + +struct IntPoint { + cInt X; + cInt Y; +#ifdef use_xyz + cInt Z; + IntPoint(cInt x = 0, cInt y = 0, cInt z = 0) : X(x), Y(y), Z(z){}; +#else + IntPoint(cInt x = 0, cInt y = 0) : X(x), Y(y){}; +#endif + + friend inline bool operator==(const IntPoint &a, const IntPoint &b) { + return a.X == b.X && a.Y == b.Y; + } + friend inline bool operator!=(const IntPoint &a, const IntPoint &b) { + return a.X != b.X || a.Y != b.Y; + } +}; +//------------------------------------------------------------------------------ + +typedef std::vector Path; +typedef std::vector Paths; + +inline Path &operator<<(Path &poly, const IntPoint &p) { + poly.push_back(p); + return poly; +} +inline Paths &operator<<(Paths &polys, const Path &p) { + polys.push_back(p); + return polys; +} + +std::ostream &operator<<(std::ostream &s, const IntPoint &p); +std::ostream &operator<<(std::ostream &s, const Path &p); +std::ostream &operator<<(std::ostream &s, const Paths &p); + +struct DoublePoint { + double X; + double Y; + DoublePoint(double x = 0, double y = 0) : X(x), Y(y) {} + DoublePoint(IntPoint ip) : X((double)ip.X), Y((double)ip.Y) {} +}; +//------------------------------------------------------------------------------ + +#ifdef use_xyz +typedef void (*ZFillCallback)(IntPoint &e1bot, IntPoint &e1top, IntPoint &e2bot, + IntPoint &e2top, IntPoint &pt); +#endif + +enum InitOptions { + ioReverseSolution = 1, + ioStrictlySimple = 2, + ioPreserveCollinear = 4 +}; +enum JoinType { jtSquare, jtRound, jtMiter }; +enum EndType { + etClosedPolygon, + etClosedLine, + etOpenButt, + etOpenSquare, + etOpenRound +}; + +class PolyNode; +typedef std::vector PolyNodes; + +class PolyNode { +public: + PolyNode(); + virtual ~PolyNode(){}; + Path Contour; + PolyNodes Childs; + PolyNode *Parent; + PolyNode *GetNext() const; + bool IsHole() const; + bool IsOpen() const; + int ChildCount() const; + +private: + // PolyNode& operator =(PolyNode& other); + unsigned Index; // node index in Parent.Childs + bool m_IsOpen; + JoinType m_jointype; + EndType m_endtype; + PolyNode *GetNextSiblingUp() const; + void AddChild(PolyNode &child); + friend class Clipper; // to access Index + friend class ClipperOffset; +}; + +class PolyTree : public PolyNode { +public: + ~PolyTree() { Clear(); }; + PolyNode *GetFirst() const; + void Clear(); + int Total() const; + +private: + // PolyTree& operator =(PolyTree& other); + PolyNodes AllNodes; + friend class Clipper; // to access AllNodes +}; + +bool Orientation(const Path &poly); +double Area(const Path &poly); +int PointInPolygon(const IntPoint &pt, const Path &path); + +void SimplifyPolygon(const Path &in_poly, Paths &out_polys, + PolyFillType fillType = pftEvenOdd); +void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, + PolyFillType fillType = pftEvenOdd); +void SimplifyPolygons(Paths &polys, PolyFillType fillType = pftEvenOdd); + +void CleanPolygon(const Path &in_poly, Path &out_poly, double distance = 1.415); +void CleanPolygon(Path &poly, double distance = 1.415); +void CleanPolygons(const Paths &in_polys, Paths &out_polys, + double distance = 1.415); +void CleanPolygons(Paths &polys, double distance = 1.415); + +void MinkowskiSum(const Path &pattern, const Path &path, Paths &solution, + bool pathIsClosed); +void MinkowskiSum(const Path &pattern, const Paths &paths, Paths &solution, + bool pathIsClosed); +void MinkowskiDiff(const Path &poly1, const Path &poly2, Paths &solution); + +void PolyTreeToPaths(const PolyTree &polytree, Paths &paths); +void ClosedPathsFromPolyTree(const PolyTree &polytree, Paths &paths); +void OpenPathsFromPolyTree(PolyTree &polytree, Paths &paths); + +void ReversePath(Path &p); +void ReversePaths(Paths &p); + +struct IntRect { + cInt left; + cInt top; + cInt right; + cInt bottom; +}; + +// enums that are used internally ... +enum EdgeSide { esLeft = 1, esRight = 2 }; + +// forward declarations (for stuff used internally) ... +struct TEdge; +struct IntersectNode; +struct LocalMinimum; +struct OutPt; +struct OutRec; +struct Join; + +typedef std::vector PolyOutList; +typedef std::vector EdgeList; +typedef std::vector JoinList; +typedef std::vector IntersectList; + +//------------------------------------------------------------------------------ + +// ClipperBase is the ancestor to the Clipper class. It should not be +// instantiated directly. This class simply abstracts the conversion of sets of +// polygon coordinates into edge objects that are stored in a LocalMinima list. +class ClipperBase { +public: + ClipperBase(); + virtual ~ClipperBase(); + virtual bool AddPath(const Path &pg, PolyType PolyTyp, bool Closed); + bool AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed); + virtual void Clear(); + IntRect GetBounds(); + bool PreserveCollinear() { return m_PreserveCollinear; }; + void PreserveCollinear(bool value) { m_PreserveCollinear = value; }; + +protected: + void DisposeLocalMinimaList(); + TEdge *AddBoundsToLML(TEdge *e, bool IsClosed); + virtual void Reset(); + TEdge *ProcessBound(TEdge *E, bool IsClockwise); + void InsertScanbeam(const cInt Y); + bool PopScanbeam(cInt &Y); + bool LocalMinimaPending(); + bool PopLocalMinima(cInt Y, const LocalMinimum *&locMin); + OutRec *CreateOutRec(); + void DisposeAllOutRecs(); + void DisposeOutRec(PolyOutList::size_type index); + void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2); + void DeleteFromAEL(TEdge *e); + void UpdateEdgeIntoAEL(TEdge *&e); + + typedef std::vector MinimaList; + MinimaList::iterator m_CurrentLM; + MinimaList m_MinimaList; + + bool m_UseFullRange; + EdgeList m_edges; + bool m_PreserveCollinear; + bool m_HasOpenPaths; + PolyOutList m_PolyOuts; + TEdge *m_ActiveEdges; + + typedef std::priority_queue ScanbeamList; + ScanbeamList m_Scanbeam; +}; +//------------------------------------------------------------------------------ + +class Clipper : public virtual ClipperBase { +public: + Clipper(int initOptions = 0); + bool Execute(ClipType clipType, Paths &solution, + PolyFillType fillType = pftEvenOdd); + bool Execute(ClipType clipType, Paths &solution, PolyFillType subjFillType, + PolyFillType clipFillType); + bool Execute(ClipType clipType, PolyTree &polytree, + PolyFillType fillType = pftEvenOdd); + bool Execute(ClipType clipType, PolyTree &polytree, PolyFillType subjFillType, + PolyFillType clipFillType); + bool ReverseSolution() { return m_ReverseOutput; }; + void ReverseSolution(bool value) { m_ReverseOutput = value; }; + bool StrictlySimple() { return m_StrictSimple; }; + void StrictlySimple(bool value) { m_StrictSimple = value; }; +// set the callback function for z value filling on intersections (otherwise Z +// is 0) +#ifdef use_xyz + void ZFillFunction(ZFillCallback zFillFunc); +#endif +protected: + virtual bool ExecuteInternal(); + +private: + JoinList m_Joins; + JoinList m_GhostJoins; + IntersectList m_IntersectList; + ClipType m_ClipType; + typedef std::list MaximaList; + MaximaList m_Maxima; + TEdge *m_SortedEdges; + bool m_ExecuteLocked; + PolyFillType m_ClipFillType; + PolyFillType m_SubjFillType; + bool m_ReverseOutput; + bool m_UsingPolyTree; + bool m_StrictSimple; +#ifdef use_xyz + ZFillCallback m_ZFill; // custom callback +#endif + void SetWindingCount(TEdge &edge); + bool IsEvenOddFillType(const TEdge &edge) const; + bool IsEvenOddAltFillType(const TEdge &edge) const; + void InsertLocalMinimaIntoAEL(const cInt botY); + void InsertEdgeIntoAEL(TEdge *edge, TEdge *startEdge); + void AddEdgeToSEL(TEdge *edge); + bool PopEdgeFromSEL(TEdge *&edge); + void CopyAELToSEL(); + void DeleteFromSEL(TEdge *e); + void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2); + bool IsContributing(const TEdge &edge) const; + bool IsTopHorz(const cInt XPos); + void DoMaxima(TEdge *e); + void ProcessHorizontals(); + void ProcessHorizontal(TEdge *horzEdge); + void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); + OutPt *AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); + OutRec *GetOutRec(int idx); + void AppendPolygon(TEdge *e1, TEdge *e2); + void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt); + OutPt *AddOutPt(TEdge *e, const IntPoint &pt); + OutPt *GetLastOutPt(TEdge *e); + bool ProcessIntersections(const cInt topY); + void BuildIntersectList(const cInt topY); + void ProcessIntersectList(); + void ProcessEdgesAtTopOfScanbeam(const cInt topY); + void BuildResult(Paths &polys); + void BuildResult2(PolyTree &polytree); + void SetHoleState(TEdge *e, OutRec *outrec); + void DisposeIntersectNodes(); + bool FixupIntersectionOrder(); + void FixupOutPolygon(OutRec &outrec); + void FixupOutPolyline(OutRec &outrec); + bool IsHole(TEdge *e); + bool FindOwnerFromSplitRecs(OutRec &outRec, OutRec *&currOrfl); + void FixHoleLinkage(OutRec &outrec); + void AddJoin(OutPt *op1, OutPt *op2, const IntPoint offPt); + void ClearJoins(); + void ClearGhostJoins(); + void AddGhostJoin(OutPt *op, const IntPoint offPt); + bool JoinPoints(Join *j, OutRec *outRec1, OutRec *outRec2); + void JoinCommonEdges(); + void DoSimplePolygons(); + void FixupFirstLefts1(OutRec *OldOutRec, OutRec *NewOutRec); + void FixupFirstLefts2(OutRec *InnerOutRec, OutRec *OuterOutRec); + void FixupFirstLefts3(OutRec *OldOutRec, OutRec *NewOutRec); +#ifdef use_xyz + void SetZ(IntPoint &pt, TEdge &e1, TEdge &e2); +#endif +}; +//------------------------------------------------------------------------------ + +class ClipperOffset { +public: + ClipperOffset(double miterLimit = 2.0, double roundPrecision = 0.25); + ~ClipperOffset(); + void AddPath(const Path &path, JoinType joinType, EndType endType); + void AddPaths(const Paths &paths, JoinType joinType, EndType endType); + void Execute(Paths &solution, double delta); + void Execute(PolyTree &solution, double delta); + void Clear(); + double MiterLimit; + double ArcTolerance; + +private: + Paths m_destPolys; + Path m_srcPoly; + Path m_destPoly; + std::vector m_normals; + double m_delta, m_sinA, m_sin, m_cos; + double m_miterLim, m_StepsPerRad; + IntPoint m_lowest; + PolyNode m_polyNodes; + + void FixOrientations(); + void DoOffset(double delta); + void OffsetPoint(int j, int &k, JoinType jointype); + void DoSquare(int j, int k); + void DoMiter(int j, int k, double r); + void DoRound(int j, int k); +}; +//------------------------------------------------------------------------------ + +class clipperException : public std::exception { +public: + clipperException(const char *description) : m_descr(description) {} + virtual ~clipperException() throw() {} + virtual const char *what() const throw() { return m_descr.c_str(); } + +private: + std::string m_descr; +}; +//------------------------------------------------------------------------------ + +} // ClipperLib namespace + +#endif // clipper_hpp diff --git a/deploy/cpp_infer/include/ocr_det.h b/deploy/cpp_infer/include/ocr_det.h new file mode 100644 index 00000000..efcaa25c --- /dev/null +++ b/deploy/cpp_infer/include/ocr_det.h @@ -0,0 +1,71 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include "paddle_api.h" +#include "paddle_inference_api.h" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +namespace PaddleOCR { + +class DBDetector { +public: + explicit DBDetector(const std::string &model_dir, bool use_gpu = false, + const int gpu_id = 0, const int max_side_len = 960) { + LoadModel(model_dir, use_gpu); + this->max_side_len_ = max_side_len; + } + + // Load Paddle inference model + void LoadModel(const std::string &model_dir, bool use_gpu, + const int min_subgraph_size = 3, const int batch_size = 1, + const int gpu_id = 0); + + // Run predictor + void Run(cv::Mat &img, std::vector>> &boxes); + +private: + std::shared_ptr predictor_; + + int max_side_len_ = 960; + + std::vector mean_ = {0.485f, 0.456f, 0.406f}; + std::vector scale_ = {1 / 0.229f, 1 / 0.224f, 1 / 0.225f}; + bool is_scale_ = true; + + // pre-process + ResizeImgType0 resize_op_; + Normalize normalize_op_; + Permute permute_op_; + + // post-process + PostProcessor post_processor_; +}; + +} // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/include/ocr_rec.h b/deploy/cpp_infer/include/ocr_rec.h new file mode 100644 index 00000000..50220136 --- /dev/null +++ b/deploy/cpp_infer/include/ocr_rec.h @@ -0,0 +1,81 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include "paddle_api.h" +#include "paddle_inference_api.h" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +namespace PaddleOCR { + +class CRNNRecognizer { +public: + explicit CRNNRecognizer(const std::string &model_dir, + const string label_path = "./tools/ppocr_keys_v1.txt", + bool use_gpu = false, const int gpu_id = 0) { + LoadModel(model_dir, use_gpu); + + this->label_list_ = ReadDict(label_path); + } + + // Load Paddle inference model + void LoadModel(const std::string &model_dir, bool use_gpu, + const int gpu_id = 0, const int min_subgraph_size = 3, + const int batch_size = 1); + + void Run(std::vector>> boxes, cv::Mat &img); + +private: + std::shared_ptr predictor_; + + std::vector label_list_; + + std::vector mean_ = {0.5f, 0.5f, 0.5f}; + std::vector scale_ = {1 / 0.5f, 1 / 0.5f, 1 / 0.5f}; + bool is_scale_ = true; + + // pre-process + CrnnResizeImg resize_op_; + Normalize normalize_op_; + Permute permute_op_; + + // post-process + PostProcessor post_processor_; + + cv::Mat get_rotate_crop_image(const cv::Mat &srcimage, + std::vector> box); + + std::vector ReadDict(const std::string &path); + + template + inline size_t argmax(ForwardIterator first, ForwardIterator last) { + return std::distance(first, std::max_element(first, last)); + } + +}; // class CrnnRecognizer + +} // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/include/postprocess_op.h b/deploy/cpp_infer/include/postprocess_op.h new file mode 100644 index 00000000..d851e29e --- /dev/null +++ b/deploy/cpp_infer/include/postprocess_op.h @@ -0,0 +1,111 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "include/clipper.h" + +using namespace std; + +namespace PaddleOCR { + +inline std::vector ReadDict(std::string path) { + std::ifstream in(path); + std::string filename; + std::string line; + std::vector m_vec; + if (in) { + while (getline(in, line)) { + m_vec.push_back(line); + } + } else { + std::cout << "no such file" << std::endl; + } + return m_vec; +} + +template +inline size_t Argmax(ForwardIterator first, ForwardIterator last) { + return std::distance(first, std::max_element(first, last)); +} + +class PostProcessor { +public: + void GetContourArea(float **box, float unclip_ratio, float &distance); + + cv::RotatedRect unclip(float **box); + + float **Mat2Vec(cv::Mat mat); + + void quickSort_vector(std::vector> &box, int l, int r, + int axis); + + std::vector> + order_points_clockwise(std::vector> pts); + + float **get_mini_boxes(cv::RotatedRect box, float &ssid); + + float box_score_fast(float **box_array, cv::Mat pred); + + std::vector>> + boxes_from_bitmap(const cv::Mat pred, const cv::Mat bitmap); + + std::vector>> + filter_tag_det_res(std::vector>> boxes, + float ratio_h, float ratio_w, cv::Mat srcimg); + + template + inline size_t argmax(ForwardIterator first, ForwardIterator last) { + return std::distance(first, std::max_element(first, last)); + } + + // CRNN + +private: + void quickSort(float **s, int l, int r); + + inline int _max(int a, int b) { return a >= b ? a : b; } + + inline int _min(int a, int b) { return a >= b ? b : a; } + + template inline T clamp(T x, T min, T max) { + if (x > max) + return max; + if (x < min) + return min; + return x; + } + inline float clampf(float x, float min, float max) { + if (x > max) + return max; + if (x < min) + return min; + return x; + } +}; + +} // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/include/preprocess_op.h b/deploy/cpp_infer/include/preprocess_op.h new file mode 100644 index 00000000..61f80449 --- /dev/null +++ b/deploy/cpp_infer/include/preprocess_op.h @@ -0,0 +1,60 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace std; +using namespace paddle; + +namespace PaddleOCR { + +class Normalize { +public: + virtual void Run(cv::Mat *im, const std::vector &mean, + const std::vector &scale, const bool is_scale = true); +}; + +// RGB -> CHW +class Permute { +public: + virtual void Run(const cv::Mat *im, float *data); +}; + +// RGB -> CHW +class ResizeImgType0 { +public: + virtual void Run(const cv::Mat &img, cv::Mat &resize_img, int max_size_len, + float &ratio_h, float &ratio_w); +}; + +class CrnnResizeImg { +public: + virtual void Run(const cv::Mat &img, cv::Mat &resize_img, float wh_ratio, + const std::vector rec_image_shape = {3, 32, 320}); +}; + +} // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/src/clipper.cpp b/deploy/cpp_infer/src/clipper.cpp new file mode 100644 index 00000000..b35c25f2 --- /dev/null +++ b/deploy/cpp_infer/src/clipper.cpp @@ -0,0 +1,4380 @@ +/******************************************************************************* +* * +* Author : Angus Johnson * +* Version : 6.4.2 * +* Date : 27 February 2017 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2017 * +* * +* License: * +* Use, modification & distribution is subject to Boost Software License Ver 1. * +* http://www.boost.org/LICENSE_1_0.txt * +* * +* Attributions: * +* The code in this library is an extension of Bala Vatti's clipping algorithm: * +* "A generic solution to polygon clipping" * +* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * +* http://portal.acm.org/citation.cfm?id=129906 * +* * +* Computer graphics and geometric modeling: implementation and algorithms * +* By Max K. Agoston * +* Springer; 1 edition (January 4, 2005) * +* http://books.google.com/books?q=vatti+clipping+agoston * +* * +* See also: * +* "Polygon Offsetting by Computing Winding Numbers" * +* Paper no. DETC2005-85513 pp. 565-575 * +* ASME 2005 International Design Engineering Technical Conferences * +* and Computers and Information in Engineering Conference (IDETC/CIE2005) * +* September 24-28, 2005 , Long Beach, California, USA * +* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * +* * +*******************************************************************************/ + +/******************************************************************************* +* * +* This is a translation of the Delphi Clipper library and the naming style * +* used has retained a Delphi flavour. * +* * +*******************************************************************************/ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "include/clipper.h" + +namespace ClipperLib { + +static double const pi = 3.141592653589793238; +static double const two_pi = pi * 2; +static double const def_arc_tolerance = 0.25; + +enum Direction { dRightToLeft, dLeftToRight }; + +static int const Unassigned = -1; // edge not currently 'owning' a solution +static int const Skip = -2; // edge that would otherwise close a path + +#define HORIZONTAL (-1.0E+40) +#define TOLERANCE (1.0e-20) +#define NEAR_ZERO(val) (((val) > -TOLERANCE) && ((val) < TOLERANCE)) + +struct TEdge { + IntPoint Bot; + IntPoint Curr; // current (updated for every new scanbeam) + IntPoint Top; + double Dx; + PolyType PolyTyp; + EdgeSide Side; // side only refers to current side of solution poly + int WindDelta; // 1 or -1 depending on winding direction + int WindCnt; + int WindCnt2; // winding count of the opposite polytype + int OutIdx; + TEdge *Next; + TEdge *Prev; + TEdge *NextInLML; + TEdge *NextInAEL; + TEdge *PrevInAEL; + TEdge *NextInSEL; + TEdge *PrevInSEL; +}; + +struct IntersectNode { + TEdge *Edge1; + TEdge *Edge2; + IntPoint Pt; +}; + +struct LocalMinimum { + cInt Y; + TEdge *LeftBound; + TEdge *RightBound; +}; + +struct OutPt; + +// OutRec: contains a path in the clipping solution. Edges in the AEL will +// carry a pointer to an OutRec when they are part of the clipping solution. +struct OutRec { + int Idx; + bool IsHole; + bool IsOpen; + OutRec *FirstLeft; // see comments in clipper.pas + PolyNode *PolyNd; + OutPt *Pts; + OutPt *BottomPt; +}; + +struct OutPt { + int Idx; + IntPoint Pt; + OutPt *Next; + OutPt *Prev; +}; + +struct Join { + OutPt *OutPt1; + OutPt *OutPt2; + IntPoint OffPt; +}; + +struct LocMinSorter { + inline bool operator()(const LocalMinimum &locMin1, + const LocalMinimum &locMin2) { + return locMin2.Y < locMin1.Y; + } +}; + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ + +inline cInt Round(double val) { + if ((val < 0)) + return static_cast(val - 0.5); + else + return static_cast(val + 0.5); +} +//------------------------------------------------------------------------------ + +inline cInt Abs(cInt val) { return val < 0 ? -val : val; } + +//------------------------------------------------------------------------------ +// PolyTree methods ... +//------------------------------------------------------------------------------ + +void PolyTree::Clear() { + for (PolyNodes::size_type i = 0; i < AllNodes.size(); ++i) + delete AllNodes[i]; + AllNodes.resize(0); + Childs.resize(0); +} +//------------------------------------------------------------------------------ + +PolyNode *PolyTree::GetFirst() const { + if (!Childs.empty()) + return Childs[0]; + else + return 0; +} +//------------------------------------------------------------------------------ + +int PolyTree::Total() const { + int result = (int)AllNodes.size(); + // with negative offsets, ignore the hidden outer polygon ... + if (result > 0 && Childs[0] != AllNodes[0]) + result--; + return result; +} + +//------------------------------------------------------------------------------ +// PolyNode methods ... +//------------------------------------------------------------------------------ + +PolyNode::PolyNode() : Parent(0), Index(0), m_IsOpen(false) {} +//------------------------------------------------------------------------------ + +int PolyNode::ChildCount() const { return (int)Childs.size(); } +//------------------------------------------------------------------------------ + +void PolyNode::AddChild(PolyNode &child) { + unsigned cnt = (unsigned)Childs.size(); + Childs.push_back(&child); + child.Parent = this; + child.Index = cnt; +} +//------------------------------------------------------------------------------ + +PolyNode *PolyNode::GetNext() const { + if (!Childs.empty()) + return Childs[0]; + else + return GetNextSiblingUp(); +} +//------------------------------------------------------------------------------ + +PolyNode *PolyNode::GetNextSiblingUp() const { + if (!Parent) // protects against PolyTree.GetNextSiblingUp() + return 0; + else if (Index == Parent->Childs.size() - 1) + return Parent->GetNextSiblingUp(); + else + return Parent->Childs[Index + 1]; +} +//------------------------------------------------------------------------------ + +bool PolyNode::IsHole() const { + bool result = true; + PolyNode *node = Parent; + while (node) { + result = !result; + node = node->Parent; + } + return result; +} +//------------------------------------------------------------------------------ + +bool PolyNode::IsOpen() const { return m_IsOpen; } +//------------------------------------------------------------------------------ + +#ifndef use_int32 + +//------------------------------------------------------------------------------ +// Int128 class (enables safe math on signed 64bit integers) +// eg Int128 val1((long64)9223372036854775807); //ie 2^63 -1 +// Int128 val2((long64)9223372036854775807); +// Int128 val3 = val1 * val2; +// val3.AsString => "85070591730234615847396907784232501249" (8.5e+37) +//------------------------------------------------------------------------------ + +class Int128 { +public: + ulong64 lo; + long64 hi; + + Int128(long64 _lo = 0) { + lo = (ulong64)_lo; + if (_lo < 0) + hi = -1; + else + hi = 0; + } + + Int128(const Int128 &val) : lo(val.lo), hi(val.hi) {} + + Int128(const long64 &_hi, const ulong64 &_lo) : lo(_lo), hi(_hi) {} + + Int128 &operator=(const long64 &val) { + lo = (ulong64)val; + if (val < 0) + hi = -1; + else + hi = 0; + return *this; + } + + bool operator==(const Int128 &val) const { + return (hi == val.hi && lo == val.lo); + } + + bool operator!=(const Int128 &val) const { return !(*this == val); } + + bool operator>(const Int128 &val) const { + if (hi != val.hi) + return hi > val.hi; + else + return lo > val.lo; + } + + bool operator<(const Int128 &val) const { + if (hi != val.hi) + return hi < val.hi; + else + return lo < val.lo; + } + + bool operator>=(const Int128 &val) const { return !(*this < val); } + + bool operator<=(const Int128 &val) const { return !(*this > val); } + + Int128 &operator+=(const Int128 &rhs) { + hi += rhs.hi; + lo += rhs.lo; + if (lo < rhs.lo) + hi++; + return *this; + } + + Int128 operator+(const Int128 &rhs) const { + Int128 result(*this); + result += rhs; + return result; + } + + Int128 &operator-=(const Int128 &rhs) { + *this += -rhs; + return *this; + } + + Int128 operator-(const Int128 &rhs) const { + Int128 result(*this); + result -= rhs; + return result; + } + + Int128 operator-() const // unary negation + { + if (lo == 0) + return Int128(-hi, 0); + else + return Int128(~hi, ~lo + 1); + } + + operator double() const { + const double shift64 = 18446744073709551616.0; // 2^64 + if (hi < 0) { + if (lo == 0) + return (double)hi * shift64; + else + return -(double)(~lo + ~hi * shift64); + } else + return (double)(lo + hi * shift64); + } +}; +//------------------------------------------------------------------------------ + +Int128 Int128Mul(long64 lhs, long64 rhs) { + bool negate = (lhs < 0) != (rhs < 0); + + if (lhs < 0) + lhs = -lhs; + ulong64 int1Hi = ulong64(lhs) >> 32; + ulong64 int1Lo = ulong64(lhs & 0xFFFFFFFF); + + if (rhs < 0) + rhs = -rhs; + ulong64 int2Hi = ulong64(rhs) >> 32; + ulong64 int2Lo = ulong64(rhs & 0xFFFFFFFF); + + // nb: see comments in clipper.pas + ulong64 a = int1Hi * int2Hi; + ulong64 b = int1Lo * int2Lo; + ulong64 c = int1Hi * int2Lo + int1Lo * int2Hi; + + Int128 tmp; + tmp.hi = long64(a + (c >> 32)); + tmp.lo = long64(c << 32); + tmp.lo += long64(b); + if (tmp.lo < b) + tmp.hi++; + if (negate) + tmp = -tmp; + return tmp; +}; +#endif + +//------------------------------------------------------------------------------ +// Miscellaneous global functions +//------------------------------------------------------------------------------ + +bool Orientation(const Path &poly) { return Area(poly) >= 0; } +//------------------------------------------------------------------------------ + +double Area(const Path &poly) { + int size = (int)poly.size(); + if (size < 3) + return 0; + + double a = 0; + for (int i = 0, j = size - 1; i < size; ++i) { + a += ((double)poly[j].X + poly[i].X) * ((double)poly[j].Y - poly[i].Y); + j = i; + } + return -a * 0.5; +} +//------------------------------------------------------------------------------ + +double Area(const OutPt *op) { + const OutPt *startOp = op; + if (!op) + return 0; + double a = 0; + do { + a += (double)(op->Prev->Pt.X + op->Pt.X) * + (double)(op->Prev->Pt.Y - op->Pt.Y); + op = op->Next; + } while (op != startOp); + return a * 0.5; +} +//------------------------------------------------------------------------------ + +double Area(const OutRec &outRec) { return Area(outRec.Pts); } +//------------------------------------------------------------------------------ + +bool PointIsVertex(const IntPoint &Pt, OutPt *pp) { + OutPt *pp2 = pp; + do { + if (pp2->Pt == Pt) + return true; + pp2 = pp2->Next; + } while (pp2 != pp); + return false; +} +//------------------------------------------------------------------------------ + +// See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & +// Agathos +// http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf +int PointInPolygon(const IntPoint &pt, const Path &path) { + // returns 0 if false, +1 if true, -1 if pt ON polygon boundary + int result = 0; + size_t cnt = path.size(); + if (cnt < 3) + return 0; + IntPoint ip = path[0]; + for (size_t i = 1; i <= cnt; ++i) { + IntPoint ipNext = (i == cnt ? path[0] : path[i]); + if (ipNext.Y == pt.Y) { + if ((ipNext.X == pt.X) || + (ip.Y == pt.Y && ((ipNext.X > pt.X) == (ip.X < pt.X)))) + return -1; + } + if ((ip.Y < pt.Y) != (ipNext.Y < pt.Y)) { + if (ip.X >= pt.X) { + if (ipNext.X > pt.X) + result = 1 - result; + else { + double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) - + (double)(ipNext.X - pt.X) * (ip.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (ipNext.Y > ip.Y)) + result = 1 - result; + } + } else { + if (ipNext.X > pt.X) { + double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) - + (double)(ipNext.X - pt.X) * (ip.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (ipNext.Y > ip.Y)) + result = 1 - result; + } + } + } + ip = ipNext; + } + return result; +} +//------------------------------------------------------------------------------ + +int PointInPolygon(const IntPoint &pt, OutPt *op) { + // returns 0 if false, +1 if true, -1 if pt ON polygon boundary + int result = 0; + OutPt *startOp = op; + for (;;) { + if (op->Next->Pt.Y == pt.Y) { + if ((op->Next->Pt.X == pt.X) || + (op->Pt.Y == pt.Y && ((op->Next->Pt.X > pt.X) == (op->Pt.X < pt.X)))) + return -1; + } + if ((op->Pt.Y < pt.Y) != (op->Next->Pt.Y < pt.Y)) { + if (op->Pt.X >= pt.X) { + if (op->Next->Pt.X > pt.X) + result = 1 - result; + else { + double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) - + (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y)) + result = 1 - result; + } + } else { + if (op->Next->Pt.X > pt.X) { + double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) - + (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y)) + result = 1 - result; + } + } + } + op = op->Next; + if (startOp == op) + break; + } + return result; +} +//------------------------------------------------------------------------------ + +bool Poly2ContainsPoly1(OutPt *OutPt1, OutPt *OutPt2) { + OutPt *op = OutPt1; + do { + // nb: PointInPolygon returns 0 if false, +1 if true, -1 if pt on polygon + int res = PointInPolygon(op->Pt, OutPt2); + if (res >= 0) + return res > 0; + op = op->Next; + } while (op != OutPt1); + return true; +} +//---------------------------------------------------------------------- + +bool SlopesEqual(const TEdge &e1, const TEdge &e2, bool UseFullInt64Range) { +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(e1.Top.Y - e1.Bot.Y, e2.Top.X - e2.Bot.X) == + Int128Mul(e1.Top.X - e1.Bot.X, e2.Top.Y - e2.Bot.Y); + else +#endif + return (e1.Top.Y - e1.Bot.Y) * (e2.Top.X - e2.Bot.X) == + (e1.Top.X - e1.Bot.X) * (e2.Top.Y - e2.Bot.Y); +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3, + bool UseFullInt64Range) { +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(pt1.Y - pt2.Y, pt2.X - pt3.X) == + Int128Mul(pt1.X - pt2.X, pt2.Y - pt3.Y); + else +#endif + return (pt1.Y - pt2.Y) * (pt2.X - pt3.X) == + (pt1.X - pt2.X) * (pt2.Y - pt3.Y); +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3, + const IntPoint pt4, bool UseFullInt64Range) { +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(pt1.Y - pt2.Y, pt3.X - pt4.X) == + Int128Mul(pt1.X - pt2.X, pt3.Y - pt4.Y); + else +#endif + return (pt1.Y - pt2.Y) * (pt3.X - pt4.X) == + (pt1.X - pt2.X) * (pt3.Y - pt4.Y); +} +//------------------------------------------------------------------------------ + +inline bool IsHorizontal(TEdge &e) { return e.Dx == HORIZONTAL; } +//------------------------------------------------------------------------------ + +inline double GetDx(const IntPoint pt1, const IntPoint pt2) { + return (pt1.Y == pt2.Y) ? HORIZONTAL + : (double)(pt2.X - pt1.X) / (pt2.Y - pt1.Y); +} +//--------------------------------------------------------------------------- + +inline void SetDx(TEdge &e) { + cInt dy = (e.Top.Y - e.Bot.Y); + if (dy == 0) + e.Dx = HORIZONTAL; + else + e.Dx = (double)(e.Top.X - e.Bot.X) / dy; +} +//--------------------------------------------------------------------------- + +inline void SwapSides(TEdge &Edge1, TEdge &Edge2) { + EdgeSide Side = Edge1.Side; + Edge1.Side = Edge2.Side; + Edge2.Side = Side; +} +//------------------------------------------------------------------------------ + +inline void SwapPolyIndexes(TEdge &Edge1, TEdge &Edge2) { + int OutIdx = Edge1.OutIdx; + Edge1.OutIdx = Edge2.OutIdx; + Edge2.OutIdx = OutIdx; +} +//------------------------------------------------------------------------------ + +inline cInt TopX(TEdge &edge, const cInt currentY) { + return (currentY == edge.Top.Y) + ? edge.Top.X + : edge.Bot.X + Round(edge.Dx * (currentY - edge.Bot.Y)); +} +//------------------------------------------------------------------------------ + +void IntersectPoint(TEdge &Edge1, TEdge &Edge2, IntPoint &ip) { +#ifdef use_xyz + ip.Z = 0; +#endif + + double b1, b2; + if (Edge1.Dx == Edge2.Dx) { + ip.Y = Edge1.Curr.Y; + ip.X = TopX(Edge1, ip.Y); + return; + } else if (Edge1.Dx == 0) { + ip.X = Edge1.Bot.X; + if (IsHorizontal(Edge2)) + ip.Y = Edge2.Bot.Y; + else { + b2 = Edge2.Bot.Y - (Edge2.Bot.X / Edge2.Dx); + ip.Y = Round(ip.X / Edge2.Dx + b2); + } + } else if (Edge2.Dx == 0) { + ip.X = Edge2.Bot.X; + if (IsHorizontal(Edge1)) + ip.Y = Edge1.Bot.Y; + else { + b1 = Edge1.Bot.Y - (Edge1.Bot.X / Edge1.Dx); + ip.Y = Round(ip.X / Edge1.Dx + b1); + } + } else { + b1 = Edge1.Bot.X - Edge1.Bot.Y * Edge1.Dx; + b2 = Edge2.Bot.X - Edge2.Bot.Y * Edge2.Dx; + double q = (b2 - b1) / (Edge1.Dx - Edge2.Dx); + ip.Y = Round(q); + if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) + ip.X = Round(Edge1.Dx * q + b1); + else + ip.X = Round(Edge2.Dx * q + b2); + } + + if (ip.Y < Edge1.Top.Y || ip.Y < Edge2.Top.Y) { + if (Edge1.Top.Y > Edge2.Top.Y) + ip.Y = Edge1.Top.Y; + else + ip.Y = Edge2.Top.Y; + if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) + ip.X = TopX(Edge1, ip.Y); + else + ip.X = TopX(Edge2, ip.Y); + } + // finally, don't allow 'ip' to be BELOW curr.Y (ie bottom of scanbeam) ... + if (ip.Y > Edge1.Curr.Y) { + ip.Y = Edge1.Curr.Y; + // use the more vertical edge to derive X ... + if (std::fabs(Edge1.Dx) > std::fabs(Edge2.Dx)) + ip.X = TopX(Edge2, ip.Y); + else + ip.X = TopX(Edge1, ip.Y); + } +} +//------------------------------------------------------------------------------ + +void ReversePolyPtLinks(OutPt *pp) { + if (!pp) + return; + OutPt *pp1, *pp2; + pp1 = pp; + do { + pp2 = pp1->Next; + pp1->Next = pp1->Prev; + pp1->Prev = pp2; + pp1 = pp2; + } while (pp1 != pp); +} +//------------------------------------------------------------------------------ + +void DisposeOutPts(OutPt *&pp) { + if (pp == 0) + return; + pp->Prev->Next = 0; + while (pp) { + OutPt *tmpPp = pp; + pp = pp->Next; + delete tmpPp; + } +} +//------------------------------------------------------------------------------ + +inline void InitEdge(TEdge *e, TEdge *eNext, TEdge *ePrev, const IntPoint &Pt) { + std::memset(e, 0, sizeof(TEdge)); + e->Next = eNext; + e->Prev = ePrev; + e->Curr = Pt; + e->OutIdx = Unassigned; +} +//------------------------------------------------------------------------------ + +void InitEdge2(TEdge &e, PolyType Pt) { + if (e.Curr.Y >= e.Next->Curr.Y) { + e.Bot = e.Curr; + e.Top = e.Next->Curr; + } else { + e.Top = e.Curr; + e.Bot = e.Next->Curr; + } + SetDx(e); + e.PolyTyp = Pt; +} +//------------------------------------------------------------------------------ + +TEdge *RemoveEdge(TEdge *e) { + // removes e from double_linked_list (but without removing from memory) + e->Prev->Next = e->Next; + e->Next->Prev = e->Prev; + TEdge *result = e->Next; + e->Prev = 0; // flag as removed (see ClipperBase.Clear) + return result; +} +//------------------------------------------------------------------------------ + +inline void ReverseHorizontal(TEdge &e) { + // swap horizontal edges' Top and Bottom x's so they follow the natural + // progression of the bounds - ie so their xbots will align with the + // adjoining lower edge. [Helpful in the ProcessHorizontal() method.] + std::swap(e.Top.X, e.Bot.X); +#ifdef use_xyz + std::swap(e.Top.Z, e.Bot.Z); +#endif +} +//------------------------------------------------------------------------------ + +void SwapPoints(IntPoint &pt1, IntPoint &pt2) { + IntPoint tmp = pt1; + pt1 = pt2; + pt2 = tmp; +} +//------------------------------------------------------------------------------ + +bool GetOverlapSegment(IntPoint pt1a, IntPoint pt1b, IntPoint pt2a, + IntPoint pt2b, IntPoint &pt1, IntPoint &pt2) { + // precondition: segments are Collinear. + if (Abs(pt1a.X - pt1b.X) > Abs(pt1a.Y - pt1b.Y)) { + if (pt1a.X > pt1b.X) + SwapPoints(pt1a, pt1b); + if (pt2a.X > pt2b.X) + SwapPoints(pt2a, pt2b); + if (pt1a.X > pt2a.X) + pt1 = pt1a; + else + pt1 = pt2a; + if (pt1b.X < pt2b.X) + pt2 = pt1b; + else + pt2 = pt2b; + return pt1.X < pt2.X; + } else { + if (pt1a.Y < pt1b.Y) + SwapPoints(pt1a, pt1b); + if (pt2a.Y < pt2b.Y) + SwapPoints(pt2a, pt2b); + if (pt1a.Y < pt2a.Y) + pt1 = pt1a; + else + pt1 = pt2a; + if (pt1b.Y > pt2b.Y) + pt2 = pt1b; + else + pt2 = pt2b; + return pt1.Y > pt2.Y; + } +} +//------------------------------------------------------------------------------ + +bool FirstIsBottomPt(const OutPt *btmPt1, const OutPt *btmPt2) { + OutPt *p = btmPt1->Prev; + while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) + p = p->Prev; + double dx1p = std::fabs(GetDx(btmPt1->Pt, p->Pt)); + p = btmPt1->Next; + while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) + p = p->Next; + double dx1n = std::fabs(GetDx(btmPt1->Pt, p->Pt)); + + p = btmPt2->Prev; + while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) + p = p->Prev; + double dx2p = std::fabs(GetDx(btmPt2->Pt, p->Pt)); + p = btmPt2->Next; + while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) + p = p->Next; + double dx2n = std::fabs(GetDx(btmPt2->Pt, p->Pt)); + + if (std::max(dx1p, dx1n) == std::max(dx2p, dx2n) && + std::min(dx1p, dx1n) == std::min(dx2p, dx2n)) + return Area(btmPt1) > 0; // if otherwise identical use orientation + else + return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n); +} +//------------------------------------------------------------------------------ + +OutPt *GetBottomPt(OutPt *pp) { + OutPt *dups = 0; + OutPt *p = pp->Next; + while (p != pp) { + if (p->Pt.Y > pp->Pt.Y) { + pp = p; + dups = 0; + } else if (p->Pt.Y == pp->Pt.Y && p->Pt.X <= pp->Pt.X) { + if (p->Pt.X < pp->Pt.X) { + dups = 0; + pp = p; + } else { + if (p->Next != pp && p->Prev != pp) + dups = p; + } + } + p = p->Next; + } + if (dups) { + // there appears to be at least 2 vertices at BottomPt so ... + while (dups != p) { + if (!FirstIsBottomPt(p, dups)) + pp = dups; + dups = dups->Next; + while (dups->Pt != pp->Pt) + dups = dups->Next; + } + } + return pp; +} +//------------------------------------------------------------------------------ + +bool Pt2IsBetweenPt1AndPt3(const IntPoint pt1, const IntPoint pt2, + const IntPoint pt3) { + if ((pt1 == pt3) || (pt1 == pt2) || (pt3 == pt2)) + return false; + else if (pt1.X != pt3.X) + return (pt2.X > pt1.X) == (pt2.X < pt3.X); + else + return (pt2.Y > pt1.Y) == (pt2.Y < pt3.Y); +} +//------------------------------------------------------------------------------ + +bool HorzSegmentsOverlap(cInt seg1a, cInt seg1b, cInt seg2a, cInt seg2b) { + if (seg1a > seg1b) + std::swap(seg1a, seg1b); + if (seg2a > seg2b) + std::swap(seg2a, seg2b); + return (seg1a < seg2b) && (seg2a < seg1b); +} + +//------------------------------------------------------------------------------ +// ClipperBase class methods ... +//------------------------------------------------------------------------------ + +ClipperBase::ClipperBase() // constructor +{ + m_CurrentLM = m_MinimaList.begin(); // begin() == end() here + m_UseFullRange = false; +} +//------------------------------------------------------------------------------ + +ClipperBase::~ClipperBase() // destructor +{ + Clear(); +} +//------------------------------------------------------------------------------ + +void RangeTest(const IntPoint &Pt, bool &useFullRange) { + if (useFullRange) { + if (Pt.X > hiRange || Pt.Y > hiRange || -Pt.X > hiRange || -Pt.Y > hiRange) + throw clipperException("Coordinate outside allowed range"); + } else if (Pt.X > loRange || Pt.Y > loRange || -Pt.X > loRange || + -Pt.Y > loRange) { + useFullRange = true; + RangeTest(Pt, useFullRange); + } +} +//------------------------------------------------------------------------------ + +TEdge *FindNextLocMin(TEdge *E) { + for (;;) { + while (E->Bot != E->Prev->Bot || E->Curr == E->Top) + E = E->Next; + if (!IsHorizontal(*E) && !IsHorizontal(*E->Prev)) + break; + while (IsHorizontal(*E->Prev)) + E = E->Prev; + TEdge *E2 = E; + while (IsHorizontal(*E)) + E = E->Next; + if (E->Top.Y == E->Prev->Bot.Y) + continue; // ie just an intermediate horz. + if (E2->Prev->Bot.X < E->Bot.X) + E = E2; + break; + } + return E; +} +//------------------------------------------------------------------------------ + +TEdge *ClipperBase::ProcessBound(TEdge *E, bool NextIsForward) { + TEdge *Result = E; + TEdge *Horz = 0; + + if (E->OutIdx == Skip) { + // if edges still remain in the current bound beyond the skip edge then + // create another LocMin and call ProcessBound once more + if (NextIsForward) { + while (E->Top.Y == E->Next->Bot.Y) + E = E->Next; + // don't include top horizontals when parsing a bound a second time, + // they will be contained in the opposite bound ... + while (E != Result && IsHorizontal(*E)) + E = E->Prev; + } else { + while (E->Top.Y == E->Prev->Bot.Y) + E = E->Prev; + while (E != Result && IsHorizontal(*E)) + E = E->Next; + } + + if (E == Result) { + if (NextIsForward) + Result = E->Next; + else + Result = E->Prev; + } else { + // there are more edges in the bound beyond result starting with E + if (NextIsForward) + E = Result->Next; + else + E = Result->Prev; + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + locMin.LeftBound = 0; + locMin.RightBound = E; + E->WindDelta = 0; + Result = ProcessBound(E, NextIsForward); + m_MinimaList.push_back(locMin); + } + return Result; + } + + TEdge *EStart; + + if (IsHorizontal(*E)) { + // We need to be careful with open paths because this may not be a + // true local minima (ie E may be following a skip edge). + // Also, consecutive horz. edges may start heading left before going right. + if (NextIsForward) + EStart = E->Prev; + else + EStart = E->Next; + if (IsHorizontal(*EStart)) // ie an adjoining horizontal skip edge + { + if (EStart->Bot.X != E->Bot.X && EStart->Top.X != E->Bot.X) + ReverseHorizontal(*E); + } else if (EStart->Bot.X != E->Bot.X) + ReverseHorizontal(*E); + } + + EStart = E; + if (NextIsForward) { + while (Result->Top.Y == Result->Next->Bot.Y && Result->Next->OutIdx != Skip) + Result = Result->Next; + if (IsHorizontal(*Result) && Result->Next->OutIdx != Skip) { + // nb: at the top of a bound, horizontals are added to the bound + // only when the preceding edge attaches to the horizontal's left vertex + // unless a Skip edge is encountered when that becomes the top divide + Horz = Result; + while (IsHorizontal(*Horz->Prev)) + Horz = Horz->Prev; + if (Horz->Prev->Top.X > Result->Next->Top.X) + Result = Horz->Prev; + } + while (E != Result) { + E->NextInLML = E->Next; + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X) + ReverseHorizontal(*E); + E = E->Next; + } + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X) + ReverseHorizontal(*E); + Result = Result->Next; // move to the edge just beyond current bound + } else { + while (Result->Top.Y == Result->Prev->Bot.Y && Result->Prev->OutIdx != Skip) + Result = Result->Prev; + if (IsHorizontal(*Result) && Result->Prev->OutIdx != Skip) { + Horz = Result; + while (IsHorizontal(*Horz->Next)) + Horz = Horz->Next; + if (Horz->Next->Top.X == Result->Prev->Top.X || + Horz->Next->Top.X > Result->Prev->Top.X) + Result = Horz->Next; + } + + while (E != Result) { + E->NextInLML = E->Prev; + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X) + ReverseHorizontal(*E); + E = E->Prev; + } + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X) + ReverseHorizontal(*E); + Result = Result->Prev; // move to the edge just beyond current bound + } + + return Result; +} +//------------------------------------------------------------------------------ + +bool ClipperBase::AddPath(const Path &pg, PolyType PolyTyp, bool Closed) { +#ifdef use_lines + if (!Closed && PolyTyp == ptClip) + throw clipperException("AddPath: Open paths must be subject."); +#else + if (!Closed) + throw clipperException("AddPath: Open paths have been disabled."); +#endif + + int highI = (int)pg.size() - 1; + if (Closed) + while (highI > 0 && (pg[highI] == pg[0])) + --highI; + while (highI > 0 && (pg[highI] == pg[highI - 1])) + --highI; + if ((Closed && highI < 2) || (!Closed && highI < 1)) + return false; + + // create a new edge array ... + TEdge *edges = new TEdge[highI + 1]; + + bool IsFlat = true; + // 1. Basic (first) edge initialization ... + try { + edges[1].Curr = pg[1]; + RangeTest(pg[0], m_UseFullRange); + RangeTest(pg[highI], m_UseFullRange); + InitEdge(&edges[0], &edges[1], &edges[highI], pg[0]); + InitEdge(&edges[highI], &edges[0], &edges[highI - 1], pg[highI]); + for (int i = highI - 1; i >= 1; --i) { + RangeTest(pg[i], m_UseFullRange); + InitEdge(&edges[i], &edges[i + 1], &edges[i - 1], pg[i]); + } + } catch (...) { + delete[] edges; + throw; // range test fails + } + TEdge *eStart = &edges[0]; + + // 2. Remove duplicate vertices, and (when closed) collinear edges ... + TEdge *E = eStart, *eLoopStop = eStart; + for (;;) { + // nb: allows matching start and end points when not Closed ... + if (E->Curr == E->Next->Curr && (Closed || E->Next != eStart)) { + if (E == E->Next) + break; + if (E == eStart) + eStart = E->Next; + E = RemoveEdge(E); + eLoopStop = E; + continue; + } + if (E->Prev == E->Next) + break; // only two vertices + else if (Closed && SlopesEqual(E->Prev->Curr, E->Curr, E->Next->Curr, + m_UseFullRange) && + (!m_PreserveCollinear || + !Pt2IsBetweenPt1AndPt3(E->Prev->Curr, E->Curr, E->Next->Curr))) { + // Collinear edges are allowed for open paths but in closed paths + // the default is to merge adjacent collinear edges into a single edge. + // However, if the PreserveCollinear property is enabled, only overlapping + // collinear edges (ie spikes) will be removed from closed paths. + if (E == eStart) + eStart = E->Next; + E = RemoveEdge(E); + E = E->Prev; + eLoopStop = E; + continue; + } + E = E->Next; + if ((E == eLoopStop) || (!Closed && E->Next == eStart)) + break; + } + + if ((!Closed && (E == E->Next)) || (Closed && (E->Prev == E->Next))) { + delete[] edges; + return false; + } + + if (!Closed) { + m_HasOpenPaths = true; + eStart->Prev->OutIdx = Skip; + } + + // 3. Do second stage of edge initialization ... + E = eStart; + do { + InitEdge2(*E, PolyTyp); + E = E->Next; + if (IsFlat && E->Curr.Y != eStart->Curr.Y) + IsFlat = false; + } while (E != eStart); + + // 4. Finally, add edge bounds to LocalMinima list ... + + // Totally flat paths must be handled differently when adding them + // to LocalMinima list to avoid endless loops etc ... + if (IsFlat) { + if (Closed) { + delete[] edges; + return false; + } + E->Prev->OutIdx = Skip; + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + locMin.LeftBound = 0; + locMin.RightBound = E; + locMin.RightBound->Side = esRight; + locMin.RightBound->WindDelta = 0; + for (;;) { + if (E->Bot.X != E->Prev->Top.X) + ReverseHorizontal(*E); + if (E->Next->OutIdx == Skip) + break; + E->NextInLML = E->Next; + E = E->Next; + } + m_MinimaList.push_back(locMin); + m_edges.push_back(edges); + return true; + } + + m_edges.push_back(edges); + bool leftBoundIsForward; + TEdge *EMin = 0; + + // workaround to avoid an endless loop in the while loop below when + // open paths have matching start and end points ... + if (E->Prev->Bot == E->Prev->Top) + E = E->Next; + + for (;;) { + E = FindNextLocMin(E); + if (E == EMin) + break; + else if (!EMin) + EMin = E; + + // E and E.Prev now share a local minima (left aligned if horizontal). + // Compare their slopes to find which starts which bound ... + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + if (E->Dx < E->Prev->Dx) { + locMin.LeftBound = E->Prev; + locMin.RightBound = E; + leftBoundIsForward = false; // Q.nextInLML = Q.prev + } else { + locMin.LeftBound = E; + locMin.RightBound = E->Prev; + leftBoundIsForward = true; // Q.nextInLML = Q.next + } + + if (!Closed) + locMin.LeftBound->WindDelta = 0; + else if (locMin.LeftBound->Next == locMin.RightBound) + locMin.LeftBound->WindDelta = -1; + else + locMin.LeftBound->WindDelta = 1; + locMin.RightBound->WindDelta = -locMin.LeftBound->WindDelta; + + E = ProcessBound(locMin.LeftBound, leftBoundIsForward); + if (E->OutIdx == Skip) + E = ProcessBound(E, leftBoundIsForward); + + TEdge *E2 = ProcessBound(locMin.RightBound, !leftBoundIsForward); + if (E2->OutIdx == Skip) + E2 = ProcessBound(E2, !leftBoundIsForward); + + if (locMin.LeftBound->OutIdx == Skip) + locMin.LeftBound = 0; + else if (locMin.RightBound->OutIdx == Skip) + locMin.RightBound = 0; + m_MinimaList.push_back(locMin); + if (!leftBoundIsForward) + E = E2; + } + return true; +} +//------------------------------------------------------------------------------ + +bool ClipperBase::AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed) { + bool result = false; + for (Paths::size_type i = 0; i < ppg.size(); ++i) + if (AddPath(ppg[i], PolyTyp, Closed)) + result = true; + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::Clear() { + DisposeLocalMinimaList(); + for (EdgeList::size_type i = 0; i < m_edges.size(); ++i) { + TEdge *edges = m_edges[i]; + delete[] edges; + } + m_edges.clear(); + m_UseFullRange = false; + m_HasOpenPaths = false; +} +//------------------------------------------------------------------------------ + +void ClipperBase::Reset() { + m_CurrentLM = m_MinimaList.begin(); + if (m_CurrentLM == m_MinimaList.end()) + return; // ie nothing to process + std::sort(m_MinimaList.begin(), m_MinimaList.end(), LocMinSorter()); + + m_Scanbeam = ScanbeamList(); // clears/resets priority_queue + // reset all edges ... + for (MinimaList::iterator lm = m_MinimaList.begin(); lm != m_MinimaList.end(); + ++lm) { + InsertScanbeam(lm->Y); + TEdge *e = lm->LeftBound; + if (e) { + e->Curr = e->Bot; + e->Side = esLeft; + e->OutIdx = Unassigned; + } + + e = lm->RightBound; + if (e) { + e->Curr = e->Bot; + e->Side = esRight; + e->OutIdx = Unassigned; + } + } + m_ActiveEdges = 0; + m_CurrentLM = m_MinimaList.begin(); +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeLocalMinimaList() { + m_MinimaList.clear(); + m_CurrentLM = m_MinimaList.begin(); +} +//------------------------------------------------------------------------------ + +bool ClipperBase::PopLocalMinima(cInt Y, const LocalMinimum *&locMin) { + if (m_CurrentLM == m_MinimaList.end() || (*m_CurrentLM).Y != Y) + return false; + locMin = &(*m_CurrentLM); + ++m_CurrentLM; + return true; +} +//------------------------------------------------------------------------------ + +IntRect ClipperBase::GetBounds() { + IntRect result; + MinimaList::iterator lm = m_MinimaList.begin(); + if (lm == m_MinimaList.end()) { + result.left = result.top = result.right = result.bottom = 0; + return result; + } + result.left = lm->LeftBound->Bot.X; + result.top = lm->LeftBound->Bot.Y; + result.right = lm->LeftBound->Bot.X; + result.bottom = lm->LeftBound->Bot.Y; + while (lm != m_MinimaList.end()) { + // todo - needs fixing for open paths + result.bottom = std::max(result.bottom, lm->LeftBound->Bot.Y); + TEdge *e = lm->LeftBound; + for (;;) { + TEdge *bottomE = e; + while (e->NextInLML) { + if (e->Bot.X < result.left) + result.left = e->Bot.X; + if (e->Bot.X > result.right) + result.right = e->Bot.X; + e = e->NextInLML; + } + result.left = std::min(result.left, e->Bot.X); + result.right = std::max(result.right, e->Bot.X); + result.left = std::min(result.left, e->Top.X); + result.right = std::max(result.right, e->Top.X); + result.top = std::min(result.top, e->Top.Y); + if (bottomE == lm->LeftBound) + e = lm->RightBound; + else + break; + } + ++lm; + } + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::InsertScanbeam(const cInt Y) { m_Scanbeam.push(Y); } +//------------------------------------------------------------------------------ + +bool ClipperBase::PopScanbeam(cInt &Y) { + if (m_Scanbeam.empty()) + return false; + Y = m_Scanbeam.top(); + m_Scanbeam.pop(); + while (!m_Scanbeam.empty() && Y == m_Scanbeam.top()) { + m_Scanbeam.pop(); + } // Pop duplicates. + return true; +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeAllOutRecs() { + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + DisposeOutRec(i); + m_PolyOuts.clear(); +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeOutRec(PolyOutList::size_type index) { + OutRec *outRec = m_PolyOuts[index]; + if (outRec->Pts) + DisposeOutPts(outRec->Pts); + delete outRec; + m_PolyOuts[index] = 0; +} +//------------------------------------------------------------------------------ + +void ClipperBase::DeleteFromAEL(TEdge *e) { + TEdge *AelPrev = e->PrevInAEL; + TEdge *AelNext = e->NextInAEL; + if (!AelPrev && !AelNext && (e != m_ActiveEdges)) + return; // already deleted + if (AelPrev) + AelPrev->NextInAEL = AelNext; + else + m_ActiveEdges = AelNext; + if (AelNext) + AelNext->PrevInAEL = AelPrev; + e->NextInAEL = 0; + e->PrevInAEL = 0; +} +//------------------------------------------------------------------------------ + +OutRec *ClipperBase::CreateOutRec() { + OutRec *result = new OutRec; + result->IsHole = false; + result->IsOpen = false; + result->FirstLeft = 0; + result->Pts = 0; + result->BottomPt = 0; + result->PolyNd = 0; + m_PolyOuts.push_back(result); + result->Idx = (int)m_PolyOuts.size() - 1; + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::SwapPositionsInAEL(TEdge *Edge1, TEdge *Edge2) { + // check that one or other edge hasn't already been removed from AEL ... + if (Edge1->NextInAEL == Edge1->PrevInAEL || + Edge2->NextInAEL == Edge2->PrevInAEL) + return; + + if (Edge1->NextInAEL == Edge2) { + TEdge *Next = Edge2->NextInAEL; + if (Next) + Next->PrevInAEL = Edge1; + TEdge *Prev = Edge1->PrevInAEL; + if (Prev) + Prev->NextInAEL = Edge2; + Edge2->PrevInAEL = Prev; + Edge2->NextInAEL = Edge1; + Edge1->PrevInAEL = Edge2; + Edge1->NextInAEL = Next; + } else if (Edge2->NextInAEL == Edge1) { + TEdge *Next = Edge1->NextInAEL; + if (Next) + Next->PrevInAEL = Edge2; + TEdge *Prev = Edge2->PrevInAEL; + if (Prev) + Prev->NextInAEL = Edge1; + Edge1->PrevInAEL = Prev; + Edge1->NextInAEL = Edge2; + Edge2->PrevInAEL = Edge1; + Edge2->NextInAEL = Next; + } else { + TEdge *Next = Edge1->NextInAEL; + TEdge *Prev = Edge1->PrevInAEL; + Edge1->NextInAEL = Edge2->NextInAEL; + if (Edge1->NextInAEL) + Edge1->NextInAEL->PrevInAEL = Edge1; + Edge1->PrevInAEL = Edge2->PrevInAEL; + if (Edge1->PrevInAEL) + Edge1->PrevInAEL->NextInAEL = Edge1; + Edge2->NextInAEL = Next; + if (Edge2->NextInAEL) + Edge2->NextInAEL->PrevInAEL = Edge2; + Edge2->PrevInAEL = Prev; + if (Edge2->PrevInAEL) + Edge2->PrevInAEL->NextInAEL = Edge2; + } + + if (!Edge1->PrevInAEL) + m_ActiveEdges = Edge1; + else if (!Edge2->PrevInAEL) + m_ActiveEdges = Edge2; +} +//------------------------------------------------------------------------------ + +void ClipperBase::UpdateEdgeIntoAEL(TEdge *&e) { + if (!e->NextInLML) + throw clipperException("UpdateEdgeIntoAEL: invalid call"); + + e->NextInLML->OutIdx = e->OutIdx; + TEdge *AelPrev = e->PrevInAEL; + TEdge *AelNext = e->NextInAEL; + if (AelPrev) + AelPrev->NextInAEL = e->NextInLML; + else + m_ActiveEdges = e->NextInLML; + if (AelNext) + AelNext->PrevInAEL = e->NextInLML; + e->NextInLML->Side = e->Side; + e->NextInLML->WindDelta = e->WindDelta; + e->NextInLML->WindCnt = e->WindCnt; + e->NextInLML->WindCnt2 = e->WindCnt2; + e = e->NextInLML; + e->Curr = e->Bot; + e->PrevInAEL = AelPrev; + e->NextInAEL = AelNext; + if (!IsHorizontal(*e)) + InsertScanbeam(e->Top.Y); +} +//------------------------------------------------------------------------------ + +bool ClipperBase::LocalMinimaPending() { + return (m_CurrentLM != m_MinimaList.end()); +} + +//------------------------------------------------------------------------------ +// TClipper methods ... +//------------------------------------------------------------------------------ + +Clipper::Clipper(int initOptions) + : ClipperBase() // constructor +{ + m_ExecuteLocked = false; + m_UseFullRange = false; + m_ReverseOutput = ((initOptions & ioReverseSolution) != 0); + m_StrictSimple = ((initOptions & ioStrictlySimple) != 0); + m_PreserveCollinear = ((initOptions & ioPreserveCollinear) != 0); + m_HasOpenPaths = false; +#ifdef use_xyz + m_ZFill = 0; +#endif +} +//------------------------------------------------------------------------------ + +#ifdef use_xyz +void Clipper::ZFillFunction(ZFillCallback zFillFunc) { m_ZFill = zFillFunc; } +//------------------------------------------------------------------------------ +#endif + +bool Clipper::Execute(ClipType clipType, Paths &solution, + PolyFillType fillType) { + return Execute(clipType, solution, fillType, fillType); +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, PolyTree &polytree, + PolyFillType fillType) { + return Execute(clipType, polytree, fillType, fillType); +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, Paths &solution, + PolyFillType subjFillType, PolyFillType clipFillType) { + if (m_ExecuteLocked) + return false; + if (m_HasOpenPaths) + throw clipperException( + "Error: PolyTree struct is needed for open path clipping."); + m_ExecuteLocked = true; + solution.resize(0); + m_SubjFillType = subjFillType; + m_ClipFillType = clipFillType; + m_ClipType = clipType; + m_UsingPolyTree = false; + bool succeeded = ExecuteInternal(); + if (succeeded) + BuildResult(solution); + DisposeAllOutRecs(); + m_ExecuteLocked = false; + return succeeded; +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, PolyTree &polytree, + PolyFillType subjFillType, PolyFillType clipFillType) { + if (m_ExecuteLocked) + return false; + m_ExecuteLocked = true; + m_SubjFillType = subjFillType; + m_ClipFillType = clipFillType; + m_ClipType = clipType; + m_UsingPolyTree = true; + bool succeeded = ExecuteInternal(); + if (succeeded) + BuildResult2(polytree); + DisposeAllOutRecs(); + m_ExecuteLocked = false; + return succeeded; +} +//------------------------------------------------------------------------------ + +void Clipper::FixHoleLinkage(OutRec &outrec) { + // skip OutRecs that (a) contain outermost polygons or + //(b) already have the correct owner/child linkage ... + if (!outrec.FirstLeft || + (outrec.IsHole != outrec.FirstLeft->IsHole && outrec.FirstLeft->Pts)) + return; + + OutRec *orfl = outrec.FirstLeft; + while (orfl && ((orfl->IsHole == outrec.IsHole) || !orfl->Pts)) + orfl = orfl->FirstLeft; + outrec.FirstLeft = orfl; +} +//------------------------------------------------------------------------------ + +bool Clipper::ExecuteInternal() { + bool succeeded = true; + try { + Reset(); + m_Maxima = MaximaList(); + m_SortedEdges = 0; + + succeeded = true; + cInt botY, topY; + if (!PopScanbeam(botY)) + return false; + InsertLocalMinimaIntoAEL(botY); + while (PopScanbeam(topY) || LocalMinimaPending()) { + ProcessHorizontals(); + ClearGhostJoins(); + if (!ProcessIntersections(topY)) { + succeeded = false; + break; + } + ProcessEdgesAtTopOfScanbeam(topY); + botY = topY; + InsertLocalMinimaIntoAEL(botY); + } + } catch (...) { + succeeded = false; + } + + if (succeeded) { + // fix orientations ... + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->Pts || outRec->IsOpen) + continue; + if ((outRec->IsHole ^ m_ReverseOutput) == (Area(*outRec) > 0)) + ReversePolyPtLinks(outRec->Pts); + } + + if (!m_Joins.empty()) + JoinCommonEdges(); + + // unfortunately FixupOutPolygon() must be done after JoinCommonEdges() + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->Pts) + continue; + if (outRec->IsOpen) + FixupOutPolyline(*outRec); + else + FixupOutPolygon(*outRec); + } + + if (m_StrictSimple) + DoSimplePolygons(); + } + + ClearJoins(); + ClearGhostJoins(); + return succeeded; +} +//------------------------------------------------------------------------------ + +void Clipper::SetWindingCount(TEdge &edge) { + TEdge *e = edge.PrevInAEL; + // find the edge of the same polytype that immediately preceeds 'edge' in AEL + while (e && ((e->PolyTyp != edge.PolyTyp) || (e->WindDelta == 0))) + e = e->PrevInAEL; + if (!e) { + if (edge.WindDelta == 0) { + PolyFillType pft = + (edge.PolyTyp == ptSubject ? m_SubjFillType : m_ClipFillType); + edge.WindCnt = (pft == pftNegative ? -1 : 1); + } else + edge.WindCnt = edge.WindDelta; + edge.WindCnt2 = 0; + e = m_ActiveEdges; // ie get ready to calc WindCnt2 + } else if (edge.WindDelta == 0 && m_ClipType != ctUnion) { + edge.WindCnt = 1; + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; // ie get ready to calc WindCnt2 + } else if (IsEvenOddFillType(edge)) { + // EvenOdd filling ... + if (edge.WindDelta == 0) { + // are we inside a subj polygon ... + bool Inside = true; + TEdge *e2 = e->PrevInAEL; + while (e2) { + if (e2->PolyTyp == e->PolyTyp && e2->WindDelta != 0) + Inside = !Inside; + e2 = e2->PrevInAEL; + } + edge.WindCnt = (Inside ? 0 : 1); + } else { + edge.WindCnt = edge.WindDelta; + } + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; // ie get ready to calc WindCnt2 + } else { + // nonZero, Positive or Negative filling ... + if (e->WindCnt * e->WindDelta < 0) { + // prev edge is 'decreasing' WindCount (WC) toward zero + // so we're outside the previous polygon ... + if (Abs(e->WindCnt) > 1) { + // outside prev poly but still inside another. + // when reversing direction of prev poly use the same WC + if (e->WindDelta * edge.WindDelta < 0) + edge.WindCnt = e->WindCnt; + // otherwise continue to 'decrease' WC ... + else + edge.WindCnt = e->WindCnt + edge.WindDelta; + } else + // now outside all polys of same polytype so set own WC ... + edge.WindCnt = (edge.WindDelta == 0 ? 1 : edge.WindDelta); + } else { + // prev edge is 'increasing' WindCount (WC) away from zero + // so we're inside the previous polygon ... + if (edge.WindDelta == 0) + edge.WindCnt = (e->WindCnt < 0 ? e->WindCnt - 1 : e->WindCnt + 1); + // if wind direction is reversing prev then use same WC + else if (e->WindDelta * edge.WindDelta < 0) + edge.WindCnt = e->WindCnt; + // otherwise add to WC ... + else + edge.WindCnt = e->WindCnt + edge.WindDelta; + } + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; // ie get ready to calc WindCnt2 + } + + // update WindCnt2 ... + if (IsEvenOddAltFillType(edge)) { + // EvenOdd filling ... + while (e != &edge) { + if (e->WindDelta != 0) + edge.WindCnt2 = (edge.WindCnt2 == 0 ? 1 : 0); + e = e->NextInAEL; + } + } else { + // nonZero, Positive or Negative filling ... + while (e != &edge) { + edge.WindCnt2 += e->WindDelta; + e = e->NextInAEL; + } + } +} +//------------------------------------------------------------------------------ + +bool Clipper::IsEvenOddFillType(const TEdge &edge) const { + if (edge.PolyTyp == ptSubject) + return m_SubjFillType == pftEvenOdd; + else + return m_ClipFillType == pftEvenOdd; +} +//------------------------------------------------------------------------------ + +bool Clipper::IsEvenOddAltFillType(const TEdge &edge) const { + if (edge.PolyTyp == ptSubject) + return m_ClipFillType == pftEvenOdd; + else + return m_SubjFillType == pftEvenOdd; +} +//------------------------------------------------------------------------------ + +bool Clipper::IsContributing(const TEdge &edge) const { + PolyFillType pft, pft2; + if (edge.PolyTyp == ptSubject) { + pft = m_SubjFillType; + pft2 = m_ClipFillType; + } else { + pft = m_ClipFillType; + pft2 = m_SubjFillType; + } + + switch (pft) { + case pftEvenOdd: + // return false if a subj line has been flagged as inside a subj polygon + if (edge.WindDelta == 0 && edge.WindCnt != 1) + return false; + break; + case pftNonZero: + if (Abs(edge.WindCnt) != 1) + return false; + break; + case pftPositive: + if (edge.WindCnt != 1) + return false; + break; + default: // pftNegative + if (edge.WindCnt != -1) + return false; + } + + switch (m_ClipType) { + case ctIntersection: + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 != 0); + case pftPositive: + return (edge.WindCnt2 > 0); + default: + return (edge.WindCnt2 < 0); + } + break; + case ctUnion: + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + break; + case ctDifference: + if (edge.PolyTyp == ptSubject) + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + else + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 != 0); + case pftPositive: + return (edge.WindCnt2 > 0); + default: + return (edge.WindCnt2 < 0); + } + break; + case ctXor: + if (edge.WindDelta == 0) // XOr always contributing unless open + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + else + return true; + break; + default: + return true; + } +} +//------------------------------------------------------------------------------ + +OutPt *Clipper::AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) { + OutPt *result; + TEdge *e, *prevE; + if (IsHorizontal(*e2) || (e1->Dx > e2->Dx)) { + result = AddOutPt(e1, Pt); + e2->OutIdx = e1->OutIdx; + e1->Side = esLeft; + e2->Side = esRight; + e = e1; + if (e->PrevInAEL == e2) + prevE = e2->PrevInAEL; + else + prevE = e->PrevInAEL; + } else { + result = AddOutPt(e2, Pt); + e1->OutIdx = e2->OutIdx; + e1->Side = esRight; + e2->Side = esLeft; + e = e2; + if (e->PrevInAEL == e1) + prevE = e1->PrevInAEL; + else + prevE = e->PrevInAEL; + } + + if (prevE && prevE->OutIdx >= 0 && prevE->Top.Y < Pt.Y && e->Top.Y < Pt.Y) { + cInt xPrev = TopX(*prevE, Pt.Y); + cInt xE = TopX(*e, Pt.Y); + if (xPrev == xE && (e->WindDelta != 0) && (prevE->WindDelta != 0) && + SlopesEqual(IntPoint(xPrev, Pt.Y), prevE->Top, IntPoint(xE, Pt.Y), + e->Top, m_UseFullRange)) { + OutPt *outPt = AddOutPt(prevE, Pt); + AddJoin(result, outPt, e->Top); + } + } + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) { + AddOutPt(e1, Pt); + if (e2->WindDelta == 0) + AddOutPt(e2, Pt); + if (e1->OutIdx == e2->OutIdx) { + e1->OutIdx = Unassigned; + e2->OutIdx = Unassigned; + } else if (e1->OutIdx < e2->OutIdx) + AppendPolygon(e1, e2); + else + AppendPolygon(e2, e1); +} +//------------------------------------------------------------------------------ + +void Clipper::AddEdgeToSEL(TEdge *edge) { + // SEL pointers in PEdge are reused to build a list of horizontal edges. + // However, we don't need to worry about order with horizontal edge + // processing. + if (!m_SortedEdges) { + m_SortedEdges = edge; + edge->PrevInSEL = 0; + edge->NextInSEL = 0; + } else { + edge->NextInSEL = m_SortedEdges; + edge->PrevInSEL = 0; + m_SortedEdges->PrevInSEL = edge; + m_SortedEdges = edge; + } +} +//------------------------------------------------------------------------------ + +bool Clipper::PopEdgeFromSEL(TEdge *&edge) { + if (!m_SortedEdges) + return false; + edge = m_SortedEdges; + DeleteFromSEL(m_SortedEdges); + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::CopyAELToSEL() { + TEdge *e = m_ActiveEdges; + m_SortedEdges = e; + while (e) { + e->PrevInSEL = e->PrevInAEL; + e->NextInSEL = e->NextInAEL; + e = e->NextInAEL; + } +} +//------------------------------------------------------------------------------ + +void Clipper::AddJoin(OutPt *op1, OutPt *op2, const IntPoint OffPt) { + Join *j = new Join; + j->OutPt1 = op1; + j->OutPt2 = op2; + j->OffPt = OffPt; + m_Joins.push_back(j); +} +//------------------------------------------------------------------------------ + +void Clipper::ClearJoins() { + for (JoinList::size_type i = 0; i < m_Joins.size(); i++) + delete m_Joins[i]; + m_Joins.resize(0); +} +//------------------------------------------------------------------------------ + +void Clipper::ClearGhostJoins() { + for (JoinList::size_type i = 0; i < m_GhostJoins.size(); i++) + delete m_GhostJoins[i]; + m_GhostJoins.resize(0); +} +//------------------------------------------------------------------------------ + +void Clipper::AddGhostJoin(OutPt *op, const IntPoint OffPt) { + Join *j = new Join; + j->OutPt1 = op; + j->OutPt2 = 0; + j->OffPt = OffPt; + m_GhostJoins.push_back(j); +} +//------------------------------------------------------------------------------ + +void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) { + const LocalMinimum *lm; + while (PopLocalMinima(botY, lm)) { + TEdge *lb = lm->LeftBound; + TEdge *rb = lm->RightBound; + + OutPt *Op1 = 0; + if (!lb) { + // nb: don't insert LB into either AEL or SEL + InsertEdgeIntoAEL(rb, 0); + SetWindingCount(*rb); + if (IsContributing(*rb)) + Op1 = AddOutPt(rb, rb->Bot); + } else if (!rb) { + InsertEdgeIntoAEL(lb, 0); + SetWindingCount(*lb); + if (IsContributing(*lb)) + Op1 = AddOutPt(lb, lb->Bot); + InsertScanbeam(lb->Top.Y); + } else { + InsertEdgeIntoAEL(lb, 0); + InsertEdgeIntoAEL(rb, lb); + SetWindingCount(*lb); + rb->WindCnt = lb->WindCnt; + rb->WindCnt2 = lb->WindCnt2; + if (IsContributing(*lb)) + Op1 = AddLocalMinPoly(lb, rb, lb->Bot); + InsertScanbeam(lb->Top.Y); + } + + if (rb) { + if (IsHorizontal(*rb)) { + AddEdgeToSEL(rb); + if (rb->NextInLML) + InsertScanbeam(rb->NextInLML->Top.Y); + } else + InsertScanbeam(rb->Top.Y); + } + + if (!lb || !rb) + continue; + + // if any output polygons share an edge, they'll need joining later ... + if (Op1 && IsHorizontal(*rb) && m_GhostJoins.size() > 0 && + (rb->WindDelta != 0)) { + for (JoinList::size_type i = 0; i < m_GhostJoins.size(); ++i) { + Join *jr = m_GhostJoins[i]; + // if the horizontal Rb and a 'ghost' horizontal overlap, then convert + // the 'ghost' join to a real join ready for later ... + if (HorzSegmentsOverlap(jr->OutPt1->Pt.X, jr->OffPt.X, rb->Bot.X, + rb->Top.X)) + AddJoin(jr->OutPt1, Op1, jr->OffPt); + } + } + + if (lb->OutIdx >= 0 && lb->PrevInAEL && + lb->PrevInAEL->Curr.X == lb->Bot.X && lb->PrevInAEL->OutIdx >= 0 && + SlopesEqual(lb->PrevInAEL->Bot, lb->PrevInAEL->Top, lb->Curr, lb->Top, + m_UseFullRange) && + (lb->WindDelta != 0) && (lb->PrevInAEL->WindDelta != 0)) { + OutPt *Op2 = AddOutPt(lb->PrevInAEL, lb->Bot); + AddJoin(Op1, Op2, lb->Top); + } + + if (lb->NextInAEL != rb) { + + if (rb->OutIdx >= 0 && rb->PrevInAEL->OutIdx >= 0 && + SlopesEqual(rb->PrevInAEL->Curr, rb->PrevInAEL->Top, rb->Curr, + rb->Top, m_UseFullRange) && + (rb->WindDelta != 0) && (rb->PrevInAEL->WindDelta != 0)) { + OutPt *Op2 = AddOutPt(rb->PrevInAEL, rb->Bot); + AddJoin(Op1, Op2, rb->Top); + } + + TEdge *e = lb->NextInAEL; + if (e) { + while (e != rb) { + // nb: For calculating winding counts etc, IntersectEdges() assumes + // that param1 will be to the Right of param2 ABOVE the intersection + // ... + IntersectEdges(rb, e, lb->Curr); // order important here + e = e->NextInAEL; + } + } + } + } +} +//------------------------------------------------------------------------------ + +void Clipper::DeleteFromSEL(TEdge *e) { + TEdge *SelPrev = e->PrevInSEL; + TEdge *SelNext = e->NextInSEL; + if (!SelPrev && !SelNext && (e != m_SortedEdges)) + return; // already deleted + if (SelPrev) + SelPrev->NextInSEL = SelNext; + else + m_SortedEdges = SelNext; + if (SelNext) + SelNext->PrevInSEL = SelPrev; + e->NextInSEL = 0; + e->PrevInSEL = 0; +} +//------------------------------------------------------------------------------ + +#ifdef use_xyz +void Clipper::SetZ(IntPoint &pt, TEdge &e1, TEdge &e2) { + if (pt.Z != 0 || !m_ZFill) + return; + else if (pt == e1.Bot) + pt.Z = e1.Bot.Z; + else if (pt == e1.Top) + pt.Z = e1.Top.Z; + else if (pt == e2.Bot) + pt.Z = e2.Bot.Z; + else if (pt == e2.Top) + pt.Z = e2.Top.Z; + else + (*m_ZFill)(e1.Bot, e1.Top, e2.Bot, e2.Top, pt); +} +//------------------------------------------------------------------------------ +#endif + +void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &Pt) { + bool e1Contributing = (e1->OutIdx >= 0); + bool e2Contributing = (e2->OutIdx >= 0); + +#ifdef use_xyz + SetZ(Pt, *e1, *e2); +#endif + +#ifdef use_lines + // if either edge is on an OPEN path ... + if (e1->WindDelta == 0 || e2->WindDelta == 0) { + // ignore subject-subject open path intersections UNLESS they + // are both open paths, AND they are both 'contributing maximas' ... + if (e1->WindDelta == 0 && e2->WindDelta == 0) + return; + + // if intersecting a subj line with a subj poly ... + else if (e1->PolyTyp == e2->PolyTyp && e1->WindDelta != e2->WindDelta && + m_ClipType == ctUnion) { + if (e1->WindDelta == 0) { + if (e2Contributing) { + AddOutPt(e1, Pt); + if (e1Contributing) + e1->OutIdx = Unassigned; + } + } else { + if (e1Contributing) { + AddOutPt(e2, Pt); + if (e2Contributing) + e2->OutIdx = Unassigned; + } + } + } else if (e1->PolyTyp != e2->PolyTyp) { + // toggle subj open path OutIdx on/off when Abs(clip.WndCnt) == 1 ... + if ((e1->WindDelta == 0) && abs(e2->WindCnt) == 1 && + (m_ClipType != ctUnion || e2->WindCnt2 == 0)) { + AddOutPt(e1, Pt); + if (e1Contributing) + e1->OutIdx = Unassigned; + } else if ((e2->WindDelta == 0) && (abs(e1->WindCnt) == 1) && + (m_ClipType != ctUnion || e1->WindCnt2 == 0)) { + AddOutPt(e2, Pt); + if (e2Contributing) + e2->OutIdx = Unassigned; + } + } + return; + } +#endif + + // update winding counts... + // assumes that e1 will be to the Right of e2 ABOVE the intersection + if (e1->PolyTyp == e2->PolyTyp) { + if (IsEvenOddFillType(*e1)) { + int oldE1WindCnt = e1->WindCnt; + e1->WindCnt = e2->WindCnt; + e2->WindCnt = oldE1WindCnt; + } else { + if (e1->WindCnt + e2->WindDelta == 0) + e1->WindCnt = -e1->WindCnt; + else + e1->WindCnt += e2->WindDelta; + if (e2->WindCnt - e1->WindDelta == 0) + e2->WindCnt = -e2->WindCnt; + else + e2->WindCnt -= e1->WindDelta; + } + } else { + if (!IsEvenOddFillType(*e2)) + e1->WindCnt2 += e2->WindDelta; + else + e1->WindCnt2 = (e1->WindCnt2 == 0) ? 1 : 0; + if (!IsEvenOddFillType(*e1)) + e2->WindCnt2 -= e1->WindDelta; + else + e2->WindCnt2 = (e2->WindCnt2 == 0) ? 1 : 0; + } + + PolyFillType e1FillType, e2FillType, e1FillType2, e2FillType2; + if (e1->PolyTyp == ptSubject) { + e1FillType = m_SubjFillType; + e1FillType2 = m_ClipFillType; + } else { + e1FillType = m_ClipFillType; + e1FillType2 = m_SubjFillType; + } + if (e2->PolyTyp == ptSubject) { + e2FillType = m_SubjFillType; + e2FillType2 = m_ClipFillType; + } else { + e2FillType = m_ClipFillType; + e2FillType2 = m_SubjFillType; + } + + cInt e1Wc, e2Wc; + switch (e1FillType) { + case pftPositive: + e1Wc = e1->WindCnt; + break; + case pftNegative: + e1Wc = -e1->WindCnt; + break; + default: + e1Wc = Abs(e1->WindCnt); + } + switch (e2FillType) { + case pftPositive: + e2Wc = e2->WindCnt; + break; + case pftNegative: + e2Wc = -e2->WindCnt; + break; + default: + e2Wc = Abs(e2->WindCnt); + } + + if (e1Contributing && e2Contributing) { + if ((e1Wc != 0 && e1Wc != 1) || (e2Wc != 0 && e2Wc != 1) || + (e1->PolyTyp != e2->PolyTyp && m_ClipType != ctXor)) { + AddLocalMaxPoly(e1, e2, Pt); + } else { + AddOutPt(e1, Pt); + AddOutPt(e2, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } else if (e1Contributing) { + if (e2Wc == 0 || e2Wc == 1) { + AddOutPt(e1, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } else if (e2Contributing) { + if (e1Wc == 0 || e1Wc == 1) { + AddOutPt(e2, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } else if ((e1Wc == 0 || e1Wc == 1) && (e2Wc == 0 || e2Wc == 1)) { + // neither edge is currently contributing ... + + cInt e1Wc2, e2Wc2; + switch (e1FillType2) { + case pftPositive: + e1Wc2 = e1->WindCnt2; + break; + case pftNegative: + e1Wc2 = -e1->WindCnt2; + break; + default: + e1Wc2 = Abs(e1->WindCnt2); + } + switch (e2FillType2) { + case pftPositive: + e2Wc2 = e2->WindCnt2; + break; + case pftNegative: + e2Wc2 = -e2->WindCnt2; + break; + default: + e2Wc2 = Abs(e2->WindCnt2); + } + + if (e1->PolyTyp != e2->PolyTyp) { + AddLocalMinPoly(e1, e2, Pt); + } else if (e1Wc == 1 && e2Wc == 1) + switch (m_ClipType) { + case ctIntersection: + if (e1Wc2 > 0 && e2Wc2 > 0) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctUnion: + if (e1Wc2 <= 0 && e2Wc2 <= 0) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctDifference: + if (((e1->PolyTyp == ptClip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || + ((e1->PolyTyp == ptSubject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctXor: + AddLocalMinPoly(e1, e2, Pt); + } + else + SwapSides(*e1, *e2); + } +} +//------------------------------------------------------------------------------ + +void Clipper::SetHoleState(TEdge *e, OutRec *outrec) { + TEdge *e2 = e->PrevInAEL; + TEdge *eTmp = 0; + while (e2) { + if (e2->OutIdx >= 0 && e2->WindDelta != 0) { + if (!eTmp) + eTmp = e2; + else if (eTmp->OutIdx == e2->OutIdx) + eTmp = 0; + } + e2 = e2->PrevInAEL; + } + if (!eTmp) { + outrec->FirstLeft = 0; + outrec->IsHole = false; + } else { + outrec->FirstLeft = m_PolyOuts[eTmp->OutIdx]; + outrec->IsHole = !outrec->FirstLeft->IsHole; + } +} +//------------------------------------------------------------------------------ + +OutRec *GetLowermostRec(OutRec *outRec1, OutRec *outRec2) { + // work out which polygon fragment has the correct hole state ... + if (!outRec1->BottomPt) + outRec1->BottomPt = GetBottomPt(outRec1->Pts); + if (!outRec2->BottomPt) + outRec2->BottomPt = GetBottomPt(outRec2->Pts); + OutPt *OutPt1 = outRec1->BottomPt; + OutPt *OutPt2 = outRec2->BottomPt; + if (OutPt1->Pt.Y > OutPt2->Pt.Y) + return outRec1; + else if (OutPt1->Pt.Y < OutPt2->Pt.Y) + return outRec2; + else if (OutPt1->Pt.X < OutPt2->Pt.X) + return outRec1; + else if (OutPt1->Pt.X > OutPt2->Pt.X) + return outRec2; + else if (OutPt1->Next == OutPt1) + return outRec2; + else if (OutPt2->Next == OutPt2) + return outRec1; + else if (FirstIsBottomPt(OutPt1, OutPt2)) + return outRec1; + else + return outRec2; +} +//------------------------------------------------------------------------------ + +bool OutRec1RightOfOutRec2(OutRec *outRec1, OutRec *outRec2) { + do { + outRec1 = outRec1->FirstLeft; + if (outRec1 == outRec2) + return true; + } while (outRec1); + return false; +} +//------------------------------------------------------------------------------ + +OutRec *Clipper::GetOutRec(int Idx) { + OutRec *outrec = m_PolyOuts[Idx]; + while (outrec != m_PolyOuts[outrec->Idx]) + outrec = m_PolyOuts[outrec->Idx]; + return outrec; +} +//------------------------------------------------------------------------------ + +void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) { + // get the start and ends of both output polygons ... + OutRec *outRec1 = m_PolyOuts[e1->OutIdx]; + OutRec *outRec2 = m_PolyOuts[e2->OutIdx]; + + OutRec *holeStateRec; + if (OutRec1RightOfOutRec2(outRec1, outRec2)) + holeStateRec = outRec2; + else if (OutRec1RightOfOutRec2(outRec2, outRec1)) + holeStateRec = outRec1; + else + holeStateRec = GetLowermostRec(outRec1, outRec2); + + // get the start and ends of both output polygons and + // join e2 poly onto e1 poly and delete pointers to e2 ... + + OutPt *p1_lft = outRec1->Pts; + OutPt *p1_rt = p1_lft->Prev; + OutPt *p2_lft = outRec2->Pts; + OutPt *p2_rt = p2_lft->Prev; + + // join e2 poly onto e1 poly and delete pointers to e2 ... + if (e1->Side == esLeft) { + if (e2->Side == esLeft) { + // z y x a b c + ReversePolyPtLinks(p2_lft); + p2_lft->Next = p1_lft; + p1_lft->Prev = p2_lft; + p1_rt->Next = p2_rt; + p2_rt->Prev = p1_rt; + outRec1->Pts = p2_rt; + } else { + // x y z a b c + p2_rt->Next = p1_lft; + p1_lft->Prev = p2_rt; + p2_lft->Prev = p1_rt; + p1_rt->Next = p2_lft; + outRec1->Pts = p2_lft; + } + } else { + if (e2->Side == esRight) { + // a b c z y x + ReversePolyPtLinks(p2_lft); + p1_rt->Next = p2_rt; + p2_rt->Prev = p1_rt; + p2_lft->Next = p1_lft; + p1_lft->Prev = p2_lft; + } else { + // a b c x y z + p1_rt->Next = p2_lft; + p2_lft->Prev = p1_rt; + p1_lft->Prev = p2_rt; + p2_rt->Next = p1_lft; + } + } + + outRec1->BottomPt = 0; + if (holeStateRec == outRec2) { + if (outRec2->FirstLeft != outRec1) + outRec1->FirstLeft = outRec2->FirstLeft; + outRec1->IsHole = outRec2->IsHole; + } + outRec2->Pts = 0; + outRec2->BottomPt = 0; + outRec2->FirstLeft = outRec1; + + int OKIdx = e1->OutIdx; + int ObsoleteIdx = e2->OutIdx; + + e1->OutIdx = + Unassigned; // nb: safe because we only get here via AddLocalMaxPoly + e2->OutIdx = Unassigned; + + TEdge *e = m_ActiveEdges; + while (e) { + if (e->OutIdx == ObsoleteIdx) { + e->OutIdx = OKIdx; + e->Side = e1->Side; + break; + } + e = e->NextInAEL; + } + + outRec2->Idx = outRec1->Idx; +} +//------------------------------------------------------------------------------ + +OutPt *Clipper::AddOutPt(TEdge *e, const IntPoint &pt) { + if (e->OutIdx < 0) { + OutRec *outRec = CreateOutRec(); + outRec->IsOpen = (e->WindDelta == 0); + OutPt *newOp = new OutPt; + outRec->Pts = newOp; + newOp->Idx = outRec->Idx; + newOp->Pt = pt; + newOp->Next = newOp; + newOp->Prev = newOp; + if (!outRec->IsOpen) + SetHoleState(e, outRec); + e->OutIdx = outRec->Idx; + return newOp; + } else { + OutRec *outRec = m_PolyOuts[e->OutIdx]; + // OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' + OutPt *op = outRec->Pts; + + bool ToFront = (e->Side == esLeft); + if (ToFront && (pt == op->Pt)) + return op; + else if (!ToFront && (pt == op->Prev->Pt)) + return op->Prev; + + OutPt *newOp = new OutPt; + newOp->Idx = outRec->Idx; + newOp->Pt = pt; + newOp->Next = op; + newOp->Prev = op->Prev; + newOp->Prev->Next = newOp; + op->Prev = newOp; + if (ToFront) + outRec->Pts = newOp; + return newOp; + } +} +//------------------------------------------------------------------------------ + +OutPt *Clipper::GetLastOutPt(TEdge *e) { + OutRec *outRec = m_PolyOuts[e->OutIdx]; + if (e->Side == esLeft) + return outRec->Pts; + else + return outRec->Pts->Prev; +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessHorizontals() { + TEdge *horzEdge; + while (PopEdgeFromSEL(horzEdge)) + ProcessHorizontal(horzEdge); +} +//------------------------------------------------------------------------------ + +inline bool IsMinima(TEdge *e) { + return e && (e->Prev->NextInLML != e) && (e->Next->NextInLML != e); +} +//------------------------------------------------------------------------------ + +inline bool IsMaxima(TEdge *e, const cInt Y) { + return e && e->Top.Y == Y && !e->NextInLML; +} +//------------------------------------------------------------------------------ + +inline bool IsIntermediate(TEdge *e, const cInt Y) { + return e->Top.Y == Y && e->NextInLML; +} +//------------------------------------------------------------------------------ + +TEdge *GetMaximaPair(TEdge *e) { + if ((e->Next->Top == e->Top) && !e->Next->NextInLML) + return e->Next; + else if ((e->Prev->Top == e->Top) && !e->Prev->NextInLML) + return e->Prev; + else + return 0; +} +//------------------------------------------------------------------------------ + +TEdge *GetMaximaPairEx(TEdge *e) { + // as GetMaximaPair() but returns 0 if MaxPair isn't in AEL (unless it's + // horizontal) + TEdge *result = GetMaximaPair(e); + if (result && + (result->OutIdx == Skip || + (result->NextInAEL == result->PrevInAEL && !IsHorizontal(*result)))) + return 0; + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::SwapPositionsInSEL(TEdge *Edge1, TEdge *Edge2) { + if (!(Edge1->NextInSEL) && !(Edge1->PrevInSEL)) + return; + if (!(Edge2->NextInSEL) && !(Edge2->PrevInSEL)) + return; + + if (Edge1->NextInSEL == Edge2) { + TEdge *Next = Edge2->NextInSEL; + if (Next) + Next->PrevInSEL = Edge1; + TEdge *Prev = Edge1->PrevInSEL; + if (Prev) + Prev->NextInSEL = Edge2; + Edge2->PrevInSEL = Prev; + Edge2->NextInSEL = Edge1; + Edge1->PrevInSEL = Edge2; + Edge1->NextInSEL = Next; + } else if (Edge2->NextInSEL == Edge1) { + TEdge *Next = Edge1->NextInSEL; + if (Next) + Next->PrevInSEL = Edge2; + TEdge *Prev = Edge2->PrevInSEL; + if (Prev) + Prev->NextInSEL = Edge1; + Edge1->PrevInSEL = Prev; + Edge1->NextInSEL = Edge2; + Edge2->PrevInSEL = Edge1; + Edge2->NextInSEL = Next; + } else { + TEdge *Next = Edge1->NextInSEL; + TEdge *Prev = Edge1->PrevInSEL; + Edge1->NextInSEL = Edge2->NextInSEL; + if (Edge1->NextInSEL) + Edge1->NextInSEL->PrevInSEL = Edge1; + Edge1->PrevInSEL = Edge2->PrevInSEL; + if (Edge1->PrevInSEL) + Edge1->PrevInSEL->NextInSEL = Edge1; + Edge2->NextInSEL = Next; + if (Edge2->NextInSEL) + Edge2->NextInSEL->PrevInSEL = Edge2; + Edge2->PrevInSEL = Prev; + if (Edge2->PrevInSEL) + Edge2->PrevInSEL->NextInSEL = Edge2; + } + + if (!Edge1->PrevInSEL) + m_SortedEdges = Edge1; + else if (!Edge2->PrevInSEL) + m_SortedEdges = Edge2; +} +//------------------------------------------------------------------------------ + +TEdge *GetNextInAEL(TEdge *e, Direction dir) { + return dir == dLeftToRight ? e->NextInAEL : e->PrevInAEL; +} +//------------------------------------------------------------------------------ + +void GetHorzDirection(TEdge &HorzEdge, Direction &Dir, cInt &Left, + cInt &Right) { + if (HorzEdge.Bot.X < HorzEdge.Top.X) { + Left = HorzEdge.Bot.X; + Right = HorzEdge.Top.X; + Dir = dLeftToRight; + } else { + Left = HorzEdge.Top.X; + Right = HorzEdge.Bot.X; + Dir = dRightToLeft; + } +} +//------------------------------------------------------------------------ + +/******************************************************************************* +* Notes: Horizontal edges (HEs) at scanline intersections (ie at the Top or * +* Bottom of a scanbeam) are processed as if layered. The order in which HEs * +* are processed doesn't matter. HEs intersect with other HE Bot.Xs only [#] * +* (or they could intersect with Top.Xs only, ie EITHER Bot.Xs OR Top.Xs), * +* and with other non-horizontal edges [*]. Once these intersections are * +* processed, intermediate HEs then 'promote' the Edge above (NextInLML) into * +* the AEL. These 'promoted' edges may in turn intersect [%] with other HEs. * +*******************************************************************************/ + +void Clipper::ProcessHorizontal(TEdge *horzEdge) { + Direction dir; + cInt horzLeft, horzRight; + bool IsOpen = (horzEdge->WindDelta == 0); + + GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); + + TEdge *eLastHorz = horzEdge, *eMaxPair = 0; + while (eLastHorz->NextInLML && IsHorizontal(*eLastHorz->NextInLML)) + eLastHorz = eLastHorz->NextInLML; + if (!eLastHorz->NextInLML) + eMaxPair = GetMaximaPair(eLastHorz); + + MaximaList::const_iterator maxIt; + MaximaList::const_reverse_iterator maxRit; + if (m_Maxima.size() > 0) { + // get the first maxima in range (X) ... + if (dir == dLeftToRight) { + maxIt = m_Maxima.begin(); + while (maxIt != m_Maxima.end() && *maxIt <= horzEdge->Bot.X) + maxIt++; + if (maxIt != m_Maxima.end() && *maxIt >= eLastHorz->Top.X) + maxIt = m_Maxima.end(); + } else { + maxRit = m_Maxima.rbegin(); + while (maxRit != m_Maxima.rend() && *maxRit > horzEdge->Bot.X) + maxRit++; + if (maxRit != m_Maxima.rend() && *maxRit <= eLastHorz->Top.X) + maxRit = m_Maxima.rend(); + } + } + + OutPt *op1 = 0; + + for (;;) // loop through consec. horizontal edges + { + + bool IsLastHorz = (horzEdge == eLastHorz); + TEdge *e = GetNextInAEL(horzEdge, dir); + while (e) { + + // this code block inserts extra coords into horizontal edges (in output + // polygons) whereever maxima touch these horizontal edges. This helps + //'simplifying' polygons (ie if the Simplify property is set). + if (m_Maxima.size() > 0) { + if (dir == dLeftToRight) { + while (maxIt != m_Maxima.end() && *maxIt < e->Curr.X) { + if (horzEdge->OutIdx >= 0 && !IsOpen) + AddOutPt(horzEdge, IntPoint(*maxIt, horzEdge->Bot.Y)); + maxIt++; + } + } else { + while (maxRit != m_Maxima.rend() && *maxRit > e->Curr.X) { + if (horzEdge->OutIdx >= 0 && !IsOpen) + AddOutPt(horzEdge, IntPoint(*maxRit, horzEdge->Bot.Y)); + maxRit++; + } + } + }; + + if ((dir == dLeftToRight && e->Curr.X > horzRight) || + (dir == dRightToLeft && e->Curr.X < horzLeft)) + break; + + // Also break if we've got to the end of an intermediate horizontal edge + // ... + // nb: Smaller Dx's are to the right of larger Dx's ABOVE the horizontal. + if (e->Curr.X == horzEdge->Top.X && horzEdge->NextInLML && + e->Dx < horzEdge->NextInLML->Dx) + break; + + if (horzEdge->OutIdx >= 0 && !IsOpen) // note: may be done multiple times + { +#ifdef use_xyz + if (dir == dLeftToRight) + SetZ(e->Curr, *horzEdge, *e); + else + SetZ(e->Curr, *e, *horzEdge); +#endif + op1 = AddOutPt(horzEdge, e->Curr); + TEdge *eNextHorz = m_SortedEdges; + while (eNextHorz) { + if (eNextHorz->OutIdx >= 0 && + HorzSegmentsOverlap(horzEdge->Bot.X, horzEdge->Top.X, + eNextHorz->Bot.X, eNextHorz->Top.X)) { + OutPt *op2 = GetLastOutPt(eNextHorz); + AddJoin(op2, op1, eNextHorz->Top); + } + eNextHorz = eNextHorz->NextInSEL; + } + AddGhostJoin(op1, horzEdge->Bot); + } + + // OK, so far we're still in range of the horizontal Edge but make sure + // we're at the last of consec. horizontals when matching with eMaxPair + if (e == eMaxPair && IsLastHorz) { + if (horzEdge->OutIdx >= 0) + AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge->Top); + DeleteFromAEL(horzEdge); + DeleteFromAEL(eMaxPair); + return; + } + + if (dir == dLeftToRight) { + IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y); + IntersectEdges(horzEdge, e, Pt); + } else { + IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y); + IntersectEdges(e, horzEdge, Pt); + } + TEdge *eNext = GetNextInAEL(e, dir); + SwapPositionsInAEL(horzEdge, e); + e = eNext; + } // end while(e) + + // Break out of loop if HorzEdge.NextInLML is not also horizontal ... + if (!horzEdge->NextInLML || !IsHorizontal(*horzEdge->NextInLML)) + break; + + UpdateEdgeIntoAEL(horzEdge); + if (horzEdge->OutIdx >= 0) + AddOutPt(horzEdge, horzEdge->Bot); + GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); + + } // end for (;;) + + if (horzEdge->OutIdx >= 0 && !op1) { + op1 = GetLastOutPt(horzEdge); + TEdge *eNextHorz = m_SortedEdges; + while (eNextHorz) { + if (eNextHorz->OutIdx >= 0 && + HorzSegmentsOverlap(horzEdge->Bot.X, horzEdge->Top.X, + eNextHorz->Bot.X, eNextHorz->Top.X)) { + OutPt *op2 = GetLastOutPt(eNextHorz); + AddJoin(op2, op1, eNextHorz->Top); + } + eNextHorz = eNextHorz->NextInSEL; + } + AddGhostJoin(op1, horzEdge->Top); + } + + if (horzEdge->NextInLML) { + if (horzEdge->OutIdx >= 0) { + op1 = AddOutPt(horzEdge, horzEdge->Top); + UpdateEdgeIntoAEL(horzEdge); + if (horzEdge->WindDelta == 0) + return; + // nb: HorzEdge is no longer horizontal here + TEdge *ePrev = horzEdge->PrevInAEL; + TEdge *eNext = horzEdge->NextInAEL; + if (ePrev && ePrev->Curr.X == horzEdge->Bot.X && + ePrev->Curr.Y == horzEdge->Bot.Y && ePrev->WindDelta != 0 && + (ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y && + SlopesEqual(*horzEdge, *ePrev, m_UseFullRange))) { + OutPt *op2 = AddOutPt(ePrev, horzEdge->Bot); + AddJoin(op1, op2, horzEdge->Top); + } else if (eNext && eNext->Curr.X == horzEdge->Bot.X && + eNext->Curr.Y == horzEdge->Bot.Y && eNext->WindDelta != 0 && + eNext->OutIdx >= 0 && eNext->Curr.Y > eNext->Top.Y && + SlopesEqual(*horzEdge, *eNext, m_UseFullRange)) { + OutPt *op2 = AddOutPt(eNext, horzEdge->Bot); + AddJoin(op1, op2, horzEdge->Top); + } + } else + UpdateEdgeIntoAEL(horzEdge); + } else { + if (horzEdge->OutIdx >= 0) + AddOutPt(horzEdge, horzEdge->Top); + DeleteFromAEL(horzEdge); + } +} +//------------------------------------------------------------------------------ + +bool Clipper::ProcessIntersections(const cInt topY) { + if (!m_ActiveEdges) + return true; + try { + BuildIntersectList(topY); + size_t IlSize = m_IntersectList.size(); + if (IlSize == 0) + return true; + if (IlSize == 1 || FixupIntersectionOrder()) + ProcessIntersectList(); + else + return false; + } catch (...) { + m_SortedEdges = 0; + DisposeIntersectNodes(); + throw clipperException("ProcessIntersections error"); + } + m_SortedEdges = 0; + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::DisposeIntersectNodes() { + for (size_t i = 0; i < m_IntersectList.size(); ++i) + delete m_IntersectList[i]; + m_IntersectList.clear(); +} +//------------------------------------------------------------------------------ + +void Clipper::BuildIntersectList(const cInt topY) { + if (!m_ActiveEdges) + return; + + // prepare for sorting ... + TEdge *e = m_ActiveEdges; + m_SortedEdges = e; + while (e) { + e->PrevInSEL = e->PrevInAEL; + e->NextInSEL = e->NextInAEL; + e->Curr.X = TopX(*e, topY); + e = e->NextInAEL; + } + + // bubblesort ... + bool isModified; + do { + isModified = false; + e = m_SortedEdges; + while (e->NextInSEL) { + TEdge *eNext = e->NextInSEL; + IntPoint Pt; + if (e->Curr.X > eNext->Curr.X) { + IntersectPoint(*e, *eNext, Pt); + if (Pt.Y < topY) + Pt = IntPoint(TopX(*e, topY), topY); + IntersectNode *newNode = new IntersectNode; + newNode->Edge1 = e; + newNode->Edge2 = eNext; + newNode->Pt = Pt; + m_IntersectList.push_back(newNode); + + SwapPositionsInSEL(e, eNext); + isModified = true; + } else + e = eNext; + } + if (e->PrevInSEL) + e->PrevInSEL->NextInSEL = 0; + else + break; + } while (isModified); + m_SortedEdges = 0; // important +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessIntersectList() { + for (size_t i = 0; i < m_IntersectList.size(); ++i) { + IntersectNode *iNode = m_IntersectList[i]; + { + IntersectEdges(iNode->Edge1, iNode->Edge2, iNode->Pt); + SwapPositionsInAEL(iNode->Edge1, iNode->Edge2); + } + delete iNode; + } + m_IntersectList.clear(); +} +//------------------------------------------------------------------------------ + +bool IntersectListSort(IntersectNode *node1, IntersectNode *node2) { + return node2->Pt.Y < node1->Pt.Y; +} +//------------------------------------------------------------------------------ + +inline bool EdgesAdjacent(const IntersectNode &inode) { + return (inode.Edge1->NextInSEL == inode.Edge2) || + (inode.Edge1->PrevInSEL == inode.Edge2); +} +//------------------------------------------------------------------------------ + +bool Clipper::FixupIntersectionOrder() { + // pre-condition: intersections are sorted Bottom-most first. + // Now it's crucial that intersections are made only between adjacent edges, + // so to ensure this the order of intersections may need adjusting ... + CopyAELToSEL(); + std::sort(m_IntersectList.begin(), m_IntersectList.end(), IntersectListSort); + size_t cnt = m_IntersectList.size(); + for (size_t i = 0; i < cnt; ++i) { + if (!EdgesAdjacent(*m_IntersectList[i])) { + size_t j = i + 1; + while (j < cnt && !EdgesAdjacent(*m_IntersectList[j])) + j++; + if (j == cnt) + return false; + std::swap(m_IntersectList[i], m_IntersectList[j]); + } + SwapPositionsInSEL(m_IntersectList[i]->Edge1, m_IntersectList[i]->Edge2); + } + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::DoMaxima(TEdge *e) { + TEdge *eMaxPair = GetMaximaPairEx(e); + if (!eMaxPair) { + if (e->OutIdx >= 0) + AddOutPt(e, e->Top); + DeleteFromAEL(e); + return; + } + + TEdge *eNext = e->NextInAEL; + while (eNext && eNext != eMaxPair) { + IntersectEdges(e, eNext, e->Top); + SwapPositionsInAEL(e, eNext); + eNext = e->NextInAEL; + } + + if (e->OutIdx == Unassigned && eMaxPair->OutIdx == Unassigned) { + DeleteFromAEL(e); + DeleteFromAEL(eMaxPair); + } else if (e->OutIdx >= 0 && eMaxPair->OutIdx >= 0) { + if (e->OutIdx >= 0) + AddLocalMaxPoly(e, eMaxPair, e->Top); + DeleteFromAEL(e); + DeleteFromAEL(eMaxPair); + } +#ifdef use_lines + else if (e->WindDelta == 0) { + if (e->OutIdx >= 0) { + AddOutPt(e, e->Top); + e->OutIdx = Unassigned; + } + DeleteFromAEL(e); + + if (eMaxPair->OutIdx >= 0) { + AddOutPt(eMaxPair, e->Top); + eMaxPair->OutIdx = Unassigned; + } + DeleteFromAEL(eMaxPair); + } +#endif + else + throw clipperException("DoMaxima error"); +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) { + TEdge *e = m_ActiveEdges; + while (e) { + // 1. process maxima, treating them as if they're 'bent' horizontal edges, + // but exclude maxima with horizontal edges. nb: e can't be a horizontal. + bool IsMaximaEdge = IsMaxima(e, topY); + + if (IsMaximaEdge) { + TEdge *eMaxPair = GetMaximaPairEx(e); + IsMaximaEdge = (!eMaxPair || !IsHorizontal(*eMaxPair)); + } + + if (IsMaximaEdge) { + if (m_StrictSimple) + m_Maxima.push_back(e->Top.X); + TEdge *ePrev = e->PrevInAEL; + DoMaxima(e); + if (!ePrev) + e = m_ActiveEdges; + else + e = ePrev->NextInAEL; + } else { + // 2. promote horizontal edges, otherwise update Curr.X and Curr.Y ... + if (IsIntermediate(e, topY) && IsHorizontal(*e->NextInLML)) { + UpdateEdgeIntoAEL(e); + if (e->OutIdx >= 0) + AddOutPt(e, e->Bot); + AddEdgeToSEL(e); + } else { + e->Curr.X = TopX(*e, topY); + e->Curr.Y = topY; +#ifdef use_xyz + e->Curr.Z = + topY == e->Top.Y ? e->Top.Z : (topY == e->Bot.Y ? e->Bot.Z : 0); +#endif + } + + // When StrictlySimple and 'e' is being touched by another edge, then + // make sure both edges have a vertex here ... + if (m_StrictSimple) { + TEdge *ePrev = e->PrevInAEL; + if ((e->OutIdx >= 0) && (e->WindDelta != 0) && ePrev && + (ePrev->OutIdx >= 0) && (ePrev->Curr.X == e->Curr.X) && + (ePrev->WindDelta != 0)) { + IntPoint pt = e->Curr; +#ifdef use_xyz + SetZ(pt, *ePrev, *e); +#endif + OutPt *op = AddOutPt(ePrev, pt); + OutPt *op2 = AddOutPt(e, pt); + AddJoin(op, op2, pt); // StrictlySimple (type-3) join + } + } + + e = e->NextInAEL; + } + } + + // 3. Process horizontals at the Top of the scanbeam ... + m_Maxima.sort(); + ProcessHorizontals(); + m_Maxima.clear(); + + // 4. Promote intermediate vertices ... + e = m_ActiveEdges; + while (e) { + if (IsIntermediate(e, topY)) { + OutPt *op = 0; + if (e->OutIdx >= 0) + op = AddOutPt(e, e->Top); + UpdateEdgeIntoAEL(e); + + // if output polygons share an edge, they'll need joining later ... + TEdge *ePrev = e->PrevInAEL; + TEdge *eNext = e->NextInAEL; + if (ePrev && ePrev->Curr.X == e->Bot.X && ePrev->Curr.Y == e->Bot.Y && + op && ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y && + SlopesEqual(e->Curr, e->Top, ePrev->Curr, ePrev->Top, + m_UseFullRange) && + (e->WindDelta != 0) && (ePrev->WindDelta != 0)) { + OutPt *op2 = AddOutPt(ePrev, e->Bot); + AddJoin(op, op2, e->Top); + } else if (eNext && eNext->Curr.X == e->Bot.X && + eNext->Curr.Y == e->Bot.Y && op && eNext->OutIdx >= 0 && + eNext->Curr.Y > eNext->Top.Y && + SlopesEqual(e->Curr, e->Top, eNext->Curr, eNext->Top, + m_UseFullRange) && + (e->WindDelta != 0) && (eNext->WindDelta != 0)) { + OutPt *op2 = AddOutPt(eNext, e->Bot); + AddJoin(op, op2, e->Top); + } + } + e = e->NextInAEL; + } +} +//------------------------------------------------------------------------------ + +void Clipper::FixupOutPolyline(OutRec &outrec) { + OutPt *pp = outrec.Pts; + OutPt *lastPP = pp->Prev; + while (pp != lastPP) { + pp = pp->Next; + if (pp->Pt == pp->Prev->Pt) { + if (pp == lastPP) + lastPP = pp->Prev; + OutPt *tmpPP = pp->Prev; + tmpPP->Next = pp->Next; + pp->Next->Prev = tmpPP; + delete pp; + pp = tmpPP; + } + } + + if (pp == pp->Prev) { + DisposeOutPts(pp); + outrec.Pts = 0; + return; + } +} +//------------------------------------------------------------------------------ + +void Clipper::FixupOutPolygon(OutRec &outrec) { + // FixupOutPolygon() - removes duplicate points and simplifies consecutive + // parallel edges by removing the middle vertex. + OutPt *lastOK = 0; + outrec.BottomPt = 0; + OutPt *pp = outrec.Pts; + bool preserveCol = m_PreserveCollinear || m_StrictSimple; + + for (;;) { + if (pp->Prev == pp || pp->Prev == pp->Next) { + DisposeOutPts(pp); + outrec.Pts = 0; + return; + } + + // test for duplicate points and collinear edges ... + if ((pp->Pt == pp->Next->Pt) || (pp->Pt == pp->Prev->Pt) || + (SlopesEqual(pp->Prev->Pt, pp->Pt, pp->Next->Pt, m_UseFullRange) && + (!preserveCol || + !Pt2IsBetweenPt1AndPt3(pp->Prev->Pt, pp->Pt, pp->Next->Pt)))) { + lastOK = 0; + OutPt *tmp = pp; + pp->Prev->Next = pp->Next; + pp->Next->Prev = pp->Prev; + pp = pp->Prev; + delete tmp; + } else if (pp == lastOK) + break; + else { + if (!lastOK) + lastOK = pp; + pp = pp->Next; + } + } + outrec.Pts = pp; +} +//------------------------------------------------------------------------------ + +int PointCount(OutPt *Pts) { + if (!Pts) + return 0; + int result = 0; + OutPt *p = Pts; + do { + result++; + p = p->Next; + } while (p != Pts); + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::BuildResult(Paths &polys) { + polys.reserve(m_PolyOuts.size()); + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + if (!m_PolyOuts[i]->Pts) + continue; + Path pg; + OutPt *p = m_PolyOuts[i]->Pts->Prev; + int cnt = PointCount(p); + if (cnt < 2) + continue; + pg.reserve(cnt); + for (int i = 0; i < cnt; ++i) { + pg.push_back(p->Pt); + p = p->Prev; + } + polys.push_back(pg); + } +} +//------------------------------------------------------------------------------ + +void Clipper::BuildResult2(PolyTree &polytree) { + polytree.Clear(); + polytree.AllNodes.reserve(m_PolyOuts.size()); + // add each output polygon/contour to polytree ... + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) { + OutRec *outRec = m_PolyOuts[i]; + int cnt = PointCount(outRec->Pts); + if ((outRec->IsOpen && cnt < 2) || (!outRec->IsOpen && cnt < 3)) + continue; + FixHoleLinkage(*outRec); + PolyNode *pn = new PolyNode(); + // nb: polytree takes ownership of all the PolyNodes + polytree.AllNodes.push_back(pn); + outRec->PolyNd = pn; + pn->Parent = 0; + pn->Index = 0; + pn->Contour.reserve(cnt); + OutPt *op = outRec->Pts->Prev; + for (int j = 0; j < cnt; j++) { + pn->Contour.push_back(op->Pt); + op = op->Prev; + } + } + + // fixup PolyNode links etc ... + polytree.Childs.reserve(m_PolyOuts.size()); + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->PolyNd) + continue; + if (outRec->IsOpen) { + outRec->PolyNd->m_IsOpen = true; + polytree.AddChild(*outRec->PolyNd); + } else if (outRec->FirstLeft && outRec->FirstLeft->PolyNd) + outRec->FirstLeft->PolyNd->AddChild(*outRec->PolyNd); + else + polytree.AddChild(*outRec->PolyNd); + } +} +//------------------------------------------------------------------------------ + +void SwapIntersectNodes(IntersectNode &int1, IntersectNode &int2) { + // just swap the contents (because fIntersectNodes is a single-linked-list) + IntersectNode inode = int1; // gets a copy of Int1 + int1.Edge1 = int2.Edge1; + int1.Edge2 = int2.Edge2; + int1.Pt = int2.Pt; + int2.Edge1 = inode.Edge1; + int2.Edge2 = inode.Edge2; + int2.Pt = inode.Pt; +} +//------------------------------------------------------------------------------ + +inline bool E2InsertsBeforeE1(TEdge &e1, TEdge &e2) { + if (e2.Curr.X == e1.Curr.X) { + if (e2.Top.Y > e1.Top.Y) + return e2.Top.X < TopX(e1, e2.Top.Y); + else + return e1.Top.X > TopX(e2, e1.Top.Y); + } else + return e2.Curr.X < e1.Curr.X; +} +//------------------------------------------------------------------------------ + +bool GetOverlap(const cInt a1, const cInt a2, const cInt b1, const cInt b2, + cInt &Left, cInt &Right) { + if (a1 < a2) { + if (b1 < b2) { + Left = std::max(a1, b1); + Right = std::min(a2, b2); + } else { + Left = std::max(a1, b2); + Right = std::min(a2, b1); + } + } else { + if (b1 < b2) { + Left = std::max(a2, b1); + Right = std::min(a1, b2); + } else { + Left = std::max(a2, b2); + Right = std::min(a1, b1); + } + } + return Left < Right; +} +//------------------------------------------------------------------------------ + +inline void UpdateOutPtIdxs(OutRec &outrec) { + OutPt *op = outrec.Pts; + do { + op->Idx = outrec.Idx; + op = op->Prev; + } while (op != outrec.Pts); +} +//------------------------------------------------------------------------------ + +void Clipper::InsertEdgeIntoAEL(TEdge *edge, TEdge *startEdge) { + if (!m_ActiveEdges) { + edge->PrevInAEL = 0; + edge->NextInAEL = 0; + m_ActiveEdges = edge; + } else if (!startEdge && E2InsertsBeforeE1(*m_ActiveEdges, *edge)) { + edge->PrevInAEL = 0; + edge->NextInAEL = m_ActiveEdges; + m_ActiveEdges->PrevInAEL = edge; + m_ActiveEdges = edge; + } else { + if (!startEdge) + startEdge = m_ActiveEdges; + while (startEdge->NextInAEL && + !E2InsertsBeforeE1(*startEdge->NextInAEL, *edge)) + startEdge = startEdge->NextInAEL; + edge->NextInAEL = startEdge->NextInAEL; + if (startEdge->NextInAEL) + startEdge->NextInAEL->PrevInAEL = edge; + edge->PrevInAEL = startEdge; + startEdge->NextInAEL = edge; + } +} +//---------------------------------------------------------------------- + +OutPt *DupOutPt(OutPt *outPt, bool InsertAfter) { + OutPt *result = new OutPt; + result->Pt = outPt->Pt; + result->Idx = outPt->Idx; + if (InsertAfter) { + result->Next = outPt->Next; + result->Prev = outPt; + outPt->Next->Prev = result; + outPt->Next = result; + } else { + result->Prev = outPt->Prev; + result->Next = outPt; + outPt->Prev->Next = result; + outPt->Prev = result; + } + return result; +} +//------------------------------------------------------------------------------ + +bool JoinHorz(OutPt *op1, OutPt *op1b, OutPt *op2, OutPt *op2b, + const IntPoint Pt, bool DiscardLeft) { + Direction Dir1 = (op1->Pt.X > op1b->Pt.X ? dRightToLeft : dLeftToRight); + Direction Dir2 = (op2->Pt.X > op2b->Pt.X ? dRightToLeft : dLeftToRight); + if (Dir1 == Dir2) + return false; + + // When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we + // want Op1b to be on the Right. (And likewise with Op2 and Op2b.) + // So, to facilitate this while inserting Op1b and Op2b ... + // when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b, + // otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.) + if (Dir1 == dLeftToRight) { + while (op1->Next->Pt.X <= Pt.X && op1->Next->Pt.X >= op1->Pt.X && + op1->Next->Pt.Y == Pt.Y) + op1 = op1->Next; + if (DiscardLeft && (op1->Pt.X != Pt.X)) + op1 = op1->Next; + op1b = DupOutPt(op1, !DiscardLeft); + if (op1b->Pt != Pt) { + op1 = op1b; + op1->Pt = Pt; + op1b = DupOutPt(op1, !DiscardLeft); + } + } else { + while (op1->Next->Pt.X >= Pt.X && op1->Next->Pt.X <= op1->Pt.X && + op1->Next->Pt.Y == Pt.Y) + op1 = op1->Next; + if (!DiscardLeft && (op1->Pt.X != Pt.X)) + op1 = op1->Next; + op1b = DupOutPt(op1, DiscardLeft); + if (op1b->Pt != Pt) { + op1 = op1b; + op1->Pt = Pt; + op1b = DupOutPt(op1, DiscardLeft); + } + } + + if (Dir2 == dLeftToRight) { + while (op2->Next->Pt.X <= Pt.X && op2->Next->Pt.X >= op2->Pt.X && + op2->Next->Pt.Y == Pt.Y) + op2 = op2->Next; + if (DiscardLeft && (op2->Pt.X != Pt.X)) + op2 = op2->Next; + op2b = DupOutPt(op2, !DiscardLeft); + if (op2b->Pt != Pt) { + op2 = op2b; + op2->Pt = Pt; + op2b = DupOutPt(op2, !DiscardLeft); + }; + } else { + while (op2->Next->Pt.X >= Pt.X && op2->Next->Pt.X <= op2->Pt.X && + op2->Next->Pt.Y == Pt.Y) + op2 = op2->Next; + if (!DiscardLeft && (op2->Pt.X != Pt.X)) + op2 = op2->Next; + op2b = DupOutPt(op2, DiscardLeft); + if (op2b->Pt != Pt) { + op2 = op2b; + op2->Pt = Pt; + op2b = DupOutPt(op2, DiscardLeft); + }; + }; + + if ((Dir1 == dLeftToRight) == DiscardLeft) { + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + } else { + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + } + return true; +} +//------------------------------------------------------------------------------ + +bool Clipper::JoinPoints(Join *j, OutRec *outRec1, OutRec *outRec2) { + OutPt *op1 = j->OutPt1, *op1b; + OutPt *op2 = j->OutPt2, *op2b; + + // There are 3 kinds of joins for output polygons ... + // 1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are vertices anywhere + // along (horizontal) collinear edges (& Join.OffPt is on the same + // horizontal). + // 2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same + // location at the Bottom of the overlapping segment (& Join.OffPt is above). + // 3. StrictSimple joins where edges touch but are not collinear and where + // Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point. + bool isHorizontal = (j->OutPt1->Pt.Y == j->OffPt.Y); + + if (isHorizontal && (j->OffPt == j->OutPt1->Pt) && + (j->OffPt == j->OutPt2->Pt)) { + // Strictly Simple join ... + if (outRec1 != outRec2) + return false; + op1b = j->OutPt1->Next; + while (op1b != op1 && (op1b->Pt == j->OffPt)) + op1b = op1b->Next; + bool reverse1 = (op1b->Pt.Y > j->OffPt.Y); + op2b = j->OutPt2->Next; + while (op2b != op2 && (op2b->Pt == j->OffPt)) + op2b = op2b->Next; + bool reverse2 = (op2b->Pt.Y > j->OffPt.Y); + if (reverse1 == reverse2) + return false; + if (reverse1) { + op1b = DupOutPt(op1, false); + op2b = DupOutPt(op2, true); + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } else { + op1b = DupOutPt(op1, true); + op2b = DupOutPt(op2, false); + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } + } else if (isHorizontal) { + // treat horizontal joins differently to non-horizontal joins since with + // them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt + // may be anywhere along the horizontal edge. + op1b = op1; + while (op1->Prev->Pt.Y == op1->Pt.Y && op1->Prev != op1b && + op1->Prev != op2) + op1 = op1->Prev; + while (op1b->Next->Pt.Y == op1b->Pt.Y && op1b->Next != op1 && + op1b->Next != op2) + op1b = op1b->Next; + if (op1b->Next == op1 || op1b->Next == op2) + return false; // a flat 'polygon' + + op2b = op2; + while (op2->Prev->Pt.Y == op2->Pt.Y && op2->Prev != op2b && + op2->Prev != op1b) + op2 = op2->Prev; + while (op2b->Next->Pt.Y == op2b->Pt.Y && op2b->Next != op2 && + op2b->Next != op1) + op2b = op2b->Next; + if (op2b->Next == op2 || op2b->Next == op1) + return false; // a flat 'polygon' + + cInt Left, Right; + // Op1 --> Op1b & Op2 --> Op2b are the extremites of the horizontal edges + if (!GetOverlap(op1->Pt.X, op1b->Pt.X, op2->Pt.X, op2b->Pt.X, Left, Right)) + return false; + + // DiscardLeftSide: when overlapping edges are joined, a spike will created + // which needs to be cleaned up. However, we don't want Op1 or Op2 caught up + // on the discard Side as either may still be needed for other joins ... + IntPoint Pt; + bool DiscardLeftSide; + if (op1->Pt.X >= Left && op1->Pt.X <= Right) { + Pt = op1->Pt; + DiscardLeftSide = (op1->Pt.X > op1b->Pt.X); + } else if (op2->Pt.X >= Left && op2->Pt.X <= Right) { + Pt = op2->Pt; + DiscardLeftSide = (op2->Pt.X > op2b->Pt.X); + } else if (op1b->Pt.X >= Left && op1b->Pt.X <= Right) { + Pt = op1b->Pt; + DiscardLeftSide = op1b->Pt.X > op1->Pt.X; + } else { + Pt = op2b->Pt; + DiscardLeftSide = (op2b->Pt.X > op2->Pt.X); + } + j->OutPt1 = op1; + j->OutPt2 = op2; + return JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide); + } else { + // nb: For non-horizontal joins ... + // 1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y + // 2. Jr.OutPt1.Pt > Jr.OffPt.Y + + // make sure the polygons are correctly oriented ... + op1b = op1->Next; + while ((op1b->Pt == op1->Pt) && (op1b != op1)) + op1b = op1b->Next; + bool Reverse1 = ((op1b->Pt.Y > op1->Pt.Y) || + !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)); + if (Reverse1) { + op1b = op1->Prev; + while ((op1b->Pt == op1->Pt) && (op1b != op1)) + op1b = op1b->Prev; + if ((op1b->Pt.Y > op1->Pt.Y) || + !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)) + return false; + }; + op2b = op2->Next; + while ((op2b->Pt == op2->Pt) && (op2b != op2)) + op2b = op2b->Next; + bool Reverse2 = ((op2b->Pt.Y > op2->Pt.Y) || + !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)); + if (Reverse2) { + op2b = op2->Prev; + while ((op2b->Pt == op2->Pt) && (op2b != op2)) + op2b = op2b->Prev; + if ((op2b->Pt.Y > op2->Pt.Y) || + !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)) + return false; + } + + if ((op1b == op1) || (op2b == op2) || (op1b == op2b) || + ((outRec1 == outRec2) && (Reverse1 == Reverse2))) + return false; + + if (Reverse1) { + op1b = DupOutPt(op1, false); + op2b = DupOutPt(op2, true); + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } else { + op1b = DupOutPt(op1, true); + op2b = DupOutPt(op2, false); + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } + } +} +//---------------------------------------------------------------------- + +static OutRec *ParseFirstLeft(OutRec *FirstLeft) { + while (FirstLeft && !FirstLeft->Pts) + FirstLeft = FirstLeft->FirstLeft; + return FirstLeft; +} +//------------------------------------------------------------------------------ + +void Clipper::FixupFirstLefts1(OutRec *OldOutRec, OutRec *NewOutRec) { + // tests if NewOutRec contains the polygon before reassigning FirstLeft + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (outRec->Pts && firstLeft == OldOutRec) { + if (Poly2ContainsPoly1(outRec->Pts, NewOutRec->Pts)) + outRec->FirstLeft = NewOutRec; + } + } +} +//---------------------------------------------------------------------- + +void Clipper::FixupFirstLefts2(OutRec *InnerOutRec, OutRec *OuterOutRec) { + // A polygon has split into two such that one is now the inner of the other. + // It's possible that these polygons now wrap around other polygons, so check + // every polygon that's also contained by OuterOutRec's FirstLeft container + //(including 0) to see if they've become inner to the new inner polygon ... + OutRec *orfl = OuterOutRec->FirstLeft; + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + + if (!outRec->Pts || outRec == OuterOutRec || outRec == InnerOutRec) + continue; + OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (firstLeft != orfl && firstLeft != InnerOutRec && + firstLeft != OuterOutRec) + continue; + if (Poly2ContainsPoly1(outRec->Pts, InnerOutRec->Pts)) + outRec->FirstLeft = InnerOutRec; + else if (Poly2ContainsPoly1(outRec->Pts, OuterOutRec->Pts)) + outRec->FirstLeft = OuterOutRec; + else if (outRec->FirstLeft == InnerOutRec || + outRec->FirstLeft == OuterOutRec) + outRec->FirstLeft = orfl; + } +} +//---------------------------------------------------------------------- +void Clipper::FixupFirstLefts3(OutRec *OldOutRec, OutRec *NewOutRec) { + // reassigns FirstLeft WITHOUT testing if NewOutRec contains the polygon + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (outRec->Pts && firstLeft == OldOutRec) + outRec->FirstLeft = NewOutRec; + } +} +//---------------------------------------------------------------------- + +void Clipper::JoinCommonEdges() { + for (JoinList::size_type i = 0; i < m_Joins.size(); i++) { + Join *join = m_Joins[i]; + + OutRec *outRec1 = GetOutRec(join->OutPt1->Idx); + OutRec *outRec2 = GetOutRec(join->OutPt2->Idx); + + if (!outRec1->Pts || !outRec2->Pts) + continue; + if (outRec1->IsOpen || outRec2->IsOpen) + continue; + + // get the polygon fragment with the correct hole state (FirstLeft) + // before calling JoinPoints() ... + OutRec *holeStateRec; + if (outRec1 == outRec2) + holeStateRec = outRec1; + else if (OutRec1RightOfOutRec2(outRec1, outRec2)) + holeStateRec = outRec2; + else if (OutRec1RightOfOutRec2(outRec2, outRec1)) + holeStateRec = outRec1; + else + holeStateRec = GetLowermostRec(outRec1, outRec2); + + if (!JoinPoints(join, outRec1, outRec2)) + continue; + + if (outRec1 == outRec2) { + // instead of joining two polygons, we've just created a new one by + // splitting one polygon into two. + outRec1->Pts = join->OutPt1; + outRec1->BottomPt = 0; + outRec2 = CreateOutRec(); + outRec2->Pts = join->OutPt2; + + // update all OutRec2.Pts Idx's ... + UpdateOutPtIdxs(*outRec2); + + if (Poly2ContainsPoly1(outRec2->Pts, outRec1->Pts)) { + // outRec1 contains outRec2 ... + outRec2->IsHole = !outRec1->IsHole; + outRec2->FirstLeft = outRec1; + + if (m_UsingPolyTree) + FixupFirstLefts2(outRec2, outRec1); + + if ((outRec2->IsHole ^ m_ReverseOutput) == (Area(*outRec2) > 0)) + ReversePolyPtLinks(outRec2->Pts); + + } else if (Poly2ContainsPoly1(outRec1->Pts, outRec2->Pts)) { + // outRec2 contains outRec1 ... + outRec2->IsHole = outRec1->IsHole; + outRec1->IsHole = !outRec2->IsHole; + outRec2->FirstLeft = outRec1->FirstLeft; + outRec1->FirstLeft = outRec2; + + if (m_UsingPolyTree) + FixupFirstLefts2(outRec1, outRec2); + + if ((outRec1->IsHole ^ m_ReverseOutput) == (Area(*outRec1) > 0)) + ReversePolyPtLinks(outRec1->Pts); + } else { + // the 2 polygons are completely separate ... + outRec2->IsHole = outRec1->IsHole; + outRec2->FirstLeft = outRec1->FirstLeft; + + // fixup FirstLeft pointers that may need reassigning to OutRec2 + if (m_UsingPolyTree) + FixupFirstLefts1(outRec1, outRec2); + } + + } else { + // joined 2 polygons together ... + + outRec2->Pts = 0; + outRec2->BottomPt = 0; + outRec2->Idx = outRec1->Idx; + + outRec1->IsHole = holeStateRec->IsHole; + if (holeStateRec == outRec2) + outRec1->FirstLeft = outRec2->FirstLeft; + outRec2->FirstLeft = outRec1; + + if (m_UsingPolyTree) + FixupFirstLefts3(outRec2, outRec1); + } + } +} + +//------------------------------------------------------------------------------ +// ClipperOffset support functions ... +//------------------------------------------------------------------------------ + +DoublePoint GetUnitNormal(const IntPoint &pt1, const IntPoint &pt2) { + if (pt2.X == pt1.X && pt2.Y == pt1.Y) + return DoublePoint(0, 0); + + double Dx = (double)(pt2.X - pt1.X); + double dy = (double)(pt2.Y - pt1.Y); + double f = 1 * 1.0 / std::sqrt(Dx * Dx + dy * dy); + Dx *= f; + dy *= f; + return DoublePoint(dy, -Dx); +} + +//------------------------------------------------------------------------------ +// ClipperOffset class +//------------------------------------------------------------------------------ + +ClipperOffset::ClipperOffset(double miterLimit, double arcTolerance) { + this->MiterLimit = miterLimit; + this->ArcTolerance = arcTolerance; + m_lowest.X = -1; +} +//------------------------------------------------------------------------------ + +ClipperOffset::~ClipperOffset() { Clear(); } +//------------------------------------------------------------------------------ + +void ClipperOffset::Clear() { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) + delete m_polyNodes.Childs[i]; + m_polyNodes.Childs.clear(); + m_lowest.X = -1; +} +//------------------------------------------------------------------------------ + +void ClipperOffset::AddPath(const Path &path, JoinType joinType, + EndType endType) { + int highI = (int)path.size() - 1; + if (highI < 0) + return; + PolyNode *newNode = new PolyNode(); + newNode->m_jointype = joinType; + newNode->m_endtype = endType; + + // strip duplicate points from path and also get index to the lowest point ... + if (endType == etClosedLine || endType == etClosedPolygon) + while (highI > 0 && path[0] == path[highI]) + highI--; + newNode->Contour.reserve(highI + 1); + newNode->Contour.push_back(path[0]); + int j = 0, k = 0; + for (int i = 1; i <= highI; i++) + if (newNode->Contour[j] != path[i]) { + j++; + newNode->Contour.push_back(path[i]); + if (path[i].Y > newNode->Contour[k].Y || + (path[i].Y == newNode->Contour[k].Y && + path[i].X < newNode->Contour[k].X)) + k = j; + } + if (endType == etClosedPolygon && j < 2) { + delete newNode; + return; + } + m_polyNodes.AddChild(*newNode); + + // if this path's lowest pt is lower than all the others then update m_lowest + if (endType != etClosedPolygon) + return; + if (m_lowest.X < 0) + m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k); + else { + IntPoint ip = m_polyNodes.Childs[(int)m_lowest.X]->Contour[(int)m_lowest.Y]; + if (newNode->Contour[k].Y > ip.Y || + (newNode->Contour[k].Y == ip.Y && newNode->Contour[k].X < ip.X)) + m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::AddPaths(const Paths &paths, JoinType joinType, + EndType endType) { + for (Paths::size_type i = 0; i < paths.size(); ++i) + AddPath(paths[i], joinType, endType); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::FixOrientations() { + // fixup orientations of all closed paths if the orientation of the + // closed path with the lowermost vertex is wrong ... + if (m_lowest.X >= 0 && + !Orientation(m_polyNodes.Childs[(int)m_lowest.X]->Contour)) { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) { + PolyNode &node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedPolygon || + (node.m_endtype == etClosedLine && Orientation(node.Contour))) + ReversePath(node.Contour); + } + } else { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) { + PolyNode &node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedLine && !Orientation(node.Contour)) + ReversePath(node.Contour); + } + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::Execute(Paths &solution, double delta) { + solution.clear(); + FixOrientations(); + DoOffset(delta); + + // now clean up 'corners' ... + Clipper clpr; + clpr.AddPaths(m_destPolys, ptSubject, true); + if (delta > 0) { + clpr.Execute(ctUnion, solution, pftPositive, pftPositive); + } else { + IntRect r = clpr.GetBounds(); + Path outer(4); + outer[0] = IntPoint(r.left - 10, r.bottom + 10); + outer[1] = IntPoint(r.right + 10, r.bottom + 10); + outer[2] = IntPoint(r.right + 10, r.top - 10); + outer[3] = IntPoint(r.left - 10, r.top - 10); + + clpr.AddPath(outer, ptSubject, true); + clpr.ReverseSolution(true); + clpr.Execute(ctUnion, solution, pftNegative, pftNegative); + if (solution.size() > 0) + solution.erase(solution.begin()); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::Execute(PolyTree &solution, double delta) { + solution.Clear(); + FixOrientations(); + DoOffset(delta); + + // now clean up 'corners' ... + Clipper clpr; + clpr.AddPaths(m_destPolys, ptSubject, true); + if (delta > 0) { + clpr.Execute(ctUnion, solution, pftPositive, pftPositive); + } else { + IntRect r = clpr.GetBounds(); + Path outer(4); + outer[0] = IntPoint(r.left - 10, r.bottom + 10); + outer[1] = IntPoint(r.right + 10, r.bottom + 10); + outer[2] = IntPoint(r.right + 10, r.top - 10); + outer[3] = IntPoint(r.left - 10, r.top - 10); + + clpr.AddPath(outer, ptSubject, true); + clpr.ReverseSolution(true); + clpr.Execute(ctUnion, solution, pftNegative, pftNegative); + // remove the outer PolyNode rectangle ... + if (solution.ChildCount() == 1 && solution.Childs[0]->ChildCount() > 0) { + PolyNode *outerNode = solution.Childs[0]; + solution.Childs.reserve(outerNode->ChildCount()); + solution.Childs[0] = outerNode->Childs[0]; + solution.Childs[0]->Parent = outerNode->Parent; + for (int i = 1; i < outerNode->ChildCount(); ++i) + solution.AddChild(*outerNode->Childs[i]); + } else + solution.Clear(); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoOffset(double delta) { + m_destPolys.clear(); + m_delta = delta; + + // if Zero offset, just copy any CLOSED polygons to m_p and return ... + if (NEAR_ZERO(delta)) { + m_destPolys.reserve(m_polyNodes.ChildCount()); + for (int i = 0; i < m_polyNodes.ChildCount(); i++) { + PolyNode &node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedPolygon) + m_destPolys.push_back(node.Contour); + } + return; + } + + // see offset_triginometry3.svg in the documentation folder ... + if (MiterLimit > 2) + m_miterLim = 2 / (MiterLimit * MiterLimit); + else + m_miterLim = 0.5; + + double y; + if (ArcTolerance <= 0.0) + y = def_arc_tolerance; + else if (ArcTolerance > std::fabs(delta) * def_arc_tolerance) + y = std::fabs(delta) * def_arc_tolerance; + else + y = ArcTolerance; + // see offset_triginometry2.svg in the documentation folder ... + double steps = pi / std::acos(1 - y / std::fabs(delta)); + if (steps > std::fabs(delta) * pi) + steps = std::fabs(delta) * pi; // ie excessive precision check + m_sin = std::sin(two_pi / steps); + m_cos = std::cos(two_pi / steps); + m_StepsPerRad = steps / two_pi; + if (delta < 0.0) + m_sin = -m_sin; + + m_destPolys.reserve(m_polyNodes.ChildCount() * 2); + for (int i = 0; i < m_polyNodes.ChildCount(); i++) { + PolyNode &node = *m_polyNodes.Childs[i]; + m_srcPoly = node.Contour; + + int len = (int)m_srcPoly.size(); + if (len == 0 || + (delta <= 0 && (len < 3 || node.m_endtype != etClosedPolygon))) + continue; + + m_destPoly.clear(); + if (len == 1) { + if (node.m_jointype == jtRound) { + double X = 1.0, Y = 0.0; + for (cInt j = 1; j <= steps; j++) { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[0].X + X * delta), + Round(m_srcPoly[0].Y + Y * delta))); + double X2 = X; + X = X * m_cos - m_sin * Y; + Y = X2 * m_sin + Y * m_cos; + } + } else { + double X = -1.0, Y = -1.0; + for (int j = 0; j < 4; ++j) { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[0].X + X * delta), + Round(m_srcPoly[0].Y + Y * delta))); + if (X < 0) + X = 1; + else if (Y < 0) + Y = 1; + else + X = -1; + } + } + m_destPolys.push_back(m_destPoly); + continue; + } + // build m_normals ... + m_normals.clear(); + m_normals.reserve(len); + for (int j = 0; j < len - 1; ++j) + m_normals.push_back(GetUnitNormal(m_srcPoly[j], m_srcPoly[j + 1])); + if (node.m_endtype == etClosedLine || node.m_endtype == etClosedPolygon) + m_normals.push_back(GetUnitNormal(m_srcPoly[len - 1], m_srcPoly[0])); + else + m_normals.push_back(DoublePoint(m_normals[len - 2])); + + if (node.m_endtype == etClosedPolygon) { + int k = len - 1; + for (int j = 0; j < len; ++j) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + } else if (node.m_endtype == etClosedLine) { + int k = len - 1; + for (int j = 0; j < len; ++j) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + m_destPoly.clear(); + // re-build m_normals ... + DoublePoint n = m_normals[len - 1]; + for (int j = len - 1; j > 0; j--) + m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y); + m_normals[0] = DoublePoint(-n.X, -n.Y); + k = 0; + for (int j = len - 1; j >= 0; j--) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + } else { + int k = 0; + for (int j = 1; j < len - 1; ++j) + OffsetPoint(j, k, node.m_jointype); + + IntPoint pt1; + if (node.m_endtype == etOpenButt) { + int j = len - 1; + pt1 = IntPoint((cInt)Round(m_srcPoly[j].X + m_normals[j].X * delta), + (cInt)Round(m_srcPoly[j].Y + m_normals[j].Y * delta)); + m_destPoly.push_back(pt1); + pt1 = IntPoint((cInt)Round(m_srcPoly[j].X - m_normals[j].X * delta), + (cInt)Round(m_srcPoly[j].Y - m_normals[j].Y * delta)); + m_destPoly.push_back(pt1); + } else { + int j = len - 1; + k = len - 2; + m_sinA = 0; + m_normals[j] = DoublePoint(-m_normals[j].X, -m_normals[j].Y); + if (node.m_endtype == etOpenSquare) + DoSquare(j, k); + else + DoRound(j, k); + } + + // re-build m_normals ... + for (int j = len - 1; j > 0; j--) + m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y); + m_normals[0] = DoublePoint(-m_normals[1].X, -m_normals[1].Y); + + k = len - 1; + for (int j = k - 1; j > 0; --j) + OffsetPoint(j, k, node.m_jointype); + + if (node.m_endtype == etOpenButt) { + pt1 = IntPoint((cInt)Round(m_srcPoly[0].X - m_normals[0].X * delta), + (cInt)Round(m_srcPoly[0].Y - m_normals[0].Y * delta)); + m_destPoly.push_back(pt1); + pt1 = IntPoint((cInt)Round(m_srcPoly[0].X + m_normals[0].X * delta), + (cInt)Round(m_srcPoly[0].Y + m_normals[0].Y * delta)); + m_destPoly.push_back(pt1); + } else { + k = 1; + m_sinA = 0; + if (node.m_endtype == etOpenSquare) + DoSquare(0, 1); + else + DoRound(0, 1); + } + m_destPolys.push_back(m_destPoly); + } + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::OffsetPoint(int j, int &k, JoinType jointype) { + // cross product ... + m_sinA = (m_normals[k].X * m_normals[j].Y - m_normals[j].X * m_normals[k].Y); + if (std::fabs(m_sinA * m_delta) < 1.0) { + // dot product ... + double cosA = + (m_normals[k].X * m_normals[j].X + m_normals[j].Y * m_normals[k].Y); + if (cosA > 0) // angle => 0 degrees + { + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta))); + return; + } + // else angle => 180 degrees + } else if (m_sinA > 1.0) + m_sinA = 1.0; + else if (m_sinA < -1.0) + m_sinA = -1.0; + + if (m_sinA * m_delta < 0) { + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta))); + m_destPoly.push_back(m_srcPoly[j]); + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[j].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta))); + } else + switch (jointype) { + case jtMiter: { + double r = 1 + (m_normals[j].X * m_normals[k].X + + m_normals[j].Y * m_normals[k].Y); + if (r >= m_miterLim) + DoMiter(j, k, r); + else + DoSquare(j, k); + break; + } + case jtSquare: + DoSquare(j, k); + break; + case jtRound: + DoRound(j, k); + break; + } + k = j; +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoSquare(int j, int k) { + double dx = std::tan(std::atan2(m_sinA, m_normals[k].X * m_normals[j].X + + m_normals[k].Y * m_normals[j].Y) / + 4); + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[j].X + m_delta * (m_normals[k].X - m_normals[k].Y * dx)), + Round(m_srcPoly[j].Y + + m_delta * (m_normals[k].Y + m_normals[k].X * dx)))); + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[j].X + m_delta * (m_normals[j].X + m_normals[j].Y * dx)), + Round(m_srcPoly[j].Y + + m_delta * (m_normals[j].Y - m_normals[j].X * dx)))); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoMiter(int j, int k, double r) { + double q = m_delta / r; + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + (m_normals[k].X + m_normals[j].X) * q), + Round(m_srcPoly[j].Y + (m_normals[k].Y + m_normals[j].Y) * q))); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoRound(int j, int k) { + double a = std::atan2(m_sinA, m_normals[k].X * m_normals[j].X + + m_normals[k].Y * m_normals[j].Y); + int steps = std::max((int)Round(m_StepsPerRad * std::fabs(a)), 1); + + double X = m_normals[k].X, Y = m_normals[k].Y, X2; + for (int i = 0; i < steps; ++i) { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + X * m_delta), + Round(m_srcPoly[j].Y + Y * m_delta))); + X2 = X; + X = X * m_cos - m_sin * Y; + Y = X2 * m_sin + Y * m_cos; + } + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[j].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta))); +} + +//------------------------------------------------------------------------------ +// Miscellaneous public functions +//------------------------------------------------------------------------------ + +void Clipper::DoSimplePolygons() { + PolyOutList::size_type i = 0; + while (i < m_PolyOuts.size()) { + OutRec *outrec = m_PolyOuts[i++]; + OutPt *op = outrec->Pts; + if (!op || outrec->IsOpen) + continue; + do // for each Pt in Polygon until duplicate found do ... + { + OutPt *op2 = op->Next; + while (op2 != outrec->Pts) { + if ((op->Pt == op2->Pt) && op2->Next != op && op2->Prev != op) { + // split the polygon into two ... + OutPt *op3 = op->Prev; + OutPt *op4 = op2->Prev; + op->Prev = op4; + op4->Next = op; + op2->Prev = op3; + op3->Next = op2; + + outrec->Pts = op; + OutRec *outrec2 = CreateOutRec(); + outrec2->Pts = op2; + UpdateOutPtIdxs(*outrec2); + if (Poly2ContainsPoly1(outrec2->Pts, outrec->Pts)) { + // OutRec2 is contained by OutRec1 ... + outrec2->IsHole = !outrec->IsHole; + outrec2->FirstLeft = outrec; + if (m_UsingPolyTree) + FixupFirstLefts2(outrec2, outrec); + } else if (Poly2ContainsPoly1(outrec->Pts, outrec2->Pts)) { + // OutRec1 is contained by OutRec2 ... + outrec2->IsHole = outrec->IsHole; + outrec->IsHole = !outrec2->IsHole; + outrec2->FirstLeft = outrec->FirstLeft; + outrec->FirstLeft = outrec2; + if (m_UsingPolyTree) + FixupFirstLefts2(outrec, outrec2); + } else { + // the 2 polygons are separate ... + outrec2->IsHole = outrec->IsHole; + outrec2->FirstLeft = outrec->FirstLeft; + if (m_UsingPolyTree) + FixupFirstLefts1(outrec, outrec2); + } + op2 = op; // ie get ready for the Next iteration + } + op2 = op2->Next; + } + op = op->Next; + } while (op != outrec->Pts); + } +} +//------------------------------------------------------------------------------ + +void ReversePath(Path &p) { std::reverse(p.begin(), p.end()); } +//------------------------------------------------------------------------------ + +void ReversePaths(Paths &p) { + for (Paths::size_type i = 0; i < p.size(); ++i) + ReversePath(p[i]); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygon(const Path &in_poly, Paths &out_polys, + PolyFillType fillType) { + Clipper c; + c.StrictlySimple(true); + c.AddPath(in_poly, ptSubject, true); + c.Execute(ctUnion, out_polys, fillType, fillType); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, + PolyFillType fillType) { + Clipper c; + c.StrictlySimple(true); + c.AddPaths(in_polys, ptSubject, true); + c.Execute(ctUnion, out_polys, fillType, fillType); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygons(Paths &polys, PolyFillType fillType) { + SimplifyPolygons(polys, polys, fillType); +} +//------------------------------------------------------------------------------ + +inline double DistanceSqrd(const IntPoint &pt1, const IntPoint &pt2) { + double Dx = ((double)pt1.X - pt2.X); + double dy = ((double)pt1.Y - pt2.Y); + return (Dx * Dx + dy * dy); +} +//------------------------------------------------------------------------------ + +double DistanceFromLineSqrd(const IntPoint &pt, const IntPoint &ln1, + const IntPoint &ln2) { + // The equation of a line in general form (Ax + By + C = 0) + // given 2 points (x�,y�) & (x�,y�) is ... + //(y� - y�)x + (x� - x�)y + (y� - y�)x� - (x� - x�)y� = 0 + // A = (y� - y�); B = (x� - x�); C = (y� - y�)x� - (x� - x�)y� + // perpendicular distance of point (x�,y�) = (Ax� + By� + C)/Sqrt(A� + B�) + // see http://en.wikipedia.org/wiki/Perpendicular_distance + double A = double(ln1.Y - ln2.Y); + double B = double(ln2.X - ln1.X); + double C = A * ln1.X + B * ln1.Y; + C = A * pt.X + B * pt.Y - C; + return (C * C) / (A * A + B * B); +} +//--------------------------------------------------------------------------- + +bool SlopesNearCollinear(const IntPoint &pt1, const IntPoint &pt2, + const IntPoint &pt3, double distSqrd) { + // this function is more accurate when the point that's geometrically + // between the other 2 points is the one that's tested for distance. + // ie makes it more likely to pick up 'spikes' ... + if (Abs(pt1.X - pt2.X) > Abs(pt1.Y - pt2.Y)) { + if ((pt1.X > pt2.X) == (pt1.X < pt3.X)) + return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; + else if ((pt2.X > pt1.X) == (pt2.X < pt3.X)) + return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + else + return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; + } else { + if ((pt1.Y > pt2.Y) == (pt1.Y < pt3.Y)) + return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; + else if ((pt2.Y > pt1.Y) == (pt2.Y < pt3.Y)) + return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + else + return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; + } +} +//------------------------------------------------------------------------------ + +bool PointsAreClose(IntPoint pt1, IntPoint pt2, double distSqrd) { + double Dx = (double)pt1.X - pt2.X; + double dy = (double)pt1.Y - pt2.Y; + return ((Dx * Dx) + (dy * dy) <= distSqrd); +} +//------------------------------------------------------------------------------ + +OutPt *ExcludeOp(OutPt *op) { + OutPt *result = op->Prev; + result->Next = op->Next; + op->Next->Prev = result; + result->Idx = 0; + return result; +} +//------------------------------------------------------------------------------ + +void CleanPolygon(const Path &in_poly, Path &out_poly, double distance) { + // distance = proximity in units/pixels below which vertices + // will be stripped. Default ~= sqrt(2). + + size_t size = in_poly.size(); + + if (size == 0) { + out_poly.clear(); + return; + } + + OutPt *outPts = new OutPt[size]; + for (size_t i = 0; i < size; ++i) { + outPts[i].Pt = in_poly[i]; + outPts[i].Next = &outPts[(i + 1) % size]; + outPts[i].Next->Prev = &outPts[i]; + outPts[i].Idx = 0; + } + + double distSqrd = distance * distance; + OutPt *op = &outPts[0]; + while (op->Idx == 0 && op->Next != op->Prev) { + if (PointsAreClose(op->Pt, op->Prev->Pt, distSqrd)) { + op = ExcludeOp(op); + size--; + } else if (PointsAreClose(op->Prev->Pt, op->Next->Pt, distSqrd)) { + ExcludeOp(op->Next); + op = ExcludeOp(op); + size -= 2; + } else if (SlopesNearCollinear(op->Prev->Pt, op->Pt, op->Next->Pt, + distSqrd)) { + op = ExcludeOp(op); + size--; + } else { + op->Idx = 1; + op = op->Next; + } + } + + if (size < 3) + size = 0; + out_poly.resize(size); + for (size_t i = 0; i < size; ++i) { + out_poly[i] = op->Pt; + op = op->Next; + } + delete[] outPts; +} +//------------------------------------------------------------------------------ + +void CleanPolygon(Path &poly, double distance) { + CleanPolygon(poly, poly, distance); +} +//------------------------------------------------------------------------------ + +void CleanPolygons(const Paths &in_polys, Paths &out_polys, double distance) { + out_polys.resize(in_polys.size()); + for (Paths::size_type i = 0; i < in_polys.size(); ++i) + CleanPolygon(in_polys[i], out_polys[i], distance); +} +//------------------------------------------------------------------------------ + +void CleanPolygons(Paths &polys, double distance) { + CleanPolygons(polys, polys, distance); +} +//------------------------------------------------------------------------------ + +void Minkowski(const Path &poly, const Path &path, Paths &solution, bool isSum, + bool isClosed) { + int delta = (isClosed ? 1 : 0); + size_t polyCnt = poly.size(); + size_t pathCnt = path.size(); + Paths pp; + pp.reserve(pathCnt); + if (isSum) + for (size_t i = 0; i < pathCnt; ++i) { + Path p; + p.reserve(polyCnt); + for (size_t j = 0; j < poly.size(); ++j) + p.push_back(IntPoint(path[i].X + poly[j].X, path[i].Y + poly[j].Y)); + pp.push_back(p); + } + else + for (size_t i = 0; i < pathCnt; ++i) { + Path p; + p.reserve(polyCnt); + for (size_t j = 0; j < poly.size(); ++j) + p.push_back(IntPoint(path[i].X - poly[j].X, path[i].Y - poly[j].Y)); + pp.push_back(p); + } + + solution.clear(); + solution.reserve((pathCnt + delta) * (polyCnt + 1)); + for (size_t i = 0; i < pathCnt - 1 + delta; ++i) + for (size_t j = 0; j < polyCnt; ++j) { + Path quad; + quad.reserve(4); + quad.push_back(pp[i % pathCnt][j % polyCnt]); + quad.push_back(pp[(i + 1) % pathCnt][j % polyCnt]); + quad.push_back(pp[(i + 1) % pathCnt][(j + 1) % polyCnt]); + quad.push_back(pp[i % pathCnt][(j + 1) % polyCnt]); + if (!Orientation(quad)) + ReversePath(quad); + solution.push_back(quad); + } +} +//------------------------------------------------------------------------------ + +void MinkowskiSum(const Path &pattern, const Path &path, Paths &solution, + bool pathIsClosed) { + Minkowski(pattern, path, solution, true, pathIsClosed); + Clipper c; + c.AddPaths(solution, ptSubject, true); + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +void TranslatePath(const Path &input, Path &output, const IntPoint delta) { + // precondition: input != output + output.resize(input.size()); + for (size_t i = 0; i < input.size(); ++i) + output[i] = IntPoint(input[i].X + delta.X, input[i].Y + delta.Y); +} +//------------------------------------------------------------------------------ + +void MinkowskiSum(const Path &pattern, const Paths &paths, Paths &solution, + bool pathIsClosed) { + Clipper c; + for (size_t i = 0; i < paths.size(); ++i) { + Paths tmp; + Minkowski(pattern, paths[i], tmp, true, pathIsClosed); + c.AddPaths(tmp, ptSubject, true); + if (pathIsClosed) { + Path tmp2; + TranslatePath(paths[i], tmp2, pattern[0]); + c.AddPath(tmp2, ptClip, true); + } + } + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +void MinkowskiDiff(const Path &poly1, const Path &poly2, Paths &solution) { + Minkowski(poly1, poly2, solution, false, true); + Clipper c; + c.AddPaths(solution, ptSubject, true); + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +enum NodeType { ntAny, ntOpen, ntClosed }; + +void AddPolyNodeToPaths(const PolyNode &polynode, NodeType nodetype, + Paths &paths) { + bool match = true; + if (nodetype == ntClosed) + match = !polynode.IsOpen(); + else if (nodetype == ntOpen) + return; + + if (!polynode.Contour.empty() && match) + paths.push_back(polynode.Contour); + for (int i = 0; i < polynode.ChildCount(); ++i) + AddPolyNodeToPaths(*polynode.Childs[i], nodetype, paths); +} +//------------------------------------------------------------------------------ + +void PolyTreeToPaths(const PolyTree &polytree, Paths &paths) { + paths.resize(0); + paths.reserve(polytree.Total()); + AddPolyNodeToPaths(polytree, ntAny, paths); +} +//------------------------------------------------------------------------------ + +void ClosedPathsFromPolyTree(const PolyTree &polytree, Paths &paths) { + paths.resize(0); + paths.reserve(polytree.Total()); + AddPolyNodeToPaths(polytree, ntClosed, paths); +} +//------------------------------------------------------------------------------ + +void OpenPathsFromPolyTree(PolyTree &polytree, Paths &paths) { + paths.resize(0); + paths.reserve(polytree.Total()); + // Open paths are top level only, so ... + for (int i = 0; i < polytree.ChildCount(); ++i) + if (polytree.Childs[i]->IsOpen()) + paths.push_back(polytree.Childs[i]->Contour); +} +//------------------------------------------------------------------------------ + +std::ostream &operator<<(std::ostream &s, const IntPoint &p) { + s << "(" << p.X << "," << p.Y << ")"; + return s; +} +//------------------------------------------------------------------------------ + +std::ostream &operator<<(std::ostream &s, const Path &p) { + if (p.empty()) + return s; + Path::size_type last = p.size() - 1; + for (Path::size_type i = 0; i < last; i++) + s << "(" << p[i].X << "," << p[i].Y << "), "; + s << "(" << p[last].X << "," << p[last].Y << ")\n"; + return s; +} +//------------------------------------------------------------------------------ + +std::ostream &operator<<(std::ostream &s, const Paths &p) { + for (Paths::size_type i = 0; i < p.size(); i++) + s << p[i]; + s << "\n"; + return s; +} +//------------------------------------------------------------------------------ + +} // ClipperLib namespace diff --git a/deploy/cpp_infer/src/main.cpp b/deploy/cpp_infer/src/main.cpp new file mode 100644 index 00000000..823f3046 --- /dev/null +++ b/deploy/cpp_infer/src/main.cpp @@ -0,0 +1,67 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +using namespace std; +using namespace cv; +using namespace PaddleOCR; + +int main(int argc, char **argv) { + if (argc < 4) { + std::cerr << "[ERROR] usage: " << argv[0] + << " det_model_file rec_model_file image_path\n"; + exit(1); + } + std::string det_model_file = argv[1]; + std::string rec_model_file = argv[2]; + std::string img_path = argv[3]; + + auto start = std::chrono::system_clock::now(); + + cv::Mat srcimg = cv::imread(img_path, cv::IMREAD_COLOR); + + DBDetector det(det_model_file); + CRNNRecognizer rec(rec_model_file); + + std::vector>> boxes; + det.Run(srcimg, boxes); + + rec.Run(boxes, srcimg); + + auto end = std::chrono::system_clock::now(); + auto duration = + std::chrono::duration_cast(end - start); + std::cout << "花费了" + << double(duration.count()) * + std::chrono::microseconds::period::num / + std::chrono::microseconds::period::den + << "秒" << std::endl; + + return 0; +} diff --git a/deploy/cpp_infer/src/ocr_det.cpp b/deploy/cpp_infer/src/ocr_det.cpp new file mode 100644 index 00000000..746c94b2 --- /dev/null +++ b/deploy/cpp_infer/src/ocr_det.cpp @@ -0,0 +1,142 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include "paddle_api.h" +#include "paddle_inference_api.h" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace PaddleOCR { + +void DBDetector::LoadModel(const std::string &model_dir, bool use_gpu, + const int gpu_id, const int min_subgraph_size, + const int batch_size) { + AnalysisConfig config; + config.SetModel(model_dir + "/model", model_dir + "/params"); + + // for cpu + config.DisableGpu(); + config.EnableMKLDNN(); // 开启MKLDNN加速 + config.SetCpuMathLibraryNumThreads(10); + + // 使用ZeroCopyTensor,此处必须设置为false + config.SwitchUseFeedFetchOps(false); + // 若输入为多个,此处必须设置为true + config.SwitchSpecifyInputNames(true); + // config.SwitchIrDebug(true); // + // 可视化调试选项,若开启,则会在每个图优化过程后生成dot文件 + // config.SwitchIrOptim(false);// 默认为true。如果设置为false,关闭所有优化 + config.EnableMemoryOptim(); // 开启内存/显存复用 + + this->predictor_ = CreatePaddlePredictor(config); + // predictor_ = std::move(CreatePaddlePredictor(config)); // PaddleDetection + // usage +} + +void DBDetector::Run(cv::Mat &img, + std::vector>> &boxes) { + float ratio_h{}; + float ratio_w{}; + + cv::Mat srcimg; + cv::Mat resize_img; + img.copyTo(srcimg); + this->resize_op_.Run(img, resize_img, this->max_side_len_, ratio_h, ratio_w); + + this->normalize_op_.Run(&resize_img, this->mean_, this->scale_, + this->is_scale_); + + float *input = new float[1 * 3 * resize_img.rows * resize_img.cols]; + this->permute_op_.Run(&resize_img, input); + + auto input_names = this->predictor_->GetInputNames(); + auto input_t = this->predictor_->GetInputTensor(input_names[0]); + input_t->Reshape({1, 3, resize_img.rows, resize_img.cols}); + input_t->copy_from_cpu(input); + + this->predictor_->ZeroCopyRun(); + + std::vector out_data; + auto output_names = this->predictor_->GetOutputNames(); + auto output_t = this->predictor_->GetOutputTensor(output_names[0]); + std::vector output_shape = output_t->shape(); + int out_num = std::accumulate(output_shape.begin(), output_shape.end(), 1, + std::multiplies()); + + out_data.resize(out_num); + output_t->copy_to_cpu(out_data.data()); + + int n2 = output_shape[2]; + int n3 = output_shape[3]; + int n = n2 * n3; + + float *pred = new float[n]; + unsigned char *cbuf = new unsigned char[n]; + + for (int i = 0; i < n; i++) { + pred[i] = float(out_data[i]); + cbuf[i] = (unsigned char)((out_data[i]) * 255); + } + + cv::Mat cbuf_map(n2, n3, CV_8UC1, (unsigned char *)cbuf); + cv::Mat pred_map(n2, n3, CV_32F, (float *)pred); + + const double threshold = 0.3 * 255; + const double maxvalue = 255; + cv::Mat bit_map; + cv::threshold(cbuf_map, bit_map, threshold, maxvalue, cv::THRESH_BINARY); + + boxes = post_processor_.boxes_from_bitmap(pred_map, bit_map); + + boxes = post_processor_.filter_tag_det_res(boxes, ratio_h, ratio_w, srcimg); + + //// visualization + cv::Point rook_points[boxes.size()][4]; + for (int n = 0; n < boxes.size(); n++) { + for (int m = 0; m < boxes[0].size(); m++) { + rook_points[n][m] = cv::Point(int(boxes[n][m][0]), int(boxes[n][m][1])); + } + } + + cv::Mat img_vis; + srcimg.copyTo(img_vis); + for (int n = 0; n < boxes.size(); n++) { + const cv::Point *ppt[1] = {rook_points[n]}; + int npt[] = {4}; + cv::polylines(img_vis, ppt, npt, 1, 1, CV_RGB(0, 255, 0), 2, 8, 0); + } + + imwrite("./det_res.png", img_vis); + + std::cout << "The detection visualized image saved in ./det_res.png" + << std::endl; + + delete[] input; + delete[] pred; + delete[] cbuf; +} + +} // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/src/ocr_rec.cpp b/deploy/cpp_infer/src/ocr_rec.cpp new file mode 100644 index 00000000..b50020e7 --- /dev/null +++ b/deploy/cpp_infer/src/ocr_rec.cpp @@ -0,0 +1,218 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include "paddle_api.h" +#include "paddle_inference_api.h" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace PaddleOCR { + +void CRNNRecognizer::Run(std::vector>> boxes, + cv::Mat &img) { + cv::Mat srcimg; + img.copyTo(srcimg); + cv::Mat crop_img; + cv::Mat resize_img; + + std::cout << "The predicted text is :" << std::endl; + int index = 0; + for (int i = boxes.size() - 1; i >= 0; i--) { + crop_img = get_rotate_crop_image(srcimg, boxes[i]); + + float wh_ratio = float(crop_img.cols) / float(crop_img.rows); + + this->resize_op_.Run(crop_img, resize_img, wh_ratio); + + this->normalize_op_.Run(&resize_img, this->mean_, this->scale_, + this->is_scale_); + + float *input = new float[1 * 3 * resize_img.rows * resize_img.cols]; + + this->permute_op_.Run(&resize_img, input); + + auto input_names = this->predictor_->GetInputNames(); + auto input_t = this->predictor_->GetInputTensor(input_names[0]); + input_t->Reshape({1, 3, resize_img.rows, resize_img.cols}); + input_t->copy_from_cpu(input); + + this->predictor_->ZeroCopyRun(); + + std::vector rec_idx; + auto output_names = this->predictor_->GetOutputNames(); + auto output_t = this->predictor_->GetOutputTensor(output_names[0]); + auto rec_idx_lod = output_t->lod(); + auto shape_out = output_t->shape(); + int out_num = std::accumulate(shape_out.begin(), shape_out.end(), 1, + std::multiplies()); + + rec_idx.resize(out_num); + output_t->copy_to_cpu(rec_idx.data()); + + std::vector pred_idx; + for (int n = int(rec_idx_lod[0][0]); n < int(rec_idx_lod[0][1]); n++) { + pred_idx.push_back(int(rec_idx[n])); + } + + if (pred_idx.size() < 1e-3) + continue; + + index += 1; + std::cout << index << "\t"; + for (int n = 0; n < pred_idx.size(); n++) { + std::cout << label_list_[pred_idx[n]]; + } + + std::vector predict_batch; + auto output_t_1 = this->predictor_->GetOutputTensor(output_names[1]); + + auto predict_lod = output_t_1->lod(); + auto predict_shape = output_t_1->shape(); + int out_num_1 = std::accumulate(predict_shape.begin(), predict_shape.end(), + 1, std::multiplies()); + + predict_batch.resize(out_num_1); + output_t_1->copy_to_cpu(predict_batch.data()); + + int argmax_idx; + int blank = predict_shape[1]; + float score = 0.f; + int count = 0; + float max_value = 0.0f; + + for (int n = predict_lod[0][0]; n < predict_lod[0][1] - 1; n++) { + argmax_idx = int(argmax(&predict_batch[n * predict_shape[1]], + &predict_batch[(n + 1) * predict_shape[1]])); + max_value = + float(*std::max_element(&predict_batch[n * predict_shape[1]], + &predict_batch[(n + 1) * predict_shape[1]])); + if (blank - 1 - argmax_idx > 1e-5) { + score += max_value; + count += 1; + } + } + score /= count; + std::cout << "\tscore: " << score << std::endl; + + delete[] input; + } +} + +void CRNNRecognizer::LoadModel(const std::string &model_dir, bool use_gpu, + const int gpu_id, const int min_subgraph_size, + const int batch_size) { + AnalysisConfig config; + config.SetModel(model_dir + "/model", model_dir + "/params"); + + // for cpu + config.DisableGpu(); + config.EnableMKLDNN(); // 开启MKLDNN加速 + config.SetCpuMathLibraryNumThreads(10); + + // 使用ZeroCopyTensor,此处必须设置为false + config.SwitchUseFeedFetchOps(false); + // 若输入为多个,此处必须设置为true + config.SwitchSpecifyInputNames(true); + // config.SwitchIrDebug(true); // + // 可视化调试选项,若开启,则会在每个图优化过程后生成dot文件 + // config.SwitchIrOptim(false);// 默认为true。如果设置为false,关闭所有优化 + config.EnableMemoryOptim(); // 开启内存/显存复用 + + this->predictor_ = CreatePaddlePredictor(config); +} + +cv::Mat +CRNNRecognizer::get_rotate_crop_image(const cv::Mat &srcimage, + std::vector> box) { + cv::Mat image; + srcimage.copyTo(image); + std::vector> points = box; + + int x_collect[4] = {box[0][0], box[1][0], box[2][0], box[3][0]}; + int y_collect[4] = {box[0][1], box[1][1], box[2][1], box[3][1]}; + int left = int(*std::min_element(x_collect, x_collect + 4)); + int right = int(*std::max_element(x_collect, x_collect + 4)); + int top = int(*std::min_element(y_collect, y_collect + 4)); + int bottom = int(*std::max_element(y_collect, y_collect + 4)); + + cv::Mat img_crop; + image(cv::Rect(left, top, right - left, bottom - top)).copyTo(img_crop); + + for (int i = 0; i < points.size(); i++) { + points[i][0] -= left; + points[i][1] -= top; + } + + int img_crop_width = int(sqrt(pow(points[0][0] - points[1][0], 2) + + pow(points[0][1] - points[1][1], 2))); + int img_crop_height = int(sqrt(pow(points[0][0] - points[3][0], 2) + + pow(points[0][1] - points[3][1], 2))); + + cv::Point2f pts_std[4]; + pts_std[0] = cv::Point2f(0., 0.); + pts_std[1] = cv::Point2f(img_crop_width, 0.); + pts_std[2] = cv::Point2f(img_crop_width, img_crop_height); + pts_std[3] = cv::Point2f(0.f, img_crop_height); + + cv::Point2f pointsf[4]; + pointsf[0] = cv::Point2f(points[0][0], points[0][1]); + pointsf[1] = cv::Point2f(points[1][0], points[1][1]); + pointsf[2] = cv::Point2f(points[2][0], points[2][1]); + pointsf[3] = cv::Point2f(points[3][0], points[3][1]); + + cv::Mat M = cv::getPerspectiveTransform(pointsf, pts_std); + + cv::Mat dst_img; + cv::warpPerspective(img_crop, dst_img, M, + cv::Size(img_crop_width, img_crop_height), + cv::BORDER_REPLICATE); + + if (float(dst_img.rows) >= float(dst_img.cols) * 1.5) { + cv::Mat srcCopy = cv::Mat(dst_img.rows, dst_img.cols, dst_img.depth()); + cv::transpose(dst_img, srcCopy); + cv::flip(srcCopy, srcCopy, 0); + return srcCopy; + } else { + return dst_img; + } +} + +std::vector CRNNRecognizer::ReadDict(const std::string &path) { + std::ifstream in(path); + std::string filename; + std::string line; + std::vector m_vec; + if (in) { + while (getline(in, line)) { + m_vec.push_back(line); + } + } else { + std::cout << "no such file" << std::endl; + } + return m_vec; +} + +} // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/src/postprocess_op.cpp b/deploy/cpp_infer/src/postprocess_op.cpp new file mode 100644 index 00000000..69036db1 --- /dev/null +++ b/deploy/cpp_infer/src/postprocess_op.cpp @@ -0,0 +1,307 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace PaddleOCR { + +void PostProcessor::GetContourArea(float **box, float unclip_ratio, + float &distance) { + int pts_num = 4; + float area = 0.0f; + float dist = 0.0f; + for (int i = 0; i < pts_num; i++) { + area += box[i][0] * box[(i + 1) % pts_num][1] - + box[i][1] * box[(i + 1) % pts_num][0]; + dist += sqrtf((box[i][0] - box[(i + 1) % pts_num][0]) * + (box[i][0] - box[(i + 1) % pts_num][0]) + + (box[i][1] - box[(i + 1) % pts_num][1]) * + (box[i][1] - box[(i + 1) % pts_num][1])); + } + area = fabs(float(area / 2.0)); + + distance = area * unclip_ratio / dist; +} + +cv::RotatedRect PostProcessor::unclip(float **box) { + float unclip_ratio = 2.0; + float distance = 1.0; + + GetContourArea(box, unclip_ratio, distance); + + ClipperLib::ClipperOffset offset; + ClipperLib::Path p; + p << ClipperLib::IntPoint(int(box[0][0]), int(box[0][1])) + << ClipperLib::IntPoint(int(box[1][0]), int(box[1][1])) + << ClipperLib::IntPoint(int(box[2][0]), int(box[2][1])) + << ClipperLib::IntPoint(int(box[3][0]), int(box[3][1])); + offset.AddPath(p, ClipperLib::jtRound, ClipperLib::etClosedPolygon); + + ClipperLib::Paths soln; + offset.Execute(soln, distance); + std::vector points; + + for (int j = 0; j < soln.size(); j++) { + for (int i = 0; i < soln[soln.size() - 1].size(); i++) { + points.emplace_back(soln[j][i].X, soln[j][i].Y); + } + } + cv::RotatedRect res = cv::minAreaRect(points); + + return res; +} + +float **PostProcessor::Mat2Vec(cv::Mat mat) { + auto **array = new float *[mat.rows]; + for (int i = 0; i < mat.rows; ++i) + array[i] = new float[mat.cols]; + for (int i = 0; i < mat.rows; ++i) { + for (int j = 0; j < mat.cols; ++j) { + array[i][j] = mat.at(i, j); + } + } + + return array; +} + +void PostProcessor::quickSort(float **s, int l, int r) { + if (l < r) { + int i = l, j = r; + float x = s[l][0]; + float *xp = s[l]; + while (i < j) { + while (i < j && s[j][0] >= x) + j--; + if (i < j) + std::swap(s[i++], s[j]); + while (i < j && s[i][0] < x) + i++; + if (i < j) + std::swap(s[j--], s[i]); + } + s[i] = xp; + quickSort(s, l, i - 1); + quickSort(s, i + 1, r); + } +} + +void PostProcessor::quickSort_vector(std::vector> &box, int l, + int r, int axis) { + if (l < r) { + int i = l, j = r; + int x = box[l][axis]; + std::vector xp(box[l]); + while (i < j) { + while (i < j && box[j][axis] >= x) + j--; + if (i < j) + std::swap(box[i++], box[j]); + while (i < j && box[i][axis] < x) + i++; + if (i < j) + std::swap(box[j--], box[i]); + } + box[i] = xp; + quickSort_vector(box, l, i - 1, axis); + quickSort_vector(box, i + 1, r, axis); + } +} + +std::vector> +PostProcessor::order_points_clockwise(std::vector> pts) { + std::vector> box = pts; + quickSort_vector(box, 0, int(box.size() - 1), 0); + std::vector> leftmost = {box[0], box[1]}; + std::vector> rightmost = {box[2], box[3]}; + + if (leftmost[0][1] > leftmost[1][1]) + std::swap(leftmost[0], leftmost[1]); + + if (rightmost[0][1] > rightmost[1][1]) + std::swap(rightmost[0], rightmost[1]); + + std::vector> rect = {leftmost[0], rightmost[0], rightmost[1], + leftmost[1]}; + return rect; +} + +float **PostProcessor::get_mini_boxes(cv::RotatedRect box, float &ssid) { + ssid = box.size.width >= box.size.height ? box.size.height : box.size.width; + + cv::Mat points; + cv::boxPoints(box, points); + // sorted box points + auto array = Mat2Vec(points); + quickSort(array, 0, 3); + + float *idx1 = array[0], *idx2 = array[1], *idx3 = array[2], *idx4 = array[3]; + if (array[3][1] <= array[2][1]) { + idx2 = array[3]; + idx3 = array[2]; + } else { + idx2 = array[2]; + idx3 = array[3]; + } + if (array[1][1] <= array[0][1]) { + idx1 = array[1]; + idx4 = array[0]; + } else { + idx1 = array[0]; + idx4 = array[1]; + } + + array[0] = idx1; + array[1] = idx2; + array[2] = idx3; + array[3] = idx4; + + return array; +} + +float PostProcessor::box_score_fast(float **box_array, cv::Mat pred) { + auto array = box_array; + int width = pred.cols; + int height = pred.rows; + + float box_x[4] = {array[0][0], array[1][0], array[2][0], array[3][0]}; + float box_y[4] = {array[0][1], array[1][1], array[2][1], array[3][1]}; + + int xmin = clamp(int(std::floor(*(std::min_element(box_x, box_x + 4)))), 0, + width - 1); + int xmax = clamp(int(std::ceil(*(std::max_element(box_x, box_x + 4)))), 0, + width - 1); + int ymin = clamp(int(std::floor(*(std::min_element(box_y, box_y + 4)))), 0, + height - 1); + int ymax = clamp(int(std::ceil(*(std::max_element(box_y, box_y + 4)))), 0, + height - 1); + + cv::Mat mask; + mask = cv::Mat::zeros(ymax - ymin + 1, xmax - xmin + 1, CV_8UC1); + + cv::Point root_point[4]; + root_point[0] = cv::Point(int(array[0][0]) - xmin, int(array[0][1]) - ymin); + root_point[1] = cv::Point(int(array[1][0]) - xmin, int(array[1][1]) - ymin); + root_point[2] = cv::Point(int(array[2][0]) - xmin, int(array[2][1]) - ymin); + root_point[3] = cv::Point(int(array[3][0]) - xmin, int(array[3][1]) - ymin); + const cv::Point *ppt[1] = {root_point}; + int npt[] = {4}; + cv::fillPoly(mask, ppt, npt, 1, cv::Scalar(1)); + + cv::Mat croppedImg; + pred(cv::Rect(xmin, ymin, xmax - xmin + 1, ymax - ymin + 1)) + .copyTo(croppedImg); + + auto score = cv::mean(croppedImg, mask)[0]; + return score; +} + +std::vector>> +PostProcessor::boxes_from_bitmap(const cv::Mat pred, const cv::Mat bitmap) { + const int min_size = 3; + const int max_candidates = 1000; + const float box_thresh = 0.5; + + int width = bitmap.cols; + int height = bitmap.rows; + + std::vector> contours; + std::vector hierarchy; + + cv::findContours(bitmap, contours, hierarchy, cv::RETR_LIST, + cv::CHAIN_APPROX_SIMPLE); + + int num_contours = + contours.size() >= max_candidates ? max_candidates : contours.size(); + + std::vector>> boxes; + + for (int _i = 0; _i < num_contours; _i++) { + float ssid; + cv::RotatedRect box = cv::minAreaRect(contours[_i]); + auto array = get_mini_boxes(box, ssid); + + auto box_for_unclip = array; + // end get_mini_box + + if (ssid < min_size) { + continue; + } + + float score; + score = box_score_fast(array, pred); + // end box_score_fast + if (score < box_thresh) + continue; + + // start for unclip + cv::RotatedRect points = unclip(box_for_unclip); + // end for unclip + + cv::RotatedRect clipbox = points; + auto cliparray = get_mini_boxes(clipbox, ssid); + + if (ssid < min_size + 2) + continue; + + int dest_width = pred.cols; + int dest_height = pred.rows; + std::vector> intcliparray; + + for (int num_pt = 0; num_pt < 4; num_pt++) { + std::vector a{int(clampf(roundf(cliparray[num_pt][0] / float(width) * + float(dest_width)), + 0, float(dest_width))), + int(clampf(roundf(cliparray[num_pt][1] / + float(height) * float(dest_height)), + 0, float(dest_height)))}; + intcliparray.push_back(a); + } + boxes.push_back(intcliparray); + + } // end for + return boxes; +} + +std::vector>> PostProcessor::filter_tag_det_res( + std::vector>> boxes, float ratio_h, + float ratio_w, cv::Mat srcimg) { + int oriimg_h = srcimg.rows; + int oriimg_w = srcimg.cols; + + std::vector>> root_points; + for (int n = 0; n < boxes.size(); n++) { + boxes[n] = order_points_clockwise(boxes[n]); + for (int m = 0; m < boxes[0].size(); m++) { + boxes[n][m][0] /= ratio_w; + boxes[n][m][1] /= ratio_h; + + boxes[n][m][0] = int(_min(_max(boxes[n][m][0], 0), oriimg_w - 1)); + boxes[n][m][1] = int(_min(_max(boxes[n][m][1], 0), oriimg_h - 1)); + } + } + + for (int n = 0; n < boxes.size(); n++) { + int rect_width, rect_height; + rect_width = int(sqrt(pow(boxes[n][0][0] - boxes[n][1][0], 2) + + pow(boxes[n][0][1] - boxes[n][1][1], 2))); + rect_height = int(sqrt(pow(boxes[n][0][0] - boxes[n][3][0], 2) + + pow(boxes[n][0][1] - boxes[n][3][1], 2))); + if (rect_width <= 10 || rect_height <= 10) + continue; + root_points.push_back(boxes[n]); + } + return root_points; +} + +} // namespace PaddleOCR diff --git a/deploy/cpp_infer/src/preprocess_op.cpp b/deploy/cpp_infer/src/preprocess_op.cpp new file mode 100644 index 00000000..5fee3c4d --- /dev/null +++ b/deploy/cpp_infer/src/preprocess_op.cpp @@ -0,0 +1,119 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include "paddle_api.h" +#include "paddle_inference_api.h" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace PaddleOCR { + +void Permute::Run(const cv::Mat *im, float *data) { + int rh = im->rows; + int rw = im->cols; + int rc = im->channels(); + for (int i = 0; i < rc; ++i) { + cv::extractChannel(*im, cv::Mat(rh, rw, CV_32FC1, data + i * rh * rw), i); + } +} + +void Normalize::Run(cv::Mat *im, const std::vector &mean, + const std::vector &scale, const bool is_scale) { + double e = 1.0; + if (is_scale) { + e /= 255.0; + } + (*im).convertTo(*im, CV_32FC3, e); + for (int h = 0; h < im->rows; h++) { + for (int w = 0; w < im->cols; w++) { + im->at(h, w)[0] = + (im->at(h, w)[0] - mean[0]) * scale[0]; + im->at(h, w)[1] = + (im->at(h, w)[1] - mean[1]) * scale[1]; + im->at(h, w)[2] = + (im->at(h, w)[2] - mean[2]) * scale[2]; + } + } +} + +void ResizeImgType0::Run(const cv::Mat &img, cv::Mat &resize_img, + int max_size_len, float &ratio_h, float &ratio_w) { + int w = img.cols; + int h = img.rows; + + float ratio = 1.f; + int max_wh = w >= h ? w : h; + if (max_wh > max_size_len) { + if (h > w) { + ratio = float(max_size_len) / float(h); + } else { + ratio = float(max_size_len) / float(w); + } + } + + int resize_h = int(float(h) * ratio); + int resize_w = int(float(w) * ratio); + if (resize_h % 32 == 0) + resize_h = resize_h; + else if (resize_h / 32 < 1 + 1e-5) + resize_h = 32; + else + resize_h = (resize_h / 32 - 1) * 32; + + if (resize_w % 32 == 0) + resize_w = resize_w; + else if (resize_w / 32 < 1) + resize_w = 32; + else + resize_w = (resize_w / 32 - 1) * 32; + + cv::resize(img, resize_img, cv::Size(resize_w, resize_h)); + + ratio_h = float(resize_h) / float(h); + ratio_w = float(resize_w) / float(w); +} + +void CrnnResizeImg::Run(const cv::Mat &img, cv::Mat &resize_img, float wh_ratio, + const std::vector rec_image_shape) { + int imgC, imgH, imgW; + imgC = rec_image_shape[0]; + imgH = rec_image_shape[1]; + imgW = rec_image_shape[2]; + + imgW = int(32 * wh_ratio); + + float ratio = float(img.cols) / float(img.rows); + int resize_w, resize_h; + if (ceilf(imgH * ratio) > imgW) + resize_w = imgW; + else + resize_w = int(ceilf(imgH * ratio)); + + cv::resize(img, resize_img, cv::Size(resize_w, imgH), 0.f, 0.f, + cv::INTER_LINEAR); +} + +} // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/tools/build.sh b/deploy/cpp_infer/tools/build.sh new file mode 100755 index 00000000..a3a709ca --- /dev/null +++ b/deploy/cpp_infer/tools/build.sh @@ -0,0 +1,24 @@ + +LIB_DIR=/paddle/code/gry/Paddle/build/fluid_inference_install_dir/ +CUDA_LIB_DIR=/usr/local/cuda/lib64 +CUDNN_LIB_DIR=/usr/lib/x86_64-linux-gnu/ +TENSORRT_ROOT_DIR=YOUR_TENSORRT_ROOT_DIR + +BUILD_DIR=build +rm -rf ${BUILD_DIR} +mkdir ${BUILD_DIR} +cd ${BUILD_DIR} +cmake .. \ + -DPADDLE_LIB=${LIB_DIR} \ + -DWITH_MKL=ON \ + -DDEMO_NAME=ocr_system \ + -DWITH_GPU=OFF \ + -DWITH_STATIC_LIB=OFF \ + -DUSE_TENSORRT=OFF \ + -DCUDNN_LIB=${CUDNN_LIB_DIR} \ + -DCUDA_LIB=${CUDA_LIB_DIR} \ + -DTENSORRT_ROOT=YOUR_TENSORRT_ROOT_DIR + +make -j + + diff --git a/deploy/cpp_infer/tools/ppocr_keys_v1.txt b/deploy/cpp_infer/tools/ppocr_keys_v1.txt new file mode 100644 index 00000000..84b885d8 --- /dev/null +++ b/deploy/cpp_infer/tools/ppocr_keys_v1.txt @@ -0,0 +1,6623 @@ +' +疗 +绚 +诚 +娇 +溜 +题 +贿 +者 +廖 +更 +纳 +加 +奉 +公 +一 +就 +汴 +计 +与 +路 +房 +原 +妇 +2 +0 +8 +- +7 +其 +> +: +] +, +, +骑 +刈 +全 +消 +昏 +傈 +安 +久 +钟 +嗅 +不 +影 +处 +驽 +蜿 +资 +关 +椤 +地 +瘸 +专 +问 +忖 +票 +嫉 +炎 +韵 +要 +月 +田 +节 +陂 +鄙 +捌 +备 +拳 +伺 +眼 +网 +盎 +大 +傍 +心 +东 +愉 +汇 +蹿 +科 +每 +业 +里 +航 +晏 +字 +平 +录 +先 +1 +3 +彤 +鲶 +产 +稍 +督 +腴 +有 +象 +岳 +注 +绍 +在 +泺 +文 +定 +核 +名 +水 +过 +理 +让 +偷 +率 +等 +这 +发 +” +为 +含 +肥 +酉 +相 +鄱 +七 +编 +猥 +锛 +日 +镀 +蒂 +掰 +倒 +辆 +栾 +栗 +综 +涩 +州 +雌 +滑 +馀 +了 +机 +块 +司 +宰 +甙 +兴 +矽 +抚 +保 +用 +沧 +秩 +如 +收 +息 +滥 +页 +疑 +埠 +! +! +姥 +异 +橹 +钇 +向 +下 +跄 +的 +椴 +沫 +国 +绥 +獠 +报 +开 +民 +蜇 +何 +分 +凇 +长 +讥 +藏 +掏 +施 +羽 +中 +讲 +派 +嘟 +人 +提 +浼 +间 +世 +而 +古 +多 +倪 +唇 +饯 +控 +庚 +首 +赛 +蜓 +味 +断 +制 +觉 +技 +替 +艰 +溢 +潮 +夕 +钺 +外 +摘 +枋 +动 +双 +单 +啮 +户 +枇 +确 +锦 +曜 +杜 +或 +能 +效 +霜 +盒 +然 +侗 +电 +晁 +放 +步 +鹃 +新 +杖 +蜂 +吒 +濂 +瞬 +评 +总 +隍 +对 +独 +合 +也 +是 +府 +青 +天 +诲 +墙 +组 +滴 +级 +邀 +帘 +示 +已 +时 +骸 +仄 +泅 +和 +遨 +店 +雇 +疫 +持 +巍 +踮 +境 +只 +亨 +目 +鉴 +崤 +闲 +体 +泄 +杂 +作 +般 +轰 +化 +解 +迂 +诿 +蛭 +璀 +腾 +告 +版 +服 +省 +师 +小 +规 +程 +线 +海 +办 +引 +二 +桧 +牌 +砺 +洄 +裴 +修 +图 +痫 +胡 +许 +犊 +事 +郛 +基 +柴 +呼 +食 +研 +奶 +律 +蛋 +因 +葆 +察 +戏 +褒 +戒 +再 +李 +骁 +工 +貂 +油 +鹅 +章 +啄 +休 +场 +给 +睡 +纷 +豆 +器 +捎 +说 +敏 +学 +会 +浒 +设 +诊 +格 +廓 +查 +来 +霓 +室 +溆 +¢ +诡 +寥 +焕 +舜 +柒 +狐 +回 +戟 +砾 +厄 +实 +翩 +尿 +五 +入 +径 +惭 +喹 +股 +宇 +篝 +| +; +美 +期 +云 +九 +祺 +扮 +靠 +锝 +槌 +系 +企 +酰 +阊 +暂 +蚕 +忻 +豁 +本 +羹 +执 +条 +钦 +H +獒 +限 +进 +季 +楦 +于 +芘 +玖 +铋 +茯 +未 +答 +粘 +括 +样 +精 +欠 +矢 +甥 +帷 +嵩 +扣 +令 +仔 +风 +皈 +行 +支 +部 +蓉 +刮 +站 +蜡 +救 +钊 +汗 +松 +嫌 +成 +可 +. +鹤 +院 +从 +交 +政 +怕 +活 +调 +球 +局 +验 +髌 +第 +韫 +谗 +串 +到 +圆 +年 +米 +/ +* +友 +忿 +检 +区 +看 +自 +敢 +刃 +个 +兹 +弄 +流 +留 +同 +没 +齿 +星 +聆 +轼 +湖 +什 +三 +建 +蛔 +儿 +椋 +汕 +震 +颧 +鲤 +跟 +力 +情 +璺 +铨 +陪 +务 +指 +族 +训 +滦 +鄣 +濮 +扒 +商 +箱 +十 +召 +慷 +辗 +所 +莞 +管 +护 +臭 +横 +硒 +嗓 +接 +侦 +六 +露 +党 +馋 +驾 +剖 +高 +侬 +妪 +幂 +猗 +绺 +骐 +央 +酐 +孝 +筝 +课 +徇 +缰 +门 +男 +西 +项 +句 +谙 +瞒 +秃 +篇 +教 +碲 +罚 +声 +呐 +景 +前 +富 +嘴 +鳌 +稀 +免 +朋 +啬 +睐 +去 +赈 +鱼 +住 +肩 +愕 +速 +旁 +波 +厅 +健 +茼 +厥 +鲟 +谅 +投 +攸 +炔 +数 +方 +击 +呋 +谈 +绩 +别 +愫 +僚 +躬 +鹧 +胪 +炳 +招 +喇 +膨 +泵 +蹦 +毛 +结 +5 +4 +谱 +识 +陕 +粽 +婚 +拟 +构 +且 +搜 +任 +潘 +比 +郢 +妨 +醪 +陀 +桔 +碘 +扎 +选 +哈 +骷 +楷 +亿 +明 +缆 +脯 +监 +睫 +逻 +婵 +共 +赴 +淝 +凡 +惦 +及 +达 +揖 +谩 +澹 +减 +焰 +蛹 +番 +祁 +柏 +员 +禄 +怡 +峤 +龙 +白 +叽 +生 +闯 +起 +细 +装 +谕 +竟 +聚 +钙 +上 +导 +渊 +按 +艾 +辘 +挡 +耒 +盹 +饪 +臀 +记 +邮 +蕙 +受 +各 +医 +搂 +普 +滇 +朗 +茸 +带 +翻 +酚 +( +光 +堤 +墟 +蔷 +万 +幻 +〓 +瑙 +辈 +昧 +盏 +亘 +蛀 +吉 +铰 +请 +子 +假 +闻 +税 +井 +诩 +哨 +嫂 +好 +面 +琐 +校 +馊 +鬣 +缂 +营 +访 +炖 +占 +农 +缀 +否 +经 +钚 +棵 +趟 +张 +亟 +吏 +茶 +谨 +捻 +论 +迸 +堂 +玉 +信 +吧 +瞠 +乡 +姬 +寺 +咬 +溏 +苄 +皿 +意 +赉 +宝 +尔 +钰 +艺 +特 +唳 +踉 +都 +荣 +倚 +登 +荐 +丧 +奇 +涵 +批 +炭 +近 +符 +傩 +感 +道 +着 +菊 +虹 +仲 +众 +懈 +濯 +颞 +眺 +南 +释 +北 +缝 +标 +既 +茗 +整 +撼 +迤 +贲 +挎 +耱 +拒 +某 +妍 +卫 +哇 +英 +矶 +藩 +治 +他 +元 +领 +膜 +遮 +穗 +蛾 +飞 +荒 +棺 +劫 +么 +市 +火 +温 +拈 +棚 +洼 +转 +果 +奕 +卸 +迪 +伸 +泳 +斗 +邡 +侄 +涨 +屯 +萋 +胭 +氡 +崮 +枞 +惧 +冒 +彩 +斜 +手 +豚 +随 +旭 +淑 +妞 +形 +菌 +吲 +沱 +争 +驯 +歹 +挟 +兆 +柱 +传 +至 +包 +内 +响 +临 +红 +功 +弩 +衡 +寂 +禁 +老 +棍 +耆 +渍 +织 +害 +氵 +渑 +布 +载 +靥 +嗬 +虽 +苹 +咨 +娄 +库 +雉 +榜 +帜 +嘲 +套 +瑚 +亲 +簸 +欧 +边 +6 +腿 +旮 +抛 +吹 +瞳 +得 +镓 +梗 +厨 +继 +漾 +愣 +憨 +士 +策 +窑 +抑 +躯 +襟 +脏 +参 +贸 +言 +干 +绸 +鳄 +穷 +藜 +音 +折 +详 +) +举 +悍 +甸 +癌 +黎 +谴 +死 +罩 +迁 +寒 +驷 +袖 +媒 +蒋 +掘 +模 +纠 +恣 +观 +祖 +蛆 +碍 +位 +稿 +主 +澧 +跌 +筏 +京 +锏 +帝 +贴 +证 +糠 +才 +黄 +鲸 +略 +炯 +饱 +四 +出 +园 +犀 +牧 +容 +汉 +杆 +浈 +汰 +瑷 +造 +虫 +瘩 +怪 +驴 +济 +应 +花 +沣 +谔 +夙 +旅 +价 +矿 +以 +考 +s +u +呦 +晒 +巡 +茅 +准 +肟 +瓴 +詹 +仟 +褂 +译 +桌 +混 +宁 +怦 +郑 +抿 +些 +余 +鄂 +饴 +攒 +珑 +群 +阖 +岔 +琨 +藓 +预 +环 +洮 +岌 +宀 +杲 +瀵 +最 +常 +囡 +周 +踊 +女 +鼓 +袭 +喉 +简 +范 +薯 +遐 +疏 +粱 +黜 +禧 +法 +箔 +斤 +遥 +汝 +奥 +直 +贞 +撑 +置 +绱 +集 +她 +馅 +逗 +钧 +橱 +魉 +[ +恙 +躁 +唤 +9 +旺 +膘 +待 +脾 +惫 +购 +吗 +依 +盲 +度 +瘿 +蠖 +俾 +之 +镗 +拇 +鲵 +厝 +簧 +续 +款 +展 +啃 +表 +剔 +品 +钻 +腭 +损 +清 +锶 +统 +涌 +寸 +滨 +贪 +链 +吠 +冈 +伎 +迥 +咏 +吁 +览 +防 +迅 +失 +汾 +阔 +逵 +绀 +蔑 +列 +川 +凭 +努 +熨 +揪 +利 +俱 +绉 +抢 +鸨 +我 +即 +责 +膦 +易 +毓 +鹊 +刹 +玷 +岿 +空 +嘞 +绊 +排 +术 +估 +锷 +违 +们 +苟 +铜 +播 +肘 +件 +烫 +审 +鲂 +广 +像 +铌 +惰 +铟 +巳 +胍 +鲍 +康 +憧 +色 +恢 +想 +拷 +尤 +疳 +知 +S +Y +F +D +A +峄 +裕 +帮 +握 +搔 +氐 +氘 +难 +墒 +沮 +雨 +叁 +缥 +悴 +藐 +湫 +娟 +苑 +稠 +颛 +簇 +后 +阕 +闭 +蕤 +缚 +怎 +佞 +码 +嘤 +蔡 +痊 +舱 +螯 +帕 +赫 +昵 +升 +烬 +岫 +、 +疵 +蜻 +髁 +蕨 +隶 +烛 +械 +丑 +盂 +梁 +强 +鲛 +由 +拘 +揉 +劭 +龟 +撤 +钩 +呕 +孛 +费 +妻 +漂 +求 +阑 +崖 +秤 +甘 +通 +深 +补 +赃 +坎 +床 +啪 +承 +吼 +量 +暇 +钼 +烨 +阂 +擎 +脱 +逮 +称 +P +神 +属 +矗 +华 +届 +狍 +葑 +汹 +育 +患 +窒 +蛰 +佼 +静 +槎 +运 +鳗 +庆 +逝 +曼 +疱 +克 +代 +官 +此 +麸 +耧 +蚌 +晟 +例 +础 +榛 +副 +测 +唰 +缢 +迹 +灬 +霁 +身 +岁 +赭 +扛 +又 +菡 +乜 +雾 +板 +读 +陷 +徉 +贯 +郁 +虑 +变 +钓 +菜 +圾 +现 +琢 +式 +乐 +维 +渔 +浜 +左 +吾 +脑 +钡 +警 +T +啵 +拴 +偌 +漱 +湿 +硕 +止 +骼 +魄 +积 +燥 +联 +踢 +玛 +则 +窿 +见 +振 +畿 +送 +班 +钽 +您 +赵 +刨 +印 +讨 +踝 +籍 +谡 +舌 +崧 +汽 +蔽 +沪 +酥 +绒 +怖 +财 +帖 +肱 +私 +莎 +勋 +羔 +霸 +励 +哼 +帐 +将 +帅 +渠 +纪 +婴 +娩 +岭 +厘 +滕 +吻 +伤 +坝 +冠 +戊 +隆 +瘁 +介 +涧 +物 +黍 +并 +姗 +奢 +蹑 +掣 +垸 +锴 +命 +箍 +捉 +病 +辖 +琰 +眭 +迩 +艘 +绌 +繁 +寅 +若 +毋 +思 +诉 +类 +诈 +燮 +轲 +酮 +狂 +重 +反 +职 +筱 +县 +委 +磕 +绣 +奖 +晋 +濉 +志 +徽 +肠 +呈 +獐 +坻 +口 +片 +碰 +几 +村 +柿 +劳 +料 +获 +亩 +惕 +晕 +厌 +号 +罢 +池 +正 +鏖 +煨 +家 +棕 +复 +尝 +懋 +蜥 +锅 +岛 +扰 +队 +坠 +瘾 +钬 +@ +卧 +疣 +镇 +譬 +冰 +彷 +频 +黯 +据 +垄 +采 +八 +缪 +瘫 +型 +熹 +砰 +楠 +襁 +箐 +但 +嘶 +绳 +啤 +拍 +盥 +穆 +傲 +洗 +盯 +塘 +怔 +筛 +丿 +台 +恒 +喂 +葛 +永 +¥ +烟 +酒 +桦 +书 +砂 +蚝 +缉 +态 +瀚 +袄 +圳 +轻 +蛛 +超 +榧 +遛 +姒 +奘 +铮 +右 +荽 +望 +偻 +卡 +丶 +氰 +附 +做 +革 +索 +戚 +坨 +桷 +唁 +垅 +榻 +岐 +偎 +坛 +莨 +山 +殊 +微 +骇 +陈 +爨 +推 +嗝 +驹 +澡 +藁 +呤 +卤 +嘻 +糅 +逛 +侵 +郓 +酌 +德 +摇 +※ +鬃 +被 +慨 +殡 +羸 +昌 +泡 +戛 +鞋 +河 +宪 +沿 +玲 +鲨 +翅 +哽 +源 +铅 +语 +照 +邯 +址 +荃 +佬 +顺 +鸳 +町 +霭 +睾 +瓢 +夸 +椁 +晓 +酿 +痈 +咔 +侏 +券 +噎 +湍 +签 +嚷 +离 +午 +尚 +社 +锤 +背 +孟 +使 +浪 +缦 +潍 +鞅 +军 +姹 +驶 +笑 +鳟 +鲁 +》 +孽 +钜 +绿 +洱 +礴 +焯 +椰 +颖 +囔 +乌 +孔 +巴 +互 +性 +椽 +哞 +聘 +昨 +早 +暮 +胶 +炀 +隧 +低 +彗 +昝 +铁 +呓 +氽 +藉 +喔 +癖 +瑗 +姨 +权 +胱 +韦 +堑 +蜜 +酋 +楝 +砝 +毁 +靓 +歙 +锲 +究 +屋 +喳 +骨 +辨 +碑 +武 +鸠 +宫 +辜 +烊 +适 +坡 +殃 +培 +佩 +供 +走 +蜈 +迟 +翼 +况 +姣 +凛 +浔 +吃 +飘 +债 +犟 +金 +促 +苛 +崇 +坂 +莳 +畔 +绂 +兵 +蠕 +斋 +根 +砍 +亢 +欢 +恬 +崔 +剁 +餐 +榫 +快 +扶 +‖ +濒 +缠 +鳜 +当 +彭 +驭 +浦 +篮 +昀 +锆 +秸 +钳 +弋 +娣 +瞑 +夷 +龛 +苫 +拱 +致 +% +嵊 +障 +隐 +弑 +初 +娓 +抉 +汩 +累 +蓖 +" +唬 +助 +苓 +昙 +押 +毙 +破 +城 +郧 +逢 +嚏 +獭 +瞻 +溱 +婿 +赊 +跨 +恼 +璧 +萃 +姻 +貉 +灵 +炉 +密 +氛 +陶 +砸 +谬 +衔 +点 +琛 +沛 +枳 +层 +岱 +诺 +脍 +榈 +埂 +征 +冷 +裁 +打 +蹴 +素 +瘘 +逞 +蛐 +聊 +激 +腱 +萘 +踵 +飒 +蓟 +吆 +取 +咙 +簋 +涓 +矩 +曝 +挺 +揣 +座 +你 +史 +舵 +焱 +尘 +苏 +笈 +脚 +溉 +榨 +诵 +樊 +邓 +焊 +义 +庶 +儋 +蟋 +蒲 +赦 +呷 +杞 +诠 +豪 +还 +试 +颓 +茉 +太 +除 +紫 +逃 +痴 +草 +充 +鳕 +珉 +祗 +墨 +渭 +烩 +蘸 +慕 +璇 +镶 +穴 +嵘 +恶 +骂 +险 +绋 +幕 +碉 +肺 +戳 +刘 +潞 +秣 +纾 +潜 +銮 +洛 +须 +罘 +销 +瘪 +汞 +兮 +屉 +r +林 +厕 +质 +探 +划 +狸 +殚 +善 +煊 +烹 +〒 +锈 +逯 +宸 +辍 +泱 +柚 +袍 +远 +蹋 +嶙 +绝 +峥 +娥 +缍 +雀 +徵 +认 +镱 +谷 += +贩 +勉 +撩 +鄯 +斐 +洋 +非 +祚 +泾 +诒 +饿 +撬 +威 +晷 +搭 +芍 +锥 +笺 +蓦 +候 +琊 +档 +礁 +沼 +卵 +荠 +忑 +朝 +凹 +瑞 +头 +仪 +弧 +孵 +畏 +铆 +突 +衲 +车 +浩 +气 +茂 +悖 +厢 +枕 +酝 +戴 +湾 +邹 +飚 +攘 +锂 +写 +宵 +翁 +岷 +无 +喜 +丈 +挑 +嗟 +绛 +殉 +议 +槽 +具 +醇 +淞 +笃 +郴 +阅 +饼 +底 +壕 +砚 +弈 +询 +缕 +庹 +翟 +零 +筷 +暨 +舟 +闺 +甯 +撞 +麂 +茌 +蔼 +很 +珲 +捕 +棠 +角 +阉 +媛 +娲 +诽 +剿 +尉 +爵 +睬 +韩 +诰 +匣 +危 +糍 +镯 +立 +浏 +阳 +少 +盆 +舔 +擘 +匪 +申 +尬 +铣 +旯 +抖 +赘 +瓯 +居 +ˇ +哮 +游 +锭 +茏 +歌 +坏 +甚 +秒 +舞 +沙 +仗 +劲 +潺 +阿 +燧 +郭 +嗖 +霏 +忠 +材 +奂 +耐 +跺 +砀 +输 +岖 +媳 +氟 +极 +摆 +灿 +今 +扔 +腻 +枝 +奎 +药 +熄 +吨 +话 +q +额 +慑 +嘌 +协 +喀 +壳 +埭 +视 +著 +於 +愧 +陲 +翌 +峁 +颅 +佛 +腹 +聋 +侯 +咎 +叟 +秀 +颇 +存 +较 +罪 +哄 +岗 +扫 +栏 +钾 +羌 +己 +璨 +枭 +霉 +煌 +涸 +衿 +键 +镝 +益 +岢 +奏 +连 +夯 +睿 +冥 +均 +糖 +狞 +蹊 +稻 +爸 +刿 +胥 +煜 +丽 +肿 +璃 +掸 +跚 +灾 +垂 +樾 +濑 +乎 +莲 +窄 +犹 +撮 +战 +馄 +软 +络 +显 +鸢 +胸 +宾 +妲 +恕 +埔 +蝌 +份 +遇 +巧 +瞟 +粒 +恰 +剥 +桡 +博 +讯 +凯 +堇 +阶 +滤 +卖 +斌 +骚 +彬 +兑 +磺 +樱 +舷 +两 +娱 +福 +仃 +差 +找 +桁 +÷ +净 +把 +阴 +污 +戬 +雷 +碓 +蕲 +楚 +罡 +焖 +抽 +妫 +咒 +仑 +闱 +尽 +邑 +菁 +爱 +贷 +沥 +鞑 +牡 +嗉 +崴 +骤 +塌 +嗦 +订 +拮 +滓 +捡 +锻 +次 +坪 +杩 +臃 +箬 +融 +珂 +鹗 +宗 +枚 +降 +鸬 +妯 +阄 +堰 +盐 +毅 +必 +杨 +崃 +俺 +甬 +状 +莘 +货 +耸 +菱 +腼 +铸 +唏 +痤 +孚 +澳 +懒 +溅 +翘 +疙 +杷 +淼 +缙 +骰 +喊 +悉 +砻 +坷 +艇 +赁 +界 +谤 +纣 +宴 +晃 +茹 +归 +饭 +梢 +铡 +街 +抄 +肼 +鬟 +苯 +颂 +撷 +戈 +炒 +咆 +茭 +瘙 +负 +仰 +客 +琉 +铢 +封 +卑 +珥 +椿 +镧 +窨 +鬲 +寿 +御 +袤 +铃 +萎 +砖 +餮 +脒 +裳 +肪 +孕 +嫣 +馗 +嵇 +恳 +氯 +江 +石 +褶 +冢 +祸 +阻 +狈 +羞 +银 +靳 +透 +咳 +叼 +敷 +芷 +啥 +它 +瓤 +兰 +痘 +懊 +逑 +肌 +往 +捺 +坊 +甩 +呻 +〃 +沦 +忘 +膻 +祟 +菅 +剧 +崆 +智 +坯 +臧 +霍 +墅 +攻 +眯 +倘 +拢 +骠 +铐 +庭 +岙 +瓠 +′ +缺 +泥 +迢 +捶 +? +? +郏 +喙 +掷 +沌 +纯 +秘 +种 +听 +绘 +固 +螨 +团 +香 +盗 +妒 +埚 +蓝 +拖 +旱 +荞 +铀 +血 +遏 +汲 +辰 +叩 +拽 +幅 +硬 +惶 +桀 +漠 +措 +泼 +唑 +齐 +肾 +念 +酱 +虚 +屁 +耶 +旗 +砦 +闵 +婉 +馆 +拭 +绅 +韧 +忏 +窝 +醋 +葺 +顾 +辞 +倜 +堆 +辋 +逆 +玟 +贱 +疾 +董 +惘 +倌 +锕 +淘 +嘀 +莽 +俭 +笏 +绑 +鲷 +杈 +择 +蟀 +粥 +嗯 +驰 +逾 +案 +谪 +褓 +胫 +哩 +昕 +颚 +鲢 +绠 +躺 +鹄 +崂 +儒 +俨 +丝 +尕 +泌 +啊 +萸 +彰 +幺 +吟 +骄 +苣 +弦 +脊 +瑰 +〈 +诛 +镁 +析 +闪 +剪 +侧 +哟 +框 +螃 +守 +嬗 +燕 +狭 +铈 +缮 +概 +迳 +痧 +鲲 +俯 +售 +笼 +痣 +扉 +挖 +满 +咋 +援 +邱 +扇 +歪 +便 +玑 +绦 +峡 +蛇 +叨 +〖 +泽 +胃 +斓 +喋 +怂 +坟 +猪 +该 +蚬 +炕 +弥 +赞 +棣 +晔 +娠 +挲 +狡 +创 +疖 +铕 +镭 +稷 +挫 +弭 +啾 +翔 +粉 +履 +苘 +哦 +楼 +秕 +铂 +土 +锣 +瘟 +挣 +栉 +习 +享 +桢 +袅 +磨 +桂 +谦 +延 +坚 +蔚 +噗 +署 +谟 +猬 +钎 +恐 +嬉 +雒 +倦 +衅 +亏 +璩 +睹 +刻 +殿 +王 +算 +雕 +麻 +丘 +柯 +骆 +丸 +塍 +谚 +添 +鲈 +垓 +桎 +蚯 +芥 +予 +飕 +镦 +谌 +窗 +醚 +菀 +亮 +搪 +莺 +蒿 +羁 +足 +J +真 +轶 +悬 +衷 +靛 +翊 +掩 +哒 +炅 +掐 +冼 +妮 +l +谐 +稚 +荆 +擒 +犯 +陵 +虏 +浓 +崽 +刍 +陌 +傻 +孜 +千 +靖 +演 +矜 +钕 +煽 +杰 +酗 +渗 +伞 +栋 +俗 +泫 +戍 +罕 +沾 +疽 +灏 +煦 +芬 +磴 +叱 +阱 +榉 +湃 +蜀 +叉 +醒 +彪 +租 +郡 +篷 +屎 +良 +垢 +隗 +弱 +陨 +峪 +砷 +掴 +颁 +胎 +雯 +绵 +贬 +沐 +撵 +隘 +篙 +暖 +曹 +陡 +栓 +填 +臼 +彦 +瓶 +琪 +潼 +哪 +鸡 +摩 +啦 +俟 +锋 +域 +耻 +蔫 +疯 +纹 +撇 +毒 +绶 +痛 +酯 +忍 +爪 +赳 +歆 +嘹 +辕 +烈 +册 +朴 +钱 +吮 +毯 +癜 +娃 +谀 +邵 +厮 +炽 +璞 +邃 +丐 +追 +词 +瓒 +忆 +轧 +芫 +谯 +喷 +弟 +半 +冕 +裙 +掖 +墉 +绮 +寝 +苔 +势 +顷 +褥 +切 +衮 +君 +佳 +嫒 +蚩 +霞 +佚 +洙 +逊 +镖 +暹 +唛 +& +殒 +顶 +碗 +獗 +轭 +铺 +蛊 +废 +恹 +汨 +崩 +珍 +那 +杵 +曲 +纺 +夏 +薰 +傀 +闳 +淬 +姘 +舀 +拧 +卷 +楂 +恍 +讪 +厩 +寮 +篪 +赓 +乘 +灭 +盅 +鞣 +沟 +慎 +挂 +饺 +鼾 +杳 +树 +缨 +丛 +絮 +娌 +臻 +嗳 +篡 +侩 +述 +衰 +矛 +圈 +蚜 +匕 +筹 +匿 +濞 +晨 +叶 +骋 +郝 +挚 +蚴 +滞 +增 +侍 +描 +瓣 +吖 +嫦 +蟒 +匾 +圣 +赌 +毡 +癞 +恺 +百 +曳 +需 +篓 +肮 +庖 +帏 +卿 +驿 +遗 +蹬 +鬓 +骡 +歉 +芎 +胳 +屐 +禽 +烦 +晌 +寄 +媾 +狄 +翡 +苒 +船 +廉 +终 +痞 +殇 +々 +畦 +饶 +改 +拆 +悻 +萄 +£ +瓿 +乃 +訾 +桅 +匮 +溧 +拥 +纱 +铍 +骗 +蕃 +龋 +缬 +父 +佐 +疚 +栎 +醍 +掳 +蓄 +x +惆 +颜 +鲆 +榆 +〔 +猎 +敌 +暴 +谥 +鲫 +贾 +罗 +玻 +缄 +扦 +芪 +癣 +落 +徒 +臾 +恿 +猩 +托 +邴 +肄 +牵 +春 +陛 +耀 +刊 +拓 +蓓 +邳 +堕 +寇 +枉 +淌 +啡 +湄 +兽 +酷 +萼 +碚 +濠 +萤 +夹 +旬 +戮 +梭 +琥 +椭 +昔 +勺 +蜊 +绐 +晚 +孺 +僵 +宣 +摄 +冽 +旨 +萌 +忙 +蚤 +眉 +噼 +蟑 +付 +契 +瓜 +悼 +颡 +壁 +曾 +窕 +颢 +澎 +仿 +俑 +浑 +嵌 +浣 +乍 +碌 +褪 +乱 +蔟 +隙 +玩 +剐 +葫 +箫 +纲 +围 +伐 +决 +伙 +漩 +瑟 +刑 +肓 +镳 +缓 +蹭 +氨 +皓 +典 +畲 +坍 +铑 +檐 +塑 +洞 +倬 +储 +胴 +淳 +戾 +吐 +灼 +惺 +妙 +毕 +珐 +缈 +虱 +盖 +羰 +鸿 +磅 +谓 +髅 +娴 +苴 +唷 +蚣 +霹 +抨 +贤 +唠 +犬 +誓 +逍 +庠 +逼 +麓 +籼 +釉 +呜 +碧 +秧 +氩 +摔 +霄 +穸 +纨 +辟 +妈 +映 +完 +牛 +缴 +嗷 +炊 +恩 +荔 +茆 +掉 +紊 +慌 +莓 +羟 +阙 +萁 +磐 +另 +蕹 +辱 +鳐 +湮 +吡 +吩 +唐 +睦 +垠 +舒 +圜 +冗 +瞿 +溺 +芾 +囱 +匠 +僳 +汐 +菩 +饬 +漓 +黑 +霰 +浸 +濡 +窥 +毂 +蒡 +兢 +驻 +鹉 +芮 +诙 +迫 +雳 +厂 +忐 +臆 +猴 +鸣 +蚪 +栈 +箕 +羡 +渐 +莆 +捍 +眈 +哓 +趴 +蹼 +埕 +嚣 +骛 +宏 +淄 +斑 +噜 +严 +瑛 +垃 +椎 +诱 +压 +庾 +绞 +焘 +廿 +抡 +迄 +棘 +夫 +纬 +锹 +眨 +瞌 +侠 +脐 +竞 +瀑 +孳 +骧 +遁 +姜 +颦 +荪 +滚 +萦 +伪 +逸 +粳 +爬 +锁 +矣 +役 +趣 +洒 +颔 +诏 +逐 +奸 +甭 +惠 +攀 +蹄 +泛 +尼 +拼 +阮 +鹰 +亚 +颈 +惑 +勒 +〉 +际 +肛 +爷 +刚 +钨 +丰 +养 +冶 +鲽 +辉 +蔻 +画 +覆 +皴 +妊 +麦 +返 +醉 +皂 +擀 +〗 +酶 +凑 +粹 +悟 +诀 +硖 +港 +卜 +z +杀 +涕 +± +舍 +铠 +抵 +弛 +段 +敝 +镐 +奠 +拂 +轴 +跛 +袱 +e +t +沉 +菇 +俎 +薪 +峦 +秭 +蟹 +历 +盟 +菠 +寡 +液 +肢 +喻 +染 +裱 +悱 +抱 +氙 +赤 +捅 +猛 +跑 +氮 +谣 +仁 +尺 +辊 +窍 +烙 +衍 +架 +擦 +倏 +璐 +瑁 +币 +楞 +胖 +夔 +趸 +邛 +惴 +饕 +虔 +蝎 +§ +哉 +贝 +宽 +辫 +炮 +扩 +饲 +籽 +魏 +菟 +锰 +伍 +猝 +末 +琳 +哚 +蛎 +邂 +呀 +姿 +鄞 +却 +歧 +仙 +恸 +椐 +森 +牒 +寤 +袒 +婆 +虢 +雅 +钉 +朵 +贼 +欲 +苞 +寰 +故 +龚 +坭 +嘘 +咫 +礼 +硷 +兀 +睢 +汶 +’ +铲 +烧 +绕 +诃 +浃 +钿 +哺 +柜 +讼 +颊 +璁 +腔 +洽 +咐 +脲 +簌 +筠 +镣 +玮 +鞠 +谁 +兼 +姆 +挥 +梯 +蝴 +谘 +漕 +刷 +躏 +宦 +弼 +b +垌 +劈 +麟 +莉 +揭 +笙 +渎 +仕 +嗤 +仓 +配 +怏 +抬 +错 +泯 +镊 +孰 +猿 +邪 +仍 +秋 +鼬 +壹 +歇 +吵 +炼 +< +尧 +射 +柬 +廷 +胧 +霾 +凳 +隋 +肚 +浮 +梦 +祥 +株 +堵 +退 +L +鹫 +跎 +凶 +毽 +荟 +炫 +栩 +玳 +甜 +沂 +鹿 +顽 +伯 +爹 +赔 +蛴 +徐 +匡 +欣 +狰 +缸 +雹 +蟆 +疤 +默 +沤 +啜 +痂 +衣 +禅 +w +i +h +辽 +葳 +黝 +钗 +停 +沽 +棒 +馨 +颌 +肉 +吴 +硫 +悯 +劾 +娈 +马 +啧 +吊 +悌 +镑 +峭 +帆 +瀣 +涉 +咸 +疸 +滋 +泣 +翦 +拙 +癸 +钥 +蜒 ++ +尾 +庄 +凝 +泉 +婢 +渴 +谊 +乞 +陆 +锉 +糊 +鸦 +淮 +I +B +N +晦 +弗 +乔 +庥 +葡 +尻 +席 +橡 +傣 +渣 +拿 +惩 +麋 +斛 +缃 +矮 +蛏 +岘 +鸽 +姐 +膏 +催 +奔 +镒 +喱 +蠡 +摧 +钯 +胤 +柠 +拐 +璋 +鸥 +卢 +荡 +倾 +^ +_ +珀 +逄 +萧 +塾 +掇 +贮 +笆 +聂 +圃 +冲 +嵬 +M +滔 +笕 +值 +炙 +偶 +蜱 +搐 +梆 +汪 +蔬 +腑 +鸯 +蹇 +敞 +绯 +仨 +祯 +谆 +梧 +糗 +鑫 +啸 +豺 +囹 +猾 +巢 +柄 +瀛 +筑 +踌 +沭 +暗 +苁 +鱿 +蹉 +脂 +蘖 +牢 +热 +木 +吸 +溃 +宠 +序 +泞 +偿 +拜 +檩 +厚 +朐 +毗 +螳 +吞 +媚 +朽 +担 +蝗 +橘 +畴 +祈 +糟 +盱 +隼 +郜 +惜 +珠 +裨 +铵 +焙 +琚 +唯 +咚 +噪 +骊 +丫 +滢 +勤 +棉 +呸 +咣 +淀 +隔 +蕾 +窈 +饨 +挨 +煅 +短 +匙 +粕 +镜 +赣 +撕 +墩 +酬 +馁 +豌 +颐 +抗 +酣 +氓 +佑 +搁 +哭 +递 +耷 +涡 +桃 +贻 +碣 +截 +瘦 +昭 +镌 +蔓 +氚 +甲 +猕 +蕴 +蓬 +散 +拾 +纛 +狼 +猷 +铎 +埋 +旖 +矾 +讳 +囊 +糜 +迈 +粟 +蚂 +紧 +鲳 +瘢 +栽 +稼 +羊 +锄 +斟 +睁 +桥 +瓮 +蹙 +祉 +醺 +鼻 +昱 +剃 +跳 +篱 +跷 +蒜 +翎 +宅 +晖 +嗑 +壑 +峻 +癫 +屏 +狠 +陋 +袜 +途 +憎 +祀 +莹 +滟 +佶 +溥 +臣 +约 +盛 +峰 +磁 +慵 +婪 +拦 +莅 +朕 +鹦 +粲 +裤 +哎 +疡 +嫖 +琵 +窟 +堪 +谛 +嘉 +儡 +鳝 +斩 +郾 +驸 +酊 +妄 +胜 +贺 +徙 +傅 +噌 +钢 +栅 +庇 +恋 +匝 +巯 +邈 +尸 +锚 +粗 +佟 +蛟 +薹 +纵 +蚊 +郅 +绢 +锐 +苗 +俞 +篆 +淆 +膀 +鲜 +煎 +诶 +秽 +寻 +涮 +刺 +怀 +噶 +巨 +褰 +魅 +灶 +灌 +桉 +藕 +谜 +舸 +薄 +搀 +恽 +借 +牯 +痉 +渥 +愿 +亓 +耘 +杠 +柩 +锔 +蚶 +钣 +珈 +喘 +蹒 +幽 +赐 +稗 +晤 +莱 +泔 +扯 +肯 +菪 +裆 +腩 +豉 +疆 +骜 +腐 +倭 +珏 +唔 +粮 +亡 +润 +慰 +伽 +橄 +玄 +誉 +醐 +胆 +龊 +粼 +塬 +陇 +彼 +削 +嗣 +绾 +芽 +妗 +垭 +瘴 +爽 +薏 +寨 +龈 +泠 +弹 +赢 +漪 +猫 +嘧 +涂 +恤 +圭 +茧 +烽 +屑 +痕 +巾 +赖 +荸 +凰 +腮 +畈 +亵 +蹲 +偃 +苇 +澜 +艮 +换 +骺 +烘 +苕 +梓 +颉 +肇 +哗 +悄 +氤 +涠 +葬 +屠 +鹭 +植 +竺 +佯 +诣 +鲇 +瘀 +鲅 +邦 +移 +滁 +冯 +耕 +癔 +戌 +茬 +沁 +巩 +悠 +湘 +洪 +痹 +锟 +循 +谋 +腕 +鳃 +钠 +捞 +焉 +迎 +碱 +伫 +急 +榷 +奈 +邝 +卯 +辄 +皲 +卟 +醛 +畹 +忧 +稳 +雄 +昼 +缩 +阈 +睑 +扌 +耗 +曦 +涅 +捏 +瞧 +邕 +淖 +漉 +铝 +耦 +禹 +湛 +喽 +莼 +琅 +诸 +苎 +纂 +硅 +始 +嗨 +傥 +燃 +臂 +赅 +嘈 +呆 +贵 +屹 +壮 +肋 +亍 +蚀 +卅 +豹 +腆 +邬 +迭 +浊 +} +童 +螂 +捐 +圩 +勐 +触 +寞 +汊 +壤 +荫 +膺 +渌 +芳 +懿 +遴 +螈 +泰 +蓼 +蛤 +茜 +舅 +枫 +朔 +膝 +眙 +避 +梅 +判 +鹜 +璜 +牍 +缅 +垫 +藻 +黔 +侥 +惚 +懂 +踩 +腰 +腈 +札 +丞 +唾 +慈 +顿 +摹 +荻 +琬 +~ +斧 +沈 +滂 +胁 +胀 +幄 +莜 +Z +匀 +鄄 +掌 +绰 +茎 +焚 +赋 +萱 +谑 +汁 +铒 +瞎 +夺 +蜗 +野 +娆 +冀 +弯 +篁 +懵 +灞 +隽 +芡 +脘 +俐 +辩 +芯 +掺 +喏 +膈 +蝈 +觐 +悚 +踹 +蔗 +熠 +鼠 +呵 +抓 +橼 +峨 +畜 +缔 +禾 +崭 +弃 +熊 +摒 +凸 +拗 +穹 +蒙 +抒 +祛 +劝 +闫 +扳 +阵 +醌 +踪 +喵 +侣 +搬 +仅 +荧 +赎 +蝾 +琦 +买 +婧 +瞄 +寓 +皎 +冻 +赝 +箩 +莫 +瞰 +郊 +笫 +姝 +筒 +枪 +遣 +煸 +袋 +舆 +痱 +涛 +母 +〇 +启 +践 +耙 +绲 +盘 +遂 +昊 +搞 +槿 +诬 +纰 +泓 +惨 +檬 +亻 +越 +C +o +憩 +熵 +祷 +钒 +暧 +塔 +阗 +胰 +咄 +娶 +魔 +琶 +钞 +邻 +扬 +杉 +殴 +咽 +弓 +〆 +髻 +】 +吭 +揽 +霆 +拄 +殖 +脆 +彻 +岩 +芝 +勃 +辣 +剌 +钝 +嘎 +甄 +佘 +皖 +伦 +授 +徕 +憔 +挪 +皇 +庞 +稔 +芜 +踏 +溴 +兖 +卒 +擢 +饥 +鳞 +煲 +‰ +账 +颗 +叻 +斯 +捧 +鳍 +琮 +讹 +蛙 +纽 +谭 +酸 +兔 +莒 +睇 +伟 +觑 +羲 +嗜 +宜 +褐 +旎 +辛 +卦 +诘 +筋 +鎏 +溪 +挛 +熔 +阜 +晰 +鳅 +丢 +奚 +灸 +呱 +献 +陉 +黛 +鸪 +甾 +萨 +疮 +拯 +洲 +疹 +辑 +叙 +恻 +谒 +允 +柔 +烂 +氏 +逅 +漆 +拎 +惋 +扈 +湟 +纭 +啕 +掬 +擞 +哥 +忽 +涤 +鸵 +靡 +郗 +瓷 +扁 +廊 +怨 +雏 +钮 +敦 +E +懦 +憋 +汀 +拚 +啉 +腌 +岸 +f +痼 +瞅 +尊 +咀 +眩 +飙 +忌 +仝 +迦 +熬 +毫 +胯 +篑 +茄 +腺 +凄 +舛 +碴 +锵 +诧 +羯 +後 +漏 +汤 +宓 +仞 +蚁 +壶 +谰 +皑 +铄 +棰 +罔 +辅 +晶 +苦 +牟 +闽 +\ +烃 +饮 +聿 +丙 +蛳 +朱 +煤 +涔 +鳖 +犁 +罐 +荼 +砒 +淦 +妤 +黏 +戎 +孑 +婕 +瑾 +戢 +钵 +枣 +捋 +砥 +衩 +狙 +桠 +稣 +阎 +肃 +梏 +诫 +孪 +昶 +婊 +衫 +嗔 +侃 +塞 +蜃 +樵 +峒 +貌 +屿 +欺 +缫 +阐 +栖 +诟 +珞 +荭 +吝 +萍 +嗽 +恂 +啻 +蜴 +磬 +峋 +俸 +豫 +谎 +徊 +镍 +韬 +魇 +晴 +U +囟 +猜 +蛮 +坐 +囿 +伴 +亭 +肝 +佗 +蝠 +妃 +胞 +滩 +榴 +氖 +垩 +苋 +砣 +扪 +馏 +姓 +轩 +厉 +夥 +侈 +禀 +垒 +岑 +赏 +钛 +辐 +痔 +披 +纸 +碳 +“ +坞 +蠓 +挤 +荥 +沅 +悔 +铧 +帼 +蒌 +蝇 +a +p +y +n +g +哀 +浆 +瑶 +凿 +桶 +馈 +皮 +奴 +苜 +佤 +伶 +晗 +铱 +炬 +优 +弊 +氢 +恃 +甫 +攥 +端 +锌 +灰 +稹 +炝 +曙 +邋 +亥 +眶 +碾 +拉 +萝 +绔 +捷 +浍 +腋 +姑 +菖 +凌 +涞 +麽 +锢 +桨 +潢 +绎 +镰 +殆 +锑 +渝 +铬 +困 +绽 +觎 +匈 +糙 +暑 +裹 +鸟 +盔 +肽 +迷 +綦 +『 +亳 +佝 +俘 +钴 +觇 +骥 +仆 +疝 +跪 +婶 +郯 +瀹 +唉 +脖 +踞 +针 +晾 +忒 +扼 +瞩 +叛 +椒 +疟 +嗡 +邗 +肆 +跆 +玫 +忡 +捣 +咧 +唆 +艄 +蘑 +潦 +笛 +阚 +沸 +泻 +掊 +菽 +贫 +斥 +髂 +孢 +镂 +赂 +麝 +鸾 +屡 +衬 +苷 +恪 +叠 +希 +粤 +爻 +喝 +茫 +惬 +郸 +绻 +庸 +撅 +碟 +宄 +妹 +膛 +叮 +饵 +崛 +嗲 +椅 +冤 +搅 +咕 +敛 +尹 +垦 +闷 +蝉 +霎 +勰 +败 +蓑 +泸 +肤 +鹌 +幌 +焦 +浠 +鞍 +刁 +舰 +乙 +竿 +裔 +。 +茵 +函 +伊 +兄 +丨 +娜 +匍 +謇 +莪 +宥 +似 +蝽 +翳 +酪 +翠 +粑 +薇 +祢 +骏 +赠 +叫 +Q +噤 +噻 +竖 +芗 +莠 +潭 +俊 +羿 +耜 +O +郫 +趁 +嗪 +囚 +蹶 +芒 +洁 +笋 +鹑 +敲 +硝 +啶 +堡 +渲 +揩 +』 +携 +宿 +遒 +颍 +扭 +棱 +割 +萜 +蔸 +葵 +琴 +捂 +饰 +衙 +耿 +掠 +募 +岂 +窖 +涟 +蔺 +瘤 +柞 +瞪 +怜 +匹 +距 +楔 +炜 +哆 +秦 +缎 +幼 +茁 +绪 +痨 +恨 +楸 +娅 +瓦 +桩 +雪 +嬴 +伏 +榔 +妥 +铿 +拌 +眠 +雍 +缇 +‘ +卓 +搓 +哌 +觞 +噩 +屈 +哧 +髓 +咦 +巅 +娑 +侑 +淫 +膳 +祝 +勾 +姊 +莴 +胄 +疃 +薛 +蜷 +胛 +巷 +芙 +芋 +熙 +闰 +勿 +窃 +狱 +剩 +钏 +幢 +陟 +铛 +慧 +靴 +耍 +k +浙 +浇 +飨 +惟 +绗 +祜 +澈 +啼 +咪 +磷 +摞 +诅 +郦 +抹 +跃 +壬 +吕 +肖 +琏 +颤 +尴 +剡 +抠 +凋 +赚 +泊 +津 +宕 +殷 +倔 +氲 +漫 +邺 +涎 +怠 +$ +垮 +荬 +遵 +俏 +叹 +噢 +饽 +蜘 +孙 +筵 +疼 +鞭 +羧 +牦 +箭 +潴 +c +眸 +祭 +髯 +啖 +坳 +愁 +芩 +驮 +倡 +巽 +穰 +沃 +胚 +怒 +凤 +槛 +剂 +趵 +嫁 +v +邢 +灯 +鄢 +桐 +睽 +檗 +锯 +槟 +婷 +嵋 +圻 +诗 +蕈 +颠 +遭 +痢 +芸 +怯 +馥 +竭 +锗 +徜 +恭 +遍 +籁 +剑 +嘱 +苡 +龄 +僧 +桑 +潸 +弘 +澶 +楹 +悲 +讫 +愤 +腥 +悸 +谍 +椹 +呢 +桓 +葭 +攫 +阀 +翰 +躲 +敖 +柑 +郎 +笨 +橇 +呃 +魁 +燎 +脓 +葩 +磋 +垛 +玺 +狮 +沓 +砜 +蕊 +锺 +罹 +蕉 +翱 +虐 +闾 +巫 +旦 +茱 +嬷 +枯 +鹏 +贡 +芹 +汛 +矫 +绁 +拣 +禺 +佃 +讣 +舫 +惯 +乳 +趋 +疲 +挽 +岚 +虾 +衾 +蠹 +蹂 +飓 +氦 +铖 +孩 +稞 +瑜 +壅 +掀 +勘 +妓 +畅 +髋 +W +庐 +牲 +蓿 +榕 +练 +垣 +唱 +邸 +菲 +昆 +婺 +穿 +绡 +麒 +蚱 +掂 +愚 +泷 +涪 +漳 +妩 +娉 +榄 +讷 +觅 +旧 +藤 +煮 +呛 +柳 +腓 +叭 +庵 +烷 +阡 +罂 +蜕 +擂 +猖 +咿 +媲 +脉 +【 +沏 +貅 +黠 +熏 +哲 +烁 +坦 +酵 +兜 +× +潇 +撒 +剽 +珩 +圹 +乾 +摸 +樟 +帽 +嗒 +襄 +魂 +轿 +憬 +锡 +〕 +喃 +皆 +咖 +隅 +脸 +残 +泮 +袂 +鹂 +珊 +囤 +捆 +咤 +误 +徨 +闹 +淙 +芊 +淋 +怆 +囗 +拨 +梳 +渤 +R +G +绨 +蚓 +婀 +幡 +狩 +麾 +谢 +唢 +裸 +旌 +伉 +纶 +裂 +驳 +砼 +咛 +澄 +樨 +蹈 +宙 +澍 +倍 +貔 +操 +勇 +蟠 +摈 +砧 +虬 +够 +缁 +悦 +藿 +撸 +艹 +摁 +淹 +豇 +虎 +榭 +ˉ +吱 +d +° +喧 +荀 +踱 +侮 +奋 +偕 +饷 +犍 +惮 +坑 +璎 +徘 +宛 +妆 +袈 +倩 +窦 +昂 +荏 +乖 +K +怅 +撰 +鳙 +牙 +袁 +酞 +X +痿 +琼 +闸 +雁 +趾 +荚 +虻 +涝 +《 +杏 +韭 +偈 +烤 +绫 +鞘 +卉 +症 +遢 +蓥 +诋 +杭 +荨 +匆 +竣 +簪 +辙 +敕 +虞 +丹 +缭 +咩 +黟 +m +淤 +瑕 +咂 +铉 +硼 +茨 +嶂 +痒 +畸 +敬 +涿 +粪 +窘 +熟 +叔 +嫔 +盾 +忱 +裘 +憾 +梵 +赡 +珙 +咯 +娘 +庙 +溯 +胺 +葱 +痪 +摊 +荷 +卞 +乒 +髦 +寐 +铭 +坩 +胗 +枷 +爆 +溟 +嚼 +羚 +砬 +轨 +惊 +挠 +罄 +竽 +菏 +氧 +浅 +楣 +盼 +枢 +炸 +阆 +杯 +谏 +噬 +淇 +渺 +俪 +秆 +墓 +泪 +跻 +砌 +痰 +垡 +渡 +耽 +釜 +讶 +鳎 +煞 +呗 +韶 +舶 +绷 +鹳 +缜 +旷 +铊 +皱 +龌 +檀 +霖 +奄 +槐 +艳 +蝶 +旋 +哝 +赶 +骞 +蚧 +腊 +盈 +丁 +` +蜚 +矸 +蝙 +睨 +嚓 +僻 +鬼 +醴 +夜 +彝 +磊 +笔 +拔 +栀 +糕 +厦 +邰 +纫 +逭 +纤 +眦 +膊 +馍 +躇 +烯 +蘼 +冬 +诤 +暄 +骶 +哑 +瘠 +」 +臊 +丕 +愈 +咱 +螺 +擅 +跋 +搏 +硪 +谄 +笠 +淡 +嘿 +骅 +谧 +鼎 +皋 +姚 +歼 +蠢 +驼 +耳 +胬 +挝 +涯 +狗 +蒽 +孓 +犷 +凉 +芦 +箴 +铤 +孤 +嘛 +坤 +V +茴 +朦 +挞 +尖 +橙 +诞 +搴 +碇 +洵 +浚 +帚 +蜍 +漯 +柘 +嚎 +讽 +芭 +荤 +咻 +祠 +秉 +跖 +埃 +吓 +糯 +眷 +馒 +惹 +娼 +鲑 +嫩 +讴 +轮 +瞥 +靶 +褚 +乏 +缤 +宋 +帧 +删 +驱 +碎 +扑 +俩 +俄 +偏 +涣 +竹 +噱 +皙 +佰 +渚 +唧 +斡 +# +镉 +刀 +崎 +筐 +佣 +夭 +贰 +肴 +峙 +哔 +艿 +匐 +牺 +镛 +缘 +仡 +嫡 +劣 +枸 +堀 +梨 +簿 +鸭 +蒸 +亦 +稽 +浴 +{ +衢 +束 +槲 +j +阁 +揍 +疥 +棋 +潋 +聪 +窜 +乓 +睛 +插 +冉 +阪 +苍 +搽 +「 +蟾 +螟 +幸 +仇 +樽 +撂 +慢 +跤 +幔 +俚 +淅 +覃 +觊 +溶 +妖 +帛 +侨 +曰 +妾 +泗 +· +: +瀘 +風 +Ë +( +) +∶ +紅 +紗 +瑭 +雲 +頭 +鶏 +財 +許 +• +¥ +樂 +焗 +麗 +— +; +滙 +東 +榮 +繪 +興 +… +門 +業 +π +楊 +國 +顧 +é +盤 +寳 +Λ +龍 +鳳 +島 +誌 +緣 +結 +銭 +萬 +勝 +祎 +璟 +優 +歡 +臨 +時 +購 += +★ +藍 +昇 +鐵 +觀 +勅 +農 +聲 +畫 +兿 +術 +發 +劉 +記 +專 +耑 +園 +書 +壴 +種 +Ο +● +褀 +號 +銀 +匯 +敟 +锘 +葉 +橪 +廣 +進 +蒄 +鑽 +阝 +祙 +貢 +鍋 +豊 +夬 +喆 +團 +閣 +開 +燁 +賓 +館 +酡 +沔 +順 ++ +硚 +劵 +饸 +陽 +車 +湓 +復 +萊 +氣 +軒 +華 +堃 +迮 +纟 +戶 +馬 +學 +裡 +電 +嶽 +獨 +マ +シ +サ +ジ +燘 +袪 +環 +❤ +臺 +灣 +専 +賣 +孖 +聖 +攝 +線 +▪ +α +傢 +俬 +夢 +達 +莊 +喬 +貝 +薩 +劍 +羅 +壓 +棛 +饦 +尃 +璈 +囍 +醫 +G +I +A +# +N +鷄 +髙 +嬰 +啓 +約 +隹 +潔 +賴 +藝 +~ +寶 +籣 +麺 +  +嶺 +√ +義 +網 +峩 +長 +∧ +魚 +機 +構 +② +鳯 +偉 +L +B +㙟 +畵 +鴿 +' +詩 +溝 +嚞 +屌 +藔 +佧 +玥 +蘭 +織 +1 +3 +9 +0 +7 +點 +砭 +鴨 +鋪 +銘 +廳 +弍 +‧ +創 +湯 +坶 +℃ +卩 +骝 +& +烜 +荘 +當 +潤 +扞 +係 +懷 +碶 +钅 +蚨 +讠 +☆ +叢 +爲 +埗 +涫 +塗 +→ +楽 +現 +鯨 +愛 +瑪 +鈺 +忄 +悶 +藥 +飾 +樓 +視 +孬 +ㆍ +燚 +苪 +師 +① +丼 +锽 +│ +韓 +標 +è +兒 +閏 +匋 +張 +漢 +Ü +髪 +會 +閑 +檔 +習 +裝 +の +峯 +菘 +輝 +И +雞 +釣 +億 +浐 +K +O +R +8 +H +E +P +T +W +D +S +C +M +F +姌 +饹 +» +晞 +廰 +ä +嵯 +鷹 +負 +飲 +絲 +冚 +楗 +澤 +綫 +區 +❋ +← +質 +靑 +揚 +③ +滬 +統 +産 +協 +﹑ +乸 +畐 +經 +運 +際 +洺 +岽 +為 +粵 +諾 +崋 +豐 +碁 +ɔ +V +2 +6 +齋 +誠 +訂 +´ +勑 +雙 +陳 +無 +í +泩 +媄 +夌 +刂 +i +c +t +o +r +a +嘢 +耄 +燴 +暃 +壽 +媽 +靈 +抻 +體 +唻 +É +冮 +甹 +鎮 +錦 +ʌ +蜛 +蠄 +尓 +駕 +戀 +飬 +逹 +倫 +貴 +極 +Я +Й +寬 +磚 +嶪 +郎 +職 +| +間 +n +d +剎 +伈 +課 +飛 +橋 +瘊 +№ +譜 +骓 +圗 +滘 +縣 +粿 +咅 +養 +濤 +彳 +® +% +Ⅱ +啰 +㴪 +見 +矞 +薬 +糁 +邨 +鲮 +顔 +罱 +З +選 +話 +贏 +氪 +俵 +競 +瑩 +繡 +枱 +β +綉 +á +獅 +爾 +™ +麵 +戋 +淩 +徳 +個 +劇 +場 +務 +簡 +寵 +h +實 +膠 +轱 +圖 +築 +嘣 +樹 +㸃 +營 +耵 +孫 +饃 +鄺 +飯 +麯 +遠 +輸 +坫 +孃 +乚 +閃 +鏢 +㎡ +題 +廠 +關 +↑ +爺 +將 +軍 +連 +篦 +覌 +參 +箸 +- +窠 +棽 +寕 +夀 +爰 +歐 +呙 +閥 +頡 +熱 +雎 +垟 +裟 +凬 +勁 +帑 +馕 +夆 +疌 +枼 +馮 +貨 +蒤 +樸 +彧 +旸 +靜 +龢 +暢 +㐱 +鳥 +珺 +鏡 +灡 +爭 +堷 +廚 +Ó +騰 +診 +┅ +蘇 +褔 +凱 +頂 +豕 +亞 +帥 +嘬 +⊥ +仺 +桖 +複 +饣 +絡 +穂 +顏 +棟 +納 +▏ +濟 +親 +設 +計 +攵 +埌 +烺 +ò +頤 +燦 +蓮 +撻 +節 +講 +濱 +濃 +娽 +洳 +朿 +燈 +鈴 +護 +膚 +铔 +過 +補 +Z +U +5 +4 +坋 +闿 +䖝 +餘 +缐 +铞 +貿 +铪 +桼 +趙 +鍊 +[ +㐂 +垚 +菓 +揸 +捲 +鐘 +滏 +𣇉 +爍 +輪 +燜 +鴻 +鮮 +動 +鹞 +鷗 +丄 +慶 +鉌 +翥 +飮 +腸 +⇋ +漁 +覺 +來 +熘 +昴 +翏 +鲱 +圧 +鄉 +萭 +頔 +爐 +嫚 +г +貭 +類 +聯 +幛 +輕 +訓 +鑒 +夋 +锨 +芃 +珣 +䝉 +扙 +嵐 +銷 +處 +ㄱ +語 +誘 +苝 +歸 +儀 +燒 +楿 +內 +粢 +葒 +奧 +麥 +礻 +滿 +蠔 +穵 +瞭 +態 +鱬 +榞 +硂 +鄭 +黃 +煙 +祐 +奓 +逺 +* +瑄 +獲 +聞 +薦 +讀 +這 +樣 +決 +問 +啟 +們 +執 +説 +轉 +單 +隨 +唘 +帶 +倉 +庫 +還 +贈 +尙 +皺 +■ +餅 +產 +○ +∈ +報 +狀 +楓 +賠 +琯 +嗮 +禮 +` +傳 +> +≤ +嗞 +Φ +≥ +換 +咭 +∣ +↓ +曬 +ε +応 +寫 +″ +終 +様 +純 +費 +療 +聨 +凍 +壐 +郵 +ü +黒 +∫ +製 +塊 +調 +軽 +確 +撃 +級 +馴 +Ⅲ +涇 +繹 +數 +碼 +證 +狒 +処 +劑 +< +晧 +賀 +衆 +] +櫥 +兩 +陰 +絶 +對 +鯉 +憶 +◎ +p +e +Y +蕒 +煖 +頓 +測 +試 +鼽 +僑 +碩 +妝 +帯 +≈ +鐡 +舖 +權 +喫 +倆 +ˋ +該 +悅 +ā +俫 +. +f +s +b +m +k +g +u +j +貼 +淨 +濕 +針 +適 +備 +l +/ +給 +謢 +強 +觸 +衛 +與 +⊙ +$ +緯 +變 +⑴ +⑵ +⑶ +㎏ +殺 +∩ +幚 +─ +價 +▲ +離 +ú +ó +飄 +烏 +関 +閟 +﹝ +﹞ +邏 +輯 +鍵 +驗 +訣 +導 +歷 +屆 +層 +▼ +儱 +錄 +熳 +ē +艦 +吋 +錶 +辧 +飼 +顯 +④ +禦 +販 +気 +対 +枰 +閩 +紀 +幹 +瞓 +貊 +淚 +△ +眞 +墊 +Ω +獻 +褲 +縫 +緑 +亜 +鉅 +餠 +{ +} +◆ +蘆 +薈 +█ +◇ +溫 +彈 +晳 +粧 +犸 +穩 +訊 +崬 +凖 +熥 +П +舊 +條 +紋 +圍 +Ⅳ +筆 +尷 +難 +雜 +錯 +綁 +識 +頰 +鎖 +艶 +□ +殁 +殼 +⑧ +├ +▕ +鵬 +ǐ +ō +ǒ +糝 +綱 +▎ +μ +盜 +饅 +醬 +籤 +蓋 +釀 +鹽 +據 +à +ɡ +辦 +◥ +彐 +┌ +婦 +獸 +鲩 +伱 +ī +蒟 +蒻 +齊 +袆 +腦 +寧 +凈 +妳 +煥 +詢 +偽 +謹 +啫 +鯽 +騷 +鱸 +損 +傷 +鎻 +髮 +買 +冏 +儥 +両 +﹢ +∞ +載 +喰 +z +羙 +悵 +燙 +曉 +員 +組 +徹 +艷 +痠 +鋼 +鼙 +縮 +細 +嚒 +爯 +≠ +維 +" +鱻 +壇 +厍 +帰 +浥 +犇 +薡 +軎 +² +應 +醜 +刪 +緻 +鶴 +賜 +噁 +軌 +尨 +镔 +鷺 +槗 +彌 +葚 +濛 +請 +溇 +緹 +賢 +訪 +獴 +瑅 +資 +縤 +陣 +蕟 +栢 +韻 +祼 +恁 +伢 +謝 +劃 +涑 +總 +衖 +踺 +砋 +凉 +籃 +駿 +苼 +瘋 +昽 +紡 +驊 +腎 +﹗ +響 +杋 +剛 +嚴 +禪 +歓 +槍 +傘 +檸 +檫 +炣 +勢 +鏜 +鎢 +銑 +尐 +減 +奪 +惡 +θ +僮 +婭 +臘 +ū +ì +殻 +鉄 +∑ +蛲 +焼 +緖 +續 +紹 +懮 \ No newline at end of file diff --git a/deploy/cpp_infer/tools/run.sh b/deploy/cpp_infer/tools/run.sh new file mode 100755 index 00000000..1989d5ea --- /dev/null +++ b/deploy/cpp_infer/tools/run.sh @@ -0,0 +1,2 @@ + +./build/ocr_system ./inference/det_db/ ./inference/rec_crnn/ ../../doc/imgs/11.jpg From b5bfe8239ebd2bfbc813e66707193024fee7e283 Mon Sep 17 00:00:00 2001 From: littletomatodonkey Date: Sun, 12 Jul 2020 18:48:10 +0000 Subject: [PATCH 2/7] add readme --- deploy/cpp_infer/CMakeLists.txt | 5 +- deploy/cpp_infer/readme.md | 149 ++++++++++++++++++++++++++++++ deploy/cpp_infer/tools/build.sh | 2 + deploy/cpp_infer/tools/run.sh | 2 +- deploy/imgs/cpp_infer_pred_12.png | Bin 0 -> 80323 bytes 5 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 deploy/cpp_infer/readme.md create mode 100644 deploy/imgs/cpp_infer_pred_12.png diff --git a/deploy/cpp_infer/CMakeLists.txt b/deploy/cpp_infer/CMakeLists.txt index 282b245d..8f06068c 100644 --- a/deploy/cpp_infer/CMakeLists.txt +++ b/deploy/cpp_infer/CMakeLists.txt @@ -27,11 +27,8 @@ if(NOT DEFINED DEMO_NAME) message(FATAL_ERROR "please set DEMO_NAME with -DDEMO_NAME=demo_name") endif() -# user ze -# find_package(OpenCV) - -set(OPENCV_DIR "/paddle/libs/opencv-3.4.7/opencv3") +set(OPENCV_DIR ${OPENCV_DIR}) find_package(OpenCV REQUIRED PATHS ${OPENCV_DIR}/share/OpenCV NO_DEFAULT_PATH) include_directories(${OpenCV_INCLUDE_DIRS}) diff --git a/deploy/cpp_infer/readme.md b/deploy/cpp_infer/readme.md new file mode 100644 index 00000000..23612bcf --- /dev/null +++ b/deploy/cpp_infer/readme.md @@ -0,0 +1,149 @@ +# 服务器端C++预测 + +本教程将介绍在服务器端部署PaddleOCR超轻量中文检测、识别模型的详细步骤。 + + +## 1. 准备环境 + +### 运行准备 +- Linux环境,推荐使用docker。 + +### 1.1 编译opencv库 + +* 首先需要从opencv官网上下载在Linux环境下源码编译的包,以opencv3.4.7为例,下载命令如下。 + +``` +wget https://github.com/opencv/opencv/archive/3.4.7.tar.gz +tar -xf 3.4.7.tar.gz +``` + +最终可以在当前目录下看到`opencv-3.4.7/`的文件夹。 + +* 编译opencv,设置opencv源码路径(`root_path`)以及安装路径(`install_path`)。进入opencv源码路径下,按照下面的方式进行编译。 + +```shell +root_path=/paddle/libs/opencv-3.4.7 +install_path=${root_path}/opencv3 + +rm -rf build +mkdir build +cd build + +cmake .. \ + -DCMAKE_INSTALL_PREFIX=${install_path} \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DWITH_IPP=OFF \ + -DBUILD_IPP_IW=OFF \ + -DWITH_LAPACK=OFF \ + -DWITH_EIGEN=OFF \ + -DCMAKE_INSTALL_LIBDIR=lib64 \ + -DWITH_ZLIB=ON \ + -DBUILD_ZLIB=ON \ + -DWITH_JPEG=ON \ + -DBUILD_JPEG=ON \ + -DWITH_PNG=ON \ + -DBUILD_PNG=ON \ + -DWITH_TIFF=ON \ + -DBUILD_TIFF=ON + +make -j +make install +``` + +最终在安装路径下的文件结构如下所示。 + +``` +opencv3/ +|-- bin +|-- include +|-- lib +|-- lib64 +|-- share +``` + +### 1.2 编译Paddle预测库 + +* 可以参考[Paddle预测库官网](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/build_and_install_lib_cn.html)的说明,从github上获取Paddle代码,然后进行编译,生成最新的预测库。使用git获取代码方法如下。 + +```shell +git clone https://github.com/PaddlePaddle/Paddle.git +``` + +* 进入Paddle目录后,编译方法如下。 + +```shell +rm -rf build +mkdir build +cd build + +cmake .. \ + -DWITH_CONTRIB=OFF \ + -DWITH_MKL=ON \ + -DWITH_MKLDNN=OFF \ + -DWITH_TESTING=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DWITH_INFERENCE_API_TEST=OFF \ + -DON_INFER=ON \ + -DWITH_PYTHON=ON +make -j16 +make inference_lib_dist +``` + +更多编译参数选项可以参考Paddle C++预测库官网:[https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/build_and_install_lib_cn.html](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/build_and_install_lib_cn.html)。 + + +* 编译完成之后,可以在`build/fluid_inference_install_dir/`文件下看到生成了以下文件及文件夹。 + +``` +build/fluid_inference_install_dir/ +|-- CMakeCache.txt +|-- paddle +|-- third_party +|-- version.txt +``` + +其中`paddle`就是之后进行C++预测时所需的Paddle库,`version.txt`中包含当前预测库的版本信息。 + + +## 2 开始运行 + +### 2.1 将模型导出为inference model + +* 可以参考[模型预测章节](../../doc/doc_ch/inference.md),导出inference model,用于模型预测。模型导出之后,假设放在`inference`目录下,则目录结构如下。 + +``` +inference/ +|-- det_db +| |--model +| |--params +|-- rec_rcnn +| |--model +| |--params +``` + + +### 2.2 编译PaddleOCR C++预测demo + +* 编译命令如下,其中Paddle C++预测库、opencv等其他依赖库的地址需要换成自己机器上的实际地址。 + + +```shell +sh tools/build.sh +``` + +* 编译完成之后,会在`build`文件夹下生成一个名为`ocr_system`的可执行文件。 + + +### 运行demo +* 执行以下命令,完成对一幅图像的OCR识别与检测,最终输出 + +```shell +sh tools/run.sh +``` + +最终屏幕上会输出检测结果如下。 + +
+ +
diff --git a/deploy/cpp_infer/tools/build.sh b/deploy/cpp_infer/tools/build.sh index a3a709ca..8eea3b85 100755 --- a/deploy/cpp_infer/tools/build.sh +++ b/deploy/cpp_infer/tools/build.sh @@ -1,4 +1,5 @@ +OPENCV_DIR=/paddle/libs/opencv-3.4.7/opencv3 LIB_DIR=/paddle/code/gry/Paddle/build/fluid_inference_install_dir/ CUDA_LIB_DIR=/usr/local/cuda/lib64 CUDNN_LIB_DIR=/usr/lib/x86_64-linux-gnu/ @@ -15,6 +16,7 @@ cmake .. \ -DWITH_GPU=OFF \ -DWITH_STATIC_LIB=OFF \ -DUSE_TENSORRT=OFF \ + -DOPENCV_DIR=${OPENCV_DIR} \ -DCUDNN_LIB=${CUDNN_LIB_DIR} \ -DCUDA_LIB=${CUDA_LIB_DIR} \ -DTENSORRT_ROOT=YOUR_TENSORRT_ROOT_DIR diff --git a/deploy/cpp_infer/tools/run.sh b/deploy/cpp_infer/tools/run.sh index 1989d5ea..dd2441c1 100755 --- a/deploy/cpp_infer/tools/run.sh +++ b/deploy/cpp_infer/tools/run.sh @@ -1,2 +1,2 @@ -./build/ocr_system ./inference/det_db/ ./inference/rec_crnn/ ../../doc/imgs/11.jpg +./build/ocr_system ./inference/det_db/ ./inference/rec_crnn/ ../../doc/imgs/12.jpg diff --git a/deploy/imgs/cpp_infer_pred_12.png b/deploy/imgs/cpp_infer_pred_12.png new file mode 100644 index 0000000000000000000000000000000000000000..0ed85a31a0c64f50c165eeb887ceebd52894d785 GIT binary patch literal 80323 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 fG(gmGE;Al?mM^vq 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 Date: Mon, 13 Jul 2020 08:59:21 +0000 Subject: [PATCH 3/7] add config --- deploy/cpp_infer/CMakeLists.txt | 6 +- deploy/cpp_infer/include/config.h | 91 +++++++++++++++++++++++ deploy/cpp_infer/include/ocr_det.h | 34 +++++++-- deploy/cpp_infer/include/ocr_rec.h | 38 +++++----- deploy/cpp_infer/include/postprocess_op.h | 44 +++-------- deploy/cpp_infer/include/preprocess_op.h | 3 +- deploy/cpp_infer/include/utility.h | 41 ++++++++++ deploy/cpp_infer/src/config.cpp | 64 ++++++++++++++++ deploy/cpp_infer/src/main.cpp | 23 ++++-- deploy/cpp_infer/src/ocr_det.cpp | 54 +++++++------- deploy/cpp_infer/src/ocr_rec.cpp | 60 ++++++--------- deploy/cpp_infer/src/postprocess_op.cpp | 27 ++++--- deploy/cpp_infer/src/preprocess_op.cpp | 2 +- deploy/cpp_infer/src/utility.cpp | 39 ++++++++++ deploy/cpp_infer/tools/config.txt | 17 +++++ deploy/cpp_infer/tools/run.sh | 2 +- 16 files changed, 392 insertions(+), 153 deletions(-) create mode 100644 deploy/cpp_infer/include/config.h create mode 100644 deploy/cpp_infer/include/utility.h create mode 100644 deploy/cpp_infer/src/config.cpp create mode 100644 deploy/cpp_infer/src/utility.cpp create mode 100644 deploy/cpp_infer/tools/config.txt diff --git a/deploy/cpp_infer/CMakeLists.txt b/deploy/cpp_infer/CMakeLists.txt index 8f06068c..1415e2cb 100644 --- a/deploy/cpp_infer/CMakeLists.txt +++ b/deploy/cpp_infer/CMakeLists.txt @@ -57,7 +57,8 @@ link_directories("${PADDLE_LIB}/third_party/install/xxhash/lib") link_directories("${PADDLE_LIB}/paddle/lib") -add_executable(${DEMO_NAME} src/main.cpp src/ocr_det.cpp src/ocr_rec.cpp src/preprocess_op.cpp src/clipper.cpp src/postprocess_op.cpp ) +AUX_SOURCE_DIRECTORY(./src SRCS) +add_executable(${DEMO_NAME} ${SRCS}) if(WITH_MKL) include_directories("${PADDLE_LIB}/third_party/install/mklml/include") @@ -81,9 +82,6 @@ else() ${PADDLE_LIB}/paddle/lib/libpaddle_fluid${CMAKE_SHARED_LIBRARY_SUFFIX}) endif() -# user ze -# set(EXTERNAL_LIB "-lrt -ldl -lpthread -lm -lopencv_world") -# gry set(EXTERNAL_LIB "-lrt -ldl -lpthread -lm") set(DEPS ${DEPS} diff --git a/deploy/cpp_infer/include/config.h b/deploy/cpp_infer/include/config.h new file mode 100644 index 00000000..37c5078a --- /dev/null +++ b/deploy/cpp_infer/include/config.h @@ -0,0 +1,91 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "include/utility.h" + +namespace PaddleOCR { + +class Config { +public: + explicit Config(const std::string &config_file) { + config_map_ = LoadConfig(config_file); + + this->use_gpu = bool(stoi(config_map_["use_gpu"])); + + this->gpu_id = stoi(config_map_["gpu_id"]); + + this->gpu_mem = stoi(config_map_["gpu_mem"]); + + this->cpu_math_library_num_threads = + stoi(config_map_["cpu_math_library_num_threads"]); + + this->max_side_len = stoi(config_map_["max_side_len"]); + + this->det_db_thresh = stod(config_map_["det_db_thresh"]); + + this->det_db_box_thresh = stod(config_map_["det_db_box_thresh"]); + + this->det_db_box_thresh = stod(config_map_["det_db_box_thresh"]); + + this->det_model_dir.assign(config_map_["det_model_dir"]); + + this->rec_model_dir.assign(config_map_["rec_model_dir"]); + + this->char_list_file.assign(config_map_["char_list_file"]); + } + + bool use_gpu = false; + + int gpu_id = 0; + + int gpu_mem = 4000; + + int cpu_math_library_num_threads = 1; + + int max_side_len = 960; + + double det_db_thresh = 0.3; + + double det_db_box_thresh = 0.5; + + double det_db_unclip_ratio = 2.0; + + std::string det_model_dir; + + std::string rec_model_dir; + + std::string char_list_file; + + void PrintConfigInfo(); + +private: + // Load configuration + std::map LoadConfig(const std::string &config_file); + + std::vector split(const std::string &str, + const std::string &delim); + + std::map config_map_; +}; + +} // namespace PaddleOCR diff --git a/deploy/cpp_infer/include/ocr_det.h b/deploy/cpp_infer/include/ocr_det.h index efcaa25c..3208d346 100644 --- a/deploy/cpp_infer/include/ocr_det.h +++ b/deploy/cpp_infer/include/ocr_det.h @@ -36,16 +36,29 @@ namespace PaddleOCR { class DBDetector { public: - explicit DBDetector(const std::string &model_dir, bool use_gpu = false, - const int gpu_id = 0, const int max_side_len = 960) { - LoadModel(model_dir, use_gpu); + explicit DBDetector(const std::string &model_dir, const bool &use_gpu = false, + const int &gpu_id = 0, const int &gpu_mem = 4000, + const int &cpu_math_library_num_threads = 4, + const int &max_side_len = 960, + const double &det_db_thresh = 0.3, + const double &det_db_box_thresh = 0.5, + const double &det_db_unclip_ratio = 2.0) { + LoadModel(model_dir); + + this->use_gpu_ = use_gpu; + this->gpu_id_ = gpu_id; + this->gpu_mem_ = gpu_mem; + this->cpu_math_library_num_threads_ = cpu_math_library_num_threads; + this->max_side_len_ = max_side_len; + + this->det_db_thresh_ = det_db_thresh; + this->det_db_box_thresh_ = det_db_box_thresh; + this->det_db_unclip_ratio_ = det_db_unclip_ratio; } // Load Paddle inference model - void LoadModel(const std::string &model_dir, bool use_gpu, - const int min_subgraph_size = 3, const int batch_size = 1, - const int gpu_id = 0); + void LoadModel(const std::string &model_dir); // Run predictor void Run(cv::Mat &img, std::vector>> &boxes); @@ -53,8 +66,17 @@ public: private: std::shared_ptr predictor_; + bool use_gpu_ = false; + int gpu_id_ = 0; + int gpu_mem_ = 4000; + int cpu_math_library_num_threads_ = 4; + int max_side_len_ = 960; + double det_db_thresh_ = 0.3; + double det_db_box_thresh_ = 0.5; + double det_db_unclip_ratio_ = 2.0; + std::vector mean_ = {0.485f, 0.456f, 0.406f}; std::vector scale_ = {1 / 0.229f, 1 / 0.224f, 1 / 0.225f}; bool is_scale_ = true; diff --git a/deploy/cpp_infer/include/ocr_rec.h b/deploy/cpp_infer/include/ocr_rec.h index 50220136..dfb02f63 100644 --- a/deploy/cpp_infer/include/ocr_rec.h +++ b/deploy/cpp_infer/include/ocr_rec.h @@ -29,29 +29,40 @@ #include #include +#include namespace PaddleOCR { class CRNNRecognizer { public: - explicit CRNNRecognizer(const std::string &model_dir, - const string label_path = "./tools/ppocr_keys_v1.txt", - bool use_gpu = false, const int gpu_id = 0) { - LoadModel(model_dir, use_gpu); + explicit CRNNRecognizer( + const std::string &model_dir, const bool &use_gpu = false, + const int &gpu_id = 0, const int &gpu_mem = 4000, + const int &cpu_math_library_num_threads = 4, + const string &label_path = "./tools/ppocr_keys_v1.txt") { + LoadModel(model_dir); - this->label_list_ = ReadDict(label_path); + this->use_gpu_ = use_gpu; + this->gpu_id_ = gpu_id; + this->gpu_mem_ = gpu_mem; + this->cpu_math_library_num_threads_ = cpu_math_library_num_threads; + + this->label_list_ = Utility::ReadDict(label_path); } // Load Paddle inference model - void LoadModel(const std::string &model_dir, bool use_gpu, - const int gpu_id = 0, const int min_subgraph_size = 3, - const int batch_size = 1); + void LoadModel(const std::string &model_dir); void Run(std::vector>> boxes, cv::Mat &img); private: std::shared_ptr predictor_; + bool use_gpu_ = false; + int gpu_id_ = 0; + int gpu_mem_ = 4000; + int cpu_math_library_num_threads_ = 4; + std::vector label_list_; std::vector mean_ = {0.5f, 0.5f, 0.5f}; @@ -66,15 +77,8 @@ private: // post-process PostProcessor post_processor_; - cv::Mat get_rotate_crop_image(const cv::Mat &srcimage, - std::vector> box); - - std::vector ReadDict(const std::string &path); - - template - inline size_t argmax(ForwardIterator first, ForwardIterator last) { - return std::distance(first, std::max_element(first, last)); - } + cv::Mat GetRotateCropImage(const cv::Mat &srcimage, + std::vector> box); }; // class CrnnRecognizer diff --git a/deploy/cpp_infer/include/postprocess_op.h b/deploy/cpp_infer/include/postprocess_op.h index d851e29e..de6a81b2 100644 --- a/deploy/cpp_infer/include/postprocess_op.h +++ b/deploy/cpp_infer/include/postprocess_op.h @@ -28,36 +28,17 @@ #include #include "include/clipper.h" +#include "include/utility.h" using namespace std; namespace PaddleOCR { -inline std::vector ReadDict(std::string path) { - std::ifstream in(path); - std::string filename; - std::string line; - std::vector m_vec; - if (in) { - while (getline(in, line)) { - m_vec.push_back(line); - } - } else { - std::cout << "no such file" << std::endl; - } - return m_vec; -} - -template -inline size_t Argmax(ForwardIterator first, ForwardIterator last) { - return std::distance(first, std::max_element(first, last)); -} - class PostProcessor { public: void GetContourArea(float **box, float unclip_ratio, float &distance); - cv::RotatedRect unclip(float **box); + cv::RotatedRect UnClip(float **box, const float &unclip_ratio); float **Mat2Vec(cv::Mat mat); @@ -67,23 +48,17 @@ public: std::vector> order_points_clockwise(std::vector> pts); - float **get_mini_boxes(cv::RotatedRect box, float &ssid); + float **GetMiniBoxes(cv::RotatedRect box, float &ssid); - float box_score_fast(float **box_array, cv::Mat pred); + float BoxScoreFast(float **box_array, cv::Mat pred); std::vector>> - boxes_from_bitmap(const cv::Mat pred, const cv::Mat bitmap); + BoxesFromBitmap(const cv::Mat pred, const cv::Mat bitmap, + const float &box_thresh, const float &det_db_unclip_ratio); std::vector>> - filter_tag_det_res(std::vector>> boxes, - float ratio_h, float ratio_w, cv::Mat srcimg); - - template - inline size_t argmax(ForwardIterator first, ForwardIterator last) { - return std::distance(first, std::max_element(first, last)); - } - - // CRNN + FilterTagDetRes(std::vector>> boxes, + float ratio_h, float ratio_w, cv::Mat srcimg); private: void quickSort(float **s, int l, int r); @@ -99,6 +74,7 @@ private: return min; return x; } + inline float clampf(float x, float min, float max) { if (x > max) return max; @@ -108,4 +84,4 @@ private: } }; -} // namespace PaddleOCR \ No newline at end of file +} // namespace PaddleOCR diff --git a/deploy/cpp_infer/include/preprocess_op.h b/deploy/cpp_infer/include/preprocess_op.h index 61f80449..309d7fd4 100644 --- a/deploy/cpp_infer/include/preprocess_op.h +++ b/deploy/cpp_infer/include/preprocess_op.h @@ -44,7 +44,6 @@ public: virtual void Run(const cv::Mat *im, float *data); }; -// RGB -> CHW class ResizeImgType0 { public: virtual void Run(const cv::Mat &img, cv::Mat &resize_img, int max_size_len, @@ -54,7 +53,7 @@ public: class CrnnResizeImg { public: virtual void Run(const cv::Mat &img, cv::Mat &resize_img, float wh_ratio, - const std::vector rec_image_shape = {3, 32, 320}); + const std::vector &rec_image_shape = {3, 32, 320}); }; } // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/include/utility.h b/deploy/cpp_infer/include/utility.h new file mode 100644 index 00000000..ebcc3e84 --- /dev/null +++ b/deploy/cpp_infer/include/utility.h @@ -0,0 +1,41 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace PaddleOCR { + +class Utility { +public: + static std::vector ReadDict(const std::string &path); + + template + inline static size_t argmax(ForwardIterator first, ForwardIterator last) { + return std::distance(first, std::max_element(first, last)); + } +}; + +} // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/src/config.cpp b/deploy/cpp_infer/src/config.cpp new file mode 100644 index 00000000..228c874d --- /dev/null +++ b/deploy/cpp_infer/src/config.cpp @@ -0,0 +1,64 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace PaddleOCR { + +std::vector Config::split(const std::string &str, + const std::string &delim) { + std::vector res; + if ("" == str) + return res; + char *strs = new char[str.length() + 1]; + std::strcpy(strs, str.c_str()); + + char *d = new char[delim.length() + 1]; + std::strcpy(d, delim.c_str()); + + char *p = std::strtok(strs, d); + while (p) { + std::string s = p; + res.push_back(s); + p = std::strtok(NULL, d); + } + + return res; +} + +std::map +Config::LoadConfig(const std::string &config_path) { + auto config = Utility::ReadDict(config_path); + + std::map dict; + for (int i = 0; i < config.size(); i++) { + // pass for empty line or comment + if (config[i].size() <= 1 or config[i][0] == '#') { + continue; + } + std::vector res = split(config[i], " "); + dict[res[0]] = res[1]; + } + return dict; +} + +void Config::PrintConfigInfo() { + std::cout << "=======Paddle OCR inference config======" << std::endl; + for (auto iter = config_map_.begin(); iter != config_map_.end(); iter++) { + std::cout << iter->first << " : " << iter->second << std::endl; + } + std::cout << "=======End of Paddle OCR inference config======" << std::endl; +} + +} // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/src/main.cpp b/deploy/cpp_infer/src/main.cpp index 823f3046..b63bdafc 100644 --- a/deploy/cpp_infer/src/main.cpp +++ b/deploy/cpp_infer/src/main.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -33,21 +34,29 @@ using namespace cv; using namespace PaddleOCR; int main(int argc, char **argv) { - if (argc < 4) { + if (argc < 3) { std::cerr << "[ERROR] usage: " << argv[0] - << " det_model_file rec_model_file image_path\n"; + << " configure_filepath image_path\n"; exit(1); } - std::string det_model_file = argv[1]; - std::string rec_model_file = argv[2]; - std::string img_path = argv[3]; + + Config config(argv[1]); + + config.PrintConfigInfo(); + + std::string img_path(argv[2]); auto start = std::chrono::system_clock::now(); cv::Mat srcimg = cv::imread(img_path, cv::IMREAD_COLOR); - DBDetector det(det_model_file); - CRNNRecognizer rec(rec_model_file); + DBDetector det(config.det_model_dir, config.use_gpu, config.gpu_id, + config.gpu_mem, config.cpu_math_library_num_threads, + config.max_side_len, config.det_db_thresh, + config.det_db_box_thresh, config.det_db_unclip_ratio); + CRNNRecognizer rec(config.rec_model_dir, config.use_gpu, config.gpu_id, + config.gpu_mem, config.cpu_math_library_num_threads, + config.char_list_file); std::vector>> boxes; det.Run(srcimg, boxes); diff --git a/deploy/cpp_infer/src/ocr_det.cpp b/deploy/cpp_infer/src/ocr_det.cpp index 746c94b2..a449e1b3 100644 --- a/deploy/cpp_infer/src/ocr_det.cpp +++ b/deploy/cpp_infer/src/ocr_det.cpp @@ -31,29 +31,28 @@ namespace PaddleOCR { -void DBDetector::LoadModel(const std::string &model_dir, bool use_gpu, - const int gpu_id, const int min_subgraph_size, - const int batch_size) { +void DBDetector::LoadModel(const std::string &model_dir) { AnalysisConfig config; config.SetModel(model_dir + "/model", model_dir + "/params"); - // for cpu - config.DisableGpu(); - config.EnableMKLDNN(); // 开启MKLDNN加速 - config.SetCpuMathLibraryNumThreads(10); + if (this->use_gpu_) { + config.EnableUseGpu(this->gpu_mem_, this->gpu_id_); + } else { + config.DisableGpu(); + config.EnableMKLDNN(); // 开启MKLDNN加速 + config.SetCpuMathLibraryNumThreads(this->cpu_math_library_num_threads_); + } - // 使用ZeroCopyTensor,此处必须设置为false + // false for zero copy tensor config.SwitchUseFeedFetchOps(false); - // 若输入为多个,此处必须设置为true + // true for multiple input config.SwitchSpecifyInputNames(true); - // config.SwitchIrDebug(true); // - // 可视化调试选项,若开启,则会在每个图优化过程后生成dot文件 - // config.SwitchIrOptim(false);// 默认为true。如果设置为false,关闭所有优化 - config.EnableMemoryOptim(); // 开启内存/显存复用 + + config.SwitchIrOptim(true); + + config.EnableMemoryOptim(); this->predictor_ = CreatePaddlePredictor(config); - // predictor_ = std::move(CreatePaddlePredictor(config)); // PaddleDetection - // usage } void DBDetector::Run(cv::Mat &img, @@ -69,13 +68,13 @@ void DBDetector::Run(cv::Mat &img, this->normalize_op_.Run(&resize_img, this->mean_, this->scale_, this->is_scale_); - float *input = new float[1 * 3 * resize_img.rows * resize_img.cols]; - this->permute_op_.Run(&resize_img, input); + std::vector input(1 * 3 * resize_img.rows * resize_img.cols, 0.0f); + this->permute_op_.Run(&resize_img, input.data()); auto input_names = this->predictor_->GetInputNames(); auto input_t = this->predictor_->GetInputTensor(input_names[0]); input_t->Reshape({1, 3, resize_img.rows, resize_img.cols}); - input_t->copy_from_cpu(input); + input_t->copy_from_cpu(input.data()); this->predictor_->ZeroCopyRun(); @@ -93,25 +92,26 @@ void DBDetector::Run(cv::Mat &img, int n3 = output_shape[3]; int n = n2 * n3; - float *pred = new float[n]; - unsigned char *cbuf = new unsigned char[n]; + std::vector pred(n, 0.0); + std::vector cbuf(n, ' '); for (int i = 0; i < n; i++) { pred[i] = float(out_data[i]); cbuf[i] = (unsigned char)((out_data[i]) * 255); } - cv::Mat cbuf_map(n2, n3, CV_8UC1, (unsigned char *)cbuf); - cv::Mat pred_map(n2, n3, CV_32F, (float *)pred); + cv::Mat cbuf_map(n2, n3, CV_8UC1, (unsigned char *)cbuf.data()); + cv::Mat pred_map(n2, n3, CV_32F, (float *)pred.data()); - const double threshold = 0.3 * 255; + const double threshold = this->det_db_thresh_ * 255; const double maxvalue = 255; cv::Mat bit_map; cv::threshold(cbuf_map, bit_map, threshold, maxvalue, cv::THRESH_BINARY); - boxes = post_processor_.boxes_from_bitmap(pred_map, bit_map); + boxes = post_processor_.BoxesFromBitmap( + pred_map, bit_map, this->det_db_box_thresh_, this->det_db_unclip_ratio_); - boxes = post_processor_.filter_tag_det_res(boxes, ratio_h, ratio_w, srcimg); + boxes = post_processor_.FilterTagDetRes(boxes, ratio_h, ratio_w, srcimg); //// visualization cv::Point rook_points[boxes.size()][4]; @@ -133,10 +133,6 @@ void DBDetector::Run(cv::Mat &img, std::cout << "The detection visualized image saved in ./det_res.png" << std::endl; - - delete[] input; - delete[] pred; - delete[] cbuf; } } // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/src/ocr_rec.cpp b/deploy/cpp_infer/src/ocr_rec.cpp index b50020e7..6173c7ce 100644 --- a/deploy/cpp_infer/src/ocr_rec.cpp +++ b/deploy/cpp_infer/src/ocr_rec.cpp @@ -41,7 +41,7 @@ void CRNNRecognizer::Run(std::vector>> boxes, std::cout << "The predicted text is :" << std::endl; int index = 0; for (int i = boxes.size() - 1; i >= 0; i--) { - crop_img = get_rotate_crop_image(srcimg, boxes[i]); + crop_img = GetRotateCropImage(srcimg, boxes[i]); float wh_ratio = float(crop_img.cols) / float(crop_img.rows); @@ -50,14 +50,14 @@ void CRNNRecognizer::Run(std::vector>> boxes, this->normalize_op_.Run(&resize_img, this->mean_, this->scale_, this->is_scale_); - float *input = new float[1 * 3 * resize_img.rows * resize_img.cols]; + std::vector input(1 * 3 * resize_img.rows * resize_img.cols, 0.0f); - this->permute_op_.Run(&resize_img, input); + this->permute_op_.Run(&resize_img, input.data()); auto input_names = this->predictor_->GetInputNames(); auto input_t = this->predictor_->GetInputTensor(input_names[0]); input_t->Reshape({1, 3, resize_img.rows, resize_img.cols}); - input_t->copy_from_cpu(input); + input_t->copy_from_cpu(input.data()); this->predictor_->ZeroCopyRun(); @@ -104,7 +104,8 @@ void CRNNRecognizer::Run(std::vector>> boxes, float max_value = 0.0f; for (int n = predict_lod[0][0]; n < predict_lod[0][1] - 1; n++) { - argmax_idx = int(argmax(&predict_batch[n * predict_shape[1]], + argmax_idx = + int(Utility::argmax(&predict_batch[n * predict_shape[1]], &predict_batch[(n + 1) * predict_shape[1]])); max_value = float(*std::max_element(&predict_batch[n * predict_shape[1]], @@ -116,37 +117,35 @@ void CRNNRecognizer::Run(std::vector>> boxes, } score /= count; std::cout << "\tscore: " << score << std::endl; - - delete[] input; } } -void CRNNRecognizer::LoadModel(const std::string &model_dir, bool use_gpu, - const int gpu_id, const int min_subgraph_size, - const int batch_size) { +void CRNNRecognizer::LoadModel(const std::string &model_dir) { AnalysisConfig config; config.SetModel(model_dir + "/model", model_dir + "/params"); - // for cpu - config.DisableGpu(); - config.EnableMKLDNN(); // 开启MKLDNN加速 - config.SetCpuMathLibraryNumThreads(10); + if (this->use_gpu_) { + config.EnableUseGpu(this->gpu_mem_, this->gpu_id_); + } else { + config.DisableGpu(); + config.EnableMKLDNN(); // 开启MKLDNN加速 + config.SetCpuMathLibraryNumThreads(this->cpu_math_library_num_threads_); + } - // 使用ZeroCopyTensor,此处必须设置为false + // false for zero copy tensor config.SwitchUseFeedFetchOps(false); - // 若输入为多个,此处必须设置为true + // true for multiple input config.SwitchSpecifyInputNames(true); - // config.SwitchIrDebug(true); // - // 可视化调试选项,若开启,则会在每个图优化过程后生成dot文件 - // config.SwitchIrOptim(false);// 默认为true。如果设置为false,关闭所有优化 - config.EnableMemoryOptim(); // 开启内存/显存复用 + + config.SwitchIrOptim(true); + + config.EnableMemoryOptim(); this->predictor_ = CreatePaddlePredictor(config); } -cv::Mat -CRNNRecognizer::get_rotate_crop_image(const cv::Mat &srcimage, - std::vector> box) { +cv::Mat CRNNRecognizer::GetRotateCropImage(const cv::Mat &srcimage, + std::vector> box) { cv::Mat image; srcimage.copyTo(image); std::vector> points = box; @@ -200,19 +199,4 @@ CRNNRecognizer::get_rotate_crop_image(const cv::Mat &srcimage, } } -std::vector CRNNRecognizer::ReadDict(const std::string &path) { - std::ifstream in(path); - std::string filename; - std::string line; - std::vector m_vec; - if (in) { - while (getline(in, line)) { - m_vec.push_back(line); - } - } else { - std::cout << "no such file" << std::endl; - } - return m_vec; -} - } // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/src/postprocess_op.cpp b/deploy/cpp_infer/src/postprocess_op.cpp index 69036db1..78c37a03 100644 --- a/deploy/cpp_infer/src/postprocess_op.cpp +++ b/deploy/cpp_infer/src/postprocess_op.cpp @@ -34,8 +34,7 @@ void PostProcessor::GetContourArea(float **box, float unclip_ratio, distance = area * unclip_ratio / dist; } -cv::RotatedRect PostProcessor::unclip(float **box) { - float unclip_ratio = 2.0; +cv::RotatedRect PostProcessor::UnClip(float **box, const float &unclip_ratio) { float distance = 1.0; GetContourArea(box, unclip_ratio, distance); @@ -136,7 +135,7 @@ PostProcessor::order_points_clockwise(std::vector> pts) { return rect; } -float **PostProcessor::get_mini_boxes(cv::RotatedRect box, float &ssid) { +float **PostProcessor::GetMiniBoxes(cv::RotatedRect box, float &ssid) { ssid = box.size.width >= box.size.height ? box.size.height : box.size.width; cv::Mat points; @@ -169,7 +168,7 @@ float **PostProcessor::get_mini_boxes(cv::RotatedRect box, float &ssid) { return array; } -float PostProcessor::box_score_fast(float **box_array, cv::Mat pred) { +float PostProcessor::BoxScoreFast(float **box_array, cv::Mat pred) { auto array = box_array; int width = pred.cols; int height = pred.rows; @@ -207,10 +206,11 @@ float PostProcessor::box_score_fast(float **box_array, cv::Mat pred) { } std::vector>> -PostProcessor::boxes_from_bitmap(const cv::Mat pred, const cv::Mat bitmap) { +PostProcessor::BoxesFromBitmap(const cv::Mat pred, const cv::Mat bitmap, + const float &box_thresh, + const float &det_db_unclip_ratio) { const int min_size = 3; const int max_candidates = 1000; - const float box_thresh = 0.5; int width = bitmap.cols; int height = bitmap.rows; @@ -229,7 +229,7 @@ PostProcessor::boxes_from_bitmap(const cv::Mat pred, const cv::Mat bitmap) { for (int _i = 0; _i < num_contours; _i++) { float ssid; cv::RotatedRect box = cv::minAreaRect(contours[_i]); - auto array = get_mini_boxes(box, ssid); + auto array = GetMiniBoxes(box, ssid); auto box_for_unclip = array; // end get_mini_box @@ -239,17 +239,16 @@ PostProcessor::boxes_from_bitmap(const cv::Mat pred, const cv::Mat bitmap) { } float score; - score = box_score_fast(array, pred); - // end box_score_fast + score = BoxScoreFast(array, pred); if (score < box_thresh) continue; // start for unclip - cv::RotatedRect points = unclip(box_for_unclip); + cv::RotatedRect points = UnClip(box_for_unclip, det_db_unclip_ratio); // end for unclip cv::RotatedRect clipbox = points; - auto cliparray = get_mini_boxes(clipbox, ssid); + auto cliparray = GetMiniBoxes(clipbox, ssid); if (ssid < min_size + 2) continue; @@ -273,9 +272,9 @@ PostProcessor::boxes_from_bitmap(const cv::Mat pred, const cv::Mat bitmap) { return boxes; } -std::vector>> PostProcessor::filter_tag_det_res( - std::vector>> boxes, float ratio_h, - float ratio_w, cv::Mat srcimg) { +std::vector>> +PostProcessor::FilterTagDetRes(std::vector>> boxes, + float ratio_h, float ratio_w, cv::Mat srcimg) { int oriimg_h = srcimg.rows; int oriimg_w = srcimg.cols; diff --git a/deploy/cpp_infer/src/preprocess_op.cpp b/deploy/cpp_infer/src/preprocess_op.cpp index 5fee3c4d..0078063e 100644 --- a/deploy/cpp_infer/src/preprocess_op.cpp +++ b/deploy/cpp_infer/src/preprocess_op.cpp @@ -97,7 +97,7 @@ void ResizeImgType0::Run(const cv::Mat &img, cv::Mat &resize_img, } void CrnnResizeImg::Run(const cv::Mat &img, cv::Mat &resize_img, float wh_ratio, - const std::vector rec_image_shape) { + const std::vector &rec_image_shape) { int imgC, imgH, imgW; imgC = rec_image_shape[0]; imgH = rec_image_shape[1]; diff --git a/deploy/cpp_infer/src/utility.cpp b/deploy/cpp_infer/src/utility.cpp new file mode 100644 index 00000000..8ca5ac67 --- /dev/null +++ b/deploy/cpp_infer/src/utility.cpp @@ -0,0 +1,39 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include + +namespace PaddleOCR { + +std::vector Utility::ReadDict(const std::string &path) { + std::ifstream in(path); + std::string line; + std::vector m_vec; + if (in) { + while (getline(in, line)) { + m_vec.push_back(line); + } + } else { + std::cout << "no such label file: " << path << ", exit the program..." + << std::endl; + exit(1); + } + return m_vec; +} + +} // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/tools/config.txt b/deploy/cpp_infer/tools/config.txt new file mode 100644 index 00000000..1ce1568e --- /dev/null +++ b/deploy/cpp_infer/tools/config.txt @@ -0,0 +1,17 @@ +# model load config +use_gpu 0 +gpu_id 0 +gpu_mem 4000 +cpu_math_library_num_threads 1 + +# det config +max_side_len 960 +det_db_thresh 0.3 +det_db_box_thresh 0.5 +det_db_unclip_ratio 2.0 +det_model_dir ./inference/det_db + +# rec config +rec_model_dir ./inference/rec_crnn +char_list_file ./tools/ppocr_keys_v1.txt +img_path ../../doc/imgs/11.jpg diff --git a/deploy/cpp_infer/tools/run.sh b/deploy/cpp_infer/tools/run.sh index dd2441c1..052b363b 100755 --- a/deploy/cpp_infer/tools/run.sh +++ b/deploy/cpp_infer/tools/run.sh @@ -1,2 +1,2 @@ -./build/ocr_system ./inference/det_db/ ./inference/rec_crnn/ ../../doc/imgs/12.jpg +./build/ocr_system ./tools/config.txt ../../doc/imgs/6.jpg From 69a7a140cf5a0f423723063a67ad7e7d64861acf Mon Sep 17 00:00:00 2001 From: littletomatodonkey Date: Mon, 13 Jul 2020 09:15:54 +0000 Subject: [PATCH 4/7] fix visualization --- deploy/cpp_infer/include/config.h | 4 ++++ deploy/cpp_infer/include/ocr_det.h | 7 ++++++- deploy/cpp_infer/include/utility.h | 8 ++++++++ deploy/cpp_infer/src/main.cpp | 3 ++- deploy/cpp_infer/src/ocr_det.cpp | 20 ++------------------ deploy/cpp_infer/src/utility.cpp | 22 ++++++++++++++++++++++ deploy/cpp_infer/tools/config.txt | 4 ++++ 7 files changed, 48 insertions(+), 20 deletions(-) diff --git a/deploy/cpp_infer/include/config.h b/deploy/cpp_infer/include/config.h index 37c5078a..4004fc04 100644 --- a/deploy/cpp_infer/include/config.h +++ b/deploy/cpp_infer/include/config.h @@ -52,6 +52,8 @@ public: this->rec_model_dir.assign(config_map_["rec_model_dir"]); this->char_list_file.assign(config_map_["char_list_file"]); + + this->visualize = bool(stoi(config_map_["visualize"])); } bool use_gpu = false; @@ -76,6 +78,8 @@ public: std::string char_list_file; + bool visualize = true; + void PrintConfigInfo(); private: diff --git a/deploy/cpp_infer/include/ocr_det.h b/deploy/cpp_infer/include/ocr_det.h index 3208d346..503d3ee3 100644 --- a/deploy/cpp_infer/include/ocr_det.h +++ b/deploy/cpp_infer/include/ocr_det.h @@ -42,7 +42,8 @@ public: const int &max_side_len = 960, const double &det_db_thresh = 0.3, const double &det_db_box_thresh = 0.5, - const double &det_db_unclip_ratio = 2.0) { + const double &det_db_unclip_ratio = 2.0, + const bool &visualize = true) { LoadModel(model_dir); this->use_gpu_ = use_gpu; @@ -55,6 +56,8 @@ public: this->det_db_thresh_ = det_db_thresh; this->det_db_box_thresh_ = det_db_box_thresh; this->det_db_unclip_ratio_ = det_db_unclip_ratio; + + this->visualize_ = visualize; } // Load Paddle inference model @@ -77,6 +80,8 @@ private: double det_db_box_thresh_ = 0.5; double det_db_unclip_ratio_ = 2.0; + bool visualize_ = true; + std::vector mean_ = {0.485f, 0.456f, 0.406f}; std::vector scale_ = {1 / 0.229f, 1 / 0.224f, 1 / 0.225f}; bool is_scale_ = true; diff --git a/deploy/cpp_infer/include/utility.h b/deploy/cpp_infer/include/utility.h index ebcc3e84..367e37e4 100644 --- a/deploy/cpp_infer/include/utility.h +++ b/deploy/cpp_infer/include/utility.h @@ -26,12 +26,20 @@ #include #include +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" + namespace PaddleOCR { class Utility { public: static std::vector ReadDict(const std::string &path); + static void + VisualizeBboxes(const cv::Mat &srcimg, + const std::vector>> &boxes); + template inline static size_t argmax(ForwardIterator first, ForwardIterator last) { return std::distance(first, std::max_element(first, last)); diff --git a/deploy/cpp_infer/src/main.cpp b/deploy/cpp_infer/src/main.cpp index b63bdafc..d5e3d4c6 100644 --- a/deploy/cpp_infer/src/main.cpp +++ b/deploy/cpp_infer/src/main.cpp @@ -53,7 +53,8 @@ int main(int argc, char **argv) { DBDetector det(config.det_model_dir, config.use_gpu, config.gpu_id, config.gpu_mem, config.cpu_math_library_num_threads, config.max_side_len, config.det_db_thresh, - config.det_db_box_thresh, config.det_db_unclip_ratio); + config.det_db_box_thresh, config.det_db_unclip_ratio, + config.visualize); CRNNRecognizer rec(config.rec_model_dir, config.use_gpu, config.gpu_id, config.gpu_mem, config.cpu_math_library_num_threads, config.char_list_file); diff --git a/deploy/cpp_infer/src/ocr_det.cpp b/deploy/cpp_infer/src/ocr_det.cpp index a449e1b3..56447b07 100644 --- a/deploy/cpp_infer/src/ocr_det.cpp +++ b/deploy/cpp_infer/src/ocr_det.cpp @@ -114,25 +114,9 @@ void DBDetector::Run(cv::Mat &img, boxes = post_processor_.FilterTagDetRes(boxes, ratio_h, ratio_w, srcimg); //// visualization - cv::Point rook_points[boxes.size()][4]; - for (int n = 0; n < boxes.size(); n++) { - for (int m = 0; m < boxes[0].size(); m++) { - rook_points[n][m] = cv::Point(int(boxes[n][m][0]), int(boxes[n][m][1])); - } + if (this->visualize_) { + Utility::VisualizeBboxes(srcimg, boxes); } - - cv::Mat img_vis; - srcimg.copyTo(img_vis); - for (int n = 0; n < boxes.size(); n++) { - const cv::Point *ppt[1] = {rook_points[n]}; - int npt[] = {4}; - cv::polylines(img_vis, ppt, npt, 1, 1, CV_RGB(0, 255, 0), 2, 8, 0); - } - - imwrite("./det_res.png", img_vis); - - std::cout << "The detection visualized image saved in ./det_res.png" - << std::endl; } } // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/src/utility.cpp b/deploy/cpp_infer/src/utility.cpp index 8ca5ac67..ffb74c2e 100644 --- a/deploy/cpp_infer/src/utility.cpp +++ b/deploy/cpp_infer/src/utility.cpp @@ -36,4 +36,26 @@ std::vector Utility::ReadDict(const std::string &path) { return m_vec; } +void Utility::VisualizeBboxes( + const cv::Mat &srcimg, + const std::vector>> &boxes) { + cv::Point rook_points[boxes.size()][4]; + for (int n = 0; n < boxes.size(); n++) { + for (int m = 0; m < boxes[0].size(); m++) { + rook_points[n][m] = cv::Point(int(boxes[n][m][0]), int(boxes[n][m][1])); + } + } + cv::Mat img_vis; + srcimg.copyTo(img_vis); + for (int n = 0; n < boxes.size(); n++) { + const cv::Point *ppt[1] = {rook_points[n]}; + int npt[] = {4}; + cv::polylines(img_vis, ppt, npt, 1, 1, CV_RGB(0, 255, 0), 2, 8, 0); + } + + cv::imwrite("./ocr_vis.png", img_vis); + std::cout << "The detection visualized image saved in ./ocr_vis.png.pn" + << std::endl; +} + } // namespace PaddleOCR \ No newline at end of file diff --git a/deploy/cpp_infer/tools/config.txt b/deploy/cpp_infer/tools/config.txt index 1ce1568e..03661bbb 100644 --- a/deploy/cpp_infer/tools/config.txt +++ b/deploy/cpp_infer/tools/config.txt @@ -15,3 +15,7 @@ det_model_dir ./inference/det_db rec_model_dir ./inference/rec_crnn char_list_file ./tools/ppocr_keys_v1.txt img_path ../../doc/imgs/11.jpg + +# show the detection results +visualize 1 + From 9d16c1178b2e10b6ce4f60c7693ddf54271e0610 Mon Sep 17 00:00:00 2001 From: littletomatodonkey Date: Mon, 13 Jul 2020 13:05:36 +0000 Subject: [PATCH 5/7] fix cpp infer --- deploy/cpp_infer/include/ocr_det.h | 19 +- deploy/cpp_infer/include/ocr_rec.h | 13 +- deploy/cpp_infer/include/postprocess_op.h | 22 +- deploy/cpp_infer/readme.md | 39 +- deploy/cpp_infer/src/main.cpp | 3 +- deploy/cpp_infer/src/ocr_det.cpp | 16 +- deploy/cpp_infer/src/postprocess_op.cpp | 100 +- deploy/cpp_infer/tools/build.sh | 12 +- deploy/cpp_infer/tools/config.txt | 2 +- deploy/cpp_infer/tools/ppocr_keys_v1.txt | 6623 --------------------- 10 files changed, 115 insertions(+), 6734 deletions(-) delete mode 100644 deploy/cpp_infer/tools/ppocr_keys_v1.txt diff --git a/deploy/cpp_infer/include/ocr_det.h b/deploy/cpp_infer/include/ocr_det.h index 503d3ee3..611fb5c1 100644 --- a/deploy/cpp_infer/include/ocr_det.h +++ b/deploy/cpp_infer/include/ocr_det.h @@ -36,16 +36,13 @@ namespace PaddleOCR { class DBDetector { public: - explicit DBDetector(const std::string &model_dir, const bool &use_gpu = false, - const int &gpu_id = 0, const int &gpu_mem = 4000, - const int &cpu_math_library_num_threads = 4, - const int &max_side_len = 960, - const double &det_db_thresh = 0.3, - const double &det_db_box_thresh = 0.5, - const double &det_db_unclip_ratio = 2.0, - const bool &visualize = true) { - LoadModel(model_dir); - + explicit DBDetector(const std::string &model_dir, const bool &use_gpu, + const int &gpu_id, const int &gpu_mem, + const int &cpu_math_library_num_threads, + const int &max_side_len, const double &det_db_thresh, + const double &det_db_box_thresh, + const double &det_db_unclip_ratio, + const bool &visualize) { this->use_gpu_ = use_gpu; this->gpu_id_ = gpu_id; this->gpu_mem_ = gpu_mem; @@ -58,6 +55,8 @@ public: this->det_db_unclip_ratio_ = det_db_unclip_ratio; this->visualize_ = visualize; + + LoadModel(model_dir); } // Load Paddle inference model diff --git a/deploy/cpp_infer/include/ocr_rec.h b/deploy/cpp_infer/include/ocr_rec.h index dfb02f63..345f4644 100644 --- a/deploy/cpp_infer/include/ocr_rec.h +++ b/deploy/cpp_infer/include/ocr_rec.h @@ -35,19 +35,18 @@ namespace PaddleOCR { class CRNNRecognizer { public: - explicit CRNNRecognizer( - const std::string &model_dir, const bool &use_gpu = false, - const int &gpu_id = 0, const int &gpu_mem = 4000, - const int &cpu_math_library_num_threads = 4, - const string &label_path = "./tools/ppocr_keys_v1.txt") { - LoadModel(model_dir); - + explicit CRNNRecognizer(const std::string &model_dir, const bool &use_gpu, + const int &gpu_id, const int &gpu_mem, + const int &cpu_math_library_num_threads, + const string &label_path) { this->use_gpu_ = use_gpu; this->gpu_id_ = gpu_id; this->gpu_mem_ = gpu_mem; this->cpu_math_library_num_threads_ = cpu_math_library_num_threads; this->label_list_ = Utility::ReadDict(label_path); + + LoadModel(model_dir); } // Load Paddle inference model diff --git a/deploy/cpp_infer/include/postprocess_op.h b/deploy/cpp_infer/include/postprocess_op.h index de6a81b2..44ca3531 100644 --- a/deploy/cpp_infer/include/postprocess_op.h +++ b/deploy/cpp_infer/include/postprocess_op.h @@ -36,21 +36,21 @@ namespace PaddleOCR { class PostProcessor { public: - void GetContourArea(float **box, float unclip_ratio, float &distance); + void GetContourArea(const std::vector> &box, + float unclip_ratio, float &distance); - cv::RotatedRect UnClip(float **box, const float &unclip_ratio); + cv::RotatedRect UnClip(std::vector> box, + const float &unclip_ratio); float **Mat2Vec(cv::Mat mat); - void quickSort_vector(std::vector> &box, int l, int r, - int axis); - std::vector> - order_points_clockwise(std::vector> pts); + OrderPointsClockwise(std::vector> pts); - float **GetMiniBoxes(cv::RotatedRect box, float &ssid); + std::vector> GetMiniBoxes(cv::RotatedRect box, + float &ssid); - float BoxScoreFast(float **box_array, cv::Mat pred); + float BoxScoreFast(std::vector> box_array, cv::Mat pred); std::vector>> BoxesFromBitmap(const cv::Mat pred, const cv::Mat bitmap, @@ -61,7 +61,11 @@ public: float ratio_h, float ratio_w, cv::Mat srcimg); private: - void quickSort(float **s, int l, int r); + static bool XsortInt(std::vector a, std::vector b); + + static bool XsortFp32(std::vector a, std::vector b); + + std::vector> Mat2Vector(cv::Mat mat); inline int _max(int a, int b) { return a >= b ? a : b; } diff --git a/deploy/cpp_infer/readme.md b/deploy/cpp_infer/readme.md index 23612bcf..aa037791 100644 --- a/deploy/cpp_infer/readme.md +++ b/deploy/cpp_infer/readme.md @@ -22,7 +22,7 @@ tar -xf 3.4.7.tar.gz * 编译opencv,设置opencv源码路径(`root_path`)以及安装路径(`install_path`)。进入opencv源码路径下,按照下面的方式进行编译。 ```shell -root_path=/paddle/libs/opencv-3.4.7 +root_path=your_opencv_root_path install_path=${root_path}/opencv3 rm -rf build @@ -51,6 +51,9 @@ make -j make install ``` + +其中`root_path`为下载的opencv源码路径,`install_path`为opencv的安装路径,`make install`完成之后,会在该文件夹下生成opencv头文件和库文件,用于后面的OCR代码编译。 + 最终在安装路径下的文件结构如下所示。 ``` @@ -62,8 +65,29 @@ opencv3/ |-- share ``` -### 1.2 编译Paddle预测库 +### 1.2 下载或者编译Paddle预测库 +* 有2种方式获取Paddle预测库,下面进行详细介绍。 + +#### 1.2.1 直接下载安装 + +* [Paddle预测库官网](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/build_and_install_lib_cn.html)上提供了不同cuda版本的Linux预测库,可以直接下载使用。 + * 如果cuda版本为cuda9.0,1.8.2版本的Paddle预测库可以从这里下载:[下载地址](https://paddle-inference-lib.bj.bcebos.com/1.8.2-gpu-cuda9-cudnn7-avx-mkl/fluid_inference.tgz)。 + * 如果cuda版本为cuda10.0,1.8.2版本的Paddle预测库可以从这里下载:[下载地址](https://paddle-inference-lib.bj.bcebos.com/1.8.2-gpu-cuda10-cudnn7-avx-mkl/fluid_inference.tgz)。 + * 更多版本的预测库可以在官网查看下载。 + +* 下载之后使用下面的方法解压 + +``` +tar -xf fluid_inference.tgz +``` + +最终会在当前的文件夹中生成`fluid_inference/`的子文件夹。 + + + +#### 1.2.2 预测库源码编译 +* 如果希望获取最新预测库特性,可以从Paddle github上克隆最新代码,源码编译预测库。 * 可以参考[Paddle预测库官网](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/build_and_install_lib_cn.html)的说明,从github上获取Paddle代码,然后进行编译,生成最新的预测库。使用git获取代码方法如下。 ```shell @@ -80,7 +104,7 @@ cd build cmake .. \ -DWITH_CONTRIB=OFF \ -DWITH_MKL=ON \ - -DWITH_MKLDNN=OFF \ + -DWITH_MKLDNN=ON \ -DWITH_TESTING=OFF \ -DCMAKE_BUILD_TYPE=Release \ -DWITH_INFERENCE_API_TEST=OFF \ @@ -132,6 +156,15 @@ inference/ sh tools/build.sh ``` +具体地,`tools/build.sh`中内容如下。 + +```shell +c +``` + +`OPENCV_DIR`为opencv编译安装的地址;`LIB_DIR`为下载(`fluid_inference`文件夹)或者编译生成的Paddle预测库地址(`build/fluid_inference_install_dir`文件夹);`CUDA_LIB_DIR`为cuda库文件地址,在docker中;为`/usr/local/cuda/lib64`;`CUDNN_LIB_DIR`为cudnn库文件地址,在docker中为`/usr/lib/x86_64-linux-gnu/`。 + + * 编译完成之后,会在`build`文件夹下生成一个名为`ocr_system`的可执行文件。 diff --git a/deploy/cpp_infer/src/main.cpp b/deploy/cpp_infer/src/main.cpp index d5e3d4c6..3bcb6708 100644 --- a/deploy/cpp_infer/src/main.cpp +++ b/deploy/cpp_infer/src/main.cpp @@ -46,8 +46,6 @@ int main(int argc, char **argv) { std::string img_path(argv[2]); - auto start = std::chrono::system_clock::now(); - cv::Mat srcimg = cv::imread(img_path, cv::IMREAD_COLOR); DBDetector det(config.det_model_dir, config.use_gpu, config.gpu_id, @@ -59,6 +57,7 @@ int main(int argc, char **argv) { config.gpu_mem, config.cpu_math_library_num_threads, config.char_list_file); + auto start = std::chrono::system_clock::now(); std::vector>> boxes; det.Run(srcimg, boxes); diff --git a/deploy/cpp_infer/src/ocr_det.cpp b/deploy/cpp_infer/src/ocr_det.cpp index 56447b07..636d2d04 100644 --- a/deploy/cpp_infer/src/ocr_det.cpp +++ b/deploy/cpp_infer/src/ocr_det.cpp @@ -12,22 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "opencv2/core.hpp" -#include "opencv2/imgcodecs.hpp" -#include "opencv2/imgproc.hpp" -#include "paddle_api.h" -#include "paddle_inference_api.h" -#include -#include -#include -#include -#include - -#include -#include -#include - #include +#include namespace PaddleOCR { diff --git a/deploy/cpp_infer/src/postprocess_op.cpp b/deploy/cpp_infer/src/postprocess_op.cpp index 78c37a03..1df51a84 100644 --- a/deploy/cpp_infer/src/postprocess_op.cpp +++ b/deploy/cpp_infer/src/postprocess_op.cpp @@ -16,8 +16,8 @@ namespace PaddleOCR { -void PostProcessor::GetContourArea(float **box, float unclip_ratio, - float &distance) { +void PostProcessor::GetContourArea(const std::vector> &box, + float unclip_ratio, float &distance) { int pts_num = 4; float area = 0.0f; float dist = 0.0f; @@ -34,7 +34,8 @@ void PostProcessor::GetContourArea(float **box, float unclip_ratio, distance = area * unclip_ratio / dist; } -cv::RotatedRect PostProcessor::UnClip(float **box, const float &unclip_ratio) { +cv::RotatedRect PostProcessor::UnClip(std::vector> box, + const float &unclip_ratio) { float distance = 1.0; GetContourArea(box, unclip_ratio, distance); @@ -74,53 +75,11 @@ float **PostProcessor::Mat2Vec(cv::Mat mat) { return array; } -void PostProcessor::quickSort(float **s, int l, int r) { - if (l < r) { - int i = l, j = r; - float x = s[l][0]; - float *xp = s[l]; - while (i < j) { - while (i < j && s[j][0] >= x) - j--; - if (i < j) - std::swap(s[i++], s[j]); - while (i < j && s[i][0] < x) - i++; - if (i < j) - std::swap(s[j--], s[i]); - } - s[i] = xp; - quickSort(s, l, i - 1); - quickSort(s, i + 1, r); - } -} - -void PostProcessor::quickSort_vector(std::vector> &box, int l, - int r, int axis) { - if (l < r) { - int i = l, j = r; - int x = box[l][axis]; - std::vector xp(box[l]); - while (i < j) { - while (i < j && box[j][axis] >= x) - j--; - if (i < j) - std::swap(box[i++], box[j]); - while (i < j && box[i][axis] < x) - i++; - if (i < j) - std::swap(box[j--], box[i]); - } - box[i] = xp; - quickSort_vector(box, l, i - 1, axis); - quickSort_vector(box, i + 1, r, axis); - } -} - std::vector> -PostProcessor::order_points_clockwise(std::vector> pts) { +PostProcessor::OrderPointsClockwise(std::vector> pts) { std::vector> box = pts; - quickSort_vector(box, 0, int(box.size() - 1), 0); + std::sort(box.begin(), box.end(), XsortInt); + std::vector> leftmost = {box[0], box[1]}; std::vector> rightmost = {box[2], box[3]}; @@ -135,16 +94,44 @@ PostProcessor::order_points_clockwise(std::vector> pts) { return rect; } -float **PostProcessor::GetMiniBoxes(cv::RotatedRect box, float &ssid) { - ssid = box.size.width >= box.size.height ? box.size.height : box.size.width; +std::vector> PostProcessor::Mat2Vector(cv::Mat mat) { + std::vector> img_vec; + std::vector tmp; + + for (int i = 0; i < mat.rows; ++i) { + tmp.clear(); + for (int j = 0; j < mat.cols; ++j) { + tmp.push_back(mat.at(i, j)); + } + img_vec.push_back(tmp); + } + return img_vec; +} + +bool PostProcessor::XsortFp32(std::vector a, std::vector b) { + if (a[0] != b[0]) + return a[0] < b[0]; + return false; +} + +bool PostProcessor::XsortInt(std::vector a, std::vector b) { + if (a[0] != b[0]) + return a[0] < b[0]; + return false; +} + +std::vector> PostProcessor::GetMiniBoxes(cv::RotatedRect box, + float &ssid) { + ssid = std::max(box.size.width, box.size.height); cv::Mat points; cv::boxPoints(box, points); - // sorted box points - auto array = Mat2Vec(points); - quickSort(array, 0, 3); - float *idx1 = array[0], *idx2 = array[1], *idx3 = array[2], *idx4 = array[3]; + auto array = Mat2Vector(points); + std::sort(array.begin(), array.end(), XsortFp32); + + std::vector idx1 = array[0], idx2 = array[1], idx3 = array[2], + idx4 = array[3]; if (array[3][1] <= array[2][1]) { idx2 = array[3]; idx3 = array[2]; @@ -168,7 +155,8 @@ float **PostProcessor::GetMiniBoxes(cv::RotatedRect box, float &ssid) { return array; } -float PostProcessor::BoxScoreFast(float **box_array, cv::Mat pred) { +float PostProcessor::BoxScoreFast(std::vector> box_array, + cv::Mat pred) { auto array = box_array; int width = pred.cols; int height = pred.rows; @@ -280,7 +268,7 @@ PostProcessor::FilterTagDetRes(std::vector>> boxes, std::vector>> root_points; for (int n = 0; n < boxes.size(); n++) { - boxes[n] = order_points_clockwise(boxes[n]); + boxes[n] = OrderPointsClockwise(boxes[n]); for (int m = 0; m < boxes[0].size(); m++) { boxes[n][m][0] /= ratio_w; boxes[n][m][1] /= ratio_h; diff --git a/deploy/cpp_infer/tools/build.sh b/deploy/cpp_infer/tools/build.sh index 8eea3b85..d8344a23 100755 --- a/deploy/cpp_infer/tools/build.sh +++ b/deploy/cpp_infer/tools/build.sh @@ -1,9 +1,8 @@ -OPENCV_DIR=/paddle/libs/opencv-3.4.7/opencv3 -LIB_DIR=/paddle/code/gry/Paddle/build/fluid_inference_install_dir/ -CUDA_LIB_DIR=/usr/local/cuda/lib64 -CUDNN_LIB_DIR=/usr/lib/x86_64-linux-gnu/ -TENSORRT_ROOT_DIR=YOUR_TENSORRT_ROOT_DIR +OPENCV_DIR=your_opencv_dir +LIB_DIR=your_paddle_inference_dir +CUDA_LIB_DIR=your_cuda_lib_dir +CUDNN_LIB_DIR=/your_cudnn_lib_dir BUILD_DIR=build rm -rf ${BUILD_DIR} @@ -19,8 +18,5 @@ cmake .. \ -DOPENCV_DIR=${OPENCV_DIR} \ -DCUDNN_LIB=${CUDNN_LIB_DIR} \ -DCUDA_LIB=${CUDA_LIB_DIR} \ - -DTENSORRT_ROOT=YOUR_TENSORRT_ROOT_DIR make -j - - diff --git a/deploy/cpp_infer/tools/config.txt b/deploy/cpp_infer/tools/config.txt index 03661bbb..2c559163 100644 --- a/deploy/cpp_infer/tools/config.txt +++ b/deploy/cpp_infer/tools/config.txt @@ -13,7 +13,7 @@ det_model_dir ./inference/det_db # rec config rec_model_dir ./inference/rec_crnn -char_list_file ./tools/ppocr_keys_v1.txt +char_list_file ../../ppocr/utils/ppocr_keys_v1.txt img_path ../../doc/imgs/11.jpg # show the detection results diff --git a/deploy/cpp_infer/tools/ppocr_keys_v1.txt b/deploy/cpp_infer/tools/ppocr_keys_v1.txt deleted file mode 100644 index 84b885d8..00000000 --- a/deploy/cpp_infer/tools/ppocr_keys_v1.txt +++ /dev/null @@ -1,6623 +0,0 @@ -' -疗 -绚 -诚 -娇 -溜 -题 -贿 -者 -廖 -更 -纳 -加 -奉 -公 -一 -就 -汴 -计 -与 -路 -房 -原 -妇 -2 -0 -8 -- -7 -其 -> -: -] -, -, -骑 -刈 -全 -消 -昏 -傈 -安 -久 -钟 -嗅 -不 -影 -处 -驽 -蜿 -资 -关 -椤 -地 -瘸 -专 -问 -忖 -票 -嫉 -炎 -韵 -要 -月 -田 -节 -陂 -鄙 -捌 -备 -拳 -伺 -眼 -网 -盎 -大 -傍 -心 -东 -愉 -汇 -蹿 -科 -每 -业 -里 -航 -晏 -字 -平 -录 -先 -1 -3 -彤 -鲶 -产 -稍 -督 -腴 -有 -象 -岳 -注 -绍 -在 -泺 -文 -定 -核 -名 -水 -过 -理 -让 -偷 -率 -等 -这 -发 -” -为 -含 -肥 -酉 -相 -鄱 -七 -编 -猥 -锛 -日 -镀 -蒂 -掰 -倒 -辆 -栾 -栗 -综 -涩 -州 -雌 -滑 -馀 -了 -机 -块 -司 -宰 -甙 -兴 -矽 -抚 -保 -用 -沧 -秩 -如 -收 -息 -滥 -页 -疑 -埠 -! -! -姥 -异 -橹 -钇 -向 -下 -跄 -的 -椴 -沫 -国 -绥 -獠 -报 -开 -民 -蜇 -何 -分 -凇 -长 -讥 -藏 -掏 -施 -羽 -中 -讲 -派 -嘟 -人 -提 -浼 -间 -世 -而 -古 -多 -倪 -唇 -饯 -控 -庚 -首 -赛 -蜓 -味 -断 -制 -觉 -技 -替 -艰 -溢 -潮 -夕 -钺 -外 -摘 -枋 -动 -双 -单 -啮 -户 -枇 -确 -锦 -曜 -杜 -或 -能 -效 -霜 -盒 -然 -侗 -电 -晁 -放 -步 -鹃 -新 -杖 -蜂 -吒 -濂 -瞬 -评 -总 -隍 -对 -独 -合 -也 -是 -府 -青 -天 -诲 -墙 -组 -滴 -级 -邀 -帘 -示 -已 -时 -骸 -仄 -泅 -和 -遨 -店 -雇 -疫 -持 -巍 -踮 -境 -只 -亨 -目 -鉴 -崤 -闲 -体 -泄 -杂 -作 -般 -轰 -化 -解 -迂 -诿 -蛭 -璀 -腾 -告 -版 -服 -省 -师 -小 -规 -程 -线 -海 -办 -引 -二 -桧 -牌 -砺 -洄 -裴 -修 -图 -痫 -胡 -许 -犊 -事 -郛 -基 -柴 -呼 -食 -研 -奶 -律 -蛋 -因 -葆 -察 -戏 -褒 -戒 -再 -李 -骁 -工 -貂 -油 -鹅 -章 -啄 -休 -场 -给 -睡 -纷 -豆 -器 -捎 -说 -敏 -学 -会 -浒 -设 -诊 -格 -廓 -查 -来 -霓 -室 -溆 -¢ -诡 -寥 -焕 -舜 -柒 -狐 -回 -戟 -砾 -厄 -实 -翩 -尿 -五 -入 -径 -惭 -喹 -股 -宇 -篝 -| -; -美 -期 -云 -九 -祺 -扮 -靠 -锝 -槌 -系 -企 -酰 -阊 -暂 -蚕 -忻 -豁 -本 -羹 -执 -条 -钦 -H -獒 -限 -进 -季 -楦 -于 -芘 -玖 -铋 -茯 -未 -答 -粘 -括 -样 -精 -欠 -矢 -甥 -帷 -嵩 -扣 -令 -仔 -风 -皈 -行 -支 -部 -蓉 -刮 -站 -蜡 -救 -钊 -汗 -松 -嫌 -成 -可 -. -鹤 -院 -从 -交 -政 -怕 -活 -调 -球 -局 -验 -髌 -第 -韫 -谗 -串 -到 -圆 -年 -米 -/ -* -友 -忿 -检 -区 -看 -自 -敢 -刃 -个 -兹 -弄 -流 -留 -同 -没 -齿 -星 -聆 -轼 -湖 -什 -三 -建 -蛔 -儿 -椋 -汕 -震 -颧 -鲤 -跟 -力 -情 -璺 -铨 -陪 -务 -指 -族 -训 -滦 -鄣 -濮 -扒 -商 -箱 -十 -召 -慷 -辗 -所 -莞 -管 -护 -臭 -横 -硒 -嗓 -接 -侦 -六 -露 -党 -馋 -驾 -剖 -高 -侬 -妪 -幂 -猗 -绺 -骐 -央 -酐 -孝 -筝 -课 -徇 -缰 -门 -男 -西 -项 -句 -谙 -瞒 -秃 -篇 -教 -碲 -罚 -声 -呐 -景 -前 -富 -嘴 -鳌 -稀 -免 -朋 -啬 -睐 -去 -赈 -鱼 -住 -肩 -愕 -速 -旁 -波 -厅 -健 -茼 -厥 -鲟 -谅 -投 -攸 -炔 -数 -方 -击 -呋 -谈 -绩 -别 -愫 -僚 -躬 -鹧 -胪 -炳 -招 -喇 -膨 -泵 -蹦 -毛 -结 -5 -4 -谱 -识 -陕 -粽 -婚 -拟 -构 -且 -搜 -任 -潘 -比 -郢 -妨 -醪 -陀 -桔 -碘 -扎 -选 -哈 -骷 -楷 -亿 -明 -缆 -脯 -监 -睫 -逻 -婵 -共 -赴 -淝 -凡 -惦 -及 -达 -揖 -谩 -澹 -减 -焰 -蛹 -番 -祁 -柏 -员 -禄 -怡 -峤 -龙 -白 -叽 -生 -闯 -起 -细 -装 -谕 -竟 -聚 -钙 -上 -导 -渊 -按 -艾 -辘 -挡 -耒 -盹 -饪 -臀 -记 -邮 -蕙 -受 -各 -医 -搂 -普 -滇 -朗 -茸 -带 -翻 -酚 -( -光 -堤 -墟 -蔷 -万 -幻 -〓 -瑙 -辈 -昧 -盏 -亘 -蛀 -吉 -铰 -请 -子 -假 -闻 -税 -井 -诩 -哨 -嫂 -好 -面 -琐 -校 -馊 -鬣 -缂 -营 -访 -炖 -占 -农 -缀 -否 -经 -钚 -棵 -趟 -张 -亟 -吏 -茶 -谨 -捻 -论 -迸 -堂 -玉 -信 -吧 -瞠 -乡 -姬 -寺 -咬 -溏 -苄 -皿 -意 -赉 -宝 -尔 -钰 -艺 -特 -唳 -踉 -都 -荣 -倚 -登 -荐 -丧 -奇 -涵 -批 -炭 -近 -符 -傩 -感 -道 -着 -菊 -虹 -仲 -众 -懈 -濯 -颞 -眺 -南 -释 -北 -缝 -标 -既 -茗 -整 -撼 -迤 -贲 -挎 -耱 -拒 -某 -妍 -卫 -哇 -英 -矶 -藩 -治 -他 -元 -领 -膜 -遮 -穗 -蛾 -飞 -荒 -棺 -劫 -么 -市 -火 -温 -拈 -棚 -洼 -转 -果 -奕 -卸 -迪 -伸 -泳 -斗 -邡 -侄 -涨 -屯 -萋 -胭 -氡 -崮 -枞 -惧 -冒 -彩 -斜 -手 -豚 -随 -旭 -淑 -妞 -形 -菌 -吲 -沱 -争 -驯 -歹 -挟 -兆 -柱 -传 -至 -包 -内 -响 -临 -红 -功 -弩 -衡 -寂 -禁 -老 -棍 -耆 -渍 -织 -害 -氵 -渑 -布 -载 -靥 -嗬 -虽 -苹 -咨 -娄 -库 -雉 -榜 -帜 -嘲 -套 -瑚 -亲 -簸 -欧 -边 -6 -腿 -旮 -抛 -吹 -瞳 -得 -镓 -梗 -厨 -继 -漾 -愣 -憨 -士 -策 -窑 -抑 -躯 -襟 -脏 -参 -贸 -言 -干 -绸 -鳄 -穷 -藜 -音 -折 -详 -) -举 -悍 -甸 -癌 -黎 -谴 -死 -罩 -迁 -寒 -驷 -袖 -媒 -蒋 -掘 -模 -纠 -恣 -观 -祖 -蛆 -碍 -位 -稿 -主 -澧 -跌 -筏 -京 -锏 -帝 -贴 -证 -糠 -才 -黄 -鲸 -略 -炯 -饱 -四 -出 -园 -犀 -牧 -容 -汉 -杆 -浈 -汰 -瑷 -造 -虫 -瘩 -怪 -驴 -济 -应 -花 -沣 -谔 -夙 -旅 -价 -矿 -以 -考 -s -u -呦 -晒 -巡 -茅 -准 -肟 -瓴 -詹 -仟 -褂 -译 -桌 -混 -宁 -怦 -郑 -抿 -些 -余 -鄂 -饴 -攒 -珑 -群 -阖 -岔 -琨 -藓 -预 -环 -洮 -岌 -宀 -杲 -瀵 -最 -常 -囡 -周 -踊 -女 -鼓 -袭 -喉 -简 -范 -薯 -遐 -疏 -粱 -黜 -禧 -法 -箔 -斤 -遥 -汝 -奥 -直 -贞 -撑 -置 -绱 -集 -她 -馅 -逗 -钧 -橱 -魉 -[ -恙 -躁 -唤 -9 -旺 -膘 -待 -脾 -惫 -购 -吗 -依 -盲 -度 -瘿 -蠖 -俾 -之 -镗 -拇 -鲵 -厝 -簧 -续 -款 -展 -啃 -表 -剔 -品 -钻 -腭 -损 -清 -锶 -统 -涌 -寸 -滨 -贪 -链 -吠 -冈 -伎 -迥 -咏 -吁 -览 -防 -迅 -失 -汾 -阔 -逵 -绀 -蔑 -列 -川 -凭 -努 -熨 -揪 -利 -俱 -绉 -抢 -鸨 -我 -即 -责 -膦 -易 -毓 -鹊 -刹 -玷 -岿 -空 -嘞 -绊 -排 -术 -估 -锷 -违 -们 -苟 -铜 -播 -肘 -件 -烫 -审 -鲂 -广 -像 -铌 -惰 -铟 -巳 -胍 -鲍 -康 -憧 -色 -恢 -想 -拷 -尤 -疳 -知 -S -Y -F -D -A -峄 -裕 -帮 -握 -搔 -氐 -氘 -难 -墒 -沮 -雨 -叁 -缥 -悴 -藐 -湫 -娟 -苑 -稠 -颛 -簇 -后 -阕 -闭 -蕤 -缚 -怎 -佞 -码 -嘤 -蔡 -痊 -舱 -螯 -帕 -赫 -昵 -升 -烬 -岫 -、 -疵 -蜻 -髁 -蕨 -隶 -烛 -械 -丑 -盂 -梁 -强 -鲛 -由 -拘 -揉 -劭 -龟 -撤 -钩 -呕 -孛 -费 -妻 -漂 -求 -阑 -崖 -秤 -甘 -通 -深 -补 -赃 -坎 -床 -啪 -承 -吼 -量 -暇 -钼 -烨 -阂 -擎 -脱 -逮 -称 -P -神 -属 -矗 -华 -届 -狍 -葑 -汹 -育 -患 -窒 -蛰 -佼 -静 -槎 -运 -鳗 -庆 -逝 -曼 -疱 -克 -代 -官 -此 -麸 -耧 -蚌 -晟 -例 -础 -榛 -副 -测 -唰 -缢 -迹 -灬 -霁 -身 -岁 -赭 -扛 -又 -菡 -乜 -雾 -板 -读 -陷 -徉 -贯 -郁 -虑 -变 -钓 -菜 -圾 -现 -琢 -式 -乐 -维 -渔 -浜 -左 -吾 -脑 -钡 -警 -T -啵 -拴 -偌 -漱 -湿 -硕 -止 -骼 -魄 -积 -燥 -联 -踢 -玛 -则 -窿 -见 -振 -畿 -送 -班 -钽 -您 -赵 -刨 -印 -讨 -踝 -籍 -谡 -舌 -崧 -汽 -蔽 -沪 -酥 -绒 -怖 -财 -帖 -肱 -私 -莎 -勋 -羔 -霸 -励 -哼 -帐 -将 -帅 -渠 -纪 -婴 -娩 -岭 -厘 -滕 -吻 -伤 -坝 -冠 -戊 -隆 -瘁 -介 -涧 -物 -黍 -并 -姗 -奢 -蹑 -掣 -垸 -锴 -命 -箍 -捉 -病 -辖 -琰 -眭 -迩 -艘 -绌 -繁 -寅 -若 -毋 -思 -诉 -类 -诈 -燮 -轲 -酮 -狂 -重 -反 -职 -筱 -县 -委 -磕 -绣 -奖 -晋 -濉 -志 -徽 -肠 -呈 -獐 -坻 -口 -片 -碰 -几 -村 -柿 -劳 -料 -获 -亩 -惕 -晕 -厌 -号 -罢 -池 -正 -鏖 -煨 -家 -棕 -复 -尝 -懋 -蜥 -锅 -岛 -扰 -队 -坠 -瘾 -钬 -@ -卧 -疣 -镇 -譬 -冰 -彷 -频 -黯 -据 -垄 -采 -八 -缪 -瘫 -型 -熹 -砰 -楠 -襁 -箐 -但 -嘶 -绳 -啤 -拍 -盥 -穆 -傲 -洗 -盯 -塘 -怔 -筛 -丿 -台 -恒 -喂 -葛 -永 -¥ -烟 -酒 -桦 -书 -砂 -蚝 -缉 -态 -瀚 -袄 -圳 -轻 -蛛 -超 -榧 -遛 -姒 -奘 -铮 -右 -荽 -望 -偻 -卡 -丶 -氰 -附 -做 -革 -索 -戚 -坨 -桷 -唁 -垅 -榻 -岐 -偎 -坛 -莨 -山 -殊 -微 -骇 -陈 -爨 -推 -嗝 -驹 -澡 -藁 -呤 -卤 -嘻 -糅 -逛 -侵 -郓 -酌 -德 -摇 -※ -鬃 -被 -慨 -殡 -羸 -昌 -泡 -戛 -鞋 -河 -宪 -沿 -玲 -鲨 -翅 -哽 -源 -铅 -语 -照 -邯 -址 -荃 -佬 -顺 -鸳 -町 -霭 -睾 -瓢 -夸 -椁 -晓 -酿 -痈 -咔 -侏 -券 -噎 -湍 -签 -嚷 -离 -午 -尚 -社 -锤 -背 -孟 -使 -浪 -缦 -潍 -鞅 -军 -姹 -驶 -笑 -鳟 -鲁 -》 -孽 -钜 -绿 -洱 -礴 -焯 -椰 -颖 -囔 -乌 -孔 -巴 -互 -性 -椽 -哞 -聘 -昨 -早 -暮 -胶 -炀 -隧 -低 -彗 -昝 -铁 -呓 -氽 -藉 -喔 -癖 -瑗 -姨 -权 -胱 -韦 -堑 -蜜 -酋 -楝 -砝 -毁 -靓 -歙 -锲 -究 -屋 -喳 -骨 -辨 -碑 -武 -鸠 -宫 -辜 -烊 -适 -坡 -殃 -培 -佩 -供 -走 -蜈 -迟 -翼 -况 -姣 -凛 -浔 -吃 -飘 -债 -犟 -金 -促 -苛 -崇 -坂 -莳 -畔 -绂 -兵 -蠕 -斋 -根 -砍 -亢 -欢 -恬 -崔 -剁 -餐 -榫 -快 -扶 -‖ -濒 -缠 -鳜 -当 -彭 -驭 -浦 -篮 -昀 -锆 -秸 -钳 -弋 -娣 -瞑 -夷 -龛 -苫 -拱 -致 -% -嵊 -障 -隐 -弑 -初 -娓 -抉 -汩 -累 -蓖 -" -唬 -助 -苓 -昙 -押 -毙 -破 -城 -郧 -逢 -嚏 -獭 -瞻 -溱 -婿 -赊 -跨 -恼 -璧 -萃 -姻 -貉 -灵 -炉 -密 -氛 -陶 -砸 -谬 -衔 -点 -琛 -沛 -枳 -层 -岱 -诺 -脍 -榈 -埂 -征 -冷 -裁 -打 -蹴 -素 -瘘 -逞 -蛐 -聊 -激 -腱 -萘 -踵 -飒 -蓟 -吆 -取 -咙 -簋 -涓 -矩 -曝 -挺 -揣 -座 -你 -史 -舵 -焱 -尘 -苏 -笈 -脚 -溉 -榨 -诵 -樊 -邓 -焊 -义 -庶 -儋 -蟋 -蒲 -赦 -呷 -杞 -诠 -豪 -还 -试 -颓 -茉 -太 -除 -紫 -逃 -痴 -草 -充 -鳕 -珉 -祗 -墨 -渭 -烩 -蘸 -慕 -璇 -镶 -穴 -嵘 -恶 -骂 -险 -绋 -幕 -碉 -肺 -戳 -刘 -潞 -秣 -纾 -潜 -銮 -洛 -须 -罘 -销 -瘪 -汞 -兮 -屉 -r -林 -厕 -质 -探 -划 -狸 -殚 -善 -煊 -烹 -〒 -锈 -逯 -宸 -辍 -泱 -柚 -袍 -远 -蹋 -嶙 -绝 -峥 -娥 -缍 -雀 -徵 -认 -镱 -谷 -= -贩 -勉 -撩 -鄯 -斐 -洋 -非 -祚 -泾 -诒 -饿 -撬 -威 -晷 -搭 -芍 -锥 -笺 -蓦 -候 -琊 -档 -礁 -沼 -卵 -荠 -忑 -朝 -凹 -瑞 -头 -仪 -弧 -孵 -畏 -铆 -突 -衲 -车 -浩 -气 -茂 -悖 -厢 -枕 -酝 -戴 -湾 -邹 -飚 -攘 -锂 -写 -宵 -翁 -岷 -无 -喜 -丈 -挑 -嗟 -绛 -殉 -议 -槽 -具 -醇 -淞 -笃 -郴 -阅 -饼 -底 -壕 -砚 -弈 -询 -缕 -庹 -翟 -零 -筷 -暨 -舟 -闺 -甯 -撞 -麂 -茌 -蔼 -很 -珲 -捕 -棠 -角 -阉 -媛 -娲 -诽 -剿 -尉 -爵 -睬 -韩 -诰 -匣 -危 -糍 -镯 -立 -浏 -阳 -少 -盆 -舔 -擘 -匪 -申 -尬 -铣 -旯 -抖 -赘 -瓯 -居 -ˇ -哮 -游 -锭 -茏 -歌 -坏 -甚 -秒 -舞 -沙 -仗 -劲 -潺 -阿 -燧 -郭 -嗖 -霏 -忠 -材 -奂 -耐 -跺 -砀 -输 -岖 -媳 -氟 -极 -摆 -灿 -今 -扔 -腻 -枝 -奎 -药 -熄 -吨 -话 -q -额 -慑 -嘌 -协 -喀 -壳 -埭 -视 -著 -於 -愧 -陲 -翌 -峁 -颅 -佛 -腹 -聋 -侯 -咎 -叟 -秀 -颇 -存 -较 -罪 -哄 -岗 -扫 -栏 -钾 -羌 -己 -璨 -枭 -霉 -煌 -涸 -衿 -键 -镝 -益 -岢 -奏 -连 -夯 -睿 -冥 -均 -糖 -狞 -蹊 -稻 -爸 -刿 -胥 -煜 -丽 -肿 -璃 -掸 -跚 -灾 -垂 -樾 -濑 -乎 -莲 -窄 -犹 -撮 -战 -馄 -软 -络 -显 -鸢 -胸 -宾 -妲 -恕 -埔 -蝌 -份 -遇 -巧 -瞟 -粒 -恰 -剥 -桡 -博 -讯 -凯 -堇 -阶 -滤 -卖 -斌 -骚 -彬 -兑 -磺 -樱 -舷 -两 -娱 -福 -仃 -差 -找 -桁 -÷ -净 -把 -阴 -污 -戬 -雷 -碓 -蕲 -楚 -罡 -焖 -抽 -妫 -咒 -仑 -闱 -尽 -邑 -菁 -爱 -贷 -沥 -鞑 -牡 -嗉 -崴 -骤 -塌 -嗦 -订 -拮 -滓 -捡 -锻 -次 -坪 -杩 -臃 -箬 -融 -珂 -鹗 -宗 -枚 -降 -鸬 -妯 -阄 -堰 -盐 -毅 -必 -杨 -崃 -俺 -甬 -状 -莘 -货 -耸 -菱 -腼 -铸 -唏 -痤 -孚 -澳 -懒 -溅 -翘 -疙 -杷 -淼 -缙 -骰 -喊 -悉 -砻 -坷 -艇 -赁 -界 -谤 -纣 -宴 -晃 -茹 -归 -饭 -梢 -铡 -街 -抄 -肼 -鬟 -苯 -颂 -撷 -戈 -炒 -咆 -茭 -瘙 -负 -仰 -客 -琉 -铢 -封 -卑 -珥 -椿 -镧 -窨 -鬲 -寿 -御 -袤 -铃 -萎 -砖 -餮 -脒 -裳 -肪 -孕 -嫣 -馗 -嵇 -恳 -氯 -江 -石 -褶 -冢 -祸 -阻 -狈 -羞 -银 -靳 -透 -咳 -叼 -敷 -芷 -啥 -它 -瓤 -兰 -痘 -懊 -逑 -肌 -往 -捺 -坊 -甩 -呻 -〃 -沦 -忘 -膻 -祟 -菅 -剧 -崆 -智 -坯 -臧 -霍 -墅 -攻 -眯 -倘 -拢 -骠 -铐 -庭 -岙 -瓠 -′ -缺 -泥 -迢 -捶 -? -? -郏 -喙 -掷 -沌 -纯 -秘 -种 -听 -绘 -固 -螨 -团 -香 -盗 -妒 -埚 -蓝 -拖 -旱 -荞 -铀 -血 -遏 -汲 -辰 -叩 -拽 -幅 -硬 -惶 -桀 -漠 -措 -泼 -唑 -齐 -肾 -念 -酱 -虚 -屁 -耶 -旗 -砦 -闵 -婉 -馆 -拭 -绅 -韧 -忏 -窝 -醋 -葺 -顾 -辞 -倜 -堆 -辋 -逆 -玟 -贱 -疾 -董 -惘 -倌 -锕 -淘 -嘀 -莽 -俭 -笏 -绑 -鲷 -杈 -择 -蟀 -粥 -嗯 -驰 -逾 -案 -谪 -褓 -胫 -哩 -昕 -颚 -鲢 -绠 -躺 -鹄 -崂 -儒 -俨 -丝 -尕 -泌 -啊 -萸 -彰 -幺 -吟 -骄 -苣 -弦 -脊 -瑰 -〈 -诛 -镁 -析 -闪 -剪 -侧 -哟 -框 -螃 -守 -嬗 -燕 -狭 -铈 -缮 -概 -迳 -痧 -鲲 -俯 -售 -笼 -痣 -扉 -挖 -满 -咋 -援 -邱 -扇 -歪 -便 -玑 -绦 -峡 -蛇 -叨 -〖 -泽 -胃 -斓 -喋 -怂 -坟 -猪 -该 -蚬 -炕 -弥 -赞 -棣 -晔 -娠 -挲 -狡 -创 -疖 -铕 -镭 -稷 -挫 -弭 -啾 -翔 -粉 -履 -苘 -哦 -楼 -秕 -铂 -土 -锣 -瘟 -挣 -栉 -习 -享 -桢 -袅 -磨 -桂 -谦 -延 -坚 -蔚 -噗 -署 -谟 -猬 -钎 -恐 -嬉 -雒 -倦 -衅 -亏 -璩 -睹 -刻 -殿 -王 -算 -雕 -麻 -丘 -柯 -骆 -丸 -塍 -谚 -添 -鲈 -垓 -桎 -蚯 -芥 -予 -飕 -镦 -谌 -窗 -醚 -菀 -亮 -搪 -莺 -蒿 -羁 -足 -J -真 -轶 -悬 -衷 -靛 -翊 -掩 -哒 -炅 -掐 -冼 -妮 -l -谐 -稚 -荆 -擒 -犯 -陵 -虏 -浓 -崽 -刍 -陌 -傻 -孜 -千 -靖 -演 -矜 -钕 -煽 -杰 -酗 -渗 -伞 -栋 -俗 -泫 -戍 -罕 -沾 -疽 -灏 -煦 -芬 -磴 -叱 -阱 -榉 -湃 -蜀 -叉 -醒 -彪 -租 -郡 -篷 -屎 -良 -垢 -隗 -弱 -陨 -峪 -砷 -掴 -颁 -胎 -雯 -绵 -贬 -沐 -撵 -隘 -篙 -暖 -曹 -陡 -栓 -填 -臼 -彦 -瓶 -琪 -潼 -哪 -鸡 -摩 -啦 -俟 -锋 -域 -耻 -蔫 -疯 -纹 -撇 -毒 -绶 -痛 -酯 -忍 -爪 -赳 -歆 -嘹 -辕 -烈 -册 -朴 -钱 -吮 -毯 -癜 -娃 -谀 -邵 -厮 -炽 -璞 -邃 -丐 -追 -词 -瓒 -忆 -轧 -芫 -谯 -喷 -弟 -半 -冕 -裙 -掖 -墉 -绮 -寝 -苔 -势 -顷 -褥 -切 -衮 -君 -佳 -嫒 -蚩 -霞 -佚 -洙 -逊 -镖 -暹 -唛 -& -殒 -顶 -碗 -獗 -轭 -铺 -蛊 -废 -恹 -汨 -崩 -珍 -那 -杵 -曲 -纺 -夏 -薰 -傀 -闳 -淬 -姘 -舀 -拧 -卷 -楂 -恍 -讪 -厩 -寮 -篪 -赓 -乘 -灭 -盅 -鞣 -沟 -慎 -挂 -饺 -鼾 -杳 -树 -缨 -丛 -絮 -娌 -臻 -嗳 -篡 -侩 -述 -衰 -矛 -圈 -蚜 -匕 -筹 -匿 -濞 -晨 -叶 -骋 -郝 -挚 -蚴 -滞 -增 -侍 -描 -瓣 -吖 -嫦 -蟒 -匾 -圣 -赌 -毡 -癞 -恺 -百 -曳 -需 -篓 -肮 -庖 -帏 -卿 -驿 -遗 -蹬 -鬓 -骡 -歉 -芎 -胳 -屐 -禽 -烦 -晌 -寄 -媾 -狄 -翡 -苒 -船 -廉 -终 -痞 -殇 -々 -畦 -饶 -改 -拆 -悻 -萄 -£ -瓿 -乃 -訾 -桅 -匮 -溧 -拥 -纱 -铍 -骗 -蕃 -龋 -缬 -父 -佐 -疚 -栎 -醍 -掳 -蓄 -x -惆 -颜 -鲆 -榆 -〔 -猎 -敌 -暴 -谥 -鲫 -贾 -罗 -玻 -缄 -扦 -芪 -癣 -落 -徒 -臾 -恿 -猩 -托 -邴 -肄 -牵 -春 -陛 -耀 -刊 -拓 -蓓 -邳 -堕 -寇 -枉 -淌 -啡 -湄 -兽 -酷 -萼 -碚 -濠 -萤 -夹 -旬 -戮 -梭 -琥 -椭 -昔 -勺 -蜊 -绐 -晚 -孺 -僵 -宣 -摄 -冽 -旨 -萌 -忙 -蚤 -眉 -噼 -蟑 -付 -契 -瓜 -悼 -颡 -壁 -曾 -窕 -颢 -澎 -仿 -俑 -浑 -嵌 -浣 -乍 -碌 -褪 -乱 -蔟 -隙 -玩 -剐 -葫 -箫 -纲 -围 -伐 -决 -伙 -漩 -瑟 -刑 -肓 -镳 -缓 -蹭 -氨 -皓 -典 -畲 -坍 -铑 -檐 -塑 -洞 -倬 -储 -胴 -淳 -戾 -吐 -灼 -惺 -妙 -毕 -珐 -缈 -虱 -盖 -羰 -鸿 -磅 -谓 -髅 -娴 -苴 -唷 -蚣 -霹 -抨 -贤 -唠 -犬 -誓 -逍 -庠 -逼 -麓 -籼 -釉 -呜 -碧 -秧 -氩 -摔 -霄 -穸 -纨 -辟 -妈 -映 -完 -牛 -缴 -嗷 -炊 -恩 -荔 -茆 -掉 -紊 -慌 -莓 -羟 -阙 -萁 -磐 -另 -蕹 -辱 -鳐 -湮 -吡 -吩 -唐 -睦 -垠 -舒 -圜 -冗 -瞿 -溺 -芾 -囱 -匠 -僳 -汐 -菩 -饬 -漓 -黑 -霰 -浸 -濡 -窥 -毂 -蒡 -兢 -驻 -鹉 -芮 -诙 -迫 -雳 -厂 -忐 -臆 -猴 -鸣 -蚪 -栈 -箕 -羡 -渐 -莆 -捍 -眈 -哓 -趴 -蹼 -埕 -嚣 -骛 -宏 -淄 -斑 -噜 -严 -瑛 -垃 -椎 -诱 -压 -庾 -绞 -焘 -廿 -抡 -迄 -棘 -夫 -纬 -锹 -眨 -瞌 -侠 -脐 -竞 -瀑 -孳 -骧 -遁 -姜 -颦 -荪 -滚 -萦 -伪 -逸 -粳 -爬 -锁 -矣 -役 -趣 -洒 -颔 -诏 -逐 -奸 -甭 -惠 -攀 -蹄 -泛 -尼 -拼 -阮 -鹰 -亚 -颈 -惑 -勒 -〉 -际 -肛 -爷 -刚 -钨 -丰 -养 -冶 -鲽 -辉 -蔻 -画 -覆 -皴 -妊 -麦 -返 -醉 -皂 -擀 -〗 -酶 -凑 -粹 -悟 -诀 -硖 -港 -卜 -z -杀 -涕 -± -舍 -铠 -抵 -弛 -段 -敝 -镐 -奠 -拂 -轴 -跛 -袱 -e -t -沉 -菇 -俎 -薪 -峦 -秭 -蟹 -历 -盟 -菠 -寡 -液 -肢 -喻 -染 -裱 -悱 -抱 -氙 -赤 -捅 -猛 -跑 -氮 -谣 -仁 -尺 -辊 -窍 -烙 -衍 -架 -擦 -倏 -璐 -瑁 -币 -楞 -胖 -夔 -趸 -邛 -惴 -饕 -虔 -蝎 -§ -哉 -贝 -宽 -辫 -炮 -扩 -饲 -籽 -魏 -菟 -锰 -伍 -猝 -末 -琳 -哚 -蛎 -邂 -呀 -姿 -鄞 -却 -歧 -仙 -恸 -椐 -森 -牒 -寤 -袒 -婆 -虢 -雅 -钉 -朵 -贼 -欲 -苞 -寰 -故 -龚 -坭 -嘘 -咫 -礼 -硷 -兀 -睢 -汶 -’ -铲 -烧 -绕 -诃 -浃 -钿 -哺 -柜 -讼 -颊 -璁 -腔 -洽 -咐 -脲 -簌 -筠 -镣 -玮 -鞠 -谁 -兼 -姆 -挥 -梯 -蝴 -谘 -漕 -刷 -躏 -宦 -弼 -b -垌 -劈 -麟 -莉 -揭 -笙 -渎 -仕 -嗤 -仓 -配 -怏 -抬 -错 -泯 -镊 -孰 -猿 -邪 -仍 -秋 -鼬 -壹 -歇 -吵 -炼 -< -尧 -射 -柬 -廷 -胧 -霾 -凳 -隋 -肚 -浮 -梦 -祥 -株 -堵 -退 -L -鹫 -跎 -凶 -毽 -荟 -炫 -栩 -玳 -甜 -沂 -鹿 -顽 -伯 -爹 -赔 -蛴 -徐 -匡 -欣 -狰 -缸 -雹 -蟆 -疤 -默 -沤 -啜 -痂 -衣 -禅 -w -i -h -辽 -葳 -黝 -钗 -停 -沽 -棒 -馨 -颌 -肉 -吴 -硫 -悯 -劾 -娈 -马 -啧 -吊 -悌 -镑 -峭 -帆 -瀣 -涉 -咸 -疸 -滋 -泣 -翦 -拙 -癸 -钥 -蜒 -+ -尾 -庄 -凝 -泉 -婢 -渴 -谊 -乞 -陆 -锉 -糊 -鸦 -淮 -I -B -N -晦 -弗 -乔 -庥 -葡 -尻 -席 -橡 -傣 -渣 -拿 -惩 -麋 -斛 -缃 -矮 -蛏 -岘 -鸽 -姐 -膏 -催 -奔 -镒 -喱 -蠡 -摧 -钯 -胤 -柠 -拐 -璋 -鸥 -卢 -荡 -倾 -^ -_ -珀 -逄 -萧 -塾 -掇 -贮 -笆 -聂 -圃 -冲 -嵬 -M -滔 -笕 -值 -炙 -偶 -蜱 -搐 -梆 -汪 -蔬 -腑 -鸯 -蹇 -敞 -绯 -仨 -祯 -谆 -梧 -糗 -鑫 -啸 -豺 -囹 -猾 -巢 -柄 -瀛 -筑 -踌 -沭 -暗 -苁 -鱿 -蹉 -脂 -蘖 -牢 -热 -木 -吸 -溃 -宠 -序 -泞 -偿 -拜 -檩 -厚 -朐 -毗 -螳 -吞 -媚 -朽 -担 -蝗 -橘 -畴 -祈 -糟 -盱 -隼 -郜 -惜 -珠 -裨 -铵 -焙 -琚 -唯 -咚 -噪 -骊 -丫 -滢 -勤 -棉 -呸 -咣 -淀 -隔 -蕾 -窈 -饨 -挨 -煅 -短 -匙 -粕 -镜 -赣 -撕 -墩 -酬 -馁 -豌 -颐 -抗 -酣 -氓 -佑 -搁 -哭 -递 -耷 -涡 -桃 -贻 -碣 -截 -瘦 -昭 -镌 -蔓 -氚 -甲 -猕 -蕴 -蓬 -散 -拾 -纛 -狼 -猷 -铎 -埋 -旖 -矾 -讳 -囊 -糜 -迈 -粟 -蚂 -紧 -鲳 -瘢 -栽 -稼 -羊 -锄 -斟 -睁 -桥 -瓮 -蹙 -祉 -醺 -鼻 -昱 -剃 -跳 -篱 -跷 -蒜 -翎 -宅 -晖 -嗑 -壑 -峻 -癫 -屏 -狠 -陋 -袜 -途 -憎 -祀 -莹 -滟 -佶 -溥 -臣 -约 -盛 -峰 -磁 -慵 -婪 -拦 -莅 -朕 -鹦 -粲 -裤 -哎 -疡 -嫖 -琵 -窟 -堪 -谛 -嘉 -儡 -鳝 -斩 -郾 -驸 -酊 -妄 -胜 -贺 -徙 -傅 -噌 -钢 -栅 -庇 -恋 -匝 -巯 -邈 -尸 -锚 -粗 -佟 -蛟 -薹 -纵 -蚊 -郅 -绢 -锐 -苗 -俞 -篆 -淆 -膀 -鲜 -煎 -诶 -秽 -寻 -涮 -刺 -怀 -噶 -巨 -褰 -魅 -灶 -灌 -桉 -藕 -谜 -舸 -薄 -搀 -恽 -借 -牯 -痉 -渥 -愿 -亓 -耘 -杠 -柩 -锔 -蚶 -钣 -珈 -喘 -蹒 -幽 -赐 -稗 -晤 -莱 -泔 -扯 -肯 -菪 -裆 -腩 -豉 -疆 -骜 -腐 -倭 -珏 -唔 -粮 -亡 -润 -慰 -伽 -橄 -玄 -誉 -醐 -胆 -龊 -粼 -塬 -陇 -彼 -削 -嗣 -绾 -芽 -妗 -垭 -瘴 -爽 -薏 -寨 -龈 -泠 -弹 -赢 -漪 -猫 -嘧 -涂 -恤 -圭 -茧 -烽 -屑 -痕 -巾 -赖 -荸 -凰 -腮 -畈 -亵 -蹲 -偃 -苇 -澜 -艮 -换 -骺 -烘 -苕 -梓 -颉 -肇 -哗 -悄 -氤 -涠 -葬 -屠 -鹭 -植 -竺 -佯 -诣 -鲇 -瘀 -鲅 -邦 -移 -滁 -冯 -耕 -癔 -戌 -茬 -沁 -巩 -悠 -湘 -洪 -痹 -锟 -循 -谋 -腕 -鳃 -钠 -捞 -焉 -迎 -碱 -伫 -急 -榷 -奈 -邝 -卯 -辄 -皲 -卟 -醛 -畹 -忧 -稳 -雄 -昼 -缩 -阈 -睑 -扌 -耗 -曦 -涅 -捏 -瞧 -邕 -淖 -漉 -铝 -耦 -禹 -湛 -喽 -莼 -琅 -诸 -苎 -纂 -硅 -始 -嗨 -傥 -燃 -臂 -赅 -嘈 -呆 -贵 -屹 -壮 -肋 -亍 -蚀 -卅 -豹 -腆 -邬 -迭 -浊 -} -童 -螂 -捐 -圩 -勐 -触 -寞 -汊 -壤 -荫 -膺 -渌 -芳 -懿 -遴 -螈 -泰 -蓼 -蛤 -茜 -舅 -枫 -朔 -膝 -眙 -避 -梅 -判 -鹜 -璜 -牍 -缅 -垫 -藻 -黔 -侥 -惚 -懂 -踩 -腰 -腈 -札 -丞 -唾 -慈 -顿 -摹 -荻 -琬 -~ -斧 -沈 -滂 -胁 -胀 -幄 -莜 -Z -匀 -鄄 -掌 -绰 -茎 -焚 -赋 -萱 -谑 -汁 -铒 -瞎 -夺 -蜗 -野 -娆 -冀 -弯 -篁 -懵 -灞 -隽 -芡 -脘 -俐 -辩 -芯 -掺 -喏 -膈 -蝈 -觐 -悚 -踹 -蔗 -熠 -鼠 -呵 -抓 -橼 -峨 -畜 -缔 -禾 -崭 -弃 -熊 -摒 -凸 -拗 -穹 -蒙 -抒 -祛 -劝 -闫 -扳 -阵 -醌 -踪 -喵 -侣 -搬 -仅 -荧 -赎 -蝾 -琦 -买 -婧 -瞄 -寓 -皎 -冻 -赝 -箩 -莫 -瞰 -郊 -笫 -姝 -筒 -枪 -遣 -煸 -袋 -舆 -痱 -涛 -母 -〇 -启 -践 -耙 -绲 -盘 -遂 -昊 -搞 -槿 -诬 -纰 -泓 -惨 -檬 -亻 -越 -C -o -憩 -熵 -祷 -钒 -暧 -塔 -阗 -胰 -咄 -娶 -魔 -琶 -钞 -邻 -扬 -杉 -殴 -咽 -弓 -〆 -髻 -】 -吭 -揽 -霆 -拄 -殖 -脆 -彻 -岩 -芝 -勃 -辣 -剌 -钝 -嘎 -甄 -佘 -皖 -伦 -授 -徕 -憔 -挪 -皇 -庞 -稔 -芜 -踏 -溴 -兖 -卒 -擢 -饥 -鳞 -煲 -‰ -账 -颗 -叻 -斯 -捧 -鳍 -琮 -讹 -蛙 -纽 -谭 -酸 -兔 -莒 -睇 -伟 -觑 -羲 -嗜 -宜 -褐 -旎 -辛 -卦 -诘 -筋 -鎏 -溪 -挛 -熔 -阜 -晰 -鳅 -丢 -奚 -灸 -呱 -献 -陉 -黛 -鸪 -甾 -萨 -疮 -拯 -洲 -疹 -辑 -叙 -恻 -谒 -允 -柔 -烂 -氏 -逅 -漆 -拎 -惋 -扈 -湟 -纭 -啕 -掬 -擞 -哥 -忽 -涤 -鸵 -靡 -郗 -瓷 -扁 -廊 -怨 -雏 -钮 -敦 -E -懦 -憋 -汀 -拚 -啉 -腌 -岸 -f -痼 -瞅 -尊 -咀 -眩 -飙 -忌 -仝 -迦 -熬 -毫 -胯 -篑 -茄 -腺 -凄 -舛 -碴 -锵 -诧 -羯 -後 -漏 -汤 -宓 -仞 -蚁 -壶 -谰 -皑 -铄 -棰 -罔 -辅 -晶 -苦 -牟 -闽 -\ -烃 -饮 -聿 -丙 -蛳 -朱 -煤 -涔 -鳖 -犁 -罐 -荼 -砒 -淦 -妤 -黏 -戎 -孑 -婕 -瑾 -戢 -钵 -枣 -捋 -砥 -衩 -狙 -桠 -稣 -阎 -肃 -梏 -诫 -孪 -昶 -婊 -衫 -嗔 -侃 -塞 -蜃 -樵 -峒 -貌 -屿 -欺 -缫 -阐 -栖 -诟 -珞 -荭 -吝 -萍 -嗽 -恂 -啻 -蜴 -磬 -峋 -俸 -豫 -谎 -徊 -镍 -韬 -魇 -晴 -U -囟 -猜 -蛮 -坐 -囿 -伴 -亭 -肝 -佗 -蝠 -妃 -胞 -滩 -榴 -氖 -垩 -苋 -砣 -扪 -馏 -姓 -轩 -厉 -夥 -侈 -禀 -垒 -岑 -赏 -钛 -辐 -痔 -披 -纸 -碳 -“ -坞 -蠓 -挤 -荥 -沅 -悔 -铧 -帼 -蒌 -蝇 -a -p -y -n -g -哀 -浆 -瑶 -凿 -桶 -馈 -皮 -奴 -苜 -佤 -伶 -晗 -铱 -炬 -优 -弊 -氢 -恃 -甫 -攥 -端 -锌 -灰 -稹 -炝 -曙 -邋 -亥 -眶 -碾 -拉 -萝 -绔 -捷 -浍 -腋 -姑 -菖 -凌 -涞 -麽 -锢 -桨 -潢 -绎 -镰 -殆 -锑 -渝 -铬 -困 -绽 -觎 -匈 -糙 -暑 -裹 -鸟 -盔 -肽 -迷 -綦 -『 -亳 -佝 -俘 -钴 -觇 -骥 -仆 -疝 -跪 -婶 -郯 -瀹 -唉 -脖 -踞 -针 -晾 -忒 -扼 -瞩 -叛 -椒 -疟 -嗡 -邗 -肆 -跆 -玫 -忡 -捣 -咧 -唆 -艄 -蘑 -潦 -笛 -阚 -沸 -泻 -掊 -菽 -贫 -斥 -髂 -孢 -镂 -赂 -麝 -鸾 -屡 -衬 -苷 -恪 -叠 -希 -粤 -爻 -喝 -茫 -惬 -郸 -绻 -庸 -撅 -碟 -宄 -妹 -膛 -叮 -饵 -崛 -嗲 -椅 -冤 -搅 -咕 -敛 -尹 -垦 -闷 -蝉 -霎 -勰 -败 -蓑 -泸 -肤 -鹌 -幌 -焦 -浠 -鞍 -刁 -舰 -乙 -竿 -裔 -。 -茵 -函 -伊 -兄 -丨 -娜 -匍 -謇 -莪 -宥 -似 -蝽 -翳 -酪 -翠 -粑 -薇 -祢 -骏 -赠 -叫 -Q -噤 -噻 -竖 -芗 -莠 -潭 -俊 -羿 -耜 -O -郫 -趁 -嗪 -囚 -蹶 -芒 -洁 -笋 -鹑 -敲 -硝 -啶 -堡 -渲 -揩 -』 -携 -宿 -遒 -颍 -扭 -棱 -割 -萜 -蔸 -葵 -琴 -捂 -饰 -衙 -耿 -掠 -募 -岂 -窖 -涟 -蔺 -瘤 -柞 -瞪 -怜 -匹 -距 -楔 -炜 -哆 -秦 -缎 -幼 -茁 -绪 -痨 -恨 -楸 -娅 -瓦 -桩 -雪 -嬴 -伏 -榔 -妥 -铿 -拌 -眠 -雍 -缇 -‘ -卓 -搓 -哌 -觞 -噩 -屈 -哧 -髓 -咦 -巅 -娑 -侑 -淫 -膳 -祝 -勾 -姊 -莴 -胄 -疃 -薛 -蜷 -胛 -巷 -芙 -芋 -熙 -闰 -勿 -窃 -狱 -剩 -钏 -幢 -陟 -铛 -慧 -靴 -耍 -k -浙 -浇 -飨 -惟 -绗 -祜 -澈 -啼 -咪 -磷 -摞 -诅 -郦 -抹 -跃 -壬 -吕 -肖 -琏 -颤 -尴 -剡 -抠 -凋 -赚 -泊 -津 -宕 -殷 -倔 -氲 -漫 -邺 -涎 -怠 -$ -垮 -荬 -遵 -俏 -叹 -噢 -饽 -蜘 -孙 -筵 -疼 -鞭 -羧 -牦 -箭 -潴 -c -眸 -祭 -髯 -啖 -坳 -愁 -芩 -驮 -倡 -巽 -穰 -沃 -胚 -怒 -凤 -槛 -剂 -趵 -嫁 -v -邢 -灯 -鄢 -桐 -睽 -檗 -锯 -槟 -婷 -嵋 -圻 -诗 -蕈 -颠 -遭 -痢 -芸 -怯 -馥 -竭 -锗 -徜 -恭 -遍 -籁 -剑 -嘱 -苡 -龄 -僧 -桑 -潸 -弘 -澶 -楹 -悲 -讫 -愤 -腥 -悸 -谍 -椹 -呢 -桓 -葭 -攫 -阀 -翰 -躲 -敖 -柑 -郎 -笨 -橇 -呃 -魁 -燎 -脓 -葩 -磋 -垛 -玺 -狮 -沓 -砜 -蕊 -锺 -罹 -蕉 -翱 -虐 -闾 -巫 -旦 -茱 -嬷 -枯 -鹏 -贡 -芹 -汛 -矫 -绁 -拣 -禺 -佃 -讣 -舫 -惯 -乳 -趋 -疲 -挽 -岚 -虾 -衾 -蠹 -蹂 -飓 -氦 -铖 -孩 -稞 -瑜 -壅 -掀 -勘 -妓 -畅 -髋 -W -庐 -牲 -蓿 -榕 -练 -垣 -唱 -邸 -菲 -昆 -婺 -穿 -绡 -麒 -蚱 -掂 -愚 -泷 -涪 -漳 -妩 -娉 -榄 -讷 -觅 -旧 -藤 -煮 -呛 -柳 -腓 -叭 -庵 -烷 -阡 -罂 -蜕 -擂 -猖 -咿 -媲 -脉 -【 -沏 -貅 -黠 -熏 -哲 -烁 -坦 -酵 -兜 -× -潇 -撒 -剽 -珩 -圹 -乾 -摸 -樟 -帽 -嗒 -襄 -魂 -轿 -憬 -锡 -〕 -喃 -皆 -咖 -隅 -脸 -残 -泮 -袂 -鹂 -珊 -囤 -捆 -咤 -误 -徨 -闹 -淙 -芊 -淋 -怆 -囗 -拨 -梳 -渤 -R -G -绨 -蚓 -婀 -幡 -狩 -麾 -谢 -唢 -裸 -旌 -伉 -纶 -裂 -驳 -砼 -咛 -澄 -樨 -蹈 -宙 -澍 -倍 -貔 -操 -勇 -蟠 -摈 -砧 -虬 -够 -缁 -悦 -藿 -撸 -艹 -摁 -淹 -豇 -虎 -榭 -ˉ -吱 -d -° -喧 -荀 -踱 -侮 -奋 -偕 -饷 -犍 -惮 -坑 -璎 -徘 -宛 -妆 -袈 -倩 -窦 -昂 -荏 -乖 -K -怅 -撰 -鳙 -牙 -袁 -酞 -X -痿 -琼 -闸 -雁 -趾 -荚 -虻 -涝 -《 -杏 -韭 -偈 -烤 -绫 -鞘 -卉 -症 -遢 -蓥 -诋 -杭 -荨 -匆 -竣 -簪 -辙 -敕 -虞 -丹 -缭 -咩 -黟 -m -淤 -瑕 -咂 -铉 -硼 -茨 -嶂 -痒 -畸 -敬 -涿 -粪 -窘 -熟 -叔 -嫔 -盾 -忱 -裘 -憾 -梵 -赡 -珙 -咯 -娘 -庙 -溯 -胺 -葱 -痪 -摊 -荷 -卞 -乒 -髦 -寐 -铭 -坩 -胗 -枷 -爆 -溟 -嚼 -羚 -砬 -轨 -惊 -挠 -罄 -竽 -菏 -氧 -浅 -楣 -盼 -枢 -炸 -阆 -杯 -谏 -噬 -淇 -渺 -俪 -秆 -墓 -泪 -跻 -砌 -痰 -垡 -渡 -耽 -釜 -讶 -鳎 -煞 -呗 -韶 -舶 -绷 -鹳 -缜 -旷 -铊 -皱 -龌 -檀 -霖 -奄 -槐 -艳 -蝶 -旋 -哝 -赶 -骞 -蚧 -腊 -盈 -丁 -` -蜚 -矸 -蝙 -睨 -嚓 -僻 -鬼 -醴 -夜 -彝 -磊 -笔 -拔 -栀 -糕 -厦 -邰 -纫 -逭 -纤 -眦 -膊 -馍 -躇 -烯 -蘼 -冬 -诤 -暄 -骶 -哑 -瘠 -」 -臊 -丕 -愈 -咱 -螺 -擅 -跋 -搏 -硪 -谄 -笠 -淡 -嘿 -骅 -谧 -鼎 -皋 -姚 -歼 -蠢 -驼 -耳 -胬 -挝 -涯 -狗 -蒽 -孓 -犷 -凉 -芦 -箴 -铤 -孤 -嘛 -坤 -V -茴 -朦 -挞 -尖 -橙 -诞 -搴 -碇 -洵 -浚 -帚 -蜍 -漯 -柘 -嚎 -讽 -芭 -荤 -咻 -祠 -秉 -跖 -埃 -吓 -糯 -眷 -馒 -惹 -娼 -鲑 -嫩 -讴 -轮 -瞥 -靶 -褚 -乏 -缤 -宋 -帧 -删 -驱 -碎 -扑 -俩 -俄 -偏 -涣 -竹 -噱 -皙 -佰 -渚 -唧 -斡 -# -镉 -刀 -崎 -筐 -佣 -夭 -贰 -肴 -峙 -哔 -艿 -匐 -牺 -镛 -缘 -仡 -嫡 -劣 -枸 -堀 -梨 -簿 -鸭 -蒸 -亦 -稽 -浴 -{ -衢 -束 -槲 -j -阁 -揍 -疥 -棋 -潋 -聪 -窜 -乓 -睛 -插 -冉 -阪 -苍 -搽 -「 -蟾 -螟 -幸 -仇 -樽 -撂 -慢 -跤 -幔 -俚 -淅 -覃 -觊 -溶 -妖 -帛 -侨 -曰 -妾 -泗 -· -: -瀘 -風 -Ë -( -) -∶ -紅 -紗 -瑭 -雲 -頭 -鶏 -財 -許 -• -¥ -樂 -焗 -麗 -— -; -滙 -東 -榮 -繪 -興 -… -門 -業 -π -楊 -國 -顧 -é -盤 -寳 -Λ -龍 -鳳 -島 -誌 -緣 -結 -銭 -萬 -勝 -祎 -璟 -優 -歡 -臨 -時 -購 -= -★ -藍 -昇 -鐵 -觀 -勅 -農 -聲 -畫 -兿 -術 -發 -劉 -記 -專 -耑 -園 -書 -壴 -種 -Ο -● -褀 -號 -銀 -匯 -敟 -锘 -葉 -橪 -廣 -進 -蒄 -鑽 -阝 -祙 -貢 -鍋 -豊 -夬 -喆 -團 -閣 -開 -燁 -賓 -館 -酡 -沔 -順 -+ -硚 -劵 -饸 -陽 -車 -湓 -復 -萊 -氣 -軒 -華 -堃 -迮 -纟 -戶 -馬 -學 -裡 -電 -嶽 -獨 -マ -シ -サ -ジ -燘 -袪 -環 -❤ -臺 -灣 -専 -賣 -孖 -聖 -攝 -線 -▪ -α -傢 -俬 -夢 -達 -莊 -喬 -貝 -薩 -劍 -羅 -壓 -棛 -饦 -尃 -璈 -囍 -醫 -G -I -A -# -N -鷄 -髙 -嬰 -啓 -約 -隹 -潔 -賴 -藝 -~ -寶 -籣 -麺 -  -嶺 -√ -義 -網 -峩 -長 -∧ -魚 -機 -構 -② -鳯 -偉 -L -B -㙟 -畵 -鴿 -' -詩 -溝 -嚞 -屌 -藔 -佧 -玥 -蘭 -織 -1 -3 -9 -0 -7 -點 -砭 -鴨 -鋪 -銘 -廳 -弍 -‧ -創 -湯 -坶 -℃ -卩 -骝 -& -烜 -荘 -當 -潤 -扞 -係 -懷 -碶 -钅 -蚨 -讠 -☆ -叢 -爲 -埗 -涫 -塗 -→ -楽 -現 -鯨 -愛 -瑪 -鈺 -忄 -悶 -藥 -飾 -樓 -視 -孬 -ㆍ -燚 -苪 -師 -① -丼 -锽 -│ -韓 -標 -è -兒 -閏 -匋 -張 -漢 -Ü -髪 -會 -閑 -檔 -習 -裝 -の -峯 -菘 -輝 -И -雞 -釣 -億 -浐 -K -O -R -8 -H -E -P -T -W -D -S -C -M -F -姌 -饹 -» -晞 -廰 -ä -嵯 -鷹 -負 -飲 -絲 -冚 -楗 -澤 -綫 -區 -❋ -← -質 -靑 -揚 -③ -滬 -統 -産 -協 -﹑ -乸 -畐 -經 -運 -際 -洺 -岽 -為 -粵 -諾 -崋 -豐 -碁 -ɔ -V -2 -6 -齋 -誠 -訂 -´ -勑 -雙 -陳 -無 -í -泩 -媄 -夌 -刂 -i -c -t -o -r -a -嘢 -耄 -燴 -暃 -壽 -媽 -靈 -抻 -體 -唻 -É -冮 -甹 -鎮 -錦 -ʌ -蜛 -蠄 -尓 -駕 -戀 -飬 -逹 -倫 -貴 -極 -Я -Й -寬 -磚 -嶪 -郎 -職 -| -間 -n -d -剎 -伈 -課 -飛 -橋 -瘊 -№ -譜 -骓 -圗 -滘 -縣 -粿 -咅 -養 -濤 -彳 -® -% -Ⅱ -啰 -㴪 -見 -矞 -薬 -糁 -邨 -鲮 -顔 -罱 -З -選 -話 -贏 -氪 -俵 -競 -瑩 -繡 -枱 -β -綉 -á -獅 -爾 -™ -麵 -戋 -淩 -徳 -個 -劇 -場 -務 -簡 -寵 -h -實 -膠 -轱 -圖 -築 -嘣 -樹 -㸃 -營 -耵 -孫 -饃 -鄺 -飯 -麯 -遠 -輸 -坫 -孃 -乚 -閃 -鏢 -㎡ -題 -廠 -關 -↑ -爺 -將 -軍 -連 -篦 -覌 -參 -箸 -- -窠 -棽 -寕 -夀 -爰 -歐 -呙 -閥 -頡 -熱 -雎 -垟 -裟 -凬 -勁 -帑 -馕 -夆 -疌 -枼 -馮 -貨 -蒤 -樸 -彧 -旸 -靜 -龢 -暢 -㐱 -鳥 -珺 -鏡 -灡 -爭 -堷 -廚 -Ó -騰 -診 -┅ -蘇 -褔 -凱 -頂 -豕 -亞 -帥 -嘬 -⊥ -仺 -桖 -複 -饣 -絡 -穂 -顏 -棟 -納 -▏ -濟 -親 -設 -計 -攵 -埌 -烺 -ò -頤 -燦 -蓮 -撻 -節 -講 -濱 -濃 -娽 -洳 -朿 -燈 -鈴 -護 -膚 -铔 -過 -補 -Z -U -5 -4 -坋 -闿 -䖝 -餘 -缐 -铞 -貿 -铪 -桼 -趙 -鍊 -[ -㐂 -垚 -菓 -揸 -捲 -鐘 -滏 -𣇉 -爍 -輪 -燜 -鴻 -鮮 -動 -鹞 -鷗 -丄 -慶 -鉌 -翥 -飮 -腸 -⇋ -漁 -覺 -來 -熘 -昴 -翏 -鲱 -圧 -鄉 -萭 -頔 -爐 -嫚 -г -貭 -類 -聯 -幛 -輕 -訓 -鑒 -夋 -锨 -芃 -珣 -䝉 -扙 -嵐 -銷 -處 -ㄱ -語 -誘 -苝 -歸 -儀 -燒 -楿 -內 -粢 -葒 -奧 -麥 -礻 -滿 -蠔 -穵 -瞭 -態 -鱬 -榞 -硂 -鄭 -黃 -煙 -祐 -奓 -逺 -* -瑄 -獲 -聞 -薦 -讀 -這 -樣 -決 -問 -啟 -們 -執 -説 -轉 -單 -隨 -唘 -帶 -倉 -庫 -還 -贈 -尙 -皺 -■ -餅 -產 -○ -∈ -報 -狀 -楓 -賠 -琯 -嗮 -禮 -` -傳 -> -≤ -嗞 -Φ -≥ -換 -咭 -∣ -↓ -曬 -ε -応 -寫 -″ -終 -様 -純 -費 -療 -聨 -凍 -壐 -郵 -ü -黒 -∫ -製 -塊 -調 -軽 -確 -撃 -級 -馴 -Ⅲ -涇 -繹 -數 -碼 -證 -狒 -処 -劑 -< -晧 -賀 -衆 -] -櫥 -兩 -陰 -絶 -對 -鯉 -憶 -◎ -p -e -Y -蕒 -煖 -頓 -測 -試 -鼽 -僑 -碩 -妝 -帯 -≈ -鐡 -舖 -權 -喫 -倆 -ˋ -該 -悅 -ā -俫 -. -f -s -b -m -k -g -u -j -貼 -淨 -濕 -針 -適 -備 -l -/ -給 -謢 -強 -觸 -衛 -與 -⊙ -$ -緯 -變 -⑴ -⑵ -⑶ -㎏ -殺 -∩ -幚 -─ -價 -▲ -離 -ú -ó -飄 -烏 -関 -閟 -﹝ -﹞ -邏 -輯 -鍵 -驗 -訣 -導 -歷 -屆 -層 -▼ -儱 -錄 -熳 -ē -艦 -吋 -錶 -辧 -飼 -顯 -④ -禦 -販 -気 -対 -枰 -閩 -紀 -幹 -瞓 -貊 -淚 -△ -眞 -墊 -Ω -獻 -褲 -縫 -緑 -亜 -鉅 -餠 -{ -} -◆ -蘆 -薈 -█ -◇ -溫 -彈 -晳 -粧 -犸 -穩 -訊 -崬 -凖 -熥 -П -舊 -條 -紋 -圍 -Ⅳ -筆 -尷 -難 -雜 -錯 -綁 -識 -頰 -鎖 -艶 -□ -殁 -殼 -⑧ -├ -▕ -鵬 -ǐ -ō -ǒ -糝 -綱 -▎ -μ -盜 -饅 -醬 -籤 -蓋 -釀 -鹽 -據 -à -ɡ -辦 -◥ -彐 -┌ -婦 -獸 -鲩 -伱 -ī -蒟 -蒻 -齊 -袆 -腦 -寧 -凈 -妳 -煥 -詢 -偽 -謹 -啫 -鯽 -騷 -鱸 -損 -傷 -鎻 -髮 -買 -冏 -儥 -両 -﹢ -∞ -載 -喰 -z -羙 -悵 -燙 -曉 -員 -組 -徹 -艷 -痠 -鋼 -鼙 -縮 -細 -嚒 -爯 -≠ -維 -" -鱻 -壇 -厍 -帰 -浥 -犇 -薡 -軎 -² -應 -醜 -刪 -緻 -鶴 -賜 -噁 -軌 -尨 -镔 -鷺 -槗 -彌 -葚 -濛 -請 -溇 -緹 -賢 -訪 -獴 -瑅 -資 -縤 -陣 -蕟 -栢 -韻 -祼 -恁 -伢 -謝 -劃 -涑 -總 -衖 -踺 -砋 -凉 -籃 -駿 -苼 -瘋 -昽 -紡 -驊 -腎 -﹗ -響 -杋 -剛 -嚴 -禪 -歓 -槍 -傘 -檸 -檫 -炣 -勢 -鏜 -鎢 -銑 -尐 -減 -奪 -惡 -θ -僮 -婭 -臘 -ū -ì -殻 -鉄 -∑ -蛲 -焼 -緖 -續 -紹 -懮 \ No newline at end of file From 51a8d23392fa0406976a0a782035ccf70922934b Mon Sep 17 00:00:00 2001 From: littletomatodonkey Date: Mon, 13 Jul 2020 13:09:58 +0000 Subject: [PATCH 6/7] fix readme --- deploy/cpp_infer/readme.md | 55 ++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/deploy/cpp_infer/readme.md b/deploy/cpp_infer/readme.md index aa037791..df30daa5 100644 --- a/deploy/cpp_infer/readme.md +++ b/deploy/cpp_infer/readme.md @@ -69,24 +69,7 @@ opencv3/ * 有2种方式获取Paddle预测库,下面进行详细介绍。 -#### 1.2.1 直接下载安装 - -* [Paddle预测库官网](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/build_and_install_lib_cn.html)上提供了不同cuda版本的Linux预测库,可以直接下载使用。 - * 如果cuda版本为cuda9.0,1.8.2版本的Paddle预测库可以从这里下载:[下载地址](https://paddle-inference-lib.bj.bcebos.com/1.8.2-gpu-cuda9-cudnn7-avx-mkl/fluid_inference.tgz)。 - * 如果cuda版本为cuda10.0,1.8.2版本的Paddle预测库可以从这里下载:[下载地址](https://paddle-inference-lib.bj.bcebos.com/1.8.2-gpu-cuda10-cudnn7-avx-mkl/fluid_inference.tgz)。 - * 更多版本的预测库可以在官网查看下载。 - -* 下载之后使用下面的方法解压 - -``` -tar -xf fluid_inference.tgz -``` - -最终会在当前的文件夹中生成`fluid_inference/`的子文件夹。 - - - -#### 1.2.2 预测库源码编译 +#### 1.2.1 预测库源码编译 * 如果希望获取最新预测库特性,可以从Paddle github上克隆最新代码,源码编译预测库。 * 可以参考[Paddle预测库官网](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/build_and_install_lib_cn.html)的说明,从github上获取Paddle代码,然后进行编译,生成最新的预测库。使用git获取代码方法如下。 @@ -110,7 +93,7 @@ cmake .. \ -DWITH_INFERENCE_API_TEST=OFF \ -DON_INFER=ON \ -DWITH_PYTHON=ON -make -j16 +make -j make inference_lib_dist ``` @@ -129,6 +112,18 @@ build/fluid_inference_install_dir/ 其中`paddle`就是之后进行C++预测时所需的Paddle库,`version.txt`中包含当前预测库的版本信息。 +#### 1.2.2 直接下载安装 + +* [Paddle预测库官网](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/build_and_install_lib_cn.html)上提供了不同cuda版本的Linux预测库,可以在官网查看并选择合适的预测库版本。 + +* 下载之后使用下面的方法解压。 + +``` +tar -xf fluid_inference.tgz +``` + +最终会在当前的文件夹中生成`fluid_inference/`的子文件夹。 + ## 2 开始运行 @@ -159,7 +154,27 @@ sh tools/build.sh 具体地,`tools/build.sh`中内容如下。 ```shell -c +OPENCV_DIR=your_opencv_dir +LIB_DIR=your_paddle_inference_dir +CUDA_LIB_DIR=your_cuda_lib_dir +CUDNN_LIB_DIR=/your_cudnn_lib_dir + +BUILD_DIR=build +rm -rf ${BUILD_DIR} +mkdir ${BUILD_DIR} +cd ${BUILD_DIR} +cmake .. \ + -DPADDLE_LIB=${LIB_DIR} \ + -DWITH_MKL=ON \ + -DDEMO_NAME=ocr_system \ + -DWITH_GPU=OFF \ + -DWITH_STATIC_LIB=OFF \ + -DUSE_TENSORRT=OFF \ + -DOPENCV_DIR=${OPENCV_DIR} \ + -DCUDNN_LIB=${CUDNN_LIB_DIR} \ + -DCUDA_LIB=${CUDA_LIB_DIR} \ + +make -j ``` `OPENCV_DIR`为opencv编译安装的地址;`LIB_DIR`为下载(`fluid_inference`文件夹)或者编译生成的Paddle预测库地址(`build/fluid_inference_install_dir`文件夹);`CUDA_LIB_DIR`为cuda库文件地址,在docker中;为`/usr/local/cuda/lib64`;`CUDNN_LIB_DIR`为cudnn库文件地址,在docker中为`/usr/lib/x86_64-linux-gnu/`。 From fd66c409b0b99c82ac7efc1e02bd50a25ef22056 Mon Sep 17 00:00:00 2001 From: littletomatodonkey Date: Mon, 13 Jul 2020 14:09:04 +0000 Subject: [PATCH 7/7] rm mkl --- deploy/cpp_infer/src/ocr_det.cpp | 2 +- deploy/cpp_infer/src/ocr_rec.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/cpp_infer/src/ocr_det.cpp b/deploy/cpp_infer/src/ocr_det.cpp index 636d2d04..ee30c1a7 100644 --- a/deploy/cpp_infer/src/ocr_det.cpp +++ b/deploy/cpp_infer/src/ocr_det.cpp @@ -25,7 +25,7 @@ void DBDetector::LoadModel(const std::string &model_dir) { config.EnableUseGpu(this->gpu_mem_, this->gpu_id_); } else { config.DisableGpu(); - config.EnableMKLDNN(); // 开启MKLDNN加速 + // config.EnableMKLDNN(); // not sugesteed to use for now config.SetCpuMathLibraryNumThreads(this->cpu_math_library_num_threads_); } diff --git a/deploy/cpp_infer/src/ocr_rec.cpp b/deploy/cpp_infer/src/ocr_rec.cpp index 6173c7ce..95ea9a5e 100644 --- a/deploy/cpp_infer/src/ocr_rec.cpp +++ b/deploy/cpp_infer/src/ocr_rec.cpp @@ -128,7 +128,7 @@ void CRNNRecognizer::LoadModel(const std::string &model_dir) { config.EnableUseGpu(this->gpu_mem_, this->gpu_id_); } else { config.DisableGpu(); - config.EnableMKLDNN(); // 开启MKLDNN加速 + // config.EnableMKLDNN(); // not sugesteed to use for now config.SetCpuMathLibraryNumThreads(this->cpu_math_library_num_threads_); }