F-G Fernandez commited on
Commit
4114231
·
1 Parent(s): 04ca68c

build(docker): switch to docker sdk

Browse files
Files changed (3) hide show
  1. Dockerfile +49 -0
  2. README.md +4 -2
  3. app.py → src/streamlit_app.py +24 -13
Dockerfile ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Builder stage
2
+ FROM python:3.11-alpine AS builder
3
+
4
+ ENV PYTHONDONTWRITEBYTECODE=1
5
+ ENV PYTHONUNBUFFERED=1
6
+ # Enable bytecode compilation
7
+ ENV UV_COMPILE_BYTECODE=1
8
+ # Copy from the cache instead of linking since it's a mounted volume
9
+ ENV UV_LINK_MODE=copy
10
+ # Install to a specific directory that we can copy later
11
+ # cf. https://github.com/astral-sh/uv/issues/8085#issuecomment-2438256688
12
+ ENV UV_PROJECT_ENVIRONMENT="/opt/venv"
13
+
14
+ RUN apk add --no-cache gcc python3-dev musl-dev linux-headers git
15
+ # Install the project's dependencies using the lockfile and settings
16
+ # https://docs.astral.sh/uv/guides/integration/docker/#using-uv-temporarily
17
+ RUN --mount=from=ghcr.io/astral-sh/uv:0.9.5,source=/uv,target=/bin/uv \
18
+ --mount=type=cache,target=/root/.cache/uv \
19
+ --mount=type=bind,source=uv.lock,target=uv.lock \
20
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
21
+ uv pip install -r requirements.txt
22
+
23
+ # Runtime stage
24
+ FROM python:3.11-alpine AS runtime
25
+
26
+ WORKDIR /app
27
+
28
+ ENV PYTHONDONTWRITEBYTECODE=1
29
+ ENV PYTHONUNBUFFERED=1
30
+ ENV PYTHONPATH="/app"
31
+ # Set the path to use our installed packages
32
+ ENV PATH="/opt/venv/bin:$PATH"
33
+ ENV PYTHON_PATH="/opt/venv/lib/python3.11/site-packages"
34
+
35
+ LABEL maintainer="F-G Fernandez <[email protected]>"
36
+
37
+ # Install only curl for healthcheck
38
+ RUN apk add --no-cache curl
39
+
40
+ # Copy installed dependencies from builder
41
+ COPY --from=builder /opt/venv /opt/venv
42
+
43
+ # Copy project code
44
+ COPY src ./src
45
+
46
+ # Entrypoint
47
+ EXPOSE 8501
48
+ HEALTHCHECK --interval=10s --timeout=3s --retries=5 CMD ["curl", "-f", "http://localhost:8501/_stcore/health", "--max-time", "3"]
49
+ ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
README.md CHANGED
@@ -3,8 +3,10 @@ title: TorchCAM
3
  emoji: 🎨
4
  colorFrom: purple
5
  colorTo: pink
6
- sdk: streamlit
7
- app_file: app.py
 
 
8
  pinned: true
9
  license: apache-2.0
10
  thumbnail: >-
 
3
  emoji: 🎨
4
  colorFrom: purple
5
  colorTo: pink
6
+ sdk: docker
7
+ app_port: 8501
8
+ tags:
9
+ - streamlit
10
  pinned: true
11
  license: apache-2.0
12
  thumbnail: >-
app.py → src/streamlit_app.py RENAMED
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2021-2023, François-Guillaume Fernandez.
2
 
3
  # This program is licensed under the Apache License 2.0.
4
  # See LICENSE or go to <https://www.apache.org/licenses/LICENSE-2.0> for full license details.
@@ -17,7 +17,17 @@ from torchcam import methods
17
  from torchcam.methods._utils import locate_candidate_layer
18
  from torchcam.utils import overlay_mask
19
 
20
- CAM_METHODS = ["CAM", "GradCAM", "GradCAMpp", "SmoothGradCAMpp", "ScoreCAM", "SSCAM", "ISCAM", "XGradCAM", "LayerCAM"]
 
 
 
 
 
 
 
 
 
 
21
  TV_MODELS = [
22
  "resnet18",
23
  "resnet50",
@@ -28,14 +38,14 @@ TV_MODELS = [
28
  "convnext_small",
29
  ]
30
  LABEL_MAP = requests.get(
31
- "https://raw.githubusercontent.com/anishathalye/imagenet-simple-labels/master/imagenet-simple-labels.json"
 
32
  ).json()
33
 
34
 
35
  def main():
36
-
37
  # Wide mode
38
- st.set_page_config(layout="wide")
39
 
40
  # Designing the interface
41
  st.title("TorchCAM: class activation explorer")
@@ -50,14 +60,12 @@ def main():
50
  # Sidebar
51
  # File selection
52
  st.sidebar.title("Input selection")
53
- # Disabling warning
54
- st.set_option("deprecation.showfileUploaderEncoding", False)
55
  # Choose your own image
56
  uploaded_file = st.sidebar.file_uploader("Upload files", type=["png", "jpeg", "jpg"])
57
  if uploaded_file is not None:
58
  img = Image.open(BytesIO(uploaded_file.read()), mode="r").convert("RGB")
59
 
60
- cols[0].image(img, use_column_width=True)
61
 
62
  # Model selection
63
  st.sidebar.title("Setup")
@@ -87,25 +95,28 @@ def main():
87
  )
88
  if cam_method is not None:
89
  cam_extractor = methods.__dict__[cam_method](
90
- model, target_layer=[s.strip() for s in target_layer.split("+")] if len(target_layer) > 0 else None
 
91
  )
92
 
93
  class_choices = [f"{idx + 1} - {class_name}" for idx, class_name in enumerate(LABEL_MAP)]
94
- class_selection = st.sidebar.selectbox("Class selection", ["Predicted class (argmax)"] + class_choices)
95
 
96
  # For newline
97
  st.sidebar.write("\n")
98
 
99
  if st.sidebar.button("Compute CAM"):
100
-
101
  if uploaded_file is None:
102
  st.sidebar.error("Please upload an image first")
103
 
104
  else:
105
  with st.spinner("Analyzing..."):
106
-
107
  # Preprocess image
108
- img_tensor = normalize(to_tensor(resize(img, (224, 224))), [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
 
 
 
 
109
 
110
  if torch.cuda.is_available():
111
  img_tensor = img_tensor.cuda()
 
1
+ # Copyright (C) 2021-2025, François-Guillaume Fernandez.
2
 
3
  # This program is licensed under the Apache License 2.0.
4
  # See LICENSE or go to <https://www.apache.org/licenses/LICENSE-2.0> for full license details.
 
17
  from torchcam.methods._utils import locate_candidate_layer
18
  from torchcam.utils import overlay_mask
19
 
20
+ CAM_METHODS = [
21
+ "CAM",
22
+ "GradCAM",
23
+ "GradCAMpp",
24
+ "SmoothGradCAMpp",
25
+ "ScoreCAM",
26
+ "SSCAM",
27
+ "ISCAM",
28
+ "XGradCAM",
29
+ "LayerCAM",
30
+ ]
31
  TV_MODELS = [
32
  "resnet18",
33
  "resnet50",
 
38
  "convnext_small",
39
  ]
40
  LABEL_MAP = requests.get(
41
+ "https://raw.githubusercontent.com/anishathalye/imagenet-simple-labels/master/imagenet-simple-labels.json",
42
+ timeout=10,
43
  ).json()
44
 
45
 
46
  def main():
 
47
  # Wide mode
48
+ st.set_page_config(page_title="TorchCAM - Class activation explorer", layout="wide")
49
 
50
  # Designing the interface
51
  st.title("TorchCAM: class activation explorer")
 
60
  # Sidebar
61
  # File selection
62
  st.sidebar.title("Input selection")
 
 
63
  # Choose your own image
64
  uploaded_file = st.sidebar.file_uploader("Upload files", type=["png", "jpeg", "jpg"])
65
  if uploaded_file is not None:
66
  img = Image.open(BytesIO(uploaded_file.read()), mode="r").convert("RGB")
67
 
68
+ cols[0].image(img, use_container_width=True)
69
 
70
  # Model selection
71
  st.sidebar.title("Setup")
 
95
  )
96
  if cam_method is not None:
97
  cam_extractor = methods.__dict__[cam_method](
98
+ model,
99
+ target_layer=[s.strip() for s in target_layer.split("+")] if len(target_layer) > 0 else None,
100
  )
101
 
102
  class_choices = [f"{idx + 1} - {class_name}" for idx, class_name in enumerate(LABEL_MAP)]
103
+ class_selection = st.sidebar.selectbox("Class selection", ["Predicted class (argmax)", *class_choices])
104
 
105
  # For newline
106
  st.sidebar.write("\n")
107
 
108
  if st.sidebar.button("Compute CAM"):
 
109
  if uploaded_file is None:
110
  st.sidebar.error("Please upload an image first")
111
 
112
  else:
113
  with st.spinner("Analyzing..."):
 
114
  # Preprocess image
115
+ img_tensor = normalize(
116
+ to_tensor(resize(img, (224, 224))),
117
+ [0.485, 0.456, 0.406],
118
+ [0.229, 0.224, 0.225],
119
+ )
120
 
121
  if torch.cuda.is_available():
122
  img_tensor = img_tensor.cuda()