diff --git a/README.md b/README.md
index c565a3d..bef1dfa 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,7 @@ Once all the requirements are installed, the program is ready to use. There are
1. manually list filter names and parameters from command line
```sh
- python3 src/main.py res/examples/Palec_P4.tif res/examples/Palec_P4_from_shell.png 600 total_variation weight=0.15 median ksize=5
+ python3 src/main.py res/examples/Palec_P4.tif res/examples/Palec_P4_from_cline.png 600 total_variation weight=0.15 median ksize=5
```
2. from preset saved in a json config file, that can be used to tune and modify existing presetrs, or create new ones
@@ -74,7 +74,9 @@ Once all the requirements are installed, the program is ready to use. There are
# Configuration and presets
-There is an option to input the filter series as a preset from json configuration file.
+There is an option to input the filter series as a preset from json configuration file.
+This preset is automatically stored inside a json file, which serves as a database for storing filters.
+This prevents losing filter preset information when modifying filter which was used to generate 3D models.
@@ -127,7 +129,17 @@ There is an option to input the filter series as a preset from json configuratio
+There is also an option to save current command line setting as a preset using -d switch:
+* General command for saving filter preset
+ ```sh
+ python3 src/main.py input_file output_file dpi -d new_preset_name filters
+ ```
+
+* Working example
+ ```sh
+ python3 src/main.py res/examples/Palec_P4.tif res/examples/Palec_P4_from_cline.png 600 -d preset_gaussian gaussian sigma=1
+ ```
All the filters used and their parameters are described below.
diff --git a/src/config_parser.py b/src/config_parser.py
index 35c09e5..44603ec 100644
--- a/src/config_parser.py
+++ b/src/config_parser.py
@@ -6,7 +6,7 @@
from os.path import exists
import json
import hashlib
-
+import log
def save_preset(filters, params, preset_name):
'''Save filter preset to database.
@@ -46,7 +46,7 @@ def store_to_db(preset, preset_name):
# If database doesn't exist, create it
if not exists("db.json"):
- print("Storing preset to database")
+ log.print_message("Storing preset to database")
with open("db.json", 'w') as db:
json.dump(preset, db)
else:
@@ -61,9 +61,9 @@ def store_to_db(preset, preset_name):
# If preset already exists, skip it
if preset_name in db_presets:
- print("Preset already exists in database, skipping")
+ log.print_message("Preset already exists in database, skipping")
else:
- print("Storing preset to database")
+ log.print_message("Storing preset to database")
db_presets.update(preset)
# Finally write the updated entries to db file
@@ -91,10 +91,10 @@ def parse_conf(preset_name, filters, params, config_file):
if attribute != "name":
params[i][attribute] = value
parse_params(params[i])
- print("Loaded preset: " + preset_name +
- " from file: " + config_file)
+ log.print_message("Loaded preset:", preset_name,
+ "from file:", config_file)
else:
- print("Preset not found")
+ log.print_message("Preset not found")
def parse_params(params):
diff --git a/src/filters.py b/src/filters.py
index b9344a3..9c07059 100644
--- a/src/filters.py
+++ b/src/filters.py
@@ -41,7 +41,6 @@ class gaussian(filter):
# Standard deviation for Gaussian kernel
sigma = float(params["sigma"]) if params["sigma"] else 1
- print("with params: sigma: " + str(sigma))
self.img = skiflt.gaussian(
self.img, sigma=sigma, preserve_range=True)
@@ -59,7 +58,6 @@ class median(filter):
# Used kernel is disk of size ksize
ksize = int(params["ksize"]) if params["ksize"] else 3
- print("with params: ksize: " + str(ksize))
self.img = skiflt.median(self.img, footprint=skimorph.disk(ksize))
@@ -84,8 +82,6 @@ class bilateral(filter):
# A larger value results in averaging of pixels with larger spatial differences
sigmaSpace = int(params["sigmaSpace"]) if params["sigmaSpace"] else 75
- print("with params: diameter: " + str(diameter) + " sigmaColor: " +
- str(sigmaColor) + " sigmaSpace: " + str(sigmaSpace))
self.img = np.uint8(self.img)
self.img = cv.bilateralFilter(
self.img, diameter, sigmaColor, sigmaSpace)
@@ -115,8 +111,6 @@ class bilateral_scikit(filter):
sigmaSpace = float(params["sigmaSpace"]
) if params["sigmaSpace"] else 9.0
- print("with params: sigma_color: " + str(sigmaColor) +
- " sigma_spatial: " + str(sigmaSpace))
self.img = skirest.denoise_bilateral(
self.img, sigma_color=sigmaColor, sigma_spatial=sigmaSpace)
self.img = np.uint8(self.img * 255.0) # converting back to uint8
@@ -144,8 +138,6 @@ class nlmeans(filter):
# Cut-off distance, higher means more smoothed image
h = float(params["h"])*sigma if params["h"] else 0.1*sigma
- print("with params: patch_size: " + str(patch_size) + " patch_distance: " +
- str(patch_distance) + " h: " + str(round(h, 4)))
self.img = skirest.denoise_nl_means(
self.img, patch_size=patch_size, fast_mode=True, patch_distance=patch_distance, h=h)
self.img = np.uint8(self.img * 255.0) # converting back to uint8
@@ -166,7 +158,6 @@ class total_variation(filter):
# Denoising weight. Larger values result in more denoising.
weight = float(params["weight"]) if params["weight"] else 0.1
- print("with params: weight: " + str(weight))
self.img = skirest.denoise_tv_chambolle(
self.img, weight=weight)
self.img = np.uint8(self.img * 255.0) # converting back to uint8
@@ -184,7 +175,6 @@ class block_match(filter):
def apply(self, params):
sigma = float(params["sigma"]) if params["sigma"] else 20
- print("with params: sigma: " + str(sigma))
self.img = bm3d.bm3d(self.img, sigma_psd=sigma,
stage_arg=bm3d.BM3DStages.ALL_STAGES)
@@ -207,8 +197,6 @@ class unsharp_mask_scikit(filter):
# strength of the unsharp mask
amount = float(params["amount"]) if params["amount"] else 1.0
- print("with params: radius: " +
- str(radius) + " amount: " + str(amount))
self.img = skiflt.unsharp_mask(self.img, radius=radius,
amount=amount, channel_axis=None)
self.img = np.uint8(self.img * 255.0) # converting back to uint
@@ -315,8 +303,6 @@ class binarize(filter):
maxval = int(params["maxval"]) if params["maxval"] else 255
type = int(params["type"]) if params["type"] else 0
- print("with params: threshold: " + str(threshold) +
- " maxval: " + str(maxval) + " type: " + str(type))
self.img = cv.threshold(self.img, threshold, maxval, type)[1]
@@ -338,8 +324,6 @@ class add_margin(filter):
margin = int(params["margin"]) if params["margin"] else 10
color = int(params["color"]) if params["color"] else 255
- print("with params: margin: " + str(margin) + " color: " + str(color))
-
self.fig.set_size_inches(
((self.width + 2 * margin) / self.dpi, (self.height + 2 * margin) / self.dpi))
self.img = cv.copyMakeBorder(
@@ -365,7 +349,6 @@ class convolve(filter):
kernel = np.array(params["kernel"]) if params["kernel"] else np.ones(
(3, 3), np.float32) / 9
- print("with params: kernel: \n" + str(kernel))
self.img = cv.filter2D(self.img, -1, kernel)
@@ -380,7 +363,6 @@ class blur(filter):
def apply(self, params):
ksize = int(params["ksize"]) if params["ksize"] else 3
- print("with params: ksize: " + str(ksize))
self.img = cv.blur(self.img, ksize=(ksize, ksize))
@@ -395,8 +377,6 @@ class denoise(filter):
sWS = int(params["searchWindowSize"]
) if params["searchWindowSize"] else 21
- # print("with params: h: " + str(h) +
- # " tWS: " + str(tWS) + " sWS: " + str(sWS))
self.img = np.uint8(self.img)
self.img = cv.fastNlMeansDenoising(
self.img, h, tWS, sWS)
@@ -413,7 +393,6 @@ class sharpen(filter):
kernel = np.matrix(params["kernel"]) if params["kernel"] else np.array(
[[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
- # print("with params: kernel: \n" + str(kernel))
self.img = cv.filter2D(self.img, ddepth=-1, kernel=kernel)
@@ -434,8 +413,6 @@ class unsharp_mask(filter):
blurred = cv.medianBlur(self.img, ksize)
lap = cv.Laplacian(blurred, cv.CV_32F)
- # print("with params: strength: " +
- # str(strength) + " ksize: " + str(ksize))
self.img = blurred - strength*lap
@@ -458,8 +435,6 @@ class unsharp_mask_pil(filter):
# Threshold controls the minimum brightness change that will be sharpened
threshold = int(params["threshold"]) if params["threshold"] else 3
- # print("with params: radius: " +
- # str(radius) + " percent: " + str(percent) + " threshold: " + str(threshold))
self.img = np.uint8(self.img)
tmp = Image.fromarray(self.img)
tmp = tmp.filter(ImageFilter.UnsharpMask(radius, percent, threshold))
@@ -481,7 +456,6 @@ class erode(filter):
kernel = np.matrix(params["kernel"]) if params["kernel"] else cv.getStructuringElement(
cv.MORPH_ELLIPSE, (3, 3))
- print("with params: kernel: \n" + str(kernel))
self.img = cv.morphologyEx(
np.uint8(self.img), op=cv.MORPH_ERODE, kernel=kernel)
@@ -501,7 +475,6 @@ class dilate(filter):
kernel = np.matrix(params["kernel"]) if params["kernel"] else cv.getStructuringElement(
cv.MORPH_ELLIPSE, (3, 3))
- print("with params: kernel: \n" + str(kernel))
self.img = cv.morphologyEx(
np.uint8(self.img), op=cv.MORPH_DILATE, kernel=kernel)
diff --git a/src/log.py b/src/log.py
new file mode 100644
index 0000000..dcf3b7f
--- /dev/null
+++ b/src/log.py
@@ -0,0 +1,22 @@
+"""! @file log.py
+ @brief File with printing functions
+ @author xlanro00
+"""
+
+import sys
+
+def print_message(*args, **kwargs):
+ '''Print given message to stderr.
+ :param message: message to be printed
+ '''
+
+ print("APP:", *args, file=sys.stderr, **kwargs)
+
+
+def error_exit(error_message):
+ '''Print given error message and exit the application.
+ :param message: error message to be printed
+ '''
+
+ print("ERROR:" + error_message, file=sys.stderr)
+ exit(1)
diff --git a/src/main.py b/src/main.py
index cb97d34..0bc1f8e 100644
--- a/src/main.py
+++ b/src/main.py
@@ -5,8 +5,6 @@
# Import basic libraries
import argparse as ap
-import sys
-import json
from os.path import exists
import hashlib
import math
@@ -22,6 +20,7 @@ import trimesh.transformations as tmtra
# Import custom image filter library
import filters as flt
import config_parser as cp
+import log
class app:
'''Main class for the application.
@@ -41,28 +40,30 @@ class app:
cp.parse_conf(self.preset_name, self.filters, self.params, self.config_file)
elif self.args.filters:
- print("No config file given, using command line arguments")
- i = 0
+ filter_index = 0
+ log.print_message(
+ "No config file given, using command line arguments")
# Otherwise expect filters from command line
for filter_part in self.args.filters:
# If no '=' char in filter, it is a new filter name
if filter_part.find('=') == -1:
self.filters.append(filter_part)
- i += 1
- self.params[i] = {} # create empty dict for params
+ filter_index += 1
+ # create empty dict for params
+ self.params[filter_index] = {}
# Otherwise it's a parameter for current filter
else:
key, value = filter_part.split('=')
- self.params[i][key] = value
+ self.params[filter_index][key] = value
- cp.parse_params(self.params[i])
+ cp.parse_params(self.params[filter_index])
# If database flag is set, save filters to database as a new preset
if self.args.database:
cp.save_preset(self.filters, self.params, self.args.database[0])
else:
- print("No filters given, saving original image")
+ log.print_message("No filters given, saving original image")
# Set input and output file paths, dpi and mirror flag for easier readability
self.input_file = self.args.input_file
@@ -72,9 +73,8 @@ class app:
if exists(self.input_file):
self.run_filtering()
-
else:
- self.error_exit("Input file " + self.input_file +
+ log.error_exit("Input file " + self.input_file +
" does not exist")
if self.args.stl:
@@ -128,12 +128,12 @@ class app:
if len(self.args.stl) < 3:
self.height_line = 2
self.height_base = 10
- print(
+ log.print_message(
"Warning: Too few arguments, using default values (10mm base, 2mm lines)")
else:
self.height_line = float(self.args.stl[1])
self.height_base = float(self.args.stl[2])
- print("Base height:", self.height_base,
+ log.print_message("Base height:", self.height_base,
"mm, lines depth/height:", self.height_line, "mm")
elif self.args.stl[0] == 'c':
@@ -145,40 +145,31 @@ class app:
self.height_base = 10
self.curv_rate_x = 2
self.curv_rate_y = 6
- print(
- "Warning: Too few arguments, using default values (2mm lines, curvature 0.5 on x, 0.5 on y)")
+ log.print_message("Warning: Too few arguments, using default values (2mm lines, curvature 0.5 on x, 0.5 on y)")
else:
self.height_line = float(self.args.stl[1])
self.height_base = float(self.args.stl[2])
self.curv_rate_x = float(self.args.stl[3])
self.curv_rate_y = float(self.args.stl[4])
- print("Line height:", self.height_line, "mm, base height: ", self.height_base,
- "mm, x axis curvature: ", self.curv_rate_x, ", y axis curvature:", self.curv_rate_y)
+ log.print_message("Line height:", self.height_line, "mm, base height:", self.height_base,
+ "mm, x axis curvature:", self.curv_rate_x, ", y axis curvature:", self.curv_rate_y)
elif self.args.stl[0] == 'm':
self.mode = "mapped"
# TODO: add default values for mapped mode, add finger model?
if len(self.args.stl) < 2:
- print(
+ log.print_message(
"Warning: Too few arguments, using default values")
else:
self.height_line = float(self.args.stl[1])
self.finger_model = self.args.stl[2]
else:
- self.error_exit("Unrecognized generation mode")
+ log.error_exit("Unrecognized generation mode")
- print("Stl generation in ", self.mode)
+ log.print_message("Stl generation in", self.mode, "mode")
self.run_stl()
- def error_exit(self, message):
- '''Print given error message and exit the application.
- :param message: error message to be printed
- '''
-
- print("ERROR:", message, file=sys.stderr)
- exit(1)
-
# ------------------------- FILTERING -------------------------#
def run_filtering(self):
@@ -216,7 +207,7 @@ class app:
'''Mirror image using opencv, should be used if we want a positive model.
'''
- print("Mirroring image", file=sys.stderr)
+ log.print_message("Mirroring image")
self.img = cv.flip(self.img, 1) # 1 for vertical mirror
def apply_filters(self):
@@ -228,7 +219,12 @@ class app:
for i, filter_name in enumerate(self.filters):
# Get filter class from filter.py, use the apply method
filter = getattr(flt, filter_name)
- print("Applying filter:", filter_name, file=sys.stderr)
+ log.print_message("Applying filter:", filter_name)
+ for param in self.params[i+1]:
+ if self.params[i+1][param] is not None:
+ log.print_message("\twith parameter", param,
+ "=", str(self.params[i+1][param]))
+
filter.apply(self, self.params[i+1])
else:
pass
@@ -237,7 +233,7 @@ class app:
'''Save processed image to the output file.
'''
- print("Saving image to", self.output_file, file=sys.stderr)
+ log.print_message("Saving image to", self.output_file)
# Colormap must be set to grayscale to avoid color mismatch.
ax.imshow(self.img, cmap="gray")
@@ -253,7 +249,7 @@ class app:
# create ID for the model from all its parameters
self.get_ID()
- print("Creating mesh", file=sys.stderr)
+ log.print_message("Creating mesh")
# Create a mesh using one of two modes
if self.mode == "planar":
@@ -267,11 +263,11 @@ class app:
self.make_stl_map()
else:
- self.error_exit("Mode not supported")
+ log.error_exit("Incorrect stl generation mode")
plt.show()
self.save_stl()
- print(f"Saving model to ", self.stl_filename, file=sys.stderr)
+ log.print_message("Saving model to", self.stl_filename)
def prepare_heightmap(self):
'''Scale image values to get values from 0 to 255.
@@ -281,27 +277,27 @@ class app:
'''
if self.img.dtype != np.uint8:
- print("Converting to uint8", file=sys.stderr)
+ log.print_message("Converting to uint8")
self.img = self.img / np.max(self.img) * 255
self.img = self.img.astype(np.uint8)
if self.mode == "planar":
if self.height_base <= 0:
- self.error_exit("Depth of plate height must be positive")
+ log.error_exit("Depth of plate height must be positive")
if self.height_line + self.height_base <= 0:
- self.error_exit("Line depth must be less than plate thickness")
+ log.error_exit("Line depth must be less than plate thickness")
if self.mode == "curved":
# Don't need to check curvature, check only heights
if self.height_base <= 0 or self.height_line <= 0:
- self.error_exit("Base and line height must both be positive")
+ log.error_exit("Base and line height must both be positive")
if self.mode == "mapped":
if self.height_line <= 0:
- self.error_exit("Line height must be positive")
+ log.error_exit("Line height must be positive")
if not exists(self.finger_model):
- self.error_exit("Finger model file does not exist")
+ log.error_exit("Finger model file does not exist")
self.height_base = 0
# TODO: curved height base could be done here?
@@ -324,10 +320,10 @@ class app:
# Truncate if necessary
if (len(self.param_string) >= 80):
self.param_string = self.param_string[:80]
- print("Warning: Parameter string too long, truncating", file=sys.stderr)
+ log.print_message("Warning: Parameter string too long, truncating")
# Overwrite stl header (which is only 80 bytes)
- print("Writing info to stl header", file=sys.stderr)
+ log.print_message("Writing info to stl header")
with open(self.stl_filename, "r+") as f:
f.write(self.param_string)
@@ -681,8 +677,7 @@ class app:
# TODO: maybe use trimesh.update_vertices
-
- print("Mapping to finger")
+ log.print_message("Mapping to finger")
# TODO: try to merge meshes? or stl files?
# trimesh library?
finger = trimesh.load(self.finger_model)
@@ -715,7 +710,6 @@ class app:
self.stl_model = trimesh.util.concatenate([finger, fingerprint])
-
def save_stl(self):
'''Save final mesh to stl file.
'''
diff --git a/src/test.sh b/src/test.sh
index 86be6f9..8ad7fbd 100644
--- a/src/test.sh
+++ b/src/test.sh
@@ -9,15 +9,15 @@ source .venv/bin/activate
#----------------------------Configuration------------------------------#
# place all image files to one folder
-input_path=res/test/test-skript
+input_path=res/test/Jenetrics/leva
# !!!!!!!!!!!!!!!!!!
# this is very important, run this on directories containing files with the same dpi
-dpi=500
+dpi=600
# !!!!!!!!!!!!!!!!!!
# recommend png, it's supported by opencv
-format=jpg
+format=png
# name of configuration file
config_file=conf/conf.json
@@ -28,16 +28,18 @@ presets=("ridge")
# generate stl files and set generation mode {"planar", "curved", "mapped"}
generate_stl=true
-generate_stl_mode="planar"
+generate_stl_mode="curved"
# in 1/10 of milimeters
-height_line=2
-height_base=3
-curv_x=1.5
-curv_y=4
+height_line=2.5
+height_base=6
+curv_x=1.3
+curv_y=3
#----------------------------Application---------------------------#
+# TODO: check if file will overwrite the original, print warning if that's the case
+
# function to apply filter to all files in directory
apply_filter() {
for in in ${file_arr[@]}
@@ -46,14 +48,19 @@ apply_filter() {
[[ -f "$in" ]] || continue
# skip stl files and files with preset name in them
- [[ "$in" == *_* ]] && continue
+ [[ "$in" == *"ridge"* ]] && continue
[[ "$in" == *".stl" ]] && continue
((i++))
- echo -e "|\n|----------------------- File no. $i: $in ------------------------------|\n"
-
+ echo -e "\n|----------------------- File no. $i: $in ------------------------------|\n"
if $generate_stl; then
+
out="${in%%${match1}*}${match1}$format"
+
+ if [[ "$in" == "$out" ]]; then
+ echo "SCRIPT: Changing filename to avoid overwrite"
+ out="${in%%${match1}*}_mod${match1}$format"
+ fi
case "$generate_stl_mode" in
"planar")
python3 $exec_path $in $out $dpi -c $config_file $1 --stl p $height_line $height_base || break
@@ -65,7 +72,7 @@ apply_filter() {
python3 $exec_path $in $out $dpi -c $config_file $1 --stl m $height_line $height_base $fp_file|| break
;;
*)
- echo "Invalid stl generation mode"
+ echo "SCRIPT: Invalid stl generation mode"
exit 1
;;
esac
@@ -93,11 +100,11 @@ done
# apply all given filter presets to all the discovered files
for preset in ${presets[@]}
do
- echo -e "\n|----------------------- Filter "$preset" ------------------------------|\n|"
+ echo -e "\n|----------------------- Filter "$preset" ------------------------------|\n"
j=0
apply_filter $preset $j
done
echo -e "\n|--------------------------- Done ----------------------------------|\n"
-echo "Skipped $j files"
-echo "Generated $i files"
\ No newline at end of file
+echo "SCRIPT: Skipped $j files"
+echo "SCRIPT: Generated $i files"