├── image
├── 1462.png
├── 3553.png
└── 17059.png
├── .gitignore
├── .idea
├── vcs.xml
├── misc.xml
├── modules.xml
└── rico_json_processing.iml
├── log_parsing
└── log_parse.py
├── draw_log_parse.py
├── compress.py
├── main.py
├── compare_org_draw.py
├── delete.py
├── settings.py
├── README.md
├── draw_image.py
└── element.py
/image/1462.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jin1ming/rico-json-processing/HEAD/image/1462.png
--------------------------------------------------------------------------------
/image/3553.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jin1ming/rico-json-processing/HEAD/image/3553.png
--------------------------------------------------------------------------------
/image/17059.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jin1ming/rico-json-processing/HEAD/image/17059.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | json/
2 | comparison/
3 | original_picture/
4 | layout/
5 | compressed_comp/
6 | __pycache__/
7 | .idea/
8 |
9 | *.pyc
10 | *.txt
11 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/log_parsing/log_parse.py:
--------------------------------------------------------------------------------
1 | file = open("log.txt")
2 | pout = open("perr.txt", mode="w")
3 | mout = open("merr.txt", mode="w")
4 |
5 | for line in file:
6 | if line[0:2] != 'Do':
7 | if line[0] == 'M':
8 | mout.write(line)
9 | elif line[0] == 'P':
10 | pout.write(line)
11 | # print(line.split(": ")[1].rstrip().split(".")[0] + ".jpg", end=' ')
--------------------------------------------------------------------------------
/draw_log_parse.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | log = open("./draw_log.txt", "r")
4 | delete_log = open("./del_log.txt", "w")
5 |
6 | for line in log:
7 | if len(line.split(":")) > 2 and (line.split(":")[1] == "WebView" or line.split(":")[1] == "google.android.gms.ads"):
8 | print(line)
9 | delete_log.write(line)
10 | os.system("./delete.py " + line.split(":")[0].split(".")[0])
11 |
--------------------------------------------------------------------------------
/.idea/rico_json_processing.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/compress.py:
--------------------------------------------------------------------------------
1 | from multiprocessing.dummy import Pool
2 | from PIL import Image
3 | from settings import *
4 | import os
5 |
6 |
7 | def work(file_name):
8 | try:
9 | origin = Image.open(os.path.join(os.path.curdir, 'comparison', file_name + '_comp.png'))
10 | except FileNotFoundError:
11 | print("File not found: " + file_name + "_comp.png")
12 | return
13 |
14 | compressed_screen_size = (int(2880 / compress_ratio), int(2560 // compress_ratio))
15 |
16 | new = origin.resize(compressed_screen_size, Image.ANTIALIAS)
17 | new.save(os.path.join(os.path.curdir, 'compressed_comp', file_name + '_' + str(compress_ratio) + 'compressed.png'), 'PNG')
18 | print(os.path.join(os.path.curdir, 'compressed_comp', file_name + '_' + str(compress_ratio) + 'compressed.png'))
19 |
20 |
21 | try:
22 | compress_ratio = float(input("Please input compression ratio: "))
23 | except:
24 | print("Wrong input. Default value(2.0) will be used.")
25 | compress_ratio = 2.0
26 |
27 | threads = Pool(40)
28 | threads.map(work, FILE_LIST)
29 | threads.close()
30 | threads.join()
31 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import json, os, csv
2 | from settings import *
3 | from element import element
4 | from multiprocessing.dummy import Pool
5 |
6 | app_info_iter = csv.reader(APP_METADATA)
7 | app_info = {i[0]: i[0:3] for i in app_info_iter}
8 |
9 |
10 | # Chunks that actually works
11 | def work(FILE_NAME):
12 | print("Do: " + os.path.join(os.path.curdir, 'json', 'raw', FILE_NAME))
13 | FILE = open(os.path.join(os.path.curdir, 'json', 'raw', FILE_NAME))
14 |
15 | data = json.load(FILE)
16 | parsed_data = {}
17 |
18 | try:
19 | parsed_data['app'] = {'package_name': data['activity_name'].split('/')[0],
20 | 'store_name': app_info[data['activity_name'].split('/')[0]][1],
21 | 'category': app_info[data['activity_name'].split('/')[0]][2]}
22 | except:
23 | print("Metadata error: " + FILE_NAME + " : " + data['activity_name'].split('/')[0])
24 | parsed_data['activity_name'] = data['activity_name'].split('/')[1].split('.')[-1]
25 | parsed_data['keyboard'] = data['is_keyboard_deployed']
26 | try:
27 | parsed_data['hierarchy'] = element(data['activity']['root']).to_dict()
28 | except:
29 | print("Parsing error: " + FILE_NAME + " : " + data['activity_name'].split('/')[0])
30 |
31 | output = open(os.path.join(os.path.curdir, 'json', 'refined', FILE_NAME), mode='w')
32 | json.dump(parsed_data, output, indent=2, sort_keys=True)
33 | print("Done: " + os.path.join(os.path.curdir, 'json', 'refined', FILE_NAME))
34 |
35 | # Simple codes that initiate multi-threads.
36 | threads = Pool(40)
37 | threads.map(work, FILE_LIST)
38 | threads.close()
39 | threads.join()
40 |
--------------------------------------------------------------------------------
/compare_org_draw.py:
--------------------------------------------------------------------------------
1 | from settings import *
2 | from PIL import Image
3 | import os
4 | from multiprocessing.dummy import Pool
5 |
6 | def work(FILE_NAME):
7 | # for FILE_NAME in FILE_LIST:
8 | canvas = Image.new("RGBA", (2880, 2560), 'white')
9 | try:
10 | origin = Image.open(os.path.join(os.path.curdir, 'original_picture', FILE_NAME.split('.')[0] + '.jpg'))
11 | except:
12 | err = open('comp_err_log.txt', 'a')
13 | err.write(FILE_NAME + ": Error occurred while opening original pic.\n")
14 | print(FILE_NAME + ": Error occurred while opening original pic.")
15 | err.close()
16 | origin = Image.new("RGBA", (1440, 2560), "black")
17 | pass
18 | try:
19 | layout = Image.open(os.path.join(os.path.curdir, 'layout', FILE_NAME + '_out.png'))
20 | except:
21 | err = open('comp_err_log.txt', 'a')
22 | err.write(FILE_NAME + ": Error occurred while opening layout pic.\n")
23 | err.close()
24 | print(FILE_NAME + ": Error occurred while opening layout pic.")
25 | layout = Image.new("RGBA", (1440, 2560), "black")
26 | pass
27 | # origin = Image.open('original_picture/' + FILE_NAME.split('.')[0] + '.jpg')
28 | # layout = Image.open('layout/' + FILE_NAME + '_out.png')
29 |
30 | origin = origin.resize((1440, 2560), Image.ANTIALIAS)
31 |
32 | canvas.paste(origin, (0, 0))
33 | canvas.paste(layout, (1440, 0))
34 |
35 | canvas.save(os.path.join(os.path.curdir, 'comparison', FILE_NAME + '_comp.png'), 'PNG')
36 | print(os.path.join(os.path.curdir, 'comparison', FILE_NAME + '_comp.png'))
37 | # canvas.save('./comparison/' + FILE_NAME + '_comp.png', 'PNG')
38 | # print('./comparison/' + FILE_NAME + '_comp.png')
39 |
40 |
41 | threads = Pool(40)
42 | threads.map(work, FILE_LIST)
43 | threads.close()
44 | threads.join()
45 |
--------------------------------------------------------------------------------
/delete.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | import json, sys, os
3 |
4 | FILES = sys.argv[1:]
5 |
6 | for FILE_NUM in FILES:
7 | try:
8 | FILE_JSON = json.load(open(os.path.join(os.path.curdir, 'json', 'refined', FILE_NUM + '.json')))
9 | except FileNotFoundError:
10 | print("File not found: %s" % os.path.join(os.path.curdir, 'json', 'refined', FILE_NUM + '.json'))
11 | continue
12 |
13 | print("File you want to delete is:")
14 | print(" - %s" % os.path.join(os.path.curdir, 'json', 'refined', FILE_NUM + '.json'))
15 | print(" - %s" % os.path.join(os.path.curdir, 'json', 'raw', FILE_NUM + '.json'))
16 | print(" - %s" % os.path.join(os.path.curdir, 'layout', FILE_NUM + '.json_out.png'))
17 | print(" - %s" % os.path.join(os.path.curdir, 'comparison', FILE_NUM + '.json_comp.png'))
18 | print("App info: ")
19 | print(" - Package name: %s" % FILE_JSON['app']['package_name'])
20 | print(" - Store name : %s" % FILE_JSON['app']['store_name'])
21 | print(" - Category : %s" % FILE_JSON['app']['category'])
22 | print("\nPlease check the information before you delete.\nDo you really want to delete this info? (Y/N) ")
23 |
24 | while True:
25 | n = input()
26 | if n == 'Y' or n == 'y':
27 | try:
28 | os.remove(os.path.join(os.path.curdir, 'json', 'refined', FILE_NUM + '.json'))
29 | except:
30 | pass
31 | try:
32 | os.remove(os.path.join(os.path.curdir, 'json', 'raw', FILE_NUM + '.json'))
33 | except:
34 | pass
35 | try:
36 | os.remove(os.path.join(os.path.curdir, 'layout', FILE_NUM + '.json_out.png'))
37 | except:
38 | pass
39 | try:
40 | os.remove(os.path.join(os.path.curdir, 'comparison', FILE_NUM + '.json_comp.png'))
41 | except:
42 | pass
43 | print("Delete complete.")
44 | break
45 | elif n == 'N' or n == 'n':
46 | print("Deletion aborted.")
47 | break
48 | else:
49 | print("Please answer with Y/N. ")
50 |
--------------------------------------------------------------------------------
/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | # Information about json files
4 | FILE_LIST = [name for name in os.listdir(os.path.join(os.path.curdir, 'json', 'raw'))]
5 | # FILE_LIST = ['1001.json']
6 |
7 | # Information about layout images
8 | SCREEN_SIZE = (1440, 2560) # Do NOT change
9 | ELEMENT_COLOR = {'EditText': 'orange',
10 | #'edit': 'orange',
11 | 'CheckedTextView': 'lime',
12 | 'Text': 'blue',
13 | 'text': 'blue',
14 | 'TextView': 'blue',
15 | 'RadioButton': 'lime',
16 | 'Button': 'green',
17 | #'button': 'green',
18 | 'Image': 'red',
19 | 'image': 'red',
20 | 'icon': 'red',
21 | 'CheckBox': 'pink',
22 | 'checkbox': 'pink',
23 | 'Checkbox': 'pink',
24 | 'checkBox': 'pink',
25 | 'WebView': 'purple',
26 | 'AdView': 'brown',
27 | 'Banner': 'brown',
28 | 'NumberPicker': 'gray',
29 | 'SpeedometerGauge': 'red',
30 | 'Gauge': 'red',
31 | 'ArcProgress': 'red',
32 | 'SimpleDraweeView': 'red',
33 | 'MonthView': 'yellow',
34 | 'AccessibleDateAnimator': 'yellow',
35 | 'MapView': 'skyblue',
36 | 'VideoView': 'navy',
37 | 'Chart': 'red',
38 | 'SeekBar': 'gold',
39 | 'PictureView': 'red',
40 | 'Picture': 'red',
41 | 'ActionMenuItemView': 'green',
42 | 'ImageSurfaceView': 'red',
43 | 'ImageView': 'red',
44 | 'SurfaceView': 'red',
45 | 'Graph': 'red',
46 | 'RadialPicker': 'ivory',
47 | 'time_picker': 'ivory',
48 | 'TimePicker': 'ivory',
49 | 'ProgressBar': 'magenta',
50 | }
51 |
52 | ELEMENT_EXCEPT = [#'feeditem',
53 | #'edit_profile',
54 | #'credit',
55 | 'background_holder',
56 | 'background_image',
57 | ]
58 |
59 | FULL_NAME_COLOR = {'google.maps.api.android': 'skyblue', # Google Maps API
60 | 'google.android.gms.ads': 'brown', # Google Ads
61 | 'mopub.mobileads': 'brown', # Mopub Ads
62 | 'maps.D': 'skyblue', # Google Maps
63 | 'maps.ad.ay$a': 'skyblue', # Google Maps
64 | 'drumpadmachine.ui.Pad': 'red', # Game image element
65 | }
66 |
67 |
68 | # Others
69 | APP_METADATA = open(os.path.join(os.path.curdir, 'metadata', 'app_details.csv'))
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JSON Processing of RICO Dataset
2 |
3 | ## **This is an amendment to the original version.**
4 | ## **这是一个对原来代码的修正版本**
5 |
6 | 如果有疑问,欢迎提issue交流
7 |
8 | ---
9 |
10 |
11 | These scripts are used to process JSON layouts of [RICO dataset](http://interactionmining.org/rico).
12 | The result of processed layouts has been used in [a paper in CHI '20](https://dl.acm.org/doi/abs/10.1145/3313831.3376327).
13 |
14 | ## Scripts
15 |
16 | We used python 3.6.2 to execute the scripts below.
17 |
18 | ### `python main.py`
19 |
20 | This script extracts required metadata from the raw RICO json files, as a raw file contains too many information about the interface.
21 | RICO json files should be stored in `json/raw/` folder, and the results will be stored in `json/refined` folder.
22 |
23 | ### `python draw_image.py`
24 |
25 | Using the refined json files produced by `main.py`, this script draws bounding boxes of layout components.
26 | You may find the mapping between colors and component categories in the `settings.py` file.
27 | The results will be stored in a `layout/{filename}_out.png` format.
28 |
29 | ### `python compare_org_draw.py`
30 |
31 | This script draws the comparison image of a UI capture image and its layout image.
32 | The purpose of this comparison is because **a few RICO images do not match to their json hierarchy information**, so we had to produce the comparsion images and manually check if all the images are correct.
33 |
34 | This script requires the original UI images in `original_picutre/` folder as well as the layout images produced by `draw_image.py`, and the results will be stored in a `comparison/{filename}_comp.png` format.
35 |
36 | ### `python compress.py`
37 |
38 | This script compresses the drawn layout images produced by `compare_org_draw.py` script.
39 | The purpose of compression is because the dimension of a raw layout image is 2880 x 2560, which is a bit large.
40 | The results will be stored in a `compressed_comp/{filename}_{compress_ratio}compressed.png` format.
41 |
42 | Here are a few examples of the produced outputs after `compress.py`, compressed by 4.0:
43 | 1462 | 3553 | 17059
44 | ---- | ---- | ----
45 |  |  | 
46 |
47 | ### `delete.py {image_numbers}`
48 |
49 | This script receives a list of RICO image numbers and deletes the data of them produced by the scripts above.
50 |
51 | ## Citation
52 |
53 | If you found these scripts helpful, please consider citing our paper.
54 | ```bibtex
55 | @inproceedings{lee2020guicomp,
56 | author = {Lee, Chunggi and Kim, Sanghoon and Han, Dongyun and Yang, Hongjun and Park, Young-Woo and Kwon, Bum Chul and Ko, Sungahn},
57 | title = {GUIComp: A GUI Design Assistant with Real-Time, Multi-Faceted Feedback},
58 | year = {2020},
59 | publisher = {Association for Computing Machinery},
60 | address = {New York, NY, USA},
61 | url = {https://doi.org/10.1145/3313831.3376327},
62 | doi = {10.1145/3313831.3376327},
63 | booktitle = {Proceedings of the 2020 CHI Conference on Human Factors in Computing Systems},
64 | pages = {1–13},
65 | series = {CHI '20}
66 | }
67 | ```
68 |
--------------------------------------------------------------------------------
/draw_image.py:
--------------------------------------------------------------------------------
1 | import json, os
2 | from settings import *
3 | from PIL import Image
4 | from multiprocessing.dummy import Pool
5 |
6 | def draw(hier, img, FILE_NAME):
7 | #
8 | # Some non-standard layout/views, they don't have standard name.
9 | # Therefore, we have to guess the label using their name and resource-id.
10 | #
11 | checked = False
12 | for element_name in ELEMENT_COLOR.keys():
13 | if hier['name'].count(element_name) > 0:
14 | except_occur = False
15 | for element_except in ELEMENT_EXCEPT:
16 | if hier['resource_id'] is not None and hier['resource_id'].count(element_except) > 0:
17 | except_occur = True
18 | break
19 | if not except_occur:
20 | hier['name'] = element_name
21 | checked = True
22 | break
23 |
24 | if not checked and hier['resource_id'] is not None:
25 | for element_name in ELEMENT_COLOR.keys():
26 | if hier['resource_id'].count(element_name) > 0:
27 | except_occur = False
28 | for element_except in ELEMENT_EXCEPT:
29 | if hier['resource_id'].count(element_except) > 0:
30 | except_occur = True
31 | break
32 | if not except_occur:
33 | hier['name'] = element_name
34 | break
35 |
36 | #
37 | # Some non-standard layout/views which can't be handled by name/resource-id,
38 | # they should be processed using full_name(class information).
39 | #
40 | full_name_checked = False
41 | for full_name in FULL_NAME_COLOR.keys():
42 | if hier['full_name'].count(full_name) > 0 and hier['visible']:
43 | full_name_checked = True
44 | b = hier['bounds']
45 | try:
46 | block = Image.new('RGBA', (b[2] - b[0], b[3] - b[1]), FULL_NAME_COLOR[full_name])
47 | img.paste(block, (b[0], b[1]))
48 | if ((b[2] - b[0]) * (b[3] - b[1]))/(SCREEN_SIZE[0] * SCREEN_SIZE[1]) > 0.8:
49 | print(FILE_NAME + ":" + full_name + ":Too large:" + str(((b[2] - b[0]) * (b[3] - b[1]))/(SCREEN_SIZE[0] * SCREEN_SIZE[1])))
50 | except ValueError:
51 | pass
52 |
53 | # Coloring
54 | if not full_name_checked and hier['name'] in ELEMENT_COLOR.keys() and hier['visible']:
55 | b = hier['bounds']
56 | try:
57 | block = Image.new('RGBA', (b[2] - b[0], b[3] - b[1]), ELEMENT_COLOR[hier['name']])
58 | img.paste(block, (b[0], b[1]))
59 | if ((b[2] - b[0]) * (b[3] - b[1])) / (SCREEN_SIZE[0] * SCREEN_SIZE[1]) > 0.8:
60 | print(FILE_NAME + ":" + hier['name'] + ":Too large:" + str(
61 | ((b[2] - b[0]) * (b[3] - b[1])) / (SCREEN_SIZE[0] * SCREEN_SIZE[1])))
62 | except ValueError:
63 | pass
64 |
65 | # Recursion
66 | if hier['children'] is not None:
67 | for child in hier['children']:
68 | draw(child, img, FILE_NAME)
69 |
70 |
71 | def work(FILE_NAME):
72 | with open(os.path.join(os.path.curdir, 'json', 'refined', FILE_NAME), mode='r') as file:
73 | data = json.load(file)
74 |
75 | img = Image.new('RGBA', SCREEN_SIZE, 'white')
76 | try:
77 | draw(data['hierarchy'], img, FILE_NAME)
78 | img.save(os.path.join(os.path.curdir, 'layout', FILE_NAME + '_out.png'), 'PNG')
79 | print(os.path.join(os.path.curdir, 'layout', FILE_NAME + '_out.png'))
80 | except:
81 | print("ERROR: " + os.path.join(os.path.curdir, 'layout', FILE_NAME + '_out.png'))
82 | pass
83 |
84 |
85 | # Multi-thread parts
86 | threads = Pool(40)
87 | threads.map(work, FILE_LIST)
88 | threads.close()
89 | threads.join()
90 |
--------------------------------------------------------------------------------
/element.py:
--------------------------------------------------------------------------------
1 | class element(object):
2 | def __init__(self, hier):
3 | if hier.__class__ != dict:
4 | self.visible = False
5 | return
6 | self.visible = hier['visible-to-user']
7 | self.clickable = hier['clickable']
8 | self.bounds = hier['bounds']
9 | self.name = hier['class'].split('.')[-1]
10 | self.full_name = hier['class']
11 | self.scrollable = {'horizontal': hier['scrollable-horizontal'],
12 | 'vertical': hier['scrollable-vertical']}
13 | self.resource_id = hier['resource-id'].split('/')[-1] if 'resource-id' in hier else None
14 | self.text = hier['text'] if 'text' in hier else None
15 | self.children = [element(i) for i in hier['children']] if 'children' in hier else list()
16 |
17 | # Exception postprocess
18 | # - if children is invisible, delete it
19 | wait_del = list()
20 | if self.children is not None:
21 | for child in self.children:
22 | if not child.visible:
23 | wait_del.append(child)
24 |
25 | for it in wait_del:
26 | self.children.remove(it)
27 |
28 | # Exception postprocess
29 | # - if DrawerLayout has 2+ layout/views (which means, drawer has opened),
30 | # delete original layout/view (which priors to the drawer view)
31 | if self.name.count("DrawerLayout") and len(self.children) > 1:
32 | self.children.remove(self.children[0])
33 |
34 | # Exception postprocess
35 | # - if SlidingMenu has 2+ layout/views (which means, slide has opened),
36 | # delete original layout/view (which priors to the slide view)
37 | if self.name == "SlidingMenu" and len(self.children) > 1:
38 | self.children.remove(self.children[1])
39 |
40 | # Exception postprocess
41 | # - if element's width/height is under 0 (which is confusing),
42 | # delete the element
43 | if self.children is not None:
44 | for child in self.children:
45 | if (child.bounds[2] - child.bounds[0]) < 0 or (child.bounds[3] - child.bounds[1]) < 0:
46 | self.children.remove(child)
47 |
48 | # Exception postprocess
49 | # - if FanView has 2+ layout/views (which means, fan has opened),
50 | # delete original layout/view (which priors to the slide view)
51 | if self.name == "FanView" and len(self.children[0].children) > 1:
52 | self.children[0].children = [self.children[0].children[0]]
53 |
54 | def __str__(self):
55 | out = '{' \
56 | + ("visible: %s, " % self.visible) \
57 | + ("clickable: %s, " % self.clickable) \
58 | + ("bounds: %s, " % self.bounds) \
59 | + ("name: '%s', " % self.name) \
60 | + ("full_name: '%s', " % self.full_name) \
61 | + ("scrollable: %s, " % self.scrollable) \
62 | + ("resource_id: %s, " % ("'" + self.resource_id + "'" if self.resource_id is not None else "None")) \
63 | + ("text: %s" % ("'" + self.text + "'" if self.text is not None else "None")) \
64 | + ("children: %s" % self.children) \
65 | + '}'
66 |
67 | return out
68 |
69 | def __repr__(self):
70 | return self.__str__()
71 |
72 | def to_dict(self):
73 | out = {'visible': self.visible,
74 | 'clickable': self.clickable,
75 | 'bounds': self.bounds,
76 | 'name': self.name,
77 | 'full_name': self.full_name,
78 | 'scrollable': self.scrollable,
79 | 'resource_id': self.resource_id,
80 | 'text': self.text,
81 | 'children': [child.to_dict() for child in self.children] if self.children is not None else None}
82 |
83 | return out
84 |
--------------------------------------------------------------------------------