visfeats 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- visfeats/__init__.py +85 -0
- visfeats/chromatic.py +60 -0
- visfeats/memorability.py +52 -0
- visfeats/misc.py +23 -0
- visfeats/spatial_frequency.py +272 -0
- visfeats-0.1.dist-info/METADATA +125 -0
- visfeats-0.1.dist-info/RECORD +10 -0
- visfeats-0.1.dist-info/WHEEL +5 -0
- visfeats-0.1.dist-info/licenses/LICENSE +21 -0
- visfeats-0.1.dist-info/top_level.txt +1 -0
visfeats/__init__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from importlib.util import find_spec
|
|
4
|
+
from typing import Dict, Union
|
|
5
|
+
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
from .chromatic import hsv_features
|
|
9
|
+
from .spatial_frequency import spatial_frequency_features
|
|
10
|
+
from .memorability import memorability_resmem
|
|
11
|
+
from .misc import rms
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_FEATURES = {
|
|
16
|
+
"hsv_features": hsv_features,
|
|
17
|
+
"spatial_frequency_features": spatial_frequency_features,
|
|
18
|
+
"rms": rms,
|
|
19
|
+
"memorability_resmem": memorability_resmem,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"extract_all_features",
|
|
24
|
+
"spatial_frequency_features",
|
|
25
|
+
"hsv_features",
|
|
26
|
+
"rms",
|
|
27
|
+
"memorability_resmem",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extract_all_features(
|
|
32
|
+
image: Image.Image,
|
|
33
|
+
features: Union[str, Sequence[str]] = "all",
|
|
34
|
+
) -> Dict[str, float]:
|
|
35
|
+
"""Extract selected visual features from an image.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
image : PIL.Image.Image
|
|
40
|
+
Input image in any Pillow mode.
|
|
41
|
+
features : str or sequence of str, default="all"
|
|
42
|
+
``"all"`` selects every package-level feature. A feature name or
|
|
43
|
+
sequence of feature names selects only those features. Available
|
|
44
|
+
names are ``"hsv_features"``, ``"spatial_frequency_features"``,
|
|
45
|
+
``"rms"``, and ``"memorability_resmem"``.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
dict of {str: float}
|
|
50
|
+
A flat mapping of feature names to values. Multi-value features
|
|
51
|
+
contribute their own keys. ``memorability_resmem`` is skipped with a
|
|
52
|
+
warning if the optional ``resmem`` package is not installed.
|
|
53
|
+
|
|
54
|
+
Raises
|
|
55
|
+
------
|
|
56
|
+
ValueError
|
|
57
|
+
If any requested feature name is not available.
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(features, str):
|
|
60
|
+
selected = list(_FEATURES) if features == "all" else [features]
|
|
61
|
+
else:
|
|
62
|
+
selected = list(dict.fromkeys(features))
|
|
63
|
+
|
|
64
|
+
unknown = set(selected) - _FEATURES.keys()
|
|
65
|
+
if unknown:
|
|
66
|
+
names = ", ".join(sorted(unknown))
|
|
67
|
+
raise ValueError(f"Unknown feature name(s): {names}")
|
|
68
|
+
|
|
69
|
+
extracted: Dict[str, float] = {}
|
|
70
|
+
for name in selected:
|
|
71
|
+
if name == "memorability_resmem" and find_spec("resmem") is None:
|
|
72
|
+
logger.warning(
|
|
73
|
+
"Skipping memorability_resmem because the optional `resmem` "
|
|
74
|
+
"package is not installed. Install it with: "
|
|
75
|
+
"pip install 'visfeats[resmem]'"
|
|
76
|
+
)
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
result = _FEATURES[name](image)
|
|
80
|
+
if isinstance(result, dict):
|
|
81
|
+
extracted.update(result)
|
|
82
|
+
else:
|
|
83
|
+
extracted[name] = float(result)
|
|
84
|
+
|
|
85
|
+
return extracted
|
visfeats/chromatic.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from PIL import Image
|
|
2
|
+
from skimage import color
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.stats import circmean, entropy
|
|
5
|
+
|
|
6
|
+
HSV_RANGE = (0.0, 1.0)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def hsv_features(image: Image.Image, bins: int = 100) -> dict:
|
|
10
|
+
"""Compute the mean and entropy of the HSV channels of an image.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
image : PIL.Image.Image
|
|
15
|
+
Input image.
|
|
16
|
+
bins : int, default=100
|
|
17
|
+
Number of bins for the histograms used to compute entropy.
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
dict of {str: float}
|
|
22
|
+
A mapping of feature names to values:
|
|
23
|
+
|
|
24
|
+
================== ======================================
|
|
25
|
+
Key Description
|
|
26
|
+
================== ======================================
|
|
27
|
+
hue_mean Circular mean hue in [0, 1).
|
|
28
|
+
saturation_mean Mean saturation in [0, 1].
|
|
29
|
+
value_mean Mean value (brightness) in [0, 1].
|
|
30
|
+
hue_entropy Entropy of the hue histogram.
|
|
31
|
+
saturation_entropy Entropy of the saturation histogram.
|
|
32
|
+
value_entropy Entropy of the value histogram.
|
|
33
|
+
================== ======================================
|
|
34
|
+
|
|
35
|
+
Notes
|
|
36
|
+
-----
|
|
37
|
+
The image is converted to RGB, then to HSV using
|
|
38
|
+
``skimage.color.rgb2hsv``. The mean and histogram entropy are computed
|
|
39
|
+
for each channel.
|
|
40
|
+
"""
|
|
41
|
+
rgb = image.convert("RGB")
|
|
42
|
+
array = np.array(rgb)
|
|
43
|
+
hsv = color.rgb2hsv(array)
|
|
44
|
+
|
|
45
|
+
hue = hsv[:, :, 0]
|
|
46
|
+
saturation = hsv[:, :, 1]
|
|
47
|
+
value = hsv[:, :, 2]
|
|
48
|
+
|
|
49
|
+
hue_pk = np.histogram(hue, bins=bins, range=HSV_RANGE, density=True)
|
|
50
|
+
saturation_pk = np.histogram(saturation, bins=bins, range=HSV_RANGE, density=True)
|
|
51
|
+
value_pk = np.histogram(value, bins=bins, range=HSV_RANGE, density=True)
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"hue_mean": float(circmean(hue, high=1.0, low=0.0)), # circular mean of hue
|
|
55
|
+
"saturation_mean": float(saturation.mean()),
|
|
56
|
+
"value_mean": float(value.mean()),
|
|
57
|
+
"hue_entropy": float(entropy(hue_pk[0])),
|
|
58
|
+
"saturation_entropy": float(entropy(saturation_pk[0])),
|
|
59
|
+
"value_entropy": float(entropy(value_pk[0])),
|
|
60
|
+
}
|
visfeats/memorability.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@lru_cache(maxsize=1)
|
|
7
|
+
def _load_resmem():
|
|
8
|
+
# Import within function so resmem/torch load only when needed.
|
|
9
|
+
try:
|
|
10
|
+
from resmem import ResMem, transformer
|
|
11
|
+
except ImportError as error:
|
|
12
|
+
raise ImportError(
|
|
13
|
+
"memorability_resmem requires the optional dependency `resmem`. "
|
|
14
|
+
"Install it with: pip install resmem"
|
|
15
|
+
) from error
|
|
16
|
+
|
|
17
|
+
model = ResMem(pretrained=True)
|
|
18
|
+
model.eval()
|
|
19
|
+
return model, transformer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def memorability_resmem(image: Image.Image) -> float:
|
|
23
|
+
"""Estimate the intrinsic memorability of an image.
|
|
24
|
+
|
|
25
|
+
The image is converted to RGB, preprocessed with the ResMem transformer
|
|
26
|
+
(resized to 227 x 227 and normalized), and passed through the pretrained
|
|
27
|
+
ResMem network, a residual neural network trained to predict human
|
|
28
|
+
memorability scores (Needell and Bainbridge, 2022). The model is loaded
|
|
29
|
+
once per process and reused across calls.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
image : PIL.Image.Image
|
|
34
|
+
Input image in any Pillow mode; converted to RGB internally.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
float
|
|
39
|
+
Predicted memorability score in [0, 1]; higher values indicate a
|
|
40
|
+
more memorable image.
|
|
41
|
+
|
|
42
|
+
References
|
|
43
|
+
----------
|
|
44
|
+
.. [1] Needell, C. D., & Bainbridge, W. A. (2022).
|
|
45
|
+
Embracing new techniques in deep learning for estimating image memorability.
|
|
46
|
+
Computational Brain & Behavior. https://doi.org/10.1007/s42113-022-00126-5
|
|
47
|
+
"""
|
|
48
|
+
model, transformer = _load_resmem()
|
|
49
|
+
image = image.convert("RGB")
|
|
50
|
+
image_x = transformer(image)
|
|
51
|
+
prediction = model(image_x.view(-1, 3, 227, 227))
|
|
52
|
+
return float(prediction.detach().numpy().squeeze())
|
visfeats/misc.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from PIL import Image
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def rms(image: Image.Image) -> float:
|
|
6
|
+
"""Compute the root-mean-square (RMS) contrast of an image.
|
|
7
|
+
|
|
8
|
+
The image is converted to 8-bit grayscale and the RMS contrast is the
|
|
9
|
+
population standard deviation of the pixel intensities (values in
|
|
10
|
+
[0, 255]).
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
image : PIL.Image.Image
|
|
15
|
+
Input image in any Pillow mode; converted to grayscale internally.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
float
|
|
20
|
+
RMS contrast. 0 for a uniform image; 127.5 is the maximum.
|
|
21
|
+
"""
|
|
22
|
+
grayscale = image.convert("L")
|
|
23
|
+
return float(np.array(grayscale).std())
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
from typing import Dict, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from PIL import Image
|
|
5
|
+
from scipy import fft
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _pad_and_resize_image(
|
|
9
|
+
image: Image.Image,
|
|
10
|
+
size: int = 1024,
|
|
11
|
+
pad: bool = True,
|
|
12
|
+
) -> np.ndarray:
|
|
13
|
+
"""Convert to grayscale, optionally pad to a square, then resize to ``size``."""
|
|
14
|
+
grayscale = np.asarray(image.convert("L"), dtype=np.float64)
|
|
15
|
+
|
|
16
|
+
if pad:
|
|
17
|
+
height, width = grayscale.shape
|
|
18
|
+
|
|
19
|
+
# Redies et al. pad using a uniform border at the image mean gray value.
|
|
20
|
+
side = max(height, width)
|
|
21
|
+
padded = np.full((side, side), grayscale.mean(), dtype=np.float64)
|
|
22
|
+
|
|
23
|
+
y0 = (side - height) // 2
|
|
24
|
+
x0 = (side - width) // 2
|
|
25
|
+
padded[y0 : y0 + height, x0 : x0 + width] = grayscale
|
|
26
|
+
grayscale = padded
|
|
27
|
+
|
|
28
|
+
# Pillow's F mode is float32; it preserves fractional values during bicubic
|
|
29
|
+
# resize (passing float64 with mode="F" would misread the buffer).
|
|
30
|
+
resized = Image.fromarray(grayscale.astype(np.float32), mode="F").resize(
|
|
31
|
+
(size, size),
|
|
32
|
+
resample=Image.Resampling.BICUBIC,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return np.asarray(resized, dtype=np.float64)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _centered_power_spectrum(
|
|
39
|
+
grayscale: np.ndarray,
|
|
40
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
41
|
+
"""Return the centered 2D power spectrum and its (fy, fx) frequency grids."""
|
|
42
|
+
height, width = grayscale.shape
|
|
43
|
+
if height != width:
|
|
44
|
+
raise ValueError("Expected a square image.")
|
|
45
|
+
|
|
46
|
+
spectrum = fft.fftshift(fft.fft2(grayscale))
|
|
47
|
+
power = np.abs(spectrum) ** 2
|
|
48
|
+
|
|
49
|
+
# Frequencies expressed in cycles per image, rather than cycles/pixel.
|
|
50
|
+
# For a 1024-pixel image, values span roughly -512 to +511 cycles/image.
|
|
51
|
+
frequencies = fft.fftshift(fft.fftfreq(height)) * height
|
|
52
|
+
fy, fx = np.meshgrid(frequencies, frequencies, indexing="ij")
|
|
53
|
+
|
|
54
|
+
return power, fy, fx
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _radially_averaged_power_spectrum(
|
|
58
|
+
grayscale: np.ndarray,
|
|
59
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
60
|
+
"""Return radial spatial frequencies and mean power for a square image."""
|
|
61
|
+
power, fy, fx = _centered_power_spectrum(grayscale)
|
|
62
|
+
radius = np.hypot(fx, fy)
|
|
63
|
+
|
|
64
|
+
# Annular radial bins: 0-1, 1-2, 2-3 ... cycles/image.
|
|
65
|
+
radial_index = np.floor(radius).astype(np.intp)
|
|
66
|
+
max_radius = radial_index.max()
|
|
67
|
+
|
|
68
|
+
power_sum = np.bincount(
|
|
69
|
+
radial_index.ravel(),
|
|
70
|
+
weights=power.ravel(),
|
|
71
|
+
minlength=max_radius + 1,
|
|
72
|
+
)
|
|
73
|
+
sample_count = np.bincount(
|
|
74
|
+
radial_index.ravel(),
|
|
75
|
+
minlength=max_radius + 1,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
radial_power = np.divide(
|
|
79
|
+
power_sum,
|
|
80
|
+
sample_count,
|
|
81
|
+
out=np.zeros_like(power_sum, dtype=np.float64),
|
|
82
|
+
where=sample_count > 0,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
radial_frequency = np.arange(max_radius + 1, dtype=np.float64)
|
|
86
|
+
|
|
87
|
+
return radial_frequency, radial_power
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _spectral_centroid_and_variance(grayscale: np.ndarray) -> Tuple[float, float]:
|
|
91
|
+
"""Return the power-weighted mean frequency and orientation circular variance."""
|
|
92
|
+
power, fy, fx = _centered_power_spectrum(grayscale)
|
|
93
|
+
|
|
94
|
+
# Exclude DC: it carries the mean luminance and has no defined orientation.
|
|
95
|
+
nonzero = np.hypot(fx, fy) > 0
|
|
96
|
+
power = power[nonzero]
|
|
97
|
+
radius = np.hypot(fx, fy)[nonzero]
|
|
98
|
+
angle = np.arctan2(fy, fx)[nonzero]
|
|
99
|
+
|
|
100
|
+
total_power = power.sum()
|
|
101
|
+
if total_power == 0:
|
|
102
|
+
raise ValueError("Total power of the Fourier spectrum is zero.")
|
|
103
|
+
|
|
104
|
+
centroid = (radius * power).sum() / total_power
|
|
105
|
+
|
|
106
|
+
# Orientation is axial (theta and theta + pi are equivalent in a
|
|
107
|
+
# real-valued image's spectrum), so angles are doubled before averaging.
|
|
108
|
+
resultant = np.abs((power * np.exp(2j * angle)).sum()) / total_power
|
|
109
|
+
|
|
110
|
+
return float(centroid), float(1.0 - resultant)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _dimensionality(power: np.ndarray) -> float:
|
|
114
|
+
"""Slope of log magnitude versus log rank of the radially averaged spectrum."""
|
|
115
|
+
# Skip the DC bin; rank all remaining components by magnitude.
|
|
116
|
+
magnitude = np.sqrt(power[1:])
|
|
117
|
+
magnitude = magnitude[magnitude > 0]
|
|
118
|
+
if magnitude.size < 2:
|
|
119
|
+
raise ValueError("At least two non-zero Fourier components are required.")
|
|
120
|
+
|
|
121
|
+
ranked = np.sort(magnitude)[::-1]
|
|
122
|
+
ranks = np.arange(1, ranked.size + 1)
|
|
123
|
+
return float(np.polyfit(np.log10(ranks), np.log10(ranked), deg=1)[0])
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def spatial_frequency_features(
|
|
127
|
+
image: Image.Image,
|
|
128
|
+
size: int = 1024,
|
|
129
|
+
min_frequency: float = 10.0,
|
|
130
|
+
max_frequency: float = 256.0,
|
|
131
|
+
log_bins: int = 17,
|
|
132
|
+
pad: bool = True,
|
|
133
|
+
) -> Dict[str, float]:
|
|
134
|
+
"""
|
|
135
|
+
Calculate Fourier slope, Fourier sigma, dimensionality, spectral centroid,
|
|
136
|
+
and spectral variance of an image.
|
|
137
|
+
|
|
138
|
+
All are derived from the Fourier power spectrum of the grayscale image.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
image : PIL.Image.Image
|
|
143
|
+
Input image.
|
|
144
|
+
size : int, default=1024
|
|
145
|
+
Square analysis resolution.
|
|
146
|
+
min_frequency : float, default=10.0
|
|
147
|
+
Lowest fitted radial spatial frequency, in cycles/image.
|
|
148
|
+
max_frequency : float, default=256.0
|
|
149
|
+
Highest fitted radial spatial frequency, in cycles/image.
|
|
150
|
+
log_bins : int, default=17
|
|
151
|
+
Number of log-spaced frequency bins used before fitting [2]_.
|
|
152
|
+
pad : bool, default=True
|
|
153
|
+
Whether to pad non-square images to a square with the mean gray value
|
|
154
|
+
before resizing. If False, the image is resized directly to
|
|
155
|
+
``size`` x ``size``, which distorts the aspect ratio of non-square
|
|
156
|
+
images.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
dict
|
|
161
|
+
==================== ==============================================
|
|
162
|
+
Key Description
|
|
163
|
+
==================== ==============================================
|
|
164
|
+
``fourier_slope`` Slope of log power versus log frequency
|
|
165
|
+
(about -2 for natural scenes and many
|
|
166
|
+
artworks).
|
|
167
|
+
``fourier_sigma`` Mean squared residual of the binned data
|
|
168
|
+
around the fitted line.
|
|
169
|
+
``dimensionality`` Slope of log magnitude versus log rank of the
|
|
170
|
+
ranked spectrum (see Notes).
|
|
171
|
+
``spectral_centroid`` Power-weighted mean spatial frequency, in
|
|
172
|
+
cycles/image.
|
|
173
|
+
``spectral_variance`` Circular variance of the power spectrum over
|
|
174
|
+
orientation, in [0, 1] (see Notes).
|
|
175
|
+
==================== ==============================================
|
|
176
|
+
|
|
177
|
+
Notes
|
|
178
|
+
-----
|
|
179
|
+
1. Convert to grayscale, optionally (``pad``) pad to a square with the
|
|
180
|
+
mean gray value, and resize to ``size`` by bicubic interpolation [1]_.
|
|
181
|
+
2. Compute the 2D power spectrum and average it in annular bins of
|
|
182
|
+
1 cycle/image.
|
|
183
|
+
3. ``fourier_slope`` and ``fourier_sigma``: keep bins between
|
|
184
|
+
``min_frequency`` and ``max_frequency``, average log power and log
|
|
185
|
+
frequency within ``log_bins`` log-spaced bins, and fit a least-squares
|
|
186
|
+
line. The slope is ``fourier_slope``; the mean squared residual is
|
|
187
|
+
``fourier_sigma`` [2]_.
|
|
188
|
+
4. ``dimensionality``: using all bins except DC (no frequency
|
|
189
|
+
restriction), rank the magnitudes (square root of power) from largest
|
|
190
|
+
to smallest and fit a line to log magnitude versus log rank [3]_.
|
|
191
|
+
5. ``spectral_centroid``: using the full 2D power spectrum without DC,
|
|
192
|
+
the mean radial frequency weighted by power (center of mass).
|
|
193
|
+
6. ``spectral_variance``: circular variance, ``1 - R``, of the spectral
|
|
194
|
+
orientation weighted by power, where ``R`` is the length of the
|
|
195
|
+
power-weighted mean resultant vector of the doubled orientation angles
|
|
196
|
+
(the spectrum is point-symmetric). 0 means all power lies along one
|
|
197
|
+
orientation; 1 means power is spread evenly across orientations.
|
|
198
|
+
|
|
199
|
+
.. [1] Redies, C., Grebenkina, M., Mohseni, M., Kaduhm, A., & Dobel, C. (2020).
|
|
200
|
+
Global Image Properties Predict Ratings of Affective Pictures. Frontiers in Psychology, 11.
|
|
201
|
+
https://doi.org/10.3389/fpsyg.2020.00953
|
|
202
|
+
.. [2] Redies, C., Hasenstein, J., & Denzler, J. (2008).
|
|
203
|
+
Fractal-like image statistics in visual art: Similarity to natural scenes. Spatial Vision, 21(1–2), 137–148.
|
|
204
|
+
https://doi.org/10.1163/156856807782753921
|
|
205
|
+
.. [3] Uran, C., Peter, A., Lazar, A., Barnes, W., Klon-Lipok, J., Shapcott, K. A., Roese, R.,
|
|
206
|
+
Fries, P., Singer, W., & Vinck, M. (2022).
|
|
207
|
+
Predictive coding of natural images by V1 firing rates and rhythmic synchronization. Neuron, 110(7),
|
|
208
|
+
1240-1257.e8. https://doi.org/10.1016/j.neuron.2022.01.002
|
|
209
|
+
"""
|
|
210
|
+
grayscale = _pad_and_resize_image(image, size=size, pad=pad)
|
|
211
|
+
frequency, power = _radially_averaged_power_spectrum(grayscale)
|
|
212
|
+
dimensionality = _dimensionality(power)
|
|
213
|
+
centroid, variance = _spectral_centroid_and_variance(grayscale)
|
|
214
|
+
# Exclude DC (0 cycles/image) and restrict to the published fit range.
|
|
215
|
+
valid = (
|
|
216
|
+
(frequency >= min_frequency)
|
|
217
|
+
& (frequency <= max_frequency)
|
|
218
|
+
& (power > 0)
|
|
219
|
+
& np.isfinite(power)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
frequency = frequency[valid]
|
|
223
|
+
power = power[valid]
|
|
224
|
+
|
|
225
|
+
if frequency.size < 2:
|
|
226
|
+
raise ValueError("Not enough valid Fourier frequencies to fit a line.")
|
|
227
|
+
|
|
228
|
+
log_frequency = np.log10(frequency)
|
|
229
|
+
log_power = np.log10(power)
|
|
230
|
+
|
|
231
|
+
# Equally spaced bins in log-frequency space.
|
|
232
|
+
edges = np.linspace(
|
|
233
|
+
np.log10(min_frequency),
|
|
234
|
+
np.log10(max_frequency),
|
|
235
|
+
log_bins + 1,
|
|
236
|
+
)
|
|
237
|
+
bin_index = np.digitize(log_frequency, edges, right=False) - 1
|
|
238
|
+
bin_index = np.clip(bin_index, 0, log_bins - 1)
|
|
239
|
+
|
|
240
|
+
counts = np.bincount(bin_index, minlength=log_bins)
|
|
241
|
+
x_sum = np.bincount(
|
|
242
|
+
bin_index,
|
|
243
|
+
weights=log_frequency,
|
|
244
|
+
minlength=log_bins,
|
|
245
|
+
)
|
|
246
|
+
y_sum = np.bincount(
|
|
247
|
+
bin_index,
|
|
248
|
+
weights=log_power,
|
|
249
|
+
minlength=log_bins,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
occupied = counts > 0
|
|
253
|
+
x = x_sum[occupied] / counts[occupied]
|
|
254
|
+
y = y_sum[occupied] / counts[occupied]
|
|
255
|
+
|
|
256
|
+
if x.size < 2:
|
|
257
|
+
raise ValueError("Not enough occupied log-frequency bins to fit a line.")
|
|
258
|
+
|
|
259
|
+
# y = intercept + slope * x
|
|
260
|
+
slope, intercept = np.polyfit(x, y, deg=1)
|
|
261
|
+
fitted_y = intercept + slope * x
|
|
262
|
+
|
|
263
|
+
# Redies et al. (2007): sum of squared deviations / number of points.
|
|
264
|
+
fourier_sigma = np.mean((y - fitted_y) ** 2)
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"fourier_slope": float(slope),
|
|
268
|
+
"fourier_sigma": float(fourier_sigma),
|
|
269
|
+
"dimensionality": dimensionality,
|
|
270
|
+
"spectral_centroid": centroid,
|
|
271
|
+
"spectral_variance": variance,
|
|
272
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: visfeats
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: A library for extracting global visual features
|
|
5
|
+
Author-email: Qian Chu <qianchu99@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: scikit-image
|
|
10
|
+
Provides-Extra: resmem
|
|
11
|
+
Requires-Dist: resmem; extra == "resmem"
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: resmem; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# VisFeats: Extraction of Global Visual Features from Images
|
|
19
|
+
|
|
20
|
+
[](https://github.com/astral-sh/ruff)
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
VisFeats is a library for extracting global visual features from images.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
VisFeats is installable via PyPI:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install visfeats
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or via GitHub (for the latest development version):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install git+https://github.com/ncc-brain/VisFeats.git
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
To use VisFeats, first load an image using PIL:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from PIL import Image
|
|
45
|
+
image = Image.open("path/to/your/image.jpg")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
All functions in VisFeats take a PIL ``Image.Image`` as input and return the corresponding feature values. Available features are described below.
|
|
49
|
+
|
|
50
|
+
### Extract multiple features
|
|
51
|
+
|
|
52
|
+
Use `extract_all_features` to collect all package-level features, or pass a
|
|
53
|
+
feature name or list of names to select a subset. The result is a flat
|
|
54
|
+
dictionary.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from visfeats import extract_all_features
|
|
58
|
+
|
|
59
|
+
features = extract_all_features(image, features=["rms", "hsv_features"])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Mean and entropy of HSV channels
|
|
63
|
+
|
|
64
|
+
Chromatic features describe color in HSV (Hue, Saturation, Value) space, including channel means and entropies. The `hsv_features` function returns a dictionary with six keys:
|
|
65
|
+
|
|
66
|
+
- `hue_mean`
|
|
67
|
+
- `saturation_mean`
|
|
68
|
+
- `value_mean`
|
|
69
|
+
- `hue_entropy`
|
|
70
|
+
- `saturation_entropy`
|
|
71
|
+
- `value_entropy`
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from visfeats import hsv_features
|
|
75
|
+
features = hsv_features(image)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Spatial frequency features
|
|
79
|
+
|
|
80
|
+
Spatial frequency features capture the distribution of different spatial frequencies in an image, which relate to the level of detail and texture. The `spatial_frequency_features` function extracts these features:
|
|
81
|
+
|
|
82
|
+
- `fourier_slope`
|
|
83
|
+
- `fourier_sigma`
|
|
84
|
+
- `dimensionality`
|
|
85
|
+
- `spectral_centroid`
|
|
86
|
+
- `spectral_variance`
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from visfeats import spatial_frequency_features
|
|
90
|
+
features = spatial_frequency_features(image)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Memorability
|
|
94
|
+
|
|
95
|
+
While memorability is often considered a subjective property, recent research has shown that it can be predicted from image content. Here the `memorability_resmem` function estimates intrinsic memorability using the pretrained ResMem model described by [Needell and Bainbridge (2022)](https://doi.org/10.1007/s42113-022-00126-5).
|
|
96
|
+
|
|
97
|
+
ResMem is an optional dependency. It can be installed along with VisFeats using:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install "visfeats[resmem]"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Or separately with:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install resmem
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then you can use the `memorability_resmem` function to estimate the memorability of an image:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from visfeats import memorability_resmem
|
|
113
|
+
resmem_score = memorability_resmem(image)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Other features
|
|
117
|
+
|
|
118
|
+
#### RMS contrast
|
|
119
|
+
|
|
120
|
+
The `rms` function computes the root-mean-square contrast as the population standard deviation of the image's grayscale pixel intensities.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from visfeats import rms
|
|
124
|
+
rms_contrast = rms(image)
|
|
125
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
visfeats/__init__.py,sha256=rprYLjIT0pEu-MelUy1msGVLwKTzpCCxh3uZ6BGK0SA,2552
|
|
2
|
+
visfeats/chromatic.py,sha256=0SFdIygC6g5CzQu7_LBZh3-2frAgV311jNMjr7HBOrM,2091
|
|
3
|
+
visfeats/memorability.py,sha256=TotQGk6R2l0I-42ilJMesl6SEDmK-HunrgWrFlGn9dw,1719
|
|
4
|
+
visfeats/misc.py,sha256=YfLC2ptyKxx5yI_T9uKI1mES49qlVTdGVLa5ZDvMacI,628
|
|
5
|
+
visfeats/spatial_frequency.py,sha256=3cdEqD-U_Ce-0Rt76BA2yS-unHSjfucaVPxPiTbbGPU,10387
|
|
6
|
+
visfeats-0.1.dist-info/licenses/LICENSE,sha256=Kp1DPQGh36k51G0yLkCg_spVbXzIi66A3FbmfEfylaE,1065
|
|
7
|
+
visfeats-0.1.dist-info/METADATA,sha256=SbsKrwc_QiqFxA-0iyFOhUL364vD27Vbibn5mN9g54I,3495
|
|
8
|
+
visfeats-0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
9
|
+
visfeats-0.1.dist-info/top_level.txt,sha256=BOjMNuJsd_gpQqQaG67djZI-cOCdYOrHkmvinMAerPM,9
|
|
10
|
+
visfeats-0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Qian Chu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
visfeats
|