vislearnlabpy 0.0.2.3__tar.gz → 0.0.2.4__tar.gz

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.
Files changed (26) hide show
  1. {vislearnlabpy-0.0.2.3/src/vislearnlabpy.egg-info → vislearnlabpy-0.0.2.4}/PKG-INFO +1 -1
  2. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/pyproject.toml +1 -1
  3. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/embeddings/stimuli_loader.py +137 -38
  4. vislearnlabpy-0.0.2.4/src/vislearnlabpy/models/dinov3_model.py +20 -0
  5. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/models/multimodal_model.py +0 -39
  6. vislearnlabpy-0.0.2.4/src/vislearnlabpy/models/vision_model.py +54 -0
  7. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4/src/vislearnlabpy.egg-info}/PKG-INFO +1 -1
  8. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy.egg-info/SOURCES.txt +3 -1
  9. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/LICENSE +0 -0
  10. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/README.md +0 -0
  11. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/setup.cfg +0 -0
  12. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/__init__.py +0 -0
  13. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/drawings/drawing.py +0 -0
  14. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/drawings/svg_render_helpers.py +0 -0
  15. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/embeddings/embedding_store.py +0 -0
  16. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/embeddings/generate_embeddings.py +0 -0
  17. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/embeddings/similarity_generator.py +0 -0
  18. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/embeddings/similarity_utils.py +0 -0
  19. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/embeddings/utils.py +0 -0
  20. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/extractions/drawingtask_extractor.py +0 -0
  21. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/extractions/mongo_extractor.py +0 -0
  22. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/models/clip_model.py +0 -0
  23. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy/models/feature_generator.py +0 -0
  24. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy.egg-info/dependency_links.txt +0 -0
  25. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy.egg-info/requires.txt +0 -0
  26. {vislearnlabpy-0.0.2.3 → vislearnlabpy-0.0.2.4}/src/vislearnlabpy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vislearnlabpy
3
- Version: 0.0.2.3
3
+ Version: 0.0.2.4
4
4
  Summary: Visual Learning Lab utility files and pipelines
5
5
  Author-email: Tarun Sepuri <tarunsepuri@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vislearnlabpy"
7
- version = "0.0.2.3"
7
+ version = "0.0.2.4"
8
8
  authors = [
9
9
  { name="Tarun Sepuri", email="tarunsepuri@gmail.com" },
10
10
  ]
@@ -10,6 +10,9 @@ from typing import Callable, Any
10
10
  import warnings
11
11
  from torchvision import transforms
12
12
  import numpy as np
13
+ from scipy import ndimage
14
+ from skimage.morphology import skeletonize, binary_dilation, disk
15
+ from skimage import img_as_bool
13
16
 
14
17
  random.seed(2)
15
18
 
@@ -24,7 +27,12 @@ class ImgExtractionSettings:
24
27
  change_stroke_color: bool = False
25
28
  stroke_color: tuple = (0, 0, 0)
26
29
  stroke_threshold: int = 200
27
-
30
+ bg_threshold: int = 200 # threshold to consider a pixel as background (0-255)
31
+ bg_component_size: int = 10 # minimum size of connected components to keep (in pixels)
32
+ filter_edge_artifacts: bool = False # whether to filter out edge artifacts during cropping
33
+ normalize_stroke_thickness: bool = False
34
+ stroke_target_thickness: int = 2 # target thickness for stroke normalization
35
+ double_resize: bool = False # whether to resize twice (once with thumbnail, once after with resize func)
28
36
 
29
37
  class ImageExtractor:
30
38
  @staticmethod
@@ -87,27 +95,115 @@ class ImageExtractor:
87
95
  result[stroke_mask, 2] = target_color[2] # B
88
96
 
89
97
  return Image.fromarray(result)
98
+
99
+ def normalize_stroke_thickness(img, target_thickness=2):
100
+ # Binarize
101
+ arr = np.array(img.convert('L'))
102
+ binary = arr < 128 # assuming dark strokes on light background
103
+
104
+ # Skeletonize to 1-pixel lines
105
+ skeleton = skeletonize(binary)
106
+
107
+ # Dilate to consistent thickness
108
+ if target_thickness > 1:
109
+ skeleton = binary_dilation(skeleton, disk(target_thickness//2))
110
+
111
+ # Convert back to image (white background, black strokes)
112
+ result = (~skeleton * 255).astype(np.uint8)
113
+ return Image.fromarray(result)
114
+
115
+ @staticmethod
116
+ def _filter_edge_artifacts(large_components, labeled, component_sizes, img_width, img_height):
117
+ for comp_id in range(1, len(component_sizes)):
118
+ if large_components[comp_id]:
119
+ comp_mask = (labeled == comp_id)
120
+ rows, cols = np.where(comp_mask)
121
+
122
+ if len(rows) > 0 or len(cols) > 0:
123
+ ylb, yub = min(rows), max(rows)
124
+ xlb, xub = min(cols), max(cols)
125
+ width = xub - xlb + 1
126
+ height = yub - ylb + 1
127
+ x_tolerance = 10
128
+ y_tolerance = 100
129
+ touches_top = ylb <= y_tolerance
130
+ touches_bottom = yub >= img_height - 1 - y_tolerance
131
+ touches_left = xlb <= x_tolerance
132
+ touches_right = xub >= img_width - 1 - x_tolerance
133
+ # Check if it's a thin strip spanning nearly the entire edge
134
+ # vertical strip is more likely to not be image artifact
135
+ is_thin_vertical_strip = (touches_top or touches_bottom and width < img_width * 0.2)
136
+ is_thin_horizontal_strip = (touches_left and touches_right and height < img_height * 0.2)
137
+ if is_thin_vertical_strip or is_thin_horizontal_strip or (width / height >= 10) or (height / width >= 10):
138
+ large_components[comp_id] = False
139
+ return large_components
90
140
 
91
141
  @staticmethod
92
- def crop_to_content(img, apply_content_crop):
93
- """Crop image to remove white space around content."""
142
+ def crop_to_content(img, apply_content_crop, stroke_threshold=200, min_component_size=10, filter_edge_artifacts=False):
143
+ """Crop image to remove white space around content, ignoring small artifacts."""
94
144
  if not apply_content_crop:
95
145
  return img
96
146
 
97
147
  arr = np.asarray(img)
98
- rows, cols, channels = np.where(arr < 255)
148
+ img_width, img_height = img.size
149
+ # Create binary mask of non-white pixels
150
+ mask = np.all(arr < stroke_threshold, axis=2) if arr.ndim == 3 else arr < stroke_threshold
99
151
 
152
+ # Label connected components
153
+ labeled, num_features = ndimage.label(mask)
154
+
155
+ # Find component sizes
156
+ component_sizes = np.bincount(labeled.ravel())
157
+ # Component 0 is background, so start from 1
158
+ large_components = component_sizes >= min_component_size
159
+ large_components[0] = False # Don't include background
160
+ # Filter out edge artifacts (components that span entire width or height)
161
+ if filter_edge_artifacts:
162
+ large_components = ImageExtractor._filter_edge_artifacts(large_components, labeled, component_sizes, img_width, img_height)
163
+
164
+ # Create cleaned mask with only large components
165
+ cleaned_mask = large_components[labeled]
166
+ if arr.ndim == 3:
167
+ white_img = np.ones_like(arr) * 255
168
+ else:
169
+ white_img = np.ones_like(arr) * 255
170
+
171
+ # Copy only the content pixels (keep background white)
172
+ white_img[cleaned_mask] = arr[cleaned_mask]
173
+ img = Image.fromarray(white_img.astype(np.uint8))
100
174
  try:
101
- xlb = min(cols) # left bound
102
- xub = max(cols) # right bound
175
+ rows, cols = np.where(cleaned_mask)
103
176
  ylb = min(rows) # top bound
104
177
  yub = max(rows) # bottom bound
105
-
106
- # Make it square by using the same bounds
107
- lb = min([xlb, ylb])
108
- ub = max([xub, yub])
109
-
110
- img = img.crop((lb, lb, ub, ub))
178
+ xlb = min(cols) # left bound
179
+ xub = max(cols) # right bound
180
+ width = xub - xlb
181
+ height = yub - ylb
182
+ max_dim = max(width, height)
183
+
184
+ x_center = (xlb + xub) // 2
185
+ y_center = (ylb + yub) // 2
186
+
187
+ lb_x = x_center - max_dim // 2
188
+ ub_x = x_center + max_dim // 2
189
+ lb_y = y_center - max_dim // 2
190
+ ub_y = y_center + max_dim // 2
191
+
192
+ # Shift if out of bounds (to maintain square)
193
+ if lb_x < 0:
194
+ ub_x -= lb_x
195
+ lb_x = 0
196
+ if lb_y < 0:
197
+ ub_y -= lb_y
198
+ lb_y = 0
199
+ if ub_x > img.size[0]:
200
+ lb_x -= (ub_x - img.size[0])
201
+ ub_x = img.size[0]
202
+ if ub_y > img.size[1]:
203
+ lb_y -= (ub_y - img.size[1])
204
+ ub_y = img.size[1]
205
+
206
+ img = img.crop((lb_x, lb_y, ub_x, ub_y))
111
207
  except ValueError:
112
208
  print('Blank image - skipping crop')
113
209
  return img
@@ -124,35 +220,38 @@ class ImageExtractor:
124
220
  # Step 1: Apply thumbnail resizing if requested
125
221
  if settings.use_thumbnail:
126
222
  image.thumbnail((settings.resize_dim, settings.resize_dim), Image.Resampling.LANCZOS)
223
+ image.thumbnail((settings.crop_dim, settings.crop_dim), Image.Resampling.LANCZOS)
224
+ # Step 2: Convert RGBA to RGB
225
+ img_rgb = ImageExtractor.RGBA2RGB(image)
127
226
 
128
- # Step 2: Change stroke color if enabled (before converting to RGB)
227
+ # Step 3: Normalize stroke thickness if enabled
228
+ if settings.normalize_stroke_thickness:
229
+ img_rgb = ImageExtractor.normalize_stroke_thickness(
230
+ img_rgb,
231
+ settings.stroke_target_thickness
232
+ )
233
+ # change stroke color if enabled
129
234
  if settings.change_stroke_color:
130
- image = ImageExtractor.change_stroke_color(
131
- image,
235
+ img_rgb = ImageExtractor.change_stroke_color(
236
+ img_rgb,
132
237
  settings.stroke_color,
133
238
  settings.stroke_threshold
134
239
  )
135
-
136
- # Step 3: Convert RGBA to RGB
137
- img_rgb = ImageExtractor.RGBA2RGB(image)
138
-
139
240
  # Step 4: Crop to content (remove whitespace) if enabled
140
- img_cropped = ImageExtractor.crop_to_content(img_rgb, settings.apply_content_crop)
241
+ img_cropped = ImageExtractor.crop_to_content(img_rgb, settings.apply_content_crop, settings.bg_threshold, settings.bg_component_size, settings.filter_edge_artifacts)
141
242
  return img_cropped
142
243
 
143
244
  # Build transformation pipeline
144
245
  transform_list = []
145
-
246
+ if settings.apply_center_crop:
247
+ transform_list.append(transforms.CenterCrop(settings.crop_dim))
248
+ transform_list.append(transforms.Lambda(combined_transform))
249
+
146
250
  # Only add regular resize if not using thumbnail
147
- if not settings.use_thumbnail:
148
- # resize first
251
+ if not settings.use_thumbnail or settings.double_resize:
252
+ # resize last to ensure highest res
149
253
  transform_list.append(transforms.Resize(settings.resize_dim))
150
254
 
151
- transform_list.append(transforms.Lambda(combined_transform))
152
-
153
- if settings.apply_center_crop:
154
- transform_list.append(transforms.CenterCrop(settings.crop_dim))
155
-
156
255
  # Add any additional transforms you might need
157
256
  #transform_list.extend([
158
257
  #transforms.ToTensor(),
@@ -210,17 +309,17 @@ class StimuliDataset(Dataset):
210
309
  else:
211
310
  with Image.open(image_path) as img:
212
311
  img_copy = img.copy() # Copy the image data to memory
213
-
214
- if self.transform is not None:
215
- # Apply the transform pipeline
216
- img_copy = self.transform(img_copy)
217
- else:
218
- # Fallback to just converting to RGB
312
+ try:
313
+ if self.transform is not None:
314
+ img_copy = self.transform(img_copy)
315
+ else:
316
+ img_copy = img_copy.convert('RGB')
317
+ except Exception as e:
318
+ print(f"Warning: failed to process image {idx} ({e}); skipping transform.")
219
319
  img_copy = img_copy.convert('RGB')
220
-
221
- full_image_paths.append(image_path)
222
- images.append(img_copy)
223
-
320
+ full_image_paths.append(image_path)
321
+ images.append(img_copy)
322
+
224
323
  # just assuming the image path as an item id for now if there is more than one item per row.
225
324
  # TODO: allow for the provision of both item ids and row ids
226
325
  row_id = [row[self.id_column] if self.id_column is not None else random.randint(0, 1000000)]
@@ -0,0 +1,20 @@
1
+ from transformers import pipeline
2
+ from vislearnlabpy.models.vision_model import VisionModel
3
+ from transformers import AutoImageProcessor, AutoModel
4
+ import torch
5
+
6
+ class DinoV3Generator(VisionModel):
7
+ def __init__(self, dataloader=None, device=None, text_prompt="a photo of a "):
8
+ self.pipe = pipeline(
9
+ task="image-feature-extraction",
10
+ model="facebook/dinov3-vits16-pretrain-lvd1689m",
11
+ dtype=torch.bfloat16,
12
+ )
13
+ self.model, self.preprocess = self.pipe.model, self.pipe.feature_extractor
14
+ super().__init__(self.model, self.preprocess, dataloader, device)
15
+ self.name = "dinov3"
16
+
17
+ def image_embeddings(self):
18
+ #
19
+ return
20
+
@@ -75,42 +75,3 @@ class MultimodalModel(FeatureGenerator):
75
75
  luce = distractor_similarity / (distractor_similarity + target_similarity)
76
76
  return luce
77
77
 
78
- def similarities(self, word1, word2, images):
79
- valid_images = [img for img in images if img is not None]
80
- similarity_scores = []
81
- # TODO: this only returns the similarity scores for the first pair of images: need to separate out, indexing is weird
82
- for image1, image2 in itertools.combinations(valid_images, 2):
83
- curr_image_embeddings = self.image_embeddings([image1, image2])
84
- curr_text_embeddings = self.text_embeddings([word1, word2])
85
- # TODO: need to fix how each row is labeled in lookit_similarities,
86
- similarity_scores.append({
87
- 'image_similarity': self.similarity(curr_image_embeddings[0], curr_image_embeddings[1]),
88
- 'text_similarity': self.similarity(curr_text_embeddings[0], curr_text_embeddings[1]),
89
- # finding distractor image to target word similarity
90
- 'multimodal_similarity': self.text_to_images_similarity(curr_image_embeddings, curr_text_embeddings[0], logit_scale=10),
91
- })
92
- if similarity_scores == []:
93
- print(f"skipping {word1} and {word2} since they do not have valid images")
94
- return [{
95
- 'image_similarity': None,
96
- 'text_similarity': None,
97
- 'multimodal_similarity': None
98
- }]
99
- else:
100
- return similarity_scores
101
-
102
- # TODO: probably move this to the dataloader row level instead of to a pair of words within a dataloader row
103
- # TODO: words or texts? what is my parameter
104
- def embeddings(self, word1, word2, dataloader_row):
105
- valid_images = [img for img in dataloader_row['images'] if img is not None]
106
- output_embeddings = []
107
- for image1, image2 in itertools.combinations(valid_images, 2):
108
- curr_image_embeddings = self.image_embeddings([image1, image2])
109
- curr_text_embeddings = self.text_embeddings([word1, word2])
110
- output_embeddings.append({
111
- 'image_embeddings': curr_image_embeddings,
112
- 'text_embeddings': curr_text_embeddings,
113
- 'multimodal_embeddings': self.multimodal_embeddings(curr_image_embeddings, curr_text_embeddings)
114
- })
115
- return output_embeddings
116
-
@@ -0,0 +1,54 @@
1
+ import itertools
2
+ from vislearnlabpy.models.feature_generator import FeatureGenerator
3
+ from vislearnlabpy.embeddings import utils
4
+ from torchvision import transforms
5
+ import torch
6
+
7
+ class VisionModel(FeatureGenerator):
8
+ """Abstract base class for multimodal models like CLIP and CVCL that extends FeatureGenerator"""
9
+
10
+ def __init__(self, model, preprocess, dataloader=None, device=None):
11
+ super().__init__(model, preprocess, dataloader, device)
12
+ self.image_word_alignment = lambda **x: self.model(**x).logits_per_image.softmax(dim=-1).detach().cpu().numpy()
13
+
14
+ # Load and preprocess images
15
+ def preprocess_image(self, image):
16
+ if isinstance(image, torch.Tensor):
17
+ transform = transforms.ToPILImage()
18
+ image = transform(image)
19
+ return self.preprocess(image).unsqueeze(0).to(self.device)
20
+
21
+ def encode_image(self, image):
22
+ return self.model.encode_image(image)
23
+
24
+ def image_embeddings(self, images, normalize_embeddings=False):
25
+ """Get image embeddings (batched for speed)"""
26
+ # Handle single image case
27
+ if not isinstance(images, list):
28
+ return self.image_embeddings([images], normalize_embeddings)[0]
29
+ # Preprocess all images → batch
30
+ preprocessed_images = [self.preprocess_image(image) for image in images]
31
+ preprocessed_images = [image.squeeze(0) if image.dim() == 4 else image for image in preprocessed_images]
32
+ # Stack into a single tensor batch (assuming tensors are returned)
33
+ image_batch = torch.stack(preprocessed_images).to(self.device)
34
+ with torch.no_grad():
35
+ embeddings = self.encode_image(image_batch) # model handles batch
36
+ if normalize_embeddings:
37
+ embeddings = utils.normalize_embeddings(embeddings)
38
+ return embeddings
39
+
40
+ # TODO: probably move this to the dataloader row level instead of to a pair of words within a dataloader row
41
+ # TODO: words or texts? what is my parameter
42
+ def embeddings(self, word1, word2, dataloader_row):
43
+ valid_images = [img for img in dataloader_row['images'] if img is not None]
44
+ output_embeddings = []
45
+ for image1, image2 in itertools.combinations(valid_images, 2):
46
+ curr_image_embeddings = self.image_embeddings([image1, image2])
47
+ curr_text_embeddings = self.text_embeddings([word1, word2])
48
+ output_embeddings.append({
49
+ 'image_embeddings': curr_image_embeddings,
50
+ 'text_embeddings': curr_text_embeddings,
51
+ 'multimodal_embeddings': self.multimodal_embeddings(curr_image_embeddings, curr_text_embeddings)
52
+ })
53
+ return output_embeddings
54
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vislearnlabpy
3
- Version: 0.0.2.3
3
+ Version: 0.0.2.4
4
4
  Summary: Visual Learning Lab utility files and pipelines
5
5
  Author-email: Tarun Sepuri <tarunsepuri@gmail.com>
6
6
  License-Expression: MIT
@@ -18,5 +18,7 @@ src/vislearnlabpy/embeddings/utils.py
18
18
  src/vislearnlabpy/extractions/drawingtask_extractor.py
19
19
  src/vislearnlabpy/extractions/mongo_extractor.py
20
20
  src/vislearnlabpy/models/clip_model.py
21
+ src/vislearnlabpy/models/dinov3_model.py
21
22
  src/vislearnlabpy/models/feature_generator.py
22
- src/vislearnlabpy/models/multimodal_model.py
23
+ src/vislearnlabpy/models/multimodal_model.py
24
+ src/vislearnlabpy/models/vision_model.py
File without changes