Files
flatcam-wsl/appPlugins/ToolImage.py
Marius Stanciu ccc71eabc2 - changed the shapely imports a bit according to the specifications of Shapely 2.0
- changed the requirements.txt file to reflect the need for at least Shapely in version 2.0
2023-04-15 21:03:30 +03:00

986 lines
40 KiB
Python

# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 3/10/2019 #
# MIT Licence #
# ##########################################################
from PyQt6 import QtWidgets, QtGui
from appTool import AppTool
from appGUI.GUIElements import VerticalScrollArea, FCLabel, FCButton, FCFrame, GLay, FCComboBox, FCCheckBox, \
FCComboBox2, RadioSet, FCDoubleSpinner, FCSpinner, FCMessageBox
from copy import deepcopy
import numpy as np
import os
from shapely import LineString, MultiLineString, Polygon, MultiPolygon, shape
from shapely.affinity import scale, translate
import gettext
import appTranslation as fcTranslate
import builtins
from rasterio import open as rasterio_open
from rasterio.features import shapes
from svgtrace import trace
from pyppeteer.chromium_downloader import check_chromium
from lxml import etree as ET
from appParsers.ParseSVG import svgparselength, svgparse_viewbox, getsvggeo, getsvgtext
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
class ToolImage(AppTool):
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.decimals = self.app.decimals
# #############################################################################
# ######################### Tool GUI ##########################################
# #############################################################################
self.ui = ImageUI(layout=self.layout, app=self.app)
self.pluginName = self.ui.pluginName
self.connect_signals_at_init()
def run(self, toggle=True):
self.app.defaults.report_usage("ToolImage()")
if toggle:
# if the splitter is hidden, display it
if self.app.ui.splitter.sizes()[0] == 0:
self.app.ui.splitter.setSizes([1, 1])
# if the Tool Tab is hidden display it, else hide it but only if the objectName is the same
found_idx = None
for idx in range(self.app.ui.notebook.count()):
if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab":
found_idx = idx
break
# show the Tab
if not found_idx:
try:
self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
except RuntimeError:
self.app.ui.plugin_tab = QtWidgets.QWidget()
self.app.ui.plugin_tab.setObjectName("plugin_tab")
self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab)
self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2)
self.app.ui.plugin_scroll_area = VerticalScrollArea()
self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area)
self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
# focus on Tool Tab
self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab)
try:
if self.app.ui.plugin_scroll_area.widget().objectName() == self.pluginName and found_idx:
# if the Tool Tab is not focused, focus on it
if not self.app.ui.notebook.currentWidget() is self.app.ui.plugin_tab:
# focus on Tool Tab
self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab)
else:
# else remove the Tool Tab
self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
self.app.ui.notebook.removeTab(2)
# if there are no objects loaded in the app then hide the Notebook widget
if not self.app.collection.get_list():
self.app.ui.splitter.setSizes([0, 1])
except AttributeError:
pass
else:
if self.app.ui.splitter.sizes()[0] == 0:
self.app.ui.splitter.setSizes([1, 1])
super().run()
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Image Import"))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, **kwargs)
def connect_signals_at_init(self):
# ## Signals
self.ui.import_button.clicked.connect(lambda: self.on_file_importimage())
self.ui.image_type.activated_custom.connect(self.ui.on_image_type)
def set_tool_ui(self):
self.clear_ui(self.layout)
self.ui = ImageUI(layout=self.layout, app=self.app)
self.pluginName = self.ui.pluginName
self.connect_signals_at_init()
# ## Initialize form
self.ui.dpi_entry.set_value(96)
self.ui.image_type.set_value('black')
self.ui.on_image_type(val=self.ui.image_type.get_value())
self.ui.min_area_entry.set_value(0.3)
self.ui.import_mode_radio.set_value('raster')
self.ui.on_import_image_mode(val=self.ui.import_mode_radio.get_value())
self.ui.control_radio.set_value('presets')
self.ui.on_tracing_control_radio(val=self.ui.control_radio.get_value())
self.ui.mask_bw_entry.set_value(250)
self.ui.mask_r_entry.set_value(250)
self.ui.mask_g_entry.set_value(250)
self.ui.mask_b_entry.set_value(250)
self.ui.error_lines_entry.set_value(1)
self.ui.error_splines_entry.set_value(0)
self.ui.path_omit_entry.set_value(8)
self.ui.enhance_rangle_cb.set_value(True)
self.ui.sampling_combo.set_value(0)
self.ui.nr_colors_entry.set_value(16)
self.ui.ratio_entry.set_value(0)
self.ui.cycles_entry.set_value(3)
self.ui.stroke_width_entry.set_value(1.0)
self.ui.line_filter_cb.set_value(False)
self.ui.rounding_entry.set_value(1)
self.ui.blur_radius_entry.set_value(1)
self.ui.blur_delta_entry.set_value(20)
def on_file_importimage(self, threaded=True):
"""
Callback for menu item File->Import IMAGE.
:return: None
"""
self.app.log.debug("on_file_importimage()")
import_mode = self.ui.import_mode_radio.get_value()
trace_options = self.ui.presets_combo.get_value() if self.ui.control_radio.get_value() == 'presets' else \
self.get_tracing_options()
type_obj = self.ui.tf_type_obj_combo.get_value()
dpi = self.ui.dpi_entry.get_value()
mode = self.ui.image_type.get_value()
min_area = self.ui.min_area_entry.get_value()
if import_mode == 'trace':
# check if Chromium is present, if not issue a warning
res = check_chromium()
if res is False:
msgbox = FCMessageBox(parent=self.app.ui)
title = _("Import warning")
txt = _("The tracing require Chromium,\n"
"but it was not detected.\n"
"\n"
"Do you want to download it (about 300MB)?")
msgbox.setWindowTitle(title) # taskbar still shows it
msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
msgbox.setText('<b>%s</b>' % title)
msgbox.setInformativeText(txt)
msgbox.setIcon(QtWidgets.QMessageBox.Icon.Warning)
bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.ButtonRole.YesRole)
bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.ButtonRole.NoRole)
msgbox.setDefaultButton(bt_yes)
msgbox.exec()
response = msgbox.clickedButton()
if response == bt_no:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
return
self.app.inform.emit(_("Please be patient. Chromium is being downloaded in the background.\n"
"The app will resume after it is installed."))
_filter = "Image Files(*.BMP *.PNG *.JPG *.JPEG);;" \
"Bitmap File (*.BMP);;" \
"PNG File (*.PNG);;" \
"Jpeg File (*.JPG);;" \
"All Files (*.*)"
try:
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import IMAGE"),
directory=self.app.get_last_folder(), filter=_filter)
except TypeError:
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import IMAGE"), filter=_filter)
filename = str(filename)
mask = [
self.ui.mask_bw_entry.get_value(),
self.ui.mask_r_entry.get_value(),
self.ui.mask_g_entry.get_value(),
self.ui.mask_b_entry.get_value()
]
if filename == "":
self.app.inform.emit(_("Cancelled."))
else:
if import_mode == 'trace':
# there are thread issues so I process this outside
svg_text = trace(filename, blackAndWhite=True if mode == 'black' else False, mode=trace_options)
else:
svg_text = None
if threaded is True:
self.app.worker_task.emit({'fcn': self.import_image,
'params': [
filename, import_mode, type_obj, dpi, mode, mask, svg_text, min_area]
})
else:
self.import_image(filename, import_mode, type_obj, dpi, mode, mask, svg_text, min_area)
def import_image(self, filename, import_mode='raster', o_type=_("Geometry"), dpi=96, mode='black',
mask=None, svg_text=None, min_area=0.0, outname=None, silent=False):
"""
Adds a new Geometry Object to the projects and populates
it with shapes extracted from the SVG file.
:param filename: Path to the SVG file.
:param import_mode: The kind of image import to be done: 'raster' or 'trace'
:param o_type: type of FlatCAM object
:param dpi: dot per inch
:param mode: black or color
:param mask: dictate the level of detail
:param svg_text: a SVG string only for when tracing
:param outname: name for the resulting file
:param min_area: the minimum area for the imported polygons for them to be kept
:param silent: bool: if False then there are no messages issued to GUI
:return:
"""
self.app.defaults.report_usage("import_image()")
if not os.path.exists(filename):
if silent:
self.app.log.debug("File no longer available.")
else:
self.app.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
return
if mask is None:
mask = [250, 250, 250, 250]
if o_type is None or o_type == _("Geometry"):
obj_type = "geometry"
elif o_type == _("Gerber"):
obj_type = "gerber"
else:
if silent is False:
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Geometry and Gerber objects are supported"))
return
def obj_init(geo_obj, app_obj):
app_obj.log.debug("ToolImage.import_image() -> importing image as: %s" % obj_type.capitalize())
if import_mode == 'raster':
image_geo = self.import_image_handler(filename, units=units, dpi=dpi, mode=mode, mask=mask)
else: # 'trace'
image_geo = self.import_image_as_trace_handler(svg_text=svg_text, obj_type=obj_type, units=units,
dpi=dpi)
if not image_geo:
app_obj.log.debug("ToolImage.import_image() -> empty geometry.")
return 'fail'
if image_geo == 'fail':
if silent is False:
app_obj.inform.emit("[ERROR_NOTCL] %s" % _("Failed."))
return "fail"
geo_obj.multigeo = False
geo_obj.multitool = False
# flatten the geo_obj.solid_geometry list
geo_obj.solid_geometry = list(self.flatten_list(image_geo))
geo_obj.solid_geometry = [p for p in geo_obj.solid_geometry if p and p.is_valid and p.area >= min_area]
if obj_type == 'geometry':
tooldia = float(self.app.options["tools_mill_tooldia"])
tooldia = float('%.*f' % (self.decimals, tooldia))
new_data = {k: v for k, v in self.app.options.items()}
geo_obj.tools.update({
1: {
'tooldia': tooldia,
'data': deepcopy(new_data),
'solid_geometry': deepcopy(geo_obj.solid_geometry)
}
})
geo_obj.tools[1]['data']['name'] = name
if svg_text is not None:
geo_obj.source_file = svg_text
else:
geo_obj.source_file = app_obj.f_handlers.export_dxf(
obj_name=None, filename=None, local_use=geo_obj, use_thread=False)
else: # 'gerber'
if 0 not in geo_obj.tools:
geo_obj.tools[0] = {
'type': 'REG',
'size': 0.0,
'geometry': []
}
try:
w_geo = geo_obj.solid_geometry.geoms if \
isinstance(geo_obj.solid_geometry, (MultiLineString, MultiPolygon)) else geo_obj.solid_geometry
for pol in w_geo:
new_el = {'solid': pol, 'follow': LineString(pol.exterior.coords)}
geo_obj.tools[0]['geometry'].append(new_el)
except TypeError:
new_el = {
'solid': geo_obj.solid_geometry,
'follow': LineString(geo_obj.solid_geometry.exterior.coords) if
isinstance(geo_obj.solid_geometry, Polygon) else geo_obj.solid_geometry
}
geo_obj.tools[0]['geometry'].append(new_el)
geo_obj.source_file = app_obj.f_handlers.export_gerber(
obj_name=None, filename=None, local_use=geo_obj, use_thread=False)
with self.app.proc_container.new('%s ...' % _("Importing")):
# Object name
name = outname or filename.split('/')[-1].split('\\')[-1]
units = self.app.app_units
res = self.app.app_obj.new_object(obj_type, name, obj_init)
if res == 'fail':
self.app.inform.emit("[ERROR_NOTCL] %s" % _("Failed."))
return
# Register recent file
self.app.file_opened.emit("image", filename)
# GUI feedback
if silent is False:
self.app.inform.emit('[success] %s: %s' % (_("Opened"), filename))
def import_image_handler(self, filename, flip=True, units='MM', dpi=96, mode='black', mask=None):
"""
Imports shapes from an IMAGE file into the object's geometry.
:param filename: Path to the IMAGE file.
:type filename: str
:param flip: Flip the object vertically.
:type flip: bool
:param units: App units
:type units: str
:param dpi: dots per inch on the imported image
:param mode: how to import the image: as 'black' or 'color'
:type mode: str
:param mask: level of detail for the import
:return: None
"""
if mask is None:
mask = [128, 128, 128, 128]
scale_factor = 25.4 / dpi if units.lower() == 'mm' else 1 / dpi
geos = []
unscaled_geos = []
with rasterio_open(filename) as src:
# if filename.lower().rpartition('.')[-1] == 'bmp':
# red = green = blue = src.read(1)
# print("BMP")
# elif filename.lower().rpartition('.')[-1] == 'png':
# red, green, blue, alpha = src.read()
# elif filename.lower().rpartition('.')[-1] == 'jpg':
# red, green, blue = src.read()
red = green = blue = src.read(1)
try:
green = src.read(2)
except Exception:
pass
try:
blue = src.read(3)
except Exception:
pass
if mode == 'black':
mask_setting = red <= mask[0]
total = red
self.app.log.debug("Image import as monochrome.")
else:
mask_setting = (red <= mask[1]) + (green <= mask[2]) + (blue <= mask[3])
total = np.zeros(red.shape, dtype=np.float32)
for band in red, green, blue:
total += band
total /= 3
self.app.log.debug("Image import as colored. Thresholds are: R = %s , G = %s, B = %s" %
(str(mask[1]), str(mask[2]), str(mask[3])))
for geom, val in shapes(total, mask=mask_setting):
unscaled_geos.append(shape(geom))
for g in unscaled_geos:
geos.append(scale(g, scale_factor, scale_factor, origin=(0, 0)))
if flip:
geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0))) for g in geos]
return geos
def import_image_as_trace_handler(self, svg_text, obj_type, flip=True, units='MM', dpi=96):
"""
Imports shapes from an IMAGE file into the object's geometry.
:param svg_text: A SVG text object
:type svg_text: str
:param obj_type: the way the image is imported. As: 'gerber' or 'geometry' objects
:type obj_type: str
:param flip: Flip the object vertically.
:type flip: bool
:param units: App units
:type units: str
:param dpi: dots per inch on the imported image
:return: None
"""
# Parse into list of shapely objects
# svg_tree = ET.parse(filename)
# svg_root = svg_tree.getroot()
svg_root = ET.fromstring(svg_text)
# Change origin to bottom left
# h = float(svg_root.get('height'))
# w = float(svg_root.get('width'))
svg_parsed_dims = svgparselength(svg_root.get('height'))
h = svg_parsed_dims[0]
svg_units = svg_parsed_dims[1]
if svg_units in ['em', 'ex', 'pt', 'px']:
self.app.log.error("ToolImage.import_image_as_trace_handler(). SVG units not supported: %s" % svg_units)
return "fail"
res = self.app.options['geometry_circle_steps']
factor = svgparse_viewbox(svg_root)
if svg_units == 'cm':
factor *= 10
geos = getsvggeo(svg_root, obj_type, units=units, res=res, factor=factor, app=self.app)
if geos is None:
return 'fail'
self.app.log.debug("ToolImage.import_image_as_trace_handler(). Finished parsing the SVG geometry.")
geos_text = getsvgtext(svg_root, obj_type, app=self.app, units=units)
if geos_text is not None:
self.app.log.debug("ToolImage.import_image_as_trace_handler(). Processing SVG text.")
geos_text_f = []
if flip:
# Change origin to bottom left
for i in geos_text:
__, minimy, __, maximy = i.bounds
h2 = (maximy - minimy) * 0.5
geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
if geos_text_f:
geos += geos_text_f
if flip:
geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0))) for g in geos]
self.app.log.debug("ToolImage.import_image_as_trace_handler(). SVG geometry was flipped.")
scale_factor = 25.4 / dpi if units.lower() == 'mm' else 1 / dpi
geos = [translate(scale(g, scale_factor, scale_factor, origin=(0, 0))) for g in geos]
return geos
def get_tracing_options(self):
opt_dict = {
'ltres': self.ui.error_lines_entry.get_value(),
'qtres': self.ui.error_splines_entry.get_value(),
'pathomit': self.ui.path_omit_entry.get_value(),
'rightangleenhance': self.ui.enhance_rangle_cb.get_value(),
'colorsampling': self.ui.sampling_combo.get_value(),
'numberofcolors': self.ui.nr_colors_entry.get_value(),
'mincolorratio': self.ui.ratio_entry.get_value(),
'colorquantcycles': self.ui.cycles_entry.get_value(),
'strokewidth': self.ui.stroke_width_entry.get_value(),
'linefilter': self.ui.line_filter_cb.get_value(),
'roundcoords': self.ui.rounding_entry.get_value(),
'blurradius': self.ui.blur_radius_entry.get_value(),
'blurdelta': self.ui.blur_delta_entry.get_value()
}
dict_as_string = '{ '
for k, v in opt_dict.items():
dict_as_string += "%s:%s, " % (str(k), str(v))
# remove last comma and space and add the terminator
dict_as_string = dict_as_string[:-2] + ' }'
return dict_as_string
def flatten_list(self, obj_list):
for item in obj_list:
if hasattr(item, '__iter__') and not isinstance(item, (str, bytes)):
yield from self.flatten_list(item)
else:
yield item
class ImageUI:
pluginName = _("Image Import")
def __init__(self, layout, app):
self.app = app
self.decimals = self.app.decimals
self.layout = layout
# ## Title
title_label = FCLabel("%s" % self.pluginName, size=16, bold=True)
self.layout.addWidget(title_label)
self.param_lbl = FCLabel('%s' % _("Parameters"), color='blue', bold=True)
self.layout.addWidget(self.param_lbl)
# #############################################################################################################
# ######################################## Parameters #########################################################
# #############################################################################################################
# add a frame and inside add a grid box layout.
par_frame = FCFrame()
self.layout.addWidget(par_frame)
par_grid = GLay(v_spacing=5, h_spacing=3)
par_frame.setLayout(par_grid)
# Type of object to create for the image
self.tf_type_obj_combo_label = FCLabel('%s:' % _("Object Type"))
self.tf_type_obj_combo_label.setToolTip(
_("Specify the type of object to create from the image.\n"
"It can be of type: Gerber or Geometry.")
)
self.tf_type_obj_combo = FCComboBox()
self.tf_type_obj_combo.addItems([_("Gerber"), _("Geometry")])
self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
self.tf_type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
par_grid.addWidget(self.tf_type_obj_combo_label, 0, 0)
par_grid.addWidget(self.tf_type_obj_combo, 0, 1, 1, 2)
# DPI value of the imported image
self.dpi_entry = FCSpinner()
self.dpi_entry.set_range(0, 99999)
self.dpi_label = FCLabel('%s:' % _("DPI value"))
self.dpi_label.setToolTip(_("Specify a DPI value for the image."))
par_grid.addWidget(self.dpi_label, 2, 0)
par_grid.addWidget(self.dpi_entry, 2, 1, 1, 2)
# Area
area_lbl = FCLabel('%s' % _("Area:"), bold=True)
area_lbl.setToolTip(
_("Polygons inside the image with less area are discarded.")
)
self.min_area_entry = FCDoubleSpinner()
self.min_area_entry.set_range(0.0000, 10000.0000)
self.min_area_entry.setSingleStep(0.1)
self.min_area_entry.set_value(0.0)
a_units = _("mm") if self.app.app_units == 'MM' else _("in")
area_units_lbl = FCLabel('%s<sup>2</sup>' % a_units)
par_grid.addWidget(area_lbl, 4, 0)
par_grid.addWidget(self.min_area_entry, 4, 1)
par_grid.addWidget(area_units_lbl, 4, 2)
# The import Mode
self.import_mode_lbl = FCLabel('%s:' % _('Mode'), color='red', bold=True)
self.import_mode_lbl.setToolTip(
_("Choose a method for the image interpretation.\n"
"B/W means a black & white image. Color means a colored image.")
)
self.import_mode_radio = RadioSet([
{'label': 'Raster', 'value': 'raster'},
{'label': 'Tracing', 'value': 'trace'}
])
mod_grid = GLay(v_spacing=5, h_spacing=3)
self.layout.addLayout(mod_grid)
mod_grid.addWidget(self.import_mode_lbl, 0, 0)
mod_grid.addWidget(self.import_mode_radio, 0, 1)
# Type of image interpretation
self.image_type_label = FCLabel('%s:' % _('Type'), bold=True)
self.image_type_label.setToolTip(
_("Choose a method for the image interpretation.\n"
"B/W means a black & white image. Color means a colored image.")
)
self.image_type = RadioSet([{'label': 'B/W', 'value': 'black'},
{'label': 'Color', 'value': 'color'}])
mod_grid.addWidget(self.image_type_label, 6, 0)
mod_grid.addWidget(self.image_type, 6, 1, 1, 2)
# #############################################################################################################
# ######################################## Raster Mode ########################################################
# #############################################################################################################
# add a frame and inside add a grid box layout.
self.raster_frame = FCFrame()
self.layout.addWidget(self.raster_frame)
raster_grid = GLay(v_spacing=5, h_spacing=3)
self.raster_frame.setLayout(raster_grid)
self.detail_label = FCLabel("%s:" % _('Level of detail'), bold=True)
raster_grid.addWidget(self.detail_label, 0, 0, 1, 2)
# Mask value of the imported image when image monochrome
self.mask_bw_entry = FCSpinner()
self.mask_bw_entry.set_range(0, 255)
self.mask_bw_label = FCLabel("%s <b>B/W</b>:" % _('Mask value'))
self.mask_bw_label.setToolTip(
_("Mask for monochrome image.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry.\n"
"0 means no detail and 255 means everything \n"
"(which is totally black).")
)
raster_grid.addWidget(self.mask_bw_label, 2, 0)
raster_grid.addWidget(self.mask_bw_entry, 2, 1)
# Mask value of the imported image for RED color when image color
self.mask_r_entry = FCSpinner()
self.mask_r_entry.set_range(0, 255)
self.mask_r_label = FCLabel("%s <b>R:</b>" % _('Mask value'))
self.mask_r_label.setToolTip(
_("Mask for RED color.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry.")
)
raster_grid.addWidget(self.mask_r_label, 4, 0)
raster_grid.addWidget(self.mask_r_entry, 4, 1)
# Mask value of the imported image for GREEN color when image color
self.mask_g_entry = FCSpinner()
self.mask_g_entry.set_range(0, 255)
self.mask_g_label = FCLabel("%s <b>G:</b>" % _('Mask value'))
self.mask_g_label.setToolTip(
_("Mask for GREEN color.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry.")
)
raster_grid.addWidget(self.mask_g_label, 6, 0)
raster_grid.addWidget(self.mask_g_entry, 6, 1)
# Mask value of the imported image for BLUE color when image color
self.mask_b_entry = FCSpinner()
self.mask_b_entry.set_range(0, 255)
self.mask_b_label = FCLabel("%s <b>B:</b>" % _('Mask value'))
self.mask_b_label.setToolTip(
_("Mask for BLUE color.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry.")
)
raster_grid.addWidget(self.mask_b_label, 8, 0)
raster_grid.addWidget(self.mask_b_entry, 8, 1)
# #############################################################################################################
# ######################################## Raster Mode ########################################################
# #############################################################################################################
# add a frame and inside add a grid box layout.
self.trace_frame = FCFrame()
self.layout.addWidget(self.trace_frame)
trace_grid = GLay(v_spacing=5, h_spacing=3)
self.trace_frame.setLayout(trace_grid)
# Options Control Mode
self.control_lbl = FCLabel('%s:' % _('Control'), color='indigo', bold=True)
self.control_lbl.setToolTip(
_("Tracing control.")
)
self.control_radio = RadioSet([
{'label': _("Presets"), 'value': 'presets'},
{'label': _("Options"), 'value': 'options'}
])
trace_grid.addWidget(self.control_lbl, 0, 0)
trace_grid.addWidget(self.control_radio, 0, 1)
# --------------------------------------------------
# Presets Frame
# --------------------------------------------------
self.preset_frame = QtWidgets.QFrame()
self.preset_frame.setContentsMargins(0, 0, 0, 0)
trace_grid.addWidget(self.preset_frame, 2, 0, 1, 2)
preset_grid = GLay(v_spacing=5, h_spacing=3)
preset_grid.setContentsMargins(0, 0, 0, 0)
self.preset_frame.setLayout(preset_grid)
# Presets
self.presets_lbl = FCLabel('%s:' % _('Presets'))
self.presets_lbl.setToolTip(
_("Options presets to control the tracing.")
)
self.presets_combo = FCComboBox()
self.presets_combo.addItems([
'default', 'posterized1', 'posterized2', 'posterized3', 'curvy', 'sharp', 'detailed', 'smoothed',
'grayscale', 'fixedpalette', 'randomsampling1', 'randomsampling2', 'artistic1', 'artistic2', 'artistic3',
'artistic4'
])
preset_grid.addWidget(self.presets_lbl, 0, 0)
preset_grid.addWidget(self.presets_combo, 0, 1)
# --------------------------------------------------
# Options Frame
# --------------------------------------------------
self.options_frame = QtWidgets.QFrame()
self.options_frame.setContentsMargins(0, 0, 0, 0)
trace_grid.addWidget(self.options_frame, 4, 0, 1, 2)
options_grid = GLay(v_spacing=5, h_spacing=3)
options_grid.setContentsMargins(0, 0, 0, 0)
self.options_frame.setLayout(options_grid)
# Error Threshold
self.error_lbl = FCLabel('%s' % _("Error Threshold"), bold=True)
self.error_lbl.setToolTip(
_("Error threshold for straight lines and quadratic splines.")
)
options_grid.addWidget(self.error_lbl, 0, 0, 1, 2)
# Error Threshold for Lines
self.error_lines_lbl = FCLabel('%s:' % _("Lines"))
self.error_lines_entry = FCDoubleSpinner()
self.error_lines_entry.set_precision(self.decimals)
self.error_lines_entry.set_range(0, 10)
self.error_lines_entry.setSingleStep(0.1)
options_grid.addWidget(self.error_lines_lbl, 2, 0)
options_grid.addWidget(self.error_lines_entry, 2, 1)
# Error Threshold for Splines
self.error_splines_lbl = FCLabel('%s:' % _("Splines"))
self.error_splines_entry = FCDoubleSpinner()
self.error_splines_entry.set_precision(self.decimals)
self.error_splines_entry.set_range(0, 10)
self.error_splines_entry.setSingleStep(0.1)
options_grid.addWidget(self.error_splines_lbl, 4, 0)
options_grid.addWidget(self.error_splines_entry, 4, 1)
# Enhance Right Angle
self.enhance_rangle_cb = FCCheckBox(_("Enhance R Angle"))
self.enhance_rangle_cb.setToolTip(
_("Enhance right angle corners.")
)
options_grid.addWidget(self.enhance_rangle_cb, 6, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
options_grid.addWidget(separator_line, 8, 0, 1, 2)
# Noise Reduction
self.noise_lbl = FCLabel('%s' % _("Noise Reduction"), bold=True)
options_grid.addWidget(self.noise_lbl, 10, 0, 1, 2)
# Path Omit
self.path_omit_lbl = FCLabel('%s' % _("Path Omit"))
self.path_omit_lbl.setToolTip(
_("Edge node paths shorter than this will be discarded for noise reduction.")
)
self.path_omit_entry = FCSpinner()
self.path_omit_entry.set_range(0, 9999)
self.path_omit_entry.setSingleStep(1)
options_grid.addWidget(self.path_omit_lbl, 12, 0)
options_grid.addWidget(self.path_omit_entry, 12, 1)
# Line Filter
self.line_filter_cb = FCCheckBox(_("Line Filter"))
options_grid.addWidget(self.line_filter_cb, 14, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
options_grid.addWidget(separator_line, 16, 0, 1, 2)
# Colors Section
self.colors_lbl = FCLabel('%s' % _("Colors"), bold=True)
options_grid.addWidget(self.colors_lbl, 18, 0, 1, 2)
# Sampling
self.samp_lbl = FCLabel('%s:' % _('Sampling'))
self.sampling_combo = FCComboBox2()
self.sampling_combo.addItems([_("Palette"), _("Random"), _("Deterministic")])
options_grid.addWidget(self.samp_lbl, 20, 0)
options_grid.addWidget(self.sampling_combo, 20, 1)
# Number of colors
self.nr_colors_lbl = FCLabel('%s' % _("Colors"))
self.nr_colors_lbl.setToolTip(
_("Number of colors to use on palette.")
)
self.nr_colors_entry = FCSpinner()
self.nr_colors_entry.set_range(0, 9999)
self.nr_colors_entry.setSingleStep(1)
options_grid.addWidget(self.nr_colors_lbl, 22, 0)
options_grid.addWidget(self.nr_colors_entry, 22, 1)
# Randomization Ratio
self.ratio_lbl = FCLabel('%s' % _("Ratio"))
self.ratio_lbl.setToolTip(
_("Color quantization will randomize a color if fewer pixels than (total pixels * ratio) has it.")
)
self.ratio_entry = FCSpinner()
self.ratio_entry.set_range(0, 10)
self.ratio_entry.setSingleStep(1)
options_grid.addWidget(self.ratio_lbl, 24, 0)
options_grid.addWidget(self.ratio_entry, 24, 1)
# Cycles of quantization
self.cycles_lbl = FCLabel('%s' % _("Cycles"))
self.cycles_lbl.setToolTip(
_("Color quantization will be repeated this many times.")
)
self.cycles_entry = FCSpinner()
self.cycles_entry.set_range(0, 20)
self.cycles_entry.setSingleStep(1)
options_grid.addWidget(self.cycles_lbl, 26, 0)
options_grid.addWidget(self.cycles_entry, 26, 1)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
options_grid.addWidget(separator_line, 28, 0, 1, 2)
# Parameters
self.par_lbl = FCLabel('%s' % _("Parameters"), bold=True)
options_grid.addWidget(self.par_lbl, 30, 0, 1, 2)
# Stroke width
self.stroke_width_lbl = FCLabel('%s' % _("Stroke"))
self.stroke_width_lbl.setToolTip(
_("Width of the stroke to be applied to the shape.")
)
self.stroke_width_entry = FCDoubleSpinner()
self.stroke_width_entry.set_precision(self.decimals)
self.stroke_width_entry.set_range(0.0000, 9999.0000)
self.stroke_width_entry.setSingleStep(0.1)
options_grid.addWidget(self.stroke_width_lbl, 32, 0)
options_grid.addWidget(self.stroke_width_entry, 32, 1)
# Rounding
self.rounding_lbl = FCLabel('%s' % _("Rounding"))
self.rounding_lbl.setToolTip(
_("Rounding coordinates to a given decimal place.")
)
self.rounding_entry = FCSpinner()
self.rounding_entry.set_range(0, 10)
self.rounding_entry.setSingleStep(1)
options_grid.addWidget(self.rounding_lbl, 34, 0)
options_grid.addWidget(self.rounding_entry, 34, 1)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
options_grid.addWidget(separator_line, 36, 0, 1, 2)
# Blur
self.blur_lbl = FCLabel('%s' % _("Blur"), bold=True)
options_grid.addWidget(self.blur_lbl, 38, 0, 1, 2)
# Radius
self.blur_radius_lbl = FCLabel('%s' % _("Radius"))
self.blur_radius_lbl.setToolTip(
_("Selective Gaussian blur preprocessing.")
)
self.blur_radius_entry = FCSpinner()
self.blur_radius_entry.set_range(0, 5)
self.blur_radius_entry.setSingleStep(1)
options_grid.addWidget(self.blur_radius_lbl, 40, 0)
options_grid.addWidget(self.blur_radius_entry, 40, 1)
# Delta
self.blur_delta_lbl = FCLabel('%s' % _("Delta"))
self.blur_delta_lbl.setToolTip(
_("RGBA delta threshold for selective Gaussian blur preprocessing.")
)
self.blur_delta_entry = FCDoubleSpinner()
self.blur_delta_entry.set_precision(self.decimals)
self.blur_delta_entry.set_range(0.0000, 9999.0000)
self.blur_delta_entry.setSingleStep(0.1)
options_grid.addWidget(self.blur_delta_lbl, 42, 0)
options_grid.addWidget(self.blur_delta_entry, 42, 1)
GLay.set_common_column_size([par_grid, mod_grid, raster_grid, trace_grid, preset_grid, options_grid], 0)
# Buttons
self.import_button = FCButton(_("Import image"))
self.import_button.setIcon(QtGui.QIcon(self.app.resource_location + '/image32.png'))
self.import_button.setToolTip(
_("Open a image of raster type and then import it in FlatCAM.")
)
self.layout.addWidget(self.import_button)
self.layout.addStretch(1)
# #################################### FINSIHED GUI ###########################
# #############################################################################
# Signals
self.import_mode_radio.activated_custom.connect(self.on_import_image_mode)
self.control_radio.activated_custom.connect(self.on_tracing_control_radio)
def on_image_type(self, val):
if val == 'color':
self.mask_r_label.setDisabled(False)
self.mask_r_entry.setDisabled(False)
self.mask_g_label.setDisabled(False)
self.mask_g_entry.setDisabled(False)
self.mask_b_label.setDisabled(False)
self.mask_b_entry.setDisabled(False)
self.mask_bw_label.setDisabled(True)
self.mask_bw_entry.setDisabled(True)
else:
self.mask_r_label.setDisabled(True)
self.mask_r_entry.setDisabled(True)
self.mask_g_label.setDisabled(True)
self.mask_g_entry.setDisabled(True)
self.mask_b_label.setDisabled(True)
self.mask_b_entry.setDisabled(True)
self.mask_bw_label.setDisabled(False)
self.mask_bw_entry.setDisabled(False)
def on_import_image_mode(self, val):
if val == 'raster':
self.raster_frame.show()
self.trace_frame.hide()
else:
self.raster_frame.hide()
self.trace_frame.show()
def on_tracing_control_radio(self, val):
if val == 'presets':
self.preset_frame.show()
self.options_frame.hide()
else:
self.preset_frame.hide()
self.options_frame.show()