Initial commit

This commit is contained in:
Yilei JIANG
2025-07-28 18:43:47 +08:00
parent 0a6e4ab682
commit 06408ffa6a
702 changed files with 153932 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# Node / JavaScript
node_modules/
dist/
.DS_Store
# Environments
.env
.venv
# API keys
doubao_api.txt

89
README.md Normal file
View File

@@ -0,0 +1,89 @@
# Screencoder
### About
This is the Screencoder Project. Screencoder generates the HTML code for a website screenshot using a modular multi-agent framework.
### Project Structure
- `main.py`: The main script to generate final HTML code for a single screenshot.
- `UIED/`: Contains the UIED (UI Element Detection) engine for analyzing screenshots and detecting components.
- `run_single.py`: Python script to run UI component detection on a single image.
- `html_generator.py`: Takes the detected component data and generates a complete HTML layout with generated code for each module.
- `image_replacer.py`: A script to replace placeholder divs in the final HTML with actual cropped images.
- `mapping.py`: Maps the detected UIED components to logical page regions.
- `requirements.txt`: Lists all the necessary Python dependencies for the project.
- `doubao_api.txt`: API key file for the Doubao model (should be kept private and is included in `.gitignore`).
### Setup and Installation
1. **Clone the repository:**
```bash
git clone https://github.com/JimmyZhengyz/screencoder.git
cd screencoder
```
2. **Create a virtual environment:**
```bash
python3 -m venv .venv
source .venv/bin/activate
```
3. **Install dependencies:**
```bash
pip install -r requirements.txt
```
4. **Set up API Key:**
- Create a file named `doubao_api.txt` in the root directory.
- Paste your Doubao API key into this file.
### Usage
The typical workflow is a multi-step process as follows:
1. **Initial Generation with Placeholders:**
Run the Python script to generate the initial HTML code for a given screenshot.
- Block Detection:
```bash
python block_parsor.py
```
- Generation with Placeholders (Gray Images Blocks):
```bash
python html_generator.py
```
2. **Final HTML Code:**
Run the python script to generate final HTML code with copped images from the original screenshot.
- Placeholder Detection:
```bash
python image_box_detection.py
```
- UI Element Detection:
```bash
python UIED/run_single.py
```
- Mapping Alignment Between Placeholders and UI Elements:
```bash
python mapping.py
```
- Placeholder Replacement:
```bash
python image_replacer.py
```
3. **Simple Run:**
Run the python script to generate the final HTML code:
```bash
python main.py
```
### Demo
To access demo, you can follow these steps:
```bash
cd demo
pnpm install
pnpm run dev
```
Then, you can see the demo running.

BIN
UIED.zip Normal file

Binary file not shown.

14
UIED/.idea/UIED.iml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/resnet" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module>

View File

@@ -0,0 +1,29 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="54" name="Python" />
</Languages>
</inspection_tool>
<inspection_tool class="PyInterpreterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="3">
<item index="0" class="java.lang.String" itemvalue="Tensorflow" />
<item index="1" class="java.lang.String" itemvalue="Sklearn" />
<item index="2" class="java.lang.String" itemvalue="Opencv" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="E501" />
</list>
</option>
</inspection_tool>
</profile>
</component>

4
UIED/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6" project-jdk-type="Python SDK" />
</project>

8
UIED/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/UIED.iml" filepath="$PROJECT_DIR$/.idea/UIED.iml" />
</modules>
</component>
</project>

6
UIED/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

336
UIED/.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,336 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="b4069649-920d-465f-ac6b-bac85007c2bb" name="Default" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="JupyterTrust" id="2ce0fe3c-0081-4dca-a6a6-b1219d650764" />
<component name="ProjectId" id="1dwCihBTog6GQX95sgxr7TpM6ZO" />
<component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
<option name="showMembers" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="node.js.detected.package.eslint" value="true" />
<property name="node.js.detected.package.tslint" value="true" />
<property name="node.js.path.for.package.eslint" value="project" />
<property name="node.js.path.for.package.tslint" value="project" />
<property name="node.js.selected.package.eslint" value="(autodetect)" />
<property name="node.js.selected.package.tslint" value="(autodetect)" />
<property name="restartRequiresConfirmation" value="false" />
<property name="settings.editor.selected.configurable" value="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" />
<property name="two.files.diff.last.used.file" value="$PROJECT_DIR$/../UI2CODE/Element-Detection/merge.py" />
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="D:\git_file\github\doing\UIED\data\demo" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="D:\git_file\github\doing\UIED\detect_compo" />
<recent name="D:\git_file\github\doing\UIED\detect_compo\deprecated" />
<recent name="D:\git_file\github\doing\UIED\utils" />
<recent name="D:\git_file\github\doing\UIED" />
<recent name="D:\git_file\github\doing\UIED\result_processing" />
</key>
</component>
<component name="RunManager" selected="Python.run_single">
<configuration name="merge" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="UIED" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/detect_merge" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/detect_merge/merge.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="merge2" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="UIED" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/merge2.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="run_single" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="UIED" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/run_single.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="run_testing(Used for Adjusting)" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="UIED" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/run_testing(Used for Adjusting).py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="text_detection" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="UIED" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/detect_text" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/detect_text/text_detection.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<list>
<item itemvalue="Python.merge2" />
<item itemvalue="Python.merge" />
<item itemvalue="Python.run_single" />
<item itemvalue="Python.run_testing(Used for Adjusting)" />
<item itemvalue="Python.text_detection" />
</list>
<recent_temporary>
<list>
<item itemvalue="Python.run_single" />
<item itemvalue="Python.run_testing(Used for Adjusting)" />
<item itemvalue="Python.merge" />
<item itemvalue="Python.merge2" />
<item itemvalue="Python.text_detection" />
</list>
</recent_temporary>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="SvnConfiguration">
<configuration />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="b4069649-920d-465f-ac6b-bac85007c2bb" name="Default" comment="" />
<created>1581826543105</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1581826543105</updated>
<workItem from="1593326898629" duration="4378000" />
<workItem from="1594035929667" duration="39000" />
<workItem from="1594085137817" duration="1275000" />
<workItem from="1594100418101" duration="3473000" />
<workItem from="1594163935256" duration="1077000" />
<workItem from="1594182213679" duration="2275000" />
<workItem from="1595462115111" duration="3597000" />
<workItem from="1595466120556" duration="3158000" />
<workItem from="1595488240502" duration="74000" />
<workItem from="1595723287741" duration="773000" />
<workItem from="1596417993986" duration="2972000" />
<workItem from="1596442796733" duration="9601000" />
<workItem from="1596604406529" duration="10000" />
<workItem from="1596685531295" duration="1526000" />
<workItem from="1596792120495" duration="27485000" />
<workItem from="1596926803756" duration="894000" />
<workItem from="1596955557805" duration="717000" />
<workItem from="1597015667916" duration="1488000" />
<workItem from="1598487885922" duration="962000" />
<workItem from="1601609000493" duration="599000" />
<workItem from="1601849117964" duration="1226000" />
<workItem from="1601850762269" duration="6477000" />
<workItem from="1601933366434" duration="6847000" />
<workItem from="1602130037944" duration="4303000" />
<workItem from="1602199380249" duration="5493000" />
<workItem from="1603669721746" duration="6042000" />
<workItem from="1604011435077" duration="517000" />
<workItem from="1604016832655" duration="5114000" />
<workItem from="1604037397074" duration="12109000" />
<workItem from="1604564719252" duration="1133000" />
<workItem from="1604619358289" duration="17569000" />
<workItem from="1604874207809" duration="171000" />
<workItem from="1605071062104" duration="3079000" />
<workItem from="1605086142565" duration="6397000" />
<workItem from="1625010508533" duration="4541000" />
<workItem from="1625099073176" duration="49720000" />
<workItem from="1625443415902" duration="26350000" />
<workItem from="1625529598285" duration="26479000" />
<workItem from="1625613709029" duration="14000" />
<workItem from="1625730508694" duration="928000" />
<workItem from="1625809233064" duration="837000" />
<workItem from="1626009011038" duration="18000" />
<workItem from="1626307428798" duration="1983000" />
<workItem from="1628054466383" duration="1894000" />
<workItem from="1628122812217" duration="7049000" />
<workItem from="1630237947629" duration="453000" />
<workItem from="1630268189943" duration="20000" />
<workItem from="1630297231550" duration="5710000" />
<workItem from="1630312264694" duration="7894000" />
<workItem from="1631149307515" duration="752000" />
<workItem from="1631576239206" duration="805000" />
<workItem from="1631584649434" duration="2272000" />
<workItem from="1648024033251" duration="1883000" />
</task>
<servers />
</component>
<component name="TestHistory">
<history-entry file="py_test_in_test_lucky_py - 2020.03.04 at 05h 51m 04s.xml">
<configuration name="py.test in test_lucky.py" configurationId="tests" />
</history-entry>
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url />
<line>147</line>
<option name="timeStamp" value="3" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url />
<line>134</line>
<option name="timeStamp" value="4" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url />
<line>135</line>
<option name="timeStamp" value="5" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url />
<line>136</line>
<option name="timeStamp" value="6" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/detect_text_east/lib_east/eval.py</url>
<line>263</line>
<option name="timeStamp" value="67" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/result_processing/eval_classes.py</url>
<line>92</line>
<option name="timeStamp" value="92" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/detect_text_east/lib_east/eval.py</url>
<line>108</line>
<option name="timeStamp" value="93" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/cnn/CNN.py</url>
<line>62</line>
<option name="timeStamp" value="94" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/run_single.py</url>
<line>27</line>
<option name="timeStamp" value="101" />
</line-breakpoint>
</breakpoints>
<default-breakpoints>
<breakpoint type="python-exception">
<properties notifyOnTerminate="true" exception="BaseException">
<option name="notifyOnTerminate" value="true" />
</properties>
</breakpoint>
</default-breakpoints>
</breakpoint-manager>
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/UIED$view_gt.coverage" NAME="view_gt Coverage Results" MODIFIED="1596418105849" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/result_processing" />
<SUITE FILE_PATH="coverage/UIED$run_single.coverage" NAME="run_single Coverage Results" MODIFIED="1648024874423" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/UIED$main_single.coverage" NAME="main_single Coverage Results" MODIFIED="1594102734340" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/UIED$run_batch.coverage" NAME="run_batch Coverage Results" MODIFIED="1596448499254" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/UIED_block$main_single.coverage" NAME="main_single Coverage Results" MODIFIED="1594035942350" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/UIED$run_testing_Used_for_Adjusting_.coverage" NAME="run_testing(Used for Adjusting) Coverage Results" MODIFIED="1631149318891" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/UIED$merge2.coverage" NAME="merge2 Coverage Results" MODIFIED="1625271385465" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/UIED$run_testing_Use_Me_for_Adjusting_.coverage" NAME="run_testing(Use Me for Adjusting) Coverage Results" MODIFIED="1605091364353" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/UIED$text_detection.coverage" NAME="text_detection Coverage Results" MODIFIED="1625107691453" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/detect_text" />
<SUITE FILE_PATH="coverage/UIED$merge.coverage" NAME="merge Coverage Results" MODIFIED="1625284362497" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/detect_merge" />
<SUITE FILE_PATH="coverage/UIED$experiment.coverage" NAME="experiment Coverage Results" MODIFIED="1605087951044" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/result_processing" />
</component>
</project>

201
UIED/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2021] [UIED mulong.xie@anu.edu.au]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

80
UIED/README.md Normal file
View File

@@ -0,0 +1,80 @@
# UIED - UI element detection, detecting UI elements from UI screenshots or drawnings
This project is still ongoing and this repo may be updated irregularly, I developed a web app for the UIED in http://uied.online
## Related Publications:
[1. UIED: a hybrid tool for GUI element detection](https://dl.acm.org/doi/10.1145/3368089.3417940)
[2. Object Detection for Graphical User Interface: Old Fashioned or Deep Learning or a Combination?](https://arxiv.org/abs/2008.05132)
>The repo has been **upgraded with Google OCR** for GUI text detection, to use the original version in our paper (using [EAST](https://github.com/argman/EAST) as text detector), check the relase [v2.3](https://github.com/MulongXie/UIED/releases/tag/v2.3) and download the pre-trained model in [this link](https://drive.google.com/drive/folders/1MK0Om7Lx0wRXGDfNcyj21B0FL1T461v5?usp=sharing).
## What is it?
UI Element Detection (UIED) is an old-fashioned computer vision (CV) based element detection approach for graphic user interface.
The input of UIED could be various UI image, such as mobile app or web page screenshot, UI design drawn by Photoshop or Sketch, and even some hand-drawn UI design. Then the approach detects and classifies text and graphic UI elements, and exports the detection result as JSON file for future application.
UIED comprises two parts to detect UI text and graphic elements, such as button, image and input bar.
* For text, it leverages [Google OCR](https://cloud.google.com/vision/docs/ocr) to perfrom detection.
* For graphical elements, it uses old-fashioned CV approaches to locate the elements and a CNN classifier to achieve classification.
> UIED is highly customizable, you can replace both parts by your choice (e.g. other text detection approaches). Unlike black-box end-to-end deep learning approach, you can revise the algorithms in the non-text detection and merging (partially or entirely) easily to fit your task.
![UIED Approach](https://github.com/MulongXie/UIED/blob/master/data/demo/approach.png)
## How to use?
### Dependency
* **Python 3.5**
* **Opencv 3.4.2**
* **Pandas**
<!-- * **Tensorflow 1.10.0**
* **Keras 2.2.4**
* **Sklearn 0.22.2** -->
### Installation
<!-- Install the mentioned dependencies, and download two pre-trained models from [this link](https://drive.google.com/drive/folders/1MK0Om7Lx0wRXGDfNcyj21B0FL1T461v5?usp=sharing) for EAST text detection and GUI element classification. -->
<!-- Change ``CNN_PATH`` and ``EAST_PATH`` in *config/CONFIG.py* to your locations. -->
The new version of UIED equipped with Google OCR is easy to deploy and no pre-trained model is needed. Simply donwload the repo along with the dependencies.
> Please replace the Google OCR key at `detect_text/ocr.py line 28` with your own (apply in [Google website](https://cloud.google.com/vision)).
### Usage
To test your own image(s):
* To test single image, change *input_path_img* in ``run_single.py`` to your input image and the results will be output to *output_root*.
* To test mutiple images, change *input_img_root* in ``run_batch.py`` to your input directory and the results will be output to *output_root*.
* To adjust the parameters lively, using ``run_testing.py``
> Note: The best set of parameters vary for different types of GUI image (Mobile App, Web, PC). I highly recommend to first play with the ``run_testing.py`` to pick a good set of parameters for your data.
## Folder structure
``cnn/``
* Used to train classifier for graphic UI elements
* Set path of the CNN classification model
``config/``
* Set data paths
* Set parameters for graphic elements detection
``data/``
* Input UI images and output detection results
``detect_compo/``
* Non-text GUI component detection
``detect_text/``
* GUI text detection using Google OCR
``detect_merge/``
* Merge the detection results of non-text and text GUI elements
The major detection algorithms are in ``detect_compo/``, ``detect_text/`` and ``detect_merge/``
## Demo
GUI element detection result for web screenshot
![UI Components detection result](https://github.com/MulongXie/UIED/blob/master/data/demo/demo.png)

125
UIED/cnn/CNN.py Normal file
View File

@@ -0,0 +1,125 @@
import keras
from keras.applications.resnet50 import ResNet50
from keras.models import Model,load_model
from keras.layers import Dense, Activation, Flatten, Dropout
from sklearn.metrics import confusion_matrix
import numpy as np
import cv2
from config.CONFIG import Config
cfg = Config()
class CNN:
def __init__(self, classifier_type, is_load=True):
'''
:param classifier_type: 'Text' or 'Noise' or 'Elements'
'''
self.data = None
self.model = None
self.classifier_type = classifier_type
self.image_shape = (32,32,3)
self.class_number = None
self.class_map = None
self.model_path = None
self.classifier_type = classifier_type
if is_load:
self.load(classifier_type)
def build_model(self, epoch_num, is_compile=True):
base_model = ResNet50(include_top=False, weights='imagenet', input_shape=self.image_shape)
for layer in base_model.layers:
layer.trainable = False
self.model = Flatten()(base_model.output)
self.model = Dense(128, activation='relu')(self.model)
self.model = Dropout(0.5)(self.model)
self.model = Dense(15, activation='softmax')(self.model)
self.model = Model(inputs=base_model.input, outputs=self.model)
if is_compile:
self.model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy'])
self.model.fit(self.data.X_train, self.data.Y_train, batch_size=64, epochs=epoch_num, verbose=1,
validation_data=(self.data.X_test, self.data.Y_test))
def train(self, data, epoch_num=30):
self.data = data
self.build_model(epoch_num)
self.model.save(self.model_path)
print("Trained model is saved to", self.model_path)
def load(self, classifier_type):
if classifier_type == 'Text':
self.model_path = 'E:/Mulong/Model/rico_compos/cnn-textview-2.h5'
self.class_map = ['Text', 'Non-Text']
elif classifier_type == 'Noise':
self.model_path = 'E:/Mulong/Model/rico_compos/cnn-noise-1.h5'
self.class_map = ['Noise', 'Non-Noise']
elif classifier_type == 'Elements':
# self.model_path = 'E:/Mulong/Model/rico_compos/resnet-ele14-19.h5'
# self.model_path = 'E:/Mulong/Model/rico_compos/resnet-ele14-28.h5'
# self.model_path = 'E:/Mulong/Model/rico_compos/resnet-ele14-45.h5'
self.model_path = 'UIED/cnn/model/cnn-rico-1.h5' # Use local model
self.class_map = cfg.element_class
self.image_shape = (64, 64, 3)
elif classifier_type == 'Image':
# Redirect 'Image' classification to use the general 'Elements' model
# as the specific model is not available in the project.
# IMPORTANT: This requires the actual model file to be present for real classification.
print("Warning: 'Image' specific model not found. Redirecting to general 'Elements' classifier.")
self.model_path = 'UIED/cnn/model/cnn-rico-1.h5' # Use local model
self.class_map = ['Image', 'Non-Image'] # Keep the class map for binary classification logic
self.class_number = len(self.class_map)
try:
self.model = load_model(self.model_path)
print('Model Loaded From', self.model_path)
except Exception as e:
print(f"Error loading model: {e}")
print("A dummy model file was created, but it's not a valid Keras model.")
print("Please replace it with the actual model file for classification to work.")
self.model = None
def preprocess_img(self, image):
image = cv2.resize(image, self.image_shape[:2])
x = (image / 255).astype('float32')
x = np.array([x])
return x
def predict(self, imgs, compos, load=False, show=False):
"""
:type img_path: list of img path
"""
if load:
self.load(self.classifier_type)
if self.model is None:
print("*** No model loaded ***")
return
for i in range(len(imgs)):
X = self.preprocess_img(imgs[i])
Y = self.class_map[np.argmax(self.model.predict(X))]
compos[i].category = Y
if show:
print(Y)
cv2.imshow('element', imgs[i])
cv2.waitKey()
def evaluate(self, data, load=True):
if load:
self.load(self.classifier_type)
X_test = data.X_test
Y_test = [np.argmax(y) for y in data.Y_test]
Y_pre = [np.argmax(y_pre) for y_pre in self.model.predict(X_test, verbose=1)]
matrix = confusion_matrix(Y_test, Y_pre)
print(matrix)
TP, FP, FN = 0, 0, 0
for i in range(len(matrix)):
TP += matrix[i][i]
FP += sum(matrix[i][:]) - matrix[i][i]
FN += sum(matrix[:][i]) - matrix[i][i]
precision = TP/(TP+FP)
recall = TP / (TP+FN)
print("Precision:%.3f, Recall:%.3f" % (precision, recall))

21
UIED/cnn/Config.py Normal file
View File

@@ -0,0 +1,21 @@
class Config:
def __init__(self):
# cnn 4 classes
# self.MODEL_PATH = 'E:/Mulong/Model/ui_compos/cnn6_icon.h5' # cnn 4 classes
# self.class_map = ['Image', 'Icon', 'Button', 'Input']
# resnet 14 classes
# self.DATA_PATH = "E:/Mulong/Datasets/rico/elements-14-2"
# self.MODEL_PATH = 'E:/Mulong/Model/rico_compos/resnet-ele14.h5'
# self.class_map = ['Button', 'CheckBox', 'Chronometer', 'EditText', 'ImageButton', 'ImageView',
# 'ProgressBar', 'RadioButton', 'RatingBar', 'SeekBar', 'Spinner', 'Switch',
# 'ToggleButton', 'VideoView', 'TextView'] # ele-14
self.DATA_PATH = "E:\Mulong\Datasets\dataset_webpage\Components3"
self.MODEL_PATH = 'E:/Mulong/Model/rico_compos/cnn2-textview.h5'
self.class_map = ['Text', 'Non-Text']
self.image_shape = (32, 32, 3)
self.class_number = len(self.class_map)

69
UIED/cnn/Data.py Normal file
View File

@@ -0,0 +1,69 @@
import cv2
import numpy as np
from os.path import join as pjoin
import glob
from tqdm import tqdm
from Config import Config
cfg = Config()
class Data:
def __init__(self):
self.data_num = 0
self.images = []
self.labels = []
self.X_train, self.Y_train = None, None
self.X_test, self.Y_test = None, None
self.image_shape = cfg.image_shape
self.class_number = cfg.class_number
self.class_map = cfg.class_map
self.DATA_PATH = cfg.DATA_PATH
def load_data(self, resize=True, shape=None, max_number=1000000):
# if customize shape
if shape is not None:
self.image_shape = shape
else:
shape = self.image_shape
# load data
for p in glob.glob(pjoin(self.DATA_PATH, '*')):
print("*** Loading components of %s: %d ***" %(p.split('\\')[-1], int(len(glob.glob(pjoin(p, '*.png'))))))
label = self.class_map.index(p.split('\\')[-1]) # map to index of classes
for i, image_path in enumerate(tqdm(glob.glob(pjoin(p, '*.png'))[:max_number])):
image = cv2.imread(image_path)
if resize:
image = cv2.resize(image, shape[:2])
self.images.append(image)
self.labels.append(label)
assert len(self.images) == len(self.labels)
self.data_num = len(self.images)
print('%d Data Loaded' % self.data_num)
def generate_training_data(self, train_data_ratio=0.8):
# transfer int into c dimensions one-hot array
def expand(label, class_number):
# return y : (num_class, num_samples)
y = np.eye(class_number)[label]
y = np.squeeze(y)
return y
# reshuffle
np.random.seed(0)
self.images = np.random.permutation(self.images)
np.random.seed(0)
self.labels = np.random.permutation(self.labels)
Y = expand(self.labels, self.class_number)
# separate dataset
cut = int(train_data_ratio * self.data_num)
self.X_train = (self.images[:cut] / 255).astype('float32')
self.X_test = (self.images[cut:] / 255).astype('float32')
self.Y_train = Y[:cut]
self.Y_test = Y[cut:]
print('X_train:%d, Y_train:%d' % (len(self.X_train), len(self.Y_train)))
print('X_test:%d, Y_test:%d' % (len(self.X_test), len(self.Y_test)))

Binary file not shown.

Binary file not shown.

View File

45
UIED/config/CONFIG.py Normal file
View File

@@ -0,0 +1,45 @@
from os.path import join as pjoin
import os
class Config:
def __init__(self):
# setting CNN (graphic elements) model
self.image_shape = (64, 64, 3)
# self.MODEL_PATH = 'E:\\Mulong\\Model\\UI2CODE\\cnn6_icon.h5'
# self.class_map = ['button', 'input', 'icon', 'img', 'text']
self.CNN_PATH = 'E:/Mulong/Model/rico_compos/cnn-rico-1.h5'
self.element_class = ['Button', 'CheckBox', 'Chronometer', 'EditText', 'ImageButton', 'ImageView',
'ProgressBar', 'RadioButton', 'RatingBar', 'SeekBar', 'Spinner', 'Switch',
'ToggleButton', 'VideoView', 'TextView']
self.class_number = len(self.element_class)
# setting EAST (ocr) model
self.EAST_PATH = 'E:/Mulong/Model/East/east_icdar2015_resnet_v1_50_rbox'
self.COLOR = {'Button': (0, 255, 0), 'CheckBox': (0, 0, 255), 'Chronometer': (255, 166, 166),
'EditText': (255, 166, 0),
'ImageButton': (77, 77, 255), 'ImageView': (255, 0, 166), 'ProgressBar': (166, 0, 255),
'RadioButton': (166, 166, 166),
'RatingBar': (0, 166, 255), 'SeekBar': (0, 166, 10), 'Spinner': (50, 21, 255),
'Switch': (80, 166, 66), 'ToggleButton': (0, 66, 80), 'VideoView': (88, 66, 0),
'TextView': (169, 255, 0), 'NonText': (0,0,255),
'Compo':(0, 0, 255), 'Text':(169, 255, 0), 'Block':(80, 166, 66)}
def build_output_folders(self):
# setting data flow paths
self.ROOT_INPUT = "E:\\Mulong\\Datasets\\rico\\combined"
self.ROOT_OUTPUT = "E:\\Mulong\\Result\\rico\\rico_uied\\rico_new_uied_v3"
self.ROOT_IMG_ORG = pjoin(self.ROOT_INPUT, "org")
self.ROOT_IP = pjoin(self.ROOT_OUTPUT, "ip")
self.ROOT_OCR = pjoin(self.ROOT_OUTPUT, "ocr")
self.ROOT_MERGE = pjoin(self.ROOT_OUTPUT, "merge")
self.ROOT_IMG_COMPONENT = pjoin(self.ROOT_OUTPUT, "components")
if not os.path.exists(self.ROOT_IP):
os.mkdir(self.ROOT_IP)
if not os.path.exists(self.ROOT_OCR):
os.mkdir(self.ROOT_OCR)
if not os.path.exists(self.ROOT_MERGE):
os.mkdir(self.ROOT_MERGE)

View File

@@ -0,0 +1,49 @@
class Config:
def __init__(self):
# Adjustable
# self.THRESHOLD_PRE_GRADIENT = 4 # dribbble:4 rico:4 web:1
# self.THRESHOLD_OBJ_MIN_AREA = 55 # bottom line 55 of small circle
# self.THRESHOLD_BLOCK_GRADIENT = 5
# *** Frozen ***
self.THRESHOLD_REC_MIN_EVENNESS = 0.7
self.THRESHOLD_REC_MAX_DENT_RATIO = 0.25
self.THRESHOLD_LINE_THICKNESS = 8
self.THRESHOLD_LINE_MIN_LENGTH = 0.95
self.THRESHOLD_COMPO_MAX_SCALE = (0.25, 0.98) # (120/800, 422.5/450) maximum height and width ratio for a atomic compo (button)
self.THRESHOLD_TEXT_MAX_WORD_GAP = 10
self.THRESHOLD_TEXT_MAX_HEIGHT = 0.04 # 40/800 maximum height of text
self.THRESHOLD_TOP_BOTTOM_BAR = (0.045, 0.94) # (36/800, 752/800) height ratio of top and bottom bar
self.THRESHOLD_BLOCK_MIN_HEIGHT = 0.03 # 24/800
# deprecated
# self.THRESHOLD_OBJ_MIN_PERIMETER = 0
# self.THRESHOLD_BLOCK_MAX_BORDER_THICKNESS = 8
# self.THRESHOLD_BLOCK_MAX_CROSS_POINT = 0.1
# self.THRESHOLD_UICOMPO_MIN_W_H_RATIO = 0.4
# self.THRESHOLD_TEXT_MAX_WIDTH = 150
# self.THRESHOLD_LINE_MIN_LENGTH_H = 50
# self.THRESHOLD_LINE_MIN_LENGTH_V = 50
# self.OCR_PADDING = 5
# self.OCR_MIN_WORD_AREA = 0.45
# self.THRESHOLD_MIN_IOU = 0.1 # dribbble:0.003 rico:0.1 web:0.1
# self.THRESHOLD_BLOCK_MIN_EDGE_LENGTH = 210 # dribbble:68 rico:210 web:70
# self.THRESHOLD_UICOMPO_MAX_W_H_RATIO = 10 # dribbble:10 rico:10 web:22
self.CLASS_MAP = {'0':'Button', '1':'CheckBox', '2':'Chronometer', '3':'EditText', '4':'ImageButton', '5':'ImageView',
'6':'ProgressBar', '7':'RadioButton', '8':'RatingBar', '9':'SeekBar', '10':'Spinner', '11':'Switch',
'12':'ToggleButton', '13':'VideoView', '14':'TextView'}
self.COLOR = {'Button': (0, 255, 0), 'CheckBox': (0, 0, 255), 'Chronometer': (255, 166, 166),
'EditText': (255, 166, 0),
'ImageButton': (77, 77, 255), 'ImageView': (255, 0, 166), 'ProgressBar': (166, 0, 255),
'RadioButton': (166, 166, 166),
'RatingBar': (0, 166, 255), 'SeekBar': (0, 166, 10), 'Spinner': (50, 21, 255),
'Switch': (80, 166, 66), 'ToggleButton': (0, 66, 80), 'VideoView': (88, 66, 0),
'TextView': (169, 255, 0),
'Text':(169, 255, 0), 'Non-Text':(255, 0, 166),
'Noise':(6,6,255), 'Non-Noise': (6,255,6),
'Image':(255,6,6), 'Non-Image':(6,6,255)}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
UIED/data/demo/approach.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
UIED/data/demo/demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
UIED/data/input/0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 KiB

BIN
UIED/data/input/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
UIED/data/input/10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
UIED/data/input/100.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
UIED/data/input/11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
UIED/data/input/11300.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

BIN
UIED/data/input/1220.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
UIED/data/input/1565.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

BIN
UIED/data/input/1627.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

BIN
UIED/data/input/18116.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

BIN
UIED/data/input/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
UIED/data/input/214.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
UIED/data/input/24.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

BIN
UIED/data/input/245.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

BIN
UIED/data/input/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
UIED/data/input/30800.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

BIN
UIED/data/input/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

BIN
UIED/data/input/413.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
UIED/data/input/472.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
UIED/data/input/472a.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

BIN
UIED/data/input/493.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
UIED/data/input/497.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
UIED/data/input/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
UIED/data/input/505.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
UIED/data/input/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

BIN
UIED/data/input/66529.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

BIN
UIED/data/input/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

BIN
UIED/data/input/8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
UIED/data/input/9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

BIN
UIED/data/input/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
UIED/data/input/a.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
UIED/data/input/b.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
UIED/data/input/d.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
UIED/data/input/e.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
UIED/data/input/f.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
UIED/data/input/g.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
UIED/data/input/x.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
UIED/data/input/y.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
UIED/data/input/z.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@@ -0,0 +1,639 @@
{
"compos": [
{
"column_min": 20,
"row_max": 44,
"id": 1,
"column_max": 49,
"width": 29,
"height": 21,
"class": "Compo",
"row_min": 23
},
{
"column_min": 95,
"row_max": 45,
"id": 2,
"column_max": 126,
"width": 31,
"height": 22,
"class": "Compo",
"row_min": 23
},
{
"column_min": 398,
"row_max": 45,
"id": 3,
"column_max": 422,
"width": 24,
"height": 23,
"class": "Compo",
"row_min": 22
},
{
"column_min": 452,
"row_max": 45,
"id": 4,
"column_max": 487,
"width": 35,
"height": 22,
"class": "Compo",
"row_min": 23
},
{
"column_min": 346,
"row_max": 38,
"id": 5,
"column_max": 360,
"width": 14,
"height": 8,
"class": "Compo",
"row_min": 30
},
{
"column_min": 0,
"row_max": 282,
"id": 6,
"column_max": 498,
"width": 498,
"height": 215,
"class": "Compo",
"row_min": 67
},
{
"column_min": 135,
"row_max": 327,
"id": 7,
"column_max": 228,
"width": 93,
"height": 22,
"class": "Compo",
"row_min": 305
},
{
"column_min": 233,
"row_max": 327,
"id": 8,
"column_max": 293,
"width": 60,
"height": 22,
"class": "Compo",
"row_min": 305
},
{
"column_min": 17,
"row_max": 407,
"id": 9,
"column_max": 117,
"width": 100,
"height": 101,
"class": "Compo",
"row_min": 306
},
{
"column_min": 298,
"row_max": 333,
"id": 10,
"column_max": 365,
"width": 67,
"height": 25,
"class": "Compo",
"row_min": 308
},
{
"column_min": 368,
"row_max": 327,
"id": 11,
"column_max": 416,
"width": 48,
"height": 17,
"class": "Compo",
"row_min": 310
},
{
"column_min": 133,
"row_max": 357,
"id": 12,
"column_max": 265,
"width": 132,
"height": 23,
"class": "Compo",
"row_min": 334
},
{
"column_min": 269,
"row_max": 354,
"id": 13,
"column_max": 376,
"width": 107,
"height": 20,
"class": "Compo",
"row_min": 334
},
{
"column_min": 134,
"row_max": 385,
"id": 14,
"column_max": 173,
"width": 39,
"height": 20,
"class": "Compo",
"row_min": 365
},
{
"column_min": 177,
"row_max": 385,
"id": 15,
"column_max": 269,
"width": 92,
"height": 21,
"class": "Compo",
"row_min": 364
},
{
"column_min": 295,
"row_max": 380,
"id": 16,
"column_max": 332,
"width": 37,
"height": 16,
"class": "Compo",
"row_min": 364
},
{
"column_min": 334,
"row_max": 381,
"id": 17,
"column_max": 390,
"width": 56,
"height": 17,
"class": "Compo",
"row_min": 364
},
{
"column_min": 402,
"row_max": 380,
"id": 18,
"column_max": 436,
"width": 34,
"height": 16,
"class": "Compo",
"row_min": 364
},
{
"column_min": 273,
"row_max": 385,
"id": 19,
"column_max": 276,
"width": 3,
"height": 21,
"class": "Compo",
"row_min": 364
},
{
"column_min": 281,
"row_max": 380,
"id": 20,
"column_max": 291,
"width": 10,
"height": 14,
"class": "Compo",
"row_min": 366
},
{
"column_min": 394,
"row_max": 382,
"id": 21,
"column_max": 397,
"width": 3,
"height": 16,
"class": "Compo",
"row_min": 366
},
{
"column_min": 16,
"row_max": 547,
"id": 22,
"column_max": 117,
"width": 101,
"height": 103,
"class": "Compo",
"row_min": 444
},
{
"column_min": 132,
"row_max": 490,
"id": 23,
"column_max": 238,
"width": 106,
"height": 44,
"class": "Compo",
"row_min": 446
},
{
"column_min": 240,
"row_max": 469,
"id": 24,
"column_max": 346,
"width": 106,
"height": 23,
"class": "Compo",
"row_min": 446
},
{
"column_min": 351,
"row_max": 468,
"id": 25,
"column_max": 367,
"width": 16,
"height": 22,
"class": "Compo",
"row_min": 446
},
{
"column_min": 372,
"row_max": 465,
"id": 26,
"column_max": 385,
"width": 13,
"height": 15,
"class": "Compo",
"row_min": 450
},
{
"column_min": 354,
"row_max": 490,
"id": 27,
"column_max": 371,
"width": 17,
"height": 21,
"class": "Compo",
"row_min": 469
},
{
"column_min": 243,
"row_max": 494,
"id": 28,
"column_max": 348,
"width": 105,
"height": 23,
"class": "Compo",
"row_min": 471
},
{
"column_min": 376,
"row_max": 490,
"id": 29,
"column_max": 419,
"width": 43,
"height": 15,
"class": "Compo",
"row_min": 475
},
{
"column_min": 133,
"row_max": 520,
"id": 30,
"column_max": 203,
"width": 70,
"height": 23,
"class": "Compo",
"row_min": 497
},
{
"column_min": 134,
"row_max": 546,
"id": 31,
"column_max": 160,
"width": 26,
"height": 19,
"class": "Compo",
"row_min": 527
},
{
"column_min": 164,
"row_max": 547,
"id": 32,
"column_max": 232,
"width": 68,
"height": 20,
"class": "Compo",
"row_min": 527
},
{
"column_min": 236,
"row_max": 545,
"id": 33,
"column_max": 240,
"width": 4,
"height": 17,
"class": "Compo",
"row_min": 528
},
{
"column_min": 244,
"row_max": 545,
"id": 34,
"column_max": 254,
"width": 10,
"height": 19,
"class": "Compo",
"row_min": 526
},
{
"column_min": 258,
"row_max": 544,
"id": 35,
"column_max": 353,
"width": 95,
"height": 18,
"class": "Compo",
"row_min": 526
},
{
"column_min": 357,
"row_max": 546,
"id": 36,
"column_max": 361,
"width": 4,
"height": 19,
"class": "Compo",
"row_min": 527
},
{
"column_min": 364,
"row_max": 543,
"id": 37,
"column_max": 384,
"width": 20,
"height": 16,
"class": "Compo",
"row_min": 527
},
{
"column_min": 295,
"row_max": 602,
"id": 38,
"column_max": 324,
"width": 29,
"height": 22,
"class": "Compo",
"row_min": 580
},
{
"column_min": 16,
"row_max": 681,
"id": 39,
"column_max": 117,
"width": 101,
"height": 100,
"class": "Compo",
"row_min": 581
},
{
"column_min": 132,
"row_max": 604,
"id": 40,
"column_max": 171,
"width": 39,
"height": 21,
"class": "Compo",
"row_min": 583
},
{
"column_min": 177,
"row_max": 605,
"id": 41,
"column_max": 219,
"width": 42,
"height": 24,
"class": "Compo",
"row_min": 581
},
{
"column_min": 221,
"row_max": 608,
"id": 42,
"column_max": 291,
"width": 70,
"height": 21,
"class": "Compo",
"row_min": 587
},
{
"column_min": 328,
"row_max": 607,
"id": 43,
"column_max": 372,
"width": 44,
"height": 20,
"class": "Compo",
"row_min": 587
},
{
"column_min": 132,
"row_max": 632,
"id": 44,
"column_max": 228,
"width": 96,
"height": 24,
"class": "Compo",
"row_min": 608
},
{
"column_min": 230,
"row_max": 629,
"id": 45,
"column_max": 279,
"width": 49,
"height": 20,
"class": "Compo",
"row_min": 609
},
{
"column_min": 280,
"row_max": 628,
"id": 46,
"column_max": 310,
"width": 30,
"height": 20,
"class": "Compo",
"row_min": 608
},
{
"column_min": 133,
"row_max": 659,
"id": 47,
"column_max": 189,
"width": 56,
"height": 21,
"class": "Compo",
"row_min": 638
},
{
"column_min": 220,
"row_max": 659,
"id": 48,
"column_max": 232,
"width": 12,
"height": 23,
"class": "Compo",
"row_min": 636
},
{
"column_min": 244,
"row_max": 657,
"id": 49,
"column_max": 253,
"width": 9,
"height": 18,
"class": "Compo",
"row_min": 639
},
{
"column_min": 257,
"row_max": 656,
"id": 50,
"column_max": 352,
"width": 95,
"height": 18,
"class": "Compo",
"row_min": 638
},
{
"column_min": 193,
"row_max": 658,
"id": 51,
"column_max": 218,
"width": 25,
"height": 18,
"class": "Compo",
"row_min": 640
},
{
"column_min": 235,
"row_max": 657,
"id": 52,
"column_max": 239,
"width": 4,
"height": 16,
"class": "Compo",
"row_min": 641
},
{
"column_min": 363,
"row_max": 655,
"id": 53,
"column_max": 383,
"width": 20,
"height": 14,
"class": "Compo",
"row_min": 641
},
{
"column_min": 17,
"row_max": 799,
"id": 54,
"column_max": 116,
"width": 99,
"height": 80,
"class": "Compo",
"row_min": 719
},
{
"column_min": 132,
"row_max": 768,
"id": 55,
"column_max": 307,
"width": 175,
"height": 49,
"class": "Compo",
"row_min": 719
},
{
"column_min": 307,
"row_max": 770,
"id": 56,
"column_max": 423,
"width": 116,
"height": 50,
"class": "Compo",
"row_min": 720
},
{
"column_min": 135,
"row_max": 793,
"id": 57,
"column_max": 200,
"width": 65,
"height": 15,
"class": "Compo",
"row_min": 778
},
{
"column_min": 205,
"row_max": 793,
"id": 58,
"column_max": 228,
"width": 23,
"height": 15,
"class": "Compo",
"row_min": 778
},
{
"column_min": 232,
"row_max": 795,
"id": 59,
"column_max": 236,
"width": 4,
"height": 16,
"class": "Compo",
"row_min": 779
},
{
"column_min": 241,
"row_max": 793,
"id": 60,
"column_max": 250,
"width": 9,
"height": 15,
"class": "Compo",
"row_min": 778
},
{
"column_min": 254,
"row_max": 793,
"id": 61,
"column_max": 349,
"width": 95,
"height": 15,
"class": "Compo",
"row_min": 778
},
{
"column_min": 353,
"row_max": 795,
"id": 62,
"column_max": 357,
"width": 4,
"height": 16,
"class": "Compo",
"row_min": 779
},
{
"column_min": 361,
"row_max": 793,
"id": 63,
"column_max": 380,
"width": 19,
"height": 15,
"class": "Compo",
"row_min": 778
}
],
"img_shape": [
800,
499,
3
]
}

BIN
UIED/data/output/ip/497.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

1059
UIED/data/output/ip/497.json Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -0,0 +1,436 @@
{
"compos": [
{
"height": 21,
"width": 29,
"position": {
"column_min": 20,
"row_max": 44,
"column_max": 49,
"row_min": 23
},
"class": "Compo",
"id": 0
},
{
"height": 23,
"width": 24,
"position": {
"column_min": 398,
"row_max": 45,
"column_max": 422,
"row_min": 22
},
"class": "Compo",
"id": 1
},
{
"height": 22,
"width": 35,
"position": {
"column_min": 452,
"row_max": 45,
"column_max": 487,
"row_min": 23
},
"class": "Compo",
"id": 2
},
{
"height": 8,
"width": 14,
"position": {
"column_min": 346,
"row_max": 38,
"column_max": 360,
"row_min": 30
},
"class": "Compo",
"id": 3
},
{
"height": 215,
"width": 498,
"position": {
"column_min": 0,
"row_max": 282,
"column_max": 498,
"row_min": 67
},
"class": "Compo",
"id": 4
},
{
"height": 101,
"width": 100,
"position": {
"column_min": 17,
"row_max": 407,
"column_max": 117,
"row_min": 306
},
"class": "Compo",
"id": 5
},
{
"height": 21,
"width": 3,
"position": {
"column_min": 273,
"row_max": 385,
"column_max": 276,
"row_min": 364
},
"class": "Compo",
"id": 6
},
{
"height": 14,
"width": 10,
"position": {
"column_min": 281,
"row_max": 380,
"column_max": 291,
"row_min": 366
},
"class": "Compo",
"id": 7
},
{
"height": 103,
"width": 101,
"position": {
"column_min": 16,
"row_max": 547,
"column_max": 117,
"row_min": 444
},
"class": "Compo",
"id": 8
},
{
"height": 17,
"width": 4,
"position": {
"column_min": 236,
"row_max": 545,
"column_max": 240,
"row_min": 528
},
"class": "Compo",
"id": 9
},
{
"height": 19,
"width": 10,
"position": {
"column_min": 244,
"row_max": 545,
"column_max": 254,
"row_min": 526
},
"class": "Compo",
"id": 10
},
{
"height": 100,
"width": 101,
"position": {
"column_min": 16,
"row_max": 681,
"column_max": 117,
"row_min": 581
},
"class": "Compo",
"id": 11
},
{
"height": 18,
"width": 9,
"position": {
"column_min": 244,
"row_max": 657,
"column_max": 253,
"row_min": 639
},
"class": "Compo",
"id": 12
},
{
"height": 16,
"width": 4,
"position": {
"column_min": 235,
"row_max": 657,
"column_max": 239,
"row_min": 641
},
"class": "Compo",
"id": 13
},
{
"height": 80,
"width": 99,
"position": {
"column_min": 17,
"row_max": 799,
"column_max": 116,
"row_min": 719
},
"class": "Compo",
"id": 14
},
{
"position": {
"column_min": 96,
"row_max": 45,
"column_max": 124,
"row_min": 24
},
"class": "Text",
"text_content": "ALL",
"id": 15,
"width": 28,
"height": 21
},
{
"position": {
"column_min": 137,
"row_max": 329,
"column_max": 415,
"row_min": 304
},
"class": "Text",
"text_content": "Microsoft thinks people want",
"id": 16,
"width": 278,
"height": 25
},
{
"position": {
"column_min": 135,
"row_max": 357,
"column_max": 373,
"row_min": 328
},
"class": "Text",
"text_content": "ultra portable headaches",
"id": 17,
"width": 238,
"height": 29
},
{
"position": {
"column_min": 137,
"row_max": 382,
"column_max": 270,
"row_min": 363
},
"class": "Text",
"text_content": "Jerry Hildenbrand",
"id": 18,
"width": 133,
"height": 19
},
{
"position": {
"column_min": 297,
"row_max": 382,
"column_max": 435,
"row_min": 363
},
"class": "Text",
"text_content": "COMMENTS 57m",
"id": 19,
"width": 138,
"height": 19
},
{
"position": {
"column_min": 137,
"row_max": 470,
"column_max": 383,
"row_min": 443
},
"class": "Text",
"text_content": "My Disney Experience is a",
"id": 20,
"width": 246,
"height": 27
},
{
"position": {
"column_min": 136,
"row_max": 493,
"column_max": 415,
"row_min": 469
},
"class": "Text",
"text_content": "whole new experience in new",
"id": 21,
"width": 279,
"height": 24
},
{
"position": {
"column_min": 136,
"row_max": 518,
"column_max": 201,
"row_min": 494
},
"class": "Text",
"text_content": "update",
"id": 22,
"width": 65,
"height": 24
},
{
"position": {
"column_min": 136,
"row_max": 547,
"column_max": 231,
"row_min": 525
},
"class": "Text",
"text_content": "Ara Wagoner",
"id": 23,
"width": 95,
"height": 22
},
{
"position": {
"column_min": 259,
"row_max": 547,
"column_max": 383,
"row_min": 525
},
"class": "Text",
"text_content": "COMMENTS 3h",
"id": 24,
"width": 124,
"height": 22
},
{
"position": {
"column_min": 136,
"row_max": 605,
"column_max": 218,
"row_min": 578
},
"class": "Text",
"text_content": "The best",
"id": 25,
"width": 82,
"height": 27
},
{
"position": {
"column_min": 226,
"row_max": 630,
"column_max": 322,
"row_min": 579
},
"class": "Text",
"text_content": "games VR for Gear",
"id": 26,
"width": 96,
"height": 51
},
{
"position": {
"column_min": 329,
"row_max": 607,
"column_max": 370,
"row_min": 580
},
"class": "Text",
"text_content": "your",
"id": 27,
"width": 41,
"height": 27
},
{
"position": {
"column_min": 135,
"row_max": 632,
"column_max": 224,
"row_min": 605
},
"class": "Text",
"text_content": "Samsung",
"id": 28,
"width": 89,
"height": 27
},
{
"position": {
"column_min": 136,
"row_max": 660,
"column_max": 230,
"row_min": 637
},
"class": "Text",
"text_content": "Russell Holly",
"id": 29,
"width": 94,
"height": 23
},
{
"position": {
"column_min": 259,
"row_max": 660,
"column_max": 382,
"row_min": 637
},
"class": "Text",
"text_content": "COMMENTS 4h",
"id": 30,
"width": 123,
"height": 23
},
{
"position": {
"column_min": 137,
"row_max": 742,
"column_max": 421,
"row_min": 717
},
"class": "Text",
"text_content": "Here's how to get a little more",
"id": 31,
"width": 284,
"height": 25
},
{
"position": {
"column_min": 136,
"row_max": 768,
"column_max": 395,
"row_min": 743
},
"class": "Text",
"text_content": "Android Central in your life !",
"id": 32,
"width": 259,
"height": 25
},
{
"position": {
"column_min": 136,
"row_max": 796,
"column_max": 379,
"row_min": 776
},
"class": "Text",
"text_content": "Florence lon5 COMMENTS 5h",
"id": 33,
"width": 243,
"height": 20
}
],
"img_shape": [
800,
499,
3
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -0,0 +1,598 @@
{
"compos": [
{
"height": 39,
"class": "Compo",
"width": 369,
"id": 0,
"position": {
"row_max": 73,
"column_max": 379,
"column_min": 10,
"row_min": 34
}
},
{
"height": 62,
"class": "Compo",
"width": 67,
"id": 1,
"position": {
"row_max": 153,
"column_max": 88,
"column_min": 21,
"row_min": 91
}
},
{
"height": 12,
"class": "Compo",
"width": 9,
"id": 2,
"position": {
"row_max": 140,
"column_max": 165,
"column_min": 156,
"row_min": 128
}
},
{
"height": 66,
"class": "Compo",
"width": 67,
"id": 3,
"position": {
"row_max": 230,
"column_max": 88,
"column_min": 21,
"row_min": 164
}
},
{
"height": 12,
"class": "Compo",
"width": 9,
"id": 4,
"position": {
"row_max": 217,
"column_max": 165,
"column_min": 156,
"row_min": 205
}
},
{
"height": 66,
"class": "Compo",
"width": 67,
"id": 5,
"position": {
"row_max": 307,
"column_max": 88,
"column_min": 21,
"row_min": 241
}
},
{
"height": 15,
"class": "Compo",
"width": 16,
"id": 6,
"position": {
"row_max": 297,
"column_max": 109,
"column_min": 93,
"row_min": 282
}
},
{
"height": 14,
"class": "Compo",
"width": 10,
"id": 7,
"position": {
"row_max": 296,
"column_max": 166,
"column_min": 156,
"row_min": 282
}
},
{
"height": 69,
"class": "Compo",
"width": 69,
"id": 8,
"position": {
"row_max": 387,
"column_max": 89,
"column_min": 20,
"row_min": 318
}
},
{
"height": 12,
"class": "Compo",
"width": 15,
"id": 9,
"position": {
"row_max": 372,
"column_max": 108,
"column_min": 93,
"row_min": 360
}
},
{
"height": 13,
"class": "Compo",
"width": 12,
"id": 10,
"position": {
"row_max": 372,
"column_max": 162,
"column_min": 150,
"row_min": 359
}
},
{
"height": 68,
"class": "Compo",
"width": 68,
"id": 11,
"position": {
"row_max": 462,
"column_max": 88,
"column_min": 20,
"row_min": 394
}
},
{
"height": 12,
"class": "Compo",
"width": 11,
"id": 12,
"position": {
"row_max": 449,
"column_max": 161,
"column_min": 150,
"row_min": 437
}
},
{
"height": 69,
"class": "Compo",
"width": 68,
"id": 13,
"position": {
"row_max": 540,
"column_max": 88,
"column_min": 20,
"row_min": 471
}
},
{
"height": 13,
"class": "Compo",
"width": 15,
"id": 14,
"position": {
"row_max": 527,
"column_max": 108,
"column_min": 93,
"row_min": 514
}
},
{
"height": 13,
"class": "Compo",
"width": 11,
"id": 15,
"position": {
"row_max": 527,
"column_max": 158,
"column_min": 147,
"row_min": 514
}
},
{
"height": 67,
"class": "Compo",
"width": 68,
"id": 16,
"position": {
"row_max": 616,
"column_max": 88,
"column_min": 20,
"row_min": 549
}
},
{
"height": 12,
"class": "Compo",
"width": 11,
"id": 17,
"position": {
"row_max": 603,
"column_max": 158,
"column_min": 147,
"row_min": 591
}
},
{
"height": 67,
"class": "Compo",
"width": 68,
"id": 18,
"position": {
"row_max": 693,
"column_max": 88,
"column_min": 20,
"row_min": 626
}
},
{
"height": 11,
"class": "Compo",
"width": 15,
"id": 19,
"position": {
"row_max": 680,
"column_max": 108,
"column_min": 93,
"row_min": 669
}
},
{
"height": 12,
"class": "Compo",
"width": 11,
"id": 20,
"position": {
"row_max": 680,
"column_max": 161,
"column_min": 150,
"row_min": 668
}
},
{
"height": 16,
"class": "Compo",
"width": 66,
"id": 21,
"position": {
"row_max": 720,
"column_max": 87,
"column_min": 21,
"row_min": 704
}
},
{
"height": 18,
"text_content": "X X Cancel",
"id": 22,
"class": "Text",
"width": 94,
"position": {
"row_max": 62,
"column_max": 437,
"column_min": 343,
"row_min": 44
}
},
{
"height": 15,
"text_content": "Stuff You Should Know",
"id": 23,
"class": "Text",
"width": 161,
"position": {
"row_max": 114,
"column_max": 255,
"column_min": 94,
"row_min": 99
}
},
{
"height": 13,
"text_content": "+ 26.7k",
"id": 24,
"class": "Text",
"width": 32,
"position": {
"row_max": 140,
"column_max": 136,
"column_min": 104,
"row_min": 127
}
},
{
"height": 12,
"text_content": "665.Ok",
"id": 25,
"class": "Text",
"width": 30,
"position": {
"row_max": 139,
"column_max": 201,
"column_min": 171,
"row_min": 127
}
},
{
"height": 19,
"text_content": "Stuff You Missed in History Class",
"id": 26,
"class": "Text",
"width": 229,
"position": {
"row_max": 195,
"column_max": 323,
"column_min": 94,
"row_min": 176
}
},
{
"height": 13,
"text_content": "& + 13.7k",
"id": 27,
"class": "Text",
"width": 42,
"position": {
"row_max": 217,
"column_max": 136,
"column_min": 94,
"row_min": 204
}
},
{
"height": 12,
"text_content": "274.Ok",
"id": 28,
"class": "Text",
"width": 30,
"position": {
"row_max": 216,
"column_max": 201,
"column_min": 171,
"row_min": 204
}
},
{
"height": 15,
"text_content": "Stuff To Blow Your Mind",
"id": 29,
"class": "Text",
"width": 170,
"position": {
"row_max": 269,
"column_max": 264,
"column_min": 94,
"row_min": 254
}
},
{
"height": 13,
"text_content": "+ 12.7k",
"id": 30,
"class": "Text",
"width": 31,
"position": {
"row_max": 294,
"column_max": 136,
"column_min": 105,
"row_min": 281
}
},
{
"height": 12,
"text_content": "190.4k",
"id": 31,
"class": "Text",
"width": 29,
"position": {
"row_max": 294,
"column_max": 201,
"column_min": 172,
"row_min": 282
}
},
{
"height": 19,
"text_content": "Stuff They Don't Want You To Know Audio",
"id": 32,
"class": "Text",
"width": 296,
"position": {
"row_max": 349,
"column_max": 390,
"column_min": 94,
"row_min": 330
}
},
{
"height": 12,
"text_content": "+ 3.4k",
"id": 33,
"class": "Text",
"width": 26,
"position": {
"row_max": 371,
"column_max": 130,
"column_min": 104,
"row_min": 359
}
},
{
"height": 12,
"text_content": "72.2k",
"id": 34,
"class": "Text",
"width": 25,
"position": {
"row_max": 371,
"column_max": 190,
"column_min": 165,
"row_min": 359
}
},
{
"height": 16,
"text_content": "Stuff Mom Never Told You",
"id": 35,
"class": "Text",
"width": 185,
"position": {
"row_max": 424,
"column_max": 279,
"column_min": 94,
"row_min": 408
}
},
{
"height": 12,
"text_content": "2+ 2.Ok",
"id": 36,
"class": "Text",
"width": 37,
"position": {
"row_max": 448,
"column_max": 130,
"column_min": 93,
"row_min": 436
}
},
{
"height": 12,
"text_content": "48.5k",
"id": 37,
"class": "Text",
"width": 25,
"position": {
"row_max": 448,
"column_max": 190,
"column_min": 165,
"row_min": 436
}
},
{
"height": 17,
"text_content": "The Purple Stuff Podcast",
"id": 38,
"class": "Text",
"width": 175,
"position": {
"row_max": 502,
"column_max": 267,
"column_min": 92,
"row_min": 485
}
},
{
"height": 13,
"text_content": "+ 949",
"id": 39,
"class": "Text",
"width": 23,
"position": {
"row_max": 526,
"column_max": 128,
"column_min": 105,
"row_min": 513
}
},
{
"height": 12,
"text_content": "625.3k",
"id": 40,
"class": "Text",
"width": 29,
"position": {
"row_max": 525,
"column_max": 192,
"column_min": 163,
"row_min": 513
}
},
{
"height": 14,
"text_content": "Catholic Stuff You Should Know",
"id": 41,
"class": "Text",
"width": 222,
"position": {
"row_max": 577,
"column_max": 316,
"column_min": 94,
"row_min": 563
}
},
{
"height": 12,
"text_content": "2+ 683",
"id": 42,
"class": "Text",
"width": 34,
"position": {
"row_max": 602,
"column_max": 128,
"column_min": 94,
"row_min": 590
}
},
{
"height": 12,
"text_content": "8.8k",
"id": 43,
"class": "Text",
"width": 18,
"position": {
"row_max": 602,
"column_max": 181,
"column_min": 163,
"row_min": 590
}
},
{
"height": 18,
"text_content": "Stuff They Don't Want You To Know",
"id": 44,
"class": "Text",
"width": 251,
"position": {
"row_max": 657,
"column_max": 345,
"column_min": 94,
"row_min": 639
}
},
{
"height": 12,
"text_content": "+ 1.1k",
"id": 45,
"class": "Text",
"width": 26,
"position": {
"row_max": 679,
"column_max": 130,
"column_min": 104,
"row_min": 667
}
},
{
"height": 12,
"text_content": "5.4k",
"id": 46,
"class": "Text",
"width": 18,
"position": {
"row_max": 679,
"column_max": 184,
"column_min": 166,
"row_min": 667
}
}
],
"img_shape": [
800,
450,
3
]
}

View File

@@ -0,0 +1,239 @@
{
"img_shape": [
835,
521,
3
],
"texts": [
{
"content": "ALL",
"column_min": 101,
"row_max": 48,
"height": 22,
"column_max": 130,
"width": 29,
"id": 0,
"row_min": 26
},
{
"content": "ASK AC",
"column_min": 19,
"row_max": 241,
"height": 20,
"column_max": 78,
"width": 59,
"id": 1,
"row_min": 221
},
{
"content": "Do really need",
"column_min": 19,
"row_max": 281,
"height": 36,
"column_max": 208,
"width": 189,
"id": 2,
"row_min": 245
},
{
"content": "a \u2022 mesh 00000",
"column_min": 218,
"row_max": 292,
"height": 47,
"column_max": 306,
"width": 88,
"id": 3,
"row_min": 245
},
{
"content": "network ?",
"column_min": 316,
"row_max": 280,
"height": 37,
"column_max": 429,
"width": 113,
"id": 4,
"row_min": 243
},
{
"content": "Microsoft thinks people want",
"column_min": 143,
"row_max": 344,
"height": 26,
"column_max": 434,
"width": 291,
"id": 5,
"row_min": 318
},
{
"content": "ultra portable headaches",
"column_min": 141,
"row_max": 373,
"height": 30,
"column_max": 390,
"width": 249,
"id": 6,
"row_min": 343
},
{
"content": "Jerry Hildenbrand",
"column_min": 144,
"row_max": 399,
"height": 20,
"column_max": 282,
"width": 138,
"id": 7,
"row_min": 379
},
{
"content": "COMMENTS 57m",
"column_min": 310,
"row_max": 399,
"height": 20,
"column_max": 455,
"width": 145,
"id": 8,
"row_min": 379
},
{
"content": "My Disney Experience is a",
"column_min": 143,
"row_max": 491,
"height": 28,
"column_max": 400,
"width": 257,
"id": 9,
"row_min": 463
},
{
"content": "whole new experience in new",
"column_min": 142,
"row_max": 515,
"height": 25,
"column_max": 434,
"width": 292,
"id": 10,
"row_min": 490
},
{
"content": "update",
"column_min": 142,
"row_max": 541,
"height": 25,
"column_max": 210,
"width": 68,
"id": 11,
"row_min": 516
},
{
"content": "Ara Wagoner",
"column_min": 142,
"row_max": 571,
"height": 23,
"column_max": 242,
"width": 100,
"id": 12,
"row_min": 548
},
{
"content": "COMMENTS 3h",
"column_min": 271,
"row_max": 571,
"height": 23,
"column_max": 400,
"width": 129,
"id": 13,
"row_min": 548
},
{
"content": "The best",
"column_min": 142,
"row_max": 632,
"height": 28,
"column_max": 228,
"width": 86,
"id": 14,
"row_min": 604
},
{
"content": "games VR for Gear",
"column_min": 236,
"row_max": 658,
"height": 53,
"column_max": 337,
"width": 101,
"id": 15,
"row_min": 605
},
{
"content": "your",
"column_min": 344,
"row_max": 634,
"height": 28,
"column_max": 387,
"width": 43,
"id": 16,
"row_min": 606
},
{
"content": "Samsung",
"column_min": 141,
"row_max": 660,
"height": 28,
"column_max": 234,
"width": 93,
"id": 17,
"row_min": 632
},
{
"content": "Russell Holly",
"column_min": 142,
"row_max": 689,
"height": 24,
"column_max": 241,
"width": 99,
"id": 18,
"row_min": 665
},
{
"content": "COMMENTS 4h",
"column_min": 271,
"row_max": 689,
"height": 24,
"column_max": 399,
"width": 128,
"id": 19,
"row_min": 665
},
{
"content": "Here's how to get a little more",
"column_min": 143,
"row_max": 775,
"height": 26,
"column_max": 440,
"width": 297,
"id": 20,
"row_min": 749
},
{
"content": "Android Central in your life !",
"column_min": 142,
"row_max": 802,
"height": 26,
"column_max": 413,
"width": 271,
"id": 21,
"row_min": 776
},
{
"content": "Florence lon5 COMMENTS 5h",
"column_min": 142,
"row_max": 831,
"height": 21,
"column_max": 396,
"width": 254,
"id": 22,
"row_min": 810
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

View File

@@ -0,0 +1,489 @@
{
"texts": [
{
"height": 12,
"row_max": 23,
"content": "FAK",
"id": 0,
"width": 31,
"column_max": 373,
"row_min": 11,
"column_min": 342
},
{
"height": 41,
"row_max": 51,
"content": "fo",
"id": 1,
"width": 96,
"column_max": 188,
"row_min": 10,
"column_min": 92
},
{
"height": 31,
"row_max": 47,
"content": "3:17",
"id": 2,
"width": 68,
"column_max": 1055,
"row_min": 16,
"column_min": 987
},
{
"height": 33,
"row_max": 143,
"content": "stuff",
"id": 3,
"width": 77,
"column_max": 249,
"row_min": 110,
"column_min": 172
},
{
"height": 45,
"row_max": 151,
"content": "X X Cancel",
"id": 4,
"width": 225,
"column_max": 1049,
"row_min": 106,
"column_min": 824
},
{
"height": 36,
"row_max": 275,
"content": "Stuff You Should Know",
"id": 5,
"width": 387,
"column_max": 613,
"row_min": 239,
"column_min": 226
},
{
"height": 18,
"row_max": 272,
"content": "STUFF",
"id": 6,
"width": 54,
"column_max": 119,
"row_min": 254,
"column_min": 65
},
{
"height": 17,
"row_max": 291,
"content": "YOU SHOULD",
"id": 7,
"width": 127,
"column_max": 193,
"row_min": 274,
"column_min": 66
},
{
"height": 18,
"row_max": 311,
"content": "KNOW",
"id": 8,
"width": 65,
"column_max": 132,
"row_min": 293,
"column_min": 67
},
{
"height": 30,
"row_max": 336,
"content": "+ 26.7k",
"id": 9,
"width": 77,
"column_max": 327,
"row_min": 306,
"column_min": 250
},
{
"height": 29,
"row_max": 335,
"content": "665.Ok",
"id": 10,
"width": 71,
"column_max": 483,
"row_min": 306,
"column_min": 412
},
{
"height": 16,
"row_max": 343,
"content": "PODCAST",
"id": 11,
"width": 80,
"column_max": 146,
"row_min": 327,
"column_min": 66
},
{
"height": 47,
"row_max": 470,
"content": "Stuff You Missed in History Class",
"id": 12,
"width": 550,
"column_max": 776,
"row_min": 423,
"column_min": 226
},
{
"height": 18,
"row_max": 482,
"content": "STUFF",
"id": 13,
"width": 32,
"column_max": 186,
"row_min": 464,
"column_min": 154
},
{
"height": 31,
"row_max": 522,
"content": "& + 13.7k",
"id": 14,
"width": 101,
"column_max": 328,
"row_min": 491,
"column_min": 227
},
{
"height": 29,
"row_max": 520,
"content": "274.Ok",
"id": 15,
"width": 71,
"column_max": 483,
"row_min": 491,
"column_min": 412
},
{
"height": 12,
"row_max": 520,
"content": "YOU MISSED IN",
"id": 16,
"width": 79,
"column_max": 141,
"row_min": 508,
"column_min": 62
},
{
"height": 20,
"row_max": 539,
"content": "HISTORY CLASS",
"id": 17,
"width": 143,
"column_max": 204,
"row_min": 519,
"column_min": 61
},
{
"height": 43,
"row_max": 654,
"content": "BLOW stuff YOUR to",
"id": 18,
"width": 79,
"column_max": 198,
"row_min": 611,
"column_min": 119
},
{
"height": 35,
"row_max": 646,
"content": "Stuff To Blow Your Mind",
"id": 19,
"width": 408,
"column_max": 634,
"row_min": 611,
"column_min": 226
},
{
"height": 32,
"row_max": 686,
"content": "MIND",
"id": 20,
"width": 98,
"column_max": 196,
"row_min": 654,
"column_min": 98
},
{
"height": 30,
"row_max": 706,
"content": "+ 12.7k",
"id": 21,
"width": 75,
"column_max": 327,
"row_min": 676,
"column_min": 252
},
{
"height": 29,
"row_max": 707,
"content": "190.4k",
"id": 22,
"width": 70,
"column_max": 483,
"row_min": 678,
"column_min": 413
},
{
"height": 68,
"row_max": 869,
"content": "AUDIO STUFF DON THEY WANT KNOW YOU TO",
"id": 23,
"width": 94,
"column_max": 187,
"row_min": 801,
"column_min": 93
},
{
"height": 44,
"row_max": 838,
"content": "Stuff They Don't Want You To Know Audio",
"id": 24,
"width": 711,
"column_max": 937,
"row_min": 794,
"column_min": 226
},
{
"height": 29,
"row_max": 891,
"content": "+ 3.4k",
"id": 25,
"width": 63,
"column_max": 313,
"row_min": 862,
"column_min": 250
},
{
"height": 29,
"row_max": 892,
"content": "72.2k",
"id": 26,
"width": 58,
"column_max": 456,
"row_min": 863,
"column_min": 398
},
{
"height": 13,
"row_max": 1003,
"content": "stuff mom",
"id": 27,
"width": 75,
"column_max": 157,
"row_min": 990,
"column_min": 82
},
{
"height": 55,
"row_max": 1056,
"content": "never told you",
"id": 28,
"width": 133,
"column_max": 198,
"row_min": 1001,
"column_min": 65
},
{
"height": 38,
"row_max": 1019,
"content": "Stuff Mom Never Told You",
"id": 29,
"width": 445,
"column_max": 671,
"row_min": 981,
"column_min": 226
},
{
"height": 18,
"row_max": 1072,
"content": "audio",
"id": 30,
"width": 44,
"column_max": 191,
"row_min": 1054,
"column_min": 147
},
{
"height": 29,
"row_max": 1076,
"content": "2+ 2.Ok",
"id": 31,
"width": 88,
"column_max": 313,
"row_min": 1047,
"column_min": 225
},
{
"height": 28,
"row_max": 1076,
"content": "48.5k",
"id": 32,
"width": 58,
"column_max": 456,
"row_min": 1048,
"column_min": 398
},
{
"height": 44,
"row_max": 1182,
"content": "PURPLE PODCAST STUFF",
"id": 33,
"width": 137,
"column_max": 204,
"row_min": 1138,
"column_min": 67
},
{
"height": 42,
"row_max": 1206,
"content": "The Purple Stuff Podcast",
"id": 34,
"width": 418,
"column_max": 641,
"row_min": 1164,
"column_min": 223
},
{
"height": 31,
"row_max": 1263,
"content": "+ 949",
"id": 35,
"width": 56,
"column_max": 308,
"row_min": 1232,
"column_min": 252
},
{
"height": 28,
"row_max": 1261,
"content": "625.3k",
"id": 36,
"width": 70,
"column_max": 463,
"row_min": 1233,
"column_min": 393
},
{
"height": 154,
"row_max": 1470,
"content": "CATHO . YOU SHOULO KNOW STUFF .",
"id": 37,
"width": 159,
"column_max": 206,
"row_min": 1316,
"column_min": 47
},
{
"height": 33,
"row_max": 1385,
"content": "Catholic Stuff You Should Know",
"id": 38,
"width": 533,
"column_max": 759,
"row_min": 1352,
"column_min": 226
},
{
"height": 28,
"row_max": 1446,
"content": "2+ 683",
"id": 39,
"width": 81,
"column_max": 308,
"row_min": 1418,
"column_min": 227
},
{
"height": 28,
"row_max": 1446,
"content": "8.8k",
"id": 40,
"width": 43,
"column_max": 436,
"row_min": 1418,
"column_min": 393
},
{
"height": 14,
"row_max": 1550,
"content": "STUFE",
"id": 41,
"width": 42,
"column_max": 152,
"row_min": 1536,
"column_min": 110
},
{
"height": 45,
"row_max": 1593,
"content": "THEY DONT WANT YOU TO KNOW",
"id": 42,
"width": 75,
"column_max": 166,
"row_min": 1548,
"column_min": 91
},
{
"height": 45,
"row_max": 1579,
"content": "Stuff They Don't Want You To Know",
"id": 43,
"width": 604,
"column_max": 830,
"row_min": 1534,
"column_min": 226
},
{
"height": 29,
"row_max": 1631,
"content": "+ 1.1k",
"id": 44,
"width": 63,
"column_max": 314,
"row_min": 1602,
"column_min": 251
},
{
"height": 28,
"row_max": 1631,
"content": "5.4k",
"id": 45,
"width": 43,
"column_max": 442,
"row_min": 1603,
"column_min": 399
},
{
"height": 22,
"row_max": 1660,
"content": "howstuffworks.com",
"id": 46,
"width": 143,
"column_max": 204,
"row_min": 1638,
"column_min": 61
},
{
"height": 16,
"row_max": 1724,
"content": "THE",
"id": 47,
"width": 38,
"column_max": 152,
"row_min": 1708,
"column_min": 114
}
],
"img_shape": [
1920,
1080,
3
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 KiB

View File

@@ -0,0 +1,56 @@
import cv2
from os.path import join as pjoin
import time
import numpy as np
from detect_compo.lib_ip.Component import Component
from config.CONFIG_UIED import Config
C = Config()
class Block(Component):
def __init__(self, region, image_shape):
super().__init__(region, image_shape)
self.category = 'Block'
self.parent = None
self.children = []
self.uicompo_ = None
self.top_or_botm = None
self.redundant = False
def block_is_uicompo(self, image_shape, max_compo_scale):
'''
Check the if the block is a ui component according to its relative size
'''
row, column = image_shape[:2]
# print(height, height / row, max_compo_scale[0], height / row > max_compo_scale[0])
# draw.draw_bounding_box(org, [corner], show=True)
# ignore atomic components
if self.bbox.height / row > max_compo_scale[0] or self.bbox.width / column > max_compo_scale[1]:
return False
return True
def block_is_top_or_bottom_bar(self, image_shape, top_bottom_height):
'''
Check if the block is top bar or bottom bar
'''
height, width = image_shape[:2]
(column_min, row_min, column_max, row_max) = self.bbox.put_bbox()
if column_min < 5 and row_min < 5 and \
width - column_max < 5 and row_max < height * top_bottom_height[0]:
self.uicompo_ = True
return True
if column_min < 5 and row_min > height * top_bottom_height[1] and \
width - column_max < 5 and height - row_max < 5:
self.uicompo_ = True
return True
return False
def block_erase_from_bin(self, binary, pad):
(column_min, row_min, column_max, row_max) = self.put_bbox()
column_min = max(column_min - pad, 0)
column_max = min(column_max + pad, binary.shape[1])
row_min = max(row_min - pad, 0)
row_max = min(row_max + pad, binary.shape[0])
cv2.rectangle(binary, (column_min, row_min), (column_max, row_max), (0), -1)

View File

@@ -0,0 +1,108 @@
import cv2
import numpy as np
from random import randint as rint
import time
import detect_compo.lib_ip.ip_preprocessing as pre
import detect_compo.lib_ip.ip_detection as det
import detect_compo.lib_ip.ip_draw as draw
import detect_compo.lib_ip.ip_segment as seg
from detect_compo.lib_ip.Block import Block
from config.CONFIG_UIED import Config
C = Config()
def block_hierarchy(blocks):
for i in range(len(blocks) - 1):
for j in range(i + 1, len(blocks)):
relation = blocks[i].compo_relation(blocks[j])
if relation == -1:
blocks[j].children.append(i)
if relation == 1:
blocks[i].children.append(j)
return
def block_bin_erase_all_blk(binary, blocks, pad=0, show=False):
'''
erase the block parts from the binary map
:param binary: binary map of original image
:param blocks_corner: corners of detected layout block
:param show: show or not
:param pad: expand the bounding boxes of blocks
:return: binary map without block parts
'''
bin_org = binary.copy()
for block in blocks:
block.block_erase_from_bin(binary, pad)
if show:
cv2.imshow('before', bin_org)
cv2.imshow('after', binary)
cv2.waitKey()
def block_division(grey, org, grad_thresh,
show=False, write_path=None,
step_h=10, step_v=10,
line_thickness=C.THRESHOLD_LINE_THICKNESS,
min_rec_evenness=C.THRESHOLD_REC_MIN_EVENNESS,
max_dent_ratio=C.THRESHOLD_REC_MAX_DENT_RATIO,
min_block_height_ratio=C.THRESHOLD_BLOCK_MIN_HEIGHT):
'''
:param grey: grey-scale of original image
:return: corners: list of [(top_left, bottom_right)]
-> top_left: (column_min, row_min)
-> bottom_right: (column_max, row_max)
'''
blocks = []
mask = np.zeros((grey.shape[0]+2, grey.shape[1]+2), dtype=np.uint8)
broad = np.zeros((grey.shape[0], grey.shape[1], 3), dtype=np.uint8)
broad_all = broad.copy()
row, column = grey.shape[0], grey.shape[1]
for x in range(0, row, step_h):
for y in range(0, column, step_v):
if mask[x, y] == 0:
# region = flood_fill_bfs(grey, x, y, mask)
# flood fill algorithm to get background (layout block)
mask_copy = mask.copy()
ff = cv2.floodFill(grey, mask, (y, x), None, grad_thresh, grad_thresh, cv2.FLOODFILL_MASK_ONLY)
# ignore small regions
if ff[0] < 500: continue
mask_copy = mask - mask_copy
region = np.reshape(cv2.findNonZero(mask_copy[1:-1, 1:-1]), (-1, 2))
region = [(p[1], p[0]) for p in region]
block = Block(region, grey.shape)
# draw.draw_region(region, broad_all)
# if block.height < 40 and block.width < 40:
# continue
if block.height < 30:
continue
# print(block.area / (row * column))
if block.area / (row * column) > 0.9:
continue
elif block.area / (row * column) > 0.7:
block.redundant = True
# get the boundary of this region
# ignore lines
if block.compo_is_line(line_thickness):
continue
# ignore non-rectangle as blocks must be rectangular
if not block.compo_is_rectangle(min_rec_evenness, max_dent_ratio):
continue
# if block.height/row < min_block_height_ratio:
# continue
blocks.append(block)
# draw.draw_region(region, broad)
if show:
cv2.imshow('flood-fill all', broad_all)
cv2.imshow('block', broad)
cv2.waitKey()
if write_path is not None:
cv2.imwrite(write_path, broad)
return blocks

View File

@@ -0,0 +1,461 @@
import numpy as np
import cv2
from collections import Counter
import lib_ip.ip_draw as draw
from config.CONFIG_UIED import Config
C = Config()
# detect object(connected region)
# def boundary_bfs_connected_area(img, x, y, mark):
# def neighbor(img, x, y, mark, stack):
# for i in range(x - 1, x + 2):
# if i < 0 or i >= img.shape[0]: continue
# for j in range(y - 1, y + 2):
# if j < 0 or j >= img.shape[1]: continue
# if img[i, j] == 255 and mark[i, j] == 0:
# stack.append([i, j])
# mark[i, j] = 255
#
# stack = [[x, y]] # points waiting for inspection
# area = [[x, y]] # points of this area
# mark[x, y] = 255 # drawing broad
#
# while len(stack) > 0:
# point = stack.pop()
# area.append(point)
# neighbor(img, point[0], point[1], mark, stack)
# return area
# def line_check_perpendicular(lines_h, lines_v, max_thickness):
# """
# lines: [line_h, line_v]
# -> line_h: horizontal {'head':(column_min, row), 'end':(column_max, row), 'thickness':int)
# -> line_v: vertical {'head':(column, row_min), 'end':(column, row_max), 'thickness':int}
# """
# is_per_h = np.full(len(lines_h), False)
# is_per_v = np.full(len(lines_v), False)
# for i in range(len(lines_h)):
# # save the intersection point of h
# lines_h[i]['inter_point'] = set()
# h = lines_h[i]
#
# for j in range(len(lines_v)):
# # save the intersection point of v
# if 'inter_point' not in lines_v[j]: lines_v[j]['inter_point'] = set()
# v = lines_v[j]
#
# # if h is perpendicular to v in head of v
# if abs(h['head'][1]-v['head'][1]) <= max_thickness:
# if abs(h['head'][0] - v['head'][0]) <= max_thickness:
# lines_h[i]['inter_point'].add('head')
# lines_v[j]['inter_point'].add('head')
# is_per_h[i] = True
# is_per_v[j] = True
# elif abs(h['end'][0] - v['head'][0]) <= max_thickness:
# lines_h[i]['inter_point'].add('end')
# lines_v[j]['inter_point'].add('head')
# is_per_h[i] = True
# is_per_v[j] = True
#
# # if h is perpendicular to v in end of v
# elif abs(h['head'][1]-v['end'][1]) <= max_thickness:
# if abs(h['head'][0] - v['head'][0]) <= max_thickness:
# lines_h[i]['inter_point'].add('head')
# lines_v[j]['inter_point'].add('end')
# is_per_h[i] = True
# is_per_v[j] = True
# elif abs(h['end'][0] - v['head'][0]) <= max_thickness:
# lines_h[i]['inter_point'].add('end')
# lines_v[j]['inter_point'].add('end')
# is_per_h[i] = True
# is_per_v[j] = True
# per_h = []
# per_v = []
# for i in range(len(is_per_h)):
# if is_per_h[i]:
# lines_h[i]['inter_point'] = list(lines_h[i]['inter_point'])
# per_h.append(lines_h[i])
# for i in range(len(is_per_v)):
# if is_per_v[i]:
# lines_v[i]['inter_point'] = list(lines_v[i]['inter_point'])
# per_v.append(lines_v[i])
# return per_h, per_v
# def line_shrink_corners(corner, lines_h, lines_v):
# """
# shrink the corner according to lines:
# col_min_shrink: shrink right (increase)
# col_max_shrink: shrink left (decrease)
# row_min_shrink: shrink down (increase)
# row_max_shrink: shrink up (decrease)
# :param lines_h: horizontal {'head':(column_min, row), 'end':(column_max, row), 'thickness':int)
# :param lines_v: vertical {'head':(column, row_min), 'end':(column, row_max), 'thickness':int}
# :return: shrunken corner: (top_left, bottom_right)
# """
# (col_min, row_min), (col_max, row_max) = corner
# col_min_shrink, row_min_shrink = col_min, row_min
# col_max_shrink, row_max_shrink = col_max, row_max
# valid_frame = False
#
# for h in lines_h:
# # ignore outer border
# if len(h['inter_point']) == 2:
# valid_frame = True
# continue
# # shrink right -> col_min move to end
# if h['inter_point'][0] == 'head':
# col_min_shrink = max(h['end'][0], col_min_shrink)
# # shrink left -> col_max move to head
# elif h['inter_point'][0] == 'end':
# col_max_shrink = min(h['head'][0], col_max_shrink)
#
# for v in lines_v:
# # ignore outer border
# if len(v['inter_point']) == 2:
# valid_frame = True
# continue
# # shrink down -> row_min move to end
# if v['inter_point'][0] == 'head':
# row_min_shrink = max(v['end'][1], row_min_shrink)
# # shrink up -> row_max move to head
# elif v['inter_point'][0] == 'end':
# row_max_shrink = min(v['head'][1], row_max_shrink)
#
# # return the shrunken corner if only there is line intersecting with two other lines
# if valid_frame:
# return (col_min_shrink, row_min_shrink), (col_max_shrink, row_max_shrink)
# return corner
# def line_cvt_relative_position(col_min, row_min, lines_h, lines_v):
# """
# convert the relative position of lines in the entire image
# :param col_min: based column the img lines belong to
# :param row_min: based row the img lines belong to
# :param lines_h: horizontal {'head':(column_min, row), 'end':(column_max, row), 'thickness':int)
# :param lines_v: vertical {'head':(column, row_min), 'end':(column, row_max), 'thickness':int}
# :return: lines_h_cvt, lines_v_cvt
# """
# for h in lines_h:
# h['head'][0] += col_min
# h['head'][1] += row_min
# h['end'][0] += col_min
# h['end'][1] += row_min
# for v in lines_v:
# v['head'][0] += col_min
# v['head'][1] += row_min
# v['end'][0] += col_min
# v['end'][1] += row_min
#
# return lines_h, lines_v
# check if an object is so slim
# @boundary: [border_up, border_bottom, border_left, border_right]
# -> up, bottom: (column_index, min/max row border)
# -> left, right: (row_index, min/max column border) detect range of each row
def clipping_by_line(boundary, boundary_rec, lines):
boundary = boundary.copy()
for orient in lines:
# horizontal
if orient == 'h':
# column range of sub area
r1, r2 = 0, 0
for line in lines[orient]:
if line[0] == 0:
r1 = line[1]
continue
r2 = line[0]
b_top = []
b_bottom = []
for i in range(len(boundary[0])):
if r2 > boundary[0][i][0] >= r1:
b_top.append(boundary[0][i])
for i in range(len(boundary[1])):
if r2 > boundary[1][i][0] >= r1:
b_bottom.append(boundary[1][i])
b_left = [x for x in boundary[2]] # (row_index, min column border)
for i in range(len(b_left)):
if b_left[i][1] < r1:
b_left[i][1] = r1
b_right = [x for x in boundary[3]] # (row_index, max column border)
for i in range(len(b_right)):
if b_right[i][1] > r2:
b_right[i][1] = r2
boundary_rec.append([b_top, b_bottom, b_left, b_right])
r1 = line[1]
# remove imgs that contain text
# def rm_text(org, corners, compo_class,
# max_text_height=C.THRESHOLD_TEXT_MAX_HEIGHT, max_text_width=C.THRESHOLD_TEXT_MAX_WIDTH,
# ocr_padding=C.OCR_PADDING, ocr_min_word_area=C.OCR_MIN_WORD_AREA, show=False):
# """
# Remove area that full of text
# :param org: original image
# :param corners: [(top_left, bottom_right)]
# -> top_left: (column_min, row_min)
# -> bottom_right: (column_max, row_max)
# :param compo_class: classes of corners
# :param max_text_height: Too large to be text
# :param max_text_width: Too large to be text
# :param ocr_padding: Padding for clipping
# :param ocr_min_word_area: If too text area ratio is too large
# :param show: Show or not
# :return: corners without text objects
# """
# new_corners = []
# new_class = []
# for i in range(len(corners)):
# corner = corners[i]
# (top_left, bottom_right) = corner
# (col_min, row_min) = top_left
# (col_max, row_max) = bottom_right
# height = row_max - row_min
# width = col_max - col_min
# # highly likely to be block or img if too large
# if height > max_text_height and width > max_text_width:
# new_corners.append(corner)
# new_class.append(compo_class[i])
# else:
# row_min = row_min - ocr_padding if row_min - ocr_padding >= 0 else 0
# row_max = row_max + ocr_padding if row_max + ocr_padding < org.shape[0] else org.shape[0]
# col_min = col_min - ocr_padding if col_min - ocr_padding >= 0 else 0
# col_max = col_max + ocr_padding if col_max + ocr_padding < org.shape[1] else org.shape[1]
# # check if this area is text
# clip = org[row_min: row_max, col_min: col_max]
# if not ocr.is_text(clip, ocr_min_word_area, show=show):
# new_corners.append(corner)
# new_class.append(compo_class[i])
# return new_corners, new_class
# def rm_img_in_compo(corners_img, corners_compo):
# """
# Remove imgs in component
# """
# corners_img_new = []
# for img in corners_img:
# is_nested = False
# for compo in corners_compo:
# if util.corner_relation(img, compo) == -1:
# is_nested = True
# break
# if not is_nested:
# corners_img_new.append(img)
# return corners_img_new
# def block_or_compo(org, binary, corners,
# max_thickness=C.THRESHOLD_BLOCK_MAX_BORDER_THICKNESS, max_block_cross_points=C.THRESHOLD_BLOCK_MAX_CROSS_POINT,
# min_compo_w_h_ratio=C.THRESHOLD_UICOMPO_MIN_W_H_RATIO, max_compo_w_h_ratio=C.THRESHOLD_UICOMPO_MAX_W_H_RATIO,
# min_block_edge=C.THRESHOLD_BLOCK_MIN_EDGE_LENGTH):
# """
# Check if the objects are img components or just block
# :param org: Original image
# :param binary: Binary image from pre-processing
# :param corners: [(top_left, bottom_right)]
# -> top_left: (column_min, row_min)
# -> bottom_right: (column_max, row_max)
# :param max_thickness: The max thickness of border of blocks
# :param max_block_cross_points: Ratio of point of interaction
# :return: corners of blocks and imgs
# """
# blocks = []
# imgs = []
# compos = []
# for corner in corners:
# (top_left, bottom_right) = corner
# (col_min, row_min) = top_left
# (col_max, row_max) = bottom_right
# height = row_max - row_min
# width = col_max - col_min
#
# block = False
# vacancy = [0, 0, 0, 0]
# for i in range(1, max_thickness):
# try:
# # top to bottom
# if vacancy[0] == 0 and (col_max - col_min - 2 * i) is not 0 and (
# np.sum(binary[row_min + i, col_min + i: col_max - i]) / 255) / (col_max - col_min - 2 * i) <= max_block_cross_points:
# vacancy[0] = 1
# # bottom to top
# if vacancy[1] == 0 and (col_max - col_min - 2 * i) is not 0 and (
# np.sum(binary[row_max - i, col_min + i: col_max - i]) / 255) / (col_max - col_min - 2 * i) <= max_block_cross_points:
# vacancy[1] = 1
# # left to right
# if vacancy[2] == 0 and (row_max - row_min - 2 * i) is not 0 and (
# np.sum(binary[row_min + i: row_max - i, col_min + i]) / 255) / (row_max - row_min - 2 * i) <= max_block_cross_points:
# vacancy[2] = 1
# # right to left
# if vacancy[3] == 0 and (row_max - row_min - 2 * i) is not 0 and (
# np.sum(binary[row_min + i: row_max - i, col_max - i]) / 255) / (row_max - row_min - 2 * i) <= max_block_cross_points:
# vacancy[3] = 1
# if np.sum(vacancy) == 4:
# block = True
# except:
# pass
#
# # too big to be UI components
# if block:
# if height > min_block_edge and width > min_block_edge:
# blocks.append(corner)
# else:
# if min_compo_w_h_ratio < width / height < max_compo_w_h_ratio:
# compos.append(corner)
# # filter out small objects
# else:
# if height > min_block_edge:
# imgs.append(corner)
# else:
# if min_compo_w_h_ratio < width / height < max_compo_w_h_ratio:
# compos.append(corner)
# return blocks, imgs, compos
# def compo_on_img(processing, org, binary, clf,
# compos_corner, compos_class):
# """
# Detect potential UI components inner img;
# Only leave non-img
# """
# pad = 2
# for i in range(len(compos_corner)):
# if compos_class[i] != 'img':
# continue
# ((col_min, row_min), (col_max, row_max)) = compos_corner[i]
# col_min = max(col_min - pad, 0)
# col_max = min(col_max + pad, org.shape[1])
# row_min = max(row_min - pad, 0)
# row_max = min(row_max + pad, org.shape[0])
# area = (col_max - col_min) * (row_max - row_min)
# if area < 600:
# continue
#
# clip_org = org[row_min:row_max, col_min:col_max]
# clip_bin_inv = pre.reverse_binary(binary[row_min:row_max, col_min:col_max])
#
# compos_boundary_new, compos_corner_new, compos_class_new = processing(clip_org, clip_bin_inv, clf)
# compos_corner_new = util.corner_cvt_relative_position(compos_corner_new, col_min, row_min)
#
# assert len(compos_corner_new) == len(compos_class_new)
#
# # only leave non-img elements
# for i in range(len(compos_corner_new)):
# ((col_min_new, row_min_new), (col_max_new, row_max_new)) = compos_corner_new[i]
# area_new = (col_max_new - col_min_new) * (row_max_new - row_min_new)
# if compos_class_new[i] != 'img' and area_new / area < 0.8:
# compos_corner.append(compos_corner_new[i])
# compos_class.append(compos_class_new[i])
#
# return compos_corner, compos_class
# def strip_img(corners_compo, compos_class, corners_img):
# """
# Separate img from other compos
# :return: compos without img
# """
# corners_compo_withuot_img = []
# compo_class_withuot_img = []
# for i in range(len(compos_class)):
# if compos_class[i] == 'img':
# corners_img.append(corners_compo[i])
# else:
# corners_compo_withuot_img.append(corners_compo[i])
# compo_class_withuot_img.append(compos_class[i])
# return corners_compo_withuot_img, compo_class_withuot_img
# def merge_corner(corners, compos_class, min_selected_IoU=C.THRESHOLD_MIN_IOU, is_merge_nested_same=True):
# """
# Calculate the Intersection over Overlap (IoU) and merge corners according to the value of IoU
# :param is_merge_nested_same: if true, merge the nested corners with same class whatever the IoU is
# :param corners: corners: [(top_left, bottom_right)]
# -> top_left: (column_min, row_min)
# -> bottom_right: (column_max, row_max)
# :return: new corners
# """
# new_corners = []
# new_class = []
# for i in range(len(corners)):
# is_intersected = False
# for j in range(len(new_corners)):
# r = util.corner_relation_nms(corners[i], new_corners[j], min_selected_IoU)
# # r = util.corner_relation(corners[i], new_corners[j])
# if is_merge_nested_same:
# if compos_class[i] == new_class[j]:
# # if corners[i] is in new_corners[j], ignore corners[i]
# if r == -1:
# is_intersected = True
# break
# # if new_corners[j] is in corners[i], replace new_corners[j] with corners[i]
# elif r == 1:
# is_intersected = True
# new_corners[j] = corners[i]
#
# # if above IoU threshold, and corners[i] is in new_corners[j], ignore corners[i]
# if r == -2:
# is_intersected = True
# break
# # if above IoU threshold, and new_corners[j] is in corners[i], replace new_corners[j] with corners[i]
# elif r == 2:
# is_intersected = True
# new_corners[j] = corners[i]
# new_class[j] = compos_class[i]
#
# # containing and too small
# elif r == -3:
# is_intersected = True
# break
# elif r == 3:
# is_intersected = True
# new_corners[j] = corners[i]
#
# # if [i] and [j] are overlapped but no containing relation, merge corners when same class
# elif r == 4:
# is_intersected = True
# if compos_class[i] == new_class[j]:
# new_corners[j] = util.corner_merge_two_corners(corners[i], new_corners[j])
#
# if not is_intersected:
# new_corners.append(corners[i])
# new_class.append(compos_class[i])
# return new_corners, new_class
# def select_corner(corners, compos_class, class_name):
# """
# Select corners in given compo type
# """
# corners_wanted = []
# for i in range(len(compos_class)):
# if compos_class[i] == class_name:
# corners_wanted.append(corners[i])
# return corners_wanted
# def flood_fill_bfs(img, x_start, y_start, mark, grad_thresh):
# def neighbor(x, y):
# for i in range(x - 1, x + 2):
# if i < 0 or i >= img.shape[0]: continue
# for j in range(y - 1, y + 2):
# if j < 0 or j >= img.shape[1]: continue
# if mark[i, j] == 0 and abs(img[i, j] - img[x, y]) < grad_thresh:
# stack.append([i, j])
# mark[i, j] = 255
#
# stack = [[x_start, y_start]] # points waiting for inspection
# region = [[x_start, y_start]] # points of this connected region
# mark[x_start, y_start] = 255 # drawing broad
# while len(stack) > 0:
# point = stack.pop()
# region.append(point)
# neighbor(point[0], point[1])
# return region

View File

@@ -0,0 +1,123 @@
import cv2
import numpy as np
import shutil
import os
from os.path import join as pjoin
def segment_img(org, segment_size, output_path, overlap=100):
if not os.path.exists(output_path):
os.mkdir(output_path)
height, width = np.shape(org)[0], np.shape(org)[1]
top = 0
bottom = segment_size
segment_no = 0
while top < height and bottom < height:
segment = org[top:bottom]
cv2.imwrite(os.path.join(output_path, str(segment_no) + '.png'), segment)
segment_no += 1
top += segment_size - overlap
bottom = bottom + segment_size - overlap if bottom + segment_size - overlap <= height else height
def clipping(img, components, pad=0, show=False):
"""
:param adjust: shrink(negative) or expand(positive) the bounding box
:param img: original image
:param corners: ((column_min, row_min),(column_max, row_max))
:return: list of clipping images
"""
clips = []
for component in components:
clip = component.compo_clipping(img, pad=pad)
clips.append(clip)
if show:
cv2.imshow('clipping', clip)
cv2.waitKey()
return clips
def dissemble_clip_img_hollow(clip_root, org, compos):
if os.path.exists(clip_root):
shutil.rmtree(clip_root)
os.mkdir(clip_root)
cls_dirs = []
bkg = org.copy()
hollow_out = np.ones(bkg.shape[:2], dtype=np.uint8) * 255
for compo in compos:
cls = compo.category
c_root = pjoin(clip_root, cls)
c_path = pjoin(c_root, str(compo.id) + '.jpg')
if cls not in cls_dirs:
os.mkdir(c_root)
cls_dirs.append(cls)
clip = compo.compo_clipping(org)
cv2.imwrite(c_path, clip)
col_min, row_min, col_max, row_max = compo.put_bbox()
hollow_out[row_min: row_max, col_min: col_max] = 0
bkg = cv2.merge((bkg, hollow_out))
cv2.imwrite(os.path.join(clip_root, 'bkg.png'), bkg)
def dissemble_clip_img_fill(clip_root, org, compos, flag='most'):
def average_pix_around(pad=6, offset=3):
up = row_min - pad if row_min - pad >= 0 else 0
left = col_min - pad if col_min - pad >= 0 else 0
bottom = row_max + pad if row_max + pad < org.shape[0] - 1 else org.shape[0] - 1
right = col_max + pad if col_max + pad < org.shape[1] - 1 else org.shape[1] - 1
average = []
for i in range(3):
avg_up = np.average(org[up:row_min - offset, left:right, i])
avg_bot = np.average(org[row_max + offset:bottom, left:right, i])
avg_left = np.average(org[up:bottom, left:col_min - offset, i])
avg_right = np.average(org[up:bottom, col_max + offset:right, i])
average.append(int((avg_up + avg_bot + avg_left + avg_right)/4))
return average
def most_pix_around(pad=6, offset=2):
up = row_min - pad if row_min - pad >= 0 else 0
left = col_min - pad if col_min - pad >= 0 else 0
bottom = row_max + pad if row_max + pad < org.shape[0] - 1 else org.shape[0] - 1
right = col_max + pad if col_max + pad < org.shape[1] - 1 else org.shape[1] - 1
most = []
for i in range(3):
val = np.concatenate((org[up:row_min - offset, left:right, i].flatten(),
org[row_max + offset:bottom, left:right, i].flatten(),
org[up:bottom, left:col_min - offset, i].flatten(),
org[up:bottom, col_max + offset:right, i].flatten()))
# print(val)
# print(np.argmax(np.bincount(val)))
most.append(int(np.argmax(np.bincount(val))))
return most
if os.path.exists(clip_root):
shutil.rmtree(clip_root)
os.mkdir(clip_root)
cls_dirs = []
bkg = org.copy()
for compo in compos:
cls = compo.category
c_root = pjoin(clip_root, cls)
c_path = pjoin(c_root, str(compo.id) + '.jpg')
if cls not in cls_dirs:
os.mkdir(c_root)
cls_dirs.append(cls)
clip = compo.compo_clipping(org)
cv2.imwrite(c_path, clip)
col_min, row_min, col_max, row_max = compo.put_bbox()
if flag == 'average':
color = average_pix_around()
elif flag == 'most':
color = most_pix_around()
cv2.rectangle(bkg, (col_min, row_min), (col_max, row_max), color, -1)
cv2.imwrite(os.path.join(clip_root, 'bkg.png'), bkg)

View File

@@ -0,0 +1,113 @@
import pytesseract as pyt
import cv2
import lib_ip.ip_draw as draw
from config.CONFIG_UIED import Config
C = Config()
def is_text(img, min_word_area, show=False):
broad = img.copy()
area_word = 0
area_total = img.shape[0] * img.shape[1]
try:
# ocr text detection
data = pyt.image_to_data(img).split('\n')
except:
print(img.shape)
return -1
word = []
for d in data[1:]:
d = d.split()
if d[-1] != '-1':
if d[-1] != '-' and d[-1] != '' and int(d[-3]) < 50 and int(d[-4]) < 100:
word.append(d)
t_l = (int(d[-6]), int(d[-5]))
b_r = (int(d[-6]) + int(d[-4]), int(d[-5]) + int(d[-3]))
area_word += int(d[-4]) * int(d[-3])
cv2.rectangle(broad, t_l, b_r, (0,0,255), 1)
if show:
for d in word: print(d)
print(area_word/area_total)
cv2.imshow('a', broad)
cv2.waitKey(0)
cv2.destroyAllWindows()
# no text in this clip or relatively small text area
if len(word) == 0 or area_word/area_total < min_word_area:
return False
return True
def text_detection(org, img_clean):
try:
data = pyt.image_to_data(img_clean).split('\n')
except:
return org, None
corners_word = []
for d in data[1:]:
d = d.split()
if d[-1] != '-1':
if d[-1] != '-' and d[-1] != '' and 5 < int(d[-3]) < 40 and 5 < int(d[-4]) < 100:
t_l = (int(d[-6]), int(d[-5]))
b_r = (int(d[-6]) + int(d[-4]), int(d[-5]) + int(d[-3]))
corners_word.append((t_l, b_r))
return corners_word
# def text_merge_word_into_line(org, corners_word, max_words_gap=C.THRESHOLD_TEXT_MAX_WORD_GAP):
#
# def is_in_line(word):
# for i in range(len(lines)):
# line = lines[i]
# # at the same row
# if abs(line['center'][1] - word['center'][1]) < max_words_gap:
# # small gap between words
# if (abs(line['center'][0] - word['center'][0]) - abs(line['width']/2 + word['width']/2)) < max_words_gap:
# return i
# return -1
#
# def merge_line(word, index):
# line = lines[index]
# # on the left
# if word['center'][0] < line['center'][0]:
# line['col_min'] = word['col_min']
# # on the right
# else:
# line['col_max'] = word['col_max']
# line['row_min'] = min(line['row_min'], word['row_min'])
# line['row_max'] = max(line['row_max'], word['row_max'])
# line['width'] = line['col_max'] - line['col_min']
# line['height'] = line['row_max'] - line['row_min']
# line['center'] = ((line['col_max'] + line['col_min'])/2, (line['row_max'] + line['row_min'])/2)
#
# words = []
# for corner in corners_word:
# word = {}
# (top_left, bottom_right) = corner
# (col_min, row_min) = top_left
# (col_max, row_max) = bottom_right
# word['col_min'], word['col_max'], word['row_min'], word['row_max'] = col_min, col_max, row_min, row_max
# word['height'] = row_max - row_min
# word['width'] = col_max - col_min
# word['center'] = ((col_max + col_min)/2, (row_max + row_min)/2)
# words.append(word)
#
# lines = []
# for word in words:
# line_index = is_in_line(word)
# # word is in current line
# if line_index != -1:
# merge_line(word, line_index)
# # word is not in current line
# else:
# # this single word as a new line
# lines.append(word)
#
# corners_line = []
# for l in lines:
# corners_line.append(((l['col_min'], l['row_min']), (l['col_max'], l['row_max'])))
# return corners_line

View File

@@ -0,0 +1,127 @@
import cv2
from os.path import join as pjoin
import time
import json
import numpy as np
import detect_compo.lib_ip.ip_preprocessing as pre
import detect_compo.lib_ip.ip_draw as draw
import detect_compo.lib_ip.ip_detection as det
import detect_compo.lib_ip.file_utils as file
import detect_compo.lib_ip.Component as Compo
from config.CONFIG_UIED import Config
C = Config()
def resolve_uicompo_containment(uicompos):
"""
Resolves containment issues among UI components.
If a component's bounding box is fully contained within another's, it is removed.
"""
def contains(bbox_a, bbox_b):
"""Checks if bbox_a completely contains bbox_b."""
return bbox_a.col_min <= bbox_b.col_min and \
bbox_a.row_min <= bbox_b.row_min and \
bbox_a.col_max >= bbox_b.col_max and \
bbox_a.row_max >= bbox_b.row_max
compos_to_remove = set()
for i, compo1 in enumerate(uicompos):
for j, compo2 in enumerate(uicompos):
if i == j:
continue
# Check if compo1 contains compo2
if contains(compo1.bbox, compo2.bbox):
compos_to_remove.add(j)
# Filter out the contained components
final_compos = [compo for i, compo in enumerate(uicompos) if i not in compos_to_remove]
if len(final_compos) < len(uicompos):
print(f"Containment resolved: Removed {len(uicompos) - len(final_compos)} contained components.")
return final_compos
def nesting_inspection(org, grey, compos, ffl_block):
'''
Inspect all big compos through block division by flood-fill
:param ffl_block: gradient threshold for flood-fill
:return: nesting compos
'''
nesting_compos = []
for i, compo in enumerate(compos):
if compo.height > 50:
replace = False
clip_grey = compo.compo_clipping(grey)
n_compos = det.nested_components_detection(clip_grey, org, grad_thresh=ffl_block, show=False)
Compo.cvt_compos_relative_pos(n_compos, compo.bbox.col_min, compo.bbox.row_min)
for n_compo in n_compos:
if n_compo.redundant:
compos[i] = n_compo
replace = True
break
if not replace:
nesting_compos += n_compos
return nesting_compos
def compo_detection(input_img_path, output_root, uied_params,
resize_by_height=800, classifier=None, show=False, wai_key=0):
start = time.perf_counter()
name = input_img_path.split('/')[-1][:-4] if '/' in input_img_path else input_img_path.split('\\')[-1][:-4]
ip_root = file.build_directory(pjoin(output_root, "ip"))
# *** Step 1 *** pre-processing: read img -> get binary map
org, grey = pre.read_img(input_img_path, resize_by_height)
binary = pre.binarization(org, grad_min=int(uied_params['min-grad']))
# *** Step 2 *** element detection
det.rm_line(binary, show=show, wait_key=wai_key)
uicompos = det.component_detection(binary, min_obj_area=int(uied_params['min-ele-area']))
# *** Step 3 *** results refinement
uicompos = det.compo_filter(uicompos, min_area=int(uied_params['min-ele-area']), img_shape=binary.shape)
uicompos = det.merge_intersected_compos(uicompos)
det.compo_block_recognition(binary, uicompos)
if uied_params['merge-contained-ele']:
uicompos = det.rm_contained_compos_not_in_block(uicompos)
Compo.compos_update(uicompos, org.shape)
Compo.compos_containment(uicompos)
# *** Step 4 ** nesting inspection: check if big compos have nesting element
uicompos += nesting_inspection(org, grey, uicompos, ffl_block=uied_params['ffl-block'])
Compo.compos_update(uicompos, org.shape)
draw.draw_bounding_box(org, uicompos, show=show, name='merged compo', write_path=pjoin(ip_root, name + '.jpg'), wait_key=wai_key)
# *** Step 5 *** image inspection: recognize image -> remove noise in image -> binarize with larger threshold and reverse -> rectangular compo detection
# if classifier is not None:
# classifier['Image'].predict(seg.clipping(org, uicompos), uicompos)
# draw.draw_bounding_box_class(org, uicompos, show=show)
# uicompos = det.rm_noise_in_large_img(uicompos, org)
# draw.draw_bounding_box_class(org, uicompos, show=show)
# det.detect_compos_in_img(uicompos, binary_org, org)
# draw.draw_bounding_box(org, uicompos, show=show)
# if classifier is not None:
# classifier['Noise'].predict(seg.clipping(org, uicompos), uicompos)
# draw.draw_bounding_box_class(org, uicompos, show=show)
# uicompos = det.rm_noise_compos(uicompos)
# *** Step 6 *** element classification: all category classification
# if classifier is not None:
# classifier['Elements'].predict([compo.compo_clipping(org) for compo in uicompos], uicompos)
# draw.draw_bounding_box_class(org, uicompos, show=show, name='cls', write_path=pjoin(ip_root, 'result.jpg'))
# draw.draw_bounding_box_class(org, uicompos, write_path=pjoin(output_root, 'result.jpg'))
# *** Step 7 *** save detection result
Compo.compos_update(uicompos, org.shape)
# *** Step 8 *** resolve containment issues among UI components
uicompos = resolve_uicompo_containment(uicompos)
file.save_corners_json(pjoin(ip_root, name + '.json'), uicompos)
print("[Compo Detection Completed in %.3f s] Input: %s Output: %s" % (time.perf_counter() - start, input_img_path, pjoin(ip_root, name + '.json')))
return uicompos

View File

@@ -0,0 +1,122 @@
import numpy as np
import detect_compo.lib_ip.ip_draw as draw
class Bbox:
def __init__(self, col_min, row_min, col_max, row_max):
self.col_min = col_min
self.row_min = row_min
self.col_max = col_max
self.row_max = row_max
self.width = col_max - col_min
self.height = row_max - row_min
self.box_area = self.width * self.height
def put_bbox(self):
return self.col_min, self.row_min, self.col_max, self.row_max
def bbox_cal_area(self):
self.box_area = self.width * self.height
return self.box_area
def bbox_relation(self, bbox_b):
"""
:return: -1 : a in b
0 : a, b are not intersected
1 : b in a
2 : a, b are identical or intersected
"""
col_min_a, row_min_a, col_max_a, row_max_a = self.put_bbox()
col_min_b, row_min_b, col_max_b, row_max_b = bbox_b.put_bbox()
# if a is in b
if col_min_a > col_min_b and row_min_a > row_min_b and col_max_a < col_max_b and row_max_a < row_max_b:
return -1
# if b is in a
elif col_min_a < col_min_b and row_min_a < row_min_b and col_max_a > col_max_b and row_max_a > row_max_b:
return 1
# a and b are non-intersect
elif (col_min_a > col_max_b or row_min_a > row_max_b) or (col_min_b > col_max_a or row_min_b > row_max_a):
return 0
# intersection
else:
return 2
def bbox_relation_nms(self, bbox_b, bias=(0, 0)):
'''
Calculate the relation between two rectangles by nms
:return: -1 : a in b
0 : a, b are not intersected
1 : b in a
2 : a, b are intersected
'''
col_min_a, row_min_a, col_max_a, row_max_a = self.put_bbox()
col_min_b, row_min_b, col_max_b, row_max_b = bbox_b.put_bbox()
bias_col, bias_row = bias
# get the intersected area
col_min_s = max(col_min_a - bias_col, col_min_b - bias_col)
row_min_s = max(row_min_a - bias_row, row_min_b - bias_row)
col_max_s = min(col_max_a + bias_col, col_max_b + bias_col)
row_max_s = min(row_max_a + bias_row, row_max_b + bias_row)
w = np.maximum(0, col_max_s - col_min_s)
h = np.maximum(0, row_max_s - row_min_s)
inter = w * h
area_a = (col_max_a - col_min_a) * (row_max_a - row_min_a)
area_b = (col_max_b - col_min_b) * (row_max_b - row_min_b)
iou = inter / (area_a + area_b - inter)
ioa = inter / self.box_area
iob = inter / bbox_b.box_area
if iou == 0 and ioa == 0 and iob == 0:
return 0
# import lib_ip.ip_preprocessing as pre
# org_iou, _ = pre.read_img('uied/data/input/7.jpg', 800)
# print(iou, ioa, iob)
# board = draw.draw_bounding_box(org_iou, [self], color=(255,0,0))
# draw.draw_bounding_box(board, [bbox_b], color=(0,255,0), show=True)
# contained by b
if ioa >= 1:
return -1
# contains b
if iob >= 1:
return 1
# not intersected with each other
# intersected
if iou >= 0.02 or iob > 0.2 or ioa > 0.2:
return 2
# if iou == 0:
# print('ioa:%.5f; iob:%.5f; iou:%.5f' % (ioa, iob, iou))
return 0
def bbox_cvt_relative_position(self, col_min_base, row_min_base):
'''
Convert to relative position based on base coordinator
'''
self.col_min += col_min_base
self.col_max += col_min_base
self.row_min += row_min_base
self.row_max += row_min_base
def bbox_merge(self, bbox_b):
'''
Merge two intersected bboxes
'''
col_min_a, row_min_a, col_max_a, row_max_a = self.put_bbox()
col_min_b, row_min_b, col_max_b, row_max_b = bbox_b.put_bbox()
col_min = min(col_min_a, col_min_b)
col_max = max(col_max_a, col_max_b)
row_min = min(row_min_a, row_min_b)
row_max = max(row_max_a, row_max_b)
new_bbox = Bbox(col_min, row_min, col_max, row_max)
return new_bbox
def bbox_padding(self, image_shape, pad):
row, col = image_shape[:2]
self.col_min = max(self.col_min - pad, 0)
self.col_max = min(self.col_max + pad, col)
self.row_min = max(self.row_min - pad, 0)
self.row_max = min(self.row_max + pad, row)

View File

@@ -0,0 +1,238 @@
from detect_compo.lib_ip.Bbox import Bbox
import detect_compo.lib_ip.ip_draw as draw
import cv2
def cvt_compos_relative_pos(compos, col_min_base, row_min_base):
for compo in compos:
compo.compo_relative_position(col_min_base, row_min_base)
def compos_containment(compos):
for i in range(len(compos) - 1):
for j in range(i + 1, len(compos)):
relation = compos[i].compo_relation(compos[j])
if relation == -1:
compos[j].contain.append(i)
if relation == 1:
compos[i].contain.append(j)
def compos_update(compos, org_shape):
for i, compo in enumerate(compos):
# start from 1, id 0 is background
compo.compo_update(i + 1, org_shape)
class Component:
def __init__(self, region, image_shape):
self.id = None
self.region = region
self.boundary = self.compo_get_boundary()
self.bbox = self.compo_get_bbox()
self.bbox_area = self.bbox.box_area
self.region_area = len(region)
self.width = len(self.boundary[0])
self.height = len(self.boundary[2])
self.image_shape = image_shape
self.area = self.width * self.height
self.category = 'Compo'
self.contain = []
self.rect_ = None
self.line_ = None
self.redundant = False
def compo_update(self, id, org_shape):
self.id = id
self.image_shape = org_shape
self.width = self.bbox.width
self.height = self.bbox.height
self.bbox_area = self.bbox.box_area
self.area = self.width * self.height
def put_bbox(self):
return self.bbox.put_bbox()
def compo_update_bbox_area(self):
self.bbox_area = self.bbox.bbox_cal_area()
def compo_get_boundary(self):
'''
get the bounding boundary of an object(region)
boundary: [top, bottom, left, right]
-> up, bottom: (column_index, min/max row border)
-> left, right: (row_index, min/max column border) detect range of each row
'''
border_up, border_bottom, border_left, border_right = {}, {}, {}, {}
for point in self.region:
# point: (row_index, column_index)
# up, bottom: (column_index, min/max row border) detect range of each column
if point[1] not in border_up or border_up[point[1]] > point[0]:
border_up[point[1]] = point[0]
if point[1] not in border_bottom or border_bottom[point[1]] < point[0]:
border_bottom[point[1]] = point[0]
# left, right: (row_index, min/max column border) detect range of each row
if point[0] not in border_left or border_left[point[0]] > point[1]:
border_left[point[0]] = point[1]
if point[0] not in border_right or border_right[point[0]] < point[1]:
border_right[point[0]] = point[1]
boundary = [border_up, border_bottom, border_left, border_right]
# descending sort
for i in range(len(boundary)):
boundary[i] = [[k, boundary[i][k]] for k in boundary[i].keys()]
boundary[i] = sorted(boundary[i], key=lambda x: x[0])
return boundary
def compo_get_bbox(self):
"""
Get the top left and bottom right points of boundary
:param boundaries: boundary: [top, bottom, left, right]
-> up, bottom: (column_index, min/max row border)
-> left, right: (row_index, min/max column border) detect range of each row
:return: corners: [(top_left, bottom_right)]
-> top_left: (column_min, row_min)
-> bottom_right: (column_max, row_max)
"""
col_min, row_min = (int(min(self.boundary[0][0][0], self.boundary[1][-1][0])), int(min(self.boundary[2][0][0], self.boundary[3][-1][0])))
col_max, row_max = (int(max(self.boundary[0][0][0], self.boundary[1][-1][0])), int(max(self.boundary[2][0][0], self.boundary[3][-1][0])))
bbox = Bbox(col_min, row_min, col_max, row_max)
return bbox
def compo_is_rectangle(self, min_rec_evenness, max_dent_ratio, test=False):
'''
detect if an object is rectangle by evenness and dent of each border
'''
dent_direction = [1, -1, 1, -1] # direction for convex
flat = 0
parameter = 0
for n, border in enumerate(self.boundary):
parameter += len(border)
# dent detection
pit = 0 # length of pit
depth = 0 # the degree of surface changing
if n <= 1:
adj_side = max(len(self.boundary[2]), len(self.boundary[3])) # get maximum length of adjacent side
else:
adj_side = max(len(self.boundary[0]), len(self.boundary[1]))
# -> up, bottom: (column_index, min/max row border)
# -> left, right: (row_index, min/max column border) detect range of each row
abnm = 0
for i in range(int(3 + len(border) * 0.02), len(border) - 1):
# calculate gradient
difference = border[i][1] - border[i + 1][1]
# the degree of surface changing
depth += difference
# ignore noise at the start of each direction
if i / len(border) < 0.08 and (dent_direction[n] * difference) / adj_side > 0.5:
depth = 0 # reset
# print(border[i][1], i / len(border), depth, (dent_direction[n] * difference) / adj_side)
# if the change of the surface is too large, count it as part of abnormal change
if abs(depth) / adj_side > 0.3:
abnm += 1 # count the size of the abnm
# if the abnm is too big, the shape should not be a rectangle
if abnm / len(border) > 0.1:
if test:
print('abnms', abnm, abnm / len(border))
draw.draw_boundary([self], self.image_shape, show=True)
self.rect_ = False
return False
continue
else:
# reset the abnm if the depth back to normal
abnm = 0
# if sunken and the surface changing is large, then counted as pit
if dent_direction[n] * depth < 0 and abs(depth) / adj_side > 0.15:
pit += 1
continue
# if the surface is not changing to a pit and the gradient is zero, then count it as flat
if abs(depth) < 1 + adj_side * 0.015:
flat += 1
if test:
print(depth, adj_side, flat)
# if the pit is too big, the shape should not be a rectangle
if pit / len(border) > max_dent_ratio:
if test:
print('pit', pit, pit / len(border))
draw.draw_boundary([self], self.image_shape, show=True)
self.rect_ = False
return False
if test:
print(flat / parameter, '\n')
draw.draw_boundary([self], self.image_shape, show=True)
# ignore text and irregular shape
if self.height / self.image_shape[0] > 0.3:
min_rec_evenness = 0.85
if (flat / parameter) < min_rec_evenness:
self.rect_ = False
return False
self.rect_ = True
return True
def compo_is_line(self, min_line_thickness):
"""
Check this object is line by checking its boundary
:param boundary: boundary: [border_top, border_bottom, border_left, border_right]
-> top, bottom: list of (column_index, min/max row border)
-> left, right: list of (row_index, min/max column border) detect range of each row
:param min_line_thickness:
:return: Boolean
"""
# horizontally
slim = 0
for i in range(self.width):
if abs(self.boundary[1][i][1] - self.boundary[0][i][1]) <= min_line_thickness:
slim += 1
if slim / len(self.boundary[0]) > 0.93:
self.line_ = True
return True
# vertically
slim = 0
for i in range(self.height):
if abs(self.boundary[2][i][1] - self.boundary[3][i][1]) <= min_line_thickness:
slim += 1
if slim / len(self.boundary[2]) > 0.93:
self.line_ = True
return True
self.line_ = False
return False
def compo_relation(self, compo_b, bias=(0, 0)):
"""
:return: -1 : a in b
0 : a, b are not intersected
1 : b in a
2 : a, b are identical or intersected
"""
return self.bbox.bbox_relation_nms(compo_b.bbox, bias)
def compo_relative_position(self, col_min_base, row_min_base):
'''
Convert to relative position based on base coordinator
'''
self.bbox.bbox_cvt_relative_position(col_min_base, row_min_base)
def compo_merge(self, compo_b):
self.bbox = self.bbox.bbox_merge(compo_b.bbox)
self.compo_update(self.id, self.image_shape)
def compo_clipping(self, img, pad=0, show=False):
(column_min, row_min, column_max, row_max) = self.put_bbox()
column_min = max(column_min - pad, 0)
column_max = min(column_max + pad, img.shape[1])
row_min = max(row_min - pad, 0)
row_max = min(row_max + pad, img.shape[0])
clip = img[row_min:row_max, column_min:column_max]
if show:
cv2.imshow('clipping', clip)
cv2.waitKey()
return clip

Some files were not shown because too many files have changed in this diff Show More