1 /*
2  * This file is part of Siril, an astronomy image processor.
3  * Copyright (C) 2005-2011 Francois Meyer (dulle at free.fr)
4  * Copyright (C) 2012-2021 team free-astro (see more in AUTHORS file)
5  * Reference site is https://free-astro.org/index.php/Siril
6  *
7  * Siril is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * Siril is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with Siril. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include <stdio.h>
22 #include <stdarg.h>
23 #include <float.h> // DBL_MIN, DBL_MAX
24 #include <assert.h>
25 #include <ctype.h>
26 #include <string.h>
27 #include "sequence_filtering.h"
28 #include "proto.h" // is_readable_file()
29 #include "registration/registration.h"
30 #include "io/sequence.h"
31 #include "stacking/stacking.h"
32 #include "gui/progress_and_log.h"
33 #include "algos/sorting.h"
34 
35 #define MAX_FILTERS 5
36 
37 /******************* IMAGE FILTERING CRITERIA *******************/
38 
39 /* a criterion exists for each image filtering method, and is called in a
40  * processing to verify if an image should be included or not.
41  * These functions have the same signature, defined in stacking.h as
42  * seq_filter, and return 1 if the tested image will be included and 0 if not.
43  * Several filters can be applied at the same time, using the multiple filter
44  * that executes a list of filter functions (seq_filter_multiple()).
45  */
46 
seq_filter_all(sequence * seq,int nb_img,double any)47 int seq_filter_all(sequence *seq, int nb_img, double any) {
48 	return 1;
49 }
50 
seq_filter_included(sequence * seq,int nb_img,double any)51 int seq_filter_included(sequence *seq, int nb_img, double any) {
52 	return (seq->imgparam[nb_img].incl);
53 }
54 
55 /* filter for deep-sky */
seq_filter_fwhm(sequence * seq,int nb_img,double max_fwhm)56 int seq_filter_fwhm(sequence *seq, int nb_img, double max_fwhm) {
57 	int layer;
58 	if (!seq->regparam) return 0;
59 	layer = get_registration_layer(seq);
60 	if (layer == -1) return 0;
61 	if (!seq->regparam[layer]) return 0;
62 	if (seq->regparam[layer][nb_img].fwhm > 0.0f)
63 		return seq->regparam[layer][nb_img].fwhm <= max_fwhm;
64 	else return 0;
65 }
66 
seq_filter_weighted_fwhm(sequence * seq,int nb_img,double max_fwhm)67 int seq_filter_weighted_fwhm(sequence *seq, int nb_img, double max_fwhm) {
68 	int layer;
69 	if (!seq->regparam) return 0;
70 	layer = get_registration_layer(seq);
71 	if (layer == -1) return 0;
72 	if (!seq->regparam[layer]) return 0;
73 	if (seq->regparam[layer][nb_img].weighted_fwhm > 0.0f)
74 		return seq->regparam[layer][nb_img].weighted_fwhm <= max_fwhm;
75 	else return 0;
76 }
77 
seq_filter_roundness(sequence * seq,int nb_img,double min_rnd)78 int seq_filter_roundness(sequence *seq, int nb_img, double min_rnd) {
79 	int layer;
80 	if (!seq->regparam) return 0;
81 	layer = get_registration_layer(seq);
82 	if (layer == -1) return 0;
83 	if (!seq->regparam[layer]) return 0;
84 	if (seq->regparam[layer][nb_img].roundness > 0.0f)
85 		return seq->regparam[layer][nb_img].roundness >= min_rnd;
86 	else return 0;
87 }
88 
89 /* filter for planetary */
seq_filter_quality(sequence * seq,int nb_img,double max_quality)90 int seq_filter_quality(sequence *seq, int nb_img, double max_quality) {
91 	int layer;
92 	if (!seq->regparam) return 0;
93 	layer = get_registration_layer(seq);
94 	if (layer == -1) return 0;
95 	if (!seq->regparam[layer]) return 0;
96 	if (seq->regparam[layer][nb_img].quality > 0.0)
97 		return seq->regparam[layer][nb_img].quality >= max_quality;
98 	else return 0;
99 }
100 
101 /* browse the images to konw how many fit the criterion, from global data */
102 // ensure that com.seq is loaded before passing it as seq: sequence_is_loaded()
compute_nb_filtered_images(sequence * seq,seq_image_filter filtering_criterion,double filtering_parameter)103 int compute_nb_filtered_images(sequence *seq, seq_image_filter filtering_criterion, double filtering_parameter) {
104 	int i, count = 0;
105 	for (i = 0; i < seq->number; i++) {
106 		if (filtering_criterion(seq, i, filtering_parameter))
107 			count++;
108 	}
109 	fprintf(stdout, "number of filtered-in images: %d\n", count);
110 	return count;
111 }
112 
113 /************** The funny existing file sequence filtering function *****************/
114 
115 static const char *_filter_prefix;
116 
seq_filter_output_doesnt_already_exists(sequence * seq,int in_index,double any)117 static int seq_filter_output_doesnt_already_exists(sequence *seq, int in_index, double any) {
118 	if (!_filter_prefix)
119 		fprintf(stderr, "USING FILTER PREFIX WITHOUT INITIALIZATION\n");
120 	if (seq->type != SEQ_REGULAR || !_filter_prefix)
121 		return 0;
122 	char *dest = fit_sequence_get_image_filename_prefixed(seq, _filter_prefix, in_index);
123 	int retval = is_readable_file(dest);
124 	//fprintf(stdout, "file %s exists: %d\n", dest, retval);
125 	free(dest);
126 	return !retval;
127 }
128 
129 /* in C, there's no dynamic function creation, like lambda functions, so we
130  * simulate that by keeping the captured variables in a static field. The
131  * returned function is simply the regular function above, which is kept
132  * private in order to avoid its uninitialized use. The limitation with this
133  * static field is that it can be used once at a time.
134  */
create_filter_prefixed_nonexisting_output(const char * prefix)135 seq_image_filter create_filter_prefixed_nonexisting_output(const char *prefix) {
136 	_filter_prefix = prefix;
137 	fprintf(stdout, "created filter for prefixed output `%s'\n", prefix);
138 	return seq_filter_output_doesnt_already_exists;
139 }
140 
141 /****************** MULTIPLE FILTERING ********************/
142 
143 static struct filtering_tuple _filters[MAX_FILTERS];
144 
seq_filter_multiple(sequence * seq,int img_index,double any)145 static int seq_filter_multiple(sequence *seq, int img_index, double any) {
146 	int f = 0;
147 	while (f < MAX_FILTERS && _filters[f].filter) {
148 		if (!_filters[f].filter(seq, img_index, _filters[f].param))
149 			return 0;
150 		f++;
151 	}
152 	return 1;
153 }
154 
155 /* configure the multiple filter */
create_multiple_filter(seq_image_filter filter1,double fparam1,...)156 seq_image_filter create_multiple_filter(seq_image_filter filter1, double fparam1, ...) {
157 	va_list args;
158 	int nb_filters = 1;
159 	_filters[0].filter = filter1;
160 	_filters[0].param = fparam1;
161 	va_start(args, fparam1);
162 	seq_image_filter f;
163 	do {
164 		_filters[nb_filters].filter = NULL;
165 		f = va_arg(args, seq_image_filter);
166 		if (f) {
167 			assert(f == seq_filter_multiple); // no multiple of multiple
168 			_filters[nb_filters].filter = f;
169 			_filters[nb_filters].param = va_arg(args, double);
170 			nb_filters++;
171 		}
172 	} while (f && nb_filters < MAX_FILTERS);
173 	fprintf(stdout, "created multiple filter (%d filters)\n", nb_filters);
174 	va_end(args);
175 	return seq_filter_multiple;
176 }
177 
create_multiple_filter_from_list(struct filtering_tuple * filters)178 seq_image_filter create_multiple_filter_from_list(struct filtering_tuple *filters) {
179 	int nb_filters = 0;
180 	while (nb_filters < MAX_FILTERS && filters[nb_filters].filter) {
181 		assert(filters[nb_filters].filter != seq_filter_multiple); // no multiple of multiple
182 		_filters[nb_filters].filter = filters[nb_filters].filter;
183 		_filters[nb_filters].param = filters[nb_filters].param;
184 		nb_filters++;
185 	}
186 	if (nb_filters < MAX_FILTERS)
187 		_filters[nb_filters].filter = NULL;
188 	fprintf(stdout, "created multiple filter (%d filters)\n", nb_filters);
189 	return seq_filter_multiple;
190 }
191 
192 /******************* filtering set-up **********************/
193 
194 // creates the filtering criterion from a stacking configuration
195 // raises an error if the configuration has duplicates
196 // at least two images to be selected
convert_stack_data_to_filter(struct stacking_configuration * arg,struct stacking_args * stackargs)197 int convert_stack_data_to_filter(struct stacking_configuration *arg, struct stacking_args *stackargs) {
198 	int nb_filters = 0;
199 	int layer = get_registration_layer(stackargs->seq);
200 	struct filtering_tuple filters[5] = { { NULL, 0.0 } };
201 
202 	if ((arg->f_fwhm_p > 0.0f && arg->f_fwhm > 0.0f) ||
203 			(arg->f_wfwhm_p > 0.0f && arg->f_wfwhm > 0.0f) ||
204 			(arg->f_round_p > 0.0f && arg->f_round > 0.0f) ||
205 			(arg->f_quality_p > 0.0f && arg->f_quality > 0.0f)) {
206 		siril_log_message(_("Sequence filter: values can only be either literal or percent\n"));
207 		return 1;
208 	}
209 	if (arg->filter_included) {
210 		filters[nb_filters].filter = seq_filter_included;
211 		filters[nb_filters].param = stackargs->seq->selnum;
212 		siril_log_message(_("Using selected images filter (%d/%d of the sequence)\n"),
213 				stackargs->seq->selnum, stackargs->seq->number);
214 		nb_filters++;
215 	}
216 	if (arg->f_fwhm_p > 0.0f || arg->f_fwhm > 0.0f) {
217 		filters[nb_filters].filter = seq_filter_fwhm;
218 		filters[nb_filters].param = arg->f_fwhm > 0.f ? arg->f_fwhm :
219 				compute_highest_accepted_fwhm(stackargs->seq, layer, arg->f_fwhm_p);
220 		siril_log_message(_("Using star FWHM images filter (below %f)\n"),
221 					filters[nb_filters].param);
222 		       	nb_filters++;
223 	}
224 	if (arg->f_wfwhm_p > 0.0f || arg->f_wfwhm > 0.0f) {
225 		filters[nb_filters].filter = seq_filter_weighted_fwhm;
226 		filters[nb_filters].param = arg->f_wfwhm > 0.f ? arg->f_wfwhm :
227 				compute_highest_accepted_weighted_fwhm(stackargs->seq, layer, arg->f_wfwhm_p);
228 		siril_log_message(_("Using star weighted FWHM images filter (below %f)\n"),
229 					filters[nb_filters].param);
230 		       	nb_filters++;
231 	}
232 	if (arg->f_round_p > 0.0f || arg->f_round > 0.0f) {
233 		filters[nb_filters].filter = seq_filter_roundness;
234 		filters[nb_filters].param = arg->f_round > 0.f ? arg->f_round :
235 			compute_lowest_accepted_roundness(stackargs->seq, layer, arg->f_round_p);
236 		siril_log_message(_("Using star roundness images filter (above %f)\n"),
237 				filters[nb_filters].param);
238 		nb_filters++;
239 	}
240 	if (arg->f_quality_p > 0.0f || arg->f_quality > 0.0f) {
241 		filters[nb_filters].filter = seq_filter_quality;
242 		filters[nb_filters].param = arg->f_quality > 0.f ? arg->f_quality :
243 			compute_lowest_accepted_quality(stackargs->seq, layer, arg->f_quality_p);
244 		siril_log_message(_("Using image quality filter (below %f)\n"),
245 				filters[nb_filters].param);
246 		nb_filters++;
247 	}
248 	filters[nb_filters].filter = NULL;
249 
250 	if (nb_filters == 0) {
251 		stackargs->filtering_criterion = seq_filter_all;
252 		stackargs->filtering_parameter = 0.0;
253 	}
254 	else if (nb_filters == 1) {
255 		stackargs->filtering_criterion = filters[0].filter;
256 		stackargs->filtering_parameter = filters[0].param;
257 	}
258 	else {
259 		stackargs->filtering_criterion = create_multiple_filter_from_list(filters);
260 		stackargs->filtering_parameter = -1.0;
261 	}
262 	return 0;
263 }
264 
265 // have to be set or init before calling:
266 // seq, filtering_criterion, filtering_parameter, image_indices
setup_filtered_data(struct stacking_args * args)267 int setup_filtered_data(struct stacking_args *args) {
268 	args->nb_images_to_stack = compute_nb_filtered_images(args->seq,
269 			args->filtering_criterion, args->filtering_parameter);
270 	if (args->nb_images_to_stack < 2) {
271 		siril_log_message(_("Provided filtering options do not allow at least two images to be processed.\n"));
272 		return 1;
273 	}
274 	if (args->image_indices)
275 		free(args->image_indices);
276 	return stack_fill_list_of_unfiltered_images(args);
277 }
278 
279 /* fill the image_indices mapping for the args->image_indices array.
280  * args->image_indices will be allocated to nb_images_to_stack. */
stack_fill_list_of_unfiltered_images(struct stacking_args * args)281 int stack_fill_list_of_unfiltered_images(struct stacking_args *args) {
282 	int i, j;
283 	if (args->image_indices) {
284 		int *newptr = realloc(args->image_indices, args->nb_images_to_stack * sizeof(int));
285 		if (!newptr) {
286 			PRINT_ALLOC_ERR;
287 			free(args->image_indices);
288 			args->image_indices = NULL;
289 			return 1;
290 		}
291 		args->image_indices = newptr;
292 	} else {
293 		args->image_indices = malloc(args->nb_images_to_stack * sizeof(int));
294 		if (!args->image_indices) {
295 			PRINT_ALLOC_ERR;
296 			return 1;
297 		}
298 	}
299 
300 	for (i = 0, j = 0; i < args->seq->number; i++) {
301 		if (args->filtering_criterion(
302 					args->seq, i,
303 					args->filtering_parameter)) {
304 			args->image_indices[j] = i;
305 			j++;
306 		}
307 		else if (i == args->ref_image) {
308 			siril_log_color_message(_("The reference image is not in the selected set of images. "
309 					"To avoid issues, please change it or change the filtering parameters.\n"), "red");
310 			args->ref_image = -1;
311 		}
312 	}
313 	if (args->ref_image == -1) {
314 		args->ref_image = args->image_indices[0];
315 		siril_log_message(_("Using image %d as temporary reference image\n"), args->ref_image + 1);
316 	}
317 	return j != args->nb_images_to_stack;
318 }
319 
320 typedef double (*regdata_selector)(regdata *reg);
321 
regdata_fwhm(regdata * reg)322 static double regdata_fwhm(regdata *reg) { return reg->fwhm; }
regdata_weighted_fwhm(regdata * reg)323 static double regdata_weighted_fwhm(regdata *reg) { return reg->weighted_fwhm; }
regdata_roundness(regdata * reg)324 static double regdata_roundness(regdata *reg) { return reg->roundness; }
regdata_quality(regdata * reg)325 static double regdata_quality(regdata *reg) { return reg->quality; }
326 
327 /* from a percentage, find the lowest or highest accepted registration property
328  * value for image filtering in sequences. */
generic_compute_accepted_value(sequence * seq,int layer,double percent,gboolean lower_is_better,regdata_selector datasel)329 static double generic_compute_accepted_value(sequence *seq, int layer, double percent, gboolean lower_is_better, regdata_selector datasel) {
330 	int i, number_images_with_data;
331 	double threshold, *val;
332 	double extreme_value = lower_is_better ? DBL_MAX : DBL_MIN;
333 	if (layer < 0 || !seq->regparam || !seq->regparam[layer]) {
334 		return 0.0;
335 	}
336 	val = malloc(seq->number * sizeof(double));
337 	if (!val) return 0.0;
338 
339 	// copy values
340 	for (i = 0; i < seq->number; i++) {
341 		double data = datasel(&seq->regparam[layer][i]);
342 		val[i] = data <= 0.0f ? extreme_value : data;
343 	}
344 
345 	//sort values
346 	quicksort_d(val, seq->number);
347 
348 	if (val[seq->number-1] != extreme_value) {
349 		number_images_with_data = seq->number;
350 	} else {
351 		for (i = 0; i < seq->number; i++)
352 			if (val[i] == extreme_value)
353 				break;
354 		number_images_with_data = i;
355 		siril_log_message(_("Warning: some images don't have information available for best "
356 				"images selection, using only available data (%d images on %d).\n"),
357 				number_images_with_data, seq->number);
358 	}
359 
360 	// get highest or lowest accepted (threshold)
361 	double images_number = (double)(number_images_with_data - 1);
362 	if (lower_is_better) {
363 		threshold = val[(int)(percent * images_number / 100.0)];
364 		if (threshold == extreme_value)
365 			threshold = 0.0;
366 	} else {
367 		threshold = val[(int)((100.0 - percent) * images_number / 100.0)];
368 	}
369 
370 	free(val);
371 	return threshold;
372 }
373 
compute_highest_accepted_fwhm(sequence * seq,int layer,double percent)374 double compute_highest_accepted_fwhm(sequence *seq, int layer, double percent) {
375 	return generic_compute_accepted_value(seq, layer, percent, TRUE, regdata_fwhm);
376 }
377 
compute_highest_accepted_weighted_fwhm(sequence * seq,int layer,double percent)378 double compute_highest_accepted_weighted_fwhm(sequence *seq, int layer, double percent) {
379 	return generic_compute_accepted_value(seq, layer, percent, TRUE, regdata_weighted_fwhm);
380 }
381 
compute_lowest_accepted_quality(sequence * seq,int layer,double percent)382 double compute_lowest_accepted_quality(sequence *seq, int layer, double percent) {
383 	return generic_compute_accepted_value(seq, layer, percent, FALSE, regdata_quality);
384 }
385 
compute_lowest_accepted_roundness(sequence * seq,int layer,double percent)386 double compute_lowest_accepted_roundness(sequence *seq, int layer, double percent) {
387 	return generic_compute_accepted_value(seq, layer, percent, FALSE, regdata_roundness);
388 }
389 
describe_filter(sequence * seq,seq_image_filter filtering_criterion,double filtering_parameter)390 char *describe_filter(sequence *seq, seq_image_filter filtering_criterion, double filtering_parameter) {
391 	GString *str = g_string_sized_new(100);
392 	int nb_images_to_stack = compute_nb_filtered_images(seq,
393 			filtering_criterion, filtering_parameter);
394 
395 	if (filtering_criterion == seq_filter_all) {
396 		g_string_printf(str, _("Processing all images in the sequence (%d)\n"), seq->number);
397 	} else if (filtering_criterion == seq_filter_included) {
398 		g_string_printf(str, _("Processing only selected images in the sequence (%d)\n"), seq->selnum);
399 	} else if (filtering_criterion == seq_filter_fwhm) {
400 		g_string_printf(str, _("Processing images of the sequence "
401 					"with a FWHM lower or equal than %g (%d)\n"),
402 				filtering_parameter, nb_images_to_stack);
403 	} else if (filtering_criterion == seq_filter_weighted_fwhm) {
404 			g_string_printf(str, _("Processing images of the sequence "
405 						"with a weighted FWHM lower or equal than %g (%d)\n"),
406 					filtering_parameter, nb_images_to_stack);
407 	} else if (filtering_criterion == seq_filter_roundness) {
408 		g_string_printf(str, _("Processing images of the sequence "
409 					"with a roundness higher or equal than %g (%d)\n"),
410 				filtering_parameter, nb_images_to_stack);
411 	} else if (filtering_criterion == seq_filter_quality) {
412 		g_string_printf(str, _("Processing images of the sequence "
413 					"with a quality higher or equal than %g (%d)\n"),
414 				filtering_parameter, nb_images_to_stack);
415 	} else if (filtering_criterion == seq_filter_output_doesnt_already_exists) {
416 		g_string_printf(str, _("Processing images whose output don't already exist (%d)"),
417 				nb_images_to_stack);
418 	} else if (filtering_criterion == seq_filter_multiple) {
419 		int f = 0;
420 		while (f < MAX_FILTERS && _filters[f].filter) {
421 			struct filtering_tuple *filter = _filters + f;
422 			char *descr = describe_filter(seq, filter->filter, filter->param);
423 			if (descr && descr[0] != '\0') {
424 				descr[strlen(descr)-1] = '\0';	// remove the new line
425 				if (f) descr[0] = tolower(descr[0]);
426 			}
427 			g_string_append(str, descr);
428 			g_string_append(str, ", ");
429 			g_free(descr);
430 			f++;
431 		}
432 		g_string_append_printf(str, _("for a total of images processed of %d)\n"),
433 				nb_images_to_stack);
434 	}
435 	//fprintf(stdout, "FILTERING DECRIPTION: %s", str->str);
436 	return g_string_free(str, FALSE);
437 }
438