Source code for lightautoml.transformers.image
"""Image features transformers."""
import logging
import os
import pickle
from copy import deepcopy
from typing import Any
from typing import Callable
from typing import List
from typing import Optional
from typing import Union
import numpy as np
import torch
from ..dataset.base import LAMLDataset
from ..dataset.np_pd_dataset import NumpyDataset
from ..dataset.np_pd_dataset import PandasDataset
from ..dataset.roles import NumericRole
from ..image.image import CreateImageFeatures
from ..image.image import DeepTimmImageEmbedder
from ..image.utils import pil_loader
from ..text.utils import get_textarr_hash
from ..text.utils import single_text_hash
from .base import LAMLTransformer
logger = logging.getLogger(__name__)
NumpyOrPandas = Union[NumpyDataset, PandasDataset]
def path_check(dataset: LAMLDataset):
"""Check if all passed vars are path.
Args:
dataset: LAMLDataset to check.
Raises:
AssertionError: If non-path features are present.
"""
roles = dataset.roles
features = dataset.features
for f in features:
assert roles[f].name == "Path", "Only path accepted in this transformer"
[docs]class ImageFeaturesTransformer(LAMLTransformer):
"""Simple image histogram."""
_fit_checks = (path_check,)
_transform_checks = ()
_fname_prefix = "img_hist"
[docs] def __init__(
self,
hist_size: int = 30,
is_hsv: bool = True,
n_jobs: int = 4,
loader: Callable = pil_loader,
):
"""Create normalized color histogram for rgb or hsv image.
Args:
hist_size: Number of bins for each channel.
is_hsv: Convert image to hsv.
n_jobs: Number of threads for multiprocessing.
loader: Callable for reading image from path.
"""
self.hist_size = hist_size
self.is_hsv = is_hsv
self.n_jobs = n_jobs
self.loader = loader
@property
def features(self) -> List[str]:
"""Features list.
Returns:
List of features names.
"""
return self._features
[docs] def fit(self, dataset: NumpyOrPandas):
"""Init hist class and create feature names.
Args:
dataset: Pandas or Numpy dataset of text features.
Returns:
self.
"""
# set transformer names and add checks
for check_func in self._fit_checks:
check_func(dataset)
# set transformer features
dataset = dataset.to_pandas()
df = dataset.data
feats = []
self.dicts = {}
for n, i in enumerate(df.columns):
fg = CreateImageFeatures(self.hist_size, self.is_hsv, self.n_jobs, self.loader)
features = list(
np.char.array([self._fname_prefix + "_"]) + np.char.array(fg.fe.get_names()) + np.char.array(["__" + i])
)
self.dicts[i] = {"fg": fg, "feats": features}
feats.extend(features)
self._features = feats
return self
[docs] def transform(self, dataset: NumpyOrPandas) -> NumpyDataset:
"""Transform image dataset to color histograms.
Args:
dataset: Pandas or Numpy dataset of image paths.
Returns:
Dataset with encoded text.
"""
# checks here
super().transform(dataset)
# convert to accepted dtype and get attributes
dataset = dataset.to_pandas()
df = dataset.data
# transform
roles = NumericRole()
outputs = []
for n, i in enumerate(df.columns):
new_arr = self.dicts[i]["fg"].transform(df[i].values)
output = dataset.empty().to_numpy()
output.set_data(new_arr, self.dicts[i]["feats"], roles)
outputs.append(output)
# create resulted
return dataset.empty().to_numpy().concat(outputs)
[docs]class AutoCVWrap(LAMLTransformer):
"""Calculate image embeddings.
Args:
model: Name of effnet model.
weights_path: Path to saved weights.
cache_dir: Path to cache directory or None.
subs: Subsample to fit transformer. If ``None`` - full data.
device: Torch device.
n_jobs: Number of threads for dataloader.
random_state: Random state to take subsample and set torch seed.
batch_size: Batch size for embedding model.
verbose: Verbose data processing.
"""
_fit_checks = (path_check,)
_transform_checks = ()
_fname_prefix = "emb_cv"
_emb_name = ""
@property
def features(self) -> List[str]:
"""Features list.
Returns:
List of features names.
"""
return self._features
def __init__(
self,
model="efficientnet_b0.ra_in1k",
weights_path: Optional[str] = None,
cache_dir: str = "./cache_CV",
subs: Optional[Any] = None,
device: torch.device = torch.device("cuda:0"),
n_jobs: int = 4,
random_state: int = 42,
batch_size: int = 128,
verbose: bool = True,
):
self.embed_model = model
self.random_state = random_state
self.subs = subs
self.dicts = {}
self.cache_dir = cache_dir
self.transformer = DeepTimmImageEmbedder(
device,
n_jobs,
random_state,
model,
weights_path,
batch_size,
verbose,
)
self._emb_name = "DI_" + single_text_hash(self.embed_model)
self.emb_size = self.transformer.model.feature_shape
[docs] def fit(self, dataset: NumpyOrPandas):
"""Fit chosen transformer and create feature names.
Args:
dataset: Pandas or Numpy dataset of text features.
Returns:
self.
"""
for check_func in self._fit_checks:
check_func(dataset)
if self.cache_dir is not None:
if not os.path.exists(self.cache_dir):
os.makedirs(self.cache_dir)
# set transformer features
# convert to accepted dtype and get attributes
dataset = dataset.to_pandas()
df = dataset.data
# fit
if self.subs is not None and df.shape[0] >= self.subs:
subs = df.sample(n=self.subs, random_state=self.random_state)
else:
subs = df
names = []
for n, i in enumerate(subs.columns):
feats = [self._fname_prefix + "_" + self._emb_name + "_" + str(x) + "__" + i for x in range(self.emb_size)]
self.dicts[i] = {
"transformer": deepcopy(self.transformer.fit(subs[i])),
"feats": feats,
}
names.extend(feats)
self._features = names
return self
[docs] def transform(self, dataset: NumpyOrPandas) -> NumpyDataset:
"""Transform dataset to image embeddings.
Args:
dataset: Pandas or Numpy dataset of image paths.
Returns:
Numpy dataset with image embeddings.
"""
# checks here
super().transform(dataset)
# convert to accepted dtype and get attributes
dataset = dataset.to_pandas()
df = dataset.data
# transform
roles = NumericRole()
outputs = []
for n, conlumn_name in enumerate(df.columns):
if self.cache_dir is not None:
full_hash = get_textarr_hash(df[conlumn_name]) + get_textarr_hash(self.dicts[conlumn_name]["feats"])
fname = os.path.join(self.cache_dir, full_hash + ".pkl")
if os.path.exists(fname):
logger.info3(f"Load saved dataset for {conlumn_name}")
with open(fname, "rb") as f:
new_arr = pickle.load(f)
else:
new_arr = self.dicts[conlumn_name]["transformer"].transform(df[conlumn_name])
with open(fname, "wb") as f:
pickle.dump(new_arr, f)
else:
new_arr = self.dicts[conlumn_name]["transformer"].transform(df[conlumn_name])
output = dataset.empty().to_numpy()
output.set_data(new_arr, self.dicts[conlumn_name]["feats"], roles)
outputs.append(output)
logger.info3(f"Feature {conlumn_name} transformed")
# create resulted
return dataset.empty().to_numpy().concat(outputs)