Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb9ab9ca5c | ||
|
|
863cd1d476 | ||
|
|
cca29c1f38 |
-387
@@ -1,387 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
# All files
|
|
||||||
[*]
|
|
||||||
indent_style = space
|
|
||||||
|
|
||||||
# Xml files
|
|
||||||
[*.xml]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
# C# files
|
|
||||||
[*.cs]
|
|
||||||
|
|
||||||
#### Core EditorConfig Options ####
|
|
||||||
|
|
||||||
# Indentation and spacing
|
|
||||||
indent_size = 2
|
|
||||||
tab_width = 2
|
|
||||||
|
|
||||||
# New line preferences
|
|
||||||
insert_final_newline = false
|
|
||||||
|
|
||||||
#### .NET Coding Conventions ####
|
|
||||||
[*.{cs,vb}]
|
|
||||||
|
|
||||||
# Organize usings
|
|
||||||
dotnet_separate_import_directive_groups = true
|
|
||||||
dotnet_sort_system_directives_first = true
|
|
||||||
file_header_template = unset
|
|
||||||
|
|
||||||
# this. and Me. preferences
|
|
||||||
dotnet_style_qualification_for_event = false:silent
|
|
||||||
dotnet_style_qualification_for_field = false:silent
|
|
||||||
dotnet_style_qualification_for_method = false:silent
|
|
||||||
dotnet_style_qualification_for_property = false:silent
|
|
||||||
|
|
||||||
# Language keywords vs BCL types preferences
|
|
||||||
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
|
|
||||||
dotnet_style_predefined_type_for_member_access = true:silent
|
|
||||||
|
|
||||||
# Parentheses preferences
|
|
||||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
|
|
||||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
|
|
||||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
|
|
||||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
|
|
||||||
|
|
||||||
# Modifier preferences
|
|
||||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
|
|
||||||
|
|
||||||
# Expression-level preferences
|
|
||||||
dotnet_style_coalesce_expression = true:suggestion
|
|
||||||
dotnet_style_collection_initializer = true:suggestion
|
|
||||||
dotnet_style_explicit_tuple_names = true:suggestion
|
|
||||||
dotnet_style_namespace_match_folder = true:suggestion
|
|
||||||
dotnet_style_null_propagation = true:suggestion
|
|
||||||
dotnet_style_object_initializer = true:suggestion
|
|
||||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
|
||||||
dotnet_style_prefer_auto_properties = true:suggestion
|
|
||||||
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
|
|
||||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
|
||||||
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
|
|
||||||
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
|
|
||||||
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion
|
|
||||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
|
||||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
|
||||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
|
||||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
|
||||||
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
|
||||||
|
|
||||||
# Field preferences
|
|
||||||
dotnet_style_readonly_field = true:warning
|
|
||||||
|
|
||||||
# Parameter preferences
|
|
||||||
dotnet_code_quality_unused_parameters = all:suggestion
|
|
||||||
|
|
||||||
# Suppression preferences
|
|
||||||
dotnet_remove_unnecessary_suppression_exclusions = none
|
|
||||||
|
|
||||||
#### C# Coding Conventions ####
|
|
||||||
[*.cs]
|
|
||||||
|
|
||||||
# diagnostics
|
|
||||||
dotnet_diagnostic.CA1303.severity = none
|
|
||||||
dotnet_diagnostic.CA1707.severity = none
|
|
||||||
dotnet_diagnostic.IDE0058.severity = none
|
|
||||||
dotnet_diagnostic.CA2007.severity = none
|
|
||||||
dotnet_diagnostic.CA1515.severity = none
|
|
||||||
dotnet_diagnostic.IDE0100.severity = none
|
|
||||||
dotnet_diagnostic.IDE0046.severity = none
|
|
||||||
|
|
||||||
# var preferences
|
|
||||||
csharp_style_var_elsewhere = true:suggestion
|
|
||||||
csharp_style_var_for_built_in_types = true:suggestion
|
|
||||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
|
||||||
|
|
||||||
# Expression-bodied members
|
|
||||||
csharp_style_expression_bodied_accessors = true:silent
|
|
||||||
csharp_style_expression_bodied_constructors = false:silent
|
|
||||||
csharp_style_expression_bodied_indexers = true:silent
|
|
||||||
csharp_style_expression_bodied_lambdas = true:suggestion
|
|
||||||
csharp_style_expression_bodied_local_functions = false:silent
|
|
||||||
csharp_style_expression_bodied_methods = false:silent
|
|
||||||
csharp_style_expression_bodied_operators = false:silent
|
|
||||||
csharp_style_expression_bodied_properties = true:silent
|
|
||||||
|
|
||||||
# Pattern matching preferences
|
|
||||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
|
||||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
|
||||||
csharp_style_prefer_extended_property_pattern = true:suggestion
|
|
||||||
csharp_style_prefer_not_pattern = true:suggestion
|
|
||||||
csharp_style_prefer_pattern_matching = true:silent
|
|
||||||
csharp_style_prefer_switch_expression = true:suggestion
|
|
||||||
|
|
||||||
# Null-checking preferences
|
|
||||||
csharp_style_conditional_delegate_call = true:suggestion
|
|
||||||
|
|
||||||
# Modifier preferences
|
|
||||||
csharp_prefer_static_anonymous_function = true:suggestion
|
|
||||||
csharp_prefer_static_local_function = true:warning
|
|
||||||
csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion
|
|
||||||
csharp_style_prefer_readonly_struct = true:suggestion
|
|
||||||
csharp_style_prefer_readonly_struct_member = true:suggestion
|
|
||||||
|
|
||||||
# Code-block preferences
|
|
||||||
csharp_prefer_braces = true:silent
|
|
||||||
csharp_prefer_simple_using_statement = true:suggestion
|
|
||||||
csharp_style_namespace_declarations = file_scoped:suggestion
|
|
||||||
csharp_style_prefer_method_group_conversion = true:silent
|
|
||||||
csharp_style_prefer_primary_constructors = true:suggestion
|
|
||||||
csharp_style_prefer_top_level_statements = true:silent
|
|
||||||
|
|
||||||
# Expression-level preferences
|
|
||||||
csharp_prefer_simple_default_expression = true:suggestion
|
|
||||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
|
||||||
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
|
|
||||||
csharp_style_inlined_variable_declaration = true:suggestion
|
|
||||||
csharp_style_prefer_index_operator = true:suggestion
|
|
||||||
csharp_style_prefer_local_over_anonymous_function = true:suggestion
|
|
||||||
csharp_style_prefer_null_check_over_type_check = true:suggestion
|
|
||||||
csharp_style_prefer_range_operator = true:suggestion
|
|
||||||
csharp_style_prefer_tuple_swap = true:suggestion
|
|
||||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
|
||||||
csharp_style_throw_expression = true:suggestion
|
|
||||||
csharp_style_unused_value_assignment_preference = discard_variable:silent
|
|
||||||
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
|
||||||
|
|
||||||
# 'using' directive preferences
|
|
||||||
csharp_using_directive_placement = outside_namespace:silent
|
|
||||||
|
|
||||||
#### C# Formatting Rules ####
|
|
||||||
|
|
||||||
# New line preferences
|
|
||||||
csharp_new_line_before_catch = true
|
|
||||||
csharp_new_line_before_else = true
|
|
||||||
csharp_new_line_before_finally = true
|
|
||||||
csharp_new_line_before_members_in_anonymous_types = true
|
|
||||||
csharp_new_line_before_members_in_object_initializers = true
|
|
||||||
csharp_new_line_before_open_brace = all
|
|
||||||
csharp_new_line_between_query_expression_clauses = true
|
|
||||||
|
|
||||||
# Indentation preferences
|
|
||||||
csharp_indent_block_contents = true
|
|
||||||
csharp_indent_braces = false
|
|
||||||
csharp_indent_case_contents = true
|
|
||||||
csharp_indent_case_contents_when_block = true
|
|
||||||
csharp_indent_labels = one_less_than_current
|
|
||||||
csharp_indent_switch_labels = true
|
|
||||||
|
|
||||||
# Space preferences
|
|
||||||
csharp_space_after_cast = false
|
|
||||||
csharp_space_after_colon_in_inheritance_clause = true
|
|
||||||
csharp_space_after_comma = true
|
|
||||||
csharp_space_after_dot = false
|
|
||||||
csharp_space_after_keywords_in_control_flow_statements = true
|
|
||||||
csharp_space_after_semicolon_in_for_statement = true
|
|
||||||
csharp_space_around_binary_operators = before_and_after
|
|
||||||
csharp_space_around_declaration_statements = false
|
|
||||||
csharp_space_before_colon_in_inheritance_clause = true
|
|
||||||
csharp_space_before_comma = false
|
|
||||||
csharp_space_before_dot = false
|
|
||||||
csharp_space_before_open_square_brackets = false
|
|
||||||
csharp_space_before_semicolon_in_for_statement = false
|
|
||||||
csharp_space_between_empty_square_brackets = false
|
|
||||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
|
||||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
|
||||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
|
||||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
|
||||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
|
||||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
|
||||||
csharp_space_between_parentheses = false
|
|
||||||
csharp_space_between_square_brackets = false
|
|
||||||
|
|
||||||
# Wrapping preferences
|
|
||||||
csharp_preserve_single_line_blocks = true
|
|
||||||
csharp_preserve_single_line_statements = true
|
|
||||||
|
|
||||||
#### Naming styles ####
|
|
||||||
[*.{cs,vb}]
|
|
||||||
|
|
||||||
# Naming rules
|
|
||||||
|
|
||||||
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
|
|
||||||
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
|
|
||||||
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
|
|
||||||
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
|
|
||||||
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
|
|
||||||
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
|
|
||||||
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
|
|
||||||
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
|
|
||||||
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
|
|
||||||
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
|
|
||||||
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
|
|
||||||
dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
|
|
||||||
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
|
|
||||||
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
|
|
||||||
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
|
|
||||||
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
|
|
||||||
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
|
|
||||||
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
|
|
||||||
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
|
|
||||||
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
|
|
||||||
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
|
|
||||||
|
|
||||||
# Symbol specifications
|
|
||||||
|
|
||||||
dotnet_naming_symbols.interfaces.applicable_kinds = interface
|
|
||||||
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.interfaces.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.enums.applicable_kinds = enum
|
|
||||||
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.enums.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.events.applicable_kinds = event
|
|
||||||
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.events.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.methods.applicable_kinds = method
|
|
||||||
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.methods.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.properties.applicable_kinds = property
|
|
||||||
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.properties.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.public_fields.applicable_kinds = field
|
|
||||||
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
|
|
||||||
dotnet_naming_symbols.public_fields.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.private_fields.applicable_kinds = field
|
|
||||||
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.private_fields.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
|
|
||||||
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.private_static_fields.required_modifiers = static
|
|
||||||
|
|
||||||
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
|
|
||||||
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
|
||||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
|
|
||||||
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
|
|
||||||
dotnet_naming_symbols.type_parameters.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
|
|
||||||
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.private_constant_fields.required_modifiers = const
|
|
||||||
|
|
||||||
dotnet_naming_symbols.local_variables.applicable_kinds = local
|
|
||||||
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
|
|
||||||
dotnet_naming_symbols.local_variables.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.local_constants.applicable_kinds = local
|
|
||||||
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
|
|
||||||
dotnet_naming_symbols.local_constants.required_modifiers = const
|
|
||||||
|
|
||||||
dotnet_naming_symbols.parameters.applicable_kinds = parameter
|
|
||||||
dotnet_naming_symbols.parameters.applicable_accessibilities = *
|
|
||||||
dotnet_naming_symbols.parameters.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
|
|
||||||
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
|
|
||||||
dotnet_naming_symbols.public_constant_fields.required_modifiers = const
|
|
||||||
|
|
||||||
dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
|
|
||||||
dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
|
|
||||||
dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
|
|
||||||
|
|
||||||
dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
|
|
||||||
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
|
|
||||||
|
|
||||||
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
|
|
||||||
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
|
|
||||||
dotnet_naming_symbols.local_functions.required_modifiers =
|
|
||||||
|
|
||||||
# Naming styles
|
|
||||||
|
|
||||||
dotnet_naming_style.pascalcase.required_prefix =
|
|
||||||
dotnet_naming_style.pascalcase.required_suffix =
|
|
||||||
dotnet_naming_style.pascalcase.word_separator =
|
|
||||||
dotnet_naming_style.pascalcase.capitalization = pascal_case
|
|
||||||
|
|
||||||
dotnet_naming_style.ipascalcase.required_prefix = I
|
|
||||||
dotnet_naming_style.ipascalcase.required_suffix =
|
|
||||||
dotnet_naming_style.ipascalcase.word_separator =
|
|
||||||
dotnet_naming_style.ipascalcase.capitalization = pascal_case
|
|
||||||
|
|
||||||
dotnet_naming_style.tpascalcase.required_prefix = T
|
|
||||||
dotnet_naming_style.tpascalcase.required_suffix =
|
|
||||||
dotnet_naming_style.tpascalcase.word_separator =
|
|
||||||
dotnet_naming_style.tpascalcase.capitalization = pascal_case
|
|
||||||
|
|
||||||
dotnet_naming_style._camelcase.required_prefix = _
|
|
||||||
dotnet_naming_style._camelcase.required_suffix =
|
|
||||||
dotnet_naming_style._camelcase.word_separator =
|
|
||||||
dotnet_naming_style._camelcase.capitalization = camel_case
|
|
||||||
|
|
||||||
dotnet_naming_style.camelcase.required_prefix =
|
|
||||||
dotnet_naming_style.camelcase.required_suffix =
|
|
||||||
dotnet_naming_style.camelcase.word_separator =
|
|
||||||
dotnet_naming_style.camelcase.capitalization = camel_case
|
|
||||||
|
|
||||||
dotnet_naming_style.s_camelcase.required_prefix = s_
|
|
||||||
dotnet_naming_style.s_camelcase.required_suffix =
|
|
||||||
dotnet_naming_style.s_camelcase.word_separator =
|
|
||||||
dotnet_naming_style.s_camelcase.capitalization = camel_case
|
|
||||||
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
name: Publish
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
paths-ignore:
|
|
||||||
- "**/README.md"
|
|
||||||
- "**/.gitignore"
|
|
||||||
- "**/.editorconfig"
|
|
||||||
- "**/LICENSE.md"
|
|
||||||
- .github/**
|
|
||||||
- src/BGR.Console.Tests/**
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
jobs:
|
|
||||||
version:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.ACTIONS_PAT }}
|
|
||||||
- name: Setup .NET 10
|
|
||||||
uses: actions/setup-dotnet@v5
|
|
||||||
with:
|
|
||||||
dotnet-version: 10.x
|
|
||||||
- name: Install versionize
|
|
||||||
run: dotnet tool install --global Versionize
|
|
||||||
- name: Setup git
|
|
||||||
run: |
|
|
||||||
git config --local user.email "65925598+StevanFreeborn@users.noreply.github.com"
|
|
||||||
git config --local user.name "Stevan Freeborn"
|
|
||||||
- name: Run versionize
|
|
||||||
id: versionize
|
|
||||||
run: |
|
|
||||||
VERSION=$(versionize -i --exit-insignificant-commits --workingDir ./src/BGR.Console --commit-suffix "[skip ci]")
|
|
||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Ensure csproj version is updated
|
|
||||||
if: steps.versionize.outcome == 'success'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.versionize.outputs.version }}"
|
|
||||||
NUMERIC="${VERSION#v}"
|
|
||||||
sed -i "s|<Version>.*</Version>|<Version>${NUMERIC}</Version>|" src/BGR.Console/BGR.Console.csproj
|
|
||||||
git add src/BGR.Console/BGR.Console.csproj
|
|
||||||
git diff --cached --quiet || git commit --amend --no-edit
|
|
||||||
- name: Upload changelog
|
|
||||||
if: steps.versionize.outcome == 'success'
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: change-log
|
|
||||||
path: src/BGR.Console/CHANGELOG.md
|
|
||||||
- name: Push changes to GitHub
|
|
||||||
if: steps.versionize.outcome == 'success'
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.ACTIONS_PAT }}
|
|
||||||
branch: ${{ github.ref }}
|
|
||||||
tags: true
|
|
||||||
outputs:
|
|
||||||
is_new_version: ${{ steps.versionize.outcome == 'success' }}
|
|
||||||
version: ${{ steps.versionize.outputs.version }}
|
|
||||||
publish:
|
|
||||||
needs: [version]
|
|
||||||
if: needs.version.outputs.is_new_version == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ github.ref }}
|
|
||||||
token: ${{ secrets.ACTIONS_PAT }}
|
|
||||||
- name: Setup .NET 10
|
|
||||||
uses: actions/setup-dotnet@v5
|
|
||||||
with:
|
|
||||||
dotnet-version: 10.x
|
|
||||||
- name: Build
|
|
||||||
run: dotnet build src
|
|
||||||
- name: Publish for mac-os
|
|
||||||
run: dotnet publish src/BGR.Console/BGR.Console.csproj -c Release -r osx-x64 --self-contained -o dist/mac-os
|
|
||||||
- name: Create zip for mac-os
|
|
||||||
run: zip -j dist/bgr_osx_x64.zip dist/mac-os/*
|
|
||||||
- name: Publish for linux-os
|
|
||||||
run: dotnet publish src/BGR.Console/BGR.Console.csproj -c Release -r linux-x64 --self-contained -o dist/linux-os
|
|
||||||
- name: Create zip for linux-os
|
|
||||||
run: zip -j dist/bgr_linux_x64.zip dist/linux-os/*
|
|
||||||
- name: Publish for windows-os
|
|
||||||
run: dotnet publish src/BGR.Console/BGR.Console.csproj -c Release -r win-x64 --self-contained -o dist/windows-os
|
|
||||||
- name: Create zip for windows-os
|
|
||||||
run: zip -j dist/bgr_windows_x64.zip dist/windows-os/*
|
|
||||||
- name: Download changlog
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
name: change-log
|
|
||||||
path: src/BGR.Console
|
|
||||||
- name: Create release
|
|
||||||
uses: softprops/action-gh-release@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.ACTIONS_PAT }}
|
|
||||||
name: bgr ${{ needs.version.outputs.version }}
|
|
||||||
tag_name: ${{ needs.version.outputs.version }}
|
|
||||||
draft: false
|
|
||||||
body_path: src/BGR.Console/CHANGELOG.md
|
|
||||||
files: |
|
|
||||||
dist/bgr_osx_x64.zip
|
|
||||||
dist/bgr_linux_x64.zip
|
|
||||||
dist/bgr_windows_x64.zip
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
name: Pull Request
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
|
||||||
paths-ignore:
|
|
||||||
- "**/README.md"
|
|
||||||
- "**/LICENSE.md"
|
|
||||||
- "**/.gitignore"
|
|
||||||
- "**/.editorconfig"
|
|
||||||
- .github/**
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
jobs:
|
|
||||||
format:
|
|
||||||
name: Run dotnet format
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.ACTIONS_PAT }}
|
|
||||||
- name: Setup .NET 10
|
|
||||||
uses: actions/setup-dotnet@v5
|
|
||||||
with:
|
|
||||||
dotnet-version: 10.x.x
|
|
||||||
- name: Format project
|
|
||||||
run: dotnet format src --verbosity normal
|
|
||||||
- name: Commit Changes
|
|
||||||
run: |
|
|
||||||
git config user.name "GitHub Actions"
|
|
||||||
git config user.email "<>"
|
|
||||||
if [[ $(git status --porcelain) ]]; then
|
|
||||||
git add .
|
|
||||||
git commit -m "chore: format fixes [skip ci]"
|
|
||||||
git fetch origin
|
|
||||||
git pull --rebase origin ${{ github.head_ref }}
|
|
||||||
git push origin HEAD:${{ github.head_ref }}
|
|
||||||
fi
|
|
||||||
test:
|
|
||||||
name: Test
|
|
||||||
needs: format
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.ACTIONS_PAT }}
|
|
||||||
- name: Setup .NET 10
|
|
||||||
uses: actions/setup-dotnet@v5
|
|
||||||
with:
|
|
||||||
dotnet-version: 10.x
|
|
||||||
- name: Install report generator
|
|
||||||
run: dotnet tool install --global dotnet-reportgenerator-globaltool
|
|
||||||
- name: Restore dependencies
|
|
||||||
run: dotnet restore src
|
|
||||||
- name: Build on ${{ matrix.os }}
|
|
||||||
run: dotnet build --no-restore src
|
|
||||||
- name: Test on ${{ matrix.os }}
|
|
||||||
run: dotnet test src --no-build --verbosity normal
|
|
||||||
- name: Rename test coverage report
|
|
||||||
run: mv src/BGR.Console.Tests/TestResults/Coverage/coverage.cobertura.xml src/BGR.Console.Tests/TestResults/Coverage/${{ matrix.os }}-coverage.cobertura.xml
|
|
||||||
- name: Upload test coverage report for ${{ matrix.os }}
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: test-coverage-${{ matrix.os }}
|
|
||||||
path: src/BGR.Console.Tests/TestResults/Coverage/${{ matrix.os }}-coverage.cobertura.xml
|
|
||||||
upload_test_coverage:
|
|
||||||
name: Upload Test Coverage
|
|
||||||
needs: test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.ACTIONS_PAT }}
|
|
||||||
- name: Download ubuntu-latest report
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
name: test-coverage-ubuntu-latest
|
|
||||||
path: ./coverage
|
|
||||||
- name: Download windows-latest report
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
name: test-coverage-windows-latest
|
|
||||||
path: ./coverage
|
|
||||||
- name: Download macos-latest report
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
name: test-coverage-macos-latest
|
|
||||||
path: ./coverage
|
|
||||||
- name: Upload test coverage reports
|
|
||||||
uses: codecov/codecov-action@v7
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
# dotenv files
|
# dotenv files
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# model files
|
# models
|
||||||
*.onnx
|
*.onnx
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="modnet.onnx" />
|
||||||
|
<EmbeddedResource Include="rmbg.onnx" />
|
||||||
|
<EmbeddedResource Include="u2net.onnx" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
using Microsoft.ML.OnnxRuntime;
|
||||||
|
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||||
|
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats.Png;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
|
if (args.Length < 1)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Usage: BackgroundRemover <input_image_path>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputImagePath = args[0];
|
||||||
|
var maskImagePath = Path.ChangeExtension(inputImagePath, null) + "_mask.png";
|
||||||
|
var outputImagePath = Path.ChangeExtension(inputImagePath, null) + "_no_bg.png";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
var resourceName = "BGR.Console.u2net.onnx";
|
||||||
|
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName) ?? throw new FileNotFoundException("Model not found in embedded resources.");
|
||||||
|
var modelBytes = new byte[stream.Length];
|
||||||
|
stream.ReadExactly(modelBytes);
|
||||||
|
|
||||||
|
using var image = Image.Load<Rgba32>(inputImagePath);
|
||||||
|
var inputTensor = CreateTensorInput(image);
|
||||||
|
|
||||||
|
using var options = new SessionOptions() { LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR };
|
||||||
|
using InferenceSession session = new(modelBytes, options);
|
||||||
|
var inputs = new List<NamedOnnxValue>()
|
||||||
|
{
|
||||||
|
NamedOnnxValue.CreateFromTensor(session.InputNames[0], inputTensor),
|
||||||
|
};
|
||||||
|
|
||||||
|
using var results = session.Run(inputs);
|
||||||
|
var outputTensor = results[0].AsTensor<float>();
|
||||||
|
|
||||||
|
using var mask = GenerateMask(outputTensor, image.Width, image.Height);
|
||||||
|
|
||||||
|
using var bgRemoved = GetImageWithBackgroundRemoved(image, mask);
|
||||||
|
|
||||||
|
var encoder = new PngEncoder { CompressionLevel = PngCompressionLevel.BestCompression };
|
||||||
|
|
||||||
|
mask.Save(maskImagePath, encoder);
|
||||||
|
bgRemoved.Save(outputImagePath, encoder);
|
||||||
|
|
||||||
|
Console.WriteLine($"Background removed and saved to {outputImagePath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
static DenseTensor<float> CreateTensorInput(Image<Rgba32> image)
|
||||||
|
{
|
||||||
|
// U2Net expects input images to be 320x320. This is dependent on the model.
|
||||||
|
const int targetWidth = 320;
|
||||||
|
const int targetHeight = 320;
|
||||||
|
|
||||||
|
// ImageNet normalization parameters
|
||||||
|
// source:
|
||||||
|
// - https://www.image-net.org/
|
||||||
|
// - https://pytorch.org calculated these values from the ImageNet dataset
|
||||||
|
// and they are commonly used for models trained on ImageNet so we use them here
|
||||||
|
// to normalize the input image to better match the distribution of the data the model was trained on
|
||||||
|
// NOTE: These values are not universal and may vary for different models
|
||||||
|
const float rMean = 0.485f; // Mean value for Red channel
|
||||||
|
const float gMean = 0.456f; // Mean value for Green channel
|
||||||
|
const float bMean = 0.406f; // Mean value for Blue channel
|
||||||
|
const float rStd = 0.229f; // Standard deviation for Red channel
|
||||||
|
const float gStd = 0.224f; // Standard deviation for Green channel
|
||||||
|
const float bStd = 0.225f; // Standard deviation for Blue channel
|
||||||
|
const float pixelMax = 255f; // Maximum pixel intensity for normalization
|
||||||
|
|
||||||
|
// Create a temporary image for preprocessing
|
||||||
|
using var resized = image.Clone();
|
||||||
|
resized.Mutate(x => x.Resize(targetWidth, targetHeight));
|
||||||
|
|
||||||
|
// Create tensor of shape (1, 3, 320, 320)
|
||||||
|
// 1 for batch size, 3 for RGB channels, 320x320 for image dimensions
|
||||||
|
DenseTensor<float> tensor = new([1, 3, targetHeight, targetWidth]);
|
||||||
|
|
||||||
|
// Normalize pixel values and copy to tensor
|
||||||
|
WalkImage(resized.Height, resized.Width, (x, y) =>
|
||||||
|
{
|
||||||
|
var pixel = resized[x, y];
|
||||||
|
|
||||||
|
// u2net expects expect input images to be normalized using ImageNet mean and std
|
||||||
|
// to better match the distribution of the data the model was trained on
|
||||||
|
// Normalize to range [0, 1] and standardize using ImageNet mean/std
|
||||||
|
// The tensor is filled with normalized pixel values
|
||||||
|
tensor[0, 0, y, x] = ((pixel.R / pixelMax) - rMean) / rStd; // Red channel
|
||||||
|
tensor[0, 1, y, x] = ((pixel.G / pixelMax) - gMean) / gStd; // Green channel
|
||||||
|
tensor[0, 2, y, x] = ((pixel.B / pixelMax) - bMean) / bStd; // Blue channel
|
||||||
|
});
|
||||||
|
|
||||||
|
return tensor;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Image<Rgba32> GenerateMask(Tensor<float> maskTensor, int width, int height)
|
||||||
|
{
|
||||||
|
var mask = new Image<Rgba32>(width, height);
|
||||||
|
|
||||||
|
var sourceHeight = maskTensor.Dimensions[2]; // Height of the original tensor mask
|
||||||
|
var sourceWidth = maskTensor.Dimensions[3]; // Width of the original tensor mask
|
||||||
|
|
||||||
|
using Image<Rgba32> tempMask = new(sourceWidth, sourceHeight);
|
||||||
|
|
||||||
|
// Sigmoid function parameters
|
||||||
|
const float sigmoidScale = 1f; // Scaling factor for sigmoid activation
|
||||||
|
const float sigmoidShift = 1f; // Shift factor in the denominator of the sigmoid function
|
||||||
|
const float sigmoidDivisor = -1f; // Multiplier for the exponent in the sigmoid function
|
||||||
|
|
||||||
|
static float CalculateSigmoid(float x)
|
||||||
|
{
|
||||||
|
return sigmoidScale / (sigmoidShift + MathF.Exp(sigmoidDivisor * x));
|
||||||
|
}
|
||||||
|
|
||||||
|
const float binarizationThreshold = 0.5f; // Threshold to determine foreground vs. background
|
||||||
|
const float normalizationFactor = 2f; // Scales the thresholded value to enhance contrast
|
||||||
|
|
||||||
|
// Pixel intensity values
|
||||||
|
const byte maxIntensity = 255; // Maximum grayscale intensity
|
||||||
|
const byte opaqueAlpha = 255; // Fully opaque alpha value
|
||||||
|
|
||||||
|
|
||||||
|
WalkImage(sourceHeight, sourceWidth, (x, y) =>
|
||||||
|
{
|
||||||
|
// a sigmoid function is a function that produces an S-shaped curve
|
||||||
|
// it is often used in machine learning and statistics to model probabilities
|
||||||
|
// the sigmoid function is defined as:
|
||||||
|
// f(x) = 1 / (1 + e^(-x))
|
||||||
|
// where e is the base of the natural logarithm and x is the input value
|
||||||
|
|
||||||
|
// the raw tensor values for our mask are going to be real unbounded numbers
|
||||||
|
// i.e. -1.5, 0.5, 2.0, etc.
|
||||||
|
// the sigmoid function will map these values to a range between 0 and 1
|
||||||
|
// this allows us to say that value closer to 0 is background and value
|
||||||
|
// closer to 1 is foreground
|
||||||
|
var sigmoidValue = CalculateSigmoid(maskTensor[0, 0, y, x]);
|
||||||
|
|
||||||
|
// now we want to threshold the sigmoid value to determine if it is foreground or background
|
||||||
|
// we are arbitrarily choosing 0.5 as the threshold. so if the sigmoid value is greater than
|
||||||
|
// 0.5 we will consider it foreground and if it is less than 0.5 we will consider it background
|
||||||
|
|
||||||
|
// when a sigmoid value is greater than 0.5 we will subtract the threshold from it
|
||||||
|
// and multiply it by 2 this way the intensity value will be larger for values closer to 1
|
||||||
|
// and create more contrast in the mask
|
||||||
|
var normalizedValue = sigmoidValue > binarizationThreshold
|
||||||
|
? (sigmoidValue - binarizationThreshold) * normalizationFactor
|
||||||
|
: 0f;
|
||||||
|
|
||||||
|
// Convert to an 8-bit grayscale intensity
|
||||||
|
var intensity = (byte)(normalizedValue * maxIntensity);
|
||||||
|
|
||||||
|
// Store the pixel with full opacity
|
||||||
|
tempMask[x, y] = new Rgba32(intensity, intensity, intensity, opaqueAlpha);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resize the mask to match the target dimensions
|
||||||
|
tempMask.Mutate(x => x.Resize(width, height));
|
||||||
|
|
||||||
|
// Copy the resized mask to the final output image
|
||||||
|
WalkImage(height, width, (x, y) => mask[x, y] = tempMask[x, y]);
|
||||||
|
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Image<Rgba32> GetImageWithBackgroundRemoved(Image<Rgba32> image, Image<Rgba32> mask)
|
||||||
|
{
|
||||||
|
Image<Rgba32> result = new(image.Width, image.Height);
|
||||||
|
|
||||||
|
const byte alphaThreshold = 20;
|
||||||
|
Rgba32 transparentPixel = new(0, 0, 0, 0);
|
||||||
|
|
||||||
|
WalkImage(image.Height, image.Width, (x, y) =>
|
||||||
|
{
|
||||||
|
var sourcePixel = image[x, y];
|
||||||
|
var maskPixel = mask[x, y];
|
||||||
|
|
||||||
|
var alpha = maskPixel.R;
|
||||||
|
|
||||||
|
result[x, y] = alpha > alphaThreshold
|
||||||
|
? new Rgba32(sourcePixel.R, sourcePixel.G, sourcePixel.B, sourcePixel.A)
|
||||||
|
: transparentPixel;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WalkImage(int height, int width, Action<int, int> action)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
action(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
# The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2025 Stevan Freeborn
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
[](https://codecov.io/gh/StevanFreeborn/bgr)
|
|
||||||
[](https://github.com/StevanFreeborn/bgr/actions/workflows/pull_request.yml)
|
|
||||||
[](https://github.com/StevanFreeborn/bgr/actions/workflows/publish.yml)
|
|
||||||

|
|
||||||

|
|
||||||
[](https://github.com/semantic-release/semantic-release)
|
|
||||||

|
|
||||||
|
|
||||||
# BGR
|
|
||||||
|
|
||||||
This is a background removal app that uses machine learning models to remove the background from an image. The app is available for download [here](https://github.com/StevanFreeborn/bgr/releases). You'll just need to unzip the files, place them in a directory, and add it to your PATH. You can rename the executable to your preferred name. The app can also be built from source using the [.NET SDK](https://dotnet.microsoft.com/download). If you'd like to read more about the idea and initial development behind the app you can read the blog post I wrote about it [here](https://blog.stevanfreeborn.com/building-a-background-removal-app-with-machine-learning-and-dotnet).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
The app is meant to be run from the command line. For example:
|
|
||||||
|
|
||||||
```pwsh
|
|
||||||
bgr /path/to/image.jpg --output /path/to/output.png
|
|
||||||
```
|
|
||||||
|
|
||||||
You can find the full list of commands, arguments, and options using the `--help` option:
|
|
||||||
|
|
||||||
```pwsh
|
|
||||||
bgr --help
|
|
||||||
```
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
#### Input Image
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### Output Image
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Feathering Options
|
|
||||||
|
|
||||||
By default, BGR applies feathering to create smooth transitions between the foreground and background, reducing harsh edges and halos. You can control the feathering range using two options:
|
|
||||||
|
|
||||||
- `--feather-min` - Mask values below this threshold become fully transparent. Default: `70`
|
|
||||||
- `--feather-max` - Mask values above this threshold become fully opaque. Default: `117`
|
|
||||||
|
|
||||||
Values between `feather-min` and `feather-max` are linearly scaled to produce partial transparency, creating a smooth gradient at edges.
|
|
||||||
|
|
||||||
```pwsh
|
|
||||||
bgr /path/to/image.jpg --feather-min 60 --feather-max 130
|
|
||||||
```
|
|
||||||
|
|
||||||
A wider range (e.g., `50` to `150`) produces softer edges, while a narrower range (e.g., `90` to `100`) produces sharper edges.
|
|
||||||
|
|
||||||
## Issues
|
|
||||||
|
|
||||||
If you encounter any issues while using the app, please open an issue on the repository. If you have any suggestions or feature requests, feel free to open an issue as well.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
If you'd like to contribute to the project, feel free to fork the repository and submit a pull request. If you have any questions or suggestions, feel free to open an issue.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 915 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 382 KiB |
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"sdk": {
|
|
||||||
"version": "10.0.301",
|
|
||||||
"rollForward": "latestFeature"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<NoWarn>1591;</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="10.0.1">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="coverlet.msbuild" Version="10.0.1">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
|
||||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
|
||||||
<PackageReference Include="Spectre.Console.Testing" Version="0.57.2" />
|
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<CollectCoverage>true</CollectCoverage>
|
|
||||||
<CoverletOutput>./TestResults/Coverage/</CoverletOutput>
|
|
||||||
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
|
|
||||||
<Include>[BGR.Console]*</Include>
|
|
||||||
<ExcludeByFile>**/Program.cs</ExcludeByFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
|
|
||||||
<Exec Command="reportgenerator -reports:./TestResults/Coverage/*.xml -targetdir:./TestResults/Coverage/Report/ -reporttypes:Html_Dark" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="Xunit" />
|
|
||||||
<Using Include="Shouldly" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\BGR.Console\BGR.Console.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Integration;
|
|
||||||
|
|
||||||
internal static class AppFactory
|
|
||||||
{
|
|
||||||
public static CommandApp<RemovalCommand> Create()
|
|
||||||
{
|
|
||||||
return Host.CreateDefaultBuilder()
|
|
||||||
.ConfigureLogging(static logging => logging.ClearProviders())
|
|
||||||
.ConfigureServices(static (_, services) =>
|
|
||||||
{
|
|
||||||
services.AddSingleton<IAnsiConsole>(new TestConsole());
|
|
||||||
services.AddSingleton<IResourceManager, ResourceManager>();
|
|
||||||
services.AddSingleton<ImageProcessor, ImageSharpProcessor>();
|
|
||||||
services.AddSingleton<IInferenceRunner, OnnxInferenceRunner>();
|
|
||||||
services.AddSingleton<IModelFactory, ModelFactory>();
|
|
||||||
})
|
|
||||||
.BuildApp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Integration;
|
|
||||||
|
|
||||||
public class RemovalCommandTests
|
|
||||||
{
|
|
||||||
private readonly CommandApp<RemovalCommand> _app = AppFactory.Create();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RunAsync_WhenCalled_ItShouldRemoveImageBackground()
|
|
||||||
{
|
|
||||||
var imagePath = $"{Guid.NewGuid()}.png";
|
|
||||||
var outputPath = $"{Guid.NewGuid()}.png";
|
|
||||||
|
|
||||||
using var testImage = new Image<Rgba32>(100, 100);
|
|
||||||
await testImage.SaveAsPngAsync(imagePath);
|
|
||||||
|
|
||||||
var result = await _app.RunAsync([imagePath, "--model", "u2net", "--output", outputPath]);
|
|
||||||
|
|
||||||
result.ShouldBe(0);
|
|
||||||
File.Exists(outputPath).ShouldBeTrue();
|
|
||||||
|
|
||||||
File.Delete(imagePath);
|
|
||||||
File.Delete(outputPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class ImageSharpProcessorTests : IDisposable
|
|
||||||
{
|
|
||||||
private const string TestImagePath = "test.jpg";
|
|
||||||
private bool _isDisposed;
|
|
||||||
private readonly ImageSharpProcessor _sut = new();
|
|
||||||
private readonly Mock<Model> _modelMock = new();
|
|
||||||
private readonly Stream _testImageStream;
|
|
||||||
|
|
||||||
public ImageSharpProcessorTests()
|
|
||||||
{
|
|
||||||
if (File.Exists(TestImagePath) is false)
|
|
||||||
{
|
|
||||||
using var testImage = new Image<Rgba32>(100, 100);
|
|
||||||
testImage.SaveAsJpeg(TestImagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
_modelMock.Setup(static x => x.InputWidth).Returns(320);
|
|
||||||
_modelMock.Setup(static x => x.InputHeight).Returns(320);
|
|
||||||
|
|
||||||
var stream = new MemoryStream();
|
|
||||||
using var image = new Image<Rgba32>(100, 100);
|
|
||||||
|
|
||||||
for (var y = 0; y < image.Height; y++)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < image.Width; x++)
|
|
||||||
{
|
|
||||||
image[x, y] = new Rgba32((byte)x, (byte)y, 128, 255);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
image.SaveAsPng(stream);
|
|
||||||
stream.Position = 0;
|
|
||||||
_testImageStream = stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task LoadImageAsync_WhenCalledWithValidPath_ItShouldReturnImage()
|
|
||||||
{
|
|
||||||
var result = await _sut.LoadImageAsync(TestImagePath);
|
|
||||||
|
|
||||||
result.ShouldBeOfType<SharpImage>();
|
|
||||||
result.ShouldNotBeNull();
|
|
||||||
result.Width.ShouldBe(100);
|
|
||||||
result.Height.ShouldBe(100);
|
|
||||||
result.Data.Length.ShouldBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task LoadImageAsync_WhenCalledWithValidPath_ItShouldReturnReusableStream()
|
|
||||||
{
|
|
||||||
var result = await _sut.LoadImageAsync(TestImagePath);
|
|
||||||
|
|
||||||
result.Data.Position.ShouldBe(0);
|
|
||||||
result.Data.CanRead.ShouldBeTrue();
|
|
||||||
|
|
||||||
var buffer = new byte[100];
|
|
||||||
await result.Data.ReadExactlyAsync(buffer);
|
|
||||||
|
|
||||||
result.Data.Position = 0;
|
|
||||||
await result.Data.ReadExactlyAsync(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateTensorInputAsync_WhenCalled_ItShouldResizeImageToModelDimensions()
|
|
||||||
{
|
|
||||||
const int modelWidth = 64;
|
|
||||||
const int modelHeight = 48;
|
|
||||||
|
|
||||||
_modelMock.Setup(static x => x.InputWidth).Returns(modelWidth);
|
|
||||||
_modelMock.Setup(static x => x.InputHeight).Returns(modelHeight);
|
|
||||||
|
|
||||||
var result = await _sut.CreateTensorInputAsync(_testImageStream, _modelMock.Object);
|
|
||||||
|
|
||||||
result.Width.ShouldBe(modelWidth);
|
|
||||||
result.Height.ShouldBe(modelHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateTensorInputAsync_WhenCalled_ItShouldCreateTensorWithCorrectDimensions()
|
|
||||||
{
|
|
||||||
var result = await _sut.CreateTensorInputAsync(_testImageStream, _modelMock.Object);
|
|
||||||
|
|
||||||
result.ShouldBeOfType<OnnxTensor>();
|
|
||||||
Should.NotThrow(() => result.GetValue(0, 2, 0, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateTensorInputAsync_WhenCalled_ItShouldNormalizePixelValues()
|
|
||||||
{
|
|
||||||
var normalizedValue = 0.5f;
|
|
||||||
_modelMock.Setup(static x => x.NormalizeRed(It.IsAny<float>())).Returns(normalizedValue);
|
|
||||||
_modelMock.Setup(static x => x.NormalizeGreen(It.IsAny<float>())).Returns(normalizedValue);
|
|
||||||
_modelMock.Setup(static x => x.NormalizeBlue(It.IsAny<float>())).Returns(normalizedValue);
|
|
||||||
|
|
||||||
var result = await _sut.CreateTensorInputAsync(_testImageStream, _modelMock.Object);
|
|
||||||
|
|
||||||
for (var y = 0; y < result.Height; y++)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < result.Width; x++)
|
|
||||||
{
|
|
||||||
result.GetValue(0, 0, y, x).ShouldBe(normalizedValue); // Red
|
|
||||||
result.GetValue(0, 1, y, x).ShouldBe(normalizedValue); // Green
|
|
||||||
result.GetValue(0, 2, y, x).ShouldBe(normalizedValue); // Blue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateTensorInputAsync_WhenCalled_ItShouldCallNormalizeForEachChannel()
|
|
||||||
{
|
|
||||||
await _sut.CreateTensorInputAsync(_testImageStream, _modelMock.Object);
|
|
||||||
|
|
||||||
_modelMock.Verify(static x => x.NormalizeRed(It.IsAny<float>()), Times.AtLeast(1));
|
|
||||||
_modelMock.Verify(static x => x.NormalizeGreen(It.IsAny<float>()), Times.AtLeast(1));
|
|
||||||
_modelMock.Verify(static x => x.NormalizeBlue(It.IsAny<float>()), Times.AtLeast(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GenerateMaskAsync_WhenCalled_ItShouldCreateMaskWithCorrectDimensions()
|
|
||||||
{
|
|
||||||
const int width = 64;
|
|
||||||
const int height = 48;
|
|
||||||
|
|
||||||
var tensor = new OnnxTensor(1, 1, height, width);
|
|
||||||
var stream = await _sut.GenerateMaskAsync(tensor, width, height);
|
|
||||||
|
|
||||||
using var mask = await Image.LoadAsync<Rgba32>(stream);
|
|
||||||
|
|
||||||
mask.Width.ShouldBe(width);
|
|
||||||
mask.Height.ShouldBe(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GenerateMaskAsync_WhenCalled_ItShouldCreateGreyscaleMask()
|
|
||||||
{
|
|
||||||
var tensor = new OnnxTensor(1, 1, 100, 100);
|
|
||||||
var stream = await _sut.GenerateMaskAsync(tensor, 100, 100);
|
|
||||||
|
|
||||||
using var mask = await Image.LoadAsync<Rgba32>(stream);
|
|
||||||
|
|
||||||
for (var y = 0; y < mask.Height; y++)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < mask.Width; x++)
|
|
||||||
{
|
|
||||||
// we expect the mask to be greyscale
|
|
||||||
// so R, G, B should be equal
|
|
||||||
mask[x, y].R.ShouldBe(mask[x, y].G);
|
|
||||||
mask[x, y].G.ShouldBe(mask[x, y].B);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RemoveBackgroundAsync_WithValidImageAndMask_ShouldReturnProcessedStream()
|
|
||||||
{
|
|
||||||
var width = 2;
|
|
||||||
var height = 2;
|
|
||||||
|
|
||||||
using var imageStream = new MemoryStream();
|
|
||||||
using var image = new Image<Rgba32>(width, height);
|
|
||||||
|
|
||||||
for (var x = 0; x < width; x++)
|
|
||||||
{
|
|
||||||
for (var y = 0; y < height; y++)
|
|
||||||
{
|
|
||||||
image[x, y] = new Rgba32(255, 0, 0, 255); // Red pixels
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await image.SaveAsPngAsync(imageStream);
|
|
||||||
imageStream.Position = 0;
|
|
||||||
|
|
||||||
using var maskStream = new MemoryStream();
|
|
||||||
using var mask = new Image<Rgba32>(width, height);
|
|
||||||
|
|
||||||
mask[0, 0] = new Rgba32(0, 0, 0, 255);
|
|
||||||
mask[0, 1] = new Rgba32(255, 255, 255, 255);
|
|
||||||
mask[1, 0] = new Rgba32(255, 255, 255, 255);
|
|
||||||
mask[1, 1] = new Rgba32(255, 255, 255, 255);
|
|
||||||
|
|
||||||
await mask.SaveAsPngAsync(maskStream);
|
|
||||||
maskStream.Position = 0;
|
|
||||||
|
|
||||||
const byte featherMin = 70;
|
|
||||||
const byte featherMax = 117;
|
|
||||||
|
|
||||||
var result = await _sut.RemoveBackgroundAsync(imageStream, maskStream, featherMin, featherMax);
|
|
||||||
|
|
||||||
result.ShouldNotBeNull();
|
|
||||||
result.Length.ShouldBeGreaterThan(0);
|
|
||||||
|
|
||||||
result.Position = 0;
|
|
||||||
using var resultImage = await Image.LoadAsync<Rgba32>(result);
|
|
||||||
resultImage.Width.ShouldBe(width);
|
|
||||||
resultImage.Height.ShouldBe(height);
|
|
||||||
|
|
||||||
resultImage[0, 0].A.ShouldBe((byte)0);
|
|
||||||
|
|
||||||
resultImage[0, 1].R.ShouldBe((byte)255);
|
|
||||||
resultImage[0, 1].A.ShouldBe((byte)255);
|
|
||||||
resultImage[1, 0].R.ShouldBe((byte)255);
|
|
||||||
resultImage[1, 0].A.ShouldBe((byte)255);
|
|
||||||
resultImage[1, 1].R.ShouldBe((byte)255);
|
|
||||||
resultImage[1, 1].A.ShouldBe((byte)255);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RemoveBackgroundAsync_WithMidRangeMaskValue_ShouldApplyPartialAlpha()
|
|
||||||
{
|
|
||||||
using var imageStream = new MemoryStream();
|
|
||||||
using var image = new Image<Rgba32>(1, 1);
|
|
||||||
image[0, 0] = new Rgba32(100, 150, 200, 255);
|
|
||||||
await image.SaveAsPngAsync(imageStream);
|
|
||||||
imageStream.Position = 0;
|
|
||||||
|
|
||||||
using var maskStream = new MemoryStream();
|
|
||||||
using var mask = new Image<Rgba32>(1, 1);
|
|
||||||
mask[0, 0] = new Rgba32(100, 100, 100, 255);
|
|
||||||
await mask.SaveAsPngAsync(maskStream);
|
|
||||||
maskStream.Position = 0;
|
|
||||||
|
|
||||||
const byte featherMin = 70;
|
|
||||||
const byte featherMax = 117;
|
|
||||||
|
|
||||||
var result = await _sut.RemoveBackgroundAsync(imageStream, maskStream, featherMin, featherMax);
|
|
||||||
|
|
||||||
result.Position = 0;
|
|
||||||
using var resultImage = await Image.LoadAsync<Rgba32>(result);
|
|
||||||
|
|
||||||
resultImage[0, 0].R.ShouldBe((byte)100);
|
|
||||||
resultImage[0, 0].G.ShouldBe((byte)150);
|
|
||||||
resultImage[0, 0].B.ShouldBe((byte)200);
|
|
||||||
|
|
||||||
var expectedAlpha = (byte)((100 - 70) / (float)(117 - 70) * 255f);
|
|
||||||
resultImage[0, 0].A.ShouldBe(expectedAlpha);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SaveImageAsync_WhenCalled_ItShouldSaveImageToDiskAtProvidedPath()
|
|
||||||
{
|
|
||||||
using var image = new Image<Rgba32>(100, 100);
|
|
||||||
var stream = new MemoryStream();
|
|
||||||
await image.SaveAsPngAsync(stream);
|
|
||||||
|
|
||||||
var path = $"{Guid.NewGuid()}.png";
|
|
||||||
await _sut.SaveImageAsync(stream, path);
|
|
||||||
|
|
||||||
File.Exists(path).ShouldBeTrue();
|
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_isDisposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
File.Delete(TestImagePath);
|
|
||||||
_testImageStream.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_isDisposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class ModNetModelTests
|
|
||||||
{
|
|
||||||
private readonly byte[] _sampleModelBytes = [0x01, 0x02, 0x03];
|
|
||||||
private readonly ModNetModel _sut;
|
|
||||||
|
|
||||||
public ModNetModelTests()
|
|
||||||
{
|
|
||||||
_sut = new ModNetModel(_sampleModelBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Id_WhenCalled_ItShouldReturnModNetId()
|
|
||||||
{
|
|
||||||
ModNetModel.Id.ShouldBe("modnet");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InputWidth_WhenCalled_ItShouldReturn512()
|
|
||||||
{
|
|
||||||
_sut.InputWidth.ShouldBe(512);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InputHeight_WhenCalled_ItShouldReturn512()
|
|
||||||
{
|
|
||||||
_sut.InputHeight.ShouldBe(512);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RedNormalizationMean_WhenCalled_ItShouldReturnCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.RedNormalizationMean.ShouldBe(0.485f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GreenNormalizationMean_WhenCalled_ItShouldReturnCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.GreenNormalizationMean.ShouldBe(0.456f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BlueNormalizationMean_WhenCalled_ItShouldReturnCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.BlueNormalizationMean.ShouldBe(0.406f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RedNormalizationStd_WhenCalled_ItShouldReturnCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.RedNormalizationStd.ShouldBe(0.229f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GreenNormalizationStd_WhenCalled_ItShouldReturnCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.GreenNormalizationStd.ShouldBe(0.224f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BlueNormalizationStd_WhenCalled_ItShouldReturnCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.BlueNormalizationStd.ShouldBe(0.225f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class ModelFactoryTests
|
|
||||||
{
|
|
||||||
private readonly Mock<IResourceManager> _resourceManagerMock;
|
|
||||||
private readonly ModelFactory _sut;
|
|
||||||
private readonly byte[] _sampleModelBytes = [0x01, 0x02, 0x03];
|
|
||||||
|
|
||||||
public ModelFactoryTests()
|
|
||||||
{
|
|
||||||
_resourceManagerMock = new Mock<IResourceManager>();
|
|
||||||
_sut = new ModelFactory(_resourceManagerMock.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("u2net.onnx", typeof(U2NetModel))]
|
|
||||||
[InlineData("rmbg.onnx", typeof(RmbgModel))]
|
|
||||||
[InlineData("modnet.onnx", typeof(ModNetModel))]
|
|
||||||
public void Create_WhenCalledWithValidModelName_ItShouldReturnCorrectModelType(string resourceName, Type expectedType)
|
|
||||||
{
|
|
||||||
SetupResourceManagerMock(resourceName);
|
|
||||||
|
|
||||||
var result = _sut.Create(resourceName);
|
|
||||||
|
|
||||||
result.ShouldBeOfType(expectedType);
|
|
||||||
VerifyResourceManagerCalled(resourceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Create_WhenCalledWithUnknownModel_ItShouldThrowArgumentException()
|
|
||||||
{
|
|
||||||
var resourceName = "unknown.onnx";
|
|
||||||
SetupResourceManagerMock(resourceName);
|
|
||||||
|
|
||||||
var exception = Should.Throw<ArgumentException>(() => _sut.Create(resourceName));
|
|
||||||
|
|
||||||
exception.Message.ShouldBe($"Unknown model name: {resourceName}");
|
|
||||||
VerifyResourceManagerCalled(resourceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Create_WhenCalledWithU2NetModel_ItShouldReadTheResourceStreamToTheEnd()
|
|
||||||
{
|
|
||||||
var resourceName = $"{U2NetModel.Id}.onnx";
|
|
||||||
var memoryStream = new MemoryStream(_sampleModelBytes);
|
|
||||||
|
|
||||||
_resourceManagerMock.Setup(x => x.GetResource(resourceName))
|
|
||||||
.Returns(memoryStream);
|
|
||||||
|
|
||||||
_sut.Create(resourceName);
|
|
||||||
|
|
||||||
memoryStream.Position.ShouldBe(memoryStream.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupResourceManagerMock(string resourceName)
|
|
||||||
{
|
|
||||||
var memoryStream = new MemoryStream(_sampleModelBytes);
|
|
||||||
|
|
||||||
_resourceManagerMock.Setup(x => x.GetResource(resourceName))
|
|
||||||
.Returns(memoryStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void VerifyResourceManagerCalled(string resourceName)
|
|
||||||
{
|
|
||||||
_resourceManagerMock.Verify(x => x.GetResource(resourceName), Times.Once);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class ModelTests
|
|
||||||
{
|
|
||||||
internal sealed class TestModel(byte[] modelBytes) : Model(modelBytes)
|
|
||||||
{
|
|
||||||
public override int InputWidth => 100;
|
|
||||||
public override int InputHeight => 100;
|
|
||||||
public override float RedNormalizationMean => 0.5f;
|
|
||||||
public override float GreenNormalizationMean => 0.5f;
|
|
||||||
public override float BlueNormalizationMean => 0.5f;
|
|
||||||
public override float RedNormalizationStd => 0.25f;
|
|
||||||
public override float GreenNormalizationStd => 0.25f;
|
|
||||||
public override float BlueNormalizationStd => 0.25f;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly byte[] _sampleBytes = [0x01, 0x02, 0x03];
|
|
||||||
private readonly TestModel _sut;
|
|
||||||
private const float Delta = 0.00001f;
|
|
||||||
|
|
||||||
public ModelTests()
|
|
||||||
{
|
|
||||||
_sut = new TestModel(_sampleBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0f)]
|
|
||||||
[InlineData(127.5f)]
|
|
||||||
[InlineData(255f)]
|
|
||||||
public void NormalizeRed_WhenCalled_ItShouldNormalizeCorrectly(float value)
|
|
||||||
{
|
|
||||||
var result = _sut.NormalizeRed(value);
|
|
||||||
|
|
||||||
var expected = ((value / 255f) - _sut.RedNormalizationMean) / _sut.RedNormalizationStd;
|
|
||||||
result.ShouldBe(expected, Delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0f)]
|
|
||||||
[InlineData(127.5f)]
|
|
||||||
[InlineData(255f)]
|
|
||||||
public void NormalizeGreen_WhenCalled_ItShouldNormalizeCorrectly(float value)
|
|
||||||
{
|
|
||||||
var result = _sut.NormalizeGreen(value);
|
|
||||||
|
|
||||||
var expected = ((value / 255f) - _sut.GreenNormalizationMean) / _sut.GreenNormalizationStd;
|
|
||||||
result.ShouldBe(expected, Delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0f)]
|
|
||||||
[InlineData(127.5f)]
|
|
||||||
[InlineData(255f)]
|
|
||||||
public void NormalizeBlue_WhenCalled_ItShouldNormalizeCorrectly(float value)
|
|
||||||
{
|
|
||||||
var result = _sut.NormalizeBlue(value);
|
|
||||||
|
|
||||||
var expected = ((value / 255f) - _sut.BlueNormalizationMean) / _sut.BlueNormalizationStd;
|
|
||||||
result.ShouldBe(expected, Delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(-1f)]
|
|
||||||
[InlineData(256f)]
|
|
||||||
public void NormalizeRed_WhenCalledWithOutOfRangeValues_ItShouldStillNormalize(float value)
|
|
||||||
{
|
|
||||||
var result = _sut.NormalizeRed(value);
|
|
||||||
|
|
||||||
var expected = ((value / 255f) - _sut.RedNormalizationMean) / _sut.RedNormalizationStd;
|
|
||||||
result.ShouldBe(expected, Delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(-1f)]
|
|
||||||
[InlineData(256f)]
|
|
||||||
public void NormalizeGreen_WhenCalledWithOutOfRangeValues_ItShouldStillNormalize(float value)
|
|
||||||
{
|
|
||||||
var result = _sut.NormalizeGreen(value);
|
|
||||||
|
|
||||||
var expected = ((value / 255f) - _sut.GreenNormalizationMean) / _sut.GreenNormalizationStd;
|
|
||||||
result.ShouldBe(expected, Delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(-1f)]
|
|
||||||
[InlineData(256f)]
|
|
||||||
public void NormalizeBlue_WhenCalledWithOutOfRangeValues_ItShouldStillNormalize(float value)
|
|
||||||
{
|
|
||||||
var result = _sut.NormalizeBlue(value);
|
|
||||||
|
|
||||||
var expected = ((value / 255f) - _sut.BlueNormalizationMean) / _sut.BlueNormalizationStd;
|
|
||||||
result.ShouldBe(expected, Delta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
|
|
||||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
|
||||||
|
|
||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class OnnxInferenceRunnerTests
|
|
||||||
{
|
|
||||||
private const string TestModel = "u2net.onnx";
|
|
||||||
private readonly ResourceManager _resourceManager = new();
|
|
||||||
private readonly OnnxInferenceRunner _sut = new();
|
|
||||||
private readonly byte[] _sampleModelBytes;
|
|
||||||
private readonly Mock<ITensor<float>> _mockInputTensor = new();
|
|
||||||
|
|
||||||
public OnnxInferenceRunnerTests()
|
|
||||||
{
|
|
||||||
var modelStream = _resourceManager.GetResource(TestModel);
|
|
||||||
var modelBytes = new byte[modelStream.Length];
|
|
||||||
modelStream.ReadExactly(modelBytes);
|
|
||||||
_sampleModelBytes = modelBytes;
|
|
||||||
|
|
||||||
_mockInputTensor
|
|
||||||
.Setup(static x => x.ToTensor())
|
|
||||||
.Returns(new DenseTensor<float>([1, 3, 320, 320]));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Run_WhenCalledWithValidInput_ItShouldReturnOutput()
|
|
||||||
{
|
|
||||||
var result = _sut.Run(_sampleModelBytes, _mockInputTensor.Object);
|
|
||||||
|
|
||||||
result.ShouldBeOfType<OnnxTensor>();
|
|
||||||
result.ShouldNotBeNull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
|
||||||
|
|
||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class OnnxTensorTests
|
|
||||||
{
|
|
||||||
private const int DefaultBatchSize = 1;
|
|
||||||
private const int DefaultChannels = 3;
|
|
||||||
private const int DefaultHeight = 4;
|
|
||||||
private const int DefaultWidth = 5;
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Constructor_WhenCalledWithDimensions_ItShouldCreateTensorWithCorrectDimensions()
|
|
||||||
{
|
|
||||||
var tensor = new OnnxTensor(DefaultBatchSize, DefaultChannels, DefaultHeight, DefaultWidth);
|
|
||||||
|
|
||||||
tensor.Height.ShouldBe(DefaultHeight);
|
|
||||||
tensor.Width.ShouldBe(DefaultWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Constructor_WhenCalledWithExistingTensor_ItShouldCreateTensorWithSameDimensions()
|
|
||||||
{
|
|
||||||
var existingTensor = new DenseTensor<float>([DefaultBatchSize, DefaultChannels, DefaultHeight, DefaultWidth]);
|
|
||||||
|
|
||||||
var tensor = new OnnxTensor(existingTensor);
|
|
||||||
|
|
||||||
tensor.Height.ShouldBe(DefaultHeight);
|
|
||||||
tensor.Width.ShouldBe(DefaultWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0, 0, 0, 0, 1.0f)]
|
|
||||||
[InlineData(0, 1, 2, 3, 2.5f)]
|
|
||||||
[InlineData(0, 2, 3, 4, -1.0f)]
|
|
||||||
public void SetValue_WhenCalledWithValidValues_ItShouldSetCorrectValueAtPosition(int batch, int channel, int y, int x, float expectedValue)
|
|
||||||
{
|
|
||||||
var tensor = new OnnxTensor(DefaultBatchSize, DefaultChannels, DefaultHeight, DefaultWidth);
|
|
||||||
|
|
||||||
tensor.SetValue(batch, channel, y, x, expectedValue);
|
|
||||||
var actualValue = tensor.GetValue(batch, channel, y, x);
|
|
||||||
|
|
||||||
actualValue.ShouldBe(expectedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0, 0, 0, 0, 1.0f)]
|
|
||||||
[InlineData(0, 1, 2, 3, 2.5f)]
|
|
||||||
[InlineData(0, 2, 3, 4, -1.0f)]
|
|
||||||
public void GetValue_WhenCalledWithValidValues_ItShouldReturnCorrectValue(int batch, int channel, int y, int x, float value)
|
|
||||||
{
|
|
||||||
var tensor = new OnnxTensor(DefaultBatchSize, DefaultChannels, DefaultHeight, DefaultWidth);
|
|
||||||
tensor.SetValue(batch, channel, y, x, value);
|
|
||||||
|
|
||||||
var result = tensor.GetValue(batch, channel, y, x);
|
|
||||||
|
|
||||||
result.ShouldBe(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ToTensor_WhenCalled_ItShouldReturnUnderlyingTensor()
|
|
||||||
{
|
|
||||||
var tensor = new OnnxTensor(DefaultBatchSize, DefaultChannels, DefaultHeight, DefaultWidth);
|
|
||||||
const float testValue = 42.0f;
|
|
||||||
tensor.SetValue(0, 0, 0, 0, testValue);
|
|
||||||
|
|
||||||
var result = tensor.ToTensor();
|
|
||||||
|
|
||||||
result.ShouldBeOfType<DenseTensor<float>>();
|
|
||||||
result[0, 0, 0, 0].ShouldBe(testValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(-1, 0, 0, 0)]
|
|
||||||
[InlineData(1, 3, 0, 0)]
|
|
||||||
[InlineData(0, 0, 4, 0)]
|
|
||||||
[InlineData(0, 0, 0, 5)]
|
|
||||||
public void SetValue_WhenCalledWithInvalidIndices_ItShouldThrowIndexOutOfRangeException(int batch, int channel, int y, int x)
|
|
||||||
{
|
|
||||||
var tensor = new OnnxTensor(DefaultBatchSize, DefaultChannels, DefaultHeight, DefaultWidth);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
tensor.SetValue(batch, channel, y, x, 0);
|
|
||||||
}
|
|
||||||
catch (IndexOutOfRangeException ex)
|
|
||||||
{
|
|
||||||
ex.Message.ShouldBe("Index was outside the bounds of the array.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(-1, 0, 0, 0)]
|
|
||||||
[InlineData(1, 3, 0, 0)]
|
|
||||||
[InlineData(0, 0, 4, 0)]
|
|
||||||
[InlineData(0, 0, 0, 5)]
|
|
||||||
public void GetValue_WhenCalledWithInvalidIndices_ItShouldThrowIndexOutOfRangeException(int batch, int channel, int y, int x)
|
|
||||||
{
|
|
||||||
var tensor = new OnnxTensor(DefaultBatchSize, DefaultChannels, DefaultHeight, DefaultWidth);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
tensor.GetValue(batch, channel, y, x);
|
|
||||||
}
|
|
||||||
catch (IndexOutOfRangeException ex)
|
|
||||||
{
|
|
||||||
ex.Message.ShouldBe("Index was outside the bounds of the array.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Constructor_WhenCalledWithZeroDimension_ItShouldCreateEmptyTensor()
|
|
||||||
{
|
|
||||||
var tensor = new OnnxTensor(0, 0, 0, 0);
|
|
||||||
|
|
||||||
tensor.Height.ShouldBe(0);
|
|
||||||
tensor.Width.ShouldBe(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class RemovalCommandTests : IDisposable
|
|
||||||
{
|
|
||||||
private bool _isDisposed;
|
|
||||||
private readonly Mock<IModelFactory> _modelFactoryMock = new();
|
|
||||||
private readonly Mock<ImageProcessor> _imageProcessorMock = new();
|
|
||||||
private readonly Mock<IInferenceRunner> _inferenceRunnerMock = new();
|
|
||||||
private readonly TestConsole _console = new();
|
|
||||||
private readonly Mock<ILogger<RemovalCommand>> _loggerMock = new();
|
|
||||||
private readonly RemovalCommand _sut;
|
|
||||||
|
|
||||||
public RemovalCommandTests()
|
|
||||||
{
|
|
||||||
_sut = new RemovalCommand(
|
|
||||||
_modelFactoryMock.Object,
|
|
||||||
_imageProcessorMock.Object,
|
|
||||||
_inferenceRunnerMock.Object,
|
|
||||||
_console,
|
|
||||||
_loggerMock.Object
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("output.png", false)]
|
|
||||||
[InlineData("", true)]
|
|
||||||
[SuppressMessage("Reliability", "CA2025:Do not pass 'IDisposable' instances into unawaited tasks", Justification = "Matching invocations")]
|
|
||||||
public async Task ExecuteAsync_WhenCalled_ItShouldProcessImage(string outputPath, bool includeMask)
|
|
||||||
{
|
|
||||||
var imagePath = $"{Guid.NewGuid()}.png";
|
|
||||||
|
|
||||||
using var testImage = new Image<Rgba32>(100, 100);
|
|
||||||
await testImage.SaveAsPngAsync(imagePath);
|
|
||||||
|
|
||||||
var resourceName = "u2net.onnx";
|
|
||||||
|
|
||||||
var settings = new RemovalCommand.Settings()
|
|
||||||
{
|
|
||||||
Image = imagePath,
|
|
||||||
Model = "u2net",
|
|
||||||
IncludeMask = includeMask,
|
|
||||||
Output = outputPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
var model = new U2NetModel([1, 2, 3]);
|
|
||||||
var image = new SharpImage(100, 100, new MemoryStream());
|
|
||||||
var inputTensor = new OnnxTensor(1, 3, 320, 320);
|
|
||||||
var outputTensor = new OnnxTensor(1, 1, 320, 320);
|
|
||||||
var maskStream = new MemoryStream();
|
|
||||||
var outputStream = new MemoryStream();
|
|
||||||
|
|
||||||
_modelFactoryMock
|
|
||||||
.Setup(m => m.Create(resourceName))
|
|
||||||
.Returns(model);
|
|
||||||
|
|
||||||
_imageProcessorMock
|
|
||||||
.Setup(p => p.LoadImageAsync(imagePath))
|
|
||||||
.ReturnsAsync(image);
|
|
||||||
|
|
||||||
_imageProcessorMock
|
|
||||||
.Setup(p => p.CreateTensorInputAsync(image.Data, model))
|
|
||||||
.ReturnsAsync(inputTensor);
|
|
||||||
|
|
||||||
_inferenceRunnerMock
|
|
||||||
.Setup(r => r.Run(model.Bytes, inputTensor))
|
|
||||||
.Returns(outputTensor);
|
|
||||||
|
|
||||||
_imageProcessorMock
|
|
||||||
.Setup(p => p.GenerateMaskAsync(outputTensor, image.Width, image.Height))
|
|
||||||
.ReturnsAsync(maskStream);
|
|
||||||
|
|
||||||
_imageProcessorMock
|
|
||||||
.Setup(p => p.RemoveBackgroundAsync(image.Data, maskStream, It.IsAny<byte>(), It.IsAny<byte>()))
|
|
||||||
.ReturnsAsync(outputStream);
|
|
||||||
|
|
||||||
var commandContext = new CommandContext(
|
|
||||||
[],
|
|
||||||
new Mock<IRemainingArguments>().Object,
|
|
||||||
"test",
|
|
||||||
new object()
|
|
||||||
);
|
|
||||||
|
|
||||||
var result = await _sut.ExecuteAsync(commandContext, settings);
|
|
||||||
|
|
||||||
result.ShouldBe(0);
|
|
||||||
|
|
||||||
_modelFactoryMock.Verify(m => m.Create(resourceName), Times.Once);
|
|
||||||
_imageProcessorMock.Verify(p => p.LoadImageAsync(imagePath), Times.Once);
|
|
||||||
_imageProcessorMock.Verify(p => p.CreateTensorInputAsync(image.Data, model), Times.Once);
|
|
||||||
_inferenceRunnerMock.Verify(r => r.Run(model.Bytes, inputTensor), Times.Once);
|
|
||||||
_imageProcessorMock.Verify(p => p.GenerateMaskAsync(outputTensor, image.Width, image.Height), Times.Once);
|
|
||||||
_imageProcessorMock.Verify(p => p.RemoveBackgroundAsync(image.Data, maskStream, It.IsAny<byte>(), It.IsAny<byte>()), Times.Once);
|
|
||||||
_imageProcessorMock.Verify(p => p.SaveImageAsync(outputStream, It.IsAny<string>()), Times.AtLeastOnce);
|
|
||||||
|
|
||||||
File.Delete(imagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Validate_WhenCalledAndFileDoesNotExist_ItShouldReturnError()
|
|
||||||
{
|
|
||||||
var settings = new RemovalCommand.Settings()
|
|
||||||
{
|
|
||||||
Image = "invalid.png",
|
|
||||||
Model = "u2net",
|
|
||||||
IncludeMask = false,
|
|
||||||
Output = "output.png",
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = settings.Validate();
|
|
||||||
|
|
||||||
result.Successful.ShouldBeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Validate_WhenCalledAndModelIsInvalid_ItShouldReturnError()
|
|
||||||
{
|
|
||||||
var imagePath = $"{Guid.NewGuid()}.png";
|
|
||||||
|
|
||||||
using var testImage = new Image<Rgba32>(100, 100);
|
|
||||||
testImage.SaveAsPng(imagePath);
|
|
||||||
|
|
||||||
var settings = new RemovalCommand.Settings()
|
|
||||||
{
|
|
||||||
Image = imagePath,
|
|
||||||
Model = "invalid",
|
|
||||||
IncludeMask = false,
|
|
||||||
Output = "output.png",
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = settings.Validate();
|
|
||||||
|
|
||||||
result.Successful.ShouldBeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Validate_WhenCalledAndSettingsValid_ItShouldReturnSuccess()
|
|
||||||
{
|
|
||||||
var imagePath = $"{Guid.NewGuid()}.png";
|
|
||||||
|
|
||||||
using var testImage = new Image<Rgba32>(100, 100);
|
|
||||||
testImage.SaveAsPng(imagePath);
|
|
||||||
|
|
||||||
var settings = new RemovalCommand.Settings()
|
|
||||||
{
|
|
||||||
Image = imagePath,
|
|
||||||
Model = "u2net",
|
|
||||||
IncludeMask = false,
|
|
||||||
Output = "output.png",
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = settings.Validate();
|
|
||||||
|
|
||||||
result.Successful.ShouldBeTrue();
|
|
||||||
|
|
||||||
File.Delete(imagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_isDisposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_console.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_isDisposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class ResourceManagerTests
|
|
||||||
{
|
|
||||||
private readonly ResourceManager _resourceManager = new();
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetResource_WhenResourceExists_ItShouldReturnStream()
|
|
||||||
{
|
|
||||||
var resourceName = "u2net.onnx";
|
|
||||||
|
|
||||||
using var stream = _resourceManager.GetResource(resourceName);
|
|
||||||
|
|
||||||
stream.ShouldNotBeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetResource_WhenResourceDoesNotExist_ItShouldThrowException()
|
|
||||||
{
|
|
||||||
var resourceName = "nonexistent.onnx";
|
|
||||||
|
|
||||||
var act = () => _resourceManager.GetResource(resourceName);
|
|
||||||
|
|
||||||
act.ShouldThrow<FileNotFoundException>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class RmbgModelTests
|
|
||||||
{
|
|
||||||
private readonly byte[] _sampleModelBytes = [0x01, 0x02, 0x03];
|
|
||||||
private readonly RmbgModel _sut;
|
|
||||||
|
|
||||||
public RmbgModelTests()
|
|
||||||
{
|
|
||||||
_sut = new RmbgModel(_sampleModelBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Id_WhenCalled_ItShouldReturnRmbgId()
|
|
||||||
{
|
|
||||||
RmbgModel.Id.ShouldBe("rmbg");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InputWidth_WhenCalled_ItShouldReturn1024()
|
|
||||||
{
|
|
||||||
_sut.InputWidth.ShouldBe(1024);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InputHeight_WhenCalled_ItShouldReturn1024()
|
|
||||||
{
|
|
||||||
_sut.InputHeight.ShouldBe(1024);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RedNormalizationMean_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.RedNormalizationMean.ShouldBe(0.485f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GreenNormalizationMean_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.GreenNormalizationMean.ShouldBe(0.456f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BlueNormalizationMean_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.BlueNormalizationMean.ShouldBe(0.406f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RedNormalizationStd_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.RedNormalizationStd.ShouldBe(0.229f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GreenNormalizationStd_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.GreenNormalizationStd.ShouldBe(0.224f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BlueNormalizationStd_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.BlueNormalizationStd.ShouldBe(0.225f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class SharpImageTests : IDisposable
|
|
||||||
{
|
|
||||||
private bool _isDisposed;
|
|
||||||
private const int DefaultWidth = 100;
|
|
||||||
private const int DefaultHeight = 200;
|
|
||||||
private readonly MemoryStream _sampleStream;
|
|
||||||
|
|
||||||
public SharpImageTests()
|
|
||||||
{
|
|
||||||
_sampleStream = new MemoryStream([0x01, 0x02, 0x03]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Constructor_WhenCalled_ItShouldSetProperties()
|
|
||||||
{
|
|
||||||
var image = new SharpImage(DefaultWidth, DefaultHeight, _sampleStream);
|
|
||||||
|
|
||||||
image.Width.ShouldBe(DefaultWidth);
|
|
||||||
image.Height.ShouldBe(DefaultHeight);
|
|
||||||
image.Data.ShouldBe(_sampleStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(1, 1)]
|
|
||||||
[InlineData(1920, 1080)]
|
|
||||||
[InlineData(int.MaxValue, int.MaxValue)]
|
|
||||||
public void Constructor_WhenCalledWithDifferentDimensions_ItShouldSetCorrectValues(int width, int height)
|
|
||||||
{
|
|
||||||
var image = new SharpImage(width, height, _sampleStream);
|
|
||||||
|
|
||||||
image.Width.ShouldBe(width);
|
|
||||||
image.Height.ShouldBe(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Constructor_WhenCalledWithNullStream_ItShouldThrowException()
|
|
||||||
{
|
|
||||||
var action = static () => new SharpImage(DefaultWidth, DefaultHeight, null!);
|
|
||||||
|
|
||||||
action.ShouldThrow<ArgumentNullException>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0, 0)]
|
|
||||||
[InlineData(-1, -1)]
|
|
||||||
[InlineData(1, -1)]
|
|
||||||
[InlineData(-1, 1)]
|
|
||||||
public void Constructor_WhenCalledWithInvalidDimensions_ItShouldThrowException(int width, int height)
|
|
||||||
{
|
|
||||||
var action = () => new SharpImage(width, height, _sampleStream);
|
|
||||||
|
|
||||||
action.ShouldThrow<ArgumentOutOfRangeException>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Data_WhenCalled_ItShouldReturnSameStreamInstance()
|
|
||||||
{
|
|
||||||
var stream = new MemoryStream();
|
|
||||||
var image = new SharpImage(DefaultWidth, DefaultHeight, stream);
|
|
||||||
|
|
||||||
image.Data.ShouldBeSameAs(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_isDisposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_sampleStream.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_isDisposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class TypeRegistrarTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Constructor_WhenCalled_ItShouldNotThrowShould()
|
|
||||||
{
|
|
||||||
var mockBuilder = new Mock<IHostBuilder>();
|
|
||||||
|
|
||||||
Should.NotThrow(() => new TypeRegistrar(mockBuilder.Object));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Build_WhenCalled_ItShouldReturnResolverAndBuildHost()
|
|
||||||
{
|
|
||||||
var mockHost = new Mock<IHost>();
|
|
||||||
var mockBuilder = new Mock<IHostBuilder>();
|
|
||||||
|
|
||||||
mockBuilder
|
|
||||||
.Setup(static b => b.Build())
|
|
||||||
.Returns(mockHost.Object);
|
|
||||||
|
|
||||||
var registrar = new TypeRegistrar(mockBuilder.Object);
|
|
||||||
|
|
||||||
var resolver = registrar.Build();
|
|
||||||
|
|
||||||
resolver.ShouldBeOfType<TypeResolver>();
|
|
||||||
mockBuilder.Verify(static b => b.Build(), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Register_WhenCalledWithType_ItShouldAddToContainer()
|
|
||||||
{
|
|
||||||
var builder = Host.CreateDefaultBuilder();
|
|
||||||
var registrar = new TypeRegistrar(builder);
|
|
||||||
registrar.Register(typeof(IService), typeof(ServiceImplementation));
|
|
||||||
|
|
||||||
using var host = builder.Build();
|
|
||||||
var service = host.Services.GetService<IService>();
|
|
||||||
|
|
||||||
service.ShouldNotBeNull();
|
|
||||||
service.ShouldBeOfType<ServiceImplementation>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RegisterInstance_WhenCalledWithInstance_ItShouldAddToContainer()
|
|
||||||
{
|
|
||||||
var builder = Host.CreateDefaultBuilder();
|
|
||||||
var registrar = new TypeRegistrar(builder);
|
|
||||||
var instance = new ServiceImplementation();
|
|
||||||
registrar.RegisterInstance(typeof(IService), instance);
|
|
||||||
|
|
||||||
using var host = builder.Build();
|
|
||||||
var service = host.Services.GetService<IService>();
|
|
||||||
|
|
||||||
service.ShouldBeSameAs(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RegisterLazy_WhenCalledWithFunc_ItShouldAddToContainer()
|
|
||||||
{
|
|
||||||
var builder = Host.CreateDefaultBuilder();
|
|
||||||
var registrar = new TypeRegistrar(builder);
|
|
||||||
registrar.RegisterLazy(typeof(IService), static () => new ServiceImplementation());
|
|
||||||
|
|
||||||
using var host = builder.Build();
|
|
||||||
var service = host.Services.GetService<IService>();
|
|
||||||
|
|
||||||
service.ShouldNotBeNull();
|
|
||||||
service.ShouldBeOfType<ServiceImplementation>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RegisterLazy_WhenFuncIsNull_ItShouldThrow()
|
|
||||||
{
|
|
||||||
var builder = Host.CreateDefaultBuilder();
|
|
||||||
var registrar = new TypeRegistrar(builder);
|
|
||||||
|
|
||||||
Should.Throw<ArgumentNullException>(() => registrar.RegisterLazy(typeof(IService), null!));
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface IService { }
|
|
||||||
private sealed class ServiceImplementation : IService { }
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class TypeResolverTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Constructor_WhenCalledWithNullHost_ItShouldThrowArgumentNullException()
|
|
||||||
{
|
|
||||||
Should.Throw<ArgumentNullException>(static () => new TypeResolver(null!));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Resolve_WhenTypeIsNull_ItShouldReturnNull()
|
|
||||||
{
|
|
||||||
var mockHost = new Mock<IHost>();
|
|
||||||
using var resolver = new TypeResolver(mockHost.Object);
|
|
||||||
|
|
||||||
var result = resolver.Resolve(null);
|
|
||||||
|
|
||||||
result.ShouldBeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Resolve_WhenCalledWithRegisteredType_ItShouldReturnAnInstance()
|
|
||||||
{
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
services.AddSingleton(new TestService());
|
|
||||||
|
|
||||||
var mockHost = new Mock<IHost>();
|
|
||||||
|
|
||||||
mockHost
|
|
||||||
.Setup(static h => h.Services)
|
|
||||||
.Returns(services.BuildServiceProvider());
|
|
||||||
|
|
||||||
using var resolver = new TypeResolver(mockHost.Object);
|
|
||||||
|
|
||||||
var result = resolver.Resolve(typeof(TestService));
|
|
||||||
|
|
||||||
result.ShouldNotBeNull();
|
|
||||||
result.ShouldBeOfType<TestService>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Resolve_WhenCalledWithUnregisteredType_ItShouldReturnNull()
|
|
||||||
{
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
var mockHost = new Mock<IHost>();
|
|
||||||
|
|
||||||
mockHost
|
|
||||||
.Setup(static h => h.Services)
|
|
||||||
.Returns(services.BuildServiceProvider());
|
|
||||||
|
|
||||||
using var resolver = new TypeResolver(mockHost.Object);
|
|
||||||
|
|
||||||
var result = resolver.Resolve(typeof(TestService));
|
|
||||||
|
|
||||||
result.ShouldBeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Dispose_WhenCalled_ItShouldAlsoDisposeHost()
|
|
||||||
{
|
|
||||||
var mockHost = new Mock<IHost>();
|
|
||||||
var resolver = new TypeResolver(mockHost.Object);
|
|
||||||
|
|
||||||
resolver.Dispose();
|
|
||||||
|
|
||||||
mockHost.Verify(static h => h.Dispose(), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TestService { }
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
namespace BGR.Console.Tests.Unit;
|
|
||||||
|
|
||||||
public class U2NetModelTests
|
|
||||||
{
|
|
||||||
private readonly byte[] _sampleModelBytes = [0x01, 0x02, 0x03];
|
|
||||||
private readonly U2NetModel _sut;
|
|
||||||
|
|
||||||
public U2NetModelTests()
|
|
||||||
{
|
|
||||||
_sut = new U2NetModel(_sampleModelBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Id_WhenCalled_ItShouldReturnU2NetId()
|
|
||||||
{
|
|
||||||
U2NetModel.Id.ShouldBe("u2net");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InputWidth_WhenCalled_ItShouldReturn320()
|
|
||||||
{
|
|
||||||
_sut.InputWidth.ShouldBe(320);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InputHeight_WhenCalled_ItShouldReturn320()
|
|
||||||
{
|
|
||||||
_sut.InputHeight.ShouldBe(320);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RedNormalizationMean_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.RedNormalizationMean.ShouldBe(0.485f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GreenNormalizationMean_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.GreenNormalizationMean.ShouldBe(0.456f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BlueNormalizationMean_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.BlueNormalizationMean.ShouldBe(0.406f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RedNormalizationStd_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.RedNormalizationStd.ShouldBe(0.229f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GreenNormalizationStd_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.GreenNormalizationStd.ShouldBe(0.224f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BlueNormalizationStd_WhenCalled_ItShouldHaveCorrectValue()
|
|
||||||
{
|
|
||||||
_sut.BlueNormalizationStd.ShouldBe(0.225f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
global using BGR.Console.Common;
|
|
||||||
global using BGR.Console.Removal;
|
|
||||||
global using BGR.Console.Removal.ImageSharp;
|
|
||||||
global using BGR.Console.Removal.Models;
|
|
||||||
global using BGR.Console.Removal.Onnx;
|
|
||||||
global using BGR.Console.Resources;
|
|
||||||
|
|
||||||
global using Microsoft.Extensions.DependencyInjection;
|
|
||||||
global using Microsoft.Extensions.Hosting;
|
|
||||||
global using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
global using Moq;
|
|
||||||
|
|
||||||
global using SixLabors.ImageSharp;
|
|
||||||
global using SixLabors.ImageSharp.PixelFormats;
|
|
||||||
|
|
||||||
global using Spectre.Console;
|
|
||||||
global using Spectre.Console.Cli;
|
|
||||||
global using Spectre.Console.Testing;
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<AssemblyTitle>BGR.Console</AssemblyTitle>
|
|
||||||
<Product>BGR.Console</Product>
|
|
||||||
<Description>A command-line app for removing backgrounds from images.</Description>
|
|
||||||
<Version>0.1.0</Version>
|
|
||||||
<Authors>Stevan Freeborn</Authors>
|
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
|
||||||
<SelfContained>true</SelfContained>
|
|
||||||
<PublishReadyToRun>true</PublishReadyToRun>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<DownloadDirectory>Resources/Files</DownloadDirectory>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<Target Name="DownloadModels" BeforeTargets="PrepareForBuild">
|
|
||||||
<Message Text="Downloading models..." Importance="high" />
|
|
||||||
|
|
||||||
<MakeDir Directories="$(DownloadDirectory)" Condition="!Exists('$(DownloadDirectory)')" />
|
|
||||||
|
|
||||||
<Exec Condition="!Exists('$(DownloadDirectory)/rmbg.onnx')" Command="curl -L "https://share.stevanfreeborn.com/bgr/rmbg.onnx" -o "$(DownloadDirectory)/rmbg.onnx"" />
|
|
||||||
|
|
||||||
<Exec Condition="!Exists('$(DownloadDirectory)/u2net.onnx')" Command="curl -L "https://share.stevanfreeborn.com/bgr/u2net.onnx" -o "$(DownloadDirectory)/u2net.onnx"" />
|
|
||||||
|
|
||||||
<Exec Condition="!Exists('$(DownloadDirectory)/modnet.onnx')" Command="curl -L "https://share.stevanfreeborn.com/bgr/modnet.onnx" -o "$(DownloadDirectory)/modnet.onnx"" />
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="Resources\Files\**\*" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
|
|
||||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.10" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.10" />
|
|
||||||
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.27.1" />
|
|
||||||
<PackageReference Include="Serilog" Version="4.4.0" />
|
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="10.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
|
||||||
<PackageReference Include="Spectre.Console" Version="0.57.2" />
|
|
||||||
<PackageReference Include="Spectre.Console.Cli" Version="0.55.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Change Log
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [versionize](https://github.com/versionize/versionize) for commit guidelines.
|
|
||||||
|
|
||||||
<a name="0.1.0"></a>
|
|
||||||
## [0.1.0](https://www.github.com/StevanFreeborn/bgr/releases/tag/v0.1.0) (2026-07-15)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* improve background removal feathering ([e099144](https://www.github.com/StevanFreeborn/bgr/commit/e099144127ae719f85f3375be8be72fe3b5914c6))
|
|
||||||
* upgrade to .NET 10 ([45dc9de](https://www.github.com/StevanFreeborn/bgr/commit/45dc9de2232e64b234b8016e64e7b281b613e6f9))
|
|
||||||
|
|
||||||
<a name="0.0.0"></a>
|
|
||||||
## [0.0.0](https://www.github.com/StevanFreeborn/bgr/releases/tag/v0.0.0) (2025-02-14)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* initial working POC ([651869c](https://www.github.com/StevanFreeborn/bgr/commit/651869c27ff9ed390d78bf7ae6b81b3aa1acefb3))
|
|
||||||
* refactor to proper app + write tests ([42435ad](https://www.github.com/StevanFreeborn/bgr/commit/42435ad9150db459ee97a399c53050a7ec58012c))
|
|
||||||
* working on adding abstractions ([baf4035](https://www.github.com/StevanFreeborn/bgr/commit/baf40356164be64735ed53fa42966d664a41a3a9))
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
namespace BGR.Console.Common;
|
|
||||||
|
|
||||||
internal static class HostBuilderExtensions
|
|
||||||
{
|
|
||||||
public static CommandApp<RemovalCommand> BuildApp(this IHostBuilder builder)
|
|
||||||
{
|
|
||||||
var registrar = new TypeRegistrar(builder);
|
|
||||||
var app = new CommandApp<RemovalCommand>(registrar);
|
|
||||||
|
|
||||||
app.Configure(static c =>
|
|
||||||
c.SetExceptionHandler(static (ex, resolver) =>
|
|
||||||
{
|
|
||||||
var logger = resolver?.Resolve(typeof(ILogger<RemovalCommand>)) as ILogger<RemovalCommand>;
|
|
||||||
logger?.RemovalCommandFailed(ex);
|
|
||||||
|
|
||||||
var console = resolver?.Resolve(typeof(IAnsiConsole)) as IAnsiConsole;
|
|
||||||
console?.WriteLine($"[red]An error occurred while executing the command:[/]");
|
|
||||||
console?.WriteException(ex, ExceptionFormats.ShortenEverything);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
namespace BGR.Console.Common;
|
|
||||||
|
|
||||||
internal class TypeRegistrar(IHostBuilder builder) : ITypeRegistrar
|
|
||||||
{
|
|
||||||
private readonly IHostBuilder _builder = builder;
|
|
||||||
|
|
||||||
public ITypeResolver Build()
|
|
||||||
{
|
|
||||||
return new TypeResolver(_builder.Build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Register(Type service, Type implementation)
|
|
||||||
{
|
|
||||||
_builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterInstance(Type service, object implementation)
|
|
||||||
{
|
|
||||||
_builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterLazy(Type service, Func<object> func)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(func);
|
|
||||||
|
|
||||||
_builder.ConfigureServices((_, services) => services.AddSingleton(service, _ => func()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
namespace BGR.Console.Common;
|
|
||||||
|
|
||||||
internal class TypeResolver(IHost provider) : ITypeResolver, IDisposable
|
|
||||||
{
|
|
||||||
private readonly IHost _host = provider ?? throw new ArgumentNullException(nameof(provider));
|
|
||||||
|
|
||||||
public object? Resolve(Type? type)
|
|
||||||
{
|
|
||||||
return type is not null ? _host.Services.GetService(type) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_host.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
||||||
|
|
||||||
namespace BGR.Console.Logging;
|
|
||||||
|
|
||||||
internal static class LoggerExtensions
|
|
||||||
{
|
|
||||||
private static readonly Action<ILogger, Exception> RemovalCommandFailedMsg = LoggerMessage.Define(
|
|
||||||
LogLevel.Error,
|
|
||||||
new EventId(0, nameof(RemovalCommandFailed)),
|
|
||||||
"An error occurred while executing the command."
|
|
||||||
);
|
|
||||||
|
|
||||||
private static readonly Action<ILogger, string, long, Exception> TimeAndLogActionMsg = LoggerMessage.Define<string, long>(
|
|
||||||
LogLevel.Information,
|
|
||||||
new EventId(0, nameof(TimeAndLogAction)),
|
|
||||||
"{Message} in {ElapsedMilliseconds}ms"
|
|
||||||
);
|
|
||||||
|
|
||||||
public static void RemovalCommandFailed(this ILogger logger, Exception ex)
|
|
||||||
{
|
|
||||||
RemovalCommandFailedMsg(logger, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task TimeAndLogActionAsync(this ILogger logger, string message, Func<Task> action)
|
|
||||||
{
|
|
||||||
var sw = new Stopwatch();
|
|
||||||
sw.Start();
|
|
||||||
await action();
|
|
||||||
sw.Stop();
|
|
||||||
TimeAndLogActionMsg(logger, message, sw.ElapsedMilliseconds, default!);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<T> TimeAndLogActionAsync<T>(this ILogger logger, string message, Func<Task<T>> action)
|
|
||||||
{
|
|
||||||
var sw = new Stopwatch();
|
|
||||||
sw.Start();
|
|
||||||
var result = await action();
|
|
||||||
sw.Stop();
|
|
||||||
TimeAndLogActionMsg(logger, message, sw.ElapsedMilliseconds, default!);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TimeAndLogAction(this ILogger logger, string message, Action action)
|
|
||||||
{
|
|
||||||
var sw = new Stopwatch();
|
|
||||||
sw.Start();
|
|
||||||
action();
|
|
||||||
sw.Stop();
|
|
||||||
TimeAndLogActionMsg(logger, message, sw.ElapsedMilliseconds, default!);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T TimeAndLogAction<T>(this ILogger logger, string message, Func<T> action)
|
|
||||||
{
|
|
||||||
var sw = new Stopwatch();
|
|
||||||
sw.Start();
|
|
||||||
var result = action();
|
|
||||||
sw.Stop();
|
|
||||||
TimeAndLogActionMsg(logger, message, sw.ElapsedMilliseconds, default!);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
Log.Logger = new LoggerConfiguration()
|
|
||||||
.WriteTo.File(
|
|
||||||
formatter: new CompactJsonFormatter(),
|
|
||||||
path: Path.Combine(AppContext.BaseDirectory, "logs", "log.jsonl"),
|
|
||||||
rollingInterval: RollingInterval.Day
|
|
||||||
)
|
|
||||||
.Enrich.FromLogContext()
|
|
||||||
.MinimumLevel.Verbose()
|
|
||||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Fatal)
|
|
||||||
.CreateLogger();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var appName = Assembly.GetExecutingAssembly().GetName().Name;
|
|
||||||
Log.Information("Starting {AppName}", appName);
|
|
||||||
|
|
||||||
await Host.CreateDefaultBuilder()
|
|
||||||
.ConfigureLogging(static logging => logging.ClearProviders())
|
|
||||||
.ConfigureServices(static (_, services) =>
|
|
||||||
{
|
|
||||||
services.AddSerilog();
|
|
||||||
services.AddSingleton(AnsiConsole.Console);
|
|
||||||
services.AddSingleton<IResourceManager, ResourceManager>();
|
|
||||||
services.AddSingleton<ImageProcessor, ImageSharpProcessor>();
|
|
||||||
services.AddSingleton<IInferenceRunner, OnnxInferenceRunner>();
|
|
||||||
services.AddSingleton<IModelFactory, ModelFactory>();
|
|
||||||
})
|
|
||||||
.BuildApp()
|
|
||||||
.RunAsync(args);
|
|
||||||
|
|
||||||
Log.Information("Stopping {AppName}", appName);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Fatal(ex, "Application terminated unexpectedly");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
await Log.CloseAndFlushAsync();
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace BGR.Console.Removal;
|
|
||||||
|
|
||||||
internal interface IImage
|
|
||||||
{
|
|
||||||
int Width { get; }
|
|
||||||
int Height { get; }
|
|
||||||
Stream Data { get; }
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace BGR.Console.Removal;
|
|
||||||
|
|
||||||
internal interface IInferenceRunner
|
|
||||||
{
|
|
||||||
ITensor<float> Run(byte[] model, ITensor<float> inputTensor);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace BGR.Console.Removal;
|
|
||||||
|
|
||||||
internal interface ITensor<T>
|
|
||||||
{
|
|
||||||
int Height { get; }
|
|
||||||
int Width { get; }
|
|
||||||
void SetValue(int batch, int channel, int y, int x, T value);
|
|
||||||
float GetValue(int batch, int channel, int y, int x);
|
|
||||||
Tensor<T> ToTensor();
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
namespace BGR.Console.Removal;
|
|
||||||
|
|
||||||
internal abstract class ImageProcessor
|
|
||||||
{
|
|
||||||
public abstract Task<IImage> LoadImageAsync(string path);
|
|
||||||
|
|
||||||
public abstract Task<ITensor<float>> CreateTensorInputAsync(Stream image, Model model);
|
|
||||||
|
|
||||||
public abstract Task<Stream> GenerateMaskAsync(ITensor<float> maskTensor, int width, int height);
|
|
||||||
|
|
||||||
public abstract Task<Stream> RemoveBackgroundAsync(Stream image, Stream mask, byte featherMin, byte featherMax);
|
|
||||||
|
|
||||||
public abstract Task SaveImageAsync(Stream image, string path);
|
|
||||||
|
|
||||||
protected static void WalkImage(int height, int width, Action<int, int> action)
|
|
||||||
{
|
|
||||||
for (var y = 0; y < height; y++)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < width; x++)
|
|
||||||
{
|
|
||||||
action(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
namespace BGR.Console.Removal.ImageSharp;
|
|
||||||
|
|
||||||
internal class ImageSharpProcessor : ImageProcessor
|
|
||||||
{
|
|
||||||
public override async Task<IImage> LoadImageAsync(string path)
|
|
||||||
{
|
|
||||||
var image = await Image.LoadAsync<Rgba32>(path);
|
|
||||||
|
|
||||||
if (image.Metadata.DecodedImageFormat is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Image format is not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var stream = new MemoryStream();
|
|
||||||
await image.SaveAsync(stream, image.Metadata.DecodedImageFormat);
|
|
||||||
stream.Position = 0;
|
|
||||||
|
|
||||||
return new SharpImage(image.Width, image.Height, stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<ITensor<float>> CreateTensorInputAsync(Stream image, Model model)
|
|
||||||
{
|
|
||||||
using var resized = await Image.LoadAsync<Rgba32>(image);
|
|
||||||
|
|
||||||
resized.Mutate(x => x.Resize(model.InputWidth, model.InputHeight));
|
|
||||||
|
|
||||||
const int batchSize = 1;
|
|
||||||
const int channels = 3;
|
|
||||||
var tensor = new OnnxTensor(batchSize, channels, model.InputHeight, model.InputWidth);
|
|
||||||
|
|
||||||
WalkImage(resized.Height, resized.Width, (x, y) =>
|
|
||||||
{
|
|
||||||
var pixel = resized[x, y];
|
|
||||||
tensor.SetValue(0, 0, y, x, model.NormalizeRed(pixel.R));
|
|
||||||
tensor.SetValue(0, 1, y, x, model.NormalizeGreen(pixel.G));
|
|
||||||
tensor.SetValue(0, 2, y, x, model.NormalizeBlue(pixel.B));
|
|
||||||
});
|
|
||||||
|
|
||||||
return tensor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<Stream> GenerateMaskAsync(ITensor<float> maskTensor, int width, int height)
|
|
||||||
{
|
|
||||||
using var mask = new Image<Rgba32>(width, height);
|
|
||||||
|
|
||||||
using Image<Rgba32> tempMask = new(maskTensor.Width, maskTensor.Height);
|
|
||||||
|
|
||||||
const byte opaqueAlpha = 255;
|
|
||||||
|
|
||||||
WalkImage(maskTensor.Height, maskTensor.Width, (x, y) =>
|
|
||||||
{
|
|
||||||
var sigmoidValue = CalculateSigmoid(maskTensor.GetValue(0, 0, y, x));
|
|
||||||
var normalizedValue = Normalize(sigmoidValue);
|
|
||||||
var intensity = ConvertToGreyscale(normalizedValue);
|
|
||||||
|
|
||||||
tempMask[x, y] = new Rgba32(intensity, intensity, intensity, opaqueAlpha);
|
|
||||||
});
|
|
||||||
|
|
||||||
tempMask.Mutate(x => x.Resize(width, height));
|
|
||||||
|
|
||||||
WalkImage(height, width, (x, y) => mask[x, y] = tempMask[x, y]);
|
|
||||||
|
|
||||||
var stream = new MemoryStream();
|
|
||||||
await mask.SaveAsync(stream, new PngEncoder());
|
|
||||||
stream.Position = 0;
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<Stream> RemoveBackgroundAsync(Stream image, Stream mask, byte featherMin, byte featherMax)
|
|
||||||
{
|
|
||||||
image.Position = 0;
|
|
||||||
mask.Position = 0;
|
|
||||||
|
|
||||||
var imageWithBg = await Image.LoadAsync<Rgba32>(image);
|
|
||||||
var maskImage = await Image.LoadAsync<Rgba32>(mask);
|
|
||||||
using var imageWithBgRemoved = new Image<Rgba32>(imageWithBg.Width, imageWithBg.Height);
|
|
||||||
|
|
||||||
WalkImage(imageWithBg.Height, imageWithBg.Width, (x, y) =>
|
|
||||||
{
|
|
||||||
var sourcePixel = imageWithBg[x, y];
|
|
||||||
var maskPixel = maskImage[x, y];
|
|
||||||
|
|
||||||
var alpha = AdjustAlpha(maskPixel.R, featherMin, featherMax);
|
|
||||||
|
|
||||||
imageWithBgRemoved[x, y] = new Rgba32(sourcePixel.R, sourcePixel.G, sourcePixel.B, alpha);
|
|
||||||
});
|
|
||||||
|
|
||||||
var result = new MemoryStream();
|
|
||||||
await imageWithBgRemoved.SaveAsync(result, new PngEncoder());
|
|
||||||
result.Position = 0;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task SaveImageAsync(Stream image, string path)
|
|
||||||
{
|
|
||||||
image.Position = 0;
|
|
||||||
var img = await Image.LoadAsync<Rgba32>(image);
|
|
||||||
await img.SaveAsync(path, new PngEncoder());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float Normalize(float value)
|
|
||||||
{
|
|
||||||
return value * value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte ConvertToGreyscale(float value)
|
|
||||||
{
|
|
||||||
const float maxIntensity = 255f;
|
|
||||||
return (byte)(value * maxIntensity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float CalculateSigmoid(float x)
|
|
||||||
{
|
|
||||||
const float sigmoidScale = 1f;
|
|
||||||
const float sigmoidShift = 1f;
|
|
||||||
const float sigmoidDivisor = -1f;
|
|
||||||
|
|
||||||
return sigmoidScale / (sigmoidShift + MathF.Exp(sigmoidDivisor * x));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte AdjustAlpha(byte maskValue, byte minVal, byte maxVal)
|
|
||||||
{
|
|
||||||
if (maskValue <= minVal)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maskValue >= maxVal)
|
|
||||||
{
|
|
||||||
return 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
var proportion = (maskValue - minVal) / (float)(maxVal - minVal);
|
|
||||||
|
|
||||||
return (byte)(proportion * 255f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
namespace BGR.Console.Removal.ImageSharp;
|
|
||||||
|
|
||||||
internal class SharpImage : IImage
|
|
||||||
{
|
|
||||||
public int Width { get; }
|
|
||||||
public int Height { get; }
|
|
||||||
public Stream Data { get; }
|
|
||||||
|
|
||||||
public SharpImage(int width, int height, Stream data)
|
|
||||||
{
|
|
||||||
if (width <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(width), "must be greater than 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (height <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(height), "must be greater than 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
Width = width;
|
|
||||||
Height = height;
|
|
||||||
Data = data ?? throw new ArgumentNullException(nameof(data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace BGR.Console.Removal.Models;
|
|
||||||
|
|
||||||
internal interface IModelFactory
|
|
||||||
{
|
|
||||||
Model Create(string resourceName);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace BGR.Console.Removal.Models;
|
|
||||||
|
|
||||||
internal class ModNetModel(byte[] modelBytes) : Model(modelBytes)
|
|
||||||
{
|
|
||||||
public const string Id = "modnet";
|
|
||||||
public override int InputWidth => 512;
|
|
||||||
public override int InputHeight => 512;
|
|
||||||
public override float RedNormalizationMean => 0.485f;
|
|
||||||
public override float GreenNormalizationMean => 0.456f;
|
|
||||||
public override float BlueNormalizationMean => 0.406f;
|
|
||||||
public override float RedNormalizationStd => 0.229f;
|
|
||||||
public override float GreenNormalizationStd => 0.224f;
|
|
||||||
public override float BlueNormalizationStd => 0.225f;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
namespace BGR.Console.Removal.Models;
|
|
||||||
|
|
||||||
internal abstract class Model
|
|
||||||
{
|
|
||||||
private const float PixelMax = 255f;
|
|
||||||
public abstract int InputWidth { get; }
|
|
||||||
public abstract int InputHeight { get; }
|
|
||||||
public abstract float RedNormalizationMean { get; }
|
|
||||||
public abstract float GreenNormalizationMean { get; }
|
|
||||||
public abstract float BlueNormalizationMean { get; }
|
|
||||||
public abstract float RedNormalizationStd { get; }
|
|
||||||
public abstract float GreenNormalizationStd { get; }
|
|
||||||
public abstract float BlueNormalizationStd { get; }
|
|
||||||
public byte[] Bytes { get; } = [];
|
|
||||||
|
|
||||||
internal Model()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Model(byte[] modelBytes)
|
|
||||||
{
|
|
||||||
Bytes = modelBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual float NormalizeRed(float value)
|
|
||||||
{
|
|
||||||
return Normalize(value, RedNormalizationMean, RedNormalizationStd);
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual float NormalizeGreen(float value)
|
|
||||||
{
|
|
||||||
return Normalize(value, GreenNormalizationMean, GreenNormalizationStd);
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual float NormalizeBlue(float value)
|
|
||||||
{
|
|
||||||
return Normalize(value, BlueNormalizationMean, BlueNormalizationStd);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float Normalize(float value, float mean, float std)
|
|
||||||
{
|
|
||||||
return ((value / PixelMax) - mean) / std;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
namespace BGR.Console.Removal.Models;
|
|
||||||
|
|
||||||
internal class ModelFactory(IResourceManager resourceManager) : IModelFactory
|
|
||||||
{
|
|
||||||
private readonly IResourceManager _resourceManager = resourceManager;
|
|
||||||
|
|
||||||
public Model Create(string resourceName)
|
|
||||||
{
|
|
||||||
var resource = _resourceManager.GetResource(resourceName);
|
|
||||||
var model = new byte[resource.Length];
|
|
||||||
resource.ReadExactly(model);
|
|
||||||
|
|
||||||
return resourceName switch
|
|
||||||
{
|
|
||||||
$"{U2NetModel.Id}.onnx" => new U2NetModel(model),
|
|
||||||
$"{RmbgModel.Id}.onnx" => new RmbgModel(model),
|
|
||||||
$"{ModNetModel.Id}.onnx" => new ModNetModel(model),
|
|
||||||
_ => throw new ArgumentException($"Unknown model name: {resourceName}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace BGR.Console.Removal.Models;
|
|
||||||
|
|
||||||
internal class RmbgModel(byte[] modelBytes) : Model(modelBytes)
|
|
||||||
{
|
|
||||||
public const string Id = "rmbg";
|
|
||||||
public override int InputWidth => 1024;
|
|
||||||
public override int InputHeight => 1024;
|
|
||||||
public override float RedNormalizationMean => 0.485f;
|
|
||||||
public override float GreenNormalizationMean => 0.456f;
|
|
||||||
public override float BlueNormalizationMean => 0.406f;
|
|
||||||
public override float RedNormalizationStd => 0.229f;
|
|
||||||
public override float GreenNormalizationStd => 0.224f;
|
|
||||||
public override float BlueNormalizationStd => 0.225f;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace BGR.Console.Removal.Models;
|
|
||||||
|
|
||||||
internal class U2NetModel(byte[] modelBytes) : Model(modelBytes)
|
|
||||||
{
|
|
||||||
public const string Id = "u2net";
|
|
||||||
public override int InputWidth => 320;
|
|
||||||
public override int InputHeight => 320;
|
|
||||||
public override float RedNormalizationMean => 0.485f;
|
|
||||||
public override float GreenNormalizationMean => 0.456f;
|
|
||||||
public override float BlueNormalizationMean => 0.406f;
|
|
||||||
public override float RedNormalizationStd => 0.229f;
|
|
||||||
public override float GreenNormalizationStd => 0.224f;
|
|
||||||
public override float BlueNormalizationStd => 0.225f;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
namespace BGR.Console.Removal.Onnx;
|
|
||||||
|
|
||||||
internal class OnnxInferenceRunner : IInferenceRunner
|
|
||||||
{
|
|
||||||
public ITensor<float> Run(byte[] model, ITensor<float> inputTensor)
|
|
||||||
{
|
|
||||||
using var options = new SessionOptions() { LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR };
|
|
||||||
using var session = new InferenceSession(model, options);
|
|
||||||
var inputs = new List<NamedOnnxValue>()
|
|
||||||
{
|
|
||||||
NamedOnnxValue.CreateFromTensor(session.InputNames[0], inputTensor.ToTensor()),
|
|
||||||
};
|
|
||||||
|
|
||||||
var results = session.Run(inputs);
|
|
||||||
var outputTensor = results[0].AsTensor<float>();
|
|
||||||
return new OnnxTensor(outputTensor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
namespace BGR.Console.Removal.Onnx;
|
|
||||||
|
|
||||||
internal class OnnxTensor : ITensor<float>
|
|
||||||
{
|
|
||||||
private readonly Tensor<float> _tensor;
|
|
||||||
|
|
||||||
public int Height => _tensor.Dimensions[2];
|
|
||||||
|
|
||||||
public int Width => _tensor.Dimensions[3];
|
|
||||||
|
|
||||||
public OnnxTensor(
|
|
||||||
int batchSize,
|
|
||||||
int channels,
|
|
||||||
int height,
|
|
||||||
int width
|
|
||||||
)
|
|
||||||
{
|
|
||||||
_tensor = new DenseTensor<float>([batchSize, channels, height, width]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnnxTensor(Tensor<float> tensor)
|
|
||||||
{
|
|
||||||
_tensor = tensor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetValue(
|
|
||||||
int batch,
|
|
||||||
int channel,
|
|
||||||
int y,
|
|
||||||
int x,
|
|
||||||
float value
|
|
||||||
)
|
|
||||||
{
|
|
||||||
_tensor[batch, channel, y, x] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float GetValue(
|
|
||||||
int batch,
|
|
||||||
int channel,
|
|
||||||
int y,
|
|
||||||
int x
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return _tensor[batch, channel, y, x];
|
|
||||||
}
|
|
||||||
|
|
||||||
public Tensor<float> ToTensor()
|
|
||||||
{
|
|
||||||
return _tensor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
namespace BGR.Console.Removal;
|
|
||||||
|
|
||||||
internal class RemovalCommand(
|
|
||||||
IModelFactory modelFactory,
|
|
||||||
ImageProcessor imageProcessor,
|
|
||||||
IInferenceRunner inferenceRunner,
|
|
||||||
IAnsiConsole console,
|
|
||||||
ILogger<RemovalCommand> logger
|
|
||||||
) : AsyncCommand<RemovalCommand.Settings>
|
|
||||||
{
|
|
||||||
private readonly IModelFactory _modelFactory = modelFactory;
|
|
||||||
private readonly ImageProcessor _imageProcessor = imageProcessor;
|
|
||||||
private readonly IInferenceRunner _inferenceRunner = inferenceRunner;
|
|
||||||
private readonly IAnsiConsole _console = console;
|
|
||||||
private readonly ILogger<RemovalCommand> _logger = logger;
|
|
||||||
|
|
||||||
public async Task<int> ExecuteAsync(
|
|
||||||
CommandContext context,
|
|
||||||
Settings settings
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return await ExecuteAsync(context, settings, CancellationToken.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task<int> ExecuteAsync(
|
|
||||||
CommandContext context,
|
|
||||||
Settings settings,
|
|
||||||
CancellationToken cancellationToken
|
|
||||||
)
|
|
||||||
{
|
|
||||||
await _console.Status()
|
|
||||||
.Spinner(Spinner.Known.Dots)
|
|
||||||
.SpinnerStyle(Style.Parse("green"))
|
|
||||||
.StartAsync("Removing background...", async ctx =>
|
|
||||||
{
|
|
||||||
ctx.Status("Loading model...");
|
|
||||||
var model = _logger.TimeAndLogAction(
|
|
||||||
"Loading model",
|
|
||||||
() => _modelFactory.Create(settings.ResourceName)
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.Status("Loading image...");
|
|
||||||
var image = await _logger.TimeAndLogActionAsync(
|
|
||||||
"Loading image",
|
|
||||||
async () => await _imageProcessor.LoadImageAsync(settings.Image)
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.Status("Creating tensor input...");
|
|
||||||
var inputTensor = await _logger.TimeAndLogActionAsync(
|
|
||||||
"Creating tensor input",
|
|
||||||
async () => await _imageProcessor.CreateTensorInputAsync(image.Data, model)
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.Status("Running inference...");
|
|
||||||
var outputTensor = _logger.TimeAndLogAction(
|
|
||||||
"Running inference",
|
|
||||||
() => _inferenceRunner.Run(model.Bytes, inputTensor)
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.Status("Generating mask...");
|
|
||||||
var mask = await _logger.TimeAndLogActionAsync(
|
|
||||||
"Generating mask",
|
|
||||||
async () => await _imageProcessor.GenerateMaskAsync(outputTensor, image.Width, image.Height)
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.Status("Removing background...");
|
|
||||||
var output = await _logger.TimeAndLogActionAsync(
|
|
||||||
"Removing background",
|
|
||||||
async () => await _imageProcessor.RemoveBackgroundAsync(image.Data, mask, settings.FeatherMin, settings.FeatherMax)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (settings.IncludeMask)
|
|
||||||
{
|
|
||||||
await _logger.TimeAndLogActionAsync(
|
|
||||||
"Saving mask",
|
|
||||||
async () => await _imageProcessor.SaveImageAsync(mask, settings.MaskPath)
|
|
||||||
);
|
|
||||||
|
|
||||||
_console.MarkupLine($"[bold]Mask saved to:[/] [blue]{settings.OutputPath}[/]");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _logger.TimeAndLogActionAsync(
|
|
||||||
"Saving output",
|
|
||||||
async () => await _imageProcessor.SaveImageAsync(output, settings.OutputPath)
|
|
||||||
);
|
|
||||||
|
|
||||||
_console.MarkupLine($"[bold]Output saved to:[/] [green]{settings.OutputPath}[/]");
|
|
||||||
});
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class Settings : CommandSettings
|
|
||||||
{
|
|
||||||
private static readonly Dictionary<string, string> Models = new()
|
|
||||||
{
|
|
||||||
{ RmbgModel.Id, "rmbg.onnx" },
|
|
||||||
{ ModNetModel.Id, "modnet.onnx" },
|
|
||||||
{ U2NetModel.Id, "u2net.onnx" },
|
|
||||||
};
|
|
||||||
|
|
||||||
[CommandArgument(0, "<image>")]
|
|
||||||
[Description("Path to the image file whose background you want to remove")]
|
|
||||||
public string Image { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[CommandOption("--model|-m")]
|
|
||||||
[Description("The model to use for background removal")]
|
|
||||||
public string Model { get; init; } = "rmbg";
|
|
||||||
|
|
||||||
[CommandOption("--include-mask|-i")]
|
|
||||||
[Description("Generate and output the mask used for background removal")]
|
|
||||||
public bool IncludeMask { get; init; } = false;
|
|
||||||
|
|
||||||
[CommandOption("--output|-o")]
|
|
||||||
[Description("Path to output image without background to. File extension will always be .png")]
|
|
||||||
public string Output { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[CommandOption("--feather-min")]
|
|
||||||
[Description("Minimum mask value below which pixels become fully transparent (default: 70)")]
|
|
||||||
public byte FeatherMin { get; init; } = 70;
|
|
||||||
|
|
||||||
[CommandOption("--feather-max")]
|
|
||||||
[Description("Maximum mask value above which pixels become fully opaque (default: 117)")]
|
|
||||||
public byte FeatherMax { get; init; } = 117;
|
|
||||||
|
|
||||||
public string ResourceName => Models[Model];
|
|
||||||
|
|
||||||
public string MaskPath => GetOutputPath("_mask");
|
|
||||||
|
|
||||||
public string OutputPath => GetOutputPath("_no_bg");
|
|
||||||
|
|
||||||
public override ValidationResult Validate()
|
|
||||||
{
|
|
||||||
if (File.Exists(Image) is false)
|
|
||||||
{
|
|
||||||
return ValidationResult.Error($"The image file '{Image}' does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Models.ContainsKey(Model) is false)
|
|
||||||
{
|
|
||||||
return ValidationResult.Error($"The model '{Model}' is not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValidationResult.Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetOutputPath(string modifier)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(Output))
|
|
||||||
{
|
|
||||||
return Path.ChangeExtension(Image, null) + modifier + ".png";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Path.ChangeExtension(Output, ".png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace BGR.Console.Resources;
|
|
||||||
|
|
||||||
internal interface IResourceManager
|
|
||||||
{
|
|
||||||
Stream GetResource(string resourceName);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace BGR.Console.Resources;
|
|
||||||
|
|
||||||
internal sealed class ResourceManager : IResourceManager
|
|
||||||
{
|
|
||||||
public Stream GetResource(string resourceName)
|
|
||||||
{
|
|
||||||
var name = $"{nameof(BGR)}.{nameof(Console)}.{nameof(Resources)}.Files.{resourceName}";
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
var names = assembly.GetManifestResourceNames();
|
|
||||||
var stream = assembly.GetManifestResourceStream(name);
|
|
||||||
return stream ?? throw new FileNotFoundException("Model not found in embedded resources.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
global using System.ComponentModel;
|
|
||||||
global using System.Reflection;
|
|
||||||
|
|
||||||
global using BGR.Console.Common;
|
|
||||||
global using BGR.Console.Logging;
|
|
||||||
global using BGR.Console.Removal;
|
|
||||||
global using BGR.Console.Removal.ImageSharp;
|
|
||||||
global using BGR.Console.Removal.Models;
|
|
||||||
global using BGR.Console.Removal.Onnx;
|
|
||||||
global using BGR.Console.Resources;
|
|
||||||
|
|
||||||
global using Microsoft.Extensions.DependencyInjection;
|
|
||||||
global using Microsoft.Extensions.Hosting;
|
|
||||||
global using Microsoft.Extensions.Logging;
|
|
||||||
global using Microsoft.ML.OnnxRuntime;
|
|
||||||
global using Microsoft.ML.OnnxRuntime.Tensors;
|
|
||||||
|
|
||||||
global using Serilog;
|
|
||||||
global using Serilog.Events;
|
|
||||||
global using Serilog.Formatting.Compact;
|
|
||||||
|
|
||||||
global using SixLabors.ImageSharp;
|
|
||||||
global using SixLabors.ImageSharp.Formats.Png;
|
|
||||||
global using SixLabors.ImageSharp.PixelFormats;
|
|
||||||
global using SixLabors.ImageSharp.Processing;
|
|
||||||
|
|
||||||
global using Spectre.Console;
|
|
||||||
global using Spectre.Console.Cli;
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.0.31903.59
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGR.Console", "BGR.Console\BGR.Console.csproj", "{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGR.Console.Tests", "BGR.Console.Tests\BGR.Console.Tests.csproj", "{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<AnalysisLevel>latest</AnalysisLevel>
|
|
||||||
<AnalysisMode>All</AnalysisMode>
|
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
|
|
||||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user