mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-16 19:25:38 +02:00
Added threshold filter
This commit is contained in:
4
.env
4
.env
@@ -1,3 +1,7 @@
|
||||
# Whether to support WASM filters
|
||||
|
||||
VITE_ENABLE_WASM_FILTERS=false
|
||||
|
||||
# ---------------------------------
|
||||
# Third party storage configuration
|
||||
# ---------------------------------
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,8 @@ node_modules
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.production
|
||||
.env.*.production
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
@@ -21,3 +23,4 @@ pnpm-debug.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
build.sh
|
||||
17
README.md
17
README.md
@@ -163,10 +163,12 @@ Once the container is started, you can access BitMappery at `http://localhost:51
|
||||
|
||||
## WebAssembly
|
||||
|
||||
BitMappery can also use WebAssembly to increase performance of image manipulation. The source
|
||||
code is C based and compiled to WASM using [Emscripten](https://github.com/emscripten-core/emscripten). Because this setup is a little more cumbersome, the repository contains precompiled binaries
|
||||
in the _./src/wasm/bin/_-folder meaning you can omit this setup if you don't intend to make changes
|
||||
to these sources.
|
||||
BitMappery can also use WebAssembly to _potentially_ increase performance of image manipulation.
|
||||
WebAssembly filtering is a user controllable feature in the preferences pane, as long as the `.env` file has set
|
||||
support for `VITE_ENABLE_WASM_FILTERS` to true.
|
||||
|
||||
The source code is C based and compiled to WASM using [Emscripten](https://github.com/emscripten-core/emscripten). Because this setup is a little more cumbersome, the repository contains precompiled binaries in the _./src/wasm/bin/_-folder meaning you can
|
||||
omit this setup if you don't intend to make changes to these sources.
|
||||
|
||||
If you do wish to make contributions on this end, to compile the source (_/src/wasm/_) C-code to WASM, you
|
||||
will first need to prepare your environment (note the last _source_ call does not permanently update your paths):
|
||||
@@ -187,7 +189,7 @@ npm run wasm
|
||||
|
||||
### Benchmarks
|
||||
|
||||
On a particular (low powered) configuration, running all filters on a particular source takes:
|
||||
On a particular (deliberately low powered) configuration, running all filters at their heaviest setting on a particular source takes:
|
||||
|
||||
* 7000+ ms in JavaScript
|
||||
* 558 ms in WebAssembly
|
||||
@@ -195,7 +197,8 @@ On a particular (low powered) configuration, running all filters on a particular
|
||||
* 603 ms in WebAssembly inside a Web Worker
|
||||
|
||||
Note that the WebAssembly Web Worker takes a performance hit from converting the ImageData buffer
|
||||
to float32 prior to allocating the buffer in the WASM instance's memory. This can benefit from
|
||||
to float32 prior to allocating the buffer in the WASM instance's memory. This could benefit from
|
||||
further tweaking to see if it gets closer to the JavaScript Web Worker performance.
|
||||
|
||||
WebAssembly filtering is a user controllable feature in the preferences pane.
|
||||
However, as in the current setup the JS solution alone is performant enough _and you would need to write the
|
||||
filter code twice_, the default for WASM is disabled.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2021-2023 - https://www.igorski.nl
|
||||
* Igor Zinken 2021-2024 - https://www.igorski.nl
|
||||
*
|
||||
* 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
|
||||
@@ -84,6 +84,15 @@
|
||||
:tooltip="'none'"
|
||||
/>
|
||||
</div>
|
||||
<div class="wrapper input">
|
||||
<label v-t="'threshold'"></label>
|
||||
<slider
|
||||
v-model="threshold"
|
||||
:min="-1"
|
||||
:max="255"
|
||||
:tooltip="'none'"
|
||||
/>
|
||||
</div>
|
||||
<div class="wrapper input">
|
||||
<label v-t="'desaturate'"></label>
|
||||
<toggle-button
|
||||
@@ -218,6 +227,14 @@ export default {
|
||||
this.internalValue.vibrance = value / 100;
|
||||
}
|
||||
},
|
||||
threshold: {
|
||||
get(): number {
|
||||
return this.internalValue.threshold;
|
||||
},
|
||||
set( value: number ): void {
|
||||
this.internalValue.threshold = value;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
internalValue: {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"contrast": "Contrast",
|
||||
"brightness": "Brightness",
|
||||
"vibrance": "Vibrance",
|
||||
"threshold": "Threshold",
|
||||
"desaturate": "Desaturate",
|
||||
"reset": "Reset",
|
||||
"cancel": "Cancel",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2021-2022 - https://www.igorski.nl
|
||||
* Igor Zinken 2021-2024 - https://www.igorski.nl
|
||||
*
|
||||
* 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
|
||||
@@ -83,16 +83,17 @@ export default {
|
||||
computed: {
|
||||
...mapGetters([
|
||||
"preferences",
|
||||
"supportWASM",
|
||||
]),
|
||||
hasWebAssembly() {
|
||||
return "WebAssembly" in window;
|
||||
return this.supportWASM;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
internalValue: {
|
||||
deep: true,
|
||||
handler( value ) {
|
||||
setWasmFilters( !!value.wasmFilters );
|
||||
setWasmFilters( this.supportWASM && !!value.wasmFilters );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -58,6 +58,7 @@ export type Filters = {
|
||||
brightness: number;
|
||||
contrast: number;
|
||||
vibrance: number;
|
||||
threshold: number;
|
||||
desaturate: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2021-2023 - https://www.igorski.nl
|
||||
* Igor Zinken 2021-2024 - https://www.igorski.nl
|
||||
*
|
||||
* 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
|
||||
@@ -36,6 +36,7 @@ const FiltersFactory = {
|
||||
brightness = .5,
|
||||
contrast = 0,
|
||||
vibrance = .5,
|
||||
threshold = -1, // -1 == off, working range is 0 - 255
|
||||
desaturate = false
|
||||
}: FiltersProps = {}): Filters {
|
||||
return {
|
||||
@@ -47,6 +48,7 @@ const FiltersFactory = {
|
||||
contrast,
|
||||
desaturate,
|
||||
vibrance,
|
||||
threshold,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -64,6 +66,7 @@ const FiltersFactory = {
|
||||
c: filters.contrast,
|
||||
d: filters.desaturate,
|
||||
v: filters.vibrance,
|
||||
t: filters.threshold,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -81,6 +84,7 @@ const FiltersFactory = {
|
||||
contrast: filters.c,
|
||||
desaturate: filters.d,
|
||||
vibrance: filters.v,
|
||||
threshold: filters.t,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -107,5 +111,6 @@ export const isEqual = ( filters: Filters, filtersToCompareTo?: Filters ): boole
|
||||
filters.brightness === filtersToCompareTo.brightness &&
|
||||
filters.contrast === filtersToCompareTo.contrast &&
|
||||
filters.desaturate === filtersToCompareTo.desaturate &&
|
||||
filters.vibrance === filtersToCompareTo.vibrance;
|
||||
filters.vibrance === filtersToCompareTo.vibrance &&
|
||||
filters.threshold === filtersToCompareTo.threshold;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,9 @@ import type { ActionContext, Module } from "vuex";
|
||||
import { isMobile } from "@/utils/environment-util";
|
||||
import { setWasmFilters } from "@/services/render-service";
|
||||
|
||||
// @ts-expect-error 'import.meta' property not allowed (not an issue, Vite takes care of it)
|
||||
const SUPPORT_WASM = import.meta.env.VITE_ENABLE_WASM_FILTERS === "true" && "WebAssembly" in window;
|
||||
|
||||
const STORAGE_KEY = "bpy_pref";
|
||||
|
||||
export type Preferences = {
|
||||
@@ -54,6 +57,7 @@ const PreferencesModule: Module<PreferencesState, any> = {
|
||||
// curried, so not reactive !
|
||||
// @ts-expect-error using string as key
|
||||
getPreference: ( state: PreferencesState ): ( n: string ) => string => ( name: string ): boolean => state.preferences[ name ],
|
||||
supportWASM: (): boolean => SUPPORT_WASM,
|
||||
},
|
||||
mutations: {
|
||||
setPreferences( state: PreferencesState, preferences: Preferences ): void {
|
||||
@@ -74,7 +78,7 @@ const PreferencesModule: Module<PreferencesState, any> = {
|
||||
if ( typeof preferences.antiAlias === "boolean" ) {
|
||||
commit( "setAntiAlias", preferences.antiAlias );
|
||||
}
|
||||
setWasmFilters( !!preferences.wasmFilters );
|
||||
setWasmFilters( SUPPORT_WASM && !!preferences.wasmFilters );
|
||||
} catch {
|
||||
// non-blocking
|
||||
}
|
||||
|
||||
@@ -81,8 +81,22 @@ inline void vibrance( float vibrance, float& r, float& g, float& b ) {
|
||||
}
|
||||
}
|
||||
|
||||
inline void threshold( float threshold, float& r, float& g, float& b ) {
|
||||
float luma = r * 0.3 + g * 0.59 + b * 0.11;
|
||||
|
||||
luma = luma < threshold ? 0 : 255;
|
||||
|
||||
r = luma;
|
||||
g = luma;
|
||||
b = luma;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
void filter( float* pixels, int length, float vGamma, float vBrightness, float vContrast, float vVibrance, bool doGamma, bool doDesaturate, bool doBrightness, bool doContrast, bool doVibrance ) {
|
||||
void filter(
|
||||
float* pixels, int length,
|
||||
float vGamma, float vBrightness, float vContrast, float vVibrance, /*float vThreshold,*/
|
||||
bool doGamma, bool doDesaturate, bool doBrightness, bool doContrast, bool doVibrance/*, bool doThreshold*/
|
||||
) {
|
||||
float r, g, b, a;
|
||||
float gammaSquared = vGamma * vGamma;
|
||||
|
||||
@@ -90,6 +104,7 @@ extern "C" {
|
||||
r = pixels[ i ];
|
||||
g = pixels[ i + 1 ];
|
||||
b = pixels[ i + 2 ];
|
||||
a = pixels[ i + 3 ];
|
||||
|
||||
if ( doGamma )
|
||||
gamma( gammaSquared, r, g, b );
|
||||
@@ -106,6 +121,9 @@ extern "C" {
|
||||
if ( doVibrance )
|
||||
vibrance( vVibrance, r, g, b );
|
||||
|
||||
// if ( doThreshold && a > 0 )
|
||||
// threshold( vThreshold, r, g, b );
|
||||
|
||||
pixels[ i ] = r;
|
||||
pixels[ i + 1 ] = g;
|
||||
pixels[ i + 2 ] = b;
|
||||
|
||||
@@ -84,10 +84,12 @@ function renderFilters( imageData: ImageData, filters: any ): Uint8ClampedArray
|
||||
const contrast = Math.pow((( filters.contrast * 100 ) + 100 ) / 100, 2 ); // -100 to 100 range
|
||||
const gamma = ( filters.gamma * 2 ); // 0 to 2 range
|
||||
const vibrance = -(( filters.vibrance * 200 ) - 100 ); // -100 to 100 range
|
||||
const { desaturate } = filters; // boolean
|
||||
|
||||
const { desaturate, threshold } = filters;
|
||||
|
||||
const pixels = imageData.data;
|
||||
let r, g, b;//, a;
|
||||
|
||||
let r, g, b, a;
|
||||
let grayScale, max, avg, amt;
|
||||
const gammaSquared = gamma * gamma;
|
||||
|
||||
@@ -95,6 +97,7 @@ function renderFilters( imageData: ImageData, filters: any ): Uint8ClampedArray
|
||||
const doContrast = filters.contrast !== defaultFilters.contrast;
|
||||
const doGamma = filters.gamma !== defaultFilters.gamma;
|
||||
const doVibrance = filters.vibrance !== defaultFilters.vibrance;
|
||||
const doThreshold = filters.threshold !== defaultFilters.threshold;
|
||||
|
||||
// loop through the pixels, note we increment the iterator by four
|
||||
// as each pixel is defined by four channel values : red, green, blue and the alpha channel
|
||||
@@ -105,7 +108,7 @@ function renderFilters( imageData: ImageData, filters: any ): Uint8ClampedArray
|
||||
r = pixels[ i ];
|
||||
g = pixels[ i + 1 ];
|
||||
b = pixels[ i + 2 ];
|
||||
//a = pixels[ i + 3 ]; // currently no filter uses alpha channel
|
||||
a = pixels[ i + 3 ];
|
||||
|
||||
// 1. adjust gamma
|
||||
if ( doGamma ) {
|
||||
@@ -153,6 +156,16 @@ function renderFilters( imageData: ImageData, filters: any ): Uint8ClampedArray
|
||||
}
|
||||
}
|
||||
|
||||
// 6. adjust threshold
|
||||
|
||||
if ( doThreshold && a > 0 ) {
|
||||
let luma = r * 0.3 + g * 0.59 + b * 0.11;
|
||||
|
||||
luma = luma < threshold ? 0 : 255;
|
||||
|
||||
r = g = b = luma;
|
||||
}
|
||||
|
||||
// commit the changes
|
||||
pixels[ i ] = r;
|
||||
pixels[ i + 1 ] = g;
|
||||
@@ -169,20 +182,21 @@ function renderFiltersWasm( imageData: ImageData, filters: any ): Uint8ClampedAr
|
||||
const contrast = Math.pow((( filters.contrast * 100 ) + 100 ) / 100, 2 ); // -100 to 100 range
|
||||
const gamma = ( filters.gamma * 2 ); // 0 to 2 range
|
||||
const vibrance = -(( filters.vibrance * 200 ) - 100 ); // -100 to 100 range
|
||||
const { desaturate } = filters; // boolean
|
||||
const { desaturate, threshold } = filters;
|
||||
|
||||
const doBrightness = filters.brightness !== defaultFilters.brightness;
|
||||
const doContrast = filters.contrast !== defaultFilters.contrast;
|
||||
const doGamma = filters.gamma !== defaultFilters.gamma;
|
||||
const doVibrance = filters.vibrance !== defaultFilters.vibrance;
|
||||
const doThreshold = filters.threshold !== defaultFilters.threshold;
|
||||
|
||||
// run WASM operations
|
||||
|
||||
return imageDataAsFloat( imageData, wasmInstance, ( memory, length ) => {
|
||||
wasmInstance._filter(
|
||||
memory, length,
|
||||
gamma, brightness, contrast, vibrance,
|
||||
doGamma, desaturate, doBrightness, doContrast, doVibrance
|
||||
gamma, brightness, contrast, vibrance,/* threshold,*/
|
||||
doGamma, desaturate, doBrightness, doContrast, doVibrance/*, doThreshold*/
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ describe( "Filters factory", () => {
|
||||
brightness: .5,
|
||||
contrast: 0,
|
||||
vibrance: .5,
|
||||
threshold: -1,
|
||||
desaturate: false,
|
||||
});
|
||||
});
|
||||
@@ -27,6 +28,7 @@ describe( "Filters factory", () => {
|
||||
brightness: .6,
|
||||
contrast: .3,
|
||||
vibrance: .2,
|
||||
threshold: 127,
|
||||
desaturate: true,
|
||||
});
|
||||
expect( filters ).toEqual({
|
||||
@@ -37,6 +39,7 @@ describe( "Filters factory", () => {
|
||||
brightness: .6,
|
||||
contrast: .3,
|
||||
vibrance: .2,
|
||||
threshold: 127,
|
||||
desaturate: true,
|
||||
});
|
||||
});
|
||||
@@ -52,6 +55,7 @@ describe( "Filters factory", () => {
|
||||
brightness: .6,
|
||||
contrast: .3,
|
||||
vibrance: .2,
|
||||
threshold: 255,
|
||||
desaturate: true,
|
||||
});
|
||||
const serialized = FiltersFactory.serialize( filters );
|
||||
@@ -86,6 +90,9 @@ describe( "Filters factory", () => {
|
||||
filter = FiltersFactory.create({ vibrance: .4 });
|
||||
expect( hasFilters( filter )).toBe( true );
|
||||
|
||||
filter = FiltersFactory.create({ threshold: 0 });
|
||||
expect( hasFilters( filter )).toBe( true );
|
||||
|
||||
filter = FiltersFactory.create({ desaturate: true });
|
||||
expect( hasFilters( filter )).toBe( true );
|
||||
});
|
||||
@@ -93,7 +100,7 @@ describe( "Filters factory", () => {
|
||||
|
||||
it( "should know when two filters instances are equal", () => {
|
||||
const defaultFilter = FiltersFactory.create();
|
||||
[ "enabled", "blendMode", "opacity", "gamma", "brightness", "contrast", "vibrance", "desaturate" ].forEach( property => {
|
||||
[ "enabled", "blendMode", "opacity", "gamma", "brightness", "contrast", "vibrance", "threshold", "desaturate" ].forEach( property => {
|
||||
const filters = FiltersFactory.create({ [ property ]: .88 });
|
||||
expect( isEqual( filters, defaultFilter )).toBe( false );
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user