forked from youen/assembly_handbook
raster rendering optimizations
This commit is contained in:
parent
ec97d10a51
commit
5e65e68c13
@ -1,5 +1,13 @@
|
|||||||
import FreeCAD as App
|
import FreeCAD as App
|
||||||
import FreeCADGui as Gui
|
import FreeCADGui as Gui
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def print_verbose(msg):
|
||||||
|
verbose = False
|
||||||
|
if verbose:
|
||||||
|
now = datetime.now()
|
||||||
|
current_time = now.strftime("%H:%M:%S")
|
||||||
|
print(current_time, msg)
|
||||||
|
|
||||||
class RasterView:
|
class RasterView:
|
||||||
def __init__(self, view):
|
def __init__(self, view):
|
||||||
@ -116,7 +124,7 @@ class RasterView:
|
|||||||
|
|
||||||
self.init_image()
|
self.init_image()
|
||||||
|
|
||||||
print('Rasterizing ' + view.Label + " to " + self.image_file_name + "...")
|
print_verbose('Rasterizing ' + view.Label + " to " + self.image_file_name + "...")
|
||||||
|
|
||||||
dir = os.path.dirname(self.image_file_name)
|
dir = os.path.dirname(self.image_file_name)
|
||||||
if not os.path.exists(dir):
|
if not os.path.exists(dir):
|
||||||
@ -127,6 +135,7 @@ class RasterView:
|
|||||||
objects_to_reset = {}
|
objects_to_reset = {}
|
||||||
duplicated_parts = {}
|
duplicated_parts = {}
|
||||||
try:
|
try:
|
||||||
|
print_verbose("Preparing scene...")
|
||||||
# construct new scene with links to the parts we want
|
# construct new scene with links to the parts we want
|
||||||
sceneGroup = tmp_doc.addObject('App::DocumentObjectGroup', 'Scene')
|
sceneGroup = tmp_doc.addObject('App::DocumentObjectGroup', 'Scene')
|
||||||
prev_parts = []
|
prev_parts = []
|
||||||
@ -259,20 +268,25 @@ class RasterView:
|
|||||||
resolution[1] = int(max_res)
|
resolution[1] = int(max_res)
|
||||||
|
|
||||||
if fast_render:
|
if fast_render:
|
||||||
|
print_verbose("Fast rasterization...")
|
||||||
composite_img = self._render_lines(tmp_doc, resolution, prev_parts + new_parts, (0.0, 0.0, 0.0), [])
|
composite_img = self._render_lines(tmp_doc, resolution, prev_parts + new_parts, (0.0, 0.0, 0.0), [])
|
||||||
else:
|
else:
|
||||||
# render old parts in gray lines
|
# render old parts in gray lines
|
||||||
|
print_verbose("Rendering old parts (gray)...")
|
||||||
prev_parts_img = self._render_lines(tmp_doc, resolution, prev_parts, (0.6, 0.6, 0.6), [], fast_render)
|
prev_parts_img = self._render_lines(tmp_doc, resolution, prev_parts, (0.6, 0.6, 0.6), [], fast_render)
|
||||||
|
|
||||||
# render new parts in black lines (old parts can mask them)
|
# render new parts in black lines (old parts can mask them)
|
||||||
|
print_verbose("Rendering new parts (black)...")
|
||||||
new_parts_img = self._render_lines(tmp_doc, resolution, new_parts, (0.0, 0.0, 0.0), prev_parts, fast_render)
|
new_parts_img = self._render_lines(tmp_doc, resolution, new_parts, (0.0, 0.0, 0.0), prev_parts, fast_render)
|
||||||
|
|
||||||
# create the composite image
|
# create the composite image
|
||||||
|
print_verbose("Compositing images...")
|
||||||
composite_img = prev_parts_img.copy()
|
composite_img = prev_parts_img.copy()
|
||||||
composite_img.paste(new_parts_img, None, new_parts_img)
|
composite_img.paste(new_parts_img, None, new_parts_img)
|
||||||
|
|
||||||
# Optimize the image to reduce storage size
|
# Optimize the image to reduce storage size
|
||||||
if not fast_render:
|
if not fast_render:
|
||||||
|
print_verbose("Optimizing PNG size...")
|
||||||
num_colors = 32
|
num_colors = 32
|
||||||
|
|
||||||
# All-or-nothing alpha: we use a white background and only make pixels fully transparent where alpha is zero, to not loose antialiasing
|
# All-or-nothing alpha: we use a white background and only make pixels fully transparent where alpha is zero, to not loose antialiasing
|
||||||
@ -286,6 +300,7 @@ class RasterView:
|
|||||||
composite_img = composite_img.quantize(colors=num_colors, dither=Image.Dither.NONE)
|
composite_img = composite_img.quantize(colors=num_colors, dither=Image.Dither.NONE)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
print_verbose("Cleaning scene...")
|
||||||
#raise Exception("test")
|
#raise Exception("test")
|
||||||
# restore properties on objects we have modified
|
# restore properties on objects we have modified
|
||||||
for obj, props in objects_to_reset.items():
|
for obj, props in objects_to_reset.items():
|
||||||
@ -302,6 +317,8 @@ class RasterView:
|
|||||||
# remove the temporary document
|
# remove the temporary document
|
||||||
App.closeDocument(tmp_doc.Name)
|
App.closeDocument(tmp_doc.Name)
|
||||||
|
|
||||||
|
print_verbose("Finalizing view...")
|
||||||
|
|
||||||
# Crop the image, which is also used to deduce the center of the source view
|
# Crop the image, which is also used to deduce the center of the source view
|
||||||
original_size = composite_img.size
|
original_size = composite_img.size
|
||||||
|
|
||||||
@ -350,6 +367,8 @@ class RasterView:
|
|||||||
image.Height = composite_img.size[1] * image_scale / 10.0 * 1.01
|
image.Height = composite_img.size[1] * image_scale / 10.0 * 1.01
|
||||||
image.recompute()
|
image.recompute()
|
||||||
|
|
||||||
|
print_verbose("Done")
|
||||||
|
|
||||||
def _render_lines(self, doc, resolution, parts, line_color, masking_parts, fast_render = True):
|
def _render_lines(self, doc, resolution, parts, line_color, masking_parts, fast_render = True):
|
||||||
import tempfile
|
import tempfile
|
||||||
from PIL import Image, ImageDraw, ImageFilter
|
from PIL import Image, ImageDraw, ImageFilter
|
||||||
@ -358,12 +377,17 @@ class RasterView:
|
|||||||
|
|
||||||
# render lines in black, background in red, fill shapes in green
|
# render lines in black, background in red, fill shapes in green
|
||||||
# the green band contains the lines images, the red band contains the inverted alpha layer
|
# the green band contains the lines images, the red band contains the inverted alpha layer
|
||||||
|
configured = []
|
||||||
|
print_verbose('Preparing objects for line rendering...')
|
||||||
for link in doc.findObjects():
|
for link in doc.findObjects():
|
||||||
if link in parts or link in masking_parts:
|
if link in parts or link in masking_parts:
|
||||||
link.ViewObject.Visibility = True
|
link.ViewObject.Visibility = True
|
||||||
|
|
||||||
# in current version of freecad, link override material does not allow to override all material properties, for example emissive color, so we have to change material of the linked object
|
# in current version of freecad, link override material does not allow to override all material properties, for example emissive color, so we have to change material of the linked object
|
||||||
for obj in self._flatten_objects_tree([link]):
|
for obj in self._flatten_objects_tree([link]):
|
||||||
|
if obj in configured: continue
|
||||||
|
configured.append(obj)
|
||||||
|
|
||||||
if self._should_render(obj) and not fast_render:
|
if self._should_render(obj) and not fast_render:
|
||||||
obj.ViewObject.LineColor = (0.0, 0.0, 0.0, 0.0) if link in parts else (1.0, 0.0, 1.0)
|
obj.ViewObject.LineColor = (0.0, 0.0, 0.0, 0.0) if link in parts else (1.0, 0.0, 1.0)
|
||||||
obj.ViewObject.ShapeMaterial.AmbientColor = (0.0, 0.0, 0.0, 0.0)
|
obj.ViewObject.ShapeMaterial.AmbientColor = (0.0, 0.0, 0.0, 0.0)
|
||||||
@ -377,6 +401,7 @@ class RasterView:
|
|||||||
else:
|
else:
|
||||||
link.ViewObject.Visibility = False
|
link.ViewObject.Visibility = False
|
||||||
|
|
||||||
|
print_verbose('Rendering lines...')
|
||||||
temp_file_name = tempfile.gettempdir() + "/ahb_temp_image.png"
|
temp_file_name = tempfile.gettempdir() + "/ahb_temp_image.png"
|
||||||
doc_view.saveImage(temp_file_name, resolution[0]+2, resolution[1]+2, "#ff0000") # we add 1 pixel border that we will need to crop later
|
doc_view.saveImage(temp_file_name, resolution[0]+2, resolution[1]+2, "#ff0000") # we add 1 pixel border that we will need to crop later
|
||||||
lines_bands_img = self._read_image(temp_file_name)
|
lines_bands_img = self._read_image(temp_file_name)
|
||||||
@ -390,14 +415,20 @@ class RasterView:
|
|||||||
# Render all shapes with different colors, in order to extract outlines (where color changes)
|
# Render all shapes with different colors, in order to extract outlines (where color changes)
|
||||||
# This is needed because FreeCAD does not render lines on the boundary of curve shapes, such as spheres or cylinders
|
# This is needed because FreeCAD does not render lines on the boundary of curve shapes, such as spheres or cylinders
|
||||||
# The technique could be improved by using the depth buffer instead, in order to detect boundaries within the same object
|
# The technique could be improved by using the depth buffer instead, in order to detect boundaries within the same object
|
||||||
|
print_verbose('Preparing objects for outline rendering...')
|
||||||
step = 8
|
step = 8
|
||||||
r = step
|
r = step
|
||||||
g = step
|
g = step
|
||||||
b = step
|
b = step
|
||||||
|
configured = []
|
||||||
for link in doc.findObjects():
|
for link in doc.findObjects():
|
||||||
if link in parts or link in masking_parts:
|
if link in parts or link in masking_parts:
|
||||||
for obj in self._flatten_objects_tree([link]):
|
for obj in self._flatten_objects_tree([link]):
|
||||||
|
if obj in configured: continue
|
||||||
|
configured.append(obj)
|
||||||
|
|
||||||
if self._should_render(obj) and obj.TypeId != 'Part::Part2DObjectPython':
|
if self._should_render(obj) and obj.TypeId != 'Part::Part2DObjectPython':
|
||||||
|
configured.append(obj)
|
||||||
obj.ViewObject.DisplayMode = 'Shaded'
|
obj.ViewObject.DisplayMode = 'Shaded'
|
||||||
obj.ViewObject.ShapeMaterial.AmbientColor = (0.0, 0.0, 0.0, 0.0)
|
obj.ViewObject.ShapeMaterial.AmbientColor = (0.0, 0.0, 0.0, 0.0)
|
||||||
obj.ViewObject.ShapeMaterial.DiffuseColor = (0.0, 0.0, 0.0, 0.0)
|
obj.ViewObject.ShapeMaterial.DiffuseColor = (0.0, 0.0, 0.0, 0.0)
|
||||||
@ -416,9 +447,11 @@ class RasterView:
|
|||||||
else:
|
else:
|
||||||
obj.ViewObject.Visibility = False
|
obj.ViewObject.Visibility = False
|
||||||
|
|
||||||
|
print_verbose('Rendering shapes...')
|
||||||
doc_view.saveImage(temp_file_name, (resolution[0]+2)*2, (resolution[1]+2)*2, "#ffffff") # shapes are rendered at twice the resolution for antialiasing
|
doc_view.saveImage(temp_file_name, (resolution[0]+2)*2, (resolution[1]+2)*2, "#ffffff") # shapes are rendered at twice the resolution for antialiasing
|
||||||
shapes_img = self._read_image(temp_file_name)
|
shapes_img = self._read_image(temp_file_name)
|
||||||
|
|
||||||
|
print_verbose('Extracting outlines...')
|
||||||
outlines_img = None
|
outlines_img = None
|
||||||
for x in range(0, 3):
|
for x in range(0, 3):
|
||||||
for y in range(0, 3):
|
for y in range(0, 3):
|
||||||
@ -434,6 +467,7 @@ class RasterView:
|
|||||||
else:
|
else:
|
||||||
outlines_img.paste(partial_outlines, None, partial_outlines.point(lambda p: 0 if p == 255 else 255))
|
outlines_img.paste(partial_outlines, None, partial_outlines.point(lambda p: 0 if p == 255 else 255))
|
||||||
|
|
||||||
|
print_verbose('Combining lines and outlines...')
|
||||||
lines_fullres = lines_img.resize(outlines_img.size, Image.NEAREST)
|
lines_fullres = lines_img.resize(outlines_img.size, Image.NEAREST)
|
||||||
lines_fullres.paste(outlines_img, None, outlines_img.point(lambda p: 255 if p == 0 else 0))
|
lines_fullres.paste(outlines_img, None, outlines_img.point(lambda p: 255 if p == 0 else 0))
|
||||||
#lines_fullres.paste(255, alpha_fullres.point(lambda p: 255 if p == 0 else 0))
|
#lines_fullres.paste(255, alpha_fullres.point(lambda p: 255 if p == 0 else 0))
|
||||||
@ -448,6 +482,7 @@ class RasterView:
|
|||||||
alpha_img = alpha_img.point(lambda p: 0 if p == 0 else 255)
|
alpha_img = alpha_img.point(lambda p: 0 if p == 0 else 255)
|
||||||
|
|
||||||
# colorize final image
|
# colorize final image
|
||||||
|
print_verbose('Colorizing image...')
|
||||||
fill_color = (1.0, 1.0, 1.0)
|
fill_color = (1.0, 1.0, 1.0)
|
||||||
result = Image.merge("RGBA", [
|
result = Image.merge("RGBA", [
|
||||||
all_lines.point(lambda p: int(fill_color[0] * p + line_color[0] * (255.0 - p))),
|
all_lines.point(lambda p: int(fill_color[0] * p + line_color[0] * (255.0 - p))),
|
||||||
|
@ -640,6 +640,10 @@ class TechDrawExtensions:
|
|||||||
def refreshOverlays(self, page, callback = None):
|
def refreshOverlays(self, page, callback = None):
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
for view in page.Views:
|
||||||
|
if view.TypeId == 'TechDraw::DrawViewPart' and 'Assembly_handbook_RasterView' in view.PropertiesList and view.Assembly_handbook_RasterView:
|
||||||
|
view.purgeTouched() # make sure we don't trigger rendering of source views (this is awfully slow and doesn't even work for a lot of models)
|
||||||
|
|
||||||
doc = page.Document
|
doc = page.Document
|
||||||
for image in page.Views:
|
for image in page.Views:
|
||||||
if image.TypeId == 'TechDraw::DrawViewImage':
|
if image.TypeId == 'TechDraw::DrawViewImage':
|
||||||
|
Loading…
Reference in New Issue
Block a user