Repository: brandonmpetty/Doxa Branch: master Commit: be67520e93a4 Files: 163 Total size: 845.9 KB Directory structure: gitextract_kry5ydl6/ ├── .claude/ │ └── settings.local.json ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── benchmarks.yml │ ├── npm-publish.yml │ ├── pythonpackage.yml │ └── test-and-coverage.yml ├── .gitignore ├── Bindings/ │ ├── Matlab/ │ │ ├── +Doxa/ │ │ │ ├── Algorithms.m │ │ │ ├── Grayscale.m │ │ │ ├── Image.m │ │ │ ├── binarize.m │ │ │ ├── buildParams.m │ │ │ ├── calculatePerformance.m │ │ │ ├── readWeights.m │ │ │ └── updateToBinary.m │ │ ├── BinarizeMex.cpp │ │ ├── CMakeLists.txt │ │ ├── CalculatePerformanceMex.cpp │ │ ├── Doxa.prj │ │ ├── DoxaMexUtils.hpp │ │ ├── ImageMex.cpp │ │ ├── README.md │ │ ├── UpdateToBinaryMex.cpp │ │ └── test/ │ │ └── TestDoxa.m │ ├── Python/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── DoxaPy.ipynb │ │ ├── README.md │ │ ├── copy-cpp-files.py │ │ ├── pyproject.toml │ │ ├── requirements.txt │ │ ├── src/ │ │ │ ├── DoxaPy.cpp │ │ │ └── doxapy/ │ │ │ └── __init__.py │ │ └── test/ │ │ ├── test_doxa.py │ │ └── test_speed.py │ └── WebAssembly/ │ ├── CMakeLists.txt │ ├── DoxaJs.nnb │ ├── DoxaWasm.cpp │ ├── README.md │ ├── dist/ │ │ ├── doxa.js │ │ ├── doxaWasm.js │ │ └── doxaWasm.wasm │ ├── doxa.js │ ├── package.json │ └── spec/ │ ├── binarization.spec.js │ ├── image.spec.js │ ├── speed.spec.js │ └── support/ │ └── jasmine.json ├── CLAUDE.md ├── CMakeLists.txt ├── CMakePresets.json ├── Demo/ │ ├── Cpp/ │ │ ├── .gitignore │ │ ├── demo.cpp │ │ ├── demoOpenCV.cpp │ │ ├── demoQt.cpp │ │ └── demoQt.pro │ ├── Matlab/ │ │ └── demo.m │ ├── NodeJS/ │ │ ├── .gitignore │ │ ├── index.js │ │ └── package.json │ ├── Python/ │ │ └── demo.py │ └── WebJS/ │ └── index.html ├── Doxa/ │ ├── AdOtsu.hpp │ ├── Algorithm.hpp │ ├── Bataineh.hpp │ ├── Bernsen.hpp │ ├── BinarizationFactory.hpp │ ├── ChanMeanCalc.hpp │ ├── ChanMeanVarianceCalc.hpp │ ├── ClassifiedPerformance.hpp │ ├── ContrastImage.hpp │ ├── DIBCOUtils.hpp │ ├── DRDM.hpp │ ├── Doxa.vcxitems │ ├── Gatos.hpp │ ├── Grayscale.hpp │ ├── GridCalc.hpp │ ├── ISauvola.hpp │ ├── Image.hpp │ ├── IntegralImageMeanVarianceCalc.hpp │ ├── LocalWindow.hpp │ ├── Morphology.hpp │ ├── MultiScale.hpp │ ├── Niblack.hpp │ ├── Nick.hpp │ ├── Otsu.hpp │ ├── PNM.hpp │ ├── Palette.hpp │ ├── Parameters.hpp │ ├── Phansalkar.hpp │ ├── Region.hpp │ ├── SIMD.h │ ├── SIMDOps.hpp │ ├── Sauvola.hpp │ ├── Su.hpp │ ├── TRSingh.hpp │ ├── Types.hpp │ ├── Wan.hpp │ ├── WienerFilter.hpp │ └── Wolf.hpp ├── Doxa.Bench/ │ ├── BenchmarkHarness.hpp │ ├── BinarizationBenchmarks.cpp │ ├── CMakeLists.txt │ ├── CalculatorBenchmarks.cpp │ ├── ClassifiedPerformanceBenchmarks.cpp │ ├── DRDMBenchmarks.cpp │ ├── GlobalThresholdBenchmarks.cpp │ ├── config.hpp.in │ └── pch.h ├── Doxa.Test/ │ ├── AlgorithmTests.cpp │ ├── BatainehTests.cpp │ ├── BinarizationTests.cpp │ ├── CMakeLists.txt │ ├── CalculatorTests.cpp │ ├── ClassifiedPerformanceTests.cpp │ ├── ContrastImageTests.cpp │ ├── DIBCOUtilsTests.cpp │ ├── DRDMTests.cpp │ ├── Doxa.Test.vcxproj │ ├── Doxa.Test.vcxproj.filters │ ├── GrayscaleTests.cpp │ ├── GridCalcTests.cpp │ ├── ISauvolaTests.cpp │ ├── ImageFixture.hpp │ ├── ImageTests.cpp │ ├── LocalWindowTests.cpp │ ├── MorphologyTests.cpp │ ├── PNMTests.cpp │ ├── PaletteTests.cpp │ ├── ParametersTests.cpp │ ├── RegionTests.cpp │ ├── Resources/ │ │ ├── 2JohnC1V3-AdOtsu.pbm │ │ ├── 2JohnC1V3-AdOtsuG.pbm │ │ ├── 2JohnC1V3-AdOtsuMS.pbm │ │ ├── 2JohnC1V3-AdOtsuMSG.pbm │ │ ├── 2JohnC1V3-Bataineh.pbm │ │ ├── 2JohnC1V3-Bensen.pbm │ │ ├── 2JohnC1V3-ContrastImage.ppm │ │ ├── 2JohnC1V3-Gatos.pbm │ │ ├── 2JohnC1V3-GroundTruth.pbm │ │ ├── 2JohnC1V3-HighContrastImage.pbm │ │ ├── 2JohnC1V3-ISauvola.pbm │ │ ├── 2JohnC1V3-NICK.pbm │ │ ├── 2JohnC1V3-Niblack.pbm │ │ ├── 2JohnC1V3-Otsu.pbm │ │ ├── 2JohnC1V3-Phansalkar.pbm │ │ ├── 2JohnC1V3-Sauvola.pbm │ │ ├── 2JohnC1V3-Su.pbm │ │ ├── 2JohnC1V3-TRSingh.pbm │ │ ├── 2JohnC1V3-WAN.pbm │ │ ├── 2JohnC1V3-Wolf.pbm │ │ ├── 2JohnC1V3.ppm │ │ └── 2JohnC1V3.psd │ ├── SIMDTests.cpp │ ├── SuTests.cpp │ ├── TestUtilities.hpp │ ├── WienerFilterTests.cpp │ ├── packages.config │ ├── pch.cpp │ └── pch.h ├── Doxa.sln ├── LICENSE └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude/settings.local.json ================================================ { "permissions": { "allow": [ "Bash(cmake:*)" ] } } ================================================ FILE: .gitattributes ================================================ ## Prevent Git from affecting line endings ## GRAPHICS *.psd binary *.ppm binary *.pbm binary ================================================ FILE: .github/workflows/benchmarks.yml ================================================ name: Performance Benchmarks on: push: branches: [ "master" ] pull_request: branches: [ "master" ] permissions: contents: write pull-requests: write jobs: benchmark: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] include: - os: ubuntu-latest platform: Linux bench_exe: ./build-bench/Doxa.Bench/doxa_bench - os: windows-latest platform: Windows bench_exe: ./build-bench/Doxa.Bench/Release/doxa_bench.exe - os: macos-latest platform: macOS bench_exe: ./build-bench/Doxa.Bench/doxa_bench steps: - uses: actions/checkout@v4 - name: Configure CMake run: cmake --preset benchmarks - name: Build run: cmake --build build-bench --config Release - name: Run Benchmarks run: ${{ matrix.bench_exe }} --benchmark_out=benchmark_results.json --benchmark_out_format=json --benchmark_min_time=1s --benchmark_repetitions=5 --benchmark_report_aggregates_only=true - name: Upload Benchmark Results uses: actions/upload-artifact@v4 with: name: benchmark-results-${{ matrix.platform }} path: benchmark_results.json - name: Store Benchmark Results uses: benchmark-action/github-action-benchmark@v1 with: tool: 'googlecpp' output-file-path: benchmark_results.json # Each platform gets its own independent dataset and history name: 'Doxa Benchmarks (${{ matrix.platform }})' github-token: ${{ secrets.GITHUB_TOKEN }} # Store history in gh-pages branch (only on push to master) auto-push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} # Alert on PRs if perf regresses beyond threshold alert-threshold: '120%' comment-on-alert: true fail-on-alert: false ================================================ FILE: .github/workflows/npm-publish.yml ================================================ name: Publish to npm permissions: contents: read # Disabled: NPM_TOKEN secret not yet configured. # Re-enable by uncommenting the release trigger below. on: # release: # types: [published] workflow_dispatch: # manual trigger only for now jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' registry-url: 'https://registry.npmjs.org' - name: Setup Emscripten uses: mymindstorm/setup-emsdk@v14 - name: Install dependencies working-directory: ./Bindings/WebAssembly run: npm install - name: Build WASM working-directory: ./Bindings/WebAssembly run: npm run build - name: Run tests working-directory: ./Bindings/WebAssembly run: npm test - name: Publish to npm working-directory: ./Bindings/WebAssembly run: npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .github/workflows/pythonpackage.yml ================================================ name: Python Package on: release: types: - published jobs: build_sdist: name: Build SDist runs-on: ubuntu-latest defaults: run: working-directory: ./Bindings/Python steps: - uses: actions/checkout@v4 with: submodules: true - name: Build SDist run: | python copy-cpp-files.py pipx run build --sdist - name: Check metadata run: pipx run twine check dist/* - uses: actions/upload-artifact@v4 with: name: dist-sdist path: Bindings/Python/dist/*.tar.gz build_wheels: name: Build Wheels runs-on: ${{ matrix.os }} defaults: run: working-directory: ./Bindings/Python strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/setup-python@v5 - uses: actions/checkout@v4 with: submodules: true - name: Build setup run: | python copy-cpp-files.py pip install -r requirements.txt python -m pip install cibuildwheel==2.22.0 - name: Build wheels run: python -m cibuildwheel --output-dir wheelhouse - name: Upload wheels uses: actions/upload-artifact@v4 with: path: Bindings/Python/wheelhouse/*.whl name: dist-${{ matrix.os }} upload_all: name: Upload to PyPi needs: [build_wheels, build_sdist] runs-on: ubuntu-latest if: github.event_name == 'release' && github.event.action == 'published' steps: - uses: actions/setup-python@v5 - uses: actions/download-artifact@v4 with: path: dist pattern: dist-* merge-multiple: true - uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} ================================================ FILE: .github/workflows/test-and-coverage.yml ================================================ # This starter workflow is for a CMake project running on multiple platforms. There is a different starter workflow if you just want a single platform. # See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-single-platform.yml name: Build and Test with Coverage on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ${{ matrix.os }} strategy: # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. fail-fast: false # Set up a matrix to run the following 3 configurations: # 1. # 2. # 3. # # To add more build types (Release, Debug, RelWithDebInfo, etc.) customize the build_type list. matrix: os: [ubuntu-latest, windows-latest, macos-latest] build_type: [Debug] # Changed to Debug for better coverage c_compiler: [gcc, clang, cl] include: - os: windows-latest c_compiler: cl cpp_compiler: cl - os: ubuntu-latest c_compiler: gcc cpp_compiler: g++ - os: macos-latest c_compiler: clang cpp_compiler: clang++ exclude: - os: windows-latest c_compiler: gcc - os: windows-latest c_compiler: clang - os: ubuntu-latest c_compiler: cl - os: ubuntu-latest c_compiler: clang - os: macos-latest c_compiler: gcc - os: macos-latest c_compiler: cl steps: - uses: actions/checkout@v4 - name: Set reusable strings # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. id: strings shell: bash run: | echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" - name: Install dependencies (Ubuntu) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y cmake pipx install gcovr - name: Install dependencies (macOS) if: runner.os == 'macOS' run: | brew install cmake - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type run: > cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_CXX_FLAGS="${{ runner.os == 'Linux' && '-fprofile-arcs -ftest-coverage' || '' }}" -DCMAKE_C_FLAGS="${{ runner.os == 'Linux' && '-fprofile-arcs -ftest-coverage' || '' }}" -S ${{ github.workspace }}/Doxa.Test - name: Build # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} - name: Test working-directory: ${{ steps.strings.outputs.build-output-dir }} # Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: ctest --build-config ${{ matrix.build_type }} --output-on-failure - name: Generate Coverage Report if: runner.os == 'Linux' working-directory: ${{ steps.strings.outputs.build-output-dir }} run: | # Use gcovr to generate XML coverage report for GCC # Filter to only the core library headers - exclude test files and build artifacts gcovr --xml --xml-pretty --gcov-ignore-parse-errors=suspicious_hits.warn --root .. --filter '../Doxa/' > coverage.xml - name: Upload Coverage Report if: runner.os == 'Linux' uses: codecov/codecov-action@v3 with: file: ${{ steps.strings.outputs.build-output-dir }}/coverage.xml fail_ci_if_error: false ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ build/ build-cpp-tests/ build-python/ build-wasm/ build-matlab/ build-bench/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.publishsettings node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml ================================================ FILE: Bindings/Matlab/+Doxa/Algorithms.m ================================================ classdef Algorithms %ALGORITHMS Binarization algorithms available in the Doxa framework. % % Global Thresholding: % Doxa.Algorithms.OTSU % % Local Adaptive Thresholding: % Doxa.Algorithms.BERNSEN, NIBLACK, SAUVOLA, WOLF, NICK, SU, % TRSINGH, BATAINEH, PHANSALKAR, ISAUVOLA, WAN, GATOS, ADOTSU % % See also Doxa.binarize enumeration OTSU BERNSEN NIBLACK SAUVOLA WOLF NICK SU TRSINGH BATAINEH PHANSALKAR ISAUVOLA WAN GATOS ADOTSU end end ================================================ FILE: Bindings/Matlab/+Doxa/Grayscale.m ================================================ classdef Grayscale %GRAYSCALE Grayscale conversion algorithms available in the Doxa framework. % % Algorithms: % Doxa.Grayscale.MEAN - Mean of R, G, B (Gleam/Intensity) % Doxa.Grayscale.QT - Qt framework formula (default) % Doxa.Grayscale.BT601 - ITU-R BT.601 (NTSC) % Doxa.Grayscale.BT709 - ITU-R BT.709 (sRGB) % Doxa.Grayscale.BT2100 - ITU-R BT.2100 % Doxa.Grayscale.VALUE - HSV Value (max channel) % Doxa.Grayscale.LUSTER - HLS Lightness % Doxa.Grayscale.LIGHTNESS - CIELAB/CIELUV Lightness % Doxa.Grayscale.MINAVG - Min-Average (for multi-color text) % Doxa.Grayscale.LABDIST - L*a*b* Euclidean Distance (used by Phansalkar) % % See also Doxa.Image enumeration MEAN QT BT601 BT709 BT2100 VALUE LUSTER LIGHTNESS MINAVG LABDIST end end ================================================ FILE: Bindings/Matlab/+Doxa/Image.m ================================================ classdef Image < handle %IMAGE Doxa image container wrapping a C++ Doxa::Image. % Handles the column-major (Matlab) to row-major (C++) memory layout % conversion at construction and extraction boundaries. % % Construction: % img = Doxa.Image('file.ppm') % from file % img = Doxa.Image('file.ppm', Doxa.Grayscale.QT) % from color file % img = Doxa.Image(gray_uint8) % from 2D uint8 % img = Doxa.Image(rgb_uint8) % from 3D, default grayscale % img = Doxa.Image(rgb_uint8, Doxa.Grayscale.QT) % from 3D, specific algorithm % % Methods: % arr = img.toArray() % convert back to Matlab uint8 matrix % disp(img) % display dimensions % % See also Doxa.Grayscale, Doxa.binarize properties (Access = {?Doxa.Image}) Handle uint64 = uint64(0) end methods function obj = Image(input, algorithm) %IMAGE Construct a Doxa.Image from a file path, 2D, or 3D uint8 array. if nargin == 0 % Internal: empty construction for fromHandle factory return; end % Load from file if given a path if ischar(input) || isstring(input) input = imread(input); % imread returns logical for PBM; scale to 0/255 if islogical(input) input = uint8(input) * 255; end input = uint8(input); end if ~isa(input, 'uint8') error('Doxa:Image:InvalidInput', 'Input must be uint8.'); end if ndims(input) == 3 % Color image: convert to grayscale if nargin < 2 algorithm = Doxa.Grayscale.MEAN; end obj.Handle = image_mex('from_grayscale', char(algorithm), input); elseif ismatrix(input) % 2D grayscale or binary obj.Handle = image_mex('create', input); else error('Doxa:Image:InvalidInput', 'Input must be a 2D or 3D uint8 array.'); end end function arr = toArray(obj) %TOARRAY Convert back to a Matlab uint8 matrix. % arr = img.toArray() obj.checkValid(); arr = image_mex('to_array', obj.Handle); end function w = width(obj) %WIDTH Image width in pixels. obj.checkValid(); w = image_mex('width', obj.Handle); end function h = height(obj) %HEIGHT Image height in pixels. obj.checkValid(); h = image_mex('height', obj.Handle); end function disp(obj) %DISP Display Doxa.Image summary. if obj.Handle == uint64(0) fprintf(' Doxa.Image: [empty]\n'); else fprintf(' Doxa.Image: %dx%d uint8\n', obj.width(), obj.height()); end end function delete(obj) %DELETE Free the underlying C++ image memory. if obj.Handle ~= uint64(0) image_mex('destroy', obj.Handle); obj.Handle = uint64(0); end end end methods (Hidden) function h = getHandle(obj) %GETHANDLE Return the raw uint64 handle for MEX interop. obj.checkValid(); h = obj.Handle; end end methods (Static, Hidden) function obj = fromHandle(handle) %FROMHANDLE Wrap an existing C++ image pointer without re-transposing. obj = Doxa.Image(); obj.Handle = handle; end end methods (Access = private) function checkValid(obj) if obj.Handle == uint64(0) error('Doxa:Image:InvalidHandle', 'Image has been destroyed or is uninitialized.'); end end end end ================================================ FILE: Bindings/Matlab/+Doxa/binarize.m ================================================ function outputImage = binarize(algorithm, inputImage, options) %BINARIZE Convert a grayscale Doxa.Image to binary. % binary = Doxa.binarize(Doxa.Algorithms.SAUVOLA, img) % binary = Doxa.binarize(Doxa.Algorithms.SAUVOLA, img, window=75, k=0.2) % % Common parameters (defaults vary by algorithm): % window - Local window size in pixels (default: 75) % k - Sensitivity parameter (default: 0.2) % % Algorithm-specific parameters: % threshold - Bernsen: global threshold (default: 100) % contrastLimit - Bernsen: contrast limit (default: 25) % R - AdOtsu: range parameter (default: 0.1) % distance - AdOtsu: grid distance (default: window/2) % minN - Su: minimum neighborhood (default: window) % glyph - Gatos: estimated stroke width (default: 60) % % See also Doxa.Algorithms, Doxa.updateToBinary, Doxa.Image arguments algorithm Doxa.Algorithms inputImage Doxa.Image options.window = [] options.k = [] options.threshold = [] options.contrastLimit = [] options.R = [] options.distance = [] options.minN = [] options.glyph = [] end params = Doxa.buildParams(options); handle = binarize_mex(char(algorithm), inputImage.getHandle(), params); outputImage = Doxa.Image.fromHandle(handle); end ================================================ FILE: Bindings/Matlab/+Doxa/buildParams.m ================================================ function params = buildParams(options) %BUILDPARAMS Convert name-value options to a parameter struct for MEX. % Internal utility. Strips empty fields before passing to the MEX layer. params = struct(); fields = fieldnames(options); for i = 1:numel(fields) value = options.(fields{i}); if ~isempty(value) params.(fields{i}) = value; end end end ================================================ FILE: Bindings/Matlab/+Doxa/calculatePerformance.m ================================================ function metrics = calculatePerformance(gtImage, binaryImage, options) %CALCULATEPERFORMANCE Calculate binarization quality metrics. % metrics = Doxa.calculatePerformance(gt, binary) % metrics = Doxa.calculatePerformance(gt, binary, precisionWeights=pw, recallWeights=rw) % % Returns a struct with all standard metrics: % accuracy, fm, recall, precision, psnr, nrm, mcc, drdm % % When precisionWeights and recallWeights are provided, pseudo-metrics % are also included: pseudoFM, pseudoPrecision, pseudoRecall % % See also Doxa.Image, Doxa.readWeights arguments gtImage Doxa.Image binaryImage Doxa.Image options.precisionWeights double = [] options.recallWeights double = [] end metrics = calculate_performance_mex( ... gtImage.getHandle(), binaryImage.getHandle(), ... options.precisionWeights, options.recallWeights); end ================================================ FILE: Bindings/Matlab/+Doxa/readWeights.m ================================================ function weights = readWeights(filePath) %READWEIGHTS Load a DIBCO-format weight file for pseudo-metrics. % pw = Doxa.readWeights('precision_weights.dat') % rw = Doxa.readWeights('recall_weights.dat') % % Returns a double column vector of weights. % % See also Doxa.calculatePerformance arguments filePath {mustBeTextScalar, mustBeFile} end fid = fopen(filePath, 'r'); weights = fscanf(fid, '%f'); fclose(fid); end ================================================ FILE: Bindings/Matlab/+Doxa/updateToBinary.m ================================================ function updateToBinary(algorithm, inputImage, options) %UPDATETOBINARY Binarize a Doxa.Image in-place. % Doxa.updateToBinary(Doxa.Algorithms.SAUVOLA, img) % Doxa.updateToBinary(Doxa.Algorithms.SAUVOLA, img, window=75, k=0.2) % % Modifies the Doxa.Image directly. No new image is created. % See Doxa.binarize for available parameters. % % See also Doxa.Algorithms, Doxa.binarize, Doxa.Image arguments algorithm Doxa.Algorithms inputImage Doxa.Image options.window = [] options.k = [] options.threshold = [] options.contrastLimit = [] options.R = [] options.distance = [] options.minN = [] options.glyph = [] end params = Doxa.buildParams(options); update_to_binary_mex(char(algorithm), inputImage.getHandle(), params); end ================================================ FILE: Bindings/Matlab/BinarizeMex.cpp ================================================ // Δoxa Binarization Framework // License: CC0 2026, "Freely you have received; freely give." - Matt 10:8 #include "mex.h" #include "DoxaMexUtils.hpp" /// /// MEX function to create a new binarized image from a Doxa.Image handle. /// Matlab Signature: handle = binarize_mex(algorithm_name, image_handle, params_struct) /// void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[]) { if (nrhs < 2 || nrhs > 3) { mexErrMsgIdAndTxt("Doxa:binarize:InvalidInput", "Usage: binarize_mex(algorithm, image_handle, params_struct)"); } // 1. Get Algorithm Enum std::string algorithmStr = mxArrayToString(prhs[0]); Doxa::Algorithms algorithmEnum = DoxaMexUtils::StringToAlgorithmEnum(algorithmStr); // 2. Get Input Image from handle Doxa::Image* grayImage = DoxaMexUtils::HandleToImage(prhs[1]); // 3. Get Parameters const mxArray* paramsMx = (nrhs == 3) ? prhs[2] : nullptr; Doxa::Parameters params = DoxaMexUtils::MxStructToParameters(paramsMx); // 4. Create output image and run algorithm Doxa::Image* binaryImage = new Doxa::Image(grayImage->width, grayImage->height); Doxa::IAlgorithm* algorithm = Doxa::BinarizationFactory::Algorithm(algorithmEnum); algorithm->Initialize(*grayImage); algorithm->ToBinary(*binaryImage, params); delete algorithm; // 5. Return handle to the new binary image plhs[0] = DoxaMexUtils::ImageToHandle(binaryImage); } ================================================ FILE: Bindings/Matlab/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.16) project(DoxaMatlab) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Find MATLAB and its components find_package(Matlab COMPONENTS MAIN_PROGRAM) # Include the root Doxa directory for header files include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../..) # Define output directory for MEX files and Matlab sources set(MEX_OUTPUT_DIR ${CMAKE_BINARY_DIR}/mex) file(MAKE_DIRECTORY ${MEX_OUTPUT_DIR}) # Add each MEX file as a separate target matlab_add_mex(NAME image_mex SRC ImageMex.cpp) matlab_add_mex(NAME binarize_mex SRC BinarizeMex.cpp) matlab_add_mex(NAME update_to_binary_mex SRC UpdateToBinaryMex.cpp) matlab_add_mex(NAME calculate_performance_mex SRC CalculatePerformanceMex.cpp) # All MEX targets set(MEX_TARGETS image_mex binarize_mex update_to_binary_mex calculate_performance_mex) # Set output directory for all MEX targets set_property(TARGET ${MEX_TARGETS} PROPERTY RUNTIME_OUTPUT_DIRECTORY ${MEX_OUTPUT_DIR}) # SIMD optimizations option(DOXA_ENABLE_SIMD "Enable SIMD optimizations" ON) if(DOXA_ENABLE_SIMD) if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|x64") if(NOT MSVC) foreach(target ${MEX_TARGETS}) target_compile_options(${target} PRIVATE -msse2) endforeach() endif() elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") # ARM64: NEON is enabled by default endif() endif() # Copy test files to the build directory (for CTest) file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/test/TestDoxa.m DESTINATION ${MEX_OUTPUT_DIR}) # Copy +Doxa package alongside the MEX files. # TARGET_FILE_DIR resolves to the correct output directory on both # single-config (mex/) and multi-config (mex/Release/) generators. add_custom_command(TARGET image_mex POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/+Doxa $/+Doxa ) # Enable testing and add the MATLAB tests to CTest enable_testing() matlab_add_unit_test( NAME MatlabTests UNITTEST_FILE ${MEX_OUTPUT_DIR}/TestDoxa.m ADDITIONAL_PATH ${MEX_OUTPUT_DIR}/$ ) ================================================ FILE: Bindings/Matlab/CalculatePerformanceMex.cpp ================================================ // Δoxa Binarization Framework // License: CC0 2026, "Freely you have received; freely give." - Matt 10:8 #include "mex.h" #include "DoxaMexUtils.hpp" /// /// MEX function to calculate performance metrics between two Doxa.Image handles. /// /// Signatures: /// metrics = calculate_performance_mex(gt_handle, bin_handle) /// metrics = calculate_performance_mex(gt_handle, bin_handle, p_weights, r_weights) /// void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[]) { if (nrhs != 2 && nrhs != 4) { mexErrMsgIdAndTxt("Doxa:calculatePerformance:InvalidInput", "Usage: calculate_performance_mex(gt_handle, bin_handle, [p_weights, r_weights])"); } // 1. Get Images from handles Doxa::Image* groundTruthImage = DoxaMexUtils::HandleToImage(prhs[0]); Doxa::Image* binaryImage = DoxaMexUtils::HandleToImage(prhs[1]); if (groundTruthImage->width != binaryImage->width || groundTruthImage->height != binaryImage->height) { mexErrMsgIdAndTxt("Doxa:calculatePerformance:MismatchedDimensions", "Input images must have the same dimensions."); } // 2. Compute classifications (with or without pseudo-weights) Doxa::ClassifiedPerformance::Classifications classifications; bool hasPseudoWeights = false; if (nrhs == 4 && !mxIsEmpty(prhs[2]) && !mxIsEmpty(prhs[3])) { std::vector precisionWeights = DoxaMexUtils::MxArrayToDoubleVector(prhs[2]); std::vector recallWeights = DoxaMexUtils::MxArrayToDoubleVector(prhs[3]); Doxa::ClassifiedPerformance::CompareImages( classifications, *groundTruthImage, *binaryImage, precisionWeights, recallWeights); hasPseudoWeights = true; } else { Doxa::ClassifiedPerformance::CompareImages( classifications, *groundTruthImage, *binaryImage); } // 3. Build output struct with all metrics int numFields = hasPseudoWeights ? 11 : 8; const char* fieldNames[] = { "accuracy", "fm", "recall", "precision", "psnr", "nrm", "mcc", "drdm", "pseudoFM", "pseudoPrecision", "pseudoRecall" }; plhs[0] = mxCreateStructMatrix(1, 1, numFields, fieldNames); mxSetField(plhs[0], 0, "accuracy", mxCreateDoubleScalar(Doxa::ClassifiedPerformance::CalculateAccuracy(classifications))); mxSetField(plhs[0], 0, "fm", mxCreateDoubleScalar(Doxa::ClassifiedPerformance::CalculateFMeasure(classifications))); mxSetField(plhs[0], 0, "recall", mxCreateDoubleScalar(Doxa::ClassifiedPerformance::CalculateRecall(classifications))); mxSetField(plhs[0], 0, "precision", mxCreateDoubleScalar(Doxa::ClassifiedPerformance::CalculatePrecision(classifications))); mxSetField(plhs[0], 0, "psnr", mxCreateDoubleScalar(Doxa::ClassifiedPerformance::CalculatePSNR(classifications))); mxSetField(plhs[0], 0, "nrm", mxCreateDoubleScalar(Doxa::ClassifiedPerformance::CalculateNRM(classifications))); mxSetField(plhs[0], 0, "mcc", mxCreateDoubleScalar(Doxa::ClassifiedPerformance::CalculateMCC(classifications))); mxSetField(plhs[0], 0, "drdm", mxCreateDoubleScalar(Doxa::DRDM::CalculateDRDM(*groundTruthImage, *binaryImage))); if (hasPseudoWeights) { mxSetField(plhs[0], 0, "pseudoFM", mxCreateDoubleScalar(Doxa::ClassifiedPerformance::CalculatePseudoFMeasure(classifications))); mxSetField(plhs[0], 0, "pseudoPrecision", mxCreateDoubleScalar(Doxa::ClassifiedPerformance::CalculatePseudoPrecision(classifications))); mxSetField(plhs[0], 0, "pseudoRecall", mxCreateDoubleScalar(Doxa::ClassifiedPerformance::CalculatePseudoRecall(classifications))); } } ================================================ FILE: Bindings/Matlab/Doxa.prj ================================================ Doxa Brandon M. Petty brandonpetty1981@gmail.com/param.email> Fast, header-only C++ image binarization framework with MATLAB bindings Doxa is an image binarization library focusing on local adaptive thresholding algorithms. It provides 13 binarization algorithms including Sauvola, Niblack, Wolf, and others, with performance metrics (Pseudo F-Measure, PSNR, DRDM) and grayscale conversion options. ${PROJECT_ROOT}\..\..\Demo\2JohnC1V3.png 1.0.0 ${PROJECT_ROOT}\Doxa.mltbx a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d % List files contained in your toolbox folder that you would like to exclude % from packaging. Excludes should be listed relative to the toolbox folder. % Some examples of how to specify excludes are provided below: % % A single file in the toolbox folder: % .svn % % A single file in a subfolder of the toolbox folder: % example/.svn % % All files in a subfolder of the toolbox folder: % example/* % % All files of a certain name in all subfolders of the toolbox folder: % **/.svn % % All files matching a pattern in all subfolders of the toolbox folder: % **/*.bak % build build-* CMakeLists.txt *.cpp *.hpp test .git* *.yml true false R2019b latest false true true true false ${PROJECT_ROOT} ${PROJECT_ROOT}\+Doxa ${PROJECT_ROOT}\README.md ${PROJECT_ROOT}\Doxa.mltbx ${MATLAB_ROOT} false false true false false false false false 10.0 false true win64 true ================================================ FILE: Bindings/Matlab/DoxaMexUtils.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2026, "Freely you have received; freely give." - Matt 10:8 #ifndef DOXAMEXUTILS_HPP #define DOXAMEXUTILS_HPP #include "mex.h" #include "Doxa/Image.hpp" #include "Doxa/Parameters.hpp" #include "Doxa/BinarizationFactory.hpp" #include "Doxa/ClassifiedPerformance.hpp" #include "Doxa/DRDM.hpp" #include "Doxa/Grayscale.hpp" #include #include #include #include namespace DoxaMexUtils { /// /// Casts a uint64 scalar mxArray handle to a Doxa::Image pointer with validation. /// inline Doxa::Image* HandleToImage(const mxArray* arr) { if (!mxIsUint64(arr) || !mxIsScalar(arr)) { mexErrMsgIdAndTxt("Doxa:InvalidHandle", "Expected a Doxa.Image handle (uint64 scalar)."); } uint64_t handle = *static_cast(mxGetData(arr)); if (handle == 0) { mexErrMsgIdAndTxt("Doxa:NullHandle", "Image handle is null or has been destroyed."); } return reinterpret_cast(static_cast(handle)); } /// /// Wraps a Doxa::Image pointer into a uint64 scalar mxArray. /// inline mxArray* ImageToHandle(Doxa::Image* image) { mxArray* arr = mxCreateNumericMatrix(1, 1, mxUINT64_CLASS, mxREAL); *static_cast(mxGetData(arr)) = static_cast(reinterpret_cast(image)); return arr; } /// /// Creates a new Doxa::Image from a Matlab 2D uint8 array, transposing from column-major to row-major. /// inline Doxa::Image* CreateImageFromMxArray(const mxArray* arr) { if (!mxIsUint8(arr) || mxGetNumberOfDimensions(arr) != 2) { mexErrMsgIdAndTxt("Doxa:InvalidImage", "Image must be a 2D uint8 matrix."); } const mwSize* dims = mxGetDimensions(arr); int height = static_cast(dims[0]); int width = static_cast(dims[1]); const uint8_t* matlabData = static_cast(mxGetData(arr)); Doxa::Image* image = new Doxa::Image(width, height); // Transpose: col-major (Matlab) → row-major (Doxa) for (int row = 0; row < height; ++row) { for (int col = 0; col < width; ++col) { image->data[row * width + col] = matlabData[col * height + row]; } } return image; } /// /// Creates a new Matlab 2D uint8 mxArray from a Doxa::Image, transposing from row-major to column-major. /// inline mxArray* ImageToMxArray(const Doxa::Image& image) { mwSize dims[2] = { (mwSize)image.height, (mwSize)image.width }; mxArray* arr = mxCreateNumericArray(2, dims, mxUINT8_CLASS, mxREAL); uint8_t* matlabData = static_cast(mxGetData(arr)); // Transpose: row-major (Doxa) → col-major (Matlab) for (int row = 0; row < image.height; ++row) { for (int col = 0; col < image.width; ++col) { matlabData[col * image.height + row] = image.data[row * image.width + col]; } } return arr; } /// /// Creates a new Doxa::Image from a Matlab 3D uint8 color array by converting to grayscale. /// Matlab stores [H,W,C] as C separate column-major planes; Doxa expects interleaved row-major RGB. /// inline Doxa::Image* CreateImageFromGrayscale(const mxArray* arr, Doxa::GrayscaleAlgorithms algorithm) { if (!mxIsUint8(arr)) { mexErrMsgIdAndTxt("Doxa:InvalidImage", "Color image must be a uint8 array."); } mwSize ndims = mxGetNumberOfDimensions(arr); const mwSize* dims = mxGetDimensions(arr); if (ndims != 3 || (dims[2] != 3 && dims[2] != 4)) { mexErrMsgIdAndTxt("Doxa:InvalidImage", "Color image must be [H,W,3] or [H,W,4] uint8."); } int height = static_cast(dims[0]); int width = static_cast(dims[1]); int channels = static_cast(dims[2]); const uint8_t* matlabData = static_cast(mxGetData(arr)); int pixelCount = width * height; // Reorder Matlab planar col-major to interleaved row-major std::vector interleaved(pixelCount * channels); for (int row = 0; row < height; ++row) { for (int col = 0; col < width; ++col) { int doxaIdx = (row * width + col) * channels; for (int c = 0; c < channels; ++c) { interleaved[doxaIdx + c] = matlabData[c * pixelCount + col * height + row]; } } } // Convert to grayscale using Doxa Doxa::Image* image = new Doxa::Image(width, height); Doxa::Grayscale::ToGrayscale(image->data, interleaved.data(), width, height, channels, algorithm); return image; } /// /// Converts a Matlab double array to a std::vector of doubles. /// inline std::vector MxArrayToDoubleVector(const mxArray* arr) { if (!mxIsDouble(arr)) { mexErrMsgIdAndTxt("Doxa:InvalidWeights", "Weights must be a double array."); } double* data = mxGetPr(arr); size_t n = mxGetNumberOfElements(arr); return std::vector(data, data + n); } /// /// Converts a Matlab struct into a Doxa::ParameterMap. /// /// /// Maps Matlab-friendly field names to C++ parameter names. /// Matlab struct fields cannot contain hyphens, so camelCase is mapped here. /// inline std::string MapParameterName(const std::string& matlabName) { static const std::unordered_map nameMap = { {"contrastLimit", "contrast-limit"} }; auto it = nameMap.find(matlabName); return (it != nameMap.end()) ? it->second : matlabName; } inline Doxa::Parameters MxStructToParameters(const mxArray* aStruct) { Doxa::ParameterMap paramMap; if (aStruct != nullptr && !mxIsEmpty(aStruct)) { if (!mxIsStruct(aStruct)) { mexErrMsgIdAndTxt("Doxa:InvalidParams", "Parameters must be a struct."); } int numFields = mxGetNumberOfFields(aStruct); for (int i = 0; i < numFields; ++i) { const char* matlabFieldName = mxGetFieldNameByNumber(aStruct, i); mxArray* fieldValue = mxGetFieldByNumber(aStruct, 0, i); std::string paramName = MapParameterName(matlabFieldName); if (mxIsScalar(fieldValue)) { if (mxIsDouble(fieldValue)) { paramMap[paramName] = mxGetScalar(fieldValue); } else { // Treat other numeric types as int paramMap[paramName] = static_cast(mxGetScalar(fieldValue)); } } } } return Doxa::Parameters(paramMap); } /// /// Converts a string algorithm name to a Doxa::Algorithms enum. /// inline Doxa::Algorithms StringToAlgorithmEnum(const std::string& algorithmStr) { static const std::unordered_map algorithmMap = { {"OTSU", Doxa::Algorithms::OTSU}, {"BERNSEN", Doxa::Algorithms::BERNSEN}, {"NIBLACK", Doxa::Algorithms::NIBLACK}, {"SAUVOLA", Doxa::Algorithms::SAUVOLA}, {"WOLF", Doxa::Algorithms::WOLF}, {"NICK", Doxa::Algorithms::NICK}, {"SU", Doxa::Algorithms::SU}, {"TRSINGH", Doxa::Algorithms::TRSINGH}, {"BATAINEH", Doxa::Algorithms::BATAINEH}, {"ISAUVOLA", Doxa::Algorithms::ISAUVOLA}, {"WAN", Doxa::Algorithms::WAN}, {"GATOS", Doxa::Algorithms::GATOS}, {"ADOTSU", Doxa::Algorithms::ADOTSU}, {"PHANSALKAR", Doxa::Algorithms::PHANSALKAR} }; auto it = algorithmMap.find(algorithmStr); if (it == algorithmMap.end()) { mexErrMsgIdAndTxt("Doxa:UnknownAlgorithm", "Unknown algorithm specified: %s", algorithmStr.c_str()); } return it->second; } /// /// Converts a string grayscale algorithm name to a Doxa::GrayscaleAlgorithms enum. /// inline Doxa::GrayscaleAlgorithms StringToGrayscaleEnum(const std::string& algorithmStr) { static const std::unordered_map grayscaleMap = { {"MEAN", Doxa::GrayscaleAlgorithms::MEAN}, {"QT", Doxa::GrayscaleAlgorithms::QT}, {"BT601", Doxa::GrayscaleAlgorithms::BT601}, {"BT709", Doxa::GrayscaleAlgorithms::BT709}, {"BT2100", Doxa::GrayscaleAlgorithms::BT2100}, {"VALUE", Doxa::GrayscaleAlgorithms::VALUE}, {"LUSTER", Doxa::GrayscaleAlgorithms::LUSTER}, {"LIGHTNESS", Doxa::GrayscaleAlgorithms::LIGHTNESS}, {"MINAVG", Doxa::GrayscaleAlgorithms::MINAVG}, {"LABDIST", Doxa::GrayscaleAlgorithms::LABDIST} }; auto it = grayscaleMap.find(algorithmStr); if (it == grayscaleMap.end()) { mexErrMsgIdAndTxt("Doxa:UnknownGrayscale", "Unknown grayscale algorithm: %s", algorithmStr.c_str()); } return it->second; } } #endif // DOXAMEXUTILS_HPP ================================================ FILE: Bindings/Matlab/ImageMex.cpp ================================================ // Δoxa Binarization Framework // License: CC0 2026, "Freely you have received; freely give." - Matt 10:8 #include "mex.h" #include "DoxaMexUtils.hpp" /// /// MEX gateway for Doxa.Image lifecycle management. /// Dispatches on an action string to create, convert, extract, or destroy images. /// /// Actions: /// handle = image_mex('create', uint8_2d_array) /// handle = image_mex('from_grayscale', algorithm_str, uint8_3d_array) /// uint8_2d = image_mex('to_array', handle) /// width = image_mex('width', handle) /// height = image_mex('height', handle) /// image_mex('destroy', handle) /// void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[]) { if (nrhs < 1) { mexErrMsgIdAndTxt("Doxa:Image:InvalidInput", "Usage: image_mex(action, ...)"); } std::string action = mxArrayToString(prhs[0]); if (action == "create") { // create(uint8_2d_array) → handle if (nrhs != 2) { mexErrMsgIdAndTxt("Doxa:Image:InvalidInput", "Usage: image_mex('create', uint8_2d_array)"); } Doxa::Image* image = DoxaMexUtils::CreateImageFromMxArray(prhs[1]); plhs[0] = DoxaMexUtils::ImageToHandle(image); } else if (action == "from_grayscale") { // from_grayscale(algorithm_str, uint8_3d_array) → handle if (nrhs != 3) { mexErrMsgIdAndTxt("Doxa:Image:InvalidInput", "Usage: image_mex('from_grayscale', algorithm, color_array)"); } std::string algorithmStr = mxArrayToString(prhs[1]); Doxa::GrayscaleAlgorithms algorithm = DoxaMexUtils::StringToGrayscaleEnum(algorithmStr); Doxa::Image* image = DoxaMexUtils::CreateImageFromGrayscale(prhs[2], algorithm); plhs[0] = DoxaMexUtils::ImageToHandle(image); } else if (action == "to_array") { // to_array(handle) → uint8_2d_array if (nrhs != 2) { mexErrMsgIdAndTxt("Doxa:Image:InvalidInput", "Usage: image_mex('to_array', handle)"); } Doxa::Image* image = DoxaMexUtils::HandleToImage(prhs[1]); plhs[0] = DoxaMexUtils::ImageToMxArray(*image); } else if (action == "width") { // width(handle) → double if (nrhs != 2) { mexErrMsgIdAndTxt("Doxa:Image:InvalidInput", "Usage: image_mex('width', handle)"); } Doxa::Image* image = DoxaMexUtils::HandleToImage(prhs[1]); plhs[0] = mxCreateDoubleScalar(static_cast(image->width)); } else if (action == "height") { // height(handle) → double if (nrhs != 2) { mexErrMsgIdAndTxt("Doxa:Image:InvalidInput", "Usage: image_mex('height', handle)"); } Doxa::Image* image = DoxaMexUtils::HandleToImage(prhs[1]); plhs[0] = mxCreateDoubleScalar(static_cast(image->height)); } else if (action == "destroy") { // destroy(handle) → void if (nrhs != 2) { mexErrMsgIdAndTxt("Doxa:Image:InvalidInput", "Usage: image_mex('destroy', handle)"); } Doxa::Image* image = DoxaMexUtils::HandleToImage(prhs[1]); delete image; } else { mexErrMsgIdAndTxt("Doxa:Image:UnknownAction", "Unknown action: %s", action.c_str()); } } ================================================ FILE: Bindings/Matlab/README.md ================================================ # Δoxa Binarization Framework - Matlab ## Introduction Doxa is an image binarization library focusing on local adaptive thresholding algorithms. In English, this means that it has the ability to turn a color or gray scale image into a black and white image. This binding provides a simple, high-level interface for using the Doxa C++ framework directly within Matlab, with idiomatic naming and a `Doxa.*` package namespace. ## Build & Test ### Using CMake Presets (Recommended) ```sh # From the project root cmake --preset matlab cmake --build build-matlab --config Release ctest --test-dir build-matlab -C Release ``` ### Package Toolbox (.mltbx) After building MEX files, create the distributable toolbox from MATLAB: ```matlab cd Bindings/Matlab matlab.addons.toolbox.packageToolbox('Doxa.prj') % Produces Doxa.mltbx ``` ## Matlab Example Add the build output directory to your Matlab path, then use the `Doxa.*` API. ```matlab % Add the build directory to the path addpath('path/to/build-matlab/mex'); % Read a color image and convert to grayscale img = Doxa.Image('photo.ppm', Doxa.Grayscale.QT); % Binarize with Sauvola (name-value parameters) binary = Doxa.binarize(Doxa.Algorithms.SAUVOLA, img, window=75, k=0.2); % Display the result imshow(binary.toArray()); % Or binarize in-place for efficiency Doxa.updateToBinary(Doxa.Algorithms.SAUVOLA, img, window=75, k=0.2); ``` ## Algorithms All 14 binarization algorithms are available via `Doxa.Algorithms`: | Global | Local Adaptive | |--------|---------------| | `OTSU` | `BERNSEN`, `NIBLACK`, `SAUVOLA`, `WOLF`, `NICK`, `SU`, `TRSINGH`, `BATAINEH`, `PHANSALKAR`, `ISAUVOLA`, `WAN`, `GATOS`, `ADOTSU` | ## Grayscale Conversion Ten grayscale algorithms are available via `Doxa.Grayscale`: `MEAN`, `QT`, `BT601`, `BT709`, `BT2100`, `VALUE`, `LUSTER`, `LIGHTNESS`, `MINAVG`, `LABDIST` ```matlab % Smart constructor handles files, 2D arrays, and 3D color arrays img = Doxa.Image('color.ppm', Doxa.Grayscale.MEAN); img = Doxa.Image(rgb_array, Doxa.Grayscale.QT); img = Doxa.Image(gray_array); ``` ## Performance Metrics ```matlab % Calculate metrics metrics = Doxa.calculatePerformance(gt, binary); % With weight files pw = Doxa.readWeights('precision_weights.dat'); rw = Doxa.readWeights('recall_weights.dat'); metrics = Doxa.calculatePerformance(gt, binary, precisionWeights=pw, recallWeights=rw); ``` **Metrics:** accuracy, fm, recall, precision, psnr, nrm, mcc, drdm, pseudoFM, pseudoPrecision, pseudoRecall ## License CC0 - Brandon M. Petty, 2026 To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. [View Online](https://creativecommons.org/publicdomain/zero/1.0/legalcode) "*Freely you have received; freely give.*" - Matt 10:8 ================================================ FILE: Bindings/Matlab/UpdateToBinaryMex.cpp ================================================ // Δoxa Binarization Framework // License: CC0 2026, "Freely you have received; freely give." - Matt 10:8 #include "mex.h" #include "DoxaMexUtils.hpp" /// /// MEX function to binarize a Doxa.Image in-place. /// Matlab Signature: update_in_place_mex(algorithm_name, image_handle, params_struct) /// void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[]) { if (nrhs < 2 || nrhs > 3) { mexErrMsgIdAndTxt("Doxa:updateToBinary:InvalidInput", "Usage: update_in_place_mex(algorithm, image_handle, params_struct)"); } if (nlhs > 0) { mexErrMsgIdAndTxt("Doxa:updateToBinary:InvalidOutput", "updateToBinary modifies the image in-place and has no return values."); } // 1. Get Algorithm Enum std::string algorithmStr = mxArrayToString(prhs[0]); Doxa::Algorithms algorithmEnum = DoxaMexUtils::StringToAlgorithmEnum(algorithmStr); // 2. Get Image from handle (already row-major, operates in-place) Doxa::Image* image = DoxaMexUtils::HandleToImage(prhs[1]); // 3. Get Parameters const mxArray* paramsMx = (nrhs == 3) ? prhs[2] : nullptr; Doxa::Parameters params = DoxaMexUtils::MxStructToParameters(paramsMx); // 4. Run algorithm in-place on the C++ buffer Doxa::IAlgorithm* algorithm = Doxa::BinarizationFactory::Algorithm(algorithmEnum); algorithm->Initialize(*image); algorithm->ToBinary(*image, params); delete algorithm; } ================================================ FILE: Bindings/Matlab/test/TestDoxa.m ================================================ classdef TestDoxa < matlab.unittest.TestCase % Test suite for the Doxa Matlab bindings. properties TestData end methods (TestClassSetup) function loadTestData(testCase) % Locate test resources baseDir = fileparts(mfilename('fullpath')); resourceDir = fullfile(baseDir, '..', '..', 'Doxa.Test', 'Resources'); % Store resource dir for weight files testCase.TestData.resourceDir = resourceDir; % Create grayscale image using Qt algorithm (matches ground truth generation) rgb = imread(fullfile(resourceDir, '2JohnC1V3.ppm')); testCase.TestData.rgb = rgb; testCase.TestData.grayscaleImage = Doxa.Image(rgb, Doxa.Grayscale.QT); % Load ground truth images testCase.TestData.groundTruth = Doxa.Image(fullfile(resourceDir, '2JohnC1V3-GroundTruth.pbm')); testCase.TestData.sauvolaGroundTruth = Doxa.Image(fullfile(resourceDir, '2JohnC1V3-Sauvola.pbm')); % Keep raw ground truth for comparison testCase.TestData.sauvolaGroundTruthArray = ... uint8(imread(fullfile(resourceDir, '2JohnC1V3-Sauvola.pbm'))) * 255; end end methods (Test) function testBinarize(testCase) % Tests the binarize function for correctness. binary = Doxa.binarize(Doxa.Algorithms.SAUVOLA, ... testCase.TestData.grayscaleImage, window=27, k=0.10); result = binary.toArray(); % Verify binary output (only 0 and 255) testCase.verifyEqual(unique(result(:)), uint8([0; 255])); % Verify against ground truth testCase.verifyEqual(result, testCase.TestData.sauvolaGroundTruthArray, ... 'Binarized image does not match ground truth.'); end function testUpdateToBinary(testCase) % Tests the in-place update function. img = Doxa.Image(testCase.TestData.grayscaleImage.toArray()); Doxa.updateToBinary(Doxa.Algorithms.SAUVOLA, img, window=27, k=0.10); testCase.verifyEqual(img.toArray(), testCase.TestData.sauvolaGroundTruthArray, ... 'In-place binarized image does not match ground truth.'); end function testCalculatePerformance(testCase) % Tests performance metrics match Python/C++ expected values. metrics = Doxa.calculatePerformance( ... testCase.TestData.groundTruth, ... testCase.TestData.sauvolaGroundTruth); testCase.verifyEqual(metrics.accuracy, 97.671, 'RelTol', 1e-2); testCase.verifyEqual(metrics.fm, 93.204, 'RelTol', 1e-2); testCase.verifyEqual(metrics.recall, 91.381, 'RelTol', 1e-2); testCase.verifyEqual(metrics.precision, 95.103, 'RelTol', 1e-2); testCase.verifyEqual(metrics.psnr, 16.329, 'RelTol', 1e-2); testCase.verifyEqual(metrics.nrm, 0.048, 'RelTol', 1e-2); testCase.verifyEqual(metrics.mcc, 0.918, 'RelTol', 1e-2); testCase.verifyEqual(metrics.drdm, 1.952, 'RelTol', 1e-2); end function testPseudoPerformance(testCase) % Tests pseudo-metrics with weight files. pWeights = Doxa.readWeights(fullfile( ... testCase.TestData.resourceDir, '2JohnC1V3-GroundTruth_PWeights.dat')); rWeights = Doxa.readWeights(fullfile( ... testCase.TestData.resourceDir, '2JohnC1V3-GroundTruth_RWeights.dat')); metrics = Doxa.calculatePerformance( ... testCase.TestData.groundTruth, ... testCase.TestData.sauvolaGroundTruth, ... precisionWeights=pWeights, recallWeights=rWeights); testCase.verifyEqual(metrics.pseudoFM, 93.393, 'RelTol', 1e-2); testCase.verifyEqual(metrics.pseudoRecall, 92.795, 'RelTol', 1e-2); testCase.verifyEqual(metrics.pseudoPrecision, 93.998, 'RelTol', 1e-2); end function testGrayscale(testCase) % Tests grayscale conversion matches manual Qt formula. rgb = testCase.TestData.rgb; img = Doxa.Image(rgb, Doxa.Grayscale.QT); result = img.toArray(); % Compute expected using Qt formula r = double(rgb(:,:,1)); g = double(rgb(:,:,2)); b = double(rgb(:,:,3)); expected = uint8((r * 11 + g * 16 + b * 5) / 32); testCase.verifyEqual(result, expected, 'AbsTol', uint8(1), ... 'Grayscale conversion does not match Qt formula.'); end function testImageFromFile(testCase) % Tests that loading from file matches loading from array. resourceDir = testCase.TestData.resourceDir; imgFromFile = Doxa.Image(fullfile(resourceDir, '2JohnC1V3.ppm'), Doxa.Grayscale.QT); imgFromArray = Doxa.Image(imread(fullfile(resourceDir, '2JohnC1V3.ppm')), Doxa.Grayscale.QT); testCase.verifyEqual(imgFromFile.toArray(), imgFromArray.toArray(), ... 'File-based and array-based construction produce different results.'); end function testReadWeights(testCase) % Tests weight file loading. weights = Doxa.readWeights(fullfile( ... testCase.TestData.resourceDir, '2JohnC1V3-GroundTruth_PWeights.dat')); testCase.verifyFalse(isempty(weights)); testCase.verifyTrue(isa(weights, 'double')); testCase.verifyGreaterThan(numel(weights), 0); end function testImageDisplay(testCase) % Tests that disp works without error. img = testCase.TestData.grayscaleImage; testCase.verifyWarningFree(@() disp(img)); end function testAllAlgorithmsRun(testCase) % Ensures all algorithms can be called without error. algorithms = enumeration('Doxa.Algorithms'); img = testCase.TestData.grayscaleImage; for i = 1:length(algorithms) alg = algorithms(i); testCase.verifyWarningFree(@() Doxa.binarize(alg, img), ... ['Algorithm ' char(alg) ' failed to run.']); end end end end ================================================ FILE: Bindings/Python/.gitignore ================================================ build dist src/Doxa __pycache__ ================================================ FILE: Bindings/Python/CMakeLists.txt ================================================ message(STATUS "DoxaPy - CMake Build") cmake_minimum_required(VERSION 3.16...3.27) project(doxapy) if (CMAKE_VERSION VERSION_LESS 3.18) set(DEV_MODULE Development) else() set(DEV_MODULE Development.Module) endif() find_package(Python 3.12 REQUIRED COMPONENTS Interpreter ${DEV_MODULE} OPTIONAL_COMPONENTS Development.SABIModule ) if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "RelWithDebInfo") endif() message(STATUS "Build Type: ${CMAKE_BUILD_TYPE}") set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_compile_definitions(NB_TARGET_ABI_VERSION=312) # SIMD support for Python bindings option(DOXAPY_ENABLE_SIMD "Enable SIMD in Python bindings" ON) if(MSVC) # so far so good else() add_compile_options("-Wno-narrowing") endif() # Detect the installed nanobind package and import it into CMake execute_process( COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT ) find_package(nanobind CONFIG REQUIRED) include_directories(./src/Doxa) nanobind_add_module( doxapy NB_STATIC STABLE_ABI LTO NOMINSIZE src/DoxaPy.cpp ) # Speed optimization for header-only library (algorithms compile within binding module) target_compile_options(doxapy PRIVATE $<$>,$>:/O2> $<$>,$>>:-O3> ) # Platform-specific SIMD flags (same logic as test) if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|x64") # x86-64: SSE2 is always available (baseline for x64) if(MSVC) # MSVC x64: SSE2 is enabled by default, no special flags needed message(STATUS "DoxaPy SIMD: x86-64 with SSE2 (MSVC)") else() # GCC/Clang: SSE2 is default for x64, but be explicit target_compile_options(doxapy PRIVATE -msse2) message(STATUS "DoxaPy SIMD: x86-64 with SSE2 (GCC/Clang)") endif() elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") message(STATUS "DoxaPy SIMD: ARM64 with NEON") endif() # Copy built module + __init__.py to dist/doxapy/ as a proper package add_custom_command(TARGET doxapy POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_SOURCE_DIR}/dist/doxapy COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_CURRENT_SOURCE_DIR}/dist/doxapy/ COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/src/doxapy/__init__.py ${CMAKE_CURRENT_SOURCE_DIR}/dist/doxapy/ COMMENT "Copying doxapy package to dist/" ) install(TARGETS doxapy LIBRARY DESTINATION doxapy) ================================================ FILE: Bindings/Python/DoxaPy.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "id": "17ccb794-7c43-4c40-89ff-693df6b7b513", "metadata": {}, "source": [ "# DoxaPy Notebook\n", "\n", "https://github.com/brandonmpetty/Doxa\n", "\n", "DoxaPy is an image binarization library focused on local adaptive algorithms and metrics.\n", "This notebook will document the API while allowing you to interact with it.\n", "\n", "## Setup\n", "The first thing to do when getting started with this library is to install it.\n", "```\n", "pip install doxapy\n", "```\n", "Form more details, see: https://pypi.org/project/doxapy\n", "\n", "Alternatively, you can build the library from source as described in the README.MD.\n", "\n", "From there, it is as simple as importing the library. NumPy and Pillow are two other libraries we will use in this demonstration." ] }, { "cell_type": "code", "execution_count": null, "id": "532b64af-c7f0-4f68-87fe-c17d3322c219", "metadata": {}, "outputs": [], "source": [ "# Prioritize a local build first\n", "import sys, os\n", "sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(\"__file__\")), \"dist\"))\n", "\n", "from PIL import Image\n", "import numpy as np\n", "import doxapy" ] }, { "cell_type": "markdown", "id": "5fd90377-1177-4e16-aeb8-b1da0a525269", "metadata": {}, "source": [ "## Reading an Image\n", "The first step is to read the image you intend on processing. The *read_image* helper function uses Pillow to read in a local image and convert it to grayscale. We then use NumPy to turn that image into an array.\n", "\n", "DoxaPy exposes a number of Color of Grayscale algorithms.\n", "\n", "### Grayscale Algorithms\n", "- **MEAN**\n", "- **QT**\n", "- **BT601**\n", "- **BT709**\n", "- **BT2100**\n", "- **VALUE**\n", "- **LUSTER**\n", "- **LIGHTNESS**\n", "- **MINAVG**" ] }, { "cell_type": "code", "execution_count": null, "id": "5993f8a0-a499-412f-b832-767d97c8efe9", "metadata": {}, "outputs": [], "source": [ "def read_image(file, algorithm=doxapy.GrayscaleAlgorithms.MEAN):\n", " '''Read an image. If it is color, turn it into 8 bit grayscale.'''\n", " image = Image.open(file)\n", "\n", " # If already in grayscale or binary, do not convert it\n", " if image.mode == 'L':\n", " return np.array(image)\n", " \n", " # Read the color image\n", " rgb_image = np.array(image.convert('RGB') if image.mode not in ('RGB', 'RGBA') else image)\n", "\n", " # Use Doxa to convert to grayscale\n", " return doxapy.to_grayscale(algorithm, rgb_image)" ] }, { "cell_type": "code", "execution_count": null, "id": "a475bd49-b1ec-4ebd-b146-8cb819817a18", "metadata": {}, "outputs": [], "source": [ "grayscale_image = read_image(\"../../Doxa.Test/Resources/2JohnC1V3.ppm\", doxapy.GrayscaleAlgorithms.LIGHTNESS)\n", "display(Image.fromarray(grayscale_image))" ] }, { "cell_type": "markdown", "id": "3665c6d0-0a4e-4950-a025-69ae0dae60e3", "metadata": {}, "source": [ "## Converting the Image to Binary\n", "Converting an image into black and white may seem easy, but it has been the focus of much research spanning decades. Doxa was designed to expose this research, traditionally mired by PHD technical jargon, in a very easy to consume fashion. A lot of work was put into ensuring these algorithms were implemented correctly and effeciently. Many of these algorithms were first made public by this project and many of them leverage state of the art enhacements to reduce memory utilization and increase speed of operation found nowhere else.\n", "\n", "### Algorithms\n", "The Doxa library implements a large number of popular and unique local adaptive binarization algorithms. Each algorithm has a set of parameters that are required for it to operate. These parameters can vary from algorithm to algorithm. Doxa provides sensible defaults that are applied automatically unless you supply your own. Below is a list of algorithms and their defaults:\n", "\n", "* **OTSU**\n", "* **BERNSEN** - {\"window\": 75, \"threshold\": 100, \"contrast-limit\": 25}\n", "* **NIBLACK** - {\"window\": 75, \"k\": 0.2}\n", "* **SAUVOLA** - {\"window\": 75, \"k\": 0.2}\n", "* **WOLF** - {\"window\": 75, \"k\": 0.2}\n", "* **NICK** - {\"window\": 75, \"k\": -0.2}\n", "* **SU** - {\"window\": 9, \"minN\": 9}\n", "* **TRSINGH** - {\"window\": 75, \"k\": 0.2}\n", "* **BATAINEH**\n", "* **ISAUVOLA** - {\"window\": 75, \"k\": 0.2}\n", "* **WAN** - {\"window\": 75, \"k\": 0.2}\n", "* **GATOS** - {\"window\": 75, \"k\": 0.2, \"glyph\": 60}\n", "* **ADOTSU** - {\"window\": 75, \"k\": 1.0, \"R\": 0.1, \"distance\": window/2}\n", "* **PHANSALKAR** - {\"window\": 75, \"k\": 0.2, \"p\": 3.0, \"q\": 10.0}" ] }, { "cell_type": "code", "execution_count": null, "id": "656a290b-9c83-4ee4-9eee-6a5126e1e574", "metadata": {}, "outputs": [], "source": [ "# Initialize an image array with the same shape as our grayscale image\n", "binary_image = doxapy.to_binary(doxapy.Binarization.Algorithms.SAUVOLA, grayscale_image, {\"window\": 75, \"k\": 0.2})\n", "display(Image.fromarray(binary_image))" ] }, { "cell_type": "markdown", "id": "58ae263b-3341-4388-b0f8-0bae06f8a13d", "metadata": {}, "source": [ "One of the quickest and most efficient ways of turning your grayscale image into a binary image is to use the *update_to_binary* function. Instead of allocating more memory to write the image to, it will update the existing image in-place. It also only takes one line to write!" ] }, { "cell_type": "code", "execution_count": null, "id": "6b444625-0b48-4197-b884-2f2ba591906e", "metadata": {}, "outputs": [], "source": [ "doxapy.update_to_binary(doxapy.Binarization.Algorithms.SAUVOLA, grayscale_image, {\"window\": 27, \"k\": 0.12})\n", "display(Image.fromarray(grayscale_image))" ] }, { "cell_type": "markdown", "id": "24d8b5dc-9b75-4c33-8814-50fc00082d40", "metadata": {}, "source": [ "## Performance Metrics\n", "In order to analyze the performance of an algorithm, Doxa provides a set of common metrics that can all be calculated with one function. To start that process you need an exemplar binary image, or \"ground truth.\" By comparing the ground truth to the resulting image of the binarization algorithm, you can start to compare the affects of different algorithms and algorithm parameters." ] }, { "cell_type": "code", "execution_count": null, "id": "70ba6fef-7a29-4dcd-a75b-5fa4cf016414", "metadata": {}, "outputs": [], "source": [ "groundtruth_image = read_image(\"../../Doxa.Test/Resources/2JohnC1V3-GroundTruth.pbm\")\n", "display(Image.fromarray(groundtruth_image))" ] }, { "cell_type": "code", "execution_count": null, "id": "73cddbae", "metadata": {}, "outputs": [], "source": [ "# Read our Pseudo F-Measure weights\n", "p_weights = doxapy.read_weights(\"../../Doxa.Test/Resources/2JohnC1V3-GroundTruth_PWeights.dat\")\n", "r_weights = doxapy.read_weights(\"../../Doxa.Test/Resources/2JohnC1V3-GroundTruth_RWeights.dat\")" ] }, { "cell_type": "code", "execution_count": null, "id": "90439c66-67d5-4b2d-a450-5c1c823f2414", "metadata": {}, "outputs": [], "source": [ "# Help us 'pretty print' our JSON\n", "import json\n", "\n", "# Both of these were done with the Sauvola algorithm, but with different parameters\n", "# NOTE: grayscale_image was updated in-place above into binary \n", "performance1 = doxapy.calculate_performance(groundtruth_image, binary_image, p_weights, r_weights)\n", "performance2 = doxapy.calculate_performance_ex(groundtruth_image, grayscale_image, drdm=True, accuracy=True, mcc=True)\n", "\n", "print(\"Sauvola - Window = 75, K = 0.2\") # Default\n", "print(json.dumps(performance1, indent=2))\n", "print()\n", "print(\"Sauvola - Window = 27, K = 0.12\") # Adjusted\n", "print(json.dumps(performance2, indent=2))" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.3" } }, "nbformat": 4, "nbformat_minor": 5 } ================================================ FILE: Bindings/Python/README.md ================================================ # Δoxa Binarization Framework - Python ## Introduction DoxaPy is an image binarization library focusing on local adaptive thresholding algorithms. In English, this means that it has the ability to turn a color or gray scale image into a black and white image. **Algorithms** * Otsu - "A threshold selection method from gray-level histograms", 1979. * Bernsen - "Dynamic thresholding of gray-level images", 1986. * Niblack - "An Introduction to Digital Image Processing", 1986. * Sauvola - "Adaptive document image binarization", 1999. * Wolf - "Extraction and Recognition of Artificial Text in Multimedia Documents", 2003. * Gatos - "Adaptive degraded document image binarization", 2005. (Partial) * NICK - "Comparison of Niblack inspired Binarization methods for ancient documents", 2009. * AdOtsu - "A multi-scale framework for adaptive binarization of degraded document images", 2010. * Su - "Binarization of Historical Document Images Using the Local Maximum and Minimum", 2010. * T.R. Singh - "A New local Adaptive Thresholding Technique in Binarization", 2011. * Bataineh - "An adaptive local binarization method for document images based on a novel thresholding method and dynamic windows", 2011. (unreproducible) * Phansalkar - "Adaptive Local Thresholding for Detection of Nuclei in Diversely Stained Cytology Images", 2011. * ISauvola - "ISauvola: Improved Sauvola's Algorithm for Document Image Binarization", 2016. * WAN - "Binarization of Document Image Using Optimum Threshold Modification", 2018. **Optimizations** * Shafait - "Efficient Implementation of Local Adaptive Thresholding Techniques Using Integral Images", 2008. * Petty - An algorithm for efficiently calculating the min and max of a local window. Unpublished, 2019. * Chan - "Memory-efficient and fast implementation of local adaptive binarization methods", 2019. * SIMD - SSE2, ARM NEON **Performance Metrics** * Overall Accuracy * F-Measure, Precision, Recall * Pseudo F-Measure, Precision, Recall - "Performance Evaluation Methodology for Historical Document Image Binarization", 2013. * Peak Signal-To-Noise Ratio (PSNR) * Negative Rate Metric (NRM) * Matthews Correlation Coefficient (MCC) * Distance-Reciprocal Distortion Measure (DRDM) - "An Objective Distortion Measure for Binary Document Images Based on Human Visual Perception", 2002. ## Overview DoxaPy uses the Δoxa Binarization Framework for quickly processing python Image files. It is comprised of three major sets of algorithms: Color to Grayscale, Grayscale to Binary, and Performance Metrics. It can be used as a full DIBCO Metrics replacement that is significantly smaller, faster, and easier to integrate into existing projects. ### Example This short demo uses DoxaPy to read in a color image, converts it to binary, and then compares it to a Ground Truth image in order to calculate performance. ```python from PIL import Image import numpy as np import doxapy def read_image(file, algorithm=doxapy.GrayscaleAlgorithms.MEAN): """Read an image. If its color, use one of our many Grayscale algorithms to convert it.""" image = Image.open(file) # If already in grayscale or binary, do not convert it if image.mode == 'L': return np.array(image) # Read the color image rgb_image = np.array(image.convert('RGB') if image.mode not in ('RGB', 'RGBA') else image) # Use Doxa to convert grayscale return doxapy.to_grayscale(algorithm, rgb_image) # Read our target image and convert it to grayscale grayscale_image = read_image("2JohnC1V3.png") # Convert the grayscale image to a binary image (algorithm parameters optional) binary_image = doxapy.to_binary(doxapy.Binarization.Algorithms.SAUVOLA, grayscale_image, {"window": 75, "k": 0.2}) # Calculate the binarization performance using a Ground Truth image groundtruth_image = read_image("2JohnC1V3-GroundTruth.png") performance = doxapy.calculate_performance(groundtruth_image, binary_image) print(performance) # Display our resulting image Image.fromarray(binary_image).show() ``` ### DoxaPy Notebook For more details, open the [DoxaPy Notebook](https://github.com/brandonmpetty/Doxa/blob/master/Bindings/Python/DoxaPy.ipynb) and to get an interactive demo. ## Building and Test DoxaPy supports 64b Linux, Windows, and Mac OSX on Python 3.x. Starting with DoxaPy 0.9.4, Python 3.12 and above are supported with full ABI compatibility. This means that new versions of DoxaPy will only be published due to feature enhancements, not Python version support. **Build from Project Root** ```bash # From the Doxa project root git clone --depth 1 https://github.com/brandonmpetty/Doxa.git cd Doxa cmake --preset python cmake --build build-python --config Release pip install -r Bindings/Python/requirements.txt ctest --test-dir build-python -C Release ``` **Local Package Build** ```bash python -m build ``` **Local Wheel Build** ```bash pip wheel . --no-deps ``` ## License CC0 - Brandon M. Petty, 2026 To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. [View Online](https://creativecommons.org/publicdomain/zero/1.0/legalcode) "*Freely you have received; freely give.*" - Matt 10:8 ================================================ FILE: Bindings/Python/copy-cpp-files.py ================================================ import glob, os, shutil from itertools import chain src_dir=os.path.join("..", "..", "Doxa") dst_dir=os.path.join("src", "Doxa") os.makedirs(dst_dir, exist_ok=True) # Copy only modified .HPP files, since the last run files = chain( glob.iglob(os.path.join(src_dir, "*.hpp")), glob.iglob(os.path.join(src_dir, "*.h")) ) for src_file in files: if os.path.isfile(src_file): dst_file=os.path.join(dst_dir, os.path.basename(src_file)) if not(os.path.isfile(dst_file)) or (os.stat(src_file).st_mtime - os.stat(dst_file).st_mtime > 0): shutil.copy2(src_file, dst_dir) print(f"Copying {src_file}") ================================================ FILE: Bindings/Python/pyproject.toml ================================================ [build-system] requires = ["cibuildwheel >= 2.22.0","scikit-build-core >= 0.11.3", "nanobind >= 2.7.0"] build-backend = "scikit_build_core.build" [project] name = "doxapy" version = "0.9.6" description = "An image binarization library focussing on local adaptive thresholding" readme = "README.md" authors = [ {name = "Brandon M. Petty", email = "brandonpetty1981@gmail.com"} ] license = {text = "CC0-1.0"} requires-python = ">=3.12" dependencies = [ "numpy>=1.20.0" ] [project.urls] Homepage = "https://github.com/brandonmpetty/doxa" [tool.scikit-build] cmake.build-type = "Release" minimum-version = "0.4" build-dir = "build/{wheel_tag}" wheel.py-api = "cp312" sdist.include = [ "src/Doxa" # Include all files in the Doxa directory ] sdist.exclude = [ "copy-cpp-files.py", "DoxaPy.ipynb", ".gitignore", "test", "dist" ] [tool.cibuildwheel] # Necessary to see build output from the actual compilation build-verbosity = 1 # Run tests to ensure correct builds #test-command = "python test/test_doxa.py" archs = ["auto64"] # Only target 64 bit architectures ================================================ FILE: Bindings/Python/requirements.txt ================================================ numpy>=1.20.0 Pillow>=8.0.0 build>=1.2.0 ================================================ FILE: Bindings/Python/src/DoxaPy.cpp ================================================ #include #include #include #include #include #include #include "Doxa/BinarizationFactory.hpp" #include "Doxa/ClassifiedPerformance.hpp" #include "Doxa/DRDM.hpp" #include "Doxa/DIBCOUtils.hpp" #include "Doxa/Grayscale.hpp" namespace nb = nanobind; using namespace Doxa; // Nanobind helper for converting from an array to a Doxa Image. This will reference the array, not create a copy. Image ArrayToImage(const nb::ndarray>& imageArray) { return Image::Reference(imageArray.shape(1), imageArray.shape(0), reinterpret_cast(imageArray.data())); } nb::dict CalculatePerformance( const nb::ndarray>& groundTruthImageArray, const nb::ndarray>& binaryImageArray, const std::vector& precisionWeights = {}, const std::vector& recallWeights = {}) { Image groundTruthImage = ArrayToImage(groundTruthImageArray); Image binaryImage = ArrayToImage(binaryImageArray); auto dict = nb::dict(); ClassifiedPerformance::Classifications classifications; if (!precisionWeights.empty() && !recallWeights.empty()) ClassifiedPerformance::CompareImages(classifications, groundTruthImage, binaryImage, precisionWeights, recallWeights); else ClassifiedPerformance::CompareImages(classifications, groundTruthImage, binaryImage); dict["accuracy"] = ClassifiedPerformance::CalculateAccuracy(classifications); dict["fm"] = ClassifiedPerformance::CalculateFMeasure(classifications); dict["recall"] = ClassifiedPerformance::CalculateRecall(classifications); dict["precision"] = ClassifiedPerformance::CalculatePrecision(classifications); dict["mcc"] = ClassifiedPerformance::CalculateMCC(classifications); dict["psnr"] = ClassifiedPerformance::CalculatePSNR(classifications); dict["nrm"] = ClassifiedPerformance::CalculateNRM(classifications); dict["drdm"] = DRDM::CalculateDRDM(groundTruthImage, binaryImage); if (!precisionWeights.empty() && !recallWeights.empty()) { dict["pseudo_fm"] = ClassifiedPerformance::CalculatePseudoFMeasure(classifications); dict["pseudo_precision"] = ClassifiedPerformance::CalculatePseudoPrecision(classifications); dict["pseudo_recall"] = ClassifiedPerformance::CalculatePseudoRecall(classifications); } return dict; } nb::dict CalculatePerformanceEx( const nb::ndarray>& groundTruthImageArray, const nb::ndarray>& binaryImageArray, bool accuracy = false, bool fm = false, bool recall = false, bool precision = false, bool mcc = false, bool psnr = false, bool nrm = false, bool drdm = false, bool pseudo_fm = false, bool pseudo_precision = false, bool pseudo_recall = false, const std::vector& precisionWeights = {}, const std::vector& recallWeights = {}) { Image groundTruthImage = ArrayToImage(groundTruthImageArray); Image binaryImage = ArrayToImage(binaryImageArray); auto dict = nb::dict(); const bool needsPseudo = (pseudo_fm || pseudo_precision || pseudo_recall) && !precisionWeights.empty() && !recallWeights.empty(); if (accuracy || fm || recall || precision || mcc || psnr || nrm || needsPseudo) { ClassifiedPerformance::Classifications classifications; if (needsPseudo) ClassifiedPerformance::CompareImages(classifications, groundTruthImage, binaryImage, precisionWeights, recallWeights); else ClassifiedPerformance::CompareImages(classifications, groundTruthImage, binaryImage); if (accuracy) dict["accuracy"] = ClassifiedPerformance::CalculateAccuracy(classifications); if (fm) dict["fm"] = ClassifiedPerformance::CalculateFMeasure(classifications); if (recall) dict["recall"] = ClassifiedPerformance::CalculateRecall(classifications); if (precision) dict["precision"] = ClassifiedPerformance::CalculatePrecision(classifications); if (mcc) dict["mcc"] = ClassifiedPerformance::CalculateMCC(classifications); if (psnr) dict["psnr"] = ClassifiedPerformance::CalculatePSNR(classifications); if (nrm) dict["nrm"] = ClassifiedPerformance::CalculateNRM(classifications); if (needsPseudo) { if (pseudo_fm) dict["pseudo_fm"] = ClassifiedPerformance::CalculatePseudoFMeasure(classifications); if (pseudo_precision) dict["pseudo_precision"] = ClassifiedPerformance::CalculatePseudoPrecision(classifications); if (pseudo_recall) dict["pseudo_recall"] = ClassifiedPerformance::CalculatePseudoRecall(classifications); } } if (drdm) { dict["drdm"] = DRDM::CalculateDRDM(groundTruthImage, binaryImage); } return dict; } nb::ndarray> ToGrayscale( GrayscaleAlgorithms algorithm, const nb::ndarray>& colorImageArray) { const int height = colorImageArray.shape(0); const int width = colorImageArray.shape(1); const int channels = colorImageArray.shape(2); Pixel8* output = new Pixel8[width * height]; Grayscale::ToGrayscale( output, reinterpret_cast(colorImageArray.data()), width, height, channels, algorithm); // Return an ND Array that will correctly manage the allocated memory const size_t shape[2] = { (size_t)height, (size_t)width }; nb::capsule owner(output, [](void* p) noexcept { delete[] static_cast(p); }); return nb::ndarray>(output, 2, shape, owner); } /// /// Binarization is a helper class to help interface the C++ Doxa framework with Python. /// It exposes through enumeration all of the algorithms supported by the library. /// class Binarization { public: Binarization(const Algorithms algorithm) : algorithm(algorithm) { algorithmPtr = BinarizationFactory::Algorithm(algorithm); } ~Binarization() { delete algorithmPtr; } void Initialize(const nb::ndarray>& grayScaleImageArray) { Image image = ArrayToImage(grayScaleImageArray); algorithmPtr->Initialize(image); } void ToBinary(const nb::ndarray>& binaryImageArray, const ParameterMap& parameters={}) { Image image = ArrayToImage(binaryImageArray); algorithmPtr->ToBinary(image, parameters); } Algorithms CurrentAlgorithm() { return algorithm; } protected: const Algorithms algorithm; IAlgorithm* algorithmPtr = nullptr; }; // Class: Binarization nb::ndarray> ToBinary( const Algorithms algorithm, const nb::ndarray>& grayImage, const ParameterMap& parameters = {}) { const int height = grayImage.shape(0); const int width = grayImage.shape(1); // Allocate new memory for the image data Pixel8* binaryImage = new Pixel8[width * height]; // Apply the algorithm const Image grayImageRef = ArrayToImage(grayImage); Image binaryImageRef = Image::Reference(width, height, binaryImage); IAlgorithm* algorithmPtr = BinarizationFactory::Algorithm(algorithm); algorithmPtr->Initialize(grayImageRef); algorithmPtr->ToBinary(binaryImageRef, parameters); delete algorithmPtr; // Return an ND Array that will correctly manage the allocated memory const size_t shape[2] = { (size_t)height, (size_t)width }; nb::capsule owner(binaryImage, [](void* p) noexcept { delete[] static_cast(p); }); return nb::ndarray>(binaryImage, 2, shape, owner); } void UpdateToBinary( const Algorithms algorithm, const nb::ndarray>& imageArray, const ParameterMap& parameters = {}) { Binarization binAlg(algorithm); binAlg.Initialize(imageArray); binAlg.ToBinary(imageArray, parameters); } // Expose routines using the PEP8 style guide NB_MODULE(doxapy, m) { m.doc() = "DoxaPy: Python bindings for the Doxa image binarization framework"; m.def("calculate_performance", &CalculatePerformance, nb::arg("groundTruthImageArray"), nb::arg("binaryImageArray"), nb::arg("precision_weights") = std::vector{}, nb::arg("recall_weights") = std::vector{}, "Obtain binarization performance information based on a Ground Truth. " "Optionally pass precision and recall weight arrays for pseudo-metrics."); m.def("calculate_performance_ex", &CalculatePerformanceEx, nb::arg("groundTruthImageArray"), nb::arg("binaryImageArray"), nb::arg("accuracy") = false, nb::arg("fm") = false, nb::arg("recall") = false, nb::arg("precision") = false, nb::arg("mcc") = false, nb::arg("psnr") = false, nb::arg("nrm") = false, nb::arg("drdm") = false, nb::arg("pseudo_fm") = false, nb::arg("pseudo_precision") = false, nb::arg("pseudo_recall") = false, nb::arg("precision_weights") = std::vector{}, nb::arg("recall_weights") = std::vector{}, "Obtain specific binarization performance information based on a Ground Truth. " "Pseudo-metrics require precision and recall weight arrays."); nb::enum_(m, "GrayscaleAlgorithms") .value("MEAN", GrayscaleAlgorithms::MEAN) .value("QT", GrayscaleAlgorithms::QT) .value("BT601", GrayscaleAlgorithms::BT601) .value("BT709", GrayscaleAlgorithms::BT709) .value("BT2100", GrayscaleAlgorithms::BT2100) .value("VALUE", GrayscaleAlgorithms::VALUE) .value("LUSTER", GrayscaleAlgorithms::LUSTER) .value("LIGHTNESS", GrayscaleAlgorithms::LIGHTNESS) .value("MINAVG", GrayscaleAlgorithms::MINAVG) .value("LABDIST", GrayscaleAlgorithms::LABDIST) .export_values(); m.def("to_grayscale", &ToGrayscale, nb::arg("algorithm"), nb::arg("color_image"), "Convert an RGB or RGBA image to 8-bit grayscale."); m.def("to_binary", &ToBinary, nb::arg("algorithm"), nb::arg("image"), nb::arg("parameters") = ParameterMap(), "Convert a grayscale image to binary, returning a new image."); m.def("update_to_binary", &UpdateToBinary, nb::arg("algorithm"), nb::arg("image"), nb::arg("parameters") = ParameterMap(), "Convert a grayscale image to binary in-place."); nb::class_ binarization(m, "Binarization"); binarization.def(nb::init()) .def("initialize", &Binarization::Initialize) .def("to_binary", &Binarization::ToBinary, nb::arg("binaryImageArray"), nb::arg("parameters") = ParameterMap()) .def("algorithm", &Binarization::CurrentAlgorithm); nb::enum_(binarization, "Algorithms") .value("OTSU", Algorithms::OTSU) .value("BERNSEN", Algorithms::BERNSEN) .value("NIBLACK", Algorithms::NIBLACK) .value("SAUVOLA", Algorithms::SAUVOLA) .value("WOLF", Algorithms::WOLF) .value("NICK", Algorithms::NICK) .value("SU", Algorithms::SU) .value("TRSINGH", Algorithms::TRSINGH) .value("BATAINEH", Algorithms::BATAINEH) .value("ISAUVOLA", Algorithms::ISAUVOLA) .value("WAN", Algorithms::WAN) .value("GATOS", Algorithms::GATOS) .value("ADOTSU", Algorithms::ADOTSU) .value("PHANSALKAR", Algorithms::PHANSALKAR) .export_values(); } ================================================ FILE: Bindings/Python/src/doxapy/__init__.py ================================================ from .doxapy import * def read_weights(file): with open(file) as f: return [float(x) for x in f.read().split()] ================================================ FILE: Bindings/Python/test/test_doxa.py ================================================ import unittest from PIL import Image, ImageChops import numpy as np import os import sys sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'dist'))) import doxapy # Get the absolute path to the RESOURCES directory (at project root) RESOURCES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'Doxa.Test', 'Resources')) def read_image(file, algorithm=doxapy.GrayscaleAlgorithms.QT): full_path = os.path.join(RESOURCES_DIR, os.path.basename(file)) image = Image.open(full_path) # If already in grayscale or binary, do not convert it if image.mode == 'L': return np.array(image) # Read the color image rgb_image = np.array(image.convert('RGB') if image.mode not in ('RGB', 'RGBA') else image) # Use Doxa to convert to grayscale return doxapy.to_grayscale(algorithm, rgb_image) def read_weights(file): full_path = os.path.join(RESOURCES_DIR, os.path.basename(file)) return doxapy.read_weights(full_path) class DoxaPyTests(unittest.TestCase): def test_binarization(self): # Read in a color image, transforming it into a grayscale image image = read_image("2JohnC1V3.ppm") # Create a new binary image binary_image = np.empty(image.shape, image.dtype) params = {"window": 27, "k": 0.10} sauvola = doxapy.Binarization(doxapy.Binarization.Algorithms.SAUVOLA) sauvola.initialize(image) sauvola.to_binary(binary_image, params) # Update the grayscale image to binary in-place doxapy.update_to_binary(doxapy.Binarization.Algorithms.SAUVOLA, image, params) # Ensure they are equal self.assertTrue((binary_image == image).all()) # Compare against our control expected_image_path = os.path.join(RESOURCES_DIR, os.path.basename("2JohnC1V3-Sauvola.pbm")) expected_image = Image.open(expected_image_path) #expected_image.save("expected_image.png") output_image = Image.fromarray(binary_image) #output_image.save('output_image.png') diff = ImageChops.difference(expected_image, output_image); #diff.show() self.assertIsNone(diff.getbbox()) def test_performance(self): # Setup image = read_image("2JohnC1V3.ppm") groundtruth_image = read_image("2JohnC1V3-GroundTruth.pbm") params = {"window": 27, "k": 0.10} doxapy.update_to_binary(doxapy.Binarization.Algorithms.SAUVOLA, image, params) # Functions under test performanceAll = doxapy.calculate_performance(groundtruth_image, image) performance = doxapy.calculate_performance_ex(groundtruth_image, image, drdm=True, psnr=True) # Assert All self.assertAlmostEqual(performanceAll.get("accuracy"), 97.671, 2) self.assertAlmostEqual(performanceAll.get("fm"), 93.204, 2) self.assertAlmostEqual(performanceAll.get("recall"), 91.3811, 2) self.assertAlmostEqual(performanceAll.get("precision"), 95.1025, 2) self.assertAlmostEqual(performanceAll.get("psnr"), 16.329, 2) self.assertAlmostEqual(performanceAll.get("nrm"), 0.048, 2) self.assertAlmostEqual(performanceAll.get("mcc"), 0.918, 2) self.assertAlmostEqual(performanceAll.get("drdm"), 1.9519, 3) # Assert Partial self.assertEqual(performance.get("accuracy"), None) self.assertEqual(performance.get("fm"), None) self.assertEqual(performance.get("nrm"), None) self.assertEqual(performance.get("mcc"), None) self.assertEqual(performance.get("psnr"), performanceAll.get("psnr")) self.assertEqual(performance.get("drdm"), performanceAll.get("drdm")) def test_pseudo_performance(self): # Setup image = read_image("2JohnC1V3.ppm") groundtruth_image = read_image("2JohnC1V3-GroundTruth.pbm") params = {"window": 27, "k": 0.10} doxapy.update_to_binary(doxapy.Binarization.Algorithms.SAUVOLA, image, params) # Load pseudo weights p_weights = read_weights("2JohnC1V3-GroundTruth_PWeights.dat") r_weights = read_weights("2JohnC1V3-GroundTruth_RWeights.dat") # Calculate performance with pseudo weights performance = doxapy.calculate_performance(groundtruth_image, image, p_weights, r_weights) # Assert pseudo metrics self.assertAlmostEqual(performance.get("pseudo_fm"), 93.393, 2) self.assertAlmostEqual(performance.get("pseudo_recall"), 92.7954, 2) self.assertAlmostEqual(performance.get("pseudo_precision"), 93.9983, 2) if __name__ == '__main__': unittest.main() ================================================ FILE: Bindings/Python/test/test_speed.py ================================================ """ Speed Tests for DoxaPy Measures execution time for: - Sauvola binarization - WAN binarization - GlobalThresholding (Otsu) binarization - DRDM calculation - Classified Performance calculation Run with: python test_speed.py """ import unittest import time import numpy as np from PIL import Image import os import sys sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'dist'))) import doxapy # Get the absolute path to the RESOURCES directory RESOURCES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'Doxa.Test', 'Resources')) # Number of iterations for timing WARMUP_ITERATIONS = 3 TIMING_ITERATIONS = 10 def read_image(file, algorithm=doxapy.GrayscaleAlgorithms.MEAN): full_path = os.path.join(RESOURCES_DIR, os.path.basename(file)) image = Image.open(full_path) # If already in grayscale or binary, do not convert it if image.mode == 'L': return np.array(image) # Read the color image rgb_image = np.array(image.convert('RGB') if image.mode not in ('RGB', 'RGBA') else image) # Use Doxa to convert to grayscale return doxapy.to_grayscale(algorithm, rgb_image) def measure_time(func, warmup=WARMUP_ITERATIONS, iterations=TIMING_ITERATIONS): """Measure execution time of a function.""" # Warmup runs for _ in range(warmup): func() # Timed runs times = [] for _ in range(iterations): start = time.perf_counter() func() end = time.perf_counter() times.append((end - start) * 1000) # Convert to ms return { 'min': min(times), 'max': max(times), 'avg': sum(times) / len(times), 'median': sorted(times)[len(times) // 2] } def format_results(name, results, image_shape): """Format timing results as a string.""" pixels = image_shape[0] * image_shape[1] mpixels = pixels / 1_000_000 throughput = mpixels / (results['avg'] / 1000) return ( f"\n{name}\n" f" Image size: {image_shape[1]}x{image_shape[0]} ({mpixels:.2f} MP)\n" f" Min: {results['min']:8.3f} ms\n" f" Max: {results['max']:8.3f} ms\n" f" Avg: {results['avg']:8.3f} ms\n" f" Median: {results['median']:8.3f} ms\n" f" Throughput: {throughput:.2f} MP/s" ) class DoxaPySpeedTests(unittest.TestCase): @classmethod def setUpClass(cls): """Load test images once for all tests.""" print("\n" + "=" * 60) print("DoxaPy Speed Tests") print("=" * 60) print(f"\nWarmup iterations: {WARMUP_ITERATIONS}") print(f"Timing iterations: {TIMING_ITERATIONS}") print("\nLoading test images...") cls.image = read_image("2JohnC1V3.ppm") cls.groundtruth_image = read_image("2JohnC1V3-GroundTruth.pbm") print(f"Grayscale image shape: {cls.image.shape}") print(f"Ground truth shape: {cls.groundtruth_image.shape}") # Create a binary image for performance metric tests cls.binary_image = np.empty(cls.image.shape, cls.image.dtype) params = {"window": 27, "k": 0.10} sauvola = doxapy.Binarization(doxapy.Binarization.Algorithms.SAUVOLA) sauvola.initialize(cls.image) sauvola.to_binary(cls.binary_image, params) print("\n" + "-" * 60) print("BINARIZATION ALGORITHMS") print("-" * 60) def test_sauvola_speed(self): """Measure Sauvola binarization speed.""" binary_image = np.empty(self.image.shape, self.image.dtype) params = {"window": 75, "k": 0.2} sauvola = doxapy.Binarization(doxapy.Binarization.Algorithms.SAUVOLA) sauvola.initialize(self.image) def run(): sauvola.to_binary(binary_image, params) results = measure_time(run) print(format_results("Sauvola", results, self.image.shape)) self.assertGreater(results['avg'], 0) def test_wan_speed(self): """Measure WAN binarization speed.""" binary_image = np.empty(self.image.shape, self.image.dtype) params = {"window": 75, "k": 0.2} wan = doxapy.Binarization(doxapy.Binarization.Algorithms.WAN) wan.initialize(self.image) def run(): wan.to_binary(binary_image, params) results = measure_time(run) print(format_results("WAN", results, self.image.shape)) self.assertGreater(results['avg'], 0) def test_otsu_speed(self): """Measure Otsu (GlobalThresholding) binarization speed.""" binary_image = np.empty(self.image.shape, self.image.dtype) params = {} otsu = doxapy.Binarization(doxapy.Binarization.Algorithms.OTSU) otsu.initialize(self.image) def run(): otsu.to_binary(binary_image, params) results = measure_time(run) print(format_results("Otsu (GlobalThresholding)", results, self.image.shape)) self.assertGreater(results['avg'], 0) def test_drdm_speed(self): """Measure DRDM calculation speed.""" print("\n" + "-" * 60) print("PERFORMANCE METRICS") print("-" * 60) def run(): doxapy.calculate_performance_ex(self.groundtruth_image, self.binary_image, drdm=True) results = measure_time(run) print(format_results("DRDM", results, self.image.shape)) self.assertGreater(results['avg'], 0) def test_classified_performance_speed(self): """Measure Classified Performance calculation speed (all metrics except DRDM).""" def run(): doxapy.calculate_performance_ex( self.groundtruth_image, self.binary_image, accuracy=True, fm=True, psnr=True, nrm=True, mcc=True ) results = measure_time(run) print(format_results("ClassifiedPerformance (no DRDM)", results, self.image.shape)) self.assertGreater(results['avg'], 0) def test_full_performance_speed(self): """Measure full performance calculation speed (all metrics including DRDM).""" def run(): doxapy.calculate_performance(self.groundtruth_image, self.binary_image) results = measure_time(run) print(format_results("Full Performance (all metrics)", results, self.image.shape)) self.assertGreater(results['avg'], 0) @classmethod def tearDownClass(cls): print("\n" + "=" * 60) print("Speed tests completed") print("=" * 60) if __name__ == '__main__': unittest.main(verbosity=2) ================================================ FILE: Bindings/WebAssembly/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.16) project(doxajs) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # This file is only used when building with Emscripten if(NOT EMSCRIPTEN) message(FATAL_ERROR "This CMakeLists.txt requires Emscripten. Use: emcmake cmake ..") endif() option(DOXAJS_ENABLE_SIMD "Enable WASM SIMD128" ON) # Include Doxa headers include_directories(${CMAKE_SOURCE_DIR}/Doxa) # Create WASM module add_executable(doxaWasm DoxaWasm.cpp) # Emscripten compile/link flags (matches build.js) target_compile_options(doxaWasm PRIVATE $,-O0,-O3> $<$:-g> ) target_link_options(doxaWasm PRIVATE -sWASM=1 -sNO_EXIT_RUNTIME=1 -sALLOW_MEMORY_GROWTH=1 "-sEXPORTED_FUNCTIONS=['_malloc','_free']" "-sEXPORTED_RUNTIME_METHODS=['HEAPU8']" --bind # Debug-only options $<$:-sEXCEPTION_DEBUG> $<$:-sNO_DISABLE_EXCEPTION_CATCHING> $<$:-g4> "$<$:--source-map-base=http://localhost:8080/>" ) # SIMD support if(DOXAJS_ENABLE_SIMD) target_compile_options(doxaWasm PRIVATE -msimd128) message(STATUS "DoxaJs SIMD: ENABLED (SIMD128)") else() message(STATUS "DoxaJs SIMD: DISABLED") endif() # Output to dist/ directory set_target_properties(doxaWasm PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/dist ) # Copy JavaScript wrapper after build add_custom_command(TARGET doxaWasm POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/doxa.js ${CMAKE_CURRENT_SOURCE_DIR}/dist/doxa.js COMMENT "Copying doxa.js wrapper to dist/" ) # CTest integration - run Jasmine tests via Node include(CTest) enable_testing() # Platform-specific script extension (avoid picking up .cmd on WSL2) if(WIN32) set(CMD_EXT ".cmd") else() set(CMD_EXT "") endif() find_program(NPM_EXECUTABLE NAMES npm${CMD_EXT}) if(NPM_EXECUTABLE) add_test(NAME wasm_tests COMMAND ${NPM_EXECUTABLE} test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) endif() ================================================ FILE: Bindings/WebAssembly/DoxaJs.nnb ================================================ { "cells": [ { "language": "markdown", "source": [ "# DoxaJs Notebook\r\n\r\nhttps://github.com/brandonmpetty/Doxa\r\n\r\nDoxaJs is an image binarization library focused on local adaptive algorithms and metrics.\r\nThis notebook will document the API while allowing you to interact with it.\r\nIt was designed in VS Code using the \"Node.js Notebooks (REPL)\" notebook kernel.\r\n\r\n## Setup\r\nDoxaJs does not currently have an NPM package you can download, but distributables are packaged in this repo. To run the notebook, you first need to install dev dependencies like'sharp'.\r\n\r\n```\r\nnpm install\r\n```\r\n\r\nThe next thing you should do is to load the **doxa.js** module. It is actually a wrapper around WASM assets built with EMScripten and the C++ based Doxa binarization framework." ], "outputs": [] }, { "language": "javascript", "source": [ "const { Doxa } = require('./dist/doxa.js');\r\nconst { display } = require('node-kernel');\r\nconst sharp = require('sharp');" ], "outputs": [] }, { "language": "markdown", "source": [ "While this demo highlights NodeJs with *sharp*, a Web demo is also available leveraging the standard HTML5 canvas.\r\nBelow are two helper functions leveraging *sharp*. Unlike other language targets of Doxa, DoxaJs will actually take in a 32 RGBA image and convert it to grayscale automatically. This approach was prefered due to the demands of 32b images when working with web technologies." ], "outputs": [] }, { "language": "javascript", "source": [ "// Read an image file and convert it to an 8-bit grayscale Doxa Image\r\nasync function readImage(file) {\r\n\treturn sharp(file)\r\n\t\t.raw()\r\n\t\t.toBuffer({ resolveWithObject: true })\r\n\t\t.then(content => {\r\n\t\t\treturn new Doxa.Image(content.info.width, content.info.height, content.data);\r\n\t\t});\r\n}\r\n\r\n// Add Notebook display support for Doxa Images\r\ndisplay.dimage = (image) => {\r\n\treturn sharp(image.data(), {\r\n\t\traw: {\r\n\t\t\twidth: image.width,\r\n\t\t\theight: image.height,\r\n\t\t\tchannels: 1\r\n\t\t}\r\n\t})\r\n\t.png()\r\n\t.toBuffer()\r\n\t.then((buf) => {\r\n\t\tdisplay.image(buf);\r\n\t});\r\n}" ], "outputs": [] }, { "language": "markdown", "source": [ "Because the JS bindings are leveraging WASM for portability and speed, the library must be initialized to register the WASM. Once initialized it will return back an enum object that represents all of the binarization algorithms available in the library." ], "outputs": [] }, { "language": "javascript", "source": [ "const Algorithms = await Doxa.initialize();" ], "outputs": [] }, { "language": "markdown", "source": [ "## Reading an Image\r\nThe next step is to read in an image. At the heart of a Doxa Image is an 8-bit byte array. Your 32-bit color or grayscale image will be converted to an 8-bit image using the mean of the R-G-B channels." ], "outputs": [] }, { "language": "javascript", "source": [ "// A snippet from a 1500 year Greek codex, hand written on velum.\r\nconst image = await readImage('../../README/2JohnC1V3.png');\r\n\r\ndisplay.dimage(image);" ], "outputs": [] }, { "language": "markdown", "source": [ "## Converting the Image to Binary\r\nConverting an image into black and white may seem easy, but it has been the focus of much research spanning decades. Doxa was designed to expose this research, traditionally mired by PHD technical jargon, in a very easy to consume fashion. A lot of work was put into ensuring these algorithms were implemented correctly and effeciently. Many of these algorithms were first made public by this project and many of them leverage state of the art enhacements to reduce memory utilization and increase speed of operation, found nowhere else. For more information on an individual algorithm, click one of the links below.\r\n\r\n### Algorithms\r\nThe Doxa library implements a large number of popular and unique local adaptive binarization algorithms. Each algorithm has a set of parameters that are required for it to operate. These parameters can vary from algorithm to algorithm. Doxa provides sensible defaults that are applied automatically unless you supply your own. Below is a list of algorithms and their defaults:\r\n\r\n* **OTSU**\r\n* **BERNSEN** - {\"window\": 75, \"threshold\": 100, \"contrast-limit\": 25}\r\n* **NIBLACK** - {\"window\": 75, \"k\": 0.2}\r\n* **SAUVOLA** - {\"window\": 75, \"k\": 0.2}\r\n* **WOLF** - {\"window\": 75, \"k\": 0.2}\r\n* **NICK** - {\"window\": 75, \"k\": -0.2}\r\n* **SU** - {\"window\": 9, \"minN\": 9}\r\n* **TRSINGH** - {\"window\": 75, \"k\": 0.2}\r\n* **BATAINEH**\r\n* **ISAUVOLA** - {\"window\": 75, \"k\": 0.2}\r\n* **WAN** - {\"window\": 75, \"k\": 0.2}\r\n* **GATOS** - {\"window\": 75, \"k\": 0.2, \"glyph\": 60}\r\n* **ADOTSU** - {\"window\": 75, \"k\": 1.0, \"R\": 0.1, \"distance\": window/2}" ], "outputs": [] }, { "language": "javascript", "source": [ "const binaryImage = Doxa.Binarization.toBinary(Algorithms.SAUVOLA, image, { window: 27, k: 0.12 });\r\n\r\ndisplay.dimage(binaryImage);" ], "outputs": [] }, { "language": "markdown", "source": [ "## Performance Metrics\r\nIn order to analyze the performance of an algorithm, Doxa provides a set of common metrics that can all be calculated with one function. To start that process you need an exemplar binary image, or \"ground truth.\" By comparing the ground truth to the resulting image of the binarization algorithm, you can start to compare the affects of different algorithms and algorithm parameters." ], "outputs": [] }, { "language": "javascript", "source": [ "const groundTruthImage = await readImage('../../README/2JohnC1V3-GroundTruth.png');\r\n\r\ndisplay.dimage(groundTruthImage);" ], "outputs": [] }, { "language": "javascript", "source": [ "const perf = Doxa.Binarization.calculatePerformance(groundTruthImage, binaryImage);\r\n\r\nconsole.dir(perf);" ], "outputs": [] }, { "language": "markdown", "source": [ "## Freeing Resources\r\nUnlike Javascript, Web Assembly (WASM) is not as forgiving when it comes to resource management. When memory is allocated in the WASM system it must be freed, much like traditional C/C++ code. While this may not be critical for short lived executions, it is best practice to free the image resources when you are finished with them." ], "outputs": [] }, { "language": "javascript", "source": [ "groundTruthImage.free();\r\nimage.free();" ], "outputs": [] } ] } ================================================ FILE: Bindings/WebAssembly/DoxaWasm.cpp ================================================ #include #include #include //#include #include "../../Doxa/BinarizationFactory.hpp" #include "../../Doxa/ClassifiedPerformance.hpp" #include "../../Doxa/DRDM.hpp" #include "../../Doxa/DIBCOUtils.hpp" #include "../../Doxa/Grayscale.hpp" //#include "../Doxa/PNM.hpp" using namespace Doxa; using namespace emscripten; /// /// Structure for measuring performance characteristics of the different algorithms /// struct Performance { double Accuracy; double FM; double Recall; double Precision; double MCC; double PSNR; double NRM; double DRDM; }; /// /// Structure for measuring pseudo performance characteristics, including standard and pseudo metrics /// struct PseudoPerformance { double Accuracy; double FM; double Recall; double Precision; double MCC; double PSNR; double NRM; double DRDM; double PseudoFM; double PseudoPrecision; double PseudoRecall; }; Performance CalculatePerformance(intptr_t groundTruthData, intptr_t binaryData, const int width, const int height) { Image groundTruthImage = Image::Reference(width, height, reinterpret_cast(groundTruthData)); Image binaryImage = Image::Reference(width, height, reinterpret_cast(binaryData)); ClassifiedPerformance::Classifications classifications; ClassifiedPerformance::CompareImages(classifications, groundTruthImage, binaryImage); // TODO: If this fails, we may want to 0 out the return values Performance perf; perf.Accuracy = ClassifiedPerformance::CalculateAccuracy(classifications); perf.FM = ClassifiedPerformance::CalculateFMeasure(classifications); perf.Recall = ClassifiedPerformance::CalculateRecall(classifications); perf.Precision = ClassifiedPerformance::CalculatePrecision(classifications); perf.MCC = ClassifiedPerformance::CalculateMCC(classifications); perf.PSNR = ClassifiedPerformance::CalculatePSNR(classifications); perf.NRM = ClassifiedPerformance::CalculateNRM(classifications); perf.DRDM = DRDM::CalculateDRDM(groundTruthImage, binaryImage); return perf; } PseudoPerformance CalculatePseudoPerformance( intptr_t groundTruthData, intptr_t binaryData, const int width, const int height, const std::string& precisionWeightsText, const std::string& recallWeightsText) { Image groundTruthImage = Image::Reference(width, height, reinterpret_cast(groundTruthData)); Image binaryImage = Image::Reference(width, height, reinterpret_cast(binaryData)); // Parse weight text via DIBCOUtils std::stringstream precisionStream(precisionWeightsText); std::stringstream recallStream(recallWeightsText); const size_t allocatedSize = static_cast(width) * height; auto precisionWeights = DIBCOUtils::ReadWeights(precisionStream, allocatedSize); auto recallWeights = DIBCOUtils::ReadWeights(recallStream, allocatedSize); // Compute weighted classifications ClassifiedPerformance::Classifications classifications; ClassifiedPerformance::CompareImages(classifications, groundTruthImage, binaryImage, precisionWeights, recallWeights); PseudoPerformance perf; perf.Accuracy = ClassifiedPerformance::CalculateAccuracy(classifications); perf.FM = ClassifiedPerformance::CalculateFMeasure(classifications); perf.Recall = ClassifiedPerformance::CalculateRecall(classifications); perf.Precision = ClassifiedPerformance::CalculatePrecision(classifications); perf.MCC = ClassifiedPerformance::CalculateMCC(classifications); perf.PSNR = ClassifiedPerformance::CalculatePSNR(classifications); perf.NRM = ClassifiedPerformance::CalculateNRM(classifications); perf.DRDM = DRDM::CalculateDRDM(groundTruthImage, binaryImage); perf.PseudoFM = ClassifiedPerformance::CalculatePseudoFMeasure(classifications); perf.PseudoPrecision = ClassifiedPerformance::CalculatePseudoPrecision(classifications); perf.PseudoRecall = ClassifiedPerformance::CalculatePseudoRecall(classifications); return perf; } void ToGrayscale(intptr_t outputData, intptr_t inputData, int width, int height, int channels, GrayscaleAlgorithms algorithm) { Grayscale::ToGrayscale( reinterpret_cast(outputData), reinterpret_cast(inputData), width, height, channels, algorithm); } /// /// Binarization is a helper class to help interface the C++ Doxa framework with WebAssembly. /// It exposes through enumeration all of the algorithms supported by the library. /// The Doxa.js code wraps the exported symbols to make it easier to use in JavaScript. /// class Binarization { public: Binarization(const Algorithms algorithm) : algorithm(algorithm) { algorithmPtr = BinarizationFactory::Algorithm(algorithm); } ~Binarization() { delete algorithmPtr; } void Initialize(intptr_t data, const int width, const int height) { // Set width and height this->width = width; this->height = height; Image image = Image::Reference(width, height, reinterpret_cast(data)); algorithmPtr->Initialize(image); } void ToBinary(intptr_t data, const std::string& params) { Image image = Image::Reference(this->width, this->height, reinterpret_cast(data)); algorithmPtr->ToBinary(image, Parameters::FromJson(params)); } Algorithms CurrentAlgorithm() { return algorithm; } /* static void Debug(Pixel8* data, const int width, const int height) { std::cout << "Debug - start (" << width << "x" << height << ")" << std::endl; const size_t size = width * height; for (size_t idx = 0; idx < size; ++idx) { const Pixel8 pixel = data[idx]; std::cout << "Index: " << idx << " Value: " << std::to_string(pixel) << std::endl; } data[0] = 42; // Verify the ability to set std::cout << "Debug - stop" << std::endl; } */ protected: const Algorithms algorithm; IAlgorithm* algorithmPtr = nullptr; int width = 0; int height = 0; }; // Class: Binarization EMSCRIPTEN_BINDINGS(doxa_wasm) { class_("Binarization") .constructor() .function("initialize", &Binarization::Initialize, allow_raw_pointers()) .function("toBinary", &Binarization::ToBinary, allow_raw_pointers()) .function("currentAlgorithm", &Binarization::CurrentAlgorithm); enum_("Binarization.Algorithms") .value("OTSU", Algorithms::OTSU) .value("BERNSEN", Algorithms::BERNSEN) .value("NIBLACK", Algorithms::NIBLACK) .value("SAUVOLA", Algorithms::SAUVOLA) .value("WOLF", Algorithms::WOLF) .value("NICK", Algorithms::NICK) .value("ADOTSU", Algorithms::ADOTSU) .value("SU", Algorithms::SU) .value("TRSINGH", Algorithms::TRSINGH) .value("BATAINEH", Algorithms::BATAINEH) .value("ISAUVOLA", Algorithms::ISAUVOLA) .value("WAN", Algorithms::WAN) .value("GATOS", Algorithms::GATOS) .value("PHANSALKAR", Algorithms::PHANSALKAR); EM_ASM( Module['Binarization']['Algorithms'] = Module['Binarization.Algorithms']; delete Module['Binarization.Algorithms']; ); value_object("Performance") .field("accuracy", &Performance::Accuracy) .field("fm", &Performance::FM) .field("recall", &Performance::Recall) .field("precision", &Performance::Precision) .field("mcc", &Performance::MCC) .field("psnr", &Performance::PSNR) .field("nrm", &Performance::NRM) .field("drdm", &Performance::DRDM); function("calculatePerformance", &CalculatePerformance, allow_raw_pointers()); value_object("PseudoPerformance") .field("accuracy", &PseudoPerformance::Accuracy) .field("fm", &PseudoPerformance::FM) .field("recall", &PseudoPerformance::Recall) .field("precision", &PseudoPerformance::Precision) .field("mcc", &PseudoPerformance::MCC) .field("psnr", &PseudoPerformance::PSNR) .field("nrm", &PseudoPerformance::NRM) .field("drdm", &PseudoPerformance::DRDM) .field("pseudoFM", &PseudoPerformance::PseudoFM) .field("pseudoPrecision", &PseudoPerformance::PseudoPrecision) .field("pseudoRecall", &PseudoPerformance::PseudoRecall); function("calculatePseudoPerformance", &CalculatePseudoPerformance, allow_raw_pointers()); enum_("Grayscale.Algorithms") .value("MEAN", GrayscaleAlgorithms::MEAN) .value("QT", GrayscaleAlgorithms::QT) .value("BT601", GrayscaleAlgorithms::BT601) .value("BT709", GrayscaleAlgorithms::BT709) .value("BT2100", GrayscaleAlgorithms::BT2100) .value("VALUE", GrayscaleAlgorithms::VALUE) .value("LUSTER", GrayscaleAlgorithms::LUSTER) .value("LIGHTNESS", GrayscaleAlgorithms::LIGHTNESS) .value("MINAVG", GrayscaleAlgorithms::MINAVG) .value("LABDIST", GrayscaleAlgorithms::LABDIST); EM_ASM( Module['Grayscale'] = { 'Algorithms': Module['Grayscale.Algorithms'] }; delete Module['Grayscale.Algorithms']; ); function("toGrayscale", &ToGrayscale, allow_raw_pointers()); }; ================================================ FILE: Bindings/WebAssembly/README.md ================================================ # Δoxa Binarization Framework - WebAssembly ## Introduction This is an **experimental** project that exposes the ΔBF, written in C++, to JavaScript via WebAssembly. It works both server side and client side. For a simple example of how it works, checkout the [WebJS](../../Demo/WebJS) and [NodeJS](../../Demo/NodeJS) demos. A Visual Studio Code [Notebook](./DoxaJs.nnb) was developed to easily test and document the API. It uses the *Node.js Notebooks (REPL)* kernel. ## Building DoxaJs is built using CMake with the Emscripten toolchain. You must have [Emscripten](https://emscripten.org/docs/getting_started/downloads.html) installed and available in your path. ### Using npm (Recommended) ```bash cd Bindings/WebAssembly npm install # Release build npm run build # Debug build (with source maps and exception debugging) npm run build:dev # Run tests npm test ``` ### Using CMake ```bash # From project root cmake --preset wasm cmake --build build-wasm --config Release npm install --prefix Bindings/WebAssembly ctest --test-dir build-wasm -C Release ``` ### Run the Web Demo ```bash emrun --no_browser --port 8080 . # Navigate to: http://localhost:8080/Demo/WebJS ``` ## License CC0 - Brandon M. Petty, 2023 To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. [View Online](https://creativecommons.org/publicdomain/zero/1.0/legalcode) "*Freely you have received; freely give.*" - Matt 10:8 ================================================ FILE: Bindings/WebAssembly/dist/doxa.js ================================================ /** * Doxa WASM * A set of classes that further simplify the Doxa WASM interface. * This same wrapper can be run in NodeJS or directly in the web. * See Demo/WebJs and Demo/NodeJS for an example of how to use it. */ const Doxa = { Wasm: (typeof Module !== 'undefined') ? Module : require('./doxaWasm.js'), initialize: async function() { if (!Doxa.Wasm) throw new Error('Missing: Doxa WASM Module'); // Extract enum values from an Emscripten enum object const extractEnums = function(enumObj) { const enums = {}; for (const key in enumObj) { const entry = enumObj[key]; if (entry?.value === undefined) continue; enums[key] = entry.value; } return enums; } const buildInstance = function() { const instance = { binarization: extractEnums(Doxa.Wasm.Binarization.Algorithms), grayscale: extractEnums(Doxa.Wasm.Grayscale.Algorithms), /** * Convert raw pixel data to an 8-bit grayscale Doxa.Image. * If the data is already single-channel, it is copied directly. * @param {Uint8Array|Uint8ClampedArray} data - Raw pixel data (1, 3, or 4 channels) * @param {number} width - Image width * @param {number} height - Image height * @param {number} channels - 1 for grayscale, 3 for RGB, 4 for RGBA * @param {number} algorithm - Grayscale algorithm enum value (e.g. doxa.grayscale.MEAN). Defaults to MEAN. * @returns {Doxa.Image} 8-bit grayscale image (caller must free) */ toGrayscale: function(data, width, height, channels, algorithm) { // Already grayscale — just copy directly if (channels === 1) { return new Doxa.Image(width, height, data); } // Allocate WASM heap for input const inputSize = width * height * channels; const inputPtr = Doxa.Wasm._malloc(inputSize); Doxa.Wasm.HEAPU8.set(data.subarray(0, inputSize), inputPtr); // Allocate output image const outputImage = new Doxa.Image(width, height); const algEnum = Doxa.Wasm.Grayscale.Algorithms.values[ algorithm ?? instance.grayscale.MEAN ]; Doxa.Wasm.toGrayscale(outputImage.heapPtr, inputPtr, width, height, channels, algEnum); // Free input buffer Doxa.Wasm._free(inputPtr); return outputImage; }, /** * Convert an HTML5 Canvas ImageData to an 8-bit grayscale Doxa.Image. * Convenience wrapper for browser usage. * @param {ImageData} imageData - Canvas ImageData (32-bit RGBA) * @param {number} algorithm - Grayscale algorithm enum value (e.g. doxa.grayscale.MEAN). Defaults to MEAN. * @returns {Doxa.Image} 8-bit grayscale image (caller must free) */ fromImageData: function(imageData, algorithm) { return instance.toGrayscale( imageData.data, imageData.width, imageData.height, 4, algorithm ); }, /** * Binarize a grayscale image using the specified algorithm. * @param {number} algorithm - Binarization algorithm enum value (e.g. doxa.binarization.SAUVOLA) * @param {Doxa.Image} imageIn - Input grayscale image * @param {object} parameters - Algorithm parameters (e.g. { window: 75, k: 0.2 }) * @param {Doxa.Image} imageOut - Optional output image (allocated if not provided) * @returns {Doxa.Image} Binary image (caller must free if newly allocated) */ toBinary: function(algorithm, imageIn, parameters, imageOut) { if (!imageOut) { imageOut = new Doxa.Image(imageIn.width, imageIn.height); } const algEnum = Doxa.Wasm.Binarization.Algorithms.values[algorithm]; const paramString = JSON.stringify(parameters || {}); const binarization = new Doxa.Wasm.Binarization(algEnum); binarization.initialize(imageIn.heapPtr, imageIn.width, imageIn.height); binarization.toBinary(imageOut.heapPtr, paramString); return imageOut; }, /** * Calculate performance metrics comparing a binary image against ground truth. * If precision/recall weight texts are provided, pseudo metrics are included. * @param {Doxa.Image} groundTruth - Ground truth binary image * @param {Doxa.Image} binary - Binary image to evaluate * @param {string} precisionWeightsText - Optional precision weights (enables pseudo metrics) * @param {string} recallWeightsText - Optional recall weights (enables pseudo metrics) * @returns {object} Performance metrics (accuracy, fm, precision, recall, mcc, psnr, nrm, drdm, and optionally pseudoFM, pseudoPrecision, pseudoRecall) */ calculatePerformance: function(groundTruth, binary, precisionWeightsText, recallWeightsText) { if (precisionWeightsText && recallWeightsText) { return Doxa.Wasm.calculatePseudoPerformance( groundTruth.heapPtr, binary.heapPtr, binary.width, binary.height, precisionWeightsText, recallWeightsText ); } return Doxa.Wasm.calculatePerformance( groundTruth.heapPtr, binary.heapPtr, binary.width, binary.height ); } }; return instance; } return new Promise((resolve) => { // Ensure the library has not already been initialized if (typeof Doxa.Wasm.Binarization !== 'undefined') { return resolve(buildInstance()); } Doxa.Wasm.onRuntimeInitialized = async _ => { resolve(buildInstance()); }; }); }, Image: class { bufferSize = 0; /** * Initialize an Image object. All Image objects allocate WASM memory. * That memory should be freed, explicitly. If 8-bit data is passed, * it will be copied directly into WASM memory. * Use doxa.fromImageData to convert from RGBA to grayscale. * @param {number} width Image Width * @param {number} height Image Height * @param {Uint8Array} data 8-bit grayscale data. Optional. */ constructor (width, height, data) { this.initialize(width || 0, height || 0); if (data) { Doxa.Wasm.HEAPU8.set(data.subarray(0, this.size), this.heapPtr); } } /** * The number of pixels in the image. */ get size() { return this.width * this.height; } /** * Draws the Image directly to an HTML5 Canvas. * The Canvas should accept 32bit data, which is standard. * @param {*} canvas HTML5 Canvas, or a node-canvas */ draw(canvas) { canvas.width = this.width; canvas.height = this.height; const ctx = canvas.getContext('2d'); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this.toImageData(imageData); ctx.putImageData(imageData, 0, 0); } data() { return new Uint8ClampedArray(Doxa.Wasm.HEAPU8.buffer, this.heapPtr, this.size); } /** * Frees the memory allocated by this object. */ free() { if (this.heapPtr != null) Doxa.Wasm._free(this.heapPtr); } initialize(width, height) { this.width = width; this.height = height; if (this.size > this.bufferSize) { this.free(); this.bufferSize = this.size; this.heapPtr = Doxa.Wasm._malloc(this.bufferSize); } } /** * Writes the 8-bit grayscale image data into a 32-bit RGBA ImageData object. * @param {ImageData} imageData - Target ImageData to populate */ toImageData(imageData) { const buffer = this.data(); const size32 = imageData.width * imageData.height * 4; for (var idx = 0; idx < size32; idx += 4) { const gsIdx = idx / 4; imageData.data[idx] = buffer[gsIdx]; imageData.data[idx+1] = buffer[gsIdx]; imageData.data[idx+2] = buffer[gsIdx]; imageData.data[idx+3] = 255; } } } } // Only export if running in NodeJS if (typeof module !== 'undefined') { module.exports = { Doxa }; } ================================================ FILE: Bindings/WebAssembly/dist/doxaWasm.js ================================================ var Module=typeof Module!="undefined"?Module:{};var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=globalThis.document?.currentScript?.src;if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);if(typeof module!="undefined"){module["exports"]=Module}quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["A"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("doxaWasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var noExitRuntime=true;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var structRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var awaitingDependencies={};var registeredTypes={};var typeDependencies={};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_object=structType=>{var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};for(var[i,field]of fieldRecords.entries()){const getterReturnType=fieldTypes[i];const getter=field.getter;const getterContext=field.getterContext;const setterArgumentType=fieldTypes[i+fieldRecords.length];const setter=field.setter;const setterContext=field.setterContext;fields[field.fieldName]={read:ptr=>getterReturnType.fromWireType(getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,o));runDestructors(destructors)},optional:getterReturnType.optional}}return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)&&!fields[fieldName].optional){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var finalizationRegistry=false;var detachFinalizer=handle=>{};var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};var heap32VectorToArray=(count,firstElement)=>{var array=[];for(var i=0;i>2])}return array};function usesDestructorStack(argTypes){for(var i=1;i{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this.fromWireType(HEAP8[pointer])}:function(pointer){return this.fromWireType(HEAPU8[pointer])};case 2:return signed?function(pointer){return this.fromWireType(HEAP16[pointer>>1])}:function(pointer){return this.fromWireType(HEAPU16[pointer>>1])};case 4:return signed?function(pointer){return this.fromWireType(HEAP32[pointer>>2])}:function(pointer){return this.fromWireType(HEAPU32[pointer>>2])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};function getEnumValueType(rawValueType){return rawValueType===0?"object":rawValueType===1?"number":"string"}var __embind_register_enum=(rawType,name,size,isSigned,rawValueType)=>{name=AsciiToString(name);const valueType=getEnumValueType(rawValueType);switch(valueType){case"object":{function ctor(){}ctor.values={};registerType(rawType,{name,constructor:ctor,valueType,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor);break}case"number":{var keysMap={};registerType(rawType,{name,keysMap,valueType,fromWireType:c=>c,toWireType:(destructors,c)=>c,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}case"string":{var valuesMap={};var reverseMap={};var keysMap={};registerType(rawType,{name,valuesMap,reverseMap,keysMap,valueType,fromWireType:function(c){return this.reverseMap[c]},toWireType:function(destructors,c){return this.valuesMap[c]},readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}}};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var __embind_register_enum_value=(rawEnumType,name,enumValue)=>{var enumType=requireRegisteredType(rawEnumType,"enum");name=AsciiToString(name);switch(enumType.valueType){case"object":{var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value;break}case"number":{enumType.keysMap[name]=enumValue;break}case"string":{enumType.valuesMap[name]=enumValue;enumType.reverseMap[enumValue]=name;enumType.keysMap[name]=name;break}}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_value_object=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{structRegistrations[rawType]={name:AsciiToString(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}};var __embind_register_value_object_field=(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{structRegistrations[structType].fields.push({fieldName:AsciiToString(fieldName),getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext,setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffset{readEmAsmArgsArray.length=0;var ch;while(ch=HEAPU8[sigPtr++]){var wide=ch!=105;wide&=ch!=112;buf+=wide&&buf%8?4:0;readEmAsmArgsArray.push(ch==112?HEAPU32[buf>>2]:ch==106?HEAP64[buf>>3]:ch==105?HEAP32[buf>>2]:HEAPF64[buf>>3]);buf+=wide?8:4}return readEmAsmArgsArray};var runEmAsmFunction=(code,sigPtr,argbuf)=>{var args=readEmAsmArgs(sigPtr,argbuf);return ASM_CONSTS[code](...args)};var _emscripten_asm_const_int=(code,sigPtr,argbuf)=>runEmAsmFunction(code,sigPtr,argbuf);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var ASM_CONSTS={26400:()=>{Module["Binarization"]["Algorithms"]=Module["Binarization.Algorithms"];delete Module["Binarization.Algorithms"]},26516:()=>{Module["Grayscale"]={Algorithms:Module["Grayscale.Algorithms"]};delete Module["Grayscale.Algorithms"]}};var ___getTypeName,_malloc,_free,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["B"];_malloc=Module["_malloc"]=wasmExports["D"];_free=Module["_free"]=wasmExports["E"];memory=wasmMemory=wasmExports["z"];__indirect_function_table=wasmTable=wasmExports["C"]}var wasmImports={e:___cxa_throw,r:__abort_js,i:__embind_finalize_value_object,n:__embind_register_bigint,w:__embind_register_bool,y:__embind_register_class,t:__embind_register_class_constructor,f:__embind_register_class_function,u:__embind_register_emval,l:__embind_register_enum,a:__embind_register_enum_value,m:__embind_register_float,h:__embind_register_function,d:__embind_register_integer,c:__embind_register_memory_view,v:__embind_register_std_string,g:__embind_register_std_wstring,j:__embind_register_value_object,b:__embind_register_value_object_field,x:__embind_register_void,o:__tzset_js,k:_emscripten_asm_const_int,s:_emscripten_resize_heap,p:_environ_get,q:_environ_sizes_get};function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;createWasm();run(); ================================================ FILE: Bindings/WebAssembly/doxa.js ================================================ /** * Doxa WASM * A set of classes that further simplify the Doxa WASM interface. * This same wrapper can be run in NodeJS or directly in the web. * See Demo/WebJs and Demo/NodeJS for an example of how to use it. */ const Doxa = { Wasm: (typeof Module !== 'undefined') ? Module : require('./doxaWasm.js'), initialize: async function() { if (!Doxa.Wasm) throw new Error('Missing: Doxa WASM Module'); // Extract enum values from an Emscripten enum object const extractEnums = function(enumObj) { const enums = {}; for (const key in enumObj) { const entry = enumObj[key]; if (entry?.value === undefined) continue; enums[key] = entry.value; } return enums; } const buildInstance = function() { const instance = { binarization: extractEnums(Doxa.Wasm.Binarization.Algorithms), grayscale: extractEnums(Doxa.Wasm.Grayscale.Algorithms), /** * Convert raw pixel data to an 8-bit grayscale Doxa.Image. * If the data is already single-channel, it is copied directly. * @param {Uint8Array|Uint8ClampedArray} data - Raw pixel data (1, 3, or 4 channels) * @param {number} width - Image width * @param {number} height - Image height * @param {number} channels - 1 for grayscale, 3 for RGB, 4 for RGBA * @param {number} algorithm - Grayscale algorithm enum value (e.g. doxa.grayscale.MEAN). Defaults to MEAN. * @returns {Doxa.Image} 8-bit grayscale image (caller must free) */ toGrayscale: function(data, width, height, channels, algorithm) { // Already grayscale — just copy directly if (channels === 1) { return new Doxa.Image(width, height, data); } // Allocate WASM heap for input const inputSize = width * height * channels; const inputPtr = Doxa.Wasm._malloc(inputSize); Doxa.Wasm.HEAPU8.set(data.subarray(0, inputSize), inputPtr); // Allocate output image const outputImage = new Doxa.Image(width, height); const algEnum = Doxa.Wasm.Grayscale.Algorithms.values[ algorithm ?? instance.grayscale.MEAN ]; Doxa.Wasm.toGrayscale(outputImage.heapPtr, inputPtr, width, height, channels, algEnum); // Free input buffer Doxa.Wasm._free(inputPtr); return outputImage; }, /** * Convert an HTML5 Canvas ImageData to an 8-bit grayscale Doxa.Image. * Convenience wrapper for browser usage. * @param {ImageData} imageData - Canvas ImageData (32-bit RGBA) * @param {number} algorithm - Grayscale algorithm enum value (e.g. doxa.grayscale.MEAN). Defaults to MEAN. * @returns {Doxa.Image} 8-bit grayscale image (caller must free) */ fromImageData: function(imageData, algorithm) { return instance.toGrayscale( imageData.data, imageData.width, imageData.height, 4, algorithm ); }, /** * Binarize a grayscale image using the specified algorithm. * @param {number} algorithm - Binarization algorithm enum value (e.g. doxa.binarization.SAUVOLA) * @param {Doxa.Image} imageIn - Input grayscale image * @param {object} parameters - Algorithm parameters (e.g. { window: 75, k: 0.2 }) * @param {Doxa.Image} imageOut - Optional output image (allocated if not provided) * @returns {Doxa.Image} Binary image (caller must free if newly allocated) */ toBinary: function(algorithm, imageIn, parameters, imageOut) { if (!imageOut) { imageOut = new Doxa.Image(imageIn.width, imageIn.height); } const algEnum = Doxa.Wasm.Binarization.Algorithms.values[algorithm]; const paramString = JSON.stringify(parameters || {}); const binarization = new Doxa.Wasm.Binarization(algEnum); binarization.initialize(imageIn.heapPtr, imageIn.width, imageIn.height); binarization.toBinary(imageOut.heapPtr, paramString); return imageOut; }, /** * Calculate performance metrics comparing a binary image against ground truth. * If precision/recall weight texts are provided, pseudo metrics are included. * @param {Doxa.Image} groundTruth - Ground truth binary image * @param {Doxa.Image} binary - Binary image to evaluate * @param {string} precisionWeightsText - Optional precision weights (enables pseudo metrics) * @param {string} recallWeightsText - Optional recall weights (enables pseudo metrics) * @returns {object} Performance metrics (accuracy, fm, precision, recall, mcc, psnr, nrm, drdm, and optionally pseudoFM, pseudoPrecision, pseudoRecall) */ calculatePerformance: function(groundTruth, binary, precisionWeightsText, recallWeightsText) { if (precisionWeightsText && recallWeightsText) { return Doxa.Wasm.calculatePseudoPerformance( groundTruth.heapPtr, binary.heapPtr, binary.width, binary.height, precisionWeightsText, recallWeightsText ); } return Doxa.Wasm.calculatePerformance( groundTruth.heapPtr, binary.heapPtr, binary.width, binary.height ); } }; return instance; } return new Promise((resolve) => { // Ensure the library has not already been initialized if (typeof Doxa.Wasm.Binarization !== 'undefined') { return resolve(buildInstance()); } Doxa.Wasm.onRuntimeInitialized = async _ => { resolve(buildInstance()); }; }); }, Image: class { bufferSize = 0; /** * Initialize an Image object. All Image objects allocate WASM memory. * That memory should be freed, explicitly. If 8-bit data is passed, * it will be copied directly into WASM memory. * Use doxa.fromImageData to convert from RGBA to grayscale. * @param {number} width Image Width * @param {number} height Image Height * @param {Uint8Array} data 8-bit grayscale data. Optional. */ constructor (width, height, data) { this.initialize(width || 0, height || 0); if (data) { Doxa.Wasm.HEAPU8.set(data.subarray(0, this.size), this.heapPtr); } } /** * The number of pixels in the image. */ get size() { return this.width * this.height; } /** * Draws the Image directly to an HTML5 Canvas. * The Canvas should accept 32bit data, which is standard. * @param {*} canvas HTML5 Canvas, or a node-canvas */ draw(canvas) { canvas.width = this.width; canvas.height = this.height; const ctx = canvas.getContext('2d'); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this.toImageData(imageData); ctx.putImageData(imageData, 0, 0); } data() { return new Uint8ClampedArray(Doxa.Wasm.HEAPU8.buffer, this.heapPtr, this.size); } /** * Frees the memory allocated by this object. */ free() { if (this.heapPtr != null) Doxa.Wasm._free(this.heapPtr); } initialize(width, height) { this.width = width; this.height = height; if (this.size > this.bufferSize) { this.free(); this.bufferSize = this.size; this.heapPtr = Doxa.Wasm._malloc(this.bufferSize); } } /** * Writes the 8-bit grayscale image data into a 32-bit RGBA ImageData object. * @param {ImageData} imageData - Target ImageData to populate */ toImageData(imageData) { const buffer = this.data(); const size32 = imageData.width * imageData.height * 4; for (var idx = 0; idx < size32; idx += 4) { const gsIdx = idx / 4; imageData.data[idx] = buffer[gsIdx]; imageData.data[idx+1] = buffer[gsIdx]; imageData.data[idx+2] = buffer[gsIdx]; imageData.data[idx+3] = 255; } } } } // Only export if running in NodeJS if (typeof module !== 'undefined') { module.exports = { Doxa }; } ================================================ FILE: Bindings/WebAssembly/package.json ================================================ { "name": "doxajs", "version": "1.0.0", "description": "Doxa Binarization Framework for JavaScript/WebAssembly", "main": "dist/doxa.js", "files": [ "dist/" ], "repository": { "type": "git", "url": "https://github.com/brandonmpetty/Doxa" }, "keywords": [ "binarization", "image-processing" ], "scripts": { "test": "jasmine", "build": "emcmake cmake -S ../.. -B ../../build-wasm -DCMAKE_BUILD_TYPE=Release && cmake --build ../../build-wasm --config Release", "build:dev": "emcmake cmake -S ../.. -B ../../build-wasm -DCMAKE_BUILD_TYPE=Debug && cmake --build ../../build-wasm --config Debug", "prepublishOnly": "npm run build" }, "author": "Brandon M. Petty", "license": "CC0-1.0", "devDependencies": { "canvas": "3.2.1", "jasmine": "6.0.0" } } ================================================ FILE: Bindings/WebAssembly/spec/binarization.spec.js ================================================ const fs = require('fs'); const path = require('path'); const { loadImage, createCanvas } = require('canvas'); const { Doxa } = require('../dist/doxa.js'); // Note: I would love to give an example of sharp but it cannot live // in the same project as canvas. See the DEMO for an example. //const sharp = require('sharp'); describe("Doxa Binarization Class Test Suite", function() { let doxa; async function readImage(file) { const image = await loadImage(file); const canvas = createCanvas(image.width, image.height); const ctx = canvas.getContext('2d'); ctx.drawImage(image, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); return doxa.fromImageData(imageData); } beforeAll(async function() { // Initializing is required for setting up the WASM module. doxa = await Doxa.initialize(); }); it("Binarization toBinary runs successfully", async function() { const rgba = new Buffer.from([ 0,0,0,0, 255,255,255,0, 120,120,120,0, 15,15,15,0, 230,230,230,0, 90,90,90,0, 5,5,5,0, 245,245,245,0, 69,1100,77,0 ]); const image = doxa.toGrayscale(rgba, 3, 3, 4); // Function under test const binImage = doxa.toBinary(doxa.binarization.OTSU, image); expect(Array.from(binImage.data())).toEqual([ 0, 255, 0, 0, 255, 0, 0, 255, 0 ]); }); it("Binarization calculatePerformance runs successfully", async function() { const groundTruthImage = await readImage('../../README/2JohnC1V3-GroundTruth.png'); const binaryImage = await readImage('../../README/2JohnC1V3-Sauvola.png'); const metrics = doxa.calculatePerformance(groundTruthImage, binaryImage); expect(metrics.accuracy).toBeCloseTo(97.671, 3); // NOTE: This PNG is slightly different than the PBM used by other tests //console.log(4122975586 / (2112 * 1000000)); // DIBCO Metrics //console.log(4122922964 / (2112 * 1000000)); // This algorithm. // Possible rounding error due to the weighted matrix? expect(metrics.drdm).toBeCloseTo(1.9522, 3); // TODO: Change to 4! expect(metrics.fm).toBeCloseTo(93.204, 3); expect(metrics.recall).toBeCloseTo(91.3811, 2); expect(metrics.precision).toBeCloseTo(95.1025, 2); expect(metrics.mcc).toBeCloseTo(0.918, 3); expect(metrics.nrm).toBeCloseTo(0.048, 3); expect(metrics.psnr).toBeCloseTo(16.329, 3); }); it("Binarization calculatePerformance with pseudo metrics runs successfully", async function() { const groundTruthImage = await readImage('../../README/2JohnC1V3-GroundTruth.png'); const binaryImage = await readImage('../../README/2JohnC1V3-Sauvola.png'); const pWeightsText = fs.readFileSync(path.resolve(__dirname, '../../../Doxa.Test/Resources/2JohnC1V3-GroundTruth_PWeights.dat'), 'utf8'); const rWeightsText = fs.readFileSync(path.resolve(__dirname, '../../../Doxa.Test/Resources/2JohnC1V3-GroundTruth_RWeights.dat'), 'utf8'); const metrics = doxa.calculatePerformance(groundTruthImage, binaryImage, pWeightsText, rWeightsText); expect(metrics.pseudoFM).toBeCloseTo(93.393, 2); expect(metrics.pseudoRecall).toBeCloseTo(92.7954, 2); expect(metrics.pseudoPrecision).toBeCloseTo(93.9983, 2); }); it("Algorithm defaults are applied", async function() { const image = await readImage('../../README/2JohnC1V3.png'); const binImage1 = doxa.toBinary(doxa.binarization.SAUVOLA, image); const binImage2 = doxa.toBinary(doxa.binarization.SAUVOLA, image, { window: 75, k: 0.2 }); const binImage3 = doxa.toBinary(doxa.binarization.SAUVOLA, image, { window: 25, k: 0.12 }); expect(binImage1.data()).toEqual(binImage2.data()); expect(binImage2.data()).not.toEqual(binImage3.data()); }); }); ================================================ FILE: Bindings/WebAssembly/spec/image.spec.js ================================================ const { createCanvas, createImageData } = require('canvas'); const { Doxa } = require('../dist/doxa.js'); describe("Doxa Image Class Test Suite", function() { let doxa; beforeAll(async function() { // Initializing is required for setting up the WASM module. doxa = await Doxa.initialize(); }); it("Constructor no arguments", function() { const image = new Doxa.Image(); expect(image.width).toBe(0); expect(image.height).toBe(0); expect(image.size).toBe(0); expect(Array.from(image.data())).toEqual([]); image.free(); }); it("Constructor no Data", function() { const image = new Doxa.Image(2, 3); expect(image.width).toBe(2); expect(image.height).toBe(3); expect(image.size).toBe(6); // Some compilers might zero out image.data, others will not. image.free(); }); it("Constructor with 8-bit Data", function() { const gray = new Uint8Array([ 255, 0, 128, 85, 85, 85, 80, 80, 80 ]); const image = new Doxa.Image(3, 3, gray); expect(image.width).toBe(3); expect(image.height).toBe(3); expect(image.size).toBe(9); expect(Array.from(image.data())).toEqual([ 255, 0, 128, 85, 85, 85, 80, 80, 80 ]); image.free(); }); it("Image Resize", function() { const image = new Doxa.Image(2, 3); expect(image.width).toBe(2); expect(image.height).toBe(3); expect(image.size).toBe(6); expect(image.bufferSize).toBe(6); image.initialize(3,3); expect(image.width).toBe(3); expect(image.height).toBe(3); expect(image.size).toBe(9); expect(image.bufferSize).toBe(9); // Buffer grew image.initialize(2,1); expect(image.width).toBe(2); expect(image.height).toBe(1); expect(image.size).toBe(2); // Reflects actual image expect(image.bufferSize).toBe(9); // Buffer retained image.free(); }); it("Grayscale fromImageData", function() { const rgba = new createImageData(new Uint8ClampedArray([ 255,255,255,0, 0,0,0,0, 128,128,128,0, 255,0,0,0, 0,255,0,0, 0,0,255,0, 70,80,90,0, 90,80,70,0, 80,90,70,0 ]), 3, 3); const image = doxa.fromImageData(rgba); // MEAN grayscale: (r+g+b)/3 expect(Array.from(image.data())).toEqual([ 255, 0, 128, 85, 85, 85, 80, 80, 80 ]); image.free(); }); it("toImageData", function() { const gray = new Uint8Array([ 255, 0, 128, 85, 85, 85, 70, 70, 70 ]); const image = new Doxa.Image(3, 3, gray); const imageData = new createImageData(3, 3); image.toImageData(imageData); expect(Array.from(imageData.data)).toEqual([ 255,255,255,255, 0,0,0,255, 128,128,128,255, 85,85,85,255, 85,85,85,255, 85,85,85,255, 70,70,70,255, 70,70,70,255, 70,70,70,255 ]); image.free(); }); it("draw", function() { const canvas = createCanvas(3, 3); const rgba = new createImageData(new Uint8ClampedArray([ 255,255,255,0, 0,0,0,0, 128,128,128,0, 255,0,0,0, 0,255,0,0, 0,0,255,0, 0,0,0,0, 255,255,255,0, 0,0,0,0, ]), 3, 3); // Create an Image via Grayscale conversion const image = doxa.fromImageData(rgba); // Draw image onto our canvas image.draw(canvas); // Pull data out of the canvas and compare const result = canvas.getContext('2d').getImageData(0, 0, image.width, image.height); expect(Array.from(result.data)).toEqual([ 255,255,255,255, 0,0,0,255, 128,128,128,255, 85,85,85,255, 85,85,85,255, 85,85,85,255, 0,0,0,255, 255,255,255,255, 0,0,0,255 ]); image.free(); }); }); ================================================ FILE: Bindings/WebAssembly/spec/speed.spec.js ================================================ /** * Speed Tests for DoxaJs * * Measures execution time for: * - Sauvola binarization * - WAN binarization * - GlobalThresholding (Otsu) binarization * - Full Performance calculation (ClassifiedPerformance + DRDM) * * Run with: npx jasmine spec/speed.spec.js */ const { loadImage, createCanvas } = require('canvas'); const { Doxa } = require('../dist/doxa.js'); // Number of iterations for timing const WARMUP_ITERATIONS = 3; const TIMING_ITERATIONS = 10; let doxa; /** * Read an image file and convert to Doxa.Image */ async function readImage(file) { const image = await loadImage(file); const canvas = createCanvas(image.width, image.height); const ctx = canvas.getContext('2d'); ctx.drawImage(image, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); return doxa.fromImageData(imageData); } /** * Measure execution time of a function. * If the function returns a Doxa.Image, it will be freed AFTER timing measurement. */ function measureTime(func, warmup = WARMUP_ITERATIONS, iterations = TIMING_ITERATIONS) { // Warmup runs (free any returned images) for (let i = 0; i < warmup; i++) { const result = func(); if (result && typeof result.free === 'function') { result.free(); } } // Timed runs const times = []; for (let i = 0; i < iterations; i++) { const start = performance.now(); const result = func(); const end = performance.now(); times.push(end - start); // Free after timing measurement if (result && typeof result.free === 'function') { result.free(); } } times.sort((a, b) => a - b); return { min: times[0], max: times[times.length - 1], avg: times.reduce((a, b) => a + b, 0) / times.length, median: times[Math.floor(times.length / 2)] }; } /** * Format timing results for display */ function formatResults(name, results, width, height) { const pixels = width * height; const mpixels = pixels / 1_000_000; const throughput = mpixels / (results.avg / 1000); return ` ${name} Image size: ${width}x${height} (${mpixels.toFixed(2)} MP) Min: ${results.min.toFixed(3)} ms Max: ${results.max.toFixed(3)} ms Avg: ${results.avg.toFixed(3)} ms Median: ${results.median.toFixed(3)} ms Throughput: ${throughput.toFixed(2)} MP/s`; } describe("Doxa Speed Test Suite", function() { let grayscaleImage; let groundTruthImage; let binaryImage; let imageWidth; let imageHeight; beforeAll(async function() { console.log('\n' + '='.repeat(60)); console.log('DoxaJs Speed Tests'); console.log('='.repeat(60)); console.log(`\nWarmup iterations: ${WARMUP_ITERATIONS}`); console.log(`Timing iterations: ${TIMING_ITERATIONS}`); // Initialize WASM module doxa = await Doxa.initialize(); // Load test images console.log('\nLoading test images...'); grayscaleImage = await readImage('../../README/2JohnC1V3.png'); binaryImage = await readImage('../../README/2JohnC1V3-Sauvola.png'); groundTruthImage = await readImage('../../README/2JohnC1V3-GroundTruth.png'); imageWidth = grayscaleImage.width; imageHeight = grayscaleImage.height; console.log(`Image: ${imageWidth}x${imageHeight}`); console.log('\n' + '-'.repeat(60)); console.log('BINARIZATION ALGORITHMS'); console.log('-'.repeat(60)); }); afterAll(function() { // Clean up WASM memory if (grayscaleImage) grayscaleImage.free(); if (groundTruthImage) groundTruthImage.free(); if (binaryImage) binaryImage.free(); console.log('\n' + '='.repeat(60)); console.log('Speed tests completed'); console.log('='.repeat(60)); }); it("Sauvola binarization performance", function() { const params = { window: 75, k: 0.2 }; const results = measureTime(() => { return doxa.toBinary(doxa.binarization.SAUVOLA, grayscaleImage, params); }); console.log(formatResults('Sauvola', results, imageWidth, imageHeight)); expect(results.avg).toBeGreaterThan(0); }); it("WAN binarization performance", function() { const params = { window: 75, k: 0.2 }; const results = measureTime(() => { return doxa.toBinary(doxa.binarization.WAN, grayscaleImage, params); }); console.log(formatResults('WAN', results, imageWidth, imageHeight)); expect(results.avg).toBeGreaterThan(0); }); it("Otsu (GlobalThresholding) binarization performance", function() { const results = measureTime(() => { return doxa.toBinary(doxa.binarization.OTSU, grayscaleImage, {}); }); console.log(formatResults('Otsu (GlobalThresholding)', results, imageWidth, imageHeight)); expect(results.avg).toBeGreaterThan(0); }); it("Full Performance (ClassifiedPerformance + DRDM) calculation", function() { console.log('\n' + '-'.repeat(60)); console.log('PERFORMANCE METRICS'); console.log('-'.repeat(60)); // Note: The WebAssembly binding calculates ClassifiedPerformance and DRDM together. // Use Python binding's calculate_performance_ex for isolated measurements. const results = measureTime(() => { return doxa.calculatePerformance(groundTruthImage, binaryImage); }); console.log(formatResults('Full Performance (ClassifiedPerformance + DRDM)', results, imageWidth, imageHeight)); expect(results.avg).toBeGreaterThan(0); }); }); ================================================ FILE: Bindings/WebAssembly/spec/support/jasmine.json ================================================ { "spec_dir": "spec", "spec_files": [ "**/*[sS]pec.?(m)js" ], "helpers": [ "helpers/**/*.?(m)js" ], "env": { "stopSpecOnExpectationFailure": false, "random": true } } ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Overview Doxa is a **header-only C++ image binarization framework** that converts grayscale images to binary (black and white). The core library has zero external dependencies and is designed to be lightweight and easy to integrate with other frameworks (OpenCV, Qt, etc.). Language bindings exist for Python and WebAssembly/JavaScript. ## Build and Test Commands The project uses CMake presets for unified builds. All commands run from the project root. ### Quick Start (CMake Presets) ```bash # Build and run C++ unit tests cmake --preset cpp-tests cmake --build build --config Release ctest --test-dir build -C Release # Build and test Python bindings cmake --preset python cmake --build build-python --config Release ctest --test-dir build-python -C Release # Build and test WebAssembly (requires Emscripten in PATH) cmake --preset wasm cmake --build build-wasm --config Release ctest --test-dir build-wasm -C Release # Build and run performance benchmarks (Google Benchmark) cmake --preset benchmarks cmake --build build-bench --config Release ./build-bench/Doxa.Bench/doxa_bench # Linux/Mac .\build-bench\Doxa.Bench\Release\doxa_bench.exe # Windows # Build everything (C++, Python, WASM) cmake --preset all cmake --build build --config Release ctest --test-dir build -C Release ``` **Note:** On Windows with Visual Studio (multi-config generator), `--config Release` and `-C Release` are required. On Linux/Mac with single-config generators (Make, Ninja), these flags are optional. ### C++ Unit Tests The C++ test suite uses Google Test (fetched automatically via CMake). ```bash # Using preset (recommended) cmake --preset cpp-tests cmake --build build --config Release ctest --test-dir build -C Release # Or build directly from Doxa.Test cd Doxa.Test cmake -S . -B ./build cmake --build ./build --config Release ctest --test-dir ./build -C Release # Or run directly: ./build/doxa_test # Linux/Mac .\build\Release\doxa_test.exe # Windows ``` ### Python Bindings (DoxaPy) DoxaPy requires Python 3.12+ and uses nanobind for C++ interop. ```bash # Using preset (recommended) cmake --preset python cmake --build build-python --config Release ctest --test-dir build-python -C Release # Or build from Bindings/Python directory cd Bindings/Python pip install -r requirements.txt python copy-cpp-files.py cmake -S . -B ./build cmake --build ./build --config Release python test/test_doxa.py # Build distributable wheel (uses cibuildwheel) python -m build ``` ### WebAssembly Bindings (DoxaJs) DoxaJs uses CMake with Emscripten toolchain. ```bash # Using preset (recommended, requires emcmake in PATH) cmake --preset wasm cmake --build build-wasm --config Release ctest --test-dir build-wasm -C Release # Or build directly with emcmake emcmake cmake -S . -B build-wasm -DCMAKE_BUILD_TYPE=Release cmake --build build-wasm --config Release cd Bindings/WebAssembly && npm test # Output: ./dist/doxaWasm.js and ./dist/doxaWasm.wasm ``` ## Architecture ### Header-Only Core Library The entire core library is in `Doxa/*.hpp` files. There is no build step for the core library - just include the headers. **Key Files:** - `Doxa/Image.hpp` - Core 8-bit image data structure with move semantics - `Doxa/Algorithm.hpp` - Base algorithm interface using CRTP (Curiously Recurring Template Pattern) - `Doxa/Parameters.hpp` - Key-value parameter system (supports `{"window": 75, "k": 0.2}`) - `Doxa/PNM.hpp` - Image I/O for Portable Any-Map formats (PBM, PGM, PPM, PAM) - `Doxa/BinarizationFactory.hpp` - Factory pattern for algorithm instantiation - `Doxa/ClassifiedPerformance.hpp` - Performance metrics (Accuracy, F-Measure, Precision, Recall, MCC, PSNR, NRM) - `Doxa/DRDM.hpp` - Distance-Reciprocal Distortion Measure metric ### Algorithm Organization All binarization algorithms inherit from `Algorithm` base class (CRTP pattern): **Global Thresholding:** - `Otsu.hpp` - Histogram-based single threshold **Local Adaptive Thresholding:** - `Sauvola.hpp`, `Niblack.hpp`, `Wolf.hpp`, `Nick.hpp`, `TRSingh.hpp`, `Bernsen.hpp`, `Phansalkar.hpp`, `ISauvola.hpp`, `Wan.hpp`, `Su.hpp`, `Gatos.hpp`, `Bataineh.hpp`, `AdOtsu.hpp` Each algorithm can be called statically via: ```cpp Image binaryImage = Sauvola::ToBinaryImage(grayImage, parameters); ``` Or instantiated for reuse: ```cpp Sauvola sauvola; sauvola.Initialize(grayImage); sauvola.ToBinary(binaryImage, parameters); ``` ### Calculation Optimizations The framework includes multiple optimization strategies for local window operations: - `ChanMeanVarianceCalc.hpp` - Memory-efficient sliding window (Chan 2019) - `IntegralImageMeanVarianceCalc.hpp` - Integral image acceleration (Shafait 2008) - `LocalWindow.hpp` - Generic window iteration template - `GridCalc.hpp` - Grid-based calculations Algorithms use these via template inheritance to avoid virtual function overhead. ### Language Bindings Architecture **Python (DoxaPy):** - `Bindings/Python/src/DoxaPy.cpp` - nanobind wrapper exposing all 14 algorithms - Converts NumPy arrays ↔ Doxa Image objects - Usage pattern: ```python sauvola = doxapy.Binarization(doxapy.Binarization.Algorithms.SAUVOLA) sauvola.initialize(grayscale_image) # NumPy array sauvola.to_binary(binary_image, {"window": 75, "k": 0.2}) ``` - Build system: scikit-build-core + cibuildwheel for cross-platform wheels - Published to PyPI as `doxapy` **WebAssembly (DoxaJs):** - `Bindings/WebAssembly/DoxaWasm.cpp` - Emscripten bindings - `Bindings/WebAssembly/doxa.js` - JavaScript convenience wrapper - Works with raw memory pointers (WASM memory model) - Build output: `./dist/doxaWasm.js` and `./dist/doxaWasm.wasm` - Live demo: https://brandonmpetty.github.io/Doxa/WebAssembly ### Integration with External Libraries The `Image` class supports external memory management via `Image::Reference()`, allowing zero-copy integration with: - OpenCV (see `Demo/Cpp/demoOpenCV.cpp`) - Qt (see `Demo/Cpp/demoQt.cpp`) - PIL/Pillow (see `Demo/Python/demo.py`) ## Testing Structure **C++ Tests (`Doxa.Test/`):** - Uses Google Test framework (fetched via CMake FetchContent) - Test files mirror the module structure: `ImageTests.cpp`, `BinarizationTests.cpp`, `CalculatorTests.cpp`, etc. - `ImageFixture.hpp` provides test utilities and sample images - `Resources/` contains ground truth images for validation - Requires C++17 standard - Tests focus on **correctness only** - no timing assertions **Performance Benchmarks (`Doxa.Bench/`):** - Uses Google Benchmark framework (fetched via CMake FetchContent) - Separate from unit tests - measures runtime performance with statistical rigor - Benchmark files: `GlobalThresholdBenchmarks.cpp`, `ClassifiedPerformanceBenchmarks.cpp`, `DRDMBenchmarks.cpp`, `CalculatorBenchmarks.cpp` - `BenchmarkHarness.hpp` exposes internal methods for benchmarking - Resource path is baked in at build time via `configure_file` (works from any working directory) - Always built in Release mode (benchmarks in Debug are meaningless) - Output formats: console table (default) or JSON (`--benchmark_out=file.json --benchmark_out_format=json`) **Comparing Benchmark Runs:** ```bash # Save JSON output from two runs (e.g., before/after a change, or different platforms) ./doxa_bench --benchmark_out=before.json --benchmark_out_format=json # ... make changes, rebuild ... ./doxa_bench --benchmark_out=after.json --benchmark_out_format=json # Compare all benchmarks python build-bench/_deps/googlebenchmark-src/tools/compare.py benchmarks before.json after.json # Compare only specific benchmarks python build-bench/_deps/googlebenchmark-src/tools/compare.py benchmarksfiltered before.json after.json BM_ToBinary # Compare two benchmarks within the same run (e.g., scalar vs SIMD) python build-bench/_deps/googlebenchmark-src/tools/compare.py filters results.json BM_ToBinary_Scalar BM_ToBinary_SIMD ``` **Python Tests:** - `Bindings/Python/test/test_doxa.py` - Basic functionality and performance tests - Run with: `python test/test_doxa.py` ## Key Design Patterns 1. **CRTP (Curiously Recurring Template Pattern)**: `Algorithm` base class provides static polymorphism without virtual function overhead 2. **Template-Based Calculators**: Algorithms inherit from calculator classes (e.g., `ChanMeanVarianceCalc`) using templates for zero-cost abstractions 3. **Factory Pattern**: `BinarizationFactory` enables dynamic algorithm selection at runtime 4. **Parameter Variant System**: `Parameters` class uses variant for flexible, serializable configuration 5. **Move Semantics**: `Image` class uses move constructors/assignment for efficient memory management ## Common Development Workflows ### Adding a New Binarization Algorithm 1. Create `Doxa/MyAlgorithm.hpp` 2. Inherit from `Algorithm` (CRTP pattern) 3. Choose appropriate calculator base class (e.g., `ChanMeanVarianceCalc`) 4. Implement `CalculateThreshold()` method with algorithm-specific logic 5. Add algorithm to `BinarizationFactory.hpp` 6. Create corresponding test in `Doxa.Test/BinarizationTests.cpp` 7. Update bindings in `DoxaPy.cpp` and `DoxaWasm.cpp` ### Modifying Python Bindings 1. Edit `Bindings/Python/src/DoxaPy.cpp` (nanobind code) 2. Rebuild: `cmake --build ./build --config Release` 3. Test: `python test/test_doxa.py` 4. For distribution: update version in `pyproject.toml` and run `python -m build` ### Modifying WebAssembly Bindings 1. Edit `Bindings/WebAssembly/DoxaWasm.cpp` (Emscripten bindings) 2. Optionally update `doxa.js` (JavaScript wrapper) 3. Rebuild: `emcmake cmake -S . -B build-wasm && cmake --build build-wasm` 4. Test: `cd Bindings/WebAssembly && npm test` ### Adding or Modifying Benchmarks 1. Edit or create benchmark files in `Doxa.Bench/` (e.g., `SIMDBenchmarks.cpp`) 2. Use `BenchmarkHarness.hpp` for test harness classes that expose internal methods 3. Setup code goes **before** the `for (auto _ : state)` loop (not timed) 4. Only the loop body is measured; use `benchmark::DoNotOptimize()` to prevent dead code elimination 5. Add new `.cpp` files to `Doxa.Bench/CMakeLists.txt` 6. Build: `cmake --build build-bench --config Release` 7. Run: `./build-bench/Doxa.Bench/doxa_bench` (or with `--benchmark_filter=BM_MyBench` to run specific benchmarks) **CI Integration:** The `benchmarks.yml` workflow runs on all 3 platforms (Linux, Windows, macOS). Each platform has its own independent baseline tracked via `benchmark-action/github-action-benchmark` in the `gh-pages` branch. PR comments alert on regressions >20%. ## Performance Metrics The framework includes comprehensive performance evaluation tools: - **ClassifiedPerformance**: Accuracy, F-Measure, Precision, Recall, MCC, PSNR, NRM - **Pseudo-Metrics**: Pseudo F-Measure, Precision, Recall (for degraded documents) - **DRDM**: Distance-Reciprocal Distortion Measure (perceptual quality) Usage: ```cpp ClassifiedPerformance performance = ClassifiedPerformance::CalculatePerformance(groundTruth, binaryImage); ``` ## Build System Notes - **C++ Core**: Header-only, no build required - **C++ Tests**: CMake 3.16+ or Visual Studio 2019+ - **Python**: CMake + nanobind + scikit-build-core (requires Python 3.12+) - **WebAssembly**: CMake + Emscripten toolchain (use `emcmake cmake`) - **Standard**: C++17 - **Architecture**: 64-bit only (enforced in cibuildwheel config) - **Performance Benchmarks**: CMake + Google Benchmark (fetched via FetchContent) - **CMake Presets**: `cpp-tests`, `cpp-tests-debug`, `python`, `wasm`, `benchmarks`, `all` ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.16) project(doxa VERSION 1.0.0) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Build options option(DOXA_BUILD_CPP_TESTS "Build C++ unit tests" ON) option(DOXA_BUILD_PYTHON "Build Python bindings" OFF) option(DOXA_BUILD_WASM "Build WebAssembly bindings (requires emcmake)" OFF) option(DOXA_BUILD_BENCHMARKS "Build performance benchmarks (Google Benchmark)" OFF) option(DOXA_BUILD_MATLAB "Build MATLAB MEX bindings (requires MATLAB)" OFF) option(DOXA_ENABLE_SIMD "Enable SIMD optimizations" ON) # Enable CTest for all test types include(CTest) enable_testing() # Propagate SIMD option to subdirectories set(DOXA_ENABLE_SIMD ${DOXA_ENABLE_SIMD} CACHE BOOL "" FORCE) set(DOXAPY_ENABLE_SIMD ${DOXA_ENABLE_SIMD} CACHE BOOL "" FORCE) # C++ Tests (native build only) if(DOXA_BUILD_CPP_TESTS AND NOT EMSCRIPTEN) add_subdirectory(Doxa.Test) endif() # Performance Benchmarks (native build only, always Release) if(DOXA_BUILD_BENCHMARKS AND NOT EMSCRIPTEN) add_subdirectory(Doxa.Bench) endif() # Python Bindings (native build only) if(DOXA_BUILD_PYTHON AND NOT EMSCRIPTEN) find_package(Python 3.12 REQUIRED COMPONENTS Interpreter) # Sync headers from core library execute_process( COMMAND ${Python_EXECUTABLE} copy-cpp-files.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/Bindings/Python ) add_subdirectory(Bindings/Python) # Add Python tests to CTest add_test(NAME python_tests COMMAND ${Python_EXECUTABLE} -m unittest discover -s test -p "test_*.py" -v WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/Bindings/Python ) # Set PYTHONPATH so tests can find the built doxapy module in dist/ set_tests_properties(python_tests PROPERTIES ENVIRONMENT "PYTHONPATH=${CMAKE_SOURCE_DIR}/Bindings/Python/dist" ) endif() # MATLAB MEX Bindings (native build only) if(DOXA_BUILD_MATLAB AND NOT EMSCRIPTEN) find_package(Matlab COMPONENTS MAIN_PROGRAM) if(Matlab_FOUND) add_subdirectory(Bindings/Matlab) else() message(WARNING "DOXA_BUILD_MATLAB=ON but MATLAB not found. " "Skipping MATLAB MEX bindings. Install MATLAB or set Matlab_ROOT_DIR.") endif() endif() # WebAssembly - two modes: # 1. Direct Emscripten build (when using emcmake) # 2. ExternalProject build (when doing native build with DOXA_BUILD_WASM=ON) if(EMSCRIPTEN) add_subdirectory(Bindings/WebAssembly) elseif(DOXA_BUILD_WASM) # Platform-specific script extensions (avoid picking up .cmd/.bat on WSL2) if(WIN32) set(CMD_EXT ".cmd") set(BAT_EXT ".bat") else() set(CMD_EXT "") set(BAT_EXT "") endif() # Build WASM as external project using emcmake find_program(EMCMAKE NAMES emcmake${BAT_EXT} HINTS ENV EMSDK PATH_SUFFIXES upstream/emscripten ) if(EMCMAKE) message(STATUS "Found emcmake: ${EMCMAKE}") include(ExternalProject) ExternalProject_Add(wasm_build SOURCE_DIR ${CMAKE_SOURCE_DIR} BINARY_DIR ${CMAKE_BINARY_DIR}/wasm CONFIGURE_COMMAND ${EMCMAKE} cmake ${CMAKE_SOURCE_DIR} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} BUILD_COMMAND cmake --build . --config ${CMAKE_BUILD_TYPE} INSTALL_COMMAND "" TEST_COMMAND "" ) # Add WASM tests to CTest (runs after wasm_build) find_program(NPM_EXECUTABLE NAMES npm${CMD_EXT}) if(NPM_EXECUTABLE) add_test(NAME wasm_tests COMMAND ${NPM_EXECUTABLE} test WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/Bindings/WebAssembly ) endif() else() message(WARNING "DOXA_BUILD_WASM=ON but emcmake not found. " "Make sure Emscripten SDK is installed and run emsdk_env before configuring CMake.") endif() endif() ================================================ FILE: CMakePresets.json ================================================ { "version": 6, "configurePresets": [ { "name": "cpp-tests", "displayName": "C++ Tests", "binaryDir": "${sourceDir}/build-cpp-tests", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "DOXA_BUILD_CPP_TESTS": "ON" } }, { "name": "cpp-tests-debug", "displayName": "C++ Tests (Debug)", "binaryDir": "${sourceDir}/build-cpp-tests", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "DOXA_BUILD_CPP_TESTS": "ON" } }, { "name": "python", "displayName": "Python Bindings", "binaryDir": "${sourceDir}/build-python", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "DOXA_BUILD_CPP_TESTS": "OFF", "DOXA_BUILD_PYTHON": "ON" } }, { "name": "wasm", "displayName": "WebAssembly Bindings", "binaryDir": "${sourceDir}/build-wasm", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "DOXA_BUILD_CPP_TESTS": "OFF", "DOXA_BUILD_WASM": "ON" } }, { "name": "benchmarks", "displayName": "Performance Benchmarks", "binaryDir": "${sourceDir}/build-bench", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "DOXA_BUILD_CPP_TESTS": "OFF", "DOXA_BUILD_BENCHMARKS": "ON" } }, { "name": "matlab", "displayName": "MATLAB MEX Bindings", "binaryDir": "${sourceDir}/build-matlab", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "DOXA_BUILD_CPP_TESTS": "OFF", "DOXA_BUILD_MATLAB": "ON" } }, { "name": "all", "displayName": "All Components (C++, Python, WASM, MATLAB)", "binaryDir": "${sourceDir}/build", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "DOXA_BUILD_CPP_TESTS": "ON", "DOXA_BUILD_PYTHON": "ON", "DOXA_BUILD_WASM": "ON", "DOXA_BUILD_MATLAB": "ON" } } ], "buildPresets": [ { "name": "cpp-tests", "configurePreset": "cpp-tests" }, { "name": "cpp-tests-debug", "configurePreset": "cpp-tests-debug" }, { "name": "python", "configurePreset": "python" }, { "name": "wasm", "configurePreset": "wasm" }, { "name": "benchmarks", "configurePreset": "benchmarks" }, { "name": "matlab", "configurePreset": "matlab" }, { "name": "all", "configurePreset": "all" } ], "testPresets": [ { "name": "cpp-tests", "configurePreset": "cpp-tests", "configuration": "Release" }, { "name": "cpp-tests-debug", "configurePreset": "cpp-tests-debug", "configuration": "Debug" }, { "name": "python", "configurePreset": "python", "configuration": "Release" }, { "name": "wasm", "configurePreset": "wasm", "configuration": "Release" }, { "name": "matlab", "configurePreset": "matlab", "configuration": "Release" }, { "name": "all", "configurePreset": "all", "configuration": "Release" } ] } ================================================ FILE: Demo/Cpp/.gitignore ================================================ *.png *.obj *.dll *.exe ================================================ FILE: Demo/Cpp/demo.cpp ================================================ #include #include "../../Doxa/Sauvola.hpp" #include "../../Doxa/ClassifiedPerformance.hpp" #include "../../Doxa/DRDM.hpp" #include "../../Doxa/PNM.hpp" // Visual C++ Compiler: //cl /EHsc /std:c++17 /O2 demo.cpp // Clang C++ Compiler - Mac OSX: //clang++ -Wall -std=c++17 -L /usr/local/Cellar/llvm/7.0.1/lib/ -lc++fs -O3 demo.cpp -o demo void DisplayPerformance(const Doxa::Image& groundTruthImage, const Doxa::Image& binaryImage) { Doxa::ClassifiedPerformance::Classifications classifications; bool canCompare = Doxa::ClassifiedPerformance::CompareImages(classifications, groundTruthImage, binaryImage); if (!canCompare) { std::cout << "Files cannot be compared. Ensure both images have the same height and width." << std::endl; return; } double scoreAccuracy = Doxa::ClassifiedPerformance::CalculateAccuracy(classifications); double scoreFM = Doxa::ClassifiedPerformance::CalculateFMeasure(classifications); double scoreMCC = Doxa::ClassifiedPerformance::CalculateMCC(classifications); double scorePSNR = Doxa::ClassifiedPerformance::CalculatePSNR(classifications); double scoreNRM = Doxa::ClassifiedPerformance::CalculateNRM(classifications); double scoreDRDM = Doxa::DRDM::CalculateDRDM(groundTruthImage, binaryImage); std::cout << std::endl << "Accuracy:\t" << scoreAccuracy << std::endl << "F-Measure:\t" << scoreFM << std::endl << "MCC:\t\t" << scoreMCC << std::endl << "PSNR:\t\t" << scorePSNR << std::endl << "NRM:\t\t" << scoreNRM << std::endl << "DRDM:\t\t" << scoreDRDM << std::endl << std::endl; } int main() { // Read image and turn it into an 8bit grayscale. // The PNM reader is obviously limiting, but does offer 8 different color to grayscale algorithms. Doxa::Image doxaGsImage = Doxa::PNM::Read(R"(2JohnC1V3.ppm)", Doxa::Parameters({{"grayscale", Doxa::GrayscaleAlgorithms::MEAN}})); // Optional // Use a binarization algorithm to convert it into black and white const Doxa::Parameters parameters({ {"window", 25}, {"k", 0.10} }); Doxa::Sauvola::UpdateToBinary(doxaGsImage, parameters); // If you want to store the binary image in a new object, run this: //Doxa::Image doxaBinImage = Doxa::Sauvola::ToBinaryImage(doxaGsImage, parameters); // Load the ground truth image Doxa::Image doxaGtImage = Doxa::PNM::Read(R"(2JohnC1V3-GroundTruth.pbm)"); // Get Performance information DisplayPerformance(doxaGtImage, doxaGsImage); // Save the processed image Doxa::PNM::Write(doxaGsImage, R"(binary.pbm)"); return 0; } ================================================ FILE: Demo/Cpp/demoOpenCV.cpp ================================================ #include #include "../../Doxa/Sauvola.hpp" #include "../../Doxa/ClassifiedPerformance.hpp" #include "../../Doxa/DRDM.hpp" // Visual C++ Compiler: //cl /EHsc /std:c++17 /O2 /I"%OPENCV_DIR%\include" "%OPENCV_DIR%\x64\vc15\lib\opencv_world451.lib" demoOpenCV.cpp Doxa::Image ToDoxaImageReference(const cv::Mat& gsImage) { assert(gsImage.channels() == 1); return Doxa::Image::Reference(gsImage.cols, gsImage.rows, (Doxa::Pixel8*)gsImage.data); } cv::Mat FromDoxaImage(const Doxa::Image& binaryImage) { return cv::Mat(binaryImage.height, binaryImage.width, CV_8UC1, binaryImage.data); } void DisplayPerformance(const Doxa::Image& groundTruthImage, const Doxa::Image& binaryImage) { Doxa::ClassifiedPerformance::Classifications classifications; bool canCompare = Doxa::ClassifiedPerformance::CompareImages(classifications, groundTruthImage, binaryImage); if (!canCompare) { std::cout << "Files cannot be compared. Ensure both images have the same height and width." << std::endl; return; } double scoreAccuracy = Doxa::ClassifiedPerformance::CalculateAccuracy(classifications); double scoreFM = Doxa::ClassifiedPerformance::CalculateFMeasure(classifications); double scoreMCC = Doxa::ClassifiedPerformance::CalculateMCC(classifications); double scorePSNR = Doxa::ClassifiedPerformance::CalculatePSNR(classifications); double scoreNRM = Doxa::ClassifiedPerformance::CalculateNRM(classifications); double scoreDRDM = Doxa::DRDM::CalculateDRDM(groundTruthImage, binaryImage); std::cout << std::endl << "Accuracy:\t" << scoreAccuracy << std::endl << "F-Measure:\t" << scoreFM << std::endl << "MCC:\t\t" << scoreMCC << std::endl << "PSNR:\t\t" << scorePSNR << std::endl << "NRM:\t\t" << scoreNRM << std::endl << "DRDM:\t\t" << scoreDRDM << std::endl << std::endl; } int main() { // Read image and turn it into an 8bit grayscale. This should be a CV_8UC1 structure. cv::Mat cvGsImage = cv::imread(R"(2JohnC1V3.png)", cv::IMREAD_GRAYSCALE); // Create a reference to the cv::Mat image. // Not only is this effecient, but any changes made by our algorithm apply directly // to the image being referenced. Doxa::Image doxaGsImage = ToDoxaImageReference(cvGsImage); // Use a binarization algorithm to convert it into black and white const Doxa::Parameters parameters({ {"window", 25}, {"k", 0.10} }); Doxa::Sauvola::UpdateToBinary(doxaGsImage, parameters); // If you want to store the binary image in a new object, run this: //Doxa::Image doxaBinImage = Doxa::Sauvola::ToBinaryImage(doxaGsImage, parameters); //cv::Mat cvBinImage = FromDoxaImage(doxaBinImage); // Load the ground truth image cv::Mat cvGtImage = cv::imread(R"(2JohnC1V3-GroundTruth.png)", cv::IMREAD_GRAYSCALE); Doxa::Image doxaGtImage = ToDoxaImageReference(cvGtImage); // Get Performance information DisplayPerformance(doxaGtImage, doxaGsImage); // Save the processed image cv::imwrite(R"(binary.png)", cvGsImage); return 0; } ================================================ FILE: Demo/Cpp/demoQt.cpp ================================================ #include #include #include #include "../../Doxa/Sauvola.hpp" #include "../../Doxa/ClassifiedPerformance.hpp" #include "../../Doxa/DRDM.hpp" // Build Instructions - Windows // qmake // nmake Doxa::Image ToDoxaImageReference(const QImage& gsImage) { assert(gsImage.format() == QImage::Format_Grayscale8); return Doxa::Image::Reference(gsImage.width(), gsImage.height(), (Doxa::Pixel8*)gsImage.bits()); } QImage FromDoxaImage(const Doxa::Image& binaryImage) { // This QImage object does not contain a copy of the image memory, but a reference to it. return QImage(binaryImage.data, binaryImage.width, binaryImage.height, binaryImage.width, QImage::Format_Grayscale8); } void DisplayPerformance(const Doxa::Image& groundTruthImage, const Doxa::Image& binaryImage) { Doxa::ClassifiedPerformance::Classifications classifications; bool canCompare = Doxa::ClassifiedPerformance::CompareImages(classifications, groundTruthImage, binaryImage); if (!canCompare) { std::cout << "Files cannot be compared. Ensure both images have the same height and width." << std::endl; return; } double scoreAccuracy = Doxa::ClassifiedPerformance::CalculateAccuracy(classifications); double scoreFM = Doxa::ClassifiedPerformance::CalculateFMeasure(classifications); double scoreMCC = Doxa::ClassifiedPerformance::CalculateMCC(classifications); double scorePSNR = Doxa::ClassifiedPerformance::CalculatePSNR(classifications); double scoreNRM = Doxa::ClassifiedPerformance::CalculateNRM(classifications); double scoreDRDM = Doxa::DRDM::CalculateDRDM(groundTruthImage, binaryImage); std::cout << std::endl << "Accuracy:\t" << scoreAccuracy << std::endl << "F-Measure:\t" << scoreFM << std::endl << "MCC:\t\t" << scoreMCC << std::endl << "PSNR:\t\t" << scorePSNR << std::endl << "NRM:\t\t" << scoreNRM << std::endl << "DRDM:\t\t" << scoreDRDM << std::endl << std::endl; } int main() { // Read image and turn it into an 8bit grayscale. QImage qtGsImage(R"(2JohnC1V3.png)"); qtGsImage = qtGsImage.convertToFormat(QImage::Format_Grayscale8); // Create a reference to the QImage. // Not only is this effecient, but any changes made by our algorithm apply directly // to the image being referenced. Doxa::Image doxaGsImage = ToDoxaImageReference(qtGsImage); // Use a binarization algorithm to convert it into black and white const Doxa::Parameters parameters({ {"window", 25}, {"k", 0.10} }); Doxa::Sauvola::UpdateToBinary(doxaGsImage, parameters); // If you want to store the binary image in a new object, run this: //Doxa::Image doxaBinImage = Doxa::Sauvola::ToBinaryImage(doxaGsImage, parameters); //QImage qtBinImage = FromDoxaImage(doxaBinImage); // Load the ground truth image QImage qtGtImage(R"(2JohnC1V3-GroundTruth.png)"); qtGtImage = qtGtImage.convertToFormat(QImage::Format_Grayscale8); Doxa::Image doxaGtImage = ToDoxaImageReference(qtGtImage); // Get Performance information DisplayPerformance(doxaGtImage, doxaGsImage); // Save the processed image qtGsImage.save(R"(binary.png)"); return 0; } ================================================ FILE: Demo/Cpp/demoQt.pro ================================================ ###################################################################### # Automatically generated by qmake (3.1) Fri Jan 22 23:00:16 2021 ###################################################################### TEMPLATE = app TARGET = demoqt INCLUDEPATH += . CONFIG += console QMAKE_CXXFLAGS += /std:c++17 # The following define makes your compiler warn you if you use any # feature of Qt which has been marked as deprecated (the exact warnings # depend on your compiler). Please consult the documentation of the # deprecated API in order to know how to port your code away from it. DEFINES += QT_DEPRECATED_WARNINGS # You can also make your code fail to compile if you use deprecated APIs. # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 # Input HEADERS += ../../Doxa/Sauvola.hpp \ ../../Doxa/Algorithm.hpp \ ../../Doxa/Image.hpp \ ../../Doxa/Types.hpp \ ../../Doxa/Parameters.hpp \ ../../Doxa/Palette.hpp \ ../../Doxa/LocalWindow.hpp \ ../../Doxa/Region.hpp \ ../../Doxa/ChanMeanVarianceCalc.hpp \ ../../Doxa/ClassifiedPerformance.hpp \ ../../Doxa/DRDM.hpp SOURCES += demoQt.cpp ================================================ FILE: Demo/Matlab/demo.m ================================================ % Doxa Binarization Framework - MATLAB Demo % % This demo shows how to read an image, convert it to grayscale, binarize it, % and calculate performance metrics using the Doxa framework. % % Requirements: % - Build the MEX files: cmake --preset matlab && cmake --build build-matlab --config Release % - Add the MEX output directory to the MATLAB path % Read a color image and convert to grayscale img = Doxa.Image('../../Doxa.Test/Resources/2JohnC1V3.ppm', Doxa.Grayscale.QT); % Binarize using the Sauvola algorithm binary = Doxa.binarize(Doxa.Algorithms.SAUVOLA, img, window=27, k=0.1); % Load a ground truth image and calculate performance gt = Doxa.Image('../../Doxa.Test/Resources/2JohnC1V3-GroundTruth.pbm'); % Example: Pseudo-metrics with weight files pw = Doxa.readWeights('../../Doxa.Test/Resources/2JohnC1V3-GroundTruth_PWeights.dat'); rw = Doxa.readWeights('../../Doxa.Test/Resources/2JohnC1V3-GroundTruth_RWeights.dat'); metrics = Doxa.calculatePerformance(gt, binary, precisionWeights=pw, recallWeights=rw); disp('Performance Metrics:'); disp(metrics); % Display results figure('Name', 'Doxa Binarization Demo'); subplot(1, 3, 1); imshow(imread('../../Doxa.Test/Resources/2JohnC1V3.ppm')); title('Original'); subplot(1, 3, 2); imshow(img.toArray()); title('Grayscale'); subplot(1, 3, 3); imshow(binary.toArray()); title('Sauvola Binary'); % Example: In-place binarization (modifies the image directly) % Doxa.updateToBinary(Doxa.Algorithms.SAUVOLA, img, window=75, k=0.2); ================================================ FILE: Demo/NodeJS/.gitignore ================================================ *.png ================================================ FILE: Demo/NodeJS/index.js ================================================ /** * Doxa NodeJS Demo * This demo uses the WASM bindings and helper classes to seemlessly call into Doxa's binarization routines. * The code below gives an example of how to read an image, convert it to binary, and get performance stats. * We are using the image processing library Sharp to show how to read and write images in NodeJS. */ const { Doxa } = require('../../Bindings/WebAssembly/dist/doxa.js'); const sharp = require('sharp'); /** * An example image reader wrapper around Sharp. * @param {object} doxa Initialized Doxa instance * @param {*} file Input file location. Should be 8b grayscale, 24b RGB, or 32b RGBA. * @param {number} algorithm Grayscale algorithm enum value (e.g. doxa.grayscale.MEAN). Defaults to MEAN. */ async function readImage(doxa, file, algorithm) { return sharp(file) .raw() .toBuffer({ resolveWithObject: true }) .then(content => { return doxa.toGrayscale( content.data, content.info.width, content.info.height, content.info.channels, algorithm); }); } /** * An example image writer wrapper around Sharp. * @param {*} image A binary Doxa Image object * @param {*} file Output file location. This can be any supported format. */ async function writeImage(image, file) { return sharp(Buffer.from(image.data()), { raw: { width: image.width, height: image.height, channels: 1 // b&w } }).toFile(file); } async function demo() { // Initialize the Doxa framework const doxa = await Doxa.initialize(); // Read in the Ground Truth - 8bit const gtImage = await readImage(doxa, '../../README/2JohnC1V3-GroundTruth.png'); // Read in the target image - 8b, 24b, 32b. If color, convert to grayscale. const image = await readImage(doxa, '../../README/2JohnC1V3.png', doxa.grayscale.MEAN); // Generate a binary image const binImage = doxa.toBinary(doxa.binarization.SAUVOLA, image, { window: 27, k: 0.10 }); // Get performance information const perf = doxa.calculatePerformance(gtImage, binImage); console.dir(perf); // Write file await writeImage(binImage, 'binary.png'); // Remember to free the memory of your WASM based images gtImage.free(); image.free(); binImage.free(); } // Run our demo demo(); ================================================ FILE: Demo/NodeJS/package.json ================================================ { "name": "nodejsdemo", "version": "1.0.0", "description": "A simple demo showing how to use the Doxa library with NodeJS.", "main": "index.js", "scripts": { "start": "node index.js" }, "author": "", "license": "CC0-1.0", "devDependencies": { "sharp": "^0.34.5" } } ================================================ FILE: Demo/Python/demo.py ================================================ from PIL import Image import numpy as np import os # Attempt to load your local doxapy.abi3.so / doxapy.pyd build first import sys sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Bindings', 'Python', 'dist'))) import doxapy # Read an image. If its color, use one of our many Grayscale algorithms to convert it def read_image(file, algorithm=doxapy.GrayscaleAlgorithms.MEAN): image = Image.open(file) # If already in grayscale or binary, do not convert it if image.mode == 'L': return np.array(image) # Read the color image rgb_image = np.array(image.convert('RGB') if image.mode not in ('RGB', 'RGBA') else image) # Use Doxa to convert grayscale return doxapy.to_grayscale(algorithm, rgb_image) # Use Doxa to convert our image into grayscale, if it isn't already grayscale_image = read_image("../../Doxa.Test/Resources/2JohnC1V3.ppm", doxapy.GrayscaleAlgorithms.MEAN) # Convert the grayscale image to binary, returning a new image # NOTE: Algorithm parameters are options. Defaults are provided. binary_image = doxapy.to_binary(doxapy.Binarization.Algorithms.SAUVOLA, grayscale_image, {"window": 75, "k": 0.2}) # Calculate the binarization performance using a Ground Truth image groundtruth_image = read_image("../../Doxa.Test/Resources/2JohnC1V3-GroundTruth.pbm") performance = doxapy.calculate_performance(groundtruth_image, binary_image) print(performance) # Display our resulting image Image.fromarray(binary_image).show() # Example: How to update a grayscale to binary in place #doxapy.update_to_binary(doxapy.Binarization.Algorithms.SAUVOLA, grayscale_image, {"window": 75, "k": 0.2}) # Example: For testing parameter changes in a tight loop #binary_image = np.empty(grayscale_image.shape, grayscale_image.dtype) #sauvola = doxapy.Binarization(doxapy.Binarization.Algorithms.SAUVOLA) #sauvola.initialize(grayscale_image) #sauvola.to_binary(binary_image, {"window": 75, "k": 0.2}) ================================================ FILE: Demo/WebJS/index.html ================================================ Δoxa Binarization Framework - WebAssembly Demo Sorry. Your browser does not support HTML5 canvas element. ================================================ FILE: Doxa/AdOtsu.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2025, "Freely you have received; freely give." - Matt 10:8 #ifndef ADOTSU_HPP #define ADOTSU_HPP #include "Algorithm.hpp" #include "LocalWindow.hpp" #include "Otsu.hpp" #include "MultiScale.hpp" #include "GridCalc.hpp" #include namespace Doxa { /// /// The AdOtsu Algorithm, v2010: Reza Farrahi Moghaddam, Mohamed Cheriet /// /// This is the core, non-multi-scale, AdOtsu algorithm referenced in (5) of their paper. /// It should be noted that the paper uses a special color to grayscale algorithm in order to create a /// "non-sensitive gray-value image." This would be "GrayscaleAlgorithms::MINAVG". /// /// The second iteration of this algorithm was introduced a year later in their work: /// "AdOtsu: An adaptive and parameterless generalization of Otsu’s method for document image binarization" /// /// This later work builds on top of their earlier work. Our implementation features their earlier work /// which will act as a base for future improvements. /// /// "A multi-scale framework for adaptive binarization of degraded document images", 2010. class AdOtsu : public Algorithm { public: static const int HISTOGRAM_SIZE = 256; void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Read parameters, utilizing defaults const int windowSize = parameters.Get("window", 75); const double k = parameters.Get("k", 1.0); const double R = parameters.Get("R", 0.1); const int distance = parameters.Get("distance", (int)(windowSize / 2)); Otsu otsu; const Pixel8 globalThreshold = otsu.Threshold(Algorithm::grayScaleImageIn); // Bypass the "Grid" optimization. There is nothing to interpolate. if (distance < 2) { LocalWindow::Process(binaryImageOut, Algorithm::grayScaleImageIn, windowSize, [&](const Region& window, const int&) { const double localThreshold = k * LocalThreshold(otsu, Algorithm::grayScaleImageIn, window); const double u = (std::abs((double)globalThreshold - localThreshold) / R); // Apply the Unit Step Function return (u < 255) ? localThreshold : -1; }); } else // Use the "Grid" optimization { GridCalc gridCalc; // Calculate all thresholds through interpolation gridCalc.Process(binaryImageOut, Algorithm::grayScaleImageIn, windowSize, distance, [&](const Region& window, const int&) { const Pixel8 localThreshold = 0.5 + k * LocalThreshold(otsu, Algorithm::grayScaleImageIn, window); return localThreshold; }); // Turn the image into a binary image using the Unit Step Function for (int idx = 0; idx < binaryImageOut.size; ++idx) { const Pixel8 localThreshold = binaryImageOut.data[idx]; const Pixel8 localPixel = Algorithm::grayScaleImageIn.data[idx]; const double u = (std::abs((double)globalThreshold - localThreshold) / R); // Apply the Unit Step Function const int unitStep = (u < 255) ? localThreshold : -1; // Binarize the pixel binaryImageOut.data[idx] = localPixel <= unitStep ? Palette::Black : Palette::White; } } // Use grid interpolation? } Pixel8 LocalThreshold(const Otsu& otsu, const Image& grayScaleImage, const Region& window) { // Create Local Histogram unsigned int histogram[HISTOGRAM_SIZE]; // Placed on stack for performance. This shouldn't be too large. memset(histogram, 0, (HISTOGRAM_SIZE) * sizeof(unsigned int)); // Initialize Histogram from Local Window LocalWindow::Iterate(grayScaleImage.width, window, [&](const int& windowPosition) { ++histogram[grayScaleImage.data[windowPosition]]; }); return otsu.Algorithm(histogram, window.Area()); } }; /// /// A multi-scale local adaptive Otsu varient /// typedef MultiScale AdOtsuMS; } #endif //ADOTSU_HPP ================================================ FILE: Doxa/Algorithm.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef ALGORITHMS_HPP #define ALGORITHMS_HPP #include "Image.hpp" #include "Parameters.hpp" #include "Palette.hpp" #include "SIMDOps.hpp" namespace Doxa { /// /// Algorithm Interface - Useful if you want to dynamically instantiate an algorithm. /// class IAlgorithm { public: virtual ~IAlgorithm() { /* Virtual DTOR */ }; /// /// Sets the Gray Scale image that will later be used to generate a binary image. /// This allows the derived class to also initialize the image with any one time calculations. /// /// An Image object containing gray scale content virtual void Initialize(const Image& grayScaleImageIn) = 0; /// /// Takes the initialized Gray Scale image and returns back a Binary image by reference. /// The Binary image memory should already be allocated before being passed by reference. /// This method was designed to be called repeatedly with different parameters. /// /// An Image object with preallocated memory which will store the output /// Any parameters the algorithm may need virtual void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) = 0; }; /// /// This is a base class for all of our binarization algorithms. /// It uses the Curiously Recurring Template Pattern for compile time inheritance. /// template class Algorithm : public IAlgorithm { public: /// /// Sets the Gray Scale image that will later be used to generate a binary image. /// This allows the derived class to also initialize the image with any one time calculations. /// /// An Image object containing gray scale content virtual void Initialize(const Image& grayScaleImageIn) { this->grayScaleImageIn = grayScaleImageIn.Reference(); } /// /// A convenience method for taking in a Gray Scale image /w params and returning a Binary image. /// static Image ToBinaryImage(const Image& grayScaleImageIn, const Parameters& parameters = Parameters()) { // Generate space for the binary image Image binaryImageOut(grayScaleImageIn.width, grayScaleImageIn.height); // Run Binarization Algorithm BinaryAlgorithm algorithm; algorithm.Initialize(grayScaleImageIn); algorithm.ToBinary(binaryImageOut, parameters); // The Move semantics allow this our underlying image to move without being copied return binaryImageOut; } /// /// A convenience method for safely converting a Gray Scale image to Binary. /// Note: You may need to create a temp image and copy it, depending on your algorithm. /// static void UpdateToBinary(Image& image, const Parameters& parameters = Parameters()) { BinaryAlgorithm algorithm; algorithm.Initialize(image); algorithm.ToBinary(image, parameters); } protected: Image grayScaleImageIn; }; /// /// The base class for all Global Thresholding algorithms. /// template class GlobalThreshold : public Algorithm { public: /// /// Calculates and returns the global threshold of the image. /// /// A global binarization threshold value virtual Pixel8 Threshold(const Image& grayScaleImage, const Parameters& parameters = Parameters()) = 0; /// /// Global binarization based on a single threshold /// void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { const Pixel8 threshold = Threshold(Algorithm::grayScaleImageIn, parameters); const int size = Algorithm::grayScaleImageIn.size; const Pixel8* input = Algorithm::grayScaleImageIn.data; Pixel8* output = binaryImageOut.data; #if defined(DOXA_SIMD) ToBinary_SIMD(input, output, size, threshold); #else ToBinary_STD(input, output, size, threshold); #endif } /// /// Scalar implementation of threshold binarization - always available /// static void ToBinary_STD(const Pixel8* input, Pixel8* output, int size, Pixel8 threshold) { for (int idx = 0; idx < size; ++idx) { output[idx] = input[idx] <= threshold ? Palette::Black : Palette::White; } } #if defined(DOXA_SIMD) /// /// SIMD implementation of threshold binarization - only available when SIMD is enabled /// static void ToBinary_SIMD(const Pixel8* input, Pixel8* output, int size, Pixel8 threshold) { using namespace SIMD; int idx = 0; const int simd_end = size - (size % SIMD_WIDTH); vec128 threshold_vec = VEC_SPLAT_U8(threshold); for (; idx < simd_end; idx += SIMD_WIDTH) { vec128 pixels = VEC_LOAD(input + idx); // Compare: mask = (pixels <= threshold) -> 0xFF if true, 0x00 if false // Use min to implement <= comparison for unsigned bytes vec128 mask = VEC_CMPEQ_U8(VEC_MIN_U8(pixels, threshold_vec), pixels); // Since Black=0x00 and White=0xFF, result is simply NOT(mask) // mask=0xFF -> ~0xFF = 0x00 (black), mask=0x00 -> ~0x00 = 0xFF (white) VEC_STORE(output + idx, VEC_NOT(mask)); } // Handle remaining pixels with scalar for (; idx < size; ++idx) { output[idx] = input[idx] <= threshold ? Palette::Black : Palette::White; } } #endif // DOXA_SIMD }; } #endif //ALGORITHMS_HPP ================================================ FILE: Doxa/Bataineh.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef BATAINEH_HPP #define BATAINEH_HPP #include "Algorithm.hpp" #include "LocalWindow.hpp" #include "IntegralImageMeanVarianceCalc.hpp" ////////////////////////////////////////////////////////////////////////////// // This algorithm is being deemed unreproducible as of 09/10/2019. // // See: BatainehTests for a deep analysis of this implementation. // ////////////////////////////////////////////////////////////////////////////// namespace Doxa { /// /// The Bataineh Algorithm: Bilal Bataineh, Siti Norul Huda Sheikh Abdullah, Khairuddin Omar /// /// This implementation was painstakingly put together and does not match up perfectly with the results of the paper. /// While the results are usable, I have been unable to work with the authors to overcome many challenges presented /// in the paper. Due to the number of issues, this work is being deemed unreproducible. /// /// This algorithm should not be cited by any reputable research paper until these items have been addressed: /// 1. An obvious divide by zero problem in the SAdaptive equation /// 2. Partial code snippet from the author shows part of the Window Threshold equation being mult. by 2 /// 3. No independent example of this algorithm actually working /// /// Bilal Bataineh did provided me with a code snippet that was of some assistance. /// It should be noted that his implementation uses a Luma BT601 conversion: GrayscaleAlgorithms::BT601 /// /// /// "An adaptive local binarization method for document images based on a novel thresholding method and dynamic windows", 2011. class Bataineh : public Algorithm, public IntegralImageMeanVarianceCalc { public: void Initialize(const Image& grayScaleImageIn) { Algorithm::Initialize(grayScaleImageIn); // Initialize Integral Images Bataineh::imageWidth = grayScaleImageIn.width; Bataineh::integralImage.resize(grayScaleImageIn.size); Bataineh::integralSqrImage.resize(grayScaleImageIn.size); BuildIntegralImages(Bataineh::integralImage, Bataineh::integralSqrImage, Algorithm::grayScaleImageIn); } void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Get global std-dev and mean values double sigmaGlobal; double meanGlobal; CalculateGlobals(meanGlobal, sigmaGlobal); // Get Max Gray Value const Pixel8 maxGrayValue = GetMaxGrayValue(); // Calculate Confusion Threshold const double confThreshold = ConfusionThreshold(meanGlobal, sigmaGlobal, maxGrayValue); // Find total Red and Black pixels and temp. store them in our output. Red pixels are the confused pixels. int redCountImage; int blackCountImage; RedBlack(redCountImage, blackCountImage, binaryImageOut, confThreshold, sigmaGlobal); // Break the image into Primary and Secondary windows // Our output image is temporarily storing our Black/White/Red image std::vector windows = GetWindows(binaryImageOut, blackCountImage, redCountImage, sigmaGlobal, maxGrayValue); // Get Sigma Max and Min as well as local window Sigma and Mean double sigmaMax; double sigmaMin; SigmaMinMaxAndMean(sigmaMin, sigmaMax, windows); // Apply threshold for (auto &detailedWindow : windows) { // Calculate threshold for the Window const double sigmaAdaptive = SigmaAdaptive(detailedWindow.stddev, sigmaMin, sigmaMax, maxGrayValue); const double threshold = WindowThreshold(detailedWindow.mean, meanGlobal, detailedWindow.stddev, sigmaAdaptive); // Convert to binary LocalWindow::Iterate(Algorithm::grayScaleImageIn.width, detailedWindow.window, [&](const int& positionWindow) { binaryImageOut.data[positionWindow] = Algorithm::grayScaleImageIn.data[positionWindow] <= threshold ? Palette::Black : Palette::White; }); } } protected: enum PixelColor { BLACK = Palette::Black, RED = 128, WHITE = Palette::White }; struct DetailedWindow { Region window; double mean; double stddev; }; void CalculateGlobals(double& mean, double& stddev) const { const Region window(Algorithm::grayScaleImageIn.width, Algorithm::grayScaleImageIn.height); CalculateMeanStdDev(mean, stddev, window); } inline void CalculateMeanStdDev(double& mean, double& stddev, const Region& window) const { double variance; CalculateMeanVariance(mean, variance, Bataineh::imageWidth, Bataineh::integralImage, Bataineh::integralSqrImage, window); stddev = std::sqrt(variance); } /// /// Calculates the size of the Primary Window. These windows are fixed to the image and do not surround the pixel. /// Note: While this algorithm does not have a fixed Windows Size nor K value, it does have these magic numbers. /// I believe these numbers assume an image of a certain size, with a certain resolution or character size. /// void inline constexpr PrimaryWindow(int& primaryWidth, int& primaryHeight, double p, double sigmaGlobal, Pixel8 maxGray, int imageWidth, int imageHeight) { if (p >= 2.5 || (sigmaGlobal < 0.1*maxGray)) { primaryWidth = imageWidth / 6; primaryHeight = imageHeight / 4; } else if (p > 1 || (imageWidth + imageHeight < 400)) { primaryWidth = imageWidth / 30; primaryHeight = imageHeight / 20; } else { primaryWidth = imageWidth / 40; primaryHeight = imageHeight / 30; } } double inline constexpr SigmaAdaptive(const double sigmaWindow, const double sigmaMin, const double sigmaMax, const Pixel8 maxGray) { // Note: In the original paper, this had a divide by 0 problem (SigmaMax - SigmaMax). Bilal helped clear this confusion. return ((sigmaWindow - sigmaMin) / (sigmaMax - sigmaMin)) * maxGray; } double inline constexpr WindowThreshold(const double meanWindow, const double meanGlobal, const double sigmaWindow, const double sigmaAdaptive) { // Note: In the original paper, the Mw^2 * Sw was Mw^2 - Sw. The authors later corrected it. // Note: The author gave me some partial code that contained this function. SigmaAdaptive was multiplied by 2! // This threshold value can go negative at times, and even return NAN! In both cases the block will be white. return meanWindow - ((meanWindow*meanWindow * sigmaWindow) / ((meanGlobal + sigmaWindow)*(2*sigmaAdaptive + sigmaWindow))); } double inline constexpr ConfusionThreshold(const double meanGlobal, const double sigmaGlobal, const Pixel8 maxGray) { // Note: In the original paper, the Mg^2 * Sg was Mg^2 - Sg. The authors later corrected it. return meanGlobal - ((meanGlobal*meanGlobal * sigmaGlobal) / ((meanGlobal + sigmaGlobal)*(0.5*maxGray + sigmaGlobal))); } /// /// Returns the largest gray value in the image. /// Pixel8 GetMaxGrayValue() const { Pixel8 maxGrayValue = 0; for (int position = 0; position < Algorithm::grayScaleImageIn.size; ++position) { const Pixel8 tmpMax = Algorithm::grayScaleImageIn.data[position]; if (tmpMax > maxGrayValue) maxGrayValue = tmpMax; } return maxGrayValue; } /// /// Gets the local std dev and mean, as well as calculates the global sigma min and max /// void SigmaMinMaxAndMean(double& sigmaMin, double& sigmaMax, std::vector& windows) const { sigmaMax = 0; sigmaMin = std::numeric_limits::max(); for (auto &detailedWindow : windows) { CalculateMeanStdDev(detailedWindow.mean, detailedWindow.stddev, detailedWindow.window); if (detailedWindow.stddev > sigmaMax) sigmaMax = detailedWindow.stddev; if (detailedWindow.stddev < sigmaMin) sigmaMin = detailedWindow.stddev; } } /// /// Returns the total Red and Black count for the image and builds a Red Black White image. /// void RedBlack(int& redCountImage, int& blackCountImage, Image& redBlackImage, const double confusionThreshold, const double sigmaGlobal) const { const double halfSigmaGlobal = sigmaGlobal / 2; redCountImage = 0; blackCountImage = 0; for (int position = 0; position < Algorithm::grayScaleImageIn.size; ++position) { const Pixel8 val = Algorithm::grayScaleImageIn.data[position]; if (val <= confusionThreshold - halfSigmaGlobal) { ++blackCountImage; redBlackImage.data[position] = PixelColor::BLACK; } else if (val >= confusionThreshold + halfSigmaGlobal) { redBlackImage.data[position] = PixelColor::WHITE; } else { ++redCountImage; redBlackImage.data[position] = PixelColor::RED; } } } /// /// Breaks the image into Primary and Secondary windows. /// This is different from most other algorithms where the window is based around the pixel. /// /// Primary and Secondary Windows. NRVO should not make a copy of the vector. std::vector GetWindows( const Image& image, const int blackCountImage, const int redCountImage, const double sigmaGlobal, const Pixel8 maxGrayValue) { // Calculate the Primary Window size int windowWidth; int windowHeight; PrimaryWindow(windowWidth, windowHeight, (double)blackCountImage / redCountImage, sigmaGlobal, maxGrayValue, image.width, image.height ); // Build list of Primary Windows std::vector windows = GetPrimaryWindows(image, windowWidth, windowHeight); // Break some Primary Windows into smaller Secondary Windows UpdateWindowsWithSecondarySize(windows, image); return windows; } /// /// Break the image into a set of fixed windows. /// We take some liberty on the edges, so that we are not left with a window that is too small. /// std::vector GetPrimaryWindows(const Image& image, const int windowWidth, const int windowHeight) const { std::vector windows; int offsetY = 0; int offsetX = 0; for (int y = 0; y < image.height; y = offsetY + 1) { // Do not overshoot Y offsetY = std::min(image.height - 1, y + (windowHeight - 1)); // Do not undershoot Y if ((image.height - 1) - offsetY < windowHeight / 2) offsetY = image.height - 1; for (int x = 0; x < image.width; x = offsetX + 1) { // Do not overshoot X offsetX = std::min(image.width - 1, x + (windowWidth - 1)); // Do not undershoot X if ((image.width - 1) - offsetX < windowWidth / 2) offsetX = image.width - 1; windows.push_back(DetailedWindow{ Region(x, y, offsetX, offsetY) }); } } return windows; } /// /// Breaks up certain Primary Windows into smaller window sizes /// void UpdateWindowsWithSecondarySize(std::vector& windows, const Image& image) const { // Iterate each primary window. If it needs to be broken up, remove it and add the secondary windows. std::vector secondaryWindows; windows.erase(std::remove_if(windows.begin(), windows.end(), [&](const DetailedWindow& detailedWindow) { int redCountWindow = 0; int blackCountWindow = 0; // Count Red and Black pixels in the Window LocalWindow::Iterate(image.width, detailedWindow.window, [&](const int& positionWindow) { const Pixel8 val = image.data[positionWindow]; if (val == PixelColor::BLACK) ++blackCountWindow; else if (val == PixelColor::RED) ++redCountWindow; }); // If there are more Red pixels, create a second window by quartering the primary window if (redCountWindow > blackCountWindow) { const int halfX = detailedWindow.window.Width() / 2; const int halfY = detailedWindow.window.Height() / 2; const Region& win = detailedWindow.window; secondaryWindows.push_back(DetailedWindow{ Region(win.upperLeft.x, win.upperLeft.y, win.upperLeft.x + halfX - 1, win.upperLeft.y + halfY - 1) }); secondaryWindows.push_back(DetailedWindow{ Region(win.upperLeft.x + halfX, win.upperLeft.y, win.bottomRight.x, win.upperLeft.y + halfY - 1) }); secondaryWindows.push_back(DetailedWindow{ Region(win.upperLeft.x, win.upperLeft.y + halfY, win.upperLeft.x + halfX - 1, win.bottomRight.y) }); secondaryWindows.push_back(DetailedWindow{ Region(win.upperLeft.x + halfX, win.upperLeft.y + halfY, win.bottomRight.x, win.bottomRight.y) }); return true; } return false; }), windows.end()); // Merge Secondary Windows into our main Window vector windows.insert(std::end(windows), std::begin(secondaryWindows), std::end(secondaryWindows)); } int imageWidth = 0; IntegralImage integralImage, integralSqrImage; }; } #endif //BATAINEH_HPP ================================================ FILE: Doxa/Bernsen.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef BERNSEN_HPP #define BERNSEN_HPP #include "Algorithm.hpp" #include "LocalWindow.hpp" #include "Morphology.hpp" namespace Doxa { /// /// The Bernsen Algorithm: John Bernsen /// /// "Dynamic thresholding of gray-level images", 1986. class Bernsen : public Algorithm { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { Pixel8 min, max; // Read parameters, utilizing defaults const int windowSize = parameters.Get("window", 75); const int GT = parameters.Get("threshold", 100); const int L = parameters.Get("contrast-limit", 25); // Build Min and Max images Image minImage(Algorithm::grayScaleImageIn.width, Algorithm::grayScaleImageIn.height); Image maxImage(Algorithm::grayScaleImageIn.width, Algorithm::grayScaleImageIn.height); Morphology::Erode(minImage, Algorithm::grayScaleImageIn, windowSize); Morphology::Dilate(maxImage, Algorithm::grayScaleImageIn, windowSize); LocalWindow::Process(binaryImageOut, Algorithm::grayScaleImageIn, windowSize, [&](const Region& window, const int& position) { min = minImage.data[position]; max = maxImage.data[position]; return (max - min) > L ? (max + min) / 2 : GT; }); } }; } #endif //BERNSEN_HPP ================================================ FILE: Doxa/BinarizationFactory.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2025, "Freely you have received; freely give." - Matt 10:8 #ifndef BINARIZATIONFACTORY_HPP #define BINARIZATIONFACTORY_HPP // Note: Only include this header if you are prepared to pull in 95%+ of the entirety of this library. #include "Otsu.hpp" #include "Bernsen.hpp" #include "Niblack.hpp" #include "Sauvola.hpp" #include "ISauvola.hpp" #include "Nick.hpp" #include "TRSingh.hpp" #include "Bataineh.hpp" #include "Wan.hpp" #include "Wolf.hpp" #include "Su.hpp" #include "Gatos.hpp" #include "AdOtsu.hpp" #include "Phansalkar.hpp" namespace Doxa { enum Algorithms { OTSU = 0, BERNSEN = 1, NIBLACK = 2, SAUVOLA = 3, WOLF = 4, NICK = 5, SU = 6, TRSINGH = 7, BATAINEH = 8, ISAUVOLA = 9, WAN = 10, GATOS = 11, ADOTSU = 12, PHANSALKAR = 13, }; /// /// A factory class for creating instances of binarization algorithms. /// class BinarizationFactory { public: static IAlgorithm* Algorithm(const Algorithms algorithm) { IAlgorithm* algorithmPtr = nullptr; switch (algorithm) { case OTSU: algorithmPtr = new Otsu(); break; case BERNSEN: algorithmPtr = new Bernsen(); break; case NIBLACK: algorithmPtr = new Niblack(); break; case SAUVOLA: algorithmPtr = new Sauvola(); break; case NICK: algorithmPtr = new Nick(); break; case WOLF: algorithmPtr = new Wolf(); break; case SU: algorithmPtr = new Su(); break; case TRSINGH: algorithmPtr = new TRSingh(); break; case BATAINEH: algorithmPtr = new Bataineh(); break; case ISAUVOLA: algorithmPtr = new ISauvola(); break; case WAN: algorithmPtr = new Wan(); break; case GATOS: algorithmPtr = new Gatos(); break; case ADOTSU: algorithmPtr = new AdOtsuMS(); break; case PHANSALKAR: algorithmPtr = new Phansalkar(); break; } return algorithmPtr; } }; } #endif // #BINARIZATIONFACTORY_HPP ================================================ FILE: Doxa/ChanMeanCalc.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2020, "Freely you have received; freely give." - Matt 10:8 #ifndef CHANMEANCALC_HPP #define CHANMEANCALC_HPP #include "Image.hpp" namespace Doxa { /// /// A version of the Chan algorithm for calculating just the Mean. /// /// class ChanMeanCalc { public: template void Process(Image& binaryImageOut, const Image& grayScaleImageIn, const int windowSize, Algorithm algorithm) { Iterate(grayScaleImageIn, windowSize, [&](const double& mean, const int position) { binaryImageOut.data[position] = grayScaleImageIn.data[position] <= algorithm(mean, position) ? Palette::Black : Palette::White; }); } template void Iterate(const Image& grayScaleImageIn, const int windowSize, Processor processor) { // Setup constants const int width = grayScaleImageIn.width; const int height = grayScaleImageIn.height; const int leftWindow = (windowSize + 1) / 2; const int rightWindow = windowSize - leftWindow; const int dr1 = rightWindow; const int dr2 = width - rightWindow + 1; // Initialize structure uint16_t* integral = new uint16_t[width + 1]; memset(integral, 0, (width + 1) * sizeof(uint16_t)); // Starting at the top of the image, sum the columns to half our window height. // Note that Left and Right Window names are synonymous with Top and Bottom since this is a square window. // Our structures have now been primed. for (int y = 0, ind = 0; y < rightWindow; ++y) { for (int x = 1; x <= width; x++, ind++) { integral[x] += grayScaleImageIn.data[ind]; } } for (int y = 0, ind = 0; y < height; ++y) { const int winTop = std::max(y - leftWindow, -1); const int winBottom = std::min(height - 1, y + rightWindow); // As our windows slides down, these two blocks will remove the top row from our structure and add on the bottom row. if (y >= leftWindow) { for (int x = 1, index = winTop * width; x <= width; ++x, ++index) { integral[x] -= grayScaleImageIn.data[index]; } } if (y + rightWindow < height) { for (int x = 1, index = winBottom * width; x <= width; ++x, ++index) { integral[x] += grayScaleImageIn.data[index]; } } // At this point we slide our window from left to right. // We calculate the sums for the first half of our window. int sum = 0; for (int x = 1; x <= dr1; x++) { sum += integral[x]; } // As our window moves across, we are now able to use our sums to calculate mean, variance, etc. // This happens until the right most edge of our windows hits the end of the image. for (int x = 1; x < dr2; ++x, ++ind) { const int winLeft = std::max(x - leftWindow, 0); const int winRight = x + rightWindow; const int area = (winBottom - winTop)*(winRight - winLeft); sum += integral[winRight] - integral[winLeft]; const double mean = ((double)sum) / area; processor(mean, ind); } // Now that our windows is sliding through the right side of the image, we have to remove the left most column. // As we do that, we are able to continue with our calculation. for (int x = dr2; x <= width; ++x, ++ind) { const int winLeft = std::max(x - leftWindow, 0); const int winRight = width; const int area = (winBottom - winTop)*(winRight - winLeft); sum -= integral[winLeft]; const double mean = ((double)sum) / area; processor(mean, ind); } } // Free up our dynamically allocated structures delete[] integral; } }; } #endif //CHANMEANCALC_HPP ================================================ FILE: Doxa/ChanMeanVarianceCalc.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2020, "Freely you have received; freely give." - Matt 10:8 #ifndef CHANMEANVARIANCECALC_HPP #define CHANMEANVARIANCECALC_HPP #include "Image.hpp" namespace Doxa { /// /// The Chan Algorithm: Chungkwong Chan /// This algorithm is an advancement over Integral Images for extremely fast Mean / Variance calculation. /// It also uses only a fraction of the memory while allowing for much larger images. /// /// "Memory-efficient and fast implementation of local adaptive binarization methods", 2019. class ChanMeanVarianceCalc { public: template void Process(Image& binaryImageOut, const Image& grayScaleImageIn, const int windowSize, Algorithm algorithm) { Iterate(grayScaleImageIn, windowSize, [&](const double& mean, const double& variance, const int position) { binaryImageOut.data[position] = grayScaleImageIn.data[position] <= algorithm(mean, variance, position) ? Palette::Black : Palette::White; }); } template void Iterate(const Image& grayScaleImageIn, const int windowSize, Processor processor) { // Setup constants const int width = grayScaleImageIn.width; const int height = grayScaleImageIn.height; const int leftWindow = (windowSize + 1) / 2; const int rightWindow = windowSize - leftWindow; const int dr1 = rightWindow; const int dr2 = width - rightWindow + 1; // Initialize structures uint16_t* integral = new uint16_t[width + 1]; int32_t* integralSquare = new int32_t[width + 1]; memset(integral, 0, (width + 1) * sizeof(uint16_t)); memset(integralSquare, 0, (width + 1) * sizeof(int32_t)); // Starting at the top of the image, sum the columns to half our window height. // Note that Left and Right Window names are synonymous with Top and Bottom since this is a square window. // Our structures have now been primed. for (int y = 0, ind = 0; y < rightWindow; ++y) { for (int x = 1; x <= width; x++, ind++) { const int pixel = grayScaleImageIn.data[ind]; integral[x] += pixel; integralSquare[x] += pixel * pixel; } } for (int y = 0, ind = 0; y < height; ++y) { const int winTop = std::max(y - leftWindow, -1); const int winBottom = std::min(height - 1, y + rightWindow); // As our windows slides down, these two blocks will remove the top row from our structure and add on the bottom row. if (y >= leftWindow) { for (int x = 1, index = winTop * width; x <= width; ++x, ++index) { const int pixel = grayScaleImageIn.data[index]; integral[x] -= pixel; integralSquare[x] -= pixel * pixel; } } if (y + rightWindow < height) { for (int x = 1, index = winBottom * width; x <= width; ++x, ++index) { const int pixel = grayScaleImageIn.data[index]; integral[x] += pixel; integralSquare[x] += pixel * pixel; } } // At this point we slide our window from left to right. // We calculate the sums for the first half of our window. int sum = 0; int squareSum = 0; for (int x = 1; x <= dr1; x++) { sum += integral[x]; squareSum += integralSquare[x]; } // As our window moves across, we are now able to use our sums to calculate mean, variance, etc. // This happens until the right most edge of our windows hits the end of the image. for (int x = 1; x < dr2; ++x, ++ind) { const int winLeft = std::max(x - leftWindow, 0); const int winRight = x + rightWindow; const int count = (winBottom - winTop)*(winRight - winLeft); sum += integral[winRight] - integral[winLeft]; squareSum += integralSquare[winRight] - integralSquare[winLeft]; const double mean = ((double)sum) / count; const double variance = ((double)squareSum) / count - mean * mean; processor(mean, variance, ind); } // Now that our windows is sliding through the right side of the image, we have to remove the left most column. // As we do that, we are able to continue with our calculation. for (int x = dr2; x <= width; ++x, ++ind) { const int winLeft = std::max(x - leftWindow, 0); const int winRight = width; const int count = (winBottom - winTop)*(winRight - winLeft); sum -= integral[winLeft]; squareSum -= integralSquare[winLeft]; const double mean = ((double)sum) / count; const double variance = ((double)squareSum) / count - mean * mean; processor(mean, variance, ind); } } // Free up our dynamically allocated structures delete[] integral; delete[] integralSquare; } }; } #endif //CHANMEANVARIANCECALC_HPP ================================================ FILE: Doxa/ClassifiedPerformance.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef CLASSIFIEDPERFORMANCE_HPP #define CLASSIFIEDPERFORMANCE_HPP #include #include "Image.hpp" #include "Palette.hpp" #include "SIMDOps.hpp" namespace Doxa { /// /// A performance calculator for the family of classification metrics. /// Implementations: Accuracy, (pseudo)Recall, (pseudo)Precision, (pseudo)F-Measure, PSNR, MCC and NMR /// class ClassifiedPerformance { public: struct Classifications { int truePositive = 0; // A correctly chosen black pixel int trueNegative = 0; // A correctly chosen white pixel int falsePositive = 0; // An incorrectly chosen black pixel int falseNegative = 0; // An incorrectly chosen white pixel double wpTruePositive = 0.0; // Weighted Precision double wpFalsePositive = 0.0; // Weighted Precision double wrTruePositive = 0.0; // Weighted Recall double wrFalseNegative = 0.0; // Weighted Recall int Total() const noexcept { return truePositive + trueNegative + falsePositive + falseNegative; } void Clear() noexcept { truePositive = trueNegative = falsePositive = falseNegative = 0; wpTruePositive = wpFalsePositive = wrTruePositive = wrFalseNegative = 0.0; } }; static bool CompareImages( Classifications& classifications, const Image& controlImage, const Image& experimentImage) { // Initialize classifications.Clear(); // Verify Input if (controlImage.width != experimentImage.width || controlImage.height != experimentImage.height) return false; #if defined(DOXA_SIMD) CompareImages_SIMD(classifications, controlImage.data, experimentImage.data, controlImage.size); #else CompareImages_STD(classifications, controlImage.data, experimentImage.data, controlImage.size); #endif return true; } /// /// Scalar implementation of image comparison - always available /// static void CompareImages_STD(Classifications& classifications, const Pixel8* control, const Pixel8* experiment, int size) { for (int i = 0; i < size; ++i) { if (control[i] == experiment[i]) if (experiment[i] == Palette::Black) classifications.truePositive++; else classifications.trueNegative++; else // Not a match if (experiment[i] == Palette::Black) classifications.falsePositive++; else classifications.falseNegative++; } } #if defined(DOXA_SIMD) /// /// SIMD implementation of image comparison - only available when SIMD is enabled /// static void CompareImages_SIMD(Classifications& classifications, const Pixel8* control, const Pixel8* experiment, int size) { using namespace SIMD; int idx = 0; const int simd_end = size - (size % SIMD_WIDTH); vec128 black_vec = VEC_SPLAT_U8(Palette::Black); vec128 ones_vec = VEC_SPLAT_U8(1); // Accumulators for SIMD counts int tp_sum = 0, tn_sum = 0, fp_sum = 0, fn_sum = 0; for (; idx < simd_end; idx += SIMD_WIDTH) { vec128 ctrl = VEC_LOAD(control + idx); vec128 exp = VEC_LOAD(experiment + idx); // match_mask: 0xFF where control == experiment, 0x00 otherwise vec128 match_mask = VEC_CMPEQ_U8(ctrl, exp); // black_mask: 0xFF where experiment == black, 0x00 otherwise vec128 black_mask = VEC_CMPEQ_U8(exp, black_vec); // TP: match AND black vec128 tp_mask = VEC_AND(match_mask, black_mask); // TN: match AND NOT black vec128 tn_mask = VEC_ANDNOT(black_mask, match_mask); // FP: NOT match AND black vec128 fp_mask = VEC_ANDNOT(match_mask, black_mask); // FN: NOT match AND NOT black vec128 not_match = VEC_NOT(match_mask); vec128 not_black = VEC_NOT(black_mask); vec128 fn_mask = VEC_AND(not_match, not_black); // Count set bytes (mask has 0xFF for set, we need count of 1s) // AND with 1s to convert 0xFF to 0x01, then horizontal sum tp_sum += vec_hsum_u8(VEC_AND(tp_mask, ones_vec)); tn_sum += vec_hsum_u8(VEC_AND(tn_mask, ones_vec)); fp_sum += vec_hsum_u8(VEC_AND(fp_mask, ones_vec)); fn_sum += vec_hsum_u8(VEC_AND(fn_mask, ones_vec)); } classifications.truePositive = tp_sum; classifications.trueNegative = tn_sum; classifications.falsePositive = fp_sum; classifications.falseNegative = fn_sum; // Handle remaining pixels with scalar for (; idx < size; ++idx) { if (control[idx] == experiment[idx]) if (experiment[idx] == Palette::Black) classifications.truePositive++; else classifications.trueNegative++; else if (experiment[idx] == Palette::Black) classifications.falsePositive++; else classifications.falseNegative++; } } #endif // DOXA_SIMD static bool CompareImages( ClassifiedPerformance::Classifications& classifications, const Image& controlImage, const Image& experimentImage, const std::vector& weightsPrecision, const std::vector& weightsRecall) { // Initialize classifications.Clear(); // Verify Input if (controlImage.width != experimentImage.width || controlImage.height != experimentImage.height) return false; // Ensure that weights are properly passed if (!weightsPrecision.size() || !weightsRecall.size()) return CompareImages(classifications, controlImage, experimentImage); // Analyze using Pseudo Weights for (int i = 0; i < controlImage.size; ++i) { if (controlImage.data[i] == experimentImage.data[i]) { if (experimentImage.data[i] == Palette::Black) { classifications.truePositive++; classifications.wpTruePositive += weightsPrecision[i]; classifications.wrTruePositive += weightsRecall[i]; } else { classifications.trueNegative++; } } else // Not a match { if (experimentImage.data[i] == Palette::Black) { classifications.falsePositive++; classifications.wpFalsePositive += weightsPrecision[i]; } else { classifications.falseNegative++; classifications.wrFalseNegative += weightsRecall[i]; } } } return true; } static double CalculateAccuracy(const Classifications& classifications) { return (((double)classifications.truePositive + classifications.trueNegative) / classifications.Total()) * 100; } static double CalculateRecall(const Classifications& classifications) { // Prevent divide by zero. Range is 0.0 to 1.0 if (classifications.truePositive == 0) return 0.0; const double recall = (double)classifications.truePositive / (classifications.truePositive + classifications.falseNegative); return recall * 100; } static double CalculatePrecision(const Classifications& classifications) { // Prevent divide by zero. Range is 0.0 to 1.0 if (classifications.truePositive == 0) return 0.0; const double precision = (double)classifications.truePositive / (classifications.truePositive + classifications.falsePositive); return precision * 100; } static double CalculateFMeasure(const Classifications& classifications) { // Prevent divide by zero. Range is 0.0 to 1.0 if (classifications.truePositive == 0) return 0.0; const double recall = (double)classifications.truePositive / (classifications.truePositive + classifications.falseNegative); const double precision = (double)classifications.truePositive / (classifications.truePositive + classifications.falsePositive); return ((2 * recall * precision) / (recall + precision)) * 100; } static double CalculatePseudoRecall(const Classifications& classifications) { // Prevent divide by zero. Range is 0.0 to 1.0 if (classifications.wrTruePositive == 0) return 0.0; const double pseudoRecall = classifications.wrTruePositive / (classifications.wrTruePositive + classifications.wrFalseNegative); return pseudoRecall * 100; } static double CalculatePseudoPrecision(const Classifications& classifications) { const double pseudoTruePositive = classifications.wpTruePositive + classifications.truePositive; const double pseudoFalsePositive = classifications.wpFalsePositive + classifications.falsePositive; // Prevent divide by zero. Range is 0.0 to 1.0 if (pseudoTruePositive == 0) return 0.0; const double pseudoPrecision = pseudoTruePositive / (pseudoTruePositive + pseudoFalsePositive); return pseudoPrecision * 100; } /// /// Pseudo F-Measure /// /// "Performance Evaluation Methodology for Historical Document Image Binarization", 2013. static double CalculatePseudoFMeasure(const Classifications& classifications) { const double pseudoTruePositivePrecision = classifications.wpTruePositive + classifications.truePositive; const double pseudoFalsePositivePrecision = classifications.wpFalsePositive + classifications.falsePositive; const double pseudoTruePositiveRecall = classifications.wrTruePositive; const double pseudoFalseNegativeRecall = classifications.wrFalseNegative; // Prevent divide by zero. Range is 0.0 to 1.0 if (pseudoTruePositivePrecision == 0 || pseudoTruePositiveRecall == 0) return 0.0; const double pseudoPrecision = pseudoTruePositivePrecision / (pseudoTruePositivePrecision + pseudoFalsePositivePrecision); const double pseudoRecall = pseudoTruePositiveRecall / (pseudoTruePositiveRecall + pseudoFalseNegativeRecall); const double pseudoFMeasure = ((2 * pseudoRecall * pseudoPrecision) / (pseudoRecall + pseudoPrecision)) * 100; return pseudoFMeasure; } static double CalculatePSNR(const Classifications& classifications) { // Calculate MSE const double mse = ((double)classifications.falsePositive + classifications.falseNegative) / classifications.Total(); // Perfect match. Prevent divide by zero error. if (mse == 0) return std::numeric_limits::max(); // Calculate Peak Signal to Noise Ratio return 10 * log10(1 / mse); } /// /// Matthews Correlation Coefficient /// This should be more reliable than F-Measure for binary classification. See article below. /// /// https://en.wikipedia.org/wiki/Matthews_correlation_coefficient /// A number between -1 and 1, where 0 is totally random guessing. static double CalculateMCC(const Classifications& classifications) { const double n = (double)classifications.truePositive * classifications.trueNegative - (double)classifications.falsePositive * classifications.falseNegative; const double d = ((double)classifications.truePositive + classifications.falsePositive) * ((double)classifications.truePositive + classifications.falseNegative) * ((double)classifications.trueNegative + classifications.falsePositive) * ((double)classifications.trueNegative + classifications.falseNegative); // If undefined, return 0 to highlight the issue. return d == 0 ? 0 : n / std::sqrt(d); } static double CalculateNRM(const Classifications& classifications) { const int fntp = classifications.falseNegative + classifications.truePositive; const int fptn = classifications.falsePositive + classifications.trueNegative; // Prevent divide by zero error if (fntp == 0 || fptn == 0) return std::numeric_limits::max(); const double nrfn = (double)classifications.falseNegative / fntp; const double nrfp = (double)classifications.falsePositive / fptn; // Calculate Negative Rate Metric return (nrfn + nrfp) / 2; } // Convenience Method template static double Calculate(const Image& controlImage, const Image& experimentImage, CalcFunc calcFunc) { Classifications classifications; return CompareImages(classifications, controlImage, experimentImage) ? calcFunc(classifications) : 0.0; } }; } #endif //CLASSIFIEDPERFORMANCE_HPP ================================================ FILE: Doxa/ContrastImage.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef CONTRASTIMAGE_HPP #define CONTRASTIMAGE_HPP #include #include "Types.hpp" #include "Otsu.hpp" #include "Palette.hpp" #include "Region.hpp" #include "Morphology.hpp" namespace Doxa { /// /// Contrast Image generation class /// /// "Binarization of Historical Document Images Using the Local Maximum and Minimum", 2010. class ContrastImage { public: static inline void GenerateContrastImage(Image& contrastImage, const Image& grayScaleImage) { const int windowSize = 3; Pixel8 min, max; Image minImage(grayScaleImage.width, grayScaleImage.height); Image maxImage(grayScaleImage.width, grayScaleImage.height); Morphology::Erode(minImage, grayScaleImage, windowSize); Morphology::Dilate(maxImage, grayScaleImage, windowSize); LocalWindow::Iterate(grayScaleImage, windowSize, [&](const Region& window, const int& position) { min = minImage.data[position]; max = maxImage.data[position]; const double contrastMultiplier = (double)(max - min) / (0.0001 + max + min); // Note: The paper leaves out the fact that the Contrast Image actually has to be normalized. // To normalize it back into an 8bit gray scale image, simply multiply by 255. contrastImage.data[position] = 255 * contrastMultiplier; }); } /// /// Estimates the text stroke width from a contrast image by scanning each row /// for peak contrast pixels and building a histogram of distances between adjacent peaks. /// The most frequent distance corresponds to the stroke width. /// static inline int EstimateStrokeWidth(const Image& contrastImage) { std::vector histogram(contrastImage.width, 0); for (int y = 0; y < contrastImage.height; ++y) { const int row = y * contrastImage.width; int lastPeakX = -1; for (int x = 1; x < contrastImage.width - 1; ++x) { const Pixel8 val = contrastImage.data[row + x]; if (val > contrastImage.data[row + x - 1] && val > contrastImage.data[row + x + 1]) { if (lastPeakX >= 0) { ++histogram[x - lastPeakX]; } lastPeakX = x; } } } // The mode is the estimated stroke width (skip distance 1 as sub-pixel noise) int strokeWidth = 3; int maxCount = 0; for (int d = 2; d < contrastImage.width; ++d) { if (histogram[d] > maxCount) { maxCount = histogram[d]; strokeWidth = d; } } return strokeWidth; } static inline void GenerateHighContrastImage(Image& highContrastImage, const Image& grayScaleImage) { // Generate Contrast Image GenerateContrastImage(highContrastImage, grayScaleImage); // Run it through Otsu binarization to make it a high contrast image Otsu otsu; otsu.Initialize(highContrastImage); otsu.ToBinary(highContrastImage); } }; } #endif //CONTRASTIMAGE_HPP ================================================ FILE: Doxa/DIBCOUtils.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2025, "Freely you have received; freely give." - Matt 10:8 #ifndef DIBCOUTILS_HPP #define DIBCOUTILS_HPP #include #include #include namespace Doxa { class DIBCOUtils { public: /// /// Read DIBCO Weighted .dat files. /// /// "Performance Evaluation Methodology for Historical Document Image Binarization", 2013. static std::vector ReadWeightsFile(const std::string& fileLocation, size_t allocatedSize = 0) { std::ifstream file; file.open(fileLocation.c_str(), std::ios::binary); const auto weights = DIBCOUtils::ReadWeights(file, allocatedSize); file.clear(); file.close(); // Automatically closes, but done explicitly for posterity. return weights; } /// /// Read DIBCO Weighted content from either a file or string stream. /// static std::vector ReadWeights(std::istream& stream, size_t allocatedSize = 0) { std::vector values; values.reserve(allocatedSize); double value; while (stream >> value) { values.push_back(value); } return values; } }; } #endif //DIBCOUTILS_HPP ================================================ FILE: Doxa/DRDM.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2026, "Freely you have received; freely give." - Matt 10:8 #ifndef DRDM_HPP #define DRDM_HPP #include #include "Image.hpp" #include "Palette.hpp" #include "Region.hpp" #include "LocalWindow.hpp" #include "SIMDOps.hpp" namespace Doxa { /// /// The Distance-Reciprocal Distortion Measure (DRDM) Algorithm: Haiping Lu, Jian Wang, A.C. Kot, Y.Q. Shi /// /// "An Objective Distortion Measure for Binary Document Images Based on Human Visual Perception", 2002. class DRDM { public: static double CalculateDRDM(const Image& controlImage, const Image& experimentImage) { const uint64_t sumDRDk = SumDRDkForMismatchedPixels(controlImage, experimentImage); // To avoid rounding issues we are using ints instead of doubles, which we accomplished by using a 1000000 multiplier. return sumDRDk / (double)(NUBN(controlImage) * 1000000); } protected: static constexpr int N = 5; static constexpr int R = N / 2; // Normalized Weighted Matrix // Values have been multiplied by 1000000 in order to avoid slight rounding errors with doubles. // These values are more granular than the example matrix given in the research paper. // If you use those values, you will hit rounding problems with their sample data because they are actually using more // precise Normalized Matrix values when calculating DRD than what is provided in that example. static constexpr uint32_t Wm[N * N] = { 25582, 32359, 36179, 32359, 25582, 32359, 51164, 72357, 51164, 32359, 36179, 72357, 0, 72357, 36179, 32359, 51164, 72357, 51164, 32359, 25582, 32359, 36179, 32359, 25582 }; /// /// Sum DRDk for all mismatched pixels between control and experiment images. /// This is an optimized algorithm, /// static uint64_t SumDRDkForMismatchedPixels(const Image& control, const Image& experiment) { uint64_t sum = 0; const int w = control.width; const int h = control.height; const Pixel8* ctl = control.data; const Pixel8* exp = experiment.data; for (int y = 0; y < h; ++y) { const int row = y * w; // Clamp region - Y const int y0 = (y - R < 0) ? 0 : y - R; const int y1 = (y + R >= h) ? h - 1 : y + R; for (int x = 0; x < w; ++x) { /* TODO: Validate that this is truly better on a wide range of images // Compare 16 pixels at once for a ~20% speedup // This is only beneficial if your images are very similar if (x + 15 < w) // Read X + 15 More = 16 { //if (VEC_ALL_EQ_U8(VEC_LOAD(ctl + row + x), VEC_LOAD(exp + row + x))) if (memcmp(ctl + row + x, exp + row + x, 16) == 0) // No SIMD necessary { x += 15; continue; } } */ const Pixel8 g = exp[row + x]; if (ctl[row + x] == g) continue; uint32_t localSum = 0; // Clamp region - X const int x0 = (x - R < 0) ? 0 : x - R; const int x1 = (x + R >= w) ? w - 1 : x + R; // Walk neighborhood for (int ny = y0; ny <= y1; ++ny) { const int base = ny * w; const int wy = ny - (y - R); const int wyRow = wy * 5; for (int nx = x0; nx <= x1; ++nx) { const int wx = nx - (x - R); // Compute weight index const uint32_t weight = Wm[wyRow + wx]; // Branchless add: localSum += weight * (ctl[base + nx] != g); } } sum += localSum; } } return sum; } /// /// Calculate the number of non-uniform MxM windows (both white and black pixels). /// /// This uses an over-engineer set of algorithms that attempt to make this otherwise /// simple calculation as fast as possible. Because M = 8 is a standard, this path /// has been optimized. /// /// NOTE: DIBCO Metrics do not process partial windows /// NOTE: DRDM defaults to 8x8 windows, which DIBCO Metrics uses /// /// static unsigned int NUBN(const Image& controlImage, const int M = 8) { // Use optimized path for standard 8x8 blocks if (M == 8) { #if defined(DOXA_SIMD) return NUBN_SIMD_8x8(controlImage); #else return NUBN_STD_8x8(controlImage); #endif } return NUBN_STD(controlImage, M); } /// /// Calculate the number of non-uniform NxN windows (both white and black pixels). /// NOTE: DIBCO Metrics do not process partial windows /// /// Algorithm /// This is a memory optimized algorithm that iterates through an image pixel by pixel /// not window by window. It has many early exit optimizations. /// /// To accomplish this it stores a special row for keeping track of the state of each /// window as we iterate across each window. /// /// static int NUBN_STD(const Image& controlImage, int N) { const int numWindowCols = controlImage.width / N; const int numWindowRows = controlImage.height / N; if (numWindowCols == 0 || numWindowRows == 0) { return 0; } constexpr int MIXED = -1; // Sentinel: valid sums are always >= 0 const int whiteRowSum = 255 * N; std::vector expected(numWindowCols); int totalMixed = 0; const uint8_t* bandPtr = controlImage.data; const int bandStride = N * controlImage.width; for (int wy = 0; wy < numWindowRows; ++wy) { int mixedCount = 0; const uint8_t* px = bandPtr; // === Row 0: Classify all windows === for (int wx = 0; wx < numWindowCols; ++wx) { int sum = 0; for (int i = 0; i < N; ++i) { sum += px[i]; } px += N; if (sum == 0 || sum == whiteRowSum) { expected[wx] = sum; } else { expected[wx] = MIXED; ++mixedCount; } } // === Rows 1 to N-1: Verify uniformity === const Pixel8* rowPtr = bandPtr + controlImage.width; for (int localY = 1; localY < N; ++localY) { if (mixedCount == numWindowCols) break; px = rowPtr; for (int wx = 0; wx < numWindowCols; ++wx) { const int exp = expected[wx]; if (exp == MIXED) { px += N; continue; } int sum = 0; for (int i = 0; i < N; ++i) { sum += px[i]; } px += N; if (sum != exp) { expected[wx] = MIXED; ++mixedCount; } } rowPtr += controlImage.width; } totalMixed += mixedCount; bandPtr += bandStride; } return totalMixed; } /// /// Calculate the number of non-uniform 8x8 windows /// NOTE: DIBCO Metrics do not process partial windows /// /// Algorithm /// This algorithm is identical to NUBN_STD, except we are able to further optimize it /// based on the known 8x8 window size, standard for DRDM. /// /// These memory tricks get this implentation about as close to SIMD speed as possible. /// /// static int NUBN_STD_8x8(const Image& controlImage) { constexpr uint64_t ALL_BLACK = 0x0000000000000000ULL; constexpr uint64_t ALL_WHITE = 0xFFFFFFFFFFFFFFFFULL; constexpr uint64_t MIXED = 1ULL; // Sentinel: impossible for uniform row constexpr unsigned int N = 8; const int numWindowCols = controlImage.width / N; // NOTE: Compiler will optimize to >> 3 const int numWindowRows = controlImage.height / N; const int rowStride = controlImage.width; const int bandStride = rowStride * N; // NOTE: Compiler will optimize to << 3 if (numWindowCols == 0 || numWindowRows == 0) { return 0; } std::vector expected(numWindowCols); int totalMixed = 0; const Pixel8* bandPtr = controlImage.data; for (int wy = 0; wy < numWindowRows; ++wy) { int mixedCount = 0; const Pixel8* px = bandPtr; // Row 0: classify all windows (no branches for state check) for (int wx = 0; wx < numWindowCols; ++wx, px += N) { uint64_t row; std::memcpy(&row, px, N); if (row == ALL_BLACK || row == ALL_WHITE) { expected[wx] = row; } else { expected[wx] = MIXED; ++mixedCount; } } // Rows 1-7: verify uniformity (no UNCLASSIFIED check) const Pixel8* rowPtr = bandPtr + rowStride; for (int localY = 1; localY < N; ++localY) { if (mixedCount == numWindowCols) break; px = rowPtr; for (int wx = 0; wx < numWindowCols; ++wx, px += N) { const uint64_t exp = expected[wx]; if (exp == MIXED) continue; uint64_t row; std::memcpy(&row, px, N); if (row != exp) { expected[wx] = MIXED; ++mixedCount; } } rowPtr += rowStride; } totalMixed += mixedCount; bandPtr += bandStride; } return totalMixed; } #if defined(DOXA_SIMD) /// /// SIMD implementation of NUBN for 8x8 blocks /// static unsigned int NUBN_SIMD_8x8(const Image& controlImage) { unsigned int nubn = 0; const int M = 8; const int stride = controlImage.width; const int columns = stride / M; const int rows = controlImage.height / M; const Pixel8* rowPtr = controlImage.data; for (int row = 0; row < rows; ++row) { const Pixel8* blockPtr = rowPtr; for (int column = 0; column < columns; ++column) { if (!IsBlock8x8Uniform(blockPtr, stride)) { ++nubn; } blockPtr += M; } rowPtr += stride * M; } return nubn; } /// /// Check if an 8x8 block is uniform (all pixels match reference value) /// SIMD Required /// static inline bool IsBlock8x8Uniform(const uint8_t* ptr, int stride) { SIMD::vec128 ref = VEC_SPLAT_U8(*ptr); for (int row = 0; row < 8; row += 2) { SIMD::vec128 rows = VEC_LOAD_2x64(ptr, ptr + stride); if (!VEC_ALL_EQ_U8(rows, ref)) return false; ptr += stride * 2; } return true; } #endif // DOXA_SIMD }; } #endif //DRDM_HPP ================================================ FILE: Doxa/Doxa.vcxitems ================================================  $(MSBuildAllProjects);$(MSBuildThisFileFullPath) true {d81f2737-6340-49c1-ae65-8d217415c67e} %(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) ================================================ FILE: Doxa/Gatos.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef GATOS_HPP #define GATOS_HPP #include "Types.hpp" #include "Sauvola.hpp" #include "Palette.hpp" #include "Region.hpp" #include "Image.hpp" #include "WienerFilter.hpp" namespace Doxa { /// /// The Gatos binarization workflow: B. Gatos, I. Pratikakis, S.J. Perantonis /// This is a 5 step workflow consisting of: /// Wiener Filter /// Sauvola binarization algorithm /// A background estimation based thresholding algorithm /// Upsampling and other post-processing measures /// /// The optional Upsampling on the fourth step is not currently performed, nor the Post-processing for the fifth step. /// /// "Adaptive degraded document image binarization", 2005. class Gatos : public Algorithm { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Read parameters, utilizing defaults const int glyphSize = parameters.Get("glyph", 60); // Step 1 - Pre-processing: Run greyscale through Wiener Filter Image filteredImage(Algorithm::grayScaleImageIn); WienerFilter::Filter(filteredImage, Algorithm::grayScaleImageIn, 3); // Step 2 - Rough estimation of foreground regions: Apply Sauvola binarization Sauvola algorithm; // TODO - Allow this algorithm to be swapped with any in the library algorithm.Initialize(filteredImage); algorithm.ToBinary(binaryImageOut, parameters); // Step 3 - Background surface estimation Image backgroundImage(filteredImage); ExtractBackground(backgroundImage, filteredImage, binaryImageOut, (glyphSize * 2) + 1); // Step 4 - Final thresholding double d, b; GatosCalculations(d, b, backgroundImage, filteredImage, binaryImageOut); for (int index = 0; index < binaryImageOut.size; ++index) { const double threshold = Threshold(backgroundImage.data[index], d, b); binaryImageOut.data[index] = backgroundImage.data[index] - filteredImage.data[index] > threshold ? Palette::Black : Palette::White; } // Step 4.5 - Upsampling // This increases your image size. Not implementing. // Step 5 - Post-processing // Resulted in a significant loss of detail. Not providing until refined. } protected: /// /// Calculates Average Foreground / Background Distance, and Average Background Text Value. /// void GatosCalculations(double& averageFgBgDistance, double& averageBgTextValue, const Image& backgroundImage, const Image& filteredImage, const Image& binaryImage) const { int backgroundCounter = 0; int numeratorAverageFgBgDistance = 0; // Calculate Average Foreground / Background Distance int numeratorAverageBgTextValue = 0; // Calculate Average Background Text Value for (int index = 0; index < binaryImage.size; ++index) { numeratorAverageFgBgDistance += backgroundImage.data[index] - filteredImage.data[index]; if (Palette::White == binaryImage.data[index]) { numeratorAverageBgTextValue += backgroundImage.data[index]; ++backgroundCounter; } } averageFgBgDistance = (double)numeratorAverageFgBgDistance / (backgroundImage.size - backgroundCounter); averageBgTextValue = (double)numeratorAverageBgTextValue / backgroundCounter; } double Threshold(const int backgroundValue, const double d, const double b, const double q = 0.6, const double p1 = 0.5, const double p2 = 0.8) const { const double expVal = exp(((-4 * backgroundValue) / (b * (1 - p1))) + ((2 * (1 + p1)) / (1 - p1))); return q * d * (((1 - p2) / (1 + expVal)) + p2); } // Note: backgroundImage must be a copy of grayScaleImage. This avoids us having to set pixels for the background entirely void ExtractBackground(Image& backgroundImage, const Image& filteredImage, const Image& binaryImage, const int windowSize = 51) const { LocalWindow::Iterate(filteredImage, windowSize, [&](const Region& window, const int& position) { if (binaryImage.data[position] == Palette::Black) { unsigned int numerator = 0; unsigned int denominator = 0; // Build a window around our black pixel and traverse it LocalWindow::Iterate(filteredImage.width, window, [&](const int& windowPosition) { // This is usually mathematically described as: // Numerator += B(x, y) * (1 − S(x, y)) // Denominator += (1 − S(x, y)) // This assumes that your binary image's black value is 1, and white 0. // Blindly following this mathematical formula also impacts performance! if (binaryImage.data[windowPosition] == Palette::White) { numerator += filteredImage.data[windowPosition]; ++denominator; } }); if (denominator > 0) { backgroundImage.data[position] = numerator / denominator; } } }); } }; } #endif //GATOS_HPP ================================================ FILE: Doxa/Grayscale.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2022, "Freely you have received; freely give." - Matt 10:8 #ifndef GRAYSCALE_HPP #define GRAYSCALE_HPP #include #include #include #include #include "Types.hpp" namespace Doxa { using GrayscaleFunc = Pixel8(*)(Pixel8, Pixel8, Pixel8); using XYZ = std::tuple; using Lab = std::tuple; enum GrayscaleAlgorithms { MEAN = 0, QT = 1, BT601 = 2, BT709 = 3, BT2100 = 4, VALUE = 5, LUSTER = 6, LIGHTNESS = 7, MINAVG = 8, LABDIST = 9 }; /// /// This entire class was greatly influenced by the Color-to-Grayscale article written by Christopher Kanan and /// Garrison W. Cottrell. /// /// With the PNM input format, despite the Ref 709 gamma correction scheme specified, there are many variants. /// The most important aspect to know, however, is not the format of sRBG, 601, 709, etc, but if gamma correction /// has been applied in the first place. Reading in a PNM color image, one should assume it is gamma corrected. /// /// The top 2 grayscale formulas found by Kanan and Cottrell are Gleam and Intensity'. /// The great news is that these share the same formula which is very fast, referred to here as Mean(...). /// The bad news is that if you are given a Linear RGB set, your Intensity' is now just Intensity and drops you /// from 2nd place to 8th. Luckily, for color images, you should safely assume you are receiving gamma corrected /// RGB values. /// /// Because we should assume some form of gamma correction, we are also far more likely to get the result of Luma /// than Luminance (exact formula isn't as important), which should be reassuring as that would be a drop from 5th /// 10th. But to show the power of correctly controlling your gamma conversion, if one applied that same formula to /// to Linear RGB values and then gamma corrected after the fact, you end up with the 3rd place finisher, Luminance'. /// Basically the same formula can take you from 3rd to 10th place, simply due to gamma correction! /// /// It is therefore this naive author's opinion that as long as you start with gamma corrected RGB values, you /// should be set regardless of your exact gamma compression scheme, or grayscale conversion formula you want to try. /// It is also recommended that when you finally do convert to Grayscale, that you apply your image processing /// algorithms to an uncompressed, linear, grayscale image. /// /// Also see: /// https://en.wikipedia.org/wiki/Grayscale /// https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/ /// /// "Color-to-Grayscale: Does the Method Matter in Image Recognition?", 2012. class Grayscale { public: /// /// Linear Y value that will be sRGB gamma corrected /// static inline void LinearTosRgb(double& y) { y = y <= 0.0031308 ? y * 12.92 : Gamma((1.055 * y - 0.055) / 1.055, 2.4); } static inline void LinearTo709(double& y) { y = y <= 0.0018 ? y * 4.5 : Gamma((1.099 * y - 0.099) / 1.055, 2.2); } /// /// Return a fast linear conversion LUT for uncompressing a color space /// static std::array LinearLUT() { std::array lut; for (int i = 0; i < 256; i++) { const float v = i / 255.0f; lut[i] = (v <= 0.04045f) ? v / 12.92f : powf((v + 0.055f) / 1.055f, 2.4f); } return lut; } /// /// RGB are linear values that this function will gamma correct. /// static inline void Gamma(double& r, double& g, double& b, const double gamma = 2.2) { r = Gamma(r, gamma); g = Gamma(g, gamma); b = Gamma(b, gamma); } static inline double Gamma(const double channel, const double gamma = 2.2) { return std::pow(channel, 1 / gamma); } /// /// The formula used by the Qt framework which is used by our default. /// Note: There are serious unanswered questions around this formula. /// template static inline constexpr T Qt(T r, T g, T b) { return (r * 11 + g * 16 + b * 5) / 32; } /// /// If RGB are Linear, this calculates the Intensity. /// If RGB are Gamma Corrected, this calculates Gleam. /// template static inline constexpr T Mean(T r, T g, T b) { return (r + g + b) / 3; } /// /// BT601 calculates the Luma when RGB are Gamma Corrected. The Y' from Y'UV & Y'IQ. /// When Linear, can generate the luminance Y from the CIE 1931 XYZ color space. /// Aka: NTSC RGB formula, with a White Point of "C". /// template static inline constexpr T BT601(T r, T g, T b) { return 0.2989 * r + 0.5865 * g + 0.1144 * b; } /// /// BT709 calculates the Luma when RGB are Gamma Corrected. The Y' from Y'UV & Y'IQ. /// When Linear, can generate the luminance Y from the CIE 1931 XYZ color space. /// Aka sRGB formula, with a White Point of "D65". /// The D50 weights, for ICC, are: 0.2225045 0.7168786 0.0606169 /// template static inline constexpr T BT709(T r, T g, T b) { return 0.2126 * r + 0.7152 * g + 0.0722 * b; } /// /// BT2100 calculates the Luma when RGB are Gamma Corrected. The Y' from Y'UV & Y'IQ. /// When Linear, can generate the luminance Y from the CIE 1931 XYZ color space. /// Has a White Point of "D65". /// template static inline constexpr T BT2100(T r, T g, T b) { return 0.2627 * r + 0.6780 * g + 0.0593 * b; } /// /// HSV V Value. Calculates Value when RGB are Linear. /// Gamma Corrected RGB values produce the same result as a Gamma Corrected Value. /// template static inline constexpr T Value(T r, T g, T b) { return std::max({ r, g, b }); } /// /// HLS L Value. Calculates Lightness when RGB are Linear. /// /// Named Luster as to not be confused by CIE Lightness template static inline constexpr T Luster(T r, T g, T b) { return (std::max({ r, g, b }) + std::min({ r, g, b })) / 2; } /// /// The purpose of MinAvg is to produce a grayscale image whose values are less sensitive to multi color text. /// It was introduced for the first AdOtsu algorithm. /// /// "A multi-scale framework for adaptive binarization of degraded document images", 2010 template static inline constexpr T MinAvg(T r, T g, T b) { return (Mean(r, g, b) + std::min({ r, g, b })) / 2; } /// /// CIELAB & CIELUV L Value. Calculates Lightness when RGB are Linear. /// Notice that this has built in gamma correction. /// Note: BT709 has a White Point of D65. The ICC calls for D50. /// This is also known as a chromatic adaptation transformation (CAT). /// See: https://drafts.csswg.org/css-color/#rgb-to-lab /// static inline constexpr float Lightness(float r, float g, float b) { const auto addGamma = [&](double y) { return y > 0.00885 ? Gamma(y, 3) : 7.78703 * y + 0.13793; }; return 116 * addGamma(BT709(r, g, b)) - 16; } /// /// Linear sRGB to XYZ color space with a D65 white point /// static constexpr inline XYZ RGBToXYZ(float r, float g, float b) { return { 0.4124564f * r + 0.3575761f * g + 0.1804375f * b, 0.2126729f * r + 0.7151522f * g + 0.0721750f * b, 0.0193339f * r + 0.1191920f * g + 0.9503041f * b }; } /// /// XYZ to L* a* b* color space with D65 whitepoint tristimulus values /// static inline Lab XYZToLab(float X, float Y, float Z) { const auto f = [](float t) { return (t > 0.008856f) ? cbrtf(t) : (7.787037f * t + 0.137931f); }; // D65 Tristimulus Values - D50: [0.96422, 1, 0.82521] const auto fx = f(X / 0.95047f); const auto fy = f(Y / 1.0f); const auto fz = f(Z / 1.08883f); return { 116.0f * fy - 16.0f, 500.0f * (fx - fy), 200.0f * (fy - fz) }; } /// /// L* a* b* Euclidean Distance /// Takes into account chromatic spearation, not just Lightness. /// All input parameters are linear and must be uncompressed. /// Source: Used by Phansalkar /// static inline float LABDist(float red, float green, float blue) { const auto [X, Y, Z] = RGBToXYZ(red, green, blue); const auto [L, a, b] = XYZToLab(X, Y, Z); return sqrtf(L * L + a * a + b * b); } /// /// Convert an RGB/RGBA buffer to 8-bit grayscale. /// static void ToGrayscale( Pixel8* output, const uint8_t* input, int width, int height, int channels, GrayscaleAlgorithms algorithm = GrayscaleAlgorithms::MEAN) { const int size = width * height; switch (algorithm) { case GrayscaleAlgorithms::QT: GrayscaleConverter>(output, input, size, channels); break; case GrayscaleAlgorithms::BT601: GrayscaleConverter>(output, input, size, channels); break; case GrayscaleAlgorithms::BT709: GrayscaleConverter>(output, input, size, channels); break; case GrayscaleAlgorithms::BT2100: GrayscaleConverter>(output, input, size, channels); break; case GrayscaleAlgorithms::VALUE: GrayscaleConverter>(output, input, size, channels); break; case GrayscaleAlgorithms::LUSTER: GrayscaleConverter>(output, input, size, channels); break; case GrayscaleAlgorithms::MINAVG: GrayscaleConverter>(output, input, size, channels); break; // Linear Algorithms - Assumes we are working in a compressed sRGB colorspace case GrayscaleAlgorithms::LIGHTNESS: GrayscaleConverterLightness(output, input, size, channels); break; case GrayscaleAlgorithms::LABDIST: GrayscaleConverterLABDist(output, input, size, channels); break; // Default - The Mean algorithm is safe, efficient, and effective default: GrayscaleConverter>(output, input, size, channels); break; } } private: static inline std::optional> m_lut; /// /// An optimized grayscale conversion loop where all algs are inlined /// template static void GrayscaleConverter( Pixel8* output, const uint8_t* input, int size, int channels) { for (int i = 0, offset = 0; i < size; ++i, offset += channels) { output[i] = Algorithm(input[offset], input[offset + 1], input[offset + 2]); } } /// /// Lightness has a known range of 0-100, allowing a single-pass conversion. /// static void GrayscaleConverterLightness( Pixel8* output, const uint8_t* input, int size, int channels) { if (!m_lut) m_lut = LinearLUT(); const float scale = 255.0f / 100.0f; for (int i = 0, offset = 0; i < size; ++i, offset += channels) { output[i] = static_cast( Lightness((*m_lut)[input[offset]], (*m_lut)[input[offset + 1]], (*m_lut)[input[offset + 2]]) * scale); } } /// /// LABDist has an unknown range requiring two passes: compute values, then normalize. /// static void GrayscaleConverterLABDist( Pixel8* output, const uint8_t* input, int size, int channels) { if (!m_lut) m_lut = LinearLUT(); std::vector values(size); for (int i = 0, offset = 0; i < size; ++i, offset += channels) { values[i] = LABDist((*m_lut)[input[offset]], (*m_lut)[input[offset + 1]], (*m_lut)[input[offset + 2]]); } auto [mn, mx] = std::minmax_element(values.data(), values.data() + size); const float min = *mn; const float scale = 255.0f / std::max(*mx - *mn, 1.0f); for (int i = 0; i < size; ++i) { output[i] = static_cast((values[i] - min) * scale); } } }; } #endif // GRAYSCALE_HPP ================================================ FILE: Doxa/GridCalc.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2025, "Freely you have received; freely give." - Matt 10:8 #ifndef GRIDCALC_HPP #define GRIDCALC_HPP #include "Image.hpp" namespace Doxa { /// /// A Grid based calculation algorithm designed originally for Sauvola and the original AdOtsu. /// It calculates a limited number of thresholds in a grid pattern throughout the image and then uses those /// values to interpolate the thresholds of the remaining pixels. /// /// By setting the Distance parameter to "windowSize / 2", you get overlapping windows as called for by /// the AdOtsu paper. If you set the Distance to windowSize, you get no overlap, like what is written about /// by Feng. /// /// Note: While statistic based algorithms have the luxury of leveraging optimizations like Chan or Integral /// Images, function based algorithms like AdOtsu do not. This is intended for un-optimized algorithms only! /// This algorithm is not a free lunch. It is semi-expensive to run and it only *estimates* thresholds. /// /// "A multi-scale framework for adaptive binarization of degraded document images", 2010. class GridCalc { public: template void Process(Image& binaryImageOut, const Image& grayScaleImageIn, const int windowSize, const int dist, Algorithm algorithm) { // Calculate thresholds at specific intervals throughout the image Iterate(grayScaleImageIn, windowSize, dist, [&](const Region& window, const int& position) { binaryImageOut.data[position] = algorithm(window, position); }); // Estimate the remaining thresholds using Bilinear Interpolation Interpolate(binaryImageOut, dist, [&](const int& position, const int& threshold) { binaryImageOut.data[position] = threshold; }); } /// /// Iterates the image, calculating the corners of the window. /// Window Size and how far windows are apart from each other are somewhat independent. /// Distances between 2 and Window Size are possible. /// template static void Iterate(const Image& imageIn, const int windowSize, const int dist, Processor processor) { const int HALF_WINDOW = windowSize / 2; // Note: Compiler rounding - 3 / 2 = 1 const int imageWidthSub = imageIn.width - 1; const int imageHeightSub = imageIn.height - 1; const int xCount = (std::ceil((float)(imageWidthSub) / dist) + 1) * dist; const int yCount = (std::ceil((float)(imageHeightSub) / dist) + 1) * dist; Region window; for (int y = 0; y < yCount; y += dist) { const int yCoord = std::min(y, imageHeightSub); const int rowIdx = imageIn.width * yCoord; window.upperLeft.y = (std::max)(0, yCoord - HALF_WINDOW); window.bottomRight.y = (std::min)(imageHeightSub, yCoord + HALF_WINDOW); for (int x = 0; x < xCount; x += dist) { const int xCoord = std::min(x, imageWidthSub); const int idx = rowIdx + xCoord; window.upperLeft.x = (std::max)(0, xCoord - HALF_WINDOW); window.bottomRight.x = (std::min)(imageWidthSub, xCoord + HALF_WINDOW); processor(window, idx); } } } /// /// Interpolates an image that has already been Iterated. /// This algorithm attempts to iterate the image in a memory efficient way. /// It is very... verbose, but it is well optimized. /// template static void Interpolate(const Image& imageIn, const int dist, Processor processor) { const int regularColCount = (imageIn.width - 1) / dist; const int regularRowCount = (imageIn.height - 1) / dist; const int remainingX = (imageIn.width - regularColCount * dist) - 1; const int remainingY = (imageIn.height - regularRowCount * dist) - 1; // An optimized algorithm for rows that pre-calculated values in them auto processRowOptimized = [&imageIn](int& idx, const int colCount, const int dist, const int remainingDist, Processor processor) { // Seed our Left-most value Pixel8 q00Val = imageIn.data[idx]; // Iterate through all standard distance cells for (int colIdx = 0; colIdx <= colCount; ++colIdx) { // Make sure we stay within the bounds of the row const int interval = (colIdx != colCount) ? dist : remainingDist; // Set our Right-most value const Pixel8 q01Val = imageIn.data[idx + interval]; idx++; // Skip - Already processed // Process standard spaced nodes for (int x = 1; x < interval; ++x, ++idx) { // Pre-calculated for optimization const float xDivInt = (float)x / interval; const Pixel8 threshold = 0.5f + (1.0f - xDivInt) * q00Val + xDivInt * q01Val; processor(idx, threshold); } // What was once our Right-most value, is now our Left-most q00Val = q01Val; } if (remainingDist > 0) { idx++; // Skip - Processed } }; // Process all of the rows for a block of rows auto processRows = [&imageIn, &processRowOptimized](int& idx, const int rowGap, const int rowCount, const int colCount, const int dist, const int remainingDist, Processor processor) { const int yIdx00 = idx; const int yIdx10 = yIdx00 + rowGap; // Process first row processRowOptimized(idx, colCount, dist, remainingDist, processor); // Iterate through every row for (int rowIdx = 1; rowIdx < rowCount; ++rowIdx) { // Seed our Left-most values Pixel8 q00Val = imageIn.data[yIdx00]; Pixel8 q10Val = imageIn.data[yIdx10]; int yIdx01 = yIdx00; int yIdx11 = yIdx10; // Pre-calculated for optimization const float rowIdxDivRowCount = (float)rowIdx / rowCount; // Iterate through all standard distance cells for (int colIdx = 0; colIdx <= colCount; ++colIdx) { // Make sure we stay within the bounds of the row const int interval = (colIdx != colCount) ? dist : remainingDist; yIdx01 += interval; yIdx11 += interval; // Set our Right-most values const Pixel8 q01Val = imageIn.data[yIdx01]; const Pixel8 q11Val = imageIn.data[yIdx11]; // Column optimized equation const Pixel8 threshold = 0.5f + (1.0 - rowIdxDivRowCount) * q00Val + rowIdxDivRowCount * q10Val; processor(idx, threshold); idx++; // Process standard spaced nodes for (int x = 1; x < interval; ++x, ++idx) { // Pre-calculated for optimization const float xDivInt = (float)x / interval; const float top = (1.0f - xDivInt) * q00Val + xDivInt * q01Val; const float bottom = (1.0f - xDivInt) * q10Val + xDivInt * q11Val; const Pixel8 threshold = 0.5f + (1.0f - rowIdxDivRowCount) * top + rowIdxDivRowCount * bottom; processor(idx, threshold); } // What was once our Right-most values, is now our Left-most q00Val = q01Val; q10Val = q11Val; } // Column optimized equation if (remainingDist > 0) { const Pixel8 threshold = 0.5f + (1.0f - rowIdxDivRowCount) * q00Val + rowIdxDivRowCount * q10Val; processor(idx, threshold); idx++; } } }; // The Interpolation Algorithm int idx = 0; const int rowGap = dist * imageIn.width; for (int y = 0; y < regularRowCount; ++y) { processRows(idx, rowGap, dist, regularColCount, dist, remainingX, processor); } if (remainingY) { processRows(idx, remainingY * imageIn.width, remainingY, regularColCount, dist, remainingX, processor); } processRowOptimized(idx, regularColCount, dist, remainingX, processor); } }; } #endif //GRIDCALC_HPP ================================================ FILE: Doxa/ISauvola.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef ISAUVOLA_HPP #define ISAUVOLA_HPP #include #include "Sauvola.hpp" #include "ContrastImage.hpp" //#include "Morphology.hpp" namespace Doxa { /// /// The ISauvola Algorithm: Zineb Hadjadj, Abdelkrimo Meziane, Yazid Cherfa, Mohamed Cheriet, Insaf Setitra /// /// This algorithm has been abstracted so that any binarization algorithm can leverage this technique. /// Example: Improved<Sauvola>::ToBinaryImage(...); /// /// "ISauvola: Improved Sauvola’s Algorithm for Document Image Binarization", 2016. template class Improved : public Algorithm> { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Step 1 - Initialization Step Image highContrastImage(Improved::grayScaleImageIn.width, Improved::grayScaleImageIn.height); ContrastImage::GenerateHighContrastImage(highContrastImage, Improved::grayScaleImageIn); // Step 1 b - Removing Open because it removes too much detail //Morphology::Open(highContrastImage, highContrastImage, 3); // Step 2 - Binarization Step Image binImage = BinarizationClass::ToBinaryImage(Improved::grayScaleImageIn, parameters); // Step 3 - Sequential Combination Combine(binaryImageOut, highContrastImage, binImage); } protected: /// /// This algorithm leverages a high contrast image to find where the foreground might be. /// Once a foreground pixel is found, all connected pixels are associated with it as part of the /// final binary image. /// /// Final image /// A high contrast image of the grayscale image /// A high detail, but probably noisy, binary image void Combine(Image& binaryImageOut, const Image& highContrastImageIn, const Image& binaryImageIn) { // Ensure that everything is white to start off with binaryImageOut.Fill(Palette::White); // Iterate every pixel of our high contrast image for (int idx = 0; idx < highContrastImageIn.size; ++idx) { // Detect a high contrast region that contains a foreground pixel if (Palette::White == highContrastImageIn.data[idx] && Palette::Black == binaryImageIn.data[idx]) { // If we haven't already analyzed this pixel's connections, analyze them if (Palette::White == binaryImageOut.data[idx]) { Spider(binaryImageOut, binaryImageIn, idx); } } } } /// /// Spider is a fairly crafty algorithm that tracks down all connected foreground pixels given a /// starting point. It does this non-recursively by searching a 3x3 area around the target pixel. /// /// This takes advantage of the fact that many Niblack based binarization algorithms create a /// small background buffer around the foreground that is free from noise. /// /// Image containing the traced foreground. /// Image containing the foreground to trace. /// A foreground pixel location to start the trace. void Spider(Image& binaryImageOut, const Image& binaryImageIn, const int startIdx) { std::unordered_set pixelPositions = { startIdx }; // Initialize our Start Index binaryImageOut.data[startIdx] = Palette::Black; // If we find an attached cell that hasn't been analyzed, mark it and add it to our list auto processCell = [&](const int position) { if (Palette::Black == binaryImageIn.data[position] && Palette::White == binaryImageOut.data[position]) { binaryImageOut.data[position] = Palette::Black; pixelPositions.insert(position); } }; // Process a Row of our 3x3 window from Left to Right auto processRow = [&](int position, const bool checkLeft, const bool checkCenter, bool const checkRight) { // Start on the Left if (checkLeft) processCell(position); // Move to Center ++position; if (checkCenter) processCell(position); // Move to Right if (checkRight) processCell(++position); // Note: Position is incremented }; // Iterate through all of the black pixels, forming a 3x3 search window. // This loop may keep adding to that list when a black neighbor is found. while (!pixelPositions.empty()) { // Pop target position index const int idx = *pixelPositions.begin(); pixelPositions.erase(pixelPositions.begin()); // Build Coordinates and detect if we are on the far left or right const int top = idx - binaryImageIn.width; const int bottom = idx + binaryImageIn.width; const bool checkLeft = idx % binaryImageIn.width; const bool checkRight = (idx + 1) % binaryImageIn.width; // Process Top Row if (top > 0) processRow(top - 1, checkLeft, true, checkRight); // Process Center Row processRow(idx - 1, checkLeft, false, checkRight); // Process Bottom Row if (bottom < binaryImageIn.size) processRow(bottom - 1, checkLeft, true, checkRight); } } }; /// /// A convenience name for backwards compatibility with this library. /// typedef Improved ISauvola; } #endif //ISAUVOLA_HPP ================================================ FILE: Doxa/Image.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef IMAGE_HPP #define IMAGE_HPP #include #include "Types.hpp" namespace Doxa { /// /// The Image structure holds the 8-bit image data and meta data. /// This struct hides all memory management details and can be thought of as living on the stack. /// Due to C++ Move semantics and our Reference(...) method, deep copies can be avoided. /// struct Image { // Default CTOR Image() {} // CTOR Image(int width, int height, const Pixel8* bits = nullptr) : width(width), height(height), size(width*height) { data = new Pixel8[size]; if (bits != nullptr) std::memcpy(data, bits, size); } // DTOR // Note: https://en.wikipedia.org/wiki/Copy_elision ~Image() { if (!managedExternally) delete[] data; } // Copy Constructor Image(const Image& image) : width(image.width), height(image.height), depth(image.depth), maxVal(image.maxVal), tupleType(image.tupleType), size(image.size) { data = new Pixel8[size]; std::memcpy(data, image.data, size); } // Move Constructor Image(Image&& image) : width(image.width), height(image.height), depth(image.depth), maxVal(image.maxVal), tupleType(image.tupleType), size(image.size), data(image.data), managedExternally(image.managedExternally) { image.managedExternally = true; // Now managed by the copy } // Copy Assignment Operator - This will always deep copy, even a reference. Image& operator=(const Image& that) { if (this != &that) { if (size != that.size) { delete[] data; // Reset in case of a thrown exception allocating memory size = 0; data = nullptr; // Reallocate data = new Pixel8[that.size]; size = that.size; } width = that.width; height = that.height; managedExternally = false; std::memcpy(data, that.data, size); } return *this; } /// /// Generates a reference image of the current image object /// Image Reference() const { return Reference(width, height, data); } /// /// Generates an image reference, typically from a 3rd party image buffer. /// Any change to the reference image will affect the original image buffer. /// The buffer must be an 8-bit grayscale or binary image. /// The buffer will not be freed when the reference Image destructs. /// static Image Reference(int width, int height, Pixel8* data) { Image referenceImage; referenceImage.width = width; referenceImage.height = height; referenceImage.size = width * height; referenceImage.data = data; referenceImage.managedExternally = true; return referenceImage; } void Fill(const Pixel8 pixel) { memset(data, pixel, size * sizeof(Pixel8)); } // External Memory Management bool managedExternally = false; // PNM Values int width = 0; int height = 0; int size = 0; int depth = 1; int maxVal = 255; std::string tupleType = TupleTypes::GRAYSCALE; // The raw image buffer containing all image pixels Pixel8* data = nullptr; inline Pixel8& Pixel(int x, int y) { return data[(y * width) + x]; } inline Pixel8 Pixel(int x, int y) const { return data[(y * width) + x]; } inline Pixel8 Pixel(int x, int y, Pixel8 defaultValue) const { return (x < 0 || x >= width || y < 0 || y >= height) ? defaultValue : Pixel(x, y); } }; } #endif // IMAGE_HPP ================================================ FILE: Doxa/IntegralImageMeanVarianceCalc.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef INTEGRALIMAGEMEANVARIANCECALC_HPP #define INTEGRALIMAGEMEANVARIANCECALC_HPP #include #include "Image.hpp" #include "Region.hpp" #include "LocalWindow.hpp" namespace Doxa { typedef std::vector IntegralImage; /// /// The Shafait Algorithm: Faisal Shafait, Daniel Keysers, Thomas M. Breuel /// An integral image based calculator for calculating Mean, Population Variance. /// /// Note: For extremely large images, this algorithm will fail. /// This algorithm has been replaced by Chan. Chan is roughly 3x faster, consumes a fraction of the memory, /// and allows for large image processing. /// /// "Efficient Implementation of Local Adaptive Thresholding Techniques Using Integral Images", 2008. class IntegralImageMeanVarianceCalc { public: template void Process(Image& binaryImageOut, const Image& grayScaleImageIn, const int windowSize, Algorithm algorithm) { const int imageWidth = grayScaleImageIn.width; // Initialize Integral Images IntegralImage integralImage(grayScaleImageIn.size), integralSqrImage(grayScaleImageIn.size); BuildIntegralImages(integralImage, integralSqrImage, grayScaleImageIn); //BuildIntegralImagesLowMem(integralImage, integralSqrImage, grayScaleImageIn); // This can significantly reduce the memory used, but may be slower // Run our binarization algorithm double mean, variance; LocalWindow::Process(binaryImageOut, grayScaleImageIn, windowSize, [&](const Region& window, const int& index) { CalculateMeanVariance(mean, variance, imageWidth, integralImage, integralSqrImage, window); return algorithm(mean, variance, index); }); } template void Iterate(const Image& grayScaleImageIn, const int windowSize, Processor processor) { const int imageWidth = grayScaleImageIn.width; // Initialize Integral Images IntegralImage integralImage(grayScaleImageIn.size), integralSqrImage(grayScaleImageIn.size); BuildIntegralImages(integralImage, integralSqrImage, grayScaleImageIn); //BuildIntegralImagesLowMem(integralImage, integralSqrImage, grayScaleImageIn); // This can significantly reduce the memory used, but may be slower // Run our binarization algorithm double mean, variance; LocalWindow::Iterate(grayScaleImageIn, windowSize, [&](const Region& window, const int& index) { CalculateMeanVariance(mean, variance, imageWidth, integralImage, integralSqrImage, window); return processor(mean, variance, index); }); } inline void CalculateMeanVariance(double& mean, double& variance, const int imageWidth, const IntegralImage& integralImage, const IntegralImage& integralSqrImage, const Region& window) const { // Note: This data type has a large impact on performance. double diff, sqdiff; CalculateDiffs(diff, sqdiff, imageWidth, integralImage, integralSqrImage, window); // Get the Mean and Variance using our Shafait inspired algorithm const int area = window.Area(); mean = diff / area; // Sample Variance //variance = (sqdiff - diff*diff / area) / (area - 1); // Population Variance variance = (sqdiff / area) - (mean * mean); } inline void CalculateDiffs(double& diff, double& sqdiff, const int imageWidth, const IntegralImage& integralImage, const IntegralImage& integralSqrImage, const Region& window) const { // Optimization Note: With MSVC, simplifying this to match MeanCalculator::CalculateDiffs incurred a small performance hit. const int bottomRight = (window.bottomRight.y * imageWidth) + window.bottomRight.x; if (window.upperLeft.x) { const int bottomLeft = (window.bottomRight.y * imageWidth) + (window.upperLeft.x - 1); if (window.upperLeft.y) { const int upperRight = ((window.upperLeft.y - 1) * imageWidth) + window.bottomRight.x; const int upperLeft = ((window.upperLeft.y - 1) * imageWidth) + (window.upperLeft.x - 1); diff = (integralImage[bottomRight] + integralImage[upperLeft]) - (integralImage[upperRight] + integralImage[bottomLeft]); sqdiff = (integralSqrImage[bottomRight] + integralSqrImage[upperLeft]) - (integralSqrImage[upperRight] + integralSqrImage[bottomLeft]); } else { diff = integralImage[bottomRight] - integralImage[bottomLeft]; sqdiff = integralSqrImage[bottomRight] - integralSqrImage[bottomLeft]; } } else { if (window.upperLeft.y) { const int upperRight = ((window.upperLeft.y - 1) * imageWidth) + window.bottomRight.x; diff = integralImage[bottomRight] - integralImage[upperRight]; sqdiff = integralSqrImage[bottomRight] - integralSqrImage[upperRight]; } else { diff = integralImage[bottomRight]; sqdiff = integralSqrImage[bottomRight]; } } } void BuildIntegralImages(IntegralImage& integralImage, IntegralImage& integralSqrImage, const Image& grayScaleImage) { // This algorithm uses two temporary images. See BuildIntegralImagesLowMem for an alternative. IntegralImage rowSumImage(grayScaleImage.size); IntegralImage rowSumSqrImage(grayScaleImage.size); int rowIdx = 0; for (int y = 0; y < grayScaleImage.height; ++y) { rowIdx = (y * grayScaleImage.width); rowSumImage[rowIdx] = grayScaleImage.data[rowIdx]; rowSumSqrImage[rowIdx] = grayScaleImage.data[rowIdx] * grayScaleImage.data[rowIdx]; } for (int y = 0; y < grayScaleImage.height; ++y) { for (int x = 1; x < grayScaleImage.width; ++x) { rowIdx = (y * grayScaleImage.width) + x; rowSumImage[rowIdx] = rowSumImage[rowIdx - 1] + grayScaleImage.data[rowIdx]; rowSumSqrImage[rowIdx] = rowSumSqrImage[rowIdx - 1] + grayScaleImage.data[rowIdx] * grayScaleImage.data[rowIdx]; } } for (int x = 0; x < grayScaleImage.width; ++x) { integralImage[x] = rowSumImage[x]; integralSqrImage[x] = rowSumSqrImage[x]; } for (int y = 1; y < grayScaleImage.height; ++y) { for (int x = 0; x < grayScaleImage.width; ++x) { rowIdx = (y * grayScaleImage.width) + x; integralImage[rowIdx] = integralImage[rowIdx - grayScaleImage.width] + rowSumImage[rowIdx]; integralSqrImage[rowIdx] = integralSqrImage[rowIdx - grayScaleImage.width] + rowSumSqrImage[rowIdx]; } } } /// /// This is a "low memory" version of BuildIntegralImages(...). /// It achieves this by removing the need for two temporary integral images. /// Warning: The speed performance compared to the reference may be slower depending on image size. /// void BuildIntegralImagesLowMem(IntegralImage& integralImage, IntegralImage& integralSqrImage, const Image& grayScaleImage) { int row; int cell; integralImage[0] = grayScaleImage.data[0]; integralSqrImage[0] = integralImage[0] * integralImage[0]; for (int y = 1; y < grayScaleImage.height; ++y) { row = (y * grayScaleImage.width); integralImage[row] = integralImage[row - grayScaleImage.width] + grayScaleImage.data[row]; integralSqrImage[row] = grayScaleImage.data[row] * grayScaleImage.data[row] + integralSqrImage[row - grayScaleImage.width]; } for (int x = 1; x < grayScaleImage.width; ++x) { integralImage[x] = integralImage[x - 1] + grayScaleImage.data[x]; integralSqrImage[x] = grayScaleImage.data[x] * grayScaleImage.data[x] + integralSqrImage[x - 1]; } for (int y = 1; y < grayScaleImage.height; ++y) { row = (y * grayScaleImage.width); int rowSum = grayScaleImage.data[row]; for (int x = 1; x < grayScaleImage.width; ++x) { cell = row + x; rowSum += grayScaleImage.data[cell]; integralImage[cell] = integralImage[cell - grayScaleImage.width] + rowSum; } } for (int x = 1; x < grayScaleImage.width; ++x) { int colSqrSum = grayScaleImage.data[x] * grayScaleImage.data[x]; for (int y = 1; y < grayScaleImage.height; ++y) { row = (y * grayScaleImage.width) + x; colSqrSum += grayScaleImage.data[row] * grayScaleImage.data[row]; integralSqrImage[row] = colSqrSum + integralSqrImage[row - 1]; } } } }; } #endif //INTEGRALIMAGEMEANVARIANCECALC_HPP ================================================ FILE: Doxa/LocalWindow.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef LOCALWINDOW_HPP #define LOCALWINDOW_HPP #include #include "Image.hpp" #include "Palette.hpp" #include "Region.hpp" namespace Doxa { class LocalWindow { public: /// /// Converts a Gray Scale image into a Binary image given a specific algorithm. /// The algorithm should return a threshold under which a gray pixel will become black and /// over which it will become white. /// template static void Process(Image& binaryImageOut, const Image& grayScaleImageIn, const int windowSize, WindowCalculator calculator) { Iterate(grayScaleImageIn, windowSize, [&](const Region& window, const int& position) { binaryImageOut.data[position] = grayScaleImageIn.data[position] <= calculator(window, position) ? Palette::Black : Palette::White; }); } /// /// Iterate an Image, providing the current position and a window. /// A Window is a region of the image surrounding a specific pixel that needs to be processed. /// template static void Iterate(const Image& imageIn, const int windowSize, Processor processor) { const int HALF_WINDOW = windowSize / 2; Region window; for (int y = 0, ind = 0; y < imageIn.height; ++y) { // Set Y Window Coordinates window.upperLeft.y = (std::max)(0, y - HALF_WINDOW); window.bottomRight.y = (std::min)(imageIn.height - 1, y + HALF_WINDOW); for (int x = 0; x < imageIn.width; ++x, ++ind) { // Set X Window Coordinates window.upperLeft.x = (std::max)(0, x - HALF_WINDOW); window.bottomRight.x = (std::min)(imageIn.width - 1, x + HALF_WINDOW); processor(window, ind); } } } /// /// Iterate a Window, providing the current position within the window. /// template static void Iterate(const int width, const Region& window, Processor processor) { for (int y = window.upperLeft.y; y <= window.bottomRight.y; ++y) { const int row = y * width; for (int x = window.upperLeft.x; x <= window.bottomRight.x; ++x) { processor(row + x); } } } }; } #endif //LOCALWINDOW_HPP ================================================ FILE: Doxa/Morphology.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef MORPHOLOGY_HPP #define MORPHOLOGY_HPP #include #include "Image.hpp" #include "LocalWindow.hpp" namespace Doxa { class Morphology { public: /// /// Open is simply Erode followed by Dilate. /// This can help reduce background noise. /// static void Open(Image& morphedImage, const Image& grayScaleImage, const int windowSize = 3) { Image erodedImage(grayScaleImage.width, grayScaleImage.height); Erode(erodedImage, grayScaleImage, windowSize); Dilate(morphedImage, erodedImage, windowSize); } /// /// Close is simply Dilate followed by Erode. /// This can help fill holes in the foreground. /// static void Close(Image& morphedImage, const Image& grayScaleImage, const int windowSize = 3) { Image dilatedImage(grayScaleImage.width, grayScaleImage.height); Dilate(dilatedImage, grayScaleImage, windowSize); Erode(morphedImage, dilatedImage, windowSize); } /// /// Iterates through the gray scale image, recording the minimum value within the window. /// static void Erode(Image& morphedImage, const Image& grayScaleImage, const int windowSize = 3) { // For small window sizes, manually analyzing the window is faster. if (windowSize < 17) { IterativelyErode(morphedImage, grayScaleImage, windowSize); } else { Morph(morphedImage, grayScaleImage, windowSize, [](const std::multiset& set) { return *set.begin(); // Min Value }); } } /// /// Iterates through the grayscale image, recording the maximum value within the window. /// static void Dilate(Image& morphedImage, const Image& grayScaleImage, const int windowSize = 3) { // For small window sizes, manually analyzing the window is faster. if (windowSize < 17) { IterativelyDilate(morphedImage, grayScaleImage, windowSize); } else { Morph(morphedImage, grayScaleImage, windowSize, [](const std::multiset& set) { return *std::prev(set.end()); // Max Value }); } } protected: /// /// Author: Brandon M. Petty /// Date: Jan 19, 2019 /// /// This routine allows one to Erode or Dilate a grayscale image efficiently for medium to large window sizes. /// Many binarization algorithms require the min/max value within a local window. Iterating through each window /// can be extremely slow with larger window sizes. The goal of this algorithm is to greatly improve the /// performance characteristics for varying window sizes. /// /// This algorithm was born out of necessity because I could not, for the life of me, understand vHGW. /// The vHGW algorithm, as referenced in many papers, can apparently do this very efficiently. /// But like most papers I've read, something supposedly so simple is completely undermined by an overly complex /// description with absolutely no example of how it works with real data. /// /// Algorithm: /// 1. For each row, iterate pixel by pixel to the end of the row, creating a 1-D horizontal window around each pixel. /// 2. For each window, take the min value (for Erode), or max value (for Dilate), and store it in a temp image /// at the same coordinates as the targeted pixel. /// 3. Using the temp image, repeat steps 1 and 2 except now you will be going down column by column using a 1-D /// vertical window to capture min / max values for your final morphed image. /// /// The algorithm's performance hinges on its ability to find the min/max of a given window. By using an ordered /// set, when sliding the window over by one pixel, we simply need to add the next pixel to the set and remove /// the single pixel no longer in the window from the set. Because the set is ordered, the first item in the /// set is the min value, and the last item is the max value. In the end, it comes down to how fast you can add /// and remove items from an ordered set. /// /// Anecdotal Evidence: /// Base on an AMD Phenom II 1090T Processor running Windows 10, compiled with MSBuild using /O2 /Ot /// The follow are: Window Size, Speed of Wan's algorithm using Morph, Speed of Wan simply searching each window. /// 3 | 0.091808 | 0.028856 * Using the Morph implementation is slower for small windows /// 17 | 0.127909 | 0.134349 * This is the window size which breaks in favor of Morph /// 25 | 0.130928 | 0.238442 * 1.8x faster /// 75 | 0.143578 | 1.652775 * 11.5x faster /// 125 | 0.146470 | 4.218453 * 28.8x faster /// 255 | 0.149844 | 15.32876 * 102.3x faster /// /// If this is a known algorithm, please provide a source and I will attribute it here. template static inline void Morph(Image& morphedImage, const Image& grayScaleImage, const int& windowSize, MorphRoutine routine) { const int windowHalfWidth = ((windowSize - 1) / 2); const int windowHalfWidthPlusOne = windowHalfWidth + 1; // Store the minimum values, by row Image tempImage(grayScaleImage.width, grayScaleImage.height); // For each row (y) in grayScaleImage, calculate the min/max value within the 1-D window for that row for (int y = 0; y < grayScaleImage.height; ++y) { // Instead of paying the cost of calling Pixel(x,y), roll our own to save a few cpu cycles const int row = y * grayScaleImage.width; // Set is used because it automatically orders everything we add into it std::multiset set; // Initialize set with first half of our 1-D window // This sets pixel 0,0 with the max of the half window. for (int w = 0; w <= windowHalfWidth; ++w) { set.insert(grayScaleImage.data[row + w]); } tempImage.data[row] = routine(set); // Fill it up until we hit the full width of the 1-D window // This allows us to set the next few pixels as we iterate to that point int x = 1; for (; x <= windowHalfWidth; ++x) { set.insert(grayScaleImage.data[row + x + windowHalfWidth]); tempImage.data[row + x] = routine(set); } // Now that we have a full window set, simply add the next item and remove the last time. // This will take use almost to the end of the row, finding the min/max value for that point within a 1-D window for (; x < grayScaleImage.width - windowHalfWidth; ++x) { // Optimization: Calculate position since we are calling this 3 times const int position = row + x; //set.erase(grayScaleImage.data[position - windowHalfWidthPlusOne]); const std::multiset::iterator hit(set.find(grayScaleImage.data[position - windowHalfWidthPlusOne])); if (hit != set.end()) set.erase(hit); set.insert(grayScaleImage.data[position + windowHalfWidth]); tempImage.data[position] = routine(set); } // Because there is nothing left to add, simply remove items from out set until we reach the end // This will wrap up the calculation for the row for (; x < grayScaleImage.width; ++x) { //set.erase(grayScaleImage.data[row + x - windowHalfWidthPlusOne]); const std::multiset::iterator hit(set.find(grayScaleImage.data[row + x - windowHalfWidthPlusOne])); if (hit != set.end()) set.erase(hit); tempImage.data[row + x] = routine(set); } } // For each column (x) in erodedImage, calculate the min/max value within the 1-D window for that column // After this routine, erodedImage will contain an array of all min/max values for (int x = 0; x < tempImage.width; ++x) { std::multiset set; // Note: We are calling Pixel(x,y) here because we are traversing columns // Initialize set with first half of our 1-D window // This sets pixel 0,0 with the max of the half window. for (int w = 0; w <= windowHalfWidth; ++w) { set.insert(tempImage.Pixel(x, w)); } morphedImage.Pixel(x, 0) = routine(set); // Fill it up until we hit the full height of the 1-D window // This allows us to set the next few pixels as we iterate to that point int y = 1; for (; y <= windowHalfWidth; ++y) { set.insert(tempImage.Pixel(x, windowHalfWidth + y)); morphedImage.Pixel(x, y) = routine(set); } // Now that we have a full window set, simply add the next item and remove the last time. // This will take use almost to the end of the column, finding the min/max value for that point within a 1-D window for (; y < tempImage.height - windowHalfWidth; ++y) { //set.erase(tempImage.Pixel(x, y - windowHalfWidthPlusOne)); const std::multiset::iterator hit(set.find(tempImage.Pixel(x, y - windowHalfWidthPlusOne))); if (hit != set.end()) set.erase(hit); set.insert(tempImage.Pixel(x, y + windowHalfWidth)); morphedImage.Pixel(x, y) = routine(set); } // Because there is nothing left to add, simply remove items from out set until we reach the end // This will wrap up the calculation for the column for (; y < tempImage.height; ++y) { //set.erase(tempImage.Pixel(x, y - windowHalfWidthPlusOne)); const std::multiset::iterator hit(set.find(tempImage.Pixel(x, y - windowHalfWidthPlusOne))); if (hit != set.end()) set.erase(hit); morphedImage.Pixel(x, y) = routine(set); } } } static inline void IterativelyErode(Image& morphedImage, const Image& grayScaleImage, const int& windowSize) { // Iterate entire image creating a window around each pixel LocalWindow::Iterate(grayScaleImage, windowSize, [&](const Region& window, const int& target) { int min = 255; // Iterate each window, finding the max value LocalWindow::Iterate(grayScaleImage.width, window, [&](const int& position) { const int value = grayScaleImage.data[position]; if (value < min) { min = value; } }); morphedImage.data[target] = min; }); } static inline void IterativelyDilate(Image& morphedImage, const Image& grayScaleImage, const int& windowSize) { // Iterate entire image creating a window around each pixel LocalWindow::Iterate(grayScaleImage, windowSize, [&](const Region& window, const int& target) { int max = 0; // Iterate each window, finding the max value LocalWindow::Iterate(grayScaleImage.width, window, [&](const int& position) { const int value = grayScaleImage.data[position]; if (value > max) { max = value; } }); morphedImage.data[target] = max; }); } }; } #endif //MORPHOLOGY_HPP ================================================ FILE: Doxa/MultiScale.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2025, "Freely you have received; freely give." - Matt 10:8 #ifndef MULTISCALE_HPP #define MULTISCALE_HPP #include #include "Algorithm.hpp" #include "Morphology.hpp" namespace Doxa { /// /// Multi-Scale Algorithm: Reza Farrahi Moghaddam, Mohamed Cheriet /// /// This algorithm takes in an initial scale, aka the window size, and stitches together a final binarized output /// based on the output for binarized images taken at different scales. These images are eroded and any connecting /// pixels are assigned to the final output image. /// /// This algorithm, in the paper, is paired with a Grid image processor. Depending on the number of scales, a /// grid-based approach may be necessary in order to reduce the overall runtime of the underlying algorithm. /// /// Note: /// This should not be confused with "Efficient Multiscale Sauvola’s Binarization" by Guillaume Lazzara and Thierry Géraud. /// /// /// "A multi-scale framework for adaptive binarization of degraded document images", 2010. template class MultiScale : public Algorithm> { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { const int Slow = 9; const int Shigh = parameters.Get("window", 75); double k = parameters.Get("k", 1.0); // Input and Output cannot be the same assert(MultiScale::grayScaleImageIn.data != binaryImageOut.data); Image binarizedMap(MultiScale::grayScaleImageIn.width, MultiScale::grayScaleImageIn.height); Image binarizedMask(MultiScale::grayScaleImageIn.width, MultiScale::grayScaleImageIn.height); // Seed our binary output BinarizationClass algorithm; algorithm.Initialize(MultiScale::grayScaleImageIn); algorithm.ToBinary(binaryImageOut, parameters); // Create our first binarized mask Morphology::Erode(binarizedMask, binaryImageOut, Shigh / 4); // Copy parameters and only change 'windows' and 'k', keeping all others Parameters msParams(parameters); // Iterate over multiple window scales for (int S = Shigh / 2; S >= Slow; S = S / 2, k = k / 2) { // Get scaled map msParams.Set("window", S); msParams.Set("k", k); algorithm.ToBinary(binarizedMap, msParams); // Apply mask for (int idx = 0; idx < binarizedMask.size; ++idx) { if (binarizedMask.data[idx] == Palette::Black && binarizedMap.data[idx] == Palette::Black) { binaryImageOut.data[idx] = Palette::Black; } } // Avoid calculating the next Mask if we aren't going to use it if (S / 2 < Slow) break; // Obtain the next Mask Morphology::Erode(binarizedMask, binarizedMap, S / 4); } } }; } #endif //MUTISCALEGRID_HPP ================================================ FILE: Doxa/Niblack.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef NIBLACK_HPP #define NIBLACK_HPP #include "Algorithm.hpp" #include "LocalWindow.hpp" #include "ChanMeanVarianceCalc.hpp" namespace Doxa { /// /// The Niblack Algorithm: Wayne Niblack /// /// "An Introduction to Digital Image Processing", 1986. class Niblack : public Algorithm, public ChanMeanVarianceCalc { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Read parameters, utilizing defaults const int windowSize = parameters.Get("window", 75); const double k = parameters.Get("k", 0.2); Process(binaryImageOut, Algorithm::grayScaleImageIn, windowSize, [&](const double& mean, const double& variance, const int&) { const double stddev = std::sqrt(variance); return (mean + (k * stddev)); }); } }; } #endif //NIBLACK_HPP ================================================ FILE: Doxa/Nick.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef NICK_HPP #define NICK_HPP #include "Algorithm.hpp" #include "LocalWindow.hpp" #include "ChanMeanVarianceCalc.hpp" namespace Doxa { /// /// The NICK Algorithm: Khurram Khurshid, Imran Siddiqi, Claudie Faure, Nicole Vincent /// /// "Comparison of Niblack inspired Binarization methods for ancient documents", 2009. class Nick : public Algorithm, public ChanMeanVarianceCalc { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Read parameters, utilizing defaults const int windowSize = parameters.Get("window", 75); const double k = parameters.Get("k", -0.2); Process(binaryImageOut, Algorithm::grayScaleImageIn, windowSize, [&](const double& mean, const double& variance, const int&) { return mean + (k * std::sqrt(variance + (mean * mean))); }); } }; } #endif //NICK_HPP ================================================ FILE: Doxa/Otsu.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2022, "Freely you have received; freely give." - Matt 10:8 #ifndef OTSU_HPP #define OTSU_HPP #include "Types.hpp" #include "Algorithm.hpp" #include "Image.hpp" #include "Palette.hpp" namespace Doxa { /// /// The Otsu Algorithm: Nobuyuki Otsu /// /// This implementation was actually inspired by: /// "A C++ Implementation of Otsu’s Image Segmentation Method", 2016. /// Thank you, Juan Pablo Balarini & Sergio Nesmachnow /// /// "A threshold selection method from gray-level histograms", 1979. class Otsu : public GlobalThreshold { public: static const int HISTOGRAM_SIZE = 256; Pixel8 Algorithm(const unsigned int* histogram, const int N) const { Pixel8 threshold = 0; // Init variables int sum = 0; int sumB = 0; int q1 = 0; double max = 0; // Calculate sum for (int idx = 0; idx < HISTOGRAM_SIZE; ++idx) { sum += idx * histogram[idx]; } for (int idx = 0; idx < HISTOGRAM_SIZE; ++idx) { // q1 = Weighted Background q1 += histogram[idx]; if (q1 == 0) continue; // q2 = Weighted Foreground const int q2 = N - q1; if (q2 == 0) break; sumB += (idx * histogram[idx]); const double m1m2 = (double)sumB / q1 - // Mean Background (double)(sum - sumB) / q2; // Mean Foreground // Note - There is an insidious casting situation going on here. // If one were to multiple by q1 or q2 first, an explicit cast would be required! const double between = m1m2 * m1m2 * q1 * q2; if (between > max) { threshold = idx; max = between; } } return threshold; } /// /// Get the Global Threshold for an image using the Otsu algorithm /// /// Grayscale input image /// This algorithm does not take parameters /// Global Threshold Pixel8 Threshold(const Image& grayScaleImage, const Parameters& parameters = Parameters()) { const int N = grayScaleImage.size; unsigned int histogram[HISTOGRAM_SIZE]; // Placed on stack for performance. This shouldn't be too large. memset(histogram, 0, (HISTOGRAM_SIZE) * sizeof(unsigned int)); for (int idx = 0; idx < N; ++idx) { ++histogram[grayScaleImage.data[idx]]; } return Algorithm(histogram, N); } }; } #endif //OTSU_HPP ================================================ FILE: Doxa/PNM.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2022, "Freely you have received; freely give." - Matt 10:8 #ifndef PNM_HPP #define PNM_HPP #include #include #include #include #include #include "Image.hpp" #include "Palette.hpp" #include "Parameters.hpp" #include "Grayscale.hpp" namespace Doxa { namespace fs = std::filesystem; /// /// Portable Any-Map (PNM) /// /// This class will allow you to read and write PNM formatted files natively, without a 3rd party library. /// /// Reading a 24 or 32 bit image will automatically convert it into grayscale. /// It should be assume that external PNM images are gamma compressed. /// See the Grayscale class for more information. /// /// File Format Support: /// Portable Bitmap (PBM P4) /// 8bit Portable Graymap (PGM P5) /// Portable Pixmap (PPM P6) /// Portable Arbitrary-Map (PAM P7) - Supports reading 8 bit Grayscale, 24 bit RGB, and 32 bit RGBA /// /// We do not support ASCII formats (P1, P2, or P3) nor 16bit Grayscale. /// class PNM { public: /// /// Reads any supported (aka: binary) PNM image from the filesystem. /// 24 and 32 bit images will be converted to 8-bit grayscale automatically. /// The PNM format specifies 709 gamma compression, but some are know to use sRGB. /// When converting to grayscale, this could impact your choice of algorithm. /// /// Select a grayscale conversion algorithm via: grayscale /// An 8 bit Image object static Image Read(const std::string& fileLocation, const Parameters& params = Parameters()) { std::ifstream file; file.open(fileLocation.c_str(), std::ios::binary); PNM pnm; Image image; pnm.ReadPNM(file, image, params); file.clear(); file.close(); // Automatically closes, but done explicitly for posterity. return image; } /// /// Writes an 8-bit grayscale Image to the filesystem /// static void Write(const Image& image, const std::string& fileLocation) { if (image.data == nullptr) return; std::ofstream file; file.open(fileLocation.c_str(), std::ios::binary); // Save the file based on file extension std::string ext = fs::path(fileLocation).extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); // C++, please steal from C# PNM pnm; if (ext == ".pbm") { pnm.WriteP4(file, image); } else if (ext == ".pgm") { pnm.WriteP5(file, image); } else if (ext == ".ppm") { pnm.WriteP6(file, image); } else if (ext == ".pam") { pnm.WriteP7(file, image); } file.clear(); file.close(); // Automatically closes, but done explicitly for posterity. } protected: // Hide default CTOR PNM() {} void Read1BitBinary(std::istream& inputStream, Image& image) { const int delta = image.width % 8; const int paddedWidth = delta == 0 ? image.width : image.width + 8 - delta; for (int y = 0; y < image.height; ++y) { for (int x = 0; x < paddedWidth; x+=8) { // Each row is always padded to a full byte const std::bitset<8> byte(inputStream.get()); const int position = (y * image.width) + x; for (int offset = 0; offset < 8 && x + offset < image.width; ++offset) { image.data[position + offset] = byte.test(7 - offset) ? Palette::Black : Palette::White; } } } } void Read8BitBinary(std::istream& inputStream, Image& image) { inputStream.read(reinterpret_cast(image.data), image.size); } template void ColorToGrayscale(std::istream& inputStream, Image& image, GrayscaleAlgorithms algorithm) { auto readRgb = [&]() -> std::tuple { Pixel8 r = inputStream.get(), g = inputStream.get(), b = inputStream.get(); if constexpr (Channels == 4) inputStream.get(); // Discard alpha return { r, g, b }; }; if (algorithm == GrayscaleAlgorithms::LABDIST) { // Two-pass: unknown range requires min/max discovery const auto lut = Grayscale::LinearLUT(); std::vector values(image.size); for (int idx = 0; idx < image.size; ++idx) { auto [r, g, b] = readRgb(); values[idx] = Grayscale::LABDist(lut[r], lut[g], lut[b]); } auto [mn, mx] = std::minmax_element(values.begin(), values.end()); const float min = *mn; const float scale = 255.0f / std::max(*mx - *mn, 1.0f); for (int idx = 0; idx < image.size; ++idx) image.data[idx] = static_cast((values[idx] - min) * scale); } else if (algorithm == GrayscaleAlgorithms::LIGHTNESS) { // Single-pass: known range 0-100 const auto lut = Grayscale::LinearLUT(); const float scale = 255.0f / 100.0f; for (int idx = 0; idx < image.size; ++idx) { auto [r, g, b] = readRgb(); image.data[idx] = static_cast( Grayscale::Lightness(lut[r], lut[g], lut[b]) * scale); } } else { // Single-pass: non-linear algorithms with gray shortcut auto toGrayscale = GrayscaleAlgorithm(algorithm); for (int idx = 0; idx < image.size; ++idx) { auto [r, g, b] = readRgb(); // If the pixel is already gray, do not apply the correction image.data[idx] = (r == g && g == b) ? b : toGrayscale(r, g, b); } } } void ReadPNM(std::istream& inputStream, Image& image, const Parameters& params = Parameters()) { std::string magicNumber; inputStream >> magicNumber; // Note: We do not currently support the ASCII formats (P1, P2, P3) if (magicNumber == "P4") // PBM 1 bit - Binary { // Read Header inputStream >> image.width >> image.height; inputStream.get(); // Skip trailing white space // Initialize storage structure image.size = image.width * image.height; image.data = new Pixel8[image.size]; return Read1BitBinary(inputStream, image); } else if (magicNumber == "P5") // PGM 8 bit - Binary { // Read Header inputStream >> image.width >> image.height >> image.maxVal; inputStream.get(); // Skip trailing white space // Assertions about our header assert(image.maxVal >= 0 && image.maxVal <= 255); // Initialize storage structure image.size = image.width * image.height; image.data = new Pixel8[image.size]; return Read8BitBinary(inputStream, image); } else if (magicNumber == "P6") // PPM 24 bit - Binary { // Read Header inputStream >> image.width >> image.height >> image.maxVal; inputStream.get(); // Skip trailing white space // Assertions about our header assert(image.maxVal >= 0 && image.maxVal <= 255); // Initialize storage structure image.size = image.width * image.height; image.data = new Pixel8[image.size]; auto algorithm = static_cast(params.Get("grayscale", (int)GrayscaleAlgorithms::MEAN)); return ColorToGrayscale<3>(inputStream, image, algorithm); } else if (magicNumber == "P7") // PAM arbitrary bit - Binary { std::string widthTxt, heightTxt, depthTxt, maxValTxt, tuplTypeTxt, endHeader; inputStream >> widthTxt >> image.width >> heightTxt >> image.height >> depthTxt >> image.depth >> maxValTxt >> image.maxVal >> tuplTypeTxt >> image.tupleType >> endHeader; // Assertions about our header assert(widthTxt == "WIDTH"); assert(heightTxt == "HEIGHT"); assert(depthTxt == "DEPTH"); assert(maxValTxt == "MAXVAL"); assert(tuplTypeTxt == "TUPLTYPE"); assert(endHeader == "ENDHDR"); inputStream.get(); // Skip trailing white space // Initialize storage structure image.size = image.width * image.height; image.data = new Pixel8[image.size]; switch (image.depth) { case 1: // "Such a PAM image has a depth of 1 and maxval 1 where the one sample in each tuple is 0 to represent a black pixel and 1 to represent a white one." // We are not supporting B&W at this time because they want 0 = black and 1 = white. It offends me to even think about writing such a routine. // Why not fall back to PBM's 1bit B&W, or 0 for black and 255 for white. But 8 bits and 1 is suppose to equal white? Please. if (image.tupleType == TupleTypes::BLACK_WHITE && image.maxVal == 1) throw "PAM: Black and White not supported. Use PBM instead."; return Read8BitBinary(inputStream, image); case 3: { auto algorithm = static_cast(params.Get("grayscale", (int)GrayscaleAlgorithms::MEAN)); return ColorToGrayscale<3>(inputStream, image, algorithm); } case 4: { if (image.tupleType != TupleTypes::RGBA) throw "PAM: Only 32bit RGBA is supported."; auto algorithm = static_cast(params.Get("grayscale", (int)GrayscaleAlgorithms::MEAN)); return ColorToGrayscale<4>(inputStream, image, algorithm); } } } throw "Unsupported Format"; } void WriteP4(std::ostream &outputStream, const Image &image) { outputStream << "P4" << std::endl << image.width << " " << image.height << std::endl; const int delta = image.width % 8; const int paddedWidth = delta == 0 ? image.width : image.width + 8 - delta; for (int y = 0; y < image.height; ++y) { for (int x = 0; x < paddedWidth; x += 8) { // Each row is always padded to a full byte std::bitset<8> byte; const int position = (y * image.width) + x; for (int offset = 0; offset < 8 && x + offset < image.width; ++offset) { byte.set(7 - offset, image.data[position + offset] == Palette::Black); } outputStream << static_cast(byte.to_ulong()); } } } void WriteP5(std::ostream &outputStream, const Image &image) { outputStream << "P5" << std::endl << image.width << " " << image.height << std::endl << image.maxVal << std::endl; const size_t size = image.size * sizeof(Pixel8); outputStream.write(reinterpret_cast(image.data), size); } void WriteP6(std::ostream& outputStream, const Image& image) { outputStream << "P6" << std::endl << image.width << " " << image.height << std::endl << image.maxVal << std::endl; for (int idx = 0; idx < image.size; ++idx) { const Pixel8 pixel = image.data[idx]; outputStream << pixel << pixel << pixel; } } void WriteP7(std::ostream &outputStream, const Image &image) { assert(image.depth == 1); // We are only supporting 8 bit for now outputStream << "P7" << std::endl << "WIDTH " << image.width << std::endl << "HEIGHT " << image.height << std::endl << "DEPTH " << image.depth << std::endl << "MAXVAL " << image.maxVal << std::endl << "TUPLTYPE " << image.tupleType << std::endl << "ENDHDR" << std::endl; const size_t size = image.size * sizeof(Pixel8); outputStream.write(reinterpret_cast(image.data), size); } GrayscaleFunc GrayscaleAlgorithm(GrayscaleAlgorithms algorithm) { switch (algorithm) { // Works in Non-Linear Color Space case GrayscaleAlgorithms::QT: return Grayscale::Qt; case GrayscaleAlgorithms::BT601: return Grayscale::BT601; case GrayscaleAlgorithms::BT709: return Grayscale::BT709; case GrayscaleAlgorithms::BT2100: return Grayscale::BT2100; case GrayscaleAlgorithms::VALUE: return Grayscale::Value; case GrayscaleAlgorithms::LUSTER: return Grayscale::Luster; case GrayscaleAlgorithms::MINAVG: return Grayscale::MinAvg; // Linear algorithms (LIGHTNESS, LABDIST) are handled directly by ColorToGrayscale // Default default: return Grayscale::Mean; } } }; } #endif // PNM_HPP ================================================ FILE: Doxa/Palette.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef PALETTE_HPP #define PALETTE_HPP #include #include "Types.hpp" namespace Doxa { /// /// Palette provides many pixel manipulation routines for getting and setting colors. /// Note that it is tuned only for Little Endian systems. /// On Big Endian systems, make sure to avoid any methods taking a Pixel32. /// class Palette { public: // RGBA 32b Pixel Decoding static inline constexpr int Red(Pixel32 rgba) { return (rgba & 0xff); } static inline constexpr int Green(Pixel32 rgba) { return ((rgba >> 8) & 0xff); } static inline constexpr int Blue(Pixel32 rgba) { return ((rgba >> 16) & 0xff); } static inline constexpr int Alpha(Pixel32 rgba) { return rgba >> 24; } // RGBA to Pixel static inline constexpr Pixel32 RGB(int r, int g, int b) { return (0xffu << 24) | ((b & 0xff) << 16) | ((g & 0xff) << 8) | (r & 0xff); } static inline constexpr Pixel32 RGBA(int r, int g, int b, int a) { return ((a & 0xff) << 24) | ((b & 0xff) << 16) | ((g & 0xff) << 8) | (r & 0xff); } static inline constexpr Pixel32 UpdateAlpha(Pixel32 rgba, int a) { return (rgba & 0x00ffffff) | (a << 24); } /// /// Using the RGB color space, calculate the difference between two colors. /// The results should be very similar to equations using the more common L*u*v* color space. /// /// https://www.compuphase.com/cmetric.htm /// static inline int ColorDistance(Pixel32 left, Pixel32 right) { const int rmean = (Red(left) + Red(right)) / 2; const int r = Red(left) - Red(right); const int g = Green(left) - Green(right); const int b = Blue(left) - Blue(right); return std::sqrt( ((2 + rmean/256) * r*r) + (4 * g*g) + ((2 + (255 - rmean)/256) * b*b) ); } static inline constexpr bool IsGray(Pixel32 rgba) { return Blue(rgba) == Green(rgba) && Blue(rgba) == Red(rgba); } // Black and White static const Pixel8 Black = 0; static const Pixel8 White = 255; }; } #endif // PALETTE_HPP ================================================ FILE: Doxa/Parameters.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef PARAMETERS_HPP #define PARAMETERS_HPP #include #include #include #include "Types.hpp" namespace Doxa { typedef std::variant ParameterValue; typedef std::map ParameterMap; /// /// Parameters can be passed into any algorithm, much like a key value pair. /// The Key is the name of the parameter, while the Value can be any Integer or Double. /// /// Note: All window values should be ODD. We are currently not enforcing this. /// class Parameters { public: template const Type Get(const std::string& name, const Type& defaultValue) const { ParameterMap::const_iterator pos = parameterMap.find(name); if (pos == parameterMap.end()) { return defaultValue; } else { return std::visit( [](auto&& value) -> Type { // Some languages, like Javascript, store 1 and 1.0 identically. // std::get(pos->second), in this situation, will throw a std::bad_variant_access // exception due to its inability to implicitly cast Int to a Float/Double. return static_cast(value); }, pos->second ); } } void Set(const std::string& name, const ParameterValue& value) { parameterMap[name] = value; } /// /// CTOR /// Example: Parameters param({ {"window", 75}, {"k", 0.1 } }); /// Parameters(const ParameterMap& parameterMap) { this->parameterMap = parameterMap; } /// /// A very naive JSON object parser. Useful for WebAssembly. /// Example: Parameters params(R"({"window": 75, "k": -0.01})"); /// /// A simple JSON object static Parameters FromJson(const std::string& json) { Parameters params; std::size_t keyStart = json.find('"'); while (keyStart != std::string::npos) { // Get Coordinates std::size_t keyStop = json.find('"', ++keyStart); std::size_t valueStart = json.find(':', keyStop); std::size_t valueStop = json.find_first_of(",}", ++valueStart); // Get Key / Value std::string key = json.substr(keyStart, keyStop - keyStart); std::string value = json.substr(valueStart, valueStop - valueStart); // Value Type: String if (value.find('"') != std::string::npos) { // Filtered Out: We do not currently support string parameters. } // Value Type: Double else if (value.find('.') != std::string::npos) { params.parameterMap[key] = std::stod(value); } // Value Type: Integer else { params.parameterMap[key] = std::stoi(value); } keyStart = json.find('"', ++keyStop); } return params; } Parameters() {} protected: ParameterMap parameterMap; }; } #endif //PARAMETERS_HPP ================================================ FILE: Doxa/Phansalkar.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2026, "Freely you have received; freely give." - Matt 10:8 #ifndef PHANSALKAR_HPP #define PHANSALKAR_HPP #include "Algorithm.hpp" #include "LocalWindow.hpp" #include "ChanMeanVarianceCalc.hpp" namespace Doxa { /// /// The Phansalkar Algorithm: Neerad Phansalkar, Sumit More, Ashish Sabale, Madhuri Joshi /// /// This papers calls for the logical OR of the thresholding algorithm ran on: /// RGB Grayscale - BT601 /// L* a* b* Grayscale - sqr_root(L*^2 + a*^2 + b*^2) /// /// This algorithm is unique in its use of LAB to take into account Color! /// NOTE: Run this twice with different grayscale algorithms and then OR the images together. /// /// Optimization Approach (from paper): /// "Such a global threshold should not reject a single pixel that corresponds to a nucleus..." /// "Such a threshold is obtained using Otsu’s method." /// This is not true of document glyphs. Because of this, the Otsu speed optimization is not implemented. /// /// "Adaptive Local Thresholding for Detection of Nuclei in Diversely Stained Cytology Images", 2011. class Phansalkar : public Algorithm, public ChanMeanVarianceCalc { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Read parameters, utilizing defaults const int windowSize = parameters.Get("window", 75); const double k = parameters.Get("k", 0.2); const double p = parameters.Get("p", 3.0); const double q = parameters.Get("q", 10.0) / 255; // Normalized to 0 to 255, not 0 to 1. Process(binaryImageOut, Algorithm::grayScaleImageIn, windowSize, [&](const double& mean, const double& variance, const int&) { const double stddev = std::sqrt(variance); return mean * (1 + p * exp(-q * (mean)) + k * ((stddev / 128) - 1)); }); } }; } #endif //PHANSALKAR_HPP ================================================ FILE: Doxa/Region.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef REGION_HPP #define REGION_HPP namespace Doxa { /// /// A structure which denotes a specific area, or window, within an Image. /// Note that only Points are stored. All other calls will be calculated. /// struct Region { public: struct Point { Point() {} Point(int x, int y) : x(x), y(y) {} bool operator==(const Point& rhs) const { return (x == rhs.x) && (y == rhs.y); } int x; int y; }; Region() {} Region(const Point& upperLeft, const Point& bottomRight) : upperLeft(upperLeft), bottomRight(bottomRight) {} Region(int x1, int y1, int x2, int y2) : upperLeft(x1, y1), bottomRight(x2, y2) {} Region(int x1, int y1, int size) : upperLeft(x1, y1), bottomRight(x1 + size - 1, y1 + size - 1) {} Region(int width, int height) : upperLeft(0, 0), bottomRight(width - 1, height - 1) {} inline bool InRegion(const Region& region) const { return (region.upperLeft.x >= upperLeft.x && region.upperLeft.y >= upperLeft.y && region.bottomRight.x <= bottomRight.x && region.bottomRight.y <= bottomRight.y); } inline int Width() const { return (bottomRight.x - upperLeft.x) + 1; } inline int Height() const { return (bottomRight.y - upperLeft.y) + 1; } inline int Area() const { return Width() * Height(); } Point upperLeft; Point bottomRight; bool operator==(const Region& rhs) const { return (upperLeft == rhs.upperLeft) && (bottomRight == rhs.bottomRight); } }; } #endif // REGION_HPP ================================================ FILE: Doxa/SIMD.h ================================================ // ?oxa Binarization Framework // License: CC0 2026, "Freely you have received; freely give." - Matt 10:8 // Purpose: Compile-time SIMD detection for SSE2, NEON, and WASM SIMD (128-bit) #ifndef SIMD_H #define SIMD_H namespace Doxa::SIMD { // ============================================================================ // Compile-Time SIMD Detection // ============================================================================ # define DOXA_SIMD 1 #if defined(__wasm_simd128__) // WebAssembly SIMD128 #define DOXA_SIMD_WASM 1 #elif defined(__SSE2__) || (defined(_MSC_VER) && defined(_M_X64)) // x86-64 with SSE2 (always available on x64, standard since Pentium 4) #define DOXA_SIMD_SSE2 1 #elif defined(__ARM_NEON) || defined(__ARM_NEON__) || defined(__aarch64__) // ARM NEON (always available on ARM64/aarch64) #define DOXA_SIMD_NEON 1 #else // No SIMD options available #undef DOXA_SIMD #endif // ============================================================================ // SIMD Width Constant // All supported SIMD architectures use 128-bit vectors = 16 bytes // ============================================================================ constexpr int SIMD_WIDTH = 16; } // namespace Doxa::SIMD #endif // SIMD_H ================================================ FILE: Doxa/SIMDOps.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2026, "Freely you have received; freely give." - Matt 10:8 // Purpose: Unified 128-bit SIMD operations for SSE2, NEON, and WASM SIMD #ifndef SIMDOPS_HPP #define SIMDOPS_HPP #include "SIMD.h" // ============================================================================ // Platform-Specific Includes // ============================================================================ #if defined(DOXA_SIMD_SSE2) #include #elif defined(DOXA_SIMD_NEON) #include #elif defined(DOXA_SIMD_WASM) #include #endif namespace Doxa::SIMD { // ============================================================================ // Vector Type Definition // ============================================================================ #if defined(DOXA_SIMD_SSE2) typedef __m128i vec128; #elif defined(DOXA_SIMD_NEON) typedef uint8x16_t vec128; #elif defined(DOXA_SIMD_WASM) typedef v128_t vec128; #endif // ============================================================================ // Unified SIMD Macros // These provide a common interface across SSE2, NEON, and WASM SIMD. // Only macros that are actually used in the codebase are defined. // ============================================================================ #if defined(DOXA_SIMD_SSE2) // SSE2 implementations #define VEC_LOAD(ptr) _mm_loadu_si128((const __m128i*)(ptr)) #define VEC_STORE(ptr, v) _mm_storeu_si128((__m128i*)(ptr), v) #define VEC_SPLAT_U8(val) _mm_set1_epi8((char)(val)) #define VEC_CMPEQ_U8(a, b) _mm_cmpeq_epi8(a, b) #define VEC_MIN_U8(a, b) _mm_min_epu8(a, b) #define VEC_AND(a, b) _mm_and_si128(a, b) #define VEC_ANDNOT(a, b) _mm_andnot_si128(a, b) // ~a & b #define VEC_NOT(a) _mm_xor_si128(a, _mm_set1_epi8(-1)) #define VEC_ALL_TRUE_U8(v) (_mm_movemask_epi8(v) == 0xFFFF) #define VEC_LOAD_2x64(p0, p1) _mm_unpacklo_epi64( \ _mm_loadl_epi64((const __m128i*)(p0)), \ _mm_loadl_epi64((const __m128i*)(p1))) #elif defined(DOXA_SIMD_NEON) // ARM NEON implementations #define VEC_LOAD(ptr) vld1q_u8((const uint8_t*)(ptr)) #define VEC_STORE(ptr, v) vst1q_u8((uint8_t*)(ptr), v) #define VEC_SPLAT_U8(val) vdupq_n_u8(val) #define VEC_CMPEQ_U8(a, b) vceqq_u8(a, b) #define VEC_MIN_U8(a, b) vminq_u8(a, b) #define VEC_AND(a, b) vandq_u8(a, b) #define VEC_ANDNOT(a, b) vbicq_u8(b, a) // b & ~a (note: reversed args) #define VEC_NOT(a) vmvnq_u8(a) #define VEC_ALL_TRUE_U8(v) (vminvq_u8(v) == 0xFF) #define VEC_LOAD_2x64(p0, p1) vcombine_u8(vld1_u8(p0), vld1_u8(p1)) #elif defined(DOXA_SIMD_WASM) // WebAssembly SIMD implementations #define VEC_LOAD(ptr) wasm_v128_load(ptr) #define VEC_STORE(ptr, v) wasm_v128_store(ptr, v) #define VEC_SPLAT_U8(val) wasm_i8x16_splat(val) #define VEC_CMPEQ_U8(a, b) wasm_i8x16_eq(a, b) #define VEC_MIN_U8(a, b) wasm_u8x16_min(a, b) #define VEC_AND(a, b) wasm_v128_and(a, b) #define VEC_ANDNOT(a, b) wasm_v128_andnot(b, a) // b & ~a #define VEC_NOT(a) wasm_v128_not(a) #define VEC_ALL_TRUE_U8(v) (wasm_i8x16_bitmask(v) == 0xFFFF) #define VEC_LOAD_2x64(p0, p1) wasm_v128_load64_lane(p1, wasm_v128_load64_zero(p0), 1) #endif #if defined(DOXA_SIMD) // Check if all bytes in two vectors are equal #define VEC_ALL_EQ_U8(a, b) VEC_ALL_TRUE_U8(VEC_CMPEQ_U8(a, b)) #endif // ============================================================================ // Helper Functions - Platform-Specific Implementations // ============================================================================ #if defined(DOXA_SIMD) // Horizontal Sum of Bytes inline int vec_hsum_u8(vec128 v) { #if defined(DOXA_SIMD_SSE2) vec128 sad = _mm_sad_epu8(v, _mm_setzero_si128()); return _mm_cvtsi128_si32(sad) + _mm_extract_epi16(sad, 4); #elif defined(DOXA_SIMD_NEON) return vaddvq_u8(v); #elif defined(DOXA_SIMD_WASM) vec128 sum16 = wasm_u16x8_extadd_pairwise_u8x16(v); vec128 sum32 = wasm_i32x4_extadd_pairwise_i16x8(sum16); return wasm_i32x4_extract_lane(sum32, 0) + wasm_i32x4_extract_lane(sum32, 1) + wasm_i32x4_extract_lane(sum32, 2) + wasm_i32x4_extract_lane(sum32, 3); #endif } #endif // DOXA_SIMD } // namespace Doxa::SIMD #endif // SIMDOPS_HPP ================================================ FILE: Doxa/Sauvola.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef SAUVOLA_HPP #define SAUVOLA_HPP #include "Algorithm.hpp" #include "LocalWindow.hpp" #include "ChanMeanVarianceCalc.hpp" namespace Doxa { /// /// The Sauvola Algorithm: J. Sauvola, M. Pietikäinen /// /// "Adaptive document image binarization", 1999. class Sauvola : public Algorithm, public ChanMeanVarianceCalc { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Read parameters, utilizing defaults const int windowSize = parameters.Get("window", 75); const double k = parameters.Get("k", 0.2); Process(binaryImageOut, Algorithm::grayScaleImageIn, windowSize, [&](const double& mean, const double& variance, const int&) { const double stddev = std::sqrt(variance); return mean * (1 + k * ((stddev / 128) - 1)); }); } }; } #endif //SAUVOLA_HPP ================================================ FILE: Doxa/Su.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef SU_HPP #define SU_HPP #include "Types.hpp" #include "Otsu.hpp" #include "Palette.hpp" #include "Region.hpp" #include "Morphology.hpp" #include "ContrastImage.hpp" namespace Doxa { /// /// The Su Algorithm: Bolan Su, Shijian Lu, and Chew Lim Tan /// This is a 3 step workflow consisting of: /// Contrast Image generation /// High contrast pixel detection using Otsu binarization /// A novel local thresholding algorithm /// /// Parameters window and minN are auto-detected from stroke width when not provided. /// /// "Binarization of Historical Document Images Using the Local Maximum and Minimum", 2010. class Su : public Algorithm { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // 0 will trigger the auto detection of these parameters as detailed in the paper int windowSize = parameters.Get("window", 0); // Based on Stroke Size int minN = parameters.Get("minN", windowSize); // Roughly based on size of window // Step 1 & 2 - Generate Hight Contrast Image Construction Image contrastImage(Algorithm::grayScaleImageIn.width, Algorithm::grayScaleImageIn.height); ContrastImage::GenerateHighContrastImage(contrastImage, Algorithm::grayScaleImageIn); // Optional Parameter Auto Detection if (windowSize == 0) { AutoDetectParameters(windowSize, minN, contrastImage); } // Step 3 - Historical Document Thresholding Threshold(binaryImageOut, contrastImage, Algorithm::grayScaleImageIn, windowSize, minN); } protected: // // Calculates Stroke Width and Min-N from the estimated stroke width. // // In Su's 2010 paper, they provide no specifics on window size... just make it bigger than the stroke width. // In their 2013 paper, "Robust Document Image Binarization Technique for Degraded Document Images", // they empirically show that (Window Size = 2 x Stroke Width) to be the best. // void AutoDetectParameters(int& windowSize, int& minN, const Image& contrastImage) { const int strokeWidth = ContrastImage::EstimateStrokeWidth(contrastImage); windowSize = strokeWidth * 2; minN = windowSize; } /// /// Calculates Ne, meanE, and stdE in one iteration. /// This is a very optimized set of calculations compared to the math found in the paper. /// void SuCalculations(int& Ne, double& meanE, double& stdE, const Image& contrastImage, const Image& grayScaleImage, const Region& window) const { uint64_t sumI = 0; // Σ I(x,y) uint64_t sumI2 = 0; // Σ I(x,y)² Ne = 0; // Single pass: accumulate first and second raw moments over high-contrast pixels. // Uses identity: Var(X) = E[X²] - (E[X])² LocalWindow::Iterate(grayScaleImage.width, window, [&](const int& position) { if (Palette::White == contrastImage.data[position]) { const uint32_t I = grayScaleImage.data[position]; // promote once sumI += I; sumI2 += I * I; ++Ne; } }); if (Ne == 0) { meanE = 0.0; stdE = 0.0; return; } meanE = static_cast(sumI) / Ne; // variance = ΣI²/Ne − meanE² const double variance = static_cast(sumI2) / Ne - meanE * meanE; // Clamp tiny negatives from floating-point round-off (happens when true variance ≈ 0) stdE = variance > 0.0 ? std::sqrt(variance) : 0.0; }; void Threshold(Image& binaryImageOut, const Image& contrastImageIn, const Image& grayScaleImageIn, int windowSize, int minN) const { int Ne; double meanE, stdE; LocalWindow::Iterate(grayScaleImageIn, windowSize, [&](const Region& window, const int& position) { SuCalculations(Ne, meanE, stdE, contrastImageIn, grayScaleImageIn, window); binaryImageOut.data[position] = (Ne >= minN && grayScaleImageIn.data[position] <= meanE + (stdE / 2)) ? Palette::Black : Palette::White; }); } }; } #endif //SU_HPP ================================================ FILE: Doxa/TRSingh.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef TRSINGH_HPP #define TRSINGH_HPP #include "Algorithm.hpp" #include "LocalWindow.hpp" #include "ChanMeanCalc.hpp" namespace Doxa { /// /// The T.R. Singh Algorithm: T. Romen Singh, Sudipta Roy, O. Imocha Singh, Tejmani Sinam, Kh. Manglem Singh /// /// "A New local Adaptive Thresholding Technique in Binarization", 2011. class TRSingh : public Algorithm, public ChanMeanCalc { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Read parameters, utilizing defaults const int windowSize = parameters.Get("window", 75); const double k = parameters.Get("k", 0.2); Process(binaryImageOut, Algorithm::grayScaleImageIn, windowSize, [&](const double& mean, const int& position) { // Unlike Sauvola, the Singh algorithm necessitates a value between 0 and 1, not 0 255. // This adapts his formula to 8bit grayscale to avoid conversion operations. constexpr double R = 255.0; // TR Singh's algorithm does not expressly mention the need for an absolute value. // However, I believe it is implied because we are talking about deviation double meandev = std::abs((double)Algorithm::grayScaleImageIn.data[position] - mean); // This clamping operation prevents a divide by zero situation // Alternative: Add std::numeric_limits::epsilon() to the denominator meandev = std::min(meandev, R - 1.0); return mean * (1 + k * ((meandev / (R - meandev)) - 1)); }); } }; } #endif //TRSINGH_HPP ================================================ FILE: Doxa/Types.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef TYPES_HPP #define TYPES_HPP #include namespace Doxa { // Forward Declarations struct Image; struct Region; // Type Definitions typedef uint32_t Pixel32; typedef uint8_t Pixel8; namespace TupleTypes { static const std::string BLACK_WHITE = "BLACKANDWHITE"; static const std::string GRAYSCALE = "GRAYSCALE"; static const std::string RGB = "RGB"; static const std::string RGBA = "RGB_ALPHA"; } } #endif // TYPES_HPP ================================================ FILE: Doxa/Wan.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef WAN_HPP #define WAN_HPP #include "Algorithm.hpp" #include "LocalWindow.hpp" #include "ChanMeanVarianceCalc.hpp" #include "Morphology.hpp" namespace Doxa { /// /// The WAN Algorithm: Wan Azani Mustafa, Mohamed Mydin M. Abdul Kader /// /// "Binarization of Document Image Using Optimum Threshold Modification", 2018. class Wan : public Algorithm, public ChanMeanVarianceCalc { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Read parameters, utilizing defaults const int windowSize = parameters.Get("window", 75); const double k = parameters.Get("k", 0.2); // Use Dilate to generate a Max Image for the target Window Image maxImage(Algorithm::grayScaleImageIn.width, Algorithm::grayScaleImageIn.height); Morphology::Dilate(maxImage, Algorithm::grayScaleImageIn, windowSize); Process(binaryImageOut, Algorithm::grayScaleImageIn, windowSize, [&](const double& mean, const double& variance, const int& position) { const double stddev = std::sqrt(variance); return (((double)maxImage.data[position] + mean) / 2) * (1 + k * ((stddev / 128) - 1)); }); } }; } #endif //WAN_HPP ================================================ FILE: Doxa/WienerFilter.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef WIENERFILTER_HPP #define WIENERFILTER_HPP #include "Types.hpp" #include "Region.hpp" #include "Image.hpp" #include "ChanMeanVarianceCalc.hpp" namespace Doxa { /// /// Wiener Filter - Implementation based on the wiener2 MathWorks algorithm. /// /// TODO: Improve performance /// /// Resource: https://www.mathworks.com/help/images/ref/wiener2.html class WienerFilter { public: static void Filter(Image& outputImage, const Image& inputImage, const int windowSize = 3) { ChanMeanVarianceCalc calculator; // Obtain the average variance for all pixels double sumVariance = 0; calculator.Iterate(inputImage, windowSize, [&](const double&, const double& variance, const int&) { sumVariance += variance; }); const double avgVariance = sumVariance / inputImage.size; // Apply Wiener Filter calculator.Iterate(inputImage, windowSize, [&](const double& mean, const double& variance, const int& position) { // The avgVariance is simulating noise-variance. It should always be greater than variance. outputImage.data[position] = variance < avgVariance ? mean : // Variance can be 0, so avoid the divide by 0 issue by using mean value. mean + ((variance - avgVariance) * (double)(inputImage.data[position] - mean)) / variance; }); } }; } #endif //WIENERFILTER_HPP ================================================ FILE: Doxa/Wolf.hpp ================================================ // Δoxa Binarization Framework // License: CC0 2018, "Freely you have received; freely give." - Matt 10:8 #ifndef WOLF_HPP #define WOLF_HPP #include "Algorithm.hpp" #include "LocalWindow.hpp" #include "ChanMeanVarianceCalc.hpp" namespace Doxa { /// /// The Wolf Algorithm: Christian Wolf, Jean-Michel Jolion /// /// "Extraction and Recognition of Artificial Text in Multimedia Documents", 2003. class Wolf : public Algorithm, public ChanMeanVarianceCalc { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Read parameters, utilizing defaults const int windowSize = parameters.Get("window", 75); const double k = parameters.Get("k", 0.2); double min = std::numeric_limits::max(); double maxVariance = std::numeric_limits::min(); // Find global min value and max standard deviation value Iterate(Algorithm::grayScaleImageIn, windowSize, [&](const double&, const double& variance, const int& position) { if (variance > maxVariance) maxVariance = variance; const double tmpMin = Algorithm::grayScaleImageIn.data[position]; if (tmpMin < min) min = tmpMin; }); const double maxStdDev = std::sqrt(maxVariance); Process(binaryImageOut, Algorithm::grayScaleImageIn, windowSize, [&](const double& mean, const double& variance, const int& position) { const double stddev = std::sqrt(variance); return mean - k * (1 - (stddev / maxStdDev)) * (mean - min); }); } }; } #endif //WOLF_HPP ================================================ FILE: Doxa.Bench/BenchmarkHarness.hpp ================================================ #ifndef BENCHMARKHARNESS_HPP #define BENCHMARKHARNESS_HPP #include "pch.h" #include "config.hpp" namespace Doxa::Benchmarks { // Exposes GlobalThreshold internals for benchmarking class GlobalThresholdBenchHarness : public GlobalThreshold { Pixel8 Threshold(const Image& grayScaleImage, const Parameters& parameters = Parameters()) { return 128; } }; // Exposes DRDM internals for benchmarking class DRDMBenchHarness : public Doxa::DRDM { public: using DRDM::NUBN_STD; using DRDM::NUBN_STD_8x8; using DRDM::SumDRDkForMismatchedPixels; #if defined(DOXA_SIMD) using DRDM::NUBN_SIMD_8x8; #endif }; // Switchable calculator backend for Niblack, used to benchmark calculators template class NiblackBase : public Algorithm>, public Calculator { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { const int windowSize = parameters.Get("window", 75); const double k = parameters.Get("k", 0.2); Calculator::Process(binaryImageOut, Algorithm>::grayScaleImageIn, windowSize, [&](const double& mean, const double& variance, const int&) { const double stddev = std::sqrt(variance); return (mean + (k * stddev)); }); } }; // Resource path helper inline std::string ResourcesDir() { return std::string(DOXA_BENCH_RESOURCES_DIR) + "/"; } } #endif // BENCHMARKHARNESS_HPP ================================================ FILE: Doxa.Bench/BinarizationBenchmarks.cpp ================================================ #include "pch.h" #include "BenchmarkHarness.hpp" namespace Doxa::Benchmarks { static Image ReadTestImage() { return PNM::Read(ResourcesDir() + "2JohnC1V3.ppm", ParameterMap{ {"grayscale", GrayscaleAlgorithms::QT} }); } // --- Global Thresholding --- static void BM_Otsu(benchmark::State& state) { const Image image = ReadTestImage(); for (auto _ : state) { Image result = Otsu::ToBinaryImage(image); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Otsu); // --- Local Adaptive Thresholding --- static void BM_Niblack(benchmark::State& state) { const Image image = ReadTestImage(); const Parameters parameters({ { "window", 75 }, { "k", 0.2 } }); for (auto _ : state) { Image result = Niblack::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Niblack); static void BM_Sauvola(benchmark::State& state) { const Image image = ReadTestImage(); const Parameters parameters({ { "window", 75 }, { "k", 0.2 } }); for (auto _ : state) { Image result = Sauvola::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Sauvola); static void BM_Wolf(benchmark::State& state) { const Image image = ReadTestImage(); const Parameters parameters({ { "window", 75 }, { "k", 0.2 } }); for (auto _ : state) { Image result = Wolf::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Wolf); static void BM_Nick(benchmark::State& state) { const Image image = ReadTestImage(); const Parameters parameters({ { "window", 75 }, { "k", -0.2 } }); for (auto _ : state) { Image result = Nick::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Nick); static void BM_Bernsen(benchmark::State& state) { const Image image = ReadTestImage(); const Parameters parameters({ { "window", 75 }, { "threshold", 100 }, { "contrast-limit", 25 } }); for (auto _ : state) { Image result = Bernsen::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Bernsen); static void BM_TRSingh(benchmark::State& state) { const Image image = ReadTestImage(); const Parameters parameters({ { "window", 75 }, { "k", 0.2 } }); for (auto _ : state) { Image result = TRSingh::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_TRSingh); static void BM_Wan(benchmark::State& state) { const Image image = ReadTestImage(); const Parameters parameters({ { "window", 75 }, { "k", 0.2 } }); for (auto _ : state) { Image result = Wan::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Wan); static void BM_Gatos(benchmark::State& state) { const Image image = ReadTestImage(); const Parameters parameters({ { "window", 75 }, { "k", 0.2 } }); for (auto _ : state) { Image result = Gatos::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Gatos); static void BM_ISauvola(benchmark::State& state) { const Image image = ReadTestImage(); const Parameters parameters({ { "window", 75 }, { "k", 0.2 } }); for (auto _ : state) { Image result = ISauvola::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_ISauvola); static void BM_Su(benchmark::State& state) { const Image image = ReadTestImage(); for (auto _ : state) { Image result = Su::ToBinaryImage(image); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Su); static void BM_Bataineh(benchmark::State& state) { const Image image = ReadTestImage(); for (auto _ : state) { Image result = Bataineh::ToBinaryImage(image); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Bataineh); static void BM_Phansalkar(benchmark::State& state) { const Image image = ReadTestImage(); const Parameters parameters({ { "window", 75 }, { "k", 0.2 } }); for (auto _ : state) { Image result = Phansalkar::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Phansalkar); static void BM_AdOtsu(benchmark::State& state) { const Image image = ReadTestImage(); for (auto _ : state) { Image result = AdOtsu::ToBinaryImage(image); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_AdOtsu); static void BM_AdOtsuMS(benchmark::State& state) { const Image image = ReadTestImage(); for (auto _ : state) { Image result = AdOtsuMS::ToBinaryImage(image); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_AdOtsuMS); } // namespace Doxa::Benchmarks ================================================ FILE: Doxa.Bench/CMakeLists.txt ================================================ message(STATUS "Doxa Bench - CMake Build") cmake_minimum_required(VERSION 3.16) project(doxa_bench) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # SIMD Options option(DOXA_ENABLE_SIMD "Enable SIMD optimizations (compile all paths, runtime detection)" ON) if(MSVC) # so far so good else() add_compile_options("-Wno-narrowing") endif() include(FetchContent) FetchContent_Declare( googlebenchmark URL https://github.com/google/benchmark/archive/refs/tags/v1.9.1.zip ) set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE) set(BENCHMARK_ENABLE_INSTALL OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googlebenchmark) include_directories(../Doxa) # Resolve absolute path to test resources (shared with Doxa.Test) get_filename_component(DOXA_RESOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Doxa.Test/Resources" ABSOLUTE) configure_file(config.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/config.hpp @ONLY) add_executable( doxa_bench GlobalThresholdBenchmarks.cpp ClassifiedPerformanceBenchmarks.cpp DRDMBenchmarks.cpp CalculatorBenchmarks.cpp BinarizationBenchmarks.cpp ) target_include_directories(doxa_bench PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) # Platform detection and SIMD compiler flags if(DOXA_ENABLE_SIMD) message(STATUS "SIMD optimizations: ENABLED (runtime detection)") if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|x64") if(MSVC) message(STATUS "SIMD: x86-64 with SSE2 (MSVC)") else() target_compile_options(doxa_bench PRIVATE -msse2) message(STATUS "SIMD: x86-64 with SSE2 (GCC/Clang)") endif() elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") message(STATUS "SIMD: ARM64 with NEON") elseif(EMSCRIPTEN) target_compile_options(doxa_bench PRIVATE -msimd128) message(STATUS "SIMD: WebAssembly with SIMD128") else() message(STATUS "SIMD: Unknown platform, will use scalar fallback") endif() else() message(STATUS "SIMD optimizations: DISABLED (scalar only)") endif() target_precompile_headers( doxa_bench PUBLIC pch.h ) target_link_libraries( doxa_bench benchmark::benchmark benchmark::benchmark_main ) message(STATUS "Compiler: ${CMAKE_CXX_COMPILER} ${CMAKE_CXX_COMPILER_VERSION}") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") message(STATUS "System processor: ${CMAKE_SYSTEM_PROCESSOR}") message(STATUS "Resources: ${DOXA_RESOURCES_DIR}") ================================================ FILE: Doxa.Bench/CalculatorBenchmarks.cpp ================================================ #include "pch.h" #include "BenchmarkHarness.hpp" namespace Doxa::Benchmarks { static void BM_Niblack_Chan(benchmark::State& state) { const std::string dir = ResourcesDir(); Image image = PNM::Read(dir + "2JohnC1V3.ppm", ParameterMap{ {"grayscale", GrayscaleAlgorithms::QT} }); const Parameters parameters({ { "window", 223 }, { "k", -0.61 } }); for (auto _ : state) { Image result = NiblackBase::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Niblack_Chan); static void BM_Niblack_IntegralImage(benchmark::State& state) { const std::string dir = ResourcesDir(); Image image = PNM::Read(dir + "2JohnC1V3.ppm", ParameterMap{ {"grayscale", GrayscaleAlgorithms::QT} }); const Parameters parameters({ { "window", 223 }, { "k", -0.61 } }); for (auto _ : state) { Image result = NiblackBase::ToBinaryImage(image, parameters); benchmark::DoNotOptimize(result.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_Niblack_IntegralImage); } // namespace Doxa::Benchmarks ================================================ FILE: Doxa.Bench/ClassifiedPerformanceBenchmarks.cpp ================================================ #include "pch.h" #include "BenchmarkHarness.hpp" namespace Doxa::Benchmarks { static void BM_CompareImages_Scalar(benchmark::State& state) { const std::string dir = ResourcesDir(); Image binaryImage = PNM::Read(dir + "2JohnC1V3-Sauvola.pbm"); Image groundTruthImage = PNM::Read(dir + "2JohnC1V3-GroundTruth.pbm"); for (auto _ : state) { ClassifiedPerformance::Classifications classifications; ClassifiedPerformance::CompareImages_STD(classifications, groundTruthImage.data, binaryImage.data, groundTruthImage.size); benchmark::DoNotOptimize(classifications); } state.SetBytesProcessed(int64_t(state.iterations()) * groundTruthImage.size); } BENCHMARK(BM_CompareImages_Scalar); #if defined(DOXA_SIMD) static void BM_CompareImages_SIMD(benchmark::State& state) { const std::string dir = ResourcesDir(); Image binaryImage = PNM::Read(dir + "2JohnC1V3-Sauvola.pbm"); Image groundTruthImage = PNM::Read(dir + "2JohnC1V3-GroundTruth.pbm"); for (auto _ : state) { ClassifiedPerformance::Classifications classifications; ClassifiedPerformance::CompareImages_SIMD(classifications, groundTruthImage.data, binaryImage.data, groundTruthImage.size); benchmark::DoNotOptimize(classifications); } state.SetBytesProcessed(int64_t(state.iterations()) * groundTruthImage.size); } BENCHMARK(BM_CompareImages_SIMD); #endif // DOXA_SIMD } // namespace Doxa::Benchmarks ================================================ FILE: Doxa.Bench/DRDMBenchmarks.cpp ================================================ #include "pch.h" #include "BenchmarkHarness.hpp" namespace Doxa::Benchmarks { static void BM_NUBN_STD(benchmark::State& state) { const std::string dir = ResourcesDir(); Image binaryImage = PNM::Read(dir + "2JohnC1V3-GroundTruth.pbm"); for (auto _ : state) { unsigned int count = DRDMBenchHarness::NUBN_STD(binaryImage, 8); benchmark::DoNotOptimize(count); } } BENCHMARK(BM_NUBN_STD); static void BM_NUBN_STD_8x8(benchmark::State& state) { const std::string dir = ResourcesDir(); Image binaryImage = PNM::Read(dir + "2JohnC1V3-GroundTruth.pbm"); for (auto _ : state) { unsigned int count = DRDMBenchHarness::NUBN_STD_8x8(binaryImage); benchmark::DoNotOptimize(count); } } BENCHMARK(BM_NUBN_STD_8x8); static void BM_SumDRDk(benchmark::State& state) { const std::string dir = ResourcesDir(); Image controlImage = PNM::Read(dir + "2JohnC1V3-GroundTruth.pbm"); Image experimentImage = PNM::Read(dir + "2JohnC1V3-Sauvola.pbm"); for (auto _ : state) { uint64_t result = DRDMBenchHarness::SumDRDkForMismatchedPixels(controlImage, experimentImage); benchmark::DoNotOptimize(result); } } BENCHMARK(BM_SumDRDk); #if defined(DOXA_SIMD) static void BM_NUBN_SIMD_8x8(benchmark::State& state) { const std::string dir = ResourcesDir(); Image binaryImage = PNM::Read(dir + "2JohnC1V3-GroundTruth.pbm"); for (auto _ : state) { unsigned int count = DRDMBenchHarness::NUBN_SIMD_8x8(binaryImage); benchmark::DoNotOptimize(count); } } BENCHMARK(BM_NUBN_SIMD_8x8); #endif // DOXA_SIMD } // namespace Doxa::Benchmarks ================================================ FILE: Doxa.Bench/GlobalThresholdBenchmarks.cpp ================================================ #include "pch.h" #include "BenchmarkHarness.hpp" namespace Doxa::Benchmarks { static void BM_GlobalThreshold_Scalar(benchmark::State& state) { const std::string dir = ResourcesDir(); Image image = PNM::Read(dir + "2JohnC1V3.ppm", ParameterMap{ {"grayscale", GrayscaleAlgorithms::QT} }); Image binary(image.width, image.height); const Pixel8 threshold = 128; for (auto _ : state) { GlobalThresholdBenchHarness::ToBinary_STD(image.data, binary.data, image.size, threshold); benchmark::DoNotOptimize(binary.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_GlobalThreshold_Scalar); #if defined(DOXA_SIMD) static void BM_GlobalThreshold_SIMD(benchmark::State& state) { const std::string dir = ResourcesDir(); Image image = PNM::Read(dir + "2JohnC1V3.ppm", ParameterMap{ {"grayscale", GrayscaleAlgorithms::QT} }); Image binary(image.width, image.height); const Pixel8 threshold = 128; for (auto _ : state) { GlobalThresholdBenchHarness::ToBinary_SIMD(image.data, binary.data, image.size, threshold); benchmark::DoNotOptimize(binary.data); } state.SetBytesProcessed(int64_t(state.iterations()) * image.size); } BENCHMARK(BM_GlobalThreshold_SIMD); #endif // DOXA_SIMD } // namespace Doxa::Benchmarks ================================================ FILE: Doxa.Bench/config.hpp.in ================================================ #pragma once #define DOXA_BENCH_RESOURCES_DIR "@DOXA_RESOURCES_DIR@" ================================================ FILE: Doxa.Bench/pch.h ================================================ // // pch.h // #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include ================================================ FILE: Doxa.Test/AlgorithmTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" namespace Doxa::UnitTests { class AlgorithmTests : public ::testing::Test { protected: const Pixel8 input[4] = { Palette::Black, Palette::White, Palette::White, Palette::Black }; const Pixel8 expected[4] = { Palette::Black, Palette::Black, Palette::Black, Palette::Black }; }; // A dummy binarization algorithm that turns everything black class BinarizationTestharness : public Algorithm { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // NOTE: Linker unable to find Palette:Black on Linux with Clang and G++ std::fill_n(binaryImageOut.data, binaryImageOut.size, 0/* Palette::Black */); } }; TEST_F(AlgorithmTests, AlgorithmToBinaryTest) { // Obtain 2x2 Gray Scale Image const Image grayScaleImageIn(2, 2, input); // Initialize the memory for our 2x2 Binary Image Image binaryImageOut(grayScaleImageIn.width, grayScaleImageIn.height); // Execute our method under test BinarizationTestharness algorithm; algorithm.Initialize(grayScaleImageIn); algorithm.ToBinary(binaryImageOut); // Assert correctness TestUtilities::AssertImageData(binaryImageOut, expected); } TEST_F(AlgorithmTests, AlgorithmToBinaryImageTest) { // Obtain 2x2 Gray Scale Image const Image grayScaleImageIn(2, 2, input); // Execute our method under test Image binaryImageOut = BinarizationTestharness::ToBinaryImage(grayScaleImageIn, Parameters()); // Assert correctness TestUtilities::AssertImageData(binaryImageOut, expected); } TEST_F(AlgorithmTests, AlgorithmUpdateToBinaryTest) { // Obtain 2x2 Gray Scale Image Image image(2, 2, input); // Execute our method under test BinarizationTestharness::UpdateToBinary(image); // Assert correctness TestUtilities::AssertImageData(image, expected); } } ================================================ FILE: Doxa.Test/BatainehTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" namespace Doxa::UnitTests { // Exposes protected members for Unit Testing class BatainehTestharness : public Bataineh { public: BatainehTestharness() : Bataineh() {} using Bataineh::GetMaxGrayValue; using Bataineh::ConfusionThreshold; using Bataineh::RedBlack; using Bataineh::PrimaryWindow; using Bataineh::GetWindows; using Bataineh::SigmaMinMaxAndMean; using Bataineh::SigmaAdaptive; using Bataineh::WindowThreshold; using Bataineh::CalculateMeanStdDev; using Bataineh::DetailedWindow; /// /// A method that draws the bottom, and right side of every window onto the image. /// /// An Image with window outlines Image GenerateWindowImage(const Image& image, const std::vector& windows) { Image windowImage(image); // Make a deep copy for (auto it = windows.begin(); it != windows.end(); ++it) { // Draw bottom horizontal bar for (int x = it->window.upperLeft.x; x < it->window.bottomRight.x; ++x) { windowImage.Pixel(x, it->window.bottomRight.y) = 75; } // Draw right vertical bar for (int y = it->window.upperLeft.y; y < it->window.bottomRight.y; ++y) { windowImage.Pixel(it->window.bottomRight.x, y) = 75; } } return windowImage; } }; TEST(BatainehTests, BatainehToBinaryTest) { // Note: This algorithm, nor the numbers asserted, may be correct. // This test is mainly used, for now, to analyze the characteristics of an image // If you just want image details - turn off the assertions. bool enableAssertions = true; SUCCEED() << "Bataineh Algorithm Analysis"; // Setup const std::string filePath = TestUtilities::ProjectFolder() + "2JohnC1V3.ppm"; const Image grayScaleImage = PNM::Read(filePath, ParameterMap{ {"grayscale", GrayscaleAlgorithms::BT601} }); BatainehTestharness bataineh; bataineh.Initialize(grayScaleImage); // Get global std-dev and mean values double sigmaGlobal; double meanGlobal; bataineh.CalculateMeanStdDev(meanGlobal, sigmaGlobal, Region(grayScaleImage.width, grayScaleImage.height)); if (enableAssertions) EXPECT_NEAR(31.6197, sigmaGlobal, 0.001); if (enableAssertions) EXPECT_NEAR(186.3858, meanGlobal, 0.001); SUCCEED() << "Mg = " << meanGlobal << ", Sg = " << sigmaGlobal; // Get Max Gray Value const Pixel8 maxGrayValue = bataineh.GetMaxGrayValue(); if (enableAssertions) EXPECT_EQ((Pixel8)222, maxGrayValue); SUCCEED() << "MAXlevel = " << maxGrayValue; // Calculate Confusion Threshold const double confThreshold = bataineh.ConfusionThreshold(meanGlobal, sigmaGlobal, maxGrayValue); if (enableAssertions) EXPECT_NEAR(151.0563, confThreshold, 0.001); SUCCEED() << "Tc = " << confThreshold; // Find total Red and Black pixels Image rbwImage(grayScaleImage.width, grayScaleImage.height); int redCountImage; int blackCountImage; bataineh.RedBlack(redCountImage, blackCountImage, rbwImage, confThreshold, sigmaGlobal); if (enableAssertions) EXPECT_EQ(19513, redCountImage); if (enableAssertions) EXPECT_EQ(34320, blackCountImage); SUCCEED() << "REDg = " << redCountImage << ", BLACKg = " << blackCountImage; // Create a Red Black White image for analysis PNM::Write(rbwImage, TestUtilities::ProjectFolder() + "2JohnC1V3-Bataineh-RBW.pgm"); // Calculate P Value - helps determine window size double p = (double)blackCountImage / redCountImage; SUCCEED() << "p = " << p; // Calculate the Primary Window size int primaryWindowWidth; int primaryWindowHeight; bataineh.PrimaryWindow(primaryWindowWidth, primaryWindowHeight, p, sigmaGlobal, maxGrayValue, rbwImage.width, rbwImage.height ); if (enableAssertions) EXPECT_EQ(23, primaryWindowWidth); if (enableAssertions) EXPECT_EQ(22, primaryWindowHeight); SUCCEED() << "PWw = " << primaryWindowWidth << ", PWh = " << primaryWindowHeight; // Break the image into Primary and Secondary windows std::vector windows = bataineh.GetWindows(rbwImage, blackCountImage, redCountImage, sigmaGlobal, maxGrayValue); if (enableAssertions) EXPECT_EQ((size_t)1121, windows.size()); SUCCEED() << "PW & SW Count = " << windows.size(); // Create a window breakdown image for analysis Image windowImage = bataineh.GenerateWindowImage(rbwImage, windows); PNM::Write(windowImage, TestUtilities::ProjectFolder() + "2JohnC1V3-Bataineh-Windows.pgm"); // Get Sigma Max and Min as well as local window Sigma and Mean double sigmaMax; double sigmaMin; bataineh.SigmaMinMaxAndMean(sigmaMin, sigmaMax, windows); // Note: Values calculated with Population Variance if (enableAssertions) EXPECT_NEAR(0.7890, sigmaMin, 0.001); if (enableAssertions) EXPECT_NEAR(52.6966, sigmaMax, 0.001); SUCCEED() << "Smin = " << sigmaMin << ", Smax = " << sigmaMax; // Get a target Window and analyze it SUCCEED() << "First Window Details:"; BatainehTestharness::DetailedWindow detailedWindow = windows.front(); // First Window if (enableAssertions) EXPECT_NEAR(185.8893, detailedWindow.mean, 0.001); if (enableAssertions) EXPECT_NEAR(26.9451, detailedWindow.stddev, 0.001); SUCCEED() << "Mw = " << detailedWindow.mean << ", Sw = " << detailedWindow.stddev; if (enableAssertions) EXPECT_EQ(23, detailedWindow.window.Width()); if (enableAssertions) EXPECT_EQ(22, detailedWindow.window.Height()); SUCCEED() << "Ww = " << detailedWindow.window.Width() << ", Wh = " << detailedWindow.window.Height(); const double sigmaAdaptive = bataineh.SigmaAdaptive(detailedWindow.stddev, sigmaMin, sigmaMax, maxGrayValue); if (enableAssertions) EXPECT_NEAR(111.865, sigmaAdaptive, 0.001); SUCCEED() << "Sadaptive = " << sigmaAdaptive; const Pixel8 threshold = bataineh.WindowThreshold(detailedWindow.mean, meanGlobal, detailedWindow.stddev, sigmaAdaptive); if (enableAssertions) EXPECT_EQ((Pixel8)168, threshold); SUCCEED() << "Tw = " << threshold; } } ================================================ FILE: Doxa.Test/BinarizationTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" #include "ImageFixture.hpp" namespace Doxa::UnitTests { class BinarizationTests : public ImageFixture {}; TEST_F(BinarizationTests, BinarizationSauvolaTest) { const Parameters parameters({ {"window", 27}, {"k", 0.10} }); Image imageSauvola = Sauvola::ToBinaryImage(image, parameters); Image imageSauvola2(image); Sauvola::UpdateToBinary(imageSauvola2, parameters); TestUtilities::AssertImages(imageSauvola, imageSauvola2); TestUtilities::AssertImageFile(imageSauvola, projFolder + "2JohnC1V3-Sauvola.pbm"); } TEST_F(BinarizationTests, BinarizationNiblackTest) { const Parameters parameters({ { "window", 223 }, { "k", -0.61 } }); Image imageNiblack = Niblack::ToBinaryImage(image, parameters); Image imageNiblack2(image); Niblack::UpdateToBinary(imageNiblack2, parameters); TestUtilities::AssertImages(imageNiblack, imageNiblack2); TestUtilities::AssertImageFile(imageNiblack, projFolder + "2JohnC1V3-Niblack.pbm"); } TEST_F(BinarizationTests, BinarizationWolfTest) { const Parameters parameters({ { "window", 21 }, { "k", 0.18 } }); Image imageWolf = Wolf::ToBinaryImage(image, parameters); Image imageWolf2(image); Wolf::UpdateToBinary(imageWolf2, parameters); TestUtilities::AssertImages(imageWolf, imageWolf2); TestUtilities::AssertImageFile(imageWolf, projFolder + "2JohnC1V3-Wolf.pbm"); } TEST_F(BinarizationTests, BinarizationNICKTest) { const Parameters parameters({ { "window", 45 }, { "k", -0.10 } }); Image imageNICK = Nick::ToBinaryImage(image, parameters); Image imageNICK2(image); Nick::UpdateToBinary(imageNICK2, parameters); TestUtilities::AssertImages(imageNICK, imageNICK2); TestUtilities::AssertImageFile(imageNICK, projFolder + "2JohnC1V3-NICK.pbm"); } TEST_F(BinarizationTests, BinarizationBernsenTest) { const Parameters parameters({ { "window", 61 }, { "threshold", 150 }, {"contrast-limit", 25} }); Image imageBernsen = Bernsen::ToBinaryImage(image, parameters); Image imageBernsen2(image); Bernsen::UpdateToBinary(imageBernsen2, parameters); TestUtilities::AssertImages(imageBernsen, imageBernsen2); TestUtilities::AssertImageFile(imageBernsen, projFolder + "2JohnC1V3-Bensen.pbm"); } TEST_F(BinarizationTests, BinarizationTRSinghTest) { const Parameters parameters({ { "window", 27 }, { "k", 0.1 } }); Image imageTRSingh = TRSingh::ToBinaryImage(image, parameters); Image imageTRSingh2(image); TRSingh::UpdateToBinary(imageTRSingh2, parameters); TestUtilities::AssertImages(imageTRSingh, imageTRSingh2); TestUtilities::AssertImageFile(imageTRSingh, projFolder + "2JohnC1V3-TRSingh.pbm"); } TEST_F(BinarizationTests, BinarizationWANTest) { const Parameters parameters({ { "window", 75 }, { "k", 0.2 } }); Image imageWAN = Wan::ToBinaryImage(image, parameters); Image imageWAN2(image); Wan::UpdateToBinary(imageWAN2, parameters); TestUtilities::AssertImages(imageWAN, imageWAN2); TestUtilities::AssertImageFile(imageWAN, projFolder + "2JohnC1V3-WAN.pbm"); } TEST_F(BinarizationTests, BinarizationGatosTest) { const Parameters parameters({ { "window", 75 }, { "k", 0.2 } }); Image imageGatos = Gatos::ToBinaryImage(image, parameters); Image imageGatos2(image); Gatos::UpdateToBinary(imageGatos2, parameters); TestUtilities::AssertImages(imageGatos, imageGatos2); TestUtilities::AssertImageFile(imageGatos, projFolder + "2JohnC1V3-Gatos.pbm"); } TEST_F(BinarizationTests, BinarizationSuTest) { Image imageSu = Su::ToBinaryImage(image); // Ensure the auto parameters are understood Image contrastImage(image.width, image.height); ContrastImage::GenerateContrastImage(contrastImage, image); std::cout << "Stroke: " << ContrastImage::EstimateStrokeWidth(image) << std::endl; Image imageSu2(image); Su::UpdateToBinary(imageSu2); TestUtilities::AssertImages(imageSu, imageSu2); TestUtilities::AssertImageFile(imageSu, projFolder + "2JohnC1V3-Su.pbm"); } TEST_F(BinarizationTests, BinarizationISauvola) { const Parameters parameters({ {"window", 15}, {"k", 0.01} }); Image imageISauvola = ISauvola::ToBinaryImage(image, parameters); Image imageISauvola2(image); ISauvola::UpdateToBinary(imageISauvola2, parameters); TestUtilities::AssertImages(imageISauvola, imageISauvola2); TestUtilities::AssertImageFile(imageISauvola, projFolder + "2JohnC1V3-ISauvola.pbm"); } TEST_F(BinarizationTests, BinarizationOtsuTest) { Image imageOtsu = Otsu::ToBinaryImage(image); Image imageOtsu2(image); Otsu::UpdateToBinary(imageOtsu2); TestUtilities::AssertImages(imageOtsu, imageOtsu2); TestUtilities::AssertImageFile(imageOtsu, projFolder + "2JohnC1V3-Otsu.pbm"); } TEST_F(BinarizationTests, BinarizationAdOtsuTest) { // AdOtsu Parameters param({ {"distance", 0} }); // Disable Grid Optimization Image imageAdOtsu = AdOtsu::ToBinaryImage(image, param); TestUtilities::AssertImageFile(imageAdOtsu, projFolder + "2JohnC1V3-AdOtsu.pbm"); } TEST_F(BinarizationTests, BinarizationAdOtsuGTest) { // AdOtsu /w Grid Optimization Image imageAdOtsuG = AdOtsu::ToBinaryImage(image); Image imageAdOtsuG2(image); AdOtsu::UpdateToBinary(imageAdOtsuG2); TestUtilities::AssertImages(imageAdOtsuG, imageAdOtsuG2); TestUtilities::AssertImageFile(imageAdOtsuG, projFolder + "2JohnC1V3-AdOtsuG.pbm"); } TEST_F(BinarizationTests, BinarizationAdOtsuMSTest) { // AdOtsu /w Multi-Scale Parameters param({ {"distance", 0} }); // Disable Grid Optimization Image imageAdOtsuMS = AdOtsuMS::ToBinaryImage(image, param); TestUtilities::AssertImageFile(imageAdOtsuMS, projFolder + "2JohnC1V3-AdOtsuMS.pbm"); } TEST_F(BinarizationTests, BinarizationAdOtsuMSGTest) { // AdOtsu /w Multi-Scale Grid Image imageAdOtsuMSG = AdOtsuMS::ToBinaryImage(image); TestUtilities::AssertImageFile(imageAdOtsuMSG, projFolder + "2JohnC1V3-AdOtsuMSG.pbm"); } TEST_F(BinarizationTests, BinarizationPhansalkarTest) { const Parameters parameters({ {"window", 27}, {"k", 0.10} }); Image imagePhansalkar = Phansalkar::ToBinaryImage(image, parameters); Image imagePhansalkar2(image); Phansalkar::UpdateToBinary(imagePhansalkar2, parameters); TestUtilities::AssertImages(imagePhansalkar, imagePhansalkar2); TestUtilities::AssertImageFile(imagePhansalkar, projFolder + "2JohnC1V3-Phansalkar.pbm"); } TEST_F(BinarizationTests, BinarizationBatainehTest) { Image imageBataineh = Bataineh::ToBinaryImage(image); Image imageBataineh2(image); Bataineh::UpdateToBinary(imageBataineh2); TestUtilities::AssertImages(imageBataineh, imageBataineh2); TestUtilities::AssertImageFile(imageBataineh, projFolder + "2JohnC1V3-Bataineh.pbm"); } } ================================================ FILE: Doxa.Test/CMakeLists.txt ================================================ message(STATUS "Doxa Test - CMake Build") cmake_minimum_required(VERSION 3.16) project(doxa_test) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # SIMD Options option(DOXA_ENABLE_SIMD "Enable SIMD optimizations (compile all paths, runtime detection)" ON) if(MSVC) # so far so good else() add_compile_options("-Wno-narrowing") endif() include(FetchContent) FetchContent_Declare( googletest URL https://github.com/google/googletest/archive/refs/tags/v1.16.0.zip ) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) enable_testing() include_directories(../Doxa) add_executable( doxa_test AlgorithmTests.cpp BatainehTests.cpp BinarizationTests.cpp CalculatorTests.cpp ContrastImageTests.cpp GrayscaleTests.cpp ImageTests.cpp ISauvolaTests.cpp LocalWindowTests.cpp MorphologyTests.cpp PaletteTests.cpp ParametersTests.cpp ClassifiedPerformanceTests.cpp DRDMTests.cpp PNMTests.cpp DIBCOUtilsTests.cpp RegionTests.cpp SIMDTests.cpp SuTests.cpp WienerFilterTests.cpp ) # Platform detection and SIMD compiler flags if(DOXA_ENABLE_SIMD) message(STATUS "SIMD optimizations: ENABLED (runtime detection)") # Detect platform and add appropriate flags if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|x64") # x86-64: SSE2 is always available (baseline for x64) if(MSVC) # MSVC x64: SSE2 is enabled by default, no special flags needed message(STATUS "SIMD: x86-64 with SSE2 (MSVC)") else() # GCC/Clang: SSE2 is default for x64, but be explicit target_compile_options(doxa_test PRIVATE -msse2) message(STATUS "SIMD: x86-64 with SSE2 (GCC/Clang)") endif() elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") # ARM64: NEON is always available, no special flags needed on GCC/Clang # MSVC ARM64: NEON is enabled by default message(STATUS "SIMD: ARM64 with NEON") elseif(EMSCRIPTEN) # WASM: Add SIMD128 flag target_compile_options(doxa_test PRIVATE -msimd128) message(STATUS "SIMD: WebAssembly with SIMD128") else() message(STATUS "SIMD: Unknown platform, will use scalar fallback") endif() else() message(STATUS "SIMD optimizations: DISABLED (scalar only)") endif() target_precompile_headers( doxa_test PUBLIC pch.h ) target_link_libraries( doxa_test GTest::gtest_main ) include(GoogleTest) gtest_discover_tests( doxa_test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) message(STATUS "Compiler: ${CMAKE_CXX_COMPILER} ${CMAKE_CXX_COMPILER_VERSION}") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") message(STATUS "System processor: ${CMAKE_SYSTEM_PROCESSOR}") ================================================ FILE: Doxa.Test/CalculatorTests.cpp ================================================ #include "pch.h" #include "ImageFixture.hpp" #include "TestUtilities.hpp" namespace Doxa::UnitTests { class CalculatorTests : public ImageFixture {}; // A sample algorithm that will allow us to switch out the calculator seemlessly // The library does not use this pattern because there is no reason to ever use Integral Images. template class NiblackBase : public Algorithm>, public Calculator { public: void ToBinary(Image& binaryImageOut, const Parameters& parameters = Parameters()) { // Read parameters, utilizing defaults const int windowSize = parameters.Get("window", 75); const double k = parameters.Get("k", 0.2); Calculator::Process(binaryImageOut, Algorithm>::grayScaleImageIn, windowSize, [&](const double& mean, const double& variance, const int&) { const double stddev = std::sqrt(variance); return (mean + (k * stddev)); }); } }; TEST_F(CalculatorTests, CalculateIntegralImagesTest) { // Setup const Pixel8 bits[] = { Grayscale::Qt(10, 20, 30), Grayscale::Qt(40, 50, 60), Grayscale::Qt(70, 80, 90), Grayscale::Qt(30, 40, 50), Grayscale::Qt(50, 05, 05), Grayscale::Qt(50, 30, 10), Grayscale::Qt(03, 05, 07), Grayscale::Qt(11, 13, 17), Grayscale::Qt(00, 25, 12) }; Image image(3, 3, bits); const IntegralImage integralImage = { 18, 66, 144, 56, 124, 235, 60, 140, 265 }; const IntegralImage integralSquareImage = { 324, 2628, 8712, 1768, 4472, 11645, 1784, 4632, 12001 }; // Test Integral and Squared Integral Image Creation IntegralImageMeanVarianceCalc meanVarianceCalculator; IntegralImage testIntegralImage(integralImage.size()); IntegralImage testIntegralSquareImage(integralSquareImage); meanVarianceCalculator.BuildIntegralImages(testIntegralImage,testIntegralSquareImage, image); // Assert Single and Squared Integral Image Creation EXPECT_TRUE(testIntegralImage == integralImage); EXPECT_TRUE(testIntegralSquareImage == integralSquareImage); } TEST_F(CalculatorTests, CalculateProcessMeanVarianceTest) { typedef std::vector< std::tuple > meanVarianceVector; // Setup Pixel8 bits[] = { Grayscale::Qt(10, 20, 30), Grayscale::Qt(40, 50, 60), Grayscale::Qt(70, 80, 90), Grayscale::Qt(30, 40, 50), Grayscale::Qt(50, 05, 05), Grayscale::Qt(50, 30, 10), Grayscale::Qt(03, 05, 07), Grayscale::Qt(11, 13, 17), Grayscale::Qt(00, 25, 12) }; Image image(3, 3, bits); Image output(3, 3); // Output variables meanVarianceVector meanVarianceII; meanVarianceVector meanChan; meanVarianceVector meanVarianceChan; // Integral Image Mean Variance IntegralImageMeanVarianceCalc meanVarianceCalculator; meanVarianceCalculator.Process(output, image, 3, [&](const double& mean, const double& variance, const int&) { meanVarianceII.push_back({ mean, variance }); return 0.0; }); // Chan Mean Variance ChanMeanVarianceCalc meanVarianceCalculatorChan; meanVarianceCalculatorChan.Process(output, image, 3, [&](const double& mean, const double& variance, const int&) { meanVarianceChan.push_back({ mean, variance }); return 0.0; }); // Chan Mean ChanMeanCalc meanCalculatorChan; meanCalculatorChan.Process(output, image, 3, [&](const double& mean, const int&) { meanChan.push_back({ mean, 0.0 }); return 0.0; }); // Assert EXPECT_NEAR(std::get<0>(meanVarianceII.at(4)), 29.44, 0.01); // Note: If you use Sample Variance the value will be 524.77. We are using Population Variance. //EXPECT_NEAR(std::get<1>(meanVarianceII.at(4)), 524.77, 0.01); EXPECT_NEAR(std::get<1>(meanVarianceII.at(4)), 466.469, 0.01); EXPECT_TRUE(meanVarianceII == meanVarianceChan); for (int i = 0; i < image.size; ++i) { EXPECT_NEAR(std::get<0>(meanVarianceII.at(i)), std::get<0>(meanChan.at(i)), 0.01); } } TEST_F(CalculatorTests, CalculatorAlgorithmTest) { const Parameters parameters({ { "window", 223 }, { "k", -0.61 } }); Image imageNiblackChan = NiblackBase::ToBinaryImage(image, parameters); Image imageNiblackII = NiblackBase::ToBinaryImage(image, parameters); TestUtilities::AssertImagesWithDetails(imageNiblackChan, imageNiblackII); } } ================================================ FILE: Doxa.Test/ClassifiedPerformanceTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" namespace Doxa::UnitTests { TEST(ClassifiedPerformanceTests, PerformanceClassificationsTest) { ClassifiedPerformance::Classifications classification; classification.truePositive = 3; classification.trueNegative = 5; classification.falsePositive = 7; classification.falseNegative = 11; EXPECT_EQ(classification.Total(), 26); classification.Clear(); EXPECT_EQ(classification.Total(), 0); } TEST(ClassifiedPerformanceTests, PerformanceTest) { Image control(3, 3); control.Pixel(0, 0) = Palette::Black; control.Pixel(1, 0) = Palette::White; control.Pixel(2, 0) = Palette::Black; control.Pixel(0, 1) = Palette::White; control.Pixel(1, 1) = Palette::Black; control.Pixel(2, 1) = Palette::White; control.Pixel(0, 2) = Palette::Black; control.Pixel(1, 2) = Palette::White; control.Pixel(2, 2) = Palette::Black; Image experiment(3, 3); experiment.Pixel(0, 0) = Palette::Black; experiment.Pixel(1, 0) = Palette::White; experiment.Pixel(2, 0) = Palette::White; // False Negative experiment.Pixel(0, 1) = Palette::White; experiment.Pixel(1, 1) = Palette::Black; experiment.Pixel(2, 1) = Palette::White; experiment.Pixel(0, 2) = Palette::Black; experiment.Pixel(1, 2) = Palette::Black; // False Positive experiment.Pixel(2, 2) = Palette::Black; // Compare ClassifiedPerformance::Classifications classifications; const bool ret = ClassifiedPerformance::CompareImages(classifications, control, experiment); EXPECT_TRUE(ret); // Assert correctness EXPECT_EQ(classifications.truePositive, 4); EXPECT_EQ(classifications.trueNegative, 3); EXPECT_EQ(classifications.falsePositive, 1); EXPECT_EQ(classifications.falseNegative, 1); // Calculate Performance EXPECT_NEAR(ClassifiedPerformance::CalculateAccuracy(classifications), 77.777, 0.001); EXPECT_NEAR(ClassifiedPerformance::CalculateFMeasure(classifications), 80.0, 0.001); EXPECT_NEAR(ClassifiedPerformance::CalculatePSNR(classifications), 6.532, 0.001); // TODO - Calculate the NRM and verify //EXPECT_EQ(ClassifiedPerformance::CalculateNRM(classifications), 0.00); } TEST(PerformanceTests, PseudoMetrics) { // NOTE: Based off of 2018 DIBCO Metrics PR sample ClassifiedPerformance::Classifications classification; /* std::string projFolder = TestUtilities::ProjectFolder(); auto precisionWeights = DIBCOUtils::ReadWeightsFile(projFolder + "PR_PWeights.dat"); auto recallWeights = DIBCOUtils::ReadWeightsFile(projFolder + "PR_RWeights.dat"); auto controlImage = PNM::Read(projFolder + "PR_GT.pbm"); auto experimentImage = PNM::Read(projFolder + "PR_bin.pbm"); ClassifiedPerformance::CompareImages(classification, controlImage, experimentImage, precisionWeights, recallWeights); */ classification.truePositive = 61573; classification.trueNegative = 251404; classification.falsePositive = 1929; classification.falseNegative = 6493; classification.wpTruePositive = 0.0; classification.wpFalsePositive = 644.86664099999996; classification.wrTruePositive = 11562.182811000408; classification.wrFalseNegative = 64.369443000000018; EXPECT_NEAR(ClassifiedPerformance::CalculatePseudoPrecision(classification), 95.9875, 0.0001); EXPECT_NEAR(ClassifiedPerformance::CalculatePseudoRecall(classification), 99.4464, 0.0001); EXPECT_NEAR(ClassifiedPerformance::CalculatePseudoFMeasure(classification), 97.6863, 0.0001); } TEST(PerformanceTests, PSNRBoundsTest) { ClassifiedPerformance::Classifications classification; classification.truePositive = 3; classification.trueNegative = 5; classification.falsePositive = 0; // Possible Divide By Zero classification.falseNegative = 0; // Possible Divide By Zero EXPECT_TRUE(ClassifiedPerformance::CalculatePSNR(classification) > 1000); } TEST(ClassifiedPerformanceTests, FMeasureBoundsTest) { ClassifiedPerformance::Classifications classification; classification.truePositive = 0; // Possible Divide By Zero classification.trueNegative = 5; classification.falsePositive = 7; classification.falseNegative = 11; EXPECT_EQ(ClassifiedPerformance::CalculateFMeasure(classification), 0.0); } TEST(ClassifiedPerformanceTests, NRMBoundsTest) { ClassifiedPerformance::Classifications classification; classification.truePositive = 0; // Possible Divide By Zero classification.trueNegative = 5; classification.falsePositive = 7; classification.falseNegative = 0; // Possible Divide By Zero EXPECT_TRUE(ClassifiedPerformance::CalculateNRM(classification) > 1000); } TEST(ClassifiedPerformanceTests, MCCTest) { // Numbers and expected value pulled from: https://en.wikipedia.org/wiki/Matthews_correlation_coefficient ClassifiedPerformance::Classifications classification; classification.truePositive = 6; classification.trueNegative = 3; classification.falsePositive = 1; classification.falseNegative = 2; EXPECT_NEAR(ClassifiedPerformance::CalculateMCC(classification), 0.478, 0.001); } TEST(ClassifiedPerformanceTests, ClassifiedPerformanceSauvola) { std::string projFolder = TestUtilities::ProjectFolder(); // Grayscale Image const std::string filePathBinary = projFolder + "2JohnC1V3-Sauvola.pbm"; Image binaryImage = PNM::Read(filePathBinary); // Ground Truth const std::string filePathGT = projFolder + "2JohnC1V3-GroundTruth.pbm"; Image groundTruthImage = PNM::Read(filePathGT); // Load Pseudo Weights const std::string filePathRWeights = projFolder + "2JohnC1V3-GroundTruth_RWeights.dat"; auto rWeights = DIBCOUtils::ReadWeightsFile(filePathRWeights); const std::string filePathPWeights = projFolder + "2JohnC1V3-GroundTruth_PWeights.dat"; auto pWeights = DIBCOUtils::ReadWeightsFile(filePathPWeights); // Run Classified Metrics ClassifiedPerformance::Classifications classifications; bool canCompare = ClassifiedPerformance::CompareImages(classifications, groundTruthImage, binaryImage, pWeights, rWeights); EXPECT_TRUE(canCompare); const double accuracy = ClassifiedPerformance::CalculateAccuracy(classifications); const double fm = ClassifiedPerformance::CalculateFMeasure(classifications); const double recall = ClassifiedPerformance::CalculateRecall(classifications); const double precision = ClassifiedPerformance::CalculatePrecision(classifications); const double pfm = ClassifiedPerformance::CalculatePseudoFMeasure(classifications); const double precall = ClassifiedPerformance::CalculatePseudoRecall(classifications); const double pprecision = ClassifiedPerformance::CalculatePseudoPrecision(classifications); const double mcc = ClassifiedPerformance::CalculateMCC(classifications); const double nrm = ClassifiedPerformance::CalculateNRM(classifications); const double psnr = ClassifiedPerformance::CalculatePSNR(classifications); // Test EXPECT_NEAR(accuracy, 97.671, 0.001); EXPECT_NEAR(fm, 93.204, 0.001); EXPECT_NEAR(recall, 91.3811, 0.001); EXPECT_NEAR(precision, 95.1025, 0.001); EXPECT_NEAR(pfm, 93.393, 0.001); EXPECT_NEAR(precall, 92.7954, 0.001); EXPECT_NEAR(pprecision, 93.9983, 0.001); EXPECT_NEAR(mcc, 0.918, 0.001); EXPECT_NEAR(nrm, 0.048, 0.001); EXPECT_NEAR(psnr, 16.329, 0.001); } } ================================================ FILE: Doxa.Test/ContrastImageTests.cpp ================================================ #include "pch.h" #include "ImageFixture.hpp" namespace Doxa::UnitTests { class ContrastImageTests : public ImageFixture {}; TEST_F(ContrastImageTests, GenerateContrastImageTest) { Image contrastImage(image.width, image.height); ContrastImage::GenerateContrastImage(contrastImage, image); PNM::Write(contrastImage, projFolder + "2JohnC1V3-ContrastImage.ppm"); } TEST_F(ContrastImageTests, EstimateStrokeWidthTest) { // Peaks spaced 5px apart constexpr int width = 30; constexpr int height = 4; Pixel8 contrastData[width * height]; std::memset(contrastData, 0, sizeof(contrastData)); for (int y = 0; y < height; ++y) { const int row = y * width; contrastData[row + 5] = 180; contrastData[row + 10] = 180; contrastData[row + 15] = 180; contrastData[row + 20] = 180; } Image contrastImage(width, height, contrastData); EXPECT_EQ(5, ContrastImage::EstimateStrokeWidth(contrastImage)); } TEST_F(ContrastImageTests, GenerateHighContrastImageTest) { Image highContrastImage(image.width, image.height); ContrastImage::GenerateHighContrastImage(highContrastImage, image); PNM::Write(highContrastImage, projFolder + "2JohnC1V3-HighContrastImage.pbm"); } } ================================================ FILE: Doxa.Test/DIBCOUtilsTests.cpp ================================================ #include "pch.h" #include #include #include "DIBCOUtils.hpp" namespace Doxa::UnitTests { TEST(DIBCOUtilsTests, ReadWeightsTest) { // Note: A sample copied from a DIBCO .dat file std::string weights = "0.000000 1.000000 0.750000 0.500000"; std::stringstream stream(weights); auto result = DIBCOUtils::ReadWeights(stream, 4); EXPECT_EQ(result.size(), 4); EXPECT_EQ(result.at(0), 0.0); EXPECT_EQ(result.at(1), 1.0); EXPECT_EQ(result.at(2), 0.75); EXPECT_EQ(result.at(3), 0.5); } } ================================================ FILE: Doxa.Test/DRDMTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" namespace Doxa::UnitTests { // Exposes protected members for Unit Testing class DRDMTestHarness : public Doxa::DRDM { public: using DRDM::SumDRDkForMismatchedPixels; using DRDM::NUBN; // NUBN may call any of these three routines using DRDM::NUBN_STD; using DRDM::NUBN_STD_8x8; #if defined(DOXA_SIMD) using DRDM::NUBN_SIMD_8x8; #endif }; TEST(DRDMTests, DRDMSauvolaTest) { // Setup std::string projFolder = TestUtilities::ProjectFolder(); const std::string filePathGT = projFolder + "2JohnC1V3-GroundTruth.pbm"; Image gtImage = PNM::Read(filePathGT); const std::string filePathSauvola = projFolder + "2JohnC1V3-Sauvola.pbm"; Image binImage = PNM::Read(filePathSauvola); // Run DRDM const double drdm = DRDMTestHarness::CalculateDRDM(gtImage, binImage); const uint64_t sumDRDk = DRDMTestHarness::SumDRDkForMismatchedPixels(gtImage, binImage); const int nubn = DRDMTestHarness::NUBN_STD(gtImage, 8); // DRDM - Value optained from the DIBCO perf tool EXPECT_NEAR(1.9519, drdm, 0.0001); EXPECT_EQ(4122339441, sumDRDk); EXPECT_EQ(2112, nubn); } TEST(DRDMTests, DRDMTest) { // (3, 4) = Black is changed to White // One 8x8 Block with a 5x5 Window Pixel8 dataGT[] = { Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, }; Image groundTruthImage(8, 8, dataGT); Pixel8 dataExp[] = { Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, }; Image expImage(8, 8, dataExp); double drdm = DRDM::CalculateDRDM(groundTruthImage, expImage); EXPECT_EQ((double)(72357 + 72357 + 32359) / 1000000, drdm); } TEST(DRDMTests, NUBNUniformityCountTest) { // This is a 24x24 image creating a 3x3 set of windows that are 8x8 Pixel8 data[] = { // ===== Window Row 1 (rows 0-7 of array) ===== // Window 1-1: All White | Window 1-2: All Black | Window 1-3: White with black center // Window row 0: Window 1-1 | Window 1-2 | Window 1-3 Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, // Window row 1: Window 1-1 | Window 1-2 | Window 1-3 Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, // Window row 2: Window 1-1 | Window 1-2 | Window 1-3 Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, // Window row 3: Window 1-1 | Window 1-2 | Window 1-3 (black center) Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, // Window row 4: Window 1-1 | Window 1-2 | Window 1-3 Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, // Window row 5: Window 1-1 | Window 1-2 | Window 1-3 Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, // Window row 6: Window 1-1 | Window 1-2 | Window 1-3 Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, // Window row 7: Window 1-1 | Window 1-2 | Window 1-3 Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, // ===== Window Row 2 (rows 8-15 of array) ===== // Window 2-1: Checkerboard | Window 2-2: All White | Window 2-3: All Black // Window row 0: Window 2-1 | Window 2-2 | Window 2-3 Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, // Window row 1: Window 2-1 | Window 2-2 | Window 2-3 Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, // Window row 2: Window 2-1 | Window 2-2 | Window 2-3 Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, // Window row 3: Window 2-1 | Window 2-2 | Window 2-3 Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, // Window row 4: Window 2-1 | Window 2-2 | Window 2-3 Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, // Window row 5: Window 2-1 | Window 2-2 | Window 2-3 Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, // Window row 6: Window 2-1 | Window 2-2 | Window 2-3 Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, // Window row 7: Window 2-1 | Window 2-2 | Window 2-3 Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, // ===== Window Row 3 (rows 16-23 of array) ===== // Window 3-1: Black with white center | Window 3-2: White with black border | Window 3-3: Black with white border // Window row 0: Window 3-1 | Window 3-2 (top border) | Window 3-3 (top border) Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, // Window row 1: Window 3-1 | Window 3-2 | Window 3-3 Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, // Window row 2: Window 3-1 | Window 3-2 | Window 3-3 Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, // Window row 3: Window 3-1 (white center) | Window 3-2 | Window 3-3 Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, // Window row 4: Window 3-1 | Window 3-2 | Window 3-3 Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, // Window row 5: Window 3-1 | Window 3-2 | Window 3-3 Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, // Window row 6: Window 3-1 | Window 3-2 | Window 3-3 Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, // Window row 7: Window 3-1 | Window 3-2 (bottom border) | Window 3-3 (bottom border) Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, }; // This image contains 4 fully black or white windows // and 5 mixed windows Image binaryImage(24, 24, data); const unsigned int countStd = DRDMTestHarness::NUBN_STD(binaryImage, 8); const unsigned int countStd8x8 = DRDMTestHarness::NUBN_STD_8x8(binaryImage); EXPECT_EQ(countStd, 5); EXPECT_EQ(countStd8x8, 5); #if defined(DOXA_SIMD) const unsigned int countSimd8x8 = DRDMTestHarness::NUBN_SIMD_8x8(binaryImage); EXPECT_EQ(countSimd8x8, 5); #endif } TEST(DRDMTests, NUBNVariousWindowSizesTest) { // This a 15x15 image with window sizes of 8x8, 7x7, and 25x25 Image binaryImage(24, 24); binaryImage.Fill(Palette::White); binaryImage.Pixel(7, 3) = Palette::Black; binaryImage.Pixel(15, 3) = Palette::Black; binaryImage.Pixel(23, 3) = Palette::Black; const unsigned int count1 = DRDMTestHarness::NUBN(binaryImage, 8); // Try an smaller window size of 7 // We do not process partial windows, so 7x7 should only see 2 const unsigned int count2 = DRDMTestHarness::NUBN(binaryImage, 7); // Lets make our window size larger than the image // Since we don't do partial windows, this is an interesting edge case const unsigned int count3 = DRDMTestHarness::NUBN(binaryImage, 25); EXPECT_EQ(count1, 3); // Window 8x8 EXPECT_EQ(count2, 2); // Window 7x7 EXPECT_EQ(count3, 0); // Window 25x25 } TEST(DRDMTests, NUBNImplementationConsistencyTest) { std::string projFolder = TestUtilities::ProjectFolder(); const std::string filePathGT = projFolder + "2JohnC1V3-GroundTruth.pbm"; Image binaryImage = PNM::Read(filePathGT); const unsigned int countSTD = DRDMTestHarness::NUBN_STD(binaryImage, 8); const unsigned int countSTD8x8 = DRDMTestHarness::NUBN_STD_8x8(binaryImage); EXPECT_EQ(countSTD, countSTD8x8); #if defined(DOXA_SIMD) const unsigned int countSimd8x8 = DRDMTestHarness::NUBN_SIMD_8x8(binaryImage); EXPECT_EQ(countSTD8x8, countSimd8x8); #endif } } ================================================ FILE: Doxa.Test/Doxa.Test.vcxproj ================================================  Debug x64 Release x64 {5723f19a-a7ab-45d0-a78c-b04fe035865f} Win32Proj 10.0.22621.0 Application v143 Unicode Create Create Use pch.h Disabled X64;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) EnableFastChecks MultiThreadedDebugDLL Level3 stdcpp17 StreamingSIMDExtensions2 true Console Use pch.h X64;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) MultiThreadedDLL Level3 ProgramDatabase stdcpp17 StreamingSIMDExtensions2 true Console true true This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. ================================================ FILE: Doxa.Test/Doxa.Test.vcxproj.filters ================================================ ================================================ FILE: Doxa.Test/GrayscaleTests.cpp ================================================ #include "pch.h" namespace Doxa::UnitTests { TEST(GrayscaleTests, GrayscaleAlgorithmsTest) { // Mean, Value, and Luster contain obvious formulas. EXPECT_EQ(100, Grayscale::Mean(100, 30, 170)); EXPECT_EQ(200, Grayscale::Value(50, 100, 200)); EXPECT_EQ(200, Grayscale::Value(50, 200, 100)); EXPECT_EQ(200, Grayscale::Value(200, 100, 50)); EXPECT_EQ(125, Grayscale::Luster(50, 100, 200)); EXPECT_EQ(150, Grayscale::MinAvg(100, 200, 300)); // Assert that all weights sum to exactly 1 EXPECT_NEAR(1.0, Grayscale::BT601(1.0, 1.0, 1.0), 0.001); EXPECT_NEAR(1.0, Grayscale::BT709(1.0, 1.0, 1.0), 0.001); EXPECT_NEAR(1.0, Grayscale::BT2100(1.0, 1.0, 1.0), 0.001); } TEST(GrayscaleTests, GrayscaleColorSpaceTest) { const Pixel8 red = 225; const Pixel8 green = 128; const Pixel8 blue = 15; // Non-Linear to Linear /w LUT const auto lut = Grayscale::LinearLUT(); const auto redLin = lut[red]; const auto greenLin = lut[green]; const auto blueLin = lut[blue]; // Validate LUT is correct EXPECT_NEAR(0.753, redLin, 0.001); EXPECT_NEAR(0.216, greenLin, 0.001); EXPECT_NEAR(0.005, blueLin, 0.001); const auto [X, Y, Z] = Grayscale::RGBToXYZ(redLin, greenLin, blueLin); EXPECT_NEAR(X, .3885, 0.0002); EXPECT_NEAR(Y, .3148, 0.0002); EXPECT_NEAR(Z, .0448, 0.0002); const auto [L, a, b] = Grayscale::XYZToLab(X, Y, Z); EXPECT_NEAR(L, 62.91, 0.01); EXPECT_NEAR(a, 30.95, 0.01); EXPECT_NEAR(b, 67.00, 0.01); } TEST(GrayscaleTests, GrayscaleLinearLightnessTest) { // 3 pixels: flanking values bracket the test pixel to enable normalization const uint8_t input[] = { 64, 32, 128, // pixel 0 225, 128, 15, // pixel 1 - test pixel 200, 160, 80 // pixel 2 }; Pixel8 output[3] = {}; Grayscale::ToGrayscale(output, input, 3, 1, 3, GrayscaleAlgorithms::LIGHTNESS); EXPECT_EQ(160, output[1]); // TODO: replace with correct expected value } TEST(GrayscaleTests, GrayscaleLinearLABDistTest) { // 3 pixels: flanking values bracket the test pixel to enable normalization const uint8_t input[] = { 64, 32, 128, // pixel 0 200, 160, 80, // pixel 1 - test pixel 186, 128, 15 // pixel 2 }; Pixel8 output[3] = {}; Grayscale::ToGrayscale(output, input, 3, 1, 3, GrayscaleAlgorithms::LABDIST); EXPECT_EQ(218, output[1]); // TODO: replace with correct expected value } } ================================================ FILE: Doxa.Test/GridCalcTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" namespace Doxa::UnitTests { TEST(GridCalcTests, GridCalcIterateInterwovenTest) { // Setup Image image(7, 3); GridCalc grid = GridCalc(); // Expected values const int ar1[8][5] = { { 0, 0, 0, 2, 2}, { 2, 0, 0, 4, 2}, { 4, 2, 0, 6, 2}, { 6, 4, 0, 6, 2}, { 14, 0, 0, 2, 2}, { 16, 0, 0, 4, 2}, { 18, 2, 0, 6, 2}, { 20, 4, 0, 6, 2} }; int inc = 0; // Function under test grid.Iterate(image, 5, 2, [&](const Region& window, const int& position) { const auto exp = ar1[inc]; EXPECT_EQ(exp[0], position); EXPECT_EQ(exp[1], window.upperLeft.x); EXPECT_EQ(exp[2], window.upperLeft.y); EXPECT_EQ(exp[3], window.bottomRight.x); EXPECT_EQ(exp[4], window.bottomRight.y); inc++; }); EXPECT_EQ(8, inc); } TEST(GridCalcTests, GridCalcIterateUnevenTest) { // Setup Image image(7, 7); GridCalc grid = GridCalc(); // Expected values const int ar1[9][5] = { { 0, 0, 0, 2, 2}, { 5, 3, 0, 6, 2}, { 6, 4, 0, 6, 2}, { 35, 0, 3, 2, 6}, { 40, 3, 3, 6, 6}, { 41, 4, 3, 6, 6}, { 42, 0, 4, 2, 6}, { 47, 3, 4, 6, 6}, { 48, 4, 4, 6, 6} }; int inc = 0; // Function under test grid.Iterate(image, 5, 5, [&](const Region& window, const int& position) { const auto exp = ar1[inc]; EXPECT_EQ(exp[0], position); EXPECT_EQ(exp[1], window.upperLeft.x); EXPECT_EQ(exp[2], window.upperLeft.y); EXPECT_EQ(exp[3], window.bottomRight.x); EXPECT_EQ(exp[4], window.bottomRight.y); inc++; }); EXPECT_EQ(9, inc); } TEST(GridCalcTests, GridCalcIterateEvenTest) { // Setup Image image(6, 6); GridCalc grid = GridCalc(); // Expected values const int ar1[4][5] = { { 0, 0, 0, 2, 2}, { 5, 3, 0, 5, 2}, { 30, 0, 3, 2, 5}, { 35, 3, 3, 5, 5} }; int inc = 0; // Function under test grid.Iterate(image, 5, 5, [&](const Region& window, const int& position) { const auto exp = ar1[inc]; EXPECT_EQ(exp[0], position); EXPECT_EQ(exp[1], window.upperLeft.x); EXPECT_EQ(exp[2], window.upperLeft.y); EXPECT_EQ(exp[3], window.bottomRight.x); EXPECT_EQ(exp[4], window.bottomRight.y); inc++; }); EXPECT_EQ(4, inc); } TEST(GridCalcTests, GridCalcInterpolateTest) { // Setup const Pixel8 bits[] = { 100, 0, 0, 80, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 0, 0, 70, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 80, 0, 0, 60 }; Image image(7, 7, bits); GridCalc grid = GridCalc(); grid.Interpolate(image, 3, [&](const int& position, const int& threshold) { image.data[position] = threshold; }); /* Directly taken from Moghaddam and Hedjam's interpolation_a_grid_on_domain Matlab routine, with a simple 3x3 dataset, expanded to 7x7. We store integers between 0-255, not decimals. 100.0000 93.3333 86.6667 80.0000 73.3333 66.6667 60.0000 96.6667 90.0000 83.3333 76.6667 70.0000 63.3333 56.6667 93.3333 86.6667 80.0000 73.3333 66.6667 60.0000 53.3333 90.0000 83.3333 76.6667 70.0000 63.3333 56.6667 50.0000 93.3333 86.6667 80.0000 73.3333 66.6667 60.0000 53.3333 96.6667 90.0000 83.3333 76.6667 70.0000 63.3333 56.6667 100.0000 93.3333 86.6667 80.0000 73.3333 66.6667 60.0000 */ const Pixel8 expectedBits[] = { 100, 93, 87, 80, 73, 67, 60, 97, 90, 83, 77, 70, 63, 57, 93, 87, 80, 73, 67, 60, 53, 90, 83, 77, 70, 63, 57, 50, 93, 87, 80, 73, 67, 60, 53, 97, 90, 83, 77, 70, 63, 57, 100, 93, 87, 80, 73, 67, 60 }; TestUtilities::AssertImageData(image, expectedBits); } TEST(GridCalcTests, GridCalcInterpolateWideTest) { // Setup const Pixel8 bits[] = { 100, 0, 0, 0, 80, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 0, 0, 0, 70, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 80, 0, 0, 0, 60 }; Image image(9, 8, bits); GridCalc grid = GridCalc(); grid.Interpolate(image, 4, [&](const int& position, const int& threshold) { image.data[position] = threshold; }); const Pixel8 expectedBits[] = { 100, 95, 90, 85, 80, 75, 70, 65, 60, 98, 93, 88, 83, 78, 73, 68, 63, 58, 95, 90, 85, 80, 75, 70, 65, 60, 55, 93, 88, 83, 78, 73, 68, 63, 58, 53, 90, 85, 80, 75, 70, 65, 60, 55, 50, 93, 88, 83, 78, 73, 68, 63, 58, 53, 97, 92, 87, 82, 77, 72, 67, 62, 57, 100, 95, 90, 85, 80, 75, 70, 65, 60 }; TestUtilities::AssertImageData(image, expectedBits); } TEST(GridCalcTests, GridCalcInterpolatePlusOneTest) { // Setup const Pixel8 bits[] = { 100, 0, 0, 80, 0, 0, 60, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 0, 0, 70, 0, 0, 50, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 60, 0, 0, 60, 100, 40, 0, 0, 60, 0, 0, 80, 100 }; Image image(8, 8, bits); GridCalc grid = GridCalc(); grid.Interpolate(image, 3, [&](const int& position, const int& threshold) { image.data[position] = threshold; }); const Pixel8 expectedBits[] = { 100, 93, 87, 80, 73, 67, 60, 40, 97, 90, 83, 77, 70, 63, 57, 47, 93, 87, 80, 73, 67, 60, 53, 53, 90, 83, 77, 70, 63, 57, 50, 60, 93, 84, 76, 67, 62, 58, 53, 73, 97, 86, 74, 63, 61, 59, 57, 87, 100, 87, 73, 60, 60, 60, 60, 100, 40, 47, 53, 60, 67, 73, 80, 100 }; TestUtilities::AssertImageData(image, expectedBits); } TEST(GridCalcTests, GridCalcInterpolatePlusTwoTest) { // Setup const uint8_t bits[] = { 100, 0, 0, 80, 0, 0, 60, 0, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 0, 0, 70, 0, 0, 50, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 60, 0, 0, 60, 0, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 60, 0, 0, 80, 0, 100 }; Image image(9, 9, bits); GridCalc grid = GridCalc(); grid.Interpolate(image, 3, [&](const int& position, const int& threshold) { image.data[position] = threshold; }); const Pixel8 expectedBits[] = { 100, 93, 87, 80, 73, 67, 60, 50, 40, 97, 90, 83, 77, 70, 63, 57, 52, 47, 93, 87, 80, 73, 67, 60, 53, 53, 53, 90, 83, 77, 70, 63, 57, 50, 55, 60, 93, 84, 76, 67, 62, 58, 53, 63, 73, 97, 86, 74, 63, 61, 59, 57, 72, 87, 100, 87, 73, 60, 60, 60, 60, 80, 100, 70, 67, 63, 60, 63, 67, 70, 85, 100, 40, 47, 53, 60, 67, 73, 80, 90, 100 }; TestUtilities::AssertImageData(image, expectedBits); } } ================================================ FILE: Doxa.Test/ISauvolaTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" namespace Doxa::UnitTests { // Exposes protected members for Unit Testing class ISauvolaTestharness : public ISauvola { public: ISauvolaTestharness() : ISauvola() {} using ISauvola::Combine; using ISauvola::Spider; }; Pixel8 sauvolaBinaryOutput[38] = { Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::White, Palette::Black }; TEST(ISauvolaTests, ISauvolaSpiderTest) { // Setup Image image(6, 6, sauvolaBinaryOutput); // Sauvola Binary Image outputImage(image.width, image.height); // Output of Spider outputImage.Fill(Palette::White); // Method under Test ISauvolaTestharness isauvola; isauvola.Spider(outputImage, image, 19); // Assert Correctness Pixel8 expected[] = { Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::White, Palette::Black }; TestUtilities::AssertImageData(outputImage, expected); } TEST(ISauvolaTests, ISauvolaSpiderSinglePixelTest) { // Setup Image image(6, 6, sauvolaBinaryOutput); // Sauvola Binary Image outputImage(image.width, image.height); // Output of Spider outputImage.Fill(Palette::White); // Method under Test ISauvolaTestharness isauvola; isauvola.Spider(outputImage, image, 30); // Assert Correctness Pixel8 expected[] = { Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::White }; TestUtilities::AssertImageData(outputImage, expected); } TEST(ISauvolaTests, ISauvolaCombineTest) { // Setup Pixel8 highContrastData[] = { Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::Black, Palette::Black }; Image highContrastImage(6, 6, highContrastData); // High Contrast Image Image sauvolaImage(6, 6, sauvolaBinaryOutput); // Sauvola Binary Image outputImage(sauvolaImage.width, sauvolaImage.height); // Output of Spider // Method under Test ISauvolaTestharness isauvola; isauvola.Combine(outputImage, highContrastImage, sauvolaImage); // Assert Correctness Pixel8 expected[] = { Palette::White, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::Black, Palette::White, Palette::Black, Palette::Black, Palette::White, Palette::Black }; TestUtilities::AssertImageData(outputImage, expected); } } ================================================ FILE: Doxa.Test/ImageFixture.hpp ================================================ #ifndef IMAGEFIXTURE_HPP #define IMAGEFIXTURE_HPP #include "pch.h" #include "TestUtilities.hpp" namespace Doxa::UnitTests { class ImageFixture : public ::testing::Test { protected: void SetUp() override { projFolder = TestUtilities::ProjectFolder(); // Load Color Image const std::string filePath = projFolder + "2JohnC1V3.ppm"; image = PNM::Read(filePath, ParameterMap{ {"grayscale", GrayscaleAlgorithms::QT} }); } Image image; std::string projFolder; }; } #endif // IMAGEFIXTURE_HPP ================================================ FILE: Doxa.Test/ImageTests.cpp ================================================ #include "pch.h" namespace Doxa::UnitTests { class ImageTest : public ::testing::Test { protected: Image image; void SetUp() override { image = Image(3, 2); // Set pixel values for image image.data[0] = 0xF0; image.data[1] = 0xF1; image.data[2] = 0xF2; image.data[3] = 0xF3; image.data[4] = 0xF4; image.data[5] = 0xF5; } }; TEST_F(ImageTest, ImagePixelTest) { // Verify each pixel EXPECT_EQ(image.Pixel(0, 0), image.data[0]); EXPECT_EQ(image.Pixel(1, 0), image.data[1]); EXPECT_EQ(image.Pixel(2, 0), image.data[2]); EXPECT_EQ(image.Pixel(0, 1), image.data[3]); EXPECT_EQ(image.Pixel(1, 1), image.data[4]); EXPECT_EQ(image.Pixel(2, 1), image.data[5]); // Out of bounds pixel EXPECT_EQ(image.Pixel(0, 3, 0xF6), (Pixel8)0xF6); } TEST_F(ImageTest, ImageCopyCTORTest) { // Copy Image using Copy CTOR Image copy(image); EXPECT_FALSE(copy.managedExternally); EXPECT_EQ(copy.height, image.height); EXPECT_EQ(copy.width, image.width); EXPECT_NE(nullptr, copy.data); EXPECT_NE(copy.data, image.data); // Copy Image using Copy CTOR - Identical to above due to direct instantiation Image copy2 = copy; EXPECT_FALSE(copy2.managedExternally); EXPECT_NE(copy.data, copy2.data); } TEST_F(ImageTest, ImageMoveCTORTest) { Image move = std::invoke([]() { Image iLiveOnTheStack; return iLiveOnTheStack; // Triggers our Move CTOR }); EXPECT_FALSE(move.managedExternally); } TEST_F(ImageTest, ImageExternalReferenceTest) { // Copy Image using Copy CTOR Image copy(image); // Create a scope to trigger the reference image destructor { // Create Image Reference Image reference = Image::Reference(image.width, image.height, image.data); EXPECT_TRUE(reference.managedExternally); EXPECT_EQ(reference.height, image.height); EXPECT_EQ(reference.width, image.width); EXPECT_EQ(reference.data, image.data); // Set pixel value from coordinate. image.Pixel(1, 1) = 0xFA; EXPECT_EQ(image.Pixel(1, 1), (Pixel8)0xFA); EXPECT_EQ(copy.Pixel(1, 1), (Pixel8)0xF4); // Copy should not change. EXPECT_EQ(reference.Pixel(1, 1), image.Pixel(1, 1)); // Reference should change // Copy a memory managed Image but no longer manage the memory Image deepReference = Image(reference); EXPECT_FALSE(deepReference.managedExternally); EXPECT_NE(image.data, deepReference.data); // Reference should not change deepReference.Pixel(1, 1) = 0xFF; EXPECT_EQ(reference.Pixel(1, 1), (Pixel8)0xFA); } // Reference should not free our image storage EXPECT_EQ(image.Pixel(1, 1), (Pixel8)0xFA); } TEST_F(ImageTest, ImageReferenceTest) { // Create a reference from an Image object Image reference = image.Reference(); EXPECT_EQ(reference.data, image.data); EXPECT_TRUE(reference.managedExternally); } TEST_F(ImageTest, ImageCopyAssignmentOperatorTest) { // Create a reference from an Image object Image reference = image.Reference(); // Ensure our Copy Assignment Operator issues a deep copy Image copy; copy = reference; EXPECT_FALSE(copy.managedExternally); // Copy should now be a deep copy of image EXPECT_EQ(image.width, copy.width); EXPECT_NE(image.data, copy.data); EXPECT_EQ(copy.Pixel(1, 1), (Pixel8)0xF4); } } ================================================ FILE: Doxa.Test/LocalWindowTests.cpp ================================================ #include "pch.h" namespace Doxa::UnitTests { TEST(LocalWindowTests, LocalWindowIterateTest) { // Build dummy image for a 3x3 window // This array will be updated by our iterator Pixel8 data[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36 }; Image image(6, 6, data); // Test that we can iterate a 3x3 window around our image LocalWindow::Iterate(image, 3, [&](const Region& window, const int& positionImage) { int sum = 0; LocalWindow::Iterate(image.width, window, [&](const int& positionWindow) { sum += image.data[positionWindow]; }); data[positionImage] = (std::min)(sum, 255); }); // Compare to our known output const Pixel8 expected[] = { 18, 30, 36, 42, 48, 34, 45, 72, 81, 90, 99, 69, 81, 126, 135, 144, 153, 105, 117, 180, 189, 198, 207, 141, 153, 234, 243, 252, 255, 177, 114, 174, 180, 186, 192, 130 }; EXPECT_TRUE(std::equal(std::begin(data), std::end(data), std::begin(expected), std::end(expected))); } } ================================================ FILE: Doxa.Test/MorphologyTests.cpp ================================================ #include "pch.h" #include "ImageFixture.hpp" #include "TestUtilities.hpp" namespace Doxa::UnitTests { class MorphologyTests : public ImageFixture {}; class MorphologyTestHarness : public Morphology { public: MorphologyTestHarness() : Morphology() {} using Morphology::Morph; using Morphology::IterativelyErode; using Morphology::IterativelyDilate; }; TEST_F(MorphologyTests, MorphologyErodeTest) { const Pixel8 data[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 50, 18, // Note 5th column 19, 20, 3, 22, 23, 24, // Note 3rd column 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36 }; // For each row (y) in data, calculate the min value within the window for that row const Pixel8 minArrayX[] = { 1, 1, 2, 3, 4, 5, 7, 7, 8, 9, 10, 11, 13, 13, 14, 15, 16, 18, 19, 3, 3, 3, 22, 23, 25, 25, 26, 27, 28, 29, 31, 31, 32, 33, 34, 35 }; // For each column (x) in minArrayX, calculate the min value within the window for that column const Pixel8 minArray[] = { 1, 1, 2, 3, 4, 5, 1, 1, 2, 3, 4, 5, 7, 3, 3, 3, 10, 11, 13, 3, 3, 3, 16, 18, 19, 3, 3, 3, 22, 23, 25, 25, 26, 27, 28, 29 }; const Image grayScaleImage(6, 6, data); Image erodedImage(6, 6); Morphology::Erode(erodedImage, grayScaleImage, 3); TestUtilities::AssertImageData(erodedImage, minArray); } TEST_F(MorphologyTests, MorphologyDilateTest) { const Pixel8 data[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 50, 18, // Note 5th column 19, 20, 3, 22, 23, 24, // Note 3rd column 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36 }; // For each row (y) in data, calculate the max value within the window for that row const Pixel8 maxArrayX[] = { 2, 3, 4, 5, 6, 6, 8, 9, 10, 11, 12, 12, 14, 15, 16, 50, 50, 50, 20, 20, 22, 23, 24, 24, 26, 27, 28, 29, 30, 30, 32, 33, 34, 35, 36, 36 }; // For each column (x) in maxArrayX, calculate the max value within the window for that column const Pixel8 maxArray[] = { 8, 9, 10, 11, 12, 12, 14, 15, 16, 50, 50, 50, 20, 20, 22, 50, 50, 50, 26, 27, 28, 50, 50, 50, 32, 33, 34, 35, 36, 36, 32, 33, 34, 35, 36, 36 }; const Image grayScaleImage(6, 6, data); Image dilatedImage(6, 6); Morphology::Dilate(dilatedImage, grayScaleImage, 3); TestUtilities::AssertImageData(dilatedImage, maxArray); } // TODO: Update this test to force calls to Morph(...), IterativelyDilate(...), etc using a test harness. TEST_F(MorphologyTests, MorphologySpeedTest) { // Find sample image const std::string filePath = TestUtilities::ProjectFolder() + "2JohnC1V3.ppm"; // Read image Image grayScaleImage = PNM::Read(filePath); Image wanBinary(grayScaleImage.width, grayScaleImage.height); // Window Size 17 is the tipping for my CPU. This will trigger the Morph algorithm to be applied. Parameters parameters({ { "window", 17 },{ "k", 0.2 } }); // Time algorithms double wanMorphedSpeed = TestUtilities::Time([&]() { Wan wan; wan.Initialize(grayScaleImage); wan.ToBinary(wanBinary, parameters); }); // This window size will trigger a manual window analysis to be ran. // This is faster for small window sizes, but very costly for large windows. parameters.Set("window", 15); double wanSpeed = TestUtilities::Time([&]() { Wan wan; wan.Initialize(grayScaleImage); wan.ToBinary(wanBinary, parameters); }); SUCCEED() << "Morphed Wan Speed (W=17): " << wanMorphedSpeed; SUCCEED() << "Manual Wan Speed (W=15): " << wanSpeed; //EXPECT_TRUE(wanSpeed < wanMorphedSpeed); } TEST_F(MorphologyTests, MorphologyErodeComparisonTest) { const int windowSize = 25; Image morphedImage(image.width, image.height); Image iterativelyMorphedImage(image.width, image.height); // Manually find the min within each window MorphologyTestHarness::IterativelyErode(iterativelyMorphedImage, image, windowSize); // Utilize a Max Image to speed up the morphology transformation MorphologyTestHarness::Morph(morphedImage, image, windowSize, [](const std::multiset& set) { return *set.begin(); // Min Value }); TestUtilities::AssertImages(iterativelyMorphedImage, morphedImage); } TEST_F(MorphologyTests, MorphologyDilateComparisonTest) { const int windowSize = 25; Image morphedImage(image.width, image.height); Image iterativelyMorphedImage(image.width, image.height); // Manually find the max within each window MorphologyTestHarness::IterativelyDilate(iterativelyMorphedImage, image, windowSize); // Utilize a Max Image to speed up the morphology transformation MorphologyTestHarness::Morph(morphedImage, image, windowSize, [](const std::multiset& set) { return *std::prev(set.end()); // Max Value }); TestUtilities::AssertImages(iterativelyMorphedImage, morphedImage); } } ================================================ FILE: Doxa.Test/PNMTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" namespace Doxa::UnitTests { // Exposes protected members for Unit Testing class PNMTestharness : public PNM { public: PNMTestharness() :PNM() {} // Reads using PNM::Read1BitBinary; using PNM::Read8BitBinary; using PNM::ColorToGrayscale; using PNM::ReadPNM; // Writes using PNM::WriteP4; using PNM::WriteP5; using PNM::WriteP6; using PNM::WriteP7; }; class PNMTests : public ::testing::Test { protected: void TestWriteAndReadGrayScale(std::function writeFunc) { // Setup Image input(3, 2); input.data[0] = 255; input.data[1] = 0; input.data[2] = 255; input.data[3] = 55; input.data[4] = 125; input.data[5] = 200; // Test TestWriteAndRead(input, writeFunc); } void TestWriteAndReadBinary(std::function writeFunc) { // Setup - Image will require padding Image input1(3, 2); // Row 1 input1.data[0] = 255; input1.data[1] = 0; input1.data[2] = 255; // Row 2 input1.data[3] = 0; input1.data[4] = 255; input1.data[5] = 255; // Test Padded Image TestWriteAndRead(input1, writeFunc); // Setup - Image will NOT require padding Image input2(8, 2); // Row 1 input2.data[0] = 255; input2.data[1] = 0; input2.data[2] = 255; input2.data[3] = 0; input2.data[4] = 255; input2.data[5] = 255; input2.data[6] = 255; input2.data[7] = 255; // Row 2 input2.data[8] = 0; input2.data[9] = 0; input2.data[10] = 0; input2.data[11] = 0; input2.data[12] = 255; input2.data[13] = 0; input2.data[14] = 0; input2.data[15] = 0; // Test Padded Image TestWriteAndRead(input2, writeFunc); } void TestWriteAndRead(const Image& inputImage, std::function writeFunc) { // Execute - Write std::ostringstream stream; writeFunc(stream, inputImage); // Execute - Read PNMTestharness pnm; Image outputImage; std::istringstream iss(stream.str()); pnm.ReadPNM(iss, outputImage); // Assert EXPECT_EQ(inputImage.width, outputImage.width); EXPECT_EQ(inputImage.height, outputImage.height); TestUtilities::AssertImages(inputImage, outputImage); } }; TEST_F(PNMTests, PNMRead1BitBinaryTest) { // Setup - 1-bit Binary Data char buffer[] = { static_cast(0xA0), static_cast(0x40), static_cast(0xA0) }; // 10100000 01000000 10100000 std::istringstream stream(buffer); // Execute - Will convert to 8-bit Gray Scale PNMTestharness pnm; Image image(3, 3); pnm.Read1BitBinary(stream, image); // Assert const Pixel8 expected[] = { Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black }; TestUtilities::AssertImageData(image, expected); } TEST_F(PNMTests, PNMRead8BitBinaryTest) { // Setup - 8-bit Gray Scale Data (0 to 255) const char buffer[] = { static_cast(0xFF), static_cast(0x01), static_cast(0xFF), static_cast(0x01), static_cast(0x7C), static_cast(0x7B), static_cast(0x7A), static_cast(0x79), static_cast(0x78) }; std::istringstream stream(buffer); // Execute PNMTestharness pnm; Image image(3, 3); pnm.Read8BitBinary(stream, image); // Assert TestUtilities::AssertImageData(image, (Pixel8*)buffer); } TEST_F(PNMTests, PNMRead24BitBinaryTest) { // Setup - 24-bit RGB Data const char buffer[] = { static_cast(0xFF), static_cast(0x01), static_cast(0x01), static_cast(0x01), static_cast(0xFF), static_cast(0x01), static_cast(0x01), static_cast(0x01), static_cast(0xFF), static_cast(0xFF), static_cast(0xFF), static_cast(0xFF), static_cast(0x78), static_cast(0x78), static_cast(0x78), static_cast(0x01), static_cast(0x01), static_cast(0x01) }; std::istringstream stream(buffer); // Execute - Will convert to 8-bit Gray Scale PNMTestharness pnm; Image image(3, 2); pnm.ColorToGrayscale<3>(stream, image, GrayscaleAlgorithms::MEAN); // Assert const Pixel8 expected[] = { Grayscale::Mean(0xFF, 0x01, 0x01), Grayscale::Mean(0x01, 0xFF, 0x01), Grayscale::Mean(0x01, 0x01, 0xFF), Grayscale::Mean(0xFF, 0xFF, 0xFF), Grayscale::Mean(0x78, 0x78, 0x78), Grayscale::Mean(0x01, 0x01, 0x01) }; TestUtilities::AssertImageData(image, expected); } TEST_F(PNMTests, PNMRead32BitBinaryTest) { // Setup - 32-bit RGBA const char buffer[] = { static_cast(0xFF), static_cast(0x01), static_cast(0x01), static_cast(0x01), static_cast(0x01), static_cast(0xFF), static_cast(0x01), static_cast(0x01), static_cast(0x01), static_cast(0x01), static_cast(0xFF), static_cast(0x01), static_cast(0xFF), static_cast(0xFF), static_cast(0xFF), static_cast(0x01), static_cast(0x78), static_cast(0x78), static_cast(0x78), static_cast(0x01), static_cast(0x01), static_cast(0x01), static_cast(0x01), static_cast(0x01) }; std::istringstream stream(buffer); // Execute - Will convert to 8-bit Gray Scale PNMTestharness pnm; Image image(3, 2); pnm.ColorToGrayscale<4>(stream, image, GrayscaleAlgorithms::MEAN); // Assert const Pixel8 expected[] = { Grayscale::Mean(0xFF, 0x01, 0x01), Grayscale::Mean(0x01, 0xFF, 0x01), Grayscale::Mean(0x01, 0x01, 0xFF), Grayscale::Mean(0xFF, 0xFF, 0xFF), Grayscale::Mean(0x78, 0x78, 0x78), Grayscale::Mean(0x01, 0x01, 0x01) }; TestUtilities::AssertImageData(image, expected); } TEST_F(PNMTests, PNMReadPNMBadFormatTest) { // There is no P8 std::istringstream stream("P8 1 0 255"); PNMTestharness pnm; Image image; EXPECT_ANY_THROW(pnm.ReadPNM(stream, image)); } TEST_F(PNMTests, PNMWriteAndReadP4Test) { PNMTestharness pnm; TestWriteAndReadBinary([&](std::ostream &outputStream, const Image &image) { pnm.WriteP4(outputStream, image); }); } TEST_F(PNMTests, PNMWriteAndReadP5Test) { PNMTestharness pnm; TestWriteAndReadGrayScale([&](std::ostream &outputStream, const Image &image) { pnm.WriteP5(outputStream, image); }); } TEST_F(PNMTests, PNMWriteAndReadP6Test) { PNMTestharness pnm; TestWriteAndReadGrayScale([&](std::ostream &outputStream, const Image &image) { pnm.WriteP6(outputStream, image); }); } TEST_F(PNMTests, PNMWriteAndReadP7Test) { PNMTestharness pnm; TestWriteAndReadGrayScale([&](std::ostream &outputStream, const Image &image) { pnm.WriteP7(outputStream, image); }); } } ================================================ FILE: Doxa.Test/PaletteTests.cpp ================================================ #include "pch.h" namespace Doxa::UnitTests { TEST(PaletteTests, PaletteTest) { constexpr Pixel32 rgb = Palette::RGB(10, 21, 32); constexpr Pixel32 rgba = Palette::RGBA(10, 21, 32, 43); constexpr Pixel32 gray = Palette::RGB(124, 124, 124); // Verify Endian EXPECT_EQ((Pixel32)0xFF20150A, rgb); // 0A = 10, 15 = 21, 20 = 32 EXPECT_EQ((Pixel32)0x2B20150A, rgba); // 2B = 43 // Compaire that the colors are equivolent EXPECT_EQ(Palette::Red(rgb), Palette::Red(rgba)); EXPECT_EQ(Palette::Green(rgb), Palette::Green(rgba)); EXPECT_EQ(Palette::Blue(rgb), Palette::Blue(rgba)); // Get individual colors from Pixel EXPECT_EQ(Palette::Red(rgba), 10); EXPECT_EQ(Palette::Green(rgba), 21); EXPECT_EQ(Palette::Blue(rgba), 32); // Update Alpha EXPECT_EQ(Palette::Alpha(rgb), 255); EXPECT_EQ(Palette::Alpha(rgba), 43); EXPECT_EQ(Palette::UpdateAlpha(rgb, 43), rgba); // Gray Values EXPECT_FALSE(Palette::IsGray(rgba)); EXPECT_TRUE(Palette::IsGray(gray)); } TEST(PaletteTests, PaletteColorDistanceTest) { // When there are no differences, 0 int noDistance = Palette::ColorDistance(Palette::RGB(255, 0, 0), Palette::RGB(255, 0, 0)); EXPECT_EQ(0, noDistance); // Changing from one color to the next should result in a distance change int hueDistance1 = Palette::ColorDistance(Palette::RGB(0, 255, 0), Palette::RGB(0, 0, 255)); EXPECT_NE(0, hueDistance1); int hueDistance2 = Palette::ColorDistance(Palette::RGB(255, 0, 0), Palette::RGB(0, 0, 255)); EXPECT_NE(0, hueDistance2); int hueDistance3 = Palette::ColorDistance(Palette::RGB(0, 0, 255), Palette::RGB(0, 255, 0)); EXPECT_NE(0, hueDistance3); // Ensure that evenly distributed changes have an effect int brightnessDistance1 = Palette::ColorDistance(Palette::RGB(10, 20, 30), Palette::RGB(12, 22, 32)); EXPECT_NE(0, brightnessDistance1); // Technically this is grayscale, it is questionable if this should effect things. // TODO: Research this. It seems to comply with CIE Delta-E results. int brightnessDistance2 = Palette::ColorDistance(Palette::RGB(10, 10, 10), Palette::RGB(12, 12, 12)); EXPECT_NE(0, brightnessDistance2); // Verify the degree of change is great enough to be detected int colorDistance1 = Palette::ColorDistance(Palette::RGB(255, 255, 0), Palette::RGB(255, 128, 0)); // Yellow to Orange int colorDistance2 = Palette::ColorDistance(Palette::RGB(255, 255, 0), Palette::RGB(128, 255, 0)); // Yellow to Light Green int colorDistance3 = Palette::ColorDistance(Palette::RGB(255, 128, 0), Palette::RGB(128, 255, 0)); // Orange to Light Green // Orange to Green should obviously be farther that the difference between Yellow to Orange, and Yellow to Light Green. EXPECT_TRUE(colorDistance3 > colorDistance1 && colorDistance3 > colorDistance2); // Even though equally distributed, Light Green is a lot closer to Yellow than Orange. // This is consistant with CIE Delta-E calculations. EXPECT_TRUE(colorDistance1 > colorDistance2); // Ensure that color comparison order does not matter int left = Palette::ColorDistance(Palette::RGB(255, 128, 50), Palette::RGB(50, 128, 128)); int right = Palette::ColorDistance(Palette::RGB(50, 128, 128), Palette::RGB(255, 128, 50)); EXPECT_EQ(left, right); } } ================================================ FILE: Doxa.Test/ParametersTests.cpp ================================================ #include "pch.h" namespace Doxa::UnitTests { TEST(ParametersTests, ParametersGetTest) { Parameters param({ {"x", 1}, {"y", 1.1 } }); // Int Exists int x = param.Get("x", 4); EXPECT_EQ(1, x); // Double Exists double y = param.Get("y", 2.2); EXPECT_NEAR(1.1, y, 0.0001); // Does not exist int z = param.Get("z", 4); EXPECT_EQ(4, z); } TEST(ParametersTests, ParametersGetCastTest) { Parameters param({ {"intVal", 1}, {"doubleVal", 1.0 } }); // Cast Double to Int int x = param.Get("doubleVal", 4); EXPECT_EQ(1, x); // Cast Int to Double double y = param.Get("intVal", 4.4); EXPECT_EQ(1.0, y); } TEST(ParametersTests, ParametersSetTest) { Parameters param({ { "z", 4 } }); // Verify it was initialized EXPECT_EQ(param.Get("z", 0), 4); // Set Int param.Set("x", 1); EXPECT_EQ(1, param.Get("x", 22)); // Set Double param.Set("y", 1.1); EXPECT_NEAR(1.1, param.Get("y", 22.22), 0.0001); // Set Existing Value param.Set("z", 5); EXPECT_EQ(5, param.Get("z", 22)); } TEST(ParametersTests, ParametersParser) { // Parsed as a JSON string Parameters params = Parameters::FromJson(R"({"window": 75, "custom": "test", "k": -0.01})"); EXPECT_EQ(params.Get("window", 0), 75); EXPECT_NEAR(params.Get("k", 0.0), -0.01, 0.0001); } } ================================================ FILE: Doxa.Test/RegionTests.cpp ================================================ #include "pch.h" namespace Doxa::UnitTests { TEST(RegionTests, RegionConstructorTest) { Region::Point upperLeft(0, 0); Region::Point bottomRight(10, 15); Region regionPoints(upperLeft, bottomRight); Region regionCoords(0, 0, 10, 15); Region regionWH(11, 16); Region regionWindow(0, 0, 15); EXPECT_TRUE(regionPoints == regionCoords); EXPECT_TRUE(regionCoords == regionWH); EXPECT_EQ(15 * 15, regionWindow.Area()); } TEST(RegionTests, RegionTest) { Region region(10, 20, 30, 40); EXPECT_EQ(region.Height(), 21); EXPECT_EQ(region.Width(), 21); EXPECT_EQ(region.Area(), 441); } TEST(RegionTests, RegionInRegionTest) { Region regionParent(10, 10, 50, 50); Region regionChild(20, 20, 40, 40); Region regionNeighbor(50, 10, 100, 50); Region regionThirdCousin(40, 15, 60, 45); EXPECT_TRUE(regionParent.InRegion(regionParent)); EXPECT_TRUE(regionParent.InRegion(regionChild)); EXPECT_FALSE(regionParent.InRegion(regionNeighbor)); EXPECT_FALSE(regionParent.InRegion(regionThirdCousin)); } TEST(RegionTests, RegionPointTest) { Region region1(0, 0, 10, 10); Region region2(10, 10, 20, 20); EXPECT_FALSE(region1.upperLeft == region1.bottomRight); EXPECT_FALSE(region1.upperLeft == region2.upperLeft); EXPECT_FALSE(region1.upperLeft == region2.bottomRight); EXPECT_TRUE(region1.bottomRight == region2.upperLeft); } } ================================================ FILE: Doxa.Test/Resources/2JohnC1V3.ppm ================================================ P6 707 441 255 ƳƳǴǴȵȵȵǴǴƳȵȵȵȵȵȵȵȵƳƳŲıııŲƳȶȸɹɹʸʸʷʷ˵ʴȰƮĬſľº·ºǼǿŮdzȱűŮƲȳͺ˸ǴððıǴǴɶʷʷɶɶȵ˸˸̹̹̹˸ʷɶʷɶɶʷ˸ȵðſºƾìȰɱȰȰɳʴ˴̵ʹ˳ɯǬæɿžşƠʤЭϴ˷ȲȲʴ̶˳ʲǭɳкѻͷʴ̶кккϹϹϹθθθкϹкѻӽҼθʴʴ˵̶ͷθθθθкккккккккѻѻккθͷ̶̶ͷθθθθ̶̶ͷͷ̶̶˵˵˵˵ʴ̶θθͷ̶ͷθ˵̶ͷθθͷ̶˵̹̹̹̹̹̹̹̹˸˸˸˸˸˸˸˸˹˹˹̺̺ͻͻͻͻͻͻͻͻͻͻͻ̹̹̹̹̹̹̹̹̹ͺͺλλϼϼϼккϹθθͷ̶̶ϹкѻҼѻкθͷλλϼϼϼннннннѾѾѾҿҿннѾѾннннѾѾѾѾннѾѾѾѾѾннϼϼϼнннннϼλͺλλλλλλλλϼϼϼϼϼϼϼϼϼϼϼλλͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺκϼмммннѽѽнннϼϼλͻλѻϹθϹнѾоϽ̼ξѾϿϼξнѾѾнϼͻͻͻͻμϽϼϼϼϼϼϼϼϼнѾѾҿҿѾѾннϼϼλλλϼϼѾѾѾннϼλͺҼҼҼѻѻԾӽкθθϹҹͲγδͳ˱ǭéêƭǯɱʲʴ˵͸ϺлѼѼлϺϺϺϺϺϺϺϺϺͶͶϸййййϸι͸͸̷̷͸͸ιιлѼлι͸ιлϹθ̶̶θϹϹθͷͷ̶ͷθϹкѽннѾѾѾҿҿҿϼϼϼϼϼϼϼлԺҶжилѽϾν̻ͼͽϼμͺͷ̶̶θкҼѻλ˸ɵӿӿѽннϼϽμμμλλϼϼϿϿ̻̻ͼνоѿҿҿѾѾѾнннμμμϽϽоооϽϽϽϽϽϽϽϽннϼλͺͺ̹̹ͺͺͺλλϼϼϼμϽϽμ̺˹ͻϽϽооѿѿѿоооϽμμоѿоϽϽϽϽϽϽϽϽϽϽϽϽϽμμμоϽμͻ̺̺ͻͻμμμϽϽϽооооϽϽϽμμμμŲŲǴǴȵȵȵǴƳŲǴǴǴǴǴǴǴǴƳŲıııŲƳƳʸʸɷɷȶȶȵȵȲDZůíſľſľļýƾðưǴɳɶʴȵȲǴȵȵƳ¯ƿðŲȵȵɶʷʷʷʷʷ̹̹̹̹̹˸ʷɶ̹ʷɶʷʷ˸ʷʴƱǰɲ˴͵͵˳ʲɱʲ˴˴ʴʱɱɯǭũŧɪ˫ʪ˫вз̶ʴɳʲ˳̴̴ʰĪŬĩȾ¨ɲθкϹ˵ʴ̶ϹкккϹϹϹθθӽѻккϹθ̶ʴϹϹθθθθϹϹккккккккккккϹθͷ̶˸˸̹̹̹̹̹̹̹̹̹̹˸˸ʷʷȲʴ̶ͷ˵ʴ˵̶ɳʴ˵˵̶ͷθθ̹̹̹̹̹̹̹̹˸˸˸˸˸˸˸˸˹˹˹̺̺ͻͻͻͻͻͻͻͻͻͻͻ̹̹̹̹̹̹̹̹ͺλλλλϼϼϼккϹθθͷ̶̶ϹкѻҼѻкθͷλλϼϼϼнннѾѾѾѾҿҿҿннϼϼннннѾѾѾѾҿҿѾѾѾѾѾннϼϼϼϼϼннϼϼλͺλλλλλλλλϼϼϼϼϼϼϼϼϼϼϼλλͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺ̹̹͹ϼϼннѾѾѾѾооϾννͼͼλѻϹθϹнѾнϼμϽооϿϿѾононѾҿѾнϼλͺͺͺ̹ϼϼϼϼϼϼϼϼѾѾѾѾѾѾѾѾнϼϼλλϼнѾҿҿѾнннϼϼռҹε̳εӺ׾ռӺзεεзѸҹззззз϶ζ͵͵̵ͶͶ̶̶˵˶ʷ̸κϻммϻκκκκκκκκκ͹͹κϻϻϻϻκ͹̸̸˷˷̸̸͹̸κϻϻ͹͹κмҽлιιϺллϺιι͸ιϺлѼҽннѾѾѾҿҿҿλλλϼϼϼнѼӻѹиιлнϾνʷ̹ͻϼѻиϵδ̵ηѺӼӼҼϹͷӿӿӿӿҿҿѾннϼλλ̺ͻͻͻͻμμμ˼˼̽;ϽϽооҾҾҾѽҽҽѼѼμμμϽϽоооϽϽϽϽϽϽϽϽннϼϼλͺ̹̹ͺͺͺλλϼϼϼϽооμ̺̺ͻϽϽооѿѿоϽϽоϽμоѿоϽϽϽϽϽϽϽϽѿооϽϽμμμѿоϽμͻͻͻμμμϽϽϽооооооϽϽϽμμμððŲƳǴȵȵǴŲıŲŲŲŲŲŲŲŲŲŲıııŲƳǴ˹˹ɷȶȶȶȵɶɳDZůíſſžĿĽĿƿïıdzȵʶɶʶɶɵǴƳı¯žƿðŲȵȵɶɶʷ˸˸̹̹̹̹̹˸ʷɶȵ˸˸ʷɶɶʷʷ̶ɴɲɲɲ˳˳˳˳ɱ˳ʳʳDZưDZɱʲȯɮͲϳ̰αжзθ̶˵ʲ˳̴̴˱˱̳εγ˰ʯʰͶθθ̶ʴʴͷϹѻкккϹϹϹϹҼѻѻккϹϹϹѻкϹθθϹкѻѻккϹϹккѻккккϹθ̶̶˸˸˸˸˸̹̹̹̹̹ͺͺ̹˸ʷʷ˵̶̶˵ɳDZȲɳͷͷͷͷθθθθ̹̹̹̹̹̹̹̹̹̹˸˸˸˸ʷʷ˹˹˹̺̺ͻͻͻμμͻͻͻͻ̺̺̹̹̹̹̹̹̹̹λλλϼϼϼϼϼϹϹϹθθͷͷͷϹкѻҼѻкθͷͺλλλϼϼϼнѾѾѾѾҿҿϼϼϼλϼннѾнѾѾҿҿҿҿѾѾѾннϼϼϼλλϼϼϼϼλͺϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼλλͺͺͺ̹̹̹̹̹̹̹̹λλͺͺͺ̹̹̹ϼнѾѾҿѾннооϾνοͼ;ͼнкθϹϼнλͺμϽоϽξξҿӽѾннѾҿҿҿѾнϼλͺ̹˸ϼϼϼϼϼϼϼϼѾѾѾѾѾѾѾѾннλλλϼѾҿҿѾѾннѾҿӽؿҹȯÿǮ϶ӺֽѺйϻϹϻкнҼѾҼнѻϼϹλλ͹͹κϻϻϻκκϻϻϻϻϻϻϻϻκκκκκκκκ͹͹̸̸̸̸͹͹˷͹κκκκϻѽҽлιιϺллιιι͸ιϺлѼҽϼнннѾѾѾҿϼϼϼϼнннѼҽлϺϻнѾпλϻнӽӻҸѴϳͳϵϹһӼһѼѻѾҾҾҾҿҿҿҿѾѾѾннϼϼϼͻͻͻͺͻͺͻμ̽;;;μϽϽϽӿҾҾҾҽһһһλμμϽϽоооооооооооѾннϼλͺͺͺͺͺͺλλϼϼϼϽооϽͻ̺μоϽооооϽμμоϽϽоѿоооϽϽϽϽоооϽμμμϽѿоϽϽμμϽϽϽϽϽϽоооѿѿоооϽϽϽϽϽðıƳȵȵǴŲıððððððððŲııııŲƳǴʷɶȵǴȵɶʷ˸ɶȵDZĮí¬¬¬½½¯ıƳɶɶɶɶʷɶȵƳǴŲððıǴǴǴǴȵɶʷ˸̹ͺͺ̹˸ʷɶȵʷʷʷɶȵȵɶ˵ȳǰǰƯȰȰɱʲ˳̴˴ɲƯŮưɳɳDZȲͶι˶̶ϹѸззε˳ʲ˳˳˱δ϶εʹεʹ̲Ͷͷ˵ʴʴ̶θкѻѻкккϹϹϹкѻѻѻккѻѻккккѻѻѻѻѻѻϹϹϹϹѻѻϹккϹϹͷ̶˵ʷʸʸʸ˹̺̺ͻ̺̺ͻͻͻ̺ʸɶ̶̶˵ɳDZDZɳ˵ккккϹϹθθ̹̹̹̹̹̹̹̹̹̹̹˸˸ʷʷʷ˸˸˸̹̹ͺͺͺλλλͺͺ̹̹̹̹̹̹̹̹̹̹̹ϼϼϼϼϼϼϼϼϹϹθθθθͷͷϹкѻҼѻкθͷ̹ͺͺͺλλλϼннѾѾѾҿҿҿϼϼλλλϼѾҿϼнҿҿҿѾѾѾннϼϼϼͺλλϼϼϼλͺϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼλλͺͺͺ̹̹̹̹̹̹̹̹ϼϼλλͺͺͺ̹ϽнѿѿѿоϿξο;;нϼͺͺλλ̹˸̹ͺͺ̹˹̺μоѼѺлϺϺлѼѼѻѻннмϻκκϼϼϼϼϼϼϼϼҿѾѾннѾѾҿѾнλͺλнҿѾѾннѾҿؿ϶tmnrzī϶ӼлϺͺιλѼѾҾҿѽнκͻκμϻϻϻκ͹͹͹κκϻϻϻϻϻϻϻϻϼλͺ̹̹ͺλϼκκ͹͹͹͹κκʶ̸κκκκмҾҿѽϻϻммϻκκκ͹κϻмѽҾϺлллѼѼѼҽҽҽҽҽѼѼѼѼӾнϼϾѾҾӻӹҶα̬ɩŪƫǯʱ̳δηϷҼҼҾҾӾӾҿҿѾнннннмϻϼϼϼкϼкϼϼпоонϼӿӿҼҼӼҹҹһλμμϽϽоооѿѿѿѿѿѿѿѿѾѾнϼλλͺͺͺͺͺλλϼϼϼооѿϽͻͻμооооооϽμͻоϽϽѿѿϽѿоϽϽоѿѿϽμμϽϽооϽϽϽоѿѿϽϽϽоооѿѿѿѿоооϽϽϽϽžžðŲǴǴǴŲı¯¯¯¯¯¯¯¯ıııııŲǴǴǴǴƳǴȵʷ̹λɶȵDZůííĮİ¿¿¿¯ŲǴȵ̹˸˸̹ͺ̹˸ɶ̸ʷǴı𯯯ŲŲŲƳǴȵɶʷͺͺͺ̹˸ʷɶȵȵɶʷʷȵȵȵʴ˶ʳʳʳ˳˳̴͵̴͵̵ɲŮĭDzʵƲŰƱʷ˸ɶɷ̹ѸѸѸз̴ʲʲʲȮͳε˲ʱ̳̳ɲ͸̶ʴɳ˵θкҼѻѻѻккϹϹϹкѻѻѻϹθϹϹҼԾտԾӽҼѻϹθθϹѻҼϹϹϹϹθͷ̶˵ʷʸʸʸʸ˹ͻͻ˹̺ͻμͻ̺ʸɶ˵˵ʴȲưDZʴͷθθϹккϹθθ̹̹̹̹̹̹̹̹̹̹̹˸˸ʷʷʷ˸˸˸̹̹ͺͺͺλλλͺͺ̹̹̹̹̹̹̹̹̹̹̹ϼϼϼϼϼϼϼϼϹϹθθθθͷͷϹкѻҼѻкθͷ̹̹̹ͺͺͺλλнннѾѾҿҿҿϼλͺͺλϼѾҿϼнҿҿѾѾѾннϼϼϼͺͺλϼϼϼλλϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼϼλλͺͺͺ̹̹̹̹̹̹̹̹нϼϼλλͺͺͺϼоѿоϿξ;ѿϼͺͺͺͺ˸ɶʷ˸˸ʷɶɶ̺ϼйжθηηϺϺлθϹϼнѽѽммϼϼϼϼϼϼϼϼҿѾѾннѾѾҿѾнλͺλнҿѾнннѾؿԻ˲hljQ_]D\ZAb`GpnUlƭͶι̹̹̹ͺϼѾҿнϼ̹ʷʷʷ̹ͺѽмκ͹̸̸͹κϻϻϻϻϻϻϻϻнλ̹ʷʷ̹λϼϻκκ͹͹κκϻʶ̸κκκκмҾҿѽϻϻммϻκκκ͹κϻмѽҽϺϺϺлллѼѼӾӾӾҽҽѼѼѼѾпѽҼҺҵͰɩĢȽɾʿ¨ŬǮȱѹѻѽѽҽҽѾѾннннннммҿӽҼҼҼҼѼѼѿннӿӿӽҼӺҹҹһλμμϽϽоооѿѿѿѿѿѿѿѿѾѾнϼϼλͺͺͺͺͺλλϼϼϼоѿѿϽͻͻμооооооμͻͻоϽоϽооооѿϽμμϽϽϽϽϽϽоѿϽϽϽооѿѿѿѿѿѿооϽϽϽϽƿĽžŲǴƳŲŲŲŲŲıð¯ðǴʷƳŲŲııŲŲƳ˵ɳưưȲʴ˸˸ʷɶȵƳııðİIJƴʸ˹ͻμͻ̺ɷǴʶȵǴƳŲŲııııƳǴɶ˸̹ͺ˸˸˸ʷʷʷɶɶ˸̹ͺ˸ɶǴȵʴƱǰʳ̵ζϷζζ˴˴˴˴̶̶˸˸ȵȵȵɷȷȷɺʷʱ˰̱̱˱˱̲ͳηηͷ̶˵ʴȲȱ͸ͷ̶̶ͷθкѻкϹϹθθϹϹккѻҼкθ̶̶ͷкϹкҼԾԾҼϹϹккѻѻккϹϹϹθͷͷͷθθʷ˸˸̹̹̹ͺͺͺ̹˸̹̹˸ǴŲDZDZDZDZɳʴ̶ͷθθθθθθθθͺͺͺ̹̹˸˸˸ɶ˸̹̹ʷȵȵɶθθθϹϹϹϹϹϹϹϹϹϹϹϹϹ̹̹̹ͺͺͺλλϼϼϼϼϼϼϼϼϹϹϹϹϹϹϹϹϹϹϹϹϹϹϹϹͺͺͺͺͺͺͺͺнϼϼϼϼнѾҿѾѾѾѾѾѾѾѾϼͺѾҿнѾѾѾнϼλλλλλλλλλλλλϼϼϼλλͺͺͺϼннѾнλͺ˸ϼϼϼλλͺͺͺͺλλλλλλλͺϼѾѾнλλλѾҿнϼϼͼʹο˿˿μλλλλλλλϹ̶ʴ˵θкϼϺ϶ҷҺҹѸѻйηѺйҼԾӿннннннннҿѾϼλλϼѾҿѾҿҿнϼнѾҿнϼλλнѾӽҹ׼ؽ̱{~cuqVsoT|auè˰϶ѽѾнλ̹ͺнҿѾѾϼͺ˸̹ϼҿӿҿҿнͺ̹ͺλнннϼϼλλλнϼ̹ʷȵʷͺнλλλλλͺͺ̹̹˸˸˸̹λϼѽӾҽѼллѼҽӾιϺϺϺллллϺηͶͶϸйһԽһӼԽԽԽӼѺлнλͼϾоͺϷжβǧŻĪʲ͵ѻӽҽлҾѿҿӿҾӽҼлѺӼԽҼѻӽӽԾԾվԽӼһλμμϽϽоооѾҿҿҿҿѾнϼͺ̹̹̹̹ͺλϼϽоѿоμͻμϽоооϽϽμμμμоѿѿϽϽоͻͻμϽѿѿоϽμμμμμμϽϽϽоооϽϽϽооѿѿѿѿѿоϽϽμͻͻͻƿ¯ıŲŲŲŲƳŲƳŲı¯ðƳɶǴǴƳƳƳƳǴǴʴDZůĮDZɳɶɶɶȵǴƳıððı°ųǵʸ˹ͻͻͻ˹ȶƳƲƳƳƳŲ𯯯ıŲǴȵʷʷ˸ʷʷʷɶɶɶɶ˸̹ͺ˸ɶǴȵʴƱȱʳ̵ζζζζ˴˴˴̴̶̶˸̸ɶɶɷȶȹɺʻ̹ɱ˰̱̱˱˱̲ͳηηͷͷ̶ʴɳɲιθͷ̶̶θϹккϹϹθθϹϹкϹкѻϹͷ̶ͷθ̶̶ͷкӽӽҼкϹккѻѻккϹθθͷͷͷθϹϹʷɶʷʷ˸̹ͺλͺ̹˸̹̹˸ȵŲʴʴɳʴʴ̶ͷθθθθθθθθθͺͺͺ̹̹˸˸˸̹ͺλͺʷɶɶʷ˵˵˵˵̶ͷθϹϹϹϹϹϹϹϹϹ̹ͺͺͺλλλϼϼϼϼϼϼϼϼϼϹϹϹϹϹϹϹϹϹϹϹϹϹϹϹϹλλλλλλλλнннϼннѾѾннннннннλͺѾѾϼϼннϼλλλϼϼϼϼϼϼϼϼϼϼнϼλλͺͺͺλλλнѾѾнϼλϼϼλλλλλλλλλλϼϼϼϼλϼнѾнϼϼϼ͸ικϺлѾѾϾп;μλλλλλλλθͷ̶ͷϹϹ˵ǰɯ̰̲ʰȮɰ˲̵йѺӽӽҾмҾннннннннѾѾнϼϼнѾѾѾҿҿѾннннϼнѾнϼλλкӻؽٿԹ¨|puéȭ̲зλмѿҾоѽҿҿѽλ̷ȵʵ̹лнѾѾϼ̹˸̹λͺͺͺλλϼϼϼϼλͺ˸ʷʷͺϼλͺ̹ʷɶʷʷ˸ʷ˸ͺλͺͺͺͺмл͹͸̸ιϻѼ͹ι̸͸͹ϺκлͷͶ̶Ͷ̶ϸϹѺϹѺѻӼѻѺϹϸӾϼννϿͻ˸ȵ˳ͳɬľ{slmkowɿʰ̴кӽԿҽҾӿѿҿҿҾҼӽӻѺһӼԺӺӺԾԾԾԾԽӼһҽμμϽϽϽоооѾѾҿҿѾѾнϼͺ̹̹̹̹ͺλϼϽоѿоμͻμϽоооϽϽμμμϽѿѿϽμϽѿμμϽооѿѿооϽμμμμμμμμϽϽϽоϽϽϽооѿѿѿѿѿоϽϽμͻͻͻ¯¯ððððıŲƳƳǴǴƳŲððŲȵȵȵȵȵȵȵȵȵȲƮĬĬưȲȲȲɶȵǵƴųIJñı¿¿¯ŲǴʷ˸˸˸ʷȵƳŲűŲǴǴǴŲ¯¯ðıŲǴɶʷʷʷʷʷɶɶȵȵȵ˸̹ͺ˸ɶǴȵʴDzȱɲ˴͵ζζζ̷̷ͷͷͷͷ͹ι͹̸ʷɶȶʸ˺λʲˮˮ̯˱̲̲̲ηϸθθ͹̸˷˶ϺϹθ̶̶ͷθϹϹϹθθθθϹϹͷϹкϹͷͷθϹʴʴ̶θҼӽѻϹϹϹккккϹϹͷͷͷͷθϹкѻϹθͷ̶̶θϹкϹθͷϹкѻϹͷθͷͷ̶̶ͷθθθθθθθθθθͺͺ̹̹̹̹˸˸ͺͺͺ˸ȵǴȵʷ̶˵˵̶ͷϹкѻϹϹϹϹϹϹϹϹͺλλλϼϼϼнϼϼϼϼϼλλλϹϹϹϹϹϹϹϹккккккккϼϼϼϼϼϼϼϼѾнннннннϼϼϼϼϼϼϼϼнͺͺҿнλλϼϼλλλϼннϼϼϼϼϼϼϼϼѾнλͺ̹ͺλϼ˸̹λϼннннλλλλλϼϼϼннннϼϼϼϼϼϼϼϼϼϼϼϺȲȲɳ˵̶̸͹ͻѾϾνοϿϿμλλλλλϹϹͷͷζииʲſøƸǹ·Ż©ͳйԾҼϹθлѽннннннннѾѾннннѾѾҿѾѾѾннннͺнҿϼ̹̹θѹӹ׽׽ҸūžĪĪéūǯʵͷϼкλθͺϹнѻϻ̶ʶʴιѼλϼϼͺ˸ʷ̹ͺͺͺͺͺͺͺͺͺϹϹͷ̶̶̶̶ͷҿнͺ˸ʷ˸̹ͺȵ̹ннλ˸˸̹кϹθͷͷθϹккϹθͷθϹѻҼϹϹθθθθϹккѻҼҼӽҼҼѻмϻλλϼλ˹ɶŲʳδƩ}j^|YxWvUyYe{ɿ˱̲϶ѻӾмѿѿҾӿҾҼϹϷηж϶ϴδ϶ԺӼӼӼӻӽӽӽտտԿӾҽнμϽϽϽооооѾѾҿҿѾнϼϼͺ̹̹̹̹ͺλϼϽоѿоμͻμϽоооϽϽμμμѿѿϽμμϽооооооооооϽϽϽϽϽμͻͻͻμμϽϽϽϽϽооооѿѿѿѿоϽϽμͻͻμıŲƳıðŲƳǴǴȵȵƳıðŲƳǴǴȵȵȵȵǴȲɱƮĬŭưȲȲDZɶɶɷȶǵƴųIJ¿ıǴȵʷʷ˸ʷɶǴŲıƳǴʷ˸˸ȵŲðŲƳǴȵʷ˸̹̹ʷʷɶɶȵȵȵȵ˸̹ͺ˸ɶǴȵʴȳȱɲʳ̴͵͵͵̷̷ͷͷͷηιιϺιʶɵȶʷ̻л˱ˮˮˮ̲̲̲˱ηηθθκ͹̸͸лкθͷ̶̶ͷθϹϹθͷͷθϹϹθϹѻкϹθкѻ̶̶ͷϹѻҼкθθϹккккϹθ̶̶̶ͷθϹѻѻкϷͷ˳˵˳ͷζ˵ʲ˵͵кҺѻккϹθͷͷͷͷͷͷͷͷͷͷͷͷͷ̹̹̹̹̹̹̹̹ʷ˸ʷɶƳƳȵʷ˵̶ͷθϹϹθθθθθθθθθθλλϼϼϼнннϼϼϼλλλλͺϹϹϹϹϹϹϹϹккккккккѾѾѾѾѾѾѾѾннѾѾннϼϼнннϼϼϼϼϼнͺϼнλϼλλλλϼнѾѾннннннннѾнλ̹̹ͺλϼ̹̹ͺͺλλλλλλλϼϼϼϼнѾѾѾннннϼѾϼλλϼϼϼιʹɯȮʰʲɳʴ͹ϺннϾϿϿμλλλλλϹϹͷ͵ζи͵}~~|z{é̲ӻѻͷ̶лҽннннннннѾѾѾннѾѾѾҿѾннѾѾнλ̹нѾͺ˸ͺѻͶδ϶ԺֽҸǮƿŻƿɿDZDZDZƯíĭƯɱȰʲ͵εʹʹʹθ̹ͺͺ̹ʷʷ̹ͺϼϼλλͺ̹˸˸кϷθ͵θ͵̶˳нϼλͺ̹̹ͺͺȵ̹ннͺ˸ͺϼкϷ͸͵̷ζιиϺζ̷̴̷ζϺҼιθ͸ͷ̷ͷ̷ͷϺкϺѻлѻлѻθκϻнннͻ̹ɴϸӹȫ{bvRwTtRrPvUc|Ǫβδ͵εһѼоѿҾӿҿӾҾѻζϷϵϳ̱˰˱ͲγϳϳϵϵϷϷϹտտҿнϼϽϽϽоооѿѿѿѿѿѿѿѿѿѿнѾѾѾѾнϼλͺ̹̹̹̹ͺλϼϽоѿоμͻμϽоооϽϽμμμѿѿϽͻͻϽѿѿѿооϽϽϽμμϽϽоϽϽϽͻͻͻͻμμϽϽоооооооооооϽϽμμμμŲƳȵı¯ŲƳƳǴȵȵǴŲıŲƳŲƳǴȵȵǴƳưɱȮƮƮȰȰȲưʷʷ˹˹ʺȸǷƴ¯®ïűdzȴɵɵɵɵȴƲűűƳȵʷ̹̹ʷǴıǴǴȵɶʷ˸̹̹ʷʷʷɶɶȵȵȵ˸̹ͺ˸ɶǴȵʴȳȱɲɲ˳̴̴͵˹̹̹̹͹ιηϷиϷ˴ɳȵ˵ͺйͰ̭ʭˮͰα̲ʰͶͶ͹κκκͻλлкθͷ̶̶ͷθϹθͷͷͷͷθϹϹѻҼҼккѻӽѻккѻҼѻθ˵θθϹккϹθθ̶̶̶ͷθϹѻѻ̴̲˳ʰʲ˱͵δ˳ɯȰʰ̴δ͵͵ϹϹͷ̶˵˵˵̶ͷͷͷͷͷͷͷͷ̹̹̹̹̹̹̹̹ɶʷ˸ʷȵǴɶʷʴ˳͵ϷϷ͵˳ɱζζζζζζζθλλϼϼϼнннϼϼϼλλͺͺ̹ϹϹϹϹϹϹϹϹккккккккҿҿҿҿҿҿҿҿϼнѾѾѾнϼϼҿҿѾѾнннϼнλнϼλѾλλλλϼнѾѾнннннннннϼλͺ̹ͺλλннϼϼλλλλϼϼϼϼϼϼϼϼѾѾѾннϼϼϼѾϼͺͺλλ̹ɴʰŪ§ƪƬūɰϹͶϺѾнμͻμоμμλλλλϹϹ͵͵ζϵʰtca~_~^z]~`pĪии̶ͷѼҽннннннннѾѾѾҿҿѾѾѾҿнϼϼѾѾϼͺλннϼ̹˸ͺл̳ɮƬɮ˱ɮ˿¸÷ĸǽɽǻȼǻ÷ùžĽ¨ɯͳͳ̱ɮȯ̶̹̹̹ʷʷ̹λλλͺͺͺͺͺ͸ѺжͶδηϵ̵ʰǴȵɶ˸˸˸ʷʷʷͺϼλ̹̹λл̵̲ɳʰȲ˱ʴͳ˵̲ʴ˱ʴͳ̶ηʴ˴ʴ˴ʴʳɳʳͷͶ̶Ͷ̶ηͷηϷкҼҾнϼͺ̷̹ӼԻɮg~tQsOrNsO{XhƻͰѵж̴ɰη׿ӾоѿӿӾѽѻиѹѷͲȮƪũǪƧȨʭ̯ϲϵϵϷտտпϽϽоооѿѿѿѿѿѿѿѿѿѿѿннѾѾннϼλͺ̹̹̹̹ͺλϼϽоѿоμͻμϽоооϽϽμμμѿѿϽͻͻϽѿооϽμμ̺ͻϽооооϽͻͻͻμμϽϽϽооооооооооϽϽϽϽμμϽŲƳɶıðƳƳıƳǴȵȵƳŲƳǴŲƳǴȵȵǴƳưʲȮǯǯȰɱDZưɶʷ˹˹˻ɹȸƴİïïİİƲdzdzȴȴdzdzdzƲƲƲŲƳȵɶɶȵƳŲŲƳƳǴȵɶɶʷ˸˸˸ʷʷʷɶɶ˸̹ͺ˸ɶǴȵʴɴɲȱȱʲ˳̴͵ʸ˸˸̸̸͸ζζϷζ˴ʳɳ˳˷ηͰ̭ɬʭαϲ̲ʰ˴̵̸͹κκμλϺϹθ̶̶ͷθϹθθͷͷͷͷθθкѻҼҼкϹкѻӽҼѻѻѻк̶ɳθθϹϹϹϹθθͷͷͷͷθϹкѹͳϲδϲϵгϵг̲ˮɯˮ̲αδͳζͷ̶˵˵˵˵˵̶̶̶̶̶̶̶̶˸˸̹̹̹̹ͺͺ̹ͺλͺʷȵǴɳ˵˳̴̴̴̴̴˳͵͵͵͵͵͵͵͵ͺλλλϼϼϼнϼϼλλͺ̹̹̹ϹϹϹϹϹϹϹϹϹϹϹϹϹϹϹϹҿҿҿҿҿҿҿҿͺλнѾҿѾнϼҿҿѾннϼнλϼҿнͺλҿϼϼλλλϼннϼϼϼϼϼϼϼϼϼϼϼλλͺͺͺҿѾϼλλλλѾѾннϼϼϼλѾѾѾнϼλλλѾϼͺͺλ̹ǴìøǻǮʱͶлнͻ˹̺ϽμμλλλλϹϹζ͵δϵʰpyZrQ}nO}nO{lO{mP|`qĽϷѹͷθҽѼннннннннѾѾҿҿҿҿѾѾнͺλѾҿϼ̹ϼλͺ̹˸ɶȵDzīʾĸ}z{Ĩ̮ϲΰ̯ʰʵ˸̹̹˸˸ͺнɶɶʷ˸̹λλϺж̰ȮȬʰ˯ȮƪıƳɶʷ˸̹̹ͺͺͺ̹˸ɶȳȯǬƭŪŬƫǮȭ˲˰̳̱ʹͲʹʹȲɳʴʴ˵ʴʴɳϹθͷ̶˵̶̶Ͷ̴зҹҼϹ̹ʷʷ͸һϹƫĻryV~nJnHpLwSd}ŸɪϲӶζɰ˴־տооѿҾҽѽѻ͵ͳʯèĹȨΰѴҸҺӽӿоооѿѿѿоооооооонннннϼλλͺ̹̹̹̹ͺλϼϽоѿоμͻμϽоооϽϽμμμѿѿϽμμϽѿѿѿооϽϽϽ˹̺μоѿѿѿоμμϽϽϽоооѿѿооооϽϽϽϽϽϽϽϽϽϽоıŲɶııǴŲ¯ŲƳȵȵǴƳǴȵŲǴɶʷʷɶǴưȰǭƮƮǯǯůíƳǴɷʸʺȸƶųŲưůůůưưưưưưưưưDZȲǴǴȵɶɶȵǴǴƳƳǴǴȵɶɶʷ̹̹̹̹˸˸ʷʷ˸̹ͺ˸ɶǴȵʴʵɲȱȱɱʲ˳̴ȸɸʷ˷˷̷͵͵Ͳ̲˲ʱʲ˱ʴͳαˬɪˬαгͳɯɲʳʶ̸ͻͻͻλιθͷ̶̶θϹкθͷͷ̶̶ͷͷθϹкѻкθ̶ͷθѻкϹккϹ̶ɳͷθθϹϹθθͷθθͷͷͷθϹϷгвϲͯʭƨ¥Ŀ½½Ʃ˭ααζͷ̶˵˵˵̶̶̶̶̶̶̶̶̶̶˸˸˸̹̹ͺͺͺͺϼнλȵðƿſſſŭȰ͵͵͵͵͵͵͵͵̹ͺͺͺλλλϼϼϼλλͺ̹˸˸ϹϹϹϹϹϹϹϹθθθθθθθθѾѾѾѾѾѾѾѾ˸ͺϼҿҿѾҿҿѾнϼλλϼͺͺλ˸ɶ̹ҿннϼλλλϼϼϼϼϼϼϼϼϼϼͺλϼннλ̹˸ͺ̹˸ʷʷʷ˸̹ҿҿнϼλͺͺѾннϼλͺͺ̹Ѿλ̹ͺλ˸ðļxrv|Ǯ˵лϺ˸ɶ˸ͺμμλλλλϹϹиζϵҸгw{ZoMxhGvgFufGseH|qUdϷҺͷθлιннннннннѾѾҿҿѾѾϼ̹ͺѾҿϼ˸ϼͺ˸̹˸ƳĽ}||ohbeezĦʪϱѴʵ˸̹̹˸̹ϼѾʷʷɶɶɶɶɶɴūƿ»Ľ»ļ¯ȵ˸ͺ˸˸ͺͺɶǿʾ¨ĨĪĨ¨ɽʾ˿DZưĮ¬ƿĪɯ̴̶˵˶̷̷ϸʴèȿcsOqKnHqM~[qĻʭҵҺ̵̳־ԾϽѾѿҾҽѽк͵̲ŪùxwxľɩвӶӻѻҾооѿѿѿооооооооϼннннϼλͺͺ̹̹̹̹ͺλϼϽоѿоμͻμϽоооϽϽμμμϽѿѿϽμϽѿѿооооооϽɷ˹μоѿоϽϽооѿѿѿѿѿѿѿооϽϽϽϽϽϽϽϽϽϽϽоıðȵıŲǴŲñƴȶȵǴǴȵɶǴȵʷ˸˸ʸȶǴǯƬŭŭƮƮĬ¬ůŲǴȵȶǵIJñƳDZDZưưưůůůůĮůưDZȲɳ˷˷˷˷˷ʷ˸˸ȵɶɶʷʷ˸̹̹ͺκͻ̹̺̹˸˸˸̹ͺ˸ɶǴȵɶʵɴȳDzɳʴ˵̶ȷɶɶʶ˶˶̴̴˱˱ʱ˲ʲʲʳ˱γʭȭʯγϵͳɱȱɲʴ˵̸͹͹͹θͷ̶̶ͷθкѹθ͵͵̴̴͵͵ζθϹкθ̶ʲʲ˳ζ͵͵θϹθ̶ɳͷθθϹϹθθͷϹϹθͷͷͷθζ̯˭Ȫ¢~ǩɬζͷͷ̶̶ͷͷθ̶̶̶̶̶̶̶̶˸˸˸̹̹ͺͺͺ̹Ϲλ̶įļü̵͵͵͵͵ͷͷͷͷͷ̹θͺθλϹϼкλθ̹ͷ̶˵ϹϷϷϷϷϷϷϷ͵͵͵͵͵͵͵͵ѼѼѼѼѺѺѺѺʳ̵ϺѽѿѾѾнϼλλͺ̹λ̶˵˵ȱŮʳһѺѺлϼλμϼμλλλλλλλλ̹λнҿҼк̷ʵįįî­îŰDzɳտҿҾмϻκ͹ннϼλͻ̺̺˹оλ̹ͺϹʴphjlp~ƭ˴кϼʷȵɶ̹λλλλλλλλѻϹкԼӻƬ`pOvfEueDrcDpaDwiNw[}жӻθηη˶ѽѽѽѽѽноноѾԾҼѻϼ̹ͺѾҿϼ˸λͷʵ̵˵īqlijmqtxz¶ȼ´why^uWtV~uV`oſʪдʵ˸̹̹̹ͺнҿϼ͸˶ȳŰʿɿެ˵ʴʴ͹͹ű~}~~¶ƪɯ̴ϹҼθιȱ§Ⱦl~ZyQrJpGwRd}Ʀҵּз϶־׿ѻкѻҽҾҾҽҽкֽӹɮĹtlh`j| ̮ϵҹмѾооѿѿооооооооϼннннϼλͺͺ̹̹̹̹ͺλϼϽоѿоμͻμϽоооϽϽμμμμоѿѿϽϽоооооооооɷ˹μѿѿѿооѿѿѿѿѿѿооϽϽϽϽϽϽϽϽϽϽϽоųȶȶųññųƴñƶʺ˻ƴIJƳƳŰƲȵʷ˹˹˻ʺʷ͵˯ǭ΍¨éĬŭȲ˵˵ȲůűŲȲƱȳ̷˶ŰíĮ¬¬¬íůȱȲ˴ι̹˸ʷɶɶʷʷɶʷʸɷƵƵȷ˹ϼλͽλͽϼϽо˺˺ʷȶȶȶȶȶ˺ʹɸȷȶȶȵɶ˷˵˵˴ʳɲǰƯƯưȲʴ̶̶˵˵Ͷʳɴ˴Ͷ̶̶̴ȱͳʹʱɯͳжзиз˲Ǯɰ϶зͲʹͲ̲ͳͳδδϴйлͶɳʲδ̲ǭǭ˱δ˳ʱ̳ϹҼθϹϹϹϹθͷ̶ͷͷͷͷͷͷͷ͵ʯʭħuic^ckwåɯ̶̹κ̸ʶɴʳ˴˴ʳɰɱ˳̵Ͷ̷ͺͺͺͺκκθϹϹ˳θʳɾ·wljkmtȾŬʳйл̶ȵʷͺ˶˴̷Ͷ͸ηкиθ͵˵ʲɳ˲ʹͳжϵδ̲ʰȮȬȬǫƪƩƩȫˮαгʹ϶ѸҷѴϰέέɪǪƫDZʷ̼̽̿ѿ͸ʵ˶ιл͸ɴɳªľüƻƨǫ˯϶лͿͺdzʵѼϼϼѾκ͹̶ͷйжǬɽȽǰ͵ѻҼѺлѼϺ˶нѾҿҿϿͽ̼̼ͺ͹кѸ̳ĩf`}Y{X]g˵ҿͺκιι̷ͺλͼ̻̽ο˺̻нϼιǿttUo`Ao`An]AiXtcIqWnƺɯ϶Ѹѷеѹиιлҿξ̼μϾҿӾһѸϹϺʷʸϽ̹ȵɵ˵˴ƪ˼ðm[yNvK|Qcuſæɬʯɰɱ̴Ϻ˺̺òt{^ynPulMvkM~uVk©ʷŴǵʷʷɳ˵ийȮǽxy|}{qihp|ķˮ˱˻sy[wWvXz]gu¸z`[}YZ[an}zspmifkzĻƩʱ˶ͺ˸ȵʷ̸ư~~UqBug8uh;vi=wkAwN^wƦбҵչֺԸѴԷֻֽսҺкϺԽҸͰ˾n\{SwMUfʽαԼнҿѾϽϽϽооѿѿѿоϽϽϽϽоѿоϽϽϽϽоѿϽμμͻͻμϽϽϽϽϽϽϽϽϽϽѿооϽμμϽϽоооѿѿѿѿоϽϽѿоϽϽоѿѿоооѿѿѿоϽϽϽϽϽѿооϽϽооѿѿIJƴųñ°°IJIJñŲʷɷ¯ļ·ûııİŭǭɯʰ˱ʰȮƬëƮʲ˳ɱƮĬëĭíDZư««ëëĬŭǮɰɳưǰdzDZƲưűİdzȴɶɶȶȶɷ˹λлϼмϼ͹ʷɶȵȳdzƲŲŲŲŴ̺˹ɹʷɶȳȳȳʱɰȯƭŬŬíí¬íĮưƳǴǴȴ̸̷̷̸̹ͷ˵ɱȮȮȭʯʮʮɮ˰ζкϹ˵˲ε϶̳εε͵ͳδδϵеηηʴǮΦ¹ĪϷθθϹϹθͷ̶̶̶̶̶̶̶̶̶̴ʹȫixQ~kCzg=i@oE}UnƽħƩȳɶʴȲȱɲ͵Ϸζ̴ʱ˲˴Ͷ̶˵̹̹̹̹͹͹ηηʹɰ˴ɲȼoqSyiHscBtdCwhKuXw©ȯιιɶǴȶ˸͸͸ͶͶ̵̵̵̵ʲ˳̲ͳδͳͳ̲ĨƿĽĽü»üĽæȫ̯ϲҹҹеͰŦûʼͻιϺҽӾ͸ȳzsswɳͺϾξκ˵˷Ϻϼλнκκ̶˳ͳ˯Ƚp|]~oPrU}`sƼǰ͵ϹͶ̷лнϼ͹κϼнϿϿϿξϿͺ̸̶ε˰Ũd{WsNrLxS`ĺƲϽϽ̻͹ι̶̷ιϼν̽̽ʾ̽˼ǶλϺīqpTq`Do[@p\At`GudJt\s©жպҹδζ͵͸ϺнҿξͽμϾҿӾӼҹкϺʷɷ˹ͺʷɶɵ˵Ǯɾ{j\vOwQ}Xf|ʽťˬͰͳ͵̶̶ιιλͼѾμɲ¬»qvWvpPpgHpjJy\uȳƳǴ̶ͷʴȳʳǭĽwkhnvɿ©ȾzkcafyŪŷnvXuUuW{^nǼ­îĭì¸r{YuOqM~pKuO[r~zvqużū˵͸ȵȵǴɴ˵ëy|S|nAqc6qd7tg;th>xlD}tM^wŧͯѴӶҵгԷؽӾлϺһҺγr`{SvLTd~Ȼ̯ӻнҿѾϽϽϽооѿѿѿоϽϽϽϽоѿоϽϽϽϽоѿϽϽμμμμϽϽϽϽϽϽϽоооѿооϽμμϽоооѿѿѿѿоϽоѿѿϽооѿооооѿѿѿѿѿѿоϽϽϽϽоѿѿоϽϽооѿоIJIJññ°°ññ°°ǴƳĽ}ummuĽſƾĪǭȮɯȮȮëŭǯȰǯŭëʿůĮʿȽǿ«««ëëŭƮǮȰʱʲ̳̳˲ʱȲDZɵʷɶȶǵȶʶȴ˴˷ͷ̸ʴǴưDzȳȴɵɶʷʷʷ͹̸ʶɵȲɲɰʰɰȯǮƭŬŬůů¬íıŲŲƲ˸κ͸̷̶ͷɱĬĪüĺƻźʼʲϹѻͷ˲ʹʹ̳εε͵͵ζδζϷ̳̳ʱĪĻzvtw|ɯ͵θθθθͷ̶˵˵˵˵˵˵˵˵˳϶ɬ»kyUnF}j@mDsK\uť˫ȬʳʵɴɳDZɲ̵Ϸζ˴ʱ˲˴̵˵˵˸˸˸˸˷˷̵̵ʱƭȱȱɽlnPwgFrbAtdCwhKvYyƭ̷͸ɶƳȶ˸ϼιηͶ̵˴ʳʳǯɯͳϵδɯé»»Ľžƿ¦ũɭ̰βѻѻзʯ~sljo|˼ɹϿ͹ιϺѼӾѼ͸ɴſ{ohfpuŽĮʶͺмϹ͹͸̹˸ͺн͹κͷʲ̲ȬdqPo`?m^?viI{^~Ŭ˴θʴȴ̷λλ̸̸ͺλξϿϿϿѾκ˷̳ʹ˰Ʃ¤f{WrMpJxS_¸űϽϽͼκϺθ̷ιϼϼͼ̽ʾο˺Ƴ̹ҽ˲ƶy]s_DnZ?r^Cq]DkW>zfN{cĸͳԺҹε̳͵ζιλλ̼˻ͽμнѾҽҽѻѺн̹ɷɷ˸˷ʶʴǰi~YvQsN~]j|̾ǫ̱γж˴ȳȲɶ̸̹ʷǵʷ̷Ͷϵɭi`ztTxrR}`vƱDzʴζϷ̴ǰƬ~od^eq¸Ŭʱʱ˲ʹ˰ĩú}mc_bk~Ⱥ´ucbdn̵ʵ˶̵̵ɰƼmzXsM~nJ|nIuO_{˥ͨʦź©ʲκϼɶɶȵȳƭȺixRnCwf;vgs^Cq[CfR9p\C~mSpͲѷѷϳεϸη˷ɵȵȵͻμϼнѼѼѺѺнμ˹ɷɶ˸˸ʵ«o^vQuPxUhzĸªǮDZư̸ȴñıǶʹʹʸƳǴɳ̴жαzrowľǯɱ̵жж̰èżxmc^`l|¬DZDZưDZɳͱ̰ǪüwcxTvS|Yivtxǻư̵̷Ϲηη̳ȾkrQmGyjCzlGuOc¼ΫаϯȪ¦ɿǻο;Ϳêȳ̹ϼϾ̻̹ʷȱ§z^tPpHlD}kE|jDyiE{kG~pK{X\hĻ˩аϯгҵѷѸϸϺѽҽϻѻε§v`xStJ|Qb|ǺˮӻнҿѾϽϽϽооѿѿѿоϽϽϽϽоѿоϽϽϽϽоѿѿѿоϽμμμϽϽϽϽооѿѿѿѿооооϽϽооѿѿѿѿооѿооѿоооооооϽϽϽμμμϽϽоѿоϽϽϽϽϽоñ°IJųųIJñųƳĭhynRrdGqbEo`Am^AsdG|nQ|`n{º»¼ſĮưűİİů̳Ͳγγͳ̲ʰɯĭūìȽĹ·ƻʿ¤ĦȮǭǭƬĪſľſſſ¯ðűȳιϺ˴ɳ˲ĬzfgjjuưϹͷDZưDZȲʴʴ˵˵˵̶˸̶ʱȮũɾµpayVrQvUarɬ̵ͷͷͷͷ̶˵˵ʴʴʴʴʴʴʴʴɰɮĨhxToI~jEmGwSeöƩǮȲɳʳɱɰʱ˲̳ʱȯɰɱɲȲdzʷʷɶɶɵȴɲȱŬìƯ˿ĸqxgIrbAo_>rbAvgJtWtɿŬ˶̷ɶƳɷ̹λι͸̷˴ʳɲʰȮɭʮǫžæŨ˯˱̵̵̲͸ιιͺͺ͸˱ra|XzW_r̾ǰ˵κι͸̷̷̷̷͸˵ͷɲsdzWuRzVewʮ̳Ϲм˶ƳǴɶɶ˷ϻθʲʰƪrwTxjEl^9j\9na?rRs˿ɰϸͷɶʸ̼ͻ˵˵˵ʷ˹ͻξϿϽ̹ɶʴ̴˱Ǫ¥atPlGlFuP|[|¸űоϾмϺθ̷ιѾѾν̻˼˿ǻɺпǴɴ̷ëνz_t_DpX>s[AoYAjTxkIeɿDZȳIJǴȷʷ̴˳˵˵ʷ̹ͻμμ˸ɶ˵͵˱Ǫ¥_sOlGnHvQ|[{İоѿνκ͸˵̵ηѼѾν̻˼˼ɺɺппȵʵ͸ë˷~tZv^DoW=pX>qY?p[@nX@p\C|hO}cvƸɯ̰ϴϵζʴƳƴǵ˹˹˸̹͸ιϸϺ̹μ̺ǵȵ̷ʴƭewVqPrOyTaªɱɲɲɲƯí¬¬ĮDZɳɳθͷεͲ̱ɬɪʫŤ¡¾§īŪȮͰͱɬä¹o`|YzW^ivè˴йͶʳ˴˴ȱ˯ѵӶͭp~Z~vR|tPtTc˽¨¨Ī˳кѼкѻϹʳɲȯǽkqNmE|jD|mFwTiãʮϵиͷ˸ͺν˻˻˸̻ξʹ˸ɳçĹjuTnMkKkKoNwV^`_}[vSuR]rķ ̰ͲͶ͵̸͸λнϼϼʶ˼q{]rPpG{Pd̿αӻԿнҿѾϽϽϽооѿѿѿоϽϽϽϽоѿоϽϽϽϽоѿѿоϽμμμϽϽооѿооооооооѿѿѿоѿѿѿоооϽϽμμμμͻͻͻμμϽоѿоϽμμϽϽñžñųȶǵųųǵʷbzkLteFueDueDl\;l\;m^?reE{mPy[hp~k|rYtjQsiNuiOwkQxjPpWgzzyxz}¾íŬ¥½}{zxvuuĽžžſſſſ®İƲŰ͸θDZƮȰ|_peIrgIwiLvhKvYsļȴɴ­ðŲǴǴǴȵɶɶʷʶȰƬçʿƹr|XoKygCmIvUgƦ̲ͷͷͷͷ̶˵ʴɳɳɳɳɳɳɳɳ©ʱѷϳȽk]zgFweAuc?zgFuSg|ŷǻƭɯɯʱ˲ʱȯƭƭDZȲƱŰɶɶȵǴdzƲǰƯƭ«ȼ¶oyhJp`?jZ9l\;qbEsVyƭ̷͸ʷȵʸμ̹ͷ˶˶˴ʳ˱˱ɿúxpjfwţȨȨʭͱʰʳ˶͸λλͼͼʸ˸̷˴m|VvQtOyViȽʮͲ˴ϺҽԿҽϺ̷˶ͺѼл˱Ʃi{TvmFulE{qMzWmȼư˷ɴƳɶͺ̹ɵмкʲ˱ǫh{mHuh>pb;oa:m_:qc@xVo˿˾IJƳ̵̲˳ʴʴ˸̺ͻͻ˸ʴ̶͵̴Ƭ`uQnIqKyT^}ïϽоͼ̸˶ɳ̵ηѼѼϼ̻˼ʻʻɸϾнɴ̷ϸŭƲypVv^DoW=mU;qY?w_Eu_Gs]EvbInU|cr̾ʭαгδʲȲǴɷʸʸʷ˸˶͸ηкʷͻ˹ƳDzʴʱ¨uzYnMmJpMyTb¨ū¨ĽƼǽȼɽ̾Ϳ¨ƮȯɰʯǪƧɪͮʩǦĥ¥èĩŬīʭͯˮƦżs^uR}pMrO]oǾ¨ɲη̵˴ηͶȱṵ̃ϳʭ¢zcyU|tP}rRa{ͿƬͳɮƬɰϹԽԾҼӽкʳȱǮƺjrOlE|hC{kGuRhħɯζϹ˸ʸͽʻʺ˻;˿ɺɸɵŭƼ|_lImNjKjKsSdsvr|[wUuQ|Xbnŷɬ˱϶ϸθ͹ͺνϼλɵɺnx\pNoF{PeϲӻӾнҿѾϽϽϽооѿѿѿоϽϽϽϽоѿоϽϽϽϽоѿѿоϽμμμϽϽоѿооооооооѿѿѿѿѿѿѿоооϽϽμμμͻͻͻͻͻμооѿоϽμμμϽ°ƿIJIJIJIJųƴǵȵjpQqbCm]p[@s^CnY>s^CxcHxcH{fKwYvżɬϳɯŮ˶ͺȵǶijðƳͷкζʴɷλ̺ǴȳʴĬƸbsR}jI~jGlInIeæ½³³ÿäƧɨί̭ȫǪǬǮůŬͰ̮Ͱ˫r\yTxjEykFrM_vǾĥƬȲ˵θϹϹθͷ̲̯̯ΰ˫ýv~]}tS}tUgʾȮϵϵζεηθ̹͹ɶ˵ȱĭêƺivSnI}iFpL^vȫϵимѾʽ˼οοʹɵëgvP|hCjKkLnO}^|ɴï{a{WuQuQuO|Uk;̱ջӺϹκͺ˸ʸʷűŶhrVjHlC{Pg̿αѹлоѿѿѿѿоооооооооμμμμμϽѿѿѿѿоϽμμͻ̺ͻμϽϽоѿооооооооѿѿѿѿѿоооооооооѿѿϽμϽооѿѿооϽѿϽͻ̺̺ͻͻͻ°ƿ°°°ñIJųƳƯɿ~iwX~oP}mLwfHpQ`s»ýŽļźźqymShZ@eWj[:|mNx[fu˺˰ͱϳжϸϺϼϼҿϼ͸ιη̵Ȯ§bwSoJpJwR|[{®˹˹ɸʶʵȲʰ̵̷̲˸˸˺ͼ˺ɸλͺƱʳͳ¬ofMrZ@pX>nY>r]BxdIr]Bu`EwbGu`CwbEnQdsˮ˯̲ϸϺɶɸƵŲǴͷкθʴʷλ̺DzȲʲ¨Ĵ||[qPlKkHmJmHbv~}{{ƭɰɳɰͰ̭˫Ťm{VtP{lE{lEqM^vȿƩƯȲ˵θϹϹθͷ̴ʰ˱ϲͰãg~_~_r÷Ĭ̴ζ϶εͶθͷ̸̸ʷ̶ȳĭªǻhuUnJjHrQazȬϵϹϻϿʽ̼Ϳοοо̹˵ū}asI{h@{gFkNrSdʵűb|XuQtPqKtNayó¥ϴҷϸкλ͹ɷ˸ȲȹlwXnLmDzOeгҺлҿоѿѿѿѿоооооооооϽμμμϽϽоѿѿѿооϽμͻͻμμϽоѿѿооооооооѿѿѿѿѿѿѿѿѿѿѿѿѿоѿѿϽμϽооѿѿооϽѿϽͻ̺̺ͻμϽ°ƿƿƿƿƿñðǰūȾ{smenǻīǮȱðð𯭭ȼu}oUk[AgW=hX>iY?rbIt]~ĵ«ƮŭƭƭŬŬƬūũũ¤üùƼȾù{veyX|sR{ZnĿªīŬŬŪĿn]tUzmMviIzoOwYg{ŽưưƲȵʷɵɵưǯƮĭ˱ŬvxmQeW:eW:bS4`Q2jZ9qbC}`sĺƯƱDZȲȴɵɶʵŮŬĪũƪyxT~lHye@|hCrM_;ȯ˵ͷͷʴɳȲȲʴʴʴʴ˵˵˵˵̶ʹ˳̲ʮħĹpsRxeDjW6iU2nZ7lX5zgFb˼ȫʮ̯ʭŨŪǬɰʱȲȲȵɶ˸˸˸ʷ˷ʶ˴˴ƭȯìŽƺltcEm]}iFvVyǼŰ̹ɶҿѾммͷ͵ѷ˯qtOpa:td@sc?scArb@jZ9zjInPrUx\izŵǫͰѶҹкθκϻѿϻϻϹʴêƼ~]sOlGmGuP{Zz®˹ʸǶɵʵʴɯ˱˴˶ʷʷʹ̻νɸ̹ʷįȱ˱˿jw_GnV>mX=mY>xdIuYuWqS}jLxcDvaB}fGpQwXwɾȫʹѻϻȴ˸ɶǴɶ̹Ϲͷʷɶͺ˹ȳȲʲξptSoNlIlInKnKdz{ywuw~|wrsy~ªʲͷͷϴˬǧǾetPpJmG~lFqM^yȬƱȵ˸ͺλλͺ̹˵˲˳ϵжˮž}vwʿȲ̶θηηθͷ̸̹˸͹θɴĭªǻjvVoKlJuTd|ɭϵϹϻξο̿˻˺̼ϿϿпоϽ͸̳ũsVmBxe;t`?{hHsSauu_zYvRuQrNqM~ZjôŨβ϶ѹϹͷȵ˶DZȺlxYqOoFxMaãҵԼѼҿҿѿѿѿѿооооооооϽϽϽμϽооѿѿѿооϽμμμϽϽϽоѿѿооооооооѿѿѿѿѿоѿѿϽμооѿѿѿѿооѿϽͻ̺ͻμμо°ƿžžžžƿ°¯íììƼŻƯȳ˵Ǵƴıðëw~pUk[AgW=iXscAvfDyiGvfExW~]{]z\|^frźŦгպӺϹκмоϽκϻθɳù|wV~lH{gBjDtO|[|®˹ʸƵȴʵ˵ɯʰ˴ʳɴɶɸ˺пʷʷȳìǰȮŹzbu]EnX@nZ?o^BsWp}ozY}jJxcD{dEfH}hI`sźƬ϶Ϲ˷̹ʷɶʷ̹ͺͷʷɶ˻˹ȳɳȮ˺kqNmJkHkHnKsPmƣơ¾}uqqrty~ʴͺϹҷˮŧú_qMnHkFkHpObƯDZȵ˸λϼϼͺ̹͹˵̶ϷѹϵȬɾdz˹˷ϺиϹθ͹͹̹̹κϹɴūëȼmvWoLmKvUf}ʮѷѹмнϾοͿͽ̸̺ͺλнѾмϻͶ̱¥hvK{h=vc8uc?zjHqQzXclkd|[{Z}Y\|XxT|X`rŶĦɮ̲͵͵ʴʳŬ~fvVrMqGwL^ĤԷԼҽҿѾѿѿѿѿѿѿѿѿѿѿѿѿооϽϽϽооѿѿѿѿооϽϽμϽооѿѿѿѿѿѿѿѿѿѿѿѿѿѿѿѿѿѿоϽооѿѿооѿϽͻͻͻμϽ°ƿƿƿƿƿ°ñ¯įíª˿ĪĪëíŰDZȵʶȵǴIJ¯ǿɾɾĬu|lRjY?gV:jV;gVxcDvXs¥ϰͰȫǬɮ̳ʹʴɳȵɶ̹˸ʷɶʶʶ̵̵Ǯȯ«ƾȼ~dudFn^=l\;o_>ufIy\ȽīǮʵ˶ʷʷʸ˹κɵ˸ͷɲɯǽhsVvgHteDueD~nLhżũʮȮƬɯ̵ͶͶ̵˴ʳʳʳ˶̴дѳЯƤw^uOrMpL{Yuħ˱ζ˶̷ιϺϺϺι͸ккθ˵̵жжͱhziMq]Bq\?w_CzeJkøŰ˸Ƴλ̸̹˷Ȳǯ˱ƪosRo_=rb@qa@ueD~nMtTnusnd}]_itǪѶӺкϻϻ˹ʸ˷͹ͷȲɾ{vU|jFwc>~f@pKxWy˹˹Ƶdzʵ˵ɯʰ˴ʳɴɴʷ̹нɶɶɴĭŮƿqrZv`Hs_FraExiLfǸa~kKvbAxaAzaBwcBrQ_u÷Ʈθϻ˸˸ʷ˸̹̹ͷ˸ȸ˻ʸDzȰǬǶhnLkI~hC{gDnKyVxŸʪʨǧǥŤâȿȿǾǾǼǼǼǼ|yw{ɴ̹ͺԻ̯ŧú_qMnIiD}iFqPfîǴʶ̸ϻммϻκϼλͺκкϷ˳ǭƬ«­ȶϽϿ̼Ϻкϼϻλͺͺ̹κθʵƬëȼmuVnMkLsSd|ɭҸҺҼҽнпϾϼ͸˵̷θѽӾѼи϶ˮľ`rF{i;zgm]m?xi@ykFzlG}oJvQ[hoyxkc\_kz̲˱ʮp{ZuRvQtJwL_ťгӻѾнѿооооѿѿѿѿѿѿоооѿѿѿѿѿѿѿѿѿѿоооѿѿѿѿѿоϽμμϽоѿ°ƿƿƿžƿ°ñŵijñijƳݬɾIJǶȶǷĴijĴĴIJIJ¯ǼƻǸgp_EcO4dP5hS6eT:k_G~i­˶ʵȳȲ̶ʴȲůĮŮƯDzƱųųųıïíhziOo[@lW:kV9kX:qdDbƫʰɯʲɳȴdzǴȵʹʷɱʯƮ¨¦q{YufEl]>l_?peItkNgDZǴǴȷɶ˵ȰʰȮêǮʾhpbE`Q2eV7dT3aQ0fV5jZ9p`>td@|lJvSyXbuǮʱ̳͵DzŰ­pyX|lJsa=sa={iE{WyƷƭȲʴ˵ʴʴ˵ͷͷͷͷ̶̶˵˵˵ɳ̶˵DZɱȱ¸gyjKtdCn[:r[9t^9kS-u]9t]=vaBmMeȪʰ̱ʹ̳ɳɵʷ˸ͺ̹ʷȵɵʶ̵Ͷʱʱĭ˿x^veGp`?m]ua>~jG}Yɾƫʯɮȯʱ˲ɰDZưűűƲȳȴɵǮǬƪƩĦ£ɾǾĹżʾŬȵ̸ʺʷθ˰ȫguRqLkIhFkLc°ŲǴʶ͹мѽѽмϻннϻͺͷ̹ͺʷǶʺ˺̸̹˷˸ʷɸɶɵ˵ʵȮŭȼfrThJ|aDiJy[sƩѴѶѸѺҺҽҽѻг̯̯γӺռӹж̱þfwN~k@j;m?xi@vjBymEtLYgv´Ⱥ˻uf^_gsçƽmzYtQuNuKyNaãͰѹѾнѿоооооѿѿѿѿѿѿѿѿѿѿѿѿѿѿѿооооѿѿѿоϽμμϽѿ°ƿžžžžƿ°²ĴȶȷǴŰ˹ʷȷǵƶŵŵŵ°ñǿļĹ{bl[A^M3cO4gS8eT:l`Hk­ʵ˵ɳȲʶɳưůĮůưɲűųųIJ¯Ƚ|qXiX>bN3cO4hS6iU:pbGlƫ˰̲ʰǯɳɳȴdzȳȵʷ˸ƱʲɱƮêī©óbwgFj[{gDoNnüͺνɶdzȳɳʱ̳ʹͶ̷˶ͺλϼɴ̷̶qtfLl[AnZ?kZ>n_BqTl|}od{XwTxT{WwTrO~lHzjH{kJtSdwëůDZɴɴ˵ͷͺλͺƳª§|^}iH|hEwc@t`=}iF{XDZ˵ɳDZɳʴɲȴȳƱƱƱȲȵɴɳʱʯʯʯʯɮɮƭƭDZʶͻμ˻ɸ˵ɰȭ¥jwSqNnLhH~iLz_³¯ȵǴʶ͹мѽѽмϻҿѾѾѽϼλλϼ˸ŴǶνʹʷʷʶɵȵȵǶǴdzʶʴǰǮȼdpQgFy^@|eEvVpƦΰϲдзѹҺѼһеͰʰγӺֽӸγŨwZ|mFwf;yf;{h=pGqJuNZjyŶοƦˮˮƧ̽ug]]doĹķkxUqLqHvKzOeãɮиҿѾнҿоооооѿѿѿѿѿѿѿѿѿѿѿѿѿѿооооѿѿѿѿоϽμμоѿƿĽ»»»üžƿ°IJųƴǵǴǶȶȶǵƴųIJóĴ²ƿƿžý~rZgY?eUvaBp]?tcGy_v´Ĭëʻôunr|ȻƩĩ̲ϵɲɵʶ~wVyiHrbAscBziK}oRoŽλϼȶƴʷͺʷ˵˵̸ιιͺͺ˵̷ʶwt[udJgV:m\>o`ArcDsSfpm^[\bnsmf}XuSqOsQvS~Zk|˽˯˰ȮȱͷϹнɶɾī§cxiJwgFwdCwcBiIyWdzɵƲŰȳ˶ͷƯǯȰɱʲʹ͵͵ͷ͸͸ιι͸͸̷˸˺̻;̽˼ʻ˸͵Ͳ˯¥mzYuSnMjKjMv[xɶȲůDZθѻθͷкӿҾмκ͹ϹкҼҼкϹϼҿҿϼ̹˵˵˵ɳȳDzɵʶȳɵ˵̵ʱƺ}_jHfA|b?{e@rNhͯαдзҺӻѼѼиһӼкиҸϴʮɾmxUzlGxiBwhAuf=~lDtN\p˻êʳɱɱʲ̱ȭæƽ~nb~[\j{®|ezTrJpFvI|OgŧѸӾҿҼҼҿҿѿоϽϽϽѿѿѿѿѿѿѿѿоϽμͻѿѿϽμμϽѿƿĽüž°°IJųƴǵǵǵǵǵǵƴųIJññ³ƿƿĿ½{qXcW?dT;`P7hTykNkūɱưȳ̷˵ͷůʴDZíǿzaj`E\N3`R7bT7bU5xiJh||se_}_eqǼŨȮűóžptRtfCj\7m_:wiD~XyǽȯȳDzȳȳɴʵʵ˶ʵʵʵɴɴȳȵȵ˷ȯŨƤ{]uH{lAvg@yiGsW{c~j~g}rVulKsfFteD|iIsTfvƲϽDzȷĴƷ˼ɹɷʶʶ˴̵̵ʹʱǮǿɽ}t^uaHp[>lX=p\AwfLz`Ʈ˱̵ζͷ̶ͶͶѼ̷Ͷ˵Ȱɱĺy^zkNveGlLmNqPjĽǰͶʳƯƯɰɰεε͵δζδ͵͵θθδĤcvJ{nAyl?rFyRoʾʿƴDzɴʵ̷ιϺллξͻ˺͸˵ʯǫŨƷa~kMvaDu`CyaGxdIiľȰθɳįɴͷ϶̵ͱ̲¦`~jIt];wbCs`BudHz_y¶Ʋȶų˾ķysuw}öȹ¥ƭƭDZʶñ{uTxgIp_Ao`CtfIzlRmǼ͸ιȶƴɷͺȷʷ̹κ̸̷˵˵̶ͶȲĸf}iQp_EjY;l]{aqdBuhHzmMxXaruŽšǣţİhvS~jG|jFtP[kvƬѸзɰDZ©ŻǽǭȬm}pPxiHubAua@iGvTzŷĮκ˵Ưĭǰȱɱɱʲ̴ε϶ззкϼннннϽϽɹɹ˻ͽξкϴͱɫfxWyXrRmMqSdijĭııƳ̹λͺλѾҿѾλͺ̹̹̹ͺҿѾѾҿҾθ˳ȲȲ̷͸̸ʶȳɴɳʲƭzyZ~fD|b=y_qMhͭϰдѵѺһѼѼιлѼ϶δγɬ£oyWpL}mI{kGvQ`isŵɯϸҽ˸̺ͻ͸̷ζиҵг¢|l`_afjmfZwMsJvKT_yŽгռҿпӽҼӺҿҿѿооооѿѿѿѿѿѿѿѿѿоϽϽѿѿѿооооѿѿоƿĽ»»»üžñųȶȶɷɷɷȶȶǵųIJIJñ°°°°ô³°°İ®Ŀ½ukR]Q9`P7]M4dP8\J2j[D{bǿɳDZŲƳȵdzŲưƯƯĬŬĮĿ¬¬ſ¬lmcJbS<[L5]O5aS9cS:xiRvƲȳ˶̶ʴηϷζ˴ɲDZůůůůĮí¬ĿÿŨʼbteFj[m^?qbCwiLcž˳ͷ̷ʵȴ˷ů˵ȰĬ~tYlcFfX=gY<`R5^P3|mPwŨƪĩwkdegouĹswUxeDr`o_;|jFzYǯ˳ϹʴŰɴȲ˲ɲɭǭƻztU~jIv_=xcDo\>l[?qVwĺ˷ι˸ǴųɷɶȰɬƥǾrkz_dmxf|lJvfEp`?l]>pbEykPq̷͸ɶɶ˹˺̺̻̹˷ʳ˴жԸʭúfpQqbCkZkHqLiƽγҷͲʯ̰ͳƯƯȲɳʴ̶ͷͷϹϹккϼϼλλϽϽμͻͽ̼̼̼лижγȫm`{YvTwU{Zf{ķɼ˾˾̿̿̿̿ʽ˾̿̿ʽɼʺ̶ȰĮư̷Ϻ͹ʶȳȳůĬȾqpQ|eC{aq]:lX5nZ9|hGbɺǮȳɴʵɴȳƱŰƱƱƱDzDzDzȳȵǷʹʶĪyY|pFxk?|oC{Ogöƹ·ƾufwU}jIyfE|iIyiHsToüūɱȳʷ̹λλ̸ʶȴȴıƾȻ~s_nZAiT7fR7kW{j>qE|Rsʭˮ˱̴͵̶ͷλϼμ̺ɷǵ˵ɰ¦wXzmCvi=wj@xkA}oJfĮ˷ʵ˸ͺλϼϼϼϼͼѾл̵γҵɩƼ~fyR}oHwhAvg@tPhʲʲͷɳƱ˶Ȳ˲ɲɭƬĹxrSkJs_n_Bv[{ǽʵ̷ɶŲų˹ͺͷͲαˮħź~o}b|az`emvyx~]wgEvfEscBo`AreE|nSt©Ͷι˸˸ͻ̻μ˺ʶɵ˴δдѴŦszXwjHo`?k[:gX7m`@qdD{nNy\fvĸǮʱ̶θι̷ʵɵȵȳ«Ƹo`yXzW|[aiwɻ¦ũƩŦǧǥſdyVykHyiEpLpK_zŦͮͮͰαβɯȱʴ̶ͷθϻϻϹϹкккϼλλѿѿϽͻ̼̼˻ͺιлй϶˱æmb}Z{Y{Y[jĽƶɹʺͽϿξϿϿϿξξξξͽ˻ȸǷȸʸ˵ȰưȲ͸Ϻϻ͹˶ʵDZĬȾp~oP}fD{c=waaS9cU;dV;bT9bR8|nT|űŰϹθͶζ͵ʳȱưůĮĮȮɮɯʯȰǯįĮ˽´~~_vgHm^?n_@paBqcF~`üǯɳʵ˶ǴʶİʴƮſ|xoRg^?cU8gYzl?}pFbyĽũʮ̲ɲ˵̶ͺμͽ˻ȸƳɳǬwXzlEvi?ug@vhA{mHf¨Ŭʴʵ˶̷͸ιιιι̹ѾϺ̲Ͱߟr]wOuKsItJ_{ɯζʲ̶ɳƱ̷Ȳ˲ɲɭƬĹwpQzgFp\9ubBq`BwhKeȳ˶˸ɶʷϼϻͷ˵ε̳ǮèƼwl{^uXuXy\}`asRp`>ueDtdCqbCtgGqVw¸īϸϺ̹ͺϼϼͽ˹ɵʶ̶ʹʭƧgsPreBob?qc@teDylLx[jxʾƭ̶̸ͺλνͼ̽˺Ƿ˹˷Įɽue^{XyVzW`qǼĥĥťƤĢufxUrNuQpL}Yk~ȧ̭Ͱʯɯɯɲ˶ͷͺλλкиѹѹѹѻккѾнλͺ̺̺ͻϼ̷лѺε̲ʮ~n`Z{VyR\püŲ˸ͺ̹ͺͺλϼϼϼϼϼ̹ɶðüư˵ʲʴ˵ιлѽҾѼϺ˵ǯrqR~jG~fByb@weAqOi̯α϶змѽϽϼϼһӺѵʭ˼h}XxPvN|Vcwť˱϶ӺվվԾѾпҾͷǯźxg}X}V~UT]l~ϯӷϸҼԿӼҼҿҿҿҿҿҿҿҿҿҿѿоооѿѿѿѿоѿѿѿѿѿѿѿѿоѿѿѿооϽѿѿѿѿѿññžĽü»»üžƿ°°ñIJųųIJIJƴųñ°°ñųǵȹƷƴųİĿ{i_FVJ2_O6`P7hT<^L4l]F~eɳDZŲŲıƿíǰȱëȰǮȯƭǯ̲yuZ`V;`R8dVm^?n_@l^Az\»ŭDZʵ̷ǴʷİɳŭľyulMd[<_Q4eW:dV9fX;v\ļɳƱȳ͸ȴDZŬ˿Ĺte]xTzWyVwR{W~ZrMuauh;uj=zVlȬʮȮʲ̶ͺμλʺƶŲȯƩx[{mHvhAse>tfAzlIfĨŪɰɲʳ˶̵͸Ͷ͸Ͷ˸лϸʰʭʩŻh~VuMxNzP}Vl¥жҺ˳̶ȲƱ̷Ȳ̳ɲɭƬĹv~oPvcBmY6vcCveGqTnūDz˶λ̹ͺϼ̸ȴdzɳdzíĮŬǻydqTzkNzkN}nOrS}mKl\:scBueDrcDuhHrUyĺƫϸйθλннͼʸȶ˵϶̱ħĻ~s_|oLpc@pc@zlItRy\q¶Ʈθ̸κ̺ʹ˼̽ʾɽŹ̼ͼ˸ȱīwg|YuRqOsQ^l£áƤȤɥšr_zWzVqMzV`rĺâʩˬɬǫǭǰȱʴ˸̹ͷѹҺҺӻӻҺҼѻкλͺ̹ͺμϽоʶлмͷ̴ͳǪȿxdZyTtM}Seu¼ªɯ͵̲˳̴͵ζϷϷϷϷª¼˳͵θθϺѼӾθʲêttUkJ~hCxdAxfBrPiƿ̰ͳ϶ϹмѽϽϽ̹θϷβƩĵuYyQyQzR[l;ͰεѹѼҼѻкмҾҿпοомкȮɾygccdm}ľ˫ԷӹζϹԿӾӼҿҿҿҿҿҿҿҿҿҿѿоооѿѿѿѿоѿѿѿѿѿѿѿѿоѿѿѿооϽϽоѿѿоIJ°žĽü»»üžƿƿžƿñƴIJųųƴƴųųIJƷǸɷȶȴűĿoj`GZN6]M4`P7cO7`N6gXAvǿDZíɶıŲűıůĭìǯɰ̱ȭĪɯ}z_bX=_Q6bT9eWo`AteFk]@~`»ªůȳȳȵıdzʴȰƬsvmNd[:^P3bT7cU:j\A{bǿDZ˸˸ʷ˸ʷƱŰêȿĹwk_xU|nKyiE}kGkF{gBxb=u]9v^:qZ:mV6wbEdξưƱDzɴ̷˶DzîƱDzȳƱîįȳ̷нưp|X}qK|oEvLYqʹ~t}^vW{lMufEzkJwUcxƸŪδ̳dzɶ˹ʸȸƶȿƽɾu^p\AmX;iU:hT9n]Cw]¨ɱ˱˴̴̶˵˴˴ʷɶdzưůūƪȨĻx`wMzoBwl>xm@wS]nƽŨɭƬȱ̶˸ʶȵǴdzǮɬæv]qLykFqc>xjExUløèǮ̲ͳͶͳʳ˱˴ͳ˷˵̳̱ȩús_~U|S[gvȾͰɯŭǯ̶ϹϺ͸ͷʹʳʮĪttUxeDt`=p]=udFy\yɴ̶λϼϼͺʵȳ˶˶ʷɴƲůĮŬ÷pz]|nQtfIrcDxhFjZ6scAscAqbAwjJwZǽŪ̵ηкҼн̹ɸȵǴʳ˲Ũ·ucwTrOsQxV`mǻƭ˵κϽλͼ̽˿̿ϽϹʳĩ{iyV|nK{mJsQyXqǥȦʧ̧šƾyg}ZyWyW^gw¸ɾ£æǬƫȮ˴ϸл͸ʵжжϵδҸ׽־ѹԼѻͷ̶ͷλλλͺ̸˷ʴʴʲʰ˯}mb\Y]iuſæȨȫˮϲгαȫü|sv»ʰ˳ͷкӾԿӾѼθ̴ǮxvWzfE~hC}iF{kGtQgƿȬʰ͵кноνϻȲ¨ǸiVuMxPXjǸƧͱηкϼѽѽҾҾҾѾҿҿпѽ̶ɰúzzŻȧ̯Ѵչ׽׿տӿѾѾѿѿѿѿѿооооооооооѿѿѿѿѿооϽϽоооѿѿų°žĽü»üĽžƿ°žƿ°ųIJųųƴƴųųIJ³ƴȶɵȴDZưnj`GYM5\L3_O6bN6_M5k\EzȲíǴ¯ıİıůì«ǯȯ˰ǬĪȮx]aW<^P5bT9fX=eWnZ7oX6oX6r[9s\:qZ:nY:{hJl¨ʴʵɴʵ˶̷ʵɴʵʵɴȳƱȳ˶ηжƿr\|oLwiDxjCzP\nx||||źĨƪƸqdvU~oN{mJ~pKzU^wɺæƬȲɴǴǵƴƾɾ~w]r^Cp[>mY>o[@wfLeǭȰʰʳ˳˵˵˴˴ʶɵǴDZǯǭȬȫϯʨǾ{j[zSwTwTzW_l~¹ſ¥ȫʭʯɮɭɭʭˬ¢u\~pMxjGxjGykH~oN{Yo¹¥ƪǪǫǪǫȫƫú~rjhirȿˬոҶδʲʲͷϹϺ͸ʴʹ˴˯ĪwyZpN~lHyiHpQf¸ȮϹϹϻϻͺͺ̷̷̹̺˸˸ʷʵȴȲѻ̳ªĸ|eqTyiGjZ6qa=p`>rcB~qO`ƫ̲̲͵Ϸθ˵˸̷̹˱yh~]zY~\iz÷ëŭưDZȴɵȴɶȵ̻̽̿оϹʰǫŦżs`uS|oM{nLyYlúǧʪ˫˫ʪ̩ɩȿunffimu}żè˯ϵϵβϲѳгαгոӹϵ̲ȮŭŭȰ̴Ϸѻннλͺ͹̸̶̶и˯ĨĻvoklnpu|¹ú¹ussvƽƦδζкѻӾӾӿӿιϺϹϷ˲Ż~axeDyeByfEzjHvUnȬʲ̶ͺͼͼ˽˽Ͼ̹ïĸzj_ai{̯ϴйͺλϼѾҾտտԾӽϷ˴˱ɭɭˮαѴԷӹԺԺսԼӽҼҼҿҿҿҿҿѾѿѿѿѿоооооѿѿѿѿѿѿѿоооѿѿѿƴIJžĽü»üĽƿñIJIJ°ƿ°IJųųƴƴųųIJôĵƴǵdzݬlh^EWK3YI0\L3`L4_M5teNŮ̶ư˸Ƴ¯ï¯Į««ūĩȭǬŨȫždi`CbT9`R7bT7bT7^O2qcHqȳįĮDZȱǯƮŮŮůưDZȲɰɰɰʱ̵̵ɲǰtyZteFl]>qbCyjKf~éŭưDzƳȸĶǷʶƮæih`r[;qZ8oX8nZ9mX9lY;ziMn¨ʴʵɴɴɴʵʵʵ̷̷ʵɴȳɴ˶̵ˮ}dtQvjDtf?xjCrKXiuz~ȼ¨ǭȮ̽{j|[tQqLsLwPbsɺĪűƳǴƳ¯ŽƻxtYr^Cp[>nZ?r^C|kQiǭǯɯɲ˳˵˵̵˴ʶʴɳɳɱɱɯɭͰˮǨpf|YyVxU{XaqĿãƧǨȩɪϰ̭t\}oLwiFxjGxjGzkJtRbrľľľſż|xroy}ȿƫ̱϶ջҸζ͵θθι͸ʴεͶ˯¨x}^tRpLoNxXmĽʰкϹκ̸ʷ˸˶̷ʷʸʷʷ˸˸ʶʶͷ˵ɳǯyc}oLo_;td@sc?wiFxVg¥ȫ̲˱ʲ͵ͷʴʷ˵ǰi|Z^dn÷Ƴ®®®ïűdzʷʹ˺˼ͽξоѻ̵˯ħǾqe_~^bq¹Ũ˫̮̬˭ͯα̯ævsomorx{ú¥Ǫɬɬ̮ΰͰʭʭ̯ʰǭſ¼¼Ȱ͵иѼнϼͺ͹̸̹̹˵ɱǯĨȿ}zz~äͮӶжиѻҼӾҽѽѽϺлѻѹ̳Ƽf~kJ}kGzjH}oL|\sɯʲ˵̹˺˺ʼɻϾʵȾ{xx̯ѷѺнͺͽϼѾӿԾؿ׾ѿտҼͷͷζζϵҷѷҺӻҺѹѹѻѻѻѻҿҿҿҿѾҿҿҿҿѾѿѿѿѿоооѿѿѿѿѿѿѿѿѿѿѿǵųƿžü»»üžƿñIJIJñ°IJųųƴƴųųIJĵŶƴƴݽki_FWK3XH/[K2`L4`N6qbK«ɳĮʷƳ¯®¯Įìì©ĪèǪȫŨɬjneHeW:`R5aS6cU8_P3oaF}hʿʵDzDZůìŭŭĭĭĮưDZȲʴʴʴ˵̶˵ɰǮʼltUrcDl]>teFpQrŹĪŭưȳǴĶĴdzéeh`|qQsǾŮʴθι˶ɴɴʳʳɳʴ̶˲īùo\RS_n¹ȿƦȫȪƨçƯʴʶűIJǵ˹̸¯ǯūȺvfV|MyMUdoüžƽ|~axiLveKveK}mSzawŭŬƯǰɱʴ˵˵˵ʲ˳͵ζϷϷθͷͷɳưDZ˸ͺ˸DZæ ;ɼŸøǹ˽¥¤ʿƽƽſýľľýľƢ˧Ǧɧʩ˩ˬͭίгѵԺйʷɷϿͽҽһѹϷ̶˵͹κɳϹӻж˰ƫʿ·ŸŸƹȽŨ˯ϵиθθ͹̸ʶɵȲDZȲɳʴ̶θкѼѼ̹̹̺̺˹ɷȶDZʿα̱джʰȰʲ̴˳ȲȲĬ»Ķ̾¨˱ιʹǺʾǽ̽˺ȸǶƵƵǴȵ˸˸ͷͷθϹϹн˾̼μλθ̴жгϲˮȫƩȫɯƭůDZʴηη͵˳϶εͷкӾԿкζβ̱˯ʮȪǩǧȨȧģǿºƾĦæƩƩǭǭȮ̲иҺҺйκμϼϽпϽ˸ɵ̺ʷǴȵ̹ϼϼλ̽нҼӽӽӿӾѾѾӿͷDZŮê¸ƻȫжһлϼλ̻˺ʼʼ˼ξмѷҴѱѯӰյ׺պѺλϽѿѾӽӽԼԺչչֺռֽտԾӽӽԾҿѾпϿҿѾннϼλϼϼнҿҿҿҿҿѿѿѿѿооѿѿѿʸɷƴųñƿ»»üž°°°ñññIJIJƴƴƴƴƴƴƴƴɹǷŵƳųŲĮlo`I]M4XH/ZI/dS9o_Ft^ưǴǴǴŲȵıŲƳǴȵʴɴɴʷɶǴDzɴɲǰǭhsjKe\;aX7d[:f]>uiQr]o÷ɽíůDZɳɳʴ̳˲ū禴^uT~nMpOwXd|ŨɭͱδʹɳǰƱŰDzȳʵȱǽoxgKl[?eT8iXq\=r]@xeGx\xžĪʹкҾм˸ǴǷʸ̸̶̷̹̺ͻμϽϽDZĶ~apIxe=tbv_@u^?vaDpRjǮ̶˷Ʋıǵ˺˷˵̷̷ͺμϽнμů~csL{h@vd>scBwhK{`ŹȱʵɳƲűưɳ˴˴ɰǰǯ϶ѹϵϲ̯ĥørdyWqMtP}YfuuezWwVtRxVcq{ʻèʰηж̵˱˴ϵʰʰ̴Ϸѻк̶ɳƮéxwZxgIp]=iV6o\r]>wdDvWs¸ʷʶʶɵɳɳʴʴ̵ͶθϹϻϻϼλ̸ʶût}oTxiJrb>wh?rd?wjGeʭϴͲ˱̲ͳͱ˰˯ʭˮɬ¥¹itQwgEscAk]:j\9ufE~\zžæɬ˱̲˳˳˵˵˷̸͹ͺλλϻ̶͵ζζ͵˳ɱȰʮɭǪź}]qbCfW6gX7dU4fW8hZ=vkOyǽɯ̵ηϹͷɵȴʶ̸˵Ʈ¨ǾycyUuQvR}[nǼƨʭ˯˱˱ͲʹβʹͶ̹ͺ̹ʴ̵ʯ´}{\|mLteDvgFzkJtSpǯ̴ɱưůĮ¬ůůưɳ̶ͷ˸ʰ¢ɼhxQ~oH}kEzhD}mKxWqſ˵̷ɴɳ˶ͺͺ͹˷˵ʹȰĨx{\{mJwdCua@ybCu^?s^?{hHqSz]enwûǿïư˲̵̵̳ͷ̹˹˹ʸʴʻapL|hCygAscBufGc¶ūȳíİİĮĮįŮưDzͷιθθθε͵̲ȭp`uRnIoKtN}Y\{WnJzfCxfBqM~]pƹħͱ˴ͷθθθθθͷȰɱ̶θθ̶ʴȲůƮǽhzkLn^=n[;lY9kX8o^@tWxí̷λ͹̸̶ͷθͶζͷͷǴʾv]{J{JRXhǾ~fyU{qM}pMsP~Zl½ǭ˵ιйϲ̬ľfyUqLmHlGvRlǪѴзиӾҼѹʯƻya~]wXuVz]}`xµƼƭϸѾҿҿѾѾнϼϼλλϼннѿооѿѿѿѿѿооо̺̺̺̺̺̺̺̺̺̺˹˹˹˹˹˹˹˹ʸʸʸʸɷɷɷȶɶȳ­|nwat\|pX|pXw]nıǴȵŲŲǴʷǴƳŲǴɶ˸ʷʶȴɴɴɴȴɵ˷˷ɶʷʷɶ˸˸¯wh~dfkvǿĮư̸ȴıʷ˷ɵdzȴɵʶʶɵȴɵ˵˵ʱŪ¥Ǿ~|~}}}yskc^~Z|[cmzǰɲǰɲʳȱŮŮɲ̵ηɯykabkwǷȱɱȰǯŮȮ˱βеͲƫƽ{fuYlaCeZ{lMevʮͱ˱ɯǯŭDZɳ̶κϻϼϻ˵̴͵͵˳ɱǯƮȯǬĨøyZn]?bQ3gV8aR3aR5cU8rgKwŻȮ̵ζθ̶ʶɶʷ̸ȲĬƿdtM|mF|mFrM\sʮ̱ɯȱ˳ʹ̶ʹ̶Ͷʷ̹ʷɳ˴ȭ|wXxiHpa@sdCxiHsRoɱ͵˳DZDZưĮƳŲŲƳɶʷʸʴǪâw_tP~lFygAxfBoMcƮɵdzɳɴʷʷɵȴɳ̳ɱĨw{\|nKyfExdC{dEx`DxcFoQx\{^{]{^~alzƮ˰˰˱ʳʴɶɷȶ˹˷;dpNzfCvd@ueCwhIe÷ƬɴĮűűðĮîîïįDZȲʶ˷̸ͷͷ͵Ͷɮĵn|[oL|hC|iA~jEmE|hCuazkNhźʳ˵ʴȲDZƱȱʳ͵˵˵ıƾɾȼt]zIyHPVfvawWxU|Y^qĻ̯Ϸ̶̷Ͷ̯ɩ}dxTpKmHmHxTnȪѳѷиѿҿкͳŨgtRsQoMoNxYdŸƣĤȽǼŻȮϵһստԾҿҿҿҿҿҿҿҿннϼϼϼϼнноѿѿѿѿѿѿѿоѿоϽϽѿ̺̺̺̺̺̺̺̺̺̺˹˹˹˹˹˹˹˹˹˹ʸʸʸɷɷɶɶƱǿsu_ocKi]EdX@gXAoaGu[xıȵǴıðıƳƳŲŲǴɶ˸ʷɵűƱƱDzdzȴʶʶʷʷƳıɶɶûux^rbHj\Am_EsgMv^mº¬Įɵdz¯ǴȴdzƲƲȴɵɵdzűdzɳʱŪƽyw|tj]uQukGnfBsiNyqZj~ûĭȱŮǰɲȱƯǰʳϵǯȼpbZYhuõȺʻϾıDzȯưĬĬĭǰʰ˱˰̱ȭǽ|mwlNi^@dY;j_Ah_Bf]@zpUrƿ˴ŮȲ̹ɳ̴˱ʲɱɳ˵̸͹ͷͶȲ¶znTkWrcBwhGrQoɱ͵˳DZDZưĮȵƳŲŲǵɷɷȵɯǪżlxT}kGye@xdA|iHy[wū˵ȵDzȳȵǴƲƲDZʱȰ¦twXzlIyfExeDydGydIlQ}bpphb}_dmvɹĩĪƫɯɲʴȵƴųȶɵ˼d~nLxdAtb>vfDxiHdĸǭʳůȴȴŲıîî®îDZȲɵ˷̸̸̸̶̶˴Ȯèĵ~dsR|hCyf>xe=xe=xb;v`9{e@lG`xŴƩȮƭưʵ˸˸̹̹˸˸ʷɶɱʲ̶̶͸̷ʵɴícseHhW9dT3lW8kV7kX:o^B~nTpǿưDZDZưĭìĭƮ˵̵űžƾƼr[xIxGNTfykilswɭҸк˶ɵʵ̰Ȩ{bwSpKmHmHyUpɫѳжи̺Ͻϼͷʰ¥z\~nJpL|lH~nLyXgʿϰϱˬˮȫħȬǫɭ̲ϵҺԻԼԾнѾѾѾҿҿѾѾнϼϼϼϼнѿѿѿμϽϽооѿѿѿѿооѿоϽμо̺̺̺̺̺̺̺̺̺̺˹˹˹˹˹˹˹˹˹˹˹ʸʸʸɷɶǴîƾºvs]o`IhYBbT:dT;l\CoVuŲȵǴð¯ǴǴǴȵʷ˸ɶȴ®îįįűdzȴɵɶɶıȵ˸û~dvgJ`Q4YK.[M2bV{mSȾƯͳ̴ʴʴʷ˸ʺʷǴîĺhpIwf;vg}tMeúǬ˲ɵʸʸ˹˸ɸǴıðƳȵƳůǰĩxsTufEn_>rcBwhGsRoƿǯ˳ɱůůĮ¬ǴƳIJųǷɹɹȶɳɰtyX|jFwc@wc@zfErRpªɵȳƱDzǴŲİİůȯŭƿnqRugDvcBubAt_DycKt\vzpjgdkzŶźȾĪȱ˵ʷȶƴñİƷ~b|lKvbAr`wPjǾȭɳǵɹʺʺʺǷŴǾóƳıí٧vtUufEn_>rcBwhGrQožŭʲǯĮí¬ŲıIJųȸɹɹȸɶɴí{xZzgFxdAyb@ybBkLjοưïŰƱǴŲİïíŬëügzkLoa>q^=r_?oYAxdL}eŵɹ´zma`ht~ǯʴɷȶôz^yiHt`?p^:scAufEbźɯͶȲϻκ˸ɶȵǴƲƲ˵̵̷ͷͺ̹˸ʷȶɶ̷˵¨u}]vb=r_7r\5u_8u_8t^7waeUufEmȯζϹ͹ʷɶɶǴIJ®ĮƭŭǭèrpRp]?mZuaH~jQrZw\}]\afqƸĩéé«ūƬƬƬ΍ľ{ybrbIbR8_N4cR8eT:_N4^N5fV=|mVʿǰ̲ʰȲȲʷ̹ʺɶƵȽz{Ywe=r_4se8qj@~yQmʰ˶ɶǷȺȻȻƹĴǾ²ıðĭutUufEn_>qbAvgFqPmĽŭʲǯííƿ°°óƶɹʺȻǹ˹ʶİ}x[|gH|eE|dBybBjKgξîïŰǴǴűï¬êëcufGk]:n[:o\sdC`źɯηʴϻκ̺ʸɶȵǵdzʴʴ̷ͷ̹̹˺ʹ̼ʸȳĮɺqxZwc@q^6o\4q^6t^7s]6t^9vb?}\sê̹ͺ˼ʹʶ˵̶θϹϹккͷͷι͸˸ʷɶʵí|alaE_Q4]N/aQ0cP0gS2gT4bQ3_P3eY?vjRycǿĭĭêĭɲ̷ʷųŲɾoXtDuE~NUfũǭȮɭɰʯ̳εεͶ̷̸̹̻˼̸̰ȧw]pMkFhDiDyVtˮҴԹֽҿϹͰŤ}`sHqIoIsO|[cíưȰɱҹռԻԸӷҶҶҶѸտҿѾнϼϼϼѿѿѿѿѿооооѿѿѿѿѿѿѿѿ̺̺̺̺̺̺̺̺̺̺˹˹˹˹˹˹˹˹̺̺̺˹˹ʸʸʷλȳìííqqeMbS<]N7`P7iY@q_G}kSpü¯ŲıðıŲɶȵȴǴǴưľ­ıƳȵŲλϼȵïźwadT:[J.WF*[L/ZJ0XJ0g[Au\ļ̶ɶ͹˸ıï¯íðŰȳȳDzŰƺuw[tiKtiIzrNxSgvɪˮʱʱůĮí¬íůȲʴDZDZDZDZůĮ¬üƬɯʰ˱ƬĪȬ˱ɿxu[{gNs_DwdF~kKyXrʽǫʲűŻξ̹ɶʷ˷˷ȴűưíſſžźaqdDcU8]R4]R6\P6pdL}d˶ȳʷѻϷζ̴˲ɳɳɵʶ˵ʳůwmRlX=gR3kT2jS1q]:uTxʾ¨ſſĿĿ¬­îǰǰȱɱɳȲDZưĭìƼ~cj\AeV7gT3uc?scAwhGoǭ̶м͹ʷȵǴű°ƿĭƭǮƬqpRq^@kX:gT6mY>ziM{apslf`]boͿ̾ɾɽȾȾɿɿĨŧħåkxiRdT;YI/]L2eT8eT:]L2[K1aQ8whQ˿ȱ̴˲DZȴ˷̹ʸȶɶɾvyXwe=r_5sd9qhA~ySn¥˱̵ʷŵƶȺɻȺĴı®ĪrtQugBm_:pb?tfC~pMjžūʰǯĮí¯ƿóǷʺ˻ɹǷ̺˶Ű|yY{hH~gG~gE{dDkLjį˾®ŲȵȵƲï®ĬbsdEiZ9lY8mZ:r^EnVuƬʰȮȬŧ˽zrjghnvǿİưĬõ{uYscBn[:mZ9m^=pcA_ĺɯкʴκκ˹ʷɶɶȶȴɴʵ˵̸̸̹˸˸ϼʷĭȾgoOzhDtb%P@'UE,WI/TE.VG0i]Ev`ǿͺ˸̸ȴ®ĮĬªʲɲ²zsVqbCl_=pc@}sO_kťʮ˱ɴͷʷʷ˸̹̹ͺͺͺƳŲǴʷͺͺȵůƾƾſ¨ƫɮ˰ȫɬʮˮŪ}el[AjY?iX>n]C}lRl¯IJĴŹĺż̼̺̺̺˸˸ʷʷ̶ɴŰ­ǿǿǿȿzqTfX=aS8TF,WI/^O8ufOxø̵ʳ˵Ϸθ̶ʴɳȴɵʶʶ̶ʳĮ´zqUmZwa:tb|pHe½æɬʯǮí®ðǷŵĴŵɶ̹ͺιѺϵƫy~[~nL~lHzfE|hGsVtĴĴƵȶɷɶȵƳűĮ|`paDgX7lY8n[;uaFsZy¨ĬĮưεɰªɽĶtrc{^ftƸʻƷxqSn^=iY8iY8hY:n`CeøDzѻκҿҿѽмͺʴƱů̷ȴȴιϺ˶˴Ϲ˳ɯĨƸcpM|jFxjEwiDvhCvfBvcBua@vaBydExcFrWpūʲɱɳʴ̶θлҽӾӾ̽ʼɹǵŲ­ʿppeO[Q8UK2WP6]S:YO4^S7`R7]O4_O6fV=n\Dp_E~mStZj~ŤƦǮʵȵųƳɾl}TtEuFzK{Rg´Ȯɱ˳̴ζζͶζȱȰƮůůưȲʱǪƦn~]nL|d@}gB{lEzTu½ѴҶηлϾѾθǰ§ɾ{i^\`acuzüǫαϵжҸӹӹҸжϷӽҿѾнѾҿҿҿҿѾнϼϽоѿѿѿѿѿϼкϼѻҼӽտҿҿҿҿҿҿҿҿҿоооѿѿѿ˹˹˹˹˹˹˹˹˹˹̺̺̺̺˹˹˹˹ͻͻͻ̺̺˹˹˸ȵDZʴȲǿxj_I\P8WK3ZJ3]M6_M5dR:zhPkĽƳŴ³ŵȺǷǵŴűůì©©ľñƶȶƵʷŲfvbJ_N4P>(O?(SD-UF/QB-QE/g[Et^ƾ̹ʷ˷dzŬŬ¨ʰȮgxhGiY8fX5k^;{oIZvǧǪʰʲƳɶ˷̸λϻнϻͺ̸ǴƲƳȴʷ˷ȵŲ§Ūɭ̰ˮʭȫƩƻj|nSjY=fU9eT:n]CsZyƹñĴƷŹźƻǾʿʻʹɸɶɶɶɶɴʵȳŰ«Ⱦ~bjZ@_O5SC*ZJ1[K4l]FkíȰȰʹʵɴɳɳɵɵɵɵ̶˴DZ÷|sTmZm]rcB~sUnƽǬǬŬǰȱɴ˵ʷɶȵȵijƳů¨¢Ž~c~qDre8l_2pc7rf<|pHgǨ̯ͲʱưŲƳĴĴŵɶ̹͹˷̴չҳǨv|X{mH{iEzfE}hIwZxŵ´ƵƵȶɷʷȵǴűız^n_BfW6kX8n[;wdFvZ{´«ŭİƲ̷ɴűïŹwewXxY`lyŵooNk[:eU4dU6iZ=pbGhźʴѾϻѾѾмκ̶ʴǰDZ̸ȳȳͷ͸ʳɰ˱ͱŨŸnySjEzhB{oI{qMsPtSrQlL{hJzeHu`E~jO|buèʰȰʴͷϹлϺϺϺʿɽǹƶij°ʾ|dj_IYN8SK4VN9YQ:YR8\R7[O5WI/XH/_N4hT;jV;uaF~kMz\jzļƣειɶŲƱ˿i{RrCrDxJ{RhĶʱ˳̴͵ζζζ͵˳ɱǯŭĬëĬĪǨƣm~^qMiEmE}mIzVr̮ʹ̶нξξϿнͺʵƯèƺ~|{{|æ˯дϳдҸӹӹҸжηӽҿҿҿѾнѾҿҿҿҿѾнϼϽоѿѿѿѿѿѻѻкѻѹӻԼսҿ˹˹˹˹˹˹˹˹˹˹̺̺̺̺ͻͻͻͻͻͻͻ̺̺˹˹˸ʷȲɳưƾyk`J_S;YM5YI2[K4\J2]K3iW?wgNiþôŶǵƵƲưĭĭìýƴɷŴıxcn^E_K3SB(TB,SC,TE.UF/PA,PD.fZD~s]ƾ˸ȵʶƲ¬©ƭŬ¨ɯūpuWm]qcHjǼ˵Ѿκϼλ̸ʶʴʴʳ̴ʳȱȱ˴ͷʹɯƪʿs^rN}kGnIwSbrzkw\mR{gLwfJ~mQ{auùůȲDZůíİƱ˺ɸƳı۬̽ʺz}drbIcS:^O8]N7]N9[O7\P6ZO3WI.VF,`O5o^B|hMlN}jL~kM{kJ{kJrOa{ưɶDzİŹexPoAqCwIzQi´Ȯɱʲ̴͵͵͵͵͵˳Ȱŭ몪éǨàh|\sOmIqItP[tɫ˲̶ѽѼлϺη̶̳˱˱ɭˮ̮˫ǧťţǧˬʭȫƩǪʭβдѵѵҸӹһѺййѾҿҿҿҿѾнϼѾҿҿҿҿѾнϼϽоѿѿооѿѿѿҿҿӽӽҼѻиииѹҿ˹˹˹˹˹˹˹˹˹˹̺̺̺ͻͻμμμͻͻͻ̺̺˹˹˸̹ɳʴưŽxk`JbV>]Q9]M6`P9eS;eS;fTzTtƾáĢã¢ãĦǭŮįì«ìĭūɯɭɭǫũ¥ƿĽnvfLgW=YI0_O6WG0WH1m^GrƮɱ˲­įưȲɵȴdzdzĮɲȲtwhIfS5aQ0^M1_P3hZ=|qSs¥¥¥ɿȾǿŽŽǿźitfKcU;bR8^M/eU4fU9ugLzĺƱʴ͹̸ʶɵȲɳɴ˵Ŭǫ§ĽpziKl\;kX8fV5fV5pcAcú¥ĪūƪĨƻwojdbekszfudHbQ5_N2`O3cS9fX=zlRêʳͷͶ˷̸̹̹ɷdzƱοxvXvcBsa=xfBtdB|nK]tƻĨǬȮƯưĮïðòòȷʷɵŭĨƽj{mFsd=n_8n^:l^9vhEhƿĪʲ˳ɳŲııǴŲůŭǭɬɬȪťu`sL}kEkFwVd;ƵǹǸǶȵɶʷ˸ɶȴdzưy{pRhY:dU4kX7n[;ziK|`ɻȱɳdzȳʷʷʵ˶˵ʴȰƬ˯ʿvmcxYz[yZuWnPtdCk[:hX7gX9iZ=qcHiƻʴϼ̸ϼλ̸˷ʴ˵˴͵ʱʱʱͲϵβȫ{evTmImItO~ZvĹovZ}lPveIyhLsVezüĬŭªêưʴκ˶ȴȲȮ§ǹi}lRgV<_N4_M5_M5^N7_O8]O4\N1[M0aR5sbFx\ijxZqQxhGscBxjGzWjyȲɴïĸj{SqCrDwIxOf¨ëƮȰ˳̴͵ζ̴ʲȰǯƮƮǯǭȩbwWpLjFoGwS_zв϶θҽҺѹϷͳʱʱʯɯȫʮ̯ˮƪŧɫ̮Ѵϲ̯ʭʮ̰βдҸҸѺһһѺѺѺѾѾҿҿѾѾнϼѾҿҿҿҿѾнϼϽоѿѿоооѿѿѿҿҿӽӽѻкиϷииҿҿҿҿҿҿҿҿҿ˹˹˹˹˹˹˹˹˹˹˹˹˹̺ͻμμμͻͻͻ̺̺˹˹˸˸ɳʴDZƾwi^H^R:[O7\L5^N7cQ9fT[K2_O6VF/UF/iZCoŭȰȯŰŰDZȲȴȴdzdzĮȱůlpaBdQ3bQ3_P3`R5l^Az\ĦåƩƩƩƩĪĪê¬įįįƱƲưíȼ÷v{mSeV?`P6_N0eU4dS7seJyĺȳ̶̸˷ʶɵɳʴʵ̶ƭǫĽlwfHm]{oUīɲ̶̵ʶ˷˸˸ɶƲ̽²b{hHs`?vd@xfB|lHxTiͿƪŪƬŭůİð±±ȷʷȵĭ§ǼkoKtd@o_;p`>pb?zkJmƬ˳ͷʴƳŲƳȵDZĬéƩǩǩȥ{h}VoIkFnKew˻ðƶƺǸǶɶʷʷ˸ʷȴDZůx{pRiZ;eV5kX7mZ:yhJ{aʼʴʴɴɴɶɶ˸̷̸ͷͷ͵ж˯˿zfrUsV|mNwfHsbDm]cP2fU7cT7`R7k]B|`ȫæŨŨƫƫūūĭĭŲƳȵȵȵȳȴʶ˷ůʾ}qYgXCaQ8`O1cS2aP4oaFu¸Ʊ˵̸˷ʶɵɳʴ˶ͷȯǫ»htcEl\;kX8fV5fV5sfDhȿĨdzȶɵɵȲDZưƭƭèż{vx~ĶǿéêĸgsdG`Q4\M0[L/`P6hZ?sYůɱ˵˴ɳʴʷʷȵű̿˿ȺrrQueDwdCvd>zhBpJ[rƹ§ĪŭDZdzȳƳŲƳɶƳ«źjoNscAm];qa@tdC}nOlƿŭ˵̶ɳưıŲDZƮūƩǩǧƣßn]yQqLtO{XuɷįǶƶŹȻȵɴʷ˸˷ʶȴDZůx|qSk\=gX7lY8lY8wfJz`ʼ˵˵ɴʷʸ˸̷̸̶̹θз˲̱ʱĸyby\|mPraCn]?mZ:hU5hU5jY;j[>rdIlȽ˵ϼ˷ннммϹͷ˴ʳƬɭʮɬȪ|g|XoKnJuS_n{§Ȯͱ§~my\tWoQnPwYjĪƬƬȰ˳ȯɭɮʭŨnmOnY<`K0aL1dN6bL4dN7iU\R7ZO3VK/XJ/\N3WG0ZJ3_P9bS<\M8XL6i]G~s]˸ȵɵdzïůưīȯɰĪé{vfEjZ9`O1_N0]N/^P3k]@xmQj}|~~{uokfffgmzèŪȬɭɬǪǪ¥|`l^CdV9bQ3bR1aQ0aQ0hY8zmKfy¾»Ľž»Ľü»{ugy]f[?YI/\L2WG._O6\L5]N7sdMxø˳̴̳ʵɴȲDZƲƲdzȴȲʳí|biZ;dQ3iX:eV9`R7g\@|_ƨˮŨħŨŪŪūūŮŰıƵʷɶǴȵ˹ϻӿ̸í~r\gXCbR9^M1aQ0\K/j\ApîȲ̸˷ʶɵɳ˵˶ͷʱȬdp_AjZ9iV6dT3eU4reCiƬƴƶǷʸʸʶȴȲưŬīƫȭƩǾ´ʼƩ«Ůīù}bm]CZK.[L/ZK.^P5g[Aw^ǰȰʴʳȲɳɶɶǴŲ®¬ŷ~{\zkLyiGuc=wd<|jDxQgȽʿūɱ˵̷˶ʵŲdzůȾŷi}lNp`?jZ9n^=tcE~mOlüªȰɳưí¬íŭūǪȪɩȥğwdWxPwR\hʵʸƷƹȻȵɴʷ˸˷ʶȴDZŭx}rTl]>hZ7lZ6lY8veIy_ȼ˵͸ʷʷͻͻͺ̹ʵɵʴ˵íɳ̶˲ǯɽxgsVtcGo^@n[=jW7jW7l[=l]@tfKmʿ̶н̸ѾѾҾҾкθ˴ʰũɬɬŧãȹm|XqM{iE~nL|Znȯж˱ǽm|_rUziKnP{^n¨ƬĪ«ĬũħƩǪt~`kNoZ=fN4fQ6iS;fP8iS_Q7`O3]M,bQ5k]BrƼ­ͷ˷˷ʶʶʴ˵˶̶ǮʮĩĽdp_AiY8gT4hX7eU4reCoǾǭʷɸȷȷȷɶɶɴƯǭǭǫƪũĨæȽȽȽɾçƪȬįĺy^k[A`P6]M3ZJ0_Q7j^Dx_Ưʴ϶ε˵ʴȵɶʷʷű˿ȼv|\qPmG{iA}kEuN^oŻƬ͵ζͷʴɳ¬íì©ƸmpRwdFr_AubDtcEziKg¨̲ʳŮìììçǪ˩ǣ¼zdzT{UZbo~ij̼Dz˸ɹǸǺɼʷɴʴɳɵʶ˵˵ës~sSo`?i[8lZ6n[:tcG{aǻɳ͸̹ʸͺͺ̷̺̹˶ȴȴɴɴɴ˴Ȳ©¶~h|kOkZxjOnůкͺ͹ннмκ̶̶˴ͳɮȫũƷsetPsOuS|Ziȿÿì˴з͵ʰĽo|buXpT~oR{`nݍɬɬʬǧĻfqP|gHt_BnY>q\A|fNpWrZpXpT|mNxkKzmMsV}`inrzunjjr|}n^uKtFq@oAuNfõĭȰʲʲȰȰʲ͵̴˳ɱƮĬ모txVqQoKlHrJuQa~¿̮ɰȲ̵ͱϰбѲѵдϳβжϵ̵˴̵ηϹѻѷдβ̰̲δϸйӼһҽѼѼлллннннннннѾҿҿҿҿѾнϼμμоѿѿѿѿѿооѿѿѿооооооннкккѻѹҺҺҺннѾѾҿҿѾѾ˹ʸͻͻͻͻͻͻͻͻʸʸ˹̺ͻͻμμμμͻ̺̺˹ʸʷ˸˵̶ưŽwcXB]Q9XL4XH1YI2\J2_M5hV>q`FsbFp_AjY;dS5cT7n_BqTd}ƲŲ¯¿ý¼üĽpy[ujNf[=dY=mbF{pThn~bk\E_O8XI2ZK4VG2RF0h\F{e̹ǴȴƲïDZưŬȯƭ¨éqo_>eU4_N0_N0[L/]O4tfLhŽº¹úĹ{kdentèŪǫȬˮǪƩ|`i[@]O4eT:`N6]K3dR:yiPp÷©ĪƬȮȮǭƬƬǭɯʰ˱ʮʮɭʮȫƩħæ¢ſýoshL_O5YI/ZJ1ZJ1TD-^O8|mVźʲ̴̳ɴȳɳȲdzdzȴȴɳĭ¬qWgX9YF(bQ3^O2ZO3i`ChƩˮɫǫǫƭƭŭƮƯȲǴȶɷ˹ʹ˸ɶɶƲ̸İ|pZ`T>_Q7cR6aQ0fU9m_DsƼ̶̸˷˷ʶʴ˵ʵ̶Ǯʮè»`m\>gW6fS3gW6fV5sfDpȿȮʷɸȷȷǶȵǴDzƱƯǰȮǭǫƬũħ¤¦çũǫȮʰDzîĺx^j\AaQ7]M3YK0]Q7j^Dx_Dz̶зε˵ɳǴȵɶʷIJ®íë÷k~\qM|mD}kErKzUbp~ǫɯǭŭĬëĭƭūɻrlNs`BmZgY4kY5jZ8qbEy_¶ű̷ͻͻͺͺ̺˹˸ʵɵʵнλ̹˶ɴĮǼ÷|z^tcGkX:hU7hU5fU7hYfS5gR3gT6hYqcHuƼ̶͹̸˷ʶʴʴʵ˵ǮɭvVfU7cS2eR2iY8jZ9zmKw¥ɭ˴ʵɴȳƱŰįîƱƱȱɲʳʳʳ˱ɭɬȭɭȭɭȯɯ̶DZ«sYgY?`R7]O4ZL1^R8j`GybɳθѸ϶ʴDZŲƳȷʹƳǴɵ˵ɰɿwmeb^}Z]apw~ƽǼ³qqSyfHq^@q^@p]?q^>}mL^zĻ¥ħǽ¸ùztqwȽŭ͵˵ʶɸɸ˻ʽȻǷȳȱȲȲɳʴ̳̳ſiujJj[:i[6o]7o_=zkNgźdz̹̺ͺϿͽ̹ʷɶ˸͸λϾ˻ɶƴŲï¬̽|`vcEhU7hS4iV8j[>ykPpí˵ʷ˷ɶɶɵʶ̶θͶͳéüzg~\|X~X^et¨ŬìíůDZȱǰƯǮȯ˲̳˳Ů·tzcuZ~pS}oRx[o¹ryYxiJqbAxiJqTf|ƺȼƻø©ǿɾ̿ìĶv}coT~kMlKoGpBqAsBtGzSiĭƮǯȰȰȰɱʲ͵̴ɱƮëſw{YpPkGgCmEyUjģв϶ʴȲ̳ϳҹԻҹ϶ʱƭǰȱʳ̵ηϸйϹҺҸѷжжѷѺѺлллϺϼϼϼϼϼϼϼϼϼϼϼϼнѾѾѾѾнϼλμϽϽоооϽϽμϽϽϽооооϽϽϽϽϽϽϼϼкккѻѹҺҺҺҿҿҿѾѾͻͻͻͻͻͻͻͻͻͻ̺ͻͻͻͻμμμμμͻ̺̺˹ʸʷŲůư¬Ž}k`J_S;\P8\L5[K4`N6r`Hi´ny[lNxeEzgIziKqTdxºf{mPqbCm^=pa@uhF~\uxsdMdT=ZK4\M6[L7^RrP|úħʮ̲˳ʲɱǯƮŭĬǯȰɳ˵̶ͷͷ͵ʰˮʰʰɯɯǰȰʷDZ«~|pVfX>`R8^P6[M3`TyiGuRjyúĻȾǮʱ̷ιλ˺ʸɹȻȻǺǷɴȱȰȰȲȲʱ˲ľetjGl^;l^9sa;sc?|mPjɾʶͺ˹ʺϾν˻˸ʷ˸λϼͼμͼ̹ɷǴİĮ̽õi|iKkX:hS6iV8m^A}oTs¬ɳȵɵʷʷʶ̸θθʳȮk}\yW~Z`ksǰŰůǯȰɰǮƭǯȰʲ˴η̶ůk}^vYx[j|}huUzmMxkKz]gzîȲɳ©æƫȬǫŬƭŮĮíñDZʼpz_qTmKmFoAp@sBuH{TjŮƮƮǯɱʲʲʲ̴˳ȰŭëſyzXnNjFiErJ}Yoƥѳҹθʶ˵εѻӽѻθɳůůưȲ˵ʹ϶϶϶ҸѷѷѷййлѼιιλϼϼϼϼϼϼϼϼϼϼϼϼϼннѾѾннϼλμϽϽооϽμμμμμμϽϽϽоϽϽϽϽϽϽϼϼкккѻѹҺҺҺннѾҿҿҿѾѾμμͻͻͻͻͻͻͻͻͻͻͻμμμμμμμͻ̺̺˹ʸʷƳưDZ¬ŽncMaU=]Q9\L5\L5cQ9xfNqʾʾĶpxZ~kKyfFwdDtcEyjMx]qh}oRrcDp`?rb@sP\vƽǥuvgPiYB`Q:`Q:\M8\P:ymW{ŰʷŲɵɵűưȲƭǮīéujZ8`P.[J,^M1[K1\M6o`KyfȽɴDzʲɱǭʲɳɳɶʴɶʴɶʴɶɳǴȲǴɳɶ˵˴ʳȰƮǬǬʮ˯Ͱ̯̯ˮɾ~crdGgV8dT3eU4gW6iZ;viInƼɲ̷ʷǵǵȶ˸˸˸˸˸ʵʵʵ̷̵Ͷϵϵ̲ǭé~vZg\@\L2WG-XH/ZJ1scLkɳζϷз̷˶˵˵ʶʶ˷˷˵ƯȽvyiOk\=`M/bQ3XJ/ZP5pfKrȭβ̰ūūììíĮůůƲDZɳʴ˵ʲɱȰ˴ѷȮ{qX^T9\Q5_N0aQ0kZ>seJuŻ­Ϲκ͹̸˷ʴʴɴʴŬǫ¾}sSiX:jZ9kX8p`?tdCxVƽƩˮ̯̯ʰɯȮǯƮƮǯȰʴ˵̶̶̶̴̲Ͱͳγ̲̱ʲ˲ȵƳî~zpWdX>aS9_Q7[O5bX?oeL~gƳɳʴɱȲȲǴȵʸ˹ʹʹȵįƯ˱̲ɬºzvxwxwurmopuy}~~vkz]qR~iJ}iH}jI}jIoMvSZjyüìʳιθ˸ʷͻͽ̼ʺȻȻȻʺʵɲɱȰDZDZȯȭ`rhEl^;m_8sa;rb>sVq̹λ̼˻νν̼˻˹̹λϽ̻ξ˹ʷɵ¬ȺnlQmZp`?n[;tdCyiH}[Ǫˮ̮̮ˮˮʭɯȮȮȰɱʴ˵ʷ˸ʷ˵ʹϳϴдϴϳʹ̳ɵƳŰ|rYfZ@aU;]Q7\P6dZAsiPiıůưƮȲɳɶʷ˹˹̺̺ɶůƯͳδʮ̬Ƥ ȿû|wrtvxz{y{~{tg}^vWwVyXzV{W}YcmzĻǾéǯʴ̶̹ͺͺ̻˻ͿͿʽɼ˼ν̷˴ʲȰǮƭƭƫawmJvhEzlEoIpLkí̹ͺ̼ͽͼͼͼͽͻμλμ;μλκDZ˽ooTs`Bp[>q^@yiO{`¸DZʴȵʶ̹˸˷͹ͷDZû}oeelwĴ˾ɬͳͶʵɸɶưêĪƬ˲̳˳Ȱʴʴ˷̹λ˹Ƿ®ŷ{wz}Ǭɲɴʵ˶͵˴ʲ̳϶ѹѹѺͷ̷͹ͼƶƿȾƴ¥Ͽ̹ıp{YnGoAp@rAsFzSjɲȰǯɱ̴ζ͵˳ȰǯƮĬ몪¨y{YpPnJpL|Tg}ȧвӺϹʶʷ˸̻ϼλͺ̶ʴȲȲȯȯɰ˰ͲγϷηϸϸϸϸιι̷̷ͺͺͺλͼͼλλλλλλλλϼннннϼλͺϽϽϽϽϽμͻ̺̺̺ͻͻͻμμμμμμμμμλλкккѻѹҺҺҺͺλλϼнѾѾҿҿѾѾѿѿѿϽϽͻͻͻͻͻͻͻͻμμμμμμμμμμͻ̺̺˹ʸʸλ˸˵ŭƾymaGfX>aQ7\L3^L4hV>~lTvȽíȱʱūǹjd{]pRxiLwiL~pUw[y^wZ{pTvkMxjMrS~_f|̿ťĥħrpaJfV?`P9]N7UF1YJ5}q[ƱɶıɵʶűưȲƭƮĬéƬzufEfW6]N/`Q4^P5[L5bV@rfPyŽŮǯƬʲƳȵɷ̹˹̹ʸʷ˹ͺ̺̹ʸ̹˹ɶ˳ʰʲɯȭǬƫƫȫȫɬ˯ɮŻjtQthBi]7eY3cU0fZ4|pJd|¹çƬƮǯɳɶʷ̹ͺ̹ʷȵƳįDzʵιηɲĺzx\eW{dʾ˲ȱɱεѸθ̷˸ʷʷʷ˸̶ʵĭ÷mqcFiZ;^M/aR3\N3^T9piOtƫ̳ʹȯȯǰǰDZDZȲɳȲɳ˵̶ε̴˳ʰȮβŪ~tXcXcW=`V=aW>u_xíìư̷˺ͽ̽˻ɺɹʻͽǸʺ˼̼ɺɹȹʷɱˮʰɬƩŨūǭ̲ūéǭextQkeAjb=ja:k_7sg=tJ[lħǫʮȯī¬ůʶ̸ɷųIJñƲʶ˵©{`ufIaQ7XH.UE+WG-eW=uiOuȮ˱Ȯ˰Ͳ̳ɶʷ̺̹ʷȳȳɲƭéjseHfW8`Q2YK.[O5b[AyrXyȰ˳ǯɳȲƳŲŲƳȵɶǴȵɶʷ̶̵˴˲ǮɮǭĹ|uXl^AfW8gW5jZ8scByZǸưƲͺ˹ȶƳDZȲ̳ʹëw}[ymGse@k]8rf@vSoƿɯ˳˳˳ʲɱȰȰǯưưDZȲɳʴʴʴʴʴ˲ʴʱɳȲȲȲĮȯƯĺfueKfU9dP7`O5eT:wgNoɽȴƱDZȲȲʴ̶ͷθ˸̶̶˵˵ʴʴʲȰǭƮūĪ騨ȾȾɿ¨ĪľĽƿéīŬȾɿȾǽƼŻǽȾũũƬǭū¨ƾļƾƾǿ«ĭŮƯȰȰɱ˵ͷϼҿͻͻμμξϽϽϼϹζ͵̴˳ʲʲʲ}x}ľư˷͹Ϻιι˸ʵɶ̷ιϻϻͺѿνͺϻҼ˴Īkz\wYy[n}¦ʳ͸ͺͺ˸ʷ̶̵ʰžsqs|¥ʰ̵ͷ˸˸˸˹ʸɶıŰưDZȲɵʶʶ͹͹κϻλλͻͺ˵̴̴̴˳ʲɱʱ͸˶ʵȳƱŮŰŰɴȲưůűdzʶ̸̸̶̷˶˶ʵɴɴ͸̷ʵı¿¯ðȴȲ«ǻqwSoIqKpKpMxXpĴƯǮůȲ̶θʷDzȵįŽŽªý~Ľūͳѹи͵ʴʴ˵̶ͷͷͷͷηʴʳɲɱȱʲ˴̵˵̶θϹкϹι͸̷͸̸̸κϻѽӾͷ˲ɳʹѻҼθɳɳͷнѾϼͻ̺ѾнϼλλλϼϼͺͺͺͺͺͺͺͺͺλϼλͺͺλнϼнѾнλθλϼͺλϼϼнѾҿҿнѾѾҿҿҿҿҿѿѿѿѿѿѿѿѿѿ̺̺ʸʸʸ˹̺ͻͻͻ̺̺̺̺ͻͻμϽͻͻͻ̺̺˹˹˹˻˸̹ǰǾoscAlY8hT3jU6kV;kU=oZñƳȵ̸λθɲŮŻyqh~^wT|U[bfnzůȲɳ˵íun^GbP:^L6bR;\K7[L7{oYļððİƲŰýǿļ¨Ȯwx[f]@^U8]S8[Q6\R7aWgS8fR7jV={jPq˼ȲDzȲȲɳʴ̶ͷθ̶̶̶˵˵ʴʴʴȲȲDZưůĮĮí¬íĮĮííůȲDZů¬¬ĮȲʴʴůůĮí¬íĮůDzȳɴɴȳŰ­­îįŰƱƱɱɱʲ˳ͷϹҼӽͺͺͻͻμμμλθζ͵͵̴˳ʲʲý®ŰʵιѼѼѼлι̷̷͸ϺллϺоλ͸͹ϹȰĪzy}ĽɭͶ͸̹̹˸ʷɴʳʮ¥~þƬ̶κͺ̼˻˺ʷɶɶŲƳƳǴȵɶʷʷ̹ͺͺλλͺͺ͹̸͸ιι͸̷˶˶ͺ̹ʷɶǴƳƳƳɶǴŲııƳɶ̸̸͸̷˶˶ʵɴɴ͸̷ʵƱ­­ð³ƷǶ®Ľt{XpLpLlK|gHlO}b|¨Ȯʳʳǰ¬íʿȽʿǿŽƾ«ëſëƮʲζииϷζʱ˲̳ʹεʹͶͶʳɲɴɴʵ˶̷͸ӽҼѻкϹϹιιιι̸̸̸͹κлε˲ɰ̳зҹθʴʴθнҿѾϼμμнϼλͺͺͺͺͺͺͺͺͺͺͺͺͺͺλϼλ̹̹λнϼнѾнλͺλϼλλϼнѾҿҿѾѾҿҿҿҿҿѾҿҿҿѿѿѿѿѿѿѿѿѿ˹̺ʸʸʸʸ˹˹˹˹̺̺̺ͻͻμμμͻͻ̺̺̺̺˹˹˻ɶɶƯɿ´sueCjW6eR1gT4gS8jV=r]³Ŵȵɶ͹ϼҼиζͳĨypv|¸ʿɳ̸̸˷ʶql\EbP:^N7`P9YH4XI4znXޝðİűŰ¼ǿǿƾū˱ūsyoTf\A\R7VL1YO4aWi`CulOaxýħȨȫ˫̯ϯϲ˫ŨǧˮήˮǪæŨǪȫǪǪǭɯǯɱ̴͵ͷ̶˵˲§zk]TR{QU\birz¸ǼǼǼŻr~brX{kQ|lR~cxùǫβѷδ϶ѸѸͶϼоѿн͸˶˴˴̳ȯrwiNeW:bT7bW9ndIy_~ǰͶʳƯȳɴ˸˸˸ʷȵǴƴǵȶɶʵ˶˴ʳ̳ʱé~{^wiLreEteD}nM}^tɳȴ˸ȷǵƴƲdzʳ˴˸̹ʳ¨uqw~ƽɯ̵ʷǴ̶̶˵˵ʴʴɳɳθθθθͷ̶˵ʴ˵˵ʴʴʴɳɳɳɰ˲˰éiwZ|mP{lOoU}c|ʾDZȳɳɳɳʴ̶ͷθ̶̶̶˵˵ʴʴʴʴʴʴʴɳɳɳɳɳʴ˵˵ʴʴ̶θ˵ɳȲɳ̶Ϲкѻθ̶ʴʴ˵ͷϹϹϺϺϺϺι͸˶ʵɴɴɴɴɴɴȳȳ̴̴̴̴ͷθϹкͺͺͻ̺̺˹˹ʷ˵̴ζϷζ̴ʲȰŭƮȰʲʲɱǯưȴ˶ιѼӾӾӾӾлι͸͸ϺϺι̷̺μпϼ͸ȴưɱɱɯƪĨħǪʭдѵҸйϺ͸˸ʷ˸ʷʷ̷ηϵ̰ɬ˩˧˩ʭˮ̲ͷͺѾнξͺ̹ʷʵʵɶʷʷʷ˸˸̹̹ͺͺλλλͺ̸̹͹ιϺϺϺιι͸ͺͺ̹ʷʷʷʷʷɶȵƳƳƳȵɶ̸˷̷̷˶˶ʵʵʵ͸͸˶DzįįƱǴŶȹŲĽvyXwdCr`{eûƳƳdzɵʵưĬūĬëƮɱǯªǼsw\oeJf]@d[>h_BlcDulMwVeoz~¼Ġɣ˧ǡžÝƢȢßſ½æĪūǭɯɱ̴Ϲθ̶ʴ˵ε˰ĥzqlffggjnsw{u|vwʾéǬ̲ͳ̴˳ʹѸѸηϼоҿѾι̷̵Ͷ˲ȯ¸}{_wlPvkOynR}bu̵η˴ȱȳɴʷ˸ʷȵǴŲȶɷʷ˸̷̷̵̵εʱèf}rV{mPtU`oë˵ʶʷɶȵǴȴȴʳʳ˸ͺ̷ǰȾøʯϸϺ˸Ǵ̶̶̶̶˵˵˵˵θϹϹϹθͷ̶˵˵˵˵ʴʴʴɳɳɱʱ˰ǭĽxmmp{Į˶ʴʴʴʴ˵̶ͷθ̶̶̶˵˵ʴʴʴʴʴʴ˵˵˵˵˵ͷθθ̶ʴɳʴ̶ʴȲDZȲ˵ͷθθ̶ɳưưɳͷθθϺϺϺιι͸̷˶˶˶ʵʵʵɴɴɴ͵̴̴̴̶ͷθϹλλͻ̺˹ʸʸɶʴ̶ͷϹθͷʴɳʴ˵˵̶˵ʴȲDZɵ˶͸ϺѼѼѼѼι̷˶˶͸͸̷ʵʹ˺λнн̸ȴĮʲʲʰʭȬȬɭʮѷϸη͸͸ͺ˺ʹɸ˸ͺлҽйͶ̲ͰͰͱ̴̲ͷ̹̺Ѿнϼλͺ̷̷˶˸˸˸˸̹̹̹̹λλλλλͺ̸̸̹ιιϺϺϺιιͺ̹˸˸ʷ˸˸̹ȵȵǴǴǴȵʷ̸˷̷˶˶˶˶ʵʵ͸͸˶ȳƱƱȳʷ˹˹ƲüqrQmZ9gT3`M-[H(VC%WC(cO7p_AuHN_jqqrrqwœÝŠǤˮ˳͵ϷϷζ̴˳ȰȰɱʲ˳ʲʲɱʱ˲˲̳̳˲ʱʱƭǮʱ̳ʹεʹʹɰ˲εѸҹҹѸзззз϶ϷѸѹѹҸжͳͳδϵϷϷϹккϹͺλнҿϼϼλͺ̹ͺͺͺͺͺͺͺͺͺͺͺλϼϼͺ˸ʷ̹ͺϼнѾнλͺλϼннѾѾѾѾҿҿҿҿҿѾннϼϼнннѾѾѾҿҿнѾҿҿѿѿѿɷɷ̺̺˹˹ʸʸɷɷʸ˹ͻϽϽμͻ̺˹˹̺̺̺̺ͻλͽȵɷɴǽmrcFhW;eT8gV:iY?xhQ{Ŷ¯ͺ̹ɵȴɳʴʲɱȱɯȮȬȬƩĽ½åħëí¯ıǵʷȶƳIJpk_IcT=]N7[O7YM7dYCjŽîǴǴȴʶ̷ȲȰƬŭŭƮǯƮĬɾh~tYulOsjKvmNzqP|sRvU}Yaflotz½þ½þ¥éūǭɯ˳ζкθ˵ɳɳ˳еͯȩģÿ~|zxy{|}}´Ĺé̱ϴγ˱ʰɱʲʹ϶зθλϽѾнϺ͸Ͷηʴȯǽ{ppu»ɯййη˴ʵʵʷɶɶȵǴƳɷʸ˸̹͸͸ͶͶͷʱīùvkiozǻǮ˵ɵ˸ʷɶɶɵɵ˴ʶ˸̺̹ʵƭ§Ĩǫ˰϶лλ˹ɷ˸̶̶̶̶̶̶̶ͷθθϹθθͷ̶̶˵˵˵ʴʴʴʴ̴˴̳Ͳʭ¦»ŭ˶Ϲ˵˵˵˵˵̶ͷθ̶̶̶˵˵ʴʴʴʴʴʴ˵̶̶ͷͷθϹθ̶ɳȲɳ˵ɳȲDZDZʴ˵˵˵ʴů¬DZ˵ͷ̶͸͸͸̷̷˶˶˶ʵʵʵʵʵɴɴɴ̴̴̴̴ͷθϹϹннϽͻ̺ʸɷɶʷ̶ͷθͷͷ˵ʴͷͷ̶˵ʴɳȲȲȴʵ̷͸ιιϺι̷˶ʵʵ̷͸̷ʵɸʹ̹ϼнλɵůɳɱȰɯɯʮ˯̲˴ʵʵ˶ͺλͼ̻ȷ˺ϼҿҿл͸ʳ˴˱ʳʳ˵˸̺ͻннϼϼϺιι͸˸˸̹̹̹̹̹̹λλλλͺ̹˸˷˷̷͸ιιιι͸˸˸ʷʷʷ˸̹̹ȵȵȵȵȵɶʷ˷ʶ˶˶˶˶˶˶˶ι͸̷ɴDzȳʵ͸̸˸DZŻlzkJfS3]J)WD$S@"M9L8XD,gW6zn>v@~IPQP|LyKxLWmšȢ̥ΫͰ̴̴̴˳ɱȰǯƮȰ˳͵Ϸζ͵̴̳̳̳ʹ̳˲ʱʱƭǮɰʱ˲̳̳̳ʹ϶ӺռռӺ϶ʹ϶϶εε϶зѸҹжжϵδδδζϷͷϹϹϹͺͺϼѾϼϼλͺͺͺͺͺͺͺͺͺͺͺͺͺλϼϼͺʷʷ˸ͺϼнѾнλͺλϼннннннннѾѾѾѾнннннѾѾѾҿҿҿѾҿҿѿѿȶȶ̺̺̺̺˹˹˹˹ʸ˹ͻϽϽμͻ̺˹˹˹̺̺ͻͻλ˻ɶ̺͸ȽqwiNn^Dk[An^DoaG~oXǺïλ͹˷ʶ˵̶̶˳͵̲̲̲ʰɬĨ¥ÿ©¬įŲƵɹͼʺȵȵ¿tk`J`T"TC)_N4xfNdia_[Rq@q[,bLfQ&sb7V|ŸţŦǬǯʲ͵ζ͵˳ʲɱʲʲ˳̴ζϷзͷηηηεʹ̳˲ɰȭƬūūǪȮɯɯ˲̴εзѸҹӺ϶ε̳˲ʰʰɯɭˮ̰̱ͱ̲ʰȱȱʲ̴θθͷ̶ͷθͺ̹˸ʷʷʷʷ˸ͺͺͺͺͺͺͺͺλϼλ̹ʷɶʷ˸ϼнѾнλͺλϼλλλλλλͺͺλϼϼннѾѾҿҿҿҿѿѿѿ˹˹ʸ˹ͻϽϽμͻ̺̺ͻͻͻͻͻͻμ̺˹˹ʸʸ˹˹ͺνʷǴ¯lighsDzԾ˸˸̹ͺϻмкѻϹѸӺӺҹзʹ˰ǮȮȮɱʵ̸μο̽ǵƴȴï~fwpVpfMrkQ|t]n¯Ųȴ˷͹θ̳˲ɰǮůůİİ˹ɶǴʴͷζ̲ʭþĿþ¼ýľſƿǪȮʲȰŭĬưȲȲɳɶʷ̹ͺλкʹγϴггϰί̬˫˫ʭʮɮȯȯȯɬ˫̮˭ɬȮʲ̴͵̲˱ͰϱаϯήʭʮɮɭȮɯ̵ϸиикϹθͷ̶˵˸ͺλλͺ̹ϹкҼиζ̴˳͵ζж˱ʰʰɯȱɲɲʳιϺнϼͺ˸ʷ˸ʷʷʷ˸˸˸ͷͷϹϷζζζ͵͵ͳηδ̶̵̵˴˴˴ͷͷͷͷ͹͹ͺͺ˸˸˸˸˶ʳȲȲưưdzɴ˸̹λλͺͷͷͷͷͷͷͷ̶̶˵˵˵ʴʴʴ˵̶̶̶̶̶̶̶̸ʴȲȯʮͯвӵ̮ΰϴѵ϶̵ȳűɳʴ˵ʴȲȲʴ̶ͷͷθθͷ˵ɳȲ˵ʴɳȲȲʴ˵̶ͷͷͷͷͷ̶ʴʴͷͷͷ̶̶̶˵˵ưưưDZɳ˵̶ͷιιϺι͸̷˶ʵŰƱDzDzƱƱȳʵ˳˳˳̴ͷθкѻҿѾоμͻ̺̺˹˸˸ʷɶɶɶʷʷɶɶɶɶȵǴƳƲ̸͸ιϺϺϺιιιιι͸͸̷̷̷ɶȶȶ˹ͻϽλ̹dzưưưȲ˵θкͺͼпҿҿ˸ͺϼнνννп;ʻʻ̺μϼϼϻϻϻкѺһһһλͺ̹ʷʷʷʷʷͺͺͺͺͺ̹˸˷ȴʵ˶̷͸͸͸͸ɶǴƳǴɶʷɶǴƳǴɶʷ˸ʷʷʶ˷̷˶ʵ˶̷ιϺι͸͸̷̷˶˶ʵͷǮëĪȾewhK]L0N=!L;Q@$VE+m]Ci{zjzNw^5hO&iQ+t`;^ú¦īƭǯɱȰɱ˳˳ɱ˳ɱȰɱ̴ϷиѸкη̵˴̳ʹʹʹƬǭȮƪçæŪʮʰͳжж̲ʰ˰ͲѶ̱ʮɭʮɭɭ˯ȬɮǭŪŪɮ˱Ȯδͳ˳ɱ˵θθ˵ɶʷ˸̹ͺλλλͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺнϼϼϼϼϼλλнϼλͺͺͺͺͺϼϼнѾҿҿҿѾѾоооѿѿѿѿѿ̺̺˹̺ͻͻμμμͻͻͻͻͻͻͻͻͻ˹˹ʸʸʸʸ˹̹ϿͼʷʷƵƾʴкʷʷ˸̹κϻϹк˵̶϶϶ε̳ʱȯŬƬƮǯdzɵʸʻʻʸǵdzʶɴtns}ͶůůŲǴɵʶ˷̸θεʹ˲ʴʴʶʶμʸǴƳȵʴʲɯƩȪȪǧġÿþ»üĽĽž¦çĨũƪǫƪƪȮʰ˳ɱƮĬůưɳɳɶʷ̹ͺλλͷεγγͲˮʭɬʭʭʭɯɰɰȲɰˮ̯ͯͯɯɱʲ̴ͷ̴˳̲ϲвϱΰͱβͱ̰˱̲ηѺζζθθͷ̶˵ʴʷ̹ͺͺ̹̹θккϹζ̴̴ʹ͵ζ̲˱˱ʰɲʳʳ˴͸ιнϼ̹ʷʷ˸ʷʷʷ˸̹̹θθѻѻѹиизϷϷ̶˶˶ʳʳɲɲɲͷͷͷͷ͹͹ͺͺ˸̷˷ʷʵʵʴʴȲɳʵ˶̹͹ϺϺθθθθθθθθͷͷ̶̶˵˵˵˵˵˵˵˵˵˵˵˵ͺ̹˵˵ʹϳҴӵͯΰϳ϶η̵ʵdzʴ˵˵ʴɳȲʴ̶ͷͷθθθ̶˵ɳ˵˵ɳɳɳʴ˵̶̶ͷͷͷ̶̶˵ʴ˵˵ʴʴʴɳɳɳɳɳɳʴ̶ͷϹкϺϺϺϺι͸̷˶ƱDzDzDzƱƱȳʵ̴̴̴͵θкѻҼҿѾϽͻ̺˹˹˹ʷʷɶɶɶʷ˸˸ͺͺͺͺ̹˸ʷʶ̸͸ιϺϺιι͸ллϺϺιιι̸ʸɷȶɷ̺ͻ̹˸ƲƲűůdzʴ͹ϻμνпҿҿҿѾлͺнпͼ̻˺ο̽˺̺λϼкκθθϹййѺѺλͺ̹˸ʷɶɶȵ˸˸˸˸˸ʷɶɵȴʵ˶͸͸ιι͸ɶȵǴȵʷʷȵƳŲǴɶ˸̹˸ɶɵ˷˶˶ʵ˶̷͸ιιι͸͸̷˶ʵʳƭ§ǼexiL_N2P?#L;Q@$XH.ueKtƸŶƴ¯s}U{_:kO*pV3~gGiŭǰɲǮǯȰȰɱʲʲɱ˳ʲȰʲ͵ϷиѸйη˴˴̳ʹʹ̱ɯɬǫƩŨŨŨƩɮʱ˲̲̲̲δѶе̱ȭʮ˯˯ɭƫȾȼŹǽŬʰ̲ͳ̴ɱʴͷͷ˵ɶʷ˸̹ͺλλλͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺϼϼϼϼϼϼϼнѾнϼλͺλλλннѾҿҿҿҿҿҿѾоооѿѿѿѿѿ̺̺ͻ̺̺̺ͻͻμϽϽμμͻͻ̺̺˹˹ʸʸɷɷʸʸ̹ͽͼʺ˸ɸȵIJųİïƱʴ̶̶̶ɶʷʷ˸̸͹κκ˴̵ͶͶ̵˳ɲǰŬŬŮƯƳǴɸʹǶȷȷȵʷ̷ƯƮԼɳɳȵȵȵȵȵȵθͷͷ̶̸̸͹͹о̺ǵŲŲDZȲȰʲ̲δϲ̮̮ͯͰɲɴɴɴȳȳȳDzȳɴʵ˶̷̷̷˴˳̴̴ʲDZůůůȵɶɶʷ̺ͻͻλθ͵ʹ˲ɰɭɭȬʮʯɯɲȲɳʴ˲βϲϲͳʰʲ˵ͷͺ̶˳̲δггαгϵϵδ̲ͳиҺͷͷͷͷ˸ʷɶȵɶ˸̹̹˸˸̹λͷͷͷͷͷͶ˵̳ͳ̲˱˱ʳʳ˴̵̷͸ϼλ̹ʷʷʷɶʷʷ˸̹ͺλλѻѻкккйϹϹ˸˸ʷ˵˵ʴʲʲ͵͵ͷͷ̹̹̺ͺ̷̵ʴʴɴ˶˷͹˷˷͸͸θθϸϸθθθθθθθθθθͷͷͷ̶̶̶˵˵˵˵˵˵˵˵̷̹̹ͷεϴввϰϰβδͶ̵˷˷ʴ˵̶ʴɳɳʴ̶ͷͷθθθθͷ̶̶˵ʴʴɳʴ˵˵̶̶̶ͷ̶˵˵ʴ˵˵ʴʴʴɳɳɳ˵˵˵˵̶θϹк͸ιι͸̷˶ʵɴƱDzȳDzƱƱȳʵ͵͵͵͵ϹкҼӽнϼμ̺˹˹ʸʸɷɷɷɷʸ˹̺ͻϽϽϽϽμͻ̸̺̹͸ιιιι͸͸ιιιι͸͸͸̸˹ȷǶǶʸ˹ʸɷŲŲŲűǴʵ̹ͺͻͼнѾѾѼϺηлҽҿͺɸȷοͼλϹкиͷεε϶ϸϸϺлͺͺͺ̹˸ɶǴƳɶɶʷʷɶɶȵȴɵʵ̷͸ιιιιɶɶɶʷ˸ʷƳðŲǴʷ̹̹˸ȵȴʶ˶ʵʵʵ˶̷͸ιιι͸͸˶ʵɲ襦·exgMaP6Q@&M<"SB(_O5pVɼȻ˻ǵz|Yy\:iL,rW9kMqíɶʷ̷ʴʲʲ˳˳˳˳̴̴˳ʲ˳͵иизϸ͸˴ʳ˲̳˱ɯʰȬƫȫǫŧʿźŹǻǮ˲δ̲ʰδ̱ɮ˯ͱ̰ƪÿ{{Ⱦǭ̲͵ɱȯ˲ʹʴʷʷ̹ͺλλλͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺλλλϼннѾѾѾѾнϼλϼϼϼѾѾѾҿҿҿҿҿѾѾҿҿҿҿѾоооѿѿѿѿѿѿͻͻμͻ̺˹˹ͻϽооϽμμ̺̺˹ʸʸʸɷɷɷɷʸ˸ͽν̼ʺɷȷɹ˹̼ͺλкѻйͶʳʶɶɶʷ˷̸̸̸ͶηηϷϷζ˶˳ʱʱɲɲʵʷ˺̻ɸ̹ͺʵɴʳɰƫǬūƮŭɱϷɳɳȵȵȵǴǴǴʴʴʴʴʶ˷̸͹ͻ̺ɷȶǴȵȵɳɳ˵͵͵̲̲ͳϷ͸̹̹˸˸ʷʷʷɶʷ˸̹ͺͺ̷̶̴̹˳ʲɳDZDZDZɶɶʷ˸˹̺ͻͺθ͵̳ʱɰɰʱʱʳ˲̴̵̶̶̳ͷжѷжζ̴ʴ˸̹ͻ̶̹͵Ϸжϵδͳδδͳ˱˱͵Ϸͷͷͷͷ̹˸ʷɶɶʷ̹˸ʷʷ˸ͺ˵̶ͷϸϸͶ˶ɳ̴˱˱ʰɲʳʳ˴˶͸λλ˸ʷʷ˸ʷʷ˸̹ͺλϼϼθθθηηη͸͸ͺ̺̺̹ͷͷ͵͵͵͵ͷͷ̹̹̺ͺͶ̵ɳɳʴ˶͹ϻ͹ι͸θͷͷͶͶ̶̶̶̶ͷͷͷͷϹϹϹθθͷͷͷͷͷͷͷ̶̶̶̶ȷʸ˹̷͵ʹͱ̮бβͳ̵̲Ͷ͹κ˵̶̶˵ɳɳʴ̶ͷθθθϹϹθθ̶̶˵˵ʴʴʴʴ˵˵̶̶̶˵˵ʴͷͷͷ̶̶̶˵˵ʴʴʴʴ˵̶ͷͷ˶˶˶˶ʵɴDzDzDzȳɴȳƱƱȳʵ̴̴̴̴θϹѻҼϼλͻ̺˹ʸʸʸȶȶȶɷʸ˹ͻͻͻͻͻͻ̺˹ʸʷ̸ιιιι͸̷̷˶˶˶˶˶˶ʵɵͻɸǶƵȶɷɷȶŲııŲƳȵʷ˸˹̹λлѼѺйѷӹӼһϺ˶ǴŴŴ˺ͼϾнкииѷϴϴ϶϶ϸϸллͺͺͺ̹˸ȵƳŲȵɶɶʷʷɶȵɵɵ˶̷͸ιιιιɶɶʷ̹̹ʷŲ¯ŲǴʷͺͺ˸ȵdzɵʵʵʵʵʵ˶˶͸ιιι̷˶ɴDZĪħ¥Ļ~|bwfLaP6SB(P?%WG-gW=|aø¹źɼŵ{wUrX7eH(lQ3hJq¬ɶ˸ʷʴȰȰʲʲɱɱ̴͵̴˳̴ζиϷ϶͸˶ʳʳ˲˲ɯȮȬǪǪɪɫŹĬʱƭéͳ̲ʰ˰Ͳͱçt|bqV{mSv\rĽȮζɱƭʱʹ˲˸˸̹ͺλλλͺͺͺͺͺͺͺͺͺ̹̹ͺͺͺͺλλͺλλϼнѾҿҿѾѾнϼϼннѾҿҿѾѾнннѾѾѾѾѾҿҿҿҿҿҿоооѿѿѿѿѿѿѿѿѿѿͻͻμͻ̺˹˹ͻϽооϽμμ̺̺˹ʸ˹ʸʸɷɷʸʸ̹̼νξͽʺɹɺ˺ʻʹʹ̹κϺζ̴ʴʴʴ˵˷˷˷˷ιιιιιθι̷̵̳ʴʴʶ˷̹̹ɶλλɵůȯʯɮγʯǬȭȯǮǮʱưưƳƳǴǴȵȵȴɴɴɴʵ˶̵Ͷʷ˸ͺλͺ̹˸˷ʶ˷̸˷ʶʶ˷̹ʷʷʷʷ˸˸˸˸ʷʷ˸̹̹̹˸˶˵ʲʴʴ˵˵ʷʷɶɶʸ˹˹̺ͻͺͷ̶˵ʴ˴͵Ϸи͸ͶͷθικͺϹѹҹҹϹ̶˷̹λͽͺ̸ͷϹѸзϴϴееϴ̶̳̳ͷθθͺλͺͺ̹˸ɴɵ̷˶ʷʷ˸ͺʶ̸ϺлϺ͸˷ɴʲɯɯȮǰȱȱɲ˶͸λλ̹˸˸̹ʶʶ̷͸λϼϼнκκιι͸͸͹̹̼̼̼ͺ͹ͷεεγεͷͷ̹̹̺ͺͶ̳˲ʱʲ͵Ϻл͸͸Ͷ͵ʹ̳̳̳ʴʴʴ˵˵̶̶̶ϹϹϹϹθθͷͷϹϹϹθθͷͷͷǸȸʹ̸ζεͱ˯дϳ̳˲ʴ̸ͻλ̶ͷͷ˵ɳɳʴ̶θθθθθθϹϹ̶̶̶̶˵˵ʴʴʴʴ˵̶̶˵˵ʴͷͷͷ̶̶̶˵˵˵˵ʴʴʴ˵̶̶˶̷̷˶˶ɴȳDzȳɴɴȳDzƱȳʵʲʲʲ˳̶θϹкλλͻ˹˹ʸ˹˹ȶȶȶɷʸ˹ͻͻ̺̺̺̺˹ʸȶȵ̸ιιι͸̷˶˶ʵʵ˶˶˶˶˶ʶ̹ɷȵȵȷȷȶƴŲŲŲƳǶȷʸʷȶʷͺлѺӹӷӷҶϳɯºļ¯ɴͶϵгѴѴѴѴѶѸѺѺлϼͺ̹̹˸ʷȵǴƳʷʷ˸̹̹˸˸˷ɵ˶̷ιιϺϺιʷʷ˸̹ͺʷƳ¯Ƴȵʷ̹̹ʷǴdzɵʵʵʵʵʵɴɴ̷͸͸͸̷ɴDzĮ¥Ļz|dygOaO7UC+RB)\L3n^EkżvqOnV4bH'fL+fGoɴʷʵDZëªƮǯŭƮ˳͵̴˳̴ζϷ͵ʹ˶ʵɲɲʱʱȮƬĨæĨǩħµ}jv\zbn˽ĪƬʯͱͱ̯̯ʭÿvx^o_EbT9]O4aV:znTo¦δʰƭʱʹ̳̹̹ͺλλλλͺͺͺͺͺͺͺͺͺ̹̹̹ͺͺλλλλλϼнѾҿҿѾннϼϼнѾѾҿҿѾннϼϼѾѾѾѾѾнннҿҿҿоооѿѿѿѿѿѿѿѿѿоϽ̺̺ͻ̺̺̺ͻͻμϽϽμμͻͻ̺̺˹̺˹˹ʸʸ˹˹ͺ˻νϿϿξͽ̽;̼̽˹̹Ϻлиζ˵˵˵˵˷˷˷̷˶˶˶˶̷̷̷˶˴ʱɳȲȴȴȵɶǴ̸̸DZŬȭ̯̯е̱ȭǬȯɰɰɰĮĮıŲƳǴȵʶʶʵʵʵ˶̷ͶηǴ˸ϼѾнλ̹̹λϼξ̼ʺʺ˻λ˸˶̷̹ͺιλϺ̷̹ͺ͸λ͸ͺ̷˵ʲɳ˵ͷθͺ̹ʷʷʸ˹˹̺̺̺˸̶̶ͶηилѼιιϺϹικͺϻиҹѻϻ͹̹̼ξξ̼ͺκмѻк϶ѸҷӸѶεʹ̶ͷθθλλλͺͺ̹ȴʶ̷̷˸˸̹λ˷̸Ϻлмκ̸ʵɱɯȮǭƯǰȱȱ˶͸ϼϼͺ̹̹ͺ̸̸ιιλϼϼϼмϻллϻϻϻμʻɻɹ˸˷˵ʹʹγγ͵ͷ̹̹̼ͺʹβ̳̳˳͵ιϺ͸Ͷ͵͵ʹβͱͱʱʴʴ˵̶̶ͷͷϹϹϹθθͷͷͷкϹϹθͷͷ̶̶Ⱥɻ˻κϺиϴдѵдʹ̳˷̸̺ͺ̹θθ̶ʴɳʴ̶Ϲθͷͷ̶ͷθθͷͷͷͷ̶˵ʴɳɳɳʴ˵˵˵˵ʴ˵˵ʴʴʴɳɳɳθͷͷ̶̶̶̶ͷ͸ιι͸͸˶ʵɴɴʵʵɴDzƱȳʵɱɱɱʲ˵ͷθϹϼλͻ̺˹˹̺̺ɷɷɷɷʸ˹̺ͻͻͻͻͻ̺˹ɷɶ̸ιιι͸̷ʵʵ͸͸͸͸ιιι͹˸ɷɶʷʹʹȶƴŲƳƳǴȷɸʸʷɷ˸͸йӹԸԷԷαȫ»éʭͯϱѳӴӴҵѶѸѺнϼ̹˸ʷɶȵȵȵɶ˸˸̹ͺͺͺͺ͹ʶ˶͸ιϺϺϺϺʷʷʷ˸̹˸Ǵıȵɶʷ˸ʷɶȵȴȴɴʵʵʵɴȳDzʵ˶̷̷ʵȳį©~j~lTbP8WE-UE,^N5rdJqþroLmU1fL)fL)jHqɲɳǰưƮǯĬǯζ͵̴˳̴͵͵˳ʱʵȳȱɲʱɰƬŨķb}lNp\Cq]EvbJqYoɹȮ¨˰γ˯ʮʭq~mSaQ7VF,SC)UG,i]Cz_»δ˱Ǯ˲϶εθͺλλλλͺͺͺͺͺͺͺͺͺͺ˸˸̹ͺͺλϼϼϼϼннѾҿҿҿϼϼϼλϼнѾѾҿҿѾѾннϼϼѾѾнннϼϼϼѾҿоооѿѿѿѿѿѿооооμ̺̺˹̺ͻͻμμμͻͻͻͻͻͻͻͻͻͻͻ̺̺̺̺ͻλξνͿ˿ξͽ̺λлѼиϷ̶˶̶̶̸˷̷̷ʵɴɵʵʵʵ˶˵ʱȮǯưŲŲŲŲȵ̶̹ǯūɬ̯ˮ̱ʮǫƪȯ˲˲ɰưưƳƳǴȵȵʶ˸ʷʷɶ˶˶̵Ͷɶ̹ϼϼ̼ʺɹʺξξ̾ʼʼ˽ͽͺͶ͸Ͷιηιη͸Ͷιηιη͸͸θ̶ʴ˵θϹͺ˸ʷʷ˹˹˻˻̼̺˸ͷκϺϺϺллϺιι̹ͺ̺˸˸θϹмκͺ˻ͽͽ̼λмѻкззҹҹѸε̳ʴ˵̶ͷ̹ͺͺͺ̺̹ɵ˷͸͸̹̹ͺϼ͹κϼϼмκι̷ʲɯɯȮǰȱȱɲ̷ιннλͺλϼϻκϺϺϼλλλϻϻϼλϻϻϺμʻʻʹ˸̷ͶʹͲγγ͵ͷ̹̹̼ͺεбϳγͳͳ̵̵ͶͶζδϴϳϳϳ̳˵̶ͷθθϹϹθθθθͷͷͷ̶θθͷ̶˵˵ʴʴǺȺʺ̹κиѶҶѵѵ϶ε̸˸ʹ˸ͺθθ̶ʴɳʴ̶кϹͷ̶˵˵̶̶ͷͷθθͷ˵ɳȲȲɳʴ˵˵˵˵˵˵˵ʴʴʴɳɳɳθͷ̶˵˵˵˵˵̷̷̷̷˶ʵȳȳʵ˶˶ɴDzƱȳʵʲʲʲʲ̶ͷϹкϼϼμͻ̺ͻͻͻʸʸɷɷɷʸ˹˹ͻͻͻͻ̺˹ɷʷ͹ιι͸̷˶ʵɴ͸ιιιϺϺлϻɶȶʸ̺ͼ̻ɷƴŵƶȷɸʸ˹˹ʷͻλлһӷӶҵӳɩƿ}m~vaxp]vn[~vapȨ̬ϯұҳѴезйλλ̹ʷȵƳƳȵʷ̹˸˸̹ͺλλͺκʶ̷͸ιϺϺϺϺ˸ʷɶʷ̹˸ɶǴ˸ʷʷɶȵȵȵɵdzɴɴʵɴȳDzƱȳɴ˶˶ɴƱ­rpXeS;YG/UE,]M4seKv»¦Ŧ£l}kGkU.fN(jP+oKsȬǬ¼¼ſ¼ſȰ̴˳˳˳͵̴ɱȯȳDzDzȱɰɰūħȿƻj|mLiX:^J1_I2aK4hT[I1VD,\L3tdKyŷɿƫǩ¤fzg?jS*gN%mT+sKrIJ̿Ũ¥˱ʲʲ̳ʹ̳ɰƭǰǰǰȱɰȭūæ¥ȿ·y}\q`B^M/S?&UA)XD,]I1gS;vdLy`n˱ɮƫʮȬtZcS:[K4VF/N?*WK5k`JrͳϲȮγӸѸϹλϼϼϼλͺͺͺͺͺͺͺͺͺͺ˸˸̹ͺͺλϼϼҿҿѾѾѾѾѾнͺͺͺͺͺλϼннннннннннннϼϼλλͺѾѾоооѿѿѿѿѿооϽϽϽ̺̺ͻͻͻͻͻͻͻͻͻμμμμμμμμͻͻͻͻͻͻͻͻͽξξξξλͺ̹ͺ͹͹ͷͷηηη̵̳̳̳̳̳̳̳ͶͶηϷηζ͵̵˵ʴɳȲDZDZDZDZȲʴ̶̶ʲȰǯǯƮĬíĮDZ˵ͷθι͸̷˶ɴȳDzDZǴȴȵɵȵʶɶʶʷ˸˸˸˸ʷɷȶ˹˹˹˹˹˹˹̸̶̶̶ͷͷθθθ̹ͷ̹̹̹̹̹̹ͷͷͷͷͷͷͷͷɳɳʴʶʶʶʶʶȵȵȵɶʶʶ˷˷̸̸˸˸˸˸˸˸˸ͺϼннϼͺ̹ɹ˸̹ͺλϼλκ˷̸͹κ͹̸ʶɵλλλͺͺ̹̹ʸͺͺͺͺͺͺͺͺͺͺͺκκκκκʶʴʴ˵˵˵˸̸ϼнμλ̺̹˻ʸλϹϹϹϹϹϹϹθθϸйлѺѼѼμξμϼμϻϻϹθθθϸθϸκϸ϶зззз϶εʹθϹϹϹϹθͷ̶͹͹κϻммѽѽ͹͹͹͹͹͹͹͹˷˷˸˷˸˷ʶʶʹɸʸ̹κκ͸˶лϺϺι͸̷̷͸ͷͷ̶˵˵˵̶̶ѻкϹͷ˵ɳȲƳȸʼ̿ξ;˺ɶƳʴ˵˷˶˶̷͸κҿпνʹȸƶǷɹ˸ʷȶǴǴȵʵ̹ƶƶƶŵƵƳdzƲ˶͸Ͷ̵ɳȲȲɳȴʶ̸ϻϼноооͼʹɸʹ˼̻˹ʷʷɶɶʷʷ˸ͺͺ̹˸˸ʷɶʷ̸κϺηʹ˲̱Ͳͳ̳ɳ˶ͼο̻DzDZʳ˶̹̹˸ʹ̻˺ʹʹ˸˸ʷɶͽҿн͹ͷϹӺ̲msnQe`Cd]AynRjĪͲз϶ѸҹҹϷϷϸϹл̸ʷȵƵųǶ̺ʷɶȴʶ̶θ͸̶͹˸ʷ˸κϻϻκ͸˶ȳƱɳ˵ͷθ̶ʴʴ˵̶ʴȲDZɶ˷˸̸ʷʶȲDZĮŬĮŬĬŬĬŨȽƹȻȹŴxlTlV>`J2V@)_K3mXɷìƭŪ˽^xa8jR&lQ$pU({Pv®ʺæĸxbfklmséǮǮɱʲŭ©èǬɬɬȫȽydxcFhS8mX=cQ9fV?bP8_M5fU;iY?rbHsYṴ̄ǭĩèæfhYDTD4L3YK@h\NsɭҴ̯ʭδҺҼϹ̶̶ͷθθͷͷͷͷθθϹϹϹϼλλͺͺλλϼнϼϼϼϼϼϼϼͺͺλϼϼнѾѾнϼͺ̹̹ͺϼнҿѾнϼϼϼϼннннѾѾҿҿҿннннннннλϼѾҿҿѾλ̺ͻ̺̺̺̺̺̺̺̺μμμμμμμμμμμμμμμμͽξξξϼλ͹̸͹͹ͷͷͷͷͷͷ̳̳̳̳̳̳̳̳ʹʹεεεʹ̳˲ɳɳȲȲDZDZȲȲȲɳ˵ʴȲDZDZȲʵȳƱDzʵ͸ϺϺʵʵʵɴȳȳDzDzƳǴǴǴȵȵȵɶʷ˸˸˸˸ʷɶȵ˸˸˸˸˸˸˸̸̶ͶͶηηϸκκ͹͹̹̹̺̺˺̹ͺθθͷͷͷͷͷʴ˵˵˵˵˵˵˵ȵȵɶɶɶʷʷʷ˸˸˸˸˸˸˸˸ͺͺλϼϼλͺͺɶʷ˸̹ͺͺͺͺͺλϼнϼλͺ̹ннϼϼλλλͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺ˷˷˷˷̸̸͹͹ннϼϼͽͽ̼ͺ̹θθθθθθθϹϹййѺѺһѻͻ̻ͻͻͻͻκκκκϺϺϸϸϸϸ϶зззз϶εʹͷͷͷθͺ̹˸˸ͺκκκϻϻϻϻκ͹͹̸̸˷˷ʶ˸˸˸˸̸̸̹̹ͺ̹̹ͺϼϼͺ˸ϻκ͸˶ʵʵ̶̷ͷ̶̶˵˵˵̶ͷккθͷ˵ɳȲǴ˽;˺ʷȳDzȲȲɴʴʵ˶̷̹ʹ˺ɸȸǷȸɹ˻̹˹˹ʷʷʵɷȸǺƹǹȷɶɵɴ͸ιϷ͵ʳɲɳ˵űƲȵʶ˹˸˹˹ѿϽ̻ʹɸɸʹ˺˸ʷʷɶɶʷʷ˸ͺͺ̹˸ʷʷɶʶ̸κϺϸεͰαϲͱ̯ɱʶ̻ο̿ʻȳɰʳ̶̹̹˺̺̺˹ʹ˸̸̹˸˸ɹ̻ͼͺ˸̷κѻ϶ƭhz]~wZrüǿǯ̶θιѼѻѻηͳ̳˳˳ʴɵɶǴƴǴɷɶȵȴɵ̶ͷ̵̶̹̹˸˷͹κͺ̷̹˴ɴɴ˵̶ͷͷͷ˵˵˵˵ʴȲǴȵɶʷʷʷɶȲȲŭƮƮƮƬūéæǼöôo{fKhP8`H0XB+eO8r]ɷп«ȹ\w`6lQ$lQ"x\-Syư˸̽z}_{hG}mLoNpQtT{]z¥Ǭƭǯǯŭê¨ƩȫǪƩźinSmX=jP7u]E{iQ|lSrbIiY@iY?gW=hX>tdJqèβɯŪèèkmaKXK;OA8O@;XKEeXOyhçϲ̮Ȫ˱ϵҼкͷ̶ͷϹθθͷͷͷθθϹϹϹϼλλͺͺλλϼнннϼϼλλͺͺͺλλϼнѾѾнϼͺ̹̹ͺϼнҿѾнϼϼϼϼннннѾѾҿҿҿннннннннλϼѾҿҿѾнλ̺ͻ̺̺̺̺̺̺̺̺μμμμμμμμμμμμμμμμλϼϼϼϼλ͹̸͹͹ͷͷͷͷεε̵̵̵̵̵̵̵̵ͶͶηηͶ̵˴˴ȲȲȲȲȲɳɳɳȲɳɳȲưůDZɳͷ˵ɳɳ̶θϹϹȲȲȲȲȲȲȲȲŲƳƳƳǴǴǴȵʷ˸˸˸˸ʷɶȵ˸˸˸˸˸˸˸̸̸͸͸͸ιι͹͹̺̺̺̺˺˺˺̹ͺθθθθθθθ̶̶̶̶̶̶̶̶ɶɶɶʷʷʷ˸˸˸˸˸˸˸˸˸˸͹͹κκκκ͹͹ɵɵʶʶ˷̸͹͹͹͹κϻϻϻκκѽѽмммϻϻϻͺͺͺͺͺͺͺͺλλλλλλλλ̶̶̸͹͹͹κκммннϼϼϼϼ̹ͷ̶̶̶̶̶̶ͷͷθθθϹϹϹ̺˺˺˺̹̹̹̹κκϺϺϸϸϸϸϸййййϸͷ̶˵˵˸̹̹̹˸˸θθθͷͷͷ̶̶ѻѻкϹθ̶̶˷˸˸̹̹͹͹κκ̹˸˸̹ͺλ͸˶Ϻллι̴˳̴Ͷ̶̶˵˵˵˵̶ͷкϹθ̶˵ɳȲǴ̼˾ʾ̽ʺʸɵɲȱɱʱʳ˳˵̵˵Ʋȵʷʷȶƴųƴȶɶʷ˸˸˸ɶǵȸȹɹɹʹ˸ʶ˶̷̷͵˳ȱȱʱ̶ȲȲȵʴɶʴʷʷϼλ̹ʷɶʷ˸˸˸ʷʷɶɶʷʷ˸ͺ̹̹˸ʷɶɶʶʷ̹ιεͲͰͰαˮʮɱʴ̹̻ʾȺȳȲʵ̶˸̹̻̺̺̺˸˸ʶʴɴʴǴȵɶɵȴȴ˷ͷεɰë¼}ȾëDZȲ̸ϻѻкʳƬʾп­űƳƳɶȵdzȴ˵̶ʳʴ̹˷˷̸̹θ̶ʴȱɯȱȱɲɲʳ˴ͷͷ˸ʷɶɶȵǴƳǴȵɶɶɶʴɳǯȰɱɱȮƬéø~wpx]mX=aI1]E-[E.kU>xcȶ̽ʾĵZwa8lT(kS%|a4X{űȸƸpnPlY8l\;l\;k\=pcCynPqȭȬūūƮūŨƩŨħxpTkW<^I.hN5~fLjmy_wfLm\BcR8_O5hX>~céжʲê§ppdNZM=PB9N?8TG?]PGriX{»ͱ˭ƨǭ˱ӽкθͷθϹϹθͷͷͷθθϹϹϹϼλλͺͺλλϼѾѾнϼλͺ̹̹ͺͺͺλϼннѾнϼͺ̹̹ͺϼнѾѾнϼϼϼϼϼнннѾѾҿҿҿннннннннλϼѾҿҿҿѾнλ̺ͻ˹˹˹˹˹˹˹˹μμμμμμμμϽϽϽϽϽϽϽϽλϼϼϼϼλ͹̸̸̸̶̶̶̶ʹʹ˵̵̵̵̵̵̵̵ͶͶηͶͶ̵ʳɳưDZȲɳʴʴ˵˵ȲȲȲưĮůDZɳ˵ɳDZDZɳ˵˵ʴɳɳɳɳȲȲȲȲŲŲŲƳƳƳǴǴʷ˸˸˸˸ʷɶȵ˸˸˸˸˸˸˸̸̸͸͸͸͸͸̸͹˹˹˹˹ʹʹʹʹͺθθθθϹϹϹθθͷͷͷͷ̶̶ʷʷʷʷ˸˸˸̹̹̹̹̹̹̹̹̹͹͹͹κκ͹͹͹ʶʶʶʶ˷̸͹κ˷˷̸̸̸̸̸̸ммϻϻϻϻϻϻ̹̹̹̹̹̹̹̹λλλλλλλλͷͷ͹κκϻϻϻммммнмѾѽͷͷͷͷ̶̶̶̶˵˵˵˵̶̶̶ͷ̹̹˺̹̹̹̹̹κκϺϺϸϸϸϸϸййййϸͷ̶˵̶̹ͺͺͺ̹̹θθͷͷ̶˵ʴʴθθθͷͷ̶̶˵˷˷̹͹κϻϻм˸ʷʷʷ̹˸˶ȳ̷ϺѼйͶ˳ɲ˴˵˵ʴʴ˵˵̶ͷϹθͷ̶˵ɳȲǴʺʺɺɸɷɵɳʱȯȮǭȯȯɱǯŭǯɲ˴˴ʴDZưůDZǴǴȵȵȵȵǵǷȸɹʹʹ˸ʶʵȳȳɱȰƯǰʱʹ̴˳ɱȰǯǯȲȲ͸̷˸ʷɶʷʷ˸˸ʷʷɶɶʷʷ˸̹̹˸˸ʷɶȵȵɵʶɳǯƩħŦǨåŨȮ˳̹ͺʼɹɶȴʵʷ˹̹̻ͻͻ͹̹ʶȲƮŮŮȲȴȲȲȲɳʴ˵ʱʱɱƮƻɿſůDZ˷ͷϹ˳¨ĵDzȵɵǴdzȴʴ˵ɴɳ˵ʴ˷̶ͷ̶ȲŭƿžĽĽǿìǰʴͷ̹ɶȵɷɷȶŲŲƳǴȵɶʴʴɱʲ˳̴ʰƬ¨ti}_uWxcH]H-]E-ZD,^H1p\D~hɹ;īȾ_{h>p[.pZ+zc7Wy³ɺǹs}lNhU4fV5eU4dU6j]=wiLpĿ˰˰ƭƬȰɯŪé¨ħŨħ¥osbF\H-S>#fL3jPu}poUjY?[J0[K1iY?~cƬҷɰƿsthPZMYLCf]Loʮ˭ŨĪȰӽѻθͷθϹϹϹͷͷͷθθϹϹϹϼλλͺͺλλϼҿѾнϼͺ̹˸˸̹̹ͺλϼϼнннϼͺ̹̹ͺϼнѾнϼϼλλϼϼнннѾѾҿҿҿннннннннλϼѾҿҿѾнϼλ̺ͻ˹˹˹˹˹˹˹˹μμμμμμμμϽϽϽϽϽϽϽϽͻμμμλͺ̹˸̶̶̶̶̴̴̴ʹ˵˷˷˷˷˷˷˷̸̸̸̸˷ʶȴȴůưȲʴ˵˵˵˵DZȲDZưĮůȲʴʶȴƲdzɵɵɵȴ͹̸˷ʶɵȴdzƲŲŲŲƳƳƳǴǴʷ˸˸˸˸ʷɶȵ˸˸˸˸˸˸˸̸̹̹̹̹̹̹̹˸˹˹ʹʹʹʹʹʹ̹̹̹ͺͺλλλλͺͺͺ̹̹˸˸ʷ˸˸˸̹̹̹̹̹̹̹̹̹̹̹̹ͷͷͷ̶̶ͷͷͷ̶˵˵˵̶ͷϹк̶̶̶˵̶̶̶̶ͷͷͷͷͷͷͷͷ̹̹̹̹̹̹̹̹λλλλλλλλͷͷθθϹϹϹϹккккϼкнѻϹϷζζ͵͵̴̴˳˳̴̴̴͵͵ͷιιλιιιϹϹκκκκϺϺϺϺϺлллϻκ͹̸ͺͺλϼϼλλͺϹζ͵̴˳ʲɱɱǯȰȰɱɱʲʲʴ˵˵˸ͷθϹϹкͺ̹˸̹͹͹ʶȴȲͷҼҹʹȯǮȯʴʴʴʴʴ˵̶ͷθͷ̶˵ʴɳȲǴɸɸȶȴȲȰȰȮƬŪĩūƬƭêǭɭ˯̰ʰɯȱɲʳɴȳƱŲŲŲIJĴŵǷɸʷʶɵɴǯȰȱǰƭǮɱ˳˱ɯƬĪééĭĭ̵̵˶˶ʵʵʵ˶˸ʷʷɶɶʷʷ˸̹̹˸ʷɶɶȵȵȵư»Ĺɯ̷ͺ̼˻ɶȶɶɷʸʺ̻̻ʷ̸̹ɳū¦çɯɱȰɱ˳͵ζζȲ˵ͷ̶ɳưĮ«êéĬŭȲ˵ɳ˵˵ŭymfiowŻȱ̶ɵȴdzȴɶ˵ɴȲȲʱʴ̴ʲŭļļįʵͺɷǵʺʺǷIJıŲƳǴȵʴ˵ɱʲ˳˳ɯƬƿyh}ZlLwdDjU8T?"ZE*[E-bL5uaImʺǭkyP}l@|jiQ+kQ,jR.pZ5rNn¸μ`}c>oQ+uS0sO-pO.mS2p\;~mOkþƿǻƻȸĵfyKze8yb8lE{Wd|]kMsY>r]@y^øƿŦ̪̪Ţɹ{xXu\>gN0_G+aL/aM2_O5o_Fu]ȮɯĪĪάĮưȲɳʴɳɳɳɷʸƴǵȶȶɷɷȶȵǴǴǴǴȲȲȰȰʲȰƮƬɱ˱̴̴˵ɳDZůĮíĮĮĮĮůưȴɳʶɷŹijŲìƬʮ˯ǫéìDzȵijǭȫʭʭˮʰʰʰɱ˳͵͵˵ɳDzǰDZưDZɳƳïñȶïİƯȮɭɭɬʬƩŨħŨƩǪǨƧâĢƩɭƭŮíūŨĢûħαɬũʮ˱ǭʳʲȲDZŲƳǴɳǯȰȱʲʳ̴˴̴ɲʲɲɯɯȮȮȮȮȮȮɯɯʰʰʰȮȮȮȮȮȮȮɬ˭ˬʫɪȩȨȨȨƧǨȬɮ˲ɱDZ¬Ƭʰ̲̱ʯʮʮǪȫǪħæȪˬæžĪ˳̶ɶɵ˷͸ϺϺ͵ɯɯ˲̴˳ɲȱȱȱɲɲ˶ʳɲưĮĮíĭǭǭȮȮȮǯƮƮëĬƮǭȮǭƬũũƪǬɭɰɯȮǭǭȮǰɯɲ˱̲̲ͳ˴˴ʳʳ˴˴̵˴˴ʳ˱˱ʰʰʰ˲˲ʱʱʱɰȲɲɳʳ˶˶ʵʵɴȳʴʴʴʴʴʴʴʴɳȲín{qXoeLmcJkaHlbIwmTjŮƯǰɲʳ˴̵̵ǰɲƯìƯn{^{rStkLtkLxrR}]o|ľ¥øxbzlOqcFseHqcFseHtWpȽøø·yi]yqMsmKrlLztTjǫʯ`oeBi]7l`8nb:pdg\@h]Af[?dX>k_Ew]«ĭʳͶͶʳɲ˴ǰƯ«ǿmyrVpiLibEmfI~wZqüƨǪɫǫƩ¥æsXvfLk[Am]Ck[Al^C}oTtæŨǧƦƦƦã{ifel|§Ͱ˫¡eviFj\9j\7l^;m_UuƮ|`~kAuc;xQqήϱǪƽȹŲvuVnS5jL0gL1kP5hN7bJ2hO;p\Dhʿ¥ɯ̳ƭíĮůưȲɳʴ˵ʴƳƳɶʷʷ˸˸ʷʷɶDZDZDZDZDZDZDZDZɳȲDZưưDZȲɳɳȲưí¬ìĮƱįįŰŰŲȳȵɶʷʷʷ̹ͺͺ̹̹ɶɶɶ˸̹˸ȵƲDZƭɰ϶Ѹʹ˱δ˱˱ɭǫƩƩƩŨʮ˯ʮɭǫƪȬʮȬǫǫǫƪǫǫǭƮůĭŮǰɲʳʳɳɱɱȰǯƬūŨæåĦĤĤĤġ¢Ũ§§ĩȭ˯ʰɯūȯʱɰȯǯǯǰǰưưȲʴʴɰǮʱɰȭǬƩŨƩƩħŨŨŨƩǪƨåɾƽżƽȿ œ½þ·ʿéëİɾŰ˵̶ͷͷɰ§}xqnrȲ˷˸ɸͻ̺̹ʶʳɰɰɭ̱ϴ϶˲ɳ˵˶ɴȶƲĸþæƫȭƬǭŪæ˽ƹĵµĺɾ¦¥æĦŧũƪǭɯɲɲɴɴǰƬūŨǪǧá˾ɼɼɾŬDZȲȴȳȳ˶˶̷̷˶˶ʵɴɳªȼxynRf[?f[=bW9aV8`U9h]Av\ɾĭǰ˴˴ʳɲɲȱȱŮǿ}jby\}`nžæƩǪƩħæ¦ĨrXtdKiY@l\CjZAk]CymSs¦ƩǨƤŦƧƨ¤Ȭ̯ǥbwiFk]:l\:o_=q`B{jNeɼñƵı˼hnHub8ud8{j>{lA|WwĻȽc}\qP}jL|kMnRiǼ«˵˵dz˷ȵǴƳŲƳǴɶʷ̹˸˸̹ͺ̹ɶDZŻouR}nMykHxjEsN]yŸǪǮDZǴɳȬˬĤʿstQwiFqc@qdAsfFwnMdǬưƱŰŰDzɴʵʵ˶ƱîįDzŰȰǷǷƵȵʷȲ˿õz|`yhJveGxgK{jNy]rí~ypmkjosyy~žçĪƬǯDZdzdzȴ˶һϷʵ˶̹ͺȶıïư˽a|nKyiE{kG}jJ{hJ~nM{ZjyǽƥȩǫĪƭ˵Ϻλ̿˿̼˹̶̶̹ͲʰȬȫ¦|hz[|SSZjɱɯȯƩt|nK`P,]M,hX7rQrĻƿ˜Šġ¢ĪͽoWgR5cL,fQ2wgFfħ̽ggIwZ:uT3qO,rP-vT/uT1uU/`=rMnѽâ˽øöv[{LqBpAXxʴ­~fwKqF[{ǻĠƦǪæømvWfH{^@rT8iN3cG/cG1iN9rYCoQvȺʮɮɱů«ĮůůưȲɳɳʴɳƳǴɶʷʷ˸˸ʷɶɶDZDZDZDZDZưưưȲưĮííĮưȲȰƮëſªĮDZưĮĭİǰȴʶȴǴȵɶʷ̹ͺλǴǴǴɶ˸˸ȵƳɳȯɰ̳ͳ˱˱δͱ˯ʮȬǪƩƩƩ˯̰̰ʮȬȬʮ̰ǫǫǫǫǫǫǫǭƮŬêêŬƭƭŬŮƬƬƬūĨ¦ƿ¢Ĥťţţť¥¦çææħƪɬȫɮ˰ʯǬŬŬŬƯĮůDZʱʱȯƭ˰ʯȭŪŨħħħͿħħɻĹøýŹʾíȽìǰʲͷͷƭ~md_ZX[gqêɳʵǴǴȵ˷̸̶˲ɰȬεз͵ɱɱ͵η̵İʿrjfluſĩǬǭǭĩǾ|wolq}úú»»½þ¦ĪƬƯŮį­«ƿſ¶Ƽ©ůȲʳʵ˶̷̷̷̷˶ʵɴȵíȼ{uYk`DeZ>_T8^S7^R8fZ@~rZ}ííŮƯȱɲʳȱƯȱʳɲĭļ{~ž¥ħƩǪƩħ¥çƪ¸~}oUrbIgW>iY@gW>hZ@vjPqŨȩŦäŦȪʬȬũçſêīŬħ ļbwiFl^;n^raC|kOgķǼʿĮʼdrKxf>tc8zi>|mD}ZzŽútvVqP|hGxeG|kMqUpĺìʴɳƲʶȵǴŲıııŲƳ˸ʷɶʷ̹̹ʷɳvyV}nMwiFugB}oJxUqħƭDZǴDZǫɪ¢źl}oLtfCrdAreBtgGypOg½ȭ­îįŰDzɴʵ˶ȳįî­­þľŽýľſªůDZŵǷǶȴ˵Ȳ˿wy]xgIwfHyhJ{jLtXiz}}vnhayXxW|[dlwǾǾú¹Ľ¦éƮǯDZȲ˷ιйζ˴˴̸̸Ǵðīŷ~~_}mKyiEzjF~kK{hJzjIpOyXevƽäƪĪīDZ˷̹ʽɽ˻˹ͺͷθϴ˱ɭƩɾvdxYxO{QZj¦Ȱ˱̳ȫqzlG_O+`P/o_>|[zĻĽžŠŢĤū̼qXgP1cL,iT5mMnúĥǷerQhFb?zZ4vT/xV0xV1uU.zZ4d>ZxѺŤĹpY|LuEvE]ë}θDzoYZqƣ¢ȿµvjbaz\iLqV;eI1dJ3iN9jR:kZ>|]ũɮĬưưưDZDZȲȲȲȲȵȵʷʷ˸˸˸ʷɶȵDZDZDZưưưưůȲưĮ¬¬ĮưȲŭ¨Ž¼ļſëǯƮŭŭŭƮǯȲĮůıůıưȵ̶ƳưŲȲȵʴǴưȯǮƭǮƬǭɯʰʮȬȬʮɬŨæħȬɭʮɭȬǫȬʮĨũƪǫȬȬɭʭƬŪèèĩĩ§¦Ǿſϼƽſȿȿȿɾ¤ũçɾżǾ¦ĨƩƩ΍èŪƮĭ¬íůɰɰǮŬƫƫƫƫǪƩæ˽˽̾ɻ¼ľÿysszŹƼǿĭ˵̶Ŭu}`vUwUzW|W~Y\bp¸īƯįưDZɶ̶ͷʹ˲˯̳ʹ˳ǯɱ͵Ͷ˱ɿ|m|`x]|`iuľéǬɮȮɬŨĻvh`ZZbrú½½žéūĭ«ƾƿžƿ¼{wttu}ǽ©ůȱɲʵʵʵʵʵɴȳDzƳð˿jvjPg\@bW;`T:_S9dX@wkSuưȱǰƯȱʳǰ«ìȱ˴ȱ«ƾƾž¥ŨƩǪǪƩħ¥Ĩǫĺ|{mSo_FeUdT;eW=sgMoħˬȩŦŦǩʬʮʮǫƪūƬȯƭêºbvhEk]:l\:o_>q`B|kMgĹȽʽõ{~[qM}kCvgpb?reC~qOjż¦İȴȴdzdzɵ̸ɵȴƲï®űȴȲDZĮíîįȶɶİĮīd~mOraCtcEwfHyhJuWipmduSwgEqc@pb=oabK,^I,bM2bN3cO4`O3r`Hoʿ¯DZDZDZDZDZDZDZDZȲ˸̹˸̹̹˸ʷɶȵǴDZDZưưůĮííưDZȲɳɳȲDZŮȼĸ÷źøõõĶĶǻëĩëǬƮĩ˿Ⱦ¨Ƭūƿççúüü»»¦çƪɭʮʮɭȫǧŢ¼||~{|ĶɾͿ˿ȾȾɿ©ŪŪĩ§¥æǨǨ£ƽ}ldz]sVwYcny¹żæƬȮɯǪũŨ~md`ahqǾĨǭǭw~avWuWxX}[_ZWUZh{Ļƿ¦Ƭʰ˴ʳȱƯɲʴ̳ʱǬsc{XxUwTxUbwǴŲĮĮǮĪǪäzaxQsKwO\qȽǩʭȭɯǰǰȮȮɯˮ̯ββϵδ̵˴ɴȳɲȮǭɬȫľt[XY_hyǽȾʿäƧǪǪƪūŮƱƱƱƱŰįîŴƵð̿qk_G\P:YM7VJ4TG4UH7dWFxhǿɲƯĭȱɲŮƯ«ǰʳǰŮȮʰʮ˯˯ʮɭɭʮȬȬȬǫǫƪũũƪĺyugMl\CdT;cS:`P7`R8nbHkƩƩɫʭˮ̯̯̲ͳȮƬĬĬůưŮĩæżgxiHj[:iY8iY8o^@wfH}`ĵɽøltWuhHreC|oL|Yjw·©®ezcCt`?t`?taCtcE~mQtĮìȲȲdz̸̶˵ɳDZDZDZȲɳʴȲưưȲɳʴɱƫȩ_yjIpb?n`;se@tfC]~ȾưƳƲǭͯƩĻfykHtfCl^;m_nbeP1_L,bO1gT6bN3_N2jX@x_Ĺ¯DZDZDZDZDZDZDZDZɳ̹̹̹̹̹˸ʷɶȵǴDZDZưůĮĮí¬ůưȲʴʴȲưĭȾĸ}}}~ǻȼƺŹȼ˿ȼǻǻƺʏž¨¨ýüƿ~ttqovũƪȬɭɭȬƪŨʪǤſ}wtrpps|~ztlgejltǹϿ̾ȼǽȾĩŪè§ǪæȿȿäŦ£Ǿr{^uXrU~pSxZfyĻƽ¢ŨȮɱȰūƬʮȫĤȿue[{T~W^rȿũūſsjjkora\W~W]lƿũ˱̲ɲíůȲɱǬæm\uQ}sOzV]dw·˸ͺǴíëƩ£v]tM|pHsM[sʿȬʯʱȱǰǰǰȮɯˮ̯ͱͱ̲˱ɲȱDzƱʳȮǭʭɬýp\Y[_kzȾ£ĥŨħçƯƱDzDzDzƱŰį²ƶij˾rj^H^R<[O9VI6PC0K>-UH7sfVsǰŮ«ȱ˴ɲ̵«ƾĭʳȱƯƯƬǫȬȬǫǫȬʮʮʮɭȬǫũĨĨƿũĺxugMl\CeUd4z^.|`0{_0{`1k?VxɶñtYHHNSiŶȺοDZɸǶ³οͿƷ|nxĵȶubrQzjHr_?dT3bO1gT6cO4aM2fR9xhOtʿðưưưDZDZȲȲȲʴ̹̹˸˸˸ʷʷɶɶɶưDZȲɳȲưíĮĮưDZȲɳɳȱǽƺqiy]d}_|^_at·ƻǼźøøøüüzjxZtV}rTuWas»üüĽȬ˯ʮũçƪǫƩ˩pd`kns{µŸȻȻȻȻƹķwkf^uT~oPpQpShŵ̻˻Źè§èŪħħäŦub|nQ|nQwZdpzƽƽż¥ȰǯǯǯƯǭǫǪĥúq`zS}tM{rK`rƽǪɭȮƬ¨~wi]Y~W]k{ħɮǮī¬íȲů«˿~kcwS}sO{qMyUh|dzƶǶƵƴŲİíëȫkyS{mFth@~rLxQpĨȭǮŮŮĭĭūƬǭǭʮ˯̲ʰǰŮƱDzǰǭȮʭǪkYTUZizɿ¥æĨĪƯDzDzȳȳDzƱƱóȸŴɼķi^Rn]?ziMsǽDZɲDZDZȴdzɳʲɱȰǯǯʲ̴˳ʲɱɱʲȰŭ¨§£^zkJoa>m_:se@ugD]}©ʴȵű˲ʭǪžoyWxkHseBqc@pb?wjG]yȿūĬĬƮȰɱɱɱʲȰƮƮɱ˳˳˳ưůí­­ųʵɶȲʲĩxsUwgF{kJueCwgFuTdöźŸs`tNuiCzmJreBreCtRoɽŹ·øƻȽǿƾǯȰȮɯʯ˰˰̶̳κθʴȰȮĩx~_tSoNnMyXf||`sN|pHuiA}qKzWdvźèɲʵ˵ɳ̸ϻ͹DZɱȮ¥xcyUtOxQ}VhĹū̳ϸ̶̳¦cxd?pX2s]6~Xyõƺʾż¹ŸqtKlY1cI$gI%x[9{XlezWsPvQvNqIpE}Odt|pqm_|Nm>e6}b3y^/z_0g;yMkvXEIQYmǺɾɽŲij̿̾îȸu`dx}xrlg~`qRq`BhW9hS6dO4bN3n]C|eǼȴȲȲȲȲȲȲȲȲɳ˸̹˸˸˸ʷʷɶɶɶDZȲɳɳȲů¬ĮůưȲɳɳɳȱȾɽĶnx\{jNpRlN}jJpPqRi|ø|e~uVtiKrgIqfHtiKuUfzĽɭ̰˯ƪũǫȬƩ¡}pc~\}[iq|ķʽ  ã¢̿ʼɼqg|[}nOteFqbCrcFtWhr~Ŵʺȼ§èŨææ££ä£¹~drUtfIykN{^n}øǾƽżſǯȲȰȰǰǰǭǫǪǾyf~W~uLzqJvSaxȿǪˮʰɯū¨ƿǽĻte\zUyT]gwȿ¨ĬǯʴįοȺn^|XvOwP|XevøʾƲŵƵƵƴųűĮŭȮhvS{mHuiCtNwSpũɰǮƯŮŮĭŭƬǭǭʮ˯̲ʰǰŮƱDzȱȮɯˮǪ~iVRS\júĠ££æħĨĪȱȳɴʵɴɴȳȳŵǶƵ̿ķv`[O7VJ4XL6]Q;_R?ZMr^=p]?p_A{jNrǽDZɲDZDZdzDZɳʲʲȰǯǯʲ̴˳ʲʲʲʲɱŭéȿ^zkJpb?l^9se@wiF`©ɳȵƲɰȭŨy^}pMrdAoa>m_o[8r^9s_:s`8yh=|Ngz°vc~Om?}e7ya3x`2zc7sHby|qRCIUat¶Ⱦ¥é®ųƴóʽǸc}SZqgwhIcR4iT7hP4eP3nZ?rYzǼȲȲȲȲȲȲȲȲɳ˸˸˸˸ʷʷʷʷɶɶʴʴʴɳDZĮ¬ůưDZȲɳɳɳȳ¬ī˿÷õpuYwfJ|iKvcEwbCjKpQküĺ¸ntVtiKoaDobBqdDtgGuUhyž¦Ȭ˯ʮƪũǫƪæyk_yW{X]o{ùȾ¢Ĥ¢¤¤¤ʿźsdqTpbEhZ=eU;p`FzjPrX|dxǷʾ§§ȾǾæƩǪä£äúwjsVxjMtfIsVløǼʿȿżżæƬǯȰȰǰǰƯǭȭæp\vMwnEymGtQbzɬɯǮǮǯȮȮũȾĻrf\yUyU}Ycpǹ¨ȱͶìʺt`wTuO{T]l|¹§ǮűųƳƳƴƴƲưȰɯƼesQzlIwjGrOuQlƪʱǮǰƱƱŮƮƮǭǭʮ˯̲ʰǰŮƱDzʳɯɯʭƩ|h}T{Q}S\oȿȤġãĥħèĩūūɲʵʵ˶˶˶ʵʵǶƵƵðµwkSXL2RF.TH0ZN8]Q;ZM:UH5UH7XM;xmYvŽ«ƯƯ˴ɲǰȱɲɲȱǰɯɯɯƬĪĪƬɯ˱ʰȮƬūĪééƿ¦sqcIgW>`P7cS:bR9eW=vjPqƿèũƫȭȮɯȱȱDzįí¬ïïîždsdEfW8eU4fV5eU4eU4m^?rUgjf|b}`}`iyƿȮɯɯɯɯĪaybBr^=q]m_:rd?tQhƿªƮʲ̴͵̴˳ʲȰƮŭǯɱʲɱȰūľýëƭƱ˶˶ʱʰæsxWzjH}mKwgEyiGyWiĸɿ§ȿq^zpMpfCl_=qdBrRcx¸ƿçƬǭǭȮʯ˰̸̸̱̳˵˳δ̰ȿs~]|lKwgE~kJvUj}v]rKylB~qG~pI~pI~pKvSfõèȮʲɳɵ̸˷ȲʴȮȿs_{SyQyR~YlȽɯζѻθ˳ǽtSnX1pW/{d;dʼġžļļ¹|^xi>kX.hO'pT-~b;oJmIv^:jS1gS0eQ.dR,hV0uf=Wt´Ⱥ˾˽ʻųybtJ|e9v_3u^4w`6mD^uzpP|CJ[kƼäƪȰŰıǵȶƹdyLnC{QlµĹ˾ƸuXgV8eP1fO/dO0kX:ziMkǻȲȲȲȲȲȲȲȲɳʷʷʷʷʷʷʷʷʷʷ˵˵˵ɳDZĮưDZȲɳʴʴɳɳƱȲưȺw}c}lP~jOwdFydGpQ{\xƿũũƿüçh|qSrgInaApcCsfF{nNavĽǫɭȬũĨĨĻ|l_xVxV`k~ƼĠŸȿʿâʿʿʾúxe{mPn`EbR8gW=k[Ao_FzhP~fƺ§ɿǾ¥ƩǪ£ä¡życvUwiLugJ{mPc|ƻʿʿʿǾżúż¢ƩĪŭƮǯƯƯŮĭɰŪȽ{dvOvjBzlG{mHwTiçƬ©īŭƮŮéȾĺ}pf]}Y}ZbpŷŮówfzWxRzTcrƽíƲïïııŲƴȴȴ˵ɱù}`{nNxiHwjH{nKzpLjǭ̳ȲȳȳDzƱǯǯǭȮʮ˯̲ʰǰŮƱDzȱȮȮɬŨ{g|SzP|R^sʧťŧƩŪŪīūĭȳɴʵ˶˶˶ʵʵǶıŲůqnbH[O5SG-TH.VJ2XL6XL6VI6UH5UJ6g\F~v_}ìƯĭƯƯƯȱɲɲǰŮɯʰʰɯƬūƬȮ˱ʰɯǭƬūĪĪçrpbHgW>`P7cS:bR9fX>wkQsƿèũƫǬȮȮȱȱȳƱůĮİİîƿdrcFeV7fV5hX7gW6gW6n_@rRdkkinr¸ü¨˳ɱƮëªūé~^xaAq]q`ByhLkùDZʳDZůƲư˳˳˳ɱǯǯʲ̴͵̴˳̴̴ʲǯĪùx|Y~oNvhEtfA}oJxUqīDZȵɵɲǮĩæƿx^qNwiDqc>se@qN`vĽªȰ͵ζ͵̴˳ɱǯƮƮȰʲʲɯĪĺüƬŰɳʵȯǭlwVyiG|lJvfDxhFyWj÷ǼʿɿŻpwWwmJna?l_=reE~qQeu¹úžçƩǪǭȮʯ˰̱̳͹̸ɶ̴ϵˮ·bvSyiGyfElKyXl}|xsof|U~qGzmC{nD~qGsLtMwR_uǾūɯȰDZȴȴƲ˵ǭżp\|T|QxQ}ZlɾɯζѻͷʲƼ|sPs[5|`9rJnàǥƣȿûûĺuUte:kZ/fO%qX/h?sJlFv`;r[9s_pcC|oOf¹žǫʮɭǫũ¦|m_wVxV^n~ţľǾãæææçèĨīũħ¢ż{~bvjPfX>fX>gW>gW@m]F~nWqƽæ¥æŦŦääȿg|tPofEn`CseHsVmǹ¤ǾĻĻƽ¢Ũ¥¨ĪƬŮŮì«ǮǬçøiwPthB|nGykF|oL^zƬȯȯȲǯĭǿº}mbyW{Y`ixtcyXwT|W_uĹĨůï²ïíîîıŲȴɵ̶Ȱzv[vhKufIviItgEukGhȮʹʴʵɴȳDzǯǯȰȮʮ˯̲ʰǰŮƱDzŮūūǪæzf}VyQ{S`uǧƨƪǪƫŬĮŮŮƱDzȳɴɴɴɴɴŲ¯Į}cj^D`U9`U9_S9]Q7[O7[O7\P:]P=\Q=XM7d\E|t]xǿŮ««ŮȱɲǰŮȱʳ˴˴ȱǰǰȱɲȱȱǰƯŮĭĪçũroaGfV=`P7bR9bR9fX>wkQsæĩƪƫǬǯǯƱƱȳƱŲŲƲƲ۬¸esdGgX9hX7jZ9gW6iY8teF|\py~}Ƹũʯʰ̴Ϲ˵DZſ¨¨a{dDr^=mY8lY;p_AudHcĮɲưůdzȲ̴̲̲ʰȮǭɯ̲ͳ̲˱̲̲ʰǭĪĺryV~oNxjGwiDsNc|ŸŨŬưǴʶ̵ʱǬŨŨƽivS{kGueAwgC~pM}ZnžƬʰʰȮƬƬūĪĪūƬƬūéȾú¸ž­ưDzŬĪĻfsRvfDzjFueAwgExVh·ĻƾƾƾfxXwjJobBo`AvgHuVcvĻƽƽŧƨǪȫɮ˰̱̳ͺ˸ɶ˳ͳŨtxWsP|lJ~lHoNyXgroga^\}V|R}Q|P{O{RzSxQ{Wh~ɯɱDZȴȴdzʴŮúlZ~S~SwP|YlȼȮ͵ϹϷ̲ɾyV|d>lDWyȸâĢȨǧ££ƽǾǽs|Rte:n]1nY,|e9vJUzTsNrPxUnMxhFwgEwTmǾŠȤĢȾĺ}f}WrJvN}Xh}}ZwHzNawƩʰʱ˵DZƬǮxvOsGzPhŷȻǼ̰ʯŪǽ}~a~gEw_;oY4lX5q^>sTwɳɳɳɳɳɳɳɳʴȵȵɶɶʷʷʷʷ˸˸̶̶ʴȲDZůĮíɳʴʴ˵˵˵ʴʴǿðı®h}mSyhLr^CvaDsVféƬȮɯʰ̲δɯū¨ſľɬŨȫúftiKj_AfY9h[;obB~qQmƽĽĽü»üçɭ̰̰ʮǫl~]rQrQ{Zi} ſſ¢ŨƩũũĪĪūƬƬũŨ¥žpu[l^DiZCfV?dT=eU>q`Lwcv¥ħæħĥĥĥ£úpyXphDg_;k]@ugJy\tæȪĦƨĦ£ȿǾǾã¥ŨƬƯŮì«ĮƭūɾpyUuiC{mFxiBxjEyVs¥ˮ̳̳ʴDZ«­ŽǿǿĽ~m~\yWsSuU}`lx|wkz^uWvU|YcnǯȴĴ÷õİíì«ĮŲdzɵ˵ưwqWqcHqaGseHqdBvlImçǭ̶˵˶ʵɴȳȰȰȰȮʰ˯̲ʰǰŮƱDzĭĪĪŨva|UxPzT`wʿŧũĪŪƭůůƯƱįŰƱDzȳȳȳȳðí{|qUg\@dY=i^BlaEmaGj^Dg[CfZBcWA]RnMaƸʴʴʴʴʴʴʴʴ˵Ǵȵɶɶɶʷʷ˸˸˸̶˵ʴȲDZưůůʴ˵˵̶̶˵ʴʴƿ¯Žɾʿi}mSyhLr^CwbEuXkɿêŬƭȯɰ˲̳˲ȯī©©©©ɬƩɬĻbsfFk^>gZ:l_?teDuTrżĽĽüüžĨǫʮ̰ɭũycrTziKpQcw¾¤ŧǪǪūĪëªëĬŭŨŨ¥ĿlqeMj^HfWBaR=aPe]9k]@ugJwZpͿŧȿȿåƩȮȱǰŰĭ¬ǮȮçv~ZymGykDvg@vhCtQlƿˮʱ˲ʴDZ¬ſǿƽðŻobrQxiH{jLqUx^{at[oVoTxW_huȻçDZǵõ¶űŬĪ«íůƲȴȵíuzkTm_En^EpbGsfF|rOvƻƪƬ˵˵̷˶ʵȳȲȲȰȮʰ˯̲ʰǰŮƱDzƯūĪæĽp[xTuOvSaxʿŧĨéīĮůűDzDzîįŰDzȳȳȳȳíŭʾhodHcXnfOiǿìƯȱǰƯìǰ˴̵˴ʳɲɲǰȱɲɲȱƯŮūĪũnl^DcS:]M4aQ8`P7cU;uiOrũʭǬȯȯǮƮĬ­­ðƲƲı®ǿ¸gueKiZ;iX:k[:hX7scB{\{ɾ¥åŤ˪ϰдʹ̴˵̸ϼϺʴƮĭĨƽtuU|hGnZ9jW9n]?q`DvYsƻŮůưʶͷζδͳʰȮȮɯ˱˱ʰʰʰʰɯūħc{qNrQuR{VfƩŬDZǴȳʵδ˲ȫʭˬǼm_zVxTuQuQ_p»éƬɯɯʰɯƬž|tosůŮê}^|lKqa?xhDvfByiGyWf÷ǾĻķòįƼ|eyYrQ}nM}nM~qN|U`vŽžĤĤƨǪɮ˰̱̳ȵȵı¸mvTykH{mHmI~lHkJpOuVyZyXyW}Zbjpzô}rj^vOvR^oĬưȴ˷̸DZ«kY~R~RzU]pʾȮ˵̶ǯǭ̾oZ^nƲŤǨ¥ũħŨƧťȾ_pGwf:n\,~j8Qh}Ʋ̸ͺóǾĢɧή̯ɬƩĦ£ɾǸó²ƶʺνͼzWnIvRmƻʰȲʳ˳ɰɮǩ¢Ĺ|z©̶äèū©ǰǰĬŮǭŻc{RuO]nĵȩʴʴʴʴʴʴʴʴ˵ǴǴɶɶɶʷʷ˸˸˸˵˵ɳȲDZưưư˵˵̶̶̶˳ʴʴ¯ıýƾɾfzjQwfLn]At`EuWmžëƭƮȯǯȯ̴ʱŭêªĩª¨ǫžũȫúz\naAh[;gZ:m`@vgFwVsŻĽžĽĽľĪéƪȬŪkz\mMwgFrSiľ¢åĦŧŨŨɯȮƬūĬŭƮǯæ¥Ŀü|uiSmaKgXCbS>aPyiGwgE{kJzYgǿǼǺƴŲ«ùqd{[tU~oP|mLvPXkĸáĤŧǪɮ˲̶̳ƳŲíûcylJugDxjE|jFzhD|hElIrRwV~\_hwƺŝ̾´q~ZsQvSzZtüĬȲʴį·n\}U}U}Ybt˿ɯ˳̴ʲ˱ɭ˼y~ɳȸȫǪʿ¥ħ¥ħȨȩɨàiuNzh@{f9rCYp˼Ϳ˼ƦƩǫħ¥ŨɰǮƫŨħä˽ħūǭǭǭ¥sPiDuSqʾʲư˶ɴʳ̳ȫƽ̿͹̶̸ҿĥèĪǮìȱDZ­ɲŮŹzuƷͿɮʴʴʴʴʴʴʴʴ˵ŲƳɶȵȵȵȵɶʷ˸ǴǴǴǴǴǴȲȲǯǯǯȰʲʰɱǯǮ¬ĮªļǽzavfMo_ErbHsbHrVyźįȲǰǯƯǯʰǭūūƬŨýɳǭȬwZm^?n_@n_@l]>rcDxYyùüſ©ëĬȰūĩũɻn~]nM~lHyiGqPjĿ¥æƩǪǪŨ¨¨éƩåþħĿ»|~r\h\F[L5`P9gU?o]Es[rľãɩɩţƥȿmvUvgFm];gW5hY8l]j[:bS2l]<{nLmħȫʯͳ˱˲ʴʴ˶ʵɴɳʴɳDZį­«y|a{hJp]|rOrŨɬʱε˵ʵ˶˶ʵʵɴȳȳDzŰî­rvXyiGtfAuiAxlB|SbxðDZʯˮˮʭɬʭ˯β˰ʯǭ΍ʼhzSoKwUco§¬ìııIJıɱɯʰʮɭȫǪƨ¤ɼzspmoosuyz}|zz{vpgb_^jw·ǼſŽȾĸqvZ~kMxeGvcE{jLtXiɿ¨ëë~pe|^uVtUxYdpzʽäũūDZ˶ʷDzëƿ|~`ykNreEqbAufE|lJpLvP~Vak~þũɭɰȯ;Ƹ~m_}WWcvȽĪū˽vga_~`hzȾǬʯͱϳȫȫȫǪŨŨȫʭʰʰɯǪƧƤȤʤ˥ͧͫȩʼtpxŵɼžǧͮβϵϷиͺͺ̻ʺȷƴŮĨħƨȭʱȶ̼ɻȶDzæsrIzc:sLiǸǼƯƲȵɸȸʷǶƳİįƳƳǴǴǴȵȵȵɶȵǴǴǴȵɶʷǴðƿƳ˸˸ʷʷʷȵȵȵȵʷʷʷǴȵȵȵȵȵȵɶʷʷǴǴǴǴǴǴȲȲƮŭŭƮȮɯǭǬǮêƭëžƼxoVn^EjZ@n^Dp`FtZ{ǼŰDzǰƯƯǰɯȮƬūĨ¦ƿ¼ʵǯǭwZo`AqbCrcDrcDyjK~_ŻŽª¬ĮƮéʿ³zbvRpL~oH~nLxUpĽĿĿĿĿ¥¥æ¥ĿåĿþ»ƿwthReYC_O8cSjZC{oWv~cvbGfU9_P3\L2\L2YK0XJ/bVq^=qa?zlI`}ýľŸĿý¼żǾɾɾȿȿƿžüüt|_{jNp_A{hGrb>qa@sQsç˱ʹʹʹ̳ɯǭĨ¨ǿȽʿí¬r{lUi[AcU;eW=i[AscJy`ëʰʰʰ˲̴̴̳ʵȳűݬ¬««íĮůưĬýlqTwjHyjIzmJ{[nƻ¨ĪưȲɴȳƯĭĪūƩǪɬʭ˯˯˱ɯȯǮéŨ§żt|[}mKzhD{iCoN^yʿƩ騦ĨǪȭжи˳ŭŭʲ͵̴íw}kWjUBdO:kVArbI|b~otVs`BcR6`Q4`P6aQ7]O4ZL1]Q7eY?tjQw^uŮɴȱȯȭȭɮʱʱʰʰʲʲɱʲʴʴ˵˵ǰǰȽxccT?WF2YH4[J6XG3SD/thRzììǭȮȮȮȰƮĬª¬¬®İDzȳDzĮvsTo`Al]ufI~b~ȨȨťàŸ¡Ļf~nMrbAt`?s`?p`?{lKeȾǣȦɦƥƥĤ¢ƿžȿɾʿ£çç騞ž÷goStdC}jIueAueDxVyūʹεεʹʰȮũĪǿǼȽ¬ííĬu|mVi[AdVi[AscJ|cȼɱͳ˱ʰʹʹ͵͵˶ɴűİ««ĮĮůưŭýmsVylJ{lK|oL}]qȽ¨éůȲȳDzŮìĪĪƩƩȫɬʮ˯˱ɯɰȯéƩ§ĻpyX{kIygCygAmL]zƩĪ騦ĨǪȭжϵʰƬȮͳͳɯzer`JfQk[AqaG}cįƱDzƯƯǰȱūūĪüDzĬ{sVo`ArcDteFteF{lMbøìŭŭí¬¬íǯū§Ƚq{[qM~lHzkDzjHvSnü¥æħƩȫȫƩħħĦ¤ť½jtiS^RteHe~ƦƦťġà¡żh~nMrbAt`?ta@qa@|mLeǽƢǥȥǦǦťãƿƿæ¢ŦǪȬȬȮȮǭȮĪźnsUwgF|iHxhD}mL`¦¨̳̳ʹʹ˱ɯƪūǿǼȽ¬ííësyjShZ@fX>hZ@hZ@ueLf˿ʲ̲ʰȮʹʹ͵͵˶ɴƲű«ìĮůůůƮľmsVylJ{lK|oL}]rʿééĮDZȳƱĭìééŨŨǪȫɭʮʰɯɰȯĪǪèúmwVzjHxfBxf@~kJ\zƩƬūĪéĨũƩȫͳ̲ʰȮ̲δȮƿt{lUlZBfR:eQ8mY@teHm||hzWpOlLsVx]~cdx]ykPl`FeY?cW=h\BtjQf|»ȮȭȭȭƫŬƭ˱δ̴˳ɱȰȲȲɳʴŮì·t_cT?[J6`O;_N:^M9[L7}q[ɲǰ˱̲̲˱ʲǯĬëí®ƱDzDzĮnpQo`Ak\;l]s`?scB}nMeźĢƦǧȧȧƨĦ¥¥Ũȫƨǩɬʭ˯˱˱˱˱ͳɯɾnrTueDyfEzjFuTlȿũūʱ˲̳̳˱ɯȬƬ«Ƚɾ¬¬¬msdMfX>hZ@i[AfX>wgNh˿ɱʰȮȮ˲̴̴̳˶ɴƲűììůůůůǯſmsVxkIzkJ{nK|\s¤ūéĮưDzƱĭ«¨¨ħŨƩǪȬɭɯȮȯǮūȫĩúluTzjHxfBwe?|iH~[zƩǭǭǭƬƪƪƩƩɯʰʰʰͳ˱žyz^l^DfU;iUtRlȫͰʹ˲ɳɴɴɴɴȳDzƱŰŰįįŰƱDzȵŵŵƴưū¥ǾƼɿȾ§ĩèĩǬŪģƧǨǪƩƪũéĭƮDZDZdzdzɴʵ˶͵̵̵̵̵ϸзϹззεʹ˳ȱɰʱʱ˲˲ʱʱŬǮȯƭīìĮǰDZǰï«ƾ}~ǹéŭ©ĿįƱíê˿ȹ³odbelwŸãͿƷxf~\{X`rúèʮ˱ʰ˳̴˳˳ʴʴʷʹ˺˼ȹȷƵƳŰŮíĨ¼|pinvȹ˼ʼ̾¦ƫ˲з϶ʱǮɯ̲δϵжδ˰ȭƫƭ˵ͷϹϷε˰ʯʭ̱Ͳ̴ͷͺλͼͼɶʶʶ˷͹ϻϻϻ˸̹˸˸ɷǵŲijƶǵȱʮ̯̮ͲεμͽǹųŰ̾_mDlCdƧȽĭűʷɸǷȵƵȵȴʵ̹̹˸˸˸ʷʷʷʷ˸̹̹̹˸ʷɶȵȵɶȵȵȵɶɶȵȵʷʷʷʷȵȵɶǴȵǴƳƳƳƳǴȵɶɶɶȵǴǴƳưưǯǯǯȰʰʰɯȭǮīǮǯ~}nWqaHjZAk[An^DueKh¬ŰƱDzƯƯǰɲǭɯɯĪžŰþªĽw~pSo`ApaBpaBpaBwhI~_øêŮǯƮĮ¬¬í먟myYqMnJ{lEzjHtQlžĦææ¥¥ææħŨŨȫʭˮɬƩŨŧĿɩþwncM_T>YM7YM7\L5]M6aO7jY?ziOwZy¼Ĥťťƣġ¡ȿlqPrbAs_>s`?p`?yjIa~ ťȨƥƥŧåæƩɬƨƨȫȫȮȮǭƬ̲ͱɯȽhoQscAvcB{kG{ZsħɭȮȯɰ˲̳˱ʰȬǭ«ɾɾ¬ȼho`IeW=i[Ai[AdV{hG~[zƩȮɯɯȮȬƪŨħǭȮʰ̲δȮeohLaS8cR8lX=o[@vcE|mNt|yopwɾȽmy]pdJj^Dg[Aj^DpfK~tYrŪǮǮʰ˱ζ̴ɱƮůůůưȱĭp[`QuSo¢ȫͰʹ˲ɳɴɴɴɴȳDzƱŰŰŰŰƱDzȳɶǷȷǷȴǯǪƩƤǽŸķȺ˽ͽ˼¦¦¦Ūɮɮ̱Ͳ˰˰Ͳ˰ƪǧɨˬɪȫȫɭɬƭǭƮƮDZDZȴɴʵ˶̷͸ͶͶͶͶηηͶͶ̵̵̵˵ƯưDZDZȲDZDZDZůȲʴʴDZí«dzƱî­­îîîǿļôǸȺȺǭȯĽƿŰDzĮŬ«¦ʾ¥ŪĬ˿ƺøummpvô;ĵ|qjksǼ¥Ȯʰɱɱ̶Ϲɱʲ˵̶˺˺˼˼ƷǸǶƳð­«ĨŢvyŶ¥Ũææƨƪ˰εѸεȯŬɯͳͳϵѷжγʯǬǮϹλϹͷ˲ɰʯ˰Ͳγͷθͻμ;ξʷɶɶ˸ͺλκ͹ͺͺ̹˸ɷǵıðóIJƯȬʭ̮̱ʹͻ̼Ƹñîɻ}ZoFoFhʫǰɵͺ̻ʺʷȷɶʶ̷ͺ̹̹˸˸ʷʷʷʷ˸ͺλϼϼλͺʷ˸˸ʷȵȵʷ˸Ǵȵʷ˸˸ʷȵǴȵɶɶƳƳŲŲƳȵʷ˸ɳʴ˵˵ʴɳDZưDZDZȲɳɳɳȲȲdzïŰƯʾwxhNm^Ak\?p`FpbGtfLhįưDZȲɳɳɳɳʴȲů¬ý¼¼ÿÿ¦ƺh}lNmZo\>o\>veI}aƸŬǭȮƬĪɿ̾ŷx{^zkLuhHzmK~sS|\sŨĮííĮƯŮƮȰʲʲǯê¦ǾȿĻz^i[@^P5YI/[K1`O5eT:lX?vbIv]nǾ¡áţȦɧŤ¡ywYraCmX;mZ:oa>znHa{žãåƧȬȬȭǬǯȰíŰǵȶȶǵƵƳůȮæsW}rDph9pg<|uKhżƩ˯δȲɳʴʴɳȲǮŬëĬªſĪĪŻ~blaEbW;dY=f[?fZ@sgOlǿŭëƭ̳ʱʱ˳̴˴ʳDZDZ¬¬¬ìîĮĮĮīh~uXulKqfFukH\qǦ̫ȧǨɪƩƩŪŪīꩨũǪȮɯȱǰŰį¬ǮŭĹjuUykH}g>zc9h?ZȺĮŰȷȶȶǴDzɯȯɮůưǮȯʱêuzmKob@_U2XP,^U6idDwrT|]»íǰʱ̳Īu}argGeV7fW6n^k\=rcD|\xƩˮ̳ɰɳɴɴɴȳDzƱƱƱƱDzȳʵ˶̷̷ưưDZDZDZDZDZDZ˵ɳDZůůDZɳ˵ʴʴɳůĮDZɳʴ̶ͷͷ̶ʴȲȵȵɶʷ˸ͺϹкʲɱɱȰȮɯɯʰ̶̶ͷθθθͷͷʴʴ˵˵˵ʴɳȲ˲˲ʱɰǯƮƮŭƯȱưíĮDZư«Ǵ®Ŀ¬Ŀÿ¦­ŰǴı±³³¿İȴDZ¬ȰǯūƿĨȬƬǭǭūƿ¦Ȭɲɲɴɴȷȷȷɶȵ˵ͷкѻѻкϹ˵ɳDZůĮíĮĮĮůůſĮɳʴȲDZ˵ϹϹϹϹθθͷͷͷθθккϹ̶ɳDZζζζ͵ʴDZĮ¬˸˸̺ͻμоѿϼλ̹˸̹λнѾλλλͺ̹˸ʷʴɭɬȮȮDzȳȷȷʹ˸ŲǿìzWrJyNg³ŢǦɭеɳʳʶʶɶɶɴɴ˸̹̹ͺͺ̹̹˸ʷʷʷ̹ϼѾѾнϽμ̺ɷƴƴȶʸɶʷ̹λλ̹ʷɶǴǴǴƳƳŲŲƳȵʷ˸˵̶̶̶˵ɳDZưDZDZȲȲȲȲDZDZȴïŰĭǻqrbHm^Ak\?p`FpbGtfLjįưDZɳɳʴɳɳʴɳưíſľýÿÿƿƺg|kMmZxsSht©ƿƳƳĮǰʱ̳ɯtz\reEn_>p`?rb@}mKzYp}éȮʰ̲ʳɲɲɲȳDzDzǰůƮǻ~sWaV:ZO1^S5^S7^S7maGjȻijȶ̷̶ʴɳDZưůůůĮ¬¬«ʾ~eyiOm^AgX;k\=teF_{¥Ǫ˰ʹʱʴɴʵʵɴȳDzDzDzDzȳɴɴʵ˶˶ȲȲȲȲȲȲȲȲɳȲůĮĮưɳ˵ʴʴȲůůȲʴ˵ͷθθͷ˵ʴʷʷɶɶɶʷ˵̶ʲɱɱȰȮɯɯʰʴ˵̶̶ͷͷ̶̶˵̶̶̶̶˵ʴʴ̳̳ʹ̳˳ʲȰȰǰȱů¬ĮDZDZĭů¬¾«íŰűƱð¯ſĿïdzȴʴ̶ɳªëĬĪé¨é¨éƬǭũü¼ľľ»ũȮɲɲʵ˶ʹɸȷɶɶ˵ͷϹккθͷʴɳDZůĮĮůưDZȲȲưíſſůɳ̶˵DZDZʴθϹϹϹθθͷͷͷθϹѻѻкθ̶˵ϷϷζ̴ʴDZůĮ˸˸̺ͻμоѿϼλ̹̹̹λнѾλλͺ̹˸˸ʷ˵˯ʮʰɯȳȳȷȷȷ˸Ǵ«ŮxXpHxNh Ŧǫ͵˵̵˷˷ɶɶɴɴ˸̹̹ͺͺ̹̹˸ʷɶɶ˸λннϼϽμͻʸǵƴȶʸɶʷ̹ͺͺ̹ʷɶȵŲŲƳƳƳƳǴȵɶʷͷͷͷ̶˵ɳDZưưDZDZȲȲDZưưȴïî«÷kk[Al]@k\?o_EpbGugMküįưDZɳʴʴʴʴ˵ʴȲůíĿÿƿŹg|kMmZp]?r_AziMfĶéŬĬªçũèɾĹjvY|oMwjGuhF}sPlȽä«ūŮū§êŪǮȬƭƪīĩ¦ȿżuvkOaS8\N3aQ7^N4]M3]M3cR8q`Fz`uâŤţţţáǾƽ}c|kMs`Bp]?qbAtgEtRk¹ĩȯʳʳȳȳɴȲƱï®ĮɽzbxN}rEzrD}VmȿǨɬʰ˴̶̶ʴɳɰɰɰɮƮƮĬ騞x\j_CaV:aV:bW;bVo_EpbGugMmžįưȲɳʴ˵˵˵̶˵ɳDZůĮíí¨Żh}lNp]?p]?o\>p\AziOgŹƬƭĬſſ¨ũ¦ƽlwY~qOylIwjHrPf·åȾȾžħŨƬǪŪĦèħ¥újpeI`R7^P5\L2_O5aQ7aQ7aP6kZ@tZp¡ŤţƤţ ȿȿźsy[{hJq`ByhJwhI}nOb~ȼéŭůİŰDzȳDZŰ®°ï¯íl}W~rJ}qGwNb{úȩʭȭɲɲͷ̶ʴȰȯȭɮʯȰȰŭéĽw[laEeZ>dY=dY=dX>vjRsŮ˳ʲ˲̳ǮȯɱɱȱȱůĮĮĮŰŰưưưƮ»x|\qP{kI{iErN^wâǨǨȫȫŪŪŪŪŬƭǮȮƪȫȮɯȱǰƱŰưȯĬe|qQviFxgwnMe~ĨǫŬ©Įʴ˸ȵ˵̵˲ʱɯȮǫƪħ~l}^qP|lK}nM~uXey»Ƭ̲δͶͶ̵ʳɴȳƱƯDZŭŹ{_mbFg\>j_AdY=mbFw]~Ƴ˺˹ʵʴDZůĮůưDZDZưĮ¬¬ĮůƯǻ~qWm]CgX;eV9j[n^DoaFugMmįưȲɳ˵˵̶̶̶˵ɳȲưůůůĮ¬ª¨Żi~oRsbDp_Al[?m\@ziOj˿ɯɰǮêÿ¨¥¦¦úmvWviFvhCxjG~pM|[qʾ¥ȿŹĻɿŨ¦Ħũƨħĥ¥ħdodHbW;bW;]O4`R7dT:`P6[K1eU;u[wƻâģĢ៾ſ¡ŸkrTveGyhLufIxiLx[u÷Ⱦ©Ī«ªíƲɵůìíƲdzıíŹv`tQznHsK}Vkʯɮȯɲɴ̶˵ɱǯǬȫɬɬɯɱƮéüz~sWlaEg\@eZ>dY=dX>xlTwĭɱɱʱɰǮȯɱɱȱȱůĮ®îîîĮĮììĺqxWoK|hC}g@rLcäƩȫɬȫɮȭǬƫŬŬƭǭǫɬɯʰɲȱDzƱưȯª~d|qQviFwh?zk@wPmæ˱˵ɵȴȴDZȯɮȮʭ˯ɬǪ¥}}`sjKbT7j\?xmOc{ĿȫũĨ¬ưƳð˵̵ʹ˲Ȯūç¦ʭȿtd{ZyY~al|üǭͳϵηη̵˴ʵȳDzǰưª{_peIj_Ak`BlaEynRjƺʷνͻ˶˵ɳưůĮĮĮưưůĮůůưƯy}mSm]CiZ=hYn^Dn`EtfLkƿįưȲʴ˵̶ͷͷ̶˵ɳȲDZưưDZůíª¨ĺk~oRsbDp_Al[?n]A}lRoë˳˲ɰƭ©ſ¥¥ħƩƿs{\ob?rd?wiF|nKuTcµ̾¦§ʿĹøȽææ¥¥æħĦĽĻww[ncGf[?f[?dV;cU:aS8^N4_O5qaGkɻ¢Ǧģ ÿſáŤyaoQxgIveGvgHqRg|ʾĬɳ̴««¬ůDZůʾŹ{exX}pNylIuO^zŪɰȯȲ˶ʵ̶̶ʲɯɬʫʫɬɯɱƮ΍ĽrwlPj_Cg\@dY=bW;cW=}qY|ìǯɱʱȯȯɰɱʲɲȱưůîîîîĮĮìΏltRmJ}g@~g>sNkƽŨǪʭˮɬ˰ʯȭƫŬŬƭȮȫɬɯʰɲȱDzƱůǮ|c|qQukHzlE~qGYsŨ̲˳ʴɳɳɰɰʯʭˮˮǪæĽuyZtkLn`CzlOcxħɬȬǫīêĮưŲưȲ˴ʹʹɯƬĨçħź}upzʰδϵηηͶ̵˶ʵɴȱůdynRshJtiKvZh}˿˸ν̺˶˵ʴɳDZůĮíĮůưDZDZDZưĭryiOk\?j[>iZ=hY:j[<|_ƻĨ¨Ŭǰưʴʵ˶̷̷̷˶˶ʵʵʵɴɴȳȳȳϹϹθθͷ̶̶˵̶̶˵˵˵˵̶̶Ȳí¼ýíɳͷDZȲȲɳɳʴ˵˵̹̹̹̹̹ͺϹкȰȰǯǯǭǭȮȮɳʴ̶ͷϹϹϹϹͷͷͷͷ̶ʴɳȲȯʱ˲ʹ˳ʲȰƮɲʳDZĮĮưĮ˿̼ueoUveKqbEm_EykQ~dxzromwķ˾ūǮƱı¯°IJǴɲɯǭƬƪƪǫȬȫɬɬȫƩƦƩȫȪȪǩȫɬʭʰ˱˳̴θϹλͺ˹ʷ˵˵ʴɳɳȲȲȲDZDZDZDZȲʴͷθ̶̶̶ͷθθͷ˵ɳȲDZưưȲɳʴ̶̶̶̶ͷͷͷͷ˵˵̶̶ͷͷθθζ͵̴˳˵ʴ˵˵˸̹μϽоϽμμ˸˸̹̹ͺͺͺͺλͺ˸˸ʷ˸̹θϸηͶ̷̹˸˺˺̻ͼ̹ɶʵ˴ŮƼôǷͽįɳ˷̹̹̺ͺ̸̸˵˵ʴʴʷʷ˸˸˸˸ʷʷͺ̹ʷ˸̹ͺ˸ʷʸ˹̺˹ɷȶȶʸȵȵȵȵȵȵȵȵȵǴǴȵȵʷʷʷɶȵǴȲDZưůůůưDZ˵˵˵˵ʴȲDZưȴïįìŹoo_Em^Aj[>m]Cm_DrdJjžįưȲʴ̶ͷͷͷ˵ʴɳȲDZDZDZDZݸi|mPp_Co^Bn]AsbFsYvĬɱ˴ʳȱī©§èĩèæƩæ~hzmJzlG{mHqLtQ|Yl~Ķ˼˽ǹŷɻŧ£ɾŦƦú}dtiMf[?aV:aV:cX<^S7]R6gY>wiNfŷŧǨĥãĤ¢ĿãťǨĦźtdrQpN~oNrQ{Zhyƻ¥Ũɭ˯ĪĪīƭŬȾxfrU|oOzmMzmKxVdùɰȲDZ˷ι̷ͷͷ͵ͳͰ̭ͮˬȮɱƮª¨ĪéžmshLi^Bi^BeZ>cXm]Cl^CqcIiĽįưȲʴ̶ͷθθʴɳȲDZDZDZDZȲîþýgzkNm\@m\@p_CwgMx_zëǯɲʳɲǮŬīŪǬè§ħħu]xSuPvQvSwT_kôȹƸŷȺͿƨƻɾƧǨȿl{pTg\@]R6ZO3YN2]R6XM1]R6shLiʼ̾¥ĦäťɩʪȨȨɩʪȩu]Y|WxSxSZhqǼģƥǨũūŬƭ©{htXugJvhKykN|oO{[l̶ɳdz͹Ѽ͸ϹϹижѴбέ̭ȮȰƮª¨ūĪƿmtiMk`DlaEi^Bf[?h\B|dźĭƮʲʹʱʱʱ˳̴˴ʳDZDZŰưưưưưŮƬapLlGgAjBzUxũɭɭββʮʮɭǫƪǭɯ̲ͳȫɬʰ˱ʳɲDzƱ̶ʹƮ·l{[wT{sL{Sd}ȫα̴̲ʹ̳˲̰ͱΰͰɫťťĤȾèǪǭʯɰȯǮǮí¬îȲƮʱ˲ɰΦççũƩħæŦƪǪǫũĨǫ˱ϵϵδͶ̵̵̵̷̷̷̵ɳǮīDZ̸ʸŲưȲʴ̶̶˵Ȳưſ¬ưʴʴȲĮin_BaR3dU6fW8fW8gX9y\ɽʰǯDZɴȴɳȳɴʵ˶˶˶˶ɴɴɴɴɴɴɴɴϹϹθͷ̶̶˵ʴͷͷ̶˵ɳȲȲDZůľȲ̶ɳɳɳʴ˵̶ͷθ̹̹˸ʷʷ˸̶ͷȰǯǯƮƬǭǭȮDZȲɳ˵̶ͷθθѻѻккθ̶˵ʴ˲˲ʱɰǯƮŭŭɲ˴ʴDZưưĸ~ezfKlX=fR7bN3`M/_L.YH._N4jY?udJ~mQrVtVtVtSpOzjHtdBp`i[@xjPqūDzɳʴ˵̶ͷ̶̶˵̶̶ʴưĮĮĮįįĮëfykPpaDrcFscIvfLy`³ǯůDZ˵˵ȯǮɰ˲Ǭũƪƨse}ZyVuTrQuT|[pz³Ʒʽ˾ĤǧǾltXujNj_C]R6ZO3`U9_V9jaD}tWmɾĨ¥ƨʫ̬ʪȨƦɨȨɩɩǨɾumd_]_cdqwúżżûŻùwn~b}a|adluɽīɳʴdzdzͺҿϹθ͵̲̯̭ͬͮȮŭſūʰƬĽkshLh]Ah]Ah]Ai^BmaGjĮ˴Ȱɱεʹεε͵̴˴ʳɳɳ˳ʱȲDZDZůªƿy}^pOhFgAtLbʰɯƬʰͳ˱̲˱ʭȫǪɬˮͰ̯αͳ̲ȱǰȳʵDZʱǯȽzldcgsͰͱʰɯǬƫȭ̱ϴвͰŧǩȪȪħžǸ˽ªǮǮȮȰɰȯȯȯɰȲȲDzȲɱ˲̳̳ʰɯǭū˯ɭȬȬʮ̯˰˯ʮɭȬǫǭȮɯʰȱʳηйѼϺ͸˴˴ʰƪž»Ũȫ̰δ̵˶˸˸DZɳ˵ͷθͷ˵ʴȲ¬ů̶˵ĮƾfpaDfW8eV7cT3fW8n_@}`ɽɯȰȲʵȴůƱɴ̷͸͸˶ʵɴɴɴʵʵ˶˶˶ϹθθͷͷθθϹ̶ͷͷ̶˵ȲưĮưſſDZ̶θͷ̶˵ʴ˵̶θϹϼͺ˸˸ͺͺ̶ʴƮȰɱɱǭūūƬưưDZʴͷϹϹθѻкϹθ̶˵ʴɳȯ˲ε̳ȰƮǯɱȱ˴DZ¬ưɳȽnqSmZl^CqWxǭDzɳʴ˵̶̶̶̶̶ͷ̶ʴDZůůůįįůĬhykPqaGsdGscIwgM{aĵíȳDZDZ˵˵Ȳȯ˲з̳Ǭǫƨ¤th`}[wUwV|ZdmxöȻ˾ɾȽʿʿ}lx\|qUshLh]Ag\@ncG{rU{^nżħǪåħǩɪʫʪɩȨͬ˪ʪʪʪŦȿĺwplkmlqv}xqlns{´Źǯ˲θθ̸̸ϼѾϹϹθ̴̱ˮˬʭƬŭëªȮ˱ƬüjqfJeZ>eZ>f[?j_CpdJqů̵ɱʲ϶ε϶϶ζ͵̵̵ʴʴ˳̳ʴʴɳůſydzYuTtR\p¼ʲɯǭ̲δ˱ϵδ̯ˮʭʭʭʭƩɬ˱˱ɲȱȳȳɳ̳ɱ}{z{ťήͰʮ˱ȮƬǭ˰̱ʭȫɬʬ̮ͯʭƩ¥ƿο«ǯʱʱ˱̴ʹʱʱʱʱɳɳȳȱʲ̳̳̳˱ʰɯɯ˯ɭǫǫɭ˯̰˯ɭȬȬǫǭȮȮɯʳ̵ϸѺѼлι̵ɲɯȬǫƩƩǧǧȫˮβϵͶ̷˸̹ʴ˵̶ͷ̶˵ʴɳ˵ůĮʴɳ¬ºao`AfW8fW6eV5iZ;sdEe˿ɲɳʶ˸ɶǴȳʵ̷͸͸˶ʵɴɴʵʵʵ˶˶˶ϹθθͷͷθθϹθθͷ̶ʴȲDZưȲɳͷθ̶̶˵ʴʴ̶ͷϹλͺ˸˸ͺͺ̶˵ǯɱʲɱǭūūƬưưDZʴθккθѻкϹθ̶˵ʴʴʱ˲̳̳ɱȰȰɱŮɲưíưƻqy[|iKyhJ{hJudFwdFziKrX}cnw}~w~wssttpj_^_^csɽī˶̹ɷƶǷȶȱɭɭɭɭɭɯȮʰʰʰʰ˱˱ʳ˳ɱǮŬŬȯ˲̶ͷ̶̶˷˷˷ʶʶʶȲDZưůůưDZȲȲȲɳ˵̶ͷθϹ˵̶̶̶ͷͷͷθθθͷ̶˵ʴʴɳDZȲɳ˵ͷϹкѻϹϹθθͷͷͷͷ˳˳˳˳˵˵˵˵ɶ˸ͻоѿоϽμ˸˸˸˸˸˸˸˸˸˸̹̹ͺͺͺλɸɸʹ˸˸̷̷̷̷̹̹̹̹̹˺˼ƹƹƹǸȹɸ˺ͺ̹̹ͺ̹͹̸̸˷ɶɶʷʷʷ˸˸˸ʷ˸̹ͺͺͺ̹˸ɷɷɷɷɷɷɷɷɶɶɶȵȵǴǴǴȵııŲƳǴȵɶɶɶɶɳȲȲȲȲɳʴ˵˵˵˵ʴʴɳɳɳɵʶ˶Ư~w]p`Fl]@m^Am]CtfK}cĺȮDzɳɳʴ˵̶̶ͷͷͷͷ˵ȲưưDZŰƱưŭhzlQrbHrbHscJyiP}fĸDZ˶ȵǴʷʵȲȲ̶иεɮŪ§§çæ}sjc`aejqw|øź·uli}avZx[dyžħƩǪǫƫǫɬɬɬʬˬί̭ʫȪʬɬȫǦĝȿ÷}z}{}ʼŮȰϷеѸкϻκλλͺθθ͵ʹ˰ɮȭƮǯǯǯ˱̲ĪgodHcXvoRiƯ¬íůDZȲDZȱǰëëëªíŬŬƭDZɳʴɳů¬ȦţĻŷʼ§¨ĨŷysollovǹʾŽưʲ̴˱ɯȫƬƬƬƮưưŲŲï®·sccbeit»ìȲ̶θͷ̶˵˵̶̶ͷͷͷͷ̶̶ͷͷ̶̶ͷͷθϹϹθͷ̶̶ͷθϹккккϹθͷ̶̶̶ͷθϹϹθθϹθͷ̶˵˵˵̶ͷθкѻѻкθͷ˹˹̺ͻμϽϽоμμμϽϽооонϼλͺͺͺλλͺͺλϼϼнѾѾоϽμͻͻμϽоооϽϽμμͻͻʷʷʷʷ˸˸˸˸ɶʷ˸˸ͺͺλϼͺͺ̹̹̹̹˸˸ȵɶʷ˸˸˸˸ʷȵDZưííĮůưưDZDZɳɳɳɳɳȲȲȲɳɳʴʴɳȲDZDZƳǴǴȵȵȵɶɶ˸ʷʷɶɶɶɶʷȵɶʷ˸˸˸ʷɶʷɶɶʷ̹ͺ̹ʷ̼ͼλϻϼϷʳǬǼp}ZrO|lHyiEwhAp`na>uhEwTfrĻȿż¹¹úĻĻ·zbziMn]AmY>p_CsdGwiLxZpoc\[YVWZciu~·ǼæƧȫɩƦãýs~bnSyfH}nOxXi{ĶͿĪéźubyVvRvTqT~nT}nO{lKxjGvhC}qK{UfuƻëŲƶƶƲȱɲʳ˴˴ʳʳǰƯŮŮŮŽz~]uP~jGvd@scA|oMhæȬ˱ɲDZʶ˸˸ʶȴí~{]sfFk^ugBwgCxhDoMyVbxƽħɬȬũūĪëĬǮɰDZƮĮªʾȼ˽ŷw\yQ|nI}oJxUcwĶȼê«ëǮ¦xrrtvxywpgy]|qUwlLyRXi{Ƹȼ˼˿Ĭǻu^|S{QxQwTcyū˱ʲȲ˵ϹͷͷθϹϹϹϹθʴ˵˵̶̶ͷͷͷͷͷͷ̶̶˵˵˵˵̶ͷͷθθͷͷθθθθθθθθкϹϹθθͷͷ̶̶̶̶ͷθкѻҼϹϹθθͷͷͷͷ˹̺̺ͻμϽооϽϽϽϽϽϽϽϽнннϼϼϼϼλѾнϼнҿҿнλͻμоѿѿѿѿоϽϽϽϽμͻ̺˹ʷ˸˸̹ͺλλλλͺ̹˸˸˸˸˸̹ͺͺͺͺ̹˸ʷ̹̹˸˸̹̹ͺλϼDZDZȲȲDZDZDZDZưưɳɳɳɳʴʴʴ˵ɳɳʴʴ˵̶̶̶ͺͺͺͺ̹˸ʷɶɶɶʷʷʷʷɶɶȵȵɶʷʷ˸̹̹̹˸ɶʷ˸˸ʷȵθͷʴɳɳʴʷ̹ʷɵȶʷͺλͺ̹ʶ˴ʵʳʵ˴̷ͶҽѺϺͶ˶ʳʵɲ̳̳̳˲ɰȭȰʯȲɳʴ˵˳˳˳ʲʰʰ˱˱̯ˮʭɬ˯̰̰̰̰˯ʮɭ˯ʮʮɭɭʮʮ˯ˮʭȫǪƩƩƪƪǭȮʰ˱ʳɲȱȮǩȨʪͬͯϰαα̯˯ʮʮȭɮȮʯǭǭǰȱʳ˴̵̵ηͶ˴ɲȱǰƯǭĩǪɬˮͭήήή̯ͯɯɰ̹н̼IJ«ȱʳͶййη̵˱ƿƿžĽüüƬ˱ͳδδ̴ͷͷͷ̶˵ʴɳɳɳɳʴʴ˵˵˵ͷ̶̶ʴȰƮ΍ƿžĽ»úǽ¥~xqopotǾƬǯɱ˳̴͵εε϶϶ζδδϲϲδη̵̵ηϸͶǰoa~[bkvɾø~dshLo`Al]{nNd{Ǽ¤ɬ̯ɮƫŪƩƧƧ|{_t`EeQ6fR7jV;kZ>qbEwZxǾǾǾȿȿȿĻn\~tP}tMwNyPV`s|øǼŭƮƮȲ̳ϵϵ̲ˮū§ȼlnWvbIsbDvgFqPavŷ¨Ƚ{rklh}cz]wVrOzlGxlD{oG}Wcxŵ̽óİǰɲʳ˴ʳɲɲǰȱǰĭǿkrQlHhF|iHoN~^x˯ʰȱǰƲȴɸʹϽɷ{z\tgGna>n`;m_8pb;ug@whAvg@zkDqL{Xj}úħǪĨƬūĬŭDZȲDZưīǻ}k|TtL|nIuR`rȼɿſ½ĮȰƬȿ|o~a|sRwOyOZjzŷȹʻο̾~`~QvIyMWctɱͷ˷ɶ˸ιθθϹϹϹϹθθʴʴʴ˵̶̶ͷͷϹϹθͷͷ̶̶˵˵˵̶ͷͷͷͷͷͷͷθθθθϹϹϹϹθθθθͷͷ̶̶̶ͷθϹѻѻͷͷ̶̶ͷθϹϹ̺̺̺ͻμμϽϽϽϽϽϽϽϽϽϽнннннϼϼϼѾнϼнѾѾϼͺμϽоѿоϽμϽϽϽϽμͻ̺˸˸̹ͺλλϼϼϼλͺ̹˸˸˸˸̹ͺͺͺͺ̹˸ʷͺͺ̹̹̹̹ͺλϼDZDZưưDZDZDZDZȲȲɳɳɳʴʴʴ˵˵ɳɳʴʴ˵̶ͷͷͺͺͺͺ̹˸ʷɶȵɶɶʷʷɶɶȵȵȵɶʷʷ˸̹̹˸ʷȵȵɶɶȵƳ˵˵ʴʴʶ˷̹ͺɶȵƶȷ˺ͼ;ͻ˶˱ʲʰʲʰʲɯҺҸѹϵ͵̲ʲɱ̵̵˴˴˴˲ɳʱȲɳʴ˵˳˳˳ʲ˱̲ͳͳͰ̯ˮʭ˯˯˯˯˯ʮɭɭ˯˯ɭɭɭɭ˯˯ʭɬǪƩħæççéūǭɯʳʳɲʮ̬ͧ˨̨ͬϭϰϰίͯ˭ɫƩæȿŹƺɽĬǯɯʰ̲˱˯ʮɭȬȬȬɬǬɬˮήаϯϮϰͯɮȯ̸͹ó|}}Ȯ˱ϵжͳƬĽ»çɭ̰ͳʲ˵˵˵˵˵ʴɳɳɳɳʴʴ˵˵˵̶̶˵ȲŬÿźĹ~nc^[\Z^kèƬȮ˱ͳδζζδϵϵгͱ̯ˮ˯ж̵ʳ˴̵ȱ}qg_^kyyy]peIo`Cl]>k\?sdGwZu˽ʰʰ˲˲ʴ˵̶ʹͮͬƦķ|{]r_AcN1gR5iV8hW9o`A|\ƽħħŨƩǪȫʭˮɬˮˮƩæƽnyYujJvlI{qMwP[hź¨ªĮŲŵƵǷ˹λλ̹ʴƳdzƳ;ue|fQq^@o_=pb?zkJ~_{ç¥ȿ¼xncyS|nGxkA}oHxSex²̹οíŮȱʳ˴˴ʳɲƯȱƯƾoqW|eE}c@hFsR`rĻ˱ȱǰɴʸʸȷȷɹƶh|oOtgDtfArd=te>xi@{iAzh@}kCrKwS^k|Ǿæ¦ǭƬŭƮȲɵȴư©ʿĶ~n`zTwQxUaqǹɽȾƾ­İƱƮƫ¦ƿƼŻǻʼʼ˻ʾs`}UxLxNYhuóǷȸó|k|QzKyLScvūʴ̸̹˺˺˸κϹϹккϹθθɳɳʴ˵̶̶ͷͷѻккϹθͷ̶̶ʴ˵̶ͷͷͷͷ̶ͷͷͷθθϹϹϹͷͷθθθθϹϹͷ̶̶̶ͷθϹк˵˵˵˵̶θкҼͻͻͻͻͻͻͻμϽϽϽϽϽϽϽϽннннннннҿнннѾѾϼͺμоѿѿϽͻμμϽϽϽμͻ̺̹̹ͺͺλϼннϼλͺ̹˸ʷʷ˸̹ͺͺͺͺ̹˸ʷλͺͺ̹̹̹ͺλλDZDZůưưDZDZȲȲɳɳɳɳʴʴ˵˵˵ɳɳʴ˵̶̶ͷͺϼϼϼλ̺˹ʸɶȵɶɶʷʷɴɶȳȳɳʴ˵˵̶ͷͷ˵ʴȲȲɵɳȴƲɵʶʶ˷̷͸̹ͺͺ̹˸̹ͼνͼ͹Ϸδδδϵжϵδϵϳϵβͱ˯ɭȮʲȳȳ˳ζη̵ʱȲɳʴ˵˵˵˵ʴ̴εζϴͳͲʰʭʮʭˮˮˮʮɭȬ̰˯ɭȮȮɯ˱̰ɭȫƩħ£ȿèǭɲʳ̴̲ӲЭͫ˩˩ˬ̭̭̮˭ȨťǾ²Ƹ˽§ƫȭ˰̰˯ʮʮʮ˯˯δ̲ʯˮͰϱί̭вΰʮʮ̳˲þslmnrʰͳϵͱĩzxurpoppx~Ļæȫɮǰȳʴ˵ʷʷʶʶɵɵȵɶʴ˵ʵ˳ȮɬȮŨĿõvcX{S{RwOvMxP]räĩȬ˱δϷϷϹϷижϵ̰ʮɬɮϸ̷ɲɲɲīĺzpf`_ctȽƦ¤wu[pdJoaGl^Cl^DvhN~d÷ȰζɳĮ¬İȴ̸θγίȪŷ{_r^CbN3gS8iU:gV:o`Cb¥ǪƫǬɭ˯ͱϳд̰ϳγˮǪjsVreEtgG{nLuR_m·¦ȭǮï®ñŵŵƸʹ̻̻ʸȵƳʸͼɶȷnmXq^@k[9iY7n_>rSrɿĩƪɭ̰ͯʬťƿ©xcvPymEtf?|nIzXi{Ŵ˻ĭǰʳ˴̴˵ʲĬƮĪuvYn`Cr_>w`>~jG{Yo·ǬȮƯDz̷κ͹ɵƴųƴdzīx}]rP~rL{mFzlE|oE~oF~oFsJzPwN}V`n¹¢ȫƬǮDZɵɶȵưʽ{pd|Y|Y}Yapŷ˿ǽĽľDzȳDZĬ©¨éūǭǮƮƮʾĬŬƿs`{RtLxR~[dsl~]xQ}PV^qǾħǭȰʴ˷̹̹˺ʷϻϹкккϹθθȲɳʴʴ˵̶ͷͷҼѻѻкϹθͷͷʴ˵̶ͷͷͷͷ̶ͷͷͷθθϹϹϹ̶ͷͷθθϹϹкͷ̶̶̶̶ͷθϹʴʴʴʴ̶Ϲѻҿλͻͻͻͻͻ̺̺ϽϽϽϽϽϽϽϽннннннннҿѾннѾѾϼͺϽооμ̺μμϽϽϽμͻͻ̹̹ͺλϼϼннϼλͺ̹˸ʷʷʷ̹ͺͺͺͺ̹˸ʷλλͺ̹̹̹ͺͺͺȵȵƳǴǴŲııƳɶʷɶɶȵȵɶɶʷǴǴȵɶɶʷ˸̹ʹ;;ɺƶǷǷIJıŲȳɵȴůűDZɰδϵʰȮʰ̴˳˲ʱɰȯǰȯʲ˳̷ϽӾӾκʶɴʵʵʵʴ̶ϹϹθ̶ɰʱ˲̳̳̳̱̱˱ʭʰˮȫæĨʯȰ˵Ϸиζ˱ɯȬʱ̳ͷη˵˶˷͸ɵɴȲ˴˵̳ɰǬŨǨɫˬɫɬʭʯ˯˱ʰǰǯȰ̴ϷɯĨżĶ§ɯʲʴ̵ʹͱ̰ͱγγ˱ɯɮŨź~xwy}ʻħɮ˯ʯɯʰ˳̴̴ɳ̵ʲʱ˳ʹͶ̴˲βѵгͮʩ¢|kb`dkx˳̲̰Ǫƻxmkhfcceikljq|Ϳʯ̴˵ʵʷ˹λϾп̻ν˸IJıʵ˵ʯȨɿ¸Ƹɼʻ~l]~X{V|VzSyQxQYfsǪβͳʳ˸ͺѾͺͺѼθȲǮ˲ɷɷɵɳȯȽylayY}]gw·¦¨˿nznXk`JmbLtiSxbyȽɳϹι̷˶˶̷ιϺʳ̳˲ȼy`p`FiX>cR8dT:bR9rdJuƼĪǭɯ˱̴̴̶̲ͷиϷ̲ʯˮitWxiJufIteHrcFvYxȾɿūŮDzDzǴȵȵɷɶɶȵǴȲʵȳʶDZǻ~pWp]?lY8hX7iZ;qbEv[ĺĭɱɱƮƬʰ˱ɯĬü~g|X}pMxjE}mI|Xahw´Ƚî­DZθ̸Į˲ƫp^vP{oIoc=ylI}ZoĻèĪĭŮǰǰǰŮĭ¬íĬūø{pfdfhlg^WvKzO}T]pþ̳ηƱİɷ˹Ůʿ~md^~Z|X{[fxĻü»¼ſªƭǮǮǮůưDZȲȲDzDzƱDzį­¬vf{Y~vRtRwV^frwzsf}\|[^~^en{üƪȮ̲˳˳˵˵̸͹κͷͷ̶̶̶˵˵˵ʴʴʴʴʴʴʴʴʴ˵θϹкϹͷ̶ưưDZȲʴ˵̶̶ͺλλλλͺ̹˸ʷʷ˸̹̹̹˸˸ͺͺͺ̹̹˸˸˸ͺͺͺͺͺͺͺͺϼϼϼλλͺͺͺ̹̹˸̹ͺλϼннннϼϼλλλͺλнҿҿнϼнннϼϼϼλλλͺͺͺͺͺͺ̹̹ͺͺλλϼϼнͺͺ̹̹̹̹˸˸λλλͺ̹˸ʷɶλλͺ̹̹ͺλλ̹ǴȵǴǴǴŲııǴɶʷɶɶȵȵɶɶʷǴǴȵɶɶʷ˸̹ȹ̿ɺȹɹɹƴƳƱdzɳDZííƮŬʯͱʮǫʰ̲˱ʯɮȯǮȮɯ̴̷̱λнι˶ɳȳʲʳʱʱʹ϶еδ̲˳̳ʹʹʹ˰ȮǭȬũ¦Ļü©ǯʲ̲˱ɯȬȬʱ˲ͶͶ˶ʵʷ˸˸˸̷̷̵ʱȯƩŦťťãæŨɭɯɲǰDZDZɳ̴Ƭ|ĹĪȯɲ˶Ͷ̵̵Ͷͷͷʳɯƻwmkipsw³˾ĦȬȭɯʲ˵͹͹̷˴ɱɲɴʷ̷˵ɳεҶгʪŢn\X\frū˳˴ʯĨŸsjheb_bcdc``gtĵͿǭʱʴ̶ʸɹ˺ͻ˼ν̼ȵDz˴ʲȬ̾}~¹ƽȾȾǽǻʼ;̽´m`~Y|W}X}XzSxQ}V_izľȫ˱˴ͺнϾ˺˺λ͹Ȳɰ˵̺˹ʶǮ~ne_]gtƻëëĮíȻs|hjq}­ȳι͸˶ʵʵ̷ιϺɴ˶Ȳźw`o_FfV=`P7cS:bShW9l[?raE|lRrȼƭǮȱɲ˵̶̶̶͹͹̸˷ɶǴưů«ǿíưêȾivcEn[:iV8fW8dT:i]C}eƾưɳDZƯɲ˵̵ȰȯƬƬũƿug]}VzQ}U^r¸éý|i{ZvSxU\ev¢ȬδϸηʵįŽûĽžü¥ħȫʭˮʭȫǪæħŨƩǧ¢pc`}Y}tS|]p~ƾýpawS}mI~nJxTajúǾ¹þĪǮǮǮǮŲŲıı¯ƿžü»üžžüykwUyfEs_q`D}mSvȼêīǰɲ̶θϹϹ˷˷˷ʶǴŲůĮŽĮêlvcEmZ9jW9gX9dT:fZ@yaŽư˵ɳƯȱʴ̵ʲɱɯɯȬŨ¥ȿzoh^Z[`ju~sdvWuW{[cq~Ʀʭͱжйη̷DzļüĽü»åŧǩʬˮˮˮˮǪƩƩƩǪƩéum`zqRzqR|_ftwc|YrO{mJrO]p|ħݍéƮȰ˲˲˲ʱǴǴƳŲı¯ƿƿžĽžžžĽx^~kJua>r[9nW5kT2mY6zgF_yħʭȫĪƬɯʰ͵͵̴͵θϹкѻккккϹϹθθθθθθθθθθθθͷ̶ʴɳDZưưưȲɳ˵ͷθϹͺλλλλͺ̹˸λλλλͺ˸ʷɶλλλͺͺͺ̹̹ͺͺͺͺͺͺͺͺϼϼϼλλͺͺͺ̹̹˸̹ͺλϼннннϼϼλλλͺλλϼϼнϼϼλλͺͺͺ̹̹̹ʷ˸̹̹λλϼннϼϼλλͺͺ̹λλͺ̹̹˸ʷʷ̹̹ͺͺͺ̹˸ʷʷʷɶɶɶɶʷʷ̹ƳǴȵɶȵǴŲƳȵʷʷɶɶȵȵɶɶʷɶɶɶɶɶɶɶʶɵ̷̷ʳɳɰſpw]|qUykPykN{^uƳɴƱDzɳʴůĮííĮƯDZȲʹʰȮǭƬƩ¨ƿžĨȬƪ¹vnfda_aeirĹĩȬȫȫȭƫŬĮdzɵdzưưɰ˰ˮȫťŽzuzýŨȫɬɬʭˮȬŨƻwf[|X[duƿƫɰ˴ǮǮǮȯʴʴȱȮ©ȽĹyvojb_dow¹¥ǫɯɱ˵ͷ˶ɴƱDZɳ˳̴˱Ͱrb\]jzȲʷǴıŲǵȴɱȮǬƪȾüùüżtlc^`ex¸ũͰϵж˱ȮǪ¦{ss~˽ƬȮƮɰɰɳɳʵɴɴȳʰʮȯǫ§ž¸~t`^`epȽƩˮβϲϲгͰȪtg]^iȽåĨĨɽíðŲƵȶ˶ɴDzDzɴ̷͸͸ȳȳȳȳȳȳȳȳįįʿwzkTdT;]M4ZJ1_O6`Q:sgO~·ƱͶ̵ʰǯǯ˵θϺι̶̳ʳǫxqTk]@eT6dS5iXs\:nW5iU2o\;qPk·ǪȫħĪǭǭ͵͵͵̴ͷθϹϹкккϹϹθθθϹϹϹϹϹϹϹϹͷͷ̶ʴɳȲDZưȲȲɳʴ˵̶ͷθͺλλλλͺ̹˸ͺλλλͺ̹˸ʷϼλλλͺͺͺͺͺͺͺͺͺͺͺͺϼϼϼλλͺͺͺ̹̹˸̹ͺλϼннннϼϼλλλͺλλλϼϼϼнϼλλλͺͺͺ̹˸̹̹ͺͺλλϼϼϼλλλλͺͺϼλλͺ˸ʷʷɶ˸̹̹ͺͺ̹˸˸˸ʷʷɶɶʷʷ˸̹ǴȵȵɶɶǴŲƳȵ˸ʷɶɶȵȵɶɶʷɶɶɶɶɶɶɶʶʶͶ̶̴̳˳i~sWwlNugJtgGvWqƵʷǴǴȵȵııíííĮůƮʰɭɭɭȬħǽzy}Ǫȫæƻ~ujgebaccjw´ʿŨɫɫȫūĬĬDZʴʴưíŭʰˮƩ|ts~ûĢǥɩʪɫȪȪɫʮȫʿyg[|V]ewȭɰ˴ȯǭǯɱʳ˴ʳʰǮǪĦʽƹ÷{ricdkrūƯʳθͷɶǴDZɳ̴ͳͱħtfZ\ewøĮɵʷƵƵǶǷɷʶʴɱɱŮƮƯʰ̲̯Ȭƨſyl`Z]ioyúǪ̯̯Ǫžzuw~øʾ¨ƮǯůȲȲɴɴɶɶɶɴʳʰȱɯȮǪé¥çĦ¥¤üqkechsľŨɬ̮ΰˬ¢nd^[fvȿȫǪĩĩêíůưƲdzȴɵ˶ȳƱƱȳʵ˶˶ʵʵʵʵɴȳDzƱįîɾvyjScS:\L3[K2_O6_P9qeM|ƱͶη˱ǯȰͷкѼϺ̶˲ȱƪs|nQj\?gV8fU7kZ>q`DqW~Ⱥ˿©ĭǰʴ̶ͷͷȴɵɵɵǴƳưưǰ¬ůŬ©ltaCjW6hU7hY:aQ7^R8}qYŽDZθ˵ŮƯȲʳ˴ʲȰĪ££ȿĹwke`zXwV`kc}rTseHn`CwiNgõ¤ɪˮˮʮͳйл͸ȳɴǰ«ĽĽ¤ŦǪɬˮˮʯɮƭůĮ¬¬éütz`ykQqcHtdJ{jPrXv]t[mR}iN|mNuSajxµùĺƼèèĽ¨éƮȰɳɳȲȲǴǴǴƳŲı¯ǴȵȵǴŲƿĿ~c~kJs_>u^>pY9mY8ubByXvżħŨ¥¨ǭɯǭζζ͵̴̶̶̶ͷϹϹϹϹθθθͷθθθθθθθθ̶̶˵ɳȲȲDZDZʴʴʴ˵˵̶̶̶ͺλλλλͺ̹˸̹ͺͺλλͺ̹̹ϼϼλλλͺͺͺͺͺͺͺͺͺͺͺϼϼϼλλͺͺͺ̹̹˸̹ͺλϼннннϼϼλλλλͺͺͺλλϼннϼϼϼλλλͺͺͺͺͺͺͺͺͺͺͺλλλλϼϼϼϼλͺ˸ʷɶɶ˸̹̹ͺͺ̹̹˸˸˸ʷʷʷʷ˸˸̹ȵȵȵɶɶǴƳƳȵ˸ʷɶɶȵȵɶɶʷɶɶɶɶɶɶɶɶʴͶʹεϷδĨ}h}rTynPviItgGuVpǶʹȵǴǴȵŲůĮ¬¬ëĮĭȭȫɬʭɬħĻ~rmovǾǪʭƩɾĹ~rnjebbdgnvǹ¦ɫɫȫƩĪŭȲ˵ͷƮĪʭ˭å}wruĠʨ̪ͭ˫ɫǩƨǩ˯ɬyc}WzT]dx¥ȭɰ˲ʯȮɱ˳̵Ͷ̵˴ȯƫĨ̿˾ʽʾ˾ĸ}rigkpr¹ƿéȮ˴ͷ˷ȴȲʲ̲βα¹ug]Z`n§ƭűǵƵ±ʻʻȸȸȶȴdzdzDZɳ̶Ϸиϵ̲̯ʬwf][]ak|ȿåģʿ}upq{õëƮŭƮDZůȲȲɴʵʷʷʷʷ˵ʲɱʲʲɯǭūɯɬȫǪæ½yohdhnuĻ ǧɩĤo_[[`rŻƩ˰ǬèĩīíůȲɳɵɵʶʶ̷ʵDzDzɴʵʵɴ˶˶˶ʵɴȳƱŰįîɾuxiRcS:\L3[K2_O6^O8pdL{Űηй̲ǯȰͷѻҽϺ˵ɰǰĨpzlOj\?kZm\@tZǹ˿êêĭƯʴ˵˵ʴȴȴʶ̸ͺ̹ɳưìŮʴ˵Ǯên{hJq^=iV8dU6bR8fZ@w_ºĮ̶̶ƯìưͶʳ˳ɲȱǭΦŨäȿƽĹxnoMo_=tdBl\;n_@gX;l]@sX|˿Ĭɰͱ̰˱ɯȱɴʷ˸Ǵȳǰ»ĽåƩȫʭˮɮǬƭǮɳɳɵȴŲ¼ýľŽƾɾu}mSm\@dP5iT7oW;nW8sZs`@zkLhϦŨƩȫˮ̲ϵѷҸζ͵͵̴̶ͷͷθθθθͷͷ̶̶̶̶̶̶˵˵ʴʴʴ˵ʴɳȲɳʴ̶ͷϹͷ˵ʴɳ˵ͷθͺͺͺͺͺͺͺͺͺͺλϼϼϼλλϼϼλͺͺ̹˸˸̹̹̹ͺͺλλλϼλͺ̹̹̹ͺͺͺͺͺλλϼϼнͺͺͺͺͺͺͺͺϼλλͺͺλλϼѾѾѾѾѾѾѾѾннϼλ̹˸ʷɶʷʷ˸̹λϼннѾнϼλ̹˸ʷɶʷʷ˸̹ͺͺͺͺ˸˸˸ʷʷɶɶȵ̹ȵǴǴȵʷʷȵȵʷ˸ɶɶɶɶɶɶɶɶȵʷ˸˸ɶǴȵȵɳε϶ʹ̲ͳʮŪprUwjJufGufGrQf˹̹ȵɶɶƱîĭĭ΍¦èŪɫƨåȽymmoxüæéĨçƿƽħȿø~jb_afl|ƩȫƪūƬȱ˴ȮĪĨæ¼qpv~ƼĠȤƤɧ̬̬ɫƨŧƨǪǧzc|VwOYe{æȫ˯βͰͱͳδ̵ʳȲDZƭƫƫƧĤġġĝĚƺztpjmuúƿíȱȲǯǭũĻzlccfkyĽ¨ƯDzİIJƶʺ˽ʼɻɻɸɸ˸˸ɶ̸κͷʴɰȯȭˮʫƥtj}\vVuU{[bjrzoib^eqȼ¬ů¬¬ůů¬ȳȳȳȳɳʴʷ̶˵ʴʴɳɳȲʱʱɰǮĩ§§ħħǥŷ|maZ_eq~vrihgnxƻŧδϵϵδͳ˱ȱǰĭŮƯǰȳɴɴȳ˶ʵɴɴɴʵ̷͸͸̷ʵɴȳȳȳȳɴŰȽtxiRcS:]M4\L3\L3[L5qeM}­˴ʳ̲͵ζϹϹллʴɰǰçq{mPl^AiX:kZo`Cw\¶íȱͳ̲̲ʳʵʵʷʷȵɴȳ«Ľ»ƿæȫʭ̯ˮȭƫŬŬDZDZdzƲïĽſǿ¬ȾqWn]AcP2gR5nW8nW8sZ;ydEbúɿƽƽǽƼǽ©©ƾľſíưDZȲȲɶǴƳƳǴǴƳıȵȵƳŲðģ`xeDs`?s_>q^>q`B|mPnɬƩƩȫʭ˱ͳδϵζ͵͵̴̶ͷͷθθθθͷͷ̶̶̶̶̶˵˵˵ʴʴʴʴʴɳɳɳʴ̶ͷθͷ˵˵ʴ˵̶ͷ̹ͺͺͺͺͺͺλͺͺλϼϼλλͺϼϼλͺͺ̹˸˸̹̹̹ͺͺλλλϼϼͺ̹̹̹̹̹̹̹ͺͺλλλϼͺͺͺͺͺͺͺͺλλͺͺͺͺλλннннннннѾнϼλͺ̹˸ʷ˸˸̹ͺͺλλϼнϼϼͺ̹˸ʷʷ˸˸̹ͺͺͺ̹̹ͺ̹˸ʷɶɶʷʷͺǴǴǴȵʷʷȵȵʷ˸ɶɶɶɶɶɶɶɶȵʷ˸˸ɶǴȵȵɳͷ͵˳ʳͳ˰ȭuvYzlOvgHteFnPdɶʷǴȳȳŮêê§å¤ɼ{qlms}¦ȬʰʯǯūééĪĨǫŨèħæƽ}tjddhs~ú¥ŨŪǮɰƭī§ȿsouƼŸȤʩǧɩɬɬɬǪǪȪɬȨʿ|f~XwO[h~ùƦɬ̰ϳβαͲ̱˲ɰDZưǮǬƫǨƥƢšŠÜ››ȿúxnlnqxſŬǭƩ|la^fq|ſɬǯưŰűȴʹ̻̼̼˻˻ͼμϽоͺλϹͷɳƮĬĪˮʬȪȨĤzgy[}pP~qQuTyX~[_]}ZxW{YgzƻŬůĮ¬íưƯìììĬŭƮȰɳʴ˵˵ʴʴʴɳɱɱȰƮ΍¨ħħ¤ʽöpcZY\dfffhlr{ȽˮԷηηηηͶ˴ʳɲĭŮƯǰȱȱȱȱɴȳȳȳȳɴʵ˶ι͸̷˶ʵʵʵʵDzŰǼqvgPdT;^N5]M4]M4[L5pdL}­˴˴ͳ͵ζθθιιʴɰƯ¦tqToaDkZ_O5dT:bR8i[@x]ÿȱɴ˶̶͸ϼнν˺ȷŴðî­¨¥çĪŮȱȱǰƯƬǭȮȮȮȮȮǭƬƬìį«ǿȽƼiufI`Q2eT6iY8hU5n[;yjKoýŭȰƮɱȾȾȾ¬¬­ǿ¬ðŲȴʶʶ˷ʷɶǴȵɶɶȵƳɶǴı¯¯ðıǯ¥ƦľyzYs`@p]=kZxiJ}c~įůưDZǰƯíƱƱDzȳɲɲȱȱ̴˳ʰʰ˱˱ȮƮʿ|gn^GhX?]M4`P6^P5eY?u\ȳ˶̸˸͸λϼͼʹǶij¦¥ç¨ĭưȱưƯǮȮŬƪĩŨŨħħĨ«ĭì¶iugLbT7gX9hY:bS2l\;}oRuý¼ªƮĬŭȾȾɿ¬í­¯ŲȴɵʶʶʷȵǴǴȵȵǴŲǴƳŲıııŲǯťývvUp]=o\|pXźîȱɲʰ˳˳˵ʴʵʵ˵϶Ͷɭ˿w|_yhJn]?hW;m\@rbHykPf|ȾƯʳʴɳ˵θʶȴȴȴɶɶɳDZ˴ì¬Ĺrx_peIkX:o\;yfHvWrŹĭ­Įůůĭì¬íŰƱDzȳɴʵ˳ʲ͵̴˳̴εε̳ɳñxem\HiYB^N5`P7`R8e[@}v\Ȳ˶͸͹̹ͺͺ˺ɸƷij¨éĨħħ¬İDZDZưůŭƭĪƩĨĥ¤ä¤æĪŮƯĪ©gshLcX:j]=h[;_R2k\=tVw¼¼ý¼ȽȽɾíí­ſ¯ıdzɵɶʷɶǴƳƳǴǴƳıııııııŲƮæǧľrrQkX8kX8bQ3_P1aS8qgLrìƭ˰ˮˮˮʰʰ˱˱̴˳˳ʲʴ˵˵̶θθθͷͷ̶̶̶ʴʴʴɳɳɳȲȲɳʴ̶ͷͷͷ̶̶˵˵ʴʴ˵ͷϹкϼϼλͺͺ̹˸˸λλλͺ̹˸ʷɶͺͺͺͺͺͺͺͺλλλͺͺ̹̹̹˸˸ʷʷʷ˸̹ͺϼλλλͺͺ̹̹ͺͺͺͺͺͺͺͺλλͺͺͺͺλλϼϼϼϼϼϼϼϼλλλλͺͺͺͺ̹̹˸˸˸˸˸˸̹̹˸˸˸˸˸˸˸˸̹ͺͺͺ̹̹ȵȵȵǴȵɶʷ˸̹ƳŲǴȵʷʷȵȵʷ˸ɶɶɶɶɶɶɶɶȵʷ˸˸ɶǴȵȵųɷʹɶǵdzðʾk}mTvfLtcIraE|kOz`¨éžü¹}qf_afqĽħǪŨΧ©ǰ˴̵˴˶˷ʶɵɵɴʵ˶̷ηθε˲ȯƭŪǭ˰ʭħþ|hc^_eq{wrjdhuĺãŤťƥŧƧȫɮʰɯɲ˴ͷ˵ư}_tMtLzRZkȾţâĥɪ̯ˬɭȫȫȭɮʱ̳̳̰̰ˮʭʪɬȲȴdzʵ˴˴ɰǮƫŨȿsd~[uStRuRtQ|nIxjEsN\rʪʭȬǫǭɰʱ̴̵̳δͷͷʴɳʵʵʷ̸ιͷͶʹ̳ɰůĮƲȳűDzȳɴɴȳDzưwz]xgIp]=r_>zfCr^;sa={hGzYtƸʾǬƫʱε˱éæɬɬɬɬȫȫɬʭʭ̲˵ͷθεʹʱɰƭƫƭƫƫƫƫȮƲɶƳƮˮȧjVxMrGrGwOaλʯȲưŲȶʺ˻̶̻̹ʷɳȵʴ˵̶˵˵̴̴̴˳ʲʲʵ˶̷ιι͸̷̷˶ʵɴDzDzƱƱDzȱɳƼqZi[A`P7]M4^N5bR9fW@s[Žîȱȱȱʲ˳˵ʴʶɵ˵θ˴ǭmqTtcEn]AraErcFtdJuZpɿƫɯ˱ϵӹ˳ʲɱɱʳʳɲɯʰǽ{}bxnUlaEm^AtcE~oRc|ɿŮëĮë¬ŮîįƱȳʵ˶̴̴͵̴̴͵зз϶̶Ƴtal[Gk[DaQ8cS:cU;h\Bx^Ȱʴ̷͹̹̹̹˸ǶŴij¯«ūƪƪũĮƲȲȲưíīīȭǬǪƩŦŦŦŨǭɱɯūƼeshLgYna7na5ob8uh>sL`sɾǪȫǬŪȬ˯ʰƬǭǭɮɮ˰ͲϴϵηηηηηͶ˴˴ɲʰȱȮȮǭƬƮȱǰŬƪȩ˾e}VsLrKtOyVoøɰɴȳDzDzǴȵȵ˵˳˵˳˵˳˳˳͵͵͵͵͵͵͵͵ϹϹϹθθͷͷͷ̶ʴȲȲʴͷθ̵ɰɯǻ}|nTeU<`N6bP8`P9[K4eV?yaǿȱɱǰƱŰįŰDZɳʴ̶˶ȳŮƯjtWrcDk\=k\=o`A{lMyYu̿ǧʪ˫̬ǨƧƧȩͮίͮʭǪʿj~sWqeKk`Dl`FwkQd|ŻéūƬǭǰƬ¨éŮįůưDZɳ˵ͷθ˵˵̶ͷͷͷ̶̴ɲǽ|do_HdT;eUhX?rdJ}c¼ǬȬƭ̶̶̶˵˵ȵǴƳDZưůůŭŭƮǯŮƯȱɲɲȱƯŮūǭȮǭ΍¨éèƫƫ¨ž¸dpbGiZ=eV9eV9iZ=l^Ay]ºûļû­­î­ǿŽŽƾ­įưƱƱƱƱƱƱŰįî­įDzȳDzŰįŭʰ¥çk{lMcR4kX:dQ3_N0cT7wlPuŪ˲ȰǯƮƮǯȰʴ̶ϹϹϹθθͷͷͷθθͷ̶̶˵ʴʴȲȲDZDZDZȲɳɳʴ̶θϹϹθ̶ʴ˵ɳDZȲ˵θϹϹкϹθͷͷͷθθθθθͷͷ̶̶̶ʷʷ˸̹ͺͺͺͺ˸˸˸˸˸˸˸˸̹ͺϼннϼͺ̹˸˸˸̹̹ͺͺͺμμμμμμμμͻͻͻͻͻͻͻͻλϼϼннϼϼλͺͺͺͺͺͺͺͺϼλͺͺ̹˸ʷʷʷʷʷ˸˸̹̹̹̹̹̹̹̹̹̹̹ʷʷʷ˸˸̹̹̹˸ǴǴȵȵȵǴǴƳƳƳʷʷʷʷʷʷʷʷʷʷʷɶɶȵȵȵųȶɶǴDzɲůhrUteFraCwgFyiH`x¦ĨŻ~l~az[}Z{WzSZjÿƨƫũǫũêīȯʱʱȲȵǶǶȷʷʷDzŰǰȱɯɯȬǫũĨƪǫɭȬũ»ubtU~oNqNtQqNxUduŦǧǧǧƦħŨɭ̰̲ͳ̵̵˶ɴDzƯæ¤x^sQqNrO|XkȾšȢʥ˦ή̬ʪʪ̬ήͰͰͱͱ̰˯ɯȮǭǭȮɯʲ˳˵˵̷̷ͷѸηʮkwS{h@uc=}kE{kGvhCuTkýƩŧɮʯɭ˰ʴɴʵʵ˶̷͸͸͸͸̷̷̷̷͸Ͷϳͱ˱ɲȳɶ˹̺ɷʷ̶϶ϵͰǩ¥e~qQylIznH{oGymCwl?tiqcI}cƼȫȫǬ˳˵˵˵ʴȲDZưưDZDZDZDZDZDZưƮǯɱʲʲɱǯƮƮǯȰǯŭëëīèǫƫ¨ƿù|bo_EhYpbEdºĭĭȱįî­­î­Žƾƾǿ­­îįŰŰƱŰŰįîîŰȳȳƱŰįƮɱè¦eufI`O1gT6hU7cR4gX;|qSyǭγưưưDZȲʴ̶ͷθθθθͷͷͷͷθͷͷ̶˵ʴʴʴȲȲDZDZȲɳʴʴ̶ͷϹккϹͷ̶˵ɳDZDZɳ˵̶̶θͷͷ̶̶ͷθθθθθͷͷ̶̶̶̹̹ͺͺͺ̹˸˸ʷʷʷʷʷʷʷʷͺͺλλλλͺͺ̹̹̹̹ͺͺͺͺμμμμμμμμͻͻͻͻͻͻͻͻλϼϼннϼϼλͺͺͺͺͺ̹̹̹ͺͺ̹̹˸ʷʷɶʷʷʷ˸˸̹̹̹̹̹̹̹̹̹̹̹ʷ˸˸˸̹̹̹̹˸ȵɶȵȵȵȵȵȵȵǴʷʷʷʷʷʷʷʷʷʷʷɶɶȵȵȵƴȶȵȵʵ̵ưfrUsdEq`BvfEzjIbǫ˯ƭêǻ}qia|W{TZgsĽūƫǮȯɳʴʴɵʷȷȷʷ˸ʷDzŰǰȱ˱ͳ̲ʰǫũǭǭƪƪũũŨŨæepQufEseBugDqNa|żĦȩǧȨɩɩȫɬ̰βжϵ̵ʳȱǰDzǰŨ¢t}\rPoKoK~Zmǽšǡɥ˧ϯͭʪʪ̬ͭͰ̯̰˯˯ʮʰɯɯȮȮɯ˳͵ͷ̶̷˶ʴ̳ʳ˯ǭ}~ZlDvd>zhBxhDvhExWqĤǪæǬ˰˯εʴɴɴʵʵ˶˶̷͸͸ιιϺϺϺιиϷζ̶˸˸˹̹ȵDzɲϵѴɪ¹ykzZsSzZfpsncyVsP~rLvPZiɬ˯̰ʮȬūūƬǭʱʹ϶з̵̵ͶͶͶ̵̵˴ʳʳɲȱǰǰƯǯȯ˯˰ʭťpzSoIxiB}mIyVdƽũɱʴʴ˵˵˵˵˵͵̴̴̴̴˳˳˳̴̴͵͵͵͵ζζθθθͷͷͷ̶̶ͷͷθϹкθʴŮèɾnyiOhX>bR9aQ8aQ:cT=thPvʲ̴ɱȳDzƱŰƱDzɴʵȳʵ̷˶˶˶˶ʴʿre{[vVuUtT|ZftĺƼɿɾź}vmedn}éūééƬǭǭǭȮɯʰ˴ưưDZȲɳ˵̶ͷ˵̶̶̶̶˵ʴʲŬ|~rXgW>^N5_O6_O6cS:pbHeƼǪȫȭȰȲȲȲȲDZưůưȲ˵θθ˵ȲưƮǯɱʲʲɱǯƮƮȰɱȰƮŭŭǮŪǫƫéŻv\k[AgX;dU8fW:m^AugJkļû«ƯƯʳƱįîîîîǿǿǿƾƾǿǿîîįŰŰŰįįįƱȳȳƱįįƮʲŪç}asdG`O1gT6hU7dS5iZ=~sU{ǭͲůưưȲɳ˵̶ͷθͷͷͷͷͷͷͷͷͷ̶̶˵ʴɳɳȲȲDZȲȲɳ˵˵ͷθϹϹϹϹθͷ˵ɳưưȲʴʴʴ̶˵˵˵̶ͷθϹθθθͷͷ̶̶̶ͺͺͺͺ̹˸ʷʷʷʷʷʷʷʷʷʷͺͺͺλλͺͺͺ̹ͺͺͺͺͺͺͺμμμμμμμμμμμμμμμμλϼϼннϼϼλͺͺͺ̹̹̹̹˸̹̹˸˸ʷʷɶɶʷʷʷ˸˸̹̹̹̹̹̹̹̹̹̹̹˸˸˸̹̹̹ͺͺ̹Ǵɶȵȵȵȵȵȵȵɶʷʷʷʷʷʷʷʷʷʷʷɶɶȵȵȵǵȶȵȵʵ̵ůǽdrUsdEp_AvfE{kJeũȬƭȯǮèŹp`~X{U}[`myüĪūǯȲȴȴȴȴȵǴȵʷ̷̹ʵȳȱɲ˴̵ͳ˱ɯȮ˱ɯǭūĨĨĨũæ~b|mNsdCtfCwiFtQeƽåȩƩȫɬɬɭɭ˯ͱͳ̲˱ɯǰƯƯǭŨp|[sQrNrN]pǿšƣɧʩϯ̬ʪɩ˫̬ˮˮʮʮʮʮʰʰʰʰɯʰ˳̴ͷ̶͸̷˵˲ŮũĪ^oJxf@{iCyiEwiFzYsƦʭǪ˰Ͳʱ˲ʴɴɴʵʵʵʵ˶̷͸ιϺлϺϺϹкϹθ͹̸̸ʷʷɴȱɮαί¢}ld^bpm^yStLwM}Vg¥ȫˮɭƪĨĨūǭɯ̲϶з˴˴̵ͶͶ̵̵˴ʳʳɲɲȱǰƯǯǮ˯̱ˮťpzSpJxiB}mIzWfǾĨɱʴ˵̶ͷͷ̶̶ζ͵͵̴̴˳˳˳̴̴̴͵͵ζζζθθͷͷͷ̶̶̶̶̶ͷϹϹͷȲìĩżgugLhZ?dT;bR9]N7fW@}qYº˳͵˳ɴȳDzDzDzɴʵ˶ʵʵ˶˶ɴȳʵͷĪ̾}tjbzZ|\afow}~{wv|ƼĭƯìƾļŮǰǰƯǰɲ˴̵̵DZDZDZȲɳʴ̶ͷ˵̶̶̶˵ʴɳɱƭ{|pVgW>_O6_O6_O6bR9pbHfƼƩǪȭǯDZDZȲDZDZưůưɳ̶ϹϹ̶ɳưƮǯɱʲʲɱǯƮƮȰʲɱǯƮƮȯǬɭǬĪçƼu[k[AhYdT;aQ8]N7m^Ggʲʹʹ˶˶ʵɴɴ˶̷ι̷ʵɴɴȳŰDz˵ɯɭø~tlhginrvx¶ȯƯƯìǿûŽìɲʳȱǰȱ˴ͶͶͶȲDZDZȲɳʴ˵̶˵̶̶̶˵ʴȲȰǮy{oUfV=_O6_O6^N5bR9rdJhƼŨƩƫƮDZDZȲȲDZưưDZɳ̶ϹϹ̶ɳDZƮǯɱʲʲɱǯƮƮȰʲʲȰǯǯɰʯ˯ȭūĨƼsYjZ@hY`U9dVk\?k\?paD|nQvŮǿ«ŮĭǰDzDzȳȳȳȳDzDzƱƱŰŰįîî­ǿǿƾƾǿǿƱȳ˶˶ɴƱŰǯª{^laEdU8eT6iV8iV8veGdæƬɮDZȲɳɳʴʴɳɳȲȲȲDZDZȲȲȲȲȲȲȲɳʴ˵̶˵̶̶̶̶ͷͷͷͷͷͷͷͷͷͷͷθͷͷ̶˵ʴɳɳʴʴɳɳɳʴ˵˵̶̶ͷθθθͷ̶˸˸˸˸ʷɶȵǴɶ˸ͺλλͺ˸ɶɶɶʷʷ˸̹ͺͺ̹̹̹ͺͺλλλ̺̺̺̺̺̺̺̺ͻͻμϽϽμͻͻͺͺͺͺͺͺͺͺ˸ʷʷ̹λннϼʷɶɶȵȵɶʷʷ̹˸ɶȵɶʷͺϼннϼλλͺͺͺͺ̹̹̹̹̹̹̹˸ɶɶȵǴǴǴǴȵɶʷʷʷʷʷɶɶɶȵȵȵȵȵȵȵȵȵɷ˹ɶŲƱì~dvkOwhKrcDxgIyXfõɽíưɵͻ˺Ƶ̺˹˸ʶɵȲʳʱͱͯ˭ƩæĿ~th~\zY~[ckĪȮɯʳ˴ι͸˸ɶǶƵƵǴȵȳDzDzŰî«|crS{lK|nKqNyVkǿƧȨȪʭȱɴ˴ʳɲɲ˱ͳ˯ɭȫȫɩɩǧŤµuWsK}qI|sJ{R[}ŤŦħʯ̶Ǯʭ˫ʪɩɩʪɬʭʮʮ˯̰ͳͳ̲̲̲˱˳ʲʴ˵͸͸˵˲ʳͱͳçlnN}iHxeDxeE}lN`źĪ˱̵ʲɳ̶ι˷˵˶͸ϺллϺϺιιϺϺιι͸˷ȵɳDZëƻuoqzʿǭʹɳʴ̶ͷθͷʹ̳θκ̺ɷɹͿɸ§³rhacgnæˮϲͰȭδͶηηηηͶͶͶͶ˴ʳɲɲɲʲȯɭ˰ɬĹ{^sLoIyjCoK{Xjȫ̰ϷккϹͷͷθкϷζ͵͵Ϸζ̴ʲɱɱʲ˳˳̴͵͵ккккккккθθθͷ̶˵˵ʲæzqq|ĽƬʯ̳϶ҸѸкϺϺϺϺιιι̷͸͸͸̷˶ʵʴɳʴ˵˵˵ʴ˲ʱʱʱʯ˰˰˰̯̯ʰʰʲʲʲʲɳɳDZưıııŲǵȶȵɶʷ˸˸˸˸˸ͺͺͺͺͺͺͺͺ̶˵ʴɳȲɳʴ˵ʴȲȲȲɳɳDZĬȾu[j^DfV=eUk]6o_=p]=fS5q]B~fþưȲĮíIJƲǵʶɶɵdzƲűƲdzȵɶɶɸȵȴɵɶʸɷȶƶŵƶƶŴȵʵDzû|wvzþŨŨæŧŨūǭ˱̴ʲȰ˳иζ͵̴˳ʲɯɯɯ̲̲˴̲˴̲˴̲˴̲˴̲˴̲˴̲ʳ˱˴ͳͶδϷжεͲʹβʹβͳ̳θͷʴɳȲʴ̳ε˲̳ε϶зϴεʹ̶˸˸˸˸̹λλ˸ʷɶȵȵɶʷ˸λλͺ̹̹˸ʷʷʷʷʷ˸˸̹̹̹оооϽϽϽμμ̺̺̺̺̺̺̺̺̺̺̺̺̺̺̺̺˹̺̺ͻͻμμϽϼλͺ̹ʷɶȵǴŲƳȵɶʷ˸˸˸ʷʷ˸̹̹ͺλλϼϼϼλλͺͺͺ̹˸ʷɶɶɶɶɶɶɶɶ˸ʷʷʷɶɶȵȵʷʷʷʷʷʷʷʷƶ̺˹ůĮŬĺ}ytt|ƽñǵʷͺмѽммллλͺ̸̹θ̵˴ɲȱƯĭìǿŽºrqt{é̲ͳ̲ʳʳɲɲʵʵ˹ɷȴɵʶɵDZůjvSwgCsc?wgC~nJbzĵåƬʮ˴̴ʵ˶˶̵̵̲̰ˮˮ˫˩˩˧ɥşœmWxOwN{R_m¢çƮθϼ˵Ͱ̯̯̰̰ͭͭͭ˲˲ʴ˵ʷ˸˸˸˷̷˶̶̸̸ιηѸеͳͯȩúy_~oP{jNwfJziMy]wèȮȭȯʰ̴ͷηͷɲDZȲ̸оѿпνϼϼλͷʹ̱̯̯Ͳ˱Ȯ¨ǽĻŻƼǮʱϺѾп˵̴˵ʳ˶кϻ̷͸˴ȯɰʰˮȬƩǪɬʭɬǪǫȬʭɮ˯̲ͳζζθθɲʳ̵ηηηηͶηηηͶͶ̵̵ͳҵѰͭĠhwOmC~kC{iC~nJ^|ɰϹλͺ̻ͺ̻λνнйϸηηϸϸͶ˴Ͷ̵˴˴ʳʳ˴˴δϵϵжжϵϵδͳδδδδͳ̲˱αɬŨŨȫʭȮǭ˱̲͵̴ʲʲ̶θ͸̵̷̷˶˶˸ʷ˸̹͸ιι͸̵˴̴ɱǯǯɱʲʲʲŭƮǯɱʲ˳˳˱̰̯˰˲ɳɵɷȶǵǵƲůƭǬɮ˱μ˾ʺȶǴȲȰȰʰ˱̴͵̶˵ȵȲʰ̰βϳϳβ̰˯ʮ̰̰ɭɭɭĨĽl}uPqiEmeAkbArgI|qUoüèŬ˱̴Ȱ˲ʲɱɱɱɱʲʲǯȰȰɱɱɱʲʲʲʲʴʴɳȲDZƳȵȶȶų°ñƶɷǴɱūƢÜz}Suh>k^4k\5kX7gR3ydIoĮí¬íůŲƲǴɵȴdzűŰƱDzǴǴȵȶǶǶƲƲȴɶȶȸƹŸƹƶƵɶʵɲ٨ÿǪɬŨ£ħŨƬȮǯƮȰ̴ζ͵̴˳˱˱̯Ͱ̲̲̲̲̲̲̲̲̲̲̲̲̲̲̲̲ʱʱ̲̲ͳδϵϵͲͲββδβϴϵϸͷ˵ɳɳʴ˵ʹ̳ʹε϶зϴγεͷ˸˸˸˸̹ͺλ̹˸ʷɶɶʷ˸̹λλͺ̹̹˸ʷʷʷʷ˸˸˸̹̹̹оϽϽϽμμμͻͻͻͻͻͻͻͻͻ̺̺̺̺̺̺̺̺̺̺ͻͻͻͻμμλλͺ̹˸ɶɶȵǴǴɶʷʷ˸ʷʷʷʷ˸̹̹ͺλλϼϼϼλλͺͺͺͺʷʷɶɶɶɶɶɶɶɶɶɶɶɶȵȵȵȵȵɶɶʷʷ˸˸̹ʺμ̺DZưɰĹƽñƴɶ̹κϻѻккϹκ͹̸̸̶˴˴ʳȱƯŮĭì«««ƾé̵̵̲˴ʳɴɴʵʵͺ˸ʷ˸˶ʵȳƱ˾drOueAqa=vfB}mIe}ƷƨȮ̵̰͵ʵ˶˴̵̵̲̰ˮʭ̬ͫάΪ˧ǡœoWyPyP}Tcqçǭθϼ̶Ͱ̯̯̰̰ͭͭͭ˲˲ʴ˵˵̶˸˸̶Ͷ̵̴̶ͷηϸѸеδͯɪ¹x^~oPziKudFxgIyZvæȬɬȭɯʰ˲˲ʲƬƭȲ̶мпϾμλͺͷʹͲγͳ̷˷˷Ȳưưȯ˲Ȳʶ̹ʹɺǻȾʻʷ͵̶ʳ̷кϻ˷ϸη̳̳̲̰ʯɬʯ̱̱ͱ̰˱ͳе϶жηϷθϹϹк̵ͶηηηηͶͶηηηηͶͶͶδӶұͭĠhwOmE~kC~lFuSi¼̳ѻϹ͸ͺ͸ͺιϼлѺϸηϸййϸͶͶͶ̵˴ʳʳ˴˴ͶηηϸϸηηͶͶͶͶͶͶ̵˴̲ϲˮƬǭʰͳ̴ʲ̴͵θ̶˵˵̹θϹθθθ̹̹̹˸̹ͺͺλϹθθͷ̴ʲȰȰʲ˳ʲɱƮǯȰʲ˳̴̴̲ͱͱͳ̲˴ʵʷɸɸʷɴȱɯɭʮɲ˹ɼȸƴŲůŭūȮɯʰ˳˵ʴȲǯɯʮ̰βϳβͱͱʮͱͱʮɭȬh~vRvmLxmM|qSz\iŬŮʲ̶ʲʱɱȰȰȰȰɱɱƮǯǯȰɱɱȰȰ̴̲˳ʲȰǯƮůȲǴŲð¯ðǵʷʴʲĪŤ{}Swj@i\2fW0gW5kX8qV~ȽªëĬŭƮŲDZǴȲȲưĮíưDZȴɵɵɶȵȵưDZȲȵɷɷɹɹƷŴŴǴȳȱƯūƿƿüüƿħȪ˭ɫĦŨƩƬǭȮʰ˱̲̲˱˱ʰʰʰ̴̴̴̴̴̴̴̴̴̴̴̴̴̴̯̯̲˴ʱʱ̲̲ͳδζζ̳̳̳ʹϷϵииϸ͹˷ʶɵʴ˵̶˵̶ͷθθ϶϶εͷ̹˸˸˸̹ͺλͺ̹̹˸˸̹̹ͺλλͺ̹̹˸ʷʷʷ˸˸˸̹̹̹̹Ͻμμμͻͻͻ̺μμμμμμμμ̺̺̺̺̺̺̺̺μμͻͻͻͻ̺̺ͺ̹̹̹˸˸ʷʷɶɶʷ˸˸ʷʷɶʷʷ˸̹̹ͺλλϼϼϼλλͺͺͺλʷɶʷʷɶɶɶɶȵȵȵȵȵȵȵȵȵȵǴȵɶɶ˸˸̹ͺ˻ͽ̺ȴȲʱƫȾżżĺŻƼɿîȿIJƴɶʷ̸͹θθθθθθθθ˴˴̵˴ʳɲǰƯŮììĭŮƯƯūüžǭ̲Ͷ̵˴ʳʵʵʵ˶λ̹˸˸˶ʵȳƱ̿`~pMwgCsc?wgCoKg~ȹƨȮ̵̰͵ʵ˶˴̵ͳ̲̰˯ʭ̬ήѯѯͩǣÝs]VW[jwĤũɯζλ̶αήήήͰͰͱͱ˲˲ʴ˵˵̶˸̶ͷ̶˵̵̶ͷͷηзϴͳ̮Ǩx|Z}nOxgIraCveGxYvĦȬʭʭɭɬȫȭȮɭɮ˲̶Ϲѽҿμͻ̹˸ͷεзϹ˺ʼʺʺ̺͹ϻѽ͹μнν̽ʾ˿̾˸ζ̶˴̷кϻʶͶͶʹ̲ͱͰͰ˯ʮ˯ʰʯȮʯ̱ϵϵδ͵̴̶ͷθϹϸϸϸϸηηͶͶηηηηηηηδгбͭšlySnFlGsM_xé̴θι͸͸͸͸ιϺлѺйηηйййηηͶ̵˴˴˴˴̵ͶηηϸϸηηͶͶͶηηͶͶ̵˴ϵ˱ɯʰͳжϷ͵͵ζθͷ̶ʷ̹λλϹϹθͺͺ̹̹ͺͺͺλϹθθθ͵˳ɱʲ˳˳ʲɱǯȰʲ˳̴͵͵͵ͳͱͳ̵˶˸ɸɸ˺ʹ˸ʵɲʰʰɲȶǷƴƳưŭūūǭȮʰ˱˳ʴɳȰȮʮ˯ͱββββ˯βϳ̰ʮǫ~m^|[ajxƼĭĬíǰʵʲDZȰȰȰȰȰȰǯƮǯȰɱʲɱɱȰ˱̯ʰȮȮǭǭǯƮůí¬íưɶͷ˵ʲū¥ģļuyPuiAi[4gW3l\:xeEgɷƭŭëëĬƮƮŭDzȰȳɳȳȲŰưíĮůDZȲȲǴȲŭƮƮDZƳƴǵƴƵƳıįìĪĪĨĨǫʯ̯ȭŨæĤ¢¢ýýſ¢¤ŨūƬȮ̲ͳ˱ʰɯȮȮǭǭɬȮ˳˳˳˳˳˳˳˳˳˳˳˳˳˳˳˳˱ʱ˱̲̲ͳ͵ζ˲˵̵͸ιϺллϻκ̸˷ʶʶ˵˵̶̶ͷͷθθθθͺ̹˸˸˸̹ͺͺͺͺͺͺͺͺͺͺͺͺͺ̹̹˸˸˸˸˸˸̹̹̹ͺͺμμͻͻͻ̺̺̺ϽϽϽϽϽϽϽϽͻͻͻͻͻͻͻͻϽμμͻͻ̺̺˹̹̹̹̹̹̹̹̹˸˸̹̹˸ʷɶȵ˸˸˸̹̹ͺͺͺϼϼϼλλͺͺͺϼɶɶʷʷʷɶɶȵȵȵȵȵȵȵȵȵȵȵǴȵɶɶ˸˸̹ͺ˻˻˹ȴDZɰȭĩħħèĩīê¬î¯ñųǵɶʷʷ˸ɳʴ˳̴͵ζϷиʳ˴ͶͶͶ˴ɲǰǰŮ««ŮǰǰȮɯǫũĨĪƬɯʰͶ̵˶˶ʵʵ˶˶λ̹˸˸˸ʷȲưɼ{}Z|nKwgCtd@vfB~nJg~Ǹƨǭʮʳ˳ʳ˴˴̵ͳ̲̰˯ˮͭϯѯҰΪȤşxhfimy¼ Ȩɭ˱ζͺ̶αΰΰΰͱͱͱͱ˲˲ʲ˳˵̶̶̶͵ʹ̴̳̲ʹʹʹεγ˱˭ŦtxX|mLvfEp`?ueDyZwãǩʫͭˮʬʬˮͯͰααͲͷκҿμͻ̺˸̸θкнɽȿɽʾ̾ͽϽϽʸ˹̺˻ɹɻʼ˻ͷϷͷ˴̷кϻʶȲȲɱʰʮʭʮ˯˱Ͳʹͳʱʰ˱͵̴̴˵ʴ˵ͷϻкййϸηͶͶͶͶηηηηηηηжͰͮ˫¡jvR{lEmIxTkƽɯ̴˵ηͶͶͶͶηϸйѺϸͶͶϸййϸηͶ̵̵˴˴̵̵͸ιιϺϺιι͸͸ιιιι͸̷˴Ϸ̴ʲ̴Ϸѹкθͷθͺ̹˸ʸ̺λϻϻϻκκκ͹͹κκκκκκκθζ̴˳˳̴̴ʲɱǯȰʲ̴͵͵ζζͳͳ͵̴̶ʷʸʸ̺̺˸̶˳˱ʰɱijĵŴƳDzǰȮȮǫȬʮ̲˴˴ʵɲɯɭʮ̰ͱͱββ˯βϳͱ˯ɭ|sr{ʾ¬ŰĮ®ŰDzȱDZǯȰɱɱȰǯǯǯȰʲ˳̴˳ʲɯͱ̯˯ʮʮʮ˯̲ȮŮì«ĭǰʵ˴ɲɲǭħ£pwNwkCnb:pb=xjGtSsųƼü»üƿ¨¨ĮǭůǰDZȱDZǰǿìŮƯƯƯĪĪĪììîððȵǴŰ«èȫ˯̭ǫ£ƻž¦çƪ˯˯ȬʮɭȬǫǫǫǫȮ˳˵˵˵˵˵˵˵˵˵˵˵˵˵˵˳˱˯˱̲˴˴̶ͷ˵˷̸ͺλϼϼнϻͺ̹˸ʷʷɶɶ˸˸˸˸̹̹ͺͺͺͺ̹˸˸˸̹ͺͺͺͺλλͺͺͺͺͺ̹̹̹̹˸˸˸˸̹̹̹ͺͺͺμμͻͻͻ̺̺̺ϽϽϽϽϽϽϽϽͻͻͻͻͻͻͻͻϽμμͻͻ̺̺˹˸̹̹̹̹ͺͺͺ̹ͺͺͺ̹˸ɶɶ˸˸̹̹̹̹ͺͺϼϼϼλλͺͺͺϼɶȵ˸˸ʷɶɶȵǴǴȵȵȵȵɶɶɶɶȵɶɶʷʷ˸˸̹ʹɹʸȴdzDZǮǬƫǪƫƫŬŲųǵȶɶʷʷʷDZȲɱʲ˱ͳδδɲʳ̵ͶͶ˴ɲȱɲƯì«ĭƯȱȱʰ˯˯˯˱ʰȮǭͶͶ̷˶˶˶˶˶ͺ̹ʷʷʷɳưĮķtvSzlIxhDsc?td@zjFg~ǸŧƬɭȱɱʳ˴˴ͳͳ̲̰˯ˮ̯ήаѯάɥšvy~ƾĠǥʪˮͳζͷ̶Ͱ̰̰̰̰ͯͯͯ˲˲ʲ˳˵̶̶̴ͳͱ˱ʱ˱ʲ˳̳ʹʹʰʭħsuU|mLvfEqa@whG}^{ŤƧɪ̬ͭɩȨ˫ήͮίϰϲʹͷлҿоμ̺˹̹κмѿɽǾǾɽ˽̼ͻ̸ʶʶʶȴǵȶ˹μθзθ̵θкϻʴ˵εδͱˮˬ˭̮γѷӹҸжϵϷϷ̴̴̶ͷθϹмѻйϸηͶ̵̵ͶͶηηηηϸϸϸжͳ˭ǪǾiwT|lHsQ^xĩδϷ̶ηͶͶͶͶηϸййη˴˴ͶϸйϸηηͶ̵̵̵̵̵͸ιιϺϺιι͸ιιϺϺι͸̷̷Ϸ͵˳̴иѹϹ̶̶ͷͺ˸ʸʸ̺ͻϻκκκ͹͹͹̶κ͹͹̸̸͹͹κθ͵̴͵ζ͵˳ȰǯȰʲ˳̴͵͵͵̲̲˳˵˵ɶɷɷʸʷ˸̶̶˳ʲʲijŴǴDzȱȮȮǫƪȬʮ̰ͳ̵˴˴ʰʮʮ˯̰̰ͱͱ˯βϳͱ̰˯ũĽʾĬůůİîðűƱƯDZȰɱʲʲɱȰǯǯȰ˳͵͵̴ʲɯгϯ̬ɩǪƩƩƪ¦ūȮɯʰȱʰɯŨǾp~WvP}qKtQxW|^sƼƼȾ¬Įů¬¬ĮƭŭǬūƫƩŨũūūŮƱƱƳƱŮéƿüüĽǾ¤ʿź{wsruy}ççĨʭˮǪǪǪǪǫȬɭʮ˱˲ʴʴʴʴʴʴʴʴʴʴʴʴʴʴ˲˱˯˱˱ʳ˴̶̶̸̸ͺͺͽϾξξμλͺ̹˸ʷɶɶ̹˸˸˸˸̹ͻλλͺ̹˸˸˸̹̹̹ͺͺλλͺͺ̹̹̹̹̹̹̹̹̹̹̹̹̹ͺͺͺλϽμμμͻͻͻ̺ϽϽϽϽϽϽϽϽμμμμμμμμμμͻͻͻͻ̺̺̹̹̹ͺͺͺͺͺͺͺͺͺͺ̹˸ʷ̹̹̹̹̹̹̹̹ϼϼϼλλͺͺͺͺɶȵ˸˸ʷɶɶȵǴǴȵȵɶɶʷʷʷ˸ʷʷʷʷʷʷʷʷʹʹ˸˶ɴǰȮɭɭɭȮǭƯŮŰįȲɵʶ˷ʷ˸ʷʷDzDzǰɯɭɬʭʮƬǰʳ̵̵ʳȱƯǰĭ««ĭǰɲɲȮʰ˱̲̲˱ȱǰι͸̷˶˸˸˸̹̹˸ʴʴɳȲůªµptQ{mJ{kGueAsc?yiEhȹŧƬɭȱȰ˱̲̲ͳͳ̰̰˯̯̯ͭήϭάʨǣɿ¹úǿǠǣǥɩˮδζͷ͵̲ͰͰͰ̰̰̰̰˰˰ʲ˳˳̴̴̲̱̮˯ʭɭɯɯɯʹʹ˱ˮħr~sS}oLxhFseB|nKb~øƥȩ˫˩ǥûƾšɨίг϶̶͸λѿоͽ˻˻ͺноɽȾǻʻͼϼ̶˲ԻҷͲȭŭȰϷսзѸθ̵θкϹʴϹѹѷβɬƨƦǩ̰ϷӻԼӻѹккͷθθϹϹϹθθηη̵˴˴̵ͶͶηηηϸϸййѷж̮ħĻpa\hsǼ˰ҸѹϹηδδδδϵжѷϸ̵ʳɲ̵ηϸϸϸηͶ̵̵̵̵Ͷ͸λλϼϼλλͺλλϼϼλλͺ̷Ϸ̴˳̴ϹϹ̶ɳʷ˸̺˹ɷɷ˹ͻλͺιιͶͶͶ̳ηͶ̵˴˶̷̹͹Ϲζ͵ζζ͵˳ȰƮǯɱ˳̴͵͵͵̳̳ʴʴɳɳɵɵȴɵ˷̶̶̳˲ʴǴƵȵȳǰƬĨæħƩȫʮ̰̲̲˱˯˯˯˯˯˯̰̰˯ββ̰ͱβʮçþ½ǻŮǰȲůİŰDZDZƲưDZɱʲ̴̴ʲɱǯŭǯʲ̴̴˳ɱǭ̬Ȧ ¼ƿƪɭɭʰ̲ʰ¥wifbdb}^hph~dmxŹȼʾο˿̾ȽɻƻżżżƽȾ¬íƾǿžżȽ˽˽˾oic]\^aezƽǾƽ£Ŧ£ǾȿæƩɬ˰δʴ˶˶˶˶˶˶˶˶˶˶˶˶˶˶ʴ˱˯˱˱ʳʳ˵˵̹ͺͻͽ̿Ϳ̿ͽϽλλͺ̹ʷɷȶ̺̺˹ʸ˹̺ͽμλͺ̹˸˸˸˸̹˸̹ͺλλͺ̹˸̹̹̹̹̹̹̹̹̹̹̹ͺͺͺλλоϽϽϽμμμͻμμμμμμμμμμμμμμμμ̺̺ͻͻͻͻμμͺͺͺͺͺͺ̹̹̹ͺͺλͺͺ̹˸̹̹̹̹̹̹̹̹ϼϼϼλλͺͺͺ̹ȵȵ˸˸ʷɶɶȵǴǴɶɶɶʷʷ˸˸̹˸ʷʷʷʷʷʷɶʻʻ˺ͺ˶ǰȮɯ̰˯ɯȮƯƯDzȲʴʴ˷̸˸˸˸ʷɴȱɯȬǪȨƧǧĪŮȱʳʳɲƯĭìǿĭȱʳ˴Ȯɯ˱̲ͳͳ˴˴ι͸̷˶˸˸˸̹̹˵ȳɳȳǯìöquR~pMoKxhDueAzjFjɺƨǭɭȱɱ˱̲̲ͳͱ̰̰˯̯ˮ̬ͭήͫʨȦ˨Żß››ǢǣƢǧʭαδζζ˱̯̯̯˯˯˯˯˰˰ʲ˳˳̴̴̲̬˫ʪȪɬȬȬȮ͵ε̲˱ũttT~pMzjFvhErOfżɩ̬̬ʨŽĠͮҵѶͷ˶˸оͽ˻˻̼ϼѾ˾ɺȹ̹ϺѺ϶ͰƧƻçзѸй̵θѻϹʴ˳̲˯ǪʿǺƹźĪȰζѹѹϷͷͷͷθϹϹθ̶ʴɳͶ̵˴ʳʳ˴ͶηηηηϸϸййѷҸ̯æ|ttøƩжӼҼиηδδδδϵжѷη˴ɲȱʳͶηηϸηͶ̵̵̵̵Ͷ͸λλϼϼλλͺλϼϼϼϼλͺ̷Ϸ̴ʲ˳θθ˵DZɶʷ˹ʸȶȶʸ̺ͺι͸ͶͶ̳̳ͱβͱ˲ʳʳ˶̷̸ϹζζζϷζ˳ȰƮǯȰʲ˳̴̴̴˲˲ɳɳɳȲȲȲDZȲʴ˵̶˵̳ʴɶɶɴȱǭĨƿ¥ħƩɬʮ˯˱ʰ̰̰˯ʮʮʮ˯˯˯ͱͱ̰ͱϳͱǫŧŧĨç¨ƬȱǰȲůĮƲɴɳDZưȰɱ˳͵͵˳ɱȰĬƮɱ˳˳ʲȰǪáľyttrvħʭˮ˱̲ɭxvsrjz]z]y_|nS{oU{apy}~÷ĵôøpc\zUvOuNwPzS{VføƽǪʰδʴ˶˶˶˶˶˶˶˶˶˶˶˶˶˶ʴ˱˯˱˱ʳʳ˵˵λλͻͽͽ̼̿̿ϼϼλλ̹ʷɷȶͻ̺˹ʸ˻̼ξϽμλ̹˸˸˸˸˸ʷ˸ͺͺͺͺ˸ʷ̹̹̹̹̹̹̹̹̹̹̹ͺͺλλλоооϽϽϽμμμμμμμμμμμμμμμμμμ˹̺̺ͻͻμμϽλλλͺͺ̹̹̹̹ͺͺλλλͺ̹̹̹̹̹̹̹̹̹ϼϼϼλλͺͺͺ˸˸ʷͺ̹ʷȵǴǴȵȵȵɶʷ˸˸˸˸˸ɶɶɶɶɶɶɶɶǷʺͻͺ˵ɱʰ˱˱˱̲ͳ͵͵̶̶̶̶̳̳˸ʷɶɶ͸Ͷ̳Ǭ¹ļĭƯǰɲǰĭ««ìĭǰʳͶϸ˱̲˴˴˴˴̷̷ιϺϼλͺ̹˸ʷ˶ʳDZ٬ĪêĪqxUuRuQoK~nJuQlʬƬƪǰ˳˱˯˯˯̰ͱαϲˮ̯αаѱаϯϭΫ̩ȥƢɥͩʥĠȨʭ̯̲ζϷ˱ʰɯɯɯɯʮ˯Ǭ̱δ˱Ȯʰ̲̱ɨɦƥƦƧƩƪǪ˱˱̲ͳǫvy[}oL|lH}oLzXnͰɬ̬ĢjaqļͮӶеϹѼѾϽϽϿϿϿϿннϽλ˸͵Ҹոϯţžzony̱˵̵̵ͷͷͷͷͶʰèĻ~ĮζϷ͵͵ζζζζѹӻϷȰŮɲȱʳͶͶ̵ʳʳ˴ϸηηйѺѺϸͳӹα¥ƸɾƪͲҸѺϹ͵ϸϵδͳͳͳͳδη̵˴ʳʳ˴Ͷη̵ͶηηͶ̵ʳɲ̷̷̷͸ιϺлѼιιϺϺϺι͸̷˳˳̴̴͵͵θθϹͷʷʷ˸˸ʸɷ˷̸ͷεͲ̯ˮ̯ɬʭ̱Ͳʹ˵ɵȴϹϷииϷ͵˳ʲǯǯǯɱ̴͵̴˳ɱǯƮƮȰȰȰƮŭƮǯǯȰɱɱɱʵʶɵůǽǾȿǾȿäȫɬƫè˯˯˯ʮɭɭ̰β̰˯ʮɭɭȬɭɭʭ̯̰˯ǭūììŭǯȲưƯƯȰɱ˳͵ζζ̴ʲȰǯɱʲ˳ʲȰŭŨľ~pe^~Z{ZxW^mäˬˮɰʱȭxgwY{mPzlO|nS}qW}cq{~~~qmomnu~{x~xlbxTqMtMsLoGqIxTew~´Ǽéʱʲ˵˵˵ʴɳʴ˵̶ͷ̶ʴ˵ͷ̶ʴϵͳ˱ʰɲʳɴȳƳ̹ͼȷȷ̽;ʹ̷̷͸ιι̷Ǵıͼ˺ɸȷȹʻ;νϽλλͺͺ̹̹̹̹̹̹̹̹̹̹̹˸˸˸˸˸̹λλλͺͺ̹̹ͺͺλϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽͻͻͻͻͻͻͻͻμμμͻͻ̺̺̺λλλͺͺ̹̹̹̹̹ͺλλλͺͺͺͺ̹̹̹ͺλλλλλλͺ̹˸˸λ˸ʷͺ̹ʷȵǴǴȵȵȵɶɶʷ˸˸˸ʷʷʷɶɶɶɶɶɷƶɹ̼̺ʷʴʲ̲δϵϵϵζ͵̶˵̳ʹ̶ͷ̹˸ʷʷ̷ʳŬ¹xpoƾĭŮƯĭ«ìĭƯȱ˴Ͷϸ̲̲˴˴˴˴˶ʵϺϺϼϼλ̹ʷɴ˵ȯƼĸ¸ĸĺjtQqNpL|lH~nJwSvæɫĪũȱ̲̰˯ʮʮɭʮʭˮˮ̯ͰͰϯϯϯϯɩȥŸɿžǣŠŽšɧ̬̮̯̲͵˱˱ʰʰʰʰ˯˯ȭ̱̲ȮȮ˱˱ȫƤͽʻʿŦȫɭȮȮ˱ͳȮzctSuSzYh{æΰͰʬjsM{oG\{ɪѴ϶θѼҿооѾѾѽѾѻζͳѴԴͩĞoZXf{ĸƤ̶̵̵̲ͷͷθζϵũpciz͵Ϸ͵̴͵̴һϵδϵδʰɰͳ˴Ͷϸϸ̵ʳʳʳʳʳ˴Ͷηη˴ɯϵ̯ƩǾǾȿçƫɮ̳ηϸкϹϸжϵδͳδδδ̵̵ͶͶͶͶ̵̵ηηηϸηηͶ̵ͶͶ͸Ͷ͸ηлйлϸϺη͸̵˶ʳ̴̲͵͵͵͵̴̶θ̶ʴɶʷ˸ʷȵ˸̶̴̱ˮˬʪ̫ɩ˪˭ˮʰ˲ʴɳ˳̴ζϷϷζ͵̴Ȱǯǯɱ̴͵̴ʲ̴˳ɲʰɯǪþĽ½»Ľ«ĭƲDZư©ŻúĩŪɭɭɭȬǫǫʮͱͱ̰˯ʮɭɭɭɭʭˮʮȬé˿˽ɻǸɻʻȼȼȾĪƮǯʲ̴̴̴ʲɱȰɱ˳˳ʲȰŭçtb|XyU}Y\hpúĥȩȫƫȯȭŪżĻƽr~^|qS{pRwY~bv÷ǻ̽οôyf]]~[^hw}|}ti[vOuNrJmEqFxQh}{rilpwĶȰȱ˲ʴ˲ɳʱɳ˲DZǮůƭDZ˲ɳǯ͵̲ʰʰʳʳʳɴɴ͸̹ǴǴͺξλ̵ͳ̵̵ηιιλ˸ʹʹʹ˼˼˼˺˹˸˸˸˸̹̹̹̹̹̹̹̹̹̹̹̹˸˸˸̹̹ͺλλͺͺ̹̹ͺͺλоооϽϽϽϽϽϽϽϽϽϽϽϽϽͻͻͻͻͻͻͻͻμμμͻͻ̺̺̺λλλͺͺ̹̹̹˸̹̹ͺλλλλλͺͺ̹̹ͺͺλͺͺͺλͺͺ̹̹ͺ˸ʷ̹˸ɶȵǴǴȵɶȵȵɶɶɶʷʷʷ˸˸ʷʷɶɶȵȶƶȸʺ˹ʷʴ˳̴δδζζ̶˵ȵȲ̳ͱʹͷ̹˸˸ʷʶȯǻj{UsKsMwûŮǰƯĭ«Ůǰʳ˴ͶͶ̵̵̵˴ʵʵɴɴλϼϼнν̻ʹɵǯʾ{etQqN~nJ|lHsO\ĵŨɫĪũɲͳβͰ̯ˮʭɬɬʭͰ̯ˮˮˮ̯αаʪãĺǽǿûĠɧ̬̯ͯ˱˱͵̴̴˳˱̲̲ͳʯ˰ɯǭɭ˯ƪ̾Ĵ|}¥ǫǭǭɱ̲ɯpdfo~žɬαγǪm{qNeY3hZ3qLoŦгε̶ϺѾϽϽϽϽнммккзγ̯ϯѮɥz]ypG~uJh˨δͷ͸̷ͷθϷиδǽkvUrP[m̵η̲˱̲˱Ѹʯƫȭ˰˰ʰ̱ϵйѺϸͶ˴ʳ˴˴˴̵ηйϸ˴ʰͳ̰˯˯˯ͰϳѵͲͳ̲˴˴̵͸ιηͶ̵˴˴˴˴̵˴̵ηййη̵ʳͶͶ̵̵̵Ͷηη̵ͳ˴̲˴ͳͶδѺѷηͳ˴˱ɲʰ̲αδδͳ̲ʰɱ͵˳ɱʴ˵˵ʴɳͷ̴ʰɬȨȥȤɤšǢȦɩʮ˱ʳ˳Ȱʲ̴ζϷζζ͵ɱȰȰʲ̴͵̴ʲʰʰɰʮŪƿŻííƺ~zuvzȫƪƪƪũĨũȬʮʮʮɭȬȬǫƪƪǩǩħƻüªƮɱ˳˳ʲɱʲʲ˳̴ʲǯĬ¦o~Z|rO}sP~[exƻåĨ§êŪƫŪħ¥nxYypQwXguùĽƼüǽƿ楮ðı̽ȹów_uQrNqOwTauŹƺźƻļûvf}VxQrJnCpFxRkŶye}V~Y\dp̼ūǮȮǰȮǰȮǰȮƯĪĪǰ˱ʳʲʲʲʲʲʳʳʳ˴ɳɳDZ¬íɳͷͷ̱˰ʱɰʱ̶Ϲѽȴʸ̺ͻ̼ʺȺȷȶȵɶɶʷ˸˸̹̹̹̹̹̹̹̹̹̹̹̹˸̹ͺͺλλͺͺ̹̹ͺͺλооооооооϽϽϽϽϽϽϽϽͻͻͻͻͻͻͻͻμμμͻͻ̺̺̺ͺͺͺͺ̹̹̹̹ʷ˸˸ͺλϼϼнλλͺ̹̹ͺͺλ̹̹̹ͺͺͺͺͺ̹˸ʷ̹˸ɶȵǴȵɶɶɶȵȵȵȵȵɶɶ̹˸˸ʷɶɶȵȶǷǺɹɷʸ˵̶̴ʰʰʲʲɳȲƳDZʱ̰˲̳ʷʷʷɴ˷ƭy{XvjBuhwTbog]}Z~Zduͽģʭʬɯͱ˴ɯɬʪɩɩȫȫɬʭα̯ʮȬǫȬʮˮũøvwÝĠţƦȨ˭α͵ͷ̶̶˴˴ͳͳʯ̱̰˯̮ʬʿysorʿȫӷϵ˳ʲ̷ϺлзɯǫƬǯʵʷʷɶȳθǯ¹ðͿȲͷ˷ȶʸͼνͺͺͺθ϶ϴϲ϶ͼѾѾҿϺʳưĮƱ˸̺ɹǸɹɵ̷͸Ϻкθ̵˱ɰβήǤĠơʣʤ̬̰˯˯ɮ̾qbdsѾŤƥǦɯ˴̵˴ʳ˴ͶйͶɲŮĭŮȱʳ˱ɯʮ̲δϷϷϹϹʶ˷̹ͺϺлллҿѾϾϼνϼνϼͶͶ̵˴ʳɲɲɲȱɲɲɲɲȱȱȮʭʪʪʪ˫̬ͭή˫̬˫ʪƦ¼ÿãǧǩȪʬ̮̯ɬƪǻumkfcfhnwȺǭȰʲ˳̴̴˳ʲ˳ʲɱɱʲɱƮΧȹ|na}VyOyR~Wdt˼§ǭŭźouRmc@pfBnd@lb?ndAxmMaz»žĨĨũũçĽÿ¼v{ZvgFueDtdCueDykHyXmøªŭȰɱʲɱȰȰɱ˳̴̴˳ɱǭǨ¢Ĺxsoe~sUodHh]AeZ>mbFy_yùêêêīƭũç¥æƦǧǤĤſþþíDZɳ˵ɭȬƭƯŲƶƺǻŷóŷboKwgCyfE|iItVo˻­¯òijǵȵɵɳ˵ʱȯǬƬũǾt^yRrK}nEsK}YsuaT{N{NzOZmsex[uXuXtWrU{mPxjM{mPuX{^hƺȲͷ͵ʰȮȭˮɾ{rkfbfoz¹ĻǾȾêưɵʶ˷̷ннϼλͺ̹˸˸ʷʷ˸̹̹ͺλλнϼλͺͺͺͺͺλͺͺ̹̹ͺͺλ˹̺̺ͻμϽооͻͻͻͻͻͻͻͻͻͻͻͻͻͻͻͻμμμͻͻ̺̺̺ʷʷʷ˸˸̹̹̹λͺ˸˸˸ͺϼѾнϼλͺ̹˸˸̹λͺ˸ɶɶʷ˸ͺн˸ʷʷɶȵǴǴɶʷ˸ʷȵƳıðıƳǴȵȵɶʷ̹ͺλμϿ˽ǷƴǴȵȲưʲ˲̳̳˵ʴɵɳ˰̯Ͳ϶θλλλdzƭ¨ɾǺŹǼȿ¥ǰʳȱǰɱɱưȲȲDZȰɱ˴Ͷϸɳȵɶʴʴʴʴ˵̶ͷλλ˺Ƶʾ¯x}[|eCt^9}iD{Ve{rjgmyɺǨˮ̯ʮʭ̯Ͱˮή̬ͭʪɬɬɬɬˮʮȬǭƬƬǭȮͲƩøǾĠžĠĠšĢĤťɫ̯͵̶˶ʵʵ˴˴ͳ˲Ͳ˱ʮ˯̰ǫ̿ɺʹȹɺ˽àǨʭдδ̴̴ϺѼҽкŬĪūƮɴɶǴƲȲҺδƪƩģɨʰιιɴȵ̹нѾ˸̶̶̴̲ͰͯͲͺϼλк̵ƯϹͷλ˾ŻŸȶʵ͸ϺϺθʳɲɯеԸаέѯѭͬʮʯɮʮɬɺx|ZmLtPazƲмģͳϵϵͳ̲̰βϴ§Ⱦù¸ŻȾȬȫɭ˯˯̰̲̲ǰǰƯǰȱʳ̵Ͷϸη͸̵̷̵̷Ͷʱ˰̳ͳ̲ʰȮƬǭǭȮɯȮƫī§æĤĤĤťǧȨɩʪ˫ʪȨã¼ĤĤŨǪʬʭǪĨ˿xqnieecdivǼ¥çƪɯʰ˱˳ʲɱȰƮƮǯƬéķ{j_}VyRwP[et̿ŨȭǭȯĪɾtxXsfDl_=j];h[9j]=wjJcĺüĽĽĽž¨ūʰʰ˱˱ɯƬé¥ģƦĢ|~]whGtdCtdBueC|nK~[s¨ĬǯɱʲʲȰǯǯȰ˳̴͵̲ʲɯαʫäĻzawlLbW9ZO1\Q3m_B~cĺƭƭ©©ƬɯǪĨŧȪʩƦŦůȲʴʴɰȯǰűŲŵŸŹĵñŷb~nJxfBwdCyfFpUnʺ­¯°òòųųƳƳdz˴ɲǮȯɯɬ§ȿs^vQ~nJyjCnHxTg{l\|RyMVXdzi_qRxkK{lMrRtUvVzkLuhHvgHxkKxiJ~qQnȴ̹ͷɱǭǬˮx`vS~pKykDzkD}oH[gt{Ⱦůʶͺͺ̹λλλͺͺͺͺͺʷʷ˸̹̹ͺλλннϼͺͺͺͺͺλͺͺ̹̹ͺͺλʸ˹˹̺ͻμϽϽͻͻͻͻͻͻͻͻͻͻͻͻͻͻͻͻμμμͻͻ̺̺̺ʷʷʷ˸˸̹̹̹ϼλ̹ʷ˸̹λннϼλͺ̹˸˸˸ϼͺ˸ɶȵɶ˸̹ϼ̹ͺͺ˸ɶǴƳǴɶʷ˸ʷɶȵȵȵȵɶ˸˸˸˸ʷɶȵȵȶȶƵŲðıDzʵƯ˳ζ͵ʱŬīŬĪë̶нͺ˹λǴɳʲɯȪǧȥȦͰ˲ɯƯƮDZȲɵȳȳdzDZȲʴ˳̶˷̹ͺκͷ̶˲ʱγϴѹҺѻ˵ƺtvUx^=rZ8hF^oƺë˲ʯɮˮ̩ͭͫͪʨάϮέʫǩ˯ϳʰεε˴̷ѻϹȲʳȮŬũƩƩƦţĢ æǫɯƱǴƴǵȶɵʶʴȲȯǮǮǮɮ˰ͰʦˤˤȤǣƥɩˮͱδηͶ˶˸̹͹͵˱ʲ˲ʴʴȲǮ̳ɯ˱ϴϲɪŧȫ̵̹̹ʷɳɳ˵͵ииϸϵδϲϲдҽͻ͸Ͷ̰ʭͭҲˮʮʲ̶ͽϾκ͹͸ιϺҼӽϷ͵˴ͳϵдϳͳεεδгʭŵmsL~f@iC{Toŵ£ȬͲˮŤ¡·{ŦĥǼȨʨɦ㤦ĨūƩɩʨ˫̪̬˩ʪɧåƦȪȩĥǾȿƩǪĥƽ¹ø¼ý¢ǧȨ˫ǧãããľzuutvŦŪƪƬǭƯ̻ŴsijouŻ¡æ˯ͳʰĭĭ«ǭʮvh\zPtLzR_myź¦ũƬɮǭȮȭĨ{|]vgHhY:iZ;gX9hYpb?vhEzXv£¨­îŰǰȱʳʳ˱ɯǭɯ˱˯Ȯū˱̲ʮçžút\ob@aT2eX8{lMlƼìǰì«éƬƩŨƩƩƧĥëí­í­Įįưíí¬¬ïıƳƴƳȳŷcxhGyfEvcCxeEoSmŹ­ııƳǴȵʴʴʴưȱʱ̳̲ʭèk\~nJ{iE|jDwc>qMxVkzsb|U{W~W_hwtuV|nK~pKn^:oatgD{Ywĥ«îįƱȱɲʳ˴̲ʰȮɯ˯ʮȬūƮʴʲǭĨť¡ƾl|oLh[8gX7teD{]qĭƯ«ëŭƬƫŨŨƨŧĦ§ŭîîîîįįŰî­ííİűdzȴưǰ¶bxeGxeEubBueDnRkĺîĮdzdzɵʶ˵˵ʵ˵ʴ˴˲ʹ̲ʭĩn`qO|jFzhDq]8zhBrPanj^Z^cny}bteFqa=yjCl]6sd=yUm~n}[zjHm];m];iY8hX7pOsðϺʴʴδǮĻytUziKwdDvbAua>u^viF~\|úȩĭŰƱDzɲʳ˴̵̲˱ɯʰʮɭǫū¬DZʲɯɭˮ̭ʦĺ{zXob@hY:m^?}lNz]oƼŬ΍éūǭƬƫŪĩèèĩƮưưůůůůůįîĮůűdzȴɵưƯʿ`vcEubBtaArbA|kOhùîưɵɵ˷̸̶ͷ̷̶̶̵˲˲ɯȫ§masQ}kGxfBmY4tb<}mK{Xccadlrl|mPgX9gW5ueAm]9xhDa²y^xhGgW6eU4cR4eT6yhJi˷ɳȲʰ©usT{jLvcCt`?vb?zfC^{~yzyƾƴʸ˻ͺμλλλͺͺ̹̹ͺͺͺͺͺͺͺͺͺͺͺλͺ̹˸˸˸̹ͺλλλλͺ̺̺ͻμμμμμͻͻ̺̺̺ͻμϽμμμμμμμμͻͻͻͻͻͻͻͻ˸˸˸˸̹̹̹̹̹̹̹ͺͺλλλ̹̹˸˸˸̹ͺλǴȵɶʷʷ˸˸ʷͺ̹ͺͺ̹ʷȵȵɶ˸ͺͺͺ̹˸ʷ˸˸˸ȵɶʷ˸̹̹̹̹ʷ̹λλ̹˸˶̷ϸ˴ɱɱ˱˱Ȯū¦¨Ůɱʴɶɷϼ̹ʵȳɲ˲ʹγǭɭ̲ζϹθι͸ϻϻι̶˳ʲɯǰ˷˷ʶ˵̳ʹγγ̱ʹͷϻϼͻȸñleqɺĥɯ˱ͳ̵ʳʴ̸ϹŬȭ̯ͬ˦Ǡ™žȥͫϮͯ˭ɮɮʯȭƪŨȬͰгϵɲƲĮīĩħȿżĻĻżǾ¥ĩƫɳʴ˷̶˵˲ɳɰ̳̱˰̯˰Ͱ̱Ͱˬ̫̭ͮʫɪʭ̯ɮ˰˰ʱȯȯɰ˲ɮǬè§ɾǼøƷ̾ǫɯ̲̲ʰȰȰȰͶ̵˴ʳʳɲǰǭĺœɤϯгʹɳʷ̹нκ͹̸ιϺѼҽԾҼкθθͷʴȲʲǫæƻq_|UwPwP}XhyƻˮǬȭxke^`fuƸ·p~]xUzW|YcqϽ½¾ʿǼźǼƻvnoja_iv}üǫǪĨǪ̰ϲβ̯˯̯̰ʭũ¥úwl~ax[uXuX}bsƿƪDZ¯οíȱ˳ǰĪȮçɻunhhmuŷȼǼwiZ{TzRZk}ȽƫʱʱDZĮìǭƬǬʭbsbDkZqc@ylI`żɬƯƱDzȳʳ˴̵̵˱˱˱ʰʮȬƪū¬ǴʴȰȮʭʫǦxyWob@gX9hY:raCzkNx\tĺçũĪʰɯȮƫè§èīưDZDZDZưưůůįįưưdzȴɵɵůƯɾ}^taCs`@r_?qa@yhLe¸įDZɵʶ˷̸̶̶˶̶̶˴ȯǮūħȾżi^pN{iEygCo[6xfB}mKzWdhnvľ¡}{^rcFfW8dT2ueAp`<{kGgótxWwgFk[:k[:kZoQnɵȲȰȮɿ{y\~oPrbAp]=wcBlKfó˻Ǹįɷʸɹɶͺͺ̹̹̹˸˸˸̹̹̹̹̹̹̹̹̹̹ͺͺ̹̹˸ʷʷ˸̹ͺͺͺͺͺ̺ͻμμϽϽμμͻͻͻͻͻͻͻμͻͻμμμμϽϽͻͻͻͻͻͻͻͻ˸̹̹̹̹̹̹̹̹̹̹ͺͺλλλͺ̹˸˸˸̹ͺͺŲƳɶ˸̹̹˸ʷ˸̹ͺλ̹ʷɶɶ˸ͺλ̹̹˸ʷʷʷʷʷɶʷ˸̹̹̹̹̹ʷ̹λλͺ˸ͷθ͵ʲŮĭĪžĭʵ̹˸н̹ȵdzʵͶηεǭɯ˴ϷкϹϺ̹λͺ˷ʴɱʲ̵̲ʴʴʴ˵ʲʲɱȰ̴̶˸ͻξ̿ɾŶ˿˽Ǫˮ͵ε˳ɳȲȳ˴ͶƩȪǤ›ĸɪ̮ͯʭɬƩæåŦɧ˨̩̮˴ȴDZǮƫħƽĻżżǾĥǪȭʱ˲˵ʹʹ̱ʱɮ˰̯̯̭̯̭ˮˬɬˬͰͰ̯̯ͰϲƩɬ̯̱˰ɮɮɮɬŨž¹úĻĻ}}üĨɭʮɯɯȮϸ̵ʳɴɴƱ­ƾ~zvtvzȣ̶̱ͭͯθнμͻ̺ͻμѾҿҿнͺ˸ʷɶdzůȬ¥źyavQtMtM{Vfz¹ũβʰũofhkp}ʼͿ˽Ȯʿȿu~]rQ~pM~pM^qƩȪȫɬɬʪȫɩȫɩɬʪȪƦǧɩƦøn__zZvV}]pŻǫжβ˱˯δϳͳ̰̲̰̲˯ȮƪǪŨ|jw]tW{lOykNy[n¥ɰí˿DZ̳ʲȯƮǬūèǻ}jbadkwǷǹŵnc]yV}W_nȽʿŨŪȯɳ˴ʵDzŰȰƬƫȫ̾}_q`BgV8bQ3_N2jY=y`ʻǯdzȳɴɴɴɴȳȳDzDzȳɴɴʵʵ̴ĪƩƩĤwsRj[:n`=oa>qbAylJbúħǰDzȳɴʳ˴̵̵ʰ˱˱˱ɭȬǫƬıǴʴȰȮɭŨɾm}nOj[paDxjM|^tƩƪ˯̯˱ǭë¬ůưDZDZDZưůůįįưDZȴɵɵɵưƯɾ{\r_Aq^>q^>p`?yhLe¸įDZȴȴɵʶʴʴɴɳ̶˴ɰǮūæȾżh{Z{kIweAxfBs_:mItRbr|ſťĥwrWn^DgX;cT3seBn`={mJgi~oPsdEm^?n_@o`CxiLfįʴɳɱȮȾ}{^|mNm]p`?ziMfùįưdzdzȴȴɳȲDzDZθͶ̳˲ɯǪǾiyXwgEuc?xfBvb=rN_rúááƦǧ£v}rVm_DiZ=eV5oa>j[:xiH`pu}^rcDk\=gX;j[>qbErUxDzʴʴʲȰ{z_|mPk\=jY;wdDnNg~Ǹ˾̾ɾƺ¶øȽŰʵ˸˹ʸʸ˸˸ʷʷʷɶɶɶʷʷʷʷʷʷʷʷͺͺͺλͺ̹˸˸ʷ˸̹ͺͺͺ̹̹ͻͻμϽооϽϽͻͻμμμͻ̺˹̺̺ͻμμϽооͻͻͻͻͻͻͻͻͺͺͺͺ̹̹̹̹̹̹̹ͺͺλλλλͺ̹˸˸˸̹̹Ǵȵ˸ͺͺͺ˸ʷɶ̹ͺλͺ˸ʷ˸̹ϼнλͺ̹˸˸˸˸˸̹̹ͺͺͺ̹˸ʷʷ˸˸ɶȵɵ˵θԻ˳ļrkf{\~]bkîɶ̺ȶŵƶ˻λͺʶȰɱ˵ͷ͹ι̹͹͸͸̶ʴǯéȾŻƻºøǽéʰͶιͺʹ˼Ƚ̶ˮʯ̯ͳϲ̲˯ͺ͸˸̷̶ͷ̵ͱǪãȻynlrṶ̃ʬťķƼǻǻɽ˿ʿǼźøƻʿ¤ũƪƩȫʯͰαͮˮɪǨȧɨɨȧȤƥŤ¦éūū禦¤¤Ǽ·ƿľſ¢ƦƥǦǾ|pf`bkq|ȿɮ϶йͶʵ˸̻ʹų{tpiecfnwß̪ϲϹͼ̻˺ͻμоѿͺ˸ʷʷʷʷɶǰͿyi}YmHmHvQ_oøũɯʱɰɭͱ˱ɱɲʲȬätzY}nMxiHvgFxWqˬϲαͳе̲˱ʰɯɯʰ˱̲ʭʫˬɪʿn}[}pN}pN}rR{[rȿƪ˴ηι˴˶Ͷ̷ȱ̷Ͷ͸̵˶ɲɴɲƮʲʳƭĶw|_zkNl_?l_?rgGyUqǫǭȮɯʳ̴˴̶ʵθλ˸ʷʷòû|of`|Z}\bljh~]qPyiGqO^lzʮ˱ȯʱȲȳʷͺνͼ̹ʲȮĩǾuqSkZqa@{jNhùîůdzdzȴȴȲȲƱư̶Ͷʹʹ˱ɬ§ȿdvUvfDvd@|jF|hCyUmøĥǧȨǧɫǩ¥}cwiOpbGk^>ob@gZ:viIyY}]a}oRoaDk]@m_DykPy^pɽDZʵʴɳɱǯky\yjKwhIqPrQct·÷ʿ¬Dzʵʷʷʸ˹̹̹̹̹˸˸ʷʷ̹̹̹̹̹̹̹̹λλλλλͺ̹̹̹ͺλϼϼϼϼλͻμϽϽооϽϽͻͻμϽμͻ˹ʸ̺̺ͻμμϽооͻͻͻͻͻͻͻͻλλλͺͺ̹̹̹̹̹̹ͺͺλλλλͺ̹˸˸˸˸̹Ǵȵʷ˸˸ʷɶȵɶ̹ͺλͺ˸ʷ˸ͺϼѾϼλͺ̹̹̹ͺͺͺͺͺͺͺ˸ʷʷʷʷɶǴƲdz˵ͷʱzy`sgOmaIl`FpgFrjFsjKypSiŽǴ˹ǵĴǷ˻ξ̹ȴɳʲ˵˵͸͸̸̸ιϺθʲé©̲ϸ͸˺˼ɽιϳзҶѸе϶γ˷˷̹ϺϹзͳ˯Ħ|milwɬˮƧƻ~tkjlimxĹȽʿħƩʭͰίͮˬɪŤƥǦȧȤƢšâȽɽ˿ɽƻøǼľãȨ̬˪ʩ̫ģŻoc`[_grƩϴѷͶ˶ͺϾϾʺıƼ}slfdekuğɨϹͼ̻˺ͻμоѿѾϼͺ˸˸ɶǴī|e}YqL}iDqL\sæ˱зϸ̵˵ĭǿžƿ¥ĥǨͮϳͳ˳˳˶̴ˮ£owX{lKxiHwhGwVs´̯ϴα̲е̵˴ʳɲɲʳ˴̵ͱ̯ʭĥ~ctRxkIzmKsSb~¹ȫ˯ηι̷ʵ̷лιȳιιι͸˶ʵʵʵůʴ˶ʳɳƮƷ|b{lOk^>i\xnKfåƬʰͳ͵̴˵̶нҿϽɷȶ˻ɹ­Ť÷vgzXsRuTtSrQoPlKvcBtdBuSiȽéǭ̲ʹɰɳƲƳǴʹ̻̻˸˳ɯèĻn}lNiX:gV8gV8fU9p_Cx_ǸƮȴɴ˶̷̷˶ɴɴȳȳȳȳȳȳȳɳŭǬƬ¦xtUk\;k\;k\;m^=xkIeƽƩƯƱDzȳȱɲɲʳĪǭ˱˱ɭȬʮ̲нʹŲŰɳƫ~c{mPk\?gX;m\BraGwcJxgKoa>sgAviFtgDxkI|Yn|¸éǿŽƾ½ĭƯȱȱǰǰ­ůDZȴȴdzƲȲȱ{\q^@o\qa@}lPjĺ­ĮdzȴȴɵɳȲƱDZɳʳ˲̳˱ȫƽ~^sRwgEygCpLnI\uȩȨǧȨȪɫƨçžrv\{mStfIpeEhZ=tiIrUynP~pSpeIwiNujNtYoǸŬʴ˶ɳȲȰƮƿ{p|_y\}^vWbnxưȲ˶˶ʷɶɷʸλλλͺͺͺ̹̹λλλλλλλλλϼϼϼϼλͺ̹λϼнѾѾѾѾнͻμϽооооϽͻμμϽμͻ˹ʸ̺̺ͻμμϽооͻͻͻͻͻͻͻͻλλλͺͺ̹̹̹̹̹̹ͺͺλλλλλ̹˸˸˸˸˸ȵȵɶɶɶȵǴǴɶͺͺ˸ʷʷ˸̹ͺϼϼ̹̹̹̹̹̹̹̹̹̹̹˸˸ʷʷʷȵ̹̹ȵdzʵ̵ʳ˲īqukRg[CcT=`R8eZ:jb>i`AriLmƳȷɷɹɹɹȸɶɵȲɳʶʶ˸̹κκ͸͸̶Ȱvojhfisídz˺ο˿ɴ̱ζжѹзй϶Ͻ˹Ǵʵ̶εɭŨʿ|sidmŨƩȫɪĹkc[VVXbq|yuv~·ƽƽǾŦƧâȿǾƽƾŽû~ýŧȪɩ˫ήѱͬʩâúq^{T}V|V}Yf~Ⱦȯʴ̷˸̺Ͽ˾ǴŪâļxqg]\kvƮ̹ϼ˸ɶͺκʶȴ͹͸ȳɴ˶ǿ{i~ZtPoKrN]pöŨͱͳ˲ͶϸɳȰƮūũǪʭˮ̯̯̱Ͳ̴˵ʷʴ˰ȿeqR{lMwhGqbAsTqȫͲϵζʹ̷ʵȳDzȳ˶ιѼ̲̰ͰƧmwVqPrcBxkIuUg̰˱͸ͺͺ̹˸ʷɶȵͺ̹˸˸ʷʷ˸˸ƳdzƳȳ̶ʲʻowhKn_@j]=m`>siEyU|Ũγе϶зϹͷѽѽнξ˻ȺŷðɩŠŻxevUlK~gGxdAvbAvcBxeDlK_t÷¨ƮǮʱ˵ʴȳȳȵɶɷʸʺʸ͵ʰƫǾgyhJkZrf@tgDylJxXg|¸ʿ¬¬ĮDZɳʴȲDZîîůưdzdzdzƲĮìyZtaCkX8q^>rbA}lPjĺîĮȴȴȴdzDZDZŰư̶Ͷ˲ʱ˱̯|ZpOyiGzhD|jF~jE_xȽ¥¥ƩȪŨȫɬȮĪ~rgb~bbgiihhrǽ¬ů˵Ȳůíëëé¨}{{|¸ŻǽŮǰʵ̷ͺ̹˸ʷλλλͺͺ̹̹̹λλλλλλλλλλͺ̹̹˸ʷʷ̹ͺλϼнѾннϽϽϽооѿѿѿμϽооϽͻ̺ʸ˹̺ͻμϽоооμμμͻͻ̺̺̺ͺ̹̹˸˸̹̹ͺ˸̹̹ͺͺ̹̹˸˸˸˸˸˸˸˸˸ɶɶɶɶɶɶɶɶ̹ͺ̹ʷʷʷʷ˸̹λλ̹̹̹̹̹̹̹̹̹̹˸˸˸ʷʷʷȵ̹̹ɵȳʵ̵ʳ̳ƭsuiQfW@bR;aQ8cX:g^=f]>sjMuļƳɸɸʺʺɹɷʷʶɵɳʶ̷̹ͺκϻҽѺͷész_rWyiPyiPzjQ{mRv[k~ǻȴͼο̾͸ϷϷиϷ͵ʳɲϾ˸ǴɴʹͳȫĤļulfguũȭǪɬŨ}]yVvPvNyPUarxifa`gs{ú¹¹żȿż¹zttqmd}a}_bflmmqwæǩˮͯͰήαаίͬʩƥʿl~XyQuOsP{Ykư˶˸̺ξξʸѷбϰ˪šº~ob\bjo|ǹɶ˵˸θ˸DZȴ̶̷Ưîh~ZtPqMtP~\oķʿɬгϷεηйʴɳȲȰȮɭ˯̯ͰͰͲʹ̶̶ʷ˵ɮżb{lMufGqbAm^=rSpǪʹϵζʹ̷ʵȳDzȳ˶ιлϷδ˯grQ{lKtdCzkJxXnç̲ʲ̷̹̹̹˸ʷȵǴ˸ʷʷɶʷʷ˸˸ɶɶǴȵ̸˵;w}mSrcFl]>l_=reBsPv¥̱γʹεͷ˵ммѾϿ̾ȺŷïʬȦ¡¸nzYmLzcAw`>t`=vb?zgFsQh~ɽƮǯȯʱʴɳȳȳȵɶɷʸʺʸ̴ʰǬǾfwfHiX:hW9eT6dS7m\@v]Ʒŭȴȳȳȳȳȳɴʵ˶ʵɴȳDzDzDzȲĬǮȮçm|mPn_@iZ9j[:m^?viIcĻŪǰȳɴʵʳʳʳʳ¨ĪéĨɯƱȵdzưĬĸewhIpa@iX:hW9s`BkPtYy[pNwiBqc>rd?tfCuhE|oMxXep~Ż©¬ůĮůưDZȲȲȲîŰDZɳɵɵɵȴůĭyZtaClY9q^>p`?{jNh¸¬ƲƲƲƲưưƱDZ˵Ͷ˲ɰʰʭɿ~{Y~nMxhFygC{iE}iD_yƽȿƿĦƩħȫɬʰȮɾĮȲ˵η˵ȲůĮŭŭĪé©ȾĸĺƼȾɿɿƯǰʵ̷ͺ̹˸ʷλλλͺͺ̹̹̹λλλλλλλλλλͺ̹̹˸ʷʷ˸̹ͺλϼϼϼϼϽϽϽооѿѿѿϽϽооϽͻ̺˹˹̺ͻμϽооϽμμμͻͻ̺̺̺ͺ̹̹˸˸̹̹ͺ˸̹̹ͺͺ̹̹˸˸˸˸˸˸˸˸˸ɶɶɶɶɶɶɶɶ̹̹̹ɶɶɶɶʷ˸̹ͺ̹̹̹̹̹̹̹̹̹˸˸˸ʷʷʷʷȵ̹ͺʶɴ˶͵˳ʹȯwwhQdT=aO9bP8`R5aV8`U7shLzʿIJ˺˺ʹʹʸʷɶʶʶ˶̷̷̹κκϻιͶɳ{_seJm]CtbJvdLxgMxhN|lRx[ky©ɵλ̻̼̹ϺкϺϺ͸˶dzƲн˸dzɳͳαƦſ|khksŻȩ˯˯ȫȫsxVtQsPxRZbrv^uR|oLzmKsQ}]j}úúúĻǾȿǾǾƽ¹tmlf{^~qQvhKtgGxkK|oOrRyYdn}ǹĦȫˮδѴгΰ˯ˮˮϯбϰͮģezU|nKxkHzmKtTi~ʿ˶̹̹ͻϽλйҶӷҵίģob^^`hy;Ůɴ˳ȲĬDZ̳˵īļrxWqMmIrN^pµǧɫͰгиѸзϹ˵˵̶͵ͳαͱͱββεεͷ͹̺˷ȭ¹|{]ufGn_@k\=hY:qRpǪ̳ͳ͵̶˶ʵȳȳȳ˶͸ϺҼϷɯ¸`|lKvfDxeD~nM|]uƬ̴ʴ˶˶˶˶ʵɴȳDzȳȳȳȳɴʵ˶ͷ̸̷ɵȵ̸ͷìƸtZvgJn_@l]viIdĻĩȱɴʵʵ˴˴ʳʳƿ¨é¨ũɯïƲȴư˿qvVm^=j\9hX7kX8q\=zeHoRvX}mKwgCtd@xhD|lJ|nK~oNrP|oOwZerƺĭůîĮîĮįưįŰɳ˵̸̸ʶɵưŮ~yZubDmZ:o\yhLeŽſïİİűưưƱDZ̶Ͷ˲ʱʰɬȾ|yW}mLwgExfB{iE}iD^|ŻŻ»ŪèŪƫȰȱưí˿ȼ廼Ľƾ«ǰʵ˶̷ιϺ˵ɳưưǯǯƯƬǭƪ§ɿǽƽǻǽ§ĪūƬǭǭƯƯDzȳʵ̷ͺ̹˸˸λλλͺͺ̹̹̹λλλλλλλλλλͺ̹̹˸ʷʷʷ˸̹λλϼϼλϽϽϽооѿѿѿϽϽоϽμͻ̺˹˹̺ͻμϽϽϽϽμμμͻͻ̺̺̺̹̹˸˸˸˸̹̹̹̹ͺͺͺͺ̹̹˸˸˸˸˸˸˸˸ʷʷʷʷʷʷʷʷ̹˸˸ɶȵȵȵɶɶ˸˸˸˸˸˸˸˸˸˸˸˸˸ʷʷʷɶɶȵ̹ͺ˷ʵ̷͵˳δʱ~|mVeS=bM8bP8\N3\Q5[P4pdJ}IJ̻̻˺̹˸˵˵ʴʶ˶̷̷͹͹κκɴɳǯ}v[qbEm\@t`GwcKyeLveKtcGxiLsV}`|ǽĭȳɶ˷͸ιϺлϺι̸ʶͻ˷ʴ̴гʭſtilvŹƢͰͳ˯ȭǪĽqyYwUzXalxȾŪͿɻö~]{nKuhEreCviGsSe¥ħƧǨǨƧģż~vvobuSviGqdDreEtgGuhHwWiyõ¥ɭūʯ͵ϵδˮȮǫƪˮαбгˬƽu]oMxjGteDreEuXpùɲιͷͺϼѾʵ˴δϵγʭæǾthb[]ezǹĪūĪɮͲ˰wesRoKpL|Zmķ¢вϲ̰̰ϵҷиз˷͹λѻѹжϵͱϳϳ϶θκ͹̸̺ɰ{xZrcFiZ;hY:gX9qRsǬ˲̴̶̶˶ʵɴȳɴʵ̷ιкϹȰ{}^zjIubAyfEoNa|Ȱ̶ɳʴʵʵʵɴȳDzDzŰƱƱDzȳʵ˶ͷϻϺ˷ʴ̷θƯ̽w]wgMn_Bk\=m^=uhHi½ɯ̲ʲ˳̶̶θλноμɷųḭ̈ΰΰʪź~amLqZ:nZ7mY8p]<{kJa~§ȯʴȲưɳ˶ʵɴɴɶɶɶɶʶʶɱɯǬǾ}`p_AbQ3gV8dS5dS7o^B{bƷªűƱŰƱƱȳɴʵʵɴȳDzȳɴ̷θȲɳǰivgJhY:gX9hY:l]>xkKhƽĩȱȳɴʵʳɲɲȱ¨éĪé¦çǫʰíưDZī`xkIl^;m]9n[:lY8mY8r]>zeFlMxeDvfB|lHwS`ca}\qRtTwW{^j{Ǽǿ­­îŰɳ˵̸˷ʶȴDZŮ}xYubDn[;n[;m]xjMdūͶͷθѻѾͺ̹̹͸ͷεʲɭèƸ}naZ[hr}Ƹ¦ɭ˯̰ȫĻygwYqPtRzXh}¢ȪдͰɭɭͱҵжϷ̸κѽӽӻҸжδжжϸϸκͻ˻˷˲xvXqbEhYl]>teHh»ʰͳ̲̲ͶηͶιϺϼͺɶƳĮ˱Ͳ̰Ǫ¹`lKnZ9mY8lW8n[;{jLg·ƭȯǮǰ˴̵˴ʲʲʴʴʴʴʴʴȰȮƫży\m\>aP2eT6dS5fU9tcGhɺªűƱƱƱƱDzȳɴʵɴɴɴɴ˶͸ϹɳɳǰƿgteHfW:hY:iZ;m^AzlOnɿŬǰDzȳȳȱǰƯƯ¨¨¨¦ĨǫʮƭƭéǼnsRseBueAuf?tb>o]9lX5mY8t`?{gFzgFoN~]n}zq_{ZtSsSxXewʽ®¯̿îįȲɳʶɵdzƲưìɾyvWs`BmZ:lY9k[:udH{`~º¼ïĮưƱȲθϸεʹͳˮȾzxV|lKvfDygC|jFkFcǽ»ƭƭ©¬­įűƲʳʲɱɱɱ˳ͷкʴʷʷȵǴǴɷ̹˵ʴɱɱ˳˳ɱȰ¨ĪūƬŮǯɱ˳͵ϷϹϹɴɴʵ˶̹ͺͺͺλλλͺͺ̹̹̹ͺͺͺͺͺͺͺͺͺͺ̹̹̹̹˸˸ͺλϼнѾѾѾѾϽϽϽооѿѿѿооϽμͻͻ̺̺̺ͻμϽϽϽμμμμμͻͻ̺̺̺̹˸ʷʷʷʷ˸̹̹ͺλλλλͺ̹̹̹̹̹̹̹̹̹˸˸˸˸˸˸˸˸̹ɶɶʷɶȵȵȵɶɶʷʷʷʷʷʷʷʷʷʷʷʷʷɶɶɶȵǴͺϼι̷θε̳˱ɰmvaLgQ|hG{Zfwŵɹsi}\vStRyVetö˾¯¯ŰŰưưƲűűİĮźurSq^@kX8lY9jZ9tcGz_~ļſ®ïůưƱDZ˵ηʹʹͳˮȾxuSzjIueCxfB{iEkFcž»ǮȯŬŬĮĮįįïįǰʲ˳˳ʲʲͷϹͺͺ̹ɶƴųǵɶ˵ʴɱʲ˳˳ȰƮǿĪƬȮʲ˳̴͵ζζͷͷʵʵʵ˶̹ͺͺλλλλͺͺ̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹λϼнѾҿҿҿҿϽϽϽооѿѿѿѿоϽμͻ̺ͻͻͻͻμϽϽμμͻμμμͻͻ̺̺̺˸˸ʷʷʷʷ˸˸ͺͺλλλλͺͺͺͺͺͺͺͺͺͺ̹̹̹̹̹̹̹̹̹ɶȵ˸ʷɶɶɶɶʷʷʷʷʷʷʷʷʷʷʷʷʷɶɶɶȵȵǴͺϼϺ͸θε˲ȮǮë{lWnUAeO:`P9bS>[L7`Q>raŵ̺˹˸ʵɲɯȬǭɲɵɵɵɷɷɵɵʵȲƮƼmwXnPzdL~eO~hPmRqSoO{kJvhEuhEvlH{qM{Wey¬įDZDzDzƱƱíǮƬɾzmgnwşɥ˩̫̭̯̰β˰ĨƼ÷ŵ̽¥Ǫγδ˱˰ɬǪáyVrf@nbkZ@qaHf¸ʱε̳̳϶Ѹ϶϶ʹ˲ɳɰDZȰɳȱǬç`|iInY:oZ;n[;o_>pQqúɿêƭǮ˱жж̲˳˳̳˲˲ʱʱʱūƬ§{tWl[=dS5aP2cR4l[?pTyóìƮʶʵɴȳDzDzDzDzɴʵ̷ιι͸˶˵ɳȲŮüdrbHdU8fW8gX9l]@|nQrŬǰȳȳȳǰǰƯŮƿžĽžçũƪƫȾf|oMtfAwiB{lEwh?sa9o]5r^9zfAqLyVrʷѾ̼Ĵ}oc~[yT\hw³Ǹ˼ıɴȳDZůİïïݬƾq}nOn[=hU5lY9jZ9tcG{`ǿ¬®®ïİůưƱDZȲʳʱ˲˱ʭǽtrPwgFrb@uc?ygC}iDaĽ»ǮǮǰȱȲɳǴŲ°­«ƮɳʴȲDZǴɶ˸˸˹ɷǵƴȶɶ̶˳ʲʲ˳ʲǯĬſý¼ļǿéƬ˳̶̶ͷ̶̶˵ʴ˵˵ʵ˶̷͸λλλλλͺͺ̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹ͺλϼѾѾҿҿѾϽϽϽооѿѿѿѿоϽͻ̺̺ͻͻͻμμϽϽμμͻμμμͻͻ̺̺̺˸ʷʷɶɶʷʷ˸ͺλλϼϼλλͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺ̹ɶȵ˸˸ʷɶɶʷʷ˸ʷ˵ʷ˵ʷ˵ʷ˵ʷ˵ʷʴɶɳȵȵǴͺѽϺ͸Ϸ͵ʲƭƭĬ·t^nXCdO:\K7_P;VG4VG4qaQt˹̷˶˳ɯȮȫǪȱȳȳȳȴȴdzDzȳDZɱƬuoppptupg~]~qOzpMxnKyqMyXh{źǿìŮƯǰéūçleczļƢ˩άήϰ̰ͱϳѵγǭê¨˿Ϳéǭǭé¨Ƭͳ˱˱ɭƨwUreCob@m`>l_=qdDvVwŬʱ̲˱ʰʰ̰Ȭƪç¦ħǪȫȩĺg|oLi\9dW5i\m^Al^AujNgɯѹӻн̽˿;оͺλкͶʯæȹmbxYtUtTxVbpokcyYwlLshHynNxXmȽũαϴ̱̰ͱϵѵеϴγδ̶͹ͺθϹϹζζζϷι͸˷ʸȶȴiykNiZ=cT5iZ;sdE|_ϴδʲʴ˵ʲɲɲɲɲɲɲɲƳï{y_}lN{fGwcBvbAzgG|`Ǽʴ˸˷˳˱̲ͳͳͳ̲̲ɯɯɯɯʰ˱̲͵ϸѹηʱɱDZªɺqYp^FgU=gU=jX@o_F~dɰε˲˲϶Ѹз϶̳ʱɰȯȯʲȲȱƬĨbyhJlY;o\ykDtM~oHxiBwgC}mI{Ygrʹ;ħ£˼;wh}[|Yagtĵ˼«̳˱ǭĬ¬¬įŰýn|kMlY9gT4kX8lY9tcE{^ĮíïİűůưƱDZĮưDZɰ˰ɬüo}nMvcBq^=sa=weAxhD_»ƭƭĮưɳʴɴDzñƮʴ˵ʴȲǴɶɶʷ˸˸˹˸μн̶˳ʲʲ˳ɱŭªëľ¼ļǿìǯDZȲɳʴ˵˵ʴʴ̶̶˶˶̷͸λϼλλλͺͺ̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹̹ͺλϼнѾннϽϽϽооѿѿѿоϽͻ̺̺ͻμμμϽϽϽμͻͻμμμͻͻ̺̺̺˸ʷʷɶɶʷʷ˸ͺλλϼϼλλͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺ̹ʷɶ̹̹̹˸˸ʷʷʷ̶͵θ͵˵ʲ˵̴˵˳˵ʲʴɱɳɳɳͶϸηϸѺͶƭëŻt]gW>_O8_N:eTB]L:UF3eVCr_~Ȱ̴ͲͲϴвͯ˲ʳɲȱȱȱȱȱʳʳȲůvl~`|qSshJtiKtVdoɿǮȮūǨĥźq~aerĶƩϳϲ̯̮αеѸѸ϶̴ʲɲ˱˴̲˱ʰɯɯȮȮ̲̲δʰ§ǻ~pUo_En_BsdGsdGpaDwZìǰʲʲ˳ʹγɮ̱ʰƬĪƬǭĩr{\rcFhYfW8sdGu[|¶ʴҽѻϼͺλѾԿҾѽκ˵̶ʹȯɿ¶w}cqWzlOwjJwjJzmMtTtUpQufIn]CsbHqYh}©˲̳δδδжҸԽջӹѷжйηϹιζͶ̵̵˴̵ηйѺй͸ɴǮȽ~^whIl]>gX7k\;teD|\Ĩеϵ϶ͷ˵̴ʲɱʲ̴͵̴ʲƴqmUxdIyaEw`Ar[fU9dS7dS7m]Cx^ȺȰ˵̷ͷͷʴȲȲʴ̶ͷθϹϹθͷ˵ɳɶɳƱȼ|bt`GkV;jU8fS5iX:{lMnƿɬ˳ͷϹͷɱƮǯȰŬêèƫȭǫпwdxT}mI{lE|nGzlEvP\dmxǻɽ˿ɿȼȾ©ƭŬɿ{ph}]|[csʻŢʦάͯǪ©ëŭ­ĿnnPq\=hT3kW6jW6s`@y[~íůDZȲɱɳŲŲ˵ʱèħýf{hGu^>mY8lY8n`=reB_ªýƮȱƯìƾDZȲʴ̶ͷͷͷ̶ɳɳɳʴʷ̶̹θͷθϹϹͷʴưĮľý¼ľíDZȵɶ˸̹̹˸ʷɶɶɶʷ˸̹ͺλʷ˸ͺλϼλͺ̹̹ͺͺͺͺͺͺͺλͺ˸ʷʷ˸̹ͺ̹̹̹̹̹ͺϼϼооооооооооϽμͻͻͻͻϽϽϽϽϽϽϽϽͻ̺̺μϽϽ̺ʸ̹˸˸ʷʷ˸˸̹ͺλλϼϼλλͺнϼλͺͺͺλλ˸˸̹λλλλλͺ˸ʷͺͺͺ̹̹̹˸̶̶͵ζ͵˳ʲ˳̴˳˳˳ʲʲɱɱʱʳϷϸηϸк̴ŭĭȾ}qYcW=]N7ZI5bO>\K9WF4^Oj[>m_By^zêĭƱʵ͸ϺϺϺι͸̷ʵʵ˶˶ɴȳưƭ˿r{lOiXbR8cS9`P7rbIiǭ˳̳ǮǮʴ̶θϹϹθ̶ʴϹϹϹθͷʴȲŮɭægyhJo^@n]?l[=eT8xiLqɽįʷҿлѼӾԿԿӾҽѼ̷˶̷лӾӾιȲDZʻy{`rdGgZ:fY9gX9j[mV7mV6kT5iT7zfMsƴʷθ϶϶εʹ˲ɰȯɰɰɰɰȯȯǮǰƱŰįĮǰǮȼmudHsbFraEk[Ao_E{kRpŽʳ̵ʳ˴Ͷηϸη˴ɲȱȱȱʲůê«ũƺ~axiLdU6j[^M;_N<^Om\>n]?fU9teHiɽŰ˸ҿϺллллϺ͸̷лϺϺѼӾѼ͸ȲͷƯʼkvhKh[;fY9gX9iZ;o^BxgKv^uóɾìƯȱ̵ѺѺͶϸйѺѺϸηηйййййййййѺѺѺйη˴ǰŬĦb|mLpa@iZ9m^=ufEaʿ˯˱εйϸ˳ʲʲ̴ϷѹѹиǴŰx^sbDnY:nZ9iT5gT4sbFjǿǴ˷ззз϶̳ʱǮƭʱɰɰɰȯȯȯǮƯĭëɯɮȺiudHudHsbFk[AtdJv]{ʳ˴ʳ˴ͶͶ̵˴ʳɲȱȱȱʲůê«ũŹ{^ufIdU6iZ;dU8gYtlGhļļûǿ««ĭŮƯŮ««ĮůDZɳ˵̶ͷθͷͷθθθͷ̶̶ʴʴʴɳȲưĮí¬¬ĮůıǴ̹ϼнλʷȵ˸˸˸˸˸̹ͺλ̹̹ͺͺλλλλλλλλͺͺͺͺλλλλλλϼϼҿѾнϼλλλλоооϽϽϽϽμͻͻͻͻμϽооϽϽϽϽϽϽϽϽμͻ̺ͻϽоϽͻͺ̹˸˸˸˸̹ͺͺλλϼϼλλͺλλͺ̹˸˸̹̹λλϼϼнϼϼλ̹λͺ̹̹̹̹˸˸ʷʷ̶ͷθͷ˵ʴ˵̶˵˵˵˵˵˵˵̶Ͷηη˴̳εʲĬ΍ĺipbH\N4WH1dS?_NpaDza˿ɴ˸λлллϺϺι͸̷ѼѼѼѼѼϺ̷Ȳ̶˳ūu|nQl]>iZ;k\=k\=p_C}lPh˻íǰʳ˴ϸһйʳ̵ͶϸϸηηйһйййййѺѺѺѺѺѺϸͶʳȱƭȪepOsdCl]zkNgȾŮʳɲʳ̵η̵ʳʳɲȱȱȱȱɱDZī«Ĩ¶x[rcFeV7hY:aR5dV9sXy©ǰDzɴʵ˶˶̷̷͸˶ʵ˶͸ι͸̵ȯǬiteFfU7fV5fV5fV5o`Az]øèūȱʴʴɳȲɳ̶ϹѻϹͷ˵ɳɳʴʴȰɯê|z^vcEhU5lW8lY8tdC}[{Ȩΰ϶˵DZĮªªªªɿĩèk{UzhBpa:sd=rKasƽèŮŰŲƳ­­îįƱƱƱƱǿǿǿŽĹ}usrrqxƲȴɳȰɽjnMu^{mF~Xn~úɿêįıųƴ¯­įDzȳDzƱǿƾƾǿƾƻɽɾǹ}ıDZDZǰʾimLw_=r[9lY8l[=xiJg»ľ¬íĮůƮǯǯưŲ¯ŲǮũƧy|\wcBv]>r[;n[:qc@xpKoºǿ«ĭŮǰƯŮĭŮǰưưưưȲɳ˵̶ʴ˵ͷθθͷ̶˵ɳȲȲDZưůĮĮ¬¬¬íííííƳȵ̹ϼнλ̹ʷͺͺ̹̹̹̹ͺλͺ̹̹̹̹ͺλϼѾннϼλλͺͺ˸̹ͺλϼϼϼλнϼλλλλλϼооϽϽμͻͻͻμμͻͻͻμϽϽϽϽϽϽϽϽϽϽ˹ʸȶɷ˹ͻͻͻͺͺ̹̹̹̹ͺͺͺλλϼϼλλͺͺ̹˸ʷʷʷʷ˸ͺͺλλͺͺ̹˸˸ͺͺͺͺ̹̹̹˸˸˸̶ͷθͷ˵ʴ˵̶˵˵˵̶̶ͷͷͷε϶ʹʱɱ˳ɯĪê{tYk]BdT:cS:`O;gVBdU@_P;_P;XI4QE-WK3e[BunRkŧʬǩȫʭͰϲее϶ε˲̶̳ͷ̶ɳDZůŬǮȰȰȮȮɯɯ̰ʮɬ̯̯Ũt_{kJp`?o_>o_>l]hY8m^=ufEa~Ǽɭ̲϶ϸ˴˳ʲɱʲ͵ζ͵̴ͳʰǫȿe}mLrb@jZ8hX6iZ9tgG_çȮʰ̲ͳͳ˱ȮƬȮȮȮȮǭǭƬƬƫŨħå£ƻs_tfCwhGzkLsVnĸǮɲȱǰʳϸйͶɲɲɲȱǰƯŮŭůŽž}z]vgJgX9hY:^O2aS6rWzīǰȳɴʵ̷ιлѼ͸̷ʵʵ˶˶ɴȱǫƩȿcpcAfW6j\9fX5gY6rcB_ƽȫȬ˳˵˵ɳDZDZʴ̶˵˵̶̶̶˵˵ʲʮˮ¥r}\|lKp`>tdByiGwVoʪʬ˲ȲůůƮƮĬ¨ūŬ©ƼivS|nGykDsK[mźƽ«î¯°°ð­­įDzȳŰîǿǿŽûĩĬɽǽĸ·ƾ¯ŰƱŰƭ˽fmLx`>t];n[;o`A~oRož®ïĮůƮƮƬŮŲıǮũƧw{[vbAw^?qZ:mZ9pb?woJoŽƾŽ«ĭŮĭĭŮƯDZDZưưưDZɳʴȲɳ˵ͷθͷ̶˵ɳɳȲDZưůůůĮĮůůůůĮĮǴɶ̹λϼλͺ˸λͺͺ̹̹̹ͺλͺͺ̹˸̹ͺλϼϼϼλͺ̹̹˸˸ɶʷ̹ͺλλλͺλͺͺͺͺλλϼооϽϽμͻ̺̺ϽϽμͻͻͻμμϽϽϽϽϽϽϽϽ̺ʸɷɷ̺μϽμλͺͺ̹̹ͺͺλͺλλϼϼλλͺ̹˸˸ʷɶɶʷʷλλϼϼλͺ̹̹˸̹̹λλλͺͺ̶̹̹̹ͷθͷ˵ʴ˵̶˵˵˵̶̶ͷͷͷε϶ʹɰɱ˱ɯĪŪu|nSj\AgW=jZAcSiX:kZ>}lPqǸ˿Įʳ˴ɲȱ̵ййһһйͶʳȱʳ˴ͶηηϸϸййййϸηηͶ̵˴ʱƻ}{ZufEj[:eV5iZ9rcBa~Ǽɭͳзηɲ̴ʲɱʲ̴͵̴ʲɯǭȬƩw|[~pMhX6fX5fW6l_=tTrǾƪȮ˱ͳͳ̲ʰȮǭǭǭƬƬūūƩǪȩŧ¢Ǿ}k]ugD~oNzZhʰɲȱƯʳйѺηɲɲɲȱǰŮĭĬí¾º»~}`zkNhY:hY:^O2aS6sX{Ŭǰȳɴʵ͸ϺѼӾι̷ʵʵʵʵȳƯũŨǾbpcAgX7k]:k]:m_t`=q`BseHuZwëĮþ®ïĮůƮƮƬŮƳ¯ıǮũŦuyYua@v]>pY9kX7n`=vnInļļººŽŽûļǿ«ĭìììĭȲDZưůůưȲɳưȲʴͷθͷ̶˵ʴɳȲDZưưưưưưDZDZDZưưůȵʷ˸ͺλλͺͺλλͺ̹̹̹ͺͺλͺ̹˸̹ͺϼнͺͺ̹˸ʷɶɶȵȵɶ˸ͺλλͺͺ̹̹̹̹̹ͺϼϼооϽμͻͻ̺˹ооϽμͻͻͻͻϽϽϽϽϽϽϽϽͻ˹ʸʸͻϽооλͺͺ̹̹ͺͺλͺλλϼϼλλͺ̹˸ʷɶɶɶɶʷнннннϼͺͺ˸λλͺͺλϼϼϼλλͺͺͺͺͺͺͺͺ̹̹̹˸˸ʷʷ˵ʱȯ˰еͳƬƬ̰˰ɽgxhNm]CiY?fV=l]Fvaqi}nWpaJbV>XL2VJ0]Q7dY=ofIfĻž½æ̯ггͰ˱˱˱̲͵͵̶˵ɳȲȯȯȯɰʯ˰̲ͳ˯˯̯ͰαϲϲϱȩŤ{ZxhGvfE|lKxW}oTx]sè̱γдʰʰɲ˳˳̴̴͵̴͵ζζζζ͵̴˳ɱǯƮŭŭƮǮŬ̳īʲƬvvgPfV=`P7bR8cS9dT:}mS{êƯɱʲɰɰ̳϶ҹ̳ȯɰ˳ɱ¸wseJdT:cS9aQ7[K1]M4oVt˳ʰɱ̳̳зҼѻкϹθθθθθθθϹ̶ȲDZȱɭǹcoQxgItcEraCm\@x[¶ůιϼ̹ҽѼлϺϺϺϺϺϺѼӾԿҽллйζδ˯Ǿ~aufGm\>lY;kX:m\@|kOq˼ůƯǰȱʳ̵ηϸֿԽѺϸ̵˴˴˴һѺйϸϸϸϸϸηййͶͶϸ̵ƭĹvpOo`?iZ9dU4fW6l]<`̰˱ʹηͶȰȰȰɱʲ˳̴͵ϴ̱ʭȫo|ZvhEi\9eX6m`>xkIzZvç˯ͱͱϳͱɭȬǫǫȬɭɭƪŨɬƨƿviarOwT_p˿ƫδɲƯȱͶйη˴ȱǰŮĭ«««ëſļž|z]whKgX9fW8aR5dV9~pU}¸êƯɴɴDzɴϺҽл͸˶ɴɴʵ˶ʵɲū¥¹_pcAgX7k\;m^=sdCuUnͰжζͷ̶˵ʴʴʴʴɳɳʴ̶θθ˵ȰǫƩu^uSrPwV^pƽɬˮɬƮĮííŭǯǯƮĪƬīƼ{iz[wR{T`s¹¥ūĭ«¯IJIJŲƱDzȳDzƱįîîî­ǿûºûìŰŮŰŮDzɲɷɷȶȵŲ¯ǿŽíªŷb}iHvbAvcBufI}rVkƮư®îįíĮĬŭūŮŲƳȵǮũɪryYq]aQ8cS9cS9dT:oU~īƯʲʲʱ˲ʹ϶з̳ɰʱ˳ȰƾvrdIcS9`P6^N4YI/ZJ1zjQu̴˱ʲ̳ʹҹѻкϹθθθθθϹϹϹθ̶ɳȲȱɭĶz|]yhJp_Am\>l[=tcGdȼȲϺнλҽҽѼлϺϺллϺѼӾӾѼлϺйζδͰbveGm\>lY;kX:l[?{lOqʾ©Ưǰǰȱʳ̵ηϸվӼѺηͶ̵̵̵ѺѺѺѺййϸϸηйϸ̵Ͷη˴Ŭøt~oNo`?j[:fW6iZ9pa@d¦̰˱ʹͶ̵ɱɱɱɱʲ˳̴͵ϴͲ˱̯ǪlxVylJqdBsfDxkItRiĨǫʮʮǫǫǫƪǫȬǫĨ¦žynhdcjwȭ̳Ͷʳǰȱ̵ϸ̵ɲȱǰƯĭì««ë¾ļĽ{x[ufIgX9fW8bS6eW:qV~©ƯɴɴDzɴιлϺ˶ʵɴɴʵʵɴǰéĽzy[k`@h[;o`?teD{nN`zħʮ˱͵̶˵ʴʴʴ˵˵ɳɳʴ̶θθ̶ɱǫƩ{h`^isæʯ̰ʰƮĮ¬¬ĬƮƮƮūƬīƼtkgkuƩȬŮĭî­¯ðıŲDzDzɴɴɴDzƱŰîįîļºļǿ­îîîįƱǴǴǴŲðƾŽīɽ{{]|lK{hH|kMsVf|ªɳdzïîįíĮŭŭƬŮŲŲȵǮũȩqsSmY8pW8pY9lY8k]:umHrƾŮƯļļƾǿìŮǰȱȱȱȲDZưůĮĮůưȲʴ̶θϹͷ˵ʴůưDZDZDZDZưưȲDZDZDZDZDZȲȲɶʷ̹ͺλͺ̹˸λͺͺͺ̹̹̹˸ʷʷ˸˸̹ͺλλ̹̹̹ͺͺͺͺͺʷʷ˸˸̹̹ͺͺ˸˸˸̹̹ͺͺͺͻͻͻͻͻͻͻͻͻμμμͻͻ̺˹ͻͻͻμμμϽϽͻͻͻͻͻͻͻͻλͺͺ̹̹ͺͺλλλλͺͺ̹̹̹˸˸˸ʷʷʷɶɶ̹̹̹̹̹˸ʷʷʷλλλλλλλλͺͺͻͻͻͻͻͻͻͻ̺̺̺̺˹˹˹˸ζ˱ɯ˱ʮȬǬʯusXn_BhY{lOqūǰǰǰȱʳ̵ηϸһѺйϸηͶηηйѺһӼһѺйϸͶϸη˴̵Ͷʳīq{lKo`?k\;gX7k\;teDi¦̰ʰ̵̳ʳɱɱɱɱʲ˳̴͵϶γϴгϲħsdyYtT~qO~qO|ZjùȾȾɿǽƼǽǽƼĺxpmoq~ʼū̳϶йη˴ȱɲ̵ͶʳǰȱȱǰƯŮììë¬ÿļüwuXrcFgX9fW8bS6fX;sXŮȳɴƱȳ͸Ϻ͸ɴȳȳɴʵʵDzĭwy]rgItgG|oOvV`sżǫɭƬ˳˵˵ʴ˵̶̶ͷȲȲȲ˵ͷθͷ̴ǭǪ¥zww¥ǬʱɳʳDZĮªŭƮǯƬƬêǽƩɯ̴ƮůĮĮĮĮůưȳȳɴʵʵȳDzƱŰŰįŽººĽııııŲƳƳDZưůíǿ©÷s|auXw[}`l{ŽǯɳűîîîíĮŭŮŬƭƳŲǴǮĨƧpoOkW6pW8qZ:o\;pb?xpKrüȱȱìǿ««ĭǰȱɲȱȱɳȲDZDZưDZDZȲɳ˵̶θͷ˵ɳDZíĮůưDZDZDZDZȲDZDZưưưDZDZȵɶ˸ͺͺͺ̹˸ͺ̹̹̹˸˸˸ʷʷʷ˸˸̹ͺͺͺͺͺͺλϼϼнн̹̹̹̹ͺͺͺͺ˸˸˸̹̹ͺͺͺͻͻͻͻͻͻͻͻμͻͻͻ̺̺̺˹ͻͻͻͻμμμϽͻͻͻͻͻͻͻͻλͺͺ̹̹ͺͺλλλλͺͺ̹̹̹˸˸˸˸ʷʷʷɶʷʷʷʷɶɶȵȵʷϼϼλλͺͺͺͺͺͺμμͻͻͻͻ̺̺̺̺̺̺̺̺̺˸и̲ɯɯ˯˯ɮȭ~ctfKgX;gX;wfJw]|ʻʻzy_seKeW<]O4ZL1]O4^P3aS6pbEvX|ΰαɬǪǪδδϷζ͵˳Ȱǯȯȯȯɰʯ˰Ͳγ̯ˮʭɬȫȫǪǪƪħhsV{lMwhI{lM{mSsYnũϲ˱ȭǮȯʱ˲ʴɳƱŰɱʲʲ˳˳ʲʲɱ˳ʲʲɱȰǯǯȯŬ˲êȰérqbKiY@cS:cS9bR8gW=x^Źƭȱ˳͵϶϶϶϶̳˲˲̴̳ǯļwxjOhX>_O5^N4^N4]M4tdKhëʰ˳ʱǮ̳θͷͷ̶ͷͷθϹкѻϹͷθкθʳȽipQraCn]?tcE{jLe̼˳̶͸λϼҽҽѼѼлѼѼҽѼӾԿҽлϺϺѹ϶ѵϲ¥cveGkZ{lOq¨Ƭǰǰȱɲʳ̵ͶηϸϸϸϸϸϸϸϸϸѺӼԽԽһйϸ̵ηͶ˴˴ͶɲênyjIn_>j[:eV5j[:ufEkɭȮ˲̵ʳʲʲʲʲʲ˳̴͵϶϶зѶж̯çżsha{Y~]fx{y|³Ǹǫ˲Ͷηηη̵ʳʳ˴˴ȱƯɲɲȱǰƯŮĭĬíÿûtqTn_BgX9fW8cT7i[>v[¸ɿŮȳɴƱȳ̷͸ʵDzDzDzɴʵɴŰ­m|`~ahqyĨʰɯǯʴʴʴʴ˵ͷθϹɳȲDZɳ̶θθζɰɮŪǾǾǪʯȯůƲɶȲĮþĬǯȰǭǭīɿ¸¢ŧŨĪƮɳưưůůůŭƮƮDzȳɴʵɴȳƱŰƱƱƱîǿûºº»ƿŲȵɶȵȵɶʵʳʳȲȲǯƮŮɿwnntyºŭưĭ­­îîíĮĭƬƫǮȵıƳǮĨänpPmY8qX9r[;r_>wiFxSyìȱȱĭ«ĭìĭƯȱɲɲɲȱɳȲȲȲȲɳɳʴ˵˵̶̶˵ɳưů¬íůưDZDZDZDZưůĮĮĮůưǴȵʷ̹̹̹˸˸̹̹˸˸˸ʷʷʷʷʷʷ˸˸̹̹ͺ˸̹ͺλϼннѾλλͺͺͺͺͺͺ˸˸˸̹̹ͺͺͺ̺̺ͻͻͻͻμμμͻͻ̺̺˹̺̺̺̺ͻͻͻμμμͻͻͻͻͻͻͻͻλͺͺ̹̹ͺͺλλλλͺͺ̹̹̹̹̹˸˸˸ʷʷʷ˸ʷʷɶȵȵȵɶʷϼϼϼλͺ̹˸̹̹ͺμμμͻͻ̺̺̺̺̺̺̺̺̺̺ͺϷ̲ʭʭ̰ͱȫ¥n{lOj[>dS7iXeW=qcIeĪδͲʮȯȯȲɳɳȲƱŰɱɱʲʲʲʲɱɱ˳˳˳ʲɱȰȰȯŬ˲©ǯ¨po`IhX?cS:cS9aQ7hX>}cȼǮȱ˳͵зз϶εʱʱ˲ʹ̴ǯŽz}oTl\B`P6_O5aQ7^N5p`Gs\ĸƬζ̳Ǯɰ̶̶˵˵̶ͷθϹθѻкͷϹҼϹǰ}~_{lMraCsbD}lNxZvŬϷͷ̷ͺϼѼѼлллѼҽҽӾӾԿҽϺιϺѹ϶дͰ}`tcEkX:gT6gT6jY=ykNréȮȱȱȱɲʳ˴ͶηηηηϸййййηйһӼԽһйϸ̵ηͶ˴˴ͶɲêmwhGm^=iZ9bS2gX7ufEkȽũƬ˲Ͷ̵˳˳ʲʲ˳˳̴͵ϸϸϹзз̲ɯƪƼxspu|ĵɺ¨ƬǯDZȳʵͷ̵̵˴˴˴ʳǰŮʳʳʳɲȱƯŮĬíÿºq}nQk\?gX9fW8eV9l^A{`ĺɿŮɴʵDzȳ˶˶ɴƱƱDzɴʵɴŰ­ľ}~Ⱦǭʳ˳˵ʴʴʴ˵̶θϹкʴȲDZȲ˵θϹϷ̳˰ɮƩƿ¥ŨȬδͶdzIJĴȷǴůĿĬǯȰȮǭŬȾƼȾŦɬɮƭííűƲƲDZDZǯǭǪƬƯDzȳȳȳƱŰįƱDzDzŰŽûûĽ®ȴ˷̸˷˷̸̴̴˳ɰɰɰȰȮžļŮȲİ¿®ïîíůŮǭǬȯɶıŲǮçlrRo[:qX9pY9s`?}oL\ìƯŮì«ĭĭŮǰɲɲɲȱǰȲȲɳʴʴ˵˵˵˵˵˵˵ɳDZĮííůưDZȲȲưůí¬íƳǴɶ˸̹̹˸ʷ̹̹˸˸˸ʷʷʷʷʷʷ˸˸˸˸̹ʷʷ˸̹ͺλλϼλλͺͺͺͺͺͺ˸˸˸̹̹ͺͺͺ̺̺̺ͻͻμμμμͻ̺˹ʸ˹˹̺̺̺̺ͻͻͻμμͻͻͻͻͻͻͻͻλͺͺ̹̹ͺͺλλλλͺͺ̹̹̹̹̹̹˸˸˸ʷʷͺ̹˸ʷɶɶʷ˸˸ϼннϼͺ˸ʷ˸̹̹ϽϽμͻͻ̺˹˹̺̺̺̺ͻͻͻͺζͳʭʭ˯ʮvuZl]@dU8dS7jY=tXwƺ˽p|av[~cx]~oRsdGhYl]^M1fU7jY;jZ9~nMlq{ax^l~Ⱥ̾ŷserUpbEj_AmdExoPcyṵ̲̈ͳ͵̴ʲȰȯȯȯȯɰ˯̰ͲαӶӴѳϱ̮ͯʭɬ˯{qXi_DdZ?cXgZ7na>tRy£Ʃǭ̲зʹ͵͵̴˳˳˳̴̴κѽӿѽ͹̸ϹӽѺδȭĪƫʭˮȫȫ̰̱ɭǬ˯̱̰ŪĨ¦ĩǫǬȬɰɰȯǮūĪĪĪĭɱ˵ɵƱıǵ˸Dzɲ˴̵˴ȱǰƯ˴̵ͶͶ̵ɲƯƮ©jyjMiX:gV8gV8iXaQ8iZCkȱȰ˳ѹѹ̴ʲ̳Ȱ˳͵˳ǯƮʲδé{y^seHj\?j[cR6_N2bS6]M3wiOvη̴ʲʲɱȰǯȰ̴ϷҺҺѹиζ͵̴̴ηϸйѺѺѺѺййһйͶ̵̵ȱçfypGqh?qhAqhA{rK\ƧȫʭҶӷͱииζ͵̴˳˳˳ϹϹϹϹϹϹιι̷̵˴ʴ˵εзѸѷжͳ˱ʰɯʰʰɯȮǭȮʰ˱ʰɱ˴˴˴ʳɲȱƯŮŮŮƱDzȲɳɶʴDZȲɳʴ˵ʴɳȲ̶ʴɳʴ̶ͷ̶ʲȭ`vcElY9iT5kV7n[=|iKmĪ˲̵͸͸ιι͸͸̷ȳDzDzƱƱDzDzȳʴ˵˵̶˵ɴDzƯƱƱŰưưDZȲȲкθ˵ɳȲɳʴ˵θͷ˵ɳʴ˵θϹʲζйѹϷδиҺɱɱɱɱȲư­¬îůƱȳʵ͸ιϺʵʳɴɲɲȱȱȱȲɳ˵̶ͷ͹˸ʷʷ˵̶˵˳ɱȰǯůDZɳ˵˵ɳưĮȲɳɳɳȲưĮ¬ƿ¯Ųȵʷ˸ʷʷ͹̸ɵƲűűŲƳǴʷ̹˸ȶǵȶ˵ǫǪƪūŭůŲƲ®ūɭˮʯʳʴɯȬ nYtJrHrHyQbxÿæŨūƩūūƬȭǭɮ˲̳˲ɰƭī©ǫɰ˲ʱɰɰɰȲDZðǿʿŲƳȵɶʷɶȵǴ̹ȵı¯ðııðȵǴǴǴȵʷ̹λ̹̹̹ͺͺλλλ̹̹̹̹̹̹̹̹λλͺ̹˸˸ʷɶȵʷͺϼнϼλͺλλͺ̹̹̹ͺͺʷʷʷʷ̹ͺϼн̹̹˸˸˸̹ͺͺλλͺ̹̹̹ͺͺͺ̹˸̹λϼλͺͻ̺̺˹˹̺̺ͻϽμμμϽоѿѿͻͻͻͻͻͻͻͻ̹˸˸ʷʷ˸˸̹λϼλͺͺͺͺͺͺͺͻ̼˾˾̿ͽͽξξ̸̺̺̹̹̹̹̹ϻκɶ®lweMiU=fP8dO4fQ4jU6o\;~kJ{ZvVz[~acjy¸ŪèƼ}x\yhJl[=l]>qbEykPcwý˲̵ʳ˳ɴƱíîŭĬŪǬ˭ΰвббέʪɩʨ˫¥vqeK`T:\P6]O5aS9dX>wmRtƬɯ˰γεε̴̴˶ʵȵȴʵ˵ͷθθθθθͷϹкϹ̶ɳȲȲǴŲðîɼ|jjYEaO;bP:eU>eU>n_HpêȱȰ˳иѹ̴ʲ̴Ȱ˳͵˳ǯƮɱͳƬȺry^ykNrcDn_>n`=sc?}mI|Uhzų̾ƢƤȦɧ˩̪ͫάͫʨǥĢýwgzY|[`nȭԻͷͷκϻξϿҿѾѻкϹθϹϹкӽҼѻϹθθθθʳȱ©w~pUiY?bS6`Q4cT7]O5uiOwͶʳʲ˳ʲɱǯȰ˳ζѹииϷϷζ͵͵ηϸйѺѺѺѺйϸѺй̵̵ͶɲħexqEpi?rkAwoH}VjȿˬˬˮѴҵͱδζ̴˳ʲʲʲʲιιιιιιιι̷̷˶˶̷ιлѼѹϷ͵˳ʲʲʲʲɱȰǯȰʲ˳ʲɱ˵˵̶˵˵ʴȲȲDZȲȲȲȲɳɳɳưDZɳ˵˵˵ʴɳ̶ʴɳʴ̶̶˵˲Ǭ½~`vcClW8jS4mV7q\?mOqĪʱ˵͸ιιιι͸̷ɴȳDzƱƱƱDzDzʴʴ˵̶˵ʴȲDZȲDZDZưưDZȲȲϹθ˵ɳȲɳʴ˵̶˵ʴʴʴ˵ͷθȰ˳ϷϷζ͵ϷѹɱʲʲʲȰƮĬëîįƱDzɴ˶͸͸ʵʵʵɴɴɴȳȳȲɳ˵̶̶̶˵˵˵˵˵˵ʴȲDZưưDZɳʴʴɳȲDZɳɳʴɳȲưĮ¬¯Ųȵʷ˸ʷʷ̺˹ȶƴIJIJųƴųȶʸɷǵųƴɳǫƩũĪìîııǿƬɭˮ˯ʲɱȮħ¸~b|OtGvI}R_sãŨȫɬȫŨéƫȭ˰˰̳̳˲ɰǮƭĪƪɭɭǮƭƭǮDZưð¯ðƳǴɶʷ˸ʷɶȵ˸ȵı¯¯ðððŲŲƳǴȵɶ˸˸̹̹̹ͺͺλλλ̹̹̹̹̹̹̹̹λλͺͺ̹˸ʷʷɶʷͺϼнϼλͺͺͺ̹̹̹ͺλλ˸˸˸˸̹ͺλϼͺ̹̹˸˸̹ͺͺλͺͺ̹̹ͺͺλͺ̹˸̹λϼλͺ̺̺˹˹˹˹̺̺μμͻͻμϽооμμͻͻͻͻͻͻ̹˸˸ʷʷ˸˸̹λλλͺͺͺͺͺͺͺͻͽ̿ͽͽ̼̼̺̺ʸ˹ͺλλͺ̸̹ι̷İt]jX@fR:bN5aM2gT6q^@{kJ{Zhfmt}ĺŪĩêīíɾxd~mQqbEk[Al^CxmQ}b|ſʱʱȯŮĬŭŪƫɭ˭ΰϰѲбͭ˫̬̬omaG`T:^R8]O5^P6_S9vlQwȮ̲ͲеʹεζϷ͸̷ɶɵɴ˵̶θϹϹϹϹϹѻѻϹ̶ɳȲȲưDZƱŰʽ}il\E^N7_O8eU>hX?seKrīɲɱ˳ии̴ʲ̴Ȱʲ̴ʲǯƮɱ˴˲ĪŹ|guXzmMwjHwiFxjGqL~Yj|¼ľãťƦãulmq|¥̱ҹͷͷκϻξϿҿҿҼѻккккѻԾӽѻϹθͷ̶̶ʳȱ©v|nShX>aR5bS6eV9^P6uiOw̵ɲ̴̴˳ʲȰȰʲ͵ϷϷϷϷϷζζζηϸйѺѺѺѺйηйϸ̵Ͷη˴ƪj}vLumFzrK|Wjżȩбα̱ееͳ˴̴ʲɱɱȰȰɱιιιιιιιι͸̷˶˶̷͸ϺлиϷζ̴˳ʲʲʲʲȰǯȰʲ˳˳ɱʴ˵˵̶̶˵ʴʴʴʴɳɳɳȲȲȲůDZɳ˵̶̶˵˵̶˵ɳʴ˵̶˵ʱƫþ~`vcCkV7jS4lU6r]@qStéɰ̶͸ιιιι͸͸ʵɴȳDzƱƱƱDzʴʴ˵˵˵ʴɳȲȲȲDZDZDZDZȲȲθͷ˵ɳɳɳʴʴʴʴʴ˵˵̶̶̶ǯɱ̴͵͵͵Ϸиʲ˳ʲʲɱǯƮŭįŰƱDzȳɴʵ˶˶˶ʵʵʵɴɴɴɳʴʴ˵̶˵˵˵̶̶˵ʴɳDZưůDZDZȲɳɳʴʴʴɳɳʴʴȲưĮíıǴɶʷʷʷ̺˹ȶƴųųųƴñųǵǵƴųųƳǭƪũĪĬĮðð¯ëǭˮ˯˯ʲʲƬĽu^yOxNVdvĦɬƩɯ˱ɯĪªŬɰεεʹ̳ɳȲDZǮƬǭƭŬêêíůůĮ­­­įűDzȵɶʷ̹̹̹ʷʷ˸Ǵð¯ðð¯ðıƳȵɶʷɶɶ̹̹ͺͺͺͺλλ̹̹̹̹̹ͺͺͺλλͺͺ̹˸˸˸ɶ˸ͺλϼϼλͺ̹̹̹̹ͺλϼнͺͺ̹̹̹ͺͺλͺͺ̹̹̹̹ͺͺλͺͺ̹̹ͺλλλ̹˸̹λϼϼͺ̺˹˹ʸʸ˹˹̺μͻͻͻͻμϽϽϽϽμμͻͻ̺̺̹̹˸˸˸˸̹̹λλͺͺͺͺͺͺͺͺͻϿξͽ̼˻ʸʸȶɷ̹ͺλͺ˸ɶȲȲĮoxhQfT>cQ9aP6fR9o^BoSfwƩȭȭƫŪŬŬů¬ȽŹnv\vhMn`EqfJ{pTbxǪƩéǿ«ŭƮǬǬɭ˯˭ͯббΰ̬̬ˮèmj^D`T:_S9\N4[M3[O5vlQ{ɯ̲γϴʹεϷѹϺιʷɵɴ˵̶θϹкккϹккθʴȲDZȲĮƮDzƯȽ|el\EaQ:_O6dT;hZ@tfLqŪʰɱ˳Ϸи̴˳͵ɱʲ˳ʲȰǯɱ̴ͳ̳ɰvjb~\xVwT}Zes~Ļ¥ƫʱ˲θθκϻϽϿϿѾҿӽҼҼѻѻѻѻԾӽѻϹͷ˵˵ʴʳɲêtykPfVpTuȯͶ͸ιιιι͸͸ʵʵȳDzDzƱDzDzʴʴʴʴʴʴʴɳɳɳȲDZDZDZDZȲͷ̶˵ɳɳɳʴʴȲɳʴ˵̶̶̶̶ǯɱ˳͵ζϷϷи˳˳˳ʲɱȰǯƮŰŰƱƱDzȳȳɴ̷˶˶˶ʵʵʵʵʴʴʴʴ˵˵ʴʴ̶̶˵ʴɳDZůĮɳɳȲȲɳʴ̶ͷɳʴʴʴɳDZĮíıƳȵʷʷʷ̺˹ɷǵƴƴƴƴñIJƴǵǵǵǵǴɱȮǭŭůůııð¯¯ůȰʰ˱ʰʲȰ¨tfbbl|¼ħȮɯĪȮ̴ʲĬªǮ̳Ѹзε̳ɳȲȲȲȱɯǰī©íů¬­ŰƲɴɶʷ̹ͺλͺ̹˸ʷǴ𯯯¯ıȵʷ̹˸ɶȵͺͺͺͺͺͺͺͺ̹̹̹ͺͺͺͺλλλλͺͺ̹̹˸˸˸ͺλλλλͺ˸˸˸̹ͺλнннϼλͺͺͺͺͺλλͺ̹̹̹̹ͺͺͺ̹̹̹ͺλϼλͺ̹ͺϼнϼλ˹˹ʸʸʸʸ˹˹ͻͻ̺̺̺ͻͻμоϽϽμͻͻ̺̺ͺ̹˸˸˸˸̹ͺλͺͺͺͺͺͺͺͺͺͻϿξͽ̼˻ʺɷɷƳȵʷ˸̶˵ɳȲŮǰí{r[n^GfV?cSaQ7bT9hZ?ugLoƩ˱ɱʲϷϷ̴˳͵ʲ˳˳ʲɱɱʲ̴̴Ͷη̲ūùwporx}¹ǾħɮγѸҹθθϻϻϽϽϿѾҿӽӽӽҼҼѻѻӽҼкθ̶˵ʴʴʳɲêrvhMdT:`Q4dU8gX;`R8xlRy˴ǰ̴͵͵̴ʲɱʲ˳̴̴͵͵ζϷϷиηϸйѺѺѺѺйͶϸϸͶηйͶɭżvu|żȫ̱γʹʹϹкϸ̵͵̴˳˳˳˳̴͸͸͸ιιϺϺϺι͸̷˶̷͸ιϺϷϷζζ͵̴̴˳ʲɱȰɱ˳̴˳ʲʴʴ̶ͷθθθͷͷͷ̶˵ʴɳȲȲưȲʴ̶θθθͷ̶˵ɳɳʴʴɳȯŪħ}{]vcCkX8jU6hS4mZ<pStʰ̵̷͸ιι͸̷̷ʵʵɴȳDzDzDzȳʴʴʴʴʴʴʴ˵ʴʴɳȲDZDZDZȲ˵˵ʴɳɳɳɳʴȲɳʴ˵̶ͷͷͷʲʲ˳͵Ϸиии˳˳ʲɱȰȰȰȰƱƱƱDzDzȳȳȳ̷̷̷̷˶˶˶ʵʴʴʴʴɳɳʴʴ̶̶˵˵ɳDZưů˵ʴɳȲɳʴ̶θʴʴ˵ʴɳDZůíðŲǴɶʷʷ̺˹ʸȶǵƴƴƴųųƴȶɷʸʸɶ˳˲ʱɰDZƲƳǴıİİưʱ̳̳ʲŭª»yy{ϦƬDZDZ¬ƮɳɳưůɳͷкϹͷ˵ʴɳɳɳ˴˴ɴ٬¬ĮDZ«ĭǰɲ˵˸ͺλλλͺ̹˸Ǵð¯ðð¯ðƳʷͺλͺ˸ɶͺͺͺͺͺͺͺͺ̹̹̹ͺͺλλϼλλλͺͺͺͺ̹̹̹ͺͺλλͺͺ˸˸˸̹ͺλннѾнϼλͺͺͺͺϼϼλͺ̹̹̹ͺ̹̹̹̹̹ͺλϼλͺ̹ͺϼнϼλ̺˹˹ʸʸ˹˹̺μͻͻ̺̺̺ͻͻоϽϽμμͻͻͻͺͺ̹˸˸̹ͺͺλͺͺͺͺͺͺͺͺͺͺξξͽ̼˻ʸɷɷǴȵɶɶʴɳDZŮŬ¨ŹhqbKgW@iYBm]FwhQw`pɿīŪƩƩǪɮ˰˰ʯƭƭưDZɳʴ˵˵Ȱõ{fzoSncGj_AshJz]uƹãʬ©ĭǯǯȭɮ̰βɫ˭̯̯ˮʬʮ˯ʯ¸pk_E`T:^R8\N4[M3ZN4}sXĽȮ˱ͲͲʹεζϷϺι˸˷̷θθϹкϹϹϹ̶̶̶ʴDZưDZȰŪʯ˱ƪy_k]BiY?aQ7cU:l^AxmOqƩ̲ɱʲζζ˳˳ζ̴̴˳˳˳˳̴͵˳̴͵͵˳ǭéŻ¹»žƿžžžŨȫƩǪȫɮ̱ϴҹԻϹϹϻϻϽϽξнҼӽӽӽӽҼѻкѻкϹͷ˵ʴʴʴʳʳīptfKcS9`Q4bS6fW:aS9znT{ʳŮ˳̴͵͵˳ʲ˳̴̴̴͵͵ζζϷϷηϸйѺѺѺѺйͶйϸͶηйͶɯ¹ƽȭʱ̶̳ͷϹϹйζζ͵͵̴͵͵ζ̷̷͸ιιϺллι͸̷˶˶̷ιϺζζϷϷζ͵̴˳˳ɱɱʲ̴͵̴ʲ˵̶ͷθθθθθθͷͷ̶˵ʴɳɳȲɳ˵ͷθθθͷ̶˵ɳɳʴʴȲƮǬƩyz]wdFmZ:mX9iV8n]AvYy¦˱ʳ˶̷̷̷̷˶ʵɴɴȳȳȳȳɴʵʴʴɳɳɳʴ˵̶˵˵ɳȲDZDZDZȲʴʴʴʴɳɳɳɳɳʴʴ˵ͷθϹϹ̴˳̴͵ииϷζ˳ʲɱȰǯǯȰȰƱƱDzDzȳȳɴɴ͸͸͸̷̷̷˶˶˵ʴʴɳȲɳɳɳ˵˵̶˵˵ɳȲDZͷ̶˵ʴʴʴ̶ͷʴ˵˵˵ʴȲůĮ¯ıƳȵʷ˸̺˹ʸɷȶǵƴųƴƴƴɷ̺ͻ̺˸ͷʹ̳ɳȴdzǴǴŲŲűȴ˵ʹ̳ʱøźȿ§ĭưŲŲíıưDZȲʴ˵ͷͷ̶̶˵ʴɳɳ˷̷ʵDzĮĮưȲ몪ª«ŮȮȱ˵˸ͺλλλͺ̹̹ɶıððııðıǴʷͺϼλ̹˸λλͺͺͺͺ̹̹̹̹ͺͺλϼϼϼλλλλλͺͺͺͺͺͺͺͺͺͺͺ̹̹̹̹ͺλϼнѾѾнϼλλλϼннλͺ̹̹̹ͺ̹̹̹̹ͺλϼнϼͺͺλнѾнλͻ̺̺˹˹̺̺ͻϽϽμͻͻͻͻμμμμϽϽϽϽϽͺͺ̹̹̹̹ͺͺλͺ̹ͺͺͺͺͺͺͺͺͽ̼̺˹˹ʸʸʷ˸ʷ˵ʴɳDZưŮ´n|lSiY@hYBpaJp[lɴʳϵϳβ̰ɭȬȮȮéĪŮƯDzȳɴʴ˵ɰĬʾpy^vhKreEyjK}\sͧĭŭŭƫǬ˯βͱβͲ̱ɯʭʰ̰Ǭpk_E^R8]Q7]O5^P6`T:{`Ȯ˱Ͳγʹʹ͵͵͸͸˸̸ιϹкккϹθͷ˵̶˵ʴDZDZɳ˳ɮαϲƪy^m_DfX=_Q4fX;tfIwYv¹Ȩ̲ɱʲζζ˳˳ζϷ͵̴̴͵ζζζ̶̶˵ʴʴʲʲ˱ɯȬũ¦ɿǾƽƽ¦¦çĨĨĨɭɭɭ˯ͱβββѵϳβ̲̲ͳͶηллллннннѻҼӽӽӽѻкϹкϹͷ̶˵˵ʴ˵ʳʳīnrdIbR8`Q4_P3eV9bT:}qW~ʳĭʲ˳͵͵̴˳̴͵͵͵͵͵͵͵ζζηϸйѺѺѺѺйηѺйηηйͶȮçƿžæǫǫǫȮ˱ʳ˴ιллϺϺζζ͵͵͵͵ζϷ̷̷͸ιιϺллϺι̷˶˶̷ιι͵ζϷϷϷζ͵̴˳ʲɱʲ̴͵̴˳̶ͷθθϹθθͷͷͷͷ̶˵˵ʴʴʴ˵ͷθϹθͷͷͷ˵ɳɳɳɳDZƮȭȫty\xeGo\>n[=mZm^GwkSlǿɶͺͷȮʮ˯˯ɭɭʰ̲ǭȮȱȱȳDzȳɴȳǰDZʱȯƺswZxiJsdCpObt§ìĬëèŪʮβддγ̱ɯɯ˱δèoj^D]Q7\P6^P6aS9dX>eçɯ˱ϴϴʹʹ̴̴˶̷˸͹ϺкккϹθͷ̶̶̶̶ʴɳɳ˵ζ̱ѴѴȫ~z^oaFbT7^P3i[>ynP_|Ļɩ̲ʲʲ͵ζ˳˳ζиζ͵͵ζϷϷζ͹̸ʴȲDZɳʴ̴̴̲˱˱˱˯ʱʯ˯˯˯˯ʮʮʮʮ˯˯̰ϳӷӷѵϳ˯˱ʰʰ˱̲ͶηллллннннкѻҼӽҼѻϹθθθ̶˵˵˵˵˵ʳ˴īmqcHaQ7_P3]N1dU8bT:~rX˴ĭȰ˳͵͵̴̴͵ζ͵͵͵͵͵͵͵͵ηϸйѺѺѺѺйϸѺѺηηй̵ƯĪçū˱ϵϵͳ˴̵˴̷лѼлλιθ͵͵̴̴͵ζζ̷̷͸ιιϺллϺι͸˶˶̷͸ι͵ζϷиϷζ͵̴˳ʲɱʲ̴͵̴˳ͷθϹϹϹθθͷͷͷͷ̶̶̶˵˵˵̶θϹϹθͷ̶ͷ˵ɳɳɳɳDZůʱɮtx[wfJp]?p]?n]?|mPqžȮζȳȳɴʵʵɴȳȳDzDzDzȳɴʵ̷͸ʴɳɳȲɳʴ̶ͷ̶˵ʴɳȲDZDZDZȲȲɳʴʴʴɳɳͷ̶˵˵̶ϹҼԾ͵˳ɱ˳͵͵ʲǯɱȰǯŭŭƮǯȰƱƱDzɴʵ˶̷̷ιιι͸͸͸̷̷̶˵ɳȲDZDZȲɳʴ˵̶ͷͷͷͷ̶ϹϹθͷ̶ʴɳɳ˵˵˵˵ʴȲưĮ¯ðŲȵʷ˸˹˹˹ʸɷȶƴųųIJIJȶ̺Ͻμ̹͸͵˶ɴdzŲııŲŲƳɵ͸ι͵˳©½»ǾʾƬȯɳ˶̺˻ɻɹƶıðƳȵŲŲDzɴ˶̷ʳȴDzǴȴɵdzűűưȲ̴˳ɱȰǭȮɯʲʴʷ̹ͺλͺ̹˸λ˸ǴŲŲƳƳŲıƳȵʷ˸˸˸˸λλλͺͺ̹̹̹̹̹ͺλϼϼнѾλλλλλλλλλλͺ̹̹̹ͺͺλλͺ̹̹̹ͺͺѾннϼϼнѾѾѾнϼλͺ̹̹̹˸˸˸̹ͺλϼнϼλͺλнѾнϼϽϽμͻͻμϽϽѿѿоϽμμμϽ̺̺ͻμоѿλͺͺ̹̹ͺͺλλͺλ̹ͺλϼнϼλͺʸ˹ͻμλͺ˸ʷͷθͷ̶ζ͵ŭùq~mSm]CgW>i[AyjS}eƾððƴʸ˸ʰɬȫɬʰ̴̴̲˳ɱȲʴͷϹ̶ɳɳDZDzηӼ̳ĸrbrQ|mLrP\rż¨ɯʰǮŬƭɰ϶϶϶϶ζζͶδ§faU;VJ0XL2^P6eW=j^DtĨδѷеҷз϶͵̴˶͸̹κϺкϹθͷͷͷͷϹθ̶˵ʴ˵̶̴γͯʭŨvvZqfJk]@gYl`FwĺǫͳδγѶзз͵͵̷͸̹κιϹθͷͷͷͷͷθͷ̶ʴʴʴ˵ʹ̰ʬȫ¥w|`|qUzlQxjMy[p̪ή˱ζѹѹиϷϷиϷϷϷϷϷϷϷϷ̶̷˶ʵɵ˷ͷθͷͷ͵͵ͶͶηη˴ͳϵжжжϵϵδϵѷѷѷϵͳ̲˳ʲȰƮǯɱ˵ͷккϼнннѾѾҼҼѻкϹϹθθͷ̶̶̶ͷ̶ɳưʳ˴|bk]B`P6`Q4_P3[L/eW=w]Ƽȱ̵˳Ϸиζζѹи̴͵͵͵͵͵͵͵͵ͶηηϸϸηηͶйѺһһйη˴ɲɱ˳ζζ͵̴̶ͷϹкϼϼϼλͺͷ͵̴ʲɱɱʲ̴͵ιιιϺϺлллϺι̷˶˶̷ιϺиѹѹѹѹиϷζ˳ʲɱɱɱ̴ζиϹϹϹϹϹϹϹϹͷͷͷͷͷͷͷͷͷθϹкϹθ̶˵ɳɳɳ˵̶˵ȲůʹƮjpVveIuaFraE~oRk»ʯεεʴɴʵʵʵɴȳDzƱƱDzȳʵ˶̷͸Ϲθͷ̶˵̶̶̶˵˵ʴɳɳɳʴʴȲȲȲɳɳʴʴʴ˵˵˵˵˵̶ͷͷʲȰȰɱ˳̴˳ɱƮǯƮŭëĬƮɱ˶˶̷̷̷̷˶˶ʵ˶͸ιι͸˶ʵʴʴʴɳɳɳȲȲɳɳʴ˵ͷθϹϹͷͷ̶˵˵ʴɳɳ̶ͷθθͷʴDZư¯ðŲƳǴȵɶɷɷɷɷȶǵǵƴǵȶʸʸȶǵǵɶ͸ζͶʴDZŮůưůůƯȲɳ˴̷ζ̳ͱͲ̲˱˱ɰɰDZȴ˸̹˹ɹȷǶƳŲıűŲŲðĭưȯŭ¨ǰʲ˵ɳưůưȲʱɰɰȯDZɰȲɳ̹ͺͺλλͺͺ̹ͺ̹˸ȵŲŲǴɶƳȵ˸̹˸ʷʷ˸ϼϼλͺͺ̹˸˸˸˸̹ͺͺλϼϼϼϼϼϼϼϼϼϼϼλλλλλλϼλλλλλλλλϼѾѾнλͺλннϼϼλͺ̹̹̹˸˸˸̹ͺϼнѾϼϼλλλϼϼнϽϽϽϽϽϽϽϽϽμͻͻͻͻμϽϽϽооооооͺͺͺͺͺͺͺͺλλϼλλϼϼϼϼλͺ̹̹ͺͺͺͺͷͷʴʴʲ˳̴Ʈhs[ygOvfMxiRw_zûDZϼϼ̺̺̻̹Ϸͳ̲̲δϵϷζȲʴ̶̶ʷʷʷ˸Ѿκ˷˵̶˴̲ɮ§tf`jyʰʰƭīʱѸ̵ηϹккϹ̹ʵ¸y_bV^R8ZN4YK1`R8g[AwƼǫɯɯ̱ӸѸѸииιι˸̸˶̶ͷθθθͷ̶̶˵ʴɳɳ˵̶ͷѺѷйʹª¸ǽʾǬ̱ʮͰӶϲʰ͵иѹϷζζϷζζζζζζζζζ̴ʲɱɱ˳ͶϸͶ˵ʶɵɵʷ˸κӾҽѼлϺιι͸лллллϺιι̸̹˸˷̸͹κϻϻϻϻмккѻѻѻѻкϹθθͷͷ̶̶̶ͷθθ˵ɳ˴Ůz}oUeW<`P6cT7^O2aR5qcIlê̵ϸӻԼѹ̴˳иѹииϷϷζζ͵͵͵̵Ͷηϸϸϸϸϸϸϸϸйϸϸηηʴ̶θθͷθѸӺε϶϶϶϶εʹ̴̳˳˳˳˳˳˳̴ιιιϺϺлллϺι̷˶˶̷ιϺиѹѹѹѹиϷζ̴̴̴̴̴̴͵͵θθθθθθθθθθθͷͷ̶̶̶˵̶ͷθθθ̶˵̶˵ʴ˵ͷͷ̶ʴʴƮùzqtzǻǫͲβ˰ȰưƱȳɴʵ˶˶˶ʵʵʵʵʵʵ˶˶ϹθθͷͷθϹϹɳɳɳɳʴ˵̶̶ɳɳɳɳɳɳɳɳɳɳʴ˵˵ʴɳȲƮƮǯȰɱȰĬȰʲ̴˳ɱǯǯǯ˶̷ιллι̷˶̷͸ιϺϺι͸̷̶̶̶̶˵˵˵ʴɳɳʴ˵ͷθϹϹ̶̶˵˵˵˵ʴʴ̶̶̶̶˵ʴɳɳıƳɶ˸̹̹˸˸ɷȶȶȶȶɷ˹̺˹˹ʸȶųIJƴɵͶ̳ǭø»ǮɰʲȱDZȲɳ˵˶̷ʵʵʵ˳̳̳βδйη˵ɳȲƮéywy|{¦ǭɱɱ̶̸˸ʷʹ˺̻̹̽˸̹ͺͺ̹˸˸ɶ˸λϼλ̹̹ͺͺλϼλ̹̹ͺϼλλͺͺͺͺ̹̹̹̹ͺͺͺͺλλλλλλλλλλλϼннннϼλλλλλλλλλλϼнϼͺ̹ͺλϼϼλͺͺ̹˸˸̹̹̹̹ͺλϼнϼλͺ̹̹˸̹̹ͻͻͻͻͻͻͻͻ̺ͻͻͻͻͻͻ̺ϽϽϽϽϽϽϽϽλλλλλλλλϼѾҿҿҿнϼλλλϼλλͺͺθθϹϹ˳͵͵̴ʰνȲDZͷкι̺νν͹͵Ͳ̱Ͳ϶ѸкϹϹкҾѽмϻϻϻͺѾн˷ȴʶθϹѸҺжǭ¼¼ǭʰ˱ɯŬŬɰε˴̵͹ϻμ̺ɸɶsYdX>`T:ZN4YK1`R8h\BvŻǫ˱̲ͲӸҹҹѹѹϺι˸˷˶ͷθккϹθͷͷ̶˵ʴ˵̶ͷϹԽӻһѺ˵Ʈư̳Ϸʲ϶еɭ˯ҵδʲ̴ϷиϷζζζ͵͵͵͵͵͵͵͵͵͵̴˳̴͵ηϸ̶˵ʶɵɶ̹λнԿҽлϺϺϺϺѼѼллϺϺϺϼ̺ͺͺͺ͹κϻмκϻϻϻкккккккϹθͷͷ̶̶̶̶ͷϹϹ̶ʴ̵ìtxjPcU:`P6dU8aR5fW:ykQuŬ˴̵ѹӻи̴˳иѹииииϷζζ͵͵˴̵Ͷηϸϸϸϸηϸϸϸϸϸйѹ˵̶ͷͷͷθҹԻʹεεεεʹ̳˲͵͵͵͵͵͵͵͵ιιιϺϺлллϺι̷˶˶̷ιϺиѹѹѹѹиϷζ̴̴͵͵͵͵͵͵ͷͷͷͷͷͷͷͷϹϹθͷͷ̶˵˵ʴ˵ͷθθθ̶̶ͷ˵ʴ˵ͷθͷ̶θɳɾʺͿū˯Ͳ̲˳ʲDZDzȳɴʵ˶˶˶˶˶˶˶ʵʵʵʵͷͷͷ̶ͷθϹϹʴʴɳɳɳʴ˵˵ʴʴɳɳɳɳȲȲȲɳʴ˵ʴɳDZưȰǯƮƮƮŭľëƮɱ˳ʲȰǯǯ˶̷͸ιι͸̷˶̷͸ιϺϺι͸̷ͷͷͷ̶̶̶˵˵ɳɳʴ˵ͷθϹϹ˵˵˵˵˵˵˵˵̶̶̶˵˵ʴʴʴŲǴɶ˸˸ʷȵǴȶȶǵǵȶʸ̺ͻɷʸʸȶƴųǵȵʴɮ{wwy|èȯ̴˳ɲȲɳ˵̷ιɴʳ̴̲γΰϰίҹй̶̳ʲǭĨ·tidfikrĽũȮȰ̶̸̹ͼ;˿̹̿̽˸̹̹̹̹˸˸ɶ̹λϼλ̹̹ͺλϼϼλ̹˸ͺϼͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺͺλϼнѾѾнϼλλλλλλλλλλнѾнλͺͺϼннϼϼλͺ̹̹ͺ̹̹̹̹ͺλϼλλͺ˸˸ʷ˸˸̺̺̺̺̺̺̺̺̺̺ͻͻͻͻ̺̺ͻͻμμϽϽϽоϼϼϼϼϼϼϼϼнҿҿҿѾϼλλϼϼϼλͺͺθθϷи͵ижͳʰǭǸʽɾŽſíȱʳθϹ̷˷ͺλ˸˳ʱʱ˲εѸѻккϹκϻмϻͺ̹˸ϼҿѾͺ˶̷ιлϸϹѸ̳ŬêƭɯɭȮƬǮȯ̵ϸ̷͸ͺͺ̺̺ʻʷ|pVdX>aU;\P6[M3dVxVg̮ͰβеҺһһйͶ̵ͶϸͶͶ̵̵̵̵ͶͶ˱̲̲̲ͳͳͳͳ̰дѵддҶѵβ̴иӻӻиϷѹԼҺиϷϷѹѹϷζδδ͵͵εзкѻ̶˵ʴʴ˴̵̵δ˱ʰɯɯ˰̱αϲѳҳбͮͰѴѴ̯αα̰ʰͳѷϵʰʱʱ˲̳ʹε϶϶ʹεͳδδͳͳ̲˲̲˴̶ιϻмоͻ̸˸͹Ϸϵͳ˱гггͰɬƩƩƩɬɬɬǪħææĪêůȲɳ˵̶ϹҼͷθ̶ɳʴϹк͵ǪƤ£ʿɾɻǹĸǽ©ƩŨƩ˭γ˳ʵɴɴɲȱȱȱɯǭƬƬǫƪçvb}VvOpI~oHrKzXbvĭƮ˳ʲȰƮƮȰʲ˳ɱʲ̴͵͵͵̴˳̹ȲͷϵɯǫƩĻzql~hpuwɯγ˲ƭʱ˲˵ʴȳDzƵɷǵɷ˸̴Ǫq^}UwQxT^mưʶɸɹɼʾȺǴưǽlz\pOoNsR^qʽɭƮɳȲůűʶ̸ʶİƲǵɷ˹ͻͻμ˿ɺǴɱͲαͲʱDZȵïj[SX]i}æȯɱ̷͸͹ι͹͵˴˴ͷ̹ͺλλϼϼнϼϼϼͺ˸˸ͺϼѾнϼͺ̹̹̹̹ͺλϼннϼλͺ̹̹˸˸̹ͺλϼͺͺλϼϼϼϼϼϼϼϼϼϼϼϼϼλλλλλλλλͻͻμμϽооооооϽμμͻͻͻμμμϽϽϽϽͻͻͻͻ̺̺̺˹̺̺̺̺̺̺̺̺ʸ˹̺ͻͻͻͻͻ˹̺μоѿѿѿоλλͺ̹̹ͺλλϼннͺ̹˸ʷ˸̹λϼλλλλλλλλϼϼϼλλͺͺ̹ͺͺͺͺλͺͺͺλλλͺͺͺͺͺ̶ʹ̳̳̳̳ʹε̶̶ͷͷͷθθθηηηͶͶ̵̵̵ͶηηϸϸηͶͶ͸͸͸͸̸̸̶̶ʲζҺѹʰĽo}^}nOuhHwiLvXoýͷѻккϹ̶ϹϹͷͷͷϹкҼӽϹ˵ʴ˵θкѻϹθʹ̳̳ʹ϶зϹϹϹϹϹϹϹϹϹϹкѻѻѻккθϹϹкѻѻҼӺзϴϴγϲϲгѴѹиϷζ͵ζϷиϷϷϷϷϷϷϷϷ˸ʷ˸λѾӽҼտӺ϶ʹ̱̱Ͳγͳδϵжϵδ͵˳ʳʳ˴̵ͶηϺлϺϺϺϺϺϺϺϺ͸лл͸͸͸ʵíǷiqQwdDta@zgFpLdwŦ˭ʭʯ̱ѹѺйη̵̵ηйͶ̵̵˴˴̵̵Ͷͳ̲̲˱˱̲ͳͳͱͱ˯ʮɭ˯̰̰ʰζҺӻѹиҺԼӻѹииѹҺѹиϵδ̴˳ʹзҼԾ˵̳˵̵̳ͳ̲˱̲˱ɯȮɮʯˮ̯ίίˬǨȩ˭ʭƨʭǪũǫͱѵ̱Ȯ̲̳̳̳̳ʹʹε̳ʹͳδδδͳ̲̲̲˳˳̶ϺлѼι̷ʴʲʱ˰ʮʭͱͰˮȫæææĿ»ƿĪǭɯ˱ͳϷζθʴȲ˵кϹʰãſĹ¥ƪȫɬ̮γ˳ʵʵʵʳʳʳʳʰǭūĪĨĨçydyR}nGvg@uf?yjC~nLzWlĪŭʲȰƮĬĬƮȰʲɱʲ˳͵͵̴˳˳̹ȵθϷǭƩpz]|mPveKsbHweM|lSu^s»Ƭ̱ɰīǮǮȯǯƯůDZȲưƯŬpb{UyRvR{YfzĹDZ̷ʷȸɹȺŷƳDZůƭ̾xerQ~nMoNyXh|¤ƬɳȲĮĮȴ̸˷dzȴǵɷʸ̺ͻͻȹıƮ̱гϴ̳DZƳ®¶l[~R}UYdx¥ɰ̴ιι͹ι͹ζͶͶϹλλϼϼнннннϼλ̹̹λнѾнϼͺ˸ʷʷʷ̹ͺϼѾѾϼͺ̹ɶʷ˸̹ͺλλλͺλϼϼннϼϼλλλλλλλλλλλλλλλλ̺̺ͻμϽооѿѿооϽμͻ̺̺μμμϽϽϽооμμͻͻͻ̺̺̺ͻͻͻͻͻͻͻͻ̺̺ͻμϽϽμμ˹ͻϽѿѿѿѿоͺͺ̹̹̹̹ͺͺϼϼн̹̹˸ʷʷ̹ͺλλλλλλλλλϼϼϼλλλλͺλλͺͺͺͺͺͺͺͺͺͺͺͺͺͺθηͶͶͶͶͶηͶͶͶηηηϸϸͷͷ̶̶˵˵˵˵̶ͷͷθθθͷͷ͸͸͸͸̸̸̶̶˳͵Ϸ̴¨}lhfhoyýDZкҼѻϹθ̶θͷ̶˵˵ͷϹкҼϷ˳ʲ̴ζϷϷϷζ̴˳˳̴ζϷϹϹϹϹϹϹϹϹϹϹкѻѻѻккϹϹϹкиѹѹѷϵϵϵгѴҵӶӶѹиζ͵͵ζϷиϷϷϷϷϷϷϷϷϼλλϼҼӽѻкиϷζ̲˱̲δжʮ˯ͱβϳϳδͳ˴˴̵ͶιιϺϺϺϺϺϺϺϺϺϺлҽл͸̷̷ƱȽw{]|lKxeEzgFrQ]vɾˬ̯̯˰˰ζͶͶ̵˴̵ϸѺͶ̵̵˴˴̵̵ͶϷ϶ͳͳͳͳββͱȬ¥ǾƽȿæȬȮ̴ѹӻѹиѹԼӻѹϷϷѹҺҺҺиϷ̶˵̶ϹҿεϴѸѶзͲʮȬβ̰ˮɬǪǪǪǪȩɨť¢ƿ»ǾȫͰͱ̯ϵϵδδͳ˱ʰʰʰ˱ͳϵϵϵͳ̴̴̲̲̲͵ͷθϹϷ̴ȯƫŪƩŦæƩħƿǼƫʯδϵϷ̴ɱ˵Ϲ̶Ƭſ||ȿǪʬˬ̭̯˳ʵʵʵʳ˴˴˴ʰȮūéĨĨç¥~i|W|mFsd=rcm]|nKaøƩɭƬŭƮǯȰȰǯƮŭǯɱʲ̴̴˳ʲʲ˸˸ѻϷɯȫżfYvOwP{R}TT]míȲ̵̲ʮ˯ʭʭʫɪäź}n_xTvR{W[kvƫêůʵʷŲóƶȸȵƳȲů©ŷ|p{ZqP{kJ}mL~oNtSezéƬǭʰ˴ʳȲɳʷ̹͹̸ʷǵɽɺɶɱ˰̯ͲεɳȵïŹnWuIvNxR]rĿɰ͵Ϻι̸̷˷̴Ͷηкϼϼλλλλλλλλ̹ʷʷ̹λѾнϼλͺͺλλͺͺͺͺͺͺͺͺ˸˸˸˸˸˸̹̹λϼннѾѾннϼϼϼϼϼϼϼϼλλλλλλλλͻͻͻͻͻͻͻμμͻͻͻͻͻͻͻϽϽϽоооѿѿϽϽμμμͻͻͻμμμμμμμμ̺ͻμϽϽϽϽϽʸ˹ͻμμμͻ̺˸˸ʷɶɶʷ˸˸ϼλϼ̹˸ʷɶʷ˸ͺλͺͺͺͺͺͺͺͺϼϼϼϼϼϼϼϼλͺ˸ʷɶɶʷ˸ȵɶɶʷʷ˸˸̸ѼѼлϺϷϷϷϷͳͳͳδδϵϵεθθθͷͷͷ̶̶̶̶θϹϹϹϹϹϺϺϺϺκκθθ͵ʲʲ̴ȮéʰȭʭȮʯγѶҸϵԾѻϹϹθͷͷθͷϹкҼҼѻкϷ̲̯˱̲ͳ̲ɱƮ˳̴ζϷϹθ̶˵ϹϹϹϹϹϹϹϹкккϹθͷ˵ʴϼλι̵˲ɯȮɭȭɭɭˮͱϳѷҸʲ˳͵ϷиииϷ͵͵͵͵͵͵͵͵θͷͷθкиζ̴ѺԺӹδĨüżɿ§ӹӹѺѺлϺϹϹϺϺϺϺϺϺϺϺ͸ιι͸лѼʵ}~²żŪȬηйһһйͶͶͶϸηηͶͶηηиε϶Ѹҹжδ̰ʮn_{[`qƮ̴ϷϷζϷѹҺϷ͵̴ϷѹҺӻϹθͷ̶ʵ˶̷͸ɩãźƨɫʬʬˮήͭȨäȿ|sljghqɿ˯дϳ˯ǫǭʰͳжҹѸ϶ʹ̴ζжҸѷ̱ȬħʻŴ{wssz~m~^yfEvb?q]s`?ubByiH}^uɿ϶жζϷ̶ūŦɿwbzSwPzPYg ǤȨʭĭŰƱDzȱȱȱȱʰʰʰɯɭȬȬȫƨu|XxhDn^:l\8m]9qc>tSqŨɬǭǯȰ˳͵̴ȰĬŭƮȰɱʲ˳˳˳̶ͷ˵ɱɯƪĻp`}S}Q}PxI{JV}bëȰɯǭũŪå̾ƻwmb^[~ZalzŻéǭǰǰDZθʷƳİðŲȵʷǴɳĮŻwfy[tS|lKwgFyiHyjIyjIrRzZm}ȫ˰̰˱ǰìůͷм̸ɶʷȼ˼˸ǯè¥Ǭ̳˵ɶdzpWxLxPyS]sȯɱ͸ι͹ι͹̴˴ʳ̶̹ͺλͺ̹ʷɶͺ̹ʷɶɶʷ̹ͺͺͺͺͺͺϼнннͺʷɶʷ̹λϼλͺ˸˸̹ͺλλλλϼϼнннннннннннϼϼλͺͺ̹˸˸̺̺̺ͻͻμμμ̺̺ͻμϽϽϽϽооооооооϽϽϽμμͻͻͻϽϽϽϽϽϽϽϽоμͻͻμμ̺ʸɷʸ˹̺̺̺˹˹̹˸ʷʷʷʷ˸̹ϼλϼλͺ̹̹˸˸̹̹ͺͺͺλλϼϼϼнннϼϼλλλλнѾѾλ̹̹̹˸˸˸̹ͺͺλλͺͺ̷̹ʶ˶ɳʳɰʱ˰ͱͲϳϴϴϷϹθͷͷͷͷͷʴ˵˵̶ͷθθϹмѼллκ͹ͷ̶ииϷζͳͳ̲̲ϵͳ̲ͳжҸйηккϹθθϹккθкϹ̶ͷѻϹʲϳггϲββ̲ʰη˴̵йл̷͸ѼͷͷθθθϹϹϹѻкϹθθθθϹѾ̹ȳīĸ||xzūҸϷϷииииϷϷζζζ͵͵͵ζζѻθ˵ʴ̴ζδδжж˰vod`__dnǬɰ̵ηθ̶̹̹ιιιϺϺлллѼϺ͸̷˶̷͸ιʳɯ¨Ƽɾ˽xl~]a{\ewȼʹйϸ̵˴ηϸ̵ͶηйѺѺйηζϹһзεδжβʭp|\|mL~oNzX_z¸Ĭ͵͵̴ζииϷϷζ͵ζϷиѹϷϵηжδǭƼ}yvxƽũȬɭʰδѵдβͱͰͰˬƧ£sdwU~qNrP}]j{ø¥Ūǫɭ˯δϵ϶εεε˳˱̲˰ɭô|pb]^clxɬ̰ˮǫŨ¦ŨȬͰʮ¥s]nMwdCq^>p`?~oPeĹɮ˱ʳ͵̴ūƧx`zSxNzPXhȿĤƨǪƯDzȳȳɲȱȱǰɯɯɯɯɭɭɭɬ̮Ʀĵz~\wgCm]9k[7k[7m_:{mJeƿƩȮǯȰ˳Ϸи˳ǯŭƮȰɱ˳˳˳˳ͷθͷ˳˱ɭp^{OuHq@rAuIb~ɽ¨ĩƫŨǸ|tkd_aflvȷŽ«ǯȱʴʴɳ˸ɶdzűıŲǴȴ˸̶ůùox\nP~nMwgFscBwgFyjIzkJ}pPuU`oåˮ̯˯ʰŮDZθѽ͹̹λ˻˼˸ȰèæǬ̳ʴȵƲpXyMxPxR\rǮɱ͸ι͹ι͹̴˴ʳͷͺλλλ̹˸ʷ̹̹˸ʷʷ˸̹̹ͺͺͺͺͺλϼнѾнλ̹˸˸̹ͺϼλͺ̹̹̹ͺλλλλϼϼнннннннннннϼϼλͺͺ̹˸˸̺̺̺ͻͻμμμͻͻμμϽϽμμооооооооϽϽϽμμͻͻͻϽϽϽϽϽϽϽϽϽͻ̺̺ͻͻ̺ʸɷʸ˹̺̺̺̺̺ͺ̹˸˸˸˸̹ͺϼλϼλλͺ̹˸˸̹̹ͺͺͺλλϼϼϼнннϼϼλλλϼнҿѾϼͺ̹ͺ˸˸̹ͺλλϼϼμϼͻͺ˸˷ʴʴɱʱʰ̱αϲгϵикϹϹθθθθ̶̶ͷθϹкѻѻѽѼѼл͹̸̶˵ӻӻӻҺҸжϵδҺиϷиѹҺѹиккϹϹϹϹккθкϹͷθѻϹʲͰ̭ʫɪȫǪƫǬ϶˲ʴͷϻ͹ͻнͷθθθϹϹϹϹѻкϹθθθϹϹҿλDzƼve_zZxWz[ftȰ˳̴ζϷиѹѹѹϷϷϷϷϷϷϷϷѸ϶ʹʹζϷδδγͲǭȼ{k}`zYwTrQrPtTb|ɮ˲ηηθθͺϼιιιϺϺлллϺϺϺιιιιιҺҸδȭɭæwepRpR|kMrXlȺεѺйͶ̵ϸϸ˴ͶηϸййϸηζкӼѸ϶δжβɬq}^~oNpNzV^yë͵Ϸ͵ζиϷϷϷζ͵͵͵ζζϵгϵͱŪùpmmp|·ʿƫǬɰʱ˱˴ηѺ͵ͳͳϲдϲͰ˫äżo|YukGpc@wjH~qQ_ræȫʮʮ˱ͳззη̳͵˱ʱʯŪɺ|j`|Y|Y^j{ʿũʯдеβȭƪƫʮ̱ϳ˰ƪǽ·n{[oNxhGxiJwZkèǫȮ˱˱ƪɪŸyazSvMxMXh~Ǿ¢ħéɲɴʵʵʳɲȱǰɯɯɯɯɭɭɭɬʬƦŶz|ZueCl\8k[7k[7j\7rdAzWqƨȮƮƮʲϷѹ͵ȰŭƮȰʲ˳̴̴̴̴ζζ͵δδŪƽźo~TsFrArA}pCvVsǹ˻ʻǸŶvplkighq{ɹ©ǮʴʴɳȲȲɵʶʶʶɵȴdzǴƳƳűʷ˵ígpTxgI{kJueDtdCzjI~oN~oN{nNylLsQ}[møĥȫȭʰȮʲͷκ̸͸λ̼˼˸Ȱĩħȭ̳ɳǴűqZ{OwOwQ~ZoþǮʲιι͹͸̸̴˴˴Ϲλλϼλͺ̹̹̹̹˸˸˸˸̹̹λͺͺͺͺλϼϼϼϼλλͺ̹̹̹λͺͺ̹̹ͺλλλλλϼϼнннннннннннϼϼλͺͺ̹˸˸̺̺̺ͻͻμμμϽϽμμμμͻͻооооооооϽϽϽμμͻͻͻϽϽϽϽϽϽϽϽμ̺˹˹ͻͻ̺ʸȶɷ˹̺ͻμμμλλͺ̹̹ͺλλϼλλϼλͺ̹̹̹̹ͺͺͺͺλλϼϼϼнннϼϼλλλλнѾϼͺ˸̹ͺ˸̹̹ͺλϼϼнͽͽ̻̺˺˸˶̶ʳ˳̲αβгϳжζθϹϹϹθθͷͷθθθϹϹккκϺιι̸̸˵˵Ϸииѹжϵͳ̲ӻҺѹѹѹѹиϷкккϹϹккккҼѻθθϹ̶ƮƩƽżĻĻȾè̳ʱȲ˵κϻμλ͸θθϹϹϹккккϹθθθϹкѿͺű÷guWrR}hIyfFxhGqTcxʲ˳̴ζϷиииϷϷииѹѹиѸѸ϶ε϶ϷжϵͳʯʯȮo}`}\vTqPpOrS`~·ȭʯͶηθϹнѾιιιϺϺлллιϺлѼллιθкҹζ̲̱Ȭza}jL}jLzgIoSi}ȺʹѺѺηͶϸϸ˴ͶηϸϸϸϸηζѻӼҹ϶δѴ̱Ǩq~]pNpL{W^{ª͵иϷϷиζииϷζζ͵͵ͳαΰ˯ħ¹zpdelwŷĨɯ̱̳˲ɳʳ˶͸Ϻ̶ʹ͵ϵждϳϲɬŦȿi~tQpfBqbAqbAzkL`xȽɬ˯ɭɯ˱ззͶɰ̲˯ɮȬæŴo^|Y{X^fv;ƫɯ˱̲̲ʰǭūɯ̲δͳʰū¨shefr§ƫȮʰʰƫɫã{cyRuLvKVf}ǼéĬ˶˶˶˶ʳɲȱȱȮȮɯɯɭɭʮɮʼtwVqa?k[7l\8m]9i[6l^;xnJa{ǩǭŭĬȰ͵ζʲŭƮǯȰʲ˳̴̴̴˳͵ζ͵δϵʱèw~WsHxIvE|oBxkHkȻǸ|pkeekpwyȹοůȵʷϼ̹ȵűıűȵʶɵɵʶ˷ʷȵƳűưůɾ|coSyhJ~nM{kJ~nMvU}\{ZrRwjH|nK~pM|YnȽħɭʰ̴̴ʴȴʵ˸ͽ̽˸ȰƫƩɮ̳DZŲİ˿r[|PwOwQ|XlǮ˳ιι͹͸̸̴˴˴кϼϼϼλλͺͺ̹̹̹˸˸̹̹̹λͺͺ̹̹ͺλλλλϼϼϼλ̹˸ͺͺ̹ͺͺͺλλλλλϼϼнннϼϼϼϼϼϼϼϼλλλͺͺ̹̹̹̺̺̺ͻͻμμμооϽμͻͻͻͻооооооооϽϽϽμμͻͻͻμμμμμμμμͻ̺ʸ˹̺ͻ̺ʸɷʸ˹ͻμϽϽϽнϼλλλλϼнϼͺλϼλͺͺ̹̹ͺͺͺͺͺλλϼϼϼнннϼϼλλλͺλϼͺ˸ɶʷ̹̹̹̹ͺͺλλλɺɺǹȷǷɶȴȳȲɲɰʮʯˮˮ˯ȱɴʵ̷̷̷˶˶ʵʵ˶˶˶˶˶̶͹ιιικκθθʲ˳˳̴̲̲˱ʳѻѻѻкϹθθθкккѻѻкккӽտӽϹθͷɳ¨żĺŬǮȲʴ͹ϻμλ͸θϹϹϹккккϹθθθϹккϽ̹ű¶bnO~jIybBt`?qa@whIv[rëʲ˳̴͵͵ζ͵͵ζϷиѹҺҺѹҹҹз϶зжѷжβȭɯʯɮξxfyXqM{kI}oLtSbǼǬɮ̵Ͷͷ˸͹λιιιϺϺллл͸ιлѼѼлϺθϹкεʰ̱ʮõ{]xeGyfHxeGlQ}bwĴ˲ϸйηηйϸ˴ͶηηηηηηζкӼҹ϶δгʯĥl{Z}mK~lHuP}Xv͵иϷииϷѹѹѹииϷζδͰ̬Ũwh_}^fvĶèʰͳ͵˵ɳȴȴɷʸ˸͹ηθз϶δδϲʬɩƦz_yoLrdAn`=qbArPf{æʮȬȮʰ϶з̵ɰʰɮǭǫ;vfuTtSxWanū̲˳ɱȰǯȰɱʲϷѹѹи̴ɱȰǭéɻȾǬɮɮʯɮǬɭ¥³|bxQtKuL{Rc|ǼĭŰ˶˶̷˶˴ʳɲȱɯɯɯɯɭɭɭȭȽŷnrQo_=iY5k[7l\8gY4hZ7pfBxUmūŭǯ˳͵̴ȰĬƮǯɱʲ̴̴͵͵Ȯ˱̲˱˴δ˲ƫĹs|WsJvKtExk>wkEeyneuXuWvXap~Ź˿İųŵǶȹν̺ǶıòŲȷ˸ɶʷ̹ͺ͹ʶdzűĮíźv{^oS|kM~nMrQ|[gpnbtRqLzlG|nI|Yl~¹ǫ̲Ϸ͵ɳDZʵ˸Ͽ;˸ȰǬǪʯ̳DZŲİ˿s\}QxPvP{Wjȯ͵Ϻι͹̷˷̴˴̵ѻϼϼλλͺͺͺ̹̹̹ͺͺ̹̹̹λͺͺ̹̹̹ͺͺͺλϼннϼͺ˸˸̹̹ͺλλλλλλλϼϼнннϼϼϼϼϼϼϼϼλλͺͺͺͺ̹̹̺̺̺ͻͻμμμѿоμͻͻͻͻμооооооооϽϽϽμμͻͻͻμμμμμμμμͻ̺˹˹ͻμͻ˹ʸ˹̺μϽϽϽϽннϼϼϼϼннϼͺͺϼϼλͺͺͺͺͺͺͺͺλλϼϼϼнннϼϼλλλͺλͺ˸ɶȵʷ˸ͺͺ̹̹̹˸˸ʹɻȺǹƸŵųIJïůƭƭŪŨŨħèìįDzɴ˶˶˶˶ɴɴʵ˶̷͸ιкϻлллϻϻϹϹ̴̴̴͵δϵѷҺϹмкϹͷ̶ͷθккѻѻѻѻккԾտԾкθͷDZynijlzŻêȲʴ˷κμͺιϹϹϹкккѻϹϹθθθϹкѻμ˸űǸemN|eEw`>p\9n[;paB|nSlëɱʲ˳̴̴̴̴̴̴ζиѹҺҺѹѸӺѸ϶϶жѷѵд˱˱ͲͲūĴlxWmIzgFoMxWcĹȮ̱йѺϼͺ͹͹ιιιϺϺллл̷͸ϺлллϺϹԿϹ̴δ˯ŷ}_yfHyfHzgIlQy^qǯ̵ηͶͶйϸ̵ͶͶͶͶͶͶͶζϹһҹ϶ͳαǬi{YmIkFtO}Xv̴ζζϷѹѹииѹѹѹиϷϵαɧýxe}[xUxYhǭʰ˳̶ɶȴȶɷɹȸȷλллϸ̶̳ʹͳˮ˭ȫĤ¼ozVvhEp`>m^=whGuVgǬȬɯ˱ʹʹ˴ɰȮȭȬæŶk~]rQrQzYizŹɰҼкͷʴʴ˵θкϹϹкϹθ̶ʴȲʱȭīƼŻǽ§ƫ˰̱˰ɮȭƬǭ{cwPsLuLzQb|Ƚ©ŰDzʵʵ˶˶˴ʳɲɲʰʰʰɯɭȬȬǬçǹkpOo_=iY5jZ6jZ6fX3gY6lb>xnK]yſĬ˳ζ͵ʲǯŭǯȰɱ˳̴͵͵͵Ȯ˱˱ɯǰʳɰƭĸr\wPvMrExk?{oGavwk_vWsTlOnQvXf|øȼʿðĴǶŹƹƻȼ;˼ȹǵƵǶʹ̻ȵʷ̹ͺ͹ʶdzİDZůønuX|kO{jLyiHvUhytavR{lEvhC~pK|YfyȿʮѹϷɳɳ͸λ;˸ɱȭɬ˰̳DZŲİ˿r[|PzRwQzVhɰилϺ̸˶ʶ˳̵Ͷкϼλͺ̹ͺͺͺ̹̹ͺͺͺͺ̹̹λͺͺ̹˸̹̹̹ͺλϼннϼλͺʷ˸̹λλλλλλλλϼϼнннλλλλλλλλͺͺͺͺͺͺͺͺ̺̺̺ͻͻμμμоϽμ̺̺ͻμϽооооооооϽϽϽμμͻͻͻͻͻͻͻͻͻͻͻͻ̺˹̺μϽμͻͻͻμϽϽϽϽμннϼϼϼϼннϼ̹ͺнϼλͺͺͺͺλͺͺͺλλϼϼϼнннϼϼλλλλλλ̹ɶɶʷ̹λͺ̹˸ʷɶȵǶƸŵ̾ɼƹöµƺŹĸ¶ûƾǿǿǿǿîƱʵ͸ϺҼѽѼлϺ̸˷ʴɳɱȰǯȰʰͳѷԼͷлҽѼι͸ͷϹккѻҼҼѻккѻӽӽкϹϹɳ¨l|X|sRxoPzqRctȾɳʴɵ̺μλιϹϹкккѻѻϹθθθθϹкѻͻʷű̽puUjHw_=qZ8jU6iX:pbGz_ŭǯȰʲ̴͵͵ζ˳͵ϷѹҺҺизպӷϴϴϵѷҶҶжδͲ̲ɯνz`rL}kGtR|[av˿Ū˴ϸнϼϼκιιιϺϺллл̷̷͸ιιιιϹԿѻζѷͱƸd{hJzeHzeH~iNuZmëɲ̵˴Ͷйй̵ηͶͶͶͶͶͶϷθѺѺε̲̯Ūȿg{YmI~jEsN|Uvª˳͵̴ϷҺҺ͵ζϷиѹиϷϵ̬ßxczUsPsMx[nʺūȮɲ˶̶ɶɷʺͽͿ˽ʻλѽҾл͸̵εеˮͮɫƦã}d|nKtdBo_=rdAxiHwWuŪȬʰ˱˲ʱʳ˲ǭɮɭͿt_tSqPsR}^r˳Ҽѽϻκ͹͹͹κʶɵʶ͹мм˷DZŭȮȱȮƯɯ˱ͳȯɰ˰ͲʰǭƪūūͿ{bwPrLsK|Uf}ȼ©ŰƳȳɴɴʵʳʳʳɲ̲˱˱ʰȬǫǫŪũĶe~nMqa?l\8l\8k[7i[6k]8lb>rhE{uQg{ýɱζʲŭĬƮǯȰɱ˳̴͵͵͵ɯͰ̲ȮŮǰDZŬǻq_zVvOsHrFvNcsoauTpQoPnRqTw\jźɿ¯ŵƺɼǾǽƾȿ˽˽ɼȹȹȹɷʸǴȵʷ˸˶ɴűïɳůg}oRyhL{jLxhG{Zs{gxT}nGwhAzlGqNwUh|̴˳DZɳιнο˸ɱɮʭ̱̳ȲƳű˿rZ{O{SxRzVg˲ҺлϺ̸˶ʶ˳̵Ͷкλͺ˸˸˸̹ͺ̹̹ͺλλͺ̹̹λͺ̹̹˸˸˸̹λλϼϼнϼϼλɶ˸̹λϼϼλλλλλϼϼнннλλλλλλλλͺͺͺͺͺͺͺͺ̺̺̺ͻͻμμμоμͻ̺̺ͻϽѿооооооооϽϽϽμμͻͻͻͻͻͻͻͻͻͻͻμͻ̺ͻϽооμϽϽооϽϽμͻннϼλλϼннϼ̹̹нϼλͺͺͺͺλͺͺͺλλϼϼϼнннϼϼλλλϼнϼͺʷ˷͹ϻϻϻ̹˸ɶǴƳIJʽʻƷ~Ź˿ĮdzʵкйηʴDZŭ먼üƼǬ˱͹лѾѼλιϻѽϷиҼӽӽҼϼλͺнѾмϼк˵ĬmyVxmMrgGtiKxZoǻʱ˵ɵ˹λλιϹϹккѻѻѻθθθθθϹѻѻ˹ȵƲëó}auTvbAo[:fS3bQ3gW=zlRwìŮȱ̴ζϷиʲ̴ϷѹҺѹϸϷ־Ӹδͳϵѷӹӷռϵ̲̲ͳūõjxV~nLuSzYzYewȼĮɴʷʷ˸ιηηϸϸйлѻ̷ͷͷͷθϹϹϹιйϹϷҷдȺdyfFu`Cs^AvaDmRdȮ˱̲δѷѷͳϵδδͳͳδδϵεѸзʹ˱ʮħǼauQzhBxd?nFwOn¨˳̲˳ϵӻԺ˳̲ζϵижϷϲƤjzUwmIxlF{mHz[sǬȮ˳θ̶˷˹μξͺκѼҽѼͷ̶϶ҷ̯̯ɬǪȫ¢tqPxhFrb@scBsdC|oOnèȬ̲̲ɱȯɳ̳ƪʭɫ˾jwVnJmIoMzYqȫζθθθ̶ʴȲưɳȲɳϹտϺʲǭʰʰȮƬǭɯʰɰɰʯ̱ɯƬĪĪé˽zavOrJsK~WfȽ©ïŰƯǰȱɲʳʳʳʳ̵˴ʳʰȮȫƪĩz~`}mLscAp`o\hW9iZ;uhH}`Ƽ˯ɯĬȰȰ˱ˮ̭Ħu\tMpIqHsJYoºÜʦгϳβ̰˯˯ʯʯƬƬȮ̲ϵδʰūƩǪǬƫƫǬʯͲ̱Ͳεε̳ɰŬ¨ĩ˼s~ZsMrJwLyQgµɾŨƭŬȬȬɯ˱ʳʳ˵˵ɳ̶ͷɱǯǬ¨üôn|[qP{mJ{mH|nItMqJzlEwgCueA{hGrQ}[uǪūªªůĮưɰ̳γϱαͲ͵˶˶ʳDZǮƬƪʼ~jyUsMtLoD}TbķõuiazZsSqPyU]ix°Ƿì̴ʴʳθмϺʹȷǶɸ˺̹ɶƳƳƳƳƳƳŲŲİƿmqUzgI|gHkKoP_tvdYvUoPmMoMpKpHvMUmū˵κ˻ɼȸɵʹε̳ɳıƳųʻsZvKuHuJzRe«ȵȵʹͼͺ͸̶ε϶ϼμͻ̺˹ʸʷʷθ̶ɳɳʴ̴̶˵˸˸˸̹̹ͺͺͺλλλλλλλλ˸˸˸ͺϼнϼͺλλλϼϼннѾϽооѿѿооϽμμμμμμμμϽϽϽϽϽϽϽϽμμμϽϽооооооооооооооϽϽμμμμμμμμμμμμμμϽϽоооѿѿѿооϽϽϽоооϽϽϽμμϽλϼϼϼϼλλͺͺͺ˸̹ͺϼѾѾҿҿннϼλλͺ̹̹ͽȸǶͻѾλ͹κ͹ι˸ƳǴųɼsjdemtxyvqlid\{WyXyY~apɷŰ͵зγǪžngc~_bsĶ̷лѽмѿλдѷѹѻннͿξνͺϼӾϺȰmwYveGr_AvcEyfH}_ʼɯǮ˵ѻλκι͸̷͸ϺлϺι͸ιιϺϺιι͸ɷ˹ɴƯƬĿkxoNf[;^P3bQ5dN7fK6|_M}lϾȲ̶ии϶зҹҼѻι͸κϼѻкͷ̶˵̶ϸйѹѹ϶̳ε϶ɰl}]sRkLlLqR~`j~DZ˶Ϲждггδδϴϴ϶ззѸййϸηϹ˲˴ͱɯ˭ʻ}^xeDubAta@vfErQcŨǪƩǪˮ̯ʭͰͰͰͰͰͰͰͰдɭʭʭǨʫť{{V|lHsd=vd<nCyNqøũϳѵдѵӷӷϳϳϳϳβͱ̰ˮßoyUwiFtfC{hGlK{XtĤȬˮͱϲγϴϵжѵѴггͱαͰ̯ʮʮʮ˯˰˰Ȯūū¶exhGp_AhW9gX9sfFz_ƫǭƿĬȰǯʰ̯ʫɾkyT~oH|mDrIsHVmżśɢɪɬʭʭʭʭȬȬŪŪǬ˰δͳɯǬåŦŨŨŨǪʯͲ˰̱ʹʹ˲ȯŬ¨ĩ˼r~ZrJrJvKyQgµ˾Ĩƫĩǫȫɭʮʳʳʷʷȵ˸̶ȲȯȭŪž±izYqO~pMtOyT_]XuN}iFyeBjHrOgy¢ĨéĬůíĮǮʯͱΰϰαͷ̷˶ʳɰȭǫƨ̽|k|UtNvNrGVjʽʽvf{[uQuOzRWdtòϿũȭβѶз˸ɶǴȵʷ˸ɶƳııı¯¯íĽ~b}lNvcCybC}dElN}[mvsfYxRuVrVtUxXxStLtIyMZmžɱ̹˻Ƚƶȴʴʹ˵ʴòŴòɺrYvKvIvI{QdĶíɶǵɹ̻ͺ̹͸ηϹϼμͻ̺˹ʸʷ˸θ˵ȲȲɱ˳˳˳˸˸˸̹̹ͺͺͺλλλλλλλλ˸˸˸ͺϼнϼͺͺͺλλϼϼϼнϽооѿѿооϽμμμμμμμμϽϽϽϽϽϽϽϽμμμϽϽооооооооооооооϽϽμμμμμμμμμμμμμϽϽϽоооѿѿѿоооϽϽоϽϽϽμμμͻϽλϼϼϼλλλλͺͺ˸̹λϼнѾѾѾннϼλλͺ̹̹ͼƷij˹ҿҿϺ͸ɴʳɵưƹlc][bqȾžğğÝ¿{l^xVzjHueDxeGpThůϹѸγŨi}pPwhIsdEpaBteFtWrǽưιҽлλѼҿѽϵϵϸϺнн̼Ͽν˸ʷϺϸʰpxZwfHr_AubDwdFy[}ǹȮǮ˵Ҽλмι̷˶͸ϺлϺι͸ιιϺϺιι͸ȶʸɴȱ˱ǩy~uVj_A^O2bN3`J3^C.jM;}bOr²ë̳ѷиʹѸҹҺҺϺϺϻѽкϹͷ̶˵̶ηϸии϶̳εҹ϶ɮ{fsR{fG{dDgHlNsWlȹů˶ηϵϳββδͳͳͳззззйϸηͶ̶ɰʳ̰ǭȪƷy|[wdCvcBubAueD}mLz[|»žĽƿħȫǪȫȫȫȫȫȫȫȫ˯ĨũŨäʿwyT|lHuf?yg?oGyNpçβдϳдҶѵβϳϳββ̰˯ʭžkuQtfCrdAzgFlKxWrũȫ˯̲ͲͲ̲̲̰̯̯̯ϳϲϲαͱͱͱβ˰ʯƬéé÷exhGq`BiX:fW8pcCuZ§ūƿĬȰƮǭˮƧ{_}oJxiByjCpIpG{TkúǿžǾȿ¥ħŨŨ¦¦¦èǬʯʯǭƫĨŨƩŨŨǪʯͲʯ˰̳̳ɳưíèʻq~ZrJqIvKyQgö˽çĩèƪǪȬʮɲʳʷʷȵʷʴDZǯɯūȿyauSoMsP~XcvwrbrOzfC{dB}iFxRd{żéŭëĬƭȭ˰Ͱϰϲ͵̷̷˴ʱȭǫƨ˼|k|UvPxPtIYsĤɻĵvi}\vRsMtL{S]qɻçʮͳͲ͸˶ɴȳɴʵɴȳįįîoxYyhJubBxaBzcCmNzXfhb~YzTxT~aeimh\zQwMyO_p¨ȳɷʿǾǷdzɳ˲˵ʴ±òǸqXuJwJvK|ReøĮ˸ȶɹ̻̹̹͸ͷϹϼμͻ̺˹˹˸˸̶ʴDZưȰʲ˳˳˸˸˸̹̹ͺͺͺλλͺͺͺͺλλ˸˸˸ͺϼнϼͺͺͺͺͺλλλλϽооѿѿооϽμμμμμμμμϽϽϽϽϽϽϽϽμμμϽϽооооооооооооооϽϽμμμμμμμμμμμμϽϽϽооооѿѿѿѿоооϽϽμμμͻͻͻ̺μλϼλλλλλλλλͺͺλϼннннϼϼϼλλͺͺͺͽʺɷͺҿӾϺ̵ŮŬªļq^[`fpϮЮϯϮͬˬɪȩǦޟ¼rgqPvgHpaDwgMy`yưѻѺ̳é{^m`@l]>gX9dU6gX;tdJ|bĸĬ϶ԾҼͷθѻҼηηϺлнѿξѾн̷ʵͶ϶ͳqxZwfHr_AtaCtaCuWwõƬǮ˵ѻϼѽ͸˶˶̷ιлϺι͸ιιϺϺιι͸ʸ˹ɴǰɯƩx|sTg\>\M0`L3aK4^E/cH5nUAmXm÷ǮжиʹѸҹҺҺѼлммϹθͷ˵˵˵Ͷηии϶̳ʹѸз̱ŨluT{fGzcC}dE|cEhLz_~Ȳ̵δβͱͱ̲̲̲̲϶зззϸη̵̵˵Ȳʳ̲ǭƨ³vvVs`?ta@r_>o_>scB|mNpüƿƿƿƿƿƿžçĽƿžĻ·twR|lHvg@yg?nFwLm̰ϳβϳддββββͱ̰ʮʭßkuQtfCrdAxeD|iH}nMhøȽ¥ƬǭȭǬūĪççħħͱͰͰ̯̰̰ͱβɮȭéʾʾȼ`vfEq`BkZ}qKavȾĪūƬǯɮʭ̯ία͵̷̵̵ʱȭƪĤȹyg{TuMxPuJZt˾¥ĩϿȹm^vRqKrLuQ~^oǼħǫȬ͸˶ɴȳȳȳȳȳįŰįzcrPyiHxeEydE}fGkLuU_`|[yX~Zcv}ucX|TZdsʿŲȿȸȴȲʱɳɳ¯ıƷmUvHvKuJ{SføĮ˸ɶʺ̻̹˸͸θкμͻ̺˹˹˹˸˸˵ȲůĮƮɱʲ˳˸˸˸̹̹ͺͺͺϼλͺ̹̹ͺλϼ˸˸˸ͺϼнϼͺͺͺͺͺͺͺͺͺϽооѿѿооϽμμμμμμμμμμμμμμμμμμμϽϽоооϽϽϽϽϽϽϽϽоооϽϽμμμϽϽϽϽϽϽϽϽϽϽϽоооѿѿѿѿѿоооμμͻͻͻ̺̺̺ͻλϼλλλλλλλλλλϼнннϼϼϼϼλλλλͺͺͽϿҿѾлϺͶ˲ƭſq~bwY|Y`o~ùȩҵҴгϲͲ̳̳ͱ̮ʭɫǪ£{fzZ}oRuYj¸˴ӽѺ˲ébpbEl]>fW8aR5cT7k[A}mSsͳּԺϷϷѹӻϺϺлнппнҿ͹˴εѵαovXudFq^@s`Bq^@qSrƬǮ˵ѻλѽ̷ʵʵ̷ιлϺι͸ιιϺϺιι͸̹ͺɴƯǭ¦wxoPhZ=\L2aM4eO8cJ4dI4jQ;p\DrYtǽͱҸѷϵϵиҺһһмϻͷεʹ̳̳ͳͳͳϵжδʰ˰γγʭɫǾmuT{fG{dE{dEx`D{cGrWsȼǮδ̰̰̰̰˱˱ʰʰεε϶϶ηͶ̵˴̶ɳ˴δȮƨtsSq^>r_?o\}kCrGgƿ˯βͱβϳβͱͱͱͱ̰˯ɭɬjuQugDpb?s`?s`?sdC^~»éêýĻü¥¥¦çĨŪèɽ÷¶yZrbAq`Bn]?l]>sfFsX{ƫʰéǯǯª¨ĻrzYxjGueCwgCxhFzjHwVn¸ȾŪ˰ȯɰɳʴɳưíª§ɺp|XpJpHsKyShķ˽ȾçħƪȬȱɲɴɴȳȳDzŮȮɭ¦ø}grQyiGwgE}oL`vķy^nM{gFxdAkY3rc:rK_sŷǬȬɯʭˮˮˮ̴̷̵̵̱ʱǬĨ¢ôtcxQsKxMtI[uƻ΍˽˻órawUpN~kJrR|^oƻ¦ȫɳɳɳȲDZưDZDZĮůů¼pyWylJwgFtdCubBxcDydEmNuVxXwY|^jwƶ̽ɺyj_[\cuɾɿȿɼɷȲȯȯDZðıŷmTvHtIsIySgĭʷʷ˸̻˸ʷ̷θкμͻ̺˹˹˹̹̹˵ȲĮíŭȰ˳˳˸˸˸̹̹ͺͺͺϼλ̹˸˸̹λϼ˸˸˸ͺϼнϼͺͺͺͺͺͺͺͺͺϽооѿѿооϽμμμμμμμμμμμμμμμμμμμϽϽоооϽϽϽϽϽϽϽϽоооϽϽμμμϽϽϽϽϽϽϽϽϽϽоооѿѿѿѿѿѿооμμͻͻͻ̺̺̺ͻλϼͺͺλλλλϼϼϼϼнннϼλλλλλλλλλϻνҿι̵ʱǭscrTlN}mL^lŸƥȩɬͰ̵̲̳̳͸ιϸздϴϲ̯ä~smq¨иӽѺ˲ƬnykNm^?dU6_P3`Q4dT:m]Cu[qȿͰϴγѷӹӹллннννϿϿϽӿϹ϶ҶвʬjqSraCp]?r_Ao\>nPoƬȯ˵кͺм˶ʵɴ˶ιϺϺι͸ιιϺϺιι͸˸̷ɲƯǭtwlPhZ=^N4cO6gQ:fM7fK6hP8hW=wgM}bx§γҸͳͳζиһһϻͷʹʹ̳̳ͳ̲̲̲ͳϵδʰʯ̱˰ȫɬƽgqPyfFwbCw`Av^Bu`C~jOe§β˯˯˯ʮʰʰɯɯ˳̳ʹεηͶͶ̵̶ʴͶϵɯũqtTr_?s`@p]=jY;n]?yjKo禦ƿƿžĽĽĽĽĽütvS{kGte>uc=zh@mEbüɭͱ̰ͱβ̰̰̰̰̰˯ʮɭȫºd{qMqc@m_m^?tgGuZ~¸ɮ˱Īǯǯª¨ȿútzYykHvfDxhFxhFpOc¸§ȭȯɰʴʴʴDZůëȹpzYoIoIrJySjķ˽Ƽ¦æũǭǰȱɴɴȳȱǰǭȬǪĽt~]~kJvfDueC{mJazµȻw~\nMxeDr^;mZ2sb7}kCxQbvǹĨǪȫˮ̯ˮʭȮʳʳ˴̲ɮǪ¤p`uLqIwLtI[wʿììοǰêǷtcwV}mLnNpP{]nǹŧůDZɳɳȲDZDZȲưưưů¬þhrOuhErcBo_>n^=s`BvcE}iNqVw[{ak{ĵȼªūũɾ~kb\]fvúƺȶDZƭŬƭðį¶kTwIsIrJxTgĭ˵ʷ˸˺˸ʷ̷κѽͻͻ̺˹˹̺̹ͺ˵ȲĮíŭɱ̴͵˸˸˸̹̹ͺͺͺϼͺ˸ʷʷ˸ͺϼ˸˸˸ͺϼнϼͺλλλλͺͺͺͺϽооѿѿооϽμμμμμμμμͻͻͻͻͻͻͻͻμμμϽϽоооμμμμμμμμоооϽϽμμμооооооооооооѿѿѿѿѿѿѿϽμμμͻͻͻ̺μλλͺͺͺλλϼϼϼнннннϼͺͺλλλλλλλϻͼӿλ͸̶ȰhuT{hHwcB~jIuTm}ƥ̭̯˯ʰ˳˵˷˷̹̹ͼͺѼйϹѹҸβƩſȮзӼйʹ˱Ȯt}oRk\?bS6]N1`Q4aQ7cS:oaGrWtǫ̱ϴϳκκͻ̺̻˺ʹ˹ͻϽѽкӺӸͰĦd}lNn]?mZn]?udFuX}ƿʮ̰ʮɭɭȬǫǫƪũũĨçç¨ĪĨƽȿʿxzW}mIte>tbs`@q^>o^@yhJ}`ùƪдд̰̰ͱͱ˯˯ʮɭȬǫǫƪǭƬ˱ʰ§Ʃƨƻ~|[oMueAtb{iA_ǫ˯˯̰̰˯˯˯˯˯ʮɭȬǪ|\siEn`=oa>ubAwdCxYrǼ˿ūɲ͵з̳ŭĽžɿ¦¥¥æĨƪȬɭɮɮǭĪé˿ewgFn]?fU7eV7qdDx]ƫȮƮȰŭǭǪȿqvWvgFvfEzjIveGuYyðIJƲ˱˯̰˯ʮƪ§ȭȭĪʾ÷õ÷¸¸Ƽêȯɰ˲˵̶˷ɵdzůǷoyXoKnHrJySkķ˽˿ȽºæĪƬǰȱȳɴDZȲȯǬȫäaqLweAvfByiG~oNilwXpOyiHr`mZ<nPpéīȲͷ˸͹͸͸͸͸͸͸͸͸̷̷̷͸ιϺлѼϷγǭ»nufOeU>eS;`N6^J2dN6lV>oZ?lX=hY8gZ8h[9naA}pPg¦ūǯɰ˵Ϲηȱɯˮ̯̯ˮɬƪũʮβͱɭʮͱʮ¥zctU|kMwfHvcEwdF}jLzgIxgIziK|mNzXtϧƫĩŬʰ̲ȱȰɱ͵кѻθʴ̸ɵ̷ͶǯelNmZqSréêů˵ɶ̸͸͸ιιιιιι̷̷͸͸ιϺлҺγ̱ǭ~gqaJeS=cQ;cO7fR:u_GnSqVoQrdAm`=j]:m`>tgEuUmȺçȮɱʴͷͶ˱˱̯̯̯ˮʭǫǫ˯βͱ˯ͱϳȬǾr{\qRnP}lNziKyhJmOziKwfHwgFxiHtQf{ƼĩŪŬȯ̲̲ɲɲ˳ζѻѻϹ˵ͺɶ˸̷ưgmRo\>p]?mZwjJm˰δƬëɱ͵ʰȫ£{}^zkLveGxgIudHoSn;Ƴɵʵ͵ͳͳ̲˱˱̲ͳʱ˲̳ʱƮĬƮȰƫèèĩī©ů˵ϹϹммϻ̸ɵDZ«ȸnwW~kJ|jFmGwSj´̾ªûž¨ĭȱɳ˵ɳɰƬũæɼnrMiBxd?ygCxhF|oOe}zoawYtTvTzW`bcZsMqJyR[rĺƦ̭̯ʭȮ˱̲ʮȫƧɾdWuJtHxLyKbĪɱȲȲȶȶȶʸ̸͸ʴĪùnb}\z\wY~`kĽ¨ūʰϵʰȮƬȮ̲Īv~^~qOwjH}pPxXbrǺʽǷͼȸǶŲƱDZ˲̯̮Ʀȿpc_ezƿǪʭɯȲȲîùvc[]dpĪ˲̳ǰȳȵɶ˸ͺμϽͻ̺̺̺̺ͻͺλθʴůůȰ̴ζζλλλλλϼнннλ̹ʷʷ˸̹ͺͺ̹˸˸˸̹λϼҿҿѾѾѾнннѿоϽϽμμ̺̺̺̺̺̺̺̺˹̺̺ͻμϽооѿѿѿѿоϽμμμμμμμμϽμμμͻͻ̺̺ͻͻμμϽϽϽоϽϽϽоооѿѿѿѿѿооϽϽϽѿѿѿѿѿѿѿѿонѾҿѾϼͺͺͺλϼϼλλϼѾѾϼλͺͺͺͺ̹̹̹̹κϻмҼ͹ɽhyhNlX=dQ3hS4hS4hT3}iHdüç禦ĪƬǰȱȰŰưƱȲʴɳɱʰʰȭǬɬ̯ɬˮαγ˲˳͵ϺϹиȱ¸voaFaQ7`P6`P7bR9dT=jZCufQsY{\yXxW{ZeuĹɾĪǭDZűűƲ˸λλ͸иѹδȬc{jLp_Aq^@s`Bp]?qSqéêĮɳǴʶ̷͸͸ιιϺϺϺ̷̷͸ιιϺлѹ˰ͯɮt_iXDcQ=cN9dP8r^Fw^qvluPthBk_9k^;l^;pa@vWpŷĪȮȯɰ˴̲ͳα̯ˮʭʭʭʭ˯˯ʮ˯γ̱zetU}nOqTtWuXwZ{]tUzkJvgFvhE}oJ}Xgɽéȯ˲˲ɰʳʳ˴ͶѻѻϹͷλʷ˸˶ůjmRo[@p\AmY>p_CpTmĩɯ˱ʰ̲ѷѷϵжжжжϵϵϵϵйɲɲʰƭ̱̰ʿzYzjHqa?tb>zhD~lFd»˯дβββͱ̰˯˯̰ͱ̰ɭƩu]uQ|nKvhEzgFmL~a̾ɭƮȮǰDZʶʴȲƮ«Īƭɯ̰αϲα˯ʮ˯ͱʯͲ˱ǭǭzz[wgFiX:iX:o`ArRxȭϴѷʰǯ̴иδɮħeqRyiHxhGwfH}lP~dôíȲ̵ϵγʰɯ˱ͳϵϵɯʰ˲ʱɰɰʱ˲ʱǮƭǮŬêů˵ѽѽѽϻͻ˹ɷǴìȸowWlK}kGmItPfõͿªļĽľſ¨ĭǰɳ˵ȰɱɯǪ£ôz`mE{ep]?n[=nPjĪŬDZʴƳɵʵ˶˶̷͸͸ιι̷͸͸ιϺϺϺи̱ΰ˰vyjUeS?dOnZ?kWsa=zhDmGdṵ̈ѵϳϳϳͱ̰˯ʮ˯̰˯ɭǪzbxT|nKvhEyfE}jIx[{ȺȮɱɲʵ˵ʶʶɳDZŮŮǮɯɭ̯αα˯ɭɭʮ˰γ̲ȮūȼmsTscBhW9l[=xiJ_Ƚ̱ѶҸ̲ɱ͵иϵγɬƻp{\~nMyiHoN~mOx[uǻƭ̲жγɯɯ˱δϵϵɯʰʱʱʱʱʱʱ̳ɰǮȯƭêůʴϻϻκ͹˹ʸʸȵĭʺpyYmL~lHnJqMb´Ϳ¬ʿŽſſ¨ìŮưDZƮɯʯŨʻkySlC|gk[AhX?`P7_O8scLq}rheegjw·Ƚůɵʸ̺̻ɸʷ̹ɶì}`xgIkZ}gPn}^xoDl`6pd}lNw[rƺȯ̵δгбί̭ˬˬ˭̮ɬŨƩˮ˯ʿ|`yXsTrSwZf~~fvS|nIqJvMyTj|ƺƮ͵εʱDZȲʳ˴ʵʵ˵˸н˸ʷɴĮpmRnZ?o[@lX=n]CnTkīʳͶ˴ʳ̵ηͶʳʳʳʳʳʳʳʳ˶ɴηϸʱ˰ǭźz[zjIqa?tb>zhDmIeṵ̈ѵдϳϳβ˯ʮɭʮ˯˯ɭǪhyU{mJvhEzgF}jIwZv´Ƭʲ˴̷ͷ̸˷ʴʴɲȱȯȮƪʭαϲͱ̰˯̰Ͳеͳū˿}|a{lMqa@m\>wfHyZqçͲеж̲˳͵ϷϷѸ̱{ftS{kJrQ}mLrQf¥ɭͳʹ˲˲˳̴̴̴̵̵ͶͶͷ̶ʴɳ˲ȯǮǮĮïɵɵɵɷʸʸʸ˹ʷƱ̼t{]oNnJpLnM_|Ϳʿªè¨¨«¬¬ƮʰɯĴ~csKlA}h={h@pJ}\n¹úƻĹsk\bn~ķ;ħĩȮȬƪŨäƻ{]{RtIuIzL{MdĻʫϲͳ̵͸̷Ѿλͷ̶ʹʹ˰ʭ¤ĽĻĹ|oprv|ʿũǫʿqcu\y`můưȴʷʸȶƵĶŶ̷̷̹͸̸ʴưī¨éæ»|mnt|ÿŨ˵˸˺ɶ¬ƽø·ĺȮ̲γ˲ȯ̴͵͸͸̹̹ʺɹ̺μоѿоͻʷǴľýľªȰ̴λϼнѾҿѾѾнͺͺͺͺͺͺλλͺͺλϼϼϼϼϼѾѾнннϼϼϼоϽϽϽϽμμμͻͻͻͻͻͻͻͻоооооѿѿѿооѿѿооϽμμμμμμμμμμμμμμμμμϽϽϽϽϽϽϽϽμμμϽϽϽооооооооооооооооооϽϼнϼλͺ̹̹ͺλϼϼλλϼѾѾϼλ˸˸˸˸̹̹̹̹ʵʶɶȵϾ~it^IlVAmWBnXCjV>hV>rbIy`rƽȿǾĻĹãŻyth|]}tUuUyY`o~ɾɮ˱ɳ˵лҽϼҾкįrl^DxhN~nTzjQk[BeU>xhQyȽĹ{rie_iyƺêȲɷ˹˺ɸɸʹǵ|_veGiX:hU7hU7dQ3udFz^ū˲ͷθɶ̸ȳȳȳȳȳȳȳȳ̷͸ϺллϺ͸ζϱа˭~r\iXFaN=cN=dOlUCr\E|hOvX}_\YX^bZnJs_>n[;taC{jNx^sžȱ˰̯̭ͮͮͮˬȪȪƩħŨȫʿxi|[xVzYgyȺȭ¥ʽp]~UyNtMuXgxĬ̳̳Ȳʴ̵˴ȳȳʵ̹ϼʷʹ˶ŲªpsXt`EuaFq]BsbHsYpêʳηʳƯǰʳʳ̵̵̵ͶͶηηη˶ʵѼһ̳ͲʰɾdqPwgFxeD|iHmIdžβӷѵѵѵд˯ɭǫȬɭʮʮɬzdwT~pM~kJ}jIy\k÷ů̵ιϼϻ͹˸̶˴ɲĮ¨Ĩɬϲҵѵϳͱͱ̱еʰĸy{aseJvgHyiHtVnǺèɭʯʯ̲ϵѹиζ͵θʹǭôk~\qO|lHzlGuR`pêεҹ϶̳ʲ̴͵͵̵ϸѻҼϹ̶ȲůĮůíİʶűƲɷ˹̺˹ɹǶŰʺsy[nMlKoKmL{ZuɺʾƱëèǿǿʿŮǬ{h{VpHk>nAvMb}ŹƻŽǿª¨ƭ¬ŨȪĢ|pfdip}ݍƪǪǪģƻ~^VxKwK{M|KcûɨίͱδζζкϹθϸ϶ʹʮǫǫŨé¦μ˹ŷ´´Ķɻ̾Ⱥxss{ʴ˷ʵι͸ͺͺ̺˹ǶŴƲůůưɳɳȴDZĮƭǮ˯ʯǩÿüþÞȪɱʸ˻ʻȶȯɭʮʹͶͶ̴̳˲˲̲˱͵ϺѼѾѾϿ;μͻ̺ɷIJƿ¼ľ¬ªĬɱ͵˸ͺϼнѾнϼͺ̹ͺͺλϼϼϼϼͺλнѾѾѾнϼнннϼϼϼλλμμμμμμμμμμμμμμμμоооϽϽϽϽϽϽооооϽμͻμμμμμμμμʸ˹˹˹̺̺ͻͻμμͻͻ̺̺̺˹ͻͻμμμϽϽϽϽϽϽооѿѿѿϽϽϽϽϽϽϽϽμͺͺ̹˸˸˸̹λнҿѾнϼϼннλ˸ʷʷʷ˸˸̷̹̹̹θϹ͸ygoUDlRArXGpYGkVCq_K|eŹɿ§ɬͰϲαʮɫƨťƣȤȢĞȼ}e|qQtjGtjG}sPh¦ϴϵʲ˵ϻҽнιѽкpn^ErYimv]yiRw`γдѴͰäx_]Z_mëȲ˷̺ξϿʼIJbxgKjY=gV:fU7bQ3udFgǭȯɲ˵˸ѽͷ̶̶˵ɳȲDZDZ̷͸ϺйлϸͶ̲ɪƧ~egXAYH4\I8_J7fQ>kVAlX@p\CyeJnPrQ`m{{ivTxeEp]=o\>o^BudHv\zĪʭ̮̯ͯˮʫǩƨ¥ƿä·k_~]yVzYnŷ¨ȮūũƸk^~TtM{lMw\k{ǻūǬʱ̳ʹ̵ɲȳ˶͸κʶ̶̵DZ¨nsWs`BtaCp\AraErXn©ʳͶɲŮƯȱɲʳ˴˴̵̵ͶͶͶ͸̷ѼѺɳͲͳ§gtSyiGyfE|jFmIcžδӷҸѵѷд˱ɭǭǫɯʮʰɬl~\tS~nLzjHrT`yĪγҹԽϷ͵˵˳ʳɯ«žǭ̲ϵδ˱ɭȬ̯гȬk~sUk`BuhH|mL`|ķŨ˯˰ɮȯ̲йӽѻθ̷Ѻ϶̱ɭͿyiuSoK|lHqLxS^lzþͳԷж̯˱ͱǭƪȮ˰зѶε˲ʱǮƭǮŬêů˵DZɳ˷͹͹˷ɵdzĬȺqxZlL}jIlKmLyYvƺȽ­ȳ¥¨Ǹp`vRmGnCtIZsɾêʿ«Ĭ馥ťŨƮ¬Ũˮ̬ĠŻyg^^cq¹ƧȪȨŸŸ}`VxOxM~Q|Pgúʫαͱͳ͵̴θθϹкҹ϶ʱƭīīǿŭŮĭƬéʾʾʿźźöʿȵͺ͹ʵϺϺͺͺ̺ʸƵŲŲůíĮůưűİĮĮƭʱʹͱȭæ£ƻĺĠßǧͰȲɷ˻˹ʷɳ̱Ͳϸη͵˳ʱʱʹ϶϶йһҾѽϽ̺ʹͺ̹ɶŲƿſªƮʲ˵̹λннϼλ̹̹̹λϼϼϼϼϼͺϼнѾҿѾнннннϼϼλλλμμμμμμμμμμμμμμμμϽϽϽϽϽϽϽϽϽооооϽμͻμμμμμμμμɷʸʸ˹˹̺̺̺ͻͻͻ̺̺˹˹ʸͻͻͻμμϽϽϽϽϽϽооѿѿѿϽϽϽϽϽϽϽϽμλλ˸˸˸̹̹̹ͺͺλͺ̹ͺλλ̹ʷƳǴȵʷ̹λϼѽηϹտϻ˼{ijP?tYFfM9mWBmXCgU?hĵǽũƨɬ̯δηͶ̵ʳ̲̳ͲͰ̭˪ʩ{nksʭ̱дϵͳ˳̴θѼ͸ϻιĬqm\BpVf|bx^seK~dĽδҸԺҸ˱¶vj`_afnz˾ɮȰDZɸǷ̿w_vdLm\BkZ@hYn]Ao^BudH{_Ŵ̾ɯεϴ̲ɯɬǪƩĥȽ·q`}Z|Ybo¬DzįŽŽŻvc~YvStSyXdtĶǩͱͲͲʹ˵˵ʴ˴Ѹʱʯˮƫ`}mLtdCp]=q^@q`B}lPnêĭƯǰȱɲɲɲʳ˴̵̵ηηϸйϹкιʳɳ̱Ȯèn|ZmI{gD~jElG`Ľ̵ҸйжϸϵǰȮȱɯǰȮƯƬǪʿl|\pO{lKsOzVhŶȨΰίɬũũɬɬĪǴʷʸdzdz˵ʹˮ̭âj{V{sL|tO~vRbzƽǪͲҸδηϺлнϼ̻̹Ͷ˱ɭǬƩǼl~\tPtPtPuQ\m~ƽȧͮέбұˬʩʫʪ˭̬ΰϱˮȭŪè§èɿŪǬȭȭƫũŨqwX}lNmMoPmN}_xŻ«ŭūƿãäĥĥä£ƧǼzb|rOwmJzpMzpM\rç¨çĨĨũǫǩťáǺoa^cköǺáʨŸze{VyT{VXf{ƿȮ̵Ͷ˴˶˶˶ͷϹϹͷʴưíưưůůŰDzȳɴ˶˶˶ʵȳƯĭìĽ»±ðIJǵ˹нϼϼѾоϽ̺ʸȵƳůſſĮDZDZDZȲʴ˵˲ǯĬĭƯɯ˱˱ʰɱDZȵɶʸ̹̹͸͸̷ʴʴɳʴʴ̶ͷθϹθͷ˵˵˸̹θ̴ζζɱª¨ĪƬȮɱ̶˸̹λλλλλɶ˸λѾѾнλ̹̺ͻоѿоϽϽϽϽϽϽϽϽμμμμμμμμϽϽϽμμͻͻͻμμμμμμμμϽϽϽμμͻͻͻμμμͻͻ̺̺̺̺̺˹ʸʸʸ˹˹̺̺̺̺̺̺̺̺̺ͻϽооϽͻ̺ͻͻμϽϽоѿѿμμμμμμμμμͺλ˸˸̹̹̹ͺͺͺλͺ̹ͺλλ̹ʷǴǴɶʷ̹ͺϼмѹиѻ̷ȹygfL;nS@bI5jT?kVAdR:|b¸¤ŦǪ˰ζкϼλʷ˸̷Ͷδ̰˯ʭ¥ýžʮͱϳδ̲˱̲ζй͸Ϲηëpn]ClQz^sYqVn`F~cý͵ε϶ζ˴ȱ¬˿zmd_Y[czɼ̾Ʊó˻r\q_GhV>gW=fX=cU8ujLhǫȮ˳ͷͺҾкйϸϸηη϶϶з϶ͳϵжϵ˯ȫǩź}{[qcFi[>cU:\N3^N4]M3^N5bR9m]DoViuɿȾh~pUm]Cl\Bm\@p_CnPj̻ū̳ζʰƬǪȫɩäżxh[Y]j|ƼDzȵIJƿǿüpczUvQvS}ZftǺŧɭ̱Ͳ̳ʴ˵Ͷӷ̰ˮ˭Ǫ_}mKscAn[;l\;o^@|kMnīŮǰȱʳ˴˴˴ʳʳ˴̵̵ͶͶηϹкι˴˲̱Ȯ¥n~\nJ}iDmHnGcļ̵йϸϸϸηǰǰǰǰǰƯŮƬǭ¦øzfwW~qOrJuM[nɼƣʪɨȫȫʭɬƩéðŵǷǵʶͷʹɬäo]zPzP}Vfuæʭδѷηηϼϼͼ̻˼˺Ϸ̲ɯɭǬ¥øna[zVtOxS`q|ݟǤ̩Э̩̩ͪΫϬЭЭϯȩŨĻżæǪǪƩħƧszZoQoQpSqTc~ȾĪƭŪƿ¡âģâ¡p}\vmLtkJypO~xXi¥ũ稨éêĪé§èƫǬǪǧãʽ~ofder~̽ȥâŷ}k_^bfuȾįʵ͸͸˶˶˶˶͸ιι̷ɴŰîðıŲǴȵȵǴǴʸʸʸɶȵǴưưſſľýľŨǭǴȶɷȶǵȶʸ̺ͻμϽоϽ̺ʸȵƳů¬íưȲʴɳɳ˵̶̶ʴDZǴȵʷ̹̹˸ʷɶɶʷʷ˸˸˸˸˸ʴ˵˵˵̶̶̶̶θͷͷͷͷͷθϷζϵϵ˱Ƭ¨é¨¨¨éūǭɱ˵˸̹ͺλλλλʷ̹ϼѾҿнλͺ̺ͻоѿоϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽμμͻͻͻμμμμμμμμϽϽϽμμͻͻͻμμμͻͻ̺̺̺̺˹˹ʸ˹˹̺̺ͻͻͻͻͻͻͻͻͻͻϽϽϽϽͻͻͻͻμϽϽоѿѿμμμμμμμμμ̹ͺ̹̹̹ͺͺͺλλϼͺͺͺλλ̹ʷǴȵɶʷ̹ͺλмԽϸϹʵƺ{hcL:gN:`G3hR=jU@dR:u[ƿæƬ˱ζкϼϽʹ̹͸ηδͱ˯ʭʭȫħžũʮ˱̲ͳ̵ʳʳ˴͵ϸ̷Ϲηªon]CxdInRudJqcHgY?}bſζ̳ɱ˳Ͷϸͷ˵̾ti\~V}UixĴννǶoYn\DeS;gW=i[@fX;vkMgǫɯ˳̶̹ѽѻѺйϸϸϸ϶з˱̲ϵждβ˯ȫħgseHgYvhNnžū̵ѺѼϼ̻˺ͼνϼлϸδ̲ʰʰ˯̲˱˱̲δйͶ˶ʵʵʵ˶̹̹ι͸ѻη˿kkZ@eQ6hW;\K1^P5^P6~cȮи̴ʲ͵йһηɲˬˬȩ¡¹xk_}[yZ|_l~s{iSo]EfTaM2eT8[J0^P5^P6~cɯ̴˳˳ϷѺһη˳̯ϱв̬ƦĻoexYsTvZcinyasaKlZBdR:dT:k]Bk]@{pRlͱϵѹѻλѾͷͶ̵̵˲˲̳̱˱ʰʮ˯˯ƪĽxsXkWaS9{`éѺһһйζ͵϶з˲϶εʯ˰гͱƩȿ~iwY}lNyfHxdIucKiWAlZBdR:_O5eWm\BfX=gY?{`ϹϹϸ̵̴͵ҹֽҺѹиʹʹ̱˱̰ƫɾõiyZ~nMn]?q_G`N8gU=iW?eU;cU:eW:~sUoβ̲˳̶˸Ϲͷ϶δͳ˱ʰʰ˯ʮʮˮˮ¥qx\l[Ap\Ct`GwfLoWh}ĺɿĭȰɰɰ̳϶̳̳˳˳ʳ˱ʱʱƬ÷j~nTscIp_Eo_E~sUaqxjltÝɣɥʭɮɰ˱̴͵ϹѺ˴˵˳ʲʰȬƫƩ̬̩ǧ{vqyȪ˭̯ε϶Ѹδƪyjgfiwĺǫδ˴ʳ̵ηϸ̵ɲʳʳɲʳ˴ͶйѺҼθʴ˵εʹȭè~j__dp~Ⱦìʳϸййϸηʳʳʳʳɲȱǰǰ̹̹ͺͺ̸̺͹͹̰ͮ˯ʫɬȫǪŪƪƪũç¨èŨǪǪŪƫǬɮʹʹ̶ͷͷθθθдϳϵδδϵϵжѺѺѺййϸϺклѼҽӾһйͶ˴ʱʱɰȯɰʯʲ̲˯˯˯ʮɭǫƪũȬƪĨ¦¦ũȬʮʰȮū¨ſſſ¨Īǭʰ̲̲ͳ̲̲˱ɭǫŨ礤ũȮ̵̳ɴı¬Ĭéé¨ſ¨éūƬƬȬɭ˯̰˱ɯƯŮì«­Űȳ˳γΰ̮ͯͱβ϶зηηιϺλϼϾнκͷ̶ʴȲưůĮůưDZȲʴ˵̶̶͸̷̷˶˶̷̷͸͸ιιιι͸̷˶ȵȵȵȵȵȵȵȵ˱̴̴̴̲˳ʴɳ˸˹̼ͽ̿Ͽμͻ̺˹˹˹˹̺˹̺̺ͻͻ̺̺˸˸˵ʴɳɳɳɳɳ˵̶θϹкϹϹθ̹ͺͺλλͺͺ̹ɶʷʷ˸˸ʷʷɶʴʴʴʴɳȲDZưůĮí¬¬íůưȲɳ˵ͷθθͷͷ̶ʴưí¬íðıŲǴɶ˸̹ͺʷ˸˸̹̹ͺͺͺμμμϽϽоооϽϽϽϽϽϽϽϽоооооооооϽͻ̺̺ͻϽоͻͻͻͻͻͻ̺̺ϽμμͻͻμμϽ̺˹ʸɷɷ˹̺ͻоϽϽμμͻͻͻμμμμμμμμμμμμμμμμμμμϽϽооооѿѿѿѿоϽμоннλλλλλλλλλͺ̹ͺλͺʷȵɶɶȵȵȵȵɶʷ˵ηͷʴ˳˴íƺz{d|lUqaHjZAj\BoaGrcLq\rôé˱е̱Ѷӷδ˱˳ʲǯɲ̵ϸѺһҽӾͺϼнϼ̻˺˺μѼѼϹ̵˿{aq`Fo[@l[?o^Dk]Bn`FgĪθϹϸηζϷԻֽӻҺѹηεʹ̴̲ͳŪpa{lMr`HaO9hV>jX@gW=dV;eW:}rTrϳδζθ̹к϶϶δͳ˱˱˯̰ʮɭȫæeynRqaGudJ}lRx^p÷ŻĬƮǮɰ̳϶̳̳˳˳ʳʳʱʱʲʾ{eoUwfLqaGvkM}tU_ltxwvz} ʦͩάʭɰ˱˱̴εϸй˴˴˲ɱȮǭīĩ̮̮ʬŧȿĦʬϱгзϹζ˱īú¦ʰɲȱȱ˴ηйη̵ʳʳʳʳ˴ͶйѺҼθ˵˵϶εʯŪ·{tuxƯ̵йѺйϸηʳʳʳʳʳɲȱǰ̷̹ͺͺ̺ͻ͹͹γϲͲͰʯɮȭȭ˯ʮɭȬǭūĪĪĩǪɬɬǬǬɮ˰ʹʹ̶ͷͷθκθжϳϵδδϵϵжѺѺйййϸϺкйҺҽӼһйη̵˲ʱɰɰɰʱ̳ʹ̲̰ͱ̰̰ʮɭȬͱ˯ɭȬȬʮͱϳ̲ʰǭū¨¨¨ĪūȮʰ̲̲̲̲̲˱ʰɭǬǪǪǨũȭ˲ʹ̷ɴŲĮƮƬūĪ騨éūƬǭǭȮɭ˯˯˱ɯƯŮì«­įDzʲͲͯͯͯͱβε϶ηηιϺϺλϼϼκͷ̶ʴȲDZůůưưDZɳʴ˵̶ͷ͸̷̷˶˶̷̷͸͸ιιιι͸̷˶ȵȵȵȵȵȵȵȵ˳̴̴̴̶˵ʴȵ˹˹̼ͽ̿Ͽμͻ̺˹˹˹˹̺̺̺ͻͻͻͻ̺̹ʷ˵ʴɳȲȲɳɳ˵̶θϹкϹθθ̹̹ͺλλͺ̹̹ʷʷ˸˸˸˸ʷʷʴ˵˵ʴʴɳDZDZưůůĮĮůDZȲȲʴ˵ͷθθͷͷ̶˵ȲůíííĮıŲƳǴɶ˸̹ͺ˸̹̹̹ͺͺλλμμμϽϽϽооϽϽϽϽϽϽϽϽооооооооϽϽͻͻͻͻϽϽϽϽϽϽϽϽϽϽϽμμͻͻμμϽͻ̺ʸɷʸ˹̺ͻϽμμμͻͻ̺̺μμμμμμμμμμμμμμμμμμμϽϽооооооооϽμμμϼϼͺͺͺͺͺͺͺͺλͺͺͺλͺʷǴƳƳƳƳǴȵɶʷʴͷϹͷ͵η˵DZqx_zjQscJrbIp`IxhQw`qȽĨȫͰгβͰͳ˱Ȯǭʰͳδͳ̵ηϸʵ̷ιιͺ̹ͺλϺϺθ˴ɽ|bsbHo[@kZ>o^Dm_DseKmȮͷθѺѺҺҺռ׾ԺӹѺйϸϸθζϸʱ˰ƪ·ybwgNdRcS9dV;k]@{]Ļʮ̲ϷθɶθиδͱͱββˮɬǾtbvXwYz\pĬƭǮɰ«ǿîȳ͸ѻ̶Ͷ̶̶̶̶˴˴ɰɰƮõ~vwuvyĻçǫˮˮɬɬͰϵ̵̷͸θθͷʹδʰɰȲDZưĮ­¬Įưȱɲʳʳ̴͵͵ͳ϶ϴ϶϶ͷ̸̶͵϶ϴβ̰ʫʫʭʭˮ̯̱Ͳɱɲ˴̵̵̵̵ͶͶ̵̵˴̵ηϸѺз϶϶зѸз϶εͱββ̰̰βдд̵̲ηͶ˴ʳɲʳʳ˴̵̵̵̵˴˴͵͵ζϷϸййѺѻииϷϷζ͵͵ʳɲɲȱɯȮǭǭǮȯɳɳɳɳʴ̶Ϲθ϶϶϶϶϶϶ййϸηηϸййѷѷжжжϵϵϵϵгϵϲδͳ̲˱ʰʰ˱̲̲̲ʹʹ̴͵ζиииииζζ͵͵͵ζиии϶̳ɰǮƭƭƭ˲̳ʹεεʹ̳ʲǰȱʰ̲αϲввԷҵϵ͵ʵʷɸʷɳɳȲDZůĮííȲȲɳʴʴ˵˵̴˱˱˱˱ʳɲȳȳŰŰŲŲŲƳƳƱȰɱ˳̴͵̴˱ʰ̲̲̲̲̲˱˱˱˴˴ʳʳɲȱȱȱɲɲɲʳ˴˴̵̵͸̷̷˶˶̷̷͸͸ιιιι͸̷˶ʷɶǴƳƳǴɶʷ˹̺̺̺̼˹ʸɷ˹˹̹ͺͺλϽϽμͻ̺˹˹˹˹̺ͻμμϽϽμμͺ˸̶˵˵˵̶̶ͷͷθϹккϹͷ̶ʷʷ˸˸˸˸ʷʷ˸̹̹ͺͺ̹̹˸˵˵˵ʴɳDZůĮííůưȲɳʴʴ˸˸ʷ˸˸˸̹̹λλ̹˸ʷɶɶɶ˸ʷȵȵȵʷ̹λλλλλλλλλͻͻͻμμμϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽμμμμμμμμμμϽϽооѿѿϽμμͻͻμμϽϽμ̺˹ʸ˹̺ͻ̺̺̺̺̺̺̺̺μμμμμμμμμμμμμμμμμμμμμμμͻμμϽϽμμͻ̺ʸѾѾѾѾѾѾѾѾѾѾнϼϼнϼ˸Ƴ¯ƿ¯Ųȵ˸ϼнҼϹθѻи͵͵Ϸ̲϶ζɱ¨Ƹqfx]tVyX]aanv¿ĽƿƿƿžžçȮ̲жһһѺһӻϸϺθƯzqWkZ@r^CgV:dS9cU:oaGnéηѺԽվվԽ־׿ֹոӸҹкмннѽʶ̶зγѳȩs}kUp^FfTxiJ|`лӿҿҿѾнϼͺ˸˸ʷʷλϼҿҿϼͺƱî͸̷ʵɴɴʵ̷͸ͷͷθθθ̶˵ʴDZưůĮưɳͷϹϹθ̶ɳȲȲȲɳůůưDZɳ˵ͷθ̶˵˵˵˵˵̶ͷѻкϹθ̶̶̶̶͵ɱĬſýýªëĬŭǯʲ͵ϷѹϹθθͷ̶˵ʴʴ̶̶̶̶̶̶̶̶̶˵ʴ˵̶ͷ̶˵̶ͷθθϹϹϹθϹͷ̶̶̶̶ʴȲ˵ͷθͷ˵ɳɳɳ̵ͶͶͶͶηηϸѺϸͶͶϸйϸη˴̵̵ͶͶ̵̵˴ʳʳ˴̵̵ʳȱǰ˴˴̵ͶηηͶͶ˴̵ͶηηηηͶιϺлѼѼѼѼѼѼллϺι͸͸̷͸͸͸̷̷̷˶˶ͶηηϸϸййѺйййййѺӼӼйϸηηͶͶηηйѺһϸ̵ʳ˴Ͷ˴ʳɲȱȱȱɲʳ˴̵ͶηϸйѺһӼӼѺйηͶ˴˴̵̵̵ͶͶͶηηι͸̷˶ʳʳʳʳ̲̲ͳδβͱ̰Ͱ˱˰ʯɮʯ̱γеγͲ̱˰˰˰˰˰ʲ˳̴̴̴˳˳ʲɱɱɱʲʲ˳˳˳̳̳̳̳̳̳̳̳ʹʹʹʹʹʹʹʹ̱̯˰˰˰˰̱̱϶εʹ̳˲˲ʴʴȱȱɲʳ˴˴̵̵ηͶ̵˴˴˴̵̵͸͸ιιιϺϺϺϺϺϺιι͸͸͸ʹʹȷǶǶȷʹ˺϶϶εʹ̳˲ʴɳʶ˷͹κϼϼϼͻ˺ʺ˺˺˺̹̹̹͹ͷηεδ̲̱ʰưưůůưDZȲȲ̶̶ͷθθθθθ̳δεε͵˳ɲȱʳɲɱʲ̳̳˱ɯȰȰǯǯƱƱDzDzȳʵ̹ϼѾѾнϼμͻ̺˹˸˸ͺͺͺͺθͷͷͷ̶̶˶̷ͺ̹ɺɺʾ˿̻̽ͺ͸δͳͳͷͺͺͺλλλϼλλλλλλλλμμϽϽϽооооϽϽϽμμμͻͽͽͽͽͽξξξϿϿϿϿξξξξͻͻͻͻͻͻͻͻϽооѿѿооϽϽϽϽϽϽϽϽϽμμμμμμμμϽμͻ̺̺̺̺ͻϽμμμμϽоѿоѻѻϹϹθͷͷθϹϹϼλλλλ˸ǴððŲȵ̹ϼѾҿҿѾѾнѾѾҿҿѾнλͺͺ̹ͷíëŭʲζ͵ɱĬɱʲ̴̴˳ʲ̴ζʰ˱̲ͳϵѷҸҸжҸӹӹѷϵϵиллʴĽjrQtdCn^=xhGxYrůԿҿѾнѾҿѾϼ̹ʷɶȵȵͺϼѾҿѾӾҽѼ͸ɴDzȳɴι͸˶ʵʵ˶͸ι˵̶ͷϹϹкϹϹϹθθθϹкҼӽϹθͷ̶˵ʴɳɳůůůưȲʴ˵̶˵̶̶̶̶̶˵˵ͷͷͷͷͷ̶˵ʴζɱª¼¼ŭ˳ʲǯƮǯ˳ϷҺθθθͷ̶̶˵˵ϹϹϹϹϹϹϹϹʴȲȲɳ˵̶̶˵θϹкѻѻѻккͷ̶˵̶ͷ̶ɳDZɳ˵̶̶˵ɳʴ˵ηηͶ̵ͶηϸйѺйηηϸѺйϸ̵̵ͶͶͶͶ̵̵ʳ˴̵̵̵˴ɲȱ̵Ͷηηϸϸηη̵ͶηηϸϸηηϺϺлѼѼѼѼлѼѼѼлϺιιι͸͸͸͸̷̷̷˶̵ͶηηййѺһѺѺййййѺһййϸηͶͶηηйѺѺϸͶ˴˴̵ʳʳɲɲɲʳ˴˴̵̵ͶηйѺһһӼһѺйηͶ̵˴̵̵̵̵ͶͶͶηιι͸͸̵˴ʳʳ˱̲ͳͳͱͱ̰̯ɯʱɰɰʱʹ϶зʹεεεεʹ̳˲˳˳̴͵͵̴̴˳ǯȰʲ˳̴˳ʲʲʱ˲˲̳̳ʹʹεʹʹʹʹʹʹʹʹγͲͲ̱̱ͲͲγз϶εʹ̳̳̳̳ɲɲʳʳ˴̵ͶͶϸηͶ̵˴˴˴̵͸ιιιϺϺϺϺлϺϺϺιιιι˺ʹȷǶǶȷɸʹ̶̶̶˵ʴʴɵɵ˸̹λϼϿϿϿϿ;˽̽˺˺ʹ˹ʷ͹ͷηηζ͵̴ʳDZưưưưDZȲȲͷ̶̶̶̶ͷθϹͶϷηͷͷ̶ʵɴʵȳɳɳ˵˴ʲȰɱɱȰǯƱDzȳȳʵ̷ϻѽѿоμ̺ͺͺ̹˸̹̹ͺλнϼθͷ̶̶ͷθϹкλͺ˻̼̼̼̺̹ͷͳδδθλλλϼϼϼнλλλλλλλλμϽϽϽооооѿоооϽϽϽμξξξξξξͽͽϿϿξξξͽϽμμͻͻ̺̺˹ϽϽооооϽϽϽϽϽϽϽϽϽϽμμμμμμμμϽμͻ̺̺̺̺ͻμͻͻͻͻμϽоϽҼҼкϹϹθθϹϹкϼϼннϼ̹ɶǴǴȵɶ̹λнҿнϼϼϼϼнѾѾѾϼλ̹̹ͺλλưǯɱ͵иϷ˳ǯ͵ϷииϷζϷиͶϵϵжѷҸӹӹѷҸԺӹѷжжѹллʴ»o}^vUrQ}^pʴӾҿҿϼλϼҿнͺ˸ɶɶȵ̹λϼнѾϼλͺҽԿԿҽϺ˶ɴɴ͸̷˶˶˶˶̷͸˵̶ͷθϹккккккѻѻкккͷͷͷͷ̶ʴɳȲưưưưDZɳʴ˵̶ͷθϹϹͷ̶˵˵̶θθθͷ˵ʴ͵ȰƮ̴ʲǯƮǯʲζѹθθθθͷͷͷͷϹϹϹϹϹϹϹϹɳȲȲɳ̶θͷ̶ккѻѻҼѻѻк̶̶ͷθϹθʴDZɳ˵ͷͷ̶̶̶θηͶ˴ʳʳ̵ηйһйηϸйѺѺй̵̵ͶηηͶ̵̵̵ͶϸϸϸηͶ̵ͶηϸйййϸϸͶͶηϸййϸϸллѼҽҽѼѼлѼѼѼллϺϺϺιι͸͸͸̷̷̷̵ͶͶηйѺѺһһѺййϸйййѺйϸηηηηϸϸййϸη̵̵̵ɲɲʳʳ˴˴̵Ͷ̵ͶηϸйѺһһһһѺйηͶ̵̵˴˴̵̵̵ͶͶͶ͸͸͸͸̵˴ʳɲ˱˱̲̲̰̰˯˱ʲʱʱʱʱ̳ʹεʹε϶з϶ε̳˲ɱʲ˳̴̴̴˳˳ƮȰʲ̴͵̴ʲɱɰʱ˲˲ʹʹε϶̳̳̳̳̳̳̳̳ϴγγͲͲγγϴ϶εʹ̳̳̳̳˳ʳʳʳ˴̵ͶͶηϸϸͶ̵̵˴̵̵ιιιϺϺϺллϺϺϺιιι͸͸˺ʹɸȷǶȷɸʹ̶̶̶˵˵˷˷˷ͺͺλϼϿϿο̾̽̽ʻʹʸɷͺ͹Ϻϸηζ͵˴ɳȲȲDZDZȲɳɳ̶˵ʴɳɳ˵ͷθηηͷͷ̶̶ʵʵʵɴɴʴ˵˵ʳɱʲʲɱɱȳȳɴɴ̷ιмӿҿнͻ˸̹̹̹̹ͺͺλλнλͷ˵ʴ˵̶θккλͺ˹̼ξʺʸ˸̶͵ͳϲδϹλϼϼϼнннϼϼϼϼϼϼϼϼϽϽϽоооѿѿѿѿѿоооϽϽϿϿϿϿξξξξϿϿξоϽμμ̺̺˹ʸμϽооооϽμϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽμͻ̺̺̺̺ͻμμͻͻμϽоооҼҼҼѻѻккѻѻҼнѾѾѾϼͺ̹˸˸̹ͺϼнҿϼλͺͺͺλϼϼϼλͺ̹̹ͺϼн̶̶̶ͷͷͷ˵ɳͷϹҼҼкϹкиѺѺѺѺѺййййһԽӼѺййҺмѼ̶ƿvtu~¨θԿҽҿϼλϼҿϼͺ˸ʷʷ˸̹ͺλϼλλͺϺллϺ̷ʵɴʵ̷̷̷˶˶̷̷̷ͷͷͷͷθθθϹθϹккϹθͷ̶˵̶̶̶˵ʴɳȲDZDZDZDZȲɳʴʴθϹкѻѻкθͷ̶ͷθϹϹθ̶˵˳Ʈſ¼ªƮƮƮƮȰɱ̴ζϷθθθθϹϹϹϹϹϹϹϹϹϹϹϹ̶ʴʴ̶θкϹθϹккѻккϹϹͷͷθкѻкͷɳʴ̶ϹϹθθϹкϸͶ̵˴˴ͶϸѺһйηϸйѺѺй̵ͶηηηηͶ̵ͶηϸййϸηͶͶηϸйййййηηϸйййййѼѼҽҽҽѼѼлѼѼѼллллϺιιι͸͸͸̷̷ͶͶηϸϸйѺѺѺѺйϸϸϸййѺйϸϸηηϸϸϸϸϸϸϸη̵˴ɲʳ˴˴̵̵ͶͶ̵̵ͶηйѺһһӻҺҺѹϷζζ͵̴̴̴͵͵͵ζζ˶̷̷̷˴ʳȱǰʰʰ˱˱˯˯ʮʰ̴˵ʴʴɳɳɳɳ˵̶ͷθͷ̶ʴɳǯȰɱʲʲʲʲʲƮȰʲ̴͵̴ʲɱȰɱʲʲ̴̴͵ζ˳˳˳˳˳˳˳˳ζζ͵͵͵͵ζζ̲˱ʰʰɯʰʰʰʳ˴˴̵ͶηηηϸϸηͶ̵̵̵ͶιιϺϺϺлллϺιιι͸͸͸͸̻˺ɸȷǶȷɸɸ˸˸˸̹̹̺̺̺̼ͽͽͽ˿̽ʻʻʺɷͺλλϺϺι͸͸˵˵ʴɳɳʴʴ˵ɳɳDZDZDZɳʴ̶ιι̸̸ʷʷɷɷ˹˸ʷ˸͹͹̷ʵʵʵɴɴɴɴʵʵ̶θкҼҼѻͺ˸̹̹ͺͺλλλλϼͺʷȵǴȵɶʷθθκ͹˸̹λнɶɶʶ˵ʹγϲϴϹλϼϼϼнннϼϼϼϼϼϼϼϼϽϽоооѿѿѿѿѿѿоооϽϽϿϿϿϿϿоϽμμ̺̺˹ʸμμϽооϽμμϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽϽμͻ̺̺̺̺ͻϽϽϽϽϽоѿѿӽӽӽҼѻѻѻѻҼӽнҿҿнλϼϼϼϼнѾѾҿнϼλͺͺͺͺͺϼϼλλλλϼϼѻкθ̶˵ʴʴʴ˵θѻҼѻккѻӼһһѺййϸϸѺӼվԽһѺѺӻҾӼϹƬùôȺĩ˱ӺԿѾϼнҿнλͺ̹̹ɶʷ̹ϼѾҿϺϺϺι̷˶̷ι͸͸͸͸͸͸͸͸кϹθͷͷͷθθкккккϹθͷ̶˵˵˵ʴʴɳɳʴɳɳȲȲɳʴʴͷθϹкккккθϹϹϹθͷ̶̶ɱƮý¼ſëƮëĬƮɱ̴͵ζζθθθϹккѻѻθθθθθθθθθͷͷθкѻѻϹϹϹккϹϹθͷϹθθкѻѻθ̶̶θкѻкϹкѻѺйϸϸϸйһӼѺйηηϸѺйϸͶͶηηηηͶͶ˴̵ͶϸϸηͶ̵ͶηϸййййϸηϸйѺѺѺѺйҽҽҽӾҽѼѼлллллϺϺϺϺϺιιι͸͸͸͸ηηϸϸϸϸйййϸϸϸϸϸйѺѺѺйϸϸϸϸϸϸηηϸйϸ̵ʳ˴˴̵ͶͶͶ̵̵˴˴̵ͶηйѺѺҺҺѹииϷζζ˳̴̴̴͵͵͵͵ʵ˶̷̷˴ɲǰƯɯɯʰ˱˯ʮɭɯ̴̶˵ʴɳȲDZưʴʴ˵˵ʴɳɳȲƮƮǯɱɱʲɱɱǯȰʲ˳̴˳ʲʲɱʲʲ˳˳̴̴͵̴̴̴̴̴̴̴̴ζ͵͵̴̴͵͵ζ˱ʰʰɯɯɯʰʰ˴˴̵ͶηηϸϸηηͶͶͶͶηϸϺϺϺϺлллѼлϺϺϺιιι͸ͺ˺ɸȷǶǶȷɸ˸˸˸̹ͻͻμμ̼̼̼̼̿˿˿ʾ˼ʻ˻ʺλλϼϼϻϺκ͹̶˵ʴʴɳɳʴʴȲȲDZDZȲȲɳʴ͹͸˷ɶɶɶȶɷ̺˹˸̹ͺκ͸˶̷˶ʵʵʵʵ˶̷̶ͷϹккϹθθ˸̹ͺλϼϼϼϼλ̹ʷȵǴǴǴȵ˷̶κ͹̹˸̹ͺǴȴɳ˲ͲγϲеθλλλϼϼϼнннннннннооооѿѿѿѿоооϽϽϽμϿϿϿϿϽμμͻͻ̺̺˹μμϽϽϽϽμμϽϽϽϽϽϽϽϽооооооооϽμͻ̺̺̺̺ͻооϽϽооѿӽӽҼѻѻккѻѻҼнҿѾϼнҿҿҿҿҿҿҿҿҿҿѾϼλͺͺͺϼϼннннϼλккϹθ̶ʴʴ˵ʴͷкҼҼѻѻѻһһһѺѺѺѺѺһԽվվӼѺѺӻԽѻ˱ŪçũɫŪ˯βͲжռҿѾҿѾϼλͺ̹̹˸̹ͺϼѾϺллϺ̷˶̷͸ϺллллллϺѻкϹθθϹкѻҼҼҼѻѻѻѻѻθͷ̶ʴʴ˵̶̶̶˵ʴʴɳʴʴ˵˵˵̶̶θϹкѻкϹθ̶˵˵ʴʴȰǯŭëëĬƮǯŭƮȰʲ̴ζϷϷͷθθϹкѻҼҼϹϹϹϹϹϹϹϹкϹθϹкѻкϹкккккϹθͷϹθͷͷϹкθͷ̶θккϹθϹкйййѺѺѺһһѺϸͶͶϸйϸηͶηηϸϸηηͶ˴̵ηϸϸϸηͶͶͶηϸϸϸϸϸϸййѺһһѺѺҽӾӾӾҽҽѼлιιιιϺϺϺϺϺϺιιι͸͸͸ййϸϸϸϸηηηηηηϸйѺһһѺйϸϸϸϸйηͶͶϸѺйͶɲ̵ͶηηηͶ̵˴ʳʳ˴̵ͶηϸйҺҺѹииϷζζ˳˳̴̴̴͵͵͵˶̷͸͸ͶʳȱǰȮɯʰʰʮʮɭȮʴɶɶɶȵǴǴƳȵǴǴƳŲŲƳDZƮǯȰɱʲʲʲʲɱɱɱʲʲ˳˳˳˳˳˳˳˳˳˳˳̴̴̴̴̴̴̴̴ͷͷ̴̴̴̴͵͵˱˱ʰʰʰ˱̵̵̯̲ͶͶηϸййͶͶ̵ͶͶϸйѺϺϺϺлллѼѼҽҽҽѼѼлллλͺ˸ɶȵȵɶɶɶɶʸ˹̺ͻͽξ̼˻ʽʽ˾̿ʾʾʾʾ̼̼ͽͽμϽϽϼλͺ˵ʴɳȲȲȲȲȲȲȲɳʴʴʴʴʴ̸˸ʷȶƵǶƶǷ˻ɹʹʹͻͺ˸ɶ̷̷˶˶˶˶˵˵̶̶͵͵̵Ͷηη˵̶θкѻѻϼϼͺͺ̹ʷɷȶǵǴɵ͸ллϺ͸̷̷DzǰɲʱͱϳдзͷͺͺͺλλλϼнннннннноооѿѿѿоϽϽϽμμμͻξξϿϿξͽͻͻͻͻͻͻͻͻͻμμϽϽμμͻϽϽϽϽϽϽϽϽооооооооϽμͻ̺̺̺̺ͻϽϽμμϽооѿѿӽӽѻккϹϹккѻнѾнѾҿҿҿҿҿҿѾϼλλλϼнѾҿҿнϼλθкѻкθ̶˵˵ɳ̶кҼҼҼҼҼѼѺѺһһӼӼӼһԽվվӼѺһӽտԾѹζ̲̱ϵӷɭϲѶжѹԼտӿҿѾѾϼλͺ̹̹̹λϼϼϼϼϼϼϼ͸ϺлϺ̷ɴɴɴҽҽӾӾӾӾҽҽѻккϹкѻӽԾӽҼҼѻѻѻҼҼкϹ̶˵ʴ̶θϹͷ̶˵˵ʴʴ˵˵ʴɳɳɳ˵ͷкҼкϹ̶ʴȲȲɳɳɱɱɱɱȰȰȰȰʲʲʲʲ̴ζиѹͷθθкѻҼӽӽккккккккѻϹθϹккϹͷѻѻѻѻкϹθθϹͷ˵˵ͷθθͷ̶θккθͷͷθͶηϸйййϸϸйη̵̵ηϸηͶͶηηϸϸηηͶͶηйһһһѺй̵ͶηϸϸϸηηϸйѺһһһһѺӾӾӾӾӾҽлл͸ιιιιιιιϺϺϺιι͸͸͸ѺѺйϸϸηͶͶͶͶͶηϸйһһһѺйϸϸϸϸйηͶͶϸһѺͶɲͶηϸϸηͶ˴ʳɲɲʳ˴̵ηηϸҺѹѹииϷϷζ˳˳˳̴̴͵͵͵̷͸ιϺη̵ɲȱȮɯɯʰʮɭɭȮȲǴȵɶɶɶȵȵǴƳŲðððıưǯȰɱʲ˳̴̴̴ʲɱɱɱɱʲ˳̴̴˳˳˳˳˳˳ʲ͵͵͵͵͵͵͵͵ͷ̶̶˳˳̴̴͵̲̲˱˱˱Ͱααͳ̵Ͷηϸϸйй̵̵̵ͶηϸѺһϺϺϺллѼѼѼԿԿԿӾӾӾҽҽλͺ˸ɶȵȵɶɶǴȵȶɷ˹̺̼ͽ˻˻ɼɼʽ˾ɿʾʾʾʾ̼ͽͽͽμϽϽϽλλɳȲDZưưưưưȲʴ˵ͷͷ̶˵ʴ˷ʷȶƴŴƵƶƶɹȸȸɸ˹˹ʷȵ͸̷̷˶˶̷˵̶̶ʹ˳˳˴̵ηй˳̶θкѻѻϼϼλλͺͺ˹ɷȶȵȴ͸Ѽҽлι̷̵ƯǮȯʱͱϳдзͷ̹̹ͺͺͺλλнннннннноооѿѿϽϽμμμͻͻͻͽξϿξͽ̼˻̺ͻͻͻͻͻͻμͻμμϽϽμμͻϽϽϽϽϽϽϽϽооооооооϽμͻ̺̺̺̺ͻμͻͻͻͻμϽооҼҼѻѻѻѻѻѻѻѻҿҿѾѾѾҿѾѾѾҿҿҿѾнϼλλλѾѾѾѾѾѾѾѾҿҿѾнннѾѾҿҿѾѾҼлллѼѼҽҽҽԿԿԿӾӾҽҽӽտԾҺиϵϳγϴѶѶзѷѹҺӽӽҿѾнϼλλλϼϼнѾѾѾннϺϺι̷̷̷̷̷ιιϺлҽӾԿԿԾԾӽӽҼѻѻѻҼѻϹθθϹѻҼкϹθͷͷͷͷͷͷ̶˵˵˵˵̶ͷ̶ͷͷθθϹϹϹѻϹ̶ɳDZưưDZʲɱȰǯȰɱ˳̴Ȱɱʲ˳͵ζϷϷкккѻѻҼҼӽϹккѻѻккϹӽҼѻкϹθθθϹϹϹϹϹϹϹϹθϹкϹθθкҼͷθθϹϹθθͷηηηϸйһӼԽηͶ̵˴̵ϸѺӼԽѺϸϸйһһһһһѺѺѺѺѺѺϸϸйййѺѺѺϸϸϸϸϸϸϸϸϺϺϺϺϺϺϺϺϺιιιιϺлѼι̷˶̷ιлѼлѺйϸͶͶηϸйѺѺйѺһѺϸ̵йϸηηϸϸ̵ʳϸηηйѺѺη̵ηϸйϸͶ˴ʳʳηͶ̵˴̵ͶϸйизззѸѸ϶ʹ̳˲ɰʱʹзѸи̷ιϺѼйϸͶ˴ƬȮʰ̲ͱ̰ʮɯͶʵɴ˶ʵƱƱɴʵȳŰŰDzȳɴȳȰȰȰɱɱʲʲʲʲʲɱɱɱʲ˳˳ȱǰǰƯǰȱʳ˴ʳȱŮŮǰʳ˴˴˶ȳįŰȳ˶̵˴δͳ̰ʮɭȬȬȬͳͶϸйѺѺйй̵̵ͶηϸϸйѺιϺлϺ͸ʵɴɴϺιлӾѼ͸ιӾι͸̷˶̷͸ιϺ˸ɶȵʷͼν̻ɸɺ̽˼ȹǻ˿ɼʽʽʽɺʻ˼;νͼʹƵǴɶ˸ʷDZɳʴɳDZưưȲDZȲɳ˵˵ʴɳɳʶ˷ʷȵƳƴȷ̻̻ͻ̺̹˸ʷʶʶʵ̷̷ʵůĮưȲʲɱȱ˱δϵδͳʲʲ˳ζкѻѻкλλͻͻ̼˻ʺ˸ɵϺӾѼ˴ƯƯɰʱ˯˯̰ͱβϳ϶Ҽϼͺ˸ʷ˸ͺλλλϼϼϼнннͻμϽооооϽоооϽϽμμμͽξϿξξξξͽ˻˹˹˹̺̺ͻͻͻͻͻͻͻͻͻͻͻμμμμμμμμоѿѿѿѿоϽμϽμμͻͻ̺̺˹ϽμμͻͻμμϽоҼҼѻѻѻѻѻѻѻѻҿҿҿѾѾѾҿҿҿѾҿҿҿѾнϼλλλѾѾѾѾѾѾѾѾҿннϼнѾҿҿҿѾѾѾнннϼллѼѼҽҽӾԿԿӾҽҽӽտԾҺиϵϵждҹҸҸѷѹҺҼӽҿѾнϼϼϼϼϼннѾѾѾнϼϺι͸̷̷̷͸͸ιιϺϺлѼѼҽѻѻѻкккккѻѻϹϹϹϹѻѻѻкϹθθθθθͷͷ̶̶̶̶ͷͷͷͷθθϹϹϹккθ̶ɳDZDZDZDZʲɱȰǯȰɱ˳̴ɱɱʲ˳͵ζϷϷкккѻѻѻҼҼϹϹкѻѻкϹϹҼҼѻккϹθͷθθθθθθθθθϹϹθ̶̶ͷϹͷθθθθθθͷηηηηϸйѺһηͶ̵˴̵ηйһԽѺϸηϸййϸϸϸϸϸϸϸϸϸϸϸϸйййѺѺηηηηηηηηллллллллϺιιιιϺлѼϺ͸̷̷ιллϺͶͶ̵˴̵ηϸѺӼѺηͶϸйϸη˴ʳʳ˴ηηͶ˴ʳɲɲ˴Ͷη̵˴ͶηϸηͶ̵˴˴ͶͶ̵˴̵Ͷηииз϶зѸѸ϶ʹ̳ʱȯɰ̳ε϶϶̷͸ϺлѺϸηͶȮɯʰ˱̰˯ʮɯϸ̵˶Ͷ̷ȱDzʳDzŮįŮDzɲʵɲȰȰȰɱɱɱʲʲ˳ʲɱɱɱʲʲ˳ƯƯƯƯǰɲ˴̵˴ɲƯŮȱʳ˴˶ʷȵƳƱʵ̷̵˴̲̲ʮɭȬȫȫȬͳͶϸййϸηηʳ˴˴̵Ͷηϸϸ̷̷̷ιι͸ʵDz˶ɴDzįļƾȳιι͸̷ʵʵ˶͸̹ʵȵʷͺλʹǶʹ;̽ȹȹ˼̽ʽ˻ʽʺȹɺ˼̻˺̹ʹɶʷ̷˹˶˵˵ʴɳȲDZưDZưDZȲʴʴʴɳȲɵʶɶȵƴƴǶɸȷȷɷȶȵȵȵɵɵ̷̷ʵưůDZɱɱȮȮʰͳϳβͱ˱˳̴ζϹкϹθλͺͻ̺˻ʺʺɹʷлӾлɲŬŬɭ˯̰ͱͱ̰ͱдҹѻϼͺ˸˸˸ͺλͺλλλϼϼϼнͻμϽооооϽоооϽϽϽμμξϿξͽͽξϿͽ̼˹˹˹̺̺ͻͻͻͻͻͻͻͻͻͻͻμμμμμμμμооѿооϽϽμμμͻͻͻͻ̺̺ϽμμͻͻμμϽоҼҼѻѻѻѻѻѻѻѻҿҿҿѾѾѾѾнѾѾҿҿҿҿнϼλλλϼѾѾѾѾѾннноϽϽоѿѿоооϽϽϼλϼннҿҿҿҿտԾҺѹѹиѹѷһӻҺѹѹҺҼӽҿѾнϼϼϼϼннѾѾѾѾнϼι͸͸̷͸͸ιϺιιιιιιϺϺθθθθθϹϹϹѻккϹϹккѻҼѻкϹϹϹккϹθθͷͷθθϹθθϹϹϹкккϹθ˵ɳȲDZȲȲʲɱȰȰȰɱ˳̴ʲʲ˳̴͵ζϷϷϹϹкккѻѻѻθϹккккϹθкѻѻѻѻϹͷ̶̶̶̶̶̶̶̶̶θθθͷʴʴ˵ͷθθθθθθθθηηͶͶͶͶηϸϸηͶ̵̵ηϸйһйηͶηϸηͶ̵̵ͶηηͶͶ̵ηηϸϸϸйййηηηηηηηηѼѼѼѼѼѼѼѼιιιιιϺлллι͸͸ϺллϺͶ̵̵̵ͶϸѺӼӼй̵ʳ˴ηϸϸ̵̵̵ηйѺϸηʳɲɲ˴Ͷϸϸη̵̵ͶͶͶͶ̵̵̵̵̵˴̵Ͷηии϶϶϶Ѹзε̳ʱɰǮȯʱ˲̳˲ȳʵ̷͸ηηͶ̵ʰʰʰʰʮɭɭɭεʱɳ̳ʴǮůɰíê¬īưɰȲȱȰȰȰȰɱɱɱʲ˳ʲɱɱɱɱʲʲŮĮĮůưȲʴ˵ɳDZůưȲɳɳȴǵƴƴȶʶ˷ʴɳȯȯǬǬȫȫɬɬͳͶηϸϸͶ̵˴ʳʳʳ˴̵̵ͶͶ͸̷͸ѼԿѼȳŽ͸ϺϺ͸ɴDzɴ˶˷ɳdzɵ˷̸ȶųʸͽ̼Ƿƶɹ˽̻ͽ˹ʺɷǶȷɸ˸ȶdzȶ˷͹ͷʷʶ͸̶ʴɳɳȲDZůưưDZȲɳȲȲȲȲȲȳɴdzƲıð®®įưDZȴɵʷʶɵDZDZɱ˳ɲɯɯ˯̱ϲαͱ˱̲ͳδζζͷ̶ͺ̹̺˹ʺɹȻȸ˸ϼѽκȲŬǬˮɬͰα̯ȭǬ̳Ѹѻλͺ˸˸̹ͺλ̹ͺͺͺλλλϼμμϽоооооооооϽϽϽμξͽ̼ͽϿϿϿͽ̺̺̺̺ͻͻͻͻͻͻͻͻͻͻͻͻμμμμμμμμоооϽϽϽμμ̺̺ͻͻͻͻμμμμͻͻͻͻμμоѻҼҼҼҼҼҼҼҼҼҿҿҿѾѾѾннϼнѾҿѾѾнϼλλλϼѾѾѾннннϼϽμμоѿѿѿѿооϽϽλϼϼнҿҿԾԾӻӻҺҺҺҺԼӻҺѹѹѹҼҼҿѾнннннѾѾҿҿѾѾнϼ͸͸͸͸͸ιϺлллϺϺιι͸͸ͷͷͷθθϹккккккккккҼҼѻкккѻѻкϹϹθθϹϹкϹϹϹϹϹкккθͷ˵ɳȲȲȲɳʲʲɱȰȰʲ˳̴ʲ˳˳̴͵ζϷϷϹϹϹϹϹкккθθϹϹϹϹθθθϹѻҼѻкͷ˵ͷͷͷͷͷͷͷͷϹϹϹͷʴɳʴ̶θͷͷͷͷͷͷθηηͶ̵˴˴̵̵йϸηͶͶͶηϸηͶ̵̵ηϸηͶʳ˴̵ηηͶ̵̵ͶηηηϸϸϸϸͶͶͶͶͶͶͶͶллллллллιι͸͸ιιϺлι͸˶̷ϺѼѼѼѺйϸϸϸѺһԽѺη˴ʳ̵ͶηηϸηͶηййϸͶ˴ʳɲʳͶϸϸϸ˴˴˴̵ηηͶ̵̵˴˴̵̵ͶͶϷз϶ε϶ззʹ˲ɰǮƭǮȯɰɰȯůƱDzɴʳʳʳʳ̲˱ʰɯȬȬȬɭ˰ǬƭȭǮĩīǬêèêŪǮȭǮƬƯǯȰȰȰɱɱɱ˳ʲɱɱȰȰɱɱưůůưưDZDZȲDZDZDZɳ˵ʴȲűųŵȶʸ˹˷ʴȲƭŬŪǪǪɬʫˮδͶηηͶ˴ɲȱʳʳ˴˴̵̵ͶͶ̷˶̷ҽԿ̷ºvrs~û̷ϺлιɴDzɴ͸̶ʱDZȲʴ˵ɵűȴ̺ͻɷŵɷʺ̹ͼ̹ɷȵǶȵȵȳű¬ïȲ̶˳ȲDZ̷˵ɳɳʴʴDZĮůưưDZDZȲȲȲȲǯDzɲȴů˾ƺö÷öĹŽůDZɵɶɵɵȲɳʲ˳̲ʰȬǪɬʭʭɬǫʰδϵϷ͵̶̶̹̹˹ʸɹɹǺǺɹλмθʱȭ̯гˮϲѴ̯Ū§Ǯʹλλͺ̹̹̹ͺλ̹̹̹ͺͺͺλλμϽооѿѿооѿѿоооϽϽϽϿξ̼˻˻̼ξϿξ̺ͻͻͻͻͻͻͻͻͻͻͻͻͻͻͻμμμμμμμμоϽϽμμμμμ˹̺̺ͻͻμμϽμμͻ̺̺ͻμμоѻҼҼҼҼҼҼҼҼҼҿҿѾѾѾнннλϼѾннϼλλλλϼѾѾѾннϼϼλѿϽϽϽѿѿѿѿѿϼϼнѾѾҿԾԼԾԾԾӽӽҼԾӽҼѻѻѻѹѻҿѾѾннѾѾҿҿҿҿҿѾнϼ͸͸͸͸ιϺлѼѼѼѼлϺιιιθθϹϹккѻѻккккккккҼѻѻкѻѻҼҼккϹϹϹϹккϹϹϹϹϹϹϹϹͷ̶ʴɳȲȲɳʴ˳ʲɱȰɱʲ̴͵˳̴̴͵ζζϷϷϹϹϹϹϹθθθͷͷθθθθͷͷͷθкѻѻϹͷ̶ͷͷͷͷͷͷͷͷкккθ˵ʴ˵ͷθͷ̶̶̶̶ͷθηͶ̵˴ʳʳʳʳϸϸηηͶͶηηʳʳʳ˴ηйϸηʳ˴ͶϸйϸηͶͶͶͶͶηηηϸͶͶͶͶͶͶͶͶιιιιιιιιι͸͸͸͸ιϺл˶ʵɴ˶ϺҽҽҽӼһйϸηϸϸйͶ̵˴̵ηηͶ˴ʳʳʳ̵ηϸη̵̵ɲǰǰɲ̵Ͷη˴ɲɲ˴ηϸͶ˴ʳ˴˴̵̵ͶͶ͵϶εʹε϶϶ʹ˲ȯǮƭǮɰɰȯƭůŰƱDzɲɲʳʳ˱˱ɯȮȬȬȬȬ̱ȫĩŨ§Ũ¥èǪȭʭǬũǭǯǯȰȰȰɱɱ˳ʲɱȰȰȰȰȰɳȲȲDZůĮí¬ưDZȲ˵̶˵DZï°ĴǷʸ˹ʶȲDZīīŪƩȫɪˬ̯δͶηηͶ˴ɲǰ̵̵̵̵ͶͶͶͶɴȳʵлѼî~izr]rjUwoZkļʳͶϸη˴ʳ̵ηεʯƭƭȯʱȲưȴ̸ɵƴȴ˹̷λͷɶɳɶȳƱìŻ÷ȼο̾ʻȼìůDZɳ˵ʴȲưưưưưDZDZȲȲȰǭǰʰȲªĵªǯʶɶȵȴɳɳɲɲɯūȾϹúúĽħ˱ϵиϷθͷ̹̺˻ʺȻȻǼǺǷ˹λͷȰǭ̯ѳ˭вӵαūĬɱͺͺͺ̹ͺͺͺλ̹̹̹ͺͺͺλλμϽоѿѿѿѿоѿѿѿоооϽϽϿξͽ˻ʺʺ˻ξϿμͻͻͻͻͻͻͻͻͻͻͻͻͻͻͻμμμμμμμμоϽμͻ̺ͻͻμ˹̺̺ͻͻμμϽμͻ̺̺̺̺ͻμоѻҼӽӽӽӽӽӽӽӽѾѾѾѾнннϼϼнҿҿϼϼλλλλϼϼѾѾннϼλλλооооѿѿѿннѾѾѾѾҿҿҿҿҿԿԾԼտտտԾԾӽտԾӽҼѻѻѹѹҿҿҿҿҿҿҿҿҿѾѾѾѾҿҿҿѾнϼϺιιιιϺϺлѼѼллϺϺιιкккѻѻѻѻѻϹккѻѻккϹѻѻкккѻҼӽкϹϹθθϹϹкϹϹθθθͷͷͷ̶˵ʴȲȲɳʴ˵˳ʲɱɱɱ˳̴͵̴͵͵͵ζζϷϷϹϹθθθͷͷͷ̶̶ͷθθͷ̶̶ͷθϹккϹθͷθθθθθθθθϹккθ̶˵̶θθͷ̶˵˵̶ͷθηͶ̵˴ʳʳʳ˴ηηηηηηηηʳɲʳ̵ϸйϸͶʳ̵ηйѺйϸͶ̵̵̵ͶͶͶηηηηηηηηηη͸͸͸͸͸͸͸͸͸͸̷̷͸ιϺϺɴȳȳʵ͸лѼѼѺйη̵˴ʳ˴˴̵˴̵ηйϸͶ˴ɲɲʳ̵йѺѺй̵ɲƯƯɲͶйѺ˴ɲȱʳηϸͶʳɲʳ˴̵Ͷ̵̵͵εʹ̳ʹεε̳ʱǮǮǮȯʱ˲ʱȯȲDzDzȳɲɲʳʳɯɯɯɯȬȬȬȬαƩȿúúƩɬȫƪƬǯǯǯȰȰȰȰ˳ʲɱȰǯǯǯǯʴʴɳDZůíʿĮ¬¬íí¬óƶȸȶdzdzDZŬŪŪǪȩʫ̭ͰδͶηηͶ˴ɲȱ̵̵ͶͶͶͶͶͶʵʵ͸ӾԿƱoxczr]}hļƱʳʳ˴̵ͶͶ̵̵˰ŪȾŻƼȾȾǽ·¬ʴɵdzȴʶ̷ι̶ʴʴʵȱìƿĺ¼Įɳ˵ʴɳɳDZDZDZưDZȲɳɱʰɬǭɭǯɽ{xz}ľƮʶȵƳƲǴɳǰƯ¨ĪͳиϷϹϹͻ̺̼˻ɼȻȽǺǷ˹̹ʴëŧ¤ȪͯͯɯƬȰ̴̹̹ͺͺͺλλλ̹ͺͺͺλλλϼϽϽоѿѿѿѿѿѿооооϿξͽ˻ʺʺ˻ξϿϿξμμμμͻͻͻͻͻͻͻͻͻͻͻͻμμμμμμμμоμͻ˹˹̺ͻͻ̺̺ͻͻͻͻμμͻͻ̺̺̺̺ͻͻоѻѻӽӽӽӽӽӽӽӽѾѾѾнннϼϼϼѾѾнϼλλͺͺλϼϼѾѾннϼλͺͺμϽоѿѿѿооҿҿѾѾѾѾннϼϼнѾҿӽԼտտӽҼѻкиѹҿҿҿҿҿҿҿҿҿѾѾѾҿҿҿѾнϼллϺιιιϺϺϺϺϺιιιιιѻѻѻѻѻкккϹϹѻѻѻѻϹϹккϹϹкѻҼҼϹϹθθθθϹϹθθͷͷ̶̶̶˵˵ʴɳȲȲʴ˵̶˳˳ʲɱɱ˳̴ζ͵͵͵ζζζϷϷθθθͷͷͷ̶̶˵̶ͷͷͷͷ̶˵θθθθθθθθ̶̶̶̶̶̶̶̶̶ͷθ̶˵ʴ̶θθͷ˵ʴʴ˵ͷθͶͶ̵˴˴˴̵̵̵̵ͶηϸϸϸϸͶ̵̵Ͷϸϸ̵ʳɲ˴ηйѺйϸͶ˴˴̵̵̵ͶͶͶηηηηηηηηιιιιιιιι͸͸̷̷͸͸ιϺ˶ɴȳɴ˶͸͸̷ϸη̵ʳʳʳ˴̵η̵˴̵ηйϸηʳɲɲʳ˴˴ʳȱĭƾĭʳϸѺͶʳȱʳηϸ̵ȱɲʳ˴̵Ͷ̵˴̴εʹ̳̳εʹ˲ɰǮǮȯʱʹε̳ʱDZƱƱŰƯƯǰǰƬǭȮʰʮɭȬǫŨú|vw{v{żäŦŨƬƮǯǯǯȰȰȰ˳ʲɱȰǯǯǯǯɳɳɳDZůíʿǼ·ʾ´ŵƶųűƲDZƭƫǬȫɪˬ̫ͮδͶηϸη̵˴ɲ̵˴˴˴˴˴˴˴ɴɴ͸ӾлƾDz͸˴ȱǰɲ̵̵ɲĭ¥¹ƼȯʴȲɳ˵̵Ͷ̴ʲ̴̵ʰùqopoqzɳʴɳʴ̶ɳȲDZDZDZɳʴ˳αʬǫɬūĹte~c~diwéʷǵIJıƳDZŮìĽwmiil|ʭδϷϹϹμͻͽ̼ʽɼɾȻȹ˺˸įüħȮ˱̵η˸̹ͺͺλλλλͺλλλϼϼϼнϽоѿѿѿѿѿѿѿоооϿϿϿξξ̼ʺʺ˻ͽξξͽϽϽϽμμͻͻͻͻͻͻͻͻͻͻͻμμμμμμμμϽμ̺ʸʸ˹̺ͻμμͻͻͻͻ̺̺ͻ̺̺˹˹̺̺ͻоѻѻӽӽӽӽӽӽӽӽѾѾѾннϼϼϼнѾѾϼλλͺͺͺλϼϼѾѾнϼλλͺ̹̹λѾҿѾѾѾѾннϼϼϼҿѾѾнϼϼ̹ͺλнҿӻԼտվԾӽҼѻиииҿҿҿҿҿҿҿҿҿҿѾѾѾҿҿѾнϼѾҼѻкιϹιϷͶζͶζͶζͶͶѺййииϷϷϹθϹѻҼҼѻλθλϹϹϹϹкѻҼϹθθͷͷθθϹͷͷͷ̶̶˵˵ʴ˵ʴɳȲɳʴ˵̶̴˳ʲɱʲ˳͵ζ͵͵ζζζζϷϷθͺͺͷͷ̶˶˳ʵ̴̶ͷͷ̶̶˵θθͷͷͷθϹϹ˵˵˵˳ʳʳʳʳɲ˳˴˳ȱʴ˵ͷθͷ˵ʴʲ˳͵ζͶ̵̵˴˴̵ͶͶ˴˴ͶηϸййййϸηжжʹʱǮǮ˱δѷѺѹϷ͵˴˴˴̵̵̵ͶͶϸϸϸϸϸϸϸϸϸϸϺϸϺϸϺϸ̶˵˵˵˵̶ͷθ̶˲ȲȯȲʱȲȯηδ̵̲˴̳ͷ϶кϵ˴˱̵жҺҺȱƯ΍ɿƼ¸¸ƫʮβ˱ɯ˱ϵи̴Ȱɱʲ̴͵Ͷ̵˴˳ʹ̳˴˵̶̶ʴȲưƮǯ˲ε϶ε˳Ī驨¨éêƬǮʰʱʰǮƫzmay[y[}_z\cpĻ¥ƬƮƮǯǯȰȰȰ˳ʲɱȰƯƬŮŮȱDZȲDZưĮ©}~˾ijijðíŭǭȬɪȧɨ˩̪ͭͰͳͶηϸϸζ̵̲˱˱˱ʱʱʱʱʱīʴһѺʵîŽû­Űɴ̵ǰĭƯ˴˴Ůƾ~tqqtt¸ǭ̲ʳ˴̷̷ͷ˳˳͵Ͷʰɿ¶u{`wZwZvYz^iƾɱɱȰʲζʲɳȲDZȲȵʷ̶δˮǫȫũjtTrRrRyYj¥˵ǵIJðŲưîül`~[}[]nũͱδζθϼϼͽͽ̼˽ʼʼ˻ͻʷǿ}yxúĩ̲ηϸ˸˸ͺͺλλλλλλϼϼϼнннϽоѿѿѿѿоооξϿϿϿϿξ̼˻ʺ˻ͽξͽ̼ϽϽϽμμͻͻͻͻͻͻͻͻͻͻͻμμμμμμμμϽμ˹ʸɷʸ̺ͻϽμμͻͻ̺̺˹ͻ̺̺˹˹̺̺ͻоҿҿҿҿҿҿҿҿҿҿϼϼϼннѾѾѾннϼϼϼнѾѾϺιι͸͸ιιϺллѼѼι˶͸Ѽϸ˱ǮɯȯɯȮɯɯϵѺϸηϸйηӽҼҼѻѻккϹθϹккѻҼӽӽҺӻԼսսսսֽӻииϷ˴ͶѺҿѿμоѿоѿϽμооϽϽнѽмкѻи϶иԹϵеѴѴϱϰѳշежҷдͳ̲γзͷϸмҾҿѾνλ̻ͺλϼϼλι͸͸̷˴̵ηϸηͶ̶ѻѻ̶ʴͷͷɳθ˵Ȳɳʴʴʴ˵ζ˴ɲȱȱɲ˴Ͷ˴̵̵ͶͶ̵̵˶λͻʹɶȳȳDZɯưȮȰʲ̶ηιηиη͵˳ʴ˵͹κθθϷδ˰ɮʮʮȫȬǬȮɰͶ͸˶ȳʵ˴˴ʰʰ˯ͱ˴˴˴˴ͶηйѺɲɲ˴ϸӼԽԽһɯδ϶ͲɮǭȬȬ̰αѵҶϵ̱̳ε̴˴˴˴˴̵ͶηһϸͶηͶ̵̵Ͷ̶зѻѸϹѸҼҹϵϵδ˱ɭũç¦ɾǼźźƻɾʿèǫɰƫ©Ʈ̲͵γδͱͳϲееɯ|zstyɨʫ̬ˮ̯˰˲̳ηη̶̶̶ͷͶϷθθκ̹ʷʷʸ˺ɸǴƳDZDZŬ·¹ĹúŹɿŭ˲̴ɳĮye]tRreEpaBxiJteHzkN}b|úũǯDZưDZǯɱʲ˳ʰʰɯȮƭǫŬūɲDzŮǰȯǮǼ~idacrĴŮͽæšƜȢǤɪͯʲ˴͵̴ʰʯʰͰȫͰαŧ˽ȺĵƻĨɮͳͶϷͷ̶˵ʴɲɲȲʳʵ̶ʴɳ˴̵Ůļ}ia}]xY|\fṴ̶̳̹̹̃˸ͷ˵Ȱǰɯĸl~mQxgI|iI{hHmNxZvƬƪ˯̯ƩʰɱȲDZȴɶɹ˸ʶ˵ǰĨ·z]}oJzlGwiDznH~ZsɲǴƳƳȵɳȲƮ¨|eyVuPsOrN_tɮ̲͵ͷ̹̹̹̹ͺνϾпͼλʴĽsc[^dtʿ̱жη̹ʷȵȵɶ˸λѾͺͺͺλλϼϼϼμϽоѿѿѿѿооϽϽϽ̼ͽϿϿ˻˻˻̼̼ͽͽͽϿϿϿξξͽͽͽ̼̼̼ͽͽξξξξξξξξϿξ̼˻ʺ˻̼ͽϿϿξξͽͽ̼ͻ̺ʸɷʸ̺μϽоҿҿҿҿҿҿϼϼϼннѾѾѾѾѾнϼϼϼннϺιι͸͸ιιϺѼлллϺ̷˶˴̳ŪȾƼŻŻĽç˯ѷѷѷӹӹҸҼҼѻѻѻѻккϹϹкѻҼҼӽӽӻԼսս־־־սҺζ̴ζη̵жӼҿѿμѿѿоμͻͻϽооϽϽμϽнѽкϹ϶ʹ˰˰̯гϰΰϮ̬˨ͪЮΰγдѴдгееεϸѼҾѿνͼͼͼϼϼϼϼιιι̷˴˴̵Ͷ̵˴˴иθɱȲ̴ͷ˲ͷ˲ȲɰȲɰɳʲ̵˴ɲȱȱɲ˴̵˴˴̵̵̵̵˴˶ʸɷȷȵdzDZȯȯƭǭǭʯ̳ηιηѹжζ̴̶ͷϺлͷ϶ϸϳ̲̰ʭ˭ǩȩȫǬȯʱ˴ʵȳɴ˴̲˱ʮˮͱ̵̵̲Ͷηϸϸϸ˴˴̵ϸѺѺη˴ȮɯǬèæŨɫͮѴԶѷϴ϶зζ̵̵̵̵ͶͶηйηηϸϸͶ˴˴ƭǬȯɮ˲ͲεͲ˰ȭèʼ͏÷ǻéɯʰ˰˰ͰαϲѳҴūzqh`}X}UYbluŻžȧ˫ȪƩȯͳͶͶͶ̶̶ͷͶͶ̶̸̹˸ȶƵĴŵʺƴ¯̽Źĸȼëɱ˵ȲðzdyVzkJm^=l[=p_AjY=teHy_zƿūƮDZDZȲɱʲʲʲʰʰȯȯǬƫŪŬǰŰĭƯǮƫ˿ƻo}\wUuQvR`wôͿêͽǷɸŶƧ̰ʲʳ̴̴˰˰ˮͰ˭ϯ̭³zt|ƻƪ˯˱ʲDZȲɳ˵˴Ͷͷη˶ͷ̶˵Ͷϸɲu_vUqQnNrR|Z~Ǩϳϵηλͻ˷˷ʴDZǰɯĹlmOzgG|gH{gFkKvVsȾĩħȫ˭ɫʭȮɰDZǴɶɻʺȸʶƱéʿw~WzlEvi?sfiX:m\@udHx^zŹūŭDZɳ˵̴̴ʲɱʰʰɰȯȭǬǬƭŮįĭƯȯƫ˿źn{ZsQoKnJ|Vlúƺo]VXctĪƯȱ̴̴̱˰ˮ̯ΰϯȩ~hvVoN~nLxUjĹȬ̲˳̶ͷϹкηͶɳȱƱɳɳȲɲ̵ɲìy~\pOkKjJqQ|ZsǨҶѷηͺ˹ƲȴȲDZȱɯĹm}jLvcCxcDvbAzcC~kKnɿƫħæħŨƩūŭưdzǴǷȸȸʶƱ¨ʿv}VykDre;n`9uiAYw£ǰ˸̺ɷð®ůƮn~[~pKygAuc=}kGwVnɾɭ˳ɳʵɶʶʶ˷̸ιϺоҿѻǭs|X~oHnFtM\vȭзһ˸˸̹ͺλλλλ̹̹ͺͺͺλλλ̺ͻμϽооϽϽоϽϽϽϽϽϽϽξϿϿξ̼̼̼ͽͽͽξξξͽͽͽͽͽͽͽ˻˻̼̼̼ͽͽͽϿξξͽͽξϿϿξξͽͽ̼̼̼̼˻˻˻̼ͽͽξξ˹˹ʸʸʸ˹̺ͻϽҿҿҿҿϼϼϼннѾѾѾѾнϼλλλϺιι͸͸ιιϺѼѼϺ͸ллî}juY}rVtXx\k{ȾͳҸййѹϷϹккѻѻҼҼӽѻҼҼҼҼӽӽӽкѻѻҼѹѹϸϸҸ̲ƭƫŪŪɮϵжӼӼϸ̵ηϸͶη̵˴̵ϸѺѺйηϸйϸͶ̵ͶжжҶϵ˯ʯͰɬvsqhhmz̯ҵжϴεεθϹλλλλѾѾѾѾѼлϺιͶ˴ʳ˱ͳϵжεɾŹĹĸø¶ĻȾüüçƬƬūūūūƬƬʰʰ˱˱˱˱ʰʰƮūé¦ȽŨȫȫʮͱдδͳ̴̴˵˵̷͸θε˴ɭpmr{ʿíư˴̵ͳ̰ͰαδηйѺйη˴ʳͶηйѺѺͶǰérc{^~^iuȽˮαͳеѸθϷϸйѺййϸηηη̵ʳͶһϸǮȺ{zrpnuzõĶĶ´~xtku[uiOrdJtfLykQ|nTu[kȫϲϱ̯vnid]\[|WuO~rLzWcjrɾƫȮȰƱȲɴʵ˴ʴʲ˲ϵ˱ƯƬĬ΍ȸxka{_{]ak{ƻȽūǭɱʲɳư¯ƽd~oRufIp_CjY=jY?kZ@xhNjǽĬưɳ˵̴˳ʲȰʰʰɰȯȭȭȭǮǰƱƯȱʱǬ˿źkwVpN}kG|jFxRhƿƼ÷pWPNZk~ƿĭǰ˳̴̱˰ˮˮ̮̬Ƨ{anNyiHwgE{mJzZr¦˱̴θϹккη˴DZƯŰɳʴȲȱ˴ʳŮºwzX~jI|eE|eEkKwUmŦѵж̵˸ɷƲȴɳȲɲʰĹkzgIs`@u`As_>v_?yfFd~¥¥æƩçéĬůŲƴƶƶȸ˸Ǵ«·yY}oHvi?pb;sg?{UrƱʸ̺ɷıïDZʲ¦~hxS}kEvd>t`=|iHzYq¸«DZɴɴɵɵʵ˶̷͸˹ϼѻʰ·{^tM~lD~oHxTnʱ̵ͺ̹ʷɶɶ˸ͺλ̹̹̹ͺͺͺλλμϽϽооооϽμϽϽϽϽϽϽϽϿϿξʺʺ˻˻˻̼̼̼̼ͽͽͽͽͽͽͽͽͽͽξξξϿϿϿξξͽͽͽξξξξξξͽͽ̼̼̼̼ͽͽξϿϿϿ̺̺̺̺ͻͻμμϽҿҿҿѾннϼϼϼннѾѾѾҿѾнϼϼϼϼϺιι͸͸ιιϺлҽл͸ϺϺîx}b{mRwlPynR|qUewѷջӼһԼӻккѻѻѻѻҼҼҼҼҼӽӽӽӽӽϹϹккиϷͶͳǭƿ÷ɿũ˯δ̰̲βδ˯δ̰̲ͱжҶҸҶжҶӹҶждѷҶҶԷеʭʭ̮ƨȻs_~Y|WtOrNyVgvǼȫͰϲϴʹʹ̶̶̹̹ͺͺѾѾѾѾлϺι͸η̵̲̲ͳͳ̲ɮüž»ƿƿƿƿƿƿƿƿéĪĪūūĪĪéſ½úåŨʮβ̴̲̲˳˵˵˶˶θεʳȬʾs_{[}]gr}Ǽ¬ư˴Ͷͳ̲ͰαͶͶηϸηͶ˴ʳ̵ͶηййͶȱūi{[uUvVanƻʮͰ̲δθʴ͵ͶϸѺѺйϸϸ̵ϸͶɲ˴ηƯùuiba|[|^bu~´ǹ̾Ϳ¥ƩȫŪ̾õqu[rdJi[AfX>gY?eW=wkQqĨϲϲ̯Ȯçĺ}{ztogZvPtQzW{Y]i}ƭȮĭƱȳʳʴʴʲʱѷͳɯƫūʺŶrdzZvYy[}]o{źåũƪɯɯʲʲɳưðê¹s|_xiLn]AiXo[:r[;s`@~pUiƿħéůŲųŵŵǷɶƳìø{]tM{nDse>rf@tQkƱȶʸʷǴŲdzʴȰŨúq~Y|mFvd>nZ7s`?~kK|]užɱdzȳǴȴɴʵ˶̷dzʷθʰĹ~`tM~lD|mFtPhǽǮȱ̹ʷǴŲŲǴʷ̹˸̹̹̹ͺͺͺͺϽϽооооϽϽμμμμϽϽϽϽϿξɹɹʺʺʺ˻˻˻̼̼̼̼ͽͽͽͽξξξϿϿϿϿξξͽ̼ͽͽͽξξϿϿξͽ̼̼ξξξξξϿϿϿͻͻͻμμμϽϽμҿҿѾнϼϼλϼϼϼннѾѾѾѾѾнϼϼϼннϺιι͸͸ιιϺϺԿӾ͸͸ιįw|_yjMwiLwiNujNv\nȾηӼҼӽտҼҼѻѻѻѻккӽӽӽӽӽӽӽӽккккиϷδͳŻ{w~ƿçǪǫŨɭȫƪǪɭˮ˯ʭʮˮ̰̯ʮʭ˯̯Ͱвβɫ˭а˫̽pyWtPqM{iC{iCpL{\qȽǪʭαг˰˰ʲʲʵʵʸʸллллϺι̵˴ϸηͳͳ̰ȬĨɿ{kedcfjvƽȿĻĽžžžžžžĽ¨¨¨¨Ľzttto{ƪʯ̲ͳ͵͵̶̶̷˶̶ʹ˴˯és~]wVvW}`iuȼ¬ů˴ͶδͳͰͰ̵̵̵̵̵˴˴˴̵˴̵ͶͶ˴ǰū|dwWrRrP|[iƻ̰α˱ͳ̶DZʲ˴ηйѺѺйϸʳϸϸ˴ʳǰq_vVwUwWzXerƻ¤ƪ˯ǫʮβѵϵͱǭĪk{mSk]CgW>gW>j\B|pVuũϲϲ̯ͰʮŪźƹp]vPrMrNtS|]mƺ¬ĮưɳʴʲȱȰβͰʮɬŧȻlg_wVuS{[fnƽȩʭʭɮʯȭȭɰʱȲDZůŬǪſo{kQjY?fU;eT:dS9hX>zjPgèĬůưưǯȰȰɱʰʰɰɰɮɮɮɰɲɴȱʳʱŪƺerQ|lJzhD{iExRjſƼ·pX~O|KSas©Ůʲ̴Ͳ˰ˮˮɫȨŦzyZyfFscBp`>k]:obBxX}ūɱȲɳʴ˵˴̵̶ηȳͷθʴȱ˴˴ȱ{{Y~jI|eE|eEjJsQhäϳδ˴˸̺ʶ̸ͷ̶̵̲ødr_AkX8oZ;mY8nW7p]=ugL~rXgu~úƿ¨îıųŵĴĴǴıĹ}_vO}oHugBqe?{nKf¥ŰƴǷȸɹ˸͸̶ŭūy^~oHuc=o[8r_>yfFqRe~ëƲDzDzDzȳɴɳ˶ɵ̹ϹʰĹ}_rKmE{lErNfǽǮɲ˸ʷɶȵȵɶʷ˸˸˸̹̹̹ͺͺͺμμϽϽϽϽμͻͻͻͻμμϽϽϽϿξʺʺʺ˻˻˻̼̼˻˻˻̼̼ͽͽͽͽͽξξξϿϿϿϿξͽͽ̼̼̼ͽξξϿϿϿξ̼˻ξξξͽͽͽͽͽ̺̺ͻͻμμμμμҿҿҿѾнϼλλͺϼϼϼннѾѾѾннϼϼϼнѾѾϺιι͸͸ιιϺιԿ͸̷ιŰ}d}nQ{lOxjOugL}qWiùʳϸϹкԾտӽҼҼѻѻккϹӽӽӽӽӽӽӽӽѻѻѻѻиϷδδ|od|_bi~ƿĽƿüüüü»üĽü»üĽŧŧ¤Ȩѱ̫Ƿu{WtPqL}iD}jBsMxZqʿƪɬ̰ѴʯʯɱɱȳɴɷʸϺллϺι͸˴˴йϸδͳʮĨĽsx]oT~mQnRuX}`rú£ĥǾ¨¨¨¨ééé騦ƿuh^|Y|Y\i}Ĩȭͳδζζθͷ͸̷˵̵̳ϳɯǼzc|YwX}^gqȼů˴ͶδͳͰͰ̵˴ʳʳʳ˴˴̵̵˴ʳʳʳɲŮĪ|byYuSuS^mȽβϲ̲ͳ̶ưǰɲͶйѺѺййǰϸһͶʳ«u|^pO|iHoNuT}\oź¤ʬ̮ɫǩ˯д˯ͱδϵϵδ˱ʰγƫe|nTxhOyiPqWgɭϲͰʭ˯̰˰ʭǫŧĤƣ ̿ʽƹq_|WlInLuV~`l}íưɳʲʲȱȰçħæäʼwh{YyWvStQxVbvĻȩίαˮʯ˰ŪŪǮȯDZDZDZȯαĤĻz}oThW=fU;cR8cR8fVo_=i[8m`@vV|Ƭʲɳɳɳɳɲ˴˵ͶDz̶̶ȲƯȱȱƯzyW|hG{dDzcC~gGpNhĥϳδ˴ͺμ˷κθͷͶ̲ø~ao\>iV6mX9kW6mV6n[;ueKuiO}rV}ajwžƿ¨ìıŲĴĴı¯˿ø}_vQ~pIvhCpd>xkHbŨŰųŵǷ̻λϼϸDZʰȫżfuQyjCo[8q_;ubBxiJsVmƲűƱDzDzɴɳ˶ͺнҼ̲ź}]~oH|jBxiB}mIaĺŬǰ̹ͺλϼλͺ̹˸˸˸˸̹̹ͺͺͺͻͻͻμμͻ̺̺ͻͻͻμμϽϽϽϿξ˻˻˻̼̼ͽͽͽ˻˻˻̼̼ͽͽͽ̼̼̼ͽͽξξξϿϿͽ̼̼̼̼̼ͽξϿϿξ̼˻ξͽͽ̼̼˻˻ʺʸ˹̺ͻͻͻͻ̺μѾѾнннϼϼλλͺλλλϼϼннннннннннлйѷййϺϺ̻̻νѾн̷˴˴ƬȾhx[whKsbHxgMueLt[}ųȵǴϻտտԾӽкθ̶ӽӽӽӽӽӽӽӽҿѾѽѽѻѹθϴɻpvXlN}jJkLmMbrý~|yyyy{~˺ū˲ʳǰĴosYkRiMiMhLuZrŴ̼ū˱ʰɮɮɮ˰ͳϵжѷӼһѺйηͶ̵̵ηϸ̵ȱǰǰĭȾqvTwgExeEtaAyeJy]rɻǽ©ūū̱ŨƩǪȫɬɬɬɬʭˮˮɬƩŨȫʭŢȻhvRoK|lH{kGpN_xĽƮͳδиѹкϹ͸̷ɳε̵ɭǭç{[yQsMuN|YjľƩʬͳϵ͸̷˹̸ͷεεεε̳˲˲̳˲˲̳ʹ˲ǮçwaxVrQsR~]kͿͲʹ̶̶ȶ̸Ϲ˳ȰʲжӹҸϵɮͲϳβǫz|_yfEyeBvcBvcB~nMeʽи̴Ȱɱ̴ѹҺҺϷиѹҺҺѹиϷ͹ʶʿ~nknzưϹӽϹɳ˴˴˴˴˴˴˴˴ϸηɲpsYwgMsdG{lOtV{]nźŨǩƬʮɰƨǣվ˵ízhZxRwhAzkD~pKyTbsèƯɲ˴ʳʳ˴̵ǰììƯǰŮǰʳƯƯǰļzpWh\Bh\Bl^CcU:aR5gX;p_A~mOnúȥ˭æƿĭȵıǵdzȴɰɰʭȬȫʰɯɯʬǪtyV}nMvgFveIyhL~mSpǾ·lZyRtMzSboĵªŮɱ˳Ͳ̱ˮʭʬȨĥ·ztUp]=jZ9iXoaDtiIujJqiEzrNi½ɫγưðȸ˿ǻǼϿdzõ|btRmHxdAzfE|^ıɷ˸̹λϼϼλ̹˸ͺͺϼннϼͺͺʸʸʸʸʸʸʸʸʸ˹̺ͻϽоѿϿξͽͽȸɹʺ˻ͽξϿϿϿϿξͽͽͽξξξξ̼˻̼ͽξϿξξξͽͽ̼̼̼̼ͽϿϿξͽʸʸʸ˹˹̺̺̺ȶȶɷʸ˹ͻͻμ̺ѾѾѾѾннϼϼϼλλλλϼϼннннннннннлйѷййϺϺͼ̻νѾн͸̵̵Ȯjx[ufIq`FtcIp`G~nUsʿȵʷϼӿҽտտӽкθӽӽӽӽӽӽӽӽҿҿҾҾѻѹϹеrvX}jL{hH}hI}jJz\o·ǼǾáȦƠ›~|{xvrqprrx̾ìŮͽ{dqVlOiLiMnSi˻ǭͳ̲ʯɮʯ˰δѷӹԺһѺйϸηͶ̵˴ʳ̵ʳǰǰɲƯx{YyiGyfFvcC|hM{_t̾ŬȮǭ̱ȫɬʭˮ̯̯̯̯ɬʭʭȫƩƩɬˮɦ˾iwSoK|lHzjFrPb}ǻçƯʲͳδϷϷϹθ͸̷ʴε˴Ȭū¦|~ZxPsMuP|YiľŨɫͳϵιιͻκͷεεεʹ̳˲ʱ˲˲˲̳ʹ̳ȯũw`wUrQrQ}\i˽˰˲˵̶ȶκк̴ɱ˳жҸѷδʯγϳȬƻk}oRscBubAvcByiHyXrťҺϷ˳˳͵ϷϷζ˳˳̴̴̴˳˳ɲ͹̸ƲøíʴϹѻθɳ̵̵̵̵̵̵̵̵˴̵ʳŮ«ļkqWteHvgJ|kMpRz\h|Ǿæūɮʱȫμư}sgYtNpIzkDsNZi{ƼģȬɲ̵̵˴ʳʳ˴ȱŮĭǰǰĭĭǰ««{yoVi]Cl`Fn`EfX=cT7fW:iX:p_AvXpƿĽžĭƱðñİůǮǮǬǪǪȮǭƬǩŨuyV}nMufEudHyhL~mSpǽĻvcYyRvO}Ygx̽īŮȰʲ̱̱ˮʭȪƦĥøztUp]=iY8hW;eV9fX;tiKjƿĦǪƩƩŨūƬǮɯĨǪȫƩǫɭǫŸutSteDqc@qc@teD{lKi˴̶κѾн̺ŴǶǴĮ}^jY;bR1hU4iV5lZ6q^=kNuZ|_|]{Z\dhrzɻèīǮȱŬ}_pOrcBvgFpa@wjJfƯưůDZʳ˴ɰǮǬǪƫçrrUubDiZ=l_?peEqgDoeBtlH]s¤ʰǮðǷʼƺʾ̾˼®ycyTqLzfCyfE|^øƳ˹˸̹λϼϼλ̹˸̹ͺλϼϼλͺ̹ʸʸʸʸʸʸʸʸ˹˹̺ͻϽоѿѿϿξͽͽɹɹʺ˻ͽξϿϿϿξξͽͽξξϿξͽ̼˻˻̼ͽξξξξͽͽ̼̼̼̼ͽξϿϿξͽ̼ʸʸ˹˹˹̺̺̺ʸ˹˹̺ͻμϽϽͻҿҿҿҿѾѾѾнннϼϼϼϼнннннннннннлйѷйϸϺϺͼͼνѾѾιηϸ˱ĩnx[sdGn]Cp_EiY@vfMwbDZ͹ҽѽ͸ҾԾտҼкҼҼҼӽӽӽӽӽӿҾҼѹϹеǭrtVzgIxeEzeFzgGxZrǩɪ̬ϯϬͧǤşÿ}rkf``eq˿ƮȮ˻sx]kNgIfJjOz`tŵũʯ̯ɬɬʯ˰ͳжӹջййϸηͶ̵˴˴ƯȱȱƯǰʳɲĩµ||ZyiGyfFvcC|hM{_sèǮȮūʯɬʭˮ̯ͰαͰͰɬɬɬȫƩǪɬˮ˫jwSoK|lH{kGsQeøéʮ̵Ϸδδζζͷͷ͸͸˵϶ʳƪ¨ʿz|XwQsOuP{XiſħȪ˱ͳ̷̷̸͹ͷεεʹʹ˲ʱɰʱʱ˲ʹεʹʱǫx`wUrQrQ{Zeǹȭȯɳ˵ɷмѻζ˳̴ϵжϵ̲˰еβʿr~sWn`Cm\>m]sdCyjIkʳʴ͹нϼʷįŰĭȽ˿}^kZy^n|xmgefdjwƸ˽ŬȱȯéxxX{lKo`?sdCn_>tgGb}ǾìŲdzdzƯĭīŬŪŨħ˽koRwdDm^?m`@obBoeBndAqgCuRa¦é¯ǵɹǹ˽̼ȷȼtb~YvQ}iDzgF~_ǽʵͺ˸̹ͺλλͺ̹˸̹ͺλλλλͺ̹˹˹˹˹˹˹˹˹˹̺̺ͻϽооѿϿͽͽ̼ʺʺ˻̼ͽξϿϿϿξξͽͽξϿϿͽ̼˻ʺʺ˻̼ͽξξͽͽͽͽ̼̼̼̼ͽͽͽͽ̼˻ʸ˹˹˹̺̺̺̺ͻͻͻμϽϽооͻҿҿҿҿҿҿҿҿҿҿϼннннннннннннннлйжϸϸιϺνννѾѾϺйѺδǬĶry\rcFm\Bn]CeUj[>iXn]?raCqRaw´ʾëëĪŨŨħ¨ľ½¥ĢȿûyyVyjIpa@q`DtcGwfLeuke`_gtʿŪȮǮǰȰȰʯ˰̯Ͱå㣷yqRkX8cS2bQ5cT7bT7eZdU4gX7l]o_EscIyjM|mPziMtcGo^@l[=udFpQdxŷĬ«ëĬĪĩèèëľþæƤȿºwwTwhGo`?p_CraEsbHy_qtwrgbeis}ɾƫɮƭȱȱʲʲ̱ͲͰαŧĤw~oPhU5aQ0aP4aR5aS6eZn]?ziKz[i~ɾƧίѴγ̱ʹ϶ϸϸϸϸηηηηŮǰƯìĭƯĭɿsyWoMsSpPnSz^xĪƫɰȮéǬɯɯ˱̲ͳͳͳͳʰǭĪĪūǭǭũķ~aoMzjHzjH{kI{kJa¨ǭǰȰʰ˱˳̴̶ʴʵɴȲʹɲĨɽfnMnJmL}oLsRbƩȪʭʭɯʰʴͷʹͲ̱˰ɮǬŪĩǬǬȭʯ̱˰ȭƪoxVpNnMnMrQzXtĩȯ̶ͷʸϻѻѹиζ̲˱̲ͳ˰ǬrujNdY=cXm\Hl]Hj[Hj[Fn`EsfFsViyħŨǩǪȫʭͰеζͶͶ˴ɲɲ˴ͶʳŮƾûļɿʾƸrrWo_EhX>gW=yjMtWchex[yjMp_Aq`BvfEpRexȼ©éééī©ŭſĿŨǥȿuuRufEn_>p_Cq`Do^DrVcfkkijs}źȾè˱̲ǰȱɲ̴͵ϴϴααȪťt{lMfS3_O.`O3`Q4aS6j_AwYv½ĿžžƼĽƿħƩƩũũƿgufGhY:hY:iZ;o`AvgHg̵˸̹̻ɵʭáſ|uViX:kZhU7n[=}lPxɾɿȽsg``dix·t{YxhFp`>teDj[:i\<|qSkúǽôŴƳǰ˯αƧzfyV{mJxjEzlIqPvU`jh_{XyVyXzXbrøƪĨt\wOuJW~Wb}·ǭʳ̶ɶʷ̹ͺͺ̹ʷɶ̹˸ʷɶɶʷ˸̹μμμμμμμμͻͻμμμμϽϽϿϿξͽ̼ʺʺɹͽͽͽξξξϿϿͽ̼̼ͽξϿϿͽ˻ʺ˻˻̼̼̼̼ͽͽξξξξͽ̼̼̼ͽ̺̺̺ͻͻͻμμͻ̺̺˹ʸɷȶȶ˹ϼϼϼннѾѾҿҿҿҽҽҽѼѼллллллллллйѷжͶ̵͸ϺпϾҿлййͳƫf}nOpaBtcGxgKiXiY?k[AwiNg¦Ȯζͷ̶̶̶θкѻθϹккѻҼӽӽҿҿѽмθ͵ʴʯéjoQ{hJ|iI}hIyeJv_x¬˴ϷӺ׾Իҹз϶ϷиϷζ̵Ͷͷθͷ̶˵˴ͳгӶԷӶѴϲаɪv}\zkJugDwgEudFp_Ap_AyiHsR~[kyµɾȨίͰʯ˲ʹйййϸϸϸϸϸìŮŮìĭǰŮtxV|lJpPnNmR{_w¨ĩƭūŪƬǭȮɯʰʰʰʰǭ΍éƬɯɯȬĦƹboNyiGyiGyiG}mLb¶ªƯDzɳǰʰ̴͵̶ʴɴDzDZ̳ɲĨɽ~d}jI|iH|iHzlI~oN_ľƿ¤ħŨūȮɳʹͲ̱̱˰ɮǬŪĩǬǬȭʯ˰ʯǬĨktR|lJ~kJ~kJoNwUtŪʱͷθʸκкѹиζ̲˱̲ͳʯèimbF^S7`U9dZ?c[Dh`KumXk|{~}{xrkcz]w[{c~hrź«ĭǯ˳͵ʲ̳ѸռֽѺѺѺѺѺѺѺѺηӼվһйѺйζͱί˭ȫqwZraEhW=aO7`N8^L8[J8_N>fUCqcI{mPczæͰǫȫǪŪĩǫ˯γҺѺйͶʳɲ˴ͶɲìǿƾļºŽ©ʼ´gvfLiY?fVsVdv}oy\ziKvcEvcC{hJuWj{§©é¨©¾Ʈ¼ƩǥǾs}sPteDn_>p_Cp_Cl[AoS|^ckoquȽ§ŪĪƬ̲͵ɱȱɲ͵ϷѶеϲαʬƦryjKeR2^N-_N2_P3bT7odFb¥æé¨ũʭ̯̯̰̰ƪƿnzkLk\=hY:hY:l]>rcFg͸̹ͺ̽ɶͰţzsTiX:kZ~nTwļǿ˲ȮɾƸpd~\{Y{ZdtmwUueCn^|qSlżǽòŰưȭˬʿraxU|nKykH{mJ}\fvqe`|[xWz[j˾¢ãʽk}TsKsJVYjĻŬɴ˵ɶʷ̹ͺͺ̹ʷɶ˸˸ɶȵȵɶ˸˸μμμμμμμμͻͻμμμμϽϽϿϿξͽ˻ʺɹɹͽͽξξξξϿϿ̼̼̼ͽξϿξ̼˻˻̼̼̼̼̼ͽͽξξξϿͽ̼̼ͽͽ̺̺̺ͻͻμμμμͻ̺˹ʸɷȶȶʸλλϼнѾҿҿҿѾѾιηϸйѺѺѺѺϸййѺѺййϸжϵηϸѼҽпҿϺɲηδǽ^xkKzkLwfHp_Ao^@m\>gV:k\?qbE|nSj»Ĩ͵ѻҼккԾԾҼӽӽӽҼҼҼѻѻҿѾмϻͷ̴ɳɮlnPnPmM{fGzfKs\vɾ˶ккͷԾӽӽҼҼѻѻѻ͸ικϻмѽҾӼзγγҷӶѴ̯ʫŧw~\|oMwjGuhEvhEsdEsdEwhG|mL}oLrM~[h~ƦȬʮɯƭ̵ϸйͶǰƯ˴йǰƯììǰƯkzX~nLoOlLxdId}ĩĪǭǬζ̴ʲʲ͵ϷииʲɱǯǯȰǯƮĪźx|[yiHtdCtdCtdCveGcƺŭ˴˶ʴ˴ͳζζθ̶˶ʵDZƭʾ~e~kKzgGxeEwhGpQ`{Ŀ¦ũƭǮǬȫȫǪƩŨŨŨƩɬɬǪǪɬȫ¤buSzjH{hG~kJ|iH|Zv´ȭʹϹкʸκѻϷ͵ζҸӹѷϵɮǽx\aV:aV:f[?[Q6[S>`ZD|fļʼĶ~pcuX{pTxlRwmT~t[mūʰūǭʰͳϴϴγͲ̵ͶηййѺѺѺйѺһһйͶɲȰϳҶϱŨúk~mSkZ@`L4]K5]K7YF5ZG8aP>j\BykPmǻ¦ɯʰ˰ɰƭīŬƭ͵̵ʳɲɲɲɲɲìƯìºǿǿù´vx\q`DeT8hW;p_CsVplqSyfHs`@p]=tdCnPe|żȾÿĿĿĬūĪĦŨţĻg|rOrcBiZ9m\@jY=fU;sWcqĶŹǭǮǮǰǰɱʲ˳ʳʳ̴ζϴϴϲϲ̮ɩo{lMhU5^N-gV:eV9gY|lRyľ¬ĭȯǬ¨ɾ´tf\zYy[ajwkrPscAm];k\;m`>pbEzoQsƺɽɽǼɾŭƪzd{X~qNxkHsP_oµ³mcyZwVbvŶοǹ}_tKrIwNsLzUmǮɴ˸ͺϼнλʷȵɶ˸ǴǴǴȵɶʷ˸̹ооϽμμͻ̺̺ϽμμμμϽоѿξ˻˻̼̼˻ʺͽͽͽͽͽͽͽͽ˻˻ʺ˻̼ξϿͽ̼˻̼̼ͽͽͽͽξξϿϿϿϿξξͻͻ̺̺ͻͻμϽ̺̺ͻͻͻμμμͻλλннѾҿҿҿҿӽϺϸйѺѺѺййϸййѺѺййϸжϵηϸѼҽѾл˴йͳ»]zmM{nNxgIraCp_An]?m\>m\>p_CyjK}`xž˳кѻͷ˵θҼӽӽҼҼҼѻѻѻкѾѾмϻθ͵ʴ˰̾ioQsUsSkL~jOxa{˶кϹͷӽҼҼҼѻѻкк͸͸͹κϻммҽѻ϶εеѴαɪťȽetRwjGsfCviF|oLwWz[{ZvS}oJ}oJwPXexȻæ˯̲ʯʴͶϸͶɲǰȱʳƯǰŮìŮǰìƼeuSyiG~kK}jJxdIféŪ©Ƭʰʯ͵˳ʲʲ̴͵͵̴ȰǯƮƮǯǯƮĪȽxz[wgFrbArbArbAtcEdƺŭ˴̹˵̵ͳζζͷ̶˶ʵDZȯĭçʾ~hnP|iIzgGxiH~oP_züžǽǽȿȿȿȿȿǾƽżżħħŨŨĹ^rPvfDubAzgFzgF}[yŷɮ̳θϹʸκϹ͵̴͵жҸѷϵƫùtX^S7_T8f[?\R7_WBe^Klļ­ŮĩɻǹȺɻɻȺɻƸqicw\w^}dt¸éǭĪƬɯ̲γγͲ̱˴̵Ͷϸййййййййη̵ɲȰϳд̰ħħz~mSkYA_J5`K6aN=]J9\I:_NudHc|ŶowZyfFr_?n[;p`?yiHyZpżéĿ¬éĪƨǪţcxnKo`?fW6kZ>jY=iX>x\sĶŷƻʾɰȲɲɲʲ˳͵͵ʳ˴͵ζϴϴϲϲ˭ɩ£o{lMjW7cS2hW;fW:i[>zoQsææħŨūƬŬǭʮͰ̯ʭʮͱ˯ũŷmwhKn_BhY:hY:m^An_Bw]ƺ˸˹ɹ̽ϼϵˮʭ·z]m\@iX:kX:hU7o\>zjPwľíįưǮǮéȽtg_xXzZ~^fss`{kIn^_L;\K;]L:^P6i[@uZyƼŪͱͳ˱˲ʱ˲̳˲ʱ̴˴˴˴ɲǰŮìȱʳĭļŽ«ǿnsSr_?gT4gT6p_A}lNmĵƷq|_}jJubAo\;n^l]@paDn_Bx^ƺ˸̺˻ο͵ɭ˯ɭɿʾx[kZ>hW;lX=iU:n[=xiLrľĮíưȳƯɲǮĩʿvj~^zZuVwV~\[sOvd@m[7hX6hY8l_=tfIxZ|Ǽ˿¯¬ʼ³vd{XwTyV~^oǾũũũũɻxha`er}nYsLtMyT]lƼůƱijƵ˸̹̹˸ɶȵȵʷȵȵȵȵɶʷ˸̹ͻͻͻͻμμμμμμμμμϽооξ̼ʺ˻ͽξξͽξξξξξξξξϿϿξξξξϿϿξͽ̼̼̼ͽͽͽͽͽξξϿϿϿϿξξͽͽ̼˹˹˹˹˹̺ͻͻʸ˹˹˹̺̺̺ͻͻϼϼнѾҿԾһһһһѺйϸηϸййѺѺййϸϵжѺѺѼѼннлйӼƬluRxkI{nL|lKpOzZ~^csSzjIzjIxiHzmKbưϹ̶ưDZͷҼѻѻкккϹϹϹϼϼϻϻθζ̶Ͳŷ|`pR{]{[oPmOlƻȱ̴ззεкккккккк̷͸͹κκκ͹͸ӿҽκ̴ͳαƨ˾g{mJwiFwiFuhEsPbs~WtKoDxg<}lAsK_t˽ǭ˱ɳɲǰǰǰƯĭɲʳȱŮŮĭļz~[{kIo_=ubBvcCwcHhƸūŪīʰϵϴ̴̴͵ζϷ͵ȰŭĬëªĬƮȰǯƬ¦tuVtcEscBvfEwfHziMk˿ưʵ˸̶ʳ˱˳ʲɳȲȳȳĮɰɲƪǻyfoQ}jLzgIyjKqRb~¼þåæǽƽĻżƽǾżúƽżryV}mKn^cT3fU9hW;l[AeŪɮǯưdzƲʶʶ˴˴ͳϵϴѷ̵̵ζϷеϴϲα˭̬ƧkwhIjW7hX7hW;fW:n`Cy[~¥¥æƩǭɯɰ˱̰α̯ɬɭͱ̰ǫʼpvgJn_Bm^Ao`CrcFo_E{aƹ̹μξ̹ƯȱɲŬǮv\kZ@iXwiL`ʿïDZǭôrdZwTxV~^k~ݦŪĩĩūǻzlb_fq{zrd|UtPxS}Zf|êȳƳĵŶɷʷʷɶɶȵȵɶɶɶɶɶɶʷ˸̹˹˹̺ͻμϽϽоμμͻͻμμϽо̼˻ʺ˻ͽϿϿϿϿϿϿϿϿϿϿϿϿϿξξξξͽͽͽͽͽͽͽͽͽͽξξϿϿϿξξξͽͽ̼̼̼˹ʸʸʸʸ˹̺ͻʸʸʸ˹˹˹̺̺ͻϼϼнѾҿҿӽһӹӹӹӹҸѷжжѷѷҸҸѷѷжϵѷһһѼѼннѼϸηĽ`xnKtgEylJ~nM|[q|}jzYrPwiFqdAtQk̶к˵ʴͷϹѻѻкккϹϹϹλλκκθ͵˵̱Ƹ|`rT|^zZmNmOnīͳͳеҶϳѸѸѸѸииии̵ηθϹϹͷ̶˶Ѿн͹˵̳βħȹ_yiGxjEzlIzlIyViȾ̿b{RqFyf;|i>~mBtNc{ͿȮ˵ʳȱȱɲʳɲȱͶͶȱì«ûx}Z{kIo_=taAubBvbGhƸĪĩêʰδγ͵ͷθкѻθɳůí¬¬íưȲȲƯ¦tsTudFveG{jL|kMpTrëDZɴɶɶǰȮǯƮưưDzDzĮɰɲũƺvblNzgIxeGxiJrUh¢ĤȪ˭ˮɬǪƩȩȩɪȩŦż¹¹żƽqxU}mKo_=jW6q^=}jIẖʱɳ˵ɷϻθϷиѹѷҸӹԺеù}}rV^S7\Q5dY=`T:dX@j_ImưʵƱȯʰ˲̲ʱʰȯɯȯɯȯɯƭƬê©ǬŪŪȭ˰̱˯ȬǪɬ̯ϲггϲγʳ˴̵̵Ͷ̵̵̵ηͶ̵ʳɲɲɲʲ˲̳̱ʰĨz~cueLl\CbR;ZI5UD2SC3XH9_P=dT;bS6hX>vhM}bxȾĩ¦ĪʯεѸ͵̵ͶͶ˴ȱĭ«ƯƯ«ƾìǰļowVxdCr^=p[gS8mZubBubBwcHjƸéĩī˱δ̱θθϹкѻϹʴưĮííĮDZȲȲƯqqTudFxgI|kM}lNtXxĴŭDZDzƵǴĭūŭĬůưȳȳDZʱȱũǻu|_|iKxeGwdFxiJtWk¢ťǧʬ̮ͰͰ̯ˮѲѲѲбͮʫƧäǾǾżżȿúryV~nLqa?n[:ubAmLj¥γʱʴ˵ɷϻкѹӻҺѷѷҸԺϴy{pT\Q5YN2`U9[O5dX@k_IkɽĮɴƱʰ̲δδ̲̲̲ͳɯɯȮɯɯ˱̲ͳѶеγͲͲ̱ʮǫȫʭͰгѴѴϲα˴˴̵ͶͶ̵̵˴ͶͶ̵̵˴ʳɲʲȯʱʰǭùltYn^EjZAdS?_P;_O?bRBhXIm^Km]DfW:bR8jZ@yiOd~ȼêūƬȮ˳ζ˴˴˴˴ɲǰŮìĭŮŮŮ~`kKt`?t`?s^?mX9n[;xeEpR{]kruxul_zT{oIrf@qe?uhEsQjĵ̼̽μ̿¨ĿƿſlvSteDfW6fU9fU9l[AhƮȳǴǴǶǶɹɷɵʱɯ˯̮ͱεηϷиеϴϲαΰ̬£hufGiV6fV5iXfR7mZ<{jNu¦ȮƯȳʷȵǴŲıDZ˲̰ȫĤövdvRweAnZ5lX3o]9jZ6gW6eX6qcF`Ŭ̾Ⱥw_{UrNvR~]k~ŭūŭŭƮǰɴȲů˿ĹzcZXZZZZ]cjqƴƷŹƼŹȶǴǴȵʷ˸˸ɶ˸˸ʷʷʷʷ˸̹ʸʸ˹̺ͻμϽϽͻͻ̺̺ͻμϽϽͽ̼ʺ˻ξξξξξξξξξϿϿϿξͽ̼˻̼ͽξξξͽͽͽͽͽξξϿϿϿͽͽͽͽͽͽͽͽ˹˹˹˹˹̺ͻͻʸ˹˹˹̺̺̺ͻͻннλλϼнѾѾнѻйҸӹԺԺԺӹӹжѷѷҸҸѷѷжδҸԽӼллҿѼȱƾh{pPqgDtgEzmKqPg̹dzzcvR|nI{mH|oMjʴϹкϹͷӽҼҼҼѻѻѻкͺͺ̸˷ʴɱưǬ|uY|kMqS~kKwbC|iKiͲոѴӶԶѲѸѸҹҹҺҺҺӻϸйѻҼѻθ˵ʵ˻ͽ˹ŰƮŪƵ|[qMvd@rb>yiEzUnȽǿ~byNqFqCpClA}kCqMxWg}øǿǰ˴˴ʳʳ̵̵̵ɲŮìowTzjHp`>vcCwdDyeJlǹĪŪƭͳδ˰ͷ̶ͷθϹθʴDZưůĮůȲɳȲƯʾopStcEveGyhJxgIsWwĴŭưŰijƳĭūĬŭưȲ˶̷˵̳ɲǫ˿vz]{gLxdIvbGxiLuXmĤǧȨʪ˫̯̯̯̯ббббϰί̭ˬƧŦä£ŦƧǾszW~nLrb@q^=yfEoNjæγ˲˵̶ɷϻкҺԼҺжϵжҸͲw{pT^S7ZO3`U9[O5dU>j[D}fƺí˶ɴ˳ζϷϷ͵͵ϷҺ͵̴ɱȰɱ˳ϷѷеϴͲ̱˰ʯʮɭȫɬ̯ϲггϲα̵̵ͶͶͶͶ̵̵̵ͶͶͶͶ˴ɲȱɱȰūsx^|lSm]Dk[Bk\GrcN~n^yh{kxeoVudHkZ>iZ=o`C|nQfv§ʮʰȮɯ˱ȱȱǰǰƯŮĭìŮ٫٫wrRzcAs\:xa?t`?kW6kX8ubB|kMvXixwh|TvjBma;m`=xiHyZv±ʹϻǿ¨¤¥d{qNqbAeV5fU9hW;p_EmɳʷǴȶȹɸ˻˹ɶʲɯɮ˫˭εϸииееϲαΰʪǾjzkLmZ:iY8l[?j[>xjMoĤŨŧħŨƩȫɯʰ˲̲ɭͰϲαϳд˯ĨƸpyiOqaGl]@m^AscItdJoʽͼǽɿ˿ǹǷ˾xv_vdLraGmY>cO4mZ<nPxĩȯƯȳ˸ʸǴȵȴʳ˲ͱβͯ ȸxalGs_:p\7q_;l\8fV5dW5qcFdžŪƺyfY{S{U_o·ǿīê¬ĮůưǴɴɲŬ¥żqc\~Y[_dnxȾdzǶŹǽǽŻɷǴƳȵ˸ͺ̹ʷ̹˸˸ʷʷʷ˸̹˹˹˹̺̺ͻͻμͻͻ̺̺ͻͻμϽξͽ˻̼ξϿξξξξξξξξͽͽξϿϿξ̼̼ʺ˻ͽξϿξͽͽͽͽͽξξϿϿϿͽͽͽξξξξξ̺̺̺˹̺ͻμμ˹̺̺̺ͻͻͻμͻннͺλϼϼннϼкйѷҸԺԺԺԺԺжѷѷҸҸѷѷжδҸּԽѹкҿһŮ}`xmMsfDwjH}nMrQg˸Ťλj{YtR}oLviI{`|ĭ̵йϸζӻӻӻҺҺҺѹѻιι̸˵ɳȰƭŪs~mQtcGzgIs`@oZ;wdFfϴչҵԷշвҹҹҹӺҺӻӻԼйѺӼԽҼй˵˵˸νʷïëvrOlIwc@q_;xhD\{ƣǽw_uK~oDtIvKqF~oH~nJ~oNtUgzȭȯŬūĭɲ̴ʳȱƯéirPxhFp`>wdDxeGzgInȺūƬȭβϵʰ̴˵˵̶ͷ̶ʴDZDZưůưȲɳȲƯɽn}nQqbCsbDudFtcEpTuëĮįŲƳĭūŭƮDZʴιϸϹζ˱Ǭçwz[|iKxeGudFxiJuXmħʬʬ˭˭ˮˮ̯̯ˮˮ̯̯ͰαϲϲɬɬǪǪʭʭtyW~nLscAs`@{hHoOi¥ϵ̳Ͷ̶˷мкҺԼҺδ̲δϴ̱wqVdV;_Q6eWrcD}oR{^zɾ˰˰ȭɯ˱ǭƬūìììììưưí©ŬɿlnNua>q]|oOrƧɩǩŨƩǪǫɯʰ˲˲ȬͰϳϳдд˯çŵnzjPqbEj[>k\?rbHtfLqʿλȽķɼ;ȷǵǼvw^xhNudHlX=bO1mZo_=hY8eW:rgKlūƩwk]|X[eq¶ȽžůŬůDZɳɳɶͷɲȮƪĥ¹sk`ciuõĹɿDZ̹ȷŹǽȾŹɷȴƳɵ̹ϻͺ̸̹͹˸˷ʷ˷˸˸˹˹˹̺̺̺ͻͻͻ̺̺̺̺ͻμϽϿͽ̼̼ξϿϿͽͽͽͽͽͽͽͽ˻̼ͽξξξ̼˻ʺ˻ͽξϿξξͽͽͽͽξξϿϿϿξξξξξξξξͻͻ̺̺ͻͻμϽ̺̺ͻͻͻμμμͻлѼϺϺϺллѼѼѺҸҶҶӷӷԸԸԸӷҶѵддддѵϳӷ׺ԺѶѸռԾѹҺҸƪq{^sdCykHxjGvfDvTsɹâŵ~i|[tSpRqTenī̳϶ҸжϵϵжҵҵѴеϸϷ̵ʱȰȮǮȭludHn]AxeGs^AoW;uaF~c§ϴ׽ӹּϵҺҺҺҺҺҺҺҺѹҺҺҺѺѹϸϹͺ͹ɵŮéɺf~gE|hCxd?uc=qLgĺǥŸp]uQzS^[]^|YsQ|oMvW_r˸âħçʲ˲ǯĬǭƪf|iIxeEubBtcEwfH|kMhɻǫʮˮ̯ͱͰ˳˳̴͵͵̴˳˳ƮǯǯǯƮĬªżl}pPo`Al]>o`ArcDyjMr¬ŰƳŲŮƯɱ̴θϹккѻ͵ʯʮħpyXzgFzgFwgFyjIxXsƩɬʭˮ̯ͰͰ̰˯̲˱ɯȮɯ˱ͳϵ̲ʰƬūɯɯpsSmMzgGxeG{hJoSq̱˱ѸӹѹԼϹϺϸй϶ε̱ʰȮʼ|_o`CeV9fW:gW=fV?m\Hp\{ʾưʴ̶˵ʶ˷͹ϻϻлͺ̹̹˸˸̹̹ͺϸҸѺϵʳɯʳδŮɯ˴ͳͶͳ̵ͶϸϺллйϸηͶδϵδ˱ͳдδȭɮæxza}mTvfOqaJsdM~oX~gx·xl]|oLreBqdAxkI}pPe{Ǽçʮ̲̯˭ŦĪǯìîİưȲƯ¨˻sxWqa@rb@qa@o_>n_@qbCxiL|nSy_mǻ¯įƴƳ}a}mLuc?xdAzhDtRf{ŻȬ§¦þĦĦzx\q\ApX>nVtaCoZ=lT8s_D}bγռѸּжҺҺҺҺҺҺҺҺѹҺҺҺҺѹиϹ͸̷Ȳì̾±x_xa?yc>vb=xf@xSnŻĢ³}l`\dpxxti|ZsQtSyWbpμ¡ŧŨĨȮɰƭ¨ũĨbzeFvcCubBudFziKpQmɻǫʮʭ̯Ͱ̴̰̲͵͵͵͵̴̴ǯǯǯǯŭëƽizoOn_@j[o`AxiLpĮıĮƱǰʲ̴θϹккиͳɮɫ¥nwV{iEwgCvfDxkIxXtȫ˰˰Ͳγδϵϳϳ̵˴ɲȱɲ˴Ͷη̵ʳƯŮɲɲttTnP{hJyfH}jLrXw¨δ̱зҸѶҸθιйѺз϶̲ʰʰ§l|kOm\@hW9gV:fV=jZC{jVqȹƱϷɳɳɳɶ˸̹͹ͻнϼϼνϽϽϽнιѺһѺη˴˴̵ȱȱǰƯǰɲͶйллллйϸηͶδϵͳʰ̰ϳͱȫǨȷf|lRscJueNzjSt]jzźǩʬǤo[~qNxkHylJ|oMyYl´ɮͱǧ¡ŨǭĭïůDzɲƭͽ±}_yiGm]o`AqcFxjMuZ|awĹ®ƴǵǶƺĸſsyYlK{dBzfAoK[qƦȿĿ£zv^s[CqU?nT=iS;jY?shLtĭ˴ʳ˴ȱŮʳηͶ̵˴ʳɲɲʲ̶θиҺҸҸϴϲɮƩɺcmLwcBscAnd@tlG~Yv˪ίˬƫƫǮǮDZȲɵʴʱ˰ʯʯͲеʯ§îpmOq`Dp_Ep`FugM|pX}į˶ͻѿѾ̹˶˴ȲȲʲǭlxZxjMteFm\>iY8zjIcɳɳưʴ̶ʴʴʴȲDZDZDZDZDZDzǰůƿxxZyfFs`@n]?iY?k_G}u`î{pnt|˿í¯²ųƱɱ̴ζϷζ͵˳ζʲƮŭĬ»¦ɭ̵̲ʵʵȷǶǸȹɺʺʷɶɶʷͺϼϼλλλλͺͺ̹̹̹ͻͻ˹˹˹˹ͻͻμϽϽϽμμͻ̺˹˹˹ʸ˹̺ͻͻϽϽϽϽμμμͻ̺ͻͻμμͻͻ̺̺ͻͻμμμͻ̺ͽͽͽͽͽͽͽͽͽͽͽͽͽͽͽͽ̺̺ͻμμͻͻ̺μμͻͻͻ̺̺̺̺ллϺлллѼѼѼѺҸҶҶӷӷԸԸԸӷӷҶѵддѵѵѴӶֹոҷѸԽԻѸϵͱƼ~a{lMqa@ta@ubA~kJeĴĴzleiotvlhivæ̯ββϲϲϲггеʹ̵ʱȯƬūĩĩisbFjY=s`BmZwc>~oH_yùȻvign|l_xYwU~Ybw;ŧǪǫǭȮū¦¦|^vcCtaCubDwfH~mOuXtɻǫˮʭ̯ͰͱͳζζϷϷζζ͵ȰȰǯƮĬªſƽhxmMm^?iZ;k\=m^?yjMpɿɾ¬ð¬Dzȱ˳͵θϹккϷ̲ɮȪοktSzhDvfBueCviGxXtȫ̱ʹεзѷҸҶҶͶ̵˴ʳ˴̵ηϸ̵ʳǰƯʳʳuvUnP{hJzgIlNv\|ūε˱϶жγж̴ͶйһҹѸ͵˱̲ʯɻbxiLl]@eV9gU=gU=o_Hv]z̽˱ɱʲ̴θϹϹθκлллннннѻϸҸԺӹж̲ɯɯʰɯǭǭɯ̲ϵйллѼѼйϸηηδδ̲ɯ˯βʮħʻlvZvfLn^EufOt]n}¶¨ͱѵϯɧ˾tcyWsQ}pN~qQ|\iq}ȺĦĤ¡¤Ʃǭū¬¬DzȱêƷlqPo_=jZ9m^=reEwjJqT}`q{ƻíȴɵȶȷƺ÷ſļn{ZyeBvb?vd@~pK^qĻæ¤£þ|x`s[CpT>lR;gQ9hW=shLt«ʳȱ˴ȱĭʳ̵ͶͶ̵̵˴ʳʲ˵ͷϷѹҸҸег˰ŨŶ|_|lKwdCueCqgCzrMbǾͬϰˬǬǬǮǮDZȲɵʴʹγ̱ɮ˰γʯèðspUtcGraGscIzlRu]Ű˶̺Ͻϼ˸ɴɴƲưȲŭo|`~pS{nNxiJwhIy[sſʴȲůȲʴɳȲȲDZưưDZDZȲDzȱȲƬƺgqSudFsbFqaGwkSo­ƿŬɳɳdzİIJųɴ˳ζиѹиϷ͵и̴ǯƮǯǯŭ¨ĽĽž¥ǫ˯ͳ̵ʵʵȷȷȹȹɺʺʷɶɶʷͺϼλͺͺͺͺͺ̹̹̹̹ͻ̺̺˹˹̺̺ͻμμμϽμͻͻ̺ʸʸʸʸʸ˹̺̺оооϽϽμμμͻͻμμμμͻͻͻͻμμμμͻ̺ͽͽͽͽͽͽͽͽͽͽͽͽͽͽͽͽ̺̺ͻͻͻͻ̺̺ͻͻͻ̺̺̺˹˹̺лллллѼѼѼҽһҸҶҶӷӷԸԸԸԸӷҶѵѵѵѵҶҶӶոӹҹҹԽҹ϶ͳɭ·{z]xiJvcCs`?ta@nMixrnrobdtṴ̃βггϲααϴʹ̵˲ɰƬūèèlwfJm\>ubDp]?nY:xgId˰ҹ϶ּжҺҺҺҺҺҺҺҺѹҺҺҺҺѹизͶͶȰ{_mLxaAxdA{iEuQh|mirýáĢƽ|oe~]}X\jɾŨȬʰ˱Ȯ¨¦}{]taAs`BubDwfHnPwZwǼȫʭɬˮͰͱδϷϷииϷϷζʲɱȰƮĬſľżfxmMo`Al]>n_@o`A{lOqɿʿíıĮɴʳ̴ζϹϹϹϹ͵˱ȭƨ˼gqPxfBsc?rbAtgGwZwǭ˲̳εѸѷӹӷԺϸηͶͶͶͶηϸ˴ʳǰǰʳʳvvUnPzgIzgImOx^ǭ϶˱εϵ̲δ˳ͶйӼԻҹζ͵̲жɮzz]vgJj[>m\BhW=hX>vfL|b{ͿƮɯ͵иϷζ̶̶̵Ͷ͸͸θθͺͷѷҸѷж̲ɯƬūȮȮȮʰ̲δδδйллѼйϸηηδδ˱Ȯʮ̰ũƽp~]tV{lOtdJ|nT~g¶ʾªǭȮɭ˫ĢŸ{k`yYxXzZ|\}]ds¢âŧȪȬūɿĬé˻y`lKp`>n_>whIuUamzǻ˿ĮDZȴdzȶȷȼ÷¯õjoNzhDrb>tfC~qObyƿƩŧŦĿzbu]EqU?kQ:eO7fU;qfJrɲȱʳȱŮʳ˴̵ͶηͶ̵˴ʳ˵̶ζиѷѷее̰ħ³{bpO}jJ}mKzpL|Wm¡έίʭȭǬǮƭưDZɵʴзеͲɮʯͲ˰Ū®yy]zkNwgMzlRs\}eƱ̷̹ͻͺʷȵɴƲűƳưû{nfcbduºů˵ȲưȲʴʴȲȲDZDZDZȲɳɳDzǰȲɯxcvXvYw]hıŨ¢ǾžũǮʰƭȱȲȱűįIJƱ˵͵ϷиѹиϷζи͵ɱɱʲ̴̴˱ƿƿ¥ƪȮȱǰʵʵɸȷȹɺɺʺʷɶȵʷ̹λλ̹ͺ̹̹̹̹̹̹̹̺̺̺̺̺̺̺̺ͻͻμμμͻͻ̺ʸɷɷɷɷʸ˹̺ооооϽϽμμͻͻμϽϽμͻͻμμϽϽμμͻ̺̼̼̼̼̼̼̼̼̼̼̼̼̼̼̼̼̺ͻͻͻͻ̺˹˹̺̺̺˹˹˹˹˹̺ϺлллѼѼѼҽҽһҸҸҸӹӹԺԺԺԺӹҸҸѷѷҸҶӷԸԺӹӺӺӽԾ϶ʹ˱ƪxz]zkL|iIydEydEqRk~{st{ŨŦzbyYaqũɭͳδͳ̲̲ʹεʹ̳˲ȮƬĩèoziMo^@vcEq^>q\=|kMg˰ѸεջжҺҺҺҺҺҺҺҺѹҺҺҺҺѹиϷз϶ʰɿqwW|iH{gF{gF}jIvTh{~{tnráȦǥɧâth[zU~Zk~ĻħʮͱʰΦž|z^taCq`BraCwfHnPvYvȽȫɬɭ˯ββϵϷииииϷϷ˳ʲȰƮĬªúdxmMqbCqbCsdEteF}nQsůǴDZ˶̵ζϷккϹϹ͵˱ɮƨɺdnMuc?qa=p`?tgGx[yƬʱ˲ʹзжҸҸӹϸϸϸηηηϸϸʳɲǰǰʳʳǿuuT~mOxgIxgInPy_²Ȯ϶˱ʹδ˱δ͵ηѺӼԻӺѸ϶˱ѷѷ¦ux]whKudHm\@hW;p_CpSiȼçɯ̲̲ɯɰɰ˱˱˴̵͵̴˵˳жϳͱ̰˯ʮɭȬũĽĽ¦ȬͱϵϸϺллϸηͶͶͳͳʰȮɭȬžv_tQrQqTpV~dyŹůDZưȯʱȮǬħǺuhe`xXsStT]p}ƨǪƪèʾǹiwV|lJwgEzkLy\n}÷˿ŬƭDZȲȳȳɶ˹ʻƷ²ƳȲͿycxW|lJviFxnKzYm|¥ĦŦ{cu]EqU?jP9cM5dS9ncGoȱǰʳȱƯ˴ʳ˴ͶηηͶ̵˴̶ͷζϷжжее˯Ħ´j|[yX{Z]h~ƧίͰʭɮȭǮƭůDZȴʴѸѸ϶ʱɰ̳˲ȯijo|by`~dovǼɴ͸̹̹˹ʷʷʷȵƲǴȲȺĭɳ̶ʴɳʴ̶˵ʴʴɳɳɳʴ˵˵ʵɲȲɯŪƸ~xwz¯ɸǴëéūȮ˱̲ɲȰ«ĮįůįůƱɳ̶̶θϹϹϹθͷθͷ̶̶̶̶ʴɱ¥ħǭɯɱɱ˵˵ʸ˹˹ʸʺʺʷɶȵʷ̹ͺͺ˸˸̹̹̹̹̹̹̹̺̺̺̺̺̺̺̺̺̺ͻμμͻͻ̺ʸɷɷɷɷʸ˹̺оооϽϽμμμͻμϽϽϽϽμͻϽϽϽϽϽμͻ̺̼̼̼̼̼̼̼̼̼̼̼̼̼̼̼̼̺ͻͻͻ̺˹ʸʸʸʸʸ˹˹˹˹˹̺ϺϺѼѼѼѼҽҽҽӼѺҸҸӹӹԺԺԺԺԺӹҸҸҸҸҸչԸӹҸҺӺӽӽ϶˱Ȯ¥rzZ}nO}hI{dE|eFpQdqtrw~ħ̮ΰͭżk}]yY`sĽƬ˱̲˱˳̴εʹʹ̱ɯƪèæoyjKn]?taAo\p`>qa@wjJ}`}Ƭʱ˲ʹ϶Ϸжѷѷϸϸϸϸϸηηηʳʳȱȱ˴˴uvUnPziKyhJnPy_~ǭεʰʹεͳжийѺһӺҹѸз˱ѷҸʰɻu}bzkLqa@jY;k[:teFuTkyĹĨ˯̰˯˰̱̰ͳδϵϵδ͵̲ͱ̯˯̯ͱ̯ʮȫ»üʮҶηιιιηͶ̵˴̲˱ɯȮȬĨk}V}oJ|nKtUf{ĸ¬ɶʷɵ˵κθɯǬƫŪ´}pcwW{nNwjHzmKyWcx½¥ǪƩæø|k|[sPpNtQ`q´Ĺɽ¨ǮȯȲȲȳɴ̹μʻȹǴɶʴéŶwhzYrPzoO~uTbnĿ£½~x`s[CpT>jP9dN6dS9ncGoǿȱȱ˴ʳǰͶʳ˴̵Ͷηηηηθθζζδϵγγ˯ƪǹ|qppvȿʫίͰ̯ʯɮǮŬůưȴʴ϶Ѹз˲ʱʹʹʱͼ}Ǽí̷Ϻͺ˸ʸʸʺ˺ɶǴǵɶŲŽøôŷͿȰȱʴʴʴ˵̶ͷͷ̶˵˵ʴʴʴ˵˵̸Ͷ̴˱Ǭ¦ǹ´Žʷ̼ʹűưɳ̶θ̶DZí¬Įưɳ˵ͷ̶̶ͷθθͷͷ̶̶ͷͷͷ˵ɳưĬ¨ƬȮɱȰ˵ʷ˹˹˹˹ʺʺʷɶȵɶ˸ͺ̹ʷ˸˸˸˸̹̹̹̹˹̺̺ͻͻ̺̺˹˹˹̺ͻͻͻͻ̺ʸʸʸʸʸ˹̺̺ϽϽμμμͻͻͻμμϽϽϽϽμμооооϽμͻ̺˻˻˻˻˻˻˻˻˻˻˻˻˻˻˻˻̺ͻͻͻ̺˹ɷɷɷɷɷʸʸʸ˹˹̺ιϺѼѼѼҽҽҽӾӼѺҸҸӹӹԺԺԺջԺӹҸҸҸҸӹռԺһҺҼӽҼҼϷ˱ƬƽmwX~nM~fJ~fJiMsV~cnuyŨʭ̮ͯͮž{k_~]gqüƪʰ˱˴ζ̳̳˱ʰȮũpyjKl]>p`?l\;p]<oNqŪ̱з̳ԺжҺҺҺҺҺҺҺҺѹҺҺҺҺѹиϵӶϲʭǼbnP{kJyfFyfFwgFzkJzXhqqήгͭͯҲαŦ´o_wUzY`fvüžütwZn]?m\@p_CteH}nQv[xȽƫȬȬ˯ϲгζζϷϷϷϷζζ͵̴ʲȰƮƮƮƬ¹avkKrcDsdEufGteF~oRt´êĮʴͺϹιϸиѹккϹϹ͵ͳ˰ƨƷ}`lKta@qa?tdC|oOfȮ̶̳ͷ϶ϷижжϸϸϸϸϸηͶͶ̵̵˴˴ηη«vxWrS|mN|kMpRy_~Ƭʹɯ̴϶ϵҹӼѻѻкѸѸѸҹʹзѸ϶Ȱȼy{\qOwgFrb@qbAwiFuT`øåȪʬͰϲ̯ͱϳдѴгͳͰ̯̯ͭή̯ǧž|vvǪϳ̵̷͸͸̵˴˴ʳʰʰɯȮȬ{^{TtOvScz÷ʾıʸȶȶ̸Ͻϻ̶ʲȰȮǭ¦øtbwWxnKsiFxnKwT`r¹£ȿk~\vUvS~[gyʿʾȼʾêưDZɲȱȳɴ̷ϼǷɸ˸˶ȲĪͿʻ|nb{[z[dmxqYnV>mQ;jP9fP8gVqa@m]qPtĺƫͲз̳ԺϵҺҺҺҺҺҺҺҺѹҺҺҺҺѹиϵҵϰȪǺ}}^}lN{jLwfHwgFvgHxkIwWhswǼ¥ɬҵӵΰΰӵѴʭʭ̰na~]|\zZgv¹qtWkZm^AteH|nQw\zǾǫȬȬ̯ϲϵ͵ζζϷϷζζ͵͵̴ʲȰǯǯȰȮ¹`ujJqbCqbCrcDqbC}nQs©ĮʴλкϺϸѹѹѻкϹθ͵ͳ˰ƨƷ}_~kJubAscBveGrUmüʲͷͷθϹииϸжϸϸϸϸηͶ̵̵ηηͶηѺйĭxzYuVpQnPrTz`~Ī˳Ȯ̴з϶ԻվӽѻкззѸҹε϶϶зѸʱ¶tf{YrOykHvhE|nKwTguȽƧɪʭ̯βϳгϲͰ˭Ͱήϯήɩľollvťɬ˴˶̷̷˴˴ʳɲɯɯɯȮǫžtzT{T~YauĸȼŲɶ˹ɷɷͻоϽ˸ȲĮŭȰȮéʿøyi{[{qNzpMuU}tUh|úżu_wUvU\lzõ¨ǭ¨ʾêĮůǰƯƱDz˶ͺǴʷ̹ͷȱƭƪǪͿwnjou½ukSiQ9kO9jP9gQ9jY?tiMuɲɲͶ̵ʳй̵˴˴˴̵ηйһѹиϷζͳͳ̱̱Ͳ̱Ȭźõ·ż£ʭϲггеѶ̱ʯǮŬĮůȲʴ̲ѷҸϵδжжͳȱɱŮªªƮʴ̶˷̸лҽϼ˸ʷ˸ɹʺȸĴƴɷǶ±̹˸˹͹̸˷ʷ̸DzůĮȲ̶̶̶˵̶̶ʴɳȲDZDZDZĮȲ̴˳ǭĩƬʰ˱ʹȲįðȶ˻ʹ̽ȹƵƵƵŴıƱƱƯȱʱ̳̳˲θθθθθθθθͷ̶˵ȲůíĮůɱȰǯɱ˳͵̶˵ɶʷ̺ͻͻ̺ʺɹʷɶȵɶ˸̹˸ɶʷʷʷ˸˸̹̹̹ʸ˹ͻμμͻ˹ʸʸʸ˹ͻͻͻͻͻ̺̺˹˹̺̺ͻμͻͻ̺̺̺˹˹˹μϽϽооϽϽμѿѿѿоϽμͻ̺˻˻˻˻˻˻˻˻˻˻˻˻˻˻˻˻ͻͻͻ̺˹ʸɷȶǵȶȶɷɷʸʸʸ̺ϺѼӾӾӾҽҽѼѼѼѺййййѺһӼԽӼйѺһѻϹ͹ιккθͷӻжèq~\rQ~nMnQw\iy±ǹĸ§ƪɯ̱ͱ̰̰ͭΩ̦Ġ|jc`enèϴϴ˱éȮȮĨçæ£eviIm`>l]k\=m^?}nQs©Įʴ̹ͷιϸиѹѻккϹϷжγɫǸ~anM{hHwgF{jLz]xʲ̶̶ͷθϹϷηηͶηηηͶ˴ɲȱȱɲʳʳ̵̵ì¸}fyYvVqRqRj´é˳ɯȰѸռҹջӺӺҹѻккϹվӼѺйηʳĮtcxVrP~qO~qO~^aiuǾäɩ˫ͰϲѳϱͰ˭ȫͭаͭug_esťɩȫȱɴ˶˶ʳ˴̵ηѷϵͳ˱çk{W[kǼʾ˿ï°²ŵʹͼνͻʷ̶̶̶˵ɱǭƪ¦Ǽuh}]wWdlt~ø{rc\_mɻƮƮêíŮǰȱǰȲɳ˵̹ϼλ̶̹ʳȯƬǫƻþ¿riQhP8jN8kQ:gQ9jY?zoSȱȱȱ˴ηηȱɲʳʳ˴̵ͶͶϷϷηηϵϵϵϵ̱˰ʯɮȭȫūǪǬʭ̱ϴееγͲɰȯǮƭůưȲɳʳ̴͵ζϷиии˵˵˵˵˵̶κκллλͺ̹̹̹̹ɹɹɹʺ˹̺ͻμʹ˸˸ʷʷɶȶɴʴʴʴʴʴʴʴʴɳȲȲDZDZDZȲȲɳɳʲ˳˱ʰɰȯȯDZDZʵ̹ͻ̼˺;˾ǺĵôIJIJűƱDzɱ˳̲ͳβʹɳʷ̹λϼλ̹˸ʷɶȵȵȵƳıíʴ˵˵̶̶˵˷ʶ͹͹λλͺͺ̹˸ʷʷʷʷʷʷʷʷɶʷ˸̹̹̹̹˸ʸ̺μμͻͻͻμμμμͻͻ̺̺̺ȶȶɷɷʸ˹̺̺˹˹˹̺̺̺ͻͻͻμμϽϽооѿѿѿѿоооϽϽͽͽͽͽͽͽͽͽͽͽͽͽͽͽͽͽμμμͻͻ̺̺̺ɷɷʸ˹˹̺̺̺μҽӾӾӾӾҽҽҽѼѼѺѺййѺһһӼԽֿֿӼѺѺӼѻкϻϻѼҼкθи˱ŻiwVpNoNvXh{˻ƪɬ̱ββͱͰͮЭͩȤ¼wmgcgrȫͰ˰ǫʰɭĨ¦¥ƽ~`sfDk^paDseJzlQi¦̰αȮǭγպϷϷϷϷϷϷϷϷζ̴ɱƮĬĬĬūĻg~sSufGn_@n_@qbCtWyƸƭư˵ͺθιϸиѹѻккϹζδ˰ŧĵ|apOnNoNsUfì̴ͷͷθϹϹϹͶͶ̵ͶηηͶ˴ɲȱʳ̵˴ʳ˴ʳ|g}]{[wXwXoĶĪ˳ȯȱиԻзԺҹҹҹѻѻккվӼйηͶ˴DzūźumigddelwžæǩȪȫǪǪʪʭǧüvtxĽǧʭ˯ʳ˶͸̷˴˴̵ηδδ˱é{pgn~ƻ¦Ĭůï°²ŵʹͼνλ̸˸ͷ̶˵ɳǯūūçǼ{vy~¹zsotŭɱȰīíŮǰȱȱȲɳ˵ͷɵɵɳɳȱȱȯȭΦȽ·ĽĿĿnkSjR:lP:kQ:gQ9jY?ynRǰǰǰ˴ͶͶɲɲʳ˴̵̵ͶͶϷϷηηϵϵϵϵͲͲ̱˰ʯɮɮȭʯ˰ͲϴееγͲʱɰǮǮưDZȲʳʳ̴͵ζϷϷии̶˵˵˵˵̶͹κлϺλ̹˸ʷʷʷʸ˸ʺ˸˹̺ͻμʹʹ˸ʷʷɶɶɶʴʴʴʴʴʴʴʴʴɳɳȲȲɳɳɳʴʴ˳˳˱ʰɯȱȲȳȳʷ̺ͻ˻ɹ̽ʽǺŶôIJųƲƱDzʲ˳ͳͳβʹʶʷ̹λλλ̹˸ɶɶȵȵȵǴƳı˷˵˷˵˷˷˷˷κκλϼλͺ̹̹˸˸˸˸˸˸˸˸ɶʷ˸̹̹̹̹˸ʸ̺μμͻ̺ͻμμμͻͻͻ̺̺̺ʸʸʸ˹̺ͻͻμͻͻͻͻμμϽϽμμϽϽϽϽϽоооооϽϽμμͽͽͽͽͽ̼̼̼ͽͽͽͽͽͽͽͽμμμͻͻ̺̺̺˹˹˹˹̺̺̺̺ͻԿӾӾӾӾҽҽҽѼһѺѺѺѺһӼԽԽֿֿվӼһһӼҾнмѽҼкʳ¨y`sRrPwUdzɹ©Ȯʲʱʰ˱ε϶϶ϵδͱҳѮΫȥý|rd`brźƨ̭ʮ˯ȭĩææȽ·y}]reCl_xlF{WǬʯʹ˲ԺѷѹҺӻӻӻҺѹѹӻиζζиҺиαǡöubxUsT|_k}ɾĨçǫϲԷҺԻԻջӹжγͲѶеͳ̴̲̲̳ʹδʮ§Ļphcq·s{`}mSwgMsXfsƿũͳϵγ̱ʹзииϷϷϷϷζζ̴˳ɱǯŭĬĬĪƽn|\pQ{lMpQvWoǭεȲʴͺϹιϸиѹѻккϹζͳǬʿ~ncjoxʳϹммммϹθ̷˴ʳ˴̵Ͷ̵˴ɲȱ̵η̵ɲǰƯzx{|{óʰηε̶йѹζзззѸѻѻӼӼӾѻθ̶˸͹̸˵ɯʭɭǫçĽþħħƪũçƿĽ»»ĽũǫǫȬͱѷϸлѼϺͶ˴̵Ͷʰ˱ūŸĤʬǭŭůƲñ°òǴ˸ιϼϹ̶ͷͷͷ̶ʴȯǮêŬȯ˲˲ɰƭī̿ʾøƻʿ¨¨ůDZɳȲůíĭĭƯǰɱɱȰȰ˲ʹĮůưDZȰɱʰʰ̲̲˲ʯɮȭǬƫ¥æ¤¿½hkSmU=pT>lR;iS;o^DtXü«ǰƯŮȱ˴˴ʳ˴˴̵̵ͶͶͶδδδδδδδδϵϵϵϵϵδδδзѸѸѸѸзεʹ̳˲ɰȯȯȯʱ˱͵͵͵͵ζϷϷиλ͸̷̷̹͸ιιͺ̹˸ɶȵǴǴǴ˸˸̹ͺͺλλͺ˸˸˸ʷʷɶɶɶ˵˵ʴʴʴʴɳɳ˵̶̶ͷͷͷͷͷεε͵͵̴˳ʴɳ˸ʷɶʷ̺˹˸ɶȸȷǶƵŴƳǴȳȳɴ˴̵ʹʹʹ̶˸˹̺ͻͻͻ̺˹ǵȶɷȶǵȶɷʸͺ˸ɶȵȵɶ˸ͺϼннннϼλͺ̹̹̹̹̹̹̹̹ɶʷ˸̹̹̹̹˸ɷ˹ͻμ̺̺̺μͻͻͻ̺̺̺˹˹˹˹̺ͻͻμϽϽμμμϽϽоооооϽϽμͻͻͻμμμͻͻ̺̺̺˻˻˻̼̼̼̼̼Ͽξξξξͽͽͽͻ̺̺̺̺̺̺̺ϽϽμͻͻ̺̺̺ʸӾӾԿԿԿӾӾӾҽҽӾӾҽҽӾԿԿӾҽӾԿѼηì~cpOykHuQcyŭѸѸзϸηҺиϹкѻӼѺεгбϱҴҴʪ{j^^hv¸ħƫɬǪƩƩʿĹz_wmIqgCqe?pd>{oI\ĩɮ˰ε̳ջжѹѹҺӻӻӻҺѹӻиζϷӻԼѹͰͿh~WvS{\pǻūɯ˰ϲѴгѸккѺҺи϶εε̳ʲȰǰȱɳ˲̲ͰʮŨǼup{·yi{a|aoǽūʰͳεʹʹεиииϷϷζζζ̴˳ɱƮëſżvh~_|]biõ˱εDZʴ̹ͷιϸиѹѻккϹиδʯ¤ĵ}v}ʼƭ̵кммммϼϹ͸̷ʳ˴̵ͶͶ˴ʳɲʳ̵˴ȱǰȱĭƿƶϿȯͳйѸкһҺи϶ζззкѻһһӽк̸̹ͺλͺιɰʯ˱˱˱ʰȮǭŧǼĹø·»ž¨ééɯȮǭǭƬūĪĪĪǭɯȮƬǭ̲жйѼѼлͶ˴̵Ͷ̲ʰūĽǼĦ̮βɮıðıǴ̶ϷкϷͷε϶εʹ˲ȲưíưɳʴȲưűǶŵƵǴǯéȽźɾŨʭ˰ʱǮĮïűƲƲƱŰŰƱȲɳʲɱɰɰ˲ʹɰɰɰɰʱʱɱɱȰɯʰ˱ʱɰǭǭææ¤h~hPmU=qU?mSveK}ažĭȱƯĭȱ˴˴˴̵̵̵̵ͶͶͶδδδδδδͶͶ͵͵͵ζζζζͶѻѻѻкϹθθͷ̶˵ʱɰɰɰʱ̲ζθͷͷͷθϹкμλͻ̹̹̹ͺλ˸˸ɶȵDzDzDzDzͷͷθϹкϹϹθ̶̶̸˷˷ʶʶʶ˵˵˵ʴʴɳɳɳ˵̶ͷϹϹϹθθ͵͵͵͵ͷ̶˷ʶͺ̹˸˸˹˹ɷǴǴǴǴǴǴȴȴɳɳʴ˵̶εεε̶̹̺̺ͻ̺̺̺˹ȶɷʸȶƴųȶʸ˻˺ɸǶǴɶ˸̹ϼнннѼлϺι̹̹̹̹̹̹̹̹ɶʷ˸̹̹̹̹˸ɷ˹ͻͻ̺˹̺ͻͻͻ̺̺̺˹˹˹ɷɷʸ˹̺̺ͻͻ̺̺ͻͻͻμμμϽϽϽμͻͻ̺̺μμͻͻ̺̺̺̺˻˻˻̼̼ͽͽͽϿϿξξͽͽͽ˹̺̺̺̺̺̺̺ϽϽμͻͻ̺̺̺ɷҽҽԿԿԿӾӾӾӾԿԿӾӾԿԿӾѼѼӾҿιʳǿw~^{lKvhCuQfʱѷϵδηͶӻиθϹҼԾҼϹγͰͰгҵͯãxlffgtȽĩʭɬƩŦǼv^wmIqgCrf@rf@tNbɮ̱ͲзʹջϵиѹҺӻӻӻҺҺӻиϷѹԼҺ˳ħo[yRyVf~éǭʲ˳ϴӸӶͲ϶ͷͶηѹҹзε̳˲ʱǯƯǰDZɲͳϵд̯ǩĵôǺƻyv{ĸɿĩĪūȮʰʹεεεѹѹиϷϷζ͵͵˳ʲȰƮëľýƽ|vuzδʹDZ̶ͺ̶ιϸиѹѻккϹижͲǩ̽ĵ±Ƕ˽éɰ̷Ϲϻммѽϼϼι͸˴̵ͶηηͶ˴ʳɲ̵̵ȱǰɲȱũäʿãťĤĪɯ˲δйҺҼӼҺϸзϷззккѺѺҼк͹͹λλͺ̹ȱɲʳʴ̶θϹкϱ̰ͯ˯˯ɭŪ§ççĪūūūĪĪ˱˱˱˱˱ɯȮȮƬƬǭǭǭɯ̲ͶϺлѼϺͶ˴̵Ͷͳ˱ǭūũĨç¦Ȭ̰дδʱĮ®ıðıDz̴Ϸиϵϵ϶϶϶ε̳ȲDZíůdzȴȴdzųIJȸŶȸ˸̶ȰūǫǫʮϲеѸθ˵ɵIJIJİűƱDzDzDzȲɳʲɱȯȯʱ̳̳̳̳˲ʱʱȰȰƮǯǰȮǮǮƬū¥æĦä¤f|fNmU=rV@oU>oYA}lRiƯɲƯŮȱ˴̵̵̵̵ͶͶͶͶͶͳͳͳͳͳͳ̵̵˳˳̴͵͵ζζζһккϹϹθθθθ̶̳ʱɰɰʱ̲ϹϹͷ̶̶ͷϹкϽμͻ̺̹̹ͺͺɶɶȵȵȳȳɴɴ͵ζиѻѻкϹθ̶̶̸˷˷ʶʶʶ̶̶˵ʴʴɳȲȲ˵̶θкккϹθ̴͵͵ζθθ͹͹λͺ˸̹˹ʸȶųűůƲDZȴɳʴʴʴ˵̶ͷεεεͷͺͻ̺̺̺˹˹˹ʸ˹˹ǵñIJȵʺʹɸȷȵɶʷ˸ϼϼϼϼлϺιι˸˸˸˸˸˸˸˸ɶʷ˸̹̹̹̹˸ȶʸͻͻ̺˹̺ͻ̺̺̺̺˹˹˹ʸȶȶȶɷʸ˹˹̺˹˹˹˹̺̺ͻͻμμμμͻͻͻͻμμμͻͻ̺̺̺˻̼̼ͽͽξϿϿϿξξͽͽ˹˹˹˹̺̺̺̺ͻͻͻͻ̺̺̺̺ɷҽҽԿԿԿӾӾӾԿԿԿԿҽллӾμʵƯr~^qPqL]uéδδ̲ͳиѹӽϹ̶θҿҿкε̱ʰ̯αͯɩƤļxoemźŪαˮŧɾu^wmIsiEsjCvmF}TmγϴϴѸεջδиѹҺӻӻӻӻҺԼѹиҺӻ͵scXWbsƸʰε϶εѶպոе϶̶̵ͶѹӺѸ϶εʹ̳ʲɲȱȲɲϷҸҶα̮ͭƥξ̼̼ο¢úĶʾŬȯǭǭǯɱεѸҹҹѹѹиϷϷζ͵͵ʲɱȰǯĬªſĻȹοʭж̳ɳϹϼ̶ιϸиѹѻккϹϷѷе̮ŨϼϼϼϾŪǭɰ˶ͷκϼнѾннϺϺ̵ͶηϸϸηͶ̵ʳͶͶɲȱʳʳɭή̨˪ήϯή˱ͳ̳ͳηҺӽһѹϸѸжззϹϹлϺнλλλϼλ̻ʷȴɴʵ˶͸Ϻϻϻдϳϳдϵδ̱ʯ˱˱ʳɲʲɱɱȰ̵̵˴˴ʳʳʳɲǰƯŮƯɲ̵ͶͶιϺлϺ̵˴̵Ͷ̲̲˱˱˯̰̰̰δδδ̴ɳű®ıðůȲ̴ϵжгϳдддϵͳʳȱƱDzȵɶɶȵǶŵȻƹȹͽͺɱȮ̲ͳδϴзϹϻκμȶǵƴƴǴȵȲȲɳʴ˲ʱȯȯʱ̳˱˱˲˲ʴɳɳɳȲȰǰƯƭƭūūƿæŧŦ¤}b}gOoW?uYCqW@r\DpVnéȱ˴ǰŮȱ̵ͶͶͶͶͶͶͶͶͶͳͳͳͳͳͳ͵͵˳˳̶ͷθϹϹкѼϻϻκκκθθϹͷ̳˲ʱʱ˲̲кϹͷ̶̶ͷϹкнϼλͺ̺̺̹ͺǴǴǴǴȳʵ˶̷ζϵѷҺҺѹзεʹʹͶ̵̵˴˴ʴ̶̶˵ʴʴɳȲȲʴ˵θкѻкϹθ˳̴ε϶Ϲϻϼϼϼͺ̹̹̹˸ǵŲĮŭưȰɳʲ˳˳˳̴͵͵ζζͷͷλͻ̺̺˹˹˹˹̺ͻ̺ǵĽŲȸɸɸɸɸɸɸɸλλλϼϺι͸͸˸˸˸˸˸˸˸˸ɶʷ˸̹̹̹̹˸ȶʸ̺ͻ˹˹˹ͻ̺̺̺˹˹˹ʸʸǵȶȶɷʸ˹˹˹ʸ˹˹˹̺̺̺̺̺ͻͻͻͻͻμμϽμμμͻͻͻͻ̼̼ͽξϿϿξξͽͽʸʸʸ˹˹̺̺̺˹˹˹˹̺̺̺̺ʸӾӾԿԿӾӾӾԿԿԿҽϺлԿѾм̶ǰĪoc{[|]p¦ʲи͵˳ζҺԾҼθ˵θҿѻ϶ʹɯɯˮ̯̯ͭɧº}mq¹ƫгͰĦʿɾɾxayoLvlIwmI|rNaxҶѸѷҹϵԺδииҺӻӻӻӻӻսҸѷӹҸȮoa]dr´¦ʯ϶ѸзѶԹֹҷѸηͶϷѹӺѸ϶зз϶ζ͵˴ʳʳиӹӶͱαѳаɧȧǥƦƦåǾ¶ƺĩŬƭʰʰȮǯɱ϶ӺռԻѹѹиϷϷζ͵͵ɱɱȰǯƮĬë¨ũ¥ͿæʭʯѶѷ̳˴ѼҼͷηϸиѹѻккϹζжѶϳɮƩŦƧçūȭɯȰȲʵ̶ͺκμнннлл̵ͶϸййϸηͶ̵ϸϸʳȱʳ˴ʰҵѱбӴӵѵγδ̲ʳ̴ѸҼѺϸηҹѸѸзϹθϺιкλλϻмκ˸ȵͺϺϼѼѾѽѾмеγγγззεδϵηͶͶζζζζͶ˴ʳɲȱɲ˴̵ʳǰƯȱ̵ϸϸͶ͸ιϺι̵˴̵Ͷʳ̵̲ͳ̲̲ϵѷѸ϶˳Ȳdzű°ʿıðůȲ̴ϵжϵдджжϵ̵ʳȱDzDzǴȵɶɶɸȸʽǺ˻пλʴɱεζεʹ˵˷͹Ͻоͻ̷ȶȳȵȳȲDZɳʴ˲ʱȯȯʱ̳ɱɱɱʱɳɳɳɳ˵ʴȱǰůƭǮǮéƩŧz]}iNrZ@v\CrZ@s^CrXpūʳ̵ȱƯɲ̵ͶͶͶͶͶͶͶͶͶͳͳͳͳͳͳ͵͵˳˳̶ͷϹккѻлκκ͹͹κθϹϹθ̶˲ɳʱʴ˴ѹзεʹ̶εϹѸннλͺ̺̹̹̹ƳDZǴȳɴ˶̷ͶζϷѹӻҺѹϹε̶̶̶̵˵˴ʴʴ̶̶˵ʴʴɳȲȲɳ˵θкѻкϹθʲ˱ͳ϶кмѼнϼλ̹̹̹ʷǵıíĬưȰʲ˳˳ʹ̴ʹε϶϶϶εͷϼϼ̺ͺ˹˹˹˹ͻμͻƴž»ĽǷɸȸʹɹɸȸȷͺͺλλ͹ι̸̷ʷʷʷʷʷʷʷʷɶʷ˸̹̹̹̹˸ȶʸ̺̺˹˹˹̺̺̺̺˹˹ʸʸʸȶȶɷɷʸ˹̺̺˹˹˹̺̺̺ͻͻ˹̺̺ͻͻμμϽϽϽϽμμμͻͻͽͽξϿϿξͽͽʸʸʸ˹˹̺̺̺ɷɷʸ˹˹̺̺̺ʸѾҿտ׾ҷĥyprv˶ͷϹкѻкϹͺͺλϼнѾҿҿԾѹϵʰǭƬɬˮͯ˭ϯϯţæƩˮͰ̯Ʃżnbadj{ǭͳһӻη͵ϷѹйѺѺѺҸѷжϵϵϳѵѵͱž~hfjxĹŨϲϴѵҶҶҶҶѵжϷϷζζϷϷѸѹѹѹиϷζ͵̴˱ϲҵҵϳͰ̯̯ˮʭȫƩħ¦¥éūƬĭĭĬǯȰɱ˳͵ϷѹҺѻѻкϹϹθͷͷͷͷ˵ʴȲưůĬūƬǭɰ˲ʹ϶зѸҹѸη̵̵ηйϸϸϸϸϸиϸϸѺϸδʹʹʹ˰ɰǯDZȲ˵θι̷ɴ͸θϼлллллϸйѺѺйϸͶ˴ηͶͶ˴ʳɲȱȱδϵϵж϶ззѸʹϹҼԽһϸͶͶӼӼһһйηͶ˴ϸϺϺкϹθͷ̶ͷθмҼӿӽѾҼηηϸϸηͶ̵͵ϷϹϹкѻкθ̶ζ͵˳ʲʲ˳̴͵͵̴̴̴͵ζϷϷιлѼϺ̷ʵɴʵ̷˴ʵɲʳ˴ͶηӼѺ̶ʵDzŰŰį­įȳ͸ϸηͶͶͶͶͶ̵ɴDzƱɴɴɴʵ̷ιнѾϽμͻ̸˷͸ͷϸϹϸͶ̷˶̷͸ιȳʲʵ̴˵ʲȰǯ˳˳ʲʲɱȰǯǯȱɴʵ̶˶̶˶ʵɴȳDzƱƱƯǰȲŲɶ˷˳ͱĥ`}jIua@p\;s^?wdFsWyƭɯ˱ͳδͳ˱ʰȮ˱δϵδδδϵͶͶͶͶͶͶͶͶϸййѺѺѹйϸѽҾӿӿҾѽϻκϻκ͹̶̸̶̸ηжжδͳ͵δижϼ̹ʷʷ˸θ͸̷ȳȱdzDZɳ˵θйҼҼҼѻкϹͺͷ̹˸˸˷ʷ̸˸̹˵˵̶̶̶˵ʴɳɳ˵ͷϹкϹͷ̴δϲѴѷҹѻһҽϺι˸ȵƲİïïƲDZȴɳʴʴʴ˴ʹʹͱͱββϳ϶λν̼̻̹̹ͺͺ˷ϻϼǴ»ž°Ƴȶ̹̺̹ɷȵ˹ͺ̺ͺ̺̹ʸʷƴǴǵȵȶʷʸ˸ƴȵɷ˸˹˸ʸɷ˹˹˹˹˹˹˹˹ʸ˹˹˹˹ʸɷȶʸʸ˹̺μϽооϽμμͻͻ̺̺˹˹˹˹̺̺̺ͻͻϽϽϽμμμͻͻͻͻμϽоѿѿѿѿϽμͻ̺̺ͻʸʸʸʸʸʸʸʸɷɷɷʸʸ˹˹˹˹нѾҿҼҷίǾ{vu}¶ij˺ͺλϼϼϼϼλɶʷ˸̹ͺϼнѻѹϵ̲ɯȮɯ̯ͰгӶҴ̮å½þ¢Ǫɬ̯ͰʭŨȿżvy|ü˱иԼԼи͵ζийѺѺѺҸѷжϵϳβͰˮŨxnpzøåˮͰαϲϳдѵѵджϷϷζζζζϷϷϷииϷζ͵̴˱ʰαѴҵѴϲαϲѴгϲͰ̯ʭɬȫĬŭƮȰȰǯƮƮȰȰȰɱ˳͵ϷиккϹϹϹϹθθϹͷ˵ɳȲưưưŮƯǰɲ˴ͶϸйͶηϸη̵̵ηйϸϸϸϸϸϸϸϸѺϸͶͶηͶ̵ʳDzȳɴ̷ϺϺ͸ʵ͸ιϺллллϺηϸйѺйϸηͶͶͶ̵̵˴ʳʳɲ˴̵ͶηйѺһӼηѺԽվӼѺϸηһһһѺйϸͶͶͶηηϸϸϸηηϹкѻҼҼҼѻкԾӽӽӽӽӽӽӽӽѻϹкѻк̶Ȳ͵̴̴̴̴͵͵ζ̴̴˳˳̴͵ζζιлѼϺ͸ʵʵʵʵʵɴȳȳʵ˶͸лι˶ȳŰįîîîîŰɴ͸ϺϺιϺллϺι̷ʵɴʵʵ˶˶͸ιлѼѼлι͸̷͸͸ι͸̷˶˶ʵʵ˶˶ʲ˳̴̴̴˳ɱȰ̴˳ʲɱȰȰȰȰ̷̷͸ιιι͸͸DzDzƱƱƱDzDzȵʻολͷͱ¤^pLxf@sa=ueC}lN|_Ƹȯʰ˱̲ͳ̲˱˱ȮʰͳδδͳδϵͶͶͶͶͶͶͶͶϸййййййϸмѽҾҾҾмϻκκκ͹̸̸͹͹ηѷжϵδδϵжѷϼ̹ʷɶ̶θ͸̷ɲɲȲȲʴ̶θлннннϼλͺ̹̹˸˸ʷʷ˸˸̹˵̶̶̶̶˵ʴɳɳɳ˵̶̶̶̶˳βϳѴжҹѸѺйϺι˸ɶdzƲƲűdzȴɵʶ˵˵˴˴̳̳̲̰ͳβϳϵͺͼ̻̻̹ͺλλ͹ѽнǴüIJųȶʸ˹ʸɷȶʸʸʸʸʸɷȶǵƴǵǵȶɷʸʸ˹ǵȶɷʸ˹ʸʸɷ˹˹˹˹˹˹˹˹ʸ˹˹˹˹ʸɷȶʸʸ˹̺ͻϽϽоμͻͻͻͻͻ̺̺˹̺̺̺ͻͻͻμϽϽμμμͻͻͻ̺̺ͻμϽооѿооμͻ̺̺̺̺ʸʸʸʸʸʸʸʸɷɷɷʸʸ˹˹˹˹ѾнннѾҿҿҿҿϹγɪĻzx|·óνͼλλλλϼннɶɶʷ˸ͺλϼкиϷζ͵̲̲ͰͰҵӶӵв̮ͯͯϱ̯ͰͰ̯ɬŨǾ˯ϵӻ־սѹζζϷйѺѺѺҸѷжϵѵϳˮǪzu~·ƨʭ̯ͰαϳϳддϵϵϷζζ͵͵ζζϷϷϷϷϷζ͵̴˱ʰαѴҵѴггѴӶӶӶҵѴггϲʲʲ˳˳ʲʲɱȰʲɱǯǯȰʲ̴ζθθϹϹϹϹкккϹ˵ɳDZDZȲɳƯǰȱʳ̵ηϸйɲ˴ηηηͶηйηηηηηηηηѺϸͶͶηηͶ˴DzDzɴ̷Ϻлι̷͸ιϺлллϺϺͶηϸйййϸϸ̵̵̵̵̵̵̵̵ʳ˴ͶηйһԽԽйһվֿԽѺййѺѺһһѺйϸη̵ͶηϸййййѻѻҼҼҼѻкϹտԾӽӽԾԾкͷϹҼҼθɳ̴̴͵ζϷϷϷϷ̴˳˳˳˳̴͵͵ιлѼл͸˶ʵ˶ʵɴȳDzȳɴ˶̷ι̷ʵDzŰî­­ŰŰDzʵιллϺлллϺι͸˶ʵ˶˶˶̷͸ιлѼҽѼϺι͸̷̷̷̷̷̷̷̷˶ʵʵ˳̴͵ζ͵̴ʲɱ͵̴ʲȰǯȰȰɱ͸ιιιιι͸̷ƱŰŰƱƱDzɴɶ̻онθ˯ɾ~`tP{kGvfD|lJxZnʾʱʳʳʳʳ˴̵̵ǰɲ̵̵˴˴̵ͶͶͶͶͶͶͶͶͶϸϸϸϸϸϸϸϸϻммѽмϻκκκ͹͹͹͹͹κлйѹииииѹѹϻ͹ʷɶ˸̹θͷ̶̶˶˶̷ιϺлннннϼλͺ̹̹˸˸ʷʷ˸˸̶̶̹ͷͷ̶̶˵ʴȲȲȲȲȲɳɳʲϵϵϵжззθͷ͹̸˸ʷɶɶɶɶɵʶ˷̸̶̶̵̵˴˴ʳʱ˴˲ͳζ̹˸˸˸̹ͺκϻѽӿнȵĽý¯ƴǵǵȶɷʸʸʸʸʸ˹˹˹ʸɷȶǵȶȶɷʸ˹˹̺ȶȶɷʸʸʸʸɷ˹˹˹˹˹˹˹˹ʸ˹˹˹˹ʸɷȶɷʸʸ̺ͻμϽϽ̺̺̺̺ͻͻͻͻ̺ͻͻͻμμμϽμμμͻͻͻ̺̺˹˹̺̺ͻμμϽϽμͻ̺˹˹˹˹˹˹˹ʸʸʸʸʸɷɷɷʸʸ˹˹˹˹ҿҿϼͺннѾҿҿѾѾнθͲʭȿƻñɹпϼϼͺͺͺϼнҿϼϼϼϼϼϼϼкϹиииϵδͳαгϲΰϱвѳҴҴααͰˮɬƩħæƽǾħǪǪʭджӻսԼҺииѹйѺѺѺҸѷжϵҶд̯ȫ¥ȽŧŧŧƩȫͰαϳддджжζζ͵͵͵͵ζζϷиииϷζ͵͵ʰͰгѴѴгѴҵҵҵҵҵҵҵҵѷζζ͵͵̴˳ʲɱ̴ʲǯŭŭǯʲ̴ͷͷθϹϹкѻѻҼк̶ɳDZȲʴ˵ȱȱʳ˴̵ηϸϸʳͶййϸηηϸηηηηηηηηѺϸηηϸϸη̵ȳȳɴ˶ιлϺ͸ιιϺлллϺι̵ͶηϸййѺѺ̵̵ͶͶηηηϸ̵ͶηϸйһӼӼйһԽվӼѺйййѺѺһһѺйϸηηϸйѺѺййккѻѻѻѻѻѻԾӽѻккҼԾѻͷʴͷӽӽθ˳͵ϷиѹѹиϷ˳˳ʲʲʲ˳˳̴ιлѼл͸˶˶̷˶ʵɴȳɴʵ̷͸ι͸̷ʵȳƱŰŰȳȳȳ˶ιллϺϺιι͸̷˶ʵʵ̷˶˶˶̷͸ιϺҽѼлι͸̷˶˶͸ιϺлϺι̷˶̴͵ζϷζ͵˳ʲζ̴ʲȰǯȰɱʲʵʵ˶˶˶ʵɴȳŰŰŰƱDzɴʵ˸̻νͺʴũ·z`uS~nJ{kIuTg~ŭ˵˴ɲȱɲʳͶηȱʳ˴̵ʳʳ˴Ͷ̵̵ͶͶͶͶηηϸϸηηηηϸϸϻϻϻϻϻϻκκκ͹͹͹͹κϻϻйѹѹѹѹѹѹѹϻ͹ɶɶʷ̹ͷͷͷͷͷθ͸ϹϺкϼϼϼϼϼλͺ̹̹˸˸ʷʷ˸˸̹ͷͷθθͷ̶˵˵ɳȲDZůůDZɳʴζϵϷϷ϶ε̶˵͹̸˸˸˸˸̸̸̹̹͹͹ͷ̶̵̵ʳʳɴɲɴʳ˶̷ʶʶʷ˷̹͹ϻмҾӿнȵƿüưɷȶǵǵȶɷʸ̺ͻͻͻͻͻ̺˹˹ȶȶɷʸ˹˹̺̺ɷɷʸʸʸʸʸʸʸʸʸʸʸʸʸʸʸ˹˹˹˹ʸɷȶȶɷʸ˹̺ͻμϽ˹˹˹̺ͻͻμμͻͻμμμϽϽϽͻͻͻͻ̺̺̺˹ʸʸ˹˹̺̺̺ͻͻͻ˹˹ʸʸʸ˹̺˹˹˹˹ʸʸʸɷɷɷʸʸ˹˹˹˹ҿѾλ̹нѾѾҿҿѾннкзϲȩȿżȿæƭʴϽнϼλͺλϼѾҿҿнϼλϹϹииѹѹжδδϲͰˮ̯ϱѳѳвͲ̱˰ʯɮȭȭȭǬǬɮͲγ̱̱βжѹӻӻӻҺӻԼйѺѺѺҸѷжϵдβ̯ʭǪæžǩΰϱˮȫʭͰϳϳдѵҸҸѷѷϷζζ͵͵ζζϷѹѹҺҺҺѹии˱ͳϵжϵжѷӹжжѷѷҸҸҸӹиϷζ͵̴˳ʲʲ͵˳ȰƮŭǯɱ˳̶ͷͷθкѻѻҼӽкͷʴɳʴ̶ͷʳʳ˴̵ͶηϸϸηѺӼӼѺϸηηηηηηηηηηѺйηϸййϸͶʵɴɴ˶ιлϺιιϺлллϺιι̵ͶͶηϸѺһһͶͶηηϸййййййййѺѺѺϸѺӼӼѺϸϸϸѺѺһһһѺййѺѺѺѺѺйϸηθθϹкѻӽԾտԾҼкϹϹкӽԾѻθ˵ͷҼտӽк̴ζϷѹѹѹиϷ̴˳˳ʲʲʲ˳˳ιлѼлι̷̷͸̷̷˶ʵʵ̷͸ϺййϸηͶ˴ʳɲʳɲɲ˴ηййϸϺϺι͸͸̷̷̷̷˶˶ʵ˶˶̷͸ѼѼлϺι̷˶ʵ̷͸ιϺι͸˶ʵ͵ζϷϷϷ͵̴˳ζ͵˳ɱȰȰɱʲǰȱȱɲɲȱǰǰŮŮƯǰȱʳ̵θϼϾ̹ưžv`{XwTxWcx̾˳̶̷ɴȳɴ˶͸Ϻɴ˶̷˶ʵʵ̷͸̵̵̵ͶͶηηηϸηͶ̵̵ͶηϸϹϹϹθϹϹϹϹϹθθͷͷθϹϹллллллллϻ̸ʶɵ˷̸͹̸͹κκκλϻͺͺλλϼϼλλͺ̹̹˸˸ʷʷ˸˸̹θθθθθͷ̶̶˵ʴDZůůDZɳ˵ͷ͵θθͷ̶ʷɶ˸˸˹˹˹̺ͽμ͹ͷθͷ͹̸˷ʶȴȴȵȴȵɵʷ˷ʶʴɶʴʷͷθϹѻҼѻʴíĮȲ˹ɷȶǵǵɷ˹̺̺ͻͻͻͻ̺˹ʸȶȶɷʸ˹˹̺̺˹˹ʸʸʸʸʸʸʸʸʸʸʸʸʸʸʸ˹˹˹˹ʸɷȶǵȶɷʸ˹̺ͻμʸʸ˹˹̺ͻͻͻͻͻμμμϽϽϽͻ̺̺̺˹˹˹˹ʸʸʸʸ˹˹˹˹̺̺˹ʸʸʸʸ˹ͻ̺̺˹˹ʸʸʸɷɷɷʸʸ˹˹˹˹ҿҿнϼҿҿҿҿѾннξкѸӸгʫŨɬ˰̳θӿϼϼϼϼϼнѾѾҿѾϼͺ̹˸ϹиииииϵϵгϲααϱѳѳвβͲ̱˰ʯʯ˰˰ͲͲϴеѶϴγϳииѹҺҺӻԼԼйѺѺѺҸѷжϵ˯˯ˮˮʭɬȫȫʫͯϱͯʭȫˮϲϳдѵҶҸҸҸѷиϷϷζζϷϷиҺҺӻӻӻӻҺѹͳδжжϵϵѷӹѷҸҸҸӹӹԺԺѹиζ͵̴˳˳̴ζ͵ʲɱȰɱ˳̴̶ͷθθккѻҼҼѻθ̶˵̶ͷθ̵̵̵ͶͶηηϸѺӼԽӼйηηϸϸϸϸϸϸϸϸϸһйϸϸѺѺйϸ̷˶ʵ˶ιϺϺ͸ϺϺлллϺι͸ͶͶͶηϸйһһηϸϸϸйѺѺѺһѺѺѺййййϸѺһһйηηϸһһӼӼһѺйϸӼӼӼһһйϸηͷͷθϹѻԾտԾҼѻѻѻҼӽտӽкϹккϹθϷϷϷииϷϷζ͵͵̴˳˳˳˳̴ιлѼлι͸͸ι͸͸̷˶˶͸ιϺѺѺѺѺйηͶ̵̵ʳɲʳ̵ϸϸηҽҽѼлϺϺлл͸͸̷˶˶˶̷̷лллϺι͸˶ʵʵʵʵʵʵɴȳDz̴͵ζϷζ͵̴ʲ̴̴̴˳ʲɱɱȰǰǰȱɲʳʳɲɲȱȱȱɲʳ̵ηкҿ̸ưüzkjkp|ɹƬζ̸̷ʵɴɴ˶͸Ϻ̷͸͸̷ʵʵ̷ι˴˴̵ͶͶηϸϸϸη̵˴˴̵ηϸкϹϹθϹкѻѻѻкϹθθθθθϺϺллллϺϺκ̸ɵɵ˷̸̸̸͹κκκκι͹̸ͺͺλλλλͺ̹̹˸˸ʷʷ˸˸̹θϹϹϹϹθͷ̶̶ʴȲưưDZɳʴ˵˵̶̶̶˵ɶɶ˸˸˹˹˹̺ͽμκθθͷ̸˷ɵɵɶȵȵȵȵɶɹ˸˵ʲʴʴ˵̶ͷθθϹϹ˵ưĮưɳ˹ʸɷȶȶɷʸ˹ɷɷʸʸɷȶȶǵǵȶȶɷʸ˹˹̺̺̺˹ʸʸʸʸ˹ɷɷɷɷɷɷɷɷʸ˹˹˹˹ʸɷȶǵǵȶɷʸ̺̺ͻ˹˹˹˹̺̺̺̺̺ͻͻͻμμμϽ̺̺˹˹˹ʸʸʸʸʸʸʸʸʸ˹˹̺˹˹ʸʸʸ˹̺ͻͻͻ̺˹˹ʸʸɷɷɷʸʸ˹˹˹˹ҿҿҿҿѾн̻ннϸѷԸӷϲɭͱβ˱̵ѼνλϼннѾѾннѾѾнϼϼλͺͺϹϹϹϹϷϷϵжжжѴѴгѴҵӶддϳβͱ̰̰˯ϳддϳдѵѵѵҺѹииѹҺҺҺйѺѺѺҸѷжϵ˯̰ͰͰ̯̯̯Ͱ̯ͰͰʭǪƩɭ̰βϳжѷѷѷҹѸѹѹииииѹѹѹҺҺӻӻҺҺѹϵжѷжϵδжҸѷѷҸҸҸҸӹӹҺѹϷζ͵͵ζζϷζ͵̴̴̴͵͵ͷθθϹϹккѻҼѻϹθͷͷͷθͶͶͶηηηηηйѺѺйϸηйһййййййййһйϸйһһѺйι͸˶̷͸ιι̷ϺллллϺι͸ϸηηͶηϸѺһйййѺѺѺѺѺйѺѺѺѺѺѺѺйһӼһйϸϸѺԽԽԽӼһѺϸηѺһһӼһһѺйϹθθϹкӽտտտԾӽҼѻккԾտԾѻθͷθкѹиϷζ͵͵͵͵ζζ͵̴˳˳̴̴ιлѼѼϺ͸ιι͸̷˶ʵ˶̷ιϺϸϸййϸη̵˴̵ʳȱɲ˴ͶηͶӾҽѼлϺлѼѼллϺι͸͸͸ιιϺϺлϺι̷˶ɴɴȳDzDzDzDzDz̴͵ζζζ̴˳ʲ˳̴͵͵͵˳ȰǯŮƯȱɲ˴˴˴˴ʳʳʳ˴̵ͶϸѻѾ̹Ȱŷ̾ƫ˱ζ˷̹˸ʷ˸̹ͺλλλλͺ˸˸ͺл˴˴̵ͶͶηϸϸηͶ˴ʳʳ˴ͶηѻкϹϹϹѻҼӽӽҼкϹͷͷͷͷ͸λϼннϼι͸θ̶ɳɳʶ̸̸̸ιιλλλ͹˸ʷ͹ͺͺλλλͺ̹̹˸˸ʷʷ˸˸̹ϹϹккϹθθͷ˵ʴȲDZưDZȲȲɵʵʶ˷˵˵ʵɴ˶˶˸ʷʹʹ̼ͻκϸϸη̸ʶȵǴɶɶȸȸȸɹɼʸ˶˱ʳɲɲʳ˴˴ʲ̴ͷ̶ɳDZȲʴ˹ʸʸɷɷʸʸʸȶȶȶɷȶǵƴƴƴǵǵȶɷʸʸ˹ͻ̺˹ʸɷʸʸ˹ɷɷɷɷɷɷɷɷʸ˹˹˹˹ʸɷȶƴǵǵɷʸ˹̺̺̺̺˹˹˹˹˹ʸ˹̺̺̺ͻͻͻμ˹˹˹ʸʸʸɷɷ˹˹˹˹˹˹˹˹̺̺˹˹˹˹̺ͻμμͻ̺˹˹ʸʸɷɷɷʸʸ˹˹˹˹ҿҿҿҿѾʹλλ̷ͶҸҸϳжԺӹ̵̷ѾҿͼͺλнҿҿѾнϼннннннѾѾϹϹθθζϷижϵжѷҵггӶոӷҶѵϳͱ̰ʮɭͱβͱ˯̰дҶҸԼӻѹииѹиϷйѺѺѺҸѷжϵϳдггαͰͰαҵҵгͰʭɬʮ˯ͱϲϵжжжѸзҺҺѹииѹҺҺииѹҺҺҺѹѹжѷѷжδδжѷϵϵϵжжжжжӻҺиϷζϷиѹζζζζζζζζθθθϹϹкккѻѻккϹθθͷηηηηηηηηηηϸηͶηѺԽѺѺѺѺѺѺѺѺһйϸйһӼһйлι̷̷͸ι͸̷лллллϺ͸͸йϸηͶηϸйһѺѺѺѺѺѺѺѺϸϸйѺһһӼԽһӼԽӼѺйѺһվվԽԽһйϸηϸйѺӼӼӼӼӼѻкϹϹкѻӽԾԾԾӽҼѻϹͷ̶кӽտҼͷ̶кտӻѹϷ͵̴˳̴͵ϷϷζ͵̴̴̴͵ιлѼѼϺιιϺ̷˶ʵɴʵ˶͸ι̵̵ͶηͶ̵ʳɲ̵ʳȱȱʳ̵ͶͶѼлϺι͸ιϺлӾҽѼлϺϺϺϺ͸ιϺлϺι͸̷̷ʵɴDzDzȳɴʵ˳̴͵ζ͵̴ʲɱʲ˳ζϷζ˳ȰƮìĭƯȱɲʳ˴˴̵̵̵̵Ͷηϸйϼ̹˵˳ȮĨȿȿĦɫ˯˰ͳ̵ͷ˷˸˸˸̹̹ͺλϼϼϼͺ̹̹λѼ˴˴̵ͶͶηϸϸηͶ˴ɲɲ˴ͶηѻѻкϹкѻӽԾԾӽѻϹͷ̶̶̶̷ͺϼϼϼϺ͸̷θ̳ɳɳʴ̶̸˷͸ιλλκ̸˷ȵ͹̹ͺλλλͺͺ̹˸˸ʷʷ˸˸̹ϹккккϹθͷɳɳȲDZưưưƲǴȵʵʶ˵ʵʳɳʴʶɵʷʷʹ˹ͺκϸηη˷ɵǴƳʷʷȸȸȸɹȼʹ˴̰ʱʰʰʰʳʳǯʲ̶ͷ˵ɳɳʴʸʸʸʸʸʸʸʸɷɷʸʸʸɷȶǵƴƴǵǵȶɷʸʸμͻ˹ʸɷʸʸ˹ɷɷɷɷɷɷɷɷʸ˹˹˹˹ʸɷȶƴƴǵȶʸ˹̺̺ͻ̺̺˹˹ʸʸɷ˹˹˹̺̺̺ͻͻ˹˹ʸʸʸɷɷɷ˹˹˹˹˹˹˹˹ͻ̺̺˹˹̺ͻͻϽμͻͻ̺˹ʸʸɷɷɷʸʸ˹˹˹˹ҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿѾѾѾнϼϼϹϷииѷѹѹѹикλλλнѾѾнннϼϼϼλλλкккккиииижжѷҵӶӶӶжϵϵδͳͳ̲̲δδδδϵжѷҸ־սҺиϷиѹҺѺѺѺйѷжжжββαϲϲгггѴѴгϲͱ̰˯ʮϵϵиѸѸҹҹҹиѹѹҺҺѹѹиҺҺҺҺҺҺҺҺииииииииϷϷϷϷϷϷϷϷѹиϷζζϷϷи͵͵ζζζϷϷϷϹϹϹккѻѻѻϹϹϹккѻѻѻѺѺйηηηηηϸϸϸййѺѺѺһһһѺѺйййййййййййιιιιιιιιιιιϺϺлллйййѺѺһһһԽӼѺййѺӼԽѺййййѺһӼӼӼӼӼӼӼӼӼֿֿԽӼһѺѺѺййϸϸϸйѺѺѻѻҼҼҼҼҼӽԾӽѻϹθͷθθѻкϹϹѻѻкϹиииииϷ͵͵̴˳˳ʲʲ˳˳̴ιι͸̷˶˶˶˶ʵɴɴɴɴʵ˶̷ηδδͳͳ̲̲˱̲ͳδϵδͳ̲ʳϺϺϺϺϺϺϺϺѼѼѼѼѼѼѼѼѼлι̷˶˶̷͸ȳɴɴʵʵɴȳȳɱʲ˳̴͵̴˳ʲ˳̴͵ζ͵˳ȰǯǭƬĪĪĪƬȮʰ˱˱̲˱ʰ˱δйϺι͸̵˱ʮɭȫααϲϴδδζͷ͹ιιιϺϺллι͸̷̷̷ιлѼͶ̵̵ͶϸϸͶ̵̵˴ʳʳ̵ͶηͶʹʹԻ϶зҹ̳ɰεҹз̳ʴɵ˹κϻκ̸˷˵˴ǭŮɱ˳ʲʴͷͷ˵̷ικ̸ʶɶ˷ʷʷ˸̹ͺϼнʷ˸ʷȵƳƳɶ˸θϹкккθ̶˵ʴʴȲDZưưưdz̸̸˶ȴưì©ŭĬíïűȵʸͺ̸йҽлʶdzƲƲǴȵƵijɸп̾ųǮæʾǽĺȮ˱ϷζʴȲȲʴɷʸ̺̺ʸʸ̺ͻμͻͻ̺˹ʸɷȶƴƴƴǵǵȶȶȶ˹ʸɷȶɷʸ̺ͻ̺̺̺˹˹ʸʸʸʸ˹̺ͻͻ˹ʸɷǵǵƴƴƴǵȶɷɷɷɷɷɷɷɷɷ̺̺̺̺̺̺̺̺ʸʸʸʸʸʸʸʸ˹˹˹˹˹˹˹˹˹˹̺ͻͻμϽϽѿоϽμ̺˹ʸʸʸʸʸʸʸʸʸʸ˹ҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿѾѾѾнϼϼλϹииѹѹѹѹкϼϼλλѾѾҿҿѾѾѾнннϼϼϼλѻѻѻѻѻѻѹѹиижѷҵӶӶӶѷѷжϵδͳ̲̲ͳͳͳδϵжҸӹ־սҺиϷиѹҺѺѺййѷжжжββϲϲϲгггѴѴгϲͱ̰˯̯ϵϵзѸѸѸҹҹѹѹҺҺҺҺѹѹҺҺҺҺҺҺҺҺииииииииϷϷϷϷϷϷϷϷииϷζ͵͵͵ζ̴̴̴͵͵ζζζϹϹϹккѻѻѻϹϹϹккѻѻѻѺйϸηηηηηϸϸϸййѺѺѺһһѺѺѺйййййййййййιιιιιιιιιιιϺϺлллйййѺѺѺһһӼӼѺѺѺѺӼӼѺййййѺһӼӼӼӼӼӼӼӼӼֿվԽһѺѺѺѺѺйϸϸϸййѺӽӽӽҼҼҼҼҼӽҼкθθθθϹѻкθϹѻѻкϹϷиииϷϷζ͵˳˳ʲʲʲ˳̴̴̷˶ʵʵɴɴɴɴ˶ʵʵɴʵʵ˶̵ͶδδδͳͳͳͳͳδϵжѷѷжжлллллллллллллллллϺ͸˶˶˶˶̷ɴɴʵ˶ʵʵɴɴʲ˳̴̴̴˳ʲɱʲ˳̴͵̴ʲȰƮǭƬĪééĪƬǭ̲ͳδͳ̲̲δж̵Ͷ̵̵ͳ̲̰̯ʮ˯̰ͲͲͳ͵ʹ̶ηлѺлϸ͸˴ɴɲɴɲʵ˴͸η˴˴˴̵ηϸη̵̵˴ʳ˴̵Ͷηη϶϶Ӻ׾׾ӺӺռӺɰêǮ̳̳̳˵̸͹κκ̸ʴȲȯǬȾ¨ū̲ռѸϹθлϺκ̸˸̸˸˸˸̹ͺλλʷ˸˸ɶȵȵ˸ͺͷͷθͷ̶ʴȲDZʴʴʴɳȲȲȲȴ˷̷˶ȲĬɿŹĶŷĶĵĸǻƳʷɴ̵ι͸ɵdzdzȴȵʷȵǴɷ̺ƴɼ¶Ƭʲ˳ȲưDZȲɷʸ̺˹ʸʸ˹ͻ̺̺̺˹ʸʸɷɷǵǵǵǵȶȶȶȶʸʸɷȶɷʸ˹̺̺̺̺˹˹ʸʸʸʸ˹̺ͻ̺˹ɷȶǵƴƴƴƴǵȶȶɷɷɷɷɷɷɷɷ̺̺̺̺̺̺̺̺ʸʸʸʸʸʸʸʸ˹˹˹˹˹˹˹˹˹˹̺ͻͻμϽϽооϽμ̺˹ʸʸʸʸʸʸʸʸʸʸ˹ҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿѾѾннϼϼккѻѻѻѻѻѻϼϼϼλѾѾҿҿҿҿѾѾҿѾѾѾнннϼннннѻѻѻѻиижѷѷҸҸҸӹӹҸѷϵδͳ̲ͳͳͳδϵѷӹԺ־ԼҺѹииѹҺѺйййжжжжβϳϲϲггггѵѵдϳβͱ̲̲ζ϶ззйѺѺкѹҺҺӻӻҺҺѹѹѹѹѹѹѹѹѹϷϷϷϷϷϷϷϷϷϷϷϷϷϷϷϷиϷζ̴˳˳˳˳ʲʲʲ˳˳̴̴̴ϹϹϹккѻѻѻϹϹϹϹккккййϸηηηηϸϸϸϸййѺѺѺһѺѺѺййййййййййййϺϺϺϺϺϺϺϺιιϺϺϺϺллййййѺѺѺһӼһһѺѺһһӼѺййййѺһӼӼӼӼӼӼӼӼӼվԽӼһѺѺѺѺѺйϸϸϸϸййԾԾԾӽҼҼѻѻѻкϹϹϹϹккѻϹθϹкѻкθϷϷϷиϷζζ͵ʲʲʲʲʲ˳͵͵˶˶ʵʵʵɴɴɴ̷˶˶ʵʵʵ˶˴̵δδδδδϵϵ̲ͳδϵжѷҸҸϺϺϺϺϺϺϺϺϺϺϺϺϺϺϺϺι͸̷˶ʵʵʵʵʵʵ˶̷̷˶˶ʵ̴̴̴̴̴ʲɱȰɱʲ˳˳ʲɱǯŭȮƬ΍¨¨ééɯ˱ͳͳ̲˱̲ͳDZȲɳ̲ͳͳͳͲȮȮʰ˱˲̳˳̳̳ϵѺԺһж˴ɯɲ˱ʳ̵̲δηηɲɲɲ˴ηϸη̵̵̵̵˴̵̵ͶηзѸѸҹֽԻ̳ŬĺɿīȯʱккϹͷȲëɽƺ»ɯ̲ʹʹͶηϺϺϺ͹˸˸˸̹̹̹̹˸̹˸ʷȵɶ˸λϹϹϹθ̶ʴȲDZɳɳʴʴʴɳȲdz̷̷ʵȱ¨¸Ǵ̷ιι͸ʶɵʴ˵ŰƱűİĮ¬ƺzwuyžĪǭǮŬƯDZȶʸ˹˹ʸʸ˹ͻ˹˹ʸʸʸʸʸʸɷɷɷɷȶȶȶȶʸɷɷɷɷʸ˹̺˹˹˹˹ʸʸʸʸʸ˹̺̺̺˹ɷȶƴųųųƴǵȶȶɷɷɷɷɷɷɷɷ̺̺̺̺̺̺̺̺ʸʸʸʸʸʸʸʸ˹˹˹˹˹˹˹˹˹˹̺ͻͻμϽϽϽϽμͻ̺˹ʸʸɷɷɷʸʸʸʸʸ˹ҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿҿѾнϼϼккѻҼҼҼѻѻнϼϼϼѾѾѾҿҿѾѾѾҿҿҿѾѾѾннѾѾѾѾҼҼҼҼиииѷѷҸҸҸջԺӹҸжϵδͳͳͳͳδжҸӹջսԼӻѹииѹѹйййϸжжϵϵϳϳϲгггѴѴѵѵдϳβͱͳ̲ε϶϶϶ййййҺҺӻӻӻӻҺҺииииииииζζζζζζζζζζζζζζζζиϷ͵˳ʲɱȰȰɱɱɱʲʲʲ˳˳ϹϹϹккѻѻѻϹϹϹϹϹϹϹкϸϸηͶͶηηϸϸϸϸййѺѺѺѺѺѺйййϸϸййййййййллллллллϺϺϺϺϺϺϺϺϸϸйййѺѺѺһһһһһһһһѺййййѺһӼӼӼӼӼӼӼӼӼԽӼһѺййѺѺѺйϸϸηηϸϸտտԾӽҼѻкккккѻѻҼҼӽкϹθθкѻϹθζζϷϷϷζζ͵ʲʲʲʲ˳̴͵ζ̷̷̷͸͸̷̷˶̷̷˶ʵʵʵʵ˴ͳͳͳδϵϵжж˱˱˱˱̲ͳϵж̵̷̷̷̷̷̷̷͸͸͸͸͸͸͸͸̷̷˶ʵɴɴȳȳʵ˶̷͸͸͸̷̷͵͵͵̴˳ʲɱȰɱɱʲʲɱǯƮŭȮƬ΍ƿƿƿéƬɯ˱˱ʰ˱˲Ĭŭȯʰ˱˱˰˰ʯʯʰ˱˱ʱɱɰδϳѷӷҸдͳ˯̲̰ͳͱͳͱͳͳȱǰȱʳͶϸηͶͶͶͶ̵̵̵ͶϸзҹзʹҹռɰƼêǮккϹ˳ë¶wqmijpĪƬǮɰͶϸκ̹̹̹̹˸˸ʷɶͺͺ̹ɶǴǴȵ˸ͷͷ̶˵ʴɳȲȲDZȲɳʴʴɳȲȴ̷˶ʴȯ¥sjjmuǼDZιι͸̷ɵȲȲȰ¨ȼzrjfgkxūƭŬƯȱȶʸ˹˹ʸɷ˹̺ɷɷɷɷʸʸʸʸʸʸʸɷɷɷȶȶɷɷɷɷʸʸ˹˹˹ʸʸʸʸʸʸʸʸ˹̺̺˹ʸȶǵIJIJIJIJųƴǵȶȶȶȶȶȶȶȶȶ˹˹˹˹˹˹˹˹ʸʸʸʸʸʸʸʸ˹˹˹˹˹˹˹˹̺̺̺ͻͻμμμϽμμͻ̺˹ʸʸȶɷɷɷɷʸʸʸ˹ ================================================ FILE: Doxa.Test/SIMDTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" #include "ImageFixture.hpp" #include namespace Doxa::UnitTests { // Implements pure virtual functions class GlobalThresholdTestHarness: public GlobalThreshold { Pixel8 Threshold(const Image& grayScaleImage, const Parameters& parameters = Parameters()) { return 128; } }; // Exposes protected members for Unit Testing class DRDMTestHarness : public Doxa::DRDM { public: using DRDM::NUBN_STD; #if defined(DOXA_SIMD) using DRDM::NUBN_SIMD_8x8; #endif }; class SIMDTests : public ImageFixture {}; TEST_F(SIMDTests, SIMDDetection) { std::cout << "\n=== SIMD Compile-Time Detection ===" << std::endl; #if defined(DOXA_SIMD) #if defined(DOXA_SIMD_SSE2) std::cout << "Detected: SSE2 (16 pixels/op)" << std::endl; EXPECT_EQ(Doxa::SIMD::SIMD_WIDTH, 16); #elif defined(DOXA_SIMD_NEON) std::cout << "Detected: NEON (16 pixels/op)" << std::endl; EXPECT_EQ(Doxa::SIMD::SIMD_WIDTH, 16); #elif defined(DOXA_SIMD_WASM) std::cout << "Detected: WASM SIMD128 (16 pixels/op)" << std::endl; EXPECT_EQ(Doxa::SIMD::SIMD_WIDTH, 16); #endif #else std::cout << "No SIMD detected - scalar only" << std::endl; #endif } #if defined(DOXA_SIMD) TEST_F(SIMDTests, SIMDGlobalThresholdToBinaryTest) { const Pixel8 threshold = 128; Image binarySTD(image.width, image.height); Image binarySIMD(image.width, image.height); GlobalThresholdTestHarness::ToBinary_STD(image.data, binarySTD.data, image.size, threshold); GlobalThresholdTestHarness::ToBinary_SIMD(image.data, binarySIMD.data, image.size, threshold); TestUtilities::AssertImages(binarySTD, binarySIMD); } TEST_F(SIMDTests, SIMDClassifiedPerformanceCompareImagesTest) { std::string projFolder = TestUtilities::ProjectFolder(); const std::string filePathBinary = projFolder + "2JohnC1V3-Sauvola.pbm"; const std::string filePathGT = projFolder + "2JohnC1V3-GroundTruth.pbm"; ClassifiedPerformance::Classifications classificationsSTD; ClassifiedPerformance::Classifications classificationsSIMD; Image binaryImage = PNM::Read(filePathBinary); Image groundTruthImage = PNM::Read(filePathGT); ClassifiedPerformance::CompareImages_STD(classificationsSTD, groundTruthImage.data, binaryImage.data, groundTruthImage.size); ClassifiedPerformance::CompareImages_SIMD(classificationsSIMD, groundTruthImage.data, binaryImage.data, groundTruthImage.size); EXPECT_EQ(classificationsSTD.truePositive, classificationsSIMD.truePositive); EXPECT_EQ(classificationsSTD.trueNegative, classificationsSIMD.trueNegative); EXPECT_EQ(classificationsSTD.falsePositive, classificationsSIMD.falsePositive); EXPECT_EQ(classificationsSTD.falseNegative, classificationsSIMD.falseNegative); } TEST_F(SIMDTests, SIMDDrdmNubnTest) { const std::string filePathBinary = projFolder + "2JohnC1V3-GroundTruth.pbm"; Image binaryImage = PNM::Read(filePathBinary); const unsigned int nubnSTD = DRDMTestHarness::NUBN_STD(binaryImage, 8); const unsigned int nubnSIMD = DRDMTestHarness::NUBN_SIMD_8x8(binaryImage); EXPECT_EQ(nubnSTD, nubnSIMD); } #endif // DOXA_SIMD } ================================================ FILE: Doxa.Test/SuTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" namespace Doxa::UnitTests { // Exposes protected members for Unit Testing class SuTestHarness : public Su { public: SuTestHarness() : Su() {} using Su::AutoDetectParameters; using Su::SuCalculations; }; TEST(SuTests, SuAutoDetectParametersTest) { // Setup - Create a small contrast image with a known stroke pattern. // Peaks at stroke edges spaced 3px apart. const int width = 20; const int height = 5; Pixel8 contrastData[width * height]; std::memset(contrastData, 0, sizeof(contrastData)); for (int y = 0; y < height; ++y) { const int row = y * width; contrastData[row + 3] = 200; contrastData[row + 6] = 200; contrastData[row + 10] = 200; contrastData[row + 13] = 200; } Image contrastImage(width, height, contrastData); // Method under Test SuTestHarness su; int windowSize = 0; int minN = 0; su.Initialize(contrastImage); su.AutoDetectParameters(windowSize, minN, contrastImage); // The dominant peak distance is 3, so strokeWidth = 3 EXPECT_EQ(6, windowSize); // 3 * 2 = 6 EXPECT_EQ(6, minN); // windowSize } TEST(SuTests, SuCalculationsTest) { // Setup - 3x3 grayscale image with known values Pixel8 grayData[] = { 100, 150, 200, 50, 100, 150, 75, 125, 175 }; Image grayImage(3, 3, grayData); // High contrast binary image: White = high contrast pixel Pixel8 contrastData[] = { Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White, Palette::Black, Palette::White }; Image contrastImage(3, 3, contrastData); // Window covers entire image Region window; window.upperLeft = { 0, 0 }; window.bottomRight = { 2, 2 }; // Method under Test SuTestHarness su; su.Initialize(grayImage); int Ne; double meanE, stdE; su.SuCalculations(Ne, meanE, stdE, contrastImage, grayImage, window); // High contrast pixels (White): (0,0)=100, (0,2)=200, (1,1)=100, (2,0)=75, (2,2)=175 EXPECT_EQ(5, Ne); EXPECT_DOUBLE_EQ(130.0, meanE); // (100+200+100+75+175) / 5 // variance = sum((x-mean)^2)/Ne = (30^2 + 70^2 + 30^2 + 55^2 + 45^2)/5 = 11750/5 = 2350 // stdE = sqrt(2350) double expectedStdE = std::sqrt(11750.0 / 5.0); EXPECT_NEAR(expectedStdE, stdE, 0.001); } } ================================================ FILE: Doxa.Test/TestUtilities.hpp ================================================ #ifndef TESTUTILITIES_HPP #define TESTUTILITIES_HPP #include "pch.h" namespace Doxa::UnitTests { class TestUtilities { public: static std::string ProjectFolder() { // GTest will work in the current directory, but IDEs will usually put it in a diff spot. // Make sure to set the current working directory to the Doxa.Test folder! auto folder = fs::current_path() / "Resources"; if (!fs::exists(folder)) { throw "Could not find resource folder."; } return folder.string() + "/"; } static double Time(std::function predicate) { std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now(); predicate(); std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now(); std::chrono::duration time_span = std::chrono::duration_cast>(t2 - t1); return time_span.count(); // In Seconds } static void AssertImageData(const Image& image, const Pixel8* data) { EXPECT_EQ(0, std::memcmp(image.data, data, sizeof(Pixel8) * image.size)); } static void AssertImages(const Image& imageA, const Image& imageB) { EXPECT_EQ(imageA.size, imageB.size); EXPECT_EQ(0, std::memcmp(imageA.data, imageB.data, sizeof(Pixel8) * imageA.size)); } static void AssertImageFile(const Image& image, std::string filePath) { Image imageFromFile = PNM::Read(filePath); AssertImages(image, imageFromFile); } static bool AssertImagesWithDetails(const Image& imageA, const Image& imageB) { char buffer[100]; if (imageA.height != imageB.height || imageA.width != imageB.width) { std::snprintf(buffer, 100, "Image dimensions do not match. %dx%d vs %dx%d", imageA.width, imageA.height, imageB.width, imageB.height); SUCCEED() << buffer; } else { bool diffFound = false; int firstHeight = 0; int firstWidth = 0; int firstIndex = 0; int count = 0; for (int index = 0; index < imageA.size; index++) { if (imageA.data[index] != imageB.data[index]) { if (!diffFound) { firstIndex = index; firstHeight = index / imageA.width; firstWidth = index % imageA.width; diffFound = true; } ++count; } } // No differences found! if (!diffFound) return true; std::snprintf(buffer, 100, "%d difference(s) found. Example: Index %d, Width %d, Height %d.", count, firstIndex, firstWidth, firstHeight); SUCCEED() << buffer; } return false; } }; } #endif // TESTUTILITIES_HPP ================================================ FILE: Doxa.Test/WienerFilterTests.cpp ================================================ #include "pch.h" #include "TestUtilities.hpp" #include namespace Doxa::UnitTests { // A Wiener filter (per MATLAB's wiener2) is an adaptive denoiser. // For each pixel, given local mean (m) and local variance (v): // // This test uses a flat background with a single bright pixel. The expected // behavior is: // - All windows that don't see the bright pixel have v=0, output = m = input. // - The 9 windows containing the bright pixel get smoothed: the bright pixel // is pulled down toward its local mean, the 8 neighbors nudged up. TEST(WienerFilterTests, WienerFilterCenterPixelTest) { const Pixel8 data[] = { 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 100, 50, 50, 50, // Bright pixel at (3,3) 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50 }; // Derivation: // 9 windows contain the spike: m = (8*50 + 100)/9 = 500/9, v = 20000/81 // 40 flat windows: m = 50, v = 0 // noiseVar = (9 * 20000/81) / 49 = 180000/3969 // noiseVar / v = 9/49, so the dampening factor (1 - noiseVar/v) = 40/49 // // Center spike (input = 100): // out = 500/9 + (40/49)*(100 - 500/9) = 40500/441 ~= 91.837 -> 91 (uint8 trunc) // // 8 neighbors (input = 50): // out = 500/9 + (40/49)*( 50 - 500/9) = 22500/441 ~= 51.020 -> 51 const Pixel8 expected[] = { 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 51, 51, 51, 50, 50, 50, 50, 51, 91, 51, 50, 50, 50, 50, 51, 51, 51, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50 }; const Image inputImage(7, 7, data); Image outputImage(7, 7); WienerFilter::Filter(outputImage, inputImage, 3); TestUtilities::AssertImageData(outputImage, expected); } } ================================================ FILE: Doxa.Test/packages.config ================================================  ================================================ FILE: Doxa.Test/pch.cpp ================================================ // // pch.cpp // #include "pch.h" ================================================ FILE: Doxa.Test/pch.h ================================================ // // pch.h // #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gtest/gtest.h" ================================================ FILE: Doxa.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.6.33829.357 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Doxa", "Doxa\Doxa.vcxitems", "{D81F2737-6340-49C1-AE65-8D217415C67E}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Doxa.Test", "Doxa.Test\Doxa.Test.vcxproj", "{5723F19A-A7AB-45D0-A78C-B04FE035865F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {5723F19A-A7AB-45D0-A78C-B04FE035865F}.Debug|x64.ActiveCfg = Debug|x64 {5723F19A-A7AB-45D0-A78C-B04FE035865F}.Debug|x64.Build.0 = Debug|x64 {5723F19A-A7AB-45D0-A78C-B04FE035865F}.Release|x64.ActiveCfg = Release|x64 {5723F19A-A7AB-45D0-A78C-B04FE035865F}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {148C3EB0-D413-43A2-A7CD-B8BD21321540} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution Doxa\Doxa.vcxitems*{5723f19a-a7ab-45d0-a78c-b04fe035865f}*SharedItemsImports = 4 Doxa\Doxa.vcxitems*{d81f2737-6340-49c1-ae65-8d217415c67e}*SharedItemsImports = 9 EndGlobalSection EndGlobal ================================================ FILE: LICENSE ================================================ CC0 1.0 Universal Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. For more information, please see ================================================ FILE: README.md ================================================ # Δoxa Binarization Framework [![codecov](https://codecov.io/gh/brandonmpetty/Doxa/graph/badge.svg?token=XMG2X9KQKJ)](https://codecov.io/gh/brandonmpetty/Doxa)

## Introduction ΔBF is an image binarization framework which focuses primarily on local adaptive thresholding algorithms. In English, this means that it has the ability to turn a color or gray scale image into a black and white image. It is written in C++ but supports multiple language bindings. **Algorithms** * Otsu - "A threshold selection method from gray-level histograms", 1979. * Bernsen - "Dynamic thresholding of gray-level images", 1986. * Niblack - "An Introduction to Digital Image Processing", 1986. * Sauvola - "Adaptive document image binarization", 1999. * Wolf - "Extraction and Recognition of Artificial Text in Multimedia Documents", 2003. * Gatos - "Adaptive degraded document image binarization", 2005. (Partial) * NICK - "Comparison of Niblack inspired Binarization methods for ancient documents", 2009. * AdOtsu - "A multi-scale framework for adaptive binarization of degraded document images", 2010. * Su - "Binarization of Historical Document Images Using the Local Maximum and Minimum", 2010. * T.R. Singh - "A New local Adaptive Thresholding Technique in Binarization", 2011. * Bataineh - "An adaptive local binarization method for document images based on a novel thresholding method and dynamic windows", 2011. (unreproducible) * Phansalkar - "Adaptive Local Thresholding for Detection of Nuclei in Diversely Stained Cytology Images", 2011. * ISauvola - "ISauvola: Improved Sauvola’s Algorithm for Document Image Binarization", 2016. * WAN - "Binarization of Document Image Using Optimum Threshold Modification", 2018. **Optimizations** * Shafait - "Efficient Implementation of Local Adaptive Thresholding Techniques Using Integral Images", 2008. * Petty - An algorithm for efficiently calculating the min and max of a local window. Unpublished, 2019. * Chan - "Memory-efficient and fast implementation of local adaptive binarization methods", 2019. * SIMD - Supporting: SSE2, ARM NEON, WASM SIMD128 **Performance Metrics** * Overall Accuracy * F-Measure, Precision, Recall * Pseudo F-Measure, Precision, Recall - "Performance Evaluation Methodology for Historical Document Image Binarization", 2013. * Peak Signal-To-Noise Ratio (PSNR) * Negative Rate Metric (NRM) * Matthews Correlation Coefficient (MCC) * Distance-Reciprocal Distortion Measure (DRDM) - "An Objective Distortion Measure for Binary Document Images Based on Human Visual Perception", 2002. **Native Image Support** * Portable Any-Map: PBM (P4), 8-bit PGM (P5), PPM (P6), PAM (P7) ## Overview The goal of this library is to provide the building blocks one might use to advance the state of handwritten manuscript binarization. What sets this binarization library apart is that it is intended to be used by those interested in experimenting with their own algorithms and enhancements. Instead of being relegated to MATLAB, or obfuscated by mathematics in a research paper, a lot of effort has gone into exposing these binarization techniques in an open and transparent way. A key objective in designing this framework was to make it modular and as easy to use as possible, without sacrificing speed and without depending heavily on 3rd party frameworks. This library is also heavily unit tested to help ensure quality, and to quickly spot problems after experimenting with the codebase. ### Example This short example shows you how easy it is to use ΔBF to process an image. ```cpp // Read a 32-bit color image and automatically convert to 8-bit gray scale Image image = PNM::Read(R"(C:\MyImage.pam)"); // Use a binarization algorithm to convert it into black and white const Parameters parameters({ {"window", 25}, {"k", 0.10} }); Image imageSauvola = Sauvola::ToBinaryImage(image, parameters); // Save the processed image PNM::Write(imageSauvola, R"(C:\MyImage-Sauvola.pam)"); ``` ΔBF is incredibly light weight, being a header-only library. It can integrate easily with other 3rd party C++ frameworks like OpenCV and Qt. Examples can be found under the Demo folder. ### Building and Testing The core library is header-only and requires no build. For bindings and tests, use CMake presets: ```bash # Build everything (C++ Tests, Python, WASM, MATLAB) cmake --preset all cmake --build build --config Release ctest --test-dir build -C Release # Build and run performance benchmarks (Google Benchmark) cmake --preset benchmarks cmake --build build-bench --config Release ./build-bench/Doxa.Bench/doxa_bench # Linux/Mac .\build-bench\Doxa.Bench\Release\doxa_bench.exe # Windows # Build and run C++ unit tests cmake --preset cpp-tests cmake --build build-cpp-tests --config Release ctest --test-dir build-cpp-tests -C Release # Build and test Python bindings (requires Python 3.12+, nanobind) cmake --preset python cmake --build build-python --config Release ctest --test-dir build-python -C Release # Build and test WebAssembly (requires Emscripten in PATH) cmake --preset wasm cmake --build build-wasm --config Release ctest --test-dir build-wasm -C Release # Build and test MATLAB bindings (requires MATLAB) cmake --preset matlab cmake --build build-matlab --config Release ctest --test-dir build-matlab -C Release ``` ### Language Bindings * Javascript / WASM * Python * Matlab Examples of how to use each binding are provided in the Demo folder. See [Bindings/Python/README.md](Bindings/Python/README.md), [Bindings/WebAssembly/README.md](Bindings/WebAssembly/README.md), and [Bindings/Matlab/README.md](Bindings/Matlab/README.md) for detailed instructions. A [Live Demo](https://brandonmpetty.github.io/Doxa/WebAssembly) has been created to highlight some of what ΔBF is capable of on the web. ### Benchmarks The project uses [Google Benchmark](https://github.com/google/benchmark) for measuring runtime performance of SIMD optimizations, calculator backends, and core operations. Benchmarks are separate from unit tests to keep correctness and runtime performance concerns independent. ```bash # Run benchmarks long enough to lower CV % ./build-bench/Doxa.Bench/doxa_bench --benchmark_min_time=1s --benchmark_repetitions=10 --benchmark_report_aggregates_only=true # Compare two runs (e.g., before/after a change, or across platforms) # Requires running doxa_bench with: --benchmark_out=results.json --benchmark_out_format=json python build-bench/_deps/googlebenchmark-src/tools/compare.py benchmarks before.json after.json ``` ### Performance Analysis Another thing that sets ΔBF apart is its focus on binarization performance. This makes it incredibly simple to see how your changes affect the overall quality of an algorithm. All DIBCO metric algorithms, past and present, are provided. ΔBF's metrics are significantly faster than calling DIBCO_metrics.exe directly. **NOTE** - DIBCO Weight generation still requires [BinEvalWeights](https://github.com/kzagoris/DibcoEvaluation/tree/master/Prerequisites/BinEvalWeights) ## License CC0 - Brandon M. Petty, 2026 To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. [View Online](https://creativecommons.org/publicdomain/zero/1.0/legalcode) "*Freely you have received; freely give.*" - Matt 10:8