Skip to content

Commit 9094303

Browse files
committed
automatic deps install + disable ms telemetry
1 parent 26d760d commit 9094303

File tree

4 files changed

+234
-125
lines changed

4 files changed

+234
-125
lines changed

__init__.py

Lines changed: 185 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,74 @@
1-
import bpy
2-
from bpy.types import (Panel, Operator, PropertyGroup)
3-
from bpy.props import (EnumProperty, PointerProperty)
4-
import pathlib
5-
import os
6-
7-
from . import infer
8-
if "bpy" in locals():
9-
import importlib
10-
importlib.reload(infer)
11-
121
bl_info = {
132
'name': 'DeepBump',
143
'description': 'Generates normal maps from image textures',
154
'author': 'Hugo Tini',
16-
'version': (0, 1, 0),
17-
'blender': (2, 82, 0),
5+
'version': (2, 0, 0),
6+
'blender': (3, 0, 0),
187
'location': 'Node Editor > DeepBump',
198
'category': 'Material',
20-
'warning': 'Extra Python dependencies must be installed first, check the readme.'
9+
'warning': 'Requires installation of dependencies',
10+
'doc_url': 'https://github.com/HugoTini/DeepBump/blob/master/readme.md'
2111
}
2212

13+
14+
import bpy
15+
from bpy.types import (Panel, Operator, PropertyGroup)
16+
from bpy.props import (EnumProperty, PointerProperty)
17+
import pathlib
18+
import os
19+
import subprocess
20+
import sys
21+
import importlib
22+
from collections import namedtuple
23+
24+
2325
# ------------------------------------------------------------------------
24-
# Scene Properties
26+
# Dependencies management utils
27+
# ------------------------------------------------------------------------
28+
29+
30+
# Python dependencies management helpers from :
31+
# https://github.com/robertguetzkow/blender-python-examples/tree/master/add_ons/install_dependencies
32+
Dependency = namedtuple('Dependency', ['module', 'package', 'name'])
33+
dependencies = (Dependency(module='onnxruntime', package=None, name='ort'),
34+
Dependency(module='numpy', package=None, name='np'))
35+
dependencies_installed = False
36+
37+
38+
def import_module(module_name, global_name=None, reload=True):
39+
if global_name is None:
40+
global_name = module_name
41+
if global_name in globals():
42+
importlib.reload(globals()[global_name])
43+
else:
44+
globals()[global_name] = importlib.import_module(module_name)
45+
46+
47+
def install_pip():
48+
try:
49+
# Check if pip is already installed
50+
subprocess.run([sys.executable, '-m', 'pip', '--version'], check=True)
51+
except subprocess.CalledProcessError:
52+
import ensurepip
53+
ensurepip.bootstrap()
54+
os.environ.pop("PIP_REQ_TRACKER", None)
55+
56+
57+
def install_and_import_module(module_name, package_name=None, global_name=None):
58+
if package_name is None:
59+
package_name = module_name
60+
if global_name is None:
61+
global_name = module_name
62+
# Create a copy of the environment variables and modify them for the subprocess call
63+
environ_copy = dict(os.environ)
64+
environ_copy['PYTHONNOUSERSITE'] = '1'
65+
subprocess.run([sys.executable, '-m', 'pip', 'install', package_name], check=True, env=environ_copy)
66+
# The installation succeeded, attempt to import the module again
67+
import_module(module_name, global_name)
68+
69+
70+
# ------------------------------------------------------------------------
71+
# Scene properties
2572
# ------------------------------------------------------------------------
2673

2774

@@ -35,120 +82,115 @@ class DeepBumpProperties(PropertyGroup):
3582
('BIG', 'Big', 'Big overlap between tiles.')]
3683
)
3784

85+
3886
# ------------------------------------------------------------------------
3987
# Operators
4088
# ------------------------------------------------------------------------
4189

4290

43-
class WM_OT_DeepBumpOperator(Operator):
44-
'''Generates a normal map from an image. Settings in DeepBump panel (Node Editor)'''
45-
91+
class DEEPBUMP_OT_DeepBumpOperator(Operator):
92+
bl_idname = 'deepbump.operator'
4693
bl_label = 'DeepBump'
47-
bl_idname = 'wm.deep_bump'
94+
bl_description = ('Generates a normal map from an image.'
95+
'Settings are in DeepBump panel (Node Editor)')
96+
4897
progress_started = False
4998
ort_session = None
5099

51100
@classmethod
52101
def poll(self, context):
53-
selected_node_type = context.active_node.bl_idname
54-
return (context.area.type == 'NODE_EDITOR') and (selected_node_type == 'ShaderNodeTexImage')
102+
if context.active_node is not None :
103+
selected_node_type = context.active_node.bl_idname
104+
return (context.area.type == 'NODE_EDITOR') and (selected_node_type == 'ShaderNodeTexImage')
105+
return False
55106

56107
def progress_print(self, current, total):
57108
wm = bpy.context.window_manager
58109
if self.progress_started:
59110
wm.progress_update(current)
60-
print('{}/{}'.format(current, total))
111+
print(f'DeepBump : {current}/{total}')
61112
else:
62113
wm.progress_begin(0, total)
63114
self.progress_started = True
64115

65116
def execute(self, context):
66-
# make sure dependencies are installed
67-
try:
68-
import numpy as np
69-
import onnxruntime as ort
70-
from . import infer
71-
except ImportError as e:
72-
self.report({'WARNING'}, 'Dependencies missing, check readme.')
73-
print(e)
74-
return {'CANCELLED'}
75-
76-
# get input image from selected node
117+
# Get input image from selected node
77118
input_node = context.active_node
78119
input_img = input_node.image
79-
if input_img == None:
120+
if input_img is None:
80121
self.report(
81122
{'WARNING'}, 'Selected image node must have an image assigned to it.')
82123
return {'CANCELLED'}
83124

84-
# convert to C,H,W numpy array
125+
# Convert to C,H,W numpy array
85126
width = input_img.size[0]
86127
height = input_img.size[1]
87128
channels = input_img.channels
88129
img = np.array(input_img.pixels)
89130
img = np.reshape(img, (channels, width, height), order='F')
90131
img = np.transpose(img, (0, 2, 1))
91-
# flip height
132+
# Flip height
92133
img = np.flip(img, axis=1)
93134

94-
# remove alpha & convert to grayscale
135+
# Remove alpha & convert to grayscale
95136
img = np.mean(img[0:3], axis=0, keepdims=True)
96137

97-
# split image in tiles
138+
# Split image in tiles
98139
tile_size = (256, 256)
99140
OVERLAP = context.scene.deep_bump_tool.tiles_overlap_enum
100141
overlaps = {'SMALL': 20, 'MEDIUM': 50, 'BIG': 124}
101142
stride_size = (tile_size[0]-overlaps[OVERLAP],
102143
tile_size[1]-overlaps[OVERLAP])
103-
print('tilling')
144+
print('DeepBump : tilling')
104145
tiles, paddings = infer.tiles_split(img, tile_size, stride_size)
105146

106-
# load model (if not already loaded)
107-
if self.ort_session == None:
108-
print('loading model')
147+
# Load model (if not already loaded)
148+
if self.ort_session is None:
149+
print('DeepBump : loading model')
109150
addon_path = str(pathlib.Path(__file__).parent.absolute())
110151
self.ort_session = ort.InferenceSession(
111152
addon_path+'/deepbump256.onnx')
153+
self.ort_session
112154

113-
# predict normal map for each tile
114-
print('generating')
155+
# Predict normal map for each tile
156+
print('DeepBump : generating')
115157
self.progress_started = False
116158
pred_tiles = infer.tiles_infer(
117159
tiles, self.ort_session, progress_callback=self.progress_print)
118160

119-
# merge tiles
120-
print('merging')
161+
# Merge tiles
162+
print('DeepBump : merging')
121163
pred_img = infer.tiles_merge(
122164
pred_tiles, stride_size, (3, img.shape[1], img.shape[2]), paddings)
123165

124-
# normalize each pixel to unit vector
166+
# Normalize each pixel to unit vector
125167
pred_img = infer.normalize(pred_img)
126168

127-
# create new image datablock
169+
# Create new image datablock
128170
img_name = os.path.splitext(input_img.name)
129171
normal_name = img_name[0] + '_normal' + img_name[1]
130172
normal_img = bpy.data.images.new(
131173
normal_name, width=width, height=height)
132174
normal_img.colorspace_settings.name = 'Non-Color'
133175

134-
# flip height
176+
# Flip height
135177
pred_img = np.flip(pred_img, axis=1)
136-
# add alpha channel
178+
# Add alpha channel
137179
pred_img = np.concatenate(
138180
[pred_img, np.ones((1, height, width))], axis=0)
139-
# flatten to array
181+
# Flatten to array
140182
pred_img = np.transpose(pred_img, (0, 2, 1)).flatten('F')
141-
# write to image block
183+
# Write to image block
142184
normal_img.pixels = pred_img
143185

144-
# create new node for normal map
186+
# Create new node for normal map
145187
normal_node = context.material.node_tree.nodes.new(
146188
type='ShaderNodeTexImage')
147189
normal_node.location = input_node.location
148190
normal_node.location[1] -= input_node.width*1.2
149191
normal_node.image = normal_img
150192

151-
# create normal vector node & link nodes
193+
# Create normal vector node & link nodes
152194
normal_vec_node = context.material.node_tree.nodes.new(
153195
type='ShaderNodeNormalMap')
154196
normal_vec_node.location = normal_node.location
@@ -157,24 +199,57 @@ def execute(self, context):
157199
links.new(normal_node.outputs['Color'],
158200
normal_vec_node.inputs['Color'])
159201

160-
# if input image was linked to a BSDF, link to BSDF normal slot
202+
# If input image was linked to a BSDF, link to BSDF normal slot
161203
if input_node.outputs['Color'].is_linked:
162204
if len(input_node.outputs['Color'].links) == 1:
163205
to_node = input_node.outputs['Color'].links[0].to_node
164206
if to_node.bl_idname == 'ShaderNodeBsdfPrincipled':
165207
links.new(
166208
normal_vec_node.outputs['Normal'], to_node.inputs['Normal'])
167209

210+
print('DeepBump : done')
168211
return {'FINISHED'}
169212

213+
214+
class DEEPBUMP_OT_install_dependencies(bpy.types.Operator):
215+
bl_idname = 'deepbump.install_dependencies'
216+
bl_label = 'Install dependencies'
217+
bl_description = 'Downloads and installs the required python packages for this add-on.'
218+
bl_options = {'REGISTER', 'INTERNAL'}
219+
220+
@classmethod
221+
def poll(self, context):
222+
# Deactivate when dependencies have been installed
223+
return not dependencies_installed
224+
225+
def execute(self, context):
226+
try:
227+
install_pip()
228+
for dependency in dependencies:
229+
install_and_import_module(module_name=dependency.module,
230+
package_name=dependency.package,
231+
global_name=dependency.name)
232+
except (subprocess.CalledProcessError, ImportError) as err:
233+
self.report({'ERROR'}, str(err))
234+
return {'CANCELLED'}
235+
236+
global dependencies_installed
237+
dependencies_installed = True
238+
239+
# Register the panels, operators, etc. since dependencies are installed
240+
register_functionality()
241+
242+
return {"FINISHED"}
243+
244+
170245
# ------------------------------------------------------------------------
171-
# Panel in Object Mode
246+
# UI (DeepBump panel & addon install dependencies button)
172247
# ------------------------------------------------------------------------
173248

174249

175-
class OBJECT_PT_DeepBumpPanel(Panel):
250+
class DEEPBUMP_PT_DeepBumpPanel(Panel):
251+
bl_idname = 'DEEPBUMP_PT_DeepBumpPanel'
176252
bl_label = 'DeepBump'
177-
bl_idname = 'OBJECT_PT_DeepBumpPanel'
178253
bl_space_type = 'NODE_EDITOR'
179254
bl_region_type = 'UI'
180255
bl_category = 'DeepBump'
@@ -192,31 +267,75 @@ def draw(self, context):
192267
layout.prop(deep_bump_tool, 'tiles_overlap_enum', text='')
193268

194269
layout.separator()
195-
layout.operator('wm.deep_bump', text='Generate Normal Map')
270+
layout.operator('deepbump.operator', text='Generate Normal Map')
271+
272+
273+
class DEEPBUMP_preferences(bpy.types.AddonPreferences):
274+
bl_idname = __name__
275+
276+
def draw(self, context):
277+
layout = self.layout
278+
if dependencies_installed :
279+
layout.label(text='Required dependencies are installed', icon='CHECKMARK')
280+
else :
281+
layout.label(text='Installing dependencies requires internet and might take a few minutes',
282+
icon='INFO')
283+
layout.operator(DEEPBUMP_OT_install_dependencies.bl_idname, icon='CONSOLE')
284+
196285

197286
# ------------------------------------------------------------------------
198287
# Registration
199288
# ------------------------------------------------------------------------
200289

201290

291+
# Classes for the addon actual functionality
202292
classes = (
203293
DeepBumpProperties,
204-
WM_OT_DeepBumpOperator,
205-
OBJECT_PT_DeepBumpPanel
294+
DEEPBUMP_OT_DeepBumpOperator,
295+
DEEPBUMP_PT_DeepBumpPanel
296+
)
297+
# Classes for downloading & installing dependencies
298+
preference_classes = (
299+
DEEPBUMP_OT_install_dependencies,
300+
DEEPBUMP_preferences
206301
)
207302

208303

209304
def register():
210-
from bpy.utils import register_class
305+
global dependencies_installed
306+
dependencies_installed = False
307+
308+
for cls in preference_classes:
309+
bpy.utils.register_class(cls)
310+
311+
try:
312+
for dependency in dependencies:
313+
import_module(module_name=dependency.module, global_name=dependency.name)
314+
except ModuleNotFoundError:
315+
# Don't register other panels, operators etc.
316+
return
317+
318+
dependencies_installed = True
319+
register_functionality()
320+
321+
322+
def register_functionality():
211323
for cls in classes:
212-
register_class(cls)
324+
bpy.utils.register_class(cls)
213325
bpy.types.Scene.deep_bump_tool = PointerProperty(type=DeepBumpProperties)
326+
from . import infer
327+
# Disable MS telemetry
328+
ort.disable_telemetry_events()
214329

215330

216331
def unregister():
217-
from bpy.utils import unregister_class
218-
for cls in reversed(classes):
219-
unregister_class(cls)
332+
for cls in preference_classes:
333+
bpy.utils.unregister_class(cls)
334+
335+
if dependencies_installed:
336+
for cls in classes:
337+
bpy.utils.unregister_class(cls)
338+
220339
del bpy.types.Scene.deep_bump_tool
221340

222341

0 commit comments

Comments
 (0)