GreenMap commited on
Commit
36b249e
·
1 Parent(s): 90fab06

update model

Browse files
README.md CHANGED
@@ -19,7 +19,7 @@ pipeline_tag: image-feature-extraction
19
 
20
  # Siamese DINOv2 Wall Hatching Matcher
21
 
22
- A TorchScript model for matching wall hatchings from architectural blueprints with legend patterns.
23
 
24
  ## Model
25
 
@@ -32,104 +32,124 @@ A TorchScript model for matching wall hatchings from architectural blueprints wi
32
  ## Inference
33
 
34
  ```python
35
- import cv2
36
- import numpy as np
 
37
  import torch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- from PIL import Image
40
- from torchvision import transforms
41
-
42
- input_data = {
43
- "legends": ["legend_correct.png", "legend_wrong.png"],
44
- "plan_image": "wall.png",
45
- "plan_obb": [
46
- 20.672607421875, # x1
47
- 20.71624755859375, # y1
48
- 42.37445068359375, # x2
49
- 20.71624755859375, # y2
50
- 42.37445068359375, # ...
51
- 111.15782165527344,
52
- 20.672607421875,
53
- 111.15782165527344
54
- ] # mask bbox for wall image
55
- }
56
-
57
-
58
- IMAGE_SIZE = 518 # don't change
59
-
60
- device = "cuda" if torch.cuda.is_available() else "cpu"
61
-
62
- image_tf = transforms.Compose([
63
- transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),
64
- transforms.ToTensor(),
65
- transforms.Normalize(
66
  mean=[0.485, 0.456, 0.406],
67
  std=[0.229, 0.224, 0.225],
68
- ),
69
- ])
70
-
71
- mask_tf = transforms.Compose([
72
- transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),
73
- transforms.ToTensor(),
74
- ])
75
-
76
-
77
- def create_full_mask(size):
78
- return Image.new("L", size, 255)
79
-
80
-
81
- def create_obb_mask(size, obb):
82
- w, h = size
83
-
84
- points = np.array(obb, dtype=np.int32).reshape(4, 2)
85
-
86
- mask = np.zeros((h, w), dtype=np.uint8)
87
- cv2.fillPoly(mask, [points], 255)
88
-
89
- return Image.fromarray(mask)
90
-
91
-
92
- def prepare(image_path, obb=None):
93
- image = Image.open(image_path).convert("RGB")
94
 
95
- if obb is None:
96
- mask = create_full_mask(image.size)
97
- else:
98
- mask = create_obb_mask(image.size, obb)
99
 
100
- image = image_tf(image).unsqueeze(0).to(device)
101
- mask = mask_tf(mask).unsqueeze(0).to(device)
102
 
103
- return image, mask
 
104
 
105
- for legend in input_data["legends"]:
106
- legend_image, legend_mask = prepare(legend)
 
107
 
108
- plan_image, plan_mask = prepare(
109
- input_data["plan_image"],
110
- input_data["plan_obb"],
111
- )
112
 
113
- model = torch.jit.load(
114
- "dino_hatching.pt",
115
- map_location=device,
116
- )
117
- model.eval()
118
-
119
- with torch.no_grad():
120
- logit = model(
121
- legend_image,
122
- legend_mask,
123
- plan_image,
124
- plan_mask,
125
  )
126
-
127
- score = torch.sigmoid(logit).item()
128
-
129
- print(f"{legend}: {score}")
130
-
131
- # legend_correct.png: 0.9882104396820068
132
- # legend_wrong.png: 0.0016661642584949732
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  ```
134
 
135
  `score` is the probability that both hatchings belong to the same wall type.
 
19
 
20
  # Siamese DINOv2 Wall Hatching Matcher
21
 
22
+ A TorchScript model for matching wall hatchings from architectural blueprints with legend patterns. Example of use in the repository.
23
 
24
  ## Model
25
 
 
32
  ## Inference
33
 
34
  ```python
35
+ import json
36
+ from pathlib import Path
37
+
38
  import torch
39
+ from PIL import Image, ImageDraw
40
+ from torchvision.transforms import InterpolationMode
41
+ from torchvision.transforms import functional as TF
42
+
43
+
44
+ ROOT = Path(__file__).resolve().parent
45
+ IMAGE_SIZE = 518
46
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
47
+
48
+
49
+ def prepare_image(filename: str, obb: list[float] | None = None):
50
+ image = Image.open(ROOT / "images" / filename).convert("RGB")
51
+ mask = Image.new("L", image.size, 0 if obb else 255)
52
+
53
+ if obb:
54
+ points = [
55
+ (int(x), int(y))
56
+ for x, y in zip(obb[::2], obb[1::2])
57
+ ]
58
+ ImageDraw.Draw(mask).polygon(points, fill=255)
59
+
60
+ width, height = image.size
61
+ scale = min(IMAGE_SIZE / width, IMAGE_SIZE / height)
62
+ new_width = min(IMAGE_SIZE, round(width * scale))
63
+ new_height = min(IMAGE_SIZE, round(height * scale))
64
+ size = [new_height, new_width]
65
+
66
+ image = TF.resize(
67
+ image,
68
+ size,
69
+ interpolation=InterpolationMode.BILINEAR,
70
+ antialias=True,
71
+ )
72
+ mask = TF.resize(
73
+ mask,
74
+ size,
75
+ interpolation=InterpolationMode.NEAREST,
76
+ )
77
 
78
+ pad_x = IMAGE_SIZE - new_width
79
+ pad_y = IMAGE_SIZE - new_height
80
+ padding = [
81
+ pad_x // 2,
82
+ pad_y // 2,
83
+ pad_x - pad_x // 2,
84
+ pad_y - pad_y // 2,
85
+ ]
86
+ image = TF.pad(image, padding, fill=255)
87
+ mask = TF.pad(mask, padding, fill=0)
88
+
89
+ image = TF.to_tensor(image)
90
+ image = TF.normalize(
91
+ image,
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  mean=[0.485, 0.456, 0.406],
93
  std=[0.229, 0.224, 0.225],
94
+ )
95
+ mask = TF.to_tensor(mask)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
+ return image.unsqueeze(0).to(DEVICE), mask.unsqueeze(0).to(DEVICE)
 
 
 
98
 
 
 
99
 
100
+ with (ROOT / "data.jsonl").open("r", encoding="utf-8") as file:
101
+ items = [json.loads(line) for line in file if line.strip()]
102
 
103
+ model = torch.jit.load(ROOT / "dino_hatching.pt", map_location=DEVICE).eval()
104
+ wall_types = list(dict.fromkeys(item["wall_type"] for item in items))
105
+ walls = {name: prepare_image(name) for name in wall_types}
106
 
107
+ print("\nScore matrix")
108
+ print(" " * 6 + "".join(f"W{i}".rjust(8) for i in range(1, len(wall_types) + 1)))
 
 
109
 
110
+ with torch.inference_mode():
111
+ for index, item in enumerate(items, start=1):
112
+ plan_image, plan_mask = prepare_image(
113
+ item["plan_image"],
114
+ item["plan_obb"],
 
 
 
 
 
 
 
115
  )
116
+ scores = []
117
+
118
+ for wall_type in wall_types:
119
+ wall_image, wall_mask = walls[wall_type]
120
+ logit = model(wall_image, wall_mask, plan_image, plan_mask)
121
+ scores.append(f"{torch.sigmoid(logit).item():.4f}")
122
+
123
+ print(f"P{index:<5}" + "".join(f"{score:>8}" for score in scores))
124
+
125
+ print("\nRows:")
126
+ for index, item in enumerate(items, start=1):
127
+ print(f" P{index}: {item['plan_image']}")
128
+
129
+ print("Columns:")
130
+ for index, wall_type in enumerate(wall_types, start=1):
131
+ print(f" W{index}: {wall_type}")
132
+
133
+ # Score matrix
134
+ # W1 W2 W3 W4 W5
135
+ # P1 0.9965 0.0005 0.0006 0.0044 0.0317
136
+ # P2 0.0026 0.9932 0.9916 0.0010 0.0028
137
+ # P3 0.0001 0.9984 0.9988 0.0001 0.0036
138
+ # P4 0.0005 0.0001 0.0002 0.9979 0.0002
139
+ # P5 0.0103 0.0004 0.0006 0.0001 0.9971
140
+
141
+ # Rows:
142
+ # P1: valid_pair_000001_plan.png
143
+ # P2: valid_pair_000004_plan.png
144
+ # P3: valid_pair_000010_plan.png
145
+ # P4: valid_pair_000013_plan.png
146
+ # P5: valid_pair_000016_plan.png
147
+ # Columns:
148
+ # W1: valid_pair_000001_legend.png
149
+ # W2: valid_pair_000004_legend.png
150
+ # W3: valid_pair_000010_legend.png
151
+ # W4: valid_pair_000013_legend.png
152
+ # W5: valid_pair_000016_legend.png
153
  ```
154
 
155
  `score` is the probability that both hatchings belong to the same wall type.
dino_hatching.pt CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:39684e4034dc7dd8f8f1266678891e2892ed84e76b807d484498bc32b73f75d1
3
- size 351342499
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dc6b05869819984378bbcf3f4c8f693a32bf456f5b237a06900739dcdb5166c0
3
+ size 351563175
example/data.jsonl ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {"plan_image":"valid_pair_000001_plan.png","plan_obb":[96.0,41.0,20.0,41.0,20.0,20.0,96.0,20.0],"wall_type":"valid_pair_000001_legend.png"}
2
+ {"plan_image":"valid_pair_000004_plan.png","plan_obb":[48.0,63.0,20.0,63.0,20.0,20.0,48.0,20.0],"wall_type":"valid_pair_000004_legend.png"}
3
+ {"plan_image":"valid_pair_000010_plan.png","plan_obb":[74.0,33.0,20.0,33.0,20.0,20.0,74.0,20.0],"wall_type":"valid_pair_000010_legend.png"}
4
+ {"plan_image":"valid_pair_000013_plan.png","plan_obb":[160.0,30.0,20.0,30.0,20.0,20.0,160.0,20.0],"wall_type":"valid_pair_000013_legend.png"}
5
+ {"plan_image":"valid_pair_000016_plan.png","plan_obb":[179.0,40.0,20.0,40.0,20.0,20.0,179.0,20.0],"wall_type":"valid_pair_000016_legend.png"}
example/example.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import torch
5
+ from PIL import Image, ImageDraw
6
+ from torchvision.transforms import InterpolationMode
7
+ from torchvision.transforms import functional as TF
8
+
9
+
10
+ ROOT = Path(__file__).resolve().parent
11
+ IMAGE_SIZE = 518
12
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
13
+
14
+
15
+ def prepare_image(filename: str, obb: list[float] | None = None):
16
+ image = Image.open(ROOT / "images" / filename).convert("RGB")
17
+ mask = Image.new("L", image.size, 0 if obb else 255)
18
+
19
+ if obb:
20
+ points = [
21
+ (int(x), int(y))
22
+ for x, y in zip(obb[::2], obb[1::2])
23
+ ]
24
+ ImageDraw.Draw(mask).polygon(points, fill=255)
25
+
26
+ width, height = image.size
27
+ scale = min(IMAGE_SIZE / width, IMAGE_SIZE / height)
28
+ new_width = min(IMAGE_SIZE, round(width * scale))
29
+ new_height = min(IMAGE_SIZE, round(height * scale))
30
+ size = [new_height, new_width]
31
+
32
+ image = TF.resize(
33
+ image,
34
+ size,
35
+ interpolation=InterpolationMode.BILINEAR,
36
+ antialias=True,
37
+ )
38
+ mask = TF.resize(
39
+ mask,
40
+ size,
41
+ interpolation=InterpolationMode.NEAREST,
42
+ )
43
+
44
+ pad_x = IMAGE_SIZE - new_width
45
+ pad_y = IMAGE_SIZE - new_height
46
+ padding = [
47
+ pad_x // 2,
48
+ pad_y // 2,
49
+ pad_x - pad_x // 2,
50
+ pad_y - pad_y // 2,
51
+ ]
52
+ image = TF.pad(image, padding, fill=255)
53
+ mask = TF.pad(mask, padding, fill=0)
54
+
55
+ image = TF.to_tensor(image)
56
+ image = TF.normalize(
57
+ image,
58
+ mean=[0.485, 0.456, 0.406],
59
+ std=[0.229, 0.224, 0.225],
60
+ )
61
+ mask = TF.to_tensor(mask)
62
+
63
+ return image.unsqueeze(0).to(DEVICE), mask.unsqueeze(0).to(DEVICE)
64
+
65
+
66
+ with (ROOT / "data.jsonl").open("r", encoding="utf-8") as file:
67
+ items = [json.loads(line) for line in file if line.strip()]
68
+
69
+ model = torch.jit.load(ROOT / "dino_hatching.pt", map_location=DEVICE).eval()
70
+ wall_types = list(dict.fromkeys(item["wall_type"] for item in items))
71
+ walls = {name: prepare_image(name) for name in wall_types}
72
+
73
+ print("\nScore matrix")
74
+ print(" " * 6 + "".join(f"W{i}".rjust(8) for i in range(1, len(wall_types) + 1)))
75
+
76
+ with torch.inference_mode():
77
+ for index, item in enumerate(items, start=1):
78
+ plan_image, plan_mask = prepare_image(
79
+ item["plan_image"],
80
+ item["plan_obb"],
81
+ )
82
+ scores = []
83
+
84
+ for wall_type in wall_types:
85
+ wall_image, wall_mask = walls[wall_type]
86
+ logit = model(wall_image, wall_mask, plan_image, plan_mask)
87
+ scores.append(f"{torch.sigmoid(logit).item():.4f}")
88
+
89
+ print(f"P{index:<5}" + "".join(f"{score:>8}" for score in scores))
90
+
91
+ print("\nRows:")
92
+ for index, item in enumerate(items, start=1):
93
+ print(f" P{index}: {item['plan_image']}")
94
+
95
+ print("Columns:")
96
+ for index, wall_type in enumerate(wall_types, start=1):
97
+ print(f" W{index}: {wall_type}")
98
+
99
+ # Score matrix
100
+ # W1 W2 W3 W4 W5
101
+ # P1 0.9965 0.0005 0.0006 0.0044 0.0317
102
+ # P2 0.0026 0.9932 0.9916 0.0010 0.0028
103
+ # P3 0.0001 0.9984 0.9988 0.0001 0.0036
104
+ # P4 0.0005 0.0001 0.0002 0.9979 0.0002
105
+ # P5 0.0103 0.0004 0.0006 0.0001 0.9971
106
+
107
+ # Rows:
108
+ # P1: valid_pair_000001_plan.png
109
+ # P2: valid_pair_000004_plan.png
110
+ # P3: valid_pair_000010_plan.png
111
+ # P4: valid_pair_000013_plan.png
112
+ # P5: valid_pair_000016_plan.png
113
+ # Columns:
114
+ # W1: valid_pair_000001_legend.png
115
+ # W2: valid_pair_000004_legend.png
116
+ # W3: valid_pair_000010_legend.png
117
+ # W4: valid_pair_000013_legend.png
118
+ # W5: valid_pair_000016_legend.png
example/images/valid_pair_000001_legend.png ADDED
example/images/valid_pair_000001_plan.png ADDED
example/images/valid_pair_000004_legend.png ADDED
example/images/valid_pair_000004_plan.png ADDED
example/images/valid_pair_000010_legend.png ADDED
example/images/valid_pair_000010_plan.png ADDED
example/images/valid_pair_000013_legend.png ADDED
example/images/valid_pair_000013_plan.png ADDED
example/images/valid_pair_000016_legend.png ADDED
example/images/valid_pair_000016_plan.png ADDED
example/inference.py DELETED
@@ -1,98 +0,0 @@
1
- import cv2
2
- import numpy as np
3
- import torch
4
-
5
- from PIL import Image
6
- from torchvision import transforms
7
-
8
- input_data = {
9
- "legends": ["legend_correct.png", "legend_wrong.png"],
10
- "plan_image": "wall.png",
11
- "plan_obb": [
12
- 20.672607421875, # x1
13
- 20.71624755859375, # y1
14
- 42.37445068359375, # x2
15
- 20.71624755859375, # y2
16
- 42.37445068359375, # ...
17
- 111.15782165527344,
18
- 20.672607421875,
19
- 111.15782165527344
20
- ] # mask bbox for wall image
21
- }
22
-
23
-
24
- IMAGE_SIZE = 518
25
-
26
- device = "cuda" if torch.cuda.is_available() else "cpu"
27
-
28
- image_tf = transforms.Compose([
29
- transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),
30
- transforms.ToTensor(),
31
- transforms.Normalize(
32
- mean=[0.485, 0.456, 0.406],
33
- std=[0.229, 0.224, 0.225],
34
- ),
35
- ])
36
-
37
- mask_tf = transforms.Compose([
38
- transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),
39
- transforms.ToTensor(),
40
- ])
41
-
42
-
43
- def create_full_mask(size):
44
- return Image.new("L", size, 255)
45
-
46
-
47
- def create_obb_mask(size, obb):
48
- w, h = size
49
-
50
- points = np.array(obb, dtype=np.int32).reshape(4, 2)
51
-
52
- mask = np.zeros((h, w), dtype=np.uint8)
53
- cv2.fillPoly(mask, [points], 255)
54
-
55
- return Image.fromarray(mask)
56
-
57
-
58
- def prepare(image_path, obb=None):
59
- image = Image.open(image_path).convert("RGB")
60
-
61
- if obb is None:
62
- mask = create_full_mask(image.size)
63
- else:
64
- mask = create_obb_mask(image.size, obb)
65
-
66
- image = image_tf(image).unsqueeze(0).to(device)
67
- mask = mask_tf(mask).unsqueeze(0).to(device)
68
-
69
- return image, mask
70
-
71
- for legend in input_data["legends"]:
72
- legend_image, legend_mask = prepare(legend)
73
-
74
- plan_image, plan_mask = prepare(
75
- input_data["plan_image"],
76
- input_data["plan_obb"],
77
- )
78
-
79
- model = torch.jit.load(
80
- "dino_hatching.pt",
81
- map_location=device,
82
- )
83
- model.eval()
84
-
85
- with torch.no_grad():
86
- logit = model(
87
- legend_image,
88
- legend_mask,
89
- plan_image,
90
- plan_mask,
91
- )
92
-
93
- score = torch.sigmoid(logit).item()
94
-
95
- print(f"{legend}: {score}")
96
-
97
- # legend_correct.png: 0.9882104396820068
98
- # legend_wrong.png: 0.0016661642584949732
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
example/legend_correct.png DELETED
Binary file (10.9 kB)
 
example/legend_wrong.png DELETED
Binary file (3.92 kB)
 
example/wall.png DELETED
Binary file (4.54 kB)