Skip to content

Commit 6fd360e

Browse files
authored
Merge pull request #1 from Oxid15/develop
0.1.0
2 parents ca3eea9 + cb88fdc commit 6fd360e

File tree

8 files changed

+192
-160
lines changed

8 files changed

+192
-160
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,35 @@
11
# ArtworkRetrievalDataGen
2+
23
Artwork image retrieval synthetic data generator
4+
5+
## Installation
6+
7+
You will need to have `blender` 3.5 installed.
8+
9+
Import image as planes addon is used and needs to be activated.
10+
11+
To install the project, clone the repo.
12+
13+
```bash
14+
git clone https://github.com/Oxid15/ArtworkRetrievalDataGen.git
15+
```
16+
17+
## Usage
18+
19+
1. Create the folder with source images and the one for results
20+
21+
```bash
22+
mkdir src
23+
mkdir dst
24+
```
25+
26+
2. Adjust parameters in the file `generate.py`
27+
3. Run the rendering
28+
29+
```bash
30+
<path-to-your-blender>/blender -b -P <path-to-this-repo>/ArtworkRetrievalDataGen/generate.py
31+
```
32+
33+
4. Review the results in the destination folder
34+
35+
![Alt text](./examples/00000.png)

config.yaml

Lines changed: 0 additions & 15 deletions
This file was deleted.

datagen/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .generator import Generator
2+
3+
__version__ = "0.1.0"
4+
__author__ = "Ilia Moiseev"
5+
__author_email__ = "ilia.moiseev.5@yandex.ru"

datagen/generator.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import os
2+
3+
import bpy
4+
5+
from .utils import *
6+
7+
8+
class Generator:
9+
def __init__(
10+
self,
11+
src,
12+
dest,
13+
camera_angles_range=((90, 0), (90, 0)),
14+
camera_distance_range=(5, 5),
15+
render_per_input=1,
16+
) -> None:
17+
self.src = src
18+
self.dest = dest
19+
self.camera_angles_range = camera_angles_range
20+
self.camera_distance_range = camera_distance_range
21+
self.render_per_input = render_per_input
22+
23+
self.parse_images()
24+
25+
def parse_images(self):
26+
self.img_names = os.listdir(self.src)
27+
28+
def _clear_scene(self):
29+
while len(bpy.data.objects):
30+
obj = bpy.data.objects[-1]
31+
obj.select_set(True)
32+
bpy.ops.object.delete(use_global=False)
33+
34+
def _add_camera(self):
35+
ang = self.camera_angles_range
36+
first_min, first_max = ang[0][0], ang[1][0]
37+
second_min, second_max = ang[0][1], ang[1][1]
38+
first_ang = np.random.random() * (first_max - first_min) + first_min
39+
second_ang = np.random.random() * (second_max - second_min) + second_min
40+
41+
d_max, d_min = self.camera_distance_range
42+
dist = np.random.random() * (d_max - d_min) + d_min
43+
44+
loc = spherical2cartesian((deg2rad(first_ang), deg2rad(second_ang)), dist)
45+
46+
bpy.ops.object.camera_add(
47+
align="VIEW",
48+
enter_editmode=False,
49+
location=loc,
50+
rotation=((0.0, 0.0, 0.0)),
51+
)
52+
53+
cam = bpy.data.objects["Camera"]
54+
constraint = cam.constraints.new(type="LIMIT_LOCATION")
55+
constraint.use_min_z = True
56+
57+
constraint = cam.constraints.new("TRACK_TO")
58+
constraint.target = self.image
59+
constraint.track_axis = "TRACK_NEGATIVE_Z"
60+
constraint.up_axis = "UP_Y"
61+
62+
def _add_lights(self):
63+
bpy.ops.object.light_add(
64+
type="POINT", radius=1, align="VIEW", location=(2, -2, 2)
65+
)
66+
67+
def _add_objects(self, img_name):
68+
bpy.ops.import_image.to_plane(
69+
files=[{"name": img_name}],
70+
directory=self.src,
71+
filter_image=True,
72+
filter_movie=True,
73+
filter_folder=False,
74+
relative=False,
75+
location=(0, 0, 0),
76+
)
77+
name = img_name.split(".")[0]
78+
self.image = bpy.data.objects[name]
79+
self.image.rotation_euler[0] = deg2rad(90)
80+
self.image.rotation_euler[2] = deg2rad(90)
81+
82+
# Wall
83+
bpy.ops.mesh.primitive_plane_add(
84+
size=1, align="VIEW", enter_editmode=False, location=(-0.01, 0, 0)
85+
)
86+
plane = bpy.data.objects["Plane"]
87+
plane.scale[0] = 1000
88+
plane.scale[1] = 1000
89+
plane.rotation_euler[0] = deg2rad(90)
90+
plane.rotation_euler[2] = deg2rad(90)
91+
92+
bpy.ops.material.new()
93+
bpy.data.materials[-1].node_tree.nodes["Principled BSDF"].inputs[
94+
0
95+
].default_value = np.random.random(4)
96+
plane.data.materials.append(bpy.data.materials[-1])
97+
98+
# Floor
99+
bpy.ops.mesh.primitive_plane_add(
100+
size=1, align="VIEW", enter_editmode=False, location=(0, 0, -0.7)
101+
)
102+
plane = bpy.data.objects["Plane.001"]
103+
plane.scale[0] = 100
104+
plane.scale[1] = 100
105+
106+
bpy.ops.material.new()
107+
bpy.data.materials[-1].node_tree.nodes["Principled BSDF"].inputs[
108+
0
109+
].default_value = np.random.random(4)
110+
plane.data.materials.append(bpy.data.materials[-1])
111+
112+
def _arrange_scene(self, img_name):
113+
self._clear_scene()
114+
self._add_lights()
115+
self._add_objects(img_name)
116+
self._add_camera()
117+
118+
def _render(self, index, img_name):
119+
bpy.context.scene.camera = bpy.data.objects["Camera"]
120+
121+
base, ext = img_name.split(os.extsep)
122+
img_name = base + "_{0:0>5d}".format(index)
123+
".".join((img_name, ext))
124+
125+
bpy.context.scene.render.filepath = os.path.join(self.dest, img_name)
126+
bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)
127+
128+
def run(self):
129+
for name in self.img_names:
130+
for i in range(self.render_per_input):
131+
self._arrange_scene(name)
132+
self._render(i, name)
File renamed without changes.

examples/00000.png

2.13 MB
Loading

generate.py

Lines changed: 21 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,35 @@
11
import os
22
import sys
3-
import argparse
4-
import yaml
5-
6-
import bpy
73

84
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
5+
BASE_DIR = os.path.dirname(SCRIPT_DIR)
96
sys.path.append(SCRIPT_DIR)
107

11-
from utils import *
12-
13-
14-
class Generator():
15-
def parse_images(self):
16-
self.img_names = os.listdir(self.src)
17-
18-
def _parse_config(self, config_path):
19-
with open(config_path, 'r') as f:
20-
cfg = yaml.safe_load(f)
21-
22-
# Required parameters
23-
if 'source_folder' in cfg:
24-
self.src = cfg['source_folder']
25-
else:
26-
raise KeyError('"source_folder" is not found in {config_path}')
27-
28-
if 'destination_folder' in cfg:
29-
self.dest = cfg['destination_folder']
30-
else:
31-
raise KeyError('"destination_folder" is not found in {config_path}')
32-
33-
# Not required parameters
34-
if 'camera_angles_range' in cfg:
35-
self.camera_angles_range = [[b for b in a] for a in cfg['camera_angles_range']]
36-
else:
37-
self.camera_angles_range = ((90, 0), (90, 0))
38-
39-
if 'camera_distance_range' in cfg:
40-
self.camera_distance_range = cfg['camera_distance_range']
41-
else:
42-
self.camera_distance_range = (5, 5)
43-
44-
if 'render_per_input' in cfg:
45-
self.render_per_input = cfg['render_per_input']
46-
else:
47-
self.render_per_input = 1
48-
49-
self.parse_images()
50-
51-
def __init__(self, config_path) -> None:
52-
self._parse_config(config_path)
53-
54-
def _clear_scene(self):
55-
while(len(bpy.data.objects)):
56-
obj = bpy.data.objects[-1]
57-
obj.select = True
58-
bpy.ops.object.delete(use_global=False)
59-
60-
def _add_camera(self):
61-
ang = self.camera_angles_range
62-
first_min, first_max = ang[0][0], ang[1][0]
63-
second_min, second_max = ang[0][1], ang[1][1]
64-
first_ang = np.random.random() * (first_max - first_min) + first_min
65-
second_ang = np.random.random() * (second_max - second_min) + second_min
66-
67-
d_max, d_min = self.camera_distance_range
68-
dist = np.random.random() * (d_max - d_min) + d_min
69-
70-
loc = spherical2cartesian((deg2rad(first_ang), deg2rad(second_ang)), dist)
71-
72-
bpy.ops.object.camera_add(
73-
view_align=True,
74-
enter_editmode=False,
75-
location=loc,
76-
rotation=((0., 0., 0.)))
77-
78-
cam = bpy.data.objects['Camera']
79-
constraint = cam.constraints.new(type='LIMIT_LOCATION')
80-
constraint.use_min_z = True
81-
82-
constraint = cam.constraints.new('TRACK_TO')
83-
constraint.target = self.image
84-
constraint.track_axis = 'TRACK_NEGATIVE_Z'
85-
constraint.up_axis = 'UP_Y'
86-
87-
def _add_lights(self):
88-
bpy.ops.object.lamp_add(type='POINT',
89-
radius=1, view_align=False,
90-
location=(2, -2, 2))
91-
92-
def _add_objects(self, img_name):
93-
bpy.ops.import_image.to_plane(
94-
files=[{"name": img_name}],
95-
directory=self.src,
96-
filter_image=True, filter_movie=True, filter_glob="", relative=False,
97-
location=(0,0,0))
98-
name = img_name.split('.')[0]
99-
self.image = bpy.data.objects[name]
100-
self.image.rotation_euler[0] = deg2rad(90)
101-
self.image.rotation_euler[2] = deg2rad(90)
102-
103-
# Wall
104-
bpy.ops.mesh.primitive_plane_add(radius=1, view_align=False, enter_editmode=False,
105-
location=(-0.01, 0, 0))
106-
plane = bpy.data.objects['Plane']
107-
y_scale = np.random.random() * 3 + 1
108-
plane.scale[0] = y_scale
109-
plane.rotation_euler[0] = deg2rad(90)
110-
plane.rotation_euler[2] = deg2rad(90)
111-
112-
bpy.ops.material.new()
113-
plane.data.materials.append(bpy.data.materials[0])
114-
plane.data.materials[0].diffuse_color = np.random.random(3)
115-
116-
# Floor
117-
bpy.ops.mesh.primitive_plane_add(radius=1, view_align=False, enter_editmode=False,
118-
location=(0, 0, -0.7))
119-
plane = bpy.data.objects['Plane.001']
120-
plane.scale[0] = 10
121-
plane.scale[1] = 10
122-
123-
bpy.ops.material.new()
124-
plane.data.materials.append(bpy.data.materials[1])
125-
plane.data.materials[0].diffuse_color = np.random.random(3)
126-
8+
from datagen import Generator
1279

128-
def _arrange_scene(self, img_name):
129-
self._clear_scene()
130-
self._add_lights()
131-
self._add_objects(img_name)
132-
self._add_camera()
13310

134-
def _render(self, index, img_name):
135-
bpy.context.scene.camera = bpy.data.objects['Camera']
11+
# The folder with images to render
12+
source_folder = os.path.join(BASE_DIR, "src")
13613

137-
base, ext = img_name.split(os.extsep)
138-
img_name = base + "_{0:0>5d}".format(index)
139-
'.'.join((img_name, ext))
14+
# The folder to place renders
15+
destination_folder = os.path.join(BASE_DIR, "dst")
14016

141-
bpy.context.scene.render.filepath = os.path.join(self.dest, img_name)
142-
bpy.ops.render.render('INVOKE_DEFAULT', write_still=True)
17+
# The angles and distance are the spherical coordinates of the camera
18+
# given that the position of the object in the scene is (0,0,0)
19+
# render_per_input times different angles and distances are sampled
20+
# from these ranges
21+
camera_angles_range = [[70, -60], [110, 60]]
14322

144-
def mainloop(self):
145-
for name in self.img_names:
146-
for i in range(self.render_per_input):
147-
self._arrange_scene(name)
148-
self._render(i, name)
23+
camera_distance_range = [3, 5]
14924

25+
render_per_input = 2
15026

151-
argv = sys.argv[sys.argv.index('--') + 1:]
152-
parser = argparse.ArgumentParser()
153-
parser.add_argument('-c', '--cfg', type=str)
154-
args = parser.parse_known_args(argv)[0]
15527

156-
if __name__ == '__main__':
157-
gen = Generator(args.cfg)
158-
gen.mainloop()
28+
if __name__ == "__main__":
29+
gen = Generator(
30+
src=source_folder,
31+
dest=destination_folder,
32+
camera_angles_range=camera_angles_range,
33+
camera_distance_range=camera_distance_range,
34+
render_per_input=render_per_input,
35+
).run()

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
pyyaml
1+
bpy==3.5.0

0 commit comments

Comments
 (0)